お米 is ライス

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

【Cg Programming/Unity】テクスチャ基礎 ~ テクスチャを描画する【順番にやっていく】

f:id:spi_8823:20200519000504p:plain

お~~~~っほっほっほっほっ!!!
庶民のみなさま!今回はこちらをやっていきますのよ!
en.wikibooks.org
en.wikibooks.org
この節では3Dオブジェクト上にテクスチャを描画する方法を学ぶそうですのよ?
テクスチャって何ですかですって!?テクスチャはテクスチャですのよ!!

テクスチャの描画

真っ白な球体を眺めているのはもう飽きてきたんじゃありませんこと?え、飽きてない?物好きなのねあなた……。
どうせまん丸球体を転がすなら、ど~んとでっかく地球ぐらいは転がしてあげようじゃないですの!そう、わたくしの手のひらの上で滑稽に回り続けるあなたたちのように転がしてあげるわ!

テクスチャの用意

まずすることはと言えば、そうですの。地球の画像をどこかから持ってくるのですわ!
めんどくさい?もう、相変わらずしょうのない人ね……。そんなこともあろうかと、うちの執事に用意させたからココからダウンロードしてきなさい。
忘れないうちにわたくしに感謝しておくこと。いいわね?
f:id:spi_8823:20200518220859j:plain

テクスチャの登録

用意したテクスチャをシェーダから使うには、シェーダ(を使用するマテリアル)にテクスチャを登録しないといけませんわね。
シェーダに外部から値を渡してあげるにはどうすればいいか覚えてらして?あら、ダチョウ頭のあなたにしては珍しくちゃんと覚えているじゃない。えらいわ。
そう、シェーダのPropertiesにパラメータを用意して、CGコードの中で同名のuniform変数を宣言するんでしたわね!
今回は_TestTexという名前にいたしますわ!

Properties

この名前でPropertiesにテクスチャのパラメータを定義するには次のように書くといいんですの!

Properties
{
    _TestTex ("Test", 2D) = "white" {}
}

2Dというのがテクスチャを表していますわ!
"white"としたことでデフォルトでは真っ白な画像が使用されますわ!
最後の波かっこは昔の記法の名残のようで、今のバージョンではもう意味が無いそうですわ。

uniform変数の宣言

Propertiesで定義したパラメータをCGコードから使うにはuniform変数を宣言しなければならないんでしたわね!
CGコードではテクスチャはsampler2Dという型になっていますのよ!
ですから、宣言は次のようになりますわね!

uniform sampler2D _TestTex;
uniform float4 _TestTex_ST;
Tiling, Offset

あら、気づいたみたいね。_TestTex_STとはなんだ、でしょう?
この変数にはテクスチャのTilingOffsetの値が入っているのですわ!
Propertiesでテクスチャを定義すると、テクスチャと一緒にTilingOffsetという2次元ベクトルを一緒に設定できるようになるのですわ!
テクスチャの変数名の後ろに_STと付けたuniform変数にはこれらのベクトルの値が入るようになっているんですの。
Tilingはテクスチャの繰り返し描画、Offsetはテクスチャの初期位置をずらすために用いられることが多いのですわよ!

テクスチャ座標

さて、これでシェーダからテクスチャを参照することができるようになりましたわ!
あとは、テクスチャ上の色を取得して、ピクセルの色として出力すればいいだけですわね!
それにはもう一つ、色を取得すべきテクスチャ上の座標の情報が必要なんですわよ。
そんなのどうやって計算すればいいのか、と困惑しなくてもよろしくってよ!
うちの執事に計算させたテクスチャ座標をTEXCOORD0というセマンティクスを使って頂点シェーダのインプットとして渡すようにしておいたわ!
テクスチャ座標の値は頂点シェーダからフラグメントシェーダへそのまま受け渡すことが多いんですの。
このとき、これまたTEXCOORD0というセマンティクスを使って受け渡すことが多いんですけれど、頂点シェーダのインプットとしてのTEXCOORD0と、頂点シェーダからフラグメントシェーダへ受け渡す際のTEXCOORD0は別のものだということは気を付けておきなさい。

テクスチャの色の取得

テクスチャ座標が取得できればあとはテクスチャ座標を使ってテクスチャ上の色を取得するだけですわね!
テクスチャ座標がtexcoordという変数名だったなら、その座標の色は次のように取得できるわ!

float4 color = tex2D(_TestTex, texcoord);

