お米 is ライス

C#やらUnityやらを勉強していて、これはメモっといたほうがええやろ、ということを書くつもりです

【Cg Programming/Unity】ライティング基礎 ~ 鏡面ハイライト①「鏡面反射」【【順番にやっていく】

f:id:spi_8823:20200517064927p:plain
皆様、お久しゅうございますわね!
ええ、今日はこちらのチュートリアルをやっていきますわ!
en.wikibooks.org

この節では「鏡面反射」と「環境光」について学びますのよ。
長くなるとお肌に悪いから前半と後半に記事を分けますのよ!
今回は「鏡面反射」についてやりますの。

鏡面反射

「鏡面反射」とは何か、ですって!?あなた、本気でそんなことをおっしゃってますの??そんなだからいつまでたってもうだつが上がらないんですのよ!!
もう、しょうがないですわね……、よろしくって?一度しか言わないからよくお聞きなさい。

鏡面反射とは

「鏡面反射」というものは「入射角と反射角が等しくなるような反射」をいうんですの!その名の通り、光が鏡で反射するときに「鏡面反射」してるんですのよ。まるでわたくしのおうちの床の大理石みたいにピカピカなんですの!

Wikipediaからわざわざ概念図を引っ張ってきて差し上げましたのよ、ふふん!これでも見てしっかりと理解することね!ふふん!
この図でいうとPが光源、ベクトルPOが入射ベクトルに当たりますわね。光源からぴったり鏡面反射したベクトルOQを次からは「鏡面反射ベクトル」と言うことにしますわ!この私が決めたのだから、しっかりと覚えておきなさい。いいですわね?
https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Reflection_angles.svg/170px-Reflection_angles.svg.png

鏡面反射ベクトルの計算

そんな鏡面反射ベクトルは光源からの入射ベクトル、反射面の法線を使って以下のように計算できるんですの。

鏡面反射ベクトル = 入射ベクトル + 法線ベクトル×2×(入射ベクトル・法線ベクトル)

え?どうしてこんな式になるのか、ですって?
仕方がありませんわ。このわたくしが自ら筆を取ってわかりやすく図を書いてあげますの。
iPadを買ったからお絵かきツールを使ってみたいだけなんてことは絶対にありませんのよ!)
これもノブレス・オブリージュというもの、顔を上げてもよろしくってよ?
f:id:spi_8823:20200517050801p:plain
男っぽい文字ですって?!傷つきますわ!!

3D面上の点Oに入射してきた光が鏡面反射ベクトルの方向にだけ反射されることを「鏡面反射」と言いますの。その場合、鏡面反射ベクトル上にカメラがあれば白、そうでなければ黒を描画してやればいいのですわ。
ただ、実際にはもう少し広がりを持った反射をすることが多いのですわ。その場合でもカメラ方向と鏡面反射ベクトルの方向が近いほどカメラに入る光の量は多くなるんですの。
え?方向が近いかどうかはどうやって計算するのか、ですって?そんなの「内積をとる」に決まってますのよ!

そういうわけで、鏡面反射を描画するには下のような値であらわされる明るさを各ピクセルの色に足してあげればいいんですの!

鏡面反射による明るさ = 元の光の強さ×(鏡面反射ベクトル・カメラ方向ベクトル)

鏡面反射成分の描画

上の方法で描画してみたものがこちらですのよ!
Diffuse Reflectionのときと違って鏡面反射の光が届くごく一部分しか明るく見えないのがわかりますわね!まるでわたくしのおうちの床の大理石みたいにピッカピカですわ!
f:id:spi_8823:20200517055221p:plain
ただ、なんだか変な格子模様が見えますわね……。
どうやら頂点シェーダ側ですべて計算してしまうと、フラグメントシェーダ側に渡す値の補間の関係で変なことになってしまっているようですわね。
頂点シェーダではローカル座標や法線を受け渡すだけにして、反射の計算はフラグメントシェーダでやってあげることにするとうまく描画できましたわ!
(あまりフラグメントシェーダで重たい計算はやりたくないのですけれど、何か他に方法はないんですの?)
f:id:spi_8823:20200517062809p:plain
どうやらこうやって法線ベクトルを補間して描画する技法を「フォンシェーディング」と呼ぶらしいんですの。
どこかでのタイミングで詳しく調べたいですわね。
ja.wikipedia.org

鏡面反射の度合い

実は、鏡面反射ベクトルとカメラ方向ベクトルの内積を反射の強度として使ってしまうと少し広がりすぎるんですの。
もっとくっきりはっきり鏡面反射らしさを出してあげるには、内積をある程度大きい値でべき乗してあげればいいんですのよ。(上の画像も実は内積を2乗した値を使ってますの)

