はじめに

はじめまして。アップフロンティアVTuber分科会会長のtgと申します。
(※ 弊社には分科会という形でVTuberにエモを感じるためだけの組織が闇運営されています。)

お手軽に3DVTuberを作ろうとしたらPixiv社さんのVroidStudio が最強です。弊社でも大事に使わせていただいております。

エンジニア3人が1時間かけただけでこんな美少女3Dモデルが作れるソフトは他にありません。

そんなVroidStudioで作ったVRMモデルをUniversalRPでも動かしてみたいと思い、今回なんちゃってMToonシェーダーの制作に挑戦してみました。

その際UniversalRPについても色々調査を行ってみました。

UniversalRP対応シェーダーの作り方

2020年4月において、UniversalRP対応のシェーダーは従来通りシェーディング言語でコーディングする他に、ノードベースの ShaderGraph を利用する方法があります。

ShaderGraph

 ShaderGraphはノード毎にアウトプットを見れて作りやすい以外にも、後述するUniversalRPにシェーダーを対応させるための細かな設定込みでコードを自動生成してくれるのでおすすめです。

ただノードベースあるあるですが、複雑な内容になってくると、ノードの数が増えていくので管理が難しくなっていきます。

それを避けるための仕組みとしてShaderGraphでは SubGraph だったり、CustomNode が用意されているのですが、それでも「ノードの方が大変だな」と感じたら無理にグラフを使う必要はなく、コーディングに寄せていくのもありだと思います。

そんなときのときに備え、今回はUniversalRPの知見を得るためにあえて ShaderGraph を使わずにコーディングを行っていこうと思っています。

TemplateShader

ということで今回は ShaderGraph を使わずにUniversalRPのシェーダーを書いていきたいと思います。
そのときとっかかりとして参考にしやすかったシェーダーをご紹介します。

UniversalPipelineTemplateShader.shader

https://gist.github.com/phi-lira/225cd7c5e8545be602dca4eb5ed111ba#file-universalpipelinetemplateshader-shader

このシェーダーはシンプルなもので分かりやすい上に、内容の1行1行にUnity社の方のコメントが付いています。

これを元にまずは以下の旧パイプラインのUnlitシェーダーをUniversalRPに対応させてみたいと思います。

開発環境は以下のようになってます。

Unity2019.3.7f1
UniversalRP7.1.8

UniversalRPの設定はデフォルトのままです。

// ====================================
// BuiltInRP
// よくあるUnlitシェーダー
// ====================================
Shader "BuildInRP/Sample"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}
// ====================================
// UniversalRP
// UniversalRPに対応させたUnlitシェーダー
// ====================================
Shader "Universal Render Pipeline/Sample"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalRenderPipeline" }
        LOD 100

        Pass
        {
            Name "Sample"
            
            HLSLPROGRAM
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            
            CBUFFER_START(UnityPerMaterial)
            sampler2D _MainTex;
            float4 _MainTex_ST;
            CBUFFER_END
            
            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;                
            };
            
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            
            Varyings vert (Attributes input)
            {
                Varyings output = (Varyings)0;
                VertexPositionInputs vertex = GetVertexPositionInputs(input.positionOS.xyz);
                output.positionCS = vertex.positionCS;
                output.uv = TRANSFORM_TEX(input.uv, _MainTex);
                return output;
            }
            
            float4 frag (Varyings input) : SV_Target
            {
                return tex2D(_MainTex, input.uv);
            }            
            ENDHLSL
        }
    }
}

変更点をまとめます。

1. HLSL

UniversalRPでは従来のCg言語ではなく、HLSLで書くことになると思います。

Cg言語で書いても動きますが、ライティングなどでUniversalRPパッケージのライブラリを使いたい場合に、そちらがHLSLで書かれているので合わせてあげる必要が出てくるためです。

2. RenderPiplineタグ

UniversalRP(LWRP)でのみ動かしたい場合は、RenderPipelineタグに UniversalRenderPipeline を指定すればいいそうです。
しかし、このパラメータを付けていても自分で作ったレンダーパイプラインでは動作してしまったので今のところどうすれば有効になるタグなのかが分っていません。