驚くほど容易いですわね!

描画結果

実際に球に地球を描画いたしましたわ!
f:id:spi_8823:20200519000504p:plain
これの半分くらいはうちの土地ですの!

え?のっぺりしてる、ですって?そりゃそうなのですわ。だってライティングをしてないんですもの。
んもう、そう言うかと思って拡散反射で描画するテクスチャの色を変える方法もやってあげたのですわ!
f:id:spi_8823:20200519001744p:plain
日本の夜明け、ですわね!

コード

今回使ったコードですわ。
ありがたく拝見することね!

Shader "Custom/TexturedSpheres"
{
    Properties
    {
        _TestTex ("Test", 2D) = "white" {}  //テクスチャのパラメータ定義ですわ!
    }

    SubShader
    {
        pass
        {
            CGPROGRAM
            #pragma vertex v
            #pragma fragment f

            #include "UnityCG.cginc"

            uniform sampler2D _TestTex;
            uniform float4 _TestTex_ST;

            struct vertexInput
            {
                float4 vertexPos : POSITION;
                float4 texcoord : TEXCOORD0;    //頂点シェーダにインプットとして渡されるテクスチャ座標ですわ!
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 sv_position : SV_POSITION;
                float2 uv_position : TEXCOORD0; //テクスチャ座標ですわ!vertexInputのtexcoordとは違うから注意するのですわよ!
                float diffuseRatio : TEXCOORD1; //拡散反射の度合いですわ!
            };

            v2f v(vertexInput input)
            {
                v2f output;
                output.sv_position = UnityObjectToClipPos(input.vertexPos);
                output.uv_position = input.texcoord;    //フラグメントシェーダにそのまま受け渡しますわ!
                output.diffuseRatio = dot(UnityObjectToWorldNormal(input.normal), normalize(_WorldSpaceLightPos0.xyz)); //拡散反射の計算をしますわ!

                return output;
            }

            float4 f(v2f input) : COLOR
            {
                //テクスチャの色を取得しますわ!
                //Tilingの値は_TestTex_ST.xy、Offsetの値は_TestTex_ST.zwに入っているんですの!
                float4 texColor = tex2D(_TestTex, input.uv_position.xy * _TestTex_ST.xy + _TestTex_ST.zw);
                return texColor * pow(max(0, input.diffuseRatio), 2);   //テクスチャの色に拡散反射の値をかけて出力しますわ!
            }
            ENDCG
        }
    }
}

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

f:id:spi_8823:20200517095233p:plain
前回に引き続きこちらのチュートリアルをやっていきますわ!
en.wikibooks.org
前回は「鏡面反射」で、今回学ぶのは「環境光」になりますのよ。

環境光

え?環境光とは何かですって?
またですの?もう、しょうがありませんわね!
高貴なるものの義務としてわたくしがじっくり丁寧に教えて差し上げますのよ!

環境光とは

前節でやった拡散反射や、前回の鏡面反射については覚えていらして?
三歩進めば忘れてしまう鳥頭のあなたにもわかるようにそれぞれ描画した画像を再掲して差し上げますわ!そのダチョウみたいに大きなお目目でしっかりと見ておきなさい!
これが拡散反射↓
f:id:spi_8823:20200508012333p:plain
こっちが鏡面反射↓ですのよ。
f:id:spi_8823:20200517064927p:plain

ダチョウ並みの脳みそでも見ればわかるでしょうけど、どちらも光の当たっている部分は真っ黒になっていますの。
でも考えてみなさい?現実では光源から直接光が当たっていなくてもモノが見えるわ。ほら、電気を消した部屋の中でも光源はないけれどちゃんとモノが見えているでしょう?
そんな場合には太陽光が一度だけ反射した光ではなく、外の建物の壁や地面に反射したうえで、例えば窓から入った光がさらに部屋の壁、天井と何度も複雑に反射した光が私たちの目に届いているの。これを「環境光」と言いますのよ。

そんなのどうやって計算するのか、ですって?あなたにしては珍しくまともな質問をするのね。
ええ、そうですわ。複雑に反射してカメラに到達する光を簡単に計算する方法は無いですのよ。これを計算するには、光源からたくさんのRayを飛ばして起こりうるすべての反射を計算するRay Tracingというものをする必要がありますの。だけどそんなに重たい処理を気楽にさせられるほど計算資源は潤沢でないというのが庶民の悲しいところですわね。みんながみんな、わたくしのように個人用のスーパーコンピュータを持っているというわけでもないでしょうし……。

