レイマーチング(ray marching)と呼ばれる手法を用いて以下のような絵を作ってみました。
目次
- 目次
- サンプル
- レイマーチングのざっくりとした解説
- 準備 : レイマーチングを行うカスタムノードの作成
- カスタムノードのメニュー位置
- とりあえずカスタムノードを使ってみる
- レイマーチングで遊んでみた
- 陰影を付けてみる
- シーンカメラとレイマーチングカメラを同期させてみる
- C#スクリプト制御でカメラを動かしてみる
サンプル
レイマーチングのざっくりとした解説
カメラ位置、カメラ向き、UV座標からレイを求め、レイがオブジェクトとぶつかるかどうかを判定します。
オブジェクトにレイがぶつかった場合は色を付ける、そうでない場合は色をつけないという描画を行います。
※ここでいうカメラはレイマーチング計算に利用する視点のことを指しており、Unityのシーンカメラとは別のものです
準備 : レイマーチングを行うカスタムノードの作成
シェーダーグラフには、シェーダーコードをノード化してシェーダーグラフ上で使えるようにするカスタムノードという機能が用意されています。
blogs.unity3d.com
レイマーチングはforループを使った複雑な処理を必要とするため、カスタムノードを利用したシェーダーコードで実装します。
レイマーチングを行うカスタムノード
今回はレイマーチングで球をたくさん描画するカスタムノードを作成してみました。
以下の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を出力)
・レイがオブジェクトに当たるまでに進んだ距離
・オブジェクト上の法線情報(影の計算などに利用できます)
カスタムノードのメニュー位置
カスタムノードは「Raymarching/Raymarchi Sphere」にいます。
とりあえずカスタムノードを使ってみる
レイマーチングのカメラ位置を動かしてみる
ノードを以下のようにつないでみます。
以下のような絵が出力されました。
zのマイナス方向(画面の奥側)を向いたカメラがzのプラスの方向(画面の手前側)へ移動しているため、
球オブジェクトが画面の奥へ移動しているような動きをします。
カメラの向きを変えてみる
カメラ向き用のVector3を作成して、カスタムノードのカメラ向きとして入力します。
マテリアル上からカメラ向きを変えられるようになりました。なかなか楽しいです。
レイマーチングで遊んでみた
今回、このレイマーチングノードを使って以下のようなシェーダーグラフを作ってみました。
ノード全体
ノード解説(左側)
レイマーチングのパラメータの指定をしています。
ノード解説(右側)
レイがオブジェクトへぶつかるまでに進んだ距離を補正し、最後にSampleGradientで色を付けて画面出力しています。
陰影を付けてみる
法線情報と光の内積をとることで、影を付けることができます。 DotProductノードを使うと内積を計算できます。
シーンカメラとレイマーチングカメラを同期させてみる
Unityのシーンにあるカメラとレイマーチングのカメラを同期させる方法を解説します。
シェーダーグラフの設定
カメラの位置、向き、上方向の計3つのプロパティを追加し、レイマーチングノードへ入力します。
Referenceの部分は以下のように設定します。
この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タブからレイマーチングのマテリアルを登録します。
再生ボタンを押してカメラ同期
Unityの再生ボタンを押すと、シーンの位置と向きがレイマーチングのカメラ情報へ反映されるようになります。
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); } }