凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity の ShaderGraph の仕組みについて調べてみた

はじめに

本記事は Unity Advent Calendar 5 日目の記事です。

以前の記事で UniversalRP(URP) の調査を行いました。

tips.hecomi.com

今回は、その続きとして URP / HDRP の目玉機能の一つである ShaderGraph について調べてみます。私もノードグラフではないですがテンプレートと正規表現ベースの簡単なシェーダジェネレータを作成したことがあるので、参考にしたいなと考えているのと単純に興味があったのでコード読みをしてみました。

tips.hecomi.com

この記事では URP やシェーダグラフの使い方という実践的な内容ではなく、ノードの拡張の前準備として、ちょっとマニアックですが生成されたシェーダやそのシェーダコード生成の仕組みの方を探ります。前回の記事の内容を読んでおいていただけると幾分読みやすいと思いますが、そもそもここで触れる内容を意識しなくて良いように ShaderGraph が作ってあるので、拡張しない場合は特に理解しなくても問題ない内容を扱う予定です。拡張の話は長くなりそうなので、そちらは今回の記事の内容での理解を使って別途まとめようと思います。

コードを調べながら記事を書いていくスタイルで行きますので、体系立っていないところが多々ありますがご容赦ください。

環境

  • Untiy 2019.3.0b11
  • Universal RP

Unlit 自動生成コード①

確認方法

まずは生成コードから見てみましょう。2019.3 では特別なパッケージを入れなくとも ShaderGraph は Create > Shader > * Graph から使えるようになっています。

f:id:hecomi:20191117165459p:plain

試しに Unlit なグラフを作成し、それをコンパイルしたシェーダを使ったマテリアルを適用してみると次のようになります。

f:id:hecomi:20191117174755p:plain

シェーダグラフを用いるとグラフィカルに見た目をいじれますが、内部でやっていることは編集されたノードの関係から ShaderLab のコードを生成し、それをコンパイルしている形です。生成されたコードを確認するには、このマスターノードを右クリックして出てくる Show Generated Code を選択します。

f:id:hecomi:20191117175241p:plain

Unlit 生成コード

生成されたコードを見てみるとこんな感じです(簡略化しています)。

Shader "Unlit Master"
{

Properties
{
}

SubShader
{

Tags
{
    "RenderPipeline"="UniversalPipeline"
    "RenderType"="Opaque"
    "Queue"="Geometry+0"
}

Pass
{
    Name "Pass"
    Tags 
    { 
    }
   
    Blend One Zero, One Zero
    Cull Back
    ZTest LEqual
    ZWrite On

    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #pragma prefer_hlslcc gles
    #pragma exclude_renderers d3d11_9x
    #pragma target 2.0
    #pragma multi_compile_instancing

    #pragma multi_compile _ LIGHTMAP_ON
    #pragma multi_compile _ DIRLIGHTMAP_COMBINED
    #pragma shader_feature _ _SAMPLE_GI
    
    #define _AlphaClip 1
    #define ATTRIBUTES_NEED_NORMAL
    #define ATTRIBUTES_NEED_TANGENT
    #define SHADERPASS_UNLIT

    #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderGraphFunctions.hlsl"

    CBUFFER_START(UnityPerMaterial)
    CBUFFER_END

    struct SurfaceDescriptionInputs
    {
    };
    
    struct SurfaceDescription
    {
        float3 Color;
        float Alpha;
        float AlphaClipThreshold;
    };
    
    SurfaceDescription SurfaceDescriptionFunction(SurfaceDescriptionInputs IN)
    {
        SurfaceDescription surface = (SurfaceDescription)0;
        surface.Color = IsGammaSpace() ? float3(0.7353569, 0.7353569, 0.7353569) : SRGBToLinear(float3(0.7353569, 0.7353569, 0.7353569));
        surface.Alpha = 1;
        surface.AlphaClipThreshold = 0.5;
        return surface;
    }

    struct Attributes
    {
        float3 positionOS : POSITION;
        float3 normalOS : NORMAL;
        float4 tangentOS : TANGENT;
        #if UNITY_ANY_INSTANCING_ENABLED
        uint instanceID : INSTANCEID_SEMANTIC;
        #endif
    };

    struct Varyings
    {
        float4 positionCS : SV_Position;
        #if UNITY_ANY_INSTANCING_ENABLED
        uint instanceID : CUSTOM_INSTANCE_ID;
        #endif
        #if defined(SHADER_STAGE_FRAGMENT) && defined(VARYINGS_NEED_CULLFACE)
        FRONT_FACE_TYPE cullFace : FRONT_FACE_SEMANTIC;
        #endif
    };
    
    struct PackedVaryings
    {
        float4 positionCS : SV_Position;
        #if UNITY_ANY_INSTANCING_ENABLED
        uint instanceID : CUSTOM_INSTANCE_ID;
        #endif
        #if defined(SHADER_STAGE_FRAGMENT) && defined(VARYINGS_NEED_CULLFACE)
        FRONT_FACE_TYPE cullFace : FRONT_FACE_SEMANTIC;
        #endif
    };
    
    PackedVaryings PackVaryings(Varyings input)
    {
        PackedVaryings output;
        output.positionCS = input.positionCS;
        #if UNITY_ANY_INSTANCING_ENABLED
        output.instanceID = input.instanceID;
        #endif
        #if defined(SHADER_STAGE_FRAGMENT) && defined(VARYINGS_NEED_CULLFACE)
        output.cullFace = input.cullFace;
        #endif
        return output;
    }
    
    Varyings UnpackVaryings(PackedVaryings input)
    {
        Varyings output;
        output.positionCS = input.positionCS;
        #if UNITY_ANY_INSTANCING_ENABLED
        output.instanceID = input.instanceID;
        #endif
        #if defined(SHADER_STAGE_FRAGMENT) && defined(VARYINGS_NEED_CULLFACE)
        output.cullFace = input.cullFace;
        #endif
        return output;
    }

    SurfaceDescriptionInputs BuildSurfaceDescriptionInputs(Varyings input)
    {
        SurfaceDescriptionInputs output;
        ZERO_INITIALIZE(SurfaceDescriptionInputs, output);
        return output;
    }

    #include "Packages/com.unity.render-pipelines.universal/Editor/ShaderGraph/Includes/Varyings.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Editor/ShaderGraph/Includes/UnlitPass.hlsl"

    ENDHLSL
}
        
Pass
{
    Name "ShadowCaster"
    ...
}
        
Pass
{
    Name "DepthOnly"
    ...
}
        
}

FallBack "Hidden/InternalErrorShader"

}

ざっと眺めると、ネーミングルールは基本的には URP 標準シェーダと同じでパスカルケースで行われているように見えます。 まだ見ているのがシンプルなシェーダなので自動生成の中身も最小限で逆に追い辛い感じもしますね(PackedVaryingsVaryings の違いなど)。。まずは分かるところから見ていきましょう。

シェーダの流れ

