SDFの視覚化(NaiveSurfaceNets)
環境
Unity2022.2.14f1
概要

NaiveSurfaceNetsを使用して、SDFの3DTextureのポリゴン化を行ってみました。
参考のqiitaのコードをComputeShaderで行ってみた感じです。
マイフレーム実行しても、実用的な速度で処理されているようです。
SDFは参考のGithubにある「SignedDistance.vf」を使用させていただきました。
.vfファイルはVisualEffectGraphにインポータが付属しています。
VectorFieldImporter.csの中のtexture.Apply()の2番目の引数をfalseにしてCPUで読み取れるようにしています。
※ComputeShaderで直接3DTextureを参照するなら必要ない。
また設定でOutput Formatをfloatにしています。
コード
using UnityEngine;
public unsafe class TestGpuNaiveSurfaceNets : MonoBehaviour
{
[SerializeField] private Texture3D _sdfTexture = null;
[SerializeField] private ComputeShader _computeShader = null;
[SerializeField] private Material _material = null;
private GraphicsBuffer _sdfVoxelBuffer = null;
private GraphicsBuffer _vertexIdBuffer = null;
private GraphicsBuffer _vertexBuffer = null;
private GraphicsBuffer _indexBuffer = null;
private GraphicsBuffer _neighborBuffer = null;
private GraphicsBuffer _edgeBuffer = null;
private GraphicsBuffer _indirectArgBuffer = null;
private Bounds _bounds;
private int _klVertices = -1;
private int _klIndices = -1;
private int _klIndirectArgs = -1;
private Vector3Int _tgVertices;
private Vector3Int _tgIndices;
private static int _spSdfVoxelSize = Shader.PropertyToID("SdfVoxelSize");
private static int _spSdfVoxels = Shader.PropertyToID("SdfVoxels");
private static int _spVertexIds = Shader.PropertyToID("VertexIds");
private static int _spVertices = Shader.PropertyToID("Vertices");
private static int _spIndices = Shader.PropertyToID("Indices");
private static int _spNeighbors = Shader.PropertyToID("Neighbors");
private static int _spEdges = Shader.PropertyToID("Edges");
private static int _spIndirectArgs = Shader.PropertyToID("IndirectArgs");
private void Start()
{
if(_sdfTexture.width != _sdfTexture.height || _sdfTexture.width != _sdfTexture.depth)
{
Debug.LogError("sdfTexture Size");
return;
}
var size = _sdfTexture.width;
var texels = _sdfTexture.GetPixelData<float>(0);
_sdfVoxelBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, size * size * size, sizeof(float));
_sdfVoxelBuffer.SetData(texels);
_vertexIdBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, size * size * size, sizeof(int));
_vertexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured | GraphicsBuffer.Target.Counter, size * size * size, sizeof(Vector3));
_vertexBuffer.SetCounterValue(0);
_indexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured /*| GraphicsBuffer.Target.Index*/ | GraphicsBuffer.Target.Counter, size * size * size * 18, sizeof(int));
_indexBuffer.SetCounterValue(0);
_indirectArgBuffer = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, 4, sizeof(uint));
_indirectArgBuffer.SetData(new uint[4]{0, 1, 0, 0});
// 立方体上の頂点の番号の決め方
var Neighbors = new Vector3Int[]
{
new Vector3Int( 0, 0, 0 ),
new Vector3Int( 1, 0, 0 ),
new Vector3Int( 1, 0, 1 ),
new Vector3Int( 0, 0, 1 ),
new Vector3Int( 0, 1, 0 ),
new Vector3Int( 1, 1, 0 ),
new Vector3Int( 1, 1, 1 ),
new Vector3Int( 0, 1, 1 ),
};
_neighborBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, Neighbors.Length, sizeof(Vector3Int));
_neighborBuffer.SetData(Neighbors);
// 辺のつながり方
var Edges = new Vector2Int[]
{
new Vector2Int( 0, 1 ),
new Vector2Int( 1, 2 ),
new Vector2Int( 2, 3 ),
new Vector2Int( 3, 0 ),
new Vector2Int( 4, 5 ),
new Vector2Int( 5, 6 ),
new Vector2Int( 6, 7 ),
new Vector2Int( 7, 4 ),
new Vector2Int( 0, 4 ),
new Vector2Int( 1, 5 ),
new Vector2Int( 2, 6 ),
new Vector2Int( 3, 7 ),
};
_edgeBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, Edges.Length, sizeof(Vector2Int));
_edgeBuffer.SetData(Edges);
float bsize = size;// * 10.0f;
_bounds = new Bounds(new Vector3(bsize * 0.5f, bsize * 0.5f, bsize * 0.5f), new Vector3(bsize, bsize, bsize));
_material.SetBuffer(_spVertices, _vertexBuffer);
_material.SetBuffer(_spIndices, _indexBuffer);
uint numThreadX, numThreadY, numThreadZ;
_computeShader.SetInt(_spSdfVoxelSize, size);
// Vertices
_klVertices = _computeShader.FindKernel("GenerateVertices");
_computeShader.SetBuffer(_klVertices, _spSdfVoxels, _sdfVoxelBuffer);
_computeShader.SetBuffer(_klVertices, _spVertexIds, _vertexIdBuffer);
_computeShader.SetBuffer(_klVertices, _spVertices, _vertexBuffer);
_computeShader.SetBuffer(_klVertices, _spEdges, _edgeBuffer);
_computeShader.SetBuffer(_klVertices, _spNeighbors, _neighborBuffer);
_computeShader.GetKernelThreadGroupSizes(_klVertices, out numThreadX, out numThreadY, out numThreadZ);
_tgVertices.x = ((size - 1) + (int)(numThreadX - 1)) / (int)numThreadX;
_tgVertices.y = ((size - 1) + (int)(numThreadY - 1)) / (int)numThreadY;
_tgVertices.z = ((size - 1) + (int)(numThreadZ - 1)) / (int)numThreadZ;
// Indices
_klIndices = _computeShader.FindKernel("GenerateIndices");
_computeShader.SetBuffer(_klIndices, _spSdfVoxels, _sdfVoxelBuffer);
_computeShader.SetBuffer(_klIndices, _spVertexIds, _vertexIdBuffer);
_computeShader.SetBuffer(_klIndices, _spVertices, _vertexBuffer);
_computeShader.SetBuffer(_klIndices, _spIndices, _indexBuffer);
_computeShader.SetBuffer(_klIndices, _spNeighbors, _neighborBuffer);
_computeShader.GetKernelThreadGroupSizes(_klIndices, out numThreadX, out numThreadY, out numThreadZ);
_tgIndices.x = ((size - 2) + (int)(numThreadX - 1)) / (int)numThreadX;
_tgIndices.y = ((size - 2) + (int)(numThreadY - 1)) / (int)numThreadY;
_tgIndices.z = ((size - 2) + (int)(numThreadZ - 1)) / (int)numThreadZ;
// IndirectArgs
_klIndirectArgs = _computeShader.FindKernel("UpdateIndirectArgs");
_computeShader.SetBuffer(_klIndirectArgs, _spIndices, _indexBuffer);
_computeShader.SetBuffer(_klIndirectArgs, _spIndirectArgs, _indirectArgBuffer);
}
private void OnDestroy()
{
if(_sdfVoxelBuffer != null)
_sdfVoxelBuffer.Dispose();
if(_vertexIdBuffer != null)
_vertexIdBuffer.Dispose();
if(_vertexBuffer != null)
_vertexBuffer.Dispose();
if(_indexBuffer != null)
_indexBuffer.Dispose();
if(_neighborBuffer != null)
_neighborBuffer.Dispose();
if(_edgeBuffer != null)
_edgeBuffer.Dispose();
if(_indirectArgBuffer != null)
_indirectArgBuffer.Dispose();
}
private void LateUpdate()
{
// Dispatch
_vertexBuffer.SetCounterValue(0);
_indexBuffer.SetCounterValue(0);
_computeShader.Dispatch(_klVertices, _tgVertices.x, _tgVertices.y, _tgVertices.z);
_computeShader.Dispatch(_klIndices, _tgIndices.x, _tgIndices.y, _tgIndices.z);
_computeShader.Dispatch(_klIndirectArgs, 1,1,1);
GL.Flush();
if(_indexBuffer != null)
Graphics.DrawProceduralIndirect(_material, _bounds, MeshTopology.Triangles, _indirectArgBuffer);
}
void OnDrawGizmos()
{
Gizmos.color = Color.blue;
Gizmos.DrawWireCube(_bounds.center, _bounds.size);
}
}
uint SdfVoxelSize;
StructuredBuffer<float> SdfVoxels;
StructuredBuffer<uint2> Edges; // 辺のつながり方
StructuredBuffer<uint3> Neighbors; // 立方体上の頂点の番号の決め方
RWStructuredBuffer<uint> VertexIds;
RWStructuredBuffer<float3> Vertices;
RWStructuredBuffer<uint> Indices;
RWStructuredBuffer<uint> IndirectArgs;
// v0, v1, v2, v3から構築される面を追加する
void MakeFace(uint v0, uint v1, uint v2, uint v3, bool outside)
{
uint indexOffset = Indices.IncrementCounter() * 6;
if (outside)
{
Indices[indexOffset ] = v0;
Indices[indexOffset + 1] = v3;
Indices[indexOffset + 2] = v2;
Indices[indexOffset + 3] = v2;
Indices[indexOffset + 4] = v1;
Indices[indexOffset + 5] = v0;
}
else
{
Indices[indexOffset ] = v0;
Indices[indexOffset + 1] = v1;
Indices[indexOffset + 2] = v2;
Indices[indexOffset + 3] = v2;
Indices[indexOffset + 4] = v3;
Indices[indexOffset + 5] = v0;
}
}
// 整数座標から配列に入るときの順序を取得
// +X+Y+Z方向に広がる立方体上のi番目の頂点として順序を取得
uint ToIdx(uint x, uint y, uint z, uint i, uint size)
{
x += Neighbors[i].x;
y += Neighbors[i].y;
z += Neighbors[i].z;
return x + y * size + z * size * size;
}
// 整数座標から配列に入るときの順序を取得
// -X-Y-Z方向に広がる立方体上のi番目の頂点として順序を取得
uint ToIdxNeg(uint x, uint y, uint z, uint i, uint size)
{
x -= Neighbors[i].x;
y -= Neighbors[i].y;
z -= Neighbors[i].z;
return x + y * size + z * size * size;
}
// 整数座標から実数座標を取得
// +X+Y+Z方向に広がる立方体上のi番目の頂点として実数座標を取得
float3 ToVec(uint i, uint j, uint k, uint neighbor)
{
i += Neighbors[neighbor].x;
j += Neighbors[neighbor].y;
k += Neighbors[neighbor].z;
return float3(i, j, k);
}
uint GetKind(uint x, uint y, uint z)
{
// ビットマスクで8つの点の状態を記憶
// iの位置の点が内側ならばi + 1番目のビットを立てる
// 頂点の位置と番号の対応は次のように決める
// 7----6
// /| /|
// 4----5 |
// | 3--|-2
// |/ |/
// (x,y,z)0----1
uint kind = 0;
if (0 > SdfVoxels[ToIdx(x, y, z, 0, SdfVoxelSize)]) kind |= 1 << 0;
if (0 > SdfVoxels[ToIdx(x, y, z, 1, SdfVoxelSize)]) kind |= 1 << 1;
if (0 > SdfVoxels[ToIdx(x, y, z, 2, SdfVoxelSize)]) kind |= 1 << 2;
if (0 > SdfVoxels[ToIdx(x, y, z, 3, SdfVoxelSize)]) kind |= 1 << 3;
if (0 > SdfVoxels[ToIdx(x, y, z, 4, SdfVoxelSize)]) kind |= 1 << 4;
if (0 > SdfVoxels[ToIdx(x, y, z, 5, SdfVoxelSize)]) kind |= 1 << 5;
if (0 > SdfVoxels[ToIdx(x, y, z, 6, SdfVoxelSize)]) kind |= 1 << 6;
if (0 > SdfVoxels[ToIdx(x, y, z, 7, SdfVoxelSize)]) kind |= 1 << 7;
return kind;
}
void GenerateVertex(uint x, uint y, uint z)
{
uint kind = GetKind(x, y, z);
// 8つの点がすべて内側またはすべて外側の場合はスキップ
if (kind == 0 || kind == 255)
return;
// 頂点の位置を算出
float3 vertex;
uint crossCount = 0;
// 現在焦点を当てている立方体上の辺をすべて列挙
for (uint i = 0; i < 12; i++) {
uint2 p = Edges[i];
uint p0 = p.x;
uint p1 = p.y;
// 異なる側同士の点でつながってない場合はスキップ
// ビットマスクからp0 + 1とp1 + 1ビット目(p0とp1の位置の点の状態)を取り出す
if ((kind >> p0 & 1) == (kind >> p1 & 1))
continue;
// 両端の点のボクセルデータ上の値を取り出す
float val0 = SdfVoxels[ToIdx(x, y, z, p0, SdfVoxelSize)];
float val1 = SdfVoxels[ToIdx(x, y, z, p1, SdfVoxelSize)];
// 線形補間によって値が0となる辺上の位置を算出して加算
vertex += lerp(ToVec(x, y, z, p0), ToVec(x, y, z, p1), (0 - val0) / (val1 - val0));
crossCount++;
}
vertex /= crossCount;
uint vertexOffset = Vertices.IncrementCounter();
Vertices[vertexOffset] = vertex;
VertexIds[ToIdx(x, y, z, 0, SdfVoxelSize)] = vertexOffset;
}
void GenerateIndex(uint x, uint y, uint z)
{
// 面の追加は0 < x, y, z < size - 1で行う
//if (x == 0 || y == 0 || z == 0)
// return;
uint kind = GetKind(x, y, z);
// 8つの点がすべて内側またはすべて外側の場合はスキップ
if (kind == 0 || kind == 255)
return;
// ビットマスクから1ビット目(0の位置の点の状態)を取り出す
bool outside = (kind & 1) != 0;
// 面を構築する頂点を取り出す
// 頂点の位置と番号の対応は次のように決める
// 1----0(x, y, z)
// /| /|
// 2----3 |
// | 5--|-4
// |/ |/
// 6----7
uint v0 = VertexIds[ToIdxNeg(x, y, z, 0, SdfVoxelSize)];
uint v1 = VertexIds[ToIdxNeg(x, y, z, 1, SdfVoxelSize)];
uint v2 = VertexIds[ToIdxNeg(x, y, z, 2, SdfVoxelSize)];
uint v3 = VertexIds[ToIdxNeg(x, y, z, 3, SdfVoxelSize)];
uint v4 = VertexIds[ToIdxNeg(x, y, z, 4, SdfVoxelSize)];
uint v5 = VertexIds[ToIdxNeg(x, y, z, 5, SdfVoxelSize)];
// var v6 = VertexIds[ToIdxNeg(x, y, z, 6, SdfVoxelSize)]; // 使われない
uint v7 = VertexIds[ToIdxNeg(x, y, z, 7, SdfVoxelSize)];
// ビットマスクから2ビット目(1の位置の点の状態)を取り出す。異なる側同士の点からなる辺ならば交わるような面を追加
bool isBit;
isBit = (kind >> 1 & 1) != 0;
if ( isBit != outside)
MakeFace(v0, v3, v7, v4, outside);
// ビットマスクから4ビット目(3の位置の点の状態)を取り出す
isBit = (kind >> 3 & 1) != 0;
if (isBit != outside)
MakeFace(v0, v4, v5, v1, outside);
// ビットマスクから5ビット目(4の位置の点の状態)を取り出す
isBit = (kind >> 4 & 1) != 0;
if (isBit != outside)
MakeFace(v0, v1, v2, v3, outside);
}
#pragma kernel GenerateVertices
[numthreads(32, 32, 1)]
void GenerateVertices(uint3 id : SV_DispatchThreadID)
{
if(id.x >= SdfVoxelSize - 1 || id.y >= SdfVoxelSize - 1 || id.z >= SdfVoxelSize - 1)
return;
uint x = id.x;
uint y = id.y;
uint z = id.z;
GenerateVertex(x, y, z);
}
#pragma kernel GenerateIndices
[numthreads(32, 32, 1)]
void GenerateIndices(uint3 id : SV_DispatchThreadID)
{
if(id.x >= SdfVoxelSize - 2 || id.y >= SdfVoxelSize - 2 || id.z >= SdfVoxelSize - 2)
return;
uint x = id.x + 1;
uint y = id.y + 1;
uint z = id.z + 1;
GenerateIndex(x, y, z);
}
#pragma kernel UpdateIndirectArgs
[numthreads(1, 1, 1)]
void UpdateIndirectArgs(uint3 id : SV_DispatchThreadID)
{
IndirectArgs[0] = Indices.IncrementCounter() * 6;
}
Shader "Unlit/GpuNaiveSurfaceNets"
{
Properties
{
}
CGINCLUDE
StructuredBuffer<float3> Vertices;
StructuredBuffer<uint> Indices;
ENDCG
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
uint vertexId : SV_VertexID;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(float4(Vertices[Indices[v.vertexId]], 1.0));
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return fixed4(1,0,0,1);
}
ENDCG
}
}
}
参考
Unityでボクセルデータをもとにメッシュを生成する方法(Naive Surface Nets) - Qiita
GitHub - foreverliu/VectorFieldExamples: Unity VFX Graph examples with vector fields