UnityでForwardのライトに対応したLambert反射モデルのシェーダを作成する

はじめに

以前に作ったUnityのシェーダはUnityのライトには反応せず 常にプロパティで与えた方向の光が当たるものでした。 このままではシーンに配置されたライトには反応してくれません。 そこで今回はUnityのForwardのライトに対応したLambertシェーダを作ってみます。 適宜Unityで用意されたマクロを利用しますが、そのマクロの中身についても見ていきます。

マクロについてはドキュメント化されていないものも多く、推測が多数含まれています。 また、私自身の知識が足りていないことによる誤りも多数含まれていると思われます。 この記事を参考にする場合は自己責任でお願いします。

96

  • 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;

_LightColor0Lighting.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_CASTERTRANSFER_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_NOPOSUNITY_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が渡される

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」の値をいじると 確かに影がノーマル方向に縮小されているのがわかります。

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.xunity_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)を 構造体に追加しています。 数字は使われるTEXCOORDNNです。 worldNormalworldPosuvTEXCOORD2まで使っているので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.

デスクトップではfloathalffixedも全部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についても平行光源っぽいので見てみます。

クッキーについては次のページに解説があります。 マスクのテクスチャを使ってライトの形状を変更する機能です。

ライトの設定からクッキーを指定できます。

24

フレームデバッガを確認してみると、 クッキーを設定した平行光源は加算パスで計算されているようです。

25


ベースパスの場合は単純な平行光源しか扱わないため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にfloat3float4_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_SCREENLIGHTMAP_ONは実行時に渡されるキーワードのようです。 いくつか試して確認してみます。

オブジェクトの「Static」をオフにしてライトの「Type」を「Realtime」にすると SHADOWS_SCREENが追加されました。

`SHADOWS_SCREEN`が追加された

オブジェクトを「Static」にしてライトの「Type」を「Realtime」にすると SHADOWS_SCREENLIGHTMAP_ONが両方追加されました。

Staticにする

Realtimeにする

`SHADOWS_SCREEN`と`LIGHTMAP_ON`が両方追加された

オブジェクトを「Static」にしてライトの「Type」を「Baked」にすると LIGHTMAP_ONが追加されました。

Staticにする

Bakedにする

`LIGHTMAP_ON`が追加された

オブジェクトを「Static」にしてライトの「Type」を「Mixed」にすると SHADOWS_SCREENLIGHTMAP_ONが両方追加されました。

Staticにする

Mixedにする

`SHADOWS_SCREEN`と`LIGHTMAP_ON`が両方追加された

オブジェクトが「Static」ではない場合、ライトが「Mixed」でもSHADOWS_SCREENのみです。

Staticではない場合

`SHADOWS_SCREEN`のみが追加された

ただし「Lightmap Static」をオンにするとLIGHTMAP_ONがオンになりました。

Lightmap Staticをオンにする

`SHADOWS_SCREEN`と`LIGHTMAP_ON`が両方追加された

設定の「Graphics」から「Screen Space Shadows」を「No Support」にすると 上記の「SHADOWS_SCREEN」が追加されていた場合でも追加されなくなりました。

Screen Space ShadowsをNo Supportにする

`SHADOWS_SCREEN`が追加されない

ただし、この場合はリアルタイムの影が描画されないようです。

リアルタイムの影が描画されない

ついでに加算パスの場合の挙動についても確認しておきます。

平行光源を2つおいてみました。

平行光源を2つ置く

2つ目の平行光源でも影を有効にします。

2つ目のライトも影を有効にする

Staticなオブジェクトを配置します。

Staticなオブジェクトを配置する

ベースパスの場合は次のとおりです。

ベースパスの場合

加算パスは次のようになっていました。

加算パスの場合

たとえStaticなオブジェクトであっても LIGHTMAP_ONが加算パスには追加されていないことがわかります。 確かにベースパスと加算パスの両方でライトマップを扱ってしまうと 多重にライトマップが適用されることになってしまいますね。