エントリポイントである頂点・フラグメントシェーダを見てみます。これらは URP 側のパッケージに含まれている UnlitPass.hlsl に記述されています(下の方で #include されています)。まずは頂点シェーダからです。

PackedVaryings vert(Attributes input)
{
    Varyings output = (Varyings)0;
    output = BuildVaryings(input);
    PackedVaryings packedOutput = PackVaryings(output);
    return packedOutput;
}

頂点シェーダでは入力された AttributesBuildVarinygs() し、更にそれを PackedVarinygs したものを出力しています。BuildVaryings は同ディレクトリの Varyings.hlsl に記述されています。この中ではその名の通り Varyings を組み立てるのですが、たくさんの #ifdef があります。ATTRIBUTES_NEED_* は頂点シェーダが必要とするデータのためのフラグ、VARYINGS_NEED_* はフラグメントシェーダが必要としているデータのためのフラグになります。

Varyings BuildVaryings(Attributes input)
{
    Varyings output = (Varyings)0;

    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_TRANSFER_INSTANCE_ID(input, output);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

    // 頂点変形に必要なオブジェクト空間の情報を収集
#if defined(FEATURES_GRAPH_VERTEX)
    VertexDescriptionInputs vertexDescriptionInputs = BuildVertexDescriptionInputs(input);
    VertexDescription vertexDescription = VertexDescriptionFunction(vertexDescriptionInputs);
    
    input.positionOS = vertexDescription.VertexPosition;
    #if defined(VARYINGS_NEED_NORMAL_WS)
        input.normalOS = vertexDescription.VertexNormal;
    #endif
    #if defined(VARYINGS_NEED_TANGENT_WS)
        input.tangentOS.xyz = vertexDescription.VertexTangent.xyz;
    #endif
#endif

    VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
    float3 positionWS = TransformObjectToWorld(input.positionOS);

    // Attributes で法線が必要か
#ifdef ATTRIBUTES_NEED_NORMAL
    float3 normalWS = TransformObjectToWorldNormal(input.normalOS);
#else
    float3 normalWS = float3(0.0, 0.0, 0.0);
#endif

    // Attributes で接線が必要か
#ifdef ATTRIBUTES_NEED_TANGENT
    float4 tangentWS = float4(TransformObjectToWorldDir(input.tangentOS.xyz), input.tangentOS.w);
#endif

    // 頂点の変形が必要か
#if defined(HAVE_VERTEX_MODIFICATION)
    ApplyVertexModification(input, normalWS, positionWS, _TimeParameters.xyz);
#endif

    // Varinygs でワールド座標が必要か
#ifdef VARYINGS_NEED_POSITION_WS
    output.positionWS = positionWS;
#endif
    
    // Varinygs で法線が必要か
#ifdef VARYINGS_NEED_NORMAL_WS
    output.normalWS = NormalizeNormalPerVertex(normalWS);
#endif

    // Varinygs で接線が必要か
#ifdef VARYINGS_NEED_TANGENT_WS
    output.tangentWS = normalize(tangentWS);
#endif

    // 通常パス、シャドウキャスターパス、メタパスで処理を変えてクリップ空間の位置を取得
#if defined(SHADERPASS_SHADOWCASTER)
    output.positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, _LightDirection));
    #if UNITY_REVERSED_Z
        output.positionCS.z = min(output.positionCS.z, output.positionCS.w * UNITY_NEAR_CLIP_VALUE);
    #else
        output.positionCS.z = max(output.positionCS.z, output.positionCS.w * UNITY_NEAR_CLIP_VALUE);
    #endif
#elif defined(SHADERPASS_META)
    output.positionCS = MetaVertexPosition(float4(input.positionOS, 0), input.uv1, input.uv2, unity_LightmapST, unity_DynamicLightmapST);
#else
    output.positionCS = TransformWorldToHClip(positionWS);
#endif

    // Varyings に必要なその他の入力情報を収集
#if defined(VARYINGS_NEED_TEXCOORD0) || defined(VARYINGS_DS_NEED_TEXCOORD0)
    output.texCoord0 = input.uv0;
#endif
#if defined(VARYINGS_NEED_TEXCOORD1) || defined(VARYINGS_DS_NEED_TEXCOORD1)
    output.texCoord1 = input.uv1;
#endif
#if defined(VARYINGS_NEED_TEXCOORD2) || defined(VARYINGS_DS_NEED_TEXCOORD2)
    output.texCoord2 = input.uv2;
#endif
#if defined(VARYINGS_NEED_TEXCOORD3) || defined(VARYINGS_DS_NEED_TEXCOORD3)
    output.texCoord3 = input.uv3;
#endif

    // Varyings で 頂点カラーが必要か
#if defined(VARYINGS_NEED_COLOR) || defined(VARYINGS_DS_NEED_COLOR)
    output.color = input.color;
#endif

    // Varyings で ビュー方向
#ifdef VARYINGS_NEED_VIEWDIRECTION_WS
    output.viewDirectionWS = _WorldSpaceCameraPos.xyz - positionWS;
#endif

    // Varyings で bitangent が必要か
#ifdef VARYINGS_NEED_BITANGENT_WS
    output.bitangentWS = cross(normalWS, tangentWS.xyz) * tangentWS.w;
#endif

    // Varyings でスクリーン座標が必要か
#ifdef VARYINGS_NEED_SCREENPOSITION
    output.screenPosition = ComputeScreenPos(output.positionCS, _ProjectionParams.x);
#endif

    // フォワードならライトマップや球面調和によるライティングの情報を収集
#if defined(SHADERPASS_FORWARD)
    OUTPUT_LIGHTMAP_UV(input.uv1, unity_LightmapST, output.lightmapUV);
    OUTPUT_SH(normalWS, output.sh);
#endif

    // フォグや頂点ライト
#ifdef VARYINGS_NEED_FOG_AND_VERTEX_LIGHT
    half3 vertexLight = VertexLighting(positionWS, normalWS);
    half fogFactor = ComputeFogFactor(output.positionCS.z);
    output.fogFactorAndVertexLight = half4(fogFactor, vertexLight);
#endif

    // _MAIN_LIGHT_SHADOWS が定義されていればシャドウマップの座標を計算
#ifdef _MAIN_LIGHT_SHADOWS
    output.shadowCoord = GetShadowCoord(vertexInput);
#endif

    return output;
}

先程の Unlit 自動生成コードを見てみるとこの内 ATTRIBUTES_NEED_NORMALATTRIBUTES_NEED_TANGENT のフラグが立っています。なので法線・接線情報が頂点シェーダ内で使うことができることになります。しかしながらこのフラグがどこからセットされているかは深くて追うのが大変なので後でまた見ることにしましょう。

PackVaryings は自動生成側にありました。これは後でもう少しだけ複雑度の上げたシェーダで見ていきましょう。

次にフラグメントシェーダです。

half4 frag(PackedVaryings packedInput) : SV_TARGET 
{    
    Varyings unpacked = UnpackVaryings(packedInput);
    UNITY_SETUP_INSTANCE_ID(unpacked);
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(unpacked);

    SurfaceDescriptionInputs surfaceDescriptionInputs = BuildSurfaceDescriptionInputs(unpacked);
    SurfaceDescription surfaceDescription = SurfaceDescriptionFunction(surfaceDescriptionInputs);

#if _AlphaClip
    clip(surfaceDescription.Alpha - surfaceDescription.AlphaClipThreshold);
#endif

#ifdef _ALPHAPREMULTIPLY_ON
    surfaceDescription.Color *= surfaceDescription.Alpha;
#endif

    return half4(surfaceDescription.Color, surfaceDescription.Alpha);
}

フラグメントシェーダの方の UnpackVaryingsBuildSurfaceDescriptionInputsSurfaceDescriptionFunctionSurfaceDescriptionFunction はいずれも自動生成コード内で作成されたものでした。SurfaceDescription という構造体が最終的に出力する情報(カラーやアルファなど)をもっていて、これに必要な情報を頂点シェーダから渡ってきた情報を PackVaryings > Varyings > SurfaceDescriptionInputs > SurfaceDescription と順に変換して求めています。

Unlit 自動生成コード②

では少しだけ複雑度を上げてみましょう。とはいえ上げすぎると追うの大変なのでノイズでディゾルブする簡単なシェーダを組みました。

f:id:hecomi:20191201185233p:plain

この結果の差分のコードを見てみます。

