C++幼女先輩

プログラミング成分多め

URP10.2.2 URPシェーダーをShaderLabで書いてみる

URPのシェーダーを作ろうとすると、UNITYからは全部ShaderGraphになる
ので、テキストエディタでShaderを書く事は推奨されていないようだし
実際に仕事でも、アーティストと会話するために、ノードエディタのShaderGraphを使う方が良いと思うが
勉強のためにあえてShaderLabで書いてみる

URP UnlitShaderを調査

まずは、最も簡単なはずのUnlitから書いてみる
テンプレートからはShaderLabは作れないので、ShaderGraphのUnlitを新規で作成します

f:id:murasame-labo:20210205123448p:plain

床に適用してみた。テクスチャも影もなくなった
f:id:murasame-labo:20210205123921p:plain

作成されたUnlitShaderGraphからShaderLabのコードを取得するには、Inspectorから ViewGeneratedShader を選択するとコードが表示される
f:id:murasame-labo:20210205123528p:plain

パスは、通常パス、ShadowCaster、DepthOnlyの3つで、それがTransparent、Opaqueの両方あるので合計6個のパスがあります
おそらく、SRP Batchingのために、TransparentとOpaqueの両方のシェーダをまとめているんだと思う

Feature関連

                // Pragmas
                #pragma target 2.0
                #pragma only_renderers gles gles3 glcore
                #pragma multi_compile_instancing
                #pragma multi_compile_fog
                #pragma vertex vert
                #pragma fragment frag
    
                // DotsInstancingOptions: <None>
                // HybridV1InjectedBuiltinProperties: <None>
    
                // Keywords
                #pragma multi_compile _ LIGHTMAP_ON
                #pragma multi_compile _ DIRLIGHTMAP_COMBINED
                #pragma shader_feature _ _SAMPLE_GI
                // GraphKeywords: <None>
    
                // Defines
                #define ATTRIBUTES_NEED_NORMAL
                #define ATTRIBUTES_NEED_TANGENT
                #define FEATURES_GRAPH_VERTEX
                /* WARNING: $splice Could not find named fragment 'PassInstancing' */
                #define SHADERPASS SHADERPASS_UNLIT
                /* WARNING: $splice Could not find named fragment 'DotsInstancingVars' */

色々と書いてあるが、おおざっぱに
Instancing、Fog、dots instancing、ライトマップ系、アトリビュート、そしてshaderpass unlit
等設定しています
dots instancingとか、最近のUNITYの高速化、Data-Oriented Technology Stackの事でしょう
今度ちゃんと調べてみたいです

Includes

      #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.core/ShaderLibrary/TextureStack.hlsl"
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShaderGraphFunctions.hlsl"

これらのコードをIncludeしています
BuiltIn-Shaderではなく、URPのシェーダーを見に行ってますね

入出力

      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(UNITY_STEREO_MULTIVIEW_ENABLED)) || (defined(UNITY_STEREO_INSTANCING_ENABLED) && (defined(SHADER_API_GLES3) || defined(SHADER_API_GLCORE)))
            uint stereoTargetEyeIndexAsBlendIdx0 : BLENDINDICES0;
            #endif
            #if (defined(UNITY_STEREO_INSTANCING_ENABLED))
            uint stereoTargetEyeIndexAsRTArrayIdx : SV_RenderTargetArrayIndex;
            #endif
            #if defined(SHADER_STAGE_FRAGMENT) && defined(VARYINGS_NEED_CULLFACE)
            FRONT_FACE_TYPE cullFace : FRONT_FACE_SEMANTIC;
            #endif
        };

おそらく入力パラメータAttributesは #defineで有効にしたものが入ってくるのだろう
それにInstancing用のID

Varyingsは、InstancingやVR等で条件コンパイルされてるが、ポジション程度しかない

それらの入力値を使い、データを抽象化しているのか、シェーダーライブラリ内で使う構造体に変換しているのだと思われる
SurfaceDescriptionInputs、VertexDescriptionInputs、PackedVaryingsと変換関数がある

実際の処理

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

実際の処理はシェーダーライブラリの中だ
その中でも実際にシェーダーの中身は下記になる

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

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
        half alpha = surfaceDescription.Alpha;
        clip(alpha - surfaceDescription.AlphaClipThreshold);
    #elif _SURFACE_TYPE_TRANSPARENT
        half alpha = surfaceDescription.Alpha;
    #else
        half alpha = 1;
    #endif

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

    return half4(surfaceDescription.BaseColor, alpha);
}

まあ、ここだけ見ると単純です

シンプルなUnlitシェーダー

白く塗りつぶすだけの非常に簡単なシェーダーを作った