環境光の近似

一様な環境光

そこで一番初めに思いつくのがどの場所、どの角度でも一様な環境光が当たっていると考える近似方法ですの。
この場合、環境光の色をそのまま出力に足してあげればいいので簡単ですわね。
Unityでは環境光の値は[Window]->[Rendering]->[Lighting]で開いたウィンドウの[Environment]タブの中にある[Environment Lighting]という項目で設定することができるんですのよ。
今考えている「一様な環境光」は[Environment Lighting]->[Source]を「Color」にして設定できる[Ambient Color]から変更できますの。
f:id:spi_8823:20200517090736p:plain
ここで指定した値はシェーダのコードの中からUNITY_LIGHTMODEL_AMBIENTで取得できるんですのよ。

グラデーションする環境光

でも一様な環境光というのはそこまで現実的ではないんですのよ。だってほら、空は青いし、地面は茶色みたいなくすんだ色をしていることが多いじゃない。だから上を向いている面には青い環境光が、下を向いている面には茶色い環境光が差し込むとしたほうがよりそれらしく見えるわ。
こんな風に面の角度によってグラデーションする環境光も[Environment Lighting]から設定できますのよ。
[Source]を「Gradient」にすればほら、[Sky Color], [Equator Color], [Ground Color]が設定できるでしょう?これはそれぞれ「空の色」「水平線の色」「地面の色」を表しているの。これら3色を面の向きに合わせてグラデーションさせてあげれば、よりそれっぽく見えますの。
f:id:spi_8823:20200517091857p:plain
シェーダからはそれぞれunity_AmbientSky, unity_AmbientEquator, unity_AmbientGroundで取得できますわ。

Skyboxによる環境光

環境光にはSkyboxも指定できるんですの。
これを設定するとまさしく空の色として使っているSkyboxの色が環境光になるようですのよ。
デフォルトのシェーダではこちらを使って綺麗に描画してるのですけれど。ごめんなさい、これをシェーダから使用する方法は見つけられなかったわ。

というわけで今回は2番目の「グラデーションする環境光」を使った描画をすると次のようになりますのよ!
少しずつ「リッチ」な見た目に近づいてきたんじゃありませんこと?もちろん、わたくしのおうちのおリッチさには到底及びもしませんけれどね!おほほほほ!!
f:id:spi_8823:20200517095233p:plain

ついでにDiffuse Reflectionも上乗せした描画結果も置いておきますわ!
よくある見た目になったんじゃありませんこと?
f:id:spi_8823:20200517131023p:plain

コード

グラデーションする方法についてはコード中に詳しく書いてあるのでしっかりと感謝しながら読むんですのよ!
前回の鏡面反射のコードも一緒に書いてあるので気を付けて読むといいのですわ。具体的には頂点シェーダの後半部分が今回の「環境光」の部分ですのよ。

Shader "Custom/SpecularHighlights"
{
    Properties
    {
        _SpecularRatio ("SpecularRatio", float) = 1
        _AmbientRatio ("AmbientRatio", float) = 0.5 //環境光の度合い(大きいほど濃く環境光が描画されますわ)
    }
    SubShader
    {
        pass
        {
            Tags { "LightMode" = "ForwardBase" }
            CGPROGRAM
            #pragma vertex v
            #pragma fragment f

            #include "UnityCG.cginc"

            uniform float _SpecularRatio;
            uniform float _AmbientRatio;

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

            v2f v(float4 vertexPos : POSITION, float3 normal : NORMAL)
            {
                v2f output;
                output.sv_position = UnityObjectToClipPos(vertexPos);
                output.vertexPos = vertexPos;
                output.normal = normal;

                //環境光の計算ですわ!
                float4 upAmbient = lerp(unity_AmbientEquator, unity_AmbientSky, normal.y);  //空の色と地平線の色を法線の上向き加減で補間しますの
                float4 downAmbient = lerp(unity_AmbientEquator, unity_AmbientGround, -normal.y);    //地面の色と地平線の色を法線の下向き加減で補完しますの

                //法線が上を向いている時はupAmbientを、下を向いているときはdownAmbientを使いますの。
                //シェーダでは並列計算を行っているのでif文はあまり推奨されませんの。その代わりにstep関数を使えば同じようなことができますのよ!
                output.ambientColor = upAmbient * step(0, normal.y) + downAmbient * step(normal.y, 0);
                output.ambientColor = output.ambientColor * _AmbientRatio;  //環境光の度合いをかけてるだけですのよ

                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);
                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) + input.ambientColor, 1);

                return color;
            }
            ENDCG
        }
    }
}