Shader "Unlit Master"
{

Properties
{
    Color_69C7B2FF("Color", Color) = (1, 0, 0, 0)
    Vector1_F86A2F65("Disappear", Range(0, 1)) = 0.5
}

SubShader
{
...

Pass
{
    ...
    #define ATTRIBUTES_NEED_TEXCOORD0
    #define VARYINGS_NEED_TEXCOORD0
    ...

    CBUFFER_START(UnityPerMaterial)
    float4 Color_69C7B2FF;
    float Vector1_F86A2F65;
    CBUFFER_END

    void Unity_Multiply_float (float4 A, float4 B, out float4 Out)
    {
        Out = A * B;
    }

    inline float Unity_SimpleNoise_RandomValue_float (float2 uv)
    {
        ...
    }

    inline float Unity_SimpleNnoise_Interpolate_float (float a, float b, float t)
    {
        ...
    }

    inline float Unity_SimpleNoise_ValueNoise_float (float2 uv)
    {
        ...
    }

    void Unity_SimpleNoise_float(float2 UV, float Scale, out float Out)
    {
        ...
    }

    struct SurfaceDescriptionInputs
    {
        float4 uv0;
    };

    struct SurfaceDescription
    {
        float3 Color;
        float Alpha;
        float AlphaClipThreshold;
    };

    SurfaceDescription SurfaceDescriptionFunction(SurfaceDescriptionInputs IN)
    {
        SurfaceDescription surface = (SurfaceDescription)0;
        float4 _Property_ABFB8CF9_Out_0 = Color_69C7B2FF;
        float4 _Multiply_D660A0A4_Out_2;
        Unity_Multiply_float(_Property_ABFB8CF9_Out_0, float4(2, 2, 2, 2), _Multiply_D660A0A4_Out_2);
        float _SimpleNoise_4A8A1A2_Out_2;
        Unity_SimpleNoise_float(IN.uv0.xy, 200, _SimpleNoise_4A8A1A2_Out_2);
        float _Property_812F2114_Out_0 = Vector1_F86A2F65;
        surface.Color = (_Multiply_D660A0A4_Out_2.xyz);
        surface.Alpha = _SimpleNoise_4A8A1A2_Out_2;
        surface.AlphaClipThreshold = _Property_812F2114_Out_0;
        return surface;
    }

    struct Attributes
    {
        ...
        float4 uv0 : TEXCOORD0;
        ...
    };

    struct Varyings
    {
        ...
        float4 texCoord0;
        ...
    };

    struct PackedVaryings
    {
        ...
        float4 interp00 : TEXCOORD0;
        ...
    };

    PackedVaryings PackVaryings(Varyings input)
    {
        ...
        output.interp00.xyzw = input.texCoord0;
        ...
    }

    Varyings UnpackVaryings(PackedVaryings input)
    {
        ...
        output.texCoord0 = input.interp00.xyzw;
        ...
    }

    SurfaceDescriptionInputs BuildSurfaceDescriptionInputs(Varyings input)
    {
        ...
        output.uv0 = input.texCoord0;
        ...
    }

    ...

    ENDHLSL
}

...

}

}

グラフ上で UV0 の入力が増え、これをフラグメントシェーダで参照するので、ATTRIBUTES_NEED_TEXCOORD0VARYINGS_NEED_TEXCOORD0 が増えています。またノイズのノードを導入したためノイズ関連の関数が入っています。このノイズ関数が SurfaceDescriptionFunction() の中で実行されています。これは次のようにサーフェスシェーダ経由で呼ばれているものでした(ちょっと変更)。

half4 frag(PackedVaryings packedInput) : SV_TARGET 
{    
    ...
    SurfaceDescriptionInputs surfaceDescriptionInputs = BuildSurfaceDescriptionInputs(unpacked);
    SurfaceDescription surfaceDescription = SurfaceDescriptionFunction(surfaceDescriptionInputs);
    clip(surfaceDescription.Alpha - surfaceDescription.AlphaClipThreshold);
    surfaceDescription.Color *= surfaceDescription.Alpha;
    return half4(surfaceDescription.Color, surfaceDescription.Alpha);
}

そしてノードをつないで行われる実際の計算が SurfaceDescriptionFunction() で行われています。ノード間の入出力をイメージできるようにインデントして書いてみると次のようになっています:

SurfaceDescription SurfaceDescriptionFunction(SurfaceDescriptionInputs IN)
{
    SurfaceDescription surface = (SurfaceDescription)0;

    // カラー
    // {
        // プロパティからの値取得
        // 入力: Color_69C7B2FF;
        // 出力: _Property_ABFB8CF9_Out_0
        float4 _Property_ABFB8CF9_Out_0 = Color_69C7B2FF;

        // 掛け算ノード
        // 入力: _Property_ABFB8CF9_Out_0, (2, 2, 2, 2)
        // 出力: _Multiply_D660A0A4_Out_2;
        float4 _Multiply_D660A0A4_Out_2;
        Unity_Multiply_float(_Property_ABFB8CF9_Out_0, float4(2, 2, 2, 2), _Multiply_D660A0A4_Out_2);
    // }

    // アルファ
    // {
        // ノイズノード
        // 入力: IN.uv0.xy
        // 出力: _SimpleNoise_4A8A1A2_Out_2
        float _SimpleNoise_4A8A1A2_Out_2;
        Unity_SimpleNoise_float(IN.uv0.xy, 200, _SimpleNoise_4A8A1A2_Out_2);
    // }

    // アルファクリップ
    // {
        // プロパティからの値取得
        // 入力: Vector1_F86A2F65
        // 出力: _Property_812F2114_Out_0
        float _Property_812F2114_Out_0 = Vector1_F86A2F65;
    // }

    // 上記最終出力結果を代入
    surface.Color = (_Property_ABFB8CF9_Out_0.xyz);
    surface.Alpha = _SimpleNoise_4A8A1A2_Out_2;
    surface.AlphaClipThreshold = _Property_812F2114_Out_0;

    return surface;
}

だんだんイメージできてきました。ではここらで実際にコード生成をしているところを探してみていきましょう。

スクリプトから見るコード生成

結構読み解くのは大変ですが頑張っていきます。以下の記事で独自のマスターノードを作成する方法が紹介されているので一読します(Google 翻訳すれば分かりやすく読めて面白いです)。

zhuanlan.zhihu.com

マスターノード

これを参考に読み解いていきましょう。まずは起点となるマスターノードのクラス、UnlitMasterNode クラスからたどってみます。これは com.unity.shadergraph/Editor/Data/MasterNodes に含まれています。

...
class UnlitMasterNode : 
    MasterNode<IUnlitSubShader>, 
    IMayRequirePosition, 
    IMayRequireNormal, 
    IMayRequireTangent
{
    ...
    public UnlitMasterNode()
    {
        UpdateNodeAfterDeserialization();
    }
    ...
}
...

MasterNode<T> クラスを継承して UpdateNodeAfterDeserialization() していますね。MasterNode<T> を見てみましょう。

abstract class MasterNode : AbstractMaterialNode, IMasterNode, IHasSettings
{
    ...
    public abstract string GetShader(...);
    ...
}
    
abstract class MasterNode<T> : 
    MasterNode where T : class, ISubShader
{
    List<T> m_SubShaders = new List<T>();
    ...

    public sealed override string GetShader(...)
    {
        // シェーダコードを生成
        var finalShader = new ShaderStringBuilder();
        finalShader.AppendLine(@"Shader ""{0}""", outputName);
        using (finalShader.BlockScope())
        {
           // m_SubShaders を回して SubShader シェーダの生成
            if (mode != GenerationMode.Preview || 
                subShader.IsPipelineCompatible(GraphicsSettings.renderPipelineAsset))
            {
                finalShader.AppendLines(
                    subShader.GetSubshader(this, mode, sourceAssetDependencyPaths));
            }
        }
        ...
        return finalShader.ToString();
    }
    ...

    public override void UpdateNodeAfterDeserialization()
    {
        base.UpdateNodeAfterDeserialization();

        // 全クラスから T を継承したクラスを探し出してリストに追加
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            foreach (var type in assembly.GetTypesOrNothing())
            {
                var isValid = 
                    !type.IsAbstract && 
                    !type.IsGenericType && 
                    type.IsClass && 
                    typeof(T).IsAssignableFrom(type);
                if (isValid && !subShaders.Any(s => s.GetType() == type))
                {
                    try
                    {
                        var subShader = (T)Activator.CreateInstance(type);
                        AddSubShader(subShader);
                    }
                    catch (Exception e)
                    {
                        Debug.LogException(e);
                    }
                }
            }
        }
    }
}