With SRP we introduce a new "RenderPipeline" tag in Subshader. This allows to create shaders
that can match multiple render pipelines. If a RenderPipeline tag is not set it will match
any render pipeline. In case you want your subshader to only run in LWRP set the tag to
"UniversalRenderPipeline"

3. OpenGLES2.0対応

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

OpenGLES2.0で動かす場合は上の設定が必要なようです。

4. SRPBatcher対応

ScriptableRPではパフォーマンス向上の新たな仕組みとして SRPBatcher というものが用意されています。

SRP Batcher:レンダリングをスピードアップ

https://blogs.unity3d.com/jp/2019/02/28/srp-batcher-speed-up-your-rendering/

従来のパイプラインでマテリアルを切り替える度にパラメータをアップロードし直していたところがSRPBatcherによって改善できるポイントです。

GPUの永続メモリ(CBUFFER)を駆使し、マテリアルパラメータを予めキャッシュしておくことで、マテリアルの切り替え時のコストを軽減するというアプローチのようです。

複雑なことをしなければシェーダーをSRPBatcherに対応させるのは難しくなく、 下のようにパラメータをCBUFFER_START(UnityPerMaterial)CBUFFER_END で囲むだけです。

CBUFFER_START(UnityPerMaterial)
sampler2D _MainTex;
float4 _MainTex_ST;
CBUFFER_END

シェーダーがSRPBatcherに対応している場合、以下のように項目が「compatible」となります。

逆にシェーダーがSRPに対応していない場合、対応できなかった理由と合わせて「not compatible」となります。

SRPBatcherが実際の描画に動いているかを調べるには、私が調べた感じ以下の2つが有効だと思います。

  1. FrameDebugger を使う
  2. SRPBatcherProfiler を使う
FrameDebuggerを使う

FrameDebugger を使うとSRPBatcherがどの程度有効に動作しているかを測ることができます。それぞれ別のマテリアルを当てている70個のCubeで検証を行ってみたいと思います。

シェーダーは下のものを使います。

Shader "Universal Render Pipeline/Sample"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _MainColor ("Main Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalRenderPipeline" }
        LOD 100

        Pass
        {
            Name "Sample"
            
            HLSLPROGRAM
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            
            CBUFFER_START(UnityPerMaterial)
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _MainColor;
            CBUFFER_END
            
            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;                
            };
            
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            
            Varyings vert (Attributes input)
            {
                Varyings output = (Varyings)0;
                VertexPositionInputs vertex = GetVertexPositionInputs(input.positionOS.xyz);
                output.positionCS = vertex.positionCS;
                output.uv = TRANSFORM_TEX(input.uv, _MainTex);
                return output;
            }
            
            float4 frag (Varyings input) : SV_Target
            {
                return tex2D(_MainTex, input.uv) * _MainColor;
            }            
            ENDHLSL
        }
    }
}

加えて以下のスクリプトを使って、上のシェーダーを割り当てたマテリアルをCube毎にそれぞれ生成していきます。

using UnityEngine;

public class AutoMaterialChange : MonoBehaviour
{
    private int _index = 0;

    private void Start()
    {
        rec(transform);
    }

    void rec(Transform parent)
    {
        foreach (Transform child in parent)
        {
            var ar = child.GetComponent<AutoRotate>();
            if (ar != null)
            {
                ar.enabled = false;
            }
            
            if (child.childCount > 0)
            {
                rec(child);
            }
            var r = child.gameObject.GetComponent<MeshRenderer>();
            if (r == null)
            {
                continue;
            }
            var material = new Material(r.material);
            var value = ++_index / 70f;
            material.SetColor("_MainColor", new Color(1, value, value,1));
            r.material = material;
        }
    }
}

SRPBatcherが効いている場合、下のように70個のCubeのDrawCallを1つのSRP Batchにまとめることができます。

Draw Callsの項目はいくつの描画命令に今回のSRPBatchが有効だったかを示しています。今回は70なので、すべてのCubeで有効であったことが分かります。

