rn.log

備忘録など

【シェーダーグラフメモ その18】レイマーチングで遊んでみた

レイマーチング(ray marching)と呼ばれる手法を用いて以下のような絵を作ってみました。
f:id:r-ngtm:20181204230928g:plain

目次

サンプル

github.com

レイマーチングのざっくりとした解説

カメラ位置、カメラ向き、UV座標からレイを求め、レイがオブジェクトとぶつかるかどうかを判定します。
オブジェクトにレイがぶつかった場合は色を付ける、そうでない場合は色をつけないという描画を行います。
f:id:r-ngtm:20181204232139p:plain
※ここでいうカメラはレイマーチング計算に利用する視点のことを指しており、Unityのシーンカメラとは別のものです

f:id:r-ngtm:20181204232123p:plain:w240

準備 : レイマーチングを行うカスタムノードの作成

シェーダーグラフには、シェーダーコードをノード化してシェーダーグラフ上で使えるようにするカスタムノードという機能が用意されています。
blogs.unity3d.com

レイマーチングはforループを使った複雑な処理を必要とするため、カスタムノードを利用したシェーダーコードで実装します。

レイマーチングを行うカスタムノード

今回はレイマーチングで球をたくさん描画するカスタムノードを作成してみました。
f:id:r-ngtm:20181204234203p:plain

以下のC#スクリプトをUnityプロジェクトに入れることで、上記のカスタムノードが使えるようになります。

using System.Reflection;
using UnityEditor.ShaderGraph;
using UnityEngine;

/// <summary>
/// レイマーチングで球をたくさん表示するカスタムノード
/// </summary>
[Title ("Raymarching", "Raymarch Sphere")]
public class RaymarchingSphereNode : CodeFunctionNode {
    public RaymarchingSphereNode () {
        name = "Raymarching(Sphere)";
    }

    protected override MethodInfo GetFunctionToConvert () {
        return GetType ().GetMethod ("RaymarchingNode_Function",
            BindingFlags.Static | BindingFlags.NonPublic);
    }

