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