UpdateNodeAfterDeserialization() ではジェネリックの型を使ってクラスを探し出しインスタンス化してリストに詰めています。ここでは IUnlitSubShader が指定されているのでこれを継承したクラスが全登録されている形になります。そして GetShader() でこの登録されたサブシェーダの中から現在のパイプラインに適用可能なサブシェーダを文字列として吐き出す形になっています。ShaderLab の文法(ShaderLab ブロックの中に SubShader ブロックが N 個あり、Pass ブロックが M 個その中にある形)を思い返してみると納得できます。

サブシェーダー

ここまでは理解できたので、次はこのサブシェーダを見てみましょう。探してみると UniversalUnlitSubShader という IUnlitSubShader を継承したクラスが見つかります。UnlitSubShader という HDRP 用のものも見つかりますが、先程見たようにループの中でこちらは弾かれるので URP 側の方の UniversalUnlitSubShader だけ見ていきましょう。

namespace UnityEditor.Rendering.Universal
{

...
class UniversalUnlitSubShader : IUnlitSubShader
{
    // Unlit 用のパスの設定
    ShaderPass m_UnlitPass = new ShaderPass
    {
        displayName = "Pass",
        referenceName = "SHADERPASS_UNLIT",
        passInclude = "Packages/com.unity.render-pipelines.universal/Editor/ShaderGraph/Includes/UnlitPass.hlsl",
        varyingsInclude = "Packages/com.unity.render-pipelines.universal/Editor/ShaderGraph/Includes/Varyings.hlsl",
        useInPreview = true,

        vertexPorts = new List<int>()
        {
            UnlitMasterNode.PositionSlotId,
            UnlitMasterNode.VertNormalSlotId,
            UnlitMasterNode.VertTangentSlotId
        },
        pixelPorts = new List<int>
        {
            UnlitMasterNode.ColorSlotId,
            UnlitMasterNode.AlphaSlotId,
            UnlitMasterNode.AlphaThresholdSlotId
        },
        includes = new List<string>()
        {
            "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl",
            "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl",
            "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl",
            "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderGraphFunctions.hlsl",
        },
        pragmas = new List<string>()
        {
            "prefer_hlslcc gles",
            "exclude_renderers d3d11_9x",
            "target 2.0",
            "multi_compile_instancing",
        },
        keywords = new KeywordDescriptor[]
        {
            s_LightmapKeyword,
            s_DirectionalLightmapCombinedKeyword,
            s_SampleGIKeyword,
        },
    };

    // デプスパスの設定
    ShaderPass m_DepthOnlyPass = new ShaderPass()
    {
        ...
    };

    // 影のパスの設定
    ShaderPass m_ShadowCasterPass = new ShaderPass()
    {
        ...
    };

    // キーワードの記述
    static KeywordDescriptor s_LightmapKeyword = new KeywordDescriptor()
    {
        displayName = "Lightmap",
        referenceName = "LIGHTMAP_ON",
        type = KeywordType.Boolean,
        definition = KeywordDefinition.MultiCompile,
        scope = KeywordScope.Global,
    };

    static KeywordDescriptor s_DirectionalLightmapCombinedKeyword = new KeywordDescriptor()
    {
        ...
    };

    ...

    private static ActiveFields GetActiveFieldsFromMasterNode(UnlitMasterNode masterNode, ShaderPass pass)
    {
        ...
    }

    private static bool GenerateShaderPass(
        UnlitMasterNode masterNode, 
        ShaderPass pass, 
        GenerationMode mode, 
        ShaderGenerator result, 
        List<string> sourceAssetDependencyPaths)
    {
        UniversalShaderGraphUtilities.SetRenderState(
            masterNode.surfaceType, 
            masterNode.alphaMode, 
            masterNode.twoSided.isOn, 
            ref pass);

        var activeFields = GetActiveFieldsFromMasterNode(masterNode, pass);

        return ShaderGraph.GenerationUtils.GenerateShaderPass(
            masterNode, 
            pass, 
            mode, 
            activeFields, 
            result, 
            sourceAssetDependencyPaths,
            UniversalShaderGraphResources.s_Dependencies, 
            UniversalShaderGraphResources.s_ResourceClassName, 
            UniversalShaderGraphResources.s_AssemblyName);
    }

    public string GetSubshader(
        IMasterNode masterNode, 
        GenerationMode mode, 
        List<string> sourceAssetDependencyPaths = null)
    {
        ...

        var unlitMasterNode = masterNode as UnlitMasterNode;
        var subShader = new ShaderGenerator();

        subShader.AddShaderChunk("SubShader", true);
        subShader.AddShaderChunk("{", true);
        subShader.Indent();
        {
            var surfaceTags = ShaderGenerator.BuildMaterialTags(unlitMasterNode.surfaceType);
            var tagsBuilder = new ShaderStringBuilder(0);
            surfaceTags.GetTags(tagsBuilder, "UniversalPipeline");
            subShader.AddShaderChunk(tagsBuilder.ToString());
            
            GenerateShaderPass(unlitMasterNode, m_UnlitPass, mode, subShader, sourceAssetDependencyPaths);
            GenerateShaderPass(unlitMasterNode, m_ShadowCasterPass, mode, subShader, sourceAssetDependencyPaths);
            GenerateShaderPass(unlitMasterNode, m_DepthOnlyPass, mode, subShader, sourceAssetDependencyPaths);   
        }
        subShader.Deindent();
        subShader.AddShaderChunk("}", true);

        return subShader.GetShaderString(0);
    }

    public bool IsPipelineCompatible(RenderPipelineAsset renderPipelineAsset)
    {
        return renderPipelineAsset is UniversalRenderPipelineAsset;
    }

    public UniversalUnlitSubShader() { }
}

}

マスターノードから呼び出されていた下の方にある GetSubshader() から見ていきましょう。ShaderGenerator はシェーダのコードを吐き出すためのクラスで、ShaderChunk という文字列とインデントレベルを保存したチャンクのリストを保存しています。なので、ここに AddShaderChunk() していきコードを生成していっている形です。まさにシェーダのブロックを組み立てている感じが読み取れますね。

上の方を見てみると Pass ブロックを組み立てるための ShaderPass 構造体が生成されています。そしてこれを GenerateShaderPass() に渡し、最終的に ShaderGraph.GenerationUtils.GenerateShaderPass() へと様々な情報が渡っている形になっています。

パス

なので次は GenerationUtils に旅立ってみましょう。なお、コードは読みやすいようにエラー処理などを省いて改変しています:

namespace UnityEditor.ShaderGraph
{

static class GenerationUtils
{
    public static bool GenerateShaderPass(...)
    {
        ...
        string templateLocation = GetTemplatePath("PassMesh.template");
        ...
        string templatePath = "Packages/com.unity.render-pipelines.universal/Editor/ShaderGraph/Templates";
        var templatePreprocessor = new ShaderSpliceUtil.TemplatePreprocessor(...);
        templatePreprocessor.ProcessTemplateFile(templateLocation);
        result.AddShaderChunk(templatePreprocessor.GetShaderCode().ToString(), false);
        ...
    }
    ...
}

}