また逆に有効に動作していない場合も、バッチが効かなかった原因を教えてくれるので改善のヒントを得ることもできます。

SRPBatchが壊れる原因の1つに 「異なるシェーダーキーワードを使う」 というのがあるので、上のCubeの半分ずつで異なるキーワードを指定するようにしてみたら以下のようになりました。

SRP Batch の項目が9つになってしまいました。

このとき、Why this draw call can't be batched with previous one というところに前回のバッチ処理に含まれなかった理由が書かれています。
今回は Node use different shader keywords と異なるシェーダーキーワードを使ったことが原因と記されています。

SRPBatcherProfilerを使う

SRPBatcherProfiler.cs

https://github.com/Unity-Technologies/SRPBatcherBenchmark/blob/master/Assets/Scripts/SRPBatcherProfiler.cs

FrameDebugger ではバッチがどれくらい効いているのかを調べることができました。それに対して、SRPBatcherProfilerSRPBatchによって描画時間がどれくらい短縮されたのか を調べることができます。

導入は簡単で、GitHubからコードを持ってきてプロジェクトにインポートし、調べたいシーンのGameObjectのどれかにアタッチするだけです。

SRPBatcherが有効な場合は以下のような表示になります。

逆にSRPBatcherが無効の場合は以下のような表示になります。

両方ともCPUがレンダリングに要した時間を表示しています。

SRPBatchを有効にした場合はSRPBatcherのパスを通って 0.05ms の時間が消費されています。無効にした場合は通常のパスを通り、0.17ms と3倍の時間に増えてしまっていることが分かります。

UniversalRPの検証

ライティング

まずはライティングの検証をやっていきたいと思います。(考えるのはフォワードのみです。)

従来パイプラインとUniversalRPの大きな違いの1つにライティングの仕様が挙げられます。従来パイプラインではライト毎に1つPassが使われる仕様になっていました。最初に ForwardBase でライト処理が行われ、その他オブジェクトに影響を及ぼすライトがある場合 ForwardAdd で追加処理が行われます。

フォワードレンダリングパスの詳細

https://docs.unity3d.com/ja/2018.4/Manual/RenderTech-ForwardRendering.html

このライティング手法は影響を受けるライトの数によってPassの数が倍数的に増えていくというデメリットがありました。

それに対して、UniversalRPでは1パス目のフラグメントシェーダーでライトの数だけループを回し、Passの数を増やさないようにするという工夫が行われています。

MainLight

実際に先ほど作ったUnlitシェーダーをメインライトのみの影響を受けるLitシェーダーに書き換えたいと思います。

Shader "Universal Render Pipeline/Sample"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalRenderPipeline" }
        LOD 100

        Pass
        {
            Name "Sample"
            
            HLSLPROGRAM
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            
            CBUFFER_START(UnityPerMaterial)
            sampler2D _MainTex;
            float4 _MainTex_ST;
            CBUFFER_END
            
            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float2 uv : TEXCOORD0;                
            };
            
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 normalWS : TEXCOORD1;
            };
            
            Varyings vert (Attributes input)
            {
                Varyings output = (Varyings)0;
                VertexPositionInputs vertex = GetVertexPositionInputs(input.positionOS.xyz);
                output.positionCS = vertex.positionCS;
                VertexNormalInputs normal = GetVertexNormalInputs(input.normalOS);
                output.normalWS = normal.normalWS;
                output.uv = TRANSFORM_TEX(input.uv, _MainTex);                
                return output;
            }
            
            float4 frag (Varyings input) : SV_Target
            {
                Light mainLight = GetMainLight();
                float strength = dot(mainLight.direction, input.normalWS);
                float4 lightColor = float4(mainLight.color, 1);
                return tex2D(_MainTex, input.uv) * strength * lightColor;
            }            
            ENDHLSL
        }
    }
}

光源の向きと法線の内積だけを使う簡易的なライティングですが、それっぽい絵を作ることができました。

UniversalRPでライティングを行う場合は Lighting.hlsl を介して光源の情報を取得します。

今回は GetMainLight() を使って、メインライトの方向や色の情報にアクセスしています。

