やぎりのブログ

やぎりのブログです。ゲーム制作、プログラミング、エフェクトのことについて書いていきます。

固定長進行レイマーチングやってみたので簡単なサンプルと解説

※この記事で扱っている、固定長レイマーチングで不透明のオブジェクトを描画する手法はレアな手法です。

通常のUnityでのレイマーチング(スフィアトレーシング)の概要を知りたい方は、以下の記事、書籍がおすすめです。

gurutaka-log.com

scrapbox.io

booth.pm

概要

f:id:yagiri000:20180912095045g:plain

最近レイトレーシング専用回路を持つGPUNVIDIAから出たり,何かとレイトレーシングが流行っていたのでレイトレーシングの一種であるレイマーチングをやってみました.

本記事のサンプルプロジェクトは以下になります.比較的重くなりやすい処理なので,描画負荷には注意してください.

↓サンプルプロジェクト

github.com

目次

主な原理

レイトレーシングは,一般的に使われるラスタライズレンダリングとは異なり,レイを飛ばしてピクセルの色を決める手法です.レイを飛ばし,何かに当たった後も物体の物理特性(光がどう跳ね返るか)に応じてレイを反射,屈折させることを繰り返し,リアルな描画を行います,レイマーチングはその一種です.

f:id:yagiri000:20180911152813j:plain 出典:Chapter1. レイトレーシング法とは何か | The Textbook of RayTracing @TDU

今回のレイマーチングでは,それぞれのピクセルからレイを飛ばして,レイを一定距離ずつ動かして,自分の指定した形状に当たったらレイを反射せず即描画(そのピクセルの色を塗る)します.*1

球を描画

f:id:yagiri000:20180911153121p:plain

サンプルプロジェクトの,RayMarchingSphereシーンについて解説していきます.このシーンでは,レイマーチングによってQuadの表面に球を描画しています.

処理手順は以下です.

  1. Quadのそれぞれのピクセルのワールド座標(インスペクタのPositionで見られる座標系と同じ座標系)を求める.
  2. ピクセルのワールド座標とカメラのワールド座標から,レイが進む方向を決定する.
  3. ピクセルのワールド座標からレイを飛ばし,少し移動させる.
  4. 移動後,その場所の形状関数から,オブジェクトに当たったか判定する.当たってなかったら3. に戻る.
  5. オブジェクトに当たったらそのピクセルの色を決定する.一定回数進んで当たらなかったらそのピクセルを黒とする.

手順2では,図に示すように,ピクセルとカメラの位置からレイの飛ぶ方向を決めます.一般的にはカメラからレイを飛ばしますが,今回ではピクセルの位置からレイを飛ばしています.これにより描画元メッシュ形状に応じた断面の描画を可能にしています.

f:id:yagiri000:20180911151420p:plain

手順3,4では,少しだけレイを進めることを繰り返します.

f:id:yagiri000:20180911152208p:plain

形状関数isInObjectはfloat3(座標)を取り,オブジェクト内かをboolで返す関数です.この関数によって形状が決まります.*2 今回は,原点から一定距離の時trueを返すので,オブジェクトの形は球です.

bool isInObject(float3 pos) {
    return distance(pos, float3(0.0, 0.0, 0.0)) < _Threshold;
}

コード全体は以下になります.

Shader "Custom/RayMarchingSphere"
{
    Properties
    {
        _Threshold("Threshold", Range(0.0,3.0)) = 0.6 // sliders
    }
    SubShader
    {
        Tags{ "Queue" = "Transparent" }
        LOD 100

        Pass
        {
            ZWrite On
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

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

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

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

            float _Threshold;

            // 座標がオブジェクト内か?を返し,形状を定義する形状関数
            // 形状は原点を中心とした球.
            // 原点から一定の距離内の座標に存在するので球になる.
            bool isInObject(float3 pos) {
                return distance(pos, float3(0.0, 0.0, 0.0)) < _Threshold;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 col;

                // 初期の色(黒)を設定
                col.xyz = 0.0;
                col.w = 1.0;

                // レイの初期位置
                float3 pos = i.pos.xyz; 

                // レイの進行方向
                float3 forward = normalize(pos.xyz - _WorldSpaceCameraPos); 

                // レイが進むことを繰り返す.
                // オブジェクト内に到達したら進行距離に応じて色決定
                // 当たらなかったらそのまま(今回は黒)
                const int StepNum = 30;
                const float MarchingDist = 0.03;
                for (int i = 0; i < StepNum; i++) {
                    if (isInObject(pos)) {
                        col.xyz = 1.0 - i * 0.02;
                        break;
                    }
                    pos.xyz += MarchingDist * forward.xyz;
                }

                return col;
            }
            ENDCG
        }
    }
}

