Continue(s)

Twitter:@dn0t_ GitHub:@ogrew

【Unity】Unity Shader Programming Vol.03読んだ。(その2)

今週はテッド チャンの「息吹」を読むのに熱心です。

taiga.hatenadiary.com

前回に引き続き、です。今回は10章から12章までPhongの反射モデルの実装に踏み込んでいきます。(短め。)

学んだことのメモ(章ごと)

第10章:Phong モデル

「Phongの反射モデル」の実装パートです。このモデルはこれまで扱ってきた拡散反射光(と環境光)に加えて、ここまで踏み込んでこなかった鏡面反射光を計算に含めるものです。鏡面反射光(=ハイライト)は肉眼で金属などの物体を見るときに観察できる鋭い反射光のことです。求め方としては視線ベクトルと(光の)反射ベクトルの差(なす角)をそのまま利用します。

    // 視線ベクトル view = カメラの(ワールド)座標 - 頂点の(ワールド)座標
    float3 view   = normalize(_WorldSpaceCameraPos - i.vertexW);
    // 反射ベクトル = 入射光(便宜的にマイナス)と頂点の法線方向から計算(以下と同義)
    float3 rflt   = normalize(reflect(-light, normal));
    // 視線ベクトルと反射ベクトルの差(角度)は内積で求める = saturate(dot(view, rflt))
    float  specular = pow(saturate(dot(view, rflt)), _Shiness);

_Shinessは鏡面反射のしやすさ(ある意味では範囲と言って良いかもしれない)に影響するもので、大きければ鋭い反射(狭い範囲)で、小さければ鈍い反射(広い範囲)で表面の反射が変わります。

視線ベクトルを求めるには、実はUnityWorldSpaceViewDirというのがUnityCG.cgincに用意されていました。

inline float3 UnityWorldSpaceViewDir( in float3 worldPos )
{
    return _WorldSpaceCameraPos.xyz - worldPos;
}

「おー、まんまやな」と言う感じです。黙ってこれ使いましょう。

ところでこの章の最後に「Phongの反射モデル」と「Phongシェーディング」は意味違うからね!との注意がありました。実際Wiki「Phongの反射モデル」にも

併せてこの論文中には、多角形面モデルからラスタライズされた個々のピクセルに対して、補間計算を行う方法も論述されていた。
この補間技術は後述するようにPhongシェーディングとして知られている。

と書いてありました。ひー、誤用して叩かれないように覚えておこう。

第11章:Blinn-Phong モデル

少し読んだだけでわからなくなりそうだったので、ググってみたところこちらのサイトなどが参考になりました。自分のノートの画像を貼っておきます。 ところで「ハーフベクトル」という概念がここで出てきましたが、正しい数学用語(?)ではなくCG界隈で便宜的に使われるものなのでしょうか。それ系のサイトばかり出てきました。

f:id:taiga006:20201005225003j:plain

第12章:フレネル反射

「フレネル」という言葉は以前Blenderでセルルックな表現に挑戦した際にキャラのリムライトに代替させる方法として出てきた言葉として覚えていました。このおかげもあって理解はしやすかったです。ざっくりと言えばオブジェクトに光を当てると輪郭がぼわーんと明るく見えるあれの実装ですね。(あるいはプールを水平に見るか垂直に見るか、みたいな例も?)

f:id:taiga006:20201006002410j:plain

ここではわざとフレネルの影響部分だけを表示させてみました。

おそらく次でVol.03の読書録は終わると思います。

参考

nn-hokuson.hatenablog.com

light11.hatenadiary.com

blog.applibot.co.jp

【Unity】Unity Shader Programming Vol.03読んだ。(その1)

約半年ほど、時間を開けてしまいましたが、こちらの読書記録シリーズの続きです。

taiga.hatenadiary.com

taiga.hatenadiary.com

もはや自分にとって教科書みたいなシリーズです。(印刷してホチキスで止めている。)

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を使うのが正義って認識で良いっぽい!?

ここまで

あくまで自分向けの読書メモです。 個人的には本書はここから先が本題だと思っているので、ゆっくり読み進めたいと思います。

xjine.booth.pm

(これ読み終わったら、UnityGraphicsProgrammingSeriesをやっとこさ読んでみようと思っています。) github.com