【Unity】コンピュートシェーダーでメッシュを細分化する方法を紹介します!
コンピュートシェーダーでメッシュを細分化する方法を紹介します。
細分化方法の検討
どのようにメッシュを細分化するか考えます。
今回はシンプルに三角形の中心に頂点xを作成し3つの三角形へ分割する方法にしましょう。
[0,1,2]の三角形に対して[0,1,x] [1,2,x] [2,0,x]の3つの三角形に分割します。
頂点数は三角形の数分増え、インデックスは三角形が3分割されるので3倍になります。
コンピュートシェーダーの実装
3つのカーネルを持つシェーダーを作成します。
- 頂点の中心を求めるCSVertex
- UVの中心を求めるCSCoord
- 3分割された三角形のインデックスバッファーを作成するCSIndex
法線や接線はMeshクラスに自動的に再計算する関数があるのでコンピュートシェーダーでは計算しません。
計算した頂点xは現在の頂点バッファーの最後に連結するようにします。
そのため頂点xのインデックスは頂点バッファーの長さ(VertexOffset)に連結予定の頂点番号を足して計算します。
SubdivisionShader.compute
#pragma kernel CSVertex
#pragma kernel CSCoord
#pragma kernel CSIndex
StructuredBuffer<int> Indices;
StructuredBuffer<float3> Vertices;
RWStructuredBuffer<float3> OutVertices;
[numthreads(8,1,1)]
void CSVertex(uint3 id : SV_DispatchThreadID)
{
OutVertices[id.x] = (
Vertices[Indices[id.x * 3 + 0]] +
Vertices[Indices[id.x * 3 + 1]] +
Vertices[Indices[id.x * 3 + 2]]
) / 3.0;
}
StructuredBuffer<float2> Coords;
RWStructuredBuffer<float2> OutCoords;
[numthreads(8,1,1)]
void CSCoord(uint3 id : SV_DispatchThreadID)
{
OutCoords[id.x] = (
Coords[Indices[id.x * 3 + 0]] +
Coords[Indices[id.x * 3 + 1]] +
Coords[Indices[id.x * 3 + 2]]
) / 3.0;
}
RWStructuredBuffer<int> OutIndices;
int VertexOffset;
[numthreads(8,1,1)]
void CSIndex(uint3 id : SV_DispatchThreadID)
{
uint index = id.x / 9;
int a = Indices[3 * index + 0];
int b = Indices[3 * index + 1];
int c = Indices[3 * index + 2];
int x = index + VertexOffset;
// (0,1,x) (1,2,x) (2,0,x)
int table[9] = { a,b,x,b,c,x,c,a,x };
OutIndices[id.x] = table[id.x % 9];
}
スクリプトの実装
細分化するメソッドをMeshに対する拡張メソッドで実装します。
注意するのは頂点バッファーに計算した頂点xのバッファーを連結してSetVertices関数を呼ぶと
法線や接線、UVバッファーなどが自動的に頂点バッファーと同じサイズにリサイズされることです。
なのでUVバッファーはSystem.Array.Copyでリサイズされた部分に計算結果をコピーしています。
法線はRecalculateNormals()、接線はRecalculateTangents()で再計算します。
SubdivisionMesh.cs
using System.Linq;
using UnityEngine;
public static class SubdivisionMesh
{
public static void Subdivide(this Mesh mesh, ComputeShader shader)
{
int vertexOffset = mesh.vertices.Length;
GraphicsBuffer vertexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, mesh.vertices.Length, 12);
vertexBuffer.SetData(mesh.vertices);
GraphicsBuffer coordBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, mesh.uv.Length, 8);
coordBuffer.SetData(mesh.uv);
for (int i=0; i<mesh.subMeshCount; i++)
{
if(mesh.GetTopology(i) == MeshTopology.Triangles)
{
uint indexCount = mesh.GetIndexCount(i);
uint newIndexCount = indexCount * 3;
int[] indices = mesh.GetIndices(i);
uint count = 8 * ((indexCount + 7) / 8);
GraphicsBuffer indexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, (int)count, sizeof(int));
indexBuffer.SetData(indices);
int indexGroupCount = (int)(newIndexCount + 7) / 8;
GraphicsBuffer outIndexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 8 * indexGroupCount, sizeof(int));
int[] newIndices = new int[newIndexCount];
count = indexCount / 3;
Vector3[] newVertices = new Vector3[count];
int vertexGroupCount = (int)(count + 7) / 8;
GraphicsBuffer outVertexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 8* vertexGroupCount, sizeof(float) * 3);
int kernel = shader.FindKernel("CSVertex");
shader.SetBuffer(kernel, "Indices", indexBuffer);
shader.SetBuffer(kernel, "Vertices", vertexBuffer);
shader.SetBuffer(kernel, "OutVertices", outVertexBuffer);
shader.Dispatch(kernel, vertexGroupCount, 1, 1);
outVertexBuffer.GetData(newVertices, 0, 0, newVertices.Length);
var vertices = mesh.vertices.Concat(newVertices).ToArray();
mesh.SetVertices(vertices);
int coordGroupCount = (int)(count + 7) / 8;
GraphicsBuffer outCoordBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 8 * coordGroupCount, sizeof(float) * 2);
kernel = shader.FindKernel("CSCoord");
shader.SetBuffer(kernel, "Indices", indexBuffer);
shader.SetBuffer(kernel, "Coords", coordBuffer);
shader.SetBuffer(kernel, "OutCoords", outCoordBuffer);
shader.Dispatch(kernel, coordGroupCount, 1, 1);
Vector2[] outCoords = new Vector2[indexCount / 3];
outCoordBuffer.GetData(outCoords, 0, 0, outCoords.Length);
var uv = mesh.uv;
System.Array.Copy(outCoords, 0, uv, vertexOffset, outCoords.Length);
mesh.SetUVs(0, uv);
kernel = shader.FindKernel("CSIndex");
shader.SetInt("VertexOffset", vertexOffset);
shader.SetBuffer(kernel, "Indices", indexBuffer);
shader.SetBuffer(kernel, "OutIndices", outIndexBuffer);
shader.Dispatch(kernel, indexGroupCount, 1, 1);
outIndexBuffer.GetData(newIndices, 0, 0, newIndices.Length);
mesh.SetIndices(newIndices, 0, (int)newIndexCount, MeshTopology.Triangles, i);
vertexOffset += (int)indexCount / 3;
outVertexBuffer.Release();
outCoordBuffer.Release();
outIndexBuffer.Release();
indexBuffer.Release();
}
}
vertexBuffer.Release();
coordBuffer.Release();
mesh.RecalculateNormals();
mesh.RecalculateTangents();
}
}
使い方はメソッドのように呼び出します。
Mesh mesh = ...;
ComputeShader shader = ...;
mesh.Subdivide(shader);
前回のスクリプトに使ってみる
前回の記事 コンピュートシェーダーで頂点ごとにパラメーターを持たせる方法 のスクリプトに使用してみます。
メッシュを細分化するだけではうまく調整できなさそうです。
トゲになる部分をランダムでなく、キチンとルール化したほうが良さそうですね。
今回はコンピュートシェーダーでメッシュを細分化する方法を紹介しました。
綺麗なトゲの表現方法をもっと考えてみようと思います。
関連ページ
こちらのページも合わせてご覧下さい。