    public override void GenerateNodeFunction (FunctionRegistry registry, GraphContext graphContext, GenerationMode generationMode) {
        registry.ProvideFunction ("distance_func", s => s.Append (@"
            // 距離関数: 点pから球オブジェクトまでの距離を求める
            #define INTERVAL interval
            float distance_func(float3 p, float size, float interval) {
                p = frac(p / INTERVAL) * INTERVAL - INTERVAL / 2.0; // -INTERVAL/2.0 ~ +INTERVAL/2.0 の繰り返しを作る
                return length(p) - size;
            }
        "));
        
        registry.ProvideFunction ("getNormal", s => s.Append (@"
            // 法線の計算
            float3 getNormal(float3 p, float size, float interval) {
                float2 e = float2(0.0001, 0.0);
                return normalize(float3(
                    distance_func(p + e.xyy, size, interval) - distance_func(p - e.xyy, size, interval),
                    distance_func(p + e.yxy, size, interval) - distance_func(p - e.yxy, size, interval),
                    distance_func(p + e.yyx, size, interval) - distance_func(p - e.yyx, size, interval)
                ));
            }
        "));


        base.GenerateNodeFunction (registry, graphContext, generationMode);
    }

    static string RaymarchingNode_Function (
        [Slot (0, Binding.MeshUV0)] Vector2 UV, 
        [Slot (1, Binding.None, 0f, 0f, 4f, 0f)] Vector3 CameraPos, // カメラ位置
        [Slot (2, Binding.None, 0f, 0f, -1f, 0f)] Vector3 CameraDir, // カメラの向きベクトル
        [Slot (3, Binding.None, 0f, 1f, 0f, 0f)] Vector3 CameraUp,  // カメラの上方向ベクトル
        [Slot (4, Binding.None, 1f, 0f, 0f, 0f)] Vector1 ObjectSize, // 球のサイズ
        [Slot (5, Binding.None, 2f, 0f, 0f, 0f)] Vector1 ObjectInterval, // 球の配置間隔
        [Slot (6, Binding.None, 32f, 0f, 0f, 0f)] Vector1 RaymarchLoop, // レイマーチングのループ回数(この数を大きくすると遠くまで描画されるようになりますが重くなります)
        [Slot (10, Binding.None)] out Vector1 Hit, // レイがオブジェクトにぶつかったら1.0, ぶつからなかったら0.0
        [Slot (11, Binding.None)] out Vector1 Distance, // レイマーチングでレイが進んだ距離
        [Slot (12, Binding.None)] out Vector3 Normal  // オブジェクト上の法線
    ) {
        Normal = Vector3.zero;
        return @"{
                #define MAX_REPEAT 100

                float2 p = UV - 0.5;

                // カメラに関する情報(Position, Direction, Up)
                #define cPos CameraPos
                #define cDir normalize(CameraDir)
                #define cUp normalize(CameraUp)
                #define cSide normalize(cross(cUp, cDir))

                // レイマーチング
                float3 ray = normalize(p.x * cSide + p.y * cUp + 1.0 * cDir); // レイの向きベクトル
                float3 rPos = cPos; // レイ位置
                float rLength = 0.0;// レイが進む長さ
                float dist = 0.0; // レイとオブジェクト間の距離
                for (int i = 0; i < min(RaymarchLoop, MAX_REPEAT); i++)
                {
                    dist = distance_func(rPos, ObjectSize, ObjectInterval); // レイ位置からオブジェクトまでの距離を求める
                    rLength += dist; // 距離を足す(レイを進める)
                    rPos = cPos + ray * rLength; // レイ位置の更新
                }

                Hit = step(dist, 0.01); // レイがオブジェクトにある程度近かったら1.0を出力、それ以外は0.0を出力
                Distance = rLength; // レイが進んだ距離を出力
                Normal = saturate(getNormal(rPos, ObjectSize, ObjectInterval)); // レイの交点におけるオブジェクト上の法線を出力
            }";
    }
}

2018-12-05追記

カスタムノードを一部修正しました。
修正前 : #define cSide normalize(cross(cDir, cUp))
修正後 : #define cSide normalize(cross(cUp, cDir))

カスタムノードの詳細

このカスタムノードを利用することで、以下の数値が利用できるようになります。
・レイがオブジェクトに当たったかどうか(当たった場合は1.0、それ以外は0.0を出力)
・レイがオブジェクトに当たるまでに進んだ距離
・オブジェクト上の法線情報(影の計算などに利用できます)

f:id:r-ngtm:20181205000343p:plain

カスタムノードのメニュー位置

カスタムノードは「Raymarching/Raymarchi Sphere」にいます。
f:id:r-ngtm:20181205001955p:plain

とりあえずカスタムノードを使ってみる

レイマーチングのカメラ位置を動かしてみる

ノードを以下のようにつないでみます。
f:id:r-ngtm:20181205003206p:plain

以下のような絵が出力されました。
f:id:r-ngtm:20181205003534g:plain:w256

zのマイナス方向(画面の奥側)を向いたカメラがzのプラスの方向(画面の手前側)へ移動しているため、
球オブジェクトが画面の奥へ移動しているような動きをします。

カメラの向きを変えてみる

カメラ向き用のVector3を作成して、カスタムノードのカメラ向きとして入力します。
f:id:r-ngtm:20181205005134p:plain

マテリアル上からカメラ向きを変えられるようになりました。なかなか楽しいです。
f:id:r-ngtm:20181205005609g:plain:w420

レイマーチングで遊んでみた

今回、このレイマーチングノードを使って以下のようなシェーダーグラフを作ってみました。
f:id:r-ngtm:20181204230928g:plain

ノード全体

f:id:r-ngtm:20181205013138p:plain

ノード解説(左側)

レイマーチングのパラメータの指定をしています。
f:id:r-ngtm:20181205011922p:plain

ノード解説(右側)

レイがオブジェクトへぶつかるまでに進んだ距離を補正し、最後にSampleGradientで色を付けて画面出力しています。
f:id:r-ngtm:20181205014530p:plain

陰影を付けてみる

f:id:r-ngtm:20181205084156p:plain:w256

法線情報と光の内積をとることで、影を付けることができます。 DotProductノードを使うと内積を計算できます。 f:id:r-ngtm:20181205084220p:plain

シーンカメラとレイマーチングカメラを同期させてみる

Unityのシーンにあるカメラとレイマーチングのカメラを同期させる方法を解説します。
f:id:r-ngtm:20181205225055g:plain:w400 f:id:r-ngtm:20181205225134g:plain:w400

シェーダーグラフの設定

カメラの位置、向き、上方向の計3つのプロパティを追加し、レイマーチングノードへ入力します。
f:id:r-ngtm:20181205224342p:plain:w512

Referenceの部分は以下のように設定します。
f:id:r-ngtm:20181205224937p:plain

このReferenceはMaterialのSetVector(name, value)メソッドのnameに指定する名前となります。

        int _cameraUp = Shader.PropertyToID("_CameraUp");
        int _cameraDir = Shader.PropertyToID("_CameraDir");
        int _cameraPos = Shader.PropertyToID("_CameraPos");
        _material.SetVector(_cameraUp, transform.up);
        _material.SetVector(_cameraDir, transform.forward);
        _material.SetVector(_cameraPos, transform.position);

カメラ位置をシェーダーグラフへコピーするC#スクリプト

以下のスクリプトCameraShaderSync.csをUnityプロジェクトへ追加します。

using UnityEngine;

public class CameraShaderSync : MonoBehaviour
{
    [SerializeField] Material _material; // カメラと同期させる対象のマテリアル
    int _cameraUp;
    int _cameraDir;
    int _cameraPos;

    void Start()
    {
        _cameraUp = Shader.PropertyToID("_CameraUp");
        _cameraDir = Shader.PropertyToID("_CameraDir");
        _cameraPos = Shader.PropertyToID("_CameraPos");
    }

    void Update()
    {
        // 位置と向きをシェーダーに渡す
        _material.SetVector(_cameraUp, transform.up);
        _material.SetVector(_cameraDir, transform.forward);
        _material.SetVector(_cameraPos, transform.position);
    }
}


CameraShaderSyncコンポーネントをシーンカメラにアタッチし、Inspectorタブからレイマーチングのマテリアルを登録します。
f:id:r-ngtm:20181205225557p:plain

再生ボタンを押してカメラ同期

Unityの再生ボタンを押すと、シーンの位置と向きがレイマーチングのカメラ情報へ反映されるようになります。
f:id:r-ngtm:20181205225055g:plain:w400

C#スクリプト制御でカメラを動かしてみる

C#スクリプトを使ってカメラを動かしてみました。

f:id:r-ngtm:20181205232323g:plain

以下のC#スクリプトをカメラにアタッチして動かしています。

using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class SetVelocity : MonoBehaviour
{
    [SerializeField] float m_AngularSpeed = -1f; // 角速度の係数
    [SerializeField] Vector3 m_AngularSpeeds = new Vector3(-0.3f, 0.4f, -0.3f);  // 角速度
    [SerializeField] float m_PositionSpeed = -1f; // 移動速度の係数
    [SerializeField] Vector3 m_PositionSpeeds = new Vector3(-7f, -7.5f, -0.2f); // 移動速度

    void Start()
    {
        var rigidbody = GetComponent<Rigidbody>();
        rigidbody.angularVelocity = m_AngularSpeed * m_AngularSpeeds;
        rigidbody.velocity = m_PositionSpeed * m_PositionSpeeds;
        Debug.Log(rigidbody.velocity);

    }
}