【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

【Cg Programming/Unity】Basic Lighting ~ Diffuse Reflection【順番にやっていく】

今回はこちら。
en.wikibooks.org
この節ではDiffuse Reflection、つまり「拡散反射」というものの描画方法について学ぶ。

拡散反射

拡散反射とは何か。例に上がっているのは月面の反射である。
月の表面は非常に凹凸が激しいがゆえに、入射した光は入射時の角度の記憶を失い、完全にランダムな方向へ反射される。
つまり、一定の光線が入射している面をどの角度から見ようと、その明るさは変わらないというわけだ。言い換えれば前節において_WorldSpaceCamraPosとして使用したような視線方向には関わらない描画となる。

一方で、ある面が感じる光の強さというものは、その面に対して垂直に光線が入射するほど大きい。つまり車のヘッドライトを横から見ると別に何ともないが、真正面から浴びせられると目が潰れてしまうほど強く光を感じるのと同様である。
面とベクトルが垂直であるかはそのベクトルと、面の法線ベクトルの内積の絶対値が1に近いかどうかで判別できるのだった。
そして一定の光量下で単純な拡散反射を考えた場合、物体が見える明るさは光線のベクトルと物体の面の法線ベクトルとの内積によってのみ決まる。簡単に書くと以下のようにあらわせる。

物体の明るさ = dot(面の法線ベクトル, 光線の方向ベクトル)

ここまでわかれば後はシェーダで書くだけだ。

コード

Shader "Custom/DiffuseReflection"
{
    SubShader
    {
        pass
        {
            Tags { "LightMode" = "ForwardBase" }
            CGPROGRAM
            #pragma vertex v
            #pragma fragment f

            #include "UnityCG.cginc"

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

            v2f v(float4 vertexPos : POSITION, float3 normal : NORMAL)
            {
                v2f output;
                output.sv_position = UnityObjectToClipPos(vertexPos);

                float4 worldPos = mul(unity_ObjectToWorld, vertexPos);  //頂点のワールド座標
                float3 worldNormal = UnityObjectToWorldNormal(normal);  //面の法線(UnityCG.cgincの関数を私用)

                //光源への向きを計算
                float3 lightDirection = _WorldSpaceLightPos0;   //Directional Light(太陽光のようにすべての空間に一定の方向で光が降り注ぐ)の場合はこう
                //float3 lightDirection = normalize((_WorldSpaceLightPos0 - worldPos).xyz);   //Point Light(点光源、つまり一点から等方的に光を発している)場合はこう
                

                //反射の度合いを計算
                //光源への向きと面の法線が同じ方向を向いているほど大きくなる
                float reflection = dot(lightDirection, worldNormal);    

                output.color = float4(float3(1, 1, 1) * max(reflection, 0), 1);

                return output;
            }

            float4 f(v2f input) : COLOR
            {
                return input.color;
            }
            ENDCG
        }

        pass
        {
            Tags { "LightMode" = "ForwardAdd" }
            Blend One One
            CGPROGRAM
            #pragma vertex v
            #pragma fragment f

            #include "UnityCG.cginc"

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

            v2f v(float4 vertexPos : POSITION, float3 normal : NORMAL)
            {
                v2f output;
                output.sv_position = UnityObjectToClipPos(vertexPos);

                float4 worldPos = mul(unity_ObjectToWorld, vertexPos);  //頂点のワールド座標
                float3 worldNormal = UnityObjectToWorldNormal(normal);  //面の法線(UnityCG.cgincの関数を私用)

                //光源への向きを計算
                //Directional Lightかどうかは_WorldSpaceLightPos0のwを見て判別する
                float3 lightDirection;
                if(_WorldSpaceLightPos0.w == 0)
                {
                    lightDirection = _WorldSpaceLightPos0;   //Directional Light(太陽光のようにすべての空間に一定の方向で光が降り注ぐ)の場合はこう
                }
                else
                {
                    lightDirection = normalize((_WorldSpaceLightPos0 - worldPos).xyz);   //Point Light(点光源、つまり一点から等方的に光を発している)場合はこう
                }

                //反射の度合いを計算
                //光源への向きと面の法線が同じ方向を向いているほど大きくなる
                float reflection = dot(lightDirection, worldNormal);    

                output.color = float4(float3(1, 1, 1) * max(reflection, 0), 1);
                //output.color = float4(lightDirection, 1);
                return output;
            }

            float4 f(v2f input) : COLOR
            {
                return input.color;
            }
            ENDCG
        }
    }
}

