レイマーチング(ray marching)と呼ばれる手法を用いて以下のような絵を作ってみました。
目次
サンプル
github.com
レイマーチングのざっくりとした解説
カメラ位置、カメラ向き、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,
[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#スクリプトを使ってカメラを動かしてみました。
以下の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);
}
}