AdditionalLight

次は複数の光源に対応してみます。下のようにPointLightを2つ追加で置いてみました。

旧パイプラインでは ForwardAdd のパスを作って対応していましたが、前述の通りUniversalRPでは1つ目のパスで光源の数だけループを回します。

Shader "Universal Render Pipeline/Sample"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalRenderPipeline" }
        LOD 100

        Pass
        {
            Name "Sample"
            
            HLSLPROGRAM
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            
            CBUFFER_START(UnityPerMaterial)
            sampler2D _MainTex;
            float4 _MainTex_ST;
            CBUFFER_END
            
            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float2 uv : TEXCOORD0;                
            };
            
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float3 positionWS : TEXCOORD0;
                float2 uv : TEXCOORD1;
                float3 normalWS : TEXCOORD2;
            };
            
            Varyings vert (Attributes input)
            {
                Varyings output = (Varyings)0;
                VertexPositionInputs vertex = GetVertexPositionInputs(input.positionOS.xyz);
                output.positionCS = vertex.positionCS;
                output.positionWS = vertex.positionWS;
                VertexNormalInputs normal = GetVertexNormalInputs(input.normalOS);
                output.normalWS = normal.normalWS;
                output.uv = TRANSFORM_TEX(input.uv, _MainTex);                
                return output;
            }
            
            float4 core (Varyings input, Light light)
            {
                float strength = dot(light.direction, input.normalWS);
                float4 lightColor = float4(light.color, 1);
                return tex2D(_MainTex, input.uv) * strength * lightColor;
            }
            
            float4 frag (Varyings input) : SV_Target
            {
                float4 col = 0;
                col += core(input, GetMainLight());
                          
                uint pixelLightCount = GetAdditionalLightsCount();
                for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex)
                {
                    Light light = GetAdditionalLight(lightIndex, input.positionWS);
                    col += core(input, light);
                }
                
                return col;
            }            
            ENDHLSL
        }
    }
}

追加したライトがCubeに影響を与えてるので上手くいってるようです。

uint pixelLightCount = GetAdditionalLightsCount();
for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex)
{
    Light light = GetAdditionalLight(lightIndex, input.positionWS);
    col += core(input, light);
}

GetAdditionalLightsCount() でメインライト以外のライトの数を取得できます。その後、その数だけ、GetAdditionalLight() でライト情報を取得して色を加算しています。

Shadow

最後は影を出してみます。
影を出すためのは旧パイプラインと同じように ShadowCasterパス を実装してShadowMapに書き込みを行います。

Shader "Universal Render Pipeline/Sample"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalRenderPipeline" }
        LOD 100

        Pass
        {
            Name "Sample"
            
            HLSLPROGRAM
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            
            CBUFFER_START(UnityPerMaterial)
            sampler2D _MainTex;
            float4 _MainTex_ST;
            CBUFFER_END
            
            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float2 uv : TEXCOORD0;                
            };
            
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 normalWS : TEXCOORD1;
            };
            
            Varyings vert (Attributes input)
            {
                Varyings output = (Varyings)0;
                VertexPositionInputs vertex = GetVertexPositionInputs(input.positionOS.xyz);
                output.positionCS = vertex.positionCS;
                VertexNormalInputs normal = GetVertexNormalInputs(input.normalOS);
                output.normalWS = normal.normalWS;
                output.uv = TRANSFORM_TEX(input.uv, _MainTex);
                return output;
            }
            
            float4 frag (Varyings input) : SV_Target
            {
                Light mainLight = GetMainLight();
                return tex2D(_MainTex, input.uv) * dot(mainLight.direction, input.normalWS) * float4(mainLight.color, 1);
            }            
            ENDHLSL
        }
        
        Pass
        {
            Name "ShadowCaster"
            Tags{"LightMode" = "ShadowCaster"}

            ZWrite On
            ZTest LEqual
            Cull Off

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

            #pragma shader_feature _ALPHATEST_ON
            #pragma shader_feature _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A

            #pragma vertex MyShadowPassVertex
            #pragma fragment MyShadowPassFragment
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            
            CBUFFER_START(UnityPerMaterial)
            sampler2D _MainTex;
            float4 _MainTex_ST;
            CBUFFER_END
            
            struct Attributes
            {
                float4 positionOS   : POSITION;
                float3 normalOS     : NORMAL;
                float2 texcoord     : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionCS   : SV_POSITION;
            };

            float4 GetShadowPositionHClip(Attributes input)
            {
                float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
                float3 normalWS = TransformObjectToWorldNormal(input.normalOS);
                Light light = GetMainLight();
                float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, light.direction));