2つ目の平行光源の影を無効にした場合は次のようになりました。

ライトの影を無効にする

SHADOWS_SCREENもLIGHTMAP_ONも追加されない

この場合はリアルタイムの影の描画を行わないので 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_BaseUnityGlobalIllumination.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.SampleCmpLevelZeroSampleCmpのミップマップのレベルを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の実態が computeCameraSpacePosFromDepthAndVSInfocomputeCameraSpacePosFromDepthAndInvProjMatの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が渡されています。

orthoPosNearorthoPosFarでは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で採用されているステレオキューブマップのレンダリング方式のようです。

ステレオキューブマップではない通常の場合は 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で線形補間してワールド座標を復元しています。

最後にcamPosvposPerspvposOrthounity_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;
}

頂点シェーダで計算したrayorthNearPosorthFarPosを使わずに unity_CameraInvProjectionを使ってデプスからワールド座標を計算しています。 これはrayとdepthを使う方法に比べて遅いけれども 特殊なprojection行列でもうまく動くとのことです。

ここまでcomputeCameraSpacePosFromDepth(i);の処理でした。

wposvposにそれぞれワールドスペースの座標とカメラスペースの座標が計算されました。

次にGET_CASCADE_WEIGHTS (wpos, vpos.z);です。

シャドウマップはSettingsのQualityでカスケードを設定できます。

カスケードの設定

カスケードはScene Viewで可視化できます。

58

Shadow Distanceを短くすると全部のカスケードを見ることもできます。

59

60

シャドウのカスケードについては次のページに書かれています。

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はそれぞれカスケードの近い方の平面の距離と 遠い方の平面の距離が入っているものと思われます。 zNearzFarの両方の条件を満たすものが対応するカスケードということになります。

カスケードの仕方がカメラに平行な面か球面かによって呼び出されているものが変わっていますが、 注意深く追いかければ どちらも戻り値の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

カスケードしたシャドウマップは一枚を分割して収められています。

4分割されたシャドウマップ

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;
}

後者の資料の図がわかりやすかったので引用します。

62

63

ソフトシャドウのために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つのライトまで格納されます。 ライトマップのベイクのときに一緒にベイクされるようです。

フレームデバッガで確認できます。 次のようなシーンで確認してみます。

64

Mixedなライトが2つとStaticなSphere2つ、そしてStaticなPlaneが1つあります。 シャドウマスクを見てみると次のとおりです。

65

66

67

68

rgbaのチャンネルに各ライトからの影の情報が書き込まれていることがわかります。

Shadowmaskを利用する場合はSHADOWS_SHADOWMASKキーワードが有効になります。

69

Shadowmaskには「Shadowmask」と「Distance Shadowmask」の2つのタイプが存在します。 後者のほうがクオリティと負荷が高いものになります。

70

ShadowmaskではStaticなオブジェクトがStaticなオブジェクトから受ける影は すべてシャドウマスクを利用します。 非Staticなオブジェクトからの影のみシャドウマップのリアルタイムの影を利用します。

次のシーンではShadowmaskモードでshadow distanceを短くして ライトマップの解像度を思いっきり下げています。 赤いオブジェクトは非Staticなオブジェクトです。

71

72

非Staticなオブジェクトからの影のみリアルタイムの影でくっきりしているのがわかります。 Staticなオブジェクトの影はベイクされた影を利用しています。 今回はベイクした影の解像度を荒くしているので見た目が残念です。 リアルタイムの影はShadow Distanceが近づくとフェードアウトします。

Distance Shadowmaskの場合、Staticなオブジェクトであっても Shadow Distanceより手前はシャドウマップを利用したリアルタイムの影を使います。 Shadow Distanceより遠くはベイクされたシャドウマスクを利用します。

次のシーンではDistance Shadowmaskモードで shadow distanceを短くしてシャドウマスクの解像度をおもいっきり下げています。 赤いオブジェクトは非Staticなオブジェクトです。

