見出し画像

Unity CustomDepthShadow

初めまして。

モバイルゲーム開発会社でTAとして従事しています。

以前、新規プロジェクトでCustomDepthShadowの実装を行った際の覚書として、実装までどのように開発、検証を行ったかを纏めたいと思います。

基本原理はこちらを参考にしています。

実装方法

今回は指向性ライトの位置を基にDepthShadowCameraを配置します。

DepthShadowCameraは指向性ライトの位置、Near、Farを参照します。

またRenderTextureを生成します。

またDepthShadowCameraはShader(ShadowMap_Caster)を差し替えて、深度テストと書き込みをオンにして、レンダリングを行うようにします。

レンダリング結果を受け取るShader(ShadowMap_Receiver)を実装します。

FragmentShaderで光源空間に変換し、計算された深度をShadowMapをサンプリングして得られた深度と比較し、シャドウ内にあるかどうかを判断します。

そのDepthShadowCameraを管理するComponentはこちらです。

CustomShadowMap.cs

public class CustomShadowMap : MonoBehaviour
{
	public enum ResolutionType
	{
		VeryLow  = 128,
		Low      = 256,
		Medium   = 512,
		High     = 1024,
		VeryHigh = 2048,
	}

	public Color shadowColor = Color.white;

	[Range(0f, 1f)]
	public float shadowStrength = 0.05f;

    public ResolutionType resolutionType = ResolutionType.VeryHigh;

    private Camera shadowCamera = default;
	private Shader shadowShader = default;
	private RenderTexture targetTexture = default;

    private void Start()
    {
        shadowCamera = shadowCamera ? shadowCamera : CreateShadowCamera();
		shadowShader = shadowShader ? shadowShader : Shader.Find("Hidden/ShadowMap_Caster");
		targetTexture = RenderTexture.GetTemporary((int)resolutionType , (int)resolutionType , 24, RenderTextureFormat.ARGB32);
		targetTexture.filterMode = FilterMode.Point;
		targetTexture.wrapMode = TextureWrapMode.Clamp;
        shadowCamera.targetTexture = targetTexture;
    }

	private void Update()
	{
		shadowCamera?.RenderWithShader(shadowShader, "");
        UpdateShaderKeyword();
        UpdateShadowCameraTransform();
	}    
    
	static Camera CreateShadowCamera()
	{
		GameObject go = new GameObject("ShadowMapCamera");
		go.hideFlags = HideFlags.DontSave;
		var shadowCamera = go.AddComponent<Camera>();
		shadowCamera.orthographic = true;
		shadowCamera.nearClipPlane = 0;
    	shadowCamera.enabled = false;
		shadowCamera.farClipPlane = 9999f;
		shadowCamera.backgroundColor = Color.white;
		shadowCamera.clearFlags = CameraClearFlags.SolidColor;
		shadowCamera.cullingMask = 1 << LayerMask.NameToLayer("Caster");
		return shadowCamera;
	}

    private void UpdateShaderKeyword()
    {
		Shader.SetGlobalTexture("_gShadowMapTexture", shadowCamera ? shadowCamera.targetTexture : targetTexture);
		Matrix4x4 projectionMatrix = GL.GetGPUProjectionMatrix(shadowCamera.projectionMatrix, false);
		Shader.SetGlobalMatrix("_gWorldToShadow", projectionMatrix * shadowCamera.worldToCameraMatrix);
		Shader.SetGlobalColor("_gShadowColor", shadowColor);

		Vector4 size = Vector4.zero;
		size.y = shadowCamera.orthographicSize * 2;
		size.x = shadowCamera.aspect * size.y;
		size.z = shadowCamera.farClipPlane;
		size.w = 1.0f / (int)resolutionType;
		Shader.SetGlobalVector("_gShadowTexScale", size);

		Vector4 param = Vector4.zero;
        param.x = shadowStrength;
		Shader.SetGlobalVector("_gShadowParams", param);
    }
    
    private void UpdateShadowCameraTransform()
    {
    	var light = FindObjectOfType<Light>();
		if (light == null)
		{
			return;
		}

		var trans = shadowCamera.transform;
		trans.position = light.transform.position;
		trans.rotation = light.transform.rotation;
		trans.LookAt(trans.position + trans.forward, trans.up);

		Vector3 center, extents;
		List<Renderer> renderers = new List<Renderer>();
		renderers.AddRange(FindObjectsOfType<Renderer>());
		GetRenderersExtents(renderers, trans, out center, out extents);
		center.z -= extents.z / 2;

		trans.position = trans.TransformPoint(center);
		shadowCamera.nearClipPlane = 0;
		shadowCamera.farClipPlane = extents.z;
		shadowCamera.aspect = extents.x / extents.y;
		shadowCamera.orthographicSize = extents.y / 2;
    }
    
    private void GetRenderersExtents(List<Renderer> renderers, Transform transform, out Vector3 center, out Vector3 extents)
    {
        //省略
    }
}

DepthShadowCameraは生成後、CasterLayerのみを描画するようにします。

RenderTexture、ComponentのパラメーターをGlobalShaderParameterとして受け渡します。

続いてShadowMap_Casterは以下になります。

ShadowMap_Caster.shader

Shader "Hidden/ShadowMap_Caster" 
{
	SubShader
	{
		Tags { "RenderType" = "Opaque" }

		Pass
		{
			Fog { Mode Off }
			Cull Front
			CGPROGRAM
			#include "UnityCG.cginc"
			#pragma vertex vert
			#pragma fragment frag

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float2 depth: TEXCOORD1;
			};

			v2f vert(appdata_base v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.depth = COMPUTE_DEPTH_01;
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				fixed4 result = fixed4(EncodeFloatRGBA(i.depth));
				return result;
			}
			ENDCG
		}
	}
}

EncodeFloatRGBAは、深度を32ビットで保存するため、深度の精度が向上します。

CullingFrontにしているのはShadowアクネの問題を解決します。

FrameDebuggerでの結果は以下となります。

画像1

続いてShadowMap_Receiverは以下になります。

ShadowMap_Receiver.shader

Shader "CustomShadow/ShadowMap_Receiver" 
{
	Properties
	{
		[Enum(OFF,0,FRONT,1,BACK,2)] _CullMode("Cull Mode", int) = 2
		[Enum(Off, 0, On, 1)] _ZWrite("ZWrite", Float) = 1.0
		_MainTex("Texture", 2D) = "white" {}
		_Color("Main Color", Color) = (1,1,1,1)
	}

	CGINCLUDE
	#include "UnityCG.cginc"
	sampler2D _MainTex; float4 _MainTex_ST;

    float4x4 _gWorldToShadow;
    sampler2D _gShadowMapTexture; float4 _gShadowMapTexture_TexelSize;
    float4 _gShadowColor;
    half _gShadowBlurWeight[25];
    half _gShadowBlurDistance;
    
    float4 _gShadowTexScale;
    #define _OrthographicSize _gShadowTexScale.xy
    #define _Far _gShadowTexScale.z
    #define _PixelSize _gShadowTexScale.w

    float4 _gShadowParams;
    #define _ShadowStrength _gShadowParams.x
    #define _ShadowBias _gShadowParams.y
    #define _ShadowThreshold _gShadowParams.z
    #define _PcfSpread _gShadowParams.w
        
    struct VSInput
    {
    	float4 vertex : POSITION;
    	float2 uv : TEXCOORD0;
    	UNITY_VERTEX_INPUT_INSTANCE_ID
    };    

    struct PSInput
    {
    	float4 pos : SV_POSITION;
    	float2 uv : TEXCOORD0;
    	float4 shadowCoord : TEXCOORD1;
    };
    
	UNITY_INSTANCING_BUFFER_START(Props)
		UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
	UNITY_INSTANCING_BUFFER_END(Props)

	PSInput VSMain(VSInput v)
	{
		PSInput o = (PSInput)0;
		UNITY_TRANSFER_INSTANCE_ID(v, o); 
		o.uv = TRANSFORM_TEX(v.uv, _MainTex);
		o.pos = UnityObjectToClipPos(v.vertex);
		o.shadowCoord = mul(_gWorldToShadow, mul(unity_ObjectToWorld, v.vertex));
		return o;
	}

	fixed4 PSMain(PSInput i) : SV_Target
	{
		UNITY_SETUP_INSTANCE_ID(i);
		float4 baseColor = tex2D(_MainTex, i.uv);
		baseColor *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
		baseColor.xyz += UNITY_LIGHTMODEL_AMBIENT.xyz;

        // 影判定を追加する。
        return baseColor;
	}

	ENDCG

	SubShader
	{
		Tags { "RenderType" = "Opaque" }

		Pass 
		{
			Name "FORWARD"

			Tags { "LightMode" = "ForwardBase" }
			Cull[_CullMode]
			ZWrite[_ZWrite]

			CGPROGRAM
			#pragma target 3.0
			#pragma vertex VSMain
			#pragma fragment PSMain
			#pragma multi_compile_instancing
			#pragma multi_compile_fwdbase
			#pragma fragmentoption ARB_precision_hint_fastest  
			ENDCG
		}
	}

	Fallback "VertexLit"
}