Shader "MyShader/URPUnlit"
{
    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
            "RenderType" = "Opaque"
            "UniversalMaterialType" = "Unlit"
            "Queue" = "Geometry"
        }
        Pass
        {
            Name "Pass"

            // Render State
            Cull Back
            Blend One Zero
            ZTest LEqual
            ZWrite On

            HLSLPROGRAM

            // Pragmas
            #pragma target 4.5
            #pragma exclude_renderers gles gles3 glcore
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/UnityInput.hlsl"

            struct Attributes
            {
                float3 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float4 tangentOS : TANGENT;
            };
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
            };

            
            float3 TransformObjectToWorld(float3 positionOS)
            {
                return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz;
            }

            float4 TransformWorldToHClip(float3 positionWS)
            {
                return mul(unity_MatrixVP, float4(positionWS, 1.0));
            }

            Varyings vert(Attributes input)
            {
                Varyings output = (Varyings)0;
                output.positionCS = TransformWorldToHClip(TransformObjectToWorld(input.positionOS));
                return output;
            }

            half4 frag(Varyings input) : SV_TARGET
            {
                return half4(1,1,1,1);
            }
            ENDHLSL
        }
    }
}

f:id:murasame-labo:20210205211051p:plain

includeは2個あるが、UNITYのシェーダーのCORE的なものと、RPから入ってくるUNITY定数の宣言
思ったより簡単に書けたが、色々な処理を無視しているし
影とかは難しそうな気配・・
PS,XBOX,Switch等の対応とかも考えると、ShaderGraphで書く方が楽だろうなあ・・・

テクスチャマッピングする

これは非常に簡単だった
今までのBuiltIn-Shaderの作法にのっとり
Texture2DをPropertiesに出し、Texture2DとSamplerを宣言し
AttributesとVaryingsに TEXCOORD0を入れて
SAMPLE_TEXTURE2Dでマッピングした結果をリターンすればいい

f:id:murasame-labo:20210205213645p:plain

意外と素直

影を落とす

"Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
をIncludeすることにした

そして今回はメインライトからの影のみを取るので
pragma multi_compile _ _MAIN_LIGHT_SHADOWS をつけた

shadowAttenuationを計算するのだが、ShadowMapをSampleするには、ワールド座標のPositionが必要なので
positionWSを計算することに
TransformObjectToWorld(input.positionOS)
で簡単に取得できた

ワールド座標系のオブジェクトポジションを使い
TransformWorldToShadowCoord(input.positionWS)
をすると、ShadowCoordが取得できるので、そこから
GetMainLightを使い、メインライトの情報、影の情報が取れる
今回はライトカラーなどは使わずshadowAttenuationだけを使い、テクスチャにそのまま乗算した

f:id:murasame-labo:20210205221320p:plain

とりあえず影はできた

今回の完成シェーダコード

Shader "MyShader/URPUnlit"
{
    Properties
    {
            [NoScaleOffset] MainTex("_MainTex", 2D) = "white" {}
            [HideInInspector][NoScaleOffset]unity_Lightmaps("unity_Lightmaps", 2DArray) = "" {}
            [HideInInspector][NoScaleOffset]unity_LightmapsInd("unity_LightmapsInd", 2DArray) = "" {}
            [HideInInspector][NoScaleOffset]unity_ShadowMasks("unity_ShadowMasks", 2DArray) = "" {}
    }


    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
            "RenderType" = "Opaque"
            "UniversalMaterialType" = "Unlit"
            "Queue" = "Geometry"
        }
        Pass
        {
            Name "Pass"

            // Render State
            Cull Back
            Blend One Zero
            ZTest LEqual
            ZWrite On

            HLSLPROGRAM

            // Pragmas
            #pragma target 4.5
            #pragma exclude_renderers gles gles3 glcore
            #pragma vertex vert
            #pragma fragment frag


            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS

            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Input.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/UnityInput.hlsl"

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            TEXTURE2D(MainTex);
            SAMPLER(samplerMainTex);

            struct Attributes
            {
                float3 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float4 tangentOS : TANGENT;
                float4 uv0 : TEXCOORD0;
            };
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float4 texCoord0 : TEXCOORD0;
                float3 positionWS: TEXCOORD1;
            };

            Varyings vert(Attributes input)
            {
                Varyings output = (Varyings)0;
                output.positionCS = TransformWorldToHClip(TransformObjectToWorld(input.positionOS));
                output.texCoord0 = input.uv0;
                output.positionWS = TransformObjectToWorld(input.positionOS);
                return output;
            }

            half4 frag(Varyings input) : SV_TARGET
            {
                half4 o = SAMPLE_TEXTURE2D(MainTex, samplerMainTex, input.texCoord0.xy);
                Light l = GetMainLight(TransformWorldToShadowCoord(input.positionWS));

                return o*l.shadowAttenuation;
            }
            ENDHLSL
        }
    }
}

Instancingやメイン以外のライト等、いろいろすっ飛ばしているので実用性はないが
URPのシェーダーは、こんな感じで書く事は可能だ
ShadowCasterやDepthOnlyパスは消してしまったが同じように簡単に作る事ができそう

ただし、URPはバージョンアップで色々変わってしまうので
ShaderGraphを使う方が楽と思う