74

shadow distanceを超えると解像度の低いシャドウマスクのシャドウに ブレンドされて切り替わっているのが確認できます。 ベイクされたシャドウとのブレンドは、 ライトマップのあるStaticなオブジェクトでしか有効になりません。 非Staticなオブジェクトの場合はShadow distance以内をリアルタイムなライトで描画し、 それ以降は影が描画されなくなっています。

非StaticなオブジェクトではMixedライトでもSHADOWS_SHADOWMASKキーワードは付きません。 次の画像はそれぞれ非Staticなオブジェクトと Staticなオブジェクトのフレームデバッガです。

76

77

Mixedなライトでも必ずしもシャドウマスクを使うとは限りません。 Shadowmaskを利用しない設定があります。 LightingウィンドウからMixed Lightingの「Lighting Mode」を 「Shadowmask」以外にするとシャドウマスクを使わない方法になります。

78

79

#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_SCREENLIGHTMAP_ONUNITY_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;}

_ShadowCoordzwにスクリーンスペースのポジションを渡しています。 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を小さくする

影がフェードする

デフォルトでは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つのライトにまで対応できました。

title

複数のポイントライトやスポットライトに対応しているのも確認できます。

title

title

フレームデバッガを見てみると加算パスが加算されているのがわかります。

title

マクロの中身

使っているマクロはベースパスと同じです。 ベースパスでは平行光源の場合のみ追いかけたので、 ここでは他の場合についてを見てみることにします。


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

unityShadowCoord4unityShadowCoord4unityShadowCoord4については UnityShadowLibrary.cgincで次のように定義されています。

#define unityShadowCoord float
#define unityShadowCoord2 float2
#define unityShadowCoord3 float3
#define unityShadowCoord4 float4

平行光源のときは空でした。 それ以外の場合はfloat2またはfloat3float4_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のテクスチャです。

title

lightCoordはライトから見た空間の座標のようです。 dot(lightCoord, lightCoord)はライトからフラグメントまでの距離の2乗になります。 これで_LightTexture0からtex2Dで値を取得しています。 多分テクスチャに距離の2乗に反比例する光の減衰が入っているものと思われます。

shadowUNITY_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_4LightPosX0unity_4LightPosY0unity_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ではl=2l=2の項まで扱っているようです。

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というのはl=2l=2までの項の数が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
}

これでより優先度の低いライトや環境光も扱えるようになりました。

94

次のようなシーンを試してみます。 Light Probeを配置してベイクをします。

95

ちゃんと環境光が反映されています。

96

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;
}

SHEvalLinearL0L1SHEvalLinearL2を呼び出しています。

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を利用して球面調和関数を全部頂点シェーダで計算します。

それ以外の場合はl=2l=2の項のみ計算をします。

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の有無で場合分けをしています。 どちらの場合もl=0l=0l=1l=1の項を計算し、l=2l=2の項を足しています。

頂点シェーダで全部計算してしまった場合にはここでは何もしません。

頂点シェーダでl=2l=2の項を計算した場合は、ここでl=0l=0l=1l=1の項を計算します。 こちらも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なオブジェクトで正しく環境光が計算されないようです。

97

98

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_GIUnityPBSLighting.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
}

UnityGlobalIlluminationUnityGlobalIllumination.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_BaseUnityGlobalIllumination.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なオブジェクトで正しく環境光を計算したいならば ここらへんのコードも必要になりそうです。

参考サイト

おわりに

マクロの定義を追いかけたのでちょっと大変でした。 微妙に全部追いかけるのは諦めて解説をすっ飛ばしたところもありますが、 だいたい処理の内容はわかった気がします。

今回の記事のソースコードはGitHubにも上げてあります。 GitHubのリポジトリはこちらです。

  • Unity
  • Shader
新しい投稿
友達とお出かけ