LoginSignup
18
11

More than 1 year has passed since last update.

【Unity】Advanced Mesh APIでメッシュの頂点をCompute Shaderで動かす

Posted at

Unity2019.3からAdvanced Mesh APIとして、よりローレベルでメッシュを操作できるようにMesh APIが拡張されています。その一環で、Unity2021.2からメッシュをCompute Shaderで直接操作できるようになりました。

この記事では、Advanced Mesh APIを使用してメッシュの頂点をCompute Shaderで動かしてみます。結果はこのようになります。
ComputeMeshDisplace.gif

検証で使用しているUnityのバージョンは2021.2.9f1です。


まず、C#のスクリプトです。
MeshFilterからメッシュを取得して参照用のメッシュoriginalMeshと変更用のメッシュdisplacedMeshをそれぞれ作成します。変更用のメッシュをMeshFilter#meshに設定しておき、こののメッシュの頂点をCompute Shaderで動かします。参照用のメッシュはCompute Shaderに渡す頂点位置のバッファだけが必要なのでMeshにする必要は必ずしもないですが、ここでは手を抜いてメッシュを生成してそこからバッファを取得しています。Update内でCompute Shaderを実行して頂点を動かします。

ComputeMeshDisplace.cs
using UnityEngine;
using UnityEngine.Rendering;

[RequireComponent(typeof(MeshFilter))]
public class ComputeMeshDisplace : MonoBehaviour
{
    [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
    struct DisplaceVertex
    {
        public Vector3 position;
        public Vector3 normal;
    }

    [SerializeField, HideInInspector] ComputeShader compute;
    Mesh originalMesh, displacedMesh;
    GraphicsBuffer originalIndexBuffer;
    GraphicsBuffer originalVertexBuffer;
    GraphicsBuffer displacedVertexBuffer;

    void Awake()
    {
        var meshFilter = GetComponent<MeshFilter>();
        originalMesh = CreateMesh(meshFilter.sharedMesh);
        displacedMesh = CreateMesh(meshFilter.sharedMesh);
        originalVertexBuffer = originalMesh.GetVertexBuffer(0);
        originalIndexBuffer = originalMesh.GetIndexBuffer();
        displacedVertexBuffer = displacedMesh.GetVertexBuffer(0);
        meshFilter.mesh = displacedMesh;
    }

    Mesh CreateMesh(Mesh original)
    {
        var originalTriangles = original.triangles;
        var originalPositions = original.vertices;
        var originalNormals = original.normals;

        var mesh = new Mesh();
        mesh.indexBufferTarget |= GraphicsBuffer.Target.Raw;
        mesh.vertexBufferTarget |= GraphicsBuffer.Target.Raw;
        var pDesc = new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3);
        var nDesc = new VertexAttributeDescriptor(VertexAttribute.Normal, VertexAttributeFormat.Float32, 3);
        mesh.SetVertexBufferParams(original.vertexCount, pDesc, nDesc);
        mesh.SetIndexBufferParams(originalTriangles.Length, IndexFormat.UInt32);
        mesh.SetSubMesh(0, new SubMeshDescriptor(0, originalTriangles.Length), MeshUpdateFlags.DontRecalculateBounds);
        mesh.SetIndexBufferData(originalTriangles, 0, 0, originalTriangles.Length);
        var vertices = new DisplaceVertex[original.vertexCount];
        for (var i = 0; i < original.vertexCount; ++i)
        {
            vertices[i].position = originalPositions[i];
            vertices[i].normal = originalNormals[i];
        }
        mesh.SetVertexBufferData(vertices, 0, 0, original.vertexCount);
        mesh.bounds = original.bounds;
        return mesh;
    }

    void OnDestroy()
    {
        originalVertexBuffer?.Dispose();
        originalVertexBuffer = null;
        originalIndexBuffer?.Dispose();
        originalIndexBuffer = null;
        displacedVertexBuffer?.Dispose();
        displacedVertexBuffer = null;
        Destroy(originalMesh);
        originalMesh = null;
        Destroy(displacedMesh);
        displacedMesh = null;
    }

