知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【UnityShader】オブジェクトの影を受け取る #108

前回の成果

オブジェクトの影を落とした。

soramamenatan.hatenablog.com


今回やること

前回のコードのままですと、他のオブジェクトからの影を受け取ることができません。
なので、その部分を修正します。

影を受け取れていない

手前のオブジェクトが前回、影を落としたオブジェクトになります。
奥のオブジェクトはUnityのデフォルトのCubeになります。

f:id:soramamenatan:20210613143028p:plain


事前準備

オブジェクトを配置し、配置したオブジェクトの影が当たる位置にオブジェクトを配置します。
影が当たる位置のオブジェクトに今回制作するマテリアルをアタッチします。

f:id:soramamenatan:20210613143140p:plain


ソースコード

Shader "Unlit/ShadowDefault" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader {

        // 通常の描画
        Pass {
            Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlights

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f {
                float2 uv : TEXCOORD0;
                fixed4 diff : COLOR0;
                float4 pos : SV_POSITION;
                SHADOW_COORDS(1)
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                half3 worldNormal = UnityObjectToWorldNormal(v.normal);
                half NdotL = saturate(dot(worldNormal, _WorldSpaceLightPos0.xyz));
                o.diff = NdotL * _LightColor0;
                TRANSFER_SHADOW(o)
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                fixed4 col = tex2D(_MainTex, i.uv);
                fixed4 shadow = SHADOW_ATTENUATION(i);
                return col * i.diff * shadow;
            }
            ENDCG
        }

        // 影の描画
        Pass {
            Tags { "LightMode"="ShadowCaster" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_shadowcaster

            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f {
                // 深度テクスチャではないキューブマップの場合
                // float3 vec : TEXCOORD0;
                // float4 pos : SV_POSITION
                // それ以外の場合
                // float4 pos : SV_POSITION
                V2F_SHADOW_CASTER;
            };

            v2f vert (appdata v) {
                v2f o;
                // 深度テクスチャではないキューブマップの場合
                // o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz;
                // o.pos = UnityObjectToClipPos(v.vertex);
                // それ以外の場合
                // o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal);
                // o.pos = UnityApplyLinearShadowBias(o.pos);
                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                // 深度テクスチャではないキューブマップの場合
                // UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w);
                // それ以外の場合
                // return 0
                SHADOW_CASTER_FRAGMENT(i)
            }
            ENDCG
        }
    }
}

LightMode Tag

Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }

今回はForwardBaseを指定しています。
このキーワードはアンビエント、メインのDirectionalLight、Vertex/SHLight、ライトマップを適応するものになります。
詳しくは以下の記事を参考にしてください。

soramamenatan.hatenablog.com


シェーダーバリアント

#pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlights

シェーダーバリアントとは

コンパイル時に複数のシェーダーコードを自動的に生成したい命令をコンパイラに伝えるものになります。

また、multi_compile_fwdbaseのようなmulti_compileで始まるものは複数のシェーダーバリアントをコンパイルしています。

今回のmulti_compile_fwdbaseでは、コンパイラフォワードレンダリングベースに必要な全てのバリアントをコンパイルしたいことを伝えています。

multi_compileの例
名前 コンパイルするバリアント
multi_compile_fwdbase フォワードレンダリングのベース処理
multi_compile_fwdadd フォワードレンダリングのライト加算処理
multi_compile_fwdadd_fullshadows multi_compile_fwdaddとライトのリアルタイムシャドウ
multi_compile_fog フォグの処理

使用されているバリアントの確認

UnityのInspectorから、使用されているバリアントを確認することができます。
シェーダーを選択肢、Inspectorから
Compile code -> ▼ -> Show
の手順で確認できます。

f:id:soramamenatan:20210615084646p:plain

今回のシェーダーですと、8つのバリアントが使用されているようです。

// Total snippets: 2
// -----------------------------------------
// Snippet #0 platforms ffffffff:
Builtin keywords used: DIRECTIONAL LIGHTPROBE_SH SHADOWS_SHADOWMASK SHADOWS_SCREEN LIGHTMAP_SHADOW_MIXING VERTEXLIGHT_ON

