5

I am working on a voxel terrain generator. Everything is fine, I have biomes, blocks, etc.

What tickles me is the speed of my project in unity. If I run everything on the main thread I can only load and render 1 to 2 chunks without dropping below 70fps. That's mainly because every block in a chunk has to check their neighbours to define their block side visibility. A block has 6 neighbours and a chunk has 16 blocks. That makes a lot of checks very quickly.

I've read that minecraft is single threaded but I have a hard time believing that since its chunk loading speed is quite fast and without fps drops.

My solution would be to run the checkings of a chunk's blocks's neighbours on a another thread. It would greatly improve my fps and my chunk loading speed. Is it the correct way though? I don't want to have to use threads because my code is not optimized. It would be like pushing the dust under the carpet.

Thanks for reading

EDIT : Code that checks for neighbours

//Block provides its mesh information
//Check for solidity of adjacent blocks
public virtual MeshData CreateBlockData(Chunk chunk, int x, int y, int z, MeshData meshData)
{
    //Set this to true to turn on collider creation shaped like the chunks
    meshData.useRenderDataForCol = true;

    if (!chunk.GetBlock(x, y + 1, z).IsSolid(Direction.down))
    {
        meshData = FaceDataUp(chunk, x, y, z, meshData);
    }

    if (!chunk.GetBlock(x, y - 1, z).IsSolid(Direction.up))
    {
        meshData = FaceDataDown(chunk, x, y, z, meshData);
    }

    if (!chunk.GetBlock(x, y, z + 1).IsSolid(Direction.south))
    {
        meshData = FaceDataNorth(chunk, x, y, z, meshData);
    }

    if (!chunk.GetBlock(x, y, z - 1).IsSolid(Direction.north))
    {
        meshData = FaceDataSouth(chunk, x, y, z, meshData);
    }

    if (!chunk.GetBlock(x + 1, y, z).IsSolid(Direction.west))
    {
        meshData = FaceDataEast(chunk, x, y, z, meshData);
    }

    if (!chunk.GetBlock(x - 1, y, z).IsSolid(Direction.east))
    {
        meshData = FaceDataWest(chunk, x, y, z, meshData);
    }

    return meshData;
}


//The center of block is the origin
protected virtual MeshData FaceDataUp(Chunk chunk, int x, int y, int z, MeshData meshData)
{
    meshData.AddVertex(new Vector3(x - 0.5f, y + 0.5f, z + 0.5f));
    meshData.AddVertex(new Vector3(x + 0.5f, y + 0.5f, z + 0.5f));
    meshData.AddVertex(new Vector3(x + 0.5f, y + 0.5f, z - 0.5f));
    meshData.AddVertex(new Vector3(x - 0.5f, y + 0.5f, z - 0.5f));
    meshData.AddQuadTriangles();
    //Adds UVs range (0 to 3) to uv list
    meshData.uv.AddRange(FaceUVs(Direction.up));
    return meshData;
}

Therefor, every chunk which is 16x16x16 blocks has 4096 blocks to run this function on.

The code that creates the blocks is simply a triple for loop containing this :

static void GeneratePlainBiome(Chunk chunk, int x, int y, int z, FastNoise noise)
{
    int stoneHeight = GetNoise2D(noise, x, z, 0, 50);
    int chunkX = (int)chunk.transform.position.x;
    int chunkY = (int)chunk.transform.position.y;
    int chunkZ = (int)chunk.transform.position.z;

    if(y == 0)
    {
        chunk.SetBlock(x - chunkX, y - chunkY, z - chunkZ, new BlockSnow());
    }
    else if(stoneHeight > y)
    {
        chunk.SetBlock(x - chunkX, y - chunkY, z - chunkZ, new BlockEarth());
    }
    else if(stoneHeight == y)
    {
        chunk.SetBlock(x - chunkX, y - chunkY, z - chunkZ, new BlockGrass());
    }
    else
    {
        chunk.SetBlock(x - chunkX, y - chunkY, z - chunkZ, new BlockAir());
    }
}

After I have filled a chunk, I render the mesh with this function :

//Sends the calculated mesh information to the mesh and collision components
void RenderMesh(MeshData meshData)
{
    //Mesh construction
    filter.mesh.Clear();
    filter.mesh.vertices = meshData.vertices.ToArray();
    filter.mesh.triangles = meshData.triangles.ToArray();

    //Uv mapping
    filter.mesh.uv = meshData.uv.ToArray();
    filter.mesh.RecalculateNormals();

    //Collision component creation
    coll.sharedMesh = null;
    Mesh meshColl = new Mesh();
    meshColl.vertices = meshData.colVertices.ToArray();
    meshColl.triangles = meshData.colTriangles.ToArray();
    meshColl.RecalculateNormals();

    coll.sharedMesh = meshColl;
}

So to resume, I'm checking the 16x16x16 blocks of a chunk to know how to render the chunk mesh based on neighbours. Once I am done with that function, I can choose to render the chunk. I'm doing that, let's say for a 16x16x16 chunks around the player. (Even if I do one chunk a frame, I get pretty bad fps drops.)

EDIT 2 :

For the chunk.SetBlock() and chunk.GetBlock() from the chunk script :

public void SetBlock(int x, int y, int z, Block block)
{
    if (InRange(x) && InRange(y) && InRange(z))
    {
        blocks[x, y, z] = block;
    }
    else
    {
        LoadBiomes.SetBlock((int)transform.position.x + x, (int)transform.position.y + y, (int)transform.position.z + z, block);
    }
}


