トップ > 【Unity】コンピュートシェーダーでメッシュを細分化する方法を紹介します!
更新日 2022/8/2

【Unity】コンピュートシェーダーでメッシュを細分化する方法を紹介します!

コンピュートシェーダーでメッシュを細分化する方法を紹介します。

MeshSubdivision0 MeshSubdivision1

細分化方法の検討

どのようにメッシュを細分化するか考えます。
今回はシンプルに三角形の中心に頂点xを作成し3つの三角形へ分割する方法にしましょう。

ComputeSubdivisionMethod

[0,1,2]の三角形に対して[0,1,x] [1,2,x] [2,0,x]の3つの三角形に分割します。
頂点数は三角形の数分増え、インデックスは三角形が3分割されるので3倍になります。


コンピュートシェーダーの実装

3つのカーネルを持つシェーダーを作成します。

  1. 頂点の中心を求めるCSVertex
  2. UVの中心を求めるCSCoord
  3. 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&lt;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);

前回のスクリプトに使ってみる

前回の記事 コンピュートシェーダーで頂点ごとにパラメーターを持たせる方法 のスクリプトに使用してみます。

ComputeSubdivisionUniInspector
ComputeSubdivisionUni

メッシュを細分化するだけではうまく調整できなさそうです。
トゲになる部分をランダムでなく、キチンとルール化したほうが良さそうですね。

今回はコンピュートシェーダーでメッシュを細分化する方法を紹介しました。
綺麗なトゲの表現方法をもっと考えてみようと思います。


関連ページ


Copyright ©2022 - 2024 うにぉらぼ