o.posはVertex Shader内では頂点の数しか計算されませんが(Quadだったら三角形2つで6回),ピクセルシェーダー内で補間されます.つまり,最終的にスクリーンに映ったピクセルごとにワールド座標o.posが求められます.

描画結果は以下のようになります. f:id:yagiri000:20180911153121p:plain

マテリアルプロパティの,Thresholdを変えると球の大きさが変わることが確認できます.

f:id:yagiri000:20180911154015p:plain

SampleSphereシーンのQuadを横に移動させると,球が同じ位置に描画されていることが確認できます.

f:id:yagiri000:20180911153757p:plain

様々な形状描画

存在関数や,メッシュの形状を工夫すると様々な形状を描画できます.

02_RayMarchingCubes f:id:yagiri000:20180911154242p:plain 等間隔に並ぶ立方体が無限に存在します.xyz各軸に等間隔に存在/非存在するので結果として立方体が並んでいます.

形状関数は以下のようになります.

bool isInObject(float3 pos) {
    const float PI2 = 6.28318530718;
    return 
        sin(PI2 * _Freqency * pos.x) < _Threshold && 
        sin(PI2 * _Freqency * pos.y) < _Threshold && 
        sin(PI2 * _Freqency * pos.z) < _Threshold;
}

alphaの値をデフォルトで0,レイがヒットしたら距離に応じた適当な値にしました.

col.w = pow(col.x, 0.3);

03_RayMarchingDistFromTransformPosition f:id:yagiri000:20180911154526p:plain

一定距離ごとに存在/非存在する形です.

形状関数は以下のようになります.

bool isInObject(float3 pos) {
    const float PI2 = 6.28318530718;
    float dist = length(pos);
    return sin(PI2 * dist * _Freqency + PI2 * _Time * _TimeRate) < _Threshold;
}

メッシュはCubeを使い,どの方向からも見えるようにしました.

_Time.yで時間を取得できます.

pos.x += _Time.y * _TimeRate;

Cubeを移動させても形状の中心をCubeの中心と一致させたかったため,形状関数に渡す座標の原点をCubeの座標にしました.

// Transfrom.positionを形状関数に渡す座標の原点に設定する
if (isInObject(pos-transformPos)) {
    col.xyz = 1.0 - i * 0.01;
    col.w = col.x;
    break;
}

以下のようにすることで,Transfromの座標を取得しています.

o.transformPos = mul(unity_ObjectToWorld, float4(0.0, 0.0, 0.0, 1.0));

04_RayMarchingSinPosPlusSinDist f:id:yagiri000:20180911155053p:plain

名状しがたい形状が無限に存在します. このように形状関数の形を複雑にすると複雑な形を表現できます.

bool isInObject(float3 pos) {
    pos.x += _Time * _TimeRate;
    pos.xyz *= _Scale;
    float dist = sin(pos.x) * sin(pos.y) * sin(pos.z);
    dist = pow(dist, 2);
    float sxsy = sin(pos.x) * sin(pos.y);
    float sxsz = sin(pos.x) * sin(pos.z);
    float szsy = sin(pos.z) * sin(pos.y);
    return sin(dist*_DistScale) + sin(sxsy * _FreqScale) + sin(sxsz * _FreqScale) + sin(szsy * _FreqScale) < _Threshold;
}

描画負荷

画面上のピクセル数 × レイの進行回数(StepNum) × 形状関数isInObjectの複雑さ が大まかな処理の重さになると思われます.つまり,StepNumを大きな数にしたり,「重めの」レイトレオブジェクトにカメラを近づけスクリーンに映る領域(ピクセル数)が大きくなると重くなります.

さいごに

最後まで読んでいただきありがとうございました.もしよく分からない部分があっても,形状関数をいじるといろいろな形状ができると思うので,ぜひオリジナル形状を作ってみてください.皆さんが作ったレイマーチングオブジェクトを見る日を楽しみにしております.

VRChatでレイマーチングを行いたい方向けサンプル

yagiri.booth.pm

参考文献

三葉レイのCG技術チャンネル - YouTube

参考になるレイマーチングの記事

wgld.org | GLSL: シェーダ内でレイを定義する |

Unity でオブジェクトスペースのレイマーチをやってみた - 凹みTips

Unity でオブジェクトスペースの Raymarching をフォワードレンダリングでやってみた - 凹みTips

*1:ちなみに,距離関数を使ってオブジェクトとの距離に応じて進む距離を変えるスフィアトレーシングのほうが一般的です.

*2:形状を定義する関数なのでこの記事では形状関数と呼びます.