8 keyword variants used in scene:

DIRECTIONAL
DIRECTIONAL LIGHTPROBE_SH
DIRECTIONAL SHADOWS_SCREEN
DIRECTIONAL LIGHTPROBE_SH SHADOWS_SCREEN
DIRECTIONAL VERTEXLIGHT_ON
DIRECTIONAL LIGHTPROBE_SH VERTEXLIGHT_ON
DIRECTIONAL SHADOWS_SCREEN VERTEXLIGHT_ON
DIRECTIONAL LIGHTPROBE_SH SHADOWS_SCREEN VERTEXLIGHT_ON


// -----------------------------------------
// Snippet #1 platforms ffffffff:
Builtin keywords used: SHADOWS_DEPTH SHADOWS_CUBE

2 keyword variants used in scene:

SHADOWS_DEPTH
SHADOWS_CUBE

シェーダーバリアントのコード生成オプション

フォワードレンダリングのバリアントを指定したあとに、いくつかのキーワードが指定されています。

#pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlights

これは、コード生成オプションと呼ばれるものになります。
multi_compileでは複数のバリアントをコンパイルしてしまうため、不要なものが出てきてしまう可能性があります。
予め、不要なものがわかっている場合は、コード生成オプションを指定することで対象のコンパイルをスキップすることができます。

今回使用しているコード生成オプション
名前 意味
nolightmap このシェーダーの全てのライトマッピングのサポートを無効にする
nodirlightmap このシェーダーのDirectional Lightmapのサポートを無効にする
nodynlightmap このシェーダーのDynamic GIを無効にする
novertexlights フォワードレンダリングで、ライトプローブと頂点ライティングを適応しない

他にもいくつかあります。
詳しくは、公式のリファレンスを参照してください。

docs.unity3d.com


組み込みシェーダーのinclude

#include "Lighting.cginc"
#include "AutoLight.cginc"

Lighting.cginc

サーフェスシェーダーにおける標準的な照明モデルになります。
サーフェスシェーダーを書く時には自動的に含まれます。
今回の場合、_LightColor0を使用するためにincludeしています。

AutoLight.cginc

ライティングとシャドウの機能になります。
サーフェスシェーダーの場合、この組み込みシェーダーを内部的に使用しています。
今回の場合、SHADOW_COORDSTRANSFER_SHADOWSHADOW_ATTENUATIONを使用するためにincludeしています。


v2f構造体のSV_POSITION

struct v2f {
    // 省略
    float4 pos : SV_POSITION;
};

影を使用する際には、SV_POSITION名をposにしないとエラーが出てしまいます。

f:id:soramamenatan:20210616090145p:plain

これはvertexからfragmentへ変換を行う際のマクロでposが決め打ちとなっているからになります。
今回ですとTRANSFER_SHADOWでposが決め打ちとなっています。

forum.unity.com

SHADOW_COORDS

struct v2f {
    // 省略
    SHADOW_COORDS(1)
};

SHADOW_COORDSの定義

#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;

引数に応じたunityShadowCoord4型のTEXCOORDを定義しています。
すでに使用しているTEXCOORDは使用できないので注意してください。
今回の場合、uvでTEXCOORD0を指定しているので、SHADOW_COORDSの引数は1にしています。

unityShadowCoord4の定義

#define unityShadowCoord4 float4

float4型となっており、ここに影の情報を入れています。


TRANSFER_SHADOW

v2f vert (appdata v) {
    // 省略
    TRANSFER_SHADOW(o)
}

TRANSFER_SHADOWの定義

#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);

SHADOW_COORDSで定義した、_ShadowCoordにスクリーンスペースの座標を入れています。
ここでposが決め打ちとなっているので、v2f構造体のSV_POSITIONの名前はposである必要があります。


SHADOW_ATTENUATION

fixed4 frag (v2f i) : SV_Target {
    // 省略
    fixed4 shadow = SHADOW_ATTENUATION(i);
}