_WorldSpaceLightPos

光源のワールド座標は_WorldSpaceLightPos0というuniform変数で取得できる。
末尾に”0”が入っていることからわかるように、複数の光源がある場合は_WorldSpaceLightPos1のようにインデックスを変えた変数名でアクセスする。

LightModeの指定

さて、ここで注意しておきたいのがpassの先頭でTags { "LightMode" = "ForwardBase/ForwardAdd" }と指定している点である。
ライティングの最適化等の観点から、優先して描画すべきライトと、簡略化してもよいライトに分けられているようです。
(簡略化というのは例えば、複数のDirectional Lightを1つの行列で表してしまうもの(多分。。。))
何もしていなければ1つ目のDirectional LightはForwardBaseで描画され、2番目以降のDirectional LightやPoint LightはForwardAddで描画される?
(ちょっとこの辺どうなってるかはわからんのでそのうち詳しく調べることにする)
とにかく、ForwardBaseと指定したpassではDirectional Lightの描画処理を、ForwardAddと指定したpassにはどっちが来るかわからんので_WorldSpaceLightPosのw座標に入っている値で判別するらしい。

そしてpassを2つ以上通るため、2つ目のpassではBlend One Oneを指定して1つ目のpassの結果に加算するように描画している。

Directional LightとPoint Lightを1つずつ用意して描画したものは以下のようになる。
f:id:spi_8823:20200508012333p:plain

passを2つ以上書き始めるといよいよコードが長くなってきましたな。

次回はこちら。
spi8823.hatenablog.com

【Cg Programming/Unity】透過 ~ シルエットの強調【順番にやっていく】

今回はこちら。
en.wikibooks.org
この節ではオブジェクトのシルエットを際立たせる効果について学ぶ。

シルエットの強調

例えばガラス製のコップを考えよう。
コップを横から見た場合、全体的に透明で向こうが透けて見えるが、両端に行くにつれて少しガラスの色が濃くなるだろう。
これは別にガラスそのものの色が濃くなっているわけではなくて、端のほうがガラスの面が視線と平行になるため、相対的にガラスが分厚くなっているためにガラスの色がより強調されて見えるのだ。
(ラップ1枚だけだと透明だが、ラップを何枚も重ねると段々白っぽくなっていくのと多分一緒)

この効果は半透明なものであれば大抵発生しているはずだ。
ならばシェーダで再現せねば。

端の判定

「面が視線と平行になっているほど」ガラスの色は濃くなる。
これを数学的に言い換えれば、「面の法線ベクトルと視線のベクトルとが垂直であるほど」ということになる。
そしてこれをベクトル計算の手法に当てはめれば「ベクトルとベクトルの内積が0に近いほど」、それらは垂直に交わっているのだ。
式で表せばこうだ。

verticality = 1 - [dot(normal, directionToCamera)の絶対値]

言われてみれば簡単な話だ。

コード

Shader "Custom/SilhouetteEnhancement"
{
    Properties
    {
        _SilhouetteColor ("Silhouette Color", Color) = (1, 0, 0, 0.5)
    }
    SubShader
    {
        Tags { "Queue" = "Transparent" }
        pass
        {
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex v
            #pragma fragment f

            //UnityCG.cgincというライブラリを読み込む
            #include "UnityCG.cginc"

            uniform float4 _SilhouetteColor;
            struct vertexInput 
            {
                float4 vertexPos : POSITION;
                float3 normal : NORMAL;
            };
            struct v2f
            {
                float4 position : SV_POSITION;
                float4 color : TEXCOORD0;
            };

            v2f v(vertexInput input)
            {
                v2f output;
                output.position = UnityObjectToClipPos(input.vertexPos);
                
                //ワールド座標系での法線ベクトル
                //UnityObjectToWorldNormalは"UnityCG.cginc"で定義されるヘルパー関数
                float3 normal = UnityObjectToWorldNormal(input.normal);
                //ヘルパー関数を使わない方法は以下
                //float4x4 modelMatrixInverse = unity_WorldToObject;
                //float3 normal = normalize(mul(float4(normal, 0), modelMatrixInverse).xyz);

                //ワールド座標系での頂点からカメラへのベクトル
                float3 viewDirection = normalize(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, input.vertexPos)).xyz;

                output.color = (1 - dot(normal, viewDirection)) * _SilhouetteColor;
                return output;
            }

            float4 f(v2f input) : COLOR
            {
                return input.color;
            }
            ENDCG
        }
    }    
}

これを描画すると以下のような見た目になる。かっこいい。
f:id:spi_8823:20200506194558p:plain

法線の変換

注意しておきたいのが、法線をワールド座標系に直す場合はunity_ObjectToWorldではなくunity_WorldToObject、つまり座標をワールド座標系からローカル座標系に戻す行列を、通常とは逆の順番でかけてやらなければならない。
これは、ベクトルと座標はまったく同じようには扱えないということが理由である。
詳しくは以下を参照。
raytracing.hatenablog.com

これでTransparencyの章は終わり。
次はこちら。
spi8823.hatenablog.com

【Cg Programming/Unity】透過 ~ 描画順に依存しない透過【順番にやっていく】

今回はこちら。
en.wikibooks.org
前節でやったような一般的なアルファブレンディングの場合、描画する順番で最終的な見え方が異なる。
そのようなアルファブレンディングは描画順がオブジェクト単位で決まってしまう環境ではうまく描画されないことがあるというのは前回言ったとおりだ。

そこで、それならば「描画順に依存しないアルファブレンディングをしてしまえばいいではないか」というのがこの節の内容である。

描画順に依存しないアルファブレンディング

前節では以下のようなブレンディングを行っていた。

Blend SrcAlpha OneMinusSrcAlpha

これだと初めに描画されたものほど次々と上書きされて行ってしまう。
これを避けるため、可換なブレンディングを行う必要がある。
可換なブレンディングというのも色々あると思うが、最も単純なのは加算または乗算によるブレンディングだ。
加算はSrcColor + DstColor = DstColor + SrcColorで可換だし、乗算はSrcColor * DstColor = DstColor * SrcColorでやはり可換だ。
(すごく適当なことを言っている)

というわけでこれらを行うために必要なブレンディングの指定方法は以下である。

加算

1つ目の項がDstColorに依存せず、2つ目の項がSrcColorおよびDstColorに依存しない値であれば加算になると思う。

Blend One One
Blend ScrAlpha One  //アルファを使いたければこう

乗算

1つ目の項はZeroで、2つ目の項はScrColorに依存した値であれば乗算ブレンディングとなる。

Blend Zero SrcColor  //2つ目の項がそのままDstColorとSrcColorをかけたものになる
Blend Zero OneMinusSrcAlpha

コード

というわけで上記のブレンディングを実際に行うとこうなる。

Shader "Custom/OrderIndependentTransparency"
{
    SubShader
    {
        Tags { "Queue" = "Transparent" }
        pass
        {
            ZWrite Off
            Cull Back

            //アルファブレンディングの形式を指定
            Blend One One   //加算
            //Blend Zero SrcColor   //乗算

            CGPROGRAM
            #pragma vertex v
            #pragma fragment f

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

            v2f v(float4 vertexPos : POSITION)
            {
                v2f output;
                output.position = UnityObjectToClipPos(vertexPos);
                output.color = float4(vertexPos.xyz, 0.5) + float4(0, 0, 0, 0);
                return output;
            }

            float4 f(v2f input) : COLOR
            {
                return input.color;
            }
            ENDCG
        }
    }
}

f:id:spi_8823:20200506072231p:plain

Sceneビューで視点をぐりぐり動かしてもらえばわかると思うが、Cube同士が重なっている箇所の色は視点を変えても不自然に変わったりしないようになっている。
(前節のブレンディングで同じように視点を動かすと、オブジェクト同士の前後関係が変わったときにチラッチラッと描画が変わってしまうのだ)

以上。
次はこちら。
spi8823.hatenablog.com

【Cg Programming/Unity】透過 ~ 透明な物体の描画【順番にやっていく】

今回はこちら。
en.wikibooks.org
この節では透過について学ぶ。
サンプル画像として載っている絵画がエッチでかわいい。

透過