内積を20乗した値を使ったのと、1乗した値を使ったのがそれぞれ以下の画像ですのよ。
上はピカピカ、下はのっぺりって感じですわね!
こんな風に、鏡面反射の度合いを変えることで質感にも差が出てくるのは覚えておくといいですわよ!
f:id:spi_8823:20200517064927p:plain
f:id:spi_8823:20200517064949p:plain

コード

というわけでわたくしが今回書いたコードですわ!
反射の計算を頂点シェーダでやったパターンとフラグメントシェーダでやったパターンを両方載せてありますの。

Shader "Custom/SpecularHighlights"
{
    Properties
    {
        _SpecularRatio ("SpecularRatio", float) = 1 //鏡面反射の度合い(大きいほどはっきりした鏡面反射になる)
    }
    SubShader
    {
        pass
        {
            //頂点シェーダ側で反射の計算をやるパターンですの!
            Tags { "LightMode" = "ForwardBase" }    //簡単のため、平行光源のみを考えますわ!
            CGPROGRAM
            #pragma vertex v
            #pragma fragment f

            #include "UnityCG.cginc"

            uniform float _SpecularRatio;

            struct vertexInput
            {
                float4 vertexPos : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 sv_position : SV_POSITION;
                float4 color : TEXCOORD0;
            };

            v2f v(vertexInput input)
            {
                //頂点シェーダで反射の計算をやってしまうんですの!

                v2f output;
                output.sv_position = UnityObjectToClipPos(input.vertexPos);

                float4 worldPos = mul(unity_ObjectToWorld, input.vertexPos);
                float3 cameraDirection = (_WorldSpaceCameraPos - worldPos).xyz;
                cameraDirection = normalize(cameraDirection);   //カメラ方向

                float4 incidentVector = normalize(-_WorldSpaceLightPos0);   //入射ベクトルは光源方向の反対向き
                float isValid = step(dot(incidentVector, input.normal), 0); //光が面の裏側にある場合(入射ベクトルと法線の内積が負)の場合は0、そうでなければ1
                float3 specularDirection = incidentVector.xyz - input.normal * 2 * dot(input.normal.xyz, incidentVector.xyz);
                specularDirection = normalize(specularDirection) * isValid; //鏡面反射ベクトル

                float intensity = dot(cameraDirection, specularDirection.xyz);  //カメラ方向と鏡面反射ベクトルの内積
                intensity = max(0, intensity);  //内積が負の場合は0にする
                intensity = pow(intensity, _SpecularRatio); //より大きな値でべき乗してあげるとより強い鏡面反射になるんですのよ!
                output.color = float4(intensity * float3(1, 1, 1), 1);

                return output;
            }

            float4 f(v2f input) : COLOR
            {
                //フラグメントシェーダでは色の出力だけやりますわ!
                return input.color;
            }
            ENDCG
        }

        pass
        {
            //フラグメントシェーダ側で反射の計算をやるパターンですの!
            Tags { "LightMode" = "ForwardBase" }
            CGPROGRAM
            #pragma vertex v
            #pragma fragment f

            #include "UnityCG.cginc"

            uniform float _SpecularRatio;

            struct v2f
            {
                float4 sv_position : SV_POSITION;
                float4 vertexPos : TEXCOORD0;
                float3 normal : TEXCOORD1;
            };

            v2f v(float4 vertexPos : POSITION, float3 normal : NORMAL)
            {
                //頂点シェーダでは値の受け渡しだけやりますわ!
                v2f output;
                output.sv_position = UnityObjectToClipPos(vertexPos);
                output.vertexPos = vertexPos;
                output.normal = normal;
                return output;
            }

            float4 f(v2f input) : COLOR
            {
                //フラグメントシェーダで反射を計算してあげるんですの!

                float4 worldPos = mul(unity_ObjectToWorld, input.vertexPos);
                float3 cameraDirection = (_WorldSpaceCameraPos - worldPos).xyz;
                cameraDirection = normalize(cameraDirection);   //カメラ方向

                float4 incidentVector = normalize(-_WorldSpaceLightPos0);   //入射ベクトルは光源方向の反対向き
                float isValid = step(dot(incidentVector, input.normal), 0); //光が面の裏側にある場合(入射ベクトルと法線の内積が負)の場合は0、そうでなければ1
                float3 specularDirection = incidentVector.xyz - input.normal * 2 * dot(input.normal.xyz, incidentVector.xyz);
                specularDirection = normalize(specularDirection) * isValid; //鏡面反射ベクトル

                float intensity = dot(cameraDirection, specularDirection.xyz);
                intensity = max(0, intensity);
                intensity = pow(intensity, _SpecularRatio);
                float4 color = float4(intensity * float3(1, 1, 1), 1);

                return color;
            }
            ENDCG
        }
    }
}

次回

次回は「環境光」の描画を学びますの!しっかりとついてくることね!
spi8823.hatenablog.com