#if UNITY_REVERSED_Z
                positionCS.z = min(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE);
#else
                positionCS.z = max(positionCS.z, positionCS.w * UNITY_NEAR_CLIP_VALUE);
#endif
                return positionCS;
            }

            Varyings MyShadowPassVertex(Attributes input)
            {
                Varyings output = (Varyings)0;
                output.positionCS = GetShadowPositionHClip(input);
                return output;
            }
            
            half4 MyShadowPassFragment(Varyings input) : SV_TARGET
            {
                return 1;
            }
            
            ENDHLSL
        }
    }
}

下に置いたPlaneにCubeの影を出すことができました。ShadowCasterの考え方は旧ライプラインと同じだと思います。

以上でUniversalRPのライティングの検証は終わりにしようと思います。

マルチパス

次はマルチパスの検証をやっていきたいと思います。

マルチパスを考える理由はキャラクターにアウトライン表現を以下のようなパスを作ってよくある描画手順を実現したいためです。

  1. 背面カリングで普通に描画
  2. 前面カリングを行った上で法線方向にオブジェクトを膨らませて描画
Shader "Universal Render Pipeline/Sample"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _OutlineWidth ("OutlineWidth", float) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalRenderPipeline" }
        LOD 100

        Pass
        {
            Name "Sample"
            Tags
            { 
                "LightMode" = "UniversalForward"
            }
            
            Cull Back
            
            HLSLPROGRAM
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            
            CBUFFER_START(UnityPerMaterial)
            sampler2D _MainTex;
            float4 _MainTex_ST;
            CBUFFER_END
            
            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;                
            };
            
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD1;
            };
            
            Varyings vert (Attributes input)
            {
                Varyings output = (Varyings)0;
                VertexPositionInputs vertex = GetVertexPositionInputs(input.positionOS.xyz);
                output.positionCS = vertex.positionCS;
                output.uv = TRANSFORM_TEX(input.uv, _MainTex);                
                return output;
            }
            
            float4 frag (Varyings input) : SV_Target
            {
                return tex2D(_MainTex, input.uv);
            }            
            ENDHLSL
        }
        
        Pass
        {
            Name "MyUnlit Outline"
            
            Cull Front
            
            HLSLPROGRAM
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0
            
            #pragma vertex vert
            #pragma fragment frag
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
                        
            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
            };
            
            struct Varyings
            {
                float4 positionCS : SV_POSITION;
            };
            
            float _OutlineWidth;
            
            Varyings vert (Attributes input)
            {
                Varyings output = (Varyings)0;
                float3 worldNormalLength = length(mul((float3x3) transpose(unity_WorldToObject), input.normalOS));
                float3 outlineOffset = 0.01 * _OutlineWidth * worldNormalLength * input.normalOS;
                input.positionOS.xyz += outlineOffset;
                VertexPositionInputs vertex = GetVertexPositionInputs(input.positionOS.xyz);
                output.positionCS = vertex.positionCS;
                return output;
            }
            
            float4 frag (Varyings input) : SV_Target
            {
                return float4(1, 0, 0, 1);
            }            
            ENDHLSL
        }
    }
}

ちゃんと2passが実行されて、Cubeの周りに赤いアウトラインを出すことができました。

UniversalRPでもマルチパスシェーダーを作ることはできるようです。
ただ上のシェーダーで一部手間取ったところがあったのでご紹介します。

LightMode

今回1Passのタグに LightMode の指定を行っています。

