Unityでステンシルシャドウボリューム

はじめに

前回、投影テクスチャマッピングシャドウを取り上げました。しかし、投影テクスチャマッピングシャドウは影生成の単位がキャラ単位なので自身の部位から受ける影「セルフシャドウ」を描画することができません。

daiki-eguchi.hatenablog.com

そこでセルフシャドウ表現まで行える技法で比較的互換性が高く、汎用性が高い技法がステンシルシャドウボリュームのようです。

ステンシルシャドウボリュームの理論

まず、視点方向からの深度情報(Zバッファ)を保持しておきます。

その後、以下のようにオブジェクトの輪郭を光源方向(光源からオブジェクトの方向)に伸ばします。

そのあと、以下のように視点からみてシャドウボリュームの表面(前面)をステンシルバッファに+1を書き込みます。 光源が複数あったり、オブジェクトが重なっているときはステンシルバッファでの+1演算が複数になります。

ステンシルバッファは算術処理用のフレームバッファです。

ステンシルバッファはあらかじめ0に初期化しておきます。

そして、視点から見てシャドウボリュームの裏面でステンシルバッファを-1します。

すると影を書く以外が0に戻り、影が生成される場所のステンシルは1以上になります。そのため、ステンシルが1以上の場所に影色つけていきます。

これによってセルフシャドウ表現もでき、モデル同士が影を落としあうこともできます。

成果物

以下のように落ち影が描画できています。

Unityでの実装

今回はURPで描画してみるのでRendererFeatureを使用してみます。 ProjectSettingsで現在使用しているRenderPipelineAssetを探します。

Stencil値の初期化

メニューの下からAdd RendererFeatureをクリックし、RenderObjectsを選択します。

名前をStencilInitializeにしてOverrides内のStencilをオン、ValueをONにしました。 これによって、画面全体のStencilが0になります。

オブジェクトの前面を伸ばし、ステンシル値をあげる

同様にRenderObjectsのRendererFeatureをもう一つ追加します。 命名をStencilIncreaseにして、新しく作成したStencilIncreaseというMaterialをOverrideの欄に入れます。

このMaterialにアサインするShaderは以下のように作成しました。

Shader "Custom/StencilIncrease"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ShadowColor ("Shadow Color", COlor) = (0, 0, 0, 0.5)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            NAME "StencilFront"
            
            Tags { "LightMode" = "UniversalForward" }
            //カラーチャンネルに書き込むレンダーターゲットを設定する
            //0の場合、全てのカラーチャンネルが無効化され何も書き込まれない
            ColorMask 0
            ZWrite Off
            Cull Back
            ZTest LEqual

            Stencil
            {
                Ref 1
                Comp Always
                Pass IncrSat
            }
            
            HLSLPROGRAM
            #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"

            struct appdata
            {
                float4 vertex : POSITION;
                float4 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            float _ShadowLength;
            half4 _ShadowColor;

            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                // ライトの取得
                Light light = GetMainLight();

                // 法線をワールド空間に変換
                float3 worldNormal = TransformObjectToWorldNormal(v.normal);
                float nDotL = dot(worldNormal, light.direction);

                // 頂点を押し出す
                float3 vertex2 = v.vertex + TransformWorldToObjectDir(-light.direction) * _ShadowLength;

                o.vertex = TransformObjectToHClip(lerp(v.vertex, vertex2, 1 - step(-0.3, nDotL)));
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                return 0;
            }
            ENDHLSL
        }
    }
}

ステンシルと頂点シェーダーがポイントになります。

まずはCullをBack指定し表面を伸ばすようにします。

Cull Back

Stencilを記述するだけなのでカラーチャンネルには何も書き込まれないように設定します。

ColorMask 0

ステンシルは以下のように記述し、必ずステンシルの値を1あげるようにしています。

Stencil
{
    Ref 1
    Comp Always
    Pass IncrSat
}

頂点シェーダー内では自分の頂点からライトの方向に対して伸ばしています。

まずは伸びた先の頂点位置を計算しています。

/ / 頂点を押し出す
float3 vertex2 = v.vertex + TransformWorldToObjectDir(-light.direction) * _ShadowLength;

その後、ライト方向と法線方向の内積から判断して頂点を伸ばしました。

o.vertex = TransformObjectToHClip(lerp(v.vertex, vertex2, 1 - step(-0.3, nDotL)));

オブジェクトの背面を伸ばし、ステンシル値を下げる

RendererFeatureを一つ追加します。RenderObjectsのRendererFeatureを追加し、名前をStencilDecreaseとしました。

Materialを新規で一つ追加し、アサインしたStencilを下げるShaderは以下のように記述しています。

Shader "Unlit/StencilDecrease"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ShadowColor ("Shadow Color", COlor) = (0, 0, 0, 0.5)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            NAME "StencilBack"
            
            Tags { "LightMode" = "UniversalForward" }
            //カラーチャンネルに書き込むレンダーターゲットを設定する
            //0の場合、全てのカラーチャンネルが無効化され何も書き込まれない
            ColorMask 0
            ZWrite Off
            Cull Front
            ZTest LEqual
            
            Stencil
            {
                Ref 1
                Comp Always
                Pass DecrSat
            }
            
            HLSLPROGRAM
            #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"

            struct appdata
            {
                float4 vertex : POSITION;
                float4 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            float3 _LightPosition;

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            float _ShadowLength;
            half4 _ShadowColor;

            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                // ライトの取得
                Light light = GetMainLight();

                // 法線をワールド空間に変換
                float3 worldNormal = TransformObjectToWorldNormal(v.normal);
                float nDotL = dot(worldNormal, light.direction);

                // 頂点を押し出す
                float3 vertex2 = v.vertex + TransformWorldToObjectDir(-light.direction) * _ShadowLength;

                o.vertex = TransformObjectToHClip(lerp(v.vertex, vertex2, 1 - step(-0.3, nDotL)));
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                return 0;
            }
            ENDHLSL
        }
    }
}

Stencilを上げるShaderと比較すると変更点は少ないです。

CullをFront指定することでオブジェクトの背面を伸ばします。

Cull Front

Stencilは以下のように記述してStencilを下げています。変更点はこれぐらいになります。

Stencil
{
    Ref 1
    Comp Always
    Pass DecrSat
}

Stencilをもとに影を描画する

Stencilは上記のRendererFeatureで操作できているので、あとはそれをもとに影を書いていきます。

RendererFeatureをまた一つ増やします。RenderObjectsを追加し、命名をDrawStencilShadowにしました。

Materialを新規で一つ作成して、それにアサインするShaderは以下のように記述しています。

Shader "Unlit/StencilShadowReceiver"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ShadowColor ("Shadow Color", COlor) = (0, 0, 0, 0.5)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            NAME "StencilShadowReceiver"
            
            Tags { "LightMode" = "ShadowStencil" }
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            
            Stencil
            {
                Ref 1
                Comp Equal
                Pass Keep
            }
            
            HLSLPROGRAM
            #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"

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

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

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            float _ShadowLength;
            half4 _ShadowColor;

            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;
            CBUFFER_END

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

            float4 frag (v2f i) : SV_Target
            {
                return _ShadowColor;
            }
            ENDHLSL
        }
    }
}

Stencilは以下のように記述しており今回は簡単なシーン設定のため、Stencilが1のところに影を書かせています。

Stencil
{
    Ref 1
    Comp Equal
    Pass Keep
 }

フラグメントシェーダーはMaterialから設定した色を描画するようにしています。

float4 frag (v2f i) : SV_Target
{
    return _ShadowColor;
}

最後に

上記によって以下のような影の描画を実現できました。

今回はステンシルシャドウボリュームを書いてみました。

ステンシルシャドウボリュームの弱点は影の生成単位がポリゴン単位なので例えば平面にTextureのアルファ抜きで模様や葉っぱを書いていた時、模様や葉っぱの影はできず平面の影ができてしまいます。

それを解決するために影生成用の引き延ばし用頂点を事前に仕込んでいたそうです。

ただ、これによって頂点シェーダーの負荷が上がるため、それを見越してポリゴン数を下げて品質が下がってしまうということもあったそうです。

参考

news.mynavi.jp

news.mynavi.jp

https://gameprogrammingunit.web.fc2.com/shadow/volume_simple.htm

zenn.dev

qiita.com