最近SDF(符号付き距離フィールド)について勉強してます。
SDFをどうやったらメッシュ化できるのか?って具体的な話がググっても出てこなかったので挑戦してみました。
SDFって何?マーチンキューブって何?って思う方もいらっしゃるでしょうが、そこから話し始めるとキリが無いので、その辺の解説はまた別途記事にするかもしれません。ひとまず今はググって調べてください。
まず、イチからマーチンキューブのプログラムを書くのは面倒なので、
https://github.com/pavelkouril/unity-marching-cubes-gpu
↑コチラのUnityでGPUでマーチンキューブを実行するプロジェクトをダウンロードして開きます。
このプロジェクト(以下MCプロジェクト)をちょっと改造して、SDFに対応させましょう。
MCプロジェクトでは、このような処理が行われます。
①メッシュ化したい物体の密度を3Dテクスチャ(サイズは64x64x64)に書き込む
②3Dテクスチャをコンピュートシェーダで読み込んでマーチンキューブでメッシュ(三角形ポリゴン)を生成する
③生成されたポリゴンを描画
しかし今回の挑戦では、①のような普通に各ピクセルに密度が書き込まれた3Dテクスチャでは無くて、SDFの3Dテクスチャを使います。
なので、どっかからSDFの3Dテクスチャを持ってくる必要があります。
https://github.com/xraxra/SDFr
↑こちらのSDFrプロジェクトは、UnityでメッシュをSDFの3Dテクスチャに変換することができます。ちょうど本記事でやろうとしてるSDF→メッシュの逆パターンですね。
SDFrのプロジェクトの中に、「sdfData_NC_Bunny」という名前の3Dテクスチャアセットが入ってます。これはスタンフォードバニーをSDFの3Dテクスチャに変換したものです。サイズが64x64x64で、MCプロジェクトで使われてる3Dテクスチャと同じサイズで都合がいいので、これをMCプロジェクトにコピーします。
MCプロジェクトのExampleシーンを開いて再生してみると、なんか球のメッシュがでかくなったり小さくなったりするのが表示されます。まあこれが普通のマーチンキューブです。
ヒエラルキービューのVoxelGridというオブジェクトを見ると、MarchingCubes、DensityFieldGeneratorという2つのスクリプトが付いてます。
MarchingCubesはマーチンキューブを実行して描画するスクリプトで、 DensityFieldGeneratorは物体の密度の分布を格納した3Dテクスチャを生成、更新するスクリプトです。
今回の挑戦では DensityFieldGeneratorは不要なので削除します。代わりに以下のようなスクリプトを作ってアタッチします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class SDFMarchingCubes : MonoBehaviour { public Texture3D sdfTex; [SerializeField] PavelKouril.MarchingCubesGPU.MarchingCubes mc; // Start is called before the first frame update void Start() { mc.DensityTexture = sdfTex; mc.isoLevel = 0f; } } |
sdfTexにはSDFrからコピーしてきた 「sdfData_NC_Bunny」 を、mcにはMarchingCubesをセットします。
このスクリプトで、MarchingCubesにスタンフォードバニーのSDF3Dテクスチャを処理させます。
ところで、MarchingCubes.csのisoLevelという変数は私が勝手に追加したものなので、追加してください。
1 2 3 4 5 |
20行目 + public float isoLevel = 0.5f; 74行目 - MarchingCubesCS.SetFloat("_isoLevel", 0.5f); + MarchingCubesCS.SetFloat("_isoLevel", isoLevel); |
isoLevelというのは、マーチンキューブでメッシュを生成する密度のしきい値です。つまり、これより密度の値が大きい場所は物体の内側で、それ以外は物体の外側という事になります。
MCプロジェクトはisoLevelが0.5fで固定されていましたが、変数にして変更できるようにしました。
というのも、SDFの場合は当然物体のしきい値は0にする必要があるからです。
さて、とりあえずここまででシーンを再生してみます。
もうスタンフォードバニーが表示されました!ハイ完成…ではないです。
なんかメッシュの向きが逆向きに生成されてます。
何でこうなるかと言うと、通常のマーチンキューブでは、密度が高いほど物体の内側ですが、SDFでは距離値が小さいほど物体の内側なので、大小が逆なせいでメッシュの方向が逆向きになってしまうわけです。
まあ、これは単に生成されるポリゴンの向きを変えればいいだけですので、MarchingCubes.computeというコンピュートシェーダのファイルをちょっといじります。
1 2 3 4 5 6 7 |
441行目 - t.v[0] = v0; - t.v[1] = v1; - t.v[2] = v2; + t.v[0] = v0; + t.v[1] = v2; + t.v[2] = v1; |
これで面が正常な向きに描画されました。
でもまだなんか表示が変です。法線も逆向きに生成されてしまってます。
法線の方向も逆にします。
1 2 3 4 5 6 7 |
437行目 - v0.vNormal = normalize(CalculateGradient(v0.vPosition)); - v1.vNormal = normalize(CalculateGradient(v1.vPosition)); - v2.vNormal = normalize(CalculateGradient(v2.vPosition)); + v0.vNormal = -normalize(CalculateGradient(v0.vPosition)); + v1.vNormal = -normalize(CalculateGradient(v1.vPosition)); + v2.vNormal = -normalize(CalculateGradient(v2.vPosition)); |
はいOK!
ただしくSDFをマーチンキューブでメッシュ化して描画できましたよ!
簡単でしたね。
ついでなのでメッシュをアセット化して保存してみましょう。
MarchingCubes.csに以下のようなコードを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
[ContextMenu("SaveMesh")] void SaveMesh() { int[] args = new int[] { 0, 1, 0, 0 }; argBuffer.SetData(args); ComputeBuffer.CopyCount(appendVertexBuffer, argBuffer, 0); argBuffer.GetData(args); int triangleCount = args[0]; float[] buffer = new float[(Resolution - 1) * (Resolution - 1) * (Resolution - 1) * 5*18]; appendVertexBuffer.GetData(buffer); int nextIndex = 0; List<int> triangleList = new List<int>(); List<Vector3> vertexList = new List<Vector3>(); List<Vector3> normalList = new List<Vector3>(); for (int i = 0; i < triangleCount; i++) { for (int j = 0; j < 3; j++) { int baseIndex = i * 18 + j*6; triangleList.Add(nextIndex++); vertexList.Add(new Vector3(buffer[baseIndex], buffer[baseIndex + 1], buffer[baseIndex + 2])); normalList.Add(new Vector3(buffer[baseIndex + 3], buffer[baseIndex + 4], buffer[baseIndex + 5])); } } Mesh mesh = new Mesh(); mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; //65536頂点超えても大丈夫にする mesh.vertices = vertexList.ToArray(); mesh.triangles = triangleList.ToArray(); mesh.normals = normalList.ToArray(); #if UNITY_EDITOR UnityEditor.AssetDatabase.CreateAsset(mesh, "Assets/savemesh.asset"); UnityEditor.AssetDatabase.SaveAssets(); #endif } |
メッシュの情報が保存されてるGPU上のバッファからデータを取り出して保存する処理です。(今回はやってないですが、実用するなら同じ座標の頂点を一つにまとめる処理を入れた方がいいです)
プレイモードに入ってMarchingCubesのコンテキストメニューから”SaveMesh”を実行するとAssets/savemesh.assetとしてメッシュが保存されます。
以上で、SDFをマーチンキューブでメッシュ化して取り出す処理が実現できました!
簡単でしたね。
あとはobjファイルに加工してBlenderに持っていくなりなんなりして好きにできます。
GPUによるマーチンキューブは高速なので、毎フレームリアルタイムで実行し続ける事もできます。ただし、現状はなんのひねりも無く総当たりで処理してるので、空間の解像度が上がると処理負荷が厳しくなってくると思われます。不要な場所の処理をスキップする最適化の余地はあると思います。