2
\$\begingroup\$

I'm working on a voxel based game, where destructible structures are made out of cubes. (Some of them has 3000 voxels)

I solved the framerate issues by combining them, but after making it able to handle multiple materials, calling the method causes a framerate drop.

  • And it's annoying during runtime, because currently it recombines the whole structure when a voxel is changed (changed material, destroyed voxel, etc).

One plan of mine is to decomposite the structures into smaller chunks, and only recombine the affected chunks instead of the whole structure.

  • This increases drawcalls, but decreases framerate drops. By finding a good chunk size and chunk "making" heuristic, it would work I think.

My other plan is to modify the combined mesh based on the affected voxels.

  • For example when a voxel is destroyed, find its vertices in the combined mesh, and remove them. Or change those faces' material, etc.

  • But I don't know how to do this.

The bottleneck of my mesh combining algorithm is this:

private static Mesh CombineMeshes(MeshFilter[] meshFilters, List<Material> materials)
{
    // Each material will have a mesh for it.
    var subMeshes = new List<Mesh>();
    foreach (var material in materials)
    {
        // Make a combiner for each (sub)mesh that is mapped to the right material.
        var combiners = new List<CombineInstance>();
        foreach (var meshFilter in meshFilters)
        {
            // The filter doesn't know what materials are involved, get the renderer.
            var renderer = meshFilter.GetComponent<MeshRenderer>();  // <-- (Easy optimization is possible here, give it a try!)

            // Let's see if their materials are the one we want right now.
            var sharedMaterials = renderer.sharedMaterials;
            for (int i = 0; i < sharedMaterials.Length; i++)
            {
                var sharedMaterial = sharedMaterials[i];
                if (sharedMaterial != material || sharedMaterial == null)
                    continue;
                // This submesh is the material we're looking for right now.
                CombineInstance ci = new CombineInstance();
                ci.mesh = meshFilter.sharedMesh;
                ci.subMeshIndex = 0; //With zero it works; with "i", every voxel in the structure will have every damagelayer.
                ci.transform = meshFilter.transform.localToWorldMatrix;
                combiners.Add(ci);
            }
        }
        // Flatten into a single mesh.
        var mesh = new Mesh();
        mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
        mesh.CombineMeshes(combiners.ToArray(), true);
        subMeshes.Add(mesh);
    }

    foreach (var meshFilter in meshFilters)
    {
        meshFilter.GetComponent<MeshRenderer>().enabled = false;
    }

    // The final mesh: combine all the material-specific meshes as independent submeshes.
    var finalCombiners = new List<CombineInstance>();
    foreach (var mesh in subMeshes)
    {
        CombineInstance ci = new CombineInstance();
        ci.mesh = mesh;
        ci.subMeshIndex = 0;
        ci.transform = Matrix4x4.identity;
        finalCombiners.Add(ci);
    }
    var finalMesh = new Mesh();
    finalMesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
    finalMesh.CombineMeshes(finalCombiners.ToArray(), false);
    return finalMesh;
}

Could you recommend something? Thanks!

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

You are on the right track to producing an efficient voxel game by combining meshes into a single mesh, but this approach isn't perfect. You have already encountered one problem, where the relatively large meshes take a noticeable amount of time to regenerate, thus creating an FPS drop. Another problem is that the number of meshes scales with the type of blocks, instead of the rendered area. Additionally, you may be rendering extra faces that aren't visible to the player as they are between blocks. For example, if there is a 2x2x2 cube of blocks, then the inner faces (12 total) won't ever be visible to the player, but it is easy to make the mistake of rendering them anyway. A solution to these problems is to use custom chunk meshes with a texture atlas or texture array instead.

Basically, this technique involves splitting the world into square chunks. Minecraft uses 16x16x256 (ordering x, y, z), but other values can work as well. There is a mesh for each chunk and this mesh contains all blocks in that chunk, of every material/type. To accomplish this, a texture atlas is used in combination with texture coordinates to specify different parts of the texture.

To modify your game to use the aforementioned chunking technique, it is necessary that it operate in a particular way. Specifically, the block data must be stored in some sort of data structure other than the objects in Unity's scene graph. A three dimensional array of block types for each chunk is a decent choice, however, it is often easier to reason about the memory layout of a 3D-indexed single dimensional array. Memory layout is important for CPU cache efficiency and can make writing or loading chunks to and from files easier too if the chunk data is ordered in a way that is amicable to compression. In addition to this type of data structure to store block data, you need to write custom code to create the mesh. Usually, this involves looping over the block type array and adding faces to a mesh depending on what blocks you encounter in the loop. Cursorily looking around, this appears to be the relevant class in the free and open source minetest, if you want to see a working example.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .