はじめに
meshをvoxel化して大量描画して当たり判定もつけてハデに破壊してみました。いくつかの記事に分けてまとめてみます。(最適化はできていないので、コンセプト的な実装として読んでください。)
- unity)映画「ピクセル」みたいにボクセルを大量描画してハデに破壊したい❶ ~cubeの大量描画編~ - のりまき日記
- unity)映画「ピクセル」みたいにボクセルを大量描画してハデに破壊したい❷ ~スクリーンスペース当たり判定編~ - のりまき日記
- unity)映画「ピクセル」みたいにボクセルを大量描画してハデに破壊したい❸ ~meshのvoxelize編~ - のりまき日記
ボクセル化する
以下のアセットを使わせていただきました。 github.com
unity-voxelの使い方
以下だけ知っていれば使えそうです。meshをこれに通すことでボクセル化情報をGPUで計算して返してくれます。返ってきたdata
はComputeBuffer
なのでCPUを介さずに、そのままこちらの計算用シェーダーに渡すことができます。
var data = GPUVoxelizer.Voxelize(voxelizer, mesh, res, false);
note
第1引数は計算用のComputeShaderです。アセットに含まれています。
第2引数はボクセル化するmeshです。
第3引数は何分割するかです。meshのboundsの一番長い辺をこの数字で割った値がボクセルのunitサイズになります。
第4引数は表面だけボクセル化するか、中も埋めるかの指定です。
memo
SkinnedMeshRenderer
を使う場合は任意のタイミングでmesh化してあげる必要があります。これはちょっと重そうです。
Mesh mesh = null; skinnedMeshRenderer.BakeMesh(mesh);
計算用shaderをボクセルに対応する
上記で得たデータを使ってキューブをエミットします。
スクリプト
private void OnEnable() { ... // カウンター用に1つ追加する var args = new int[5] { m_CubeMesh.GetIndices(0).Length, m_CubeCount, 0, 0, 0 }; m_ArgumentsBuffer = new ComputeBuffer(1, sizeof(int) * args.Length, ComputeBufferType.IndirectArguments); m_ArgumentsBuffer.SetData(args); } ... private void Update() { if (Input.GetMouseButtonDown(0)) { var ray = Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out var hit, float.MaxValue)) { // MeshRendererがあればボクセル化する var renderer = hit.collider.GetComponent<MeshRenderer>(); if (renderer != null) { Voxelize(renderer); } } } DispatchUpdate(); } ... void Voxelize(MeshRenderer renderer) { var filter = renderer.GetComponent<MeshFilter>(); // boundsの長い辺を取得 var max = Mathf.Max(filter.mesh.bounds.size.x, filter.mesh.bounds.size.y, filter.mesh.bounds.size.z); // キューブの大きさを揃えたいので分割数を計算する var res = Mathf.CeilToInt(max / .1f); var data = GPUVoxelizer.Voxelize(m_Voxelizer, filter.mesh, res, false); DispatchEmit(data, filter.transform.localToWorldMatrix, renderer.sharedMaterial.mainTexture); data.Dispose(); } ... void DispatchEmit(GPUVoxelData vox, Matrix4x4 localToWorldMatrix, Texture tex) { var counter = new ComputeBuffer(vox.Buffer.count, sizeof(int), ComputeBufferType.Counter); counter.SetCounterValue(0); m_CubeComputeShader.SetBuffer(m_EmitKernel, "_Particles", m_ParticlesBuffer); m_CubeComputeShader.SetInt("_CurrentParticleIndex", m_CurrentParticleIndex); // ボクセル化情報を渡す m_CubeComputeShader.SetBuffer(m_EmitKernel, "_Voxels", vox.Buffer); m_CubeComputeShader.SetMatrix("_VoxelMatrix", localToWorldMatrix); // 色を取得するためにマテリアルに貼ってある画像も渡す m_CubeComputeShader.SetTexture(m_EmitKernel, "_VoxelTexture", tex); m_CubeComputeShader.SetVector("_VoxelTextureSize", new Vector2(tex.width, tex.height)); // 使ったパーティクルのカウント用 m_CubeComputeShader.SetBuffer(m_EmitKernel, "_VoxelCounter", counter); m_CubeComputeShader.Dispatch(m_EmitKernel, vox.Buffer.count / 8, 1, 1); // 何個使ったか var toCPU = new int[5] { 0, 0, 0, 0, 0 }; ComputeBuffer.CopyCount(counter, m_ArgumentsBuffer, 4 * 4); m_ArgumentsBuffer.GetData(toCPU); counter.Release(); m_CurrentParticleIndex = (m_CurrentParticleIndex + toCPU[4]) % m_CubeCount; } ... void DispatchInit() { ... m_CubeComputeShader.SetInt("_MaxParticles", m_CubeCount); ... }
計算用shader
struct Voxel { float3 position; float2 uv; bool fill; bool front; }; ... [numthreads(8,1,1)] void Emit (uint id : SV_DispatchThreadID) { // 空白のボクセルは無視 Voxel v = _Voxels[id]; if (v.fill < 1) { return; } uint pid = (_CurrentParticleIndex + _VoxelCounter.IncrementCounter() - 1) % _MaxParticles; Particle p = _Particles[pid]; float2 seed = float2(pid, 0); float3 rand = random3(seed); float2 coord = v.uv * _VoxelTextureSize; float4 col = _VoxelTexture[coord]; p.active = true; p.position = mul(_VoxelMatrix, float4(v.position, 1)).xyz; p.rotation = 0; p.scale = .1; p.velocity = normalize(rand); p.angularVelocity = normalize(rand); p.color = col; p.time = 0; p.lifeTime = 3; _Particles[pid] = p; }
描画用shader
struct Input { float2 uv_MainTex; float4 color; }; ... void vert (inout appdata v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); #if defined(SHADER_API_D3D11) || defined(SHADER_API_METAL) Particle p = _Particles[_ActiveIndexes[v.instanceID]]; v.vertex.xyz = rotate(v.vertex.xyz, normalize(p.rotation), length(p.rotation)); v.vertex.xyz *= p.scale * pow(1 - p.time / p.lifeTime, 2); v.vertex.xyz += p.position; o.color = p.color; #endif } void surf (Input IN, inout SurfaceOutputStandard o) { o.Albedo = IN.color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = 1; }
note
// ボクセル化情報を渡す m_CubeComputeShader.SetBuffer(m_EmitKernel, "_Voxels", vox.Buffer); m_CubeComputeShader.SetMatrix("_VoxelMatrix", localToWorldMatrix);
ボクセル化情報の中のポジションはmeshのローカル座標でのポジションなので、rendererのlocalToWorldMatrix
を使ってワールド座標にしてあげます。
// 色を取得するためにマテリアルに貼ってある画像も渡す m_CubeComputeShader.SetTexture(m_EmitKernel, "_VoxelTexture", tex); m_CubeComputeShader.SetVector("_VoxelTextureSize", new Vector2(tex.width, tex.height));
ボクセル化情報の中にはuvも入っているので画像から色をピックアップするのに使います。本当はタイリングなども考慮しないといけないです。
var counter = new ComputeBuffer(vox.Buffer.count, sizeof(int), ComputeBufferType.Counter); counter.SetCounterValue(0); ... // 何個使ったか var toCPU = new int[5] { 0, 0, 0, 0, 0 }; ComputeBuffer.CopyCount(counter, m_ArgumentsBuffer, 4 * 4); m_ArgumentsBuffer.GetData(toCPU); counter.Release(); m_CurrentParticleIndex = (m_CurrentParticleIndex + toCPU[4]) % m_CubeCount;
// 空白のボクセルは無視 Voxel v = _Voxels[id]; if (v.fill < 1) { return; } uint pid = (_CurrentParticleIndex + _VoxelCounter.IncrementCounter() - 1) % _MaxParticles;
ボクセル化情報はboundsを細かく分割した分だけデータが入ってます。なので、meshがない部分や表面ではない部分のデータも含まれています。fill
の値が0のものがそれに該当するため、計算用shader内で弾いています。実際に何個エミットしたかを取得する必要があったので、カウンター用のComputeBuffer
を作って使ってみました。使った分だけカウントをインクリメントしています。
// カウンター用に1つ追加する var args = new int[5] { m_CubeMesh.GetIndices(0).Length, m_CubeCount, 0, 0, 0 }; m_ArgumentsBuffer = new ComputeBuffer(1, sizeof(int) * args.Length, ComputeBufferType.IndirectArguments); m_ArgumentsBuffer.SetData(args); ... // 何個使ったか var toCPU = new int[5] { 0, 0, 0, 0, 0 }; ComputeBuffer.CopyCount(counter, m_ArgumentsBuffer, 4 * 4); m_ArgumentsBuffer.GetData(toCPU); counter.Release(); m_CurrentParticleIndex = (m_CurrentParticleIndex + toCPU[4]) % m_CubeCount;
カウント数は上記のようにしてcpu側で取得しています。コピー先のバッファはm_ArgumentsBuffer
を1つ拡張して再利用しています。
「何個エミットしたかを取得する」のはもっといい方法がありそうですが、動いてるのでオッケーです。
void vert (inout appdata v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); ... }
surface shaderのvertからsurfへ値を渡すための記述です。
追記
Graphics-DrawMeshInstancedIndirect - Unity スクリプトリファレンス
Buffer with arguments, bufferWithArgs, has to have five integer numbers at given argsOffset offset: index count per instance, instance count, start index location, base vertex location, start instance location.
DrawMeshInstancedIndirect
の場合bufferWithArgs
は5個必要だったみたいです。この記事内では4個で記載していました。
おわり
必要な機能は実装できました。あとはいろいろ整えて完成です。