約半年ほど、時間を開けてしまいましたが、こちらの読書記録シリーズの続きです。
もはや自分にとって教科書みたいなシリーズです。(印刷してホチキスで止めている。)
vol.3は全18章(改訂ではさらに+1章らしい)もあるので、区切りのいいところ(頭がパンクしたところ)で分割して理解を深めていきたいと思います。
今回は各種光源の影響を実装する9章までです。
学んだことのメモ(章ごと)
第1章:法線の可視化
ここではまずオブジェクトの法線方向を単純に可視化させます。 UnityObjectToWorldNormal関数というのが出てきて、これでオブジェクト空間での法線からワールド空間での法線を求めることができるようです。早速UnityCG.cgincで中身を見てみます。
// Transforms normal from object to world space inline float3 UnityObjectToWorldNormal( in float3 norm ) { #ifdef UNITY_ASSUME_UNIFORM_SCALING return UnityObjectToWorldDir(norm); #else // mul(IT_M, norm) => mul(norm, I_M) => {dot(norm, I_M.col0), dot(norm, I_M.col1), dot(norm, I_M.col2)} return normalize(mul(norm, (float3x3)unity_WorldToObject)); #endif }
UNITY_ASSUME_UNIFORM_SCALING
がなんだかわかりませんが、ワールド行列の逆変換(unity_WorldToObject
)とかけたりしていますね?うむ、ひとまず深追いはやめておきます。あくまで雰囲気。
また、本来法線をつかって色付けをさせるとポリゴンのつなぎ目が見えるはずが見えていない、つまり補間(ラスタライズ)がされていることがわかります。(これは、よくよくかんがえるとよくわからなくなるやつです。) blogs.nvidia.co.jp
それと、最初のサンプルでは appdata
(Vertexシェーダへの入力項目を定義する構造体) を自分で書いてましたが、雛形がいくつかUnityCG.cgincに用意されています。このあとのサンプルでも出てくるappdata_base
は以下のような定義でした。
struct appdata_base { float4 vertex : POSITION; // 頂点 float3 normal : NORMAL; // 法線 float4 texcoord : TEXCOORD0; // UV UNITY_VERTEX_INPUT_INSTANCE_ID };
UNITY_VERTEX_INPUT_INSTANCE_ID
はちゃんと調べていませんが、名前から察するにインスタンシングさせた際などに使われる個々の固有のIDでしょう。ちなみにUnityCG.cgincには他に appdata_img
appdata_tan
appdata_full
とかが雛形として用意してありました。
第2章:Lambertシェーダ
光源情報 _WorldSpaceLightPos0.xyz
は、光源の位置を意味しますが、シーンに並行光源しかない場合は、位置ではなく光源ベクトルと同義となります。拡散反射光(の強さ?)を算出するにはLambertの余弦則「法線と光源ベクトルの差が小さい(cosθが大きい)とき、より拡散反射する」というのが使えるようです。法線と光源ベクトルそれぞれの単位ベクトルの内積からcosθを求めます。
float3 normal = normalize(i.normal); float3 light = normalize(_WorldSpaceLightPos0.xyz); // ここでは平行光源なので光源ベクトル float diffuse = saturate(dot(normal, light));
Lambertの余弦則については初耳だったのでこちらのサイトを読みました。
第3章:Flat, Phong, Gouraud シェーダ
先に出てきたラスタライズ時の補間は修飾子一つで無効化できるようです。(interpolation
= 補間)
struct v2f
{
float4 vertex : SV_POSITION;
nointerpolation float3 normal : TEXCOORD0;
};
このように補完処理をせず計算量を減らす「Flatシェーディング」とは別に、法線と光源ベクトルの内積を求めるのが高コストだった時代に使われていた手法として、画素単位(fragment shader側)ではなく頂点単位(vertex shader側)で光線ベクトルの内積を算出する「Gouraudシェーディング」というのもあったそうです。ただ、これは(画素単位で内積を計算する)Phongシェーディングよりは軽量ですが、当然その分見栄えは悪くなってしまいます。(まあ、ポリゴン数増やせば当然解決されますが、負荷の側面では本末転倒。)
第4章:ColoredLight
Lambertシェーディングでcosθを少し補正することで擬似的に影をわずかに明るくさせる手法(⇒Half-Lambertシェーディング)が提案されました。
float diffuse = saturate(dot(normal, light)); diffuse = pow(diffuse * 0.5 + 0.5, 2);
式を見て、割と”ご都合”ですなーと思いつつ、名前から補正するにあたって半分にしたりしてるからHalf-
が頭についているのかな?と思ったら、そうではなくあのValve社の「Half-Life」シリーズで初めて実装されたらしくこの名前らしいです。へぇ。
ちなみに、UnityCG.cgincを見たんですが、DecodeDirectionalLightmapってところにHalf-Lambertの計算式っぽいのが出てきました。
第5章:AmbientLight
環境光をシェーダ側で得る方法には、①unity_AmbientSky②ShadeSH9関数を使う方法の2つがあるようです。 ①の方法はそのままunity_AmbientSkyでUnityが用意した環境光をfloat4の色情報として取得できます。シンプル。
return _MainColor * ((diffuse * _LightColor0) + unity_AmbientSky);
②のShadeSH9関数は引数に(正規化された)法線を取ります。
// 引数はfloat4 or half4、結果はfloat3 float3 ambient = ShadeSH9(half4(normal, 1)); fixed4 color = diffuse * _MainColor * _LightColor0; // ambientはfloat3なのでちょっと面倒 color.rgb += ambient * _MainColor; return color;
ShadeSH9の中身は見たんですが、ちょっとよくわかりませんでした。
ググって出てきたこのサイトも読んだけど今の自分には難しい。
第6章:MultiLight
サンプルのシェーダーを見ると、SubShader下にPass構文が2つ定義されています。長い。違うのはLightMode。
● LightMode=ForwardBaseはフォーワードレンダリングの最初に実行されるPass ● LightMode=ForwadAddはForwardBaseに追加して実行されるPass
ちなみに、環境光はForwadBaseだけで考慮すれば良い(↔環境光専用のPassはない)です。
共通処理をライブラリ化するにあたってLightModeを簡単に判別する仕組みがあるみたいです。
1つがUNITY_PASS_FORWARDBASE
です。これによって、「現在のPassがForwardBaseかどうか?」がわかります。
が!それよりもさらに便利なのが先のShadeSH9関数で、これはForwardBase以外のPassでは常に(0,0,0)を返します!
引数と結果の型が違ったりで扱いにくいと思ったけど、すごい便利!
第7章:PointLight
「光は減衰する」。 減衰率(attenuation)の算出方法はいろいろあり、サンプルでは点光源の場合の3種類の方法が紹介されていました。
// 1. 光源までの距離で"線形"に減衰 float attenuation = saturate(lerp(1, 0, llength / range)); // 2. 光源までの距離の"二乗に反比例"する減衰(rangeを考慮しない) // (分母の+1は0になるのを防ぐため) float attenuation = 1 / (1 + llength * llength); // 3. 2のrangeを考慮した減衰 float attenuation = saturate(lerp(1, 0, llength / range));
それと点光源の範囲外では_WorldSpaceLightPos0は0になります。
第8章:SpotLight
スポットライトの照射角の内か外かの判定=光線ベクトル(L)とスポットライトの方向ベクトル(S)のなす角度との大小比較(この角度はLとSの内積から求められる)で求めます。
さらに、スポットライトの光は外側に向かってわずかに漏れて光る(減衰していく)現象を「フォールオフ」と呼ぶらしいです。一瞬「?」となりますが、画像とか見るとわかりました。ライトの柔らかさ、みたいなところですね。
この「フォールオフ」による減衰と距離による減衰を同時に考慮するのは少しややこしい+算出コストも高いようです。これは直感でもわかりました。
第9章:VariousLight
光源の種類をシェーダー側で判定する方法には①Unityが用意したPOINT,SPOTなどのキーワード定義②シェーダーバリアントを作るmulti_compile_fwdaddがあります。
②のmulti_compile_fwdaddは multi_compile DIRECTIONAL POINT SPOT
のショートカット、つまり、②も内々では①を使ってコンパイル結果を分けているにすぎないみたいです。(multi_compile_fwdaddの宣言みたいなのはどこにあるんだろう?)multi_compile_fwdaddを使うと、UNITY_LIGHT_ATTENUATION
マクロから任意の光源の減衰が得られる仕組みのようです(強い)。
UNITY_LIGHT_ATTENUATIONは UnityCG.cgincと同じディレクトリにある AutoLight.cginc に定義されていました。ただ、これはまずファイルの読み方がわからない…(UNITY_SHADOW_ATTENUATION
から先が追えない。)スポットライトやポイントライトごとに定義されているところまではわかりました。
ひとまず、UNITY_LIGHT_ATTENUATION
の引数は①減衰率を受け取る変数②Vertexシェーダからの入力③ワールド空間の頂点座標だということ、①は呼び出し側では定義不要(地味だけど重要)なことだけ抑えておきます。
というか、つまり個々まで各章でいろいろやってきたけど、よほどのことがない限り、このUNITY_LIGHT_ATTENUATIONを使うのが正義って認識で良いっぽい!?
ここまで
あくまで自分向けの読書メモです。 個人的には本書はここから先が本題だと思っているので、ゆっくり読み進めたいと思います。
(これ読み終わったら、UnityGraphicsProgrammingSeriesをやっとこさ読んでみようと思っています。) github.com