大枠の仕組みは PassMesh.template というファイルにパスのテンプレートが書かれており、このテンプレートファイルに入力する変数を組み立てる、という流れになっています。このテンプレートファイルは次のような形です。

Pass
{
    $splice(PassName)
    Tags 
    { 
        $splice(LightMode)
    }
   
    // Render State
    $splice(Blending)
    $splice(Culling)
    $splice(ZTest)
    $splice(ZWrite)
    $splice(ColorMask)
    $splice(Stencil)

    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    // Debug
    $splice(Debug)

    // --------------------------------------------------
    // Pass

    // Pragmas
    $splice(PassPragmas)

    // Keywords
    $splice(PassKeywords)
    $splice(GraphKeywords)
    
    // Defines
    $SurfaceType.Transparent:           #define _SURFACE_TYPE_TRANSPARENT 1
    $AlphaClip:                         #define _AlphaClip 1
    $Normal:                            #define _NORMALMAP 1
    $SpecularSetup:                     #define _SPECULAR_SETUP
    $BlendMode.Add:                     #define _BLENDMODE_ADD 1
    $BlendMode.Premultiply:             #define _ALPHAPREMULTIPLY_ON 1
    $Attributes.normalOS:               #define ATTRIBUTES_NEED_NORMAL
    $Attributes.tangentOS:              #define ATTRIBUTES_NEED_TANGENT
    $Attributes.uv0:                    #define ATTRIBUTES_NEED_TEXCOORD0
    $Attributes.uv1:                    #define ATTRIBUTES_NEED_TEXCOORD1
    $Attributes.uv2:                    #define ATTRIBUTES_NEED_TEXCOORD2
    $Attributes.uv3:                    #define ATTRIBUTES_NEED_TEXCOORD3
    $Attributes.color:                  #define ATTRIBUTES_NEED_COLOR
    $Varyings.positionWS:               #define VARYINGS_NEED_POSITION_WS 
    $Varyings.normalWS:                 #define VARYINGS_NEED_NORMAL_WS
    $Varyings.tangentWS:                #define VARYINGS_NEED_TANGENT_WS
    $Varyings.texCoord0:                #define VARYINGS_NEED_TEXCOORD0
    $Varyings.texCoord1:                #define VARYINGS_NEED_TEXCOORD1
    $Varyings.texCoord2:                #define VARYINGS_NEED_TEXCOORD2
    $Varyings.texCoord3:                #define VARYINGS_NEED_TEXCOORD3
    $Varyings.color:                    #define VARYINGS_NEED_COLOR
    $Varyings.viewDirectionWS:          #define VARYINGS_NEED_VIEWDIRECTION_WS
    $Varyings.bitangentWS:              #define VARYINGS_NEED_BITANGENT_WS
    $Varyings.screenPosition:           #define VARYINGS_NEED_SCREENPOSITION
    $Varyings.fogFactorAndVertexLight:  #define VARYINGS_NEED_FOG_AND_VERTEX_LIGHT
    $Varyings.cullFace:                 #define VARYINGS_NEED_CULLFACE
    $features.graphVertex:              #define FEATURES_GRAPH_VERTEX
    $splice(GraphDefines)

    // Includes
    $splice(PassIncludes)

    // --------------------------------------------------
    // Graph

    // Graph Properties
    $splice(GraphProperties)

    // Graph Functions
    $splice(GraphFunctions)

    // Graph Vertex
    $splice(GraphVertex)
    
    // Graph Pixel
    $splice(GraphPixel)

    // --------------------------------------------------
    // Structs and Packing

    $buildType(Attributes)

    $buildType(Varyings)

    // --------------------------------------------------
    // Build Graph Inputs

    $features.graphVertex:  $include("BuildVertexDescriptionInputs.template.hlsl")
    $features.graphPixel:   $include("BuildSurfaceDescriptionInputs.template.hlsl")

    // --------------------------------------------------
    // Main

    $splice(MainInclude)

    ENDHLSL
}

$splice() コマンドは辞書に登録した文字列を展開してくれるもの、$var: となっているものは ActiveFields というクラスでフラグが立っていたときに右側の文字列が書き出されるもの、$buildType は指定した C# 側の構造体をシェーダ形式にメタデータを見て変換してくれるものです。GenerateShaderPass() ではこのテンプレート展開に必要な情報をかき集めている形になります。テンプレートの形を凝視してるとなんとなく先程見た自動生成コードが見えてくる気がし...ますね。例えば ActiveFieldsAttributes.uv0 が追加されていた場合、#define ATTRIBUTES_NEED_TEXCOORD0 が追加され、先程シェーダの項で見たようにシェーダコード側の Attributes 構造体に TEXCOORD0 が渡ってくるようになります。

いくつか展開の例をざっくりと眺めてみましょう。さきほどの UniversalUnlitSubShaderGenerateShaderPass() を並べてみてみます。

ShaderPass m_UnlitPass = new ShaderPass
{
    displayName = "Pass",
    ...
    pragmas = new List<string>()
    {
        "prefer_hlslcc gles",
        "exclude_renderers d3d11_9x",
        "target 2.0",
        "multi_compile_instancing",
    },
};

public static bool GenerateShaderPass(..., ShaderPass pass, ...)
{
    spliceCommands.Add("PassName", $"Name \"{pass.displayName}\"");

    ...

    using (var passPragmaBuilder = new ShaderStringBuilder())
    {
        foreach(string pragma in pass.pragmas)
        {
            passPragmaBuilder.AppendLine($"#pragma {pragma}");
        }
        spliceCommands.Add("PassPragmas", passPragmaBuilder.ToCodeBlack());
    }
}

例えばここでは引数で渡ってきた passdisplayName がパスの名前として展開されます。同様に渡ってきた pragmasforeach で回って複数の #pragma 文として書き出されます。こんな感じでサブシェーダ側から渡ってきた構造体の情報を使ってゴリゴリ文字列を作成している感じです。ActiveFields も次のように呼び出されて TemplatePreprocessor へと渡っていきます。

class UniversalUnlitSubShader : IUnlitSubShader
{
    ...
    private static ActiveFields GetActiveFieldsFromMasterNode(
        UnlitMasterNode masterNode,
        ShaderPass pass)
    {
        var activeFields = new ActiveFields();

        ...

        baseActiveFields.Add("features.graphPixel");

        if (masterNode.IsSlotConnected(UnlitMasterNode.AlphaThresholdSlotId) || ...)
        {
            baseActiveFields.Add("AlphaClip");
        }

        if (masterNode.surfaceType != ShaderGraph.SurfaceType.Opaque)
        {
            baseActiveFields.Add("SurfaceType.Transparent");
            ...
        }

        return activeFields;
    }
    ...
}

static class GenerationUtils
{
    public static bool GenerateShaderPass(..., ActiveFields activeFields, ...)
    {
        ...
        // 恐らくここでノードの接続を辿って必要な ActiveField を集めている
        GetActiveFieldsAndPermutationsForNodes(..., activeFields, ...);
        ...
        // 渡ってきたパスで必要な ActiveField を追加
        // 例えば ShadowCaster パスではバイアスの計算のため 
        // Attributes.normalOS が要求されている
        AddRequiredFields(pass.requiredAttributes, activeFields.baseInstance);
        AddRequiredFields(pass.requiredVaryings, activeFields.baseInstance);
        ...
        var tp= new ShaderSpliceUtil.TemplatePreprocessor(activeFields, ...);
        tp.ProcessTemplateFile(...);
        ...
    }
}

構造体生成部分は次のような感じでシェーダグラフ上の情報を集めて構造体の必要なメンバを文字列として書き出していっています。先程シェーダで見た SurfaceDescriptionInputs 構造体などがここで生成されています。