こちらはScriptableRPのシェーダーフィルタリングのために使われるタグと認識していたのですが、マルチパスにおいては書き方によって描画順が逆になったり、2Passが実行されないという挙動の変化が起きたので下にまとめます。

1Pass_Tag2Pass_Tag結果(描画順)
未指定未指定1Pass
UniversalForward未指定1Pass -> 2Pass
未指定UniversalForward2Pass -> 1Pass
UniversalForwardUniversalForward1Pass

今回は「1Pass -> 2Pass」としたいので、「UniversalForward-未指定」の組み合わせを選びました。

SRPBatcher

別の問題でマルチパスを採用するとSRPBatchが効かないことに気付きました。

さきほどのCube70個シーンでマルチパスシェーダーを使用して、FrameDebugger で描画状態を調べてみました。

結果としてSRPBatchは効かず、以下のように140個(70×2)のPassが順番に実行されるようになりました。

An object is using a multi-pass shader とマルチパスシェーダーの利用がバッチが効かない原因とされています。マルチパスとUniversalRPは相性が悪いみたいです。

それの回答として、実はアウトラインに関してはUnity公式の実装サンプルがあります。

UniversalRenderingExamples

https://github.com/Unity-Technologies/UniversalRenderingExamples#toon-outline

こちらではマルチパスを使わずに、RenderFeature というUniversalRPに自分の好きなパスを差し込む仕組みを利用してポストプロセスとしてアウトラインを描画しています。

ポストプロセスではオブジェクトそれぞれのパラメータを拾うことはできないので、クオリティが下がる面も出てきますが、そこはパフォーマンスとのトレードオフだと思います。

なんちゃってMToonシェーダーを作る

上の情報を元にUniversalRPで動くなんちゃってMToonシェーダーを作ってみました。

やってみた結果としてまだまだUniversalRPの理解が足りないところがあり、主に影のところで再現ができなかったところがありました。

とはいえ色々試行錯誤してみたところもあるので、その点を残したいと思います。

Shadow

旧パイプラインでは影を受けるところを UNITY_LIGHT_ATTENUATION のマクロを使ってもってくると思います。shadowAttenuation という0~1のパラメータを受け取り、0が影、1が光のあたる場所として扱います。

UniversalRPではこのパラメータをどう取得するかというと、前述した Lighting.hlsl を使って持ってきます。

// メインライトの場合
float4 shadowCoord = TransformWorldToShadowCoord(input.positionWS);
Light mainLight = GetMainLight(shadowCoord);

TransformWorldToShadowCoord() を使ってワールド座標からシャドウマップのUVを取得します。
そして受け取ったパラメータをそのままLight関数に渡すことで shadowAttenuation を取得できます。

受け取った結果の shadowAttenuation ですが、私の手元ではUniversalRPと旧パイプラインで結果が異なるものになりました。

おそらく何か見直せば改善できる気がしてますが、現状だとシャドウが以下のように全然違うふうになっており、最終のカラーも影響を受けて同じようにズレてしまっています。

オリジナル

UniversalRP

ShadowCaster

ShadowCasterパスでマテリアルパラメータを参照する場合、CBUFFER_START(UnityPerMaterial) で定義するパラメータは1Passと完全に一致するようにした方がいいと思います。

異なる場合、以下のようにSRPBatcherが無効になる原因になります。

補足

前述のようにShadowCasterパスでマテリアルパラメータが1Passより足りない場合は、問答無用でSRPBatcherが無効になります。

しかし、逆に増えてる場合はただちに無効になりませんでした。

増やすのはOKなのかなと思いましたが、vert/frag でそのパラメータにアクセスすると同じように無効になってしまうという挙動になりました。

CBufferにアップロードされるかどうかは実際にパラメータが使われているのかも判定基準になっているのかもと思いました。

まとめ

UniversalRPは最初はShaderGraphがないと手に負えないと思っていましたが、意外とコーディングすることもできて良かったです。

しかし、やっぱりパイプラインが変わるというのは影響が大きくて、今までにない制限や問題がたまに出てきました。

今後も使い続けてUniveraslRPと友達になれたらいいなと思います。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる