MRが楽しい

MRやVRについて学習したことを書き残す

シェーダ(TiledWall)を読み解く

前回、紹介したシェーダ(TiledWall)ですが、Unityではシェーダでああいった表現が可能なんですね。
・空間マッピングのシェーダを変更する
 http://bluebirdofoz.hatenablog.com/entry/2017/04/16/170500
これまでシェーダについては単なるテクスチャというレベルの認識でしかありませんでした。

しかし、有名なhololensのアプリの一つであるHoleLenz(ホールレンズ)でもシェーダの機能を活用しているようです。
MRの活用には今後、避けては通れない技術っぽいです。
・別世界への穴へ入れるHoloLens向けアプリ『HoleLenz Gate』が配信
 http://www.moguravr.com/hololenz-gate/


今回初めて動的なシェーダを活用したので、その理解を深めておきたいところです。
という訳で、今回はTiledWallのコードを読み解いていきます。

そもそもシェーダの基礎知識についてですが、同ブログの以下記事で紹介されていました。
・Unity のシェーダの基礎を勉強してみたのでやる気出してまとめてみた
 http://tips.hecomi.com/entry/2014/03/16/233943

細かな用語や修飾子については以下のページがまとまっています。
・Unity Shader まとめ
 http://unitech.hatenablog.com/entry/2015/03/11/001953

これを理解した上で、TiledWallのコードを見てみます。
TiledWall.shader

Shader "HoloLens/SpatialMapping/TiledWall"
{
    // Properties
    // カラーやベクトルなどのプロパティをこの箇所に記述
    Properties
    {
        _Color("Color", Color) = (1, 1, 1, 1)
        _TilesPerMeter("Tiles per Meter", Float) = 10
        _Mask("Mask", Int) = 1
    }
    CGINCLUDE

    // "UnityCG.cginc"をインクルード
    #include "UnityCG.cginc"
    // "UnityCG.cginc"に定義される以下の"appdata_base"構造体を利用するため
    // struct appdata_base {
    //     float4 vertex   : POSITION;
    //     float3 normal   : NORMAL;
    //     float4 texcoord : TEXCOORD0;
    // };
    // "UnityCG.cginc"には他にも色々なヘルパ関数が存在する
    // ・UnityCG.cginc
    //   https://gist.github.com/hecomi/9580605

    // v2f構造体の定義
    // v2fという名前は慣例で Vertex To Fragmentの略
    // 頂点シェーダからフラグメントシェーダに複数の値を渡す時に利用する
    struct v2f
    {
        float4 vertex : SV_POSITION;
        float3 worldPos : TEXCOORD0;
        UNITY_VERTEX_OUTPUT_STEREO
    };
    // "SV_POSITION","	TEXCOORD0"の記載はセマンティクスと呼ばれ、変数のタグの役割をする
    // フラグメントシェーダへの入力(頂点シェーダの出力)には以下の種類が定義されている
    // Type(s)         Tag          Description
    // float4          SV_POSITION  MVP 変換後の座標
    // float3          NORMAL       MVP 変換後の法線
    // float4          TEXCOORD0    1番目のテクスチャの UV 座標
    // float4          TEXCOORD1    2番目のテクスチャの UV 座標
    // float4          TANGENT      接線
    // float4, fixed4  COLOR0       線形補間された色
    // float4, fixed4  COLOR1       線形補間された色
    // Any                          タグを持たない何でも良い値
    // UV座標とはテクスチャ画像の意味
    // http://alfa.hatenablog.jp/entry/2015/08/04/022548
    // "worldPos"変数が"TEXCOORD0"タグなのは"vert"関数での変換により
    // 入力したワールド座標をテクスチャの座標として利用しようとしているため

    // "Properties"の値を利用するため、同名変数を宣言する
    // "_TilesPerMeter"の参照変数
    float _TilesPerMeter;
    // "_Color"の参照変数
    fixed4 _Color;

    // 座標と時間を元に異なる少数値を算出する関数
    inline float toIntensity(float3 pos)
    {
        // fracは引数の小数部を返す
        // lengthは引数の長さを返す
        // _Timeは時間を取得する
        // _Timeの構造体は"float4(t/20、t、t×2、t×3)"であり
        // _Time.yは処理されていない時間情報(t)
        return frac(length(pos) - _Time.y);
    }

    // 頂点シェーダ
    v2f vert(appdata_base v)
    {
        // 返り値として利用するv2f構造体の変数を作成
        v2f o;
        // インスタンス ID がシェーダー関数にアクセス可能になる
        UNITY_SETUP_INSTANCE_ID(v);
        // VR向けの頂点シェーダーへの変換を行う
        UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
        // 同次座標において、オブジェクト空間からカメラのクリップ空間へ点を変換する
        o.vertex = UnityObjectToClipPos(v.vertex);
        // mulは乗算 unity_ObjectToWorldはモデル(ワールド)マトリクス
        // 物体の頂点ごとのワールド座標を取得し、worldPosに代入している
        o.worldPos = mul(unity_ObjectToWorld, v.vertex);
        // 構造体を返す
        return o;
    }
    // 利用関数の詳細な説明は以下にある
    // https://docs.unity3d.com/ja/540/Manual/GPUInstancing.html
    // https://docs.unity3d.com/ja/540/Manual/SL-BuiltinFunctions.html

    // フラグメントシェーダ
    fixed4 frag(v2f i) : SV_Target
    {
        // floorは指定された値以下の最大の整数を返す(返却値は浮動小数点のため、7.8なら7.0)
        float3 worldIndex = floor(i.worldPos.xyz * _TilesPerMeter);
        // ワールド座標に_TilesPerMeterを乗算してから除算することで空間をボックスセルに区切る
        float3 boxelCenter = worldIndex / _TilesPerMeter;
        // toIntensity関数を通して_Colorに乗算する小数値を作成する
        float intensity = toIntensity(boxelCenter);
        // 描画するカラーを返却する
        return _Color * intensity;
    }

    ENDCG

    // SubShader
    // この箇所に以下の本体を記述
    //  - サーフェイスシェーダ または
    //  - 頂点およびフラグメントシェーダ または
    //  - 固定関数シェーダ
    SubShader
    {
        Tags 
        { 
            // Tagはあくまでタグ付けなので実動作には影響しない?
            // "RenderType(レンダータイプ)"を"Opaque(不透明)"に指定
            "RenderType"="Opaque" 
            // "Queue(描画順)"を"Geometry(デフォルト)-1"に指定
            // Background→Geometry→AlphaTest→Transparent→Overlayの順で描画される。
            // つまり通常オブジェクトより背景に近いシェーダとして設定している
            "Queue"="Geometry-1"
        }

        // OCCLUSIONのシェーダーからの名前付きパスを使用する
        // 本プログラム中で何に利用しているかは不明
        UsePass "HoloLens/SpatialMapping/Occlusion/OCCLUSION"

        Pass
        {
            // ZWrite:デプスバッファに書き込みするか制御
            // 部分的に透過のエフェクトを描く場合、"ZWrite Off"に切り替え
            ZWrite Off
            // ZTest:デプステストの実行方法
            // LEqual:既に描画されているオブジェクトと距離が等しいか、より近い場合に描画する
            //        それより遠い場合はオブジェクトで隠す
            ZTest LEqual
            // Blend:透過のカラー作成に使用
            // SrcAlpha:このステージの値はソースα値を乗算する
            // OneMinusSrcAlpha:このステージの値はフレームバッファの(1-Source Alpha)を乗算する
            Blend SrcAlpha OneMinusSrcAlpha
            // 詳細は以下を参照
            // https://docs.unity3d.com/ja/540/Manual/SL-CullAndDepth.html
            // https://docs.unity3d.com/ja/540/Manual/SL-Blend.html

            // Stencil:ステンシルバッファはピクセルマスクごとにピクセルを保存や廃棄することを目的とする
            // ステンシルバッファは、通常、1 ピクセルあたり 8 ビットの整数である
            Stencil 
            {
                // 比較した結果、値がバッファに 0 ? 255 の整数で書き込まれる
                Ref [_Mask]
                // Comp:関数はバッファの現在の内容と基準値の比較に使用される
                // NotEqual:ピクセルのレファレンス値がバッファの値と等しくない場合のみレンダリングします
                Comp NotEqual
            }
            // 本シェーダでは特に処理を担っていない?(OCCLUSIONの引用?)

            // サーフェイスシェーダのブロック開始
            CGPROGRAM
            // 関数"vert"を頂点シェーダーとしてコンパイル
            #pragma vertex vert
            // 関数"frag"をフラグメントシェーダーとしてコンパイル
            #pragma fragment frag
            // コンパイルするシェーダーターゲットの指定。
            // 5.0の場合、DX11 シェーダーモデル 5.0.を指定している。
            #pragma target 5.0
            // 指定のレンダラー用にのみシェーダーをコンパイルします。
            // d3d11はDirect3D 11/12.
            #pragma only_renderers d3d11
            ENDCG
            // サーフェイスシェーダのブロック終了
            // Cg/HLSL スニペット(頂点シェーダ / フラグメントシェーダ)
            // 頂点シェーダはすべての頂点に対して座標変換を行う
            // フラグメントシェーダはすべてのピクセルに対して計算を行う
            // 処理の流れは 頂点シェーダ→フラグメントシェーダ となる
        }
        // ここでは用いられていないがライティングに作用するシェーダーを書きたいならば
        // 以下のサーフェスシェーダーの記述を利用する
        // #pragma surface 関数名 [optionalparams]
        // このシェーダはライティングによる影響を記述していない
        // (頂点シェーダ / フラグメントシェーダを使うシェーダはそもそもテクスチャや形状の変化が目的のため)
    }
}

