23
\$\begingroup\$

I have a world made of many cubes (like in Minecraft), they have only color (not texture). I am rendering them using OpenGL 3.3 core profile (GLFW3, GLAD, GLM). I am already have done some optimizations:

  • not rendering internal faces
  • backface culling
  • rendering faces using GL_TRIANGLES and indexing

But it's still a lot slower than Minecraft, but Minecraft is in Java and blocks have textures, there is bigger world (there are entities etc. too)! How can I optimize my program further?
It may be something with pre-computing. Player rarely change the world (only small part of ticks).
I am currently filtering faces with this code:

// for every block
if (blocks[x][y][z] != nullptr) { // nullptr means that block is air
    bool drawSides[6];
    if (world.GetBlockAt(BlockPos(x, y, z - 1)) != nullptr)
        drawSides[0] = false; // internal face, culled
    else
        drawSides[0] = true; // draw that side
    // other 5 sides
    // ...
    blocks[x][y][z].draw(sides);
}
\$\endgroup\$
8
  • 9
    \$\begingroup\$ Do I understand correctly that every frame, you are looping over every possible face in your world, checking whether it's potentially visible, and if it is, submitting a draw call to draw just that one face? Have you considered building a chunk mesh, so you can draw something like a 16x16x16 block of visible faces with a single draw call? \$\endgroup\$ – DMGregory May 3 '20 at 20:31
  • 2
    \$\begingroup\$ @DMGregory Yes, you understood correctly. But what is that chunk mesh? \$\endgroup\$ – galaxy001 May 3 '20 at 20:43
  • 1
    \$\begingroup\$ Check a look at this he ues chunk for optimization: youtube.com/watch?v=Nj8gt_92c-M \$\endgroup\$ – Mederic May 4 '20 at 5:57
  • 1
    \$\begingroup\$ Just because you're coding in C++ doesn't mean you're going to get faster results. Think about high-level optimizations long before you deal with low-level micro-optimizations. For example, having all the blocks in one huge three-dimensional array probably isn't very good for quickly discarding blocks that aren't visible. That's one of the big reasons why most block games have chunks (e.g. 16x16x16 blocks in a single chunk). Random memory access also tends to be very slow, both on the CPU and GPU. As for threading... don't. You'll be disappointed, and it comes at great cost. \$\endgroup\$ – Luaan May 5 '20 at 7:10
  • 1
    \$\begingroup\$ Minecraft is a mature game that's had many years for the algorithms to be honed. Please don't try to compare your code to that sort of code. \$\endgroup\$ – NomadMaker May 5 '20 at 13:40
54
\$\begingroup\$

No high-performance voxel renderer draws individual cubes or cube faces.

To get good performance, groups of voxels are assembled together into a single mesh, called a "Chunk".

A chunk is typically something like 16x16x16 voxels, but might be larger or smaller as you need. It contains only the visible faces of voxels in that chunk, stitched together into a single mesh that can be drawn with a single draw call.

Compare that to drawing a solid 16x16x16 block of voxels using your current method:

  • iterate over all 16x16x16 voxels (4096 iterations)
  • check all 6 faces of each one (24 576 iterations)
  • draw those that border empty space (1 536 draw calls)

Instead:

  • check if chunk is in camera view frustum (1 check per chunk)
  • if so, draw the whole thing (1 draw call per chunk)

We've cut our work down by several orders of magnitude!

This means less work for your CPU to do in preparing the drawing instructions for each frame, and each draw call gives more for the GPU to churn through in parallel, making use of the dedicated hardware it has for this purpose.

When a voxel's state changes, you re-compute the visible faces in the affected chunk, and update its mesh accordingly. As you point out, this happens much more rarely than we render a frame, so we can skip that work for most chunks, most of the time, and just keep re-using our cached meshes.

You can also use your chunks for a form of occlusion culling. A single voxel doesn't occlude much, but a chunk's worth of voxels might be enough to completely block the view through one of the far sides. If it does, you can skip drawing whole chunks worth of content behind it, saving yourself a lot of work and overdraw.

Example of culling from the link above

Taking a look at the demo applet from Tommo's Advanced Cave Culling Algorithm article linked above, you can see the kind of savings chunks can offer. The dark shaded chunks are behind the camera frustum, so they get skipped. The green lines trace the order in which chunks are evaluated, stepping outward from the camera. The pink shaded chunks are completely hidden from view by closer chunks, and can be skipped too. So here the engine has to draw only 19 chunks, instead of millions of individual faces.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ I gave up that project a while ago because of much bugs I made. I've come back to it, and fixed much of them and now I have 5x more FPS 25x bigger world! Only using chunks and one VAO per chunk can make that much better results? WOW, amazing... \$\endgroup\$ – galaxy001 Jun 22 '20 at 23:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.