のりまき日記

Unityなどの活用リファレンスブログ。「こうしたい時どうする」をまとめたい

unity)映画「ピクセル」みたいにボクセルを大量描画してハデに破壊したい❸ ~meshのvoxelize編~

めざすもの

はじめに

meshをvoxel化して大量描画して当たり判定もつけてハデに破壊してみました。いくつかの記事に分けてまとめてみます。(最適化はできていないので、コンセプト的な実装として読んでください。)

できたもの

ボクセル化する

以下のアセットを使わせていただきました。 github.com

unity-voxelの使い方

以下だけ知っていれば使えそうです。meshをこれに通すことでボクセル化情報をGPUで計算して返してくれます。返ってきたdataComputeBufferなので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個で記載していました。

おわり

必要な機能は実装できました。あとはいろいろ整えて完成です。

できたもの