【Unity】【レイトレ】Unityでレイトレーシング入門④ レイを反射させる

レイトレーシングの概念を理解するためにUnityでレイトレしてみるシリーズ第四弾。
今回は前回の描画結果に対してレイを反射させて金属反射のような表現をしてみます。

Unity2018.4

この記事は第三回の続きです

このシリーズはほとんど、以下のとても分かりやすいレイトレのチュートリアルを参考にさせていただき、
自分の中でしっかり理解することを目的としてUnityで実装しなおしただけのものです。

qiita.com

上の記事の方がしっかり解説されていますのでUnityのコードが必要ない方は上の記事を読むことをお勧めします。

さて、前回はレイトレーシングで複数の球体と平面を描画しました。

light11.hatenadiary.com

本記事では前回の実装を元に実装を行いますので、必要に応じて前回の記事を参照してください。

またこのシリーズはこの回で終了となります。

衝突判定関数を修正する

レイを反射させるために、まずは衝突判定の関数を修正します。

struct Intersection {
    int hit;
    float3 hitPoint;
    float3 normal;
    float distance;
    float3  color;
    float3 rayDirection;
};

// レイと球との当たり判定
void intersectSphere(Ray ray, Sphere sphere, inout Intersection i)
{
    float3 center = sphere.center;
    center -= ray.origin;

    float a = dot(ray.direction, ray.direction);
    float b = dot(ray.direction, center);
    float c = dot(center, center) - sphere.radius * sphere.radius;

    if (a == 0.0f) {
        // レイの長さがゼロ
        return;
    }

    float s = b * b - a * c;
    if (s < 0.0f) {
        // 衝突していない
        return;
    }

    s = sqrt(s);
    float a1 = (b - s) / a;

    if (a1 < 0.0f) {
        // レイの反対側で衝突
        return;
    }

    if (a1 >= i.distance)
    {
        // 既にヒットしている位置より遠い場合
        return;
    }

    i.hit++;
    // レイの射出点から近いほうの交点
    i.hitPoint = ray.origin + a1 * ray.direction;
    // 法線
    i.normal = i.hitPoint - sphere.center;
    i.normal = normalize(i.normal);
    i.distance = a1;
    i.color = sphere.color;
    i.rayDirection = ray.direction;

    return;
}

// レイと平面との当たり判定
void intersectPlane(Ray ray, Plane plane, inout Intersection i)
{
    float h = dot(plane.normal, plane.position);
    float t = (h - dot(plane.normal, ray.origin)) / dot(plane.normal, ray.direction);

    if (t < 0)
    {
        return;
    }

    if (t >= i.distance)
    {
        return;
    }

    i.hit++;
    i.hitPoint = ray.origin + normalize(ray.direction) * t;
    i.normal = plane.normal;
    i.distance = t;
    i.color = plane.color;
    i.rayDirection = ray.direction;
}

まずIntersection構造体のhit変数は、反射回数を表すようにint型に変更しています。
また衝突した結果のレイは次の反射方向のために使用するので、rayDirection変数を追加しています。

そして球と平面それぞれの衝突判定関数でこれらに値を代入しています。

レイを反射させる

次にこれを使ってフラグメントシェーダでレイを反射させます。
例のごとく以下にシェーダの全文を記載します。

Shader "RayTracing"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            float3 lightDir = float3(1.0, 1.0, 1.0);

            struct Ray {
                float3 origin;
                float3 direction;
            };

            struct Sphere {
                float radius;
                float3  center;
                float3  color;
            };

            struct Plane {
                float3 position;
                float3 normal;
                float3 color;
            };

            struct Intersection {
                int hit;
                float3 hitPoint;
                float3 normal;
                float distance;
                float3  color;
                float3 rayDirection;
            };

            // レイと球との当たり判定
            void intersectSphere(Ray ray, Sphere sphere, inout Intersection i)
            {
                float3 center = sphere.center;
                center -= ray.origin;

                float a = dot(ray.direction, ray.direction);
                float b = dot(ray.direction, center);
                float c = dot(center, center) - sphere.radius * sphere.radius;

                if (a == 0.0f) {
                    // レイの長さがゼロ
                    return;
                }

                float s = b * b - a * c;
                if (s < 0.0f) {
                    // 衝突していない
                    return;
                }

                s = sqrt(s);
                float a1 = (b - s) / a;

                if (a1 < 0.0f) {
                    // レイの反対側で衝突
                    return;
                }

                if (a1 >= i.distance)
                {
                    // 既にヒットしている位置より遠い場合
                    return;
                }

                i.hit++;
                // レイの射出点から近いほうの交点
                i.hitPoint = ray.origin + a1 * ray.direction;
                // 法線
                i.normal = i.hitPoint - sphere.center;
                i.normal = normalize(i.normal);
                i.distance = a1;
                i.color = sphere.color;
                i.rayDirection = ray.direction;

                return;
            }

            // レイと平面との当たり判定
            void intersectPlane(Ray ray, Plane plane, inout Intersection i)
            {
                float h = dot(plane.normal, plane.position);
                float t = (h - dot(plane.normal, ray.origin)) / dot(plane.normal, ray.direction);

                if (t < 0)
                {
                    return;
                }

                if (t >= i.distance)
                {
                    return;
                }

                i.hit++;
                i.hitPoint = ray.origin + normalize(ray.direction) * t;
                i.normal = plane.normal;
                i.distance = t;
                i.color = plane.color;
                i.rayDirection = ray.direction;
            }

            void vert(float4 vertex : POSITION, out float4 position : SV_POSITION)
            {
                position = UnityObjectToClipPos(vertex);
            }

            fixed4 frag(UNITY_VPOS_TYPE vpos : VPOS) : SV_Target
            {
                float3 lightDir = normalize(float3(1.0, -1.0, 1.0) * -1);
                // 縦横のうち短いほうが-1~1になるような値を計算する
                float2 pos = (vpos.xy * 2.0 - _ScreenParams.xy) / min(_ScreenParams.x, _ScreenParams.y);
                // プラットフォームの違いを吸収
                #if UNITY_UV_STARTS_AT_TOP
                    pos.y *= -1;
                #endif

                // レイを定義
                Ray ray;
                ray.origin = float3(0.0, 0.0, -50.0);
                ray.direction = normalize(float3(pos.x, pos.y, 6.0));
                // 球を定義
                Sphere sphere1;
                sphere1.radius = 3.0;
                sphere1.center = float3(-6.0, 0.0, 0.0);
                sphere1.color = float3(1.0, 0.2, 0.2);
                Sphere sphere2;
                sphere2.radius = 2.0;
                sphere2.center = float3(0.0, 0.0, 0.0);
                sphere2.color = float3(0.2, 1.0, 0.2);
                Sphere sphere3;
                sphere3.radius = 3.0;
                sphere3.center = float3(6.0, 0.0, 0.0);
                sphere3.color = float3(0.2, 0.2, 1.0);
                // 平面を定義
                Plane plane;
                plane.position = float3(0.0, -3.0, 0.0);
                plane.normal = float3(0.0, 1.0, 0.0);
                plane.color = float3(0.5, 0.5, 0.5);

                float3 color = float3(0.1, 0.1, 0.1);

                // 当たり判定
                float3 tempColor = 1.0;
                Intersection i = (Intersection)0;
                i.distance = 1.0e+30; // 大きい値を入れておく

                intersectSphere(ray, sphere1, i);
                intersectSphere(ray, sphere2, i);
                intersectSphere(ray, sphere3, i);
                intersectPlane(ray, plane, i);
                if (i.hit > 0) {
                    float diffuse = max(0, dot(lightDir, i.normal));
                    float3 diffuseColor = i.color * diffuse;
                    color = diffuseColor;
                    tempColor *= diffuseColor;

                    for (int j = 1; j < 2; j++)
                    {
                        // 同じ物体にまた当たらないように少し法線方向に押し出す
                        ray.origin = i.hitPoint + i.normal * 0.0001;
                        ray.direction = reflect(i.rayDirection, i.normal);
                        i.distance = 1.0e+30;

                        intersectSphere(ray, sphere1, i);
                        intersectSphere(ray, sphere2, i);
                        intersectSphere(ray, sphere3, i);
                        intersectPlane(ray, plane, i);
                        if (i.hit > j)
                        {
                            diffuse = max(0, dot(lightDir, i.normal));
                            diffuseColor = i.color * diffuse;
                            color += tempColor * diffuseColor;
                            tempColor *= diffuseColor;
                        }
                    }
                }

                return float4(color, 1.0);
            }
            ENDCG
        }
    }
}

衝突判定周りの処理が前回から大きく変わりました。

衝突判定の結果として取得できるrayDirectionの反射方向をreflect関数で求めています。
そしてこの方向に再びレイを飛ばしてレイトレーシングします。

得られた色をかけ合わせることで反射色が得られます。

結果

レンダリング結果は次のようになります。

f:id:halya_11:20190717002847p:plain

正常に反射が描画されていることが確認できました。

関連

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com

参考

qiita.com