ShadowMap_Receiverでは通常のPBRShaderやNPRShaderのPixelShadingを行い、影判定を追加するのみとなります。

GlobalParameterの_gWorldToShadowを受け取り、光源空間に変換された現在のフラグメントの均一な座標を取得します。

受け取ったParameterを基に影判定を実装してみます。

fixed4 PSMain(PSInput i) : SV_Target
{
	UNITY_SETUP_INSTANCE_ID(i);
	float4 baseColor = tex2D(_MainTex, i.uv);
	baseColor *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
	baseColor.xyz += UNITY_LIGHTMODEL_AMBIENT.xyz;

    // Hard Shadow
    // 射影変換後、サンプリング座標xyの範囲は-1から1であり、0から1にマップする必要がある。
	i.shadowCoord.xy = i.shadowCoord.xy / i.shadowCoord.w;
	float2 uv = i.shadowCoord.xy;
	uv = uv * 0.5 + 0.5;

	float depth = i.shadowCoord.z / i.shadowCoord.w;
#if defined(SHADER_TARGET_GLSL)
	depth = depth * 0.5 + 0.5;
#elif defined(UNITY_REVERSED_Z)
	depth = 1 - depth;
#endif
	float4 col = tex2D(_gShadowMapTexture, uv);
	float sampleDepth = DecodeFloatRGBA(col);
	float shadow = sampleDepth < depth ? _ShadowStrength : 1;
	return baseColor * shadow;
}

早速結果を見ていきます。

画像2

影描画は出来たのですが、ジャギーが目立ちますね...

クオリティに問題がありそうです。そこでPCFソフトシャドウを実装してみます。

Percentage Closer Filtering

Percentage Closer Filtering(PCF)は、近くのピクセルの複数のサンプルを平均化してソフトシャドウの効果を実現することにより、シャドウエッジのアンチエイリアシングを実現します。

早速実装してみます。

float PCF6x6(float depth, float2 uv)
{
	float shadow = 0.0;
	float step = 0.1;

	for (int x = -2.5; x <= 2.5; ++x)
	{
		for (int y = -2.5; y <= 2.5; ++y)
		{
			step += 0.1;
			float4 col = tex2D(_gShadowMapTexture, uv + float2(x, y) * _gShadowMapTexture_TexelSize.xy);
			float sampleDepth = DecodeFloatRGBA(col);
			shadow += sampleDepth < depth ? saturate(_ShadowStrength * step)  : 1;
		}
	}
	return shadow / 36;
}

fixed4 PSMain(PSInput i) : SV_Target
{
    UNITY_SETUP_INSTANCE_ID(i);
	float4 baseColor = tex2D(_MainTex, i.uv);
	baseColor *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
	baseColor.xyz += UNITY_LIGHTMODEL_AMBIENT.xyz;

    // PCFShadow
    // 射影変換後、サンプリング座標xyの範囲は-1から1であり、0から1にマップする必要がある。    
	i.shadowCoord.xy = i.shadowCoord.xy / i.shadowCoord.w;
	float2 uv = i.shadowCoord.xy;
	uv = uv * 0.5 + 0.5;

	float depth = i.shadowCoord.z / i.shadowCoord.w;
#if defined(SHADER_TARGET_GLSL)
	depth = depth * 0.5 + 0.5;
#elif defined(UNITY_REVERSED_Z)
	depth = 1 - depth;
#endif
	float shadow = saturate(PCF6x6(depth, uv));
	if (shadow <= _ShadowThreshold)
	{
		return baseColor * _gShadowColor * shadow;
	}
	return baseColor;
}

画像3

中々良くなったかと思います。

他にも沢山の技法がありますが、基本的なアプローチは同じです。

DepthShadow技法について

最後にShadowMapのPCF技法を実装しましたが、他にも

PSM(PerspectiveShadowMap)

VSM(VarianceShadowMap)

SDF(Distance Field Soft Shadows)

等多岐にわたります。

興味がある方はこちらを参考にして見て下さい。

© UTJ/UCL

この記事が気に入ったら、サポートをしてみませんか?
気軽にクリエイターの支援と、記事のオススメができます!
10
UnrealEngine Unity DirectX12の技術ブログです。