UnityでForwardのライトに対応したLambert反射モデルのシェーダを作成する
はじめに
以前に作ったUnityのシェーダはUnityのライトには反応せず 常にプロパティで与えた方向の光が当たるものでした。 このままではシーンに配置されたライトには反応してくれません。 そこで今回はUnityのForwardのライトに対応したLambertシェーダを作ってみます。 適宜Unityで用意されたマクロを利用しますが、そのマクロの中身についても見ていきます。
マクロについてはドキュメント化されていないものも多く、推測が多数含まれています。 また、私自身の知識が足りていないことによる誤りも多数含まれていると思われます。 この記事を参考にする場合は自己責任でお願いします。
- Unityのバージョン: 2018.3.0b5
Forwardのライティングパス
UnityのForwardのライティングパスについては次のページが詳しいです。
公式のドキュメントは次のページです。
実際に次のようなポイントライトを8つおいたシーンを作成して試してみます。 球はライトのある平面より少し下に配置しています。 球体のマテリアルはStandard Shaderです。
フレームデバッガで確認してみると次のようになります。
1パス目のベースパスが暗いですが、これは平行光源が置かれていないシーンのためです。 どうやらベースパスでは平行光源しか扱わないようです。 この平行光源のないシーンでは加算パスが計4回実行されていました。
平行光源1つに対応したシェーダを書く
まずは平行光源1つに対応するところから始めてみます。
Shader "ForwardLambert"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Pass
{
Tags { "LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
void vert (in appdata v, out v2f o)
{
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
}
void frag (in v2f i, out fixed4 col : SV_Target)
{
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 normal = normalize(i.worldNormal);
float NL = dot(normal, lightDir);
float3 baseColor = tex2D(_MainTex, i.uv);
float3 lightColor = _LightColor0;
col = fixed4(baseColor * lightColor * max(NL, 0), 0);
}
ENDCG
}
}
}
プロパティとしてベースカラーをテクスチャで渡すことにします。
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
シェーダのパスにはTags{"LightMode"="ForwardBase"}
を指定します。
これでこのパスがForwardのベースパスとして呼ばれるようになります。
Pass
{
Tags { "LightMode"="ForwardBase"}
...
頂点シェーダはフラグメントシェーダで必要なものを詰めているだけです。
フラグメントシェーダでは次のようにして平行光源の方向を取得しています。
float3 lightDir = _WorldSpaceLightPos0.xyz;
_WorldSpaceLightPos0.xyz
は現在扱っている光源が点光源の場合は光源の位置が、
平行光源の場合には光源の方向が渡ってくるようです。
ベースパスの場合は平行光源しか処理されないようなので
点光源の位置が渡ってくることはないようです。
「Create > Shader > Standard Surface Shader」でSurface Shaderを作成し、 「show generated code」で変換したコードを見てみると次のように記述されています。
#ifndef USING_DIRECTIONAL_LIGHT
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
#else
fixed3 lightDir = _WorldSpaceLightPos0.xyz;
#endif
Surface Shaderから生成されたコードについては次のページで詳しく解説されています。
ベースパスの場合は平行光源を扱うので常にUSING_DIRECTIONAL_LIGHT
になるため
このような分岐は必要ないと思うのですが……。
ベースパスでは呼ばれないであろう方のUnityWorldSpaceLightDir
は
UnityCG.cgincで定義されています。
定義は次のとおりです。
// Computes world space light direction, from world space position
inline float3 UnityWorldSpaceLightDir( in float3 worldPos )
{
#ifndef USING_LIGHT_MULTI_COMPILE
return _WorldSpaceLightPos0.xyz - worldPos * _WorldSpaceLightPos0.w;
#else
#ifndef USING_DIRECTIONAL_LIGHT
return _WorldSpaceLightPos0.xyz - worldPos;
#else
return _WorldSpaceLightPos0.xyz;
#endif
#endif
}
この関数の中でもさらに分岐があるようですね。
ベースパスは平行光源のみなので
float3 lightDir = _WorldSpaceLightPos0.xyz;
だけで済ませることにしました。
ライトの色は_LightColor0
で取得します。
float3 lightColor = _LightColor0;
_LightColor0
はLighting.cginc
でインクルードされている
UnityLightingCommon.cginc
で定義されています。
Lighting.cginc
をインクルードしておきましょう。
#include "UnityCG.cginc"
#include "Lighting.cginc"
手に入れたライトの方向と色、そして法線からLambert反射を計算しています。
float NL = dot(normal, lightDir);
...
col = fixed4(baseColor * lightColor * max(NL, 0) 0);
これでUnityの一番明るいDirectional Lightに対応してライティングが変わるようになります。
次の画像はライトを回転させてみた様子です。
シーンに平行光源が存在しない場合には_LightColor0
に0が渡されてくるため
平行光源によるライトの影響は消えるようです。
影を落とすようにする
このマテリアルはまだ影を落としてくれません。 次の画像はStandard Shaderとの比較です。
影を落とすためにはパスを追加する必要があります。 これについてはUnityの公式マニュアルが詳しいです。
マニュアルにしたがって新しいパスを追加します。
Pass
{
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
}
これで影を落とすようになりました。
マクロの中身
ShadowCasterパスで使われている
V2F_SHADOW_CASTER
、TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
、
SHADOW_CASTER_FRAGMENT(i)
はどれもUnityCG.cgincで定義されているマクロです。
順に見ていきます。
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
はUnityCG.cgincで次のように定義されています。
#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
SHADOWS_CUBE
などはShader Variantです。
Shader Variantについては次のページがわかりやすいです。
見たところシャドウマップにキューブマップを使うポイントライトと それ以外のライトで分岐しているようです。 キューブマップでもデプステクスチャを利用している場合は後者が呼ばれるようです。 デプステクスチャでないキューブマップはレガシーなものだそうです。
SHADOWS_CUBE
の定義はcgincファイルには見当たらないので実行時に渡されるもののようです。
試しに影のあるポイントライトを設定してみるとSHADOWS_CUBE
が定義されました。
SHADOWS_CUBE_IN_DEPTH_TEX
はUnityCG.cgincで次のように定義されていました。
#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
プラットフォームによって分岐しているようですね。
デプステクスチャでないキューブマップの場合は
V2F_SHADOW_CASTER_NOPOS
が次のように定義されています。
#define V2F_SHADOW_CASTER_NOPOS float3 vec : TEXCOORD0;
キューブマップ用のベクトルをv2fに定義しているようです。
デプステクスチャでないキューブマップ以外の場合はV2F_SHADOW_CASTER_NOPOS
が空のようです。
#define V2F_SHADOW_CASTER_NOPOS
次にUNITY_POSITION(pos)
を見てみます。
UNITY_POSITION(pos)
はUnityCG.cgincで次のように定義されています。
// On D3D reading screen space coordinates from fragment shader requires SM3.0
#define UNITY_POSITION(pos) float4 pos : SV_POSITION
SV_POSITION
を定義しているだけのようですね。
まとめると、デプステクスチャでないキューブマップのときにはv2fにvecとposを、 それ以外の場合にはposのみを定義するマクロということになります。
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
次に頂点シェーダで呼ばれている
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
を見ていきます。
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
の定義は次のとおりです。
// 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_NORMALOFFSET(o)
は
TRANSFER_SHADOW_CASTER_NOPOS(o,o.pos)
に書き換わるだけのようです。
TRANSFER_SHADOW_CASTER_NOPOS(o,o.pos)
は先ほどと同じ場所で定義されています。
#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
デプステクスチャでないキューブマップの場合は次のようになるようです。
#define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex);
_LightPositionRange
はUnityShaderVariables.cgincで次のように記述されています。
float4 _LightPositionRange; // xyz = pos, w = 1/range
_LightPositionRange.xyz
はライトの座標のようですね。
頂点のワールド座標からライトの座標を引いてvecに代入し、
posにはクリップ座標を計算して入れているようです。
デプステクスチャでないキューブマップ以外の場合次のようになります。
#define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \
opos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); \
opos = UnityApplyLinearShadowBias(opos);
UnityClipSpaceShadowCasterPos(v.vertex, v.normal);
の呼び出しと
UnityApplyLinearShadowBias(opos);
の呼び出しに展開されるようです。
それぞれ順番に見ていきます。
UnityClipSpaceShadowCasterPos(v.vertex, v.normal)
は
UnityCG.cgincで次のように定義されています。
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);
}
オフセットが与えられた場合に それを考慮してノーマル方向に縮小して、 それからClipスペースのポジションを計算するようです。 コメントにあるページでこのオフセットの計算について書かれています。
バイアスの量は各ライトの設定で与えられるようです。
ライトの設定の「Normal Bias」の値をいじると 確かに影がノーマル方向に縮小されているのがわかります。
次にUnityApplyLinearShadowBias(opos)
を見てみます。
UnityApplyLinearShadowBias(opos)
はUnityCG.cgincで次のように定義されています。
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))
と書かれていますが、今回は
defined(SHADOWS_CUBE) && defined(SHADOWS_CUBE_IN_DEPTH_TEX)
でないときに
この関数を呼んでいるのでこの分岐の中は実行されます。
unity_LightShadowBias.x
とunity_LightShadowBias.y
については情報がないので
よくわからないのですが、ライトの設定に「Normal Bias」と「Bias」が存在するので、
こちらでは「Bias」の方を処理しているものと思われます。
クリップスペースのzを増加させてクランプしています。
UNITY_REVERSED_Z
は特定の環境でZの向きが逆向きになっているものに対応しているようです。
Zの向きについては次のページに解説がありました。
これらのバイアスの計算はシャドウアクネ(Shadow Acne)の対策です。 シャドウアクネについては次のページがわかりやすいです。
ライトのデフォルト設定では影には問題がなさそうに見えます。
バイアスを両方とも0にするとシャドウアクネの発生が確認できます。
バイアスが小さいとその分シャドウアクネが起きやすくなります。 しかし、バイアスを大きくするとそれはそれで別の問題が発生します。
影が痩せてしまったり、 ピーターパン現象という影がオブジェクトから離れてしまう現象などが 発生しています。
これらのバイアスの最適な値はシーンに応じて変わってくるので、 シーンに合わせて調整する必要があります。
まとめると、頂点シェーダに与える
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
マクロは次のような動作になります。
デプステクスチャでないキューブマップの場合は
vecとクリップスペースの位置を計算します。
それ以外の場合ではシャドウアクネ対策としてNormal BiasとBiasを与えて
クリップスペースを計算します。
SHADOW_CASTER_FRAGMENT(i)
フラグメントシェーダの
SHADOW_CASTER_FRAGMENT(i)
についてもさきほどと同様の場所で定義されています。
#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に詰め込んでいるようです。
マクロを展開して整理すると次のようになります。
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;
float4 pos : SV_POSITION;
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
float3 vec : TEXCOORD0;
#endif
};
void vert(in appdata_base v, out v2f o)
{
// TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz;
o.pos = UnityObjectToClipPos(v.vertex);
#else
o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal);
o.pos = UnityApplyLinearShadowBias(o.pos);
#endif
}
float4 frag(v2f i) : SV_Target
{
// SHADOW_CASTER_FRAGMENT(i)
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
return UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w);
#else
return 0;
#endif
}
ENDCG
}
影を受けるようにする
現状ではまだ影を受けることができません。 次の画像はStandard Shaderとの比較です。
マニュアルに影を受ける例も載っているのですが、 Surface Shaderから生成したコードと少し異なるようです。
マニュアルではSHADOW_COORDS(n)
、TRANSFER_SHADOW(o)
、
そしてSHADOW_ATTENUATION(i)
というマクロ使っています。
Surface Shaderから生成したコードではUNITY_SHADOW_COORDS(n)
、
UNITY_TRANSFER_SHADOW(o,coord)
、
それからUNITY_LIGHT_ATTENUATION(attenuation, i, worldPos)
というマクロ使っています。
今回はSurface Shaderから生成したコードをベースにします。
まずは#pragma
を追加します。
#pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlight
「Compiled code」の「Compile adn show code」の右の矢印から「Show」で 生成されるキーワードを確認できます。
この#pragma
で次のようなキーワードが生成されるようです。
// -----------------------------------------
// Snippet #1 platforms ffffffff:
Builtin keywords used: DIRECTIONAL LIGHTMAP_SHADOW_MIXING LIGHTPROBE_SH SHADOWS_SCREEN SHADOWS_SHADOWMASK
4 keyword variants used in scene:
DIRECTIONAL
DIRECTIONAL LIGHTPROBE_SH
DIRECTIONAL SHADOWS_SCREEN
DIRECTIONAL LIGHTPROBE_SH SHADOWS_SCREEN
次に#include "AutoLight.cginc"
を加えます。
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
この#include
したファイルの中で定義されているマクロを利用します。
appdata構造体を書き換えます。
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
float2 texcoord1: TEXCOORD1;
};
float2 texcoord1: TEXCOORD1;
を追加しました。
ライトマップがある場合にライトマップのUVが渡されてきます。
v2f構造体を次のように書き換えます。
struct v2f
{
// float4 vertex : SV_POSITION;
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
#ifdef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
UNITY_LIGHTING_COORDS(3,4)
#else
UNITY_SHADOW_COORDS(3)
#endif
};
pos
の名前を決め打ちでマクロが作られているようで、
別の名前だとエラーが出ます。
そのためvertex
からpos
に名前を変えています。
worldPos
も追加します。
UNITY_LIGHTING_COORDS(3,4)
もしくはUNITY_SHADOW_COORDS(3)
を
構造体に追加しています。
数字は使われるTEXCOORDN
のN
です。
worldNormal
とworldPos
、uv
でTEXCOORD2
まで使っているので3と4を指定しました。
頂点シェーダにUNITY_INITIALIZE_OUTPUT(v2f, o);
と
UNITY_TRANSFER_SHADOW(o,v.texcoord1.xy);
を追加します。
void vert (in appdata v, out v2f o)
{
UNITY_INITIALIZE_OUTPUT(v2f, o);
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy);
}
フラグメントシェーダでUNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
を使います。
このマクロで定義されるattenuation
は影で0、光の当たる部分で1となります。
attenuation
をライティング結果にかけ合わせます。
void frag (in v2f i, out fixed4 col : SV_Target)
{
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 normal = normalize(i.worldNormal);
float NL = dot(normal, lightDir);
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
float3 baseColor = tex2D(_MainTex, i.uv);
float3 lightColor = _LightColor0;
col = fixed4(baseColor * lightColor * max(NL, 0) * attenuation, 0);
}
これで影を受け取れるようになりました。
マクロの中身
v2f構造体に追加したマクロ
v2f構造体に次のようなマクロを追加しました。
#ifdef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
UNITY_LIGHTING_COORDS(3,4)
#else
UNITY_SHADOW_COORDS(3)
#endif
この場合分けはSurface Shaderから生成したコードにならっています。
Surface Shaderから生成したコードは次のとおりです。
#ifndef LIGHTMAP_ON
// half-precision fragment shader registers:
#ifdef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
#define FOG_COMBINED_WITH_WORLD_POS
struct v2f_surf {
UNITY_POSITION(pos);
float2 pack0 : TEXCOORD0; // _MainTex
float3 worldNormal : TEXCOORD1;
float4 worldPos : TEXCOORD2;
#if UNITY_SHOULD_SAMPLE_SH
half3 sh : TEXCOORD3; // SH
#endif
UNITY_LIGHTING_COORDS(4,5)
#if SHADER_TARGET >= 30
float4 lmap : TEXCOORD6;
#endif
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
#endif
// high-precision fragment shader registers:
#ifndef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
struct v2f_surf {
UNITY_POSITION(pos);
float2 pack0 : TEXCOORD0; // _MainTex
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
#if UNITY_SHOULD_SAMPLE_SH
half3 sh : TEXCOORD3; // SH
#endif
UNITY_FOG_COORDS(4)
UNITY_SHADOW_COORDS(5)
#if SHADER_TARGET >= 30
float4 lmap : TEXCOORD6;
#endif
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
#endif
#endif
// with lightmaps:
#ifdef LIGHTMAP_ON
// half-precision fragment shader registers:
#ifdef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
#define FOG_COMBINED_WITH_WORLD_POS
struct v2f_surf {
UNITY_POSITION(pos);
float2 pack0 : TEXCOORD0; // _MainTex
float3 worldNormal : TEXCOORD1;
float4 worldPos : TEXCOORD2;
float4 lmap : TEXCOORD3;
UNITY_LIGHTING_COORDS(4,5)
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
#endif
// high-precision fragment shader registers:
#ifndef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
struct v2f_surf {
UNITY_POSITION(pos);
float2 pack0 : TEXCOORD0; // _MainTex
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
float4 lmap : TEXCOORD3;
UNITY_FOG_COORDS(4)
UNITY_SHADOW_COORDS(5)
#ifdef DIRLIGHTMAP_COMBINED
float3 tSpace0 : TEXCOORD6;
float3 tSpace1 : TEXCOORD7;
float3 tSpace2 : TEXCOORD8;
#endif
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
#endif
#endif
UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
について次のページ説明がありました。
UNITYHALFPRECISIONFRAGMENTSHADERREGISTERS : UNITYHALFPRECISIONFRAGMENTSHADERREGISTERS is set automatically for platforms that don't require full floating-point precision support in fragment shaders.
デスクトップではfloat
もhalf
もfixed
も全部32bit精度で計算されます。
これのことを言っているのだとしたら、 デスクトップとそれ以外で場合分けしているということなのでしょうか。
結局今回調べた範囲ではこの場合分けは必要ないように思えます。
この場合分けはベースパスにのみ書かれていました。
ベースパスでは平行光源のみ扱います。
後で見ますが、平行光源の場合はUNITY_LIGHTING_COORDS(3,4)
と
UNITY_SHADOW_COORDS(3)
はほとんど同じものになります。
UNITY_LIGHTING_COORDS(3,4)
について見ていきます。
AutoLight.cgincで次のように定義されています。
#define UNITY_LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) UNITY_SHADOW_COORDS(idx2)
DECLARE_LIGHT_COORDS(idx1)
とUNITY_SHADOW_COORDS(idx2)
に展開されます。
DECLARE_LIGHT_COORDS(idx1)
について見てみます。
AutoLight.cgincで次のとおり定義されていました。
#ifdef POINT
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord3 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xyz;
# define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).r * SHADOW_ATTENUATION(a))
#endif
#ifdef SPOT
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex));
# define LIGHT_ATTENUATION(a) ( (a._LightCoord.z > 0) * UnitySpotCookie(a._LightCoord) * UnitySpotAttenuate(a._LightCoord.xyz) * SHADOW_ATTENUATION(a) )
#endif
#ifdef DIRECTIONAL
# define DECLARE_LIGHT_COORDS(idx)
# define COMPUTE_LIGHT_COORDS(a)
# define LIGHT_ATTENUATION(a) SHADOW_ATTENUATION(a)
#endif
#ifdef POINT_COOKIE
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord3 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xyz;
# define LIGHT_ATTENUATION(a) (tex2D(_LightTextureB0, dot(a._LightCoord,a._LightCoord).rr).r * texCUBE(_LightTexture0, a._LightCoord).w * SHADOW_ATTENUATION(a))
#endif
#ifdef DIRECTIONAL_COOKIE
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord2 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
# define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, a._LightCoord).w * SHADOW_ATTENUATION(a))
#endif
ライトの種類によって変わってくるようです。
DIRECTIONAL
の場合を見てみると次のとおりです。
#ifdef DIRECTIONAL
# define DECLARE_LIGHT_COORDS(idx)
# define COMPUTE_LIGHT_COORDS(a)
# define LIGHT_ATTENUATION(a) SHADOW_ATTENUATION(a)
#endif
DECLARE_LIGHT_COORDS(idx)
は空です。
つまりDIRECTIONAL
の場合はUNITY_LIGHTING_COORDS(3,4)
は
ただUNITY_SHADOW_COORDS(4)
に展開されることになります。
DIRECTIONAL_COOKIE
についても平行光源っぽいので見てみます。
クッキーについては次のページに解説があります。 マスクのテクスチャを使ってライトの形状を変更する機能です。
ライトの設定からクッキーを指定できます。
フレームデバッガを確認してみると、 クッキーを設定した平行光源は加算パスで計算されているようです。
ベースパスの場合は単純な平行光源しか扱わないためUNITY_SHADOW_COORDS
の
場合だけを考えれば良さそうです。
UNITY_SHADOW_COORDS
について見ていきます。
AutoLight.cgincに次のように定義されています。
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI) // handles shadows in the depths of the GI function for performance reasons
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) SHADOW_ATTENUATION(a)
#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS) // no lightmap uv thus store screenPos instead
// can happen if we have two directional lights. main light gets handled in GI code, but 2nd dir light can have shadow screen and mask.
// - Disabled on ES2 because WebGL 1.0 seems to have junk in .w (even though it shouldn't)
# if defined(SHADOWS_SHADOWMASK) && !defined(SHADER_API_GLES)
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
# define UNITY_TRANSFER_SHADOW(a, coord) {a._ShadowCoord.xy = coord * unity_LightmapST.xy + unity_LightmapST.zw; a._ShadowCoord.zw = ComputeScreenPos(a.pos).xy;}
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, float4(a._ShadowCoord.zw, 0.0, UNITY_SHADOW_W(a.pos.w)));
# else
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, a._ShadowCoord)
# endif
#else
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
# if defined(SHADOWS_SHADOWMASK)
# define UNITY_TRANSFER_SHADOW(a, coord) a._ShadowCoord.xy = coord.xy * unity_LightmapST.xy + unity_LightmapST.zw;
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE) || UNITY_LIGHT_PROBE_PROXY_VOLUME)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, 0, 0)
# endif
# else
# if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define UNITY_TRANSFER_SHADOW(a, coord)
# else
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# endif
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE))
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# if UNITY_LIGHT_PROBE_PROXY_VOLUME
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, 0, 0)
# endif
# endif
# endif
#endif
いろいろと分岐がありますがUNITY_SHADOW_COORDS(idx1)
については
次の二択のようです。
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
SHADOW_COORDS(idx1)
の定義についてはAutoLight.cgincにて次のとおりです。
#if defined (SHADOWS_SCREEN)
...
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
...
#endif
// -----------------------------
// Light/Shadow helpers (4.x version)
// -----------------------------
// This version computes light coordinates in the vertex shader and passes them to the fragment shader.
// ---- Spot light shadows
#if defined (SHADOWS_DEPTH) && defined (SPOT)
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
...
#endif
// ---- Point light shadows
#if defined (SHADOWS_CUBE)
#define SHADOW_COORDS(idx1) unityShadowCoord3 _ShadowCoord : TEXCOORD##idx1;
...
#endif
// ---- Shadows off
#if !defined (SHADOWS_SCREEN) && !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE)
#define SHADOW_COORDS(idx1)
...
#endif
UNITY_SHADOW_COORDS(idx1)
はunityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
もしくはunityShadowCoord3 _ShadowCoord : TEXCOORD##idx1;
もしくは空に展開されるということになります。
unityShadowCoord4
はUnityShadowLibrary.cgincで次のように定義されています。
#define unityShadowCoord4 float4
unityShadowCoord3
はUnityShadowLibrary.cgincで次のように定義されています。
#define unityShadowCoord3 float3
UNITY_SHADOW_COORDS(idx1)
はシャドウがオフでない場合にv2fにfloat3
かfloat4
の_ShadowCoord
を定義するものということになります。
頂点シェーダに追加したマクロ
頂点シェーダに追加したマクロについて見ていきます。
まず最初にUNITY_INITIALIZE_OUTPUT(v2f, o);
を見ていきます。
HLSLSupport.cgincで次のように定義されています。
// Initialize arbitrary structure with zero values.
// Not supported on some backends (e.g. Cg-based particularly with nested structs).
// hlsl2glsl would almost support it, except with structs that have arrays -- so treat as not supported there either :(
#if defined(UNITY_COMPILER_HLSL) || defined(SHADER_API_PSSL) || defined(UNITY_COMPILER_HLSLCC)
#define UNITY_INITIALIZE_OUTPUT(type,name) name = (type)0;
#else
#define UNITY_INITIALIZE_OUTPUT(type,name)
#endif
v2f
型のo
を初期化するものです。
次にUNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy);
を見ていきます。
UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy);
AutoLight.cgincで次のように定義されています。
#define UNITY_TRANSFER_LIGHTING(a, coord) COMPUTE_LIGHT_COORDS(a) UNITY_TRANSFER_SHADOW(a, coord)
COMPUTE_LIGHT_COORDS(a)
とUNITY_TRANSFER_SHADOW(a, coord)
を
並べたものに展開されます。
COMPUTE_LIGHT_COORDS
の定義は次のとおりです。
#ifdef POINT
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord3 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xyz;
# define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).r * SHADOW_ATTENUATION(a))
#endif
#ifdef SPOT
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex));
# define LIGHT_ATTENUATION(a) ( (a._LightCoord.z > 0) * UnitySpotCookie(a._LightCoord) * UnitySpotAttenuate(a._LightCoord.xyz) * SHADOW_ATTENUATION(a) )
#endif
#ifdef DIRECTIONAL
# define DECLARE_LIGHT_COORDS(idx)
# define COMPUTE_LIGHT_COORDS(a)
# define LIGHT_ATTENUATION(a) SHADOW_ATTENUATION(a)
#endif
#ifdef POINT_COOKIE
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord3 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xyz;
# define LIGHT_ATTENUATION(a) (tex2D(_LightTextureB0, dot(a._LightCoord,a._LightCoord).rr).r * texCUBE(_LightTexture0, a._LightCoord).w * SHADOW_ATTENUATION(a))
#endif
#ifdef DIRECTIONAL_COOKIE
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord2 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
# define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, a._LightCoord).w * SHADOW_ATTENUATION(a))
#endif
DIRECTIONAL
の場合は空です。
DIRECTIONAL
の場合UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy);
は
UNITY_TRANSFER_SHADOW(a, coord)
に展開されることになります。
UNITY_TRANSFER_SHADOW(a, coord)
については
AutoLight.cgincで次のとおり定義されています。
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI) // handles shadows in the depths of the GI function for performance reasons
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) SHADOW_ATTENUATION(a)
#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS) // no lightmap uv thus store screenPos instead
// can happen if we have two directional lights. main light gets handled in GI code, but 2nd dir light can have shadow screen and mask.
// - Disabled on ES2 because WebGL 1.0 seems to have junk in .w (even though it shouldn't)
# if defined(SHADOWS_SHADOWMASK) && !defined(SHADER_API_GLES)
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
# define UNITY_TRANSFER_SHADOW(a, coord) {a._ShadowCoord.xy = coord * unity_LightmapST.xy + unity_LightmapST.zw; a._ShadowCoord.zw = ComputeScreenPos(a.pos).xy;}
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, float4(a._ShadowCoord.zw, 0.0, UNITY_SHADOW_W(a.pos.w)));
# else
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, a._ShadowCoord)
# endif
#else
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
# if defined(SHADOWS_SHADOWMASK)
# define UNITY_TRANSFER_SHADOW(a, coord) a._ShadowCoord.xy = coord.xy * unity_LightmapST.xy + unity_LightmapST.zw;
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE) || UNITY_LIGHT_PROBE_PROXY_VOLUME)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, 0, 0)
# endif
# else
# if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define UNITY_TRANSFER_SHADOW(a, coord)
# else
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# endif
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE))
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# if UNITY_LIGHT_PROBE_PROXY_VOLUME
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, 0, 0)
# endif
# endif
# endif
#endif
これについては後でフラグメントシェーダとまとめて見ていきます。
フラグメントシェーダに追加したマクロ
次にフラグメントシェーダで使われているマクロについて見てみます。
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
について。
平行光源の場合はAutoLight.cgincで次のように定義されています。
#ifdef DIRECTIONAL
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) fixed destName = UNITY_SHADOW_ATTENUATION(input, worldPos);
#endif
UNITY_SHADOW_ATTENUATION
の結果をfixed
で定義したdestName
に代入しています。
UNITY_SHADOW_ATTENUATION
の定義は先ほどと同じ場所で次のとおり定義されています。
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI) // handles shadows in the depths of the GI function for performance reasons
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) SHADOW_ATTENUATION(a)
#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS) // no lightmap uv thus store screenPos instead
// can happen if we have two directional lights. main light gets handled in GI code, but 2nd dir light can have shadow screen and mask.
// - Disabled on ES2 because WebGL 1.0 seems to have junk in .w (even though it shouldn't)
# if defined(SHADOWS_SHADOWMASK) && !defined(SHADER_API_GLES)
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
# define UNITY_TRANSFER_SHADOW(a, coord) {a._ShadowCoord.xy = coord * unity_LightmapST.xy + unity_LightmapST.zw; a._ShadowCoord.zw = ComputeScreenPos(a.pos).xy;}
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, float4(a._ShadowCoord.zw, 0.0, UNITY_SHADOW_W(a.pos.w)));
# else
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, a._ShadowCoord)
# endif
#else
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
# if defined(SHADOWS_SHADOWMASK)
# define UNITY_TRANSFER_SHADOW(a, coord) a._ShadowCoord.xy = coord.xy * unity_LightmapST.xy + unity_LightmapST.zw;
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE) || UNITY_LIGHT_PROBE_PROXY_VOLUME)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, 0, 0)
# endif
# else
# if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define UNITY_TRANSFER_SHADOW(a, coord)
# else
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# endif
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE))
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# if UNITY_LIGHT_PROBE_PROXY_VOLUME
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, 0, 0)
# endif
# endif
# endif
#endif
この部分のコードについて詳しく見ていきます。
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI)
のとき
まずは最初の分岐から見ていきます。
HANDLE_SHADOWS_BLENDING_IN_GI
について
最初の分岐はHANDLE_SHADOWS_BLENDING_IN_GI
が定義されているかどうかで決まります。
HANDLE_SHADOWS_BLENDING_IN_GI
はUnityShadowLibrary.cgincで
次のように定義されています。
#if defined( SHADOWS_SCREEN ) && defined( LIGHTMAP_ON )
#define HANDLE_SHADOWS_BLENDING_IN_GI 1
#endif
SHADOWS_SCREEN
とLIGHTMAP_ON
は実行時に渡されるキーワードのようです。
いくつか試して確認してみます。
オブジェクトの「Static」をオフにしてライトの「Type」を「Realtime」にすると
SHADOWS_SCREEN
が追加されました。
オブジェクトを「Static」にしてライトの「Type」を「Realtime」にすると
SHADOWS_SCREEN
とLIGHTMAP_ON
が両方追加されました。
オブジェクトを「Static」にしてライトの「Type」を「Baked」にすると
LIGHTMAP_ON
が追加されました。
オブジェクトを「Static」にしてライトの「Type」を「Mixed」にすると
SHADOWS_SCREEN
とLIGHTMAP_ON
が両方追加されました。
オブジェクトが「Static」ではない場合、ライトが「Mixed」でもSHADOWS_SCREEN
のみです。
ただし「Lightmap Static」をオンにするとLIGHTMAP_ON
がオンになりました。
設定の「Graphics」から「Screen Space Shadows」を「No Support」にすると
上記の「SHADOWS_SCREEN
」が追加されていた場合でも追加されなくなりました。
ただし、この場合はリアルタイムの影が描画されないようです。
ついでに加算パスの場合の挙動についても確認しておきます。
平行光源を2つおいてみました。
2つ目の平行光源でも影を有効にします。
Staticなオブジェクトを配置します。
ベースパスの場合は次のとおりです。
加算パスは次のようになっていました。
たとえStaticなオブジェクトであっても
LIGHTMAP_ON
が加算パスには追加されていないことがわかります。
確かにベースパスと加算パスの両方でライトマップを扱ってしまうと
多重にライトマップが適用されることになってしまいますね。
2つ目の平行光源の影を無効にした場合は次のようになりました。
この場合はリアルタイムの影の描画を行わないので
SHADOWS_SCREEN
も追加されていないことがわかります。
まとめると次のようになります。
設定を弄らない限り、デスクトップ上では
ライトが「Realtime」や「Mixed」でリアルタイムの影が描画される場合は
SHADOWS_SCREEN
が追加されます。
Staticなオブジェクトなどのライトマップが作成される場合はLIGHTMAP_ON
が追加されます。
両方の条件が重なる「Realtime」や「Mixed」でStaticなオブジェクトの場合には
両方が追加されます。
また、加算パスの場合はたとえStaticなオブジェクトであってもLIGHTMAP_ON
が追加されません。
HANDLE_SHADOWS_BLENDING_IN_GI
はリアルタイムの影がオンでかつライトマップがオンの場合
オンになるようです。
Surface Shaderから生成したコードでもHANDLE_SHADOWS_BLENDING_IN_GI
を
利用している箇所があります。
Surface Shaderから生成したコードのフラグメントシェーダでLightingStandard_GI(o, giInput, gi);
を呼び出しています。
このLightingStandard_GI
の内部でUnityGI_Base
という関数を呼び出しています。
UnityGI_Base
はUnityGlobalIllumination.cginc
で次のように定義されています。
inline UnityGI UnityGI_Base(UnityGIInput data, half occlusion, half3 normalWorld)
{
UnityGI o_gi;
ResetUnityGI(o_gi);
// Base pass with Lightmap support is responsible for handling ShadowMask / blending here for performance reason
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI)
half bakedAtten = UnitySampleBakedOcclusion(data.lightmapUV.xy, data.worldPos);
float zDist = dot(_WorldSpaceCameraPos - data.worldPos, UNITY_MATRIX_V[2].xyz);
float fadeDist = UnityComputeShadowFadeDistance(data.worldPos, zDist);
data.atten = UnityMixRealtimeAndBakedShadows(data.atten, bakedAtten, UnityComputeShadowFade(fadeDist));
#endif
o_gi.light = data.light;
o_gi.light.color *= data.atten;
#if UNITY_SHOULD_SAMPLE_SH
o_gi.indirect.diffuse = ShadeSHPerPixel(normalWorld, data.ambient, data.worldPos);
#endif
#if defined(LIGHTMAP_ON)
// Baked lightmaps
half4 bakedColorTex = UNITY_SAMPLE_TEX2D(unity_Lightmap, data.lightmapUV.xy);
half3 bakedColor = DecodeLightmap(bakedColorTex);
#ifdef DIRLIGHTMAP_COMBINED
fixed4 bakedDirTex = UNITY_SAMPLE_TEX2D_SAMPLER (unity_LightmapInd, unity_Lightmap, data.lightmapUV.xy);
o_gi.indirect.diffuse += DecodeDirectionalLightmap (bakedColor, bakedDirTex, normalWorld);
#if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN)
ResetUnityLight(o_gi.light);
o_gi.indirect.diffuse = SubtractMainLightWithRealtimeAttenuationFromLightmap (o_gi.indirect.diffuse, data.atten, bakedColorTex, normalWorld);
#endif
#else // not directional lightmap
o_gi.indirect.diffuse += bakedColor;
#if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN)
ResetUnityLight(o_gi.light);
o_gi.indirect.diffuse = SubtractMainLightWithRealtimeAttenuationFromLightmap(o_gi.indirect.diffuse, data.atten, bakedColorTex, normalWorld);
#endif
#endif
#endif
#ifdef DYNAMICLIGHTMAP_ON
// Dynamic lightmaps
fixed4 realtimeColorTex = UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, data.lightmapUV.zw);
half3 realtimeColor = DecodeRealtimeLightmap (realtimeColorTex);
#ifdef DIRLIGHTMAP_COMBINED
half4 realtimeDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_DynamicDirectionality, unity_DynamicLightmap, data.lightmapUV.zw);
o_gi.indirect.diffuse += DecodeDirectionalLightmap (realtimeColor, realtimeDirTex, normalWorld);
#else
o_gi.indirect.diffuse += realtimeColor;
#endif
#endif
o_gi.indirect.diffuse *= occlusion;
return o_gi;
}
最初の部分でHANDLE_SHADOWS_BLENDING_IN_GI
を利用しています。
// Base pass with Lightmap support is responsible for handling ShadowMask / blending here for performance reason
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI)
half bakedAtten = UnitySampleBakedOcclusion(data.lightmapUV.xy, data.worldPos);
float zDist = dot(_WorldSpaceCameraPos - data.worldPos, UNITY_MATRIX_V[2].xyz);
float fadeDist = UnityComputeShadowFadeDistance(data.worldPos, zDist);
data.atten = UnityMixRealtimeAndBakedShadows(data.atten, bakedAtten, UnityComputeShadowFade(fadeDist));
#endif
リアルタイムシャドウのフェードを計算し、 ベイクされたシャドウとリアルタイムのシャドウのブレンドの計算をここで行っているようです。
その分、UNITY_SHADOW_ATTENUATION
などの処理は
フェード処理を省いた簡略化したものになっています。
HANDLE_SHADOWS_BLENDING_IN_GI
のとき
HANDLE_SHADOWS_BLENDING_IN_GI
が定義されている場合には、
最初の分岐に入って次の処理が行われます。
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI) // handles shadows in the depths of the GI function for performance reasons
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) SHADOW_ATTENUATION(a)
#elif
UNITY_SHADOW_COORDS(idx1)
と
UNITY_TRANSFER_SHADOW(a, coord)
、
UNITY_SHADOW_ATTENUATION(a, worldPos)
は
それぞれSHADOW_COORDS(idx1)
とTRANSFER_SHADOW(a)
、
SHADOW_ATTENUATION(a)
に展開されます。
定義は次のとおりです。
// ---- Screen space direction light shadows helpers (any version)
#if defined (SHADOWS_SCREEN)
#if defined(UNITY_NO_SCREENSPACE_SHADOWS)
UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_WorldToShadow[0], mul( unity_ObjectToWorld, 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(_ShadowMapTexture, shadowCoord.xy);
// tegra is confused if we use _LightShadowData.x directly
// with "ambiguous overloaded function reference max(mediump float, float)"
unityShadowCoord lightShadowDataX = _LightShadowData.x;
unityShadowCoord threshold = shadowCoord.z;
return max(dist > threshold, lightShadowDataX);
#endif
}
#else // UNITY_NO_SCREENSPACE_SHADOWS
UNITY_DECLARE_SCREENSPACE_SHADOWMAP(_ShadowMapTexture);
#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
{
fixed shadow = UNITY_SAMPLE_SCREEN_SHADOW(_ShadowMapTexture, shadowCoord);
return shadow;
}
#endif
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
#endif
// -----------------------------
// Light/Shadow helpers (4.x version)
// -----------------------------
// This version computes light coordinates in the vertex shader and passes them to the fragment shader.
// ---- 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_WorldToShadow[0], mul(unity_ObjectToWorld,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.xyz = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz;
#define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord)
#define READ_SHADOW_COORDS(a) unityShadowCoord4(a._ShadowCoord.xyz, 1.0)
#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
#define READ_SHADOW_COORDS(a) 0
#else
#ifndef READ_SHADOW_COORDS
#define READ_SHADOW_COORDS(a) a._ShadowCoord
#endif
#endif
HANDLE_SHADOWS_BLENDING_IN_GI
が定義されている場合は
SHADOWS_SCREEN
が定義されているので最初の方になるようですね。
2つ目の方は気にしなくて良さそうです。
さきほども見ましたが、
SHADOW_COORDS(idx1)
の定義を抜き出すと次のとおりです。
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
与えられた番号のTEXCORRDで_ShadowCoord
をv2f構造体に定義するようです。
unityShadowCoord4
はUnityShadowLibrary.cdgincで次のように定義されています。
#define unityShadowCoord4 float4
TRANSFER_SHADOW(a)
とSHADOW_ATTENUATION(a)
は
UNITY_NO_SCREENSPACE_SHADOWS
で分岐がなされているようです。
UNITY_NO_SCREENSPACE_SHADOWS
については次のページに載っています。
UNITYNOSCREENSPACE_SHADOWS : Defined on platforms that do not use cascaded screenspace shadowmaps (mobile platforms).
モバイルの場合はUNITY_NO_SCREENSPACE_SHADOWS
が定義されるようですね。
順に見ていくことにします。
モバイルの場合は次のようになります。
UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_WorldToShadow[0], mul( unity_ObjectToWorld, 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(_ShadowMapTexture, shadowCoord.xy);
// tegra is confused if we use _LightShadowData.x directly
// with "ambiguous overloaded function reference max(mediump float, float)"
unityShadowCoord lightShadowDataX = _LightShadowData.x;
unityShadowCoord threshold = shadowCoord.z;
return max(dist > threshold, lightShadowDataX);
#endif
}
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
最初にUNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
で
シャドウマップテクスチャを定義しています。
UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
はHLSLSupport.cgincで定義されていて、
プラットフォームに合わせてテクスチャとシャドウ用のサンプラーの定義をしているようです。
UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
の定義の付近を次に抜き出します。
// Macros to declare and sample shadow maps.
//
// UNITY_DECLARE_SHADOWMAP declares a shadowmap.
// UNITY_SAMPLE_SHADOW samples with a float3 coordinate (UV in xy, Z in z) and returns 0..1 scalar result.
// UNITY_SAMPLE_SHADOW_PROJ samples with a projected coordinate (UV and Z divided by w).
#if !defined(SHADER_API_GLES)
// all platforms except GLES2.0 have built-in shadow comparison samplers
#define SHADOWS_NATIVE
#elif defined(SHADER_API_GLES) && defined(UNITY_ENABLE_NATIVE_SHADOW_LOOKUPS)
// GLES2.0 also has built-in shadow comparison samplers, but only on platforms where we pass UNITY_ENABLE_NATIVE_SHADOW_LOOKUPS from the editor
#define SHADOWS_NATIVE
#endif
#if defined(SHADER_API_D3D11) || (defined(UNITY_COMPILER_HLSLCC) && defined(SHADOWS_NATIVE))
// DX11 & hlslcc platforms: built-in PCF
#define UNITY_DECLARE_SHADOWMAP(tex) Texture2D tex; SamplerComparisonState sampler##tex
#define UNITY_DECLARE_TEXCUBE_SHADOWMAP(tex) TextureCube tex; SamplerComparisonState sampler##tex
#define UNITY_SAMPLE_SHADOW(tex,coord) tex.SampleCmpLevelZero (sampler##tex,(coord).xy,(coord).z)
#define UNITY_SAMPLE_SHADOW_PROJ(tex,coord) tex.SampleCmpLevelZero (sampler##tex,(coord).xy/(coord).w,(coord).z/(coord).w)
#if defined(SHADER_API_GLCORE) || defined(SHADER_API_GLES3) || defined(SHADER_API_VULKAN) || defined(SHADER_API_SWITCH)
// GLSL does not have textureLod(samplerCubeShadow, ...) support. GLES2 does not have core support for samplerCubeShadow, so we ignore it.
#define UNITY_SAMPLE_TEXCUBE_SHADOW(tex,coord) tex.SampleCmp (sampler##tex,(coord).xyz,(coord).w)
#else
#define UNITY_SAMPLE_TEXCUBE_SHADOW(tex,coord) tex.SampleCmpLevelZero (sampler##tex,(coord).xyz,(coord).w)
#endif
#elif defined(UNITY_COMPILER_HLSL2GLSL) && defined(SHADOWS_NATIVE)
// OpenGL-like hlsl2glsl platforms: most of them always have built-in PCF
#define UNITY_DECLARE_SHADOWMAP(tex) sampler2DShadow tex
#define UNITY_DECLARE_TEXCUBE_SHADOWMAP(tex) samplerCUBEShadow tex
#define UNITY_SAMPLE_SHADOW(tex,coord) shadow2D (tex,(coord).xyz)
#define UNITY_SAMPLE_SHADOW_PROJ(tex,coord) shadow2Dproj (tex,coord)
#define UNITY_SAMPLE_TEXCUBE_SHADOW(tex,coord) ((texCUBE(tex,(coord).xyz) < (coord).w) ? 0.0 : 1.0)
#elif defined(SHADER_API_PSSL)
// PS4: built-in PCF
#define UNITY_DECLARE_SHADOWMAP(tex) Texture2D tex; SamplerComparisonState sampler##tex
#define UNITY_DECLARE_TEXCUBE_SHADOWMAP(tex) TextureCube tex; SamplerComparisonState sampler##tex
#define UNITY_SAMPLE_SHADOW(tex,coord) tex.SampleCmpLOD0(sampler##tex,(coord).xy,(coord).z)
#define UNITY_SAMPLE_SHADOW_PROJ(tex,coord) tex.SampleCmpLOD0(sampler##tex,(coord).xy/(coord).w,(coord).z/(coord).w)
#define UNITY_SAMPLE_TEXCUBE_SHADOW(tex,coord) tex.SampleCmpLOD0(sampler##tex,(coord).xyz,(coord).w)
#else
// Fallback / No built-in shadowmap comparison sampling: regular texture sample and do manual depth comparison
#define UNITY_DECLARE_SHADOWMAP(tex) sampler2D_float tex
#define UNITY_DECLARE_TEXCUBE_SHADOWMAP(tex) samplerCUBE_float tex
#define UNITY_SAMPLE_SHADOW(tex,coord) ((SAMPLE_DEPTH_TEXTURE(tex,(coord).xy) < (coord).z) ? 0.0 : 1.0)
#define UNITY_SAMPLE_SHADOW_PROJ(tex,coord) ((SAMPLE_DEPTH_TEXTURE_PROJ(tex,UNITY_PROJ_COORD(coord)) < ((coord).z/(coord).w)) ? 0.0 : 1.0)
#define UNITY_SAMPLE_TEXCUBE_SHADOW(tex,coord) ((SAMPLE_DEPTH_CUBE_TEXTURE(tex,(coord).xyz) < (coord).w) ? 0.0 : 1.0)
#endif
プラットフォームごとに4パターンに分かれているようです。 シャドウの比較用のサンプラーがプラットフォームにある場合には それを使うようにしているようです。 後で使うサンプリングの方も一緒に定義されていますね。
たとえばD3D11の場合次のようになります。
#define UNITY_DECLARE_SHADOWMAP(tex) Texture2D tex; SamplerComparisonState sampler##tex
#define UNITY_SAMPLE_SHADOW(tex,coord) tex.SampleCmpLevelZero (sampler##tex,(coord).xy,(coord).z)
tex.SampleCmpLevelZero
はSampleCmp
のミップマップのレベルを0に固定したものです。
次のページに解説があります。
(coord).z
と比較してパスした場合には1でそれ以外の場合には0になります。
このSampleCmp
はPCFも計算してくれます。
PCFはPercentage Closer Filteringの略です。
シャドウマップはその比較して使うという特殊な用途から通常のフィルタリングがうまくいきません。
シャドウマップと比較した結果は結局0と1の2値として返されるためエイリアシングが除去できません。
PCFではフィルタリングの前に比較テストを行い、それからサンプリングします。
このためSampleCmp
が使える環境では通常のサンプリングではなく
こちらを使ったほうが影の質がよくなるようです。
TRANSFER_SHADOW(a)
ではunity_WorldToShadow[0]
を使って頂点をシャドウ用に
変換しているようです。
unity_WorldToShadow
には0から3まであって、それぞれ
カスケードに対応したシャドウ用の変換行列が入っているようなのですが
モバイルの場合は0固定でカスケードには対応していないということなのでしょうか。
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_WorldToShadow[0], mul( unity_ObjectToWorld, v.vertex ) );
SHADOW_ATTENUATION(a)
は次のように定義されています。
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
SHADOW_ATTENUATION(a)
の実態のunitySampleShadow(a._ShadowCoord)
では
SHADOWS_NATIVE
の有無で処理がわかれています。
SHADOWS_NATIVE
の場合は次のとおりです。
#if defined(SHADOWS_NATIVE)
fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord.xyz);
shadow = _LightShadowData.r + shadow * (1-_LightShadowData.r);
return shadow;
#else
UNITY_SAMPLE_SHADOW
は上で見たとおりです。
完全な影の場合0が、完全な日向の場合1が渡されます。
_LightShadowData
は次のページに情報がありました。
_LightShadowData.x - shadow strength
_LightShadowData.y - Appears to be unused
_LightShadowData.z - 1.0 / shadow far distance
_LightShadowData.w - shadow near distance
_LightShadowData.r
はシャドウの強さのようです。
シャドウの強さはエディタ上でライトの設定で行なえます。
shadow = _LightShadowData.r + shadow * (1-_LightShadowData.r);
は
パッと見わかりにくいですが式変形すると次のようになります。
shadow = 1 - ((1 - shadow) * (1-_LightShadowData.r));
この式を見る限り_LightShadowData.r
は0でシャドウが最大で
1でシャドウが最小になっているようです。
エディタの値とは逆です。
サンプルしたシャドウの0から1の値にシャドウの強さを適用しています。
SHADOWS_NATIVE
ではない場合は次のとおりです。
#else
unityShadowCoord dist = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, shadowCoord.xy);
// tegra is confused if we use _LightShadowData.x directly
// with "ambiguous overloaded function reference max(mediump float, float)"
unityShadowCoord lightShadowDataX = _LightShadowData.x;
unityShadowCoord threshold = shadowCoord.z;
return max(dist > threshold, lightShadowDataX);
#endif
デプステクスチャからサンプリングして渡されたshadowCoord
のzと比較しています。
シャドウマップが渡されたzより大きい場合1が、
そうでない場合には_LightShadowData.x
が返されます。
_LightShadowData.r
については上で見たとおりシャドウの強さです。
0でシャドウが最大に、1でシャドウが最小になります。
影の場合はこれをそのまま返せばよいわけですね。
PCFを使わないため0と1の2値になっているので
SHADOWS_NATIVE
のときとは違う計算になっています。
ここまでUNITY_NO_SCREENSPACE_SHADOWS
が定義されているモバイルの場合でした。
モバイルではない場合は次のようになります。
UNITY_DECLARE_SCREENSPACE_SHADOWMAP(_ShadowMapTexture);
#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
{
fixed shadow = UNITY_SAMPLE_SCREEN_SHADOW(_ShadowMapTexture, shadowCoord);
return shadow;
}
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
最初にUNITY_DECLARE_SCREENSPACE_SHADOWMAP(_ShadowMapTexture);
で
スクリーンスペースのシャドウマップを定義しています。
UNITY_DECLARE_SCREENSPACE_SHADOWMAP(_ShadowMapTexture);
はHLSLSupport.cgincで
次のように定義されています。
#if defined(UNITY_STEREO_INSTANCING_ENABLED) || defined(UNITY_STEREO_MULTIVIEW_ENABLED)
#undef UNITY_DECLARE_DEPTH_TEXTURE_MS
#define UNITY_DECLARE_DEPTH_TEXTURE_MS(tex) UNITY_DECLARE_TEX2DARRAY_MS (tex)
#undef UNITY_DECLARE_DEPTH_TEXTURE
#define UNITY_DECLARE_DEPTH_TEXTURE(tex) UNITY_DECLARE_TEX2DARRAY (tex)
#undef SAMPLE_DEPTH_TEXTURE
#define SAMPLE_DEPTH_TEXTURE(sampler, uv) UNITY_SAMPLE_TEX2DARRAY(sampler, float3((uv).x, (uv).y, (float)unity_StereoEyeIndex)).r
#undef SAMPLE_DEPTH_TEXTURE_PROJ
#define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) UNITY_SAMPLE_TEX2DARRAY(sampler, float3((uv).x/(uv).w, (uv).y/(uv).w, (float)unity_StereoEyeIndex)).r
#undef SAMPLE_DEPTH_TEXTURE_LOD
#define SAMPLE_DEPTH_TEXTURE_LOD(sampler, uv) UNITY_SAMPLE_TEX2DARRAY_LOD(sampler, float3((uv).xy, (float)unity_StereoEyeIndex), (uv).w).r
#undef SAMPLE_RAW_DEPTH_TEXTURE
#define SAMPLE_RAW_DEPTH_TEXTURE(tex, uv) UNITY_SAMPLE_TEX2DARRAY(tex, float3((uv).xy, (float)unity_StereoEyeIndex))
#undef SAMPLE_RAW_DEPTH_TEXTURE_PROJ
#define SAMPLE_RAW_DEPTH_TEXTURE_PROJ(sampler, uv) UNITY_SAMPLE_TEX2DARRAY(sampler, float3((uv).x/(uv).w, (uv).y/(uv).w, (float)unity_StereoEyeIndex))
#undef SAMPLE_RAW_DEPTH_TEXTURE_LOD
#define SAMPLE_RAW_DEPTH_TEXTURE_LOD(sampler, uv) UNITY_SAMPLE_TEX2DARRAY_LOD(sampler, float3((uv).xy, (float)unity_StereoEyeIndex), (uv).w)
#define UNITY_DECLARE_SCREENSPACE_SHADOWMAP UNITY_DECLARE_TEX2DARRAY
#define UNITY_SAMPLE_SCREEN_SHADOW(tex, uv) UNITY_SAMPLE_TEX2DARRAY( tex, float3((uv).x/(uv).w, (uv).y/(uv).w, (float)unity_StereoEyeIndex) ).r
#define UNITY_DECLARE_SCREENSPACE_TEXTURE UNITY_DECLARE_TEX2DARRAY
#define UNITY_SAMPLE_SCREENSPACE_TEXTURE(tex, uv) UNITY_SAMPLE_TEX2DARRAY(tex, float3((uv).xy, (float)unity_StereoEyeIndex))
#else
#define UNITY_DECLARE_DEPTH_TEXTURE_MS(tex) Texture2DMS<float> tex;
#define UNITY_DECLARE_DEPTH_TEXTURE(tex) sampler2D_float tex
#define UNITY_DECLARE_SCREENSPACE_SHADOWMAP(tex) sampler2D tex
#define UNITY_SAMPLE_SCREEN_SHADOW(tex, uv) tex2Dproj( tex, UNITY_PROJ_COORD(uv) ).r
#define UNITY_DECLARE_SCREENSPACE_TEXTURE(tex) sampler2D tex;
#define UNITY_SAMPLE_SCREENSPACE_TEXTURE(tex, uv) tex2D(tex, uv)
#endif
VRなどの場合のステレオ描画に対応させているようです。
頂点シェーダのTRANSFER_SHADOW(a)
では
_ShadowCoord
にスクリーンスペースの位置を代入しています。
#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
フラグメントシェーダのSHADOW_ATTENUATION(a)
では
シャドウマップからサンプルしたシャドウを返しています。
inline fixed unitySampleShadow (unityShadowCoord4 shadowCoord)
{
fixed shadow = UNITY_SAMPLE_SCREEN_SHADOW(_ShadowMapTexture, shadowCoord);
return shadow;
}
フレームデバッガを見てみるとShadows.CollectShadowsというパスで スクリーンスペースのシャドウを作っているようです。
平行光源が複数ある場合はその分だけスクリーンスペースシャドウが作られます。
確かにこのスクリーンスペースのシャドウマップを利用すれば ただスクリーンスペースでサンプリングするだけで良さそうです。
スクリーンスペースシャドウ
それではスクリーンスペースのシャドウマップについて詳しく見ていきます。
Shadows.CollectShaadowsパスはデプステクスチャからワールド座標を復元して それをシャドウマップと比較することで影かどうかを判定する処理になっています。
デプステクスチャはレンダリングパスの最初で作られています。 Shadow Casterでバイアスをすべて0にしてdepthをレンダリングしています。
Shadow Casterパスを用意しないと影を受け取る計算の方も正しくできないことになります。
フレームデバッガによるとShadows.CollectShadowsは Hidden/Internal-ScreenSpaceShadowsというシェーダが使われています。
次のページからビルトインシェーダをダウンロードして確認してみます。
DefaultResourcesExtra/内にInternal-ScreenSpaceShadows.shaderがあります。
ちょっと長いですが全文貼ります。
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
// Collects cascaded shadows into screen space buffer
Shader "Hidden/Internal-ScreenSpaceShadows" {
Properties {
_ShadowMapTexture ("", any) = "" {}
_ODSWorldTexture("", 2D) = "" {}
}
CGINCLUDE
UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
float4 _ShadowMapTexture_TexelSize;
#define SHADOWMAPSAMPLER_AND_TEXELSIZE_DEFINED
sampler2D _ODSWorldTexture;
#include "UnityCG.cginc"
#include "UnityShadowLibrary.cginc"
// Configuration
// Should receiver plane bias be used? This estimates receiver slope using derivatives,
// and tries to tilt the PCF kernel along it. However, since we're doing it in screenspace
// from the depth texture, the derivatives are wrong on edges or intersections of objects,
// leading to possible shadow artifacts. So it's disabled by default.
// See also UnityGetReceiverPlaneDepthBias in UnityShadowLibrary.cginc.
//#define UNITY_USE_RECEIVER_PLANE_BIAS
// Blend between shadow cascades to hide the transition seams?
#define UNITY_USE_CASCADE_BLENDING 0
#define UNITY_CASCADE_BLEND_DISTANCE 0.1
struct appdata {
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
#ifdef UNITY_STEREO_INSTANCING_ENABLED
float3 ray0 : TEXCOORD1;
float3 ray1 : TEXCOORD2;
#else
float3 ray : TEXCOORD1;
#endif
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f {
float4 pos : SV_POSITION;
// xy uv / zw screenpos
float4 uv : TEXCOORD0;
// View space ray, for perspective case
float3 ray : TEXCOORD1;
// Orthographic view space positions (need xy as well for oblique matrices)
float3 orthoPosNear : TEXCOORD2;
float3 orthoPosFar : TEXCOORD3;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
v2f vert (appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
float4 clipPos;
#if defined(STEREO_CUBEMAP_RENDER_ON)
clipPos = mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, v.vertex));
#else
clipPos = UnityObjectToClipPos(v.vertex);
#endif
o.pos = clipPos;
o.uv.xy = v.texcoord;
// unity_CameraInvProjection at the PS level.
o.uv.zw = ComputeNonStereoScreenPos(clipPos);
// Perspective case
#ifdef UNITY_STEREO_INSTANCING_ENABLED
o.ray = unity_StereoEyeIndex == 0 ? v.ray0 : v.ray1;
#else
o.ray = v.ray;
#endif
// To compute view space position from Z buffer for orthographic case,
// we need different code than for perspective case. We want to avoid
// doing matrix multiply in the pixel shader: less operations, and less
// constant registers used. Particularly with constant registers, having
// unity_CameraInvProjection in the pixel shader would push the PS over SM2.0
// limits.
clipPos.y *= _ProjectionParams.x;
float3 orthoPosNear = mul(unity_CameraInvProjection, float4(clipPos.x,clipPos.y,-1,1)).xyz;
float3 orthoPosFar = mul(unity_CameraInvProjection, float4(clipPos.x,clipPos.y, 1,1)).xyz;
orthoPosNear.z *= -1;
orthoPosFar.z *= -1;
o.orthoPosNear = orthoPosNear;
o.orthoPosFar = orthoPosFar;
return o;
}
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
// sizes of cascade projections, relative to first one
float4 unity_ShadowCascadeScales;
//
// Keywords based defines
//
#if defined (SHADOWS_SPLIT_SPHERES)
#define GET_CASCADE_WEIGHTS(wpos, z) getCascadeWeights_splitSpheres(wpos)
#else
#define GET_CASCADE_WEIGHTS(wpos, z) getCascadeWeights( wpos, z )
#endif
#if defined (SHADOWS_SINGLE_CASCADE)
#define GET_SHADOW_COORDINATES(wpos,cascadeWeights) getShadowCoord_SingleCascade(wpos)
#else
#define GET_SHADOW_COORDINATES(wpos,cascadeWeights) getShadowCoord(wpos,cascadeWeights)
#endif
/**
* Gets the cascade weights based on the world position of the fragment.
* Returns a float4 with only one component set that corresponds to the appropriate cascade.
*/
inline fixed4 getCascadeWeights(float3 wpos, float z)
{
fixed4 zNear = float4( z >= _LightSplitsNear );
fixed4 zFar = float4( z < _LightSplitsFar );
fixed4 weights = zNear * zFar;
return weights;
}
/**
* Gets the cascade weights based on the world position of the fragment and the poisitions of the split spheres for each cascade.
* Returns a float4 with only one component set that corresponds to the appropriate cascade.
*/
inline fixed4 getCascadeWeights_splitSpheres(float3 wpos)
{
float3 fromCenter0 = wpos.xyz - unity_ShadowSplitSpheres[0].xyz;
float3 fromCenter1 = wpos.xyz - unity_ShadowSplitSpheres[1].xyz;
float3 fromCenter2 = wpos.xyz - unity_ShadowSplitSpheres[2].xyz;
float3 fromCenter3 = wpos.xyz - unity_ShadowSplitSpheres[3].xyz;
float4 distances2 = float4(dot(fromCenter0,fromCenter0), dot(fromCenter1,fromCenter1), dot(fromCenter2,fromCenter2), dot(fromCenter3,fromCenter3));
fixed4 weights = float4(distances2 < unity_ShadowSplitSqRadii);
weights.yzw = saturate(weights.yzw - weights.xyz);
return weights;
}
/**
* Returns the shadowmap coordinates for the given fragment based on the world position and z-depth.
* These coordinates belong to the shadowmap atlas that contains the maps for all cascades.
*/
inline float4 getShadowCoord( float4 wpos, fixed4 cascadeWeights )
{
float3 sc0 = mul (unity_WorldToShadow[0], wpos).xyz;
float3 sc1 = mul (unity_WorldToShadow[1], wpos).xyz;
float3 sc2 = mul (unity_WorldToShadow[2], wpos).xyz;
float3 sc3 = mul (unity_WorldToShadow[3], wpos).xyz;
float4 shadowMapCoordinate = float4(sc0 * cascadeWeights[0] + sc1 * cascadeWeights[1] + sc2 * cascadeWeights[2] + sc3 * cascadeWeights[3], 1);
#if defined(UNITY_REVERSED_Z)
float noCascadeWeights = 1 - dot(cascadeWeights, float4(1, 1, 1, 1));
shadowMapCoordinate.z += noCascadeWeights;
#endif
return shadowMapCoordinate;
}
/**
* Same as the getShadowCoord; but optimized for single cascade
*/
inline float4 getShadowCoord_SingleCascade( float4 wpos )
{
return float4( mul (unity_WorldToShadow[0], wpos).xyz, 0);
}
/**
* Get camera space coord from depth and inv projection matrices
*/
inline float3 computeCameraSpacePosFromDepthAndInvProjMat(v2f i)
{
float zdepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv.xy);
#if defined(UNITY_REVERSED_Z)
zdepth = 1 - zdepth;
#endif
// View position calculation for oblique clipped projection case.
// this will not be as precise nor as fast as the other method
// (which computes it from interpolated ray & depth) but will work
// with funky projections.
float4 clipPos = float4(i.uv.zw, zdepth, 1.0);
clipPos.xyz = 2.0f * clipPos.xyz - 1.0f;
float4 camPos = mul(unity_CameraInvProjection, clipPos);
camPos.xyz /= camPos.w;
camPos.z *= -1;
return camPos.xyz;
}
/**
* Get camera space coord from depth and info from VS
*/
inline float3 computeCameraSpacePosFromDepthAndVSInfo(v2f i)
{
float zdepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv.xy);
// 0..1 linear depth, 0 at camera, 1 at far plane.
float depth = lerp(Linear01Depth(zdepth), zdepth, unity_OrthoParams.w);
#if defined(UNITY_REVERSED_Z)
zdepth = 1 - zdepth;
#endif
// view position calculation for perspective & ortho cases
float3 vposPersp = i.ray * depth;
float3 vposOrtho = lerp(i.orthoPosNear, i.orthoPosFar, zdepth);
// pick the perspective or ortho position as needed
float3 camPos = lerp(vposPersp, vposOrtho, unity_OrthoParams.w);
return camPos.xyz;
}
inline float3 computeCameraSpacePosFromDepth(v2f i);
/**
* Hard shadow
*/
fixed4 frag_hard (v2f i) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); // required for sampling the correct slice of the shadow map render texture array
float4 wpos;
float3 vpos;
#if defined(STEREO_CUBEMAP_RENDER_ON)
wpos.xyz = tex2D(_ODSWorldTexture, i.uv.xy).xyz;
wpos.w = 1.0f;
vpos = mul(unity_WorldToCamera, wpos).xyz;
#else
vpos = computeCameraSpacePosFromDepth(i);
wpos = mul (unity_CameraToWorld, float4(vpos,1));
#endif
fixed4 cascadeWeights = GET_CASCADE_WEIGHTS (wpos, vpos.z);
float4 shadowCoord = GET_SHADOW_COORDINATES(wpos, cascadeWeights);
//1 tap hard shadow
fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord);
shadow = lerp(_LightShadowData.r, 1.0, shadow);
fixed4 res = shadow;
return res;
}
/**
* Soft Shadow (SM 3.0)
*/
fixed4 frag_pcfSoft(v2f i) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); // required for sampling the correct slice of the shadow map render texture array
float4 wpos;
float3 vpos;
#if defined(STEREO_CUBEMAP_RENDER_ON)
wpos.xyz = tex2D(_ODSWorldTexture, i.uv.xy).xyz;
wpos.w = 1.0f;
vpos = mul(unity_WorldToCamera, wpos).xyz;
#else
vpos = computeCameraSpacePosFromDepth(i);
// sample the cascade the pixel belongs to
wpos = mul(unity_CameraToWorld, float4(vpos,1));
#endif
fixed4 cascadeWeights = GET_CASCADE_WEIGHTS(wpos, vpos.z);
float4 coord = GET_SHADOW_COORDINATES(wpos, cascadeWeights);
float3 receiverPlaneDepthBias = 0.0;
#ifdef UNITY_USE_RECEIVER_PLANE_BIAS
// Reveiver plane depth bias: need to calculate it based on shadow coordinate
// as it would be in first cascade; otherwise derivatives
// at cascade boundaries will be all wrong. So compute
// it from cascade 0 UV, and scale based on which cascade we're in.
float3 coordCascade0 = getShadowCoord_SingleCascade(wpos);
float biasMultiply = dot(cascadeWeights,unity_ShadowCascadeScales);
receiverPlaneDepthBias = UnityGetReceiverPlaneDepthBias(coordCascade0.xyz, biasMultiply);
#endif
#if defined(SHADER_API_MOBILE)
half shadow = UnitySampleShadowmap_PCF5x5(coord, receiverPlaneDepthBias);
#else
half shadow = UnitySampleShadowmap_PCF7x7(coord, receiverPlaneDepthBias);
#endif
shadow = lerp(_LightShadowData.r, 1.0f, shadow);
// Blend between shadow cascades if enabled
//
// Not working yet with split spheres, and no need when 1 cascade
#if UNITY_USE_CASCADE_BLENDING && !defined(SHADOWS_SPLIT_SPHERES) && !defined(SHADOWS_SINGLE_CASCADE)
half4 z4 = (float4(vpos.z,vpos.z,vpos.z,vpos.z) - _LightSplitsNear) / (_LightSplitsFar - _LightSplitsNear);
half alpha = dot(z4 * cascadeWeights, half4(1,1,1,1));
UNITY_BRANCH
if (alpha > 1 - UNITY_CASCADE_BLEND_DISTANCE)
{
// get alpha to 0..1 range over the blend distance
alpha = (alpha - (1 - UNITY_CASCADE_BLEND_DISTANCE)) / UNITY_CASCADE_BLEND_DISTANCE;
// sample next cascade
cascadeWeights = fixed4(0, cascadeWeights.xyz);
coord = GET_SHADOW_COORDINATES(wpos, cascadeWeights);
#ifdef UNITY_USE_RECEIVER_PLANE_BIAS
biasMultiply = dot(cascadeWeights,unity_ShadowCascadeScales);
receiverPlaneDepthBias = UnityGetReceiverPlaneDepthBias(coordCascade0.xyz, biasMultiply);
#endif
half shadowNextCascade = UnitySampleShadowmap_PCF3x3(coord, receiverPlaneDepthBias);
shadowNextCascade = lerp(_LightShadowData.r, 1.0f, shadowNextCascade);
shadow = lerp(shadow, shadowNextCascade, alpha);
}
#endif
return shadow;
}
ENDCG
// ----------------------------------------------------------------------------------------
// Subshader for hard shadows:
// Just collect shadows into the buffer. Used on pre-SM3 GPUs and when hard shadows are picked.
SubShader {
Tags{ "ShadowmapFilter" = "HardShadow" }
Pass {
ZWrite Off ZTest Always Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag_hard
#pragma multi_compile_shadowcollector
inline float3 computeCameraSpacePosFromDepth(v2f i)
{
return computeCameraSpacePosFromDepthAndVSInfo(i);
}
ENDCG
}
}
// ----------------------------------------------------------------------------------------
// Subshader for hard shadows:
// Just collect shadows into the buffer. Used on pre-SM3 GPUs and when hard shadows are picked.
// This version does inv projection at the PS level, slower and less precise however more general.
SubShader {
Tags{ "ShadowmapFilter" = "HardShadow_FORCE_INV_PROJECTION_IN_PS" }
Pass{
ZWrite Off ZTest Always Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag_hard
#pragma multi_compile_shadowcollector
inline float3 computeCameraSpacePosFromDepth(v2f i)
{
return computeCameraSpacePosFromDepthAndInvProjMat(i);
}
ENDCG
}
}
// ----------------------------------------------------------------------------------------
// Subshader that does soft PCF filtering while collecting shadows.
// Requires SM3 GPU.
Subshader {
Tags {"ShadowmapFilter" = "PCF_SOFT"}
Pass {
ZWrite Off ZTest Always Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag_pcfSoft
#pragma multi_compile_shadowcollector
#pragma target 3.0
inline float3 computeCameraSpacePosFromDepth(v2f i)
{
return computeCameraSpacePosFromDepthAndVSInfo(i);
}
ENDCG
}
}
// ----------------------------------------------------------------------------------------
// Subshader that does soft PCF filtering while collecting shadows.
// Requires SM3 GPU.
// This version does inv projection at the PS level, slower and less precise however more general.
Subshader{
Tags{ "ShadowmapFilter" = "PCF_SOFT_FORCE_INV_PROJECTION_IN_PS" }
Pass{
ZWrite Off ZTest Always Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag_pcfSoft
#pragma multi_compile_shadowcollector
#pragma target 3.0
inline float3 computeCameraSpacePosFromDepth(v2f i)
{
return computeCameraSpacePosFromDepthAndInvProjMat(i);
}
ENDCG
}
}
Fallback Off
}
最後の4つのSubshaderが本体です。
ソフトな影かハードな影かによってフラグメントシェーダが異なっています。
そしてフラグメントシェーダで呼ばれるcomputeCameraSpacePosFromDepth
の実態が
computeCameraSpacePosFromDepthAndVSInfo
と
computeCameraSpacePosFromDepthAndInvProjMat
の2つ用意されています。
computeCameraSpacePosFromDepth
はprojectionがperspectiveやorthographicなどの
単純な場合に利用される軽量な方法です。
computeCameraSpacePosFromDepthAndInvProjMat
はprojectionが
特殊な行列の場合に利用される方法です。
最初に頂点シェーダの入力を見てみます。
struct appdata {
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
#ifdef UNITY_STEREO_INSTANCING_ENABLED
float3 ray0 : TEXCOORD1;
float3 ray1 : TEXCOORD2;
#else
float3 ray : TEXCOORD1;
#endif
UNITY_VERTEX_INPUT_INSTANCE_ID
};
このScreenSpaceShadowsのパスではスクリーンを覆うquadを生成して描画するようです。 rayというのはそのquadのメッシュの頂点データとしてレイの方向が渡されるようです。
後のフラグメントシェーダでの処理を見てみると rayにはfar平面の四隅へのベクトルが渡されるようです。
UNITY_STEREO_INSTANCING_ENABLED
で分岐しているのはVR向けの場合に
インスタンシングで描画しているとかそういうのだと思います。
以降もインスタンシングの有無で分岐している箇所が多々あります。
次にv2f構造体を見てみます。
struct v2f {
float4 pos : SV_POSITION;
// xy uv / zw screenpos
float4 uv : TEXCOORD0;
// View space ray, for perspective case
float3 ray : TEXCOORD1;
// Orthographic view space positions (need xy as well for oblique matrices)
float3 orthoPosNear : TEXCOORD2;
float3 orthoPosFar : TEXCOORD3;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
書かれているとおりです。
次に頂点シェーダを見てみます。
v2f vert (appdata v)
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
float4 clipPos;
#if defined(STEREO_CUBEMAP_RENDER_ON)
clipPos = mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, v.vertex));
#else
clipPos = UnityObjectToClipPos(v.vertex);
#endif
o.pos = clipPos;
o.uv.xy = v.texcoord;
// unity_CameraInvProjection at the PS level.
o.uv.zw = ComputeNonStereoScreenPos(clipPos);
// Perspective case
#ifdef UNITY_STEREO_INSTANCING_ENABLED
o.ray = unity_StereoEyeIndex == 0 ? v.ray0 : v.ray1;
#else
o.ray = v.ray;
#endif
// To compute view space position from Z buffer for orthographic case,
// we need different code than for perspective case. We want to avoid
// doing matrix multiply in the pixel shader: less operations, and less
// constant registers used. Particularly with constant registers, having
// unity_CameraInvProjection in the pixel shader would push the PS over SM2.0
// limits.
clipPos.y *= _ProjectionParams.x;
float3 orthoPosNear = mul(unity_CameraInvProjection, float4(clipPos.x,clipPos.y,-1,1)).xyz;
float3 orthoPosFar = mul(unity_CameraInvProjection, float4(clipPos.x,clipPos.y, 1,1)).xyz;
orthoPosNear.z *= -1;
orthoPosFar.z *= -1;
o.orthoPosNear = orthoPosNear;
o.orthoPosFar = orthoPosFar;
return o;
}
最初の方でインスタンシングとpos``clipPos``uv
などを計算しています。
ray
はインスタンシングの有無で分岐して頂点属性として渡されてきたレイの方向を
渡しています。
_ProjectionParams.x
はyがフリップした環境では-1が、それ以外では1が渡されています。
orthoPosNear
とorthoPosFar
ではnearとfarのワールド座標の位置を計算しています。
カメラがorthgraphicの場合にはフラグメントシェーダでこれらの値を使って
ワールド座標を復元します。
フラグメントシェーダはハードな影とソフトな影で別のものになります。 まずはハードな影から見ていきます。
/**
* Hard shadow
*/
fixed4 frag_hard (v2f i) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); // required for sampling the correct slice of the shadow map render texture array
float4 wpos;
float3 vpos;
#if defined(STEREO_CUBEMAP_RENDER_ON)
wpos.xyz = tex2D(_ODSWorldTexture, i.uv.xy).xyz;
wpos.w = 1.0f;
vpos = mul(unity_WorldToCamera, wpos).xyz;
#else
vpos = computeCameraSpacePosFromDepth(i);
wpos = mul (unity_CameraToWorld, float4(vpos,1));
#endif
fixed4 cascadeWeights = GET_CASCADE_WEIGHTS (wpos, vpos.z);
float4 shadowCoord = GET_SHADOW_COORDINATES(wpos, cascadeWeights);
//1 tap hard shadow
fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord);
shadow = lerp(_LightShadowData.r, 1.0, shadow);
fixed4 res = shadow;
return res;
}
ステレオキューブマップのレンダリングがオンになっている場合は_ODSWorldTexture
という
テクスチャからワールド座標をサンプリングしているようですね。
ODSというのはUnityで採用されているステレオキューブマップのレンダリング方式のようです。
- How to Use Unity 2018.1 to Capture Stereoscopic 360 Images and Videos | VeeR VR Blog
- rendering-ods-content.pdf
ステレオキューブマップではない通常の場合は
computeCameraSpacePosFromDepth(i);
で
デプステクスチャからカメラスペースの位置を計算していますね。
computeCameraSpacePosFromDepth(v2f i)
は
projectionが通常の場合と特殊な場合で2つに分かれています。
それぞれ頂点シェーダの出力とデプスバッファからカメラスペースの座標の計算をするものと、
projectionの逆行列とデプスバッファからカメラスペースの座標を計算するものです。
頂点シェーダの出力とデプスバッファからカメラスペースの座標を計算する場合は次のとおりです。
inline float3 computeCameraSpacePosFromDepth(v2f i)
{
return computeCameraSpacePosFromDepthAndVSInfo(i);
}
/**
* Get camera space coord from depth and info from VS
*/
inline float3 computeCameraSpacePosFromDepthAndVSInfo(v2f i)
{
float zdepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv.xy);
// 0..1 linear depth, 0 at camera, 1 at far plane.
float depth = lerp(Linear01Depth(zdepth), zdepth, unity_OrthoParams.w);
#if defined(UNITY_REVERSED_Z)
zdepth = 1 - zdepth;
#endif
// view position calculation for perspective & ortho cases
float3 vposPersp = i.ray * depth;
float3 vposOrtho = lerp(i.orthoPosNear, i.orthoPosFar, zdepth);
// pick the perspective or ortho position as needed
float3 camPos = lerp(vposPersp, vposOrtho, unity_OrthoParams.w);
return camPos.xyz;
}
最初にデプステクスチャからサンプリングしていますね。
i.uv.xy
には頂点シェーダでfloat2 texcoord : TEXCOORD0;
が渡されています。
unity_OrthoParams.w
は1.0でカメラがorthographicの場合、
0.0でperspectiveの場合です。
lerpの第三引数にunity_OrthoParams.w
を使うことで、
カメラがorthographicかどうかで第一引数と第二引数のどちらかを採用し代入しています。
perspectiveの場合はzdepthを0から1までの線形にしたものが、
orthgraphicの場合にはもとから線形なのでzdepthをそのまま渡しています。
perspectiveの場合にはLinear01Depth(zdepth)
でzがリバースしている場合の差異を
吸収していますが、orthgraphicの場合は差異を手作業で修正しています。
perspectiveの場合にはrayにdepthをかけています。 処理から見るにrayはfar平面へのベクトルが与えられているようですね。
orthgraphicの場合には頂点シェーダで計算したワールド空間のNearとFarを
zdepth
で線形補間してワールド座標を復元しています。
最後にcamPos
でvposPersp
とvposOrtho
をunity_OrthoParams.w
で切り替えて
渡して返しています。
フラグメントシェーダでプロジェクションの逆行列とデプスバッファから カメラスペースの座標を復元する場合は次のとおりです。
inline float3 computeCameraSpacePosFromDepth(v2f i)
{
return computeCameraSpacePosFromDepthAndInvProjMat(i);
}
/**
* Get camera space coord from depth and inv projection matrices
*/
inline float3 computeCameraSpacePosFromDepthAndInvProjMat(v2f i)
{
float zdepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv.xy);
#if defined(UNITY_REVERSED_Z)
zdepth = 1 - zdepth;
#endif
// View position calculation for oblique clipped projection case.
// this will not be as precise nor as fast as the other method
// (which computes it from interpolated ray & depth) but will work
// with funky projections.
float4 clipPos = float4(i.uv.zw, zdepth, 1.0);
clipPos.xyz = 2.0f * clipPos.xyz - 1.0f;
float4 camPos = mul(unity_CameraInvProjection, clipPos);
camPos.xyz /= camPos.w;
camPos.z *= -1;
return camPos.xyz;
}
頂点シェーダで計算したray
やorthNearPos
、orthFarPos
を使わずに
unity_CameraInvProjection
を使ってデプスからワールド座標を計算しています。
これはrayとdepthを使う方法に比べて遅いけれども
特殊なprojection行列でもうまく動くとのことです。
ここまでcomputeCameraSpacePosFromDepth(i);
の処理でした。
wpos
とvpos
にそれぞれワールドスペースの座標とカメラスペースの座標が計算されました。
次にGET_CASCADE_WEIGHTS (wpos, vpos.z);
です。
シャドウマップはSettingsのQualityでカスケードを設定できます。
カスケードはScene Viewで可視化できます。
Shadow Distanceを短くすると全部のカスケードを見ることもできます。
シャドウのカスケードについては次のページに書かれています。
GET_CASCADE_WEIGHTS (wpos, vpos.z);
はこのカスケードに関する部分になります。
GET_CASCADE_WEIGHTS (wpos, vpos.z);
の定義は次のとおりです。
//
// Keywords based defines
//
#if defined (SHADOWS_SPLIT_SPHERES)
#define GET_CASCADE_WEIGHTS(wpos, z) getCascadeWeights_splitSpheres(wpos)
#else
#define GET_CASCADE_WEIGHTS(wpos, z) getCascadeWeights( wpos, z )
#endif
呼び出される関数を順番に見ていきます。
getCascadeWeights_splitSpheres(wpos)
は次のとおりです。
/**
* Gets the cascade weights based on the world position of the fragment and the poisitions of the split spheres for each cascade.
* Returns a float4 with only one component set that corresponds to the appropriate cascade.
*/
inline fixed4 getCascadeWeights_splitSpheres(float3 wpos)
{
float3 fromCenter0 = wpos.xyz - unity_ShadowSplitSpheres[0].xyz;
float3 fromCenter1 = wpos.xyz - unity_ShadowSplitSpheres[1].xyz;
float3 fromCenter2 = wpos.xyz - unity_ShadowSplitSpheres[2].xyz;
float3 fromCenter3 = wpos.xyz - unity_ShadowSplitSpheres[3].xyz;
float4 distances2 = float4(dot(fromCenter0,fromCenter0), dot(fromCenter1,fromCenter1), dot(fromCenter2,fromCenter2), dot(fromCenter3,fromCenter3));
fixed4 weights = float4(distances2 < unity_ShadowSplitSqRadii);
weights.yzw = saturate(weights.yzw - weights.xyz);
return weights;
}
渡されたワールド座標をもとに対応するカスケードの場所で1が、それ以外で0の fixed4を返しています。
unity_ShadowSplitSpheres[n]
はそれぞれカスケードの球面の中心だと思われます。
distance2
で各中心からの距離の2乗を計算しています。
fixed4 weights = float4(distances2 < unity_ShadowSplitSqRadii);
で
距離の2乗がカスケード距離を超えているかを計算しているようです。
unity_ShadowSplitSqRadii
は4つのカスケード距離の2乗が入っていると思われます。
真理値は0と1として扱われることに注意します。
weights.yzw = saturate(weights.yzw - weights.xyz);
で
ずらして引き算することで、手前のカスケードよりは奥側だが奥のカスケードよりは手前である
カスケードの範囲に入っている場合で1に、それ以外では0になります。
getCascadeWeights( wpos, z )
の定義は次のとおりです。
/**
* Gets the cascade weights based on the world position of the fragment.
* Returns a float4 with only one component set that corresponds to the appropriate cascade.
*/
inline fixed4 getCascadeWeights(float3 wpos, float z)
{
fixed4 zNear = float4( z >= _LightSplitsNear );
fixed4 zFar = float4( z < _LightSplitsFar );
fixed4 weights = zNear * zFar;
return weights;
}
計算したビューのzをもとにカスケードのどの位置に当たるかを計算します。
_LightSplitsNear
と_LightSplitsFar
はそれぞれカスケードの近い方の平面の距離と
遠い方の平面の距離が入っているものと思われます。
zNear
とzFar
の両方の条件を満たすものが対応するカスケードということになります。
カスケードの仕方がカメラに平行な面か球面かによって呼び出されているものが変わっていますが、
注意深く追いかければ
どちらも戻り値のfixed4
のうち対応するカスケードが1に、それ以外が0になるのが分かります。
続いてGET_SHADOW_COORDINATES(wpos,cascadeWeights)
の定義は次のとおりです。
#if defined (SHADOWS_SINGLE_CASCADE)
#define GET_SHADOW_COORDINATES(wpos,cascadeWeights) getShadowCoord_SingleCascade(wpos)
#else
#define GET_SHADOW_COORDINATES(wpos,cascadeWeights) getShadowCoord(wpos,cascadeWeights)
#endif
カスケードしたシャドウマップは一枚を分割して収められています。
GET_SHADOW_COORDINATES(wpos, cascadeWeights);
ではその一枚のシャドウマップから
カスケードの位置をもとにシャドウマップのサンプルする座標を計算しています。
カスケードが1つかどうかでgetShadowCoord_SingleCascade(wpos)
と
getShadowCoord(wpos,cascadeWeights)
を場合分けしています。
それぞれ次のようになっています。
/**
* Returns the shadowmap coordinates for the given fragment based on the world position and z-depth.
* These coordinates belong to the shadowmap atlas that contains the maps for all cascades.
*/
inline float4 getShadowCoord( float4 wpos, fixed4 cascadeWeights )
{
float3 sc0 = mul (unity_WorldToShadow[0], wpos).xyz;
float3 sc1 = mul (unity_WorldToShadow[1], wpos).xyz;
float3 sc2 = mul (unity_WorldToShadow[2], wpos).xyz;
float3 sc3 = mul (unity_WorldToShadow[3], wpos).xyz;
float4 shadowMapCoordinate = float4(sc0 * cascadeWeights[0] + sc1 * cascadeWeights[1] + sc2 * cascadeWeights[2] + sc3 * cascadeWeights[3], 1);
#if defined(UNITY_REVERSED_Z)
float noCascadeWeights = 1 - dot(cascadeWeights, float4(1, 1, 1, 1));
shadowMapCoordinate.z += noCascadeWeights;
#endif
return shadowMapCoordinate;
}
/**
* Same as the getShadowCoord; but optimized for single cascade
*/
inline float4 getShadowCoord_SingleCascade( float4 wpos )
{
return float4( mul (unity_WorldToShadow[0], wpos).xyz, 0);
}
unity_WorldToShadow
はそれぞれワールドからそれぞれのカスケードのシャドウのクリップ空間への
行列だと思われます。
cascadeWeights
は上で説明したとおり適切なカスケードで1が立っているものです。
noCascadeWeights
はどのカスケードにも入り切らなかった場合のものだと思われます。
手に入れたシャドウのcoordをもとにシャドウをサンプリングします。
//1 tap hard shadow
fixed shadow = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, shadowCoord);
UNITY_SAMPLE_SHADOW
については上の方で見ました。
影比較用のサンプラーがある場合はそれを使ってサンプルするというものでした。
最後に手に入れたシャドウを_LightShadowData.r
との間で補間して
最終的なシャドウを求めています。
shadow = lerp(_LightShadowData.r, 1.0, shadow);
fixed4 res = shadow;
return res;
ハードな影についてでした。
次はソフトな影を見ていきます。
ソフトな影には上でも説明したPCFをソフトな影に応用したものを使っています。 PCFを利用したソフトシャドウはPCSS(percentage-closer soft shadow)と呼ばれます。
ソフトな影で使われるフラグメントシェーダは次のとおりです。
/**
* Soft Shadow (SM 3.0)
*/
fixed4 frag_pcfSoft(v2f i) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i); // required for sampling the correct slice of the shadow map render texture array
float4 wpos;
float3 vpos;
#if defined(STEREO_CUBEMAP_RENDER_ON)
wpos.xyz = tex2D(_ODSWorldTexture, i.uv.xy).xyz;
wpos.w = 1.0f;
vpos = mul(unity_WorldToCamera, wpos).xyz;
#else
vpos = computeCameraSpacePosFromDepth(i);
// sample the cascade the pixel belongs to
wpos = mul(unity_CameraToWorld, float4(vpos,1));
#endif
fixed4 cascadeWeights = GET_CASCADE_WEIGHTS(wpos, vpos.z);
float4 coord = GET_SHADOW_COORDINATES(wpos, cascadeWeights);
float3 receiverPlaneDepthBias = 0.0;
#ifdef UNITY_USE_RECEIVER_PLANE_BIAS
// Reveiver plane depth bias: need to calculate it based on shadow coordinate
// as it would be in first cascade; otherwise derivatives
// at cascade boundaries will be all wrong. So compute
// it from cascade 0 UV, and scale based on which cascade we're in.
float3 coordCascade0 = getShadowCoord_SingleCascade(wpos);
float biasMultiply = dot(cascadeWeights,unity_ShadowCascadeScales);
receiverPlaneDepthBias = UnityGetReceiverPlaneDepthBias(coordCascade0.xyz, biasMultiply);
#endif
#if defined(SHADER_API_MOBILE)
half shadow = UnitySampleShadowmap_PCF5x5(coord, receiverPlaneDepthBias);
#else
half shadow = UnitySampleShadowmap_PCF7x7(coord, receiverPlaneDepthBias);
#endif
shadow = lerp(_LightShadowData.r, 1.0f, shadow);
// Blend between shadow cascades if enabled
//
// Not working yet with split spheres, and no need when 1 cascade
#if UNITY_USE_CASCADE_BLENDING && !defined(SHADOWS_SPLIT_SPHERES) && !defined(SHADOWS_SINGLE_CASCADE)
half4 z4 = (float4(vpos.z,vpos.z,vpos.z,vpos.z) - _LightSplitsNear) / (_LightSplitsFar - _LightSplitsNear);
half alpha = dot(z4 * cascadeWeights, half4(1,1,1,1));
UNITY_BRANCH
if (alpha > 1 - UNITY_CASCADE_BLEND_DISTANCE)
{
// get alpha to 0..1 range over the blend distance
alpha = (alpha - (1 - UNITY_CASCADE_BLEND_DISTANCE)) / UNITY_CASCADE_BLEND_DISTANCE;
// sample next cascade
cascadeWeights = fixed4(0, cascadeWeights.xyz);
coord = GET_SHADOW_COORDINATES(wpos, cascadeWeights);
#ifdef UNITY_USE_RECEIVER_PLANE_BIAS
biasMultiply = dot(cascadeWeights,unity_ShadowCascadeScales);
receiverPlaneDepthBias = UnityGetReceiverPlaneDepthBias(coordCascade0.xyz, biasMultiply);
#endif
half shadowNextCascade = UnitySampleShadowmap_PCF3x3(coord, receiverPlaneDepthBias);
shadowNextCascade = lerp(_LightShadowData.r, 1.0f, shadowNextCascade);
shadow = lerp(shadow, shadowNextCascade, alpha);
}
#endif
return shadow;
}
GET_SHADOW_COORDINATES
まではハードな影と同じですね。
その後UNITY_USE_RECEIVER_PLANE_BIAS
の場合receiverPlaneDepthBias
というのを
計算しています。
float3 receiverPlaneDepthBias = 0.0;
#ifdef UNITY_USE_RECEIVER_PLANE_BIAS
// Reveiver plane depth bias: need to calculate it based on shadow coordinate
// as it would be in first cascade; otherwise derivatives
// at cascade boundaries will be all wrong. So compute
// it from cascade 0 UV, and scale based on which cascade we're in.
float3 coordCascade0 = getShadowCoord_SingleCascade(wpos);
float biasMultiply = dot(cascadeWeights,unity_ShadowCascadeScales);
receiverPlaneDepthBias = UnityGetReceiverPlaneDepthBias(coordCascade0.xyz, biasMultiply);
#endif
receiverPlaneDepthBias
についてはUnityGetReceiverPlaneDepthBias
の
定義のところに次のように書かれていました。
// ------------------------------------------------------------------
// Bias
// ------------------------------------------------------------------
/**
* Computes the receiver plane depth bias for the given shadow coord in screen space.
* Inspirations:
* http://mynameismjp.wordpress.com/2013/09/10/shadow-maps/
* http://amd-dev.wpengine.netdna-cdn.com/wordpress/media/2012/10/Isidoro-ShadowMapping.pdf
*/
float3 UnityGetReceiverPlaneDepthBias(float3 shadowCoord, float biasMultiply)
{
// Should receiver plane bias be used? This estimates receiver slope using derivatives,
// and tries to tilt the PCF kernel along it. However, when doing it in screenspace from the depth texture
// (ie all light in deferred and directional light in both forward and deferred), the derivatives are wrong
// on edges or intersections of objects, leading to shadow artifacts. Thus it is disabled by default.
float3 biasUVZ = 0;
#if defined(UNITY_USE_RECEIVER_PLANE_BIAS) && defined(SHADOWMAPSAMPLER_AND_TEXELSIZE_DEFINED)
float3 dx = ddx(shadowCoord);
float3 dy = ddy(shadowCoord);
biasUVZ.x = dy.y * dx.z - dx.y * dy.z;
biasUVZ.y = dx.x * dy.z - dy.x * dx.z;
biasUVZ.xy *= biasMultiply / ((dx.x * dy.y) - (dx.y * dy.x));
// Static depth biasing to make up for incorrect fractional sampling on the shadow map grid.
const float UNITY_RECEIVER_PLANE_MIN_FRACTIONAL_ERROR = 0.01f;
float fractionalSamplingError = dot(_ShadowMapTexture_TexelSize.xy, abs(biasUVZ.xy));
biasUVZ.z = -min(fractionalSamplingError, UNITY_RECEIVER_PLANE_MIN_FRACTIONAL_ERROR);
#if defined(UNITY_REVERSED_Z)
biasUVZ.z *= -1;
#endif
#endif
return biasUVZ;
}
- http://mynameismjp.wordpress.com/2013/09/10/shadow-maps/
- http://amd-dev.wpengine.netdna-cdn.com/wordpress/media/2012/10/Isidoro-ShadowMapping.pdf
後者の資料の図がわかりやすかったので引用します。
ソフトシャドウのためにPCFカーネルが大きくなると単一の中心のdepthと比較するだけでは うまく行かなくなります。 これに対応するものとなります。
ただしコメントにもあるとおり、デプステクスチャから復元したスクリーンスペースでは ddx、ddyの結果がエッジやオブジェクトが交差している点でおかしくなるため このバイアスは0になっているようです。
このバイアスの計算のあとにシャドウのサンプリング処理がなされています。
#if defined(SHADER_API_MOBILE)
half shadow = UnitySampleShadowmap_PCF5x5(coord, receiverPlaneDepthBias);
#else
half shadow = UnitySampleShadowmap_PCF7x7(coord, receiverPlaneDepthBias);
#endif
shadow = lerp(_LightShadowData.r, 1.0f, shadow);
環境によって5x5と7x7を分けているようです。
まずは5x5から見ていきます。 UnityShadowLibrary.cgincに次のように定義されています。
half UnitySampleShadowmap_PCF5x5(float4 coord, float3 receiverPlaneDepthBias)
{
return UnitySampleShadowmap_PCF5x5Tent(coord, receiverPlaneDepthBias);
}
実態はUnitySampleShadowmap_PCF5x5Tent
になるようです。
これもUnityShadowLibrary.cgincに定義されています。
/**
* PCF tent shadowmap filtering based on a 5x5 kernel (optimized with 9 taps)
*/
half UnitySampleShadowmap_PCF5x5Tent(float4 coord, float3 receiverPlaneDepthBias)
{
half shadow = 1;
#ifdef SHADOWMAPSAMPLER_AND_TEXELSIZE_DEFINED
#ifndef SHADOWS_NATIVE
// when we don't have hardware PCF sampling, fallback to a simple 3x3 sampling with averaged results.
return UnitySampleShadowmap_PCF3x3NoHardwareSupport(coord, receiverPlaneDepthBias);
#endif
// tent base is 5x5 base thus covering from 25 to 36 texels, thus we need 9 bilinear PCF fetches
float2 tentCenterInTexelSpace = coord.xy * _ShadowMapTexture_TexelSize.zw;
float2 centerOfFetchesInTexelSpace = floor(tentCenterInTexelSpace + 0.5);
float2 offsetFromTentCenterToCenterOfFetches = tentCenterInTexelSpace - centerOfFetchesInTexelSpace;
// find the weight of each texel based on the area of a 45 degree slop tent above each of them.
float3 texelsWeightsU_A, texelsWeightsU_B;
float3 texelsWeightsV_A, texelsWeightsV_B;
_UnityInternalGetWeightPerTexel_5TexelsWideTriangleFilter(offsetFromTentCenterToCenterOfFetches.x, texelsWeightsU_A, texelsWeightsU_B);
_UnityInternalGetWeightPerTexel_5TexelsWideTriangleFilter(offsetFromTentCenterToCenterOfFetches.y, texelsWeightsV_A, texelsWeightsV_B);
// each fetch will cover a group of 2x2 texels, the weight of each group is the sum of the weights of the texels
float3 fetchesWeightsU = float3(texelsWeightsU_A.xz, texelsWeightsU_B.y) + float3(texelsWeightsU_A.y, texelsWeightsU_B.xz);
float3 fetchesWeightsV = float3(texelsWeightsV_A.xz, texelsWeightsV_B.y) + float3(texelsWeightsV_A.y, texelsWeightsV_B.xz);
// move the PCF bilinear fetches to respect texels weights
float3 fetchesOffsetsU = float3(texelsWeightsU_A.y, texelsWeightsU_B.xz) / fetchesWeightsU.xyz + float3(-2.5,-0.5,1.5);
float3 fetchesOffsetsV = float3(texelsWeightsV_A.y, texelsWeightsV_B.xz) / fetchesWeightsV.xyz + float3(-2.5,-0.5,1.5);
fetchesOffsetsU *= _ShadowMapTexture_TexelSize.xxx;
fetchesOffsetsV *= _ShadowMapTexture_TexelSize.yyy;
// fetch !
float2 bilinearFetchOrigin = centerOfFetchesInTexelSpace * _ShadowMapTexture_TexelSize.xy;
shadow = fetchesWeightsU.x * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.y * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.y, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.z * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.z, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.x * fetchesWeightsV.y * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.y), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.y * fetchesWeightsV.y * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.y, fetchesOffsetsV.y), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.z * fetchesWeightsV.y * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.z, fetchesOffsetsV.y), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.x * fetchesWeightsV.z * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.z), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.y * fetchesWeightsV.z * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.y, fetchesOffsetsV.z), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.z * fetchesWeightsV.z * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.z, fetchesOffsetsV.z), coord.z, receiverPlaneDepthBias));
#endif
return shadow;
}
BilinearのPCFサンプリングを使うことで 9回のテクスチャアクセスで5x5の情報を集めているようです。
まず最初に、PCFサンプリングが実装されていないハードウェアの場合には
UnitySampleShadowmap_PCF3x3NoHardwareSupport(coord, receiverPlaneDepthBias);
の
呼び出しにフォールバックされるようです。
/**
* PCF gaussian shadowmap filtering based on a 3x3 kernel (9 taps no PCF hardware support)
*/
half UnitySampleShadowmap_PCF3x3NoHardwareSupport(float4 coord, float3 receiverPlaneDepthBias)
{
half shadow = 1;
#ifdef SHADOWMAPSAMPLER_AND_TEXELSIZE_DEFINED
// when we don't have hardware PCF sampling, then the above 5x5 optimized PCF really does not work.
// Fallback to a simple 3x3 sampling with averaged results.
float2 base_uv = coord.xy;
float2 ts = _ShadowMapTexture_TexelSize.xy;
shadow = 0;
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(-ts.x, -ts.y), coord.z, receiverPlaneDepthBias));
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(0, -ts.y), coord.z, receiverPlaneDepthBias));
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(ts.x, -ts.y), coord.z, receiverPlaneDepthBias));
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(-ts.x, 0), coord.z, receiverPlaneDepthBias));
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(0, 0), coord.z, receiverPlaneDepthBias));
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(ts.x, 0), coord.z, receiverPlaneDepthBias));
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(-ts.x, ts.y), coord.z, receiverPlaneDepthBias));
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(0, ts.y), coord.z, receiverPlaneDepthBias));
shadow += UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(base_uv, float2(ts.x, ts.y), coord.z, receiverPlaneDepthBias));
shadow /= 9.0;
#endif
return shadow;
}
こちらは愚直に9回テクスチャアクセスをして平均していますね。
UNITY_SAMPLE_SHADOW
は上でも見たとおりzを比較して影ならば0、日向ならば1を返すものです。
比較してからフィルタリングをするということでPCFのわかりやすい例になっています。
7x7の場合も5x5と同じ感じです。 Bilinearを利用して16回のテクスチャアクセスで値を集めるようになっていました。
half UnitySampleShadowmap_PCF7x7(float4 coord, float3 receiverPlaneDepthBias)
{
return UnitySampleShadowmap_PCF7x7Tent(coord, receiverPlaneDepthBias);
}
/**
* PCF tent shadowmap filtering based on a 7x7 kernel (optimized with 16 taps)
*/
half UnitySampleShadowmap_PCF7x7Tent(float4 coord, float3 receiverPlaneDepthBias)
{
half shadow = 1;
#ifdef SHADOWMAPSAMPLER_AND_TEXELSIZE_DEFINED
#ifndef SHADOWS_NATIVE
// when we don't have hardware PCF sampling, fallback to a simple 3x3 sampling with averaged results.
return UnitySampleShadowmap_PCF3x3NoHardwareSupport(coord, receiverPlaneDepthBias);
#endif
// tent base is 7x7 base thus covering from 49 to 64 texels, thus we need 16 bilinear PCF fetches
float2 tentCenterInTexelSpace = coord.xy * _ShadowMapTexture_TexelSize.zw;
float2 centerOfFetchesInTexelSpace = floor(tentCenterInTexelSpace + 0.5);
float2 offsetFromTentCenterToCenterOfFetches = tentCenterInTexelSpace - centerOfFetchesInTexelSpace;
// find the weight of each texel based on the area of a 45 degree slop tent above each of them.
float4 texelsWeightsU_A, texelsWeightsU_B;
float4 texelsWeightsV_A, texelsWeightsV_B;
_UnityInternalGetWeightPerTexel_7TexelsWideTriangleFilter(offsetFromTentCenterToCenterOfFetches.x, texelsWeightsU_A, texelsWeightsU_B);
_UnityInternalGetWeightPerTexel_7TexelsWideTriangleFilter(offsetFromTentCenterToCenterOfFetches.y, texelsWeightsV_A, texelsWeightsV_B);
// each fetch will cover a group of 2x2 texels, the weight of each group is the sum of the weights of the texels
float4 fetchesWeightsU = float4(texelsWeightsU_A.xz, texelsWeightsU_B.xz) + float4(texelsWeightsU_A.yw, texelsWeightsU_B.yw);
float4 fetchesWeightsV = float4(texelsWeightsV_A.xz, texelsWeightsV_B.xz) + float4(texelsWeightsV_A.yw, texelsWeightsV_B.yw);
// move the PCF bilinear fetches to respect texels weights
float4 fetchesOffsetsU = float4(texelsWeightsU_A.yw, texelsWeightsU_B.yw) / fetchesWeightsU.xyzw + float4(-3.5,-1.5,0.5,2.5);
float4 fetchesOffsetsV = float4(texelsWeightsV_A.yw, texelsWeightsV_B.yw) / fetchesWeightsV.xyzw + float4(-3.5,-1.5,0.5,2.5);
fetchesOffsetsU *= _ShadowMapTexture_TexelSize.xxxx;
fetchesOffsetsV *= _ShadowMapTexture_TexelSize.yyyy;
// fetch !
float2 bilinearFetchOrigin = centerOfFetchesInTexelSpace * _ShadowMapTexture_TexelSize.xy;
shadow = fetchesWeightsU.x * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.y * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.y, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.z * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.z, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.w * fetchesWeightsV.x * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.w, fetchesOffsetsV.x), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.x * fetchesWeightsV.y * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.y), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.y * fetchesWeightsV.y * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.y, fetchesOffsetsV.y), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.z * fetchesWeightsV.y * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.z, fetchesOffsetsV.y), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.w * fetchesWeightsV.y * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.w, fetchesOffsetsV.y), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.x * fetchesWeightsV.z * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.z), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.y * fetchesWeightsV.z * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.y, fetchesOffsetsV.z), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.z * fetchesWeightsV.z * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.z, fetchesOffsetsV.z), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.w * fetchesWeightsV.z * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.w, fetchesOffsetsV.z), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.x * fetchesWeightsV.w * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.x, fetchesOffsetsV.w), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.y * fetchesWeightsV.w * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.y, fetchesOffsetsV.w), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.z * fetchesWeightsV.w * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.z, fetchesOffsetsV.w), coord.z, receiverPlaneDepthBias));
shadow += fetchesWeightsU.w * fetchesWeightsV.w * UNITY_SAMPLE_SHADOW(_ShadowMapTexture, UnityCombineShadowcoordComponents(bilinearFetchOrigin, float2(fetchesOffsetsU.w, fetchesOffsetsV.w), coord.z, receiverPlaneDepthBias));
#endif
return shadow;
}
UnitySampleShadowmap_PCF5x5(coord, receiverPlaneDepthBias);
ないし
UnitySampleShadowmap_PCF7x7(coord, receiverPlaneDepthBias);
でシャドウを
サンプリングしたあとにカスケード間のブレンドを行っているようです。
// Blend between shadow cascades if enabled
//
// Not working yet with split spheres, and no need when 1 cascade
#if UNITY_USE_CASCADE_BLENDING && !defined(SHADOWS_SPLIT_SPHERES) && !defined(SHADOWS_SINGLE_CASCADE)
half4 z4 = (float4(vpos.z,vpos.z,vpos.z,vpos.z) - _LightSplitsNear) / (_LightSplitsFar - _LightSplitsNear);
half alpha = dot(z4 * cascadeWeights, half4(1,1,1,1));
UNITY_BRANCH
if (alpha > 1 - UNITY_CASCADE_BLEND_DISTANCE)
{
// get alpha to 0..1 range over the blend distance
alpha = (alpha - (1 - UNITY_CASCADE_BLEND_DISTANCE)) / UNITY_CASCADE_BLEND_DISTANCE;
// sample next cascade
cascadeWeights = fixed4(0, cascadeWeights.xyz);
coord = GET_SHADOW_COORDINATES(wpos, cascadeWeights);
#ifdef UNITY_USE_RECEIVER_PLANE_BIAS
biasMultiply = dot(cascadeWeights,unity_ShadowCascadeScales);
receiverPlaneDepthBias = UnityGetReceiverPlaneDepthBias(coordCascade0.xyz, biasMultiply);
#endif
half shadowNextCascade = UnitySampleShadowmap_PCF3x3(coord, receiverPlaneDepthBias);
shadowNextCascade = lerp(_LightShadowData.r, 1.0f, shadowNextCascade);
shadow = lerp(shadow, shadowNextCascade, alpha);
}
#endif
ブレンドする場合はcascadeWeights = fixed4(0, cascadeWeights.xyz);
で1つずらして
ひとつ先のカスケードについて計算をするようです。
ひとつ先のカスケードは3x3で計算するようですね。
こうして計算した影を返して終わりです。
return shadow;
}
ソフトな影でした。
HANDLE_SHADOWS_BLENDING_IN_GI
のときのまとめ
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI)
の場合についてまとめると
次のとおりです。
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI)
は
リアルタイムの影がオンでライトマップがオンの場合になります。
これはStaticなオブジェクトにリアルタイムなライトがあたったときなどになります。
UNITY_SHADOW_COORDS(idx1)
、UNITY_TRANSFER_SHADOW(a, coord)
、
UNITY_SHADOW_ATTENUATION(a, worldPos)
が
それぞれSHADOW_COORDS(idx1)
とTRANSFER_SHADOW(a)
、
SHADOW_ATTENUATION(a)
に展開されます。
SHADOW_COORDS(idx1)
はv2fにfloat4 _ShadowCoord : TEXCOORD##idx1;
を定義します。
TRANSFER_SHADOW(a)
とSHADOW_ATTENUATION(a)
はさらにプラットフォームがモバイルかそれ以外かで分岐します。
モバイルの場合は普通にシャドウマップからシャドウを計算していました。 シャドウ用の比較サンプラーが定義されているプラットフォームではそれを使うように 場合分けされていました。
モバイルではない場合にはスクリーンスペースのシャドウマップから シャドウを取得する処理になっていました。
スクリーンスペースのシャドウはShadows.CollectShadowsというパスで生成されていました。 このパスで使われているシェーダはHidden/Internal-ScreenSpaceShadowです。 このパスでは画面を覆うquadを描画します。 あらかじめ描画しておいたデプスを使ってワールド座標を復元し、 そこからシャドウマップと比較をしてシャドウを描画していました。
次の分岐も見ていきましょう。
#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS)
のとき
次にHANDLE_SHADOWS_BLENDING_IN_GI
が定義されていない場合を見てみます。
Shadowmaskについて
この#elif
とこの次の#else
の場合はShadowmaskが関係してきます。
まずはShadowmaskについて見ていきます。
ShadowmaskはライトがMixedの場合に利用されるものです。 公式マニュアルは次のとおりです。
また、次のページがわかりやすいです。
Shadowmaskはライトマップと解像度、UVを共有するマップです。 rgbaの各チャンネルにライトが1つずつ、最大4つのライトまで格納されます。 ライトマップのベイクのときに一緒にベイクされるようです。
フレームデバッガで確認できます。 次のようなシーンで確認してみます。
Mixedなライトが2つとStaticなSphere2つ、そしてStaticなPlaneが1つあります。 シャドウマスクを見てみると次のとおりです。
rgbaのチャンネルに各ライトからの影の情報が書き込まれていることがわかります。
Shadowmaskを利用する場合はSHADOWS_SHADOWMASK
キーワードが有効になります。
Shadowmaskには「Shadowmask」と「Distance Shadowmask」の2つのタイプが存在します。 後者のほうがクオリティと負荷が高いものになります。
ShadowmaskではStaticなオブジェクトがStaticなオブジェクトから受ける影は すべてシャドウマスクを利用します。 非Staticなオブジェクトからの影のみシャドウマップのリアルタイムの影を利用します。
次のシーンではShadowmaskモードでshadow distanceを短くして ライトマップの解像度を思いっきり下げています。 赤いオブジェクトは非Staticなオブジェクトです。
非Staticなオブジェクトからの影のみリアルタイムの影でくっきりしているのがわかります。 Staticなオブジェクトの影はベイクされた影を利用しています。 今回はベイクした影の解像度を荒くしているので見た目が残念です。 リアルタイムの影はShadow Distanceが近づくとフェードアウトします。
Distance Shadowmaskの場合、Staticなオブジェクトであっても Shadow Distanceより手前はシャドウマップを利用したリアルタイムの影を使います。 Shadow Distanceより遠くはベイクされたシャドウマスクを利用します。
次のシーンではDistance Shadowmaskモードで shadow distanceを短くしてシャドウマスクの解像度をおもいっきり下げています。 赤いオブジェクトは非Staticなオブジェクトです。
shadow distanceを超えると解像度の低いシャドウマスクのシャドウに ブレンドされて切り替わっているのが確認できます。 ベイクされたシャドウとのブレンドは、 ライトマップのあるStaticなオブジェクトでしか有効になりません。 非Staticなオブジェクトの場合はShadow distance以内をリアルタイムなライトで描画し、 それ以降は影が描画されなくなっています。
非StaticなオブジェクトではMixedライトでもSHADOWS_SHADOWMASK
キーワードは付きません。
次の画像はそれぞれ非Staticなオブジェクトと
Staticなオブジェクトのフレームデバッガです。
Mixedなライトでも必ずしもシャドウマスクを使うとは限りません。 Shadowmaskを利用しない設定があります。 LightingウィンドウからMixed Lightingの「Lighting Mode」を 「Shadowmask」以外にするとシャドウマスクを使わない方法になります。
#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS)
のとき
ソースコードを見ていきます。
#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS) // no lightmap uv thus store screenPos instead
// can happen if we have two directional lights. main light gets handled in GI code, but 2nd dir light can have shadow screen and mask.
// - Disabled on ES2 because WebGL 1.0 seems to have junk in .w (even though it shouldn't)
# if defined(SHADOWS_SHADOWMASK) && !defined(SHADER_API_GLES)
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
# define UNITY_TRANSFER_SHADOW(a, coord) {a._ShadowCoord.xy = coord * unity_LightmapST.xy + unity_LightmapST.zw; a._ShadowCoord.zw = ComputeScreenPos(a.pos).xy;}
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, float4(a._ShadowCoord.zw, 0.0, UNITY_SHADOW_W(a.pos.w)));
# else
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, a._ShadowCoord)
# endif
#else
HANDLE_SHADOWS_BLENDING_IN_GI
が定義されていない場合は、
さらにdefined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS)
で分岐をするようです。
SHADOWS_SCREEN
とLIGHTMAP_ON
、UNITY_NO_SCREENSPACE_SHADOWS
は上で説明しました。
スクリーンスペースのシャドウで、ライトマップがない非Staticのオブジェクト もしくは加算パスのStaticのオブジェクトで、モバイルではない場合この分岐に入ります。
この内部ではさらに分岐しています。
コメントによるとWebGL1.0では.wの問題からSHADOWS_SHADOWMASK
が定義されていても
シャドウマスクを使わないようですね。
最初のWebGL1.0以外でかつシャドウマスクを使う場合と、
それ以外の場合で分岐しています。
どちらの場合でもフラグメントシェーダではhalf UnityComputeForwardShadows(float2 lightmapUV, float3 worldPos, float4 screenPos)
の呼び出しになります。
最初にWebGL1.0以外でかつシャドウマスクを使う場合を見てみます。
v2f構造体で使うUNITY_SHADOW_COORDS(idx1)
は次のとおりです。
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
unityShadowCoord4
は上でも出てきました。
UnityShadowLibrary.cgincで次のように定義されています。
#define unityShadowCoord4 float4
頂点シェーダで使うUNITY_TRANSFER_SHADOW(a, coord)
は次のとおりです。
# define UNITY_TRANSFER_SHADOW(a, coord) {a._ShadowCoord.xy = coord * unity_LightmapST.xy + unity_LightmapST.zw; a._ShadowCoord.zw = ComputeScreenPos(a.pos).xy;}
_ShadowCoord
のzw
にスクリーンスペースのポジションを渡しています。
xy
には渡されたライトマップ用のcoord
にライトマップのST
を使って補正したUVを
格納しています。
フラグメントシェーダのUNITY_SHADOW_ATTENUATION(a, worldPos)
は次のとおりです。
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, float4(a._ShadowCoord.zw, 0.0, UNITY_SHADOW_W(a.pos.w)));
第一引数には頂点シェーダで計算したライトマップのUVを渡しています。
第二引数には渡されたワールドスペースの位置を渡しています。
第三引数には頂点シェーダで計算したスクリーンスペースの座標とa.pos.w
を渡しています。
UNITY_SHADOW_W
はAutoLight.cgincで次のように定義されています。
#if defined(SHADER_API_D3D11) || defined(SHADER_API_D3D12) || defined(SHADER_API_XBOXONE) || defined(SHADER_API_PSSL)
# define UNITY_SHADOW_W(_w) _w
#else
# define UNITY_SHADOW_W(_w) (1.0/_w)
#endif
UnityComputeForwardShadows
の中身は後回しにして、
次にシャドウマスクを使わない場合、もしくはWebGL1.0の場合を見てみます。
v2f構造体で使うUNITY_SHADOW_COORDS(idx1)
はSHADOW_COORDS(idx1)
となります。
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
SHADOW_COORDS(idx1)
は上でも出てきました。
SHADOW_COORDS(idx1)
の定義を抜き出すと次のとおりです。
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
頂点シェーダで使うUNITY_TRANSFER_SHADOW(a, coord)
はTRANSFER_SHADOW(a)
になります。
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
TRANSFER_SHADOW(a)
は上でも出てきました。
UNITY_NO_SCREENSPACE_SHADOWS
ではない場合はスクリーンスペースの位置を返すものでした。
フラグメントシェーダで使うUNITY_SHADOW_ATTENUATION(a, worldPos)
は次のとおりです。
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, a._ShadowCoord)
第一引数のライトマップのUVには0を渡しています。 ライトマップのUVを利用するシャドウマスクを使わないため必要ないということなのでしょう。 第二引数には渡されたワールドスペースの座標を渡しています。 第三引数には頂点シェーダで計算したシャドウの座標が渡されています。
half UnityComputeForwardShadows(float2 lightmapUV, float3 worldPos, float4 screenPos)
UnityComputeForwardShadows
について詳しく見ていきます。
UnityComputeForwardShadows
の定義はAutoLight.cgincにあります。
定義は次のとおりです。
half UnityComputeForwardShadows(float2 lightmapUV, float3 worldPos, float4 screenPos)
{
//fade value
float zDist = dot(_WorldSpaceCameraPos - worldPos, UNITY_MATRIX_V[2].xyz);
float fadeDist = UnityComputeShadowFadeDistance(worldPos, zDist);
half realtimeToBakedShadowFade = UnityComputeShadowFade(fadeDist);
//baked occlusion if any
half shadowMaskAttenuation = UnitySampleBakedOcclusion(lightmapUV, worldPos);
half realtimeShadowAttenuation = 1.0f;
//directional realtime shadow
#if defined (SHADOWS_SCREEN)
#if defined(UNITY_NO_SCREENSPACE_SHADOWS) && !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
realtimeShadowAttenuation = unitySampleShadow(mul(unity_WorldToShadow[0], unityShadowCoord4(worldPos, 1)));
#else
//Only reached when LIGHTMAP_ON is NOT defined (and thus we use interpolator for screenPos rather than lightmap UVs). See HANDLE_SHADOWS_BLENDING_IN_GI below.
realtimeShadowAttenuation = unitySampleShadow(screenPos);
#endif
#endif
#if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT) && !defined(LIGHTMAP_SHADOW_MIXING)
//avoid expensive shadows fetches in the distance where coherency will be good
UNITY_BRANCH
if (realtimeToBakedShadowFade < (1.0f - 1e-2f))
{
#endif
//spot realtime shadow
#if (defined (SHADOWS_DEPTH) && defined (SPOT))
#if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
unityShadowCoord4 spotShadowCoord = mul(unity_WorldToShadow[0], unityShadowCoord4(worldPos, 1));
#else
unityShadowCoord4 spotShadowCoord = screenPos;
#endif
realtimeShadowAttenuation = UnitySampleShadowmap(spotShadowCoord);
#endif
//point realtime shadow
#if defined (SHADOWS_CUBE)
realtimeShadowAttenuation = UnitySampleShadowmap(worldPos - _LightPositionRange.xyz);
#endif
#if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT) && !defined(LIGHTMAP_SHADOW_MIXING)
}
#endif
return UnityMixRealtimeAndBakedShadows(realtimeShadowAttenuation, shadowMaskAttenuation, realtimeToBakedShadowFade);
}
最初にフェードの処理が書かれています。
//fade value
float zDist = dot(_WorldSpaceCameraPos - worldPos, UNITY_MATRIX_V[2].xyz);
float fadeDist = UnityComputeShadowFadeDistance(worldPos, zDist);
half realtimeToBakedShadowFade = UnityComputeShadowFade(fadeDist);
フェードというのは影がShadow Distanceに近づいた場合のフェード処理のことだと思われます。 リアルタイムライトはShadow Distance以内の距離で影を生成します。
試しにShadow Distanceを小さくするとフェードが確認できます。
デフォルトではShadow Distanceは150に設定されていました。
Staticなオブジェクトの場合、 ライトがMixedでDistance Shadowmaskの場合はShadow Distanceより先は ベイクした影になるようです。 非Staticなオブジェクトの場合はShadow Distanceより先は影ができません。
UnityComputeShadowFadeDistance(worldPos, zDist);
と
UnityComputeShadowFade(fadeDist);
は
UnityShadowLibrary.cginc内に次のように定義されています。
float UnityComputeShadowFadeDistance(float3 wpos, float z)
{
float sphereDist = distance(wpos, unity_ShadowFadeCenterAndType.xyz);
return lerp(z, sphereDist, unity_ShadowFadeCenterAndType.w);
}
// ------------------------------------------------------------------
half UnityComputeShadowFade(float fadeDist)
{
return saturate(fadeDist * _LightShadowData.z + _LightShadowData.w);
}
UnityComputeShadowFadeDistance(float3 wpos, float z)
では
unity_ShadowFadeCenterAndType
という変数が使われています。
unity_ShadowFadeCenterAndType
はUnityShaderVariables.cgincで定義されていますが
詳しいことはわかりませんでした。
やっていることと名前から察するに、xyz
はフェードのセンターの位置を与えているようです。
sphereDist
はフェードのセンターからの距離を見ています。
渡されるzはfloat zDist = dot(_WorldSpaceCameraPos - worldPos, UNITY_MATRIX_V[2].xyz);
として計算されています。
w
でカメラに平行な平面での距離zと球面での距離を線形補間する形になっています。
w
はこのタイプに合わせて0か1の値が入ってきそうですね。
UnityComputeShadowFade(float fadeDist)
では
その計算した距離と_LightShadowData
という変数を使って計算しています。
_LightShadowData
という変数については次のページに情報があります。
_LightShadowData.x - shadow strength
_LightShadowData.y - Appears to be unused
_LightShadowData.z - 1.0 / shadow far distance
_LightShadowData.w - shadow near distance
この情報が正しいのかイマイチわかりませんが、
saturate
されているので0~1の値になるようです。
ここまでフェードの処理でした。
フェード処理の後にベイクされた減衰が計算されるようです。
//baked occlusion if any
half shadowMaskAttenuation = UnitySampleBakedOcclusion(lightmapUV, worldPos);
UnitySampleBakedOcclusion(lightmapUV, worldPos);
は
UnityShadowLibrary.cgincで次のように定義されています。
// ------------------------------------------------------------------
// Used by the forward rendering path
fixed UnitySampleBakedOcclusion (float2 lightmapUV, float3 worldPos)
{
#if defined (SHADOWS_SHADOWMASK)
#if defined(LIGHTMAP_ON)
fixed4 rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
#else
fixed4 rawOcclusionMask = fixed4(1.0, 1.0, 1.0, 1.0);
#if UNITY_LIGHT_PROBE_PROXY_VOLUME
if (unity_ProbeVolumeParams.x == 1.0)
rawOcclusionMask = LPPV_SampleProbeOcclusion(worldPos);
else
rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
#else
rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
#endif
#endif
return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));
#else
//In forward dynamic objects can only get baked occlusion from LPPV, light probe occlusion is done on the CPU by attenuating the light color.
fixed atten = 1.0f;
#if defined(UNITY_INSTANCING_ENABLED) && defined(UNITY_USE_SHCOEFFS_ARRAYS)
// ...unless we are doing instancing, and the attenuation is packed into SHC array's .w component.
atten = unity_SHC.w;
#endif
#if UNITY_LIGHT_PROBE_PROXY_VOLUME && !defined(LIGHTMAP_ON) && !UNITY_STANDARD_SIMPLE
fixed4 rawOcclusionMask = atten.xxxx;
if (unity_ProbeVolumeParams.x == 1.0)
rawOcclusionMask = LPPV_SampleProbeOcclusion(worldPos);
return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));
#endif
return atten;
#endif
}
最初にSHADOWS_SHADOWMASK
の有無によって分岐がなされています。
まずはSHADOWS_SHADOWMASK
がある場合から見ていきます。
#if defined (SHADOWS_SHADOWMASK)
#if defined(LIGHTMAP_ON)
fixed4 rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
#else
fixed4 rawOcclusionMask = fixed4(1.0, 1.0, 1.0, 1.0);
#if UNITY_LIGHT_PROBE_PROXY_VOLUME
if (unity_ProbeVolumeParams.x == 1.0)
rawOcclusionMask = LPPV_SampleProbeOcclusion(worldPos);
else
rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
#else
rawOcclusionMask = UNITY_SAMPLE_TEX2D(unity_ShadowMask, lightmapUV.xy);
#endif
#endif
return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));
#else
基本的にベイクされたシャドウマスクから サンプリングしたものが使われるようです。
UNITY_LIGHT_PROBE_PROXY_VOLUME
の場合だけLPPVを使うようです。
LPPVについては次のページがわかりやすいです。
unity_OcclusionMaskSelector
については推測ですが、
現在のライトに対応するところで1が立っているものと思われます。
シャドウマスクには最大4つのライトの情報を4チャンネルに分けて格納します。
必要なチャンネルの値のみを拾って返しているものと思われます。
次にSHADOWS_SHADOWMASK
がない場合を見てみます。
#else
//In forward dynamic objects can only get baked occlusion from LPPV, light probe occlusion is done on the CPU by attenuating the light color.
fixed atten = 1.0f;
#if defined(UNITY_INSTANCING_ENABLED) && defined(UNITY_USE_SHCOEFFS_ARRAYS)
// ...unless we are doing instancing, and the attenuation is packed into SHC array's .w component.
atten = unity_SHC.w;
#endif
#if UNITY_LIGHT_PROBE_PROXY_VOLUME && !defined(LIGHTMAP_ON) && !UNITY_STANDARD_SIMPLE
fixed4 rawOcclusionMask = atten.xxxx;
if (unity_ProbeVolumeParams.x == 1.0)
rawOcclusionMask = LPPV_SampleProbeOcclusion(worldPos);
return saturate(dot(rawOcclusionMask, unity_OcclusionMaskSelector));
#endif
return atten;
#endif
UNITY_USE_SHCOEFFS_ARRAYS
の「SHCOEFFS」は「SH coefficients」でしょうか?
球面調和関数の係数はUnityShaderVariables.cgincで次のように定義されています。
// SH lighting environment
half4 unity_SHAr;
half4 unity_SHAg;
half4 unity_SHAb;
half4 unity_SHBr;
half4 unity_SHBg;
half4 unity_SHBb;
half4 unity_SHC;
また、UNITY_INSTANCING_ENABLE
の場合には
に次のような定義がありました。
////////////////////////////////////////////////////////
// instanced property arrays
#if defined(UNITY_INSTANCING_ENABLED)
UNITY_INSTANCING_BUFFER_START(PerDraw2)
...
#ifdef UNITY_USE_SHCOEFFS_ARRAYS
UNITY_DEFINE_INSTANCED_PROP(half4, unity_SHArArray)
UNITY_DEFINE_INSTANCED_PROP(half4, unity_SHAgArray)
UNITY_DEFINE_INSTANCED_PROP(half4, unity_SHAbArray)
UNITY_DEFINE_INSTANCED_PROP(half4, unity_SHBrArray)
UNITY_DEFINE_INSTANCED_PROP(half4, unity_SHBgArray)
UNITY_DEFINE_INSTANCED_PROP(half4, unity_SHBbArray)
UNITY_DEFINE_INSTANCED_PROP(half4, unity_SHCArray)
#define unity_SHAr UNITY_ACCESS_INSTANCED_PROP(unity_Builtins2, unity_SHArArray)
#define unity_SHAg UNITY_ACCESS_INSTANCED_PROP(unity_Builtins2, unity_SHAgArray)
#define unity_SHAb UNITY_ACCESS_INSTANCED_PROP(unity_Builtins2, unity_SHAbArray)
#define unity_SHBr UNITY_ACCESS_INSTANCED_PROP(unity_Builtins2, unity_SHBrArray)
#define unity_SHBg UNITY_ACCESS_INSTANCED_PROP(unity_Builtins2, unity_SHBgArray)
#define unity_SHBb UNITY_ACCESS_INSTANCED_PROP(unity_Builtins2, unity_SHBbArray)
#define unity_SHC UNITY_ACCESS_INSTANCED_PROP(unity_Builtins2, unity_SHCArray)
#endif
...
#else // UNITY_INSTANCING_ENABLED
これらは球面調和関数から環境光の値を取得するShadeSH9
で使われています。
// normal should be normalized, w=1.0
// output in active color space
half3 ShadeSH9 (half4 normal)
{
// Linear + constant polynomial terms
half3 res = SHEvalLinearL0L1 (normal);
// Quadratic polynomials
res += SHEvalLinearL2 (normal);
# ifdef UNITY_COLORSPACE_GAMMA
res = LinearToGammaSpace (res);
# endif
return res;
}
// normal should be normalized, w=1.0
half3 SHEvalLinearL0L1 (half4 normal)
{
half3 x;
// Linear (L1) + constant (L0) polynomial terms
x.r = dot(unity_SHAr,normal);
x.g = dot(unity_SHAg,normal);
x.b = dot(unity_SHAb,normal);
return x;
}
// normal should be normalized, w=1.0
half3 SHEvalLinearL2 (half4 normal)
{
half3 x1, x2;
// 4 of the quadratic (L2) polynomials
half4 vB = normal.xyzz * normal.yzzx;
x1.r = dot(unity_SHBr,vB);
x1.g = dot(unity_SHBg,vB);
x1.b = dot(unity_SHBb,vB);
// Final (5th) quadratic (L2) polynomial
half vC = normal.x*normal.x - normal.y*normal.y;
x2 = unity_SHC.rgb * vC;
return x1 + x2;
}
インスタンシングを行う場合は計算された遮蔽が球面調和関数の計算用の係数の
unity_SHC.w
に入れられてくることがあるようです。
その後、
UNITY_LIGHT_PROBE_PROXY_VOLUME
があればLPPVから拾った値を使っているようです。
この場合も最大4つのライトについて対応しているチャンネルから拾ってきています。
ここまでベイクされた遮蔽でした。 Light Probeもあるので完全にベイクされたものというわけでもなさそうですが リアルタイムの直接の影ではないものでした。
次にリアルタイムのDirectioinalライトの影について処理しています。
half realtimeShadowAttenuation = 1.0f;
//directional realtime shadow
#if defined (SHADOWS_SCREEN)
#if defined(UNITY_NO_SCREENSPACE_SHADOWS) && !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
realtimeShadowAttenuation = unitySampleShadow(mul(unity_WorldToShadow[0], unityShadowCoord4(worldPos, 1)));
#else
//Only reached when LIGHTMAP_ON is NOT defined (and thus we use interpolator for screenPos rather than lightmap UVs). See HANDLE_SHADOWS_BLENDING_IN_GI below.
realtimeShadowAttenuation = unitySampleShadow(screenPos);
#endif
#endif
unitySampleShadow
については上で出てきました。
SHADOW_ATTENUATION(a)
で使われていたものです。
モバイルの場合は ネイティブのPCFの比較用のサンプラがある場合とない場合で分岐して シャドウを計算していました。 非モバイルの場合はスクリーンスペースのシャドウをサンプリングしていました。
SHADOW_ATTENUATION(a)
でのときと同じように
unitySampleShadow
にわたす座標をUNITY_NO_SCREENSPACE_SHADOWS
の有無で
変えています。
UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
についてはちょっと謎です。
よくわかりませんが、これもシャドウをスクリーンスペースで計算するかどうかに
関係しているようです。
今見ているのは#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS)
のときなのでUNITY_NO_SCREENSPACE_SHADOWS
ではない場合のスクリーンスペースシャドウになります。
UnityComputeForwardShadows
の後半はスポットライトとポイントライトですね。
ベースパスでは平行光源しか扱わないので今は関係ありませんが、
ついでに見ていきましょう。
#if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT) && !defined(LIGHTMAP_SHADOW_MIXING)
//avoid expensive shadows fetches in the distance where coherency will be good
UNITY_BRANCH
if (realtimeToBakedShadowFade < (1.0f - 1e-2f))
{
#endif
//spot realtime shadow
#if (defined (SHADOWS_DEPTH) && defined (SPOT))
#if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
unityShadowCoord4 spotShadowCoord = mul(unity_WorldToShadow[0], unityShadowCoord4(worldPos, 1));
#else
unityShadowCoord4 spotShadowCoord = screenPos;
#endif
realtimeShadowAttenuation = UnitySampleShadowmap(spotShadowCoord);
#endif
//point realtime shadow
#if defined (SHADOWS_CUBE)
realtimeShadowAttenuation = UnitySampleShadowmap(worldPos - _LightPositionRange.xyz);
#endif
#if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT) && !defined(LIGHTMAP_SHADOW_MIXING)
}
#endif
UNITY_BRANCH
についてはHLSLSupport.cgincで次のように定義されています。
// HLSL attributes
#if defined(UNITY_COMPILER_HLSL)
#define UNITY_BRANCH [branch]
#define UNITY_FLATTEN [flatten]
#define UNITY_UNROLL [unroll]
#define UNITY_LOOP [loop]
#define UNITY_FASTOPT [fastopt]
#else
#define UNITY_BRANCH
#define UNITY_FLATTEN
#define UNITY_UNROLL
#define UNITY_LOOP
#define UNITY_FASTOPT
#endif
[branch]
属性については次のページに書かれています。
通常、GPUの条件分岐はCPUの命令ジャンプとは異なり
必要ない方を無視する形のプレディケート実行を利用しています。
[branch]
をつけるとその動作を変更できるようです。
パフォーマンスの為必要に応じて分岐を用意しているということだと思われます。
[branch]
によるオーバーヘッドよりも内部のnopになる命令の数のほうが
多いということなのでしょうか。
分岐の内部では最初にスポットライトの場合が書かれています。
//spot realtime shadow
#if (defined (SHADOWS_DEPTH) && defined (SPOT))
#if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
unityShadowCoord4 spotShadowCoord = mul(unity_WorldToShadow[0], unityShadowCoord4(worldPos, 1));
#else
unityShadowCoord4 spotShadowCoord = screenPos;
#endif
realtimeShadowAttenuation = UnitySampleShadowmap(spotShadowCoord);
#endif
UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
についてはよくわかりませんが
UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
ではない場合は
シャドウ用の座標を、それ以外の場合はスクリーン座標を渡しているようだ。
UnitySampleShadowmap
のスポットライトの場合は
UnityShadowLibrary.cgincで次のとおりです。
// ------------------------------------------------------------------
// Spot light shadows
// ------------------------------------------------------------------
#if defined (SHADOWS_DEPTH) && defined (SPOT)
// declare shadowmap
#if !defined(SHADOWMAPSAMPLER_DEFINED)
UNITY_DECLARE_SHADOWMAP(_ShadowMapTexture);
#define SHADOWMAPSAMPLER_DEFINED
#endif
// shadow sampling offsets and texel size
#if defined (SHADOWS_SOFT)
float4 _ShadowOffsets[4];
float4 _ShadowMapTexture_TexelSize;
#define SHADOWMAPSAMPLER_AND_TEXELSIZE_DEFINED
#endif
inline fixed UnitySampleShadowmap (float4 shadowCoord)
{
#if defined (SHADOWS_SOFT)
half shadow = 1;
// No hardware comparison sampler (ie some mobile + xbox360) : simple 4 tap PCF
#if !defined (SHADOWS_NATIVE)
float3 coord = shadowCoord.xyz / shadowCoord.w;
float4 shadowVals;
shadowVals.x = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[0].xy);
shadowVals.y = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[1].xy);
shadowVals.z = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[2].xy);
shadowVals.w = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[3].xy);
half4 shadows = (shadowVals < coord.zzzz) ? _LightShadowData.rrrr : 1.0f;
shadow = dot(shadows, 0.25f);
#else
// Mobile with comparison sampler : 4-tap linear comparison filter
#if defined(SHADER_API_MOBILE)
float3 coord = shadowCoord.xyz / shadowCoord.w;
half4 shadows;
shadows.x = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[0]);
shadows.y = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[1]);
shadows.z = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[2]);
shadows.w = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[3]);
shadow = dot(shadows, 0.25f);
// Everything else
#else
float3 coord = shadowCoord.xyz / shadowCoord.w;
float3 receiverPlaneDepthBias = UnityGetReceiverPlaneDepthBias(coord, 1.0f);
shadow = UnitySampleShadowmap_PCF3x3(float4(coord, 1), receiverPlaneDepthBias);
#endif
shadow = lerp(_LightShadowData.r, 1.0f, shadow);
#endif
#else
// 1-tap shadows
#if defined (SHADOWS_NATIVE)
half shadow = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, shadowCoord);
shadow = lerp(_LightShadowData.r, 1.0f, shadow);
#else
half shadow = SAMPLE_DEPTH_TEXTURE_PROJ(_ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord)) < (shadowCoord.z / shadowCoord.w) ? _LightShadowData.r : 1.0;
#endif
#endif
return shadow;
}
#endif // #if defined (SHADOWS_DEPTH) && defined (SPOT)
ソフトシャドウの場合とそうでない場合で別になっています。
ソフトシャドウの場合、ネイティブのPCF比較サンプラがある場合とない場合でさらにわかれます。
ネイティブのシャドウ比較用のサンプラが存在しない場合には テクスチャに4回アクセスしてそれをxyzwに詰めて0.25とdotすることで4つの平均をとっています。
ネイティブのシャドウ比較用のサンプラが存在する場合には さらにモバイルかどうかで分岐しているようです。
モバイルではない場合はPCF3x3を利用するようですね。
UnityGetReceiverPlaneDepthBias(coord, 1.0f);
については
上で説明したとおりです。
ハードな影の場合は1回のテクスチャアクセスで計算をしています。 ネイティブにPCFの比較サンプラがある場合とない場合で分岐しています。
次にポイントライトの場合が書かれています。
//point realtime shadow
#if defined (SHADOWS_CUBE)
realtimeShadowAttenuation = UnitySampleShadowmap(worldPos - _LightPositionRange.xyz);
#endif
_LightPositionRange.xyz
は上でも出てきました。
_LightPositionRange
はUnityShaderVariables.cgincで次のように記述されています。
float4 _LightPositionRange; // xyz = pos, w = 1/range
現在のフラグメントのワールド座標からライトの座標を引いたベクトルで サンプルをするようです。
UnitySampleShadowmap
のポイントライトの場合は
UnityShadowLibrary.cgincで次のとおりです。
// ------------------------------------------------------------------
// Point light shadows
// ------------------------------------------------------------------
#if defined (SHADOWS_CUBE)
#if defined(SHADOWS_CUBE_IN_DEPTH_TEX)
UNITY_DECLARE_TEXCUBE_SHADOWMAP(_ShadowMapTexture);
#else
UNITY_DECLARE_TEXCUBE(_ShadowMapTexture);
inline float SampleCubeDistance (float3 vec)
{
return UnityDecodeCubeShadowDepth(UNITY_SAMPLE_TEXCUBE_LOD(_ShadowMapTexture, vec, 0));
}
#endif
inline half UnitySampleShadowmap (float3 vec)
{
#if defined(SHADOWS_CUBE_IN_DEPTH_TEX)
float3 absVec = abs(vec);
float dominantAxis = max(max(absVec.x, absVec.y), absVec.z); // TODO use max3() instead
dominantAxis = max(0.00001, dominantAxis - _LightProjectionParams.z); // shadow bias from point light is apllied here.
dominantAxis *= _LightProjectionParams.w; // bias
float mydist = -_LightProjectionParams.x + _LightProjectionParams.y/dominantAxis; // project to shadow map clip space [0; 1]
#if defined(UNITY_REVERSED_Z)
mydist = 1.0 - mydist; // depth buffers are reversed! Additionally we can move this to CPP code!
#endif
#else
float mydist = length(vec) * _LightPositionRange.w;
mydist *= _LightProjectionParams.w; // bias
#endif
#if defined (SHADOWS_SOFT)
float z = 1.0/128.0;
float4 shadowVals;
// No hardware comparison sampler (ie some mobile + xbox360) : simple 4 tap PCF
#if defined (SHADOWS_CUBE_IN_DEPTH_TEX)
shadowVals.x = UNITY_SAMPLE_TEXCUBE_SHADOW(_ShadowMapTexture, float4(vec+float3( z, z, z), mydist));
shadowVals.y = UNITY_SAMPLE_TEXCUBE_SHADOW(_ShadowMapTexture, float4(vec+float3(-z,-z, z), mydist));
shadowVals.z = UNITY_SAMPLE_TEXCUBE_SHADOW(_ShadowMapTexture, float4(vec+float3(-z, z,-z), mydist));
shadowVals.w = UNITY_SAMPLE_TEXCUBE_SHADOW(_ShadowMapTexture, float4(vec+float3( z,-z,-z), mydist));
half shadow = dot(shadowVals, 0.25);
return lerp(_LightShadowData.r, 1.0, shadow);
#else
shadowVals.x = SampleCubeDistance (vec+float3( z, z, z));
shadowVals.y = SampleCubeDistance (vec+float3(-z,-z, z));
shadowVals.z = SampleCubeDistance (vec+float3(-z, z,-z));
shadowVals.w = SampleCubeDistance (vec+float3( z,-z,-z));
half4 shadows = (shadowVals < mydist.xxxx) ? _LightShadowData.rrrr : 1.0f;
return dot(shadows, 0.25);
#endif
#else
#if defined (SHADOWS_CUBE_IN_DEPTH_TEX)
half shadow = UNITY_SAMPLE_TEXCUBE_SHADOW(_ShadowMapTexture, float4(vec, mydist));
return lerp(_LightShadowData.r, 1.0, shadow);
#else
half shadowVal = UnityDecodeCubeShadowDepth(UNITY_SAMPLE_TEXCUBE(_ShadowMapTexture, vec));
half shadow = shadowVal < mydist ? _LightShadowData.r : 1.0;
return shadow;
#endif
#endif
}
#endif // #if defined (SHADOWS_CUBE)
最後にUnityMixRealtimeAndBakedShadows
したものを返しています。
return UnityMixRealtimeAndBakedShadows(realtimeShadowAttenuation, shadowMaskAttenuation, realtimeToBakedShadowFade);
UnityMixRealtimeAndBakedShadows
はUnityShadowLibrary.cgincで
次のように定義されています。
// ------------------------------------------------------------------
// Used by both the forward and the deferred rendering path
half UnityMixRealtimeAndBakedShadows(half realtimeShadowAttenuation, half bakedShadowAttenuation, half fade)
{
// -- Static objects --
// FWD BASE PASS
// ShadowMask mode = LIGHTMAP_ON + SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
// Distance shadowmask mode = LIGHTMAP_ON + SHADOWS_SHADOWMASK
// Subtractive mode = LIGHTMAP_ON + LIGHTMAP_SHADOW_MIXING
// Pure realtime direct lit = LIGHTMAP_ON
// FWD ADD PASS
// ShadowMask mode = SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
// Distance shadowmask mode = SHADOWS_SHADOWMASK
// Pure realtime direct lit = LIGHTMAP_ON
// DEFERRED LIGHTING PASS
// ShadowMask mode = LIGHTMAP_ON + SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
// Distance shadowmask mode = LIGHTMAP_ON + SHADOWS_SHADOWMASK
// Pure realtime direct lit = LIGHTMAP_ON
// -- Dynamic objects --
// FWD BASE PASS + FWD ADD ASS
// ShadowMask mode = LIGHTMAP_SHADOW_MIXING
// Distance shadowmask mode = N/A
// Subtractive mode = LIGHTMAP_SHADOW_MIXING (only matter for LPPV. Light probes occlusion being done on CPU)
// Pure realtime direct lit = N/A
// DEFERRED LIGHTING PASS
// ShadowMask mode = SHADOWS_SHADOWMASK + LIGHTMAP_SHADOW_MIXING
// Distance shadowmask mode = SHADOWS_SHADOWMASK
// Pure realtime direct lit = N/A
#if !defined(SHADOWS_DEPTH) && !defined(SHADOWS_SCREEN) && !defined(SHADOWS_CUBE)
#if defined(LIGHTMAP_ON) && defined (LIGHTMAP_SHADOW_MIXING) && !defined (SHADOWS_SHADOWMASK)
//In subtractive mode when there is no shadow we kill the light contribution as direct as been baked in the lightmap.
return 0.0;
#else
return bakedShadowAttenuation;
#endif
#endif
#if (SHADER_TARGET <= 20) || UNITY_STANDARD_SIMPLE
//no fading nor blending on SM 2.0 because of instruction count limit.
#if defined(SHADOWS_SHADOWMASK) || defined(LIGHTMAP_SHADOW_MIXING)
return min(realtimeShadowAttenuation, bakedShadowAttenuation);
#else
return realtimeShadowAttenuation;
#endif
#endif
#if defined(LIGHTMAP_SHADOW_MIXING)
//Subtractive or shadowmask mode
realtimeShadowAttenuation = saturate(realtimeShadowAttenuation + fade);
return min(realtimeShadowAttenuation, bakedShadowAttenuation);
#endif
//In distance shadowmask or realtime shadow fadeout we lerp toward the baked shadows (bakedShadowAttenuation will be 1 if no baked shadows)
return lerp(realtimeShadowAttenuation, bakedShadowAttenuation, fade);
}
条件に応じてリアルタイムの影とブレンドした影を適当にブレンドして返しています。 リアルタイムとベイクの影を実際にlerpでブレンドするのは最後の場合の 「Distance shadowmask mode」の場合だけのようです。
#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS)
のときのまとめ
#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS)
のときについてまとめると次のとおりです。
スクリーンスペースのシャドウで、ライトマップがない非Staticのオブジェクト もしくは加算パスのStaticのオブジェクトで、モバイルではない場合この分岐に入ります。
Mixedなライティングでシャドウマスクを使う場合とそれ以外の場合で分岐していました。 WebGL 1.0だけは強制的にシャドウマスクを使わないようにしていました。
UNITY_SHADOW_COORDS(idx1)
ではv2fにfloat4
の_ShadowCoord
を
定義していました。
頂点シェーダで使うUNITY_TRANSFER_SHADOW(a, coord)
は
シャドウマスクを使う場合とそうでない場合で別の処理になりました。
シャドウマスクを使う場合はxy
にライトマップのUV座標をzw
にスクリーンスペースの座標を
詰めていました。
シャドウマスクを使わない場合にはxy
にスクリーンスペースの座標を詰めていました。
UNITY_SHADOW_ATTENUATION(a, worldPos)
は
シャドウマスクを使う場合と使わない場合のどちらでも
UnityComputeForwardShadows
の呼び出しに変わりました。
シャドウマスクを使わない場合にはライトマップのUVに0が渡されていました。
UnityComputeForwardShadows
の内部では最初にフェード処理が書かれていました。
Shadow Distanceに近づいた際にStaticなオブジェクトではベイクされた影と切り替わり、
非Staticなオブジェクトでは影がフェードアウトする部分の処理です。
その次にリアルタイムの直接の影以外のベイクされたものやLPPVの 影を計算しました。
次にリアルタイムのシャドウを計算していました。
平行光源の場合にはunitySampleShadow
を利用していました。
これはUNITY_NO_SCREENSPACE_SHADOWS
でない場合には
スクリーンスペースのシャドウさサンプルするものでした。
スポットライトとポイントライトの場合もそれぞれ適切にシャドウを計算しています。
最後に計算したベイクされたシャドウとリアルタイムのシャドウを モードに合わせてフェードの値で適切にブレンドしていました。
#else
最後に#else
の場合を見ていく。
#else
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
# if defined(SHADOWS_SHADOWMASK)
# define UNITY_TRANSFER_SHADOW(a, coord) a._ShadowCoord.xy = coord.xy * unity_LightmapST.xy + unity_LightmapST.zw;
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE) || UNITY_LIGHT_PROBE_PROXY_VOLUME)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, 0, 0)
# endif
# else
# if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define UNITY_TRANSFER_SHADOW(a, coord)
# else
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# endif
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE))
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# if UNITY_LIGHT_PROBE_PROXY_VOLUME
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, 0, 0)
# endif
# endif
# endif
#endif
最初にUNITY_SHADOW_COORDS(idx1)
が定義されています。
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
unityShadowCoord4
は上でも出てきました。
UnityShadowLibrary.cgincで次のように定義されています。
#define unityShadowCoord4 float4
次にSHADOWS_SHADOWMASK
の有無で分岐されています。
SHADOWS_SHADOWMASK
が定義されている場合から見ていきます。
# if defined(SHADOWS_SHADOWMASK)
# define UNITY_TRANSFER_SHADOW(a, coord) a._ShadowCoord.xy = coord.xy * unity_LightmapST.xy + unity_LightmapST.zw;
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE) || UNITY_LIGHT_PROBE_PROXY_VOLUME)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, 0, 0)
# endif
# else
頂点シェーダではライトマップのUVを計算しています。
define UNITY_TRANSFER_SHADOW(a, coord) a._ShadowCoord.xy = coord.xy * unity_LightmapST.xy + unity_LightmapST.zw;
フラグメントシェーダではさらに場合分けがあります。 リアルタイムに計算する影の要素があるのか、完全にベイクされたものだけでよいのかで 場合分けされているように見えます。
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE) || UNITY_LIGHT_PROBE_PROXY_VOLUME)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, 0, 0)
# endif
UNITY_READ_SHADOW_COORDS
は次のとおりです。
#if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define UNITY_READ_SHADOW_COORDS(input) 0
#else
# define UNITY_READ_SHADOW_COORDS(input) READ_SHADOW_COORDS(input)
#endif
UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
ではない場合は常に0を返すようです。
READ_SHADOW_COORDS
は次のとおりです。
// -----------------------------
// Light/Shadow helpers (4.x version)
// -----------------------------
// This version computes light coordinates in the vertex shader and passes them to the fragment shader.
...
// ---- Point light shadows
#if defined (SHADOWS_CUBE)
...
#define READ_SHADOW_COORDS(a) unityShadowCoord4(a._ShadowCoord.xyz, 1.0)
#endif
// ---- Shadows off
#if !defined (SHADOWS_SCREEN) && !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE)
...
#define READ_SHADOW_COORDS(a) 0
#else
#ifndef READ_SHADOW_COORDS
#define READ_SHADOW_COORDS(a) a._ShadowCoord
#endif
#endif
unityShadowCoord4(a._ShadowCoord.xyz, 1.0)
か0か
a._ShadowCoord
になるかのようですね。
unityShadowCoord4
はUnityShadowLibrary.cgincで次のとおりです。
#define unityShadowCoord4 float4
a._ShadowCoord
はスクリーンスペースの位置ではなくて
ライトマップのUVになる気がするのですが。
スクリーンスペースの位置を渡すべきUnityComputeForwardShadows
の第三引数に
a._ShadowCoord
が渡される可能性があるのは謎です。
SHADOWS_SHADOWMASK
が定義されていない場合を見ていきます。
# else
# if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define UNITY_TRANSFER_SHADOW(a, coord)
# else
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# endif
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE))
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# if UNITY_LIGHT_PROBE_PROXY_VOLUME
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, 0, 0)
# endif
# endif
# endif
シャドウマスクを使わないためUnityComputeForwardShadows
の第一引数の
ライトマップのUVは常に0が渡されています。
UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
ではない場合は
UNITY_TRANSFER_SHADOW(a, coord)
は常に空です。
UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
の場合には
TRANSFER_SHADOW(a)
になります。
フラグメントシェーダについて、
リアルタイムの影、もしくはLPPVがある場合は、UnityComputeForwardShadows
にワールド座標と
UNITY_READ_SHADOW_COORDS(a)
を渡しています。
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE))
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# if UNITY_LIGHT_PROBE_PROXY_VOLUME
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
...
# endif
# endif
リアルタイムに計算するものがない場合にはすべての引数が0で呼ばれています。
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, 0, 0)
# endif
非Staticのオブジェクトで、このライトでは影を落とさない設定になっていた場合に 呼ばれるものと思われます。
長くなりましたが、ベースパスで追加したマクロの平行光源の場合についてでした。
加算パスを追加する
これまでFowardBaseでの平行光源を1つを扱ってきました。 次にForwardAddで加算される3つのライトを追加します。 このパスでは平行光源と点光源、スポットライトを扱うことになります。
加算パスのプログラムは次のとおりです。
Pass
{
Tags { "LightMode"="ForwardAdd"}
ZWrite Off
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd_fullshadows
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
float2 texcoord1: TEXCOORD1;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
UNITY_LIGHTING_COORDS(3,4)
};
sampler2D _MainTex;
float4 _MainTex_ST;
void vert (in appdata v, out v2f o)
{
UNITY_INITIALIZE_OUTPUT(v2f, o);
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy);
}
void frag (in v2f i, out fixed4 col : SV_Target)
{
#ifndef USING_DIRECTIONAL_LIGHT
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
#else
fixed3 lightDir = _WorldSpaceLightPos0.xyz;
#endif
float3 normal = normalize(i.worldNormal);
float NL = dot(normal, lightDir);
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
float3 baseColor = tex2D(_MainTex, i.uv);
float3 lightColor = _LightColor0;
col = fixed4(baseColor * lightColor * max(NL, 0) * attenuation, 0);
}
ENDCG
}
あたらしいパスを追加してTags{"LightMode"="ForwardAdd"}
を与えているのがわかります。
このタグを与えたパスがForwardの加算パスとして実行されます。
加算パスではZテストをオフにしてブレンドモードを加算にしています。
ZWrite Off
Blend One One
#pragma multi_compile_fwdadd_fullshadows
を追加します。
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd_fullshadows
Lighting.cgincとAutoLight.cgincをインクルードします。
#include "Lighting.cginc"
#include "UnityCG.cginc"
#include "AutoLight.cginc"
v2f構造体にUNITY_LIGHTING_COORDS(3,4)
を加えます。
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
UNITY_LIGHTING_COORDS(3,4)
};
頂点シェーダは次のとおりです。
void vert (in appdata v, out v2f o)
{
UNITY_INITIALIZE_OUTPUT(v2f, o);
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy);
}
UNITY_INITIALIZE_OUTPUT(v2f, o);
で初期化しています。
UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy);
を使っています。
フラグメントシェーダは次のとおりです。
void frag (in v2f i, out fixed4 col : SV_Target)
{
#ifndef USING_DIRECTIONAL_LIGHT
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
#else
fixed3 lightDir = _WorldSpaceLightPos0.xyz;
#endif
float3 normal = normalize(i.worldNormal);
float NL = dot(normal, lightDir);
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
float3 baseColor = tex2D(_MainTex, i.uv);
float3 lightColor = _LightColor0;
col = fixed4(baseColor * lightColor * max(NL, 0) * attenuation, 0);
}
次の部分でライトの方向を取得しています。
#ifndef USING_DIRECTIONAL_LIGHT
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
#else
fixed3 lightDir = _WorldSpaceLightPos0.xyz;
#endif
次の部分でライトの減衰を計算しています。
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
これで4つのライトにまで対応できました。
複数のポイントライトやスポットライトに対応しているのも確認できます。
フレームデバッガを見てみると加算パスが加算されているのがわかります。
マクロの中身
使っているマクロはベースパスと同じです。 ベースパスでは平行光源の場合のみ追いかけたので、 ここでは他の場合についてを見てみることにします。
UNITY_LIGHTING_COORDS(idx1, idx2)
AutoLight.cgincで次のように定義されています。
#define UNITY_LIGHTING_COORDS(idx1, idx2) DECLARE_LIGHT_COORDS(idx1) UNITY_SHADOW_COORDS(idx2)
UNITY_LIGHTING_COORDS(idx1, idx2)
は
DECLARE_LIGHT_COORDS(idx1)
とUNITY_SHADOW_COORDS(idx2)
を並べたものになります。
順に見ていきます。
DECLARE_LIGHT_COORDS(idx1)
#ifdef POINT
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord3 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xyz;
# define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).r * SHADOW_ATTENUATION(a))
#endif
#ifdef SPOT
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex));
# define LIGHT_ATTENUATION(a) ( (a._LightCoord.z > 0) * UnitySpotCookie(a._LightCoord) * UnitySpotAttenuate(a._LightCoord.xyz) * SHADOW_ATTENUATION(a) )
#endif
#ifdef DIRECTIONAL
# define DECLARE_LIGHT_COORDS(idx)
# define COMPUTE_LIGHT_COORDS(a)
# define LIGHT_ATTENUATION(a) SHADOW_ATTENUATION(a)
#endif
#ifdef POINT_COOKIE
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord3 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xyz;
# define LIGHT_ATTENUATION(a) (tex2D(_LightTextureB0, dot(a._LightCoord,a._LightCoord).rr).r * texCUBE(_LightTexture0, a._LightCoord).w * SHADOW_ATTENUATION(a))
#endif
#ifdef DIRECTIONAL_COOKIE
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord2 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
# define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, a._LightCoord).w * SHADOW_ATTENUATION(a))
#endif
unityShadowCoord4
、unityShadowCoord4
、unityShadowCoord4
については
UnityShadowLibrary.cgincで次のように定義されています。
#define unityShadowCoord float
#define unityShadowCoord2 float2
#define unityShadowCoord3 float3
#define unityShadowCoord4 float4
平行光源のときは空でした。
それ以外の場合はfloat2
またはfloat3
、float4
で_LightCoord
というものを
v2fに定義するものになります。
UNITY_SHADOW_COORDS(idx2)
これについては上で説明しました。
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI) // handles shadows in the depths of the GI function for performance reasons
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) SHADOW_ATTENUATION(a)
#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS) // no lightmap uv thus store screenPos instead
// can happen if we have two directional lights. main light gets handled in GI code, but 2nd dir light can have shadow screen and mask.
// - Disabled on ES2 because WebGL 1.0 seems to have junk in .w (even though it shouldn't)
# if defined(SHADOWS_SHADOWMASK) && !defined(SHADER_API_GLES)
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
# define UNITY_TRANSFER_SHADOW(a, coord) {a._ShadowCoord.xy = coord * unity_LightmapST.xy + unity_LightmapST.zw; a._ShadowCoord.zw = ComputeScreenPos(a.pos).xy;}
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, float4(a._ShadowCoord.zw, 0.0, UNITY_SHADOW_W(a.pos.w)));
# else
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, a._ShadowCoord)
# endif
#else
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
# if defined(SHADOWS_SHADOWMASK)
# define UNITY_TRANSFER_SHADOW(a, coord) a._ShadowCoord.xy = coord.xy * unity_LightmapST.xy + unity_LightmapST.zw;
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE) || UNITY_LIGHT_PROBE_PROXY_VOLUME)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, 0, 0)
# endif
# else
# if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define UNITY_TRANSFER_SHADOW(a, coord)
# else
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# endif
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE))
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# if UNITY_LIGHT_PROBE_PROXY_VOLUME
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, 0, 0)
# endif
# endif
# endif
#endif
UNITY_TRANSFER_LIGHTING(a, coord)
AutoLight.cgincで次のように定義されています。
#define UNITY_TRANSFER_LIGHTING(a, coord) COMPUTE_LIGHT_COORDS(a) UNITY_TRANSFER_SHADOW(a, coord)
UNITY_TRANSFER_LIGHTING(a, coord)
は
COMPUTE_LIGHT_COORDS(a)
とUNITY_TRANSFER_SHADOW(a, coord)
を
並べたものになります。
COMPUTE_LIGHT_COORDS(a)
AutoLight.cgincで次のように定義されています。
#ifdef POINT
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord3 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xyz;
# define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).r * SHADOW_ATTENUATION(a))
#endif
#ifdef SPOT
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord4 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex));
# define LIGHT_ATTENUATION(a) ( (a._LightCoord.z > 0) * UnitySpotCookie(a._LightCoord) * UnitySpotAttenuate(a._LightCoord.xyz) * SHADOW_ATTENUATION(a) )
#endif
#ifdef DIRECTIONAL
# define DECLARE_LIGHT_COORDS(idx)
# define COMPUTE_LIGHT_COORDS(a)
# define LIGHT_ATTENUATION(a) SHADOW_ATTENUATION(a)
#endif
#ifdef POINT_COOKIE
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord3 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xyz;
# define LIGHT_ATTENUATION(a) (tex2D(_LightTextureB0, dot(a._LightCoord,a._LightCoord).rr).r * texCUBE(_LightTexture0, a._LightCoord).w * SHADOW_ATTENUATION(a))
#endif
#ifdef DIRECTIONAL_COOKIE
# define DECLARE_LIGHT_COORDS(idx) unityShadowCoord2 _LightCoord : TEXCOORD##idx;
# define COMPUTE_LIGHT_COORDS(a) a._LightCoord = mul(unity_WorldToLight, mul(unity_ObjectToWorld, v.vertex)).xy;
# define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, a._LightCoord).w * SHADOW_ATTENUATION(a))
#endif
平行光源のときは空でした。
それ以外の場合はunity_WorldToLight
を使ってライトから見た座標を計算して
a._LightCoord
に代入しているようです。
UNITY_TRANSFER_SHADOW(a, coord)
UNITY_TRANSFER_SHADOW(a, coord)
については上で説明しました。
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI) // handles shadows in the depths of the GI function for performance reasons
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) SHADOW_ATTENUATION(a)
#elif defined(SHADOWS_SCREEN) && !defined(LIGHTMAP_ON) && !defined(UNITY_NO_SCREENSPACE_SHADOWS) // no lightmap uv thus store screenPos instead
// can happen if we have two directional lights. main light gets handled in GI code, but 2nd dir light can have shadow screen and mask.
// - Disabled on ES2 because WebGL 1.0 seems to have junk in .w (even though it shouldn't)
# if defined(SHADOWS_SHADOWMASK) && !defined(SHADER_API_GLES)
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
# define UNITY_TRANSFER_SHADOW(a, coord) {a._ShadowCoord.xy = coord * unity_LightmapST.xy + unity_LightmapST.zw; a._ShadowCoord.zw = ComputeScreenPos(a.pos).xy;}
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, float4(a._ShadowCoord.zw, 0.0, UNITY_SHADOW_W(a.pos.w)));
# else
# define UNITY_SHADOW_COORDS(idx1) SHADOW_COORDS(idx1)
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, a._ShadowCoord)
# endif
#else
# define UNITY_SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
# if defined(SHADOWS_SHADOWMASK)
# define UNITY_TRANSFER_SHADOW(a, coord) a._ShadowCoord.xy = coord.xy * unity_LightmapST.xy + unity_LightmapST.zw;
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE) || UNITY_LIGHT_PROBE_PROXY_VOLUME)
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(a._ShadowCoord.xy, 0, 0)
# endif
# else
# if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define UNITY_TRANSFER_SHADOW(a, coord)
# else
# define UNITY_TRANSFER_SHADOW(a, coord) TRANSFER_SHADOW(a)
# endif
# if (defined(SHADOWS_DEPTH) || defined(SHADOWS_SCREEN) || defined(SHADOWS_CUBE))
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# if UNITY_LIGHT_PROBE_PROXY_VOLUME
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, worldPos, UNITY_READ_SHADOW_COORDS(a))
# else
# define UNITY_SHADOW_ATTENUATION(a, worldPos) UnityComputeForwardShadows(0, 0, 0)
# endif
# endif
# endif
#endif
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
SutoLight.cginに次のように定義されています。
#ifdef POINT
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz; \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).r * shadow;
#endif
#ifdef SPOT
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
sampler2D_float _LightTextureB0;
inline fixed UnitySpotCookie(unityShadowCoord4 LightCoord)
{
return tex2D(_LightTexture0, LightCoord.xy / LightCoord.w + 0.5).w;
}
inline fixed UnitySpotAttenuate(unityShadowCoord3 LightCoord)
{
return tex2D(_LightTextureB0, dot(LightCoord, LightCoord).xx).r;
}
#if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
#define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord4 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1))
#else
#define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord4 lightCoord = input._LightCoord
#endif
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
DECLARE_LIGHT_COORD(input, worldPos); \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = (lightCoord.z > 0) * UnitySpotCookie(lightCoord) * UnitySpotAttenuate(lightCoord.xyz) * shadow;
#endif
#ifdef DIRECTIONAL
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) fixed destName = UNITY_SHADOW_ATTENUATION(input, worldPos);
#endif
#ifdef POINT_COOKIE
samplerCUBE_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
sampler2D_float _LightTextureB0;
# if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz
# else
# define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord3 lightCoord = input._LightCoord
# endif
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
DECLARE_LIGHT_COORD(input, worldPos); \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).r * texCUBE(_LightTexture0, lightCoord).w * shadow;
#endif
#ifdef DIRECTIONAL_COOKIE
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
# if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord2 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xy
# else
# define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord2 lightCoord = input._LightCoord
# endif
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
DECLARE_LIGHT_COORD(input, worldPos); \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = tex2D(_LightTexture0, lightCoord).w * shadow;
#endif
ライトの種類によって場合分けされています。 順番に見ていきます。
ポイントライトの場合
#ifdef POINT
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz; \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).r * shadow;
#endif
_LightTexture0
はフレームデバッガを見てみるとR16の1024x1のテクスチャです。
lightCoord
はライトから見た空間の座標のようです。
dot(lightCoord, lightCoord)
はライトからフラグメントまでの距離の2乗になります。
これで_LightTexture0
からtex2D
で値を取得しています。
多分テクスチャに距離の2乗に反比例する光の減衰が入っているものと思われます。
shadow
はUNITY_SHADOW_ATTENUATION(input, worldPos);
が渡されます。
これについてはベースパスの平行光源のところで見ました。
この2つを掛け合わせることでポイントライトの減衰としているようです。
スポットライトの場合
#ifdef SPOT
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
sampler2D_float _LightTextureB0;
inline fixed UnitySpotCookie(unityShadowCoord4 LightCoord)
{
return tex2D(_LightTexture0, LightCoord.xy / LightCoord.w + 0.5).w;
}
inline fixed UnitySpotAttenuate(unityShadowCoord3 LightCoord)
{
return tex2D(_LightTextureB0, dot(LightCoord, LightCoord).xx).r;
}
#if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
#define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord4 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1))
#else
#define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord4 lightCoord = input._LightCoord
#endif
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
DECLARE_LIGHT_COORD(input, worldPos); \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = (lightCoord.z > 0) * UnitySpotCookie(lightCoord) * UnitySpotAttenuate(lightCoord.xyz) * shadow;
#endif
スポットライトにはデフォルトでクッキーが与えられています。 デフォルトのクッキーはLightingウィンドウで確認できます。
sampler2D_float _LightTexture0;
にはクッキーのテクスチャが入っています。
sampler2D_float _LightTextureB0;
にはポイントライトで見た
減衰のテクスチャが入っています。
UNITY_SHADOW_ATTENUATION
で計算した影に、さらに距離の2乗に反比例した
光の減衰と、スポットライトのクッキーの計算を行っています。
平行光源の場合
#ifdef DIRECTIONAL
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) fixed destName = UNITY_SHADOW_ATTENUATION(input, worldPos);
#endif
ベースパスで説明しました。
クッキーの設定されたポイントライトの場合
#ifdef POINT_COOKIE
samplerCUBE_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
sampler2D_float _LightTextureB0;
# if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz
# else
# define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord3 lightCoord = input._LightCoord
# endif
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
DECLARE_LIGHT_COORD(input, worldPos); \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).r * texCUBE(_LightTexture0, lightCoord).w * shadow;
#endif
samplerCUBE_float _LightTexture0;
がクッキーテクスチャのようです。
sampler2D_float _LightTextureB0;
はポイントライトの減衰です。
UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
ではない場合には
フラグメントシェーダ上でライト空間の座標を計算し、
UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
の場合には
頂点シェーダで計算したライト空間の座標を採用するようです。
UNITY_SHADOW_ATTENUATION
の結果に光の減衰とクッキーによる減衰をかけ合わせて
返しているようです。
クッキーの設定された平行光源の場合
#ifdef DIRECTIONAL_COOKIE
sampler2D_float _LightTexture0;
unityShadowCoord4x4 unity_WorldToLight;
# if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
# define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord2 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xy
# else
# define DECLARE_LIGHT_COORD(input, worldPos) unityShadowCoord2 lightCoord = input._LightCoord
# endif
# define UNITY_LIGHT_ATTENUATION(destName, input, worldPos) \
DECLARE_LIGHT_COORD(input, worldPos); \
fixed shadow = UNITY_SHADOW_ATTENUATION(input, worldPos); \
fixed destName = tex2D(_LightTexture0, lightCoord).w * shadow;
#endif
sampler2D_float _LightTexture0;
がクッキーテクスチャです。
UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
の有無で
ライト空間での座標をフラグメントシェーダで計算するか
頂点シェーダで計算したものを使うか分けています。
UNITY_SHADOW_ATTENUATION
にクッキーテクスチャの減衰をかけたものを返しています。
ベースパスの頂点シェーダでのライティングを追加する
ここまでのシェーダで影響度の強い4つのライトには対応しました。 次は影響度の低いライトの処理を追加していきます。
#pragma
からnovertexlight
を取り除きます。
#pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap
ベースパスのv2f構造体を次のように書き換えます。
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
float3 vertexLight: TEXCOORD3;
#ifdef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
UNITY_LIGHTING_COORDS(4,5)
#else
UNITY_SHADOW_COORDS(4)
#endif
};
float3 vertexLight: TEXCOORD3;
を追加しました。
これで頂点シェーダでライティングした結果を色として受け渡すようにしました。
頂点シェーダには次のようにしてライティング処理を追加します。
void vert (in appdata v, out v2f o)
{
UNITY_INITIALIZE_OUTPUT(v2f, o);
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
#ifdef VERTEXLIGHT_ON
o.vertexLight = Shade4PointLights(
unity_4LightPosX0,
unity_4LightPosY0,
unity_4LightPosZ0,
unity_LightColor[0].rgb,
unity_LightColor[1].rgb,
unity_LightColor[2].rgb,
unity_LightColor[3].rgb,
unity_4LightAtten0,
o.worldPos,
o.worldNormal);
#endif
UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy);
}
VERTEXLIGHT_ON
があるときだけShade4PointLights
で重要度の低い
4つのライトについて計算します。
フラグメントシェーダの最後を次のようにします。
col = fixed4(
baseColor * lightColor * max(NL, 0) * attenuation + i.vertexLight,
0);
頂点シェーダから渡された色を加算するようにしました。
これで頂点シェーダでのライティングもできました。
フレームデバッグでベースパスのみ表示すると次のとおりです。
Shade4PointLights
頂点シェーダにShade4PointLights
を付け加えました。
#ifdef VERTEXLIGHT_ON
o.vertexLight = Shade4PointLights(
unity_4LightPosX0,
unity_4LightPosY0,
unity_4LightPosZ0,
unity_LightColor[0].rgb,
unity_LightColor[1].rgb,
unity_LightColor[2].rgb,
unity_LightColor[3].rgb,
unity_4LightAtten0,
o.worldPos,
o.worldNormal);
#endif
unity_4LightPosX0
、unity_4LightPosY0
、unity_4LightPosZ0
は
それぞれ4つのライトのX座標Y座標Z座標が各チャンネルに格納されています。
他にも色や遮蔽など4つのライトに関係した変数を渡しています。
Shade4PointLights
はUnityCG.cgincで次のように定義されています。
// Used in ForwardBase pass: Calculates diffuse lighting from 4 point lights, with data packed in a special way.
float3 Shade4PointLights (
float4 lightPosX, float4 lightPosY, float4 lightPosZ,
float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3,
float4 lightAttenSq,
float3 pos, float3 normal)
{
// to light vectors
float4 toLightX = lightPosX - pos.x;
float4 toLightY = lightPosY - pos.y;
float4 toLightZ = lightPosZ - pos.z;
// squared lengths
float4 lengthSq = 0;
lengthSq += toLightX * toLightX;
lengthSq += toLightY * toLightY;
lengthSq += toLightZ * toLightZ;
// don't produce NaNs if some vertex position overlaps with the light
lengthSq = max(lengthSq, 0.000001);
// NdotL
float4 ndotl = 0;
ndotl += toLightX * normal.x;
ndotl += toLightY * normal.y;
ndotl += toLightZ * normal.z;
// correct NdotL
float4 corr = rsqrt(lengthSq);
ndotl = max (float4(0,0,0,0), ndotl * corr);
// attenuation
float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq);
float4 diff = ndotl * atten;
// final color
float3 col = 0;
col += lightColor0 * diff.x;
col += lightColor1 * diff.y;
col += lightColor2 * diff.z;
col += lightColor3 * diff.w;
return col;
}
少々トリッキーな計算の仕方になっていますが、 ちゃんと読んでいくとLambert反射を計算しているのがわかります。
このShade4PointLights
はSurface Shaderでも使われています。
物理ベースのレンダリングでも重要でないライトはLambert反射で雑に近似しているのですね。
球面調和関数による環境光を追加する
より優先度の低いライトは環境光と一緒くたにされます。
環境光は球面調和関数の形で表現されています。 球面調和関数については次のページの解説がわかりやすいです。
大雑把に説明するとフーリエ変換のように周波数の低い成分から順に合わせていくことで 球面の値の近似を行うものです。 Unityではの項まで扱っているようです。
v2f構造体を次のように書き換えます。
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
// float3 vertexLight: TEXCOORD3;
#if UNITY_SHOULD_SAMPLE_SH
float3 sh: TEXCOORD3;
#endif
#ifdef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
UNITY_LIGHTING_COORDS(4,5)
#else
UNITY_SHADOW_COORDS(4)
#endif
};
頂点シェーダを次のようにします。
void vert (in appdata v, out v2f o)
{
UNITY_INITIALIZE_OUTPUT(v2f, o);
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
#if UNITY_SHOULD_SAMPLE_SH
o.sh = 0;
#ifdef VERTEXLIGHT_ON
o.sh += Shade4PointLights(
unity_4LightPosX0,
unity_4LightPosY0,
unity_4LightPosZ0,
unity_LightColor[0].rgb,
unity_LightColor[1].rgb,
unity_LightColor[2].rgb,
unity_LightColor[3].rgb,
unity_4LightAtten0,
o.worldPos,
o.worldNormal);
#endif
o.sh += ShadeSH9(half4(o.worldNormal, 1));
#endif
UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy);
}
ShadeSH9(normal)
は法線を渡すことで球面調和関数から値をサンプリングしてくれるものです。
9というのはまでの項の数が9個だからでしょう。
フラグメントシェーダを次のようにします。
void frag (in v2f i, out fixed4 col : SV_Target)
{
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 normal = normalize(i.worldNormal);
float NL = dot(normal, lightDir);
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
float3 baseColor = tex2D(_MainTex, i.uv);
float3 lightColor = _LightColor0;
#if UNITY_SHOULD_SAMPLE_SH
col = fixed4(baseColor * lightColor * max(NL, 0) * attenuation + i.sh, 1);
#else
col = fixed4(baseColor * lightColor * max(NL, 0) * attenuation, 1);
#endif
}
これでより優先度の低いライトや環境光も扱えるようになりました。
次のようなシーンを試してみます。 Light Probeを配置してベイクをします。
ちゃんと環境光が反映されています。
UNITY_SHOULD_SAMPLE_SH
UnityCG.cgincで次のように定義されています。
// Should SH (light probe / ambient) calculations be performed?
// - When both static and dynamic lightmaps are available, no SH evaluation is performed
// - When static and dynamic lightmaps are not available, SH evaluation is always performed
// - For low level LODs, static lightmap and real-time GI from light probes can be combined together
// - Passes that don't do ambient (additive, shadowcaster etc.) should not do SH either.
#define UNITY_SHOULD_SAMPLE_SH (defined(LIGHTPROBE_SH) && !defined(UNITY_PASS_FORWARDADD) && !defined(UNITY_PASS_PREPASSBASE) && !defined(UNITY_PASS_SHADOWCASTER) && !defined(UNITY_PASS_META))
ShadeSH9(normal)
UnityCG.cgincで定義されている関数です。 定義は次のとおりです。
// normal should be normalized, w=1.0
// output in active color space
half3 ShadeSH9 (half4 normal)
{
// Linear + constant polynomial terms
half3 res = SHEvalLinearL0L1 (normal);
// Quadratic polynomials
res += SHEvalLinearL2 (normal);
# ifdef UNITY_COLORSPACE_GAMMA
res = LinearToGammaSpace (res);
# endif
return res;
}
SHEvalLinearL0L1
とSHEvalLinearL2
を呼び出しています。
SHEvalLinearL0L1
の定義はUnityCG.cgincで次のとおりです。
// normal should be normalized, w=1.0
half3 SHEvalLinearL0L1 (half4 normal)
{
half3 x;
// Linear (L1) + constant (L0) polynomial terms
x.r = dot(unity_SHAr,normal);
x.g = dot(unity_SHAg,normal);
x.b = dot(unity_SHAb,normal);
return x;
}
SHEvalLinearL2
の定義はUnityCG.cgincで次のとおりです。
// normal should be normalized, w=1.0
half3 SHEvalLinearL2 (half4 normal)
{
half3 x1, x2;
// 4 of the quadratic (L2) polynomials
half4 vB = normal.xyzz * normal.yzzx;
x1.r = dot(unity_SHBr,vB);
x1.g = dot(unity_SHBg,vB);
x1.b = dot(unity_SHBb,vB);
// Final (5th) quadratic (L2) polynomial
half vC = normal.x*normal.x - normal.y*normal.y;
x2 = unity_SHC.rgb * vC;
return x1 + x2;
}
渡された係数をもとに球面調和関数の値を計算しているようです。
球面調和関数の一部をフラグメントシェーダに移す
Surface Shaderでは環境に合わせて球面調和関数を全部頂点シェーダで計算せずに、 一部をフラグメントシェーダで計算したり、あるいは全部をフラグメントシェーダで計算するようです。
頂点シェーダを次のように修正します。
// #if UNITY_SHOULD_SAMPLE_SH
#if UNITY_SHOULD_SAMPLE_SH && !UNITY_SAMPLE_FULL_SH_PER_PIXEL
o.sh = 0;
#ifdef VERTEXLIGHT_ON
o.sh += Shade4PointLights(
unity_4LightPosX0,
unity_4LightPosY0,
unity_4LightPosZ0,
unity_LightColor[0].rgb,
unity_LightColor[1].rgb,
unity_LightColor[2].rgb,
unity_LightColor[3].rgb,
unity_4LightAtten0,
o.worldPos,
o.worldNormal);
#endif
// o.sh += ShadeSH9(half4(o.worldNormal, 1));
o.sh = ShadeSHPerVertex (o.worldNormal, o.sh);
#endif
フラグメントシェーダの最後を次のように修正します。
#if UNITY_SHOULD_SAMPLE_SH
float3 sh = ShadeSHPerPixel(normal, i.sh, i.worldPos);
col = fixed4(baseColor * lightColor * max(NL, 0) * attenuation + sh, 1);
#else
col = fixed4(baseColor * lightColor * max(NL, 0) * attenuation, 1);
#endif
ShadeSHPerVertex
ShadeSHPerVertex
はUnityStandardUtils.cgincで次のように定義されています。
half3 ShadeSHPerVertex (half3 normal, half3 ambient)
{
#if UNITY_SAMPLE_FULL_SH_PER_PIXEL
// Completely per-pixel
// nothing to do here
#elif (SHADER_TARGET < 30) || UNITY_STANDARD_SIMPLE
// Completely per-vertex
ambient += max(half3(0,0,0), ShadeSH9 (half4(normal, 1.0)));
#else
// L2 per-vertex, L0..L1 & gamma-correction per-pixel
// NOTE: SH data is always in Linear AND calculation is split between vertex & pixel
// Convert ambient to Linear and do final gamma-correction at the end (per-pixel)
#ifdef UNITY_COLORSPACE_GAMMA
ambient = GammaToLinearSpace (ambient);
#endif
ambient += SHEvalLinearL2 (half4(normal, 1.0)); // no max since this is only L2 contribution
#endif
return ambient;
}
球面調和関数の計算をフラグメントシェーダで全部行う場合は渡されたambientを返すだけです。
Shader Modelが3.0より小さいか単純化されたStandardシェーダの場合には
ShadeSH9
を利用して球面調和関数を全部頂点シェーダで計算します。
それ以外の場合はの項のみ計算をします。
ShadeSHPerPixel
ShadeSHPerPixel
はUnityStandardUtils.cgincで次のように定義されています。
half3 ShadeSHPerPixel (half3 normal, half3 ambient, float3 worldPos)
{
half3 ambient_contrib = 0.0;
#if UNITY_SAMPLE_FULL_SH_PER_PIXEL
// Completely per-pixel
#if UNITY_LIGHT_PROBE_PROXY_VOLUME
if (unity_ProbeVolumeParams.x == 1.0)
ambient_contrib = SHEvalLinearL0L1_SampleProbeVolume(half4(normal, 1.0), worldPos);
else
ambient_contrib = SHEvalLinearL0L1(half4(normal, 1.0));
#else
ambient_contrib = SHEvalLinearL0L1(half4(normal, 1.0));
#endif
ambient_contrib += SHEvalLinearL2(half4(normal, 1.0));
ambient += max(half3(0, 0, 0), ambient_contrib);
#ifdef UNITY_COLORSPACE_GAMMA
ambient = LinearToGammaSpace(ambient);
#endif
#elif (SHADER_TARGET < 30) || UNITY_STANDARD_SIMPLE
// Completely per-vertex
// nothing to do here. Gamma conversion on ambient from SH takes place in the vertex shader, see ShadeSHPerVertex.
#else
// L2 per-vertex, L0..L1 & gamma-correction per-pixel
// Ambient in this case is expected to be always Linear, see ShadeSHPerVertex()
#if UNITY_LIGHT_PROBE_PROXY_VOLUME
if (unity_ProbeVolumeParams.x == 1.0)
ambient_contrib = SHEvalLinearL0L1_SampleProbeVolume (half4(normal, 1.0), worldPos);
else
ambient_contrib = SHEvalLinearL0L1 (half4(normal, 1.0));
#else
ambient_contrib = SHEvalLinearL0L1 (half4(normal, 1.0));
#endif
ambient = max(half3(0, 0, 0), ambient+ambient_contrib); // include L2 contribution in vertex shader before clamp.
#ifdef UNITY_COLORSPACE_GAMMA
ambient = LinearToGammaSpace (ambient);
#endif
#endif
return ambient;
}
球面調和関数の計算をフラグメントシェーダで全部行う場合は、 さらにLPPVの有無で場合分けをしています。 どちらの場合もとの項を計算し、の項を足しています。
頂点シェーダで全部計算してしまった場合にはここでは何もしません。
頂点シェーダでの項を計算した場合は、ここでとの項を計算します。 こちらもLPPVの有無で場合分けしています。
シェーダのソースコード全文
完成したシェーダのコード全文を載せておきます。
Shader "ForwardLambert"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
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;
float4 pos : SV_POSITION;
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
float3 vec : TEXCOORD0;
#endif
};
void vert(in appdata_base v, out v2f o)
{
// TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz;
o.pos = UnityObjectToClipPos(v.vertex);
#else
o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal);
o.pos = UnityApplyLinearShadowBias(o.pos);
#endif
}
float4 frag(v2f i) : SV_Target
{
// SHADOW_CASTER_FRAGMENT(i)
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
return UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w);
#else
return 0;
#endif
}
ENDCG
}
Pass
{
Tags { "LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
float2 texcoord1: TEXCOORD1;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
#if UNITY_SHOULD_SAMPLE_SH
float3 sh: TEXCOORD3;
#endif
#ifdef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
UNITY_LIGHTING_COORDS(4,5)
#else
UNITY_SHADOW_COORDS(4)
#endif
};
sampler2D _MainTex;
float4 _MainTex_ST;
void vert (in appdata v, out v2f o)
{
UNITY_INITIALIZE_OUTPUT(v2f, o);
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
#if UNITY_SHOULD_SAMPLE_SH && !UNITY_SAMPLE_FULL_SH_PER_PIXEL
o.sh = 0;
#ifdef VERTEXLIGHT_ON
o.sh += Shade4PointLights(
unity_4LightPosX0,
unity_4LightPosY0,
unity_4LightPosZ0,
unity_LightColor[0].rgb,
unity_LightColor[1].rgb,
unity_LightColor[2].rgb,
unity_LightColor[3].rgb,
unity_4LightAtten0,
o.worldPos,
o.worldNormal);
#endif
o.sh = ShadeSHPerVertex (o.worldNormal, o.sh);
#endif
UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy);
}
void frag (in v2f i, out fixed4 col : SV_Target)
{
float3 lightDir = _WorldSpaceLightPos0.xyz;
float3 normal = normalize(i.worldNormal);
float NL = dot(normal, lightDir);
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
float3 baseColor = tex2D(_MainTex, i.uv);
float3 lightColor = _LightColor0;
#if UNITY_SHOULD_SAMPLE_SH
float3 sh = ShadeSHPerPixel(normal, i.sh, i.worldPos);
col = fixed4(baseColor * lightColor * max(NL, 0) * attenuation + sh, 1);
#else
col = fixed4(baseColor * lightColor * max(NL, 0) * attenuation, 1);
#endif
}
ENDCG
}
Pass
{
Tags { "LightMode"="ForwardAdd"}
ZWrite Off
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd_fullshadows
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
float2 texcoord1: TEXCOORD1;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
UNITY_LIGHTING_COORDS(3,4)
};
sampler2D _MainTex;
float4 _MainTex_ST;
void vert (in appdata v, out v2f o)
{
UNITY_INITIALIZE_OUTPUT(v2f, o);
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy);
}
void frag (in v2f i, out fixed4 col : SV_Target)
{
#ifndef USING_DIRECTIONAL_LIGHT
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
#else
fixed3 lightDir = _WorldSpaceLightPos0.xyz;
#endif
float3 normal = normalize(i.worldNormal);
float NL = dot(normal, lightDir);
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
float3 baseColor = tex2D(_MainTex, i.uv);
float3 lightColor = _LightColor0;
col = fixed4(baseColor * lightColor * max(NL, 0) * attenuation, 0);
}
ENDCG
}
}
}
補足:Surface Shaderでの環境光
このコードではStaticなオブジェクトで正しく環境光が計算されないようです。
Surface Shaderでは環境光を扱う部分でさらにいろいろとコードを生成しています。 きちんと環境光を扱うには現状のコードでは不十分で、さらにいろいろとコードが必要なようです。 ちょっとそこまで追いかけきれていないので補足という形でメモ書きだけ残しておきます。
Surface Shaderから生成されたコードのフラグメントシェーダでの 環境光の計算の部分は次のようになっています。
// compute lighting & shadowing factor
UNITY_LIGHT_ATTENUATION(atten, IN, worldPos)
fixed4 c = 0;
// Setup lighting environment
UnityGI gi;
UNITY_INITIALIZE_OUTPUT(UnityGI, gi);
gi.indirect.diffuse = 0;
gi.indirect.specular = 0;
gi.light.color = _LightColor0.rgb;
gi.light.dir = lightDir;
// Call GI (lightmaps/SH/reflections) lighting function
UnityGIInput giInput;
UNITY_INITIALIZE_OUTPUT(UnityGIInput, giInput);
giInput.light = gi.light;
giInput.worldPos = worldPos;
giInput.worldViewDir = worldViewDir;
giInput.atten = atten;
#if defined(LIGHTMAP_ON) || defined(DYNAMICLIGHTMAP_ON)
giInput.lightmapUV = IN.lmap;
#else
giInput.lightmapUV = 0.0;
#endif
#if UNITY_SHOULD_SAMPLE_SH && !UNITY_SAMPLE_FULL_SH_PER_PIXEL
giInput.ambient = IN.sh;
#else
giInput.ambient.rgb = 0.0;
#endif
giInput.probeHDR[0] = unity_SpecCube0_HDR;
giInput.probeHDR[1] = unity_SpecCube1_HDR;
#if defined(UNITY_SPECCUBE_BLENDING) || defined(UNITY_SPECCUBE_BOX_PROJECTION)
giInput.boxMin[0] = unity_SpecCube0_BoxMin; // .w holds lerp value for blending
#endif
#ifdef UNITY_SPECCUBE_BOX_PROJECTION
giInput.boxMax[0] = unity_SpecCube0_BoxMax;
giInput.probePosition[0] = unity_SpecCube0_ProbePosition;
giInput.boxMax[1] = unity_SpecCube1_BoxMax;
giInput.boxMin[1] = unity_SpecCube1_BoxMin;
giInput.probePosition[1] = unity_SpecCube1_ProbePosition;
#endif
LightingStandard_GI(o, giInput, gi);
// realtime lighting: call lighting function
c += LightingStandard (o, worldViewDir, gi);
ShadeSHPerPixel(normal, i.sh, i.worldPos);
ではなく
LightingStandard_GI(o, giInput, gi);
を使って環境光を計算しています。
ShadeSHPerPixel(normal, i.sh, i.worldPos);
は
LightingStandard_GI(o, giInput, gi);
の中で呼ばれています。
LightingStandard_GI
はUnityPBSLighting.cginc
で次のように定義されています。
inline void LightingStandard_GI (
SurfaceOutputStandard s,
UnityGIInput data,
inout UnityGI gi)
{
#if defined(UNITY_PASS_DEFERRED) && UNITY_ENABLE_REFLECTION_BUFFERS
gi = UnityGlobalIllumination(data, s.Occlusion, s.Normal);
#else
Unity_GlossyEnvironmentData g = UnityGlossyEnvironmentSetup(s.Smoothness, data.worldViewDir, s.Normal, lerp(unity_ColorSpaceDielectricSpec.rgb, s.Albedo, s.Metallic));
gi = UnityGlobalIllumination(data, s.Occlusion, s.Normal, g);
#endif
}
UnityGlobalIllumination
はUnityGlobalIllumination.cginc
で
次のように定義されています。
inline UnityGI UnityGlobalIllumination (UnityGIInput data, half occlusion, half3 normalWorld)
{
return UnityGI_Base(data, occlusion, normalWorld);
}
inline UnityGI UnityGlobalIllumination (UnityGIInput data, half occlusion, half3 normalWorld, Unity_GlossyEnvironmentData glossIn)
{
UnityGI o_gi = UnityGI_Base(data, occlusion, normalWorld);
o_gi.indirect.specular = UnityGI_IndirectSpecular(data, occlusion, glossIn);
return o_gi;
}
UnityGI_Base
はUnityGlobalIllumination.cginc
で次のように定義されています。
inline UnityGI UnityGI_Base(UnityGIInput data, half occlusion, half3 normalWorld)
{
UnityGI o_gi;
ResetUnityGI(o_gi);
// Base pass with Lightmap support is responsible for handling ShadowMask / blending here for performance reason
#if defined(HANDLE_SHADOWS_BLENDING_IN_GI)
half bakedAtten = UnitySampleBakedOcclusion(data.lightmapUV.xy, data.worldPos);
float zDist = dot(_WorldSpaceCameraPos - data.worldPos, UNITY_MATRIX_V[2].xyz);
float fadeDist = UnityComputeShadowFadeDistance(data.worldPos, zDist);
data.atten = UnityMixRealtimeAndBakedShadows(data.atten, bakedAtten, UnityComputeShadowFade(fadeDist));
#endif
o_gi.light = data.light;
o_gi.light.color *= data.atten;
#if UNITY_SHOULD_SAMPLE_SH
o_gi.indirect.diffuse = ShadeSHPerPixel(normalWorld, data.ambient, data.worldPos);
#endif
#if defined(LIGHTMAP_ON)
// Baked lightmaps
half4 bakedColorTex = UNITY_SAMPLE_TEX2D(unity_Lightmap, data.lightmapUV.xy);
half3 bakedColor = DecodeLightmap(bakedColorTex);
#ifdef DIRLIGHTMAP_COMBINED
fixed4 bakedDirTex = UNITY_SAMPLE_TEX2D_SAMPLER (unity_LightmapInd, unity_Lightmap, data.lightmapUV.xy);
o_gi.indirect.diffuse += DecodeDirectionalLightmap (bakedColor, bakedDirTex, normalWorld);
#if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN)
ResetUnityLight(o_gi.light);
o_gi.indirect.diffuse = SubtractMainLightWithRealtimeAttenuationFromLightmap (o_gi.indirect.diffuse, data.atten, bakedColorTex, normalWorld);
#endif
#else // not directional lightmap
o_gi.indirect.diffuse += bakedColor;
#if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN)
ResetUnityLight(o_gi.light);
o_gi.indirect.diffuse = SubtractMainLightWithRealtimeAttenuationFromLightmap(o_gi.indirect.diffuse, data.atten, bakedColorTex, normalWorld);
#endif
#endif
#endif
#ifdef DYNAMICLIGHTMAP_ON
// Dynamic lightmaps
fixed4 realtimeColorTex = UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, data.lightmapUV.zw);
half3 realtimeColor = DecodeRealtimeLightmap (realtimeColorTex);
#ifdef DIRLIGHTMAP_COMBINED
half4 realtimeDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_DynamicDirectionality, unity_DynamicLightmap, data.lightmapUV.zw);
o_gi.indirect.diffuse += DecodeDirectionalLightmap (realtimeColor, realtimeDirTex, normalWorld);
#else
o_gi.indirect.diffuse += realtimeColor;
#endif
#endif
o_gi.indirect.diffuse *= occlusion;
return o_gi;
}
ShadeSHPerPixel
を呼んでいる部分があります。
#if UNITY_SHOULD_SAMPLE_SH
o_gi.indirect.diffuse = ShadeSHPerPixel(normalWorld, data.ambient, data.worldPos);
#endif
ShadeSHPerPixel
以外にもライトマップからのサンプリングが行われています。
Staticなオブジェクトで正しく環境光を計算したいならば
ここらへんのコードも必要になりそうです。
参考サイト
- Forward Renderingについてまとめてみた - しゅみぷろ
- Unity - Manual: Forward Rendering Path Details
- Unity で Standard Surface Shader の変換後のコードを追ってみた (Forward) - 凹みTips
- Unity - Manual: Vertex and fragment shader examples
- Shader Variantについて調べてみた - しゅみぷろ
- http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/
- Unity - Manual: Platform-specific rendering differences
- ☆PROJECT ASURA☆ [Direct3D 11] 『シャドウマッピングの基本』
- ComputeScreenPos & Render queue issues - Unity Forum
- Unity - Scripting API: BuiltinShaderDefine
- Unity - Manual: Shader data types and precision
- Unity - Manual: Cookies
- Unity - Manual: Predefined Shader preprocessor macros
- SampleCmpLevelZero (DirectX HLSL テクスチャー オブジェクト)
- SampleCmp (DirectX HLSL テクスチャー オブジェクト)
- what is in float4 _LightShadowData? - Unity Forum
- Unity - Beta
- Question in “Hidden/Internal-ScreenSpaceShadows” - Unity Forum
- Unity - Manual: Built-in shader variables
- How to Use Unity 2018.1 to Capture Stereoscopic 360 Images and Videos | VeeR VR Blog
- rendering-ods-content.pdf
- Unity - Manual: Directional light shadows
- http://mynameismjp.wordpress.com/2013/09/10/shadow-maps/
- http://amd-dev.wpengine.netdna-cdn.com/wordpress/media/2012/10/Isidoro-ShadowMapping.pdf
- Unity - Manual: Mixed lighting
- Unity - Manual: Shadowmask mode
- 【Unity】リアルタイムな影とベイクした影を混ぜる、Shadow Mask 特集号 - テラシュールブログ
- Unity - Manual: Shadowmask
- Unity - Manual: Distance Shadowmask
- Unity - Manual: Real-time lighting
- 【Unity】Lightprobeの影響をモデルやパーティクルの一部に限定する、LightprobeProxyVolumeを使ってみる - テラシュールブログ
- if ステートメント (DirectX HLSL)
- directx11 - how does an SM5 shader handle loops and if statements? (HLSL/CG) - Game Development Stack Exchange
- 3Dグラフィックス・マニアックス(68) 事前計算放射輝度伝搬(PRT)~PRTの基本。静的PRT(3) | マイナビニュース
おわりに
マクロの定義を追いかけたのでちょっと大変でした。 微妙に全部追いかけるのは諦めて解説をすっ飛ばしたところもありますが、 だいたい処理の内容はわかった気がします。
今回の記事のソースコードはGitHubにも上げてあります。 GitHubのリポジトリはこちらです。