知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【UnityShader】レイマーチング入門【4】 #35

前回の成果

レイマーチングでライティングができた

soramamenatan.hatenablog.com


今回やること

前回制作したものにソフトシャドウを実装してみます

gurutaka-log.com


ソフトシャドウとは、

大きさを持つ光源や、間接光により生じる影である。

影はまったく光があたらない影と一部の光がさえぎられて生じる影に分類され、前者を本影、後者を半影という。

この半影を含む影をソフトシャドーという。すなわち影の境界がくっきりしていない影である。

ソフトシャドー - Wikipedia:より引用

のことです。


事前準備

Scene上にQuadを配置します。
この際にTransformを画像の通りにしてください。
f:id:soramamenatan:20200106160246p:plain

CameraとLightもQuadと同じように配置します。
f:id:soramamenatan:20200106160414p:plainf:id:soramamenatan:20200106160418p:plain


ソースコード

Shader "Unlit/softShadowRaymarching" {
    Properties {
        _Radius("Radius", Range(0.0, 1.0)) = 0.3
        _BlurShadow("BlurShadow", Range(0.0, 50.0)) = 16.0
    }
    SubShader {
        Tags{ "Queue" = "Transparent" "LightMode"="ForwardBase"}
        LOD 100

        Pass {
            ZWrite On
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

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

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

            struct v2f {
                float2 uv : TEXCOORD0;
                float4 pos : POSITION1;
                float4 vertex : SV_POSITION;
            };

            float _Radius;
            float _BlurShadow;

            float sphere(float3 pos) {
                return length(pos) - _Radius;
            }

            float plane(float3 pos) {
                float4 n = float4(0.0, 0.8, 0.0, 1);
                return dot(pos, n.xyz) + n.w;
            }

            float getDist(float3 pos) {
                return min(plane(pos), sphere(pos));
            }

            float3 getNormal(float3 pos) {
                float d = 0.001;
                return normalize(float3(
                    getDist(pos + float3(d, 0, 0)) - getDist(pos + float3(-d, 0, 0)),
                    getDist(pos + float3(0, d, 0)) - getDist(pos + float3(0, -d, 0)),
                    getDist(pos + float3(0, 0, d)) - getDist(pos + float3(0, 0, -d))
                ));
            }

            float genShadow(float3 pos, float3 lightDir) {
                float marchingDist = 0.0;
                float c = 0.001;
                float r = 1.0;
                float shadowCoef = 0.5;
                for (float t = 0.0; t < 50.0; t++) {
                    marchingDist = getDist(pos + lightDir * c);
                    if (marchingDist < 0.001) {
                        return shadowCoef;
                    }
                    r = min(r, marchingDist * _BlurShadow / c);
                    c += marchingDist;
                }
                return 1.0 - shadowCoef + r * shadowCoef;
            }

            v2f vert(appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.pos = mul(unity_ObjectToWorld, v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                float3 pos = i.pos.xyz;
                float3 rayDir = normalize(pos.xyz - _WorldSpaceCameraPos);
                const int StepNum = 30;

                for (int i = 0; i < StepNum; i++) {
                    float marchingDist = getDist(pos);
                    if (marchingDist < 0.001) {
                        float3 lightDir = _WorldSpaceLightPos0.xyz;
                        float3 normal = getNormal(pos);
                        float3 lightColor = _LightColor0;
                        float shadow = genShadow(pos + normal * 0.001, lightDir);
                        fixed4 col = fixed4(lightColor * max(dot(normal, lightDir), 0) * max(0.5, shadow), 1.0);
                        col.rgb += fixed3(0.2f, 0.2f, 0.2f);
                        return col;
                    }
                    pos.xyz += marchingDist * rayDir.xyz;
                }
                return 0;
            }
            ENDCG
        }
    }
}

前回のライティングの処理に影の処理を追加しました。


plane関数

float plane(float3 pos) {
    float4 n = float4(0.0, 0.8, 0.0, 1);
    return dot(pos, n.xyz) + n.w;
}

これは、Planeを描画するための距離函数です。

nを引数に持たせることも出来ますが、その際は必ず正規化してください。


この関数で求めることができる理由

下記の画像の記号を

 \displaystyle
\vec{n} = (p, q, r)
\\ 点P(x, y, z)
\\ 点A(x_0, y_0, z_0)

と定義します。

f:id:soramamenatan:20200108150644p:plain

平面の方程式とその3通りの求め方 | 高校数学の美しい物語:より引用

そして、平面の公式が

 \displaystyle
p(x-x_0)+q(y-y_0)+r(z-z_0)=0

なので、

 \displaystyle
p_x + q_y + r_z + D=0

と置くことができます。
これをソースコードに移し換えると、

dot(p,n.xyz) + n.w

となるので、求めることができます。

また、nは

float4 n = float4(x方向の傾き, y方向の傾き, z方向の傾き, 高さ(増やすと低くなる)

となっています。
正直ちょっと怪しいけど多分あってる

mathinsight.org

qiita.com


getDist関数

float getDist(float3 pos) {
    return min(plane(pos), sphere(pos));
}

minを使用することによって、OR合成を行っています。
またmaxを使用することで、AND合成を行えます。

簡単にいうと、minを使用することにより2つの図形を合成することができます。
詳しくは下記の画像を参照してください。

f:id:soramamenatan:20200108165342j:plain

シェーダだけで世界を創る!three.jsによるレイマーチング:より引用


影の落とし方

genShadow関数で影を描画しているのですが、まずはレイマーチングで影をどう描画するかの考え方から学んでいきます。


影ができる条件は、光源とオブジェクトの間に何か他のオブジェクトがある時です。
つまり、オブジェクトから光源に向かってレイを飛ばし、レイが光源以外にヒットした時に影ができます

f:id:soramamenatan:20200109120013j:plain

wgld.org | GLSL: レイマーチングソフトシャドウ |:より引用


genShadow関数

ソースコードの説明に入ります。

float genShadow(float3 pos, float3 lightDir) {
    float marchingDist = 0.0;
    float c = 0.001;
    float r = 1.0;
    float shadowCoef = 0.5;
    for (float t = 0.0; t < 50.0; t++) {
        marchingDist = getDist(pos + lightDir * c);
        if (marchingDist < 0.001) {
            return shadowCoef;
        }
        r = min(r, marchingDist * _BlurShadow / c);
        c += marchingDist;
    }
    return 1.0 - shadowCoef + r * shadowCoef;
}

オブジェクトから光源に向かって、レイを飛ばしてマーチングループをしないといけないので、for文を回しています。


ヒット時

if (marchingDist < 0.001) {
    return shadowCoef;
}

ここは、単に影の係数を返しているだけです。


非ヒット時

for (float t = 0.0; t < 50.0; t++) {
    marchingDist = getDist(pos + lightDir * c);
    // if (marchingDist < 0.001) {
    //     return shadowCoef;
    // }
    r = min(r, marchingDist * _BlurShadow / c);
    c += marchingDist;
}
return 1.0 - shadowCoef + r * shadowCoef;

ヒット時の処理は、一時的にコメントアウトしています。

注目して欲しいのは、マーチングループをすればするほど、cが増えrが減ることです。


反影と本影

getDist(pos + lightDir * c)

cとライトの方向をposに加えることにより、光源を考慮したヒット判定がでます。
しかし、そのヒット判定はオブジェクトの本影ではなく、反影なので、rを用いて影の色を薄くしています。
本影と反影のイメージは下記の画像を見てくださるとわかりやすいです。

f:id:soramamenatan:20200111142334j:plain

電球に詳しい人教えてください!! - Goo知恵袋:より引用

これにより、影のぼかしができます。

ぼかしなし

f:id:soramamenatan:20200111141359p:plain

ぼかしあり

f:id:soramamenatan:20200111141406p:plain


genShadow関数の引数

引数にposだけでなく、

genShadow(pos + normal * 0.001, lightDir);

法線を小さくしたものを足しています。

これはレイがオブジェクトにめりこんでしまうのを防ぐためです。
めりこんでしまうと、本来であれば影が発生しないところで発生してしまう可能性があります。

f:id:soramamenatan:20200111143226j:plain

wgld.org | GLSL: レイマーチングソフトシャドウ |:より引用

引数に法線を渡さなかった場合の影

f:id:soramamenatan:20200111142903p:plain


結果

下記の画像のように下のPlaneに球の影が落ちれば成功です。

f:id:soramamenatan:20200111143331p:plain


ソースコードにコメントを付与

Shader "Unlit/softShadowRaymarching" {
    Properties {
        _Radius("Radius", Range(0.0, 1.0)) = 0.3
        _BlurShadow("BlurShadow", Range(0.0, 50.0)) = 16.0
    }
    SubShader {
        Tags{ "Queue" = "Transparent" "LightMode"="ForwardBase"}
        LOD 100

        Pass {
            ZWrite On
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

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

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

            struct v2f {
                float2 uv : TEXCOORD0;
                float4 pos : POSITION1;
                float4 vertex : SV_POSITION;
            };

            // 球の大きさ
            float _Radius;
            // ブラーの強さ
            float _BlurShadow;

            // 中心との距離から球を描画
            float sphere(float3 pos) {
                return length(pos) - _Radius;
            }

            // planeの描画
            float plane(float3 pos) {
                // planeの傾き
                float4 n = float4(0.0, 0.8, 0.0, 1.0);
                return dot(pos, n.xyz) + n.w;
            }

            // planeと球との距離
            float getDist(float3 pos) {
                return min(plane(pos), sphere(pos));
            }

            // 法線を取得
            float3 getNormal(float3 pos) {
                // δ
                float d = 0.001;
                // 法線の公式より、各変数の偏微分から計算
                return normalize(float3(
                    getDist(pos + float3(d, 0, 0)) - getDist(pos + float3(-d, 0, 0)),
                    getDist(pos + float3(0, d, 0)) - getDist(pos + float3(0, -d, 0)),
                    getDist(pos + float3(0, 0, d)) - getDist(pos + float3(0, 0, -d))
                ));
            }

            // 光源に向かってレイを飛ばす
            float genShadow(float3 pos, float3 lightDir) {
                float marchingDist = 0.0;
                float c = 0.001;
                float r = 1.0;
                float shadowCoef = 0.5;
                for (float t = 0.0; t < 50.0; t++) {
                    marchingDist = getDist(pos + lightDir * c);
                    // hitしたら影を落とす
                    if (marchingDist < 0.001) {
                        return shadowCoef;
                    }
                    // 反影の計算
                    r = min(r, marchingDist * _BlurShadow / c);
                    c += marchingDist;
                }
                // hitしなかった場合、反影を描画
                return 1.0 - shadowCoef + r * shadowCoef;
            }

            v2f vert(appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.pos = mul(unity_ObjectToWorld, v.vertex);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                float3 pos = i.pos.xyz;
                // レイのベクトル
                float3 rayDir = normalize(pos.xyz - _WorldSpaceCameraPos);
                const int StepNum = 30;

                for (int j = 0; j < StepNum; j++) {
                    // レイを進める距離
                    float marchingDist = getDist(pos);
                    // 衝突検知
                    if (marchingDist < 0.001) {
                        float3 lightDir = _WorldSpaceLightPos0.xyz;
                        float3 normal = getNormal(pos);
                        float3 lightColor = _LightColor0;
                        // レイがオブジェクトにめり込むのを防ぐ
                        float shadow = genShadow(pos + normal * 0.001, lightDir);
                        // 内積によって色を変化させる
                        fixed4 col = fixed4(lightColor * max(dot(normal, lightDir), 0) * max(0.5, shadow), 1.0);
                        // 環境光のオフセット
                        col.rgb += fixed3(0.2f, 0.2f, 0.2f);
                        return col;
                    }
                    // レイを進める
                    pos.xyz += marchingDist * rayDir.xyz;
                }
                // stepNum回レイを進めても衝突しなかったらピクセルを透明にする
                return 0;
            }
            ENDCG
        }
    }
}

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