Voxel LOD?

Started by
4 comments, last by Beosar 7 years, 10 months ago

Hi,

I want to implement LOD for the terrain in my game, which consists of cubes like in Minecraft and similar games.

However, I don't know how to do this without making some things look weird, like logs of trees which are 1x1 voxels (width x depth) and will be either 0x0 or 2x2, 4x4 and so on voxels in a lower level of detail... If I just keep it an 1x1 log on a lower LOD, I end up with almost the same amount of voxels.

Is there any solution for this problem? Maybe an article or something? I found results for smooth terrains (no cubes) on Google, but not for Minecraft-like terrains.

- Magogan

Advertisement

Usually when performing LOD with voxels you sample the grid at a lower resolution. 1x1x1 becomes 2x2x2 becomes 4x4x4. With cubic geometry you'll run into issues preserving the integrity of the mesh (a low res tree may not look like a tree) and seams will crop up between resolution transitions.

Firstly I would do is make sure you're minimizing the number of triangles in your geometry to begin with. Greedy method explained in this article: https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/

And then perhaps you could generate geometry in different size chunks at different distances. Something like 64x64x64 chunks super far away and 16x16x16 blocks close up in order to reduce draw calls.

Honestly I wouldn't bother with cubic LOD, the geometry should already be fairly low res.

The trouble with the trees (that's an old song :D ) is that you won't be able to make your trees using the same voxel method as your terrain. I would have trees as a separate system that doesn't go through a LOD like your terrain, but instead each tree would LOD individually on a static XZ detail. I hope that makes sense.....

To try to clarify what I'm saying: Each LOD further away from the center bridges multiple vertices of the adjacent higher detailed LOD. That's the problem you are describing. So don't do that with trees. Keep the spacing between trees at a constant, just make them less detailed as they become further away from the camera. This ultimately means you will need to have two maps; one for the terrain (as you already have), and the other with trees and any other objects like rocks and bushes.

The problem is that trees are made of cubes like the terrain and that they are randomly generated...

I could however try to combine blocks to a bigger one based on some more complex constraints like if there are 3 blocks in the shape of a L, just ignore the air (or water) block and generate a 2x2 block. I just don't know if this makes sense, since you still have to render so many blocks, even if there is an optimization like the one GameGeezer mentioned.

The problem is that trees are made of cubes like the terrain and that they are randomly generated...

I could however try to combine blocks to a bigger one based on some more complex constraints like if there are 3 blocks in the shape of a L, just ignore the air (or water) block and generate a 2x2 block. I just don't know if this makes sense, since you still have to render so many blocks, even if there is an optimization like the one GameGeezer mentioned.

Try and stay away from special case scenarios such as the blocks being in an "L" formation. Hawkblood was recommending having multiple grids, one for terrain that will have LOD and another for voxels that should keep their resolution.

From the sound of it you're generating cubic terrain as well.. cubes. What you should be doing is stepping over chunks of voxels (16 x 16 x 16 bricks or something) and extracting geometry along the isosurface. Or rather, only generate faces of the cube that are visible. The link I posted last explains this step by step.

Here's a few goals. Shoot for them in order and you'll learn a lot in the process!

  1. Extract Voxels as 6 sides cubes in a 16 x 16 x 16 grid
  2. Generate a 16 x 16 hightmap using perlin noise
  3. Use the heightmap to fill your grid and then extract!
  4. Extract only the visible faces of the cubes and place the vertices into a single mesh using the culling method in this article (https://0fps.net/2012/06/30/meshing-in-a-minecraft-game/)
  5. Learn about octree and implement the data structure
  6. Store a 16 x 16 x 16 brick of voxels as an octree leaf node
  7. When extracting a chunk check the neighboring brick entries for transitions

Here's a naive implementation from a couple years ago (don't judge me on the code haha). It's very verbose, I hope it helps ideas in your head click together.

Man I was obsessed with pools. Give a man a hammer...


public class CubicChunkExtractor {

    private VoxelMaterialAtlas materialAtlas;

    public CubicChunkExtractor(VoxelMaterialAtlas materialAtlas)
    {
        this.materialAtlas = materialAtlas;
    }

    public void Extract(BrickTree brickTree, Vector3i brickWorld, ref List<Color> colors, ref List<Vector3> vertices, ref List<Vector3> normals, ref List<Vector2> uv, ref List<int> indices, ref Pool<Color> colorPool, ref Pool<Vector2> vector2Pool, ref Pool<Vector3> vector3Pool)
    {
        int xOffset = brickTree.BrickDimensionX * brickWorld.x;
        int yOffset = brickTree.BrickDimensionY * brickWorld.y;
        int zOffset = brickTree.BrickDimensionZ * brickWorld.z;
        ColorUtil colorUtil = new ColorUtil();
        int normalDirection;
        for (int x = 0; x < brickTree.BrickDimensionX; ++x)
        {
            for (int y = 0; y < brickTree.BrickDimensionY; ++y)
            {
                for (int z = 0; z < brickTree.BrickDimensionZ; ++z)
                {
                    int trueX = x + xOffset;
                    int trueY = y + yOffset;
                    int trueZ = z + zOffset;

                    VoxelMaterial voxel = materialAtlas.GetVoxelMaterial(brickTree.GetVoxelAt(trueX, trueY, trueZ));
                    VoxelMaterial voxelPlusX = materialAtlas.GetVoxelMaterial(brickTree.GetVoxelAt(trueX + 1, trueY, trueZ));
                    VoxelMaterial voxelPlusY = materialAtlas.GetVoxelMaterial(brickTree.GetVoxelAt(trueX, trueY + 1, trueZ));
                    VoxelMaterial voxelPlusZ = materialAtlas.GetVoxelMaterial(brickTree.GetVoxelAt(trueX, trueY, trueZ + 1));

                    if (CheckForTransition(voxel, voxelPlusX, out normalDirection))
                    {
                        AddQuadX(voxel, x, y, z, normalDirection, ref colors, ref vertices, ref normals, ref uv, ref indices, ref colorPool, ref vector2Pool, ref vector3Pool, colorUtil);
                    }

                    if (CheckForTransition(voxel, voxelPlusY, out normalDirection))
                    {
                        AddQuadY(voxel, x, y, z, normalDirection, ref colors, ref vertices, ref normals, ref uv, ref indices, ref colorPool, ref vector2Pool, ref vector3Pool, colorUtil);
                    }

                    if (CheckForTransition(voxel, voxelPlusZ, out normalDirection))
                    {
                        AddQuadZ(voxel, x, y, z, normalDirection, ref colors, ref vertices, ref normals, ref uv, ref indices, ref colorPool, ref vector2Pool, ref vector3Pool, colorUtil);
                    }
                }
            }
        }
    }

    private bool CheckForTransition(VoxelMaterial start, VoxelMaterial end, out int normalDirection)
    {
        bool containsStart = start.stateOfMatter == StateOfMatter.GAS;
        normalDirection = Convert.ToInt32(!containsStart);
        return containsStart != (end.stateOfMatter == StateOfMatter.GAS);
    }

    private void AddQuadX(VoxelMaterial voxel, int x, int y, int z, int normalDirection, ref List<Color> colors, ref List<Vector3> vertices, ref List<Vector3> normals, ref List<Vector2> uv, ref List<int> indices, ref Pool<Color> colorPool, ref Pool<Vector2> vector2Pool, ref Pool<Vector3> vector3Pool, ColorUtil colorUtil)
    {
        int vertexIndex = vertices.Count;

        Color color = colorPool.Catch();
        colorUtil.Set(ref color, voxel.color, 0.7f, 0.1f, 0.4f);
        colors.Add(color);
        color = colorPool.Catch();
        colorUtil.Set(ref color, voxel.color, 0.7f, 0.1f, 0.4f);
        colors.Add(color);
        color = colorPool.Catch();
        colorUtil.Set(ref color, voxel.color, 0.7f, 0.1f, 0.4f);
        colors.Add(color);
        color = colorPool.Catch();
        colorUtil.Set(ref color, voxel.color, 0.7f, 0.1f, 0.4f);
        colors.Add(color);

        Vector3 fish = vector3Pool.Catch();
        fish.Set(x + 1, y, z);
        vertices.Add(fish);
        fish = vector3Pool.Catch();
        fish.Set(x + 1, y + 1, z);
        vertices.Add(fish);
        fish = vector3Pool.Catch();
        fish.Set(x + 1, y, z + 1);
        vertices.Add(fish);
        fish = vector3Pool.Catch();
        fish.Set(x + 1, y + 1, z + 1);
        vertices.Add(fish);

        fish = vector3Pool.Catch();
        fish.Set(normalDirection, 0, 0);
        normals.Add(fish);
        normals.Add(fish);
        normals.Add(fish);
        normals.Add(fish);

        Vector2 smallFish = vector2Pool.Catch();
        smallFish.Set(0, 0);
        uv.Add(smallFish);
        smallFish = vector2Pool.Catch();
        smallFish.Set(1, 0);
        uv.Add(smallFish);
        smallFish = vector2Pool.Catch();
        smallFish.Set(0, 1);
        uv.Add(smallFish);
        smallFish = vector2Pool.Catch();
        smallFish.Set(1, 1);
        uv.Add(smallFish);

        if (voxel.stateOfMatter == StateOfMatter.GAS)
        {
            indices.Add(vertexIndex + 2);
            indices.Add(vertexIndex + 1);
            indices.Add(vertexIndex);
            

            indices.Add(vertexIndex + 1);
            indices.Add(vertexIndex + 2);
            indices.Add(vertexIndex + 3);
        }
        else
        {
            indices.Add(vertexIndex);
            indices.Add(vertexIndex + 1);
            indices.Add(vertexIndex + 2);

            indices.Add(vertexIndex + 3);
            indices.Add(vertexIndex + 2);
            indices.Add(vertexIndex + 1);
        }
    }

    private void AddQuadY(VoxelMaterial voxel, int x, int y, int z, int normalDirection, ref List<Color> colors, ref List<Vector3> vertices, ref List<Vector3> normals, ref List<Vector2> uv, ref List<int> indices, ref Pool<Color> colorPool, ref Pool<Vector2> vector2Pool, ref Pool<Vector3> vector3Pool, ColorUtil colorUtil)
    {
        int vertexIndex = vertices.Count;

        Color color = colorPool.Catch();
        colorUtil.Set(ref color, voxel.color, 0.1f, 0.1f, 0.1f);
        colors.Add(color);
        color = colorPool.Catch();
        colorUtil.Set(ref color, voxel.color, 0.1f, 0.1f, 0.1f);
        colors.Add(color);
        color = colorPool.Catch();
        colorUtil.Set(ref color, voxel.color, 0.1f, 0.1f, 0.1f);
        colors.Add(color);
        color = colorPool.Catch();
        colorUtil.Set(ref color, voxel.color, 0.1f, 0.1f, 0.1f);
        colors.Add(color);

        Vector3 fish = vector3Pool.Catch();
        fish.Set(x, y + 1, z);
        vertices.Add(fish);
        fish = vector3Pool.Catch();
        fish.Set(x + 1, y + 1, z);
        vertices.Add(fish);
        fish = vector3Pool.Catch();
        fish.Set(x, y + 1, z + 1);
        vertices.Add(fish);
        fish = vector3Pool.Catch();
        fish.Set(x + 1, y + 1, z + 1);
        vertices.Add(fish);

        fish = vector3Pool.Catch();
        fish.Set(0, normalDirection, 0);
        normals.Add(fish);
        normals.Add(fish);
        normals.Add(fish);
        normals.Add(fish);

        Vector2 smallFish = vector2Pool.Catch();
        smallFish.Set(0, 0);
        uv.Add(smallFish);
        smallFish = vector2Pool.Catch();
        smallFish.Set(1, 0);
        uv.Add(smallFish);
        smallFish = vector2Pool.Catch();
        smallFish.Set(0, 1);
        uv.Add(smallFish);
        smallFish = vector2Pool.Catch();
        smallFish.Set(1, 1);
        uv.Add(smallFish);


        if (voxel.stateOfMatter == StateOfMatter.GAS)
        { 
            indices.Add(vertexIndex + 2);
            indices.Add(vertexIndex);
            indices.Add(vertexIndex + 1);

            indices.Add(vertexIndex + 2);
            indices.Add(vertexIndex + 1);
            indices.Add(vertexIndex + 3);
        }
        else
        {
            indices.Add(vertexIndex + 2);
            indices.Add(vertexIndex + 1);
            indices.Add(vertexIndex);
            

            indices.Add(vertexIndex + 1);
            indices.Add(vertexIndex + 2);
            indices.Add(vertexIndex + 3);
        }
    }

    private void AddQuadZ(VoxelMaterial voxel, int x, int y, int z, int normalDirection, ref List<Color> colors, ref List<Vector3> vertices, ref List<Vector3> normals, ref List<Vector2> uv, ref List<int> indices, ref Pool<Color> colorPool, ref Pool<Vector2> vector2Pool, ref Pool<Vector3> vector3Pool, ColorUtil colorUtil)
    {
        int vertexIndex = vertices.Count;


        Color color = colorPool.Catch();
        colorUtil.Set(ref color, voxel.color, 0.7f, 0.1f, 0.4f);
        colors.Add(color);
        color = colorPool.Catch();
        colorUtil.Set(ref color, voxel.color, 0.7f, 0.1f, 0.4f);
        colors.Add(color);
        color = colorPool.Catch();
        colorUtil.Set(ref color, voxel.color, 0.7f, 0.1f, 0.4f);
        colors.Add(color);
        color = colorPool.Catch();
        colorUtil.Set(ref color, voxel.color, 0.7f, 0.1f, 0.4f);
        colors.Add(color);

        Vector3 fish = vector3Pool.Catch();
        fish.Set(x, y, z + 1);
        vertices.Add(fish);
        fish = vector3Pool.Catch();
        fish.Set(x + 1, y, z + 1);
        vertices.Add(fish);
        fish = vector3Pool.Catch();
        fish.Set(x, y + 1, z + 1);
        vertices.Add(fish);
        fish = vector3Pool.Catch();
        fish.Set(x + 1, y + 1, z + 1);
        vertices.Add(fish);

        fish = vector3Pool.Catch();
        fish.Set(0, 0, normalDirection);
        normals.Add(fish);
        normals.Add(fish);
        normals.Add(fish);
        normals.Add(fish);

        Vector2 smallFish = vector2Pool.Catch();
        smallFish.Set(0, 0);
        uv.Add(smallFish);
        smallFish = vector2Pool.Catch();
        smallFish.Set(1, 0);
        uv.Add(smallFish);
        smallFish = vector2Pool.Catch();
        smallFish.Set(0, 1);
        uv.Add(smallFish);
        smallFish = vector2Pool.Catch();
        smallFish.Set(1, 1);
        uv.Add(smallFish);

        if (voxel.stateOfMatter == StateOfMatter.GAS)
        {
            indices.Add(vertexIndex + 2);
            indices.Add(vertexIndex + 1);
            indices.Add(vertexIndex);


            indices.Add(vertexIndex + 1);
            indices.Add(vertexIndex + 2);
            indices.Add(vertexIndex + 3);     
        }
        else
        {
            indices.Add(vertexIndex + 2);
            indices.Add(vertexIndex);
            indices.Add(vertexIndex + 1);

            indices.Add(vertexIndex + 2);
            indices.Add(vertexIndex + 1);
            indices.Add(vertexIndex + 3);
        }
    }
}

I already have a fast optimization for adjacent cubes - however, I optimize it for space, too. I don't want to change it, it's already much faster than similar games, I think. I have 60 FPS most of the time. Only too many caves and too many trees make my FPS drop (30 FPS at 512 blocks visual range at 4K resolution).

I just want to add LOD to generate less vertices for far away chunks.

This topic is closed to new replies.

Advertisement