はじめに
こんにちは、避雷です。この前こんなゲームを作りました。
幾何学的な構造物の中を縦横無尽に飛び回るゲームを作っています、黒魔術によって全体で5000ポリゴンいかないぐらいになっています(Raymarching活用事例) pic.twitter.com/fi2746jrwV
— 避雷 (@lucknknock) September 16, 2019
このゲームでは
- shaderで距離関数を記述し、レイマーチングを描画する
- C#で同じ距離関数を記述し、当たり判定を取得する
という二つの手法を用いてレンダリングとアルゴリズムを構築しています。この際に距離関数をHLSLからC#に翻訳する必要が出てくるのですが、この際にUnity.Mathematics
というパッケージを用いると比較的楽に翻訳行為が出来ることが分かったのでその知見を共有します。
レイマーチングで描画された図形をUnityに表示する方法については凹氏の記事を参照するのをお勧めします(当記事では紹介しません)
http://tips.hecomi.com/entry/2016/03/17/020610
Unity.Mathematicsについて
概要
Unity.Mathematicsは、Unity公式によって提供されるPackageの一つで、githubのreadmeによると、数学的な操作をshaderライクに記述可能にすることを目的にしたパッケージで、おもにBurst-Compiler,ECSにおける数学的な処理の記述に用いられることを想定しているようです。Unity.MathematicsのPackageをPackageManagerからインポートするとdot,fracなどの関数やswizzle記法などを使えるようになります。
詳しくは下記のgithubを参照すると良いでしょう。
https://github.com/Unity-Technologies/Unity.Mathematics
出来ること
Package/Mathematicsの中身を覗いてどんなことが出来るか見てみましょう。
数学的な関数
hlslで使える関数はおおむね使えるみたいです。先述したgithubのサンプルには以下のような関数の紹介がされていました。
using static Unity.Mathematics.math;
namespace MyNamespace
{
using Unity.Mathematics;
...
var v1 = float3(1,2,3);
var v2 = float3(4,5,6);
v1 = normalize(v1);
v2 = normalize(v2);
var v3 = dot(v1, v2);
...
}
このサンプルが示すように、名前空間の使用を宣言することでシェーダー言語と同じように数学関数を使用することができます。
精度を指定したベクトル、行列の生成
配列を使うことなく1~4次元ベクトル、2x2~4x4行列を生成することができます。型も様々に指定することが出来て、float,double,half,int,uintなどを使うことができます。
Swizzle演算子
float4のコードを見てみるとわかるように、拡張としてswizzleが用意されています。
//...
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public float3 xxz
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get { return new float3(x, x, z); }
}
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public float3 xyx
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get { return new float3(x, y, x); }
}
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public float3 xyy
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get { return new float3(x, y, y); }
}
//...
これによってglsl,hlslのようにとある次元のベクトルから任意の成分を引き継いだベクトルを得ることができます。
UnityC#の型との変換
Unity.Mathematicsで提供される型とUnityC#で提供される型(Vector3,Quaternionなど)の間は変換可能であることが保証されています。
Unity.Mathematicsが最適化のために用いられることを考慮すると
UnityC#の型→Mathematicsの型→Burstで高速処理→UnityC#の型→Unityの各システムに値を渡す
という流れを想定しているものと思われます。
移植してみよう
先述したように、Unity.MathematicはBurstCompiler、ECSといった最適化のための厳しい条件下での数学的な記述に用いられることを想定したパッケージですが、それとはまったく関係なく、「可能な限り改変をせずにC#とhlslの距離関数を記述する」用途で用いることができます。例えば、
float DistanceFunc(float3 p)
{
p = (frac(p / 30. + 0.5) - 0.5) * 30.;
float cube = max(abs(p.x),max(abs(p.y),abs(p.z))) - 8.;
float sp = length(p)- 10.;
float3 q = abs(p);
float pipeZ = length(q.xy - 7.) - 0.5 - 0.25 * floor(sin(p.z * 3.1415 + _Time.z));
float pipeX = length(q.yz - 7.) - 0.5 - 0.25 * floor(sin(p.x * 3.1415 + _Time.z));
float pipeY = length(q.zx - 7.) - 0.5 - 0.25 * floor(sin(p.y * 3.1415 + _Time.z));
float pipe = min(pipeX,min(pipeY,pipeZ));
cube = min(cube,pipe);
return max(cube, -sp);
}
という距離関数を
public float GetDistance(Vector3 a)
{
float3 p = a;
p = (frac(p / 30f + 0.5f) - 0.5f) * 30f;
float cube = max(abs(p.x), max(abs(p.y), abs(p.z))) - 8f;
float sp = length(p) - 10f;
float3 q = abs(p);
float pipeZ = length(q.xy - 7f) - 0.5f - 0.25f * floor(sin(p.z * 3.1415f + Time.time/20f));
float pipeX = length(q.yz - 7f) - 0.5f - 0.25f * floor(sin(p.x * 3.1415f + Time.time/20f));
float pipeY = length(q.zx - 7f) - 0.5f - 0.25f * floor(sin(p.y * 3.1415f + Time.time/20f));
float pipe = min(pipeX, min(pipeY, pipeZ));
cube = min(cube, pipe);
return max(cube, -sp);
}
という風に書き換えるだけで、合同な距離関数を記述することができます。要するに浮動小数を表記する際のブレを修正するだけでhlslの距離関数をC#でも使うことができるということですね。ちなみにこの距離関数で描画される図形はこんな感じの物です。
もしMathematicsを採用しないとすると、C#やUnityEngine.Mathfにはfracや3次元abs、swizzle記法は存在しないためそれらを自分で実装するか、Mathf.repeatなどで代用してやる必要が出てきます。
C#に距離関数を移植すると何が嬉しいのか?
C#で距離関数を用いることで、Unityにおいて当たり判定と似たようなロジックを組むことができ、さらに法線を取ることで簡単な衝突の演算も出来ます。これによってhlslレイマーチングによって出力した立体(実際はplaneに貼り付けられたハリボテ)の見た目と一致するように衝突判定を持たせることができます。
Unityにおけるゲーム開発に関しても、hlslによる距離関数+レイマーチングでレンダリング周りを処理し、C#による距離関数+スクリプトでゲームロジック周りを処理することが出来れば、極端な例を言えばポリゴンを一切用いずにゲームを作ることも可能であるといえるでしょう。
おわりに
現世界線ではポリゴンレンダリングが3DCG開発の主流なので、ゲームなどといった用途でこのような距離関数が活きてくることは少ないと思われるかもしれませんが、近年ではGPUの発達も相まって距離関数やレイマーチングをゲームに用いる事例が増えつつあります。
例)
ゼノブレイド2…雲の描画にレイマーチングが使われています
https://www.famitsu.com/news/201808/23162812.html
AceCombat7…雲の描画にレイマーチングが使われており、雲内にいるときは一部兵器の挙動が変わる
https://www.famitsu.com/news/201909/05182747.html
MarbleMarcher…全編レイマーチングで描画された玉転がしゲーム、フラクタル図形に当たり判定が付いている
https://codeparade.itch.io/marblemarcher
(これらの例は@FMS_cat氏,@notargs氏に教えていただきました(ありがとうございます))
現代ではGPUが描画目的以外の用途にも使いやすく、高性能になってきており、GPUベースの技術がどんどん開発されてきています。これを機にGPUをブン回してシェーダーベースのゲームを作ってみるのも面白いかもしれませんね。