これだけでも半日掛かりでコメントを付けることになりました。
尚、まだ不明な点があったりします。(UsePassやStencilの用途とか)

ある程度は理解できたので、TiledWallに少し手を加えて確認してみます。
現状だと走査エフェクトの間隔が短く、動きが速すぎるので、少々目立ちすぎるように感じます。
toIntensity関数を以下のように変更してみました。

    inline float toIntensity(float3 pos)
    {
        float timeval = _Time.y / 2;
        float result = frac(length(pos) - timeval);
        float coef = (length(pos) - timeval) * -1;
        int sector = coef % 5;
        if (sector > 0) result = 0;
        return result;
    }

利用する時間係数を2で割ることで時間による変化がゆっくりと進むようにしています。
またセクタ数を判定することで、5回に1回だけ走査エフェクトが走るようにしました。

反映して確認してみます。
f:id:bluebirdofoz:20170417002249j:plain
画像だと分かりづらいですが、走査エフェクトがゆっくりと今までの5回に1回の頻度で走るようになりました。
これであまり目立たず、しかし定期的に空間マッピングを確認できます。

長くなりました。ここまでです。
一つのシェーダの理解だけで一苦労でした。
昔、シェーダの技術はそれだけで本が一つ書けると聞きましたが、こういうことですか。
そもそもシェーダ用の言語(Cg言語)を理解する必要があるため、かなり奥深いです。

更に困りごととしてデバッグが非常に難しいです。ログは仕込めるのでしょうか。
今後の学習課題とします。