public static bool GenerateShaderPass(
    ...,
    string resourceClassName, 
    string assemblyName)
{
    ...
    var pixelSlots = FindMaterialSlotsOnNode(pass.pixelPorts, masterNode);
    ...
    string pixelGraphInputName = "SurfaceDescriptionInputs";
    string pixelGraphOutputName = "SurfaceDescription";
    string pixelGraphFunctionName = "SurfaceDescriptionFunction";
    var pixelGraphInputGenerator = new ShaderGenerator();
    var pixelGraphOutputBuilder = new ShaderStringBuilder();
    var pixelGraphFunctionBuilder = new ShaderStringBuilder();

    ShaderSpliceUtil.BuildType(
        GetTypeForStruct(
            "SurfaceDescriptionInputs", 
            resourceClassName, 
            assemblyName), 
        activeFields, 
        pixelGraphInputGenerator, 
        isDebug);

    SubShaderGenerator.GenerateSurfaceDescriptionStruct(
        pixelGraphOutputBuilder, 
        pixelSlots, 
        pixelGraphOutputName, 
        activeFields.baseInstance);
}

public static Type GetTypeForStruct(
    string structName, 
    string resourceClassName, 
    string assemblyName)
{
    string assemblyQualifiedTypeName = $"{resourceClassName}+{structName}, {assemblyName}";
    return Type.GetType(assemblyQualifiedTypeName);
}

...

static class UniversalShaderGraphResources
{
    ...
    internal struct SurfaceDescriptionInputs
    {
        ...
        [Optional] Vector3 ObjectSpacePosition;
        [Optional] Vector3 ViewSpacePosition;
        [Optional] Vector3 WorldSpacePosition;
        [Optional] Vector3 TangentSpacePosition;
        [Optional] Vector3 AbsoluteWorldSpacePosition;
        [Optional] Vector4 ScreenPosition;
        [Optional] Vector4 uv0;
        [Optional] Vector4 uv1;
        ...
    }
    ...
}

static class ShaderSpliceUtil
{
    public static void BuildType(
        System.Type t, 
        ActiveFields activeFields, 
        ShaderGenerator result, 
        bool isDebug)
    {
        result.AddShaderChunk("struct " + t.Name);
        result.AddShaderChunk("{");
        result.Indent();

        foreach (FieldInfo field in t.GetFields(
            BindingFlags.Instance | 
            BindingFlags.NonPublic | 
            BindingFlags.Public))
        {
            ...
        }

        result.Deindent();
        result.AddShaderChunk("};");
        ...
    }
}

static class SubShaderGenerator
{
    public static void GenerateSurfaceDescriptionStruct(
        ShaderStringBuilder surfaceDescriptionStruct, 
        List<MaterialSlot> slots, 
        string structName = "SurfaceDescription", 
        ...)
    {
        surfaceDescriptionStruct.AppendLine("struct {0}", structName);
        using (surfaceDescriptionStruct.BlockSemicolonScope())
        {
            foreach (var slot in slots)
            {
                string hlslName = NodeUtils.GetHLSLSafeName(slot.shaderOutputName);
                ...
                surfaceDescriptionStruct.AppendLine(
                    "{0} {1};", 
                    slot.concreteValueType.ToShaderString(slot.owner.concretePrecision), 
                    hlslName);
                ...
            }
        }
    }
}

これでパスの文字列がサブシェーダ側から渡ってきた情報をテンプレートファイルに食わせて生成している仕組みの大枠が見れました。

ノードグラフ

ではこの中に更に入って各ノードがどのように出力されているかを見てみましょう。コード生成は主に 2 箇所見ていきます。1つ目はノードに紐づく関数の生成部分、2 つ目はフラグメントシェーダ内でインプットとアウトプットを行うコード部分です。

GenerateShaderPass() から辿っても良いのですが、先にノードから見てしまったほうが理解しやすいと思うので、まずはノイズ関数のノードのコードを見てみましょう。

[Title("Procedural", "Noise", "Simple Noise")]
class NoiseNode : CodeFunctionNode
{
    public NoiseNode()
    {
        name = "Simple Noise";
    }

    protected override MethodInfo GetFunctionToConvert()
    {
        return GetType().GetMethod(
            "Unity_SimpleNoise", 
            BindingFlags.Static | BindingFlags.NonPublic);
    }

    static string Unity_SimpleNoise(
        [Slot(0, Binding.MeshUV0)] Vector2 UV,
        [Slot(1, Binding.None, 500f, 500f, 500f, 500f)] Vector1 Scale,
        [Slot(2, Binding.None)] out Vector1 Out)
    {
        return
            @"
{
$precision t = 0.0;

$precision freq = pow(2.0, $precision(0));
$precision amp = pow(0.5, $precision(3-0));
t += Unity_SimpleNoise_ValueNoise_$precision($precision2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;

freq = pow(2.0, $precision(1));
amp = pow(0.5, $precision(3-1));
t += Unity_SimpleNoise_ValueNoise_$precision($precision2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;

freq = pow(2.0, $precision(2));
amp = pow(0.5, $precision(3-2));
t += Unity_SimpleNoise_ValueNoise_$precision($precision2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;

Out = t;
}
";
    }

    public override void GenerateNodeFunction(
        FunctionRegistry registry, 
        GenerationMode generationMode)
    {
        // ノイズ関数内で使う関数を登録
        registry.ProvideFunction(
            $"Unity_SimpleNoise_RandomValue_{concretePrecision.ToShaderString()}", 
            s => s.Append(@"
inline $precision Unity_SimpleNoise_RandomValue_$precision ($precision2 uv)
{
return frac(sin(dot(uv, $precision2(12.9898, 78.233)))*43758.5453);
}"));

        registry.ProvideFunction(
            $"Unity_SimpleNnoise_Interpolate_{concretePrecision.ToShaderString()}", 
            s => s.Append(@"
inline $precision Unity_SimpleNnoise_Interpolate_$precision ($precision a, $precision b, $precision t)
{
return (1.0-t)*a + (t*b);
}
"));

        registry.ProvideFunction(
            $"Unity_SimpleNoise_ValueNoise_{concretePrecision.ToShaderString()}", 
            s => s.Append(@"
inline $precision Unity_SimpleNoise_ValueNoise_$precision ($precision2 uv)
{
$precision2 i = floor(uv);
$precision2 f = frac(uv);
f = f * f * (3.0 - 2.0 * f);

uv = abs(frac(uv) - 0.5);
$precision2 c0 = i + $precision2(0.0, 0.0);
$precision2 c1 = i + $precision2(1.0, 0.0);
$precision2 c2 = i + $precision2(0.0, 1.0);
$precision2 c3 = i + $precision2(1.0, 1.0);
$precision r0 = Unity_SimpleNoise_RandomValue_$precision(c0);
$precision r1 = Unity_SimpleNoise_RandomValue_$precision(c1);
$precision r2 = Unity_SimpleNoise_RandomValue_$precision(c2);
$precision r3 = Unity_SimpleNoise_RandomValue_$precision(c3);

$precision bottomOfGrid = Unity_SimpleNnoise_Interpolate_$precision(r0, r1, f.x);
$precision topOfGrid = Unity_SimpleNnoise_Interpolate_$precision(r2, r3, f.x);
$precision t = Unity_SimpleNnoise_Interpolate_$precision(bottomOfGrid, topOfGrid, f.y);
return t;
}"));

        // ノイズ関数本体を登録
        base.GenerateNodeFunction(registry, generationMode);
    }
}

AbstractMaterialNode を継承した CodeFunctionNode 派生のクラスになっています。CodeFunctionNodeC# の関数をノードに変換してくれる便利クラスで、Unity_SimpleNoise() を属性付きの引数で作成しておき、これを GetFunctionToConvert() に登録します。すると関数のメタ情報から自動的にノードが生成されるという仕組みです。コードを覗いてみましょう。

abstract class CodeFunctionNode : AbstractMaterialNode
    , IGeneratesBodyCode
    , IGeneratesFunction
    , IMayRequireNormal
    , IMayRequireTangent
    , IMayRequireBitangent
    , IMayRequireMeshUV
    , IMayRequireScreenPosition
    , IMayRequireViewDirection
    , IMayRequirePosition
    , IMayRequireVertexColor
{
    ...
    protected abstract MethodInfo GetFunctionToConvert();
    ...
    // 関数の呼び出し部分文字列を生成
    public void GenerateNodeCode(ShaderStringBuilder sb, GenerationMode generationMode)
    {
        // コードの出力の変数の生成
        // e.g. float _SimpleNoise_4A8A1A2_Out_2;
        s_TempSlots.Clear();
        GetOutputSlots(s_TempSlots);
        foreach (var outSlot in s_TempSlots)
        {
            sb.AppendLine(
                outSlot.concreteValueType.ToShaderString() + 
                " " + 
                GetVariableNameForSlot(outSlot.id) + ";");
        }

        // コード呼び出し部分の生成
        // e.g. Unity_SimpleNoise_float(IN.uv0.xy, 200, _SimpleNoise_4A8A1A2_Out_2);
        string call = GetFunctionName() + "(";
        bool first = true;
        s_TempSlots.Clear();
        GetSlots(s_TempSlots);
        s_TempSlots.Sort((slot1, slot2) => slot1.id.CompareTo(slot2.id));
        foreach (var slot in s_TempSlots)
        {
            if (!first)
            {
                call += ", ";
            }
            first = false;

            if (slot.isInputSlot)
                call += GetSlotValue(slot.id, generationMode);
            else
                call += GetVariableNameForSlot(slot.id);
        }
        call += ");";

        sb.AppendLine(call);
    }
    ...
    // 関数本体を関数レジストリに登録
    public virtual void GenerateNodeFunction(
        FunctionRegistry registry, 
        GenerationMode generationMode)
    {
        registry.ProvideFunction(GetFunctionName(), s =>
        {
            s.AppendLine(GetFunctionHeader());
            var functionBody = GetFunctionBody(GetFunctionToConvert());
            var lines = functionBody.Trim('\r', '\n', '\t', ' ');
            s.AppendLines(lines);
        });
    }
    ...
    // ノードの生成
    public sealed override void UpdateNodeAfterDeserialization()
    {
        var method = GetFunctionToConvert();
        ...
    }
}

GenerateNodeFunction() で関数を作成し、GenerateNodeCode() で呼び出し部分のコードを作成しています。なお、関数内で使う関数(e.g. Unity_SimpleNoise_RandomValue_float)たちはノイズのノード(NoseNode)の方で関数レジストリFunctionRegistry)に登録している形になっています。

掛け算をする MultiplyNode もチラ見してみましょう。

[Title("Math", "Basic", "Multiply")]
class MultiplyNode : 
    AbstractMaterialNode, 
    IGeneratesBodyCode, 
    IGeneratesFunction
{
    public MultiplyNode()
    {
        name = "Multiply";
        UpdateNodeAfterDeserialization();
    }

    const int Input1SlotId = 0;
    const int Input2SlotId = 1;
    const int OutputSlotId = 2;
    const string kInput1SlotName = "A";
    const string kInput2SlotName = "B";
    const string kOutputSlotName = "Out";

    ...

    public sealed override void UpdateNodeAfterDeserialization()
    {
        AddSlot(new DynamicValueMaterialSlot(Input1SlotId, kInput1SlotName, kInput1SlotName, SlotType.Input, Matrix4x4.zero));
        AddSlot(new DynamicValueMaterialSlot(Input2SlotId, kInput2SlotName, kInput2SlotName, SlotType.Input, new Matrix4x4(...)));
        AddSlot(new DynamicValueMaterialSlot(OutputSlotId, kOutputSlotName, kOutputSlotName, SlotType.Output, Matrix4x4.zero));
        RemoveSlotsNameNotMatching(new[] { Input1SlotId, Input2SlotId, OutputSlotId });
    }

    string GetFunctionHeader()
    {
        return "Unity_Multiply" + "_" + concretePrecision.ToShaderString() + ...;
    }

    public void GenerateNodeCode(ShaderStringBuilder sb, GenerationMode generationMode)
    {
        var input1Value = GetSlotValue(Input1SlotId, generationMode);
        var input2Value = GetSlotValue(Input2SlotId, generationMode);
        var outputValue = GetSlotValue(OutputSlotId, generationMode);

        sb.AppendLine("{0} {1};", ...);
        sb.AppendLine("{0}({1}, {2}, {3});", GetFunctionHeader(), input1Value, input2Value, outputValue);
    }

    string GetFunctionName()
    {
        return $"Unity_Multiply_{..}_{...}";
    }

    public void GenerateNodeFunction(FunctionRegistry registry, GenerationMode generationMode)
    {
        registry.ProvideFunction(GetFunctionName(), s =>
        {
            s.AppendLine("void {0}({1} A, {2} B, out {3} Out)",
                GetFunctionHeader(),
                FindInputSlot<MaterialSlot>(Input1SlotId).concreteValueType.ToShaderString(),
                FindInputSlot<MaterialSlot>(Input2SlotId).concreteValueType.ToShaderString(),
                FindOutputSlot<MaterialSlot>(OutputSlotId).concreteValueType.ToShaderString());
            using (s.BlockScope())
            {
                switch (m_MultiplyType)
                {
                    case MultiplyType.Vector:
                        s.AppendLine("Out = A * B;");
                        break;
                    default:
                        s.AppendLine("Out = mul(A, B);");
                        break;
                }
            }
        });
    }

    ...
}

実際はバリデーションのコードがあるのでもっと長いですが、コア部分の関数を定義する GenerateNodeFunction() 部分と、関数の呼び出し部分の GenerateNodeCode() は先程に比べてシンプルです。

ノードの結合

それぞれの関数の定義と呼び出しも分かったので、最後にそれぞれのノード間の接続を見てみます。先程のコードを見返すと、関数呼び出しの GenerateNodeCode() の方で、入力や出力の変数名は GetSlotValue() で得られているのがわかります。これは AbstractMaterialNode クラスの方で定義されている関数で、ID に従って接続を探して前後のノードの入出力が同じ名前になるように名前を引っ張ってきてくれています。

こうした各ノードのコードを生成を結合している元を探していくと、再び GenerationUtils に戻ってきます。

static class GenerationUtils
{
    ...
    public static bool GenerateShaderPass(...)
    {
        ...
        SubShaderGenerator.GenerateSurfaceDescriptionFunction(...);
        ...
    }
    ...
}


static class SubShaderGenerator
{
    ...
    public static void GenerateSurfaceDescriptionFunction(...)
    {
        ...
        graph.CollectShaderProperties(shaderProperties, mode);

        // SurfaceDescription SurfaceDescriptionFunction(SurfaceDescriptionInputs IN) になる
        surfaceDescriptionFunction.AppendLine(
            String.Format(
                "{0} {1}(SurfaceDescriptionInputs IN)", 
                surfaceDescriptionName, 
                functionName), 
            false);

        // 関数スコープ
        using (surfaceDescriptionFunction.BlockScope())
        {
            // SurfaceDescription surface = (SurfaceDescription)0;
            surfaceDescriptionFunction.AppendLine(
                "{0} surface = ({0})0;", 
                surfaceDescriptionName);

            // 各ノードの関数呼び出し
            for (int i = 0; i < nodes.Count; i++)
            {
                GenerateDescriptionForNode(
                    nodes[i], 
                    keywordPermutationsPerNode[i], 
                    functionRegistry, 
                    surfaceDescriptionFunction,
                    shaderProperties, 
                    shaderKeywords,
                    graph, 
                    mode);
            }

            ...
            surfaceDescriptionFunction.AppendLine("return surface;");
        }
    }

    static void GenerateDescriptionForNode(...)
    {
        // 関数生成するノードだったら関数レジストリに登録しておく
        if (activeNode is IGeneratesFunction functionNode)
        {
            ...
            functionNode.GenerateNodeFunction(functionRegistry, mode);
            ...
        }

        // 呼び出し部分を生成するコードだったら生成
        if (activeNode is IGeneratesBodyCode bodyNode)
        {
            ...
            bodyNode.GenerateNodeCode(descriptionFunction, mode);
            ...
        }
        ...
    }
    ...
}

こんな流れでコードの展開がされる形です。IGeneratesFunction は関数生成、IGeneratesBodyCode は関数呼び出し生成をするためのインターフェースというのもここで分かりました。

見れていない粗はまだまだありますが、これで大枠の登場人物と大体の接続のされ方の勘所が掴めたのではないでしょうか。

見れていないその他の点

データのシリアライズ・デシリアライズ

シェーダグラフのアセット自体は JSON です。これは .shadergraph ファイルを適当なテキストエディタで開いていみると分かります。

{
    "m_SerializedProperties": [
        {
            "typeInfo": {
                "fullName": "UnityEditor.ShaderGraph.Internal.ColorShaderProperty"
            },
            "JSONnodeData": "{\n
                \"m_Guid\": {\n
                    \"m_GuidSerialized\": \"ff7d96be-6f98-49a3-8e6f-3f7ceb2d6120\"\n
                },\n
                \"m_Name\": \"Color\",\n
                \"m_DefaultReferenceName\": \"Color_69C7B2FF\",\n
                ...
            }"
        },
        {
            "typeInfo": {
                "fullName": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty"
            },
            "JSONnodeData": "{\n
                \"m_Guid\": {\n
                    \"m_GuidSerialized\": \"1d767e50-c7f9-4762-921a-84326048da10\"\n
                },\n
                \"m_Name\": \"Disappear\",\n
                \"m_DefaultReferenceName\": \"Vector1_F86A2F65\",\n
                ...
            }"
        }
    ],
    "m_SerializedKeywords": [],
    "m_SerializableNodes": [
        {
            "typeInfo": {
                "fullName": "UnityEditor.ShaderGraph.MultiplyNode"
            },
            "JSONnodeData": "{\n
                \"m_GuidSerialized\": \"fd26db1d-0f47-42b1-b99d-898a5f0104d8\",\n
                ...
                \"m_Name\": \"Multiply\",\n
                ...
            }"
        },
        {
            "typeInfo": {
                "fullName": "UnityEditor.ShaderGraph.PropertyNode"
            },
            "JSONnodeData": "{\n
                \"m_GuidSerialized\": \"17c2b52a-af86-4f81-93a1-98d25b339a76\",\n
                ...
                \"m_Name\": \"Property\",\n
                ...
            }"
        },
        {
            "typeInfo": {
                "fullName": "UnityEditor.ShaderGraph.NoiseNode"
            },
            "JSONnodeData": "{\n
                \"m_GuidSerialized\": \"49cfb815-c8fe-4690-be1a-52add74aadb7\",\n
                ...
                \"m_Name\": \"Simple Noise\",\n
                ...
                \"m_SerializableSlots\": [\n
                    {\n
                        \"typeInfo\": {\n
                            \"fullName\": \"UnityEditor.ShaderGraph.UVMaterialSlot\"\n
                        },\n
                        \"JSONnodeData\": \"{\\n
                            \\\"m_Id\\\": 0,\\n
                            \\\"m_DisplayName\\\": \\\"UV\\\",\\n
                            \\\"m_SlotType\\\": 0,\\n
                            ...
                        }\"\n
                    },\n
                    {\n
                        \"typeInfo\": {\n
                            \"fullName\": \"UnityEditor.ShaderGraph.Vector1MaterialSlot\"\n
                        },\n
                        \"JSONnodeData\": \"{\\n
                            \\\"m_Id\\\": 1,\\n
                            \\\"m_DisplayName\\\": \\\"Scale\\\",\\n
                            \\\"m_SlotType\\\": 0,\\n
                            ...
                        }\"\n
                    },\n
                    {\n
                        \"typeInfo\": {\n
                            \"fullName\": \"UnityEditor.ShaderGraph.Vector1MaterialSlot\"\n
                        },\n
                        \"JSONnodeData\": \"{\\n
                            \\\"m_Id\\\": 2,\\n
                            \\\"m_DisplayName\\\": \\\"Out\\\",\\n
                            \\\"m_SlotType\\\": 1,\\n
                            ...
                        }\"\n
                    }\n
                ],\n
                ...
            }"
        },
        ...
        {
            "typeInfo": {
                "fullName": "UnityEditor.ShaderGraph.UnlitMasterNode"
            },
            "JSONnodeData": "{\n
                \"m_GuidSerialized\": \"92661195-fcc4-4f7f-a496-1324ec64c09e\",\n
                ...
                \"m_Name\": \"Unlit Master\",\n
                ...
            }"
        }
    ],
    ...
    "m_SerializableEdges": [
        {
            "typeInfo": {
                "fullName": "UnityEditor.Graphing.Edge"
            },
            "JSONnodeData": "{\n
                \"m_OutputSlot\": {\n
                    \"m_SlotId\": 2,\n
                    \"m_NodeGUIDSerialized\": \"49cfb815-c8fe-4690-be1a-52add74aadb7\"\n
                },\n
                \"m_InputSlot\": {\n
                    \"m_SlotId\": 7,\n
                    \"m_NodeGUIDSerialized\": \"92661195-fcc4-4f7f-a496-1324ec64c09e\"\n
                }\n
            }"
        },
        ...
    ],
    ...
}