例えば色ガラスを覗いたときのように、向こうのものが透けて見えるオブジェクトを描画したい場合、透けて見える景色の色と、色ガラス自身の色とを混ぜ合わせた色を描画する必要がある。
このような操作をBlendingと呼ぶ。
大抵の場合、向こうの景色の色を何割、色ガラスの色を残りの何割といったような感じで混ぜ合わせるのだが、何割という値を指定するのにColorの4番目のパラメータであるα値を用いる。このようなBlendingを特にアルファブレンディングと呼ぶ。

用語

これから描画しようとしているフラグメントシェーダの出力値をSource、すでに描画されている背景のことをDestinationと呼ぶ。

一般的なアルファブレンディング

一般的なアルファブレンディングでは最終的に描画される色は以下のようになっている。

result = SrcAlpha * SrcColor + (1 - SrcAlpha) * DstColor

ただし、SrcAlphaは4つのパラメータすべてがSourceのアルファ値であるfloat4ベクトル。
SrcColorはそのままSourceの色(float4)、DstColorDestinationの色(float4)である。

このようなブレンディングを行いたい場合、コード上のCGPROGRAMの外側で以下のように指定してやる。

Blend SrcAlpha OneMinusSrcAlpha

Blendの後ろ、1つ目にSourceの色に掛け合わせる値(float4)の名前、2つ目にDestinationの色に掛け合わせる値(float4)の名前を指定する。

Blend係数

SrcAlphaのように指定できる値はBlend係数と呼び、ほかにも色々ある。

One

float4(1, 1, 1, 1)

Zero

float4(0, 0, 0, 0)

SrcColor

Sourceの色(フラグメントシェーダで返す値)

SrcAlpha

SrcColor.aaaa

DstColor

背景の色

DstAlpha

DstColor.aaaa

OneMinusSrcColor

float4(1, 1, 1, 1) - SrcColor

OneMinusSrcAlpha

float4(1, 1, 1, 1) - SrcAlpha

詳しくは公式マニュアルで。(ただし時々翻訳が頼りないので注意)
docs.unity3d.com

コード

実際にBlendingを行うシェーダのコードは以下のようになる。

Shader "Custom/Transparency"
{
    SubShader
    {
        Tags { "Queue" = "Transparent" }
        pass
        {
            ZWrite Off
            Cull Front

            //アルファブレンディングの形式を指定
            //この場合はSrcAlphaがこのパスで出力する色のアルファ値で、
            //最終的には (このパスで出力する色)xSrcAlpha + (背景にすでに描画されている色)x(1-SrcAlpha)がピクセルの色になる
            Blend SrcAlpha OneMinusSrcAlpha
            //Blend One Zero    //こんな書き方をすると単純に上書きしているのと同じ
            //Blend SrcColor DstColor   //こんな書き方をすると色の2乗同士を足し合わせることになるが、使う場面が思いつかない

            CGPROGRAM
            #pragma vertex v
            #pragma fragment f

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

            v2f v(float4 vertexPos : POSITION)
            {
                v2f output;
                output.position = UnityObjectToClipPos(vertexPos);
                output.color = float4(vertexPos.xyz, 0.5) + float4(0, 0, 0, 0);
                return output;
            }

            float4 f(v2f input) : COLOR
            {
                return input.color;
            }
            ENDCG
        }
        
        pass
        {
            ZWrite Off
            Cull Back
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex v
            #pragma fragment f

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

            v2f v(float4 vertexPos : POSITION)
            {
                v2f output;
                output.position = UnityObjectToClipPos(vertexPos);
                output.color = float4(vertexPos.xyz, 0.5) + float4(1, 1, 1, 0);
                return output;
            }

            float4 f(v2f input) : COLOR
            {
                return input.color;
            }
            ENDCG
        }
    }
}

まず裏面をアルファブレンディングで背景に重ねて描画し、そのあとに表面を同じように重ねて描画している。
こうすることによってCubeが透明な箱のように見える。
わかりやすくするため同じシェーダを適用したCubeをもう一つ少しずらした位置に置いてある。
f:id:spi_8823:20200505235624p:plain

注意点

さて、ここで少し注意しておきたいのが、描画がオブジェクト単位で行われるということだ。
例えば箱同士が重なっている場合、正確には箱①表、箱②表、箱①裏、箱②裏の順に描画されてほしいところだがこの方法だと箱①表、箱①裏、箱②表、箱②裏になってしまう。
これを解決する方法はのちのち出てくると思う。

と思っていたら次の節で解決方法が1つ示されていた。
spi8823.hatenablog.com