【UnityShader】オブジェクトの影を落とす #107
前回の成果
Bloomシェーダーについて学んだ。
今回やること
シェーダーでの影について学んでいきます。
事前準備
Scene上にCubeを配置します。
Cubeの影が出るようにPlaneを配置して準備は完了となります。
影
Unityのデフォルトのマテリアルを使用する際には特に意識せずとも、オブジェクトの影が表示されています。
ですが、自分でシェーダーを使用し、そのシェーダーを元にしたマテリアルを使用すると影の表示がなくなってしまいます。
影が出ない例
UnlitShaderを制作し、そのマテリアルをアタッチしています。
Planeに影が出ていません。
なので、シェーダーで影を描画していきます。
シェーダー
Shader "Unlit/ShadowDef" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { // 通常の描画 Pass { Tags { "RenderType"="Opaque" } 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 } // 影の描画 Pass { Tags { "LightMode"="ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { V2F_SHADOW_CASTER; }; v2f vert (appdata v) { v2f o; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o); return o; } fixed4 frag (v2f i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG } } }
1Pass目でオブジェクトの描画をし、2Pass目で影の描画をしていきます。
影自体の箇所は短いのですが、組み込みの関数を使用しているので中身について解説していきます。
LightMode Tag
以下の箇所で使用しています。
Tags { "LightMode"="ShadowCaster" }
RenderType
やQueue
と同じTagになります。
このタグは、ライティングに使用されるTagになっています。
組み込みのレンダリングパイプラインで、LightModeTagを設定しない場合には、Unityはライティングや影なしで描画しようとします。
ですので、UnlitShaderだと影やライティングが描画されません。
今回は、ShadowCasterを指定しているので、オブジェクトの深度をシャドウマップから深度テクスチャにレンダリングすることをUnityに伝えています。
LightMode Tagの一例
Tag名 | 意味 |
---|---|
Always | 常にレンダリングされ、ライティングは適応されない デフォルト値 |
ForwardBase | Forwardレンダリングで使用 アンビエント、メインのDirectionalLight、Vertex/SHLight、ライトマップを適応 |
Deferred | DeferredShading(遅延シェーディング)で使用 g-Bufferをレンダリング |
ShadowCaster | オブジェクトの深度をシャドウマップから深度テクスチャにレンダリング |
他にもLightMode Tagは種類があるのですが、一部割愛させていただきます。
以下リファレンスに他の種類が記載されています。
V2F_SHADOW_CASTER
以下の箇所で使用しています。
struct v2f {
V2F_SHADOW_CASTER;
};
V2F_SHADOW_CASTERの定義
#define V2F_SHADOW_CASTER V2F_SHADOW_CASTER_NOPOS UNITY_POSITION(pos)
V2F_SHADOW_CASTER_NOPOS
のあとにUNITY_POSITION(pos)
を呼んでいるのと同義です。
更に展開します。
V2F_SHADOW_CASTER_NOPOSの定義
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX) // 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(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(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 #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 = UnityObjectToClipPos(v.vertex.xyz); \ opos = UnityApplyLinearShadowBias(opos); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \ opos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); \ opos = UnityApplyLinearShadowBias(opos); #define SHADOW_CASTER_FRAGMENT(i) return 0; #endif
V2F_SHADOW_CASTER_NOPOSの解説
defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
は、シャドウマップにキューブマップを使うポイントライトかを見ているifになります。
また、SHADOWS_CUBE
はLightコンポーネントのTypeとShadowTypeによって定義されているものになります。
Lightコンポーネント
もう片方の条件となっているSHADOWS_CUBE_IN_DEPTH_TEX
の定義は以下になります。
SHADOWS_CUBE_IN_DEPTH_TEXの定義
#if defined(SHADER_API_D3D11) || defined(SHADER_API_PSSL) || defined(SHADER_API_METAL) || defined(SHADER_API_GLCORE) || defined(SHADER_API_GLES3) || defined(SHADER_API_VULKAN) || defined(SHADER_API_SWITCH) // D3D11, D3D12, XB1, PS4, iOS, macOS, tvOS, glcore, gles3, webgl2.0, Switch // Real-support for depth-format cube shadow map. #define SHADOWS_CUBE_IN_DEPTH_TEX #endif
基本的なプラットフォームは記載されているので、あまり気にする必要はなさそうです。
UNITY_POSITION(pos)の定義
// On D3D reading screen space coordinates from fragment shader requires SM3.0 #define UNITY_POSITION(pos) float4 pos : SV_POSITION
こちらは単純で、SV_POSITIONの定義だけになります。
V2F_SHADOW_CASTER_NOPOSのまとめ
少し長くなってしまったので、まとめます。
深度テクスチャではないキューブマップ
float3 vec : TEXCOORD0; float4 pos : SV_POSITION
それ以外(基本的にはこちら)
float4 pos : SV_POSITION
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
以下の箇所で使用しています。
v2f vert (appdata v) {
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);
return o;
}
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)の定義
TRANSFER_SHADOW_CASTER_NOPOS(o,o.pos)
に置き換わっているだけとなります。
// Vertex shader part, with support for normal offset shadows. Requires // position and normal to be present in the vertex input. #define TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) TRANSFER_SHADOW_CASTER_NOPOS(o,o.pos)
TRANSFER_SHADOW_CASTER_NOPOSの定義
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX) // 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(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(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 #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 = UnityObjectToClipPos(v.vertex.xyz); \ opos = UnityApplyLinearShadowBias(opos); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \ opos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); \ opos = UnityApplyLinearShadowBias(opos); #define SHADOW_CASTER_FRAGMENT(i) return 0; #endif
最初の条件式はV2F_SHADOW_CASTER
と同じく深度テクスチャではないキューブマップかを見ています。
TRANSFER_SHADOW_CASTER_NOPOSの解説(深度テクスチャではないキューブマップ)
レガシーな方は割愛させていただきます。
定義
#define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex);
_LightPositionRange
の定義はこちらです。
float4 _LightPositionRange; // xyz = pos, w = 1/range
つまり、TRANSFER_SHADOW_CASTER_NOPOSはこうなります。
o.vec = 頂点のワールド座標 - ライトの座標 opos = 頂点のクリップ座標
TRANSFER_SHADOW_CASTER_NOPOSの解説(それ以外の場合)
深度テクスチャではないキューブマップ以外の場合になります。
こちらもレガシーなものは割愛します。
定義
#define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \ opos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); \ opos = UnityApplyLinearShadowBias(opos);
UnityClipSpaceShadowCasterPos(v.vertex, v.normal)
と
UnityApplyLinearShadowBias(opos)
に展開されて処理されています。
UnityClipSpaceShadowCasterPos
定義
float4 UnityClipSpaceShadowCasterPos(float4 vertex, float3 normal) { float4 wPos = mul(unity_ObjectToWorld, vertex); if (unity_LightShadowBias.z != 0.0) { float3 wNormal = UnityObjectToWorldNormal(normal); float3 wLight = normalize(UnityWorldSpaceLightDir(wPos.xyz)); // apply normal offset bias (inset position along the normal) // bias needs to be scaled by sine between normal and light direction // (http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/) // // unity_LightShadowBias.z contains user-specified normal offset amount // scaled by world space texel size. float shadowCos = dot(wNormal, wLight); float shadowSine = sqrt(1-shadowCos*shadowCos); float normalBias = unity_LightShadowBias.z * shadowSine; wPos.xyz -= wNormal * normalBias; } return mul(UNITY_MATRIX_VP, wPos); } // Legacy, not used anymore; kept around to not break existing user shaders float4 UnityClipSpaceShadowCasterPos(float3 vertex, float3 normal) { return UnityClipSpaceShadowCasterPos(float4(vertex, 1), normal); }
unity_LightShadowBias.z
はユーザーが指定した法線のオフセット量になります。
基本的には、オブジェクトのクリップ座標を計算しているものになります。
法線方向にnormalBias
を乗算しているのは、シャドウアクネを軽減させるためになります。
このバイアスのスケールは法線とライトのアークコサインのサインに比例します。
ですので、
float shadowSine = sqrt(1-shadowCos*shadowCos);
で求めています。
このバイアスの意味合いは以下サイト様に記載されています。
Shadow Mapping Summary – Part 1 – The Witness
シャドウアクネ
少し脱線してしまいますが、シャドウアクネについて解説します。
以下の画像でシャドウアクネが発生していることが確認できます。
オブジェクトの影が描画されているのですが、影の他に縦方向の縞模様のようなものができてしまっています。
それがシャドウアクネと呼ばれるものになります。
影 - Unity マニュアル:より引用
発生の原因
シャドウマップで指定された距離のピクセルが遠くにあるように計算されてしまう場合に発生します。
以下の画像のように本来ポリゴンは頂点AからBにかけてなめらかになっています。
それがシャドウマップのテクセルによってZ値でまとめられ、一定の間隔で区切られてしまいます。
シャドウマップを使用し影をつけることは、この一定間隔に区切られたポリゴンとの比較をすることになります。
つまり以下画像の緑色の箇所が影と判定されてしまいます。
なので縞模様ができてしまっています。
対策として最も良いのは、1テクセルの大きさを小さくしてシャドウマップの解像度を上げることになります。
ですがこの方法ですとパフォーマンスが著しく落ちてしまいます。
なので、バイアスをかけてあげることによって対策をしています。
ただし、バイアスをかけすぎてしまうとオブジェクトと影が離れてしまうピーターパン現象と呼ばれるものが発生します。
ピーターパン現象の例
影 - Unity マニュアル:より引用
試してみる
実際にUnity上で試してみたいと思います。
特になにも考えずにオブジェクトをScene上に配置します。
違和感なく影が出ているのがわかるかと思います。
通常の影
LightコンポーネントのBias
とNormalBias
の値を0にしてみます。
そうすると、オブジェクトの影自体は問題なく描画されていますが、Planeに縞模様ができてしまっています。
シャドウアクネ
逆にLightコンポーネントのBias
とNormalBias
の値を最大値にしてみます。
そうすると、オブジェクトと影との距離が離れてしまっています。
ピーターパン現象
オブジェクトの大きさがそのままだと現象が発生していることが確認しにくかったので、縦方向にスケールさせています。
UnityApplyLinearShadowBiasの定義
float4 UnityApplyLinearShadowBias(float4 clipPos) { // For point lights that support depth cube map, the bias is applied in the fragment shader sampling the shadow map. // This is because the legacy behaviour for point light shadow map cannot be implemented by offseting the vertex position // in the vertex shader generating the shadow map. #if !(defined(SHADOWS_CUBE) && defined(SHADOWS_CUBE_IN_DEPTH_TEX)) #if defined(UNITY_REVERSED_Z) // We use max/min instead of clamp to ensure proper handling of the rare case // where both numerator and denominator are zero and the fraction becomes NaN. clipPos.z += max(-1, min(unity_LightShadowBias.x / clipPos.w, 0)); #else clipPos.z += saturate(unity_LightShadowBias.x/clipPos.w); #endif #endif #if defined(UNITY_REVERSED_Z) float clamped = min(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE); #else float clamped = max(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE); #endif clipPos.z = lerp(clipPos.z, clamped, unity_LightShadowBias.y); return clipPos; }
#if !(defined(SHADOWS_CUBE) && defined(SHADOWS_CUBE_IN_DEPTH_TEX))
で分岐されていますが、UnityApplyLinearShadowBias
を呼ぶ際にこの条件式を通らないと呼ばないので、分岐の中は実行されています。
UnityApplyLinearShadowBiasの解説
unity_LightShadowBias .x
とunity_LightShadowBias .y
にはこのブログを書いている段階では情報がありませんでした。
ですが、おそらくLightコンポーネントのBias
の値を元に処理をしているものだと思われます。
Bias
深度値のものであるZをバイアスに応じて増加させて、その値をClampしています。
UNITY_REVERSED_Z
UNITY_REVERSED_Z
はプラットフォーム毎のZの向きに対応するものになります。
UNITY_REVERSED_Zの値 | プラッフォーム | Zバッファの範囲 |
---|---|---|
1 | DX11/12 PS4 XboxOne Metal |
1 ~ 0 |
0 | その他のプラットフォーム | 0 ~ 1 |
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)のまとめ
こちらもまとめます。
深度テクスチャではないキューブマップ
o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; o.pos = UnityObjectToClipPos(v.vertex);
それ以外(基本的にはこちら)
o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); o.pos = UnityApplyLinearShadowBias(o.pos);
SHADOW_CASTER_FRAGMENT
使用箇所は以下になります。
fixed4 frag (v2f i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) }
SHADOW_CASTER_FRAGMENTの定義
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX) // 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(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(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 #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 = UnityObjectToClipPos(v.vertex.xyz); \ opos = UnityApplyLinearShadowBias(opos); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \ opos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); \ opos = UnityApplyLinearShadowBias(opos); #define SHADOW_CASTER_FRAGMENT(i) return 0; #endif
ここも他と同様に深度テクスチャではないキューブマップとそれ以外を見ています。
それ以外の場合は0を返しています。
これは深度を利用しているので、色は0で問題ないからになります。
UnityEncodeCubeShadowDepth
定義
// Encoding/decoding [0..1) floats into 8 bit/channel RGBA. Note that 1.0 will not be encoded properly. inline float4 EncodeFloatRGBA( float v ) { float4 kEncodeMul = float4(1.0, 255.0, 65025.0, 16581375.0); float kEncodeBit = 1.0/255.0; float4 enc = kEncodeMul * v; enc = frac (enc); enc -= enc.yzww * kEncodeBit; return enc; } float4 UnityEncodeCubeShadowDepth (float z) { #ifdef UNITY_USE_RGBA_FOR_POINT_SHADOWS return EncodeFloatRGBA (min(z, 0.999)); #else return z; #endif }
floatの精度を必要に応じて、rgbaに入れています。
必要なプラットフォームの分岐はUNITY_USE_RGBA_FOR_POINT_SHADOWS
で見ています。
これは、
// SHADER_API_GLES : OpenGL ES 2.0 // SHADER_API_GLES3 : OpenGL ES 3.0/3.1 defined(SHADER_API_GLES) || defined(SHADER_API_GLES3)
の場合となります。
SHADOW_CASTER_FRAGMENTのまとめ
深度テクスチャではないキューブマップ
return UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w);
それ以外(基本的にはこちら)
return 0
シェーダーにコメントを付与
全体のまとめとして、コメントに記載しました。
Shader "Unlit/ShadowDef" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { // 通常の描画 Pass { Tags { "RenderType"="Opaque" } 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 } // 影の描画 Pass { Tags { "LightMode"="ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { // 深度テクスチャではないキューブマップの場合 // float3 vec : TEXCOORD0; // float4 pos : SV_POSITION // それ以外の場合 // float4 pos : SV_POSITION V2F_SHADOW_CASTER; }; v2f vert (appdata v) { v2f o; // 深度テクスチャではないキューブマップの場合 // o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; // o.pos = UnityObjectToClipPos(v.vertex); // それ以外の場合 // o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); // o.pos = UnityApplyLinearShadowBias(o.pos); TRANSFER_SHADOW_CASTER_NORMALOFFSET(o); return o; } fixed4 frag (v2f i) : SV_Target { // 深度テクスチャではないキューブマップの場合 // UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w); // それ以外の場合 // return 0 SHADOW_CASTER_FRAGMENT(i) } ENDCG } } }
結果
右が通常のオブジェクト、左が今回制作したシェーダーのオブジェクトになります。
左のオブジェクトでも影がPlaneに落ちていることが確認できます。
今回は以上となります。
ここまでご視聴ありがとうございました。