public Block GetBlock(int x, int y, int z)
{
    if(InRange(x) && InRange(y) && InRange(z))
    {
        Block block = blocks[x, y, z];

        return block;
    }
    else
    {
        //return new BlockAir();

        int xPos = (int)transform.position.x + x;
        int yPos = (int)transform.position.y + y;
        int zPos = (int)transform.position.z + z;
        Block blockToReturn = LoadBiomes.GetBlock(xPos,yPos,zPos); 

        return blockToReturn;
    }

}

//This work since the values passed to the function are block position - chunk position
public static bool InRange(int index)
{
    if (index < 0 || index >= CHUNK_SIZE)
        return false;

    return true;
}

The isSolid in the block script (is not really important if game only have cubes

//Every face is solid for a cube
public virtual bool IsSolid(Direction direction)
{
    switch (direction)
    {
        case Direction.north:
            return true;
        case Direction.east:
            return true;
        case Direction.south:
            return true;
        case Direction.west:
            return true;
        case Direction.up:
            return true;
        case Direction.down:
            return true;
    }
    return false;
}

And the image from the profiler (not sure if that is what was asked)

Profiler

Pouissante
  • 56
  • 1
  • 5
  • 25
  • Are you calculating block side visibility for all ~loaded blocks~ Or just those that are in the current FOV? – FiringSquadWitness Apr 15 '19 at 00:23
  • 1
    I have a queue of chunks around the player (let's say 8x8x8) and I check every block of every chunk before rendering the chunk mesh based on visibility of the blocks @FiringSquadWitness – Pouissante Apr 15 '19 at 00:27
  • Did you measure your performance in a debug or release build? – user743414 Apr 15 '19 at 07:27
  • I didn't do a build to have the stats and profiler tab available. @user743414 – Pouissante Apr 15 '19 at 07:34
  • I don't think it would be of any help though if I was had a release build. Maybe I would grasp some fps but that is not really the point. – Pouissante Apr 15 '19 at 18:47
  • Could you post the code responsible for the block generation and neighbour checking? – Immorality Apr 16 '19 at 08:45
  • Instead of using multithreading, I think you should just cache visible layer along with generated chunk. It costs you nothing on hard drive and gives many advantages. Imagine it is alternative to Indexes in SQL database. Yes, calculation of those indexes should be asynchronous, in pool, in GPU or wherever you want, but you can calculate it on demand and save for later. Next time you walk across chunk - you just load its serialized mesh. One another thing - you should load only chunks in your FOV (Field of View, i.e. camera). – eocron Apr 16 '19 at 08:46
  • I did it @Immorality, thanks for reading – Pouissante Apr 16 '19 at 18:53
  • Mhhh, do you have some links where I can look up what you're saying @eocron? What tickles me is that minecraft still shows caves etc. which you can see when you glitch a bit. Which makes me think it doesn't rely on that caching example? Thank you for reading – Pouissante Apr 16 '19 at 18:58
  • Could you also provide the chunk.GetBlock, .SetBlock and .IsSolid code? Hard to say where your performance drop is comming from. (running a profiler could really help!) – Immorality Apr 16 '19 at 21:36
  • Sorry for the late answer, I've tried to provide everything you asked for. Thanks for the help @Immorality – Pouissante Apr 18 '19 at 20:45
  • It should also be noted that Minecraft does not check every block's side visibility every render frame. It asks once and computes a mesh that it uses to render until something changes. – Draco18s no longer trusts SE Apr 20 '19 at 17:06
  • I do also do that by updating a chunk and its blocks if something changed. The fps drop happens when chunks are generating not when the area around the player is loaded. Thanks you for the piece of advice still. @Draco18s – Pouissante Apr 20 '19 at 17:37

1 Answers1

1

I am not an expert but as far as I know unity 3d uses polygons and is not a voxel engine. Voxel engines are different.

A direct consequence of this difference is that polygons can efficiently represent simple 3D structures with lots of empty or homogeneously filled space, while voxels excel at representing regularly sampled spaces that are non-homogeneously filled.

https://en.wikipedia.org/wiki/Voxel

For technical details see:

What some voxel engines do is use big arrays then use them to determine what is in the field of view and what is not. This is very different from the classic 3d polygon way of doing things which started with Quake. Famous voxel games include Comanche series, Outcast ... and now Minecraft.

Christophe Roussy
  • 16,299
  • 4
  • 85
  • 85
  • 1
    Though, Minecraft is most known voxel game, but its performance is very bad, if not the baddest. Unity3d provide more than enough power for Minecraft clone (someone actually coded minecraft in couple of nights on youtube using Unity3d). Im just looking at some other multiplayer voxel projects, and see what could be done with much less resource consumption. – eocron Apr 16 '19 at 08:57
  • I do not know any details about how Minecraft works, I guess in 2019 you can just bruteforce your way to do a voxel engine with polygons because of the hardware acceleration. – Christophe Roussy Apr 16 '19 at 09:18
  • Thank for the answer, I don't think it is a problem related to unity. I think I just have too many iterations as you can see in the edit I've made. Moreover, I'm rendering what is next to the player. It's not that I render too much at a time, it's just that I have to many iterations and I don't know how I could reduce them. – Pouissante Apr 16 '19 at 19:06
  • Funny, I hear everyone say minecraft has bad performance but I can not find one tutorial which does better or nearly approaches its performance @eocron – Pouissante Apr 22 '19 at 18:49