SHADOW_ATTENUATIONの定義

#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)

バーテックスシェーダーでTRANSFER_SHADOWマクロを用いてスクリーンスペース座標を計算した_ShadowCoordを引数としていれています。

unitySampleShadowの定義

inline fixed unitySampleShadow (float4 shadowCoord)
{
    fixed shadow = tex2Dproj( _ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord) ).r;
    return shadow;
}

tex2Dprojを使用して、オブジェクトにシャドウマップのテクセルを参照しています。
R値を返しているのは、シャドウマップはR値で深度を参照するからになります。

シャドウマップの例

f:id:soramamenatan:20210618090034p:plain

【Unity】シャドウマップを自前で作成する(ライトからの深度をRenderTextureに描画する) - LIGHT11:より引用

tex2Dproj

tex2Dprojは、投影テクスチャマッピングの際に必要なtex2D処理となります。
これは仮想視点での射影変換をしているので、wでの除算が必要になるためになります。

// 元定義
fixed shadow = tex2Dproj( _ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord) ).r;
return shadow;

// tex2Dprojの置き換え
float2 uv = shadowCoord.xy / shadowCoord.w;
fixed shadow = tex2D(_ShadowMapTexture, uv).r
return shadow

UNITY_PROJ_COORD

#if defined(SHADER_API_PSP2)
#define UNITY_BUGGY_TEX2DPROJ4
#define UNITY_PROJ_COORD(a) (a).xyw
#else
#define UNITY_PROJ_COORD(a) a
#endif

SHADER_API_PSP2PlayStation Vitaかを判断するマクロになります。
ですので、ほとんどのプラットフォームの場合、aをそのまま返すだけになります。


シェーダーにコメントを付与

まとめとして、コメントを記載しました。

Shader "Unlit/ShadowDefault" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader {

        // 通常の描画
        Pass {
            Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlights

            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f {
                float2 uv : TEXCOORD0;
                fixed4 diff : COLOR0;
                // 変数名は"pos"固定
                float4 pos : SV_POSITION;
                // unityShadowCoord4型(float4)を定義
                // 影の情報を入れる
                SHADOW_COORDS(1)
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                half3 worldNormal = UnityObjectToWorldNormal(v.normal);
                half NdotL = saturate(dot(worldNormal, _WorldSpaceLightPos0.xyz));
                o.diff = NdotL * _LightColor0;
                // SHADOW_COORDSにスクリーンスペース座標を入れる
                TRANSFER_SHADOW(o)
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                fixed4 col = tex2D(_MainTex, i.uv);
                // シャドウマップのテクセルを反映
                fixed4 shadow = SHADOW_ATTENUATION(i);
                return col * i.diff * shadow;
            }
            ENDCG
        }

        // 影の描画
        Pass {
            Tags { "LightMode"="ShadowCaster" }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_shadowcaster

            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f {
                // 深度テクスチャではないキューブマップの場合
                // float3 vec : TEXCOORD0;
                // float4 pos : SV_POSITION
                // それ以外の場合
                // float4 pos : SV_POSITION
                V2F_SHADOW_CASTER;
            };

            v2f vert (appdata v) {
                v2f o;
                // 深度テクスチャではないキューブマップの場合
                // o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz;
                // o.pos = UnityObjectToClipPos(v.vertex);
                // それ以外の場合
                // o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal);
                // o.pos = UnityApplyLinearShadowBias(o.pos);
                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                // 深度テクスチャではないキューブマップの場合
                // UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w);
                // それ以外の場合
                // return 0
                SHADOW_CASTER_FRAGMENT(i)
            }
            ENDCG
        }
    }
}


結果

手前が今回制作したシェーダーのオブジェクト
奥がUnityのデフォルトシェーダーのオブジェクトになります。
手前のオブジェクトにも影が落ちるようになりました。

f:id:soramamenatan:20210623091736p:plain


今回は以上となります。
ここまでご視聴ありがとうございました。


参考サイト様

qiita.com