はじめに
こりんさん(@korinVR)が VRChat で行われる以下のようなイベントを企画してくれました。
8/2(日)13:00~15:00にVRChatで囲む会をやります。今回のスペシャルゲストは「凹みTips」 https://t.co/mZ2BmIxFST の凹さん @hecomi です! XRやUnityの話題でわいわい雑談したい方どなたでも大歓迎です。当日 ID: korinVR にフレンド申請・joinしてください。 #凹さんを囲む会 pic.twitter.com/9FZJdljx5e
— こりん / korinVR (@korinVR) 2020年7月27日
以前簡単な Avatar を作ってみただけで VRChat ではあまり活動できていなかったのですが、今回のイベント用になにかワールドを作りたいなと思い、uRaymarching を使ったワールドの作成に取り組んでみました。幸い、特に uRaymarching の更新はせずにそのまま出力したシェーダを VRChat で使うことが出来ました。本記事は作ってみたデモの内容について紹介したいと思います。
デモ
キューブだけで出来たワールドを作ってみました。
VRChat でキューブポリゴンだけ使って初めてのワールド作ってみた #MadeWithUdon #VRChat pic.twitter.com/7sx8fseYX8
— 凹 (@hecomi) 2020年7月27日
この前半部のメタボールの紹介をします。
uRaymarching について
距離関数を記述するだけで他のポリゴンのオブジェクトやライティングと整合の取れたレイマーチングによるオブジェクトを出すことのできる Unity のアセットです。HDRP は未だですが、Forward / Deferred / URP に対応していて、VR にも凡そ対応しています。シェーダだけで完結するので VRChat でもそのまま使えます(ただしレイマーチングは御存知の通り重いので用法・容量には注意が必要です)。
非 VR 版解説
デモの内容は、以下の GitHub にあげてある uRaymarching のデモの移植になります。
まずはこちらの Examples の解説から見ていきましょう。
設定と距離関数
uRaymarching の Conditions > World Space にチェックを入れて、距離関数にワールド座標が入ってくるようにします。
その上で次のような距離関数を記述します:
float4x4 _Cube; float4x4 _Sphere; float4x4 _Torus; float4x4 _Plane; float _Smooth; inline float DistanceFunction(float3 wpos) { float4 cPos = mul(_Cube, float4(wpos, 1.0)); float4 sPos = mul(_Sphere, float4(wpos, 1.0)); float4 tPos = mul(_Torus, float4(wpos, 1.0)); float4 pPos = mul(_Plane, float4(wpos, 1.0)); float s = Sphere(sPos, 0.5); float c = Box(cPos, 0.5); float t = Torus(tPos, float2(0.5, 0.2)); float p = Plane(pPos, float3(0, 1, 0)); float sc = SmoothMin(s, c, _Smooth); float tp = SmoothMin(t, p, _Smooth); return SmoothMin(sc, tp, _Smooth); }
幾つか uRaymarching 付属の関数を使ってしまっていて読みづらいかもしれませんが...、各プリミティブの距離関数(Sphere
や Box
など)を SmoothMin
(滑らかに結合)で合成しています。オブジェクトそれぞれのトランスフォームの逆行列を float4x4
で C# 側から与えています。つまり、DistanceFunction()
にはワールド座標 wpos
が入ってきているのですが、ここにその逆行列をかけると各ポリゴンのローカル座標へと変換され、そのローカル座標を距離関数にいれることで C# 側から与えた位置で移動・回転可能なレイマーチングのオブジェクトを記述できます。
トランスフォームの受け渡し
using UnityEngine; [ExecuteInEditMode] public class TransformProvider : MonoBehaviour { [System.Serializable] public class NameTransformPair { public string name; public Transform transform; } [SerializeField] NameTransformPair[] pairs; void Update() { var renderer = GetComponent<Renderer>(); if (!renderer) return; var material = renderer.sharedMaterial; if (!material) return; foreach (var pair in pairs) { var pos = pair.transform.position; var rot = pair.transform.rotation; var mat = Matrix4x4.TRS(pos, rot, Vector3.one); var invMat = Matrix4x4.Inverse(mat); material.SetMatrix(pair.name, invMat); } } }
シェーダ側に渡すキーワード名と、渡す座標の Transform
をペアにして登録し、この Transform
から Matrix4x4
を取り出して逆行列にしたものを Material.SetMatrix()
する形になります。インスペクタでは次のように登録します。
これで準備は出来ました。この上でレンダリングする方法は 2 通りあります。
レンダリング①:単一のキューブで描画
キューブポリゴンを 1 つ用意し、ここに上記シェーダから生成したマテリアルを指定します。Cube や Sphere などは Renderer
コンポーネントを持たない単なる Transform
を提供するだけのロケータとして扱います。これで次のようになります。
レンダリング②:複数のキューブで描画
もう 1 つの方法は、各 Cube や Sphere ゲームオブジェクトを Transform
の提供のロケータとして使うと同時に、Renderer
で描画もそこに行う方法です。各キューブポリゴンはオーバーラップしますが、SmoothMin
によってオーバーラップがきれいに解決されます。
メリット・デメリット
②の方法は描画エリアが限定されるので、小さいものがたくさんあるような場合は隙間のピクセルのレンダリングが走らないので軽くなります。ただ、たくさんオーバーラップしてかつカメラが近い場合は重複してピクセルシェーダが起動されるので重くなります。
①の方法はたくさんのピクセルシェーダが起動されてしまいますが安定してシンプルです。また、ライティング周りでライトプローブの影響などを考慮したい場合も整合が取れます。①の方法だと各ポリゴン単位でライトプローブのサンプリングが行われてしまうのでオーバーラップした接続部でライティングの不整合が起こり、破綻してしまいます。
色付け
色つけですが、こんな感じで PostEffect
で再度レイマーチングを走らせ、その結果の距離から近ければその色が濃くなるようにすればキレイに混ざり合うブレンドが可能です。
float4 _CubeColor; float4 _SphereColor; float4 _TorusColor; float4 _PlaneColor; inline void PostEffect(RaymarchInfo ray, inout PostEffectOutput o) { float3 wpos = ray.endPos; float4 cPos = mul(_Cube, float4(wpos, 1.0)); float4 sPos = mul(_Sphere, float4(wpos, 1.0)); float4 tPos = mul(_Torus, float4(wpos, 1.0)); float4 pPos = mul(_Plane, float4(wpos, 1.0)); float s = Sphere(sPos, 0.5); float c = Box(cPos, 0.5); float t = Torus(tPos, float2(0.5, 0.2)); float p = Plane(pPos, float3(0, 1, 0)); float4 a = normalize(float4(1.0 / s, 1.0 / c, 1.0 / t, 1.0 / p)); o.Albedo = a.x * _SphereColor + a.y * _CubeColor + a.z * _TorusColor + a.w * _PlaneColor; }
VRChat への移植
uRaymarching で生成したオブジェクトは基本的にはそのまま publish してもらえれば VR 内でも問題なく見れると思います。ただ今回はトランスフォームの受け渡しのところで見たようにスクリプトが必要です。このスクリプトの部分には Udon を使うことにします。U# を使ったほうが簡単とは思いますが、まずは初めてなので Udon Graph で実装してみました。全体像はこんな感じです:
レンダラのループ
renderers
という Renderer[]
な public な変数を用意しておきます。これを LateUpdate()
起点で for ループで回して一つ一つ処理していきます。
トランスフォームの受け渡し
各図形のトランスフォームを public な変数として用意しておき、ここから先程と同様に行列を作成、逆行列にしてマテリアルに SetMatrix()
します。配列に詰めたりすればもう少しシンプルに書けるとは思うのですが、まぁ量がそれほど多くないので愚直に 4 つ並べてみました。
持てるようにする
適当なコライダを各トランスフォームゲームオブジェクトに取り付け、その上で VRC_Pickup
コンポーネントをつけておきました。UdonBehavior
で Synchronize Position
つけておけばこれで同期されるんでしょうか...、イベント前には試しておきたいです。
レンダリング
ベイクしたライトプローブによるライティングの反映も見てみたかったので今回はレンダリング①を採用しました。キューブの中に入る形になるので、Camera Inside Object
にチェックを入れ、かつ裏面も描画されるようにマテリアルの設定で Culling
を Off
にしておきます。
なお、この範囲から外に持って出るとそのオブジェクトは消えてしまいます。見失わないように中心部に小さいキューブポリゴンを仕込んであります(冒頭の動画のトーラスの中心を見ると入っているのが分かると思います…)。
おわりに
イベントまでには少し調整して皆さんに公開できるようにしておきます。初めて試しましたが、uRaymarching が VRChat でもそのまま動いて嬉しかったです!ただライティングや影周りでまだ幾つかバグを見つけたので継続して改善していきたいです。