🌈

そろそろShaderをやるパート71 影を受けるオクルージョン用のShader

2022/04/08に公開

そろそろShaderをやります

そろそろShaderをやります。そろそろShaderをやりたいからです。
パート100までダラダラ頑張ります。10年かかってもいいのでやります。
100記事分くらい学べば私レベルの初心者でもまあまあ理解できるかなと思っています。

という感じでやってます。

※初心者がメモレベルで記録するので
 技術記事としてはお力になれないかもしれません。

下準備

下記参考
そろそろShaderをやるパート1 Unite 2017の動画を見る(基礎知識~フラグメントシェーダーで色を変える)

デモ

前回のオクルージョン記事の続きです。
【参考リンク】:そろそろShaderをやるパート70 オクルージョン用のShaderを作成する

オクルージョンしつつ影が落ちるようにしてみました。

Shaderサンプル

Shader "Custom/ShadowOcclusion"
{
    Properties
    {
        _ShadowIntensity ("Shadow Intensity", Range (0, 1)) = 0.6
    }

    SubShader
    {
        Pass
        {
            Tags
            {
                "Queue"="geometry-1"
                "LightMode" = "ForwardBase"
            }
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fwdbase

            #include "UnityCG.cginc"
            #include "AutoLight.cginc"
            
            float _ShadowIntensity;
            //グローバル変数
            float _ShadowDistance;

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

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 worldNormal:TEXCOORD0;
                float3 worldPos : WORLD_POS;
                SHADOW_COORDS(1)
            };

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                //影の計算のマクロ
                TRANSFER_SHADOW(o);
                //法線方向のベクトル
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                return o;
            }

            float4 frag(v2f i) : COLOR
            {
                // カメラとオブジェクトの距離(長さ)を取得
                // _WorldSpaceCameraPos:定義済の値 ワールド座標系のカメラの位置
                float cameraToObjLength = clamp(length(_WorldSpaceCameraPos - i.worldPos), 0, _ShadowDistance);
                //1つ目のライトのベクトルを正規化
                float3 L = normalize(_WorldSpaceLightPos0.xyz);
                //ワールド座標系の法線を正規化
                float3 N = normalize(i.worldNormal);
                //内積の結果が0以上なら1 この値を使って裏側の影は描画しない
                float front = step(0, dot(N, L));
                //影の場合0、それ以外は1
                float attenuation = SHADOW_ATTENUATION(i);
                //影の減衰率
                float fade = 1 - pow(cameraToObjLength / _ShadowDistance, _ShadowDistance);
                return float4(0, 0, 0, (1 - attenuation) * _ShadowIntensity * front * fade);
            }
            ENDCG
        }
        
        //影を落とす処理を行うPass
        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
        }
    }
}

オクルージョン処理自体は前回記事と同様です。
影を受ける処理について追記しています。

ただ、単純にSHADOW_ATTENUATIONで影を塗分けるだけでは不十分でした。

角度によっては破綻した見た目になります。

裏側の影は必要ないので、ライトのベクトルと法線を用いて内積を計算し、
裏側の場合は影を描画しないという処理を追加しています。

//1つ目のライトのベクトルを正規化
float3 L = normalize(_WorldSpaceLightPos0.xyz);
//ワールド座標系の法線を正規化
float3 N = normalize(i.worldNormal);
//内積の結果が0以上なら1 この値を使って裏側の影は描画しない
float front = step(0, dot(N, L));

加えて、影の距離減衰の表現も追加しています。

【参考リンク】:そろそろShaderをやるパート14 カメラとの距離を測って使う

BulitInのShaderで影を受けると、"影の描画距離の設定"に応じて減衰していきます。

何もしない場合、下記の比較画像の上側の画像のようにくっきりと描画され、
一定の距離になった途端に消えてしまいます。
下側のようにぼやけながら消えていく方が都合良い場面が多いかと思います。

【引用元】:Shadow distance rendering in a custom shader

Shader.SetGlobalFloat

そこで、"QualitySettingsの影の描画距離"をShaderへ反映する手法を取りました。
Shader.SetGlobalFloatを利用することで、C#側から指定した名前と同一のShader内の変数を一括で変更できます。

Shader版のグローバル変数のようなイメージです。

C#スクリプト

using UnityEngine;

/// <summary>
/// 影の描画距離をShaderのグローバル変数で設定する
/// </summary>
[ExecuteAlways]
public class SetShadowDistance : MonoBehaviour
{
    private void Start()
    {
        Shader.SetGlobalFloat("_ShadowDistance",QualitySettings.shadowDistance); 
    }

    void Update()
    {
        //設定値をリアルタイムに反映するのはEditor上だけで良い
        if(!Application.isEditor) return;
        Shader.SetGlobalFloat("_ShadowDistance",QualitySettings.shadowDistance); 
    }
}

追記

どうやら影を落とす処理も書かないと影を受けられない?可能性があったので追記しました。

参考リンク

UnityにはShaderのグローバル変数が存在する
QualitySettings.shadowDistance
【AR Foundation】モデルに影をつける
【Unity】影だけ映る地面を用意し、かつ地面の下が見えないようにする
ARで影を描画する(影だけをレンダリングするシェーダ)

Discussion