Forward RenderingのShadowingについて調べてみた
はじめに
ForwardRenderingのShadowingについて調べたことをまとめます。
今回のサンプルでは以下の様な絵が出てきます。左2つの球はそれぞれvertex/fragment lightingを適用し、shadingを行ったもので、shadowingを行っていません。一番右の球はshadowingを行っていますがshadingは行っていません。 下に敷いてあるPlaneオブジェクトは一番右の球と同じシェーダーを適用しています。
影をつける際に用いられる手法は様々ですが、よく知られている方法に
- ステンシルシャドウ
- 深度バッファシャドウ
かあるそうです。
その44 深度バッファシャドウの根っこ:0から原理を眺めてみよう
シェーダーの全体像
ForwardRenderingについては以下を参照してください。
影をつけるためのシェーダーコードは以下のようになります。
Shader "Shadow/Shadowing" { SubShader { Pass { Name "ShadowCast" Tags {"LightMode" = "ShadowCaster"} CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include "UnityCG.cginc" struct v2f { V2F_SHADOW_CASTER; }; v2f vert(appdata_base v) { v2f o; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) return o; } float4 frag(v2f i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG } Pass { Name "ShadowReceive" Tags {"LightMode" = "ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #pragma multi_compile_fwdbase // shadow helper functions and macros #include "AutoLight.cginc" struct v2f { float2 uv : TEXCOORD0; SHADOW_COORDS(1) // put shadows data into TEXCOORD1 float4 pos : SV_POSITION; }; v2f vert(appdata_base v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = v.texcoord; // compute shadows data TRANSFER_SHADOW(o) return o; } fixed4 frag(v2f i) : SV_Target { fixed4 col = fixed4(0.5,0.5,0.5,1); // compute shadow attenuation (1.0 = fully lit, 0.0 = fully shadowed) fixed shadow = SHADOW_ATTENUATION(i); col.rgb *= shadow; return col; } ENDCG } } }
includeファイル内のマクロを利用しているため、かなり短いコードで影の描画を行うことができます。
定義した2つのパスの役割
ShadowCastパスはオブジェクトの作る影を投影するためのパスです。 このパスでオブジェクトの深度値をシャドウマップや深度テクスチャに描画します。
ShadowReceiveパスは、影を受けるオブジェクトを描画するパスです。 ShadowCastパスで描画されたシャドウマップや深度テクスチャから影を計算し、反映させます。
ShadowCaster
Tags {"LightMode" = "ShadowCaster"}
でShadowCasterパスでのレンダリングを指示しています。オブジェクトのデプスをシャドウマップや深度テクスチャに描画するパスです。あるオブジェクトに他のオブジェクト(セルフシャドウも)からの影を投影する場合、ForwardRenderingであろうとDeferredShadingであろうとこのパスが必要です。
ShadowCastで使われているマクロ
ShadowCastでは以下のマクロ定義が使われています。
- V2F_SHADOW_CASTER
- TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
- SHADOW_CASTER_FRAGMENT(i)
これらの定義を見ていきます。
#if defined(UNITY_MIGHT_NOT_HAVE_DEPTH_TEXTURE) #define UNITY_TRANSFER_DEPTH(oo) oo = o.pos.zw #define UNITY_OUTPUT_DEPTH(i) return i.x/i.y #else #define UNITY_TRANSFER_DEPTH(oo) #define UNITY_OUTPUT_DEPTH(i) return 0 #endif #ifdef SHADOWS_CUBE // Rendering into point light (cubemap) shadows #define V2F_SHADOW_CASTER_NOPOS float3 vec : TEXCOORD0; #define TRANSFER_SHADOW_CASTER_NOPOS_LEGACY(o,opos) o.vec = mul(_Object2World, v.vertex).xyz - _LightPositionRange.xyz; opos = mul(UNITY_MATRIX_MVP, v.vertex); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) o.vec = mul(_Object2World, v.vertex).xyz - _LightPositionRange.xyz; opos = mul(UNITY_MATRIX_MVP, v.vertex); #define SHADOW_CASTER_FRAGMENT(i) return UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w); #else // Rendering into directional or spot light shadows #if defined(UNITY_MIGHT_NOT_HAVE_DEPTH_TEXTURE) // Platform might not have actual depth textures, so output depth from pixel shader #define V2F_SHADOW_CASTER_NOPOS float4 hpos : TEXCOORD0; #define TRANSFER_SHADOW_CASTER_NOPOS_LEGACY(o,opos) \ opos = mul(UNITY_MATRIX_MVP, float4(v.vertex.xyz,1)); \ opos = UnityApplyLinearShadowBias(opos); \ o.hpos = opos; #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \ opos = UnityClipSpaceShadowCasterPos(v.vertex.xyz, v.normal); \ opos = UnityApplyLinearShadowBias(opos); \ o.hpos = opos; #else #define V2F_SHADOW_CASTER_NOPOS // Let embedding code know that V2F_SHADOW_CASTER_NOPOS is empty; so that it can workaround // empty structs that could possibly be produced. #define V2F_SHADOW_CASTER_NOPOS_IS_EMPTY #define TRANSFER_SHADOW_CASTER_NOPOS_LEGACY(o,opos) \ opos = mul(UNITY_MATRIX_MVP, float4(v.vertex.xyz,1)); \ opos = UnityApplyLinearShadowBias(opos); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \ opos = UnityClipSpaceShadowCasterPos(v.vertex.xyz, v.normal); \ opos = UnityApplyLinearShadowBias(opos); #endif #define SHADOW_CASTER_FRAGMENT(i) UNITY_OUTPUT_DEPTH(i.hpos.zw); #endif #define V2F_SHADOW_CASTER V2F_SHADOW_CASTER_NOPOS float4 pos : SV_POSITION #define TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) TRANSFER_SHADOW_CASTER_NOPOS(o,o.pos)
このコード内で上記で示した3つのマクロが定義されています。様々なケースで場合分けされています。
たとえば、シャドウマップを用いる方法でレンダリングを行うシェーダ内では以下のように展開されます。
struct v2f { float4 hpos : TEXCOORD0; float4 pos : SV_POSITION }; v2f vert(appdata_base v) { v2f o; o.pos = UnityClipSpaceShadowCasterPos(v.vertex.xyz, v.normal); o.pos = UnityApplyLinearShadowBias(o.pos); o.hpos = o.pos; return o; } float4 frag(v2f i) : SV_Target { return i.hpos.zw.x / i.hpos.zw.y; }
ShadowCastパスでは、SV_Target(フラグメントシェーダーの出力色)はシャドウマップをレンダリングターゲットとします。 このパスでは以下の様なシャドウマップが生成されます。
また、その後に呼ばれるパスでは、ShadowCastパスで作られたシャドウマップとカメラの深度テクスチャからスクリーンスペースのシャドウマップが作られます。
このスクリーンスペースのシャドウマップには
sampler2D _ShadowMapTexture
という記述を加える事でアクセスできるようになります。DeferredShadingについて記事を書いた時にこれを表示してみたことがあります。
ShadowReceiveパスで使われているマクロ
- SHADOW_COORDS(1)
- TRANSFER_SHADOW(o)
- SHADOW_ATTENUATION(i);
これらの定義は様々な条件によって変わってきますが、今回はスクリーンスペースでの影の描画を例に定義を見ていきます。
// ---- Screen space shadows #if defined (SHADOWS_SCREEN) #define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1; #if defined(UNITY_NO_SCREENSPACE_SHADOWS) UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture); #define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_World2Shadow[0], mul( _Object2World, v.vertex ) ); inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord) { #if defined(SHADOWS_NATIVE) fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord.xyz); shadow = _LightShadowData.r + shadow * (1-_LightShadowData.r); return shadow; #else unityShadowCoord dist = SAMPLE_DEPTH_TEXTURE_PROJ(_ShadowMapTexture, shadowCoord); // tegra is confused if we useツ_LightShadowData.x directly // with "ambiguous overloaded function reference max(mediump float, float)" half lightShadowDataX = _LightShadowData.x; return max(dist > (shadowCoord.z/shadowCoord.w), lightShadowDataX); #endif } #else // UNITY_NO_SCREENSPACE_SHADOWS sampler2D _ShadowMapTexture; #define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos); inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord) { fixed shadow = tex2Dproj( _ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord) ).r; return shadow; } #endif #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord) #endif // ---- Spot light shadows #if defined (SHADOWS_DEPTH) && defined (SPOT) #define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1; #define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_World2Shadow[0], mul(_Object2World,v.vertex)); #define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord) #endif // ---- Point light shadows #if defined (SHADOWS_CUBE) #define SHADOW_COORDS(idx1) unityShadowCoord3 _ShadowCoord : TEXCOORD##idx1; #define TRANSFER_SHADOW(a) a._ShadowCoord = mul(_Object2World, v.vertex).xyz - _LightPositionRange.xyz; #define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord) #endif // ---- Shadows off #if !defined (SHADOWS_SCREEN) && !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE) #define SHADOW_COORDS(idx1) #define TRANSFER_SHADOW(a) #define SHADOW_ATTENUATION(a) 1.0 #endif
これもまた色々な条件で分岐があるのですが、スクリーンスペースのシャドウマップを使う場合は以下のように展開されます。
struct v2f { float2 uv : TEXCOORD0; unityShadowCoord4 _ShadowCoord : TEXCOORD1; float4 pos : SV_POSITION; }; v2f vert(appdata_base v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = v.texcoord; // compute shadows data o._ShadowCoord = ComputeScreenPos(o.pos); return o; } fixed4 frag(v2f i) : SV_Target { fixed4 col = fixed4(0.5,0.5,0.5,1); // compute shadow attenuation (1.0 = fully lit, 0.0 = fully shadowed) fixed shadow = unitySampleShadow(i._ShadowCoord) col.rgb *= shadow; return col; }
このパスによって、以下のようにshadowingを行うオブジェクトが描画されていきます。
同じシェーダーを適用している場合、オブジェクトのZ値によって描画順は前後しますが、ShadowCasterで描画されたスクリーンスペースのシャドウマップを用いて影を描画しているのがわかると思います。