    void Update()
    {
        DisplaceMesh();
    }

    void DisplaceMesh()
    {
        compute.SetFloat("Time", Time.time);
        compute.SetBuffer(0, "OriginalIndices", originalIndexBuffer);
        compute.SetBuffer(0, "OriginalVertices", originalVertexBuffer);
        compute.SetBuffer(0, "DisplacedVertices", displacedVertexBuffer);
        DispatchThreads(0, originalMesh.triangles.Length / 3);
    }

    void DispatchThreads(int kernel, int count)
    {
        uint x, y, z;
        compute.GetKernelThreadGroupSizes(kernel, out x, out y, out z);
        var groups = (count + (int)x - 1) / (int)x;
        compute.Dispatch(kernel, groups, 1, 1);
    }
}

次に、Compute Shaderです。
DisplaceMeshがメッシュを構成する三角形ポリゴンの数だけ実行されるので、参照用のメッシュのバッファから三角形を構成する3つの頂点位置を取得して摂動を加えてから、法線を再計算して変更用のメッシュのバッファに頂点位置と法線を格納しています。

ComputeMeshDisplace.compute
#pragma kernel DisplaceMesh

float Time;
ByteAddressBuffer OriginalIndices;
ByteAddressBuffer OriginalVertices;
RWByteAddressBuffer DisplacedVertices;

float3 LoadOriginalVertex(uint index)
{
    uint pi = index * 6 * 4; // index * (3[position.xyz] + 3[normal.xyz]) * 4[bytes]
    return asfloat(OriginalVertices.Load3(pi));
}

void StoreDisplacedVertex(uint index, float3 position, float3 normal)
{
    uint pi = index * 6 * 4; // index * (3[position.xyz] + 3[normal.xyz]) * 4[bytes]
    uint ni = pi + 3 * 4; // pi + 3[position.xyz] * 4[bytes]
    DisplacedVertices.Store3(pi, asuint(position));
    DisplacedVertices.Store3(ni, asuint(normal));
}

float3 DispalcePosition(float3 p)
{
    float3 disp = float3(
        0.1 * (sin(1.3 * p.y + 4.1 * Time) + sin(2.9 * p.z + 5.3 * Time)),
        0.1 * (sin(1.9 * p.z + 4.3 * Time) + sin(3.1 * p.x + 5.9 * Time)),
        0.1 * (sin(2.3 * p.x + 4.7 * Time) + sin(3.7 * p.y + 6.1 * Time))
    );
    return p + disp;
}

float3 CalculateNormal(float3 p0, float3 p1, float3 p2)
{
    float3 d10 = p1 - p0;
    float3 d20 = p2 - p0;
    return normalize(cross(d10, d20));
}

[numthreads(256, 1, 1)]
void DisplaceMesh(uint id : SV_DispatchThreadID)
{
    // Load original vertices
    uint3 triIndex = OriginalIndices.Load3(id * 3 * 4); // id * 3[triangle vertices] * 4[bytes]
    float3 p0 = LoadOriginalVertex(triIndex.x);
    float3 p1 = LoadOriginalVertex(triIndex.y);
    float3 p2 = LoadOriginalVertex(triIndex.z);

    // Displace positions
    float3 dp0 = DispalcePosition(p0);
    float3 dp1 = DispalcePosition(p1);
    float3 dp2 = DispalcePosition(p2);

    // Calculate normals
    float3 cn = CalculateNormal(dp0, dp1, dp2);

    // Store modified vertices
    StoreDisplacedVertex(triIndex.x, dp0, cn);
    StoreDisplacedVertex(triIndex.y, dp1, cn);
    StoreDisplacedVertex(triIndex.z, dp2, cn);
}

先ほどのComputeMeshDisplaceコンポーネントをMeshFilterを持つオブジェクトに追加して、computeプロパティにこのCompute Shaderを設定するとメッシュの頂点が動くようになります。Compute Shaderのコードを見るとわかるように、この方法だとフラットシェーディングになってしまうので、記事先頭の結果ではあらかじめフラットシェーディング用のメッシュに適用しています。


以下、参考にした記事です。

18
11
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
11