例えばこれをじっと見てみると NoiseNodeUnlitMasterNodeシリアライズされており、m_SerializableEdges ではその接続が記述されていて、例えばノイズの 2 番スロット(アウトプット)とマスターノードの 7 番スロット(アルファ)が接続されているのが分かります。この JSON ファイルのインポートは ShaderGraphImporter という ScriptedImporter という任意のファイルを Unity で扱える形式にコンバートする仕組みでインポートされています。JsonUtility でパースしている形です。ちなみに ScriptedImporter は以前別記事で扱ったのでご興味あればこちらも見ていただけると嬉しいです。

tips.hecomi.com

ノードの拡張

さて、ノードの拡張方法ですが上記で見てきたクラスはだいたい internal 修飾子がついているため、自分のプロジェクトのスクリプトからはアクセスできません。パッケージとしてではなく持ってきて同一プロジェクト内で拡張のコードを書く必要があります。。

まとめ

長くなりましたがまとめると次のような感じです。

  • マスターノードが起点
  • 対応するサブシェーダークラスを探し出してサブシェーダコードを吐き出す
  • サブシェーダ内には複数のパスの設定が記述されている
  • このパスのコードは GenerationUtils で書き出されている
  • パスの雛形は PassMesh.template にテンプレート化されている
  • パスの設定やノードの接続関係を見てテンプレートを展開する
  • 各ノードのクラスは関数定義と呼び出し部分の文字列を提供する
  • ノードの接続関係を見てこれらの入出力をつなぐ
  • ちなみにノードの接続関係は JSON で保存されている

おわりに

オープンソースで公開してくれているおかげでこうやってコードを読んで中身を考える事ができるのは面白いですね。次回は実際にノードを拡張してみるところをやってみます。