【URP】3Dモデルの頂点を引き伸ばして、シャドウボリュームを描画してみる

2023/04/03に公開

はじめに

シャドウボリュームを描画してみました。
3Dモデルの頂点を引き伸ばすことで、影のように見せています。

https://www.youtube.com/watch?v=jO0276HarS4

環境

Unity 2021.3.16f1
Universal RP 12.1.8

考え方

3Dモデルの陰になっている部分に注目します。

陰の部分の頂点を、光の進む方向へ引き伸ばすことで、シャドウボリュームになります。

シャドウボリュームを描画するための設定

Universal Renderer Data にRender Objectsを追加します。

Render Objects は以下のように設定します。

  1. Event は AfterRenderingTransparentsに設定
  2. LightMode Tags に ShadowForward を設定

これにより、"LightMode" = "ShadowForward" というTagが指定されたシェーダーのパスが、半透明描画後に実行されるようになります。

シェーダーの実装

シェーダーには、2つのパスを持たせます。

  1. 3Dモデルを描画するパス
  2. シャドウボリュームを描画するパス

BasePass.hlsl

3Dモデル本体を描画するパス用のシェーダー関数(vertやfrag)をBasePass.hlslに定義しました。

BasePass.hlsl
#ifndef BASE_PASS_INCLUDED
#define BASE_PASS_INCLUDED
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

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

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

sampler2D _MainTex;
float4 _MainTex_ST;

BaseVertexOutput vert (BaseVertexInput v)
{
    BaseVertexOutput o;
    o.vertex = TransformObjectToHClip(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    return o;
}

half4 frag (BaseVertexOutput i) : SV_Target
{
    half4 col = tex2D(_MainTex, i.uv);
    return col;
}
#endif

ShadowPass.hlsl

シャドウボリュームを描画するパス用のシェーダー関数は ShadowPass.hlsl に定義しました。

ShadowPass.hlsl
#ifndef SHADOW_PASS_INCLUDED
#define SHADOW_PASS_INCLUDED

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

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

struct ShadowVertexOutput
{
    float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
float4 _MainTex_ST;
float _ShadowLength;
half4 _ShadowColor;

ShadowVertexOutput vert (ShadowVertexInput v)
{
    ShadowVertexOutput o;

    // ライトの取得
    Light light = GetMainLight();

    // 法線をワールド空間に変換
    float3 worldNormal = TransformObjectToWorld(v.normal);

    // マスクの作成 (陰になっている部分は1.0になる)
    float nDotL = dot(worldNormal, light.direction);
    float mask = step(nDotL, 0.0); 
    
    // 頂点を押し出す
    float3 vertex2 = v.vertex + TransformWorldToObjectDir(light.direction) * _ShadowLength;

    o.vertex = TransformObjectToHClip(lerp(v.vertex, vertex2, mask));
}

half4 frag (ShadowVertexOutput i) : SV_Target
{
    return _ShadowColor;
}

#endif

MyShader.shader

シェーダーの実装は以下のようになります。
BaseパスとShadowパスの二つで構成されています。

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

        Pass
        {
            // 3dモデル本体を描画するパス

            Name "Base"
            
            Tags { "LightMode" = "UniversalForward" } 
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "BasePass.hlsl"
            ENDHLSL
        }
                
        Pass
        {
            // シャドウボリュームを描画するパス

            Name "Shadow"

            Tags { "LightMode" = "ShadowForward" }
            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite Off
            Cull Off
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "ShadowPass.hlsl"
            ENDHLSL
        }

    }
}

このシェーダーを3Dモデルにアタッチすることで、シャドウボリュームが設定されます。

影を1回だけ描画する

今回の方法だと、影が重なる部分が出てしまい、半透明のオーバードローが発生してしまいます。

以下のようなステンシルブロックを記述することで、影が1回だけ描画されるようにでき、オーバードローを回避できます。

Stencil
{
    Ref 1
    Comp Greater
    Pass IncrSat
}

Shadowパスを以下のように書き換えます。

MyShader.shader
Pass
{
    Name "Shadow"

    Tags { "RenderType"="Opaque" } 
    
+   Stencil
+   {
+       Ref 1
+       Comp Greater
+       Pass IncrSat
+   }
            
    Blend SrcAlpha OneMinusSrcAlpha
    ZWrite Off
    Cull Off
    
    HLSLPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #include "ShadowPass.hlsl"

    ENDHLSL
}

Discussion