Unity2019.3からAdvanced Mesh APIとして、よりローレベルでメッシュを操作できるようにMesh APIが拡張されています。その一環で、Unity2021.2からメッシュをCompute Shaderで直接操作できるようになりました。
この記事では、Advanced Mesh APIを使用してメッシュの頂点をCompute Shaderで動かしてみます。結果はこのようになります。
検証で使用しているUnityのバージョンは2021.2.9f1です。
まず、C#のスクリプトです。
MeshFilter
からメッシュを取得して参照用のメッシュoriginalMesh
と変更用のメッシュdisplacedMesh
をそれぞれ作成します。変更用のメッシュをMeshFilter#mesh
に設定しておき、こののメッシュの頂点をCompute Shaderで動かします。参照用のメッシュはCompute Shaderに渡す頂点位置のバッファだけが必要なのでMeshにする必要は必ずしもないですが、ここでは手を抜いてメッシュを生成してそこからバッファを取得しています。Update
内でCompute Shaderを実行して頂点を動かします。
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つの頂点位置を取得して摂動を加えてから、法線を再計算して変更用のメッシュのバッファに頂点位置と法線を格納しています。
#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のコードを見るとわかるように、この方法だとフラットシェーディングになってしまうので、記事先頭の結果ではあらかじめフラットシェーディング用のメッシュに適用しています。
以下、参考にした記事です。
- UnityEngine.Mesh - Unity スクリプトリファレンス
- 2019.3 Mesh API Improvements - Google ドキュメント
- 2020.1 Mesh API Improvements - Google ドキュメント
- 2021.2 Mesh API Compute Shader Access improvements - Google ドキュメント
- 鬼弾幕!新Mesh APIで本気出したら凄いことになった…【Unity】 - YouTube
- UnityグラフィックスAPI総点検!〜最近こんなの増えてました〜 - Unityステーション - YouTube
- keijiro/NoiseBall6: Unity sample project: Direct mesh data access from compute shaders
- keijiro/ComputeMarchingCubes: [Unity] GPU-optimized marching cubes isosurface reconstruction