【Unity】【レイトレ】Unityでレイトレーシング入門① 球を表示してライティングする

レイトレーシングの概念を理解するためにUnityでレイトレしてみます。
今回は球を表示して簡単なライティングを行うところまで実装します。

Unity2018.4

はじめに

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

qiita.com

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

レイトレーシングとは?

レイトレーシングは視点からスクリーンに対してレイを飛ばし、
視点からの経路をトレースすることで物体を描画するレンダリング方法です。

レイトレーシングの概念として、まず視点の位置をスクリーンよりも後ろ側に定義して、
それとは反対側にオブジェクトを定義します。

f:id:halya_11:20190715231352p:plain

次に視点からスクリーンの各ピクセルに向けてレイを飛ばします。

f:id:halya_11:20190715231510p:plain

そしてこれらのレイと、オブジェクトとの衝突判定を取ります。
オブジェクトと衝突があった場合には、そのレイが通ったスクリーン上のピクセルにオブジェクトの色を描画します。
これを繰り返すことでオブジェクトを描画できます。

f:id:halya_11:20190715232344p:plain

これがレイトレーシングの基本的な仕組みです。
(本当はここからさらにレイを跳ね返らせて計算を続けたりしますが・・)
以降、これをUnityで実装する方法を紹介していきます。

準備

まずは準備としてベースとなる仕組みを作ります。
画面全体に対して効果を掛けるシェーダを書きたいので、Unityのポストエフェクトの仕組みを使います。

まずはポストエフェクト用のスクリプトを作ります。

using UnityEngine;

[ExecuteInEditMode]
public class PostEffect : MonoBehaviour {

    [SerializeField]
    private Shader _shader = default;

    private Material _material;

    private void OnRenderImage(RenderTexture source, RenderTexture dest){
        if (_shader != null) {
            if (_material == null) {
                _material = new Material(_shader);
            }
            // Materialを使って描画
            Graphics.Blit(source, dest, _material);
        }
        else {
            // 何もしない
            Graphics.Blit(source, dest);
        }
    }
}

このあたりの技術的詳細については以下の記事を参照してください。

light11.hatenadiary.com

次にシェーダのベースを作ります。

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

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

            fixed4 frag(UNITY_VPOS_TYPE vpos : VPOS) : SV_Target
            {
                return 1;
            }
            ENDCG
        }
    }
}

頂点シェーダでは今回は何も行わず、処理は主にフラグメントシェーダで行います。
レイトレーシングにはスクリーン座標を使うため、VPOSによりスクリーン座標を使えるようにしています。

レイと球の構造体を追加して衝突判定関数を書く

次にシェーダにレイと球の構造体を定義します。

struct Ray {
    float3 origin;
    float3 direction;
};

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

レイは起点と方向を持ちます。
球は半径と中心座標、そして色を持たせておきます。

次にこれらの衝突判定を行う関数を追加します。

struct Intersection {
    bool hit;
    float3 hitPoint;
    float3 normal;
    float3 color;
};

// レイと球との当たり判定
Intersection intersectSphere(Ray ray, Sphere sphere)
{
    Intersection i = (Intersection)0;
    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 i;
    }

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

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

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

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

    return i;
}

交差状況を表すIntersection構造体を定義し、衝突があった場合には衝突した点とその点における法線と色を返します。
レイと球との衝突判定アルゴリズムは下記のサイトを参考にさせていただきました。

marupeke296.com

これで準備は整いました。
あとは例を飛ばすだけです。

フラグメントシェーダでレイを飛ばす

さてそれではいよいよレイを飛ばします。

ちょっと長くなりますが見やすいように、シェーダ全文を記載しておきます。
前節からの変更点はフラグメントシェーダのみです。

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 Intersection {
                bool hit;
                float3 hitPoint;
                float3 normal;
                float3 color;
            };

            // レイと球との当たり判定
            Intersection intersectSphere(Ray ray, Sphere sphere)
            {
                Intersection i = (Intersection)0;
                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 i;
                }

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

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

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

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

                return i;
            }

            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, -35.0);
                ray.direction = normalize(float3(pos.x, pos.y, 6.0));
                Sphere sphere;
                sphere.radius = 2.0;
                sphere.center = float3(0.0, 0.0, 0.0);
                sphere.color = float3(1.0, 0.2, 0.2);

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

                // 当たり判定
                Intersection i = intersectSphere(ray, sphere);
                if (i.hit) {
                    // 当たっていたらDiffuseを計算
                    float diffuse = max(0, dot(lightDir, i.normal));
                    color = sphere.color * diffuse;
                }

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

細かい説明はフラグメントシェーダのコメントとして記載しました。
前節で定義した衝突判定を元に判定を行い、衝突があった場合には法線を使ってDiffuse色を計算しています。

結果

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

f:id:halya_11:20190715233351p:plain

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

参考

marupeke296.com

qiita.com