13
\$\begingroup\$

I have a cube-based world like Minecraft and I'm wondering if there's a way to construct a cube with fewer than 24 vertices so I can reduce memory usage.

It doesn't seem possible to me for 2 reasons: the normals wouldn't come out right and per-face textures wouldn't work.

Is this the case or am I wrong? Maybe there's some fancy new DX11 tech that can help?

Edit: Just to clarify, I have 2 requirements: I need surface normals for each cube face in order to do proper lighting and I need a way to address a different index in a texture array for each cube face

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Do you want to reduce graphics card memory usage or RAM usage? \$\endgroup\$ Jun 26, 2012 at 8:36
  • 1
    \$\begingroup\$ The vertex data is stored in RAM and on the GPU right now, so reducing it would reduce both. \$\endgroup\$
    – Telanor
    Jun 26, 2012 at 8:53

2 Answers 2

9
\$\begingroup\$

If you only need per-face normals, and if your texcoords for a face are strictly 0/0, 0/1, 1/0, 1/1 (or similar to suit your layout) then you can construct a cube with 8 verts and either 30 (strip with restart) or 36 (list) indexes. Fetch the normals and texcoords using a constant array lookup based on SV_VertexID in your vertex shader.

Doing this means that you don't even need to include texcoords or normals in your vertex buffer, which will give you even more of a memory saving.

Going further, you could still go all the way to 24 verts per cube but also use instancing. Each cube would be a fixed-size in your vertex buffer (1x1x1) and you'd have a scaling factor and position (assuming your cubes don't rotate, a matrix if they do) as per-instance data. In the non-rotating case you've a once-off cost of 24 verts, but then each cube just needs 6 floats to fully specify. In the rotating case you're looking at 16 floats, but even that is a substantial saving (you're more likely to bottleneck CPU-side on matrix transforms in this case - for the non-rotating case constructing a matrix on the fly in your vertex shader - even if it's done per-vertex, is so stupidly fast that you don't even need to worry about it).

For per-face textures, just use a texture array. You need to ensure that each such texture in the array is the same size, of course, and you'll still need to break your current batch if the array itself needs to change, but otherwise it'll do the job just fine. Add a third texcoord to your vertex definition which defines the array slice to use for each face.

You don't need a GS with this, and it should run faster than using one as having the geometry shader stage enabled will impose extra overhead of it's own.

I've benchmark code in my engine that just draws a bunch of cubes using this method, and I can easily chew through over 300,000 cubes while still clearing 60fps, on a relatively low-end GPU, and without doing anything else to optimize the process. Admittedly I'm neither lighting nor texturing them, but I do have alpha blending enabled, backface culling disabled, and overall it balances against my "not doing anything else to optimize" part, so it should give you a reasonable idea of the kind of ballpark you can hit with this method.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ I was using instancing before and it works great for under 40k cubes. But I have at least 256k cubes, and instancing wasn't handling it as well. Using SV_VertexID with a lookup sounds very promising though. \$\endgroup\$
    – Telanor
    Jun 26, 2012 at 22:28
  • \$\begingroup\$ How big was your per-instance buffer? Trying to stuff them all into a huge buffer may well end up choking your GPU; I generally keep per-instance buffers at enough space for 64k objects (splitting the draw calls if necessary), use the discard/no-overwrite pattern, and write directly to the mapped pointer instead of copying from an intermediate array, which works very well. \$\endgroup\$ Jun 26, 2012 at 22:51
  • \$\begingroup\$ Also - keeping number of maps low is essential. Map/write one cube/unmap 40k times is going to be much slower than map/write 40k cubes/unmap just once. If you were doing the former try switching to the latter. \$\endgroup\$ Jun 26, 2012 at 22:55
15
\$\begingroup\$

I think the main optimization you can make, is based on the fact that not every cube will actually need all 24 vertices. In fact, the only cubes that need 24 vertices are the ones that are floating in midair, which is probably a rare occurrence.

In general, only generate quads for the faces that are in contact with air. This means that if two cubes are touching each other, you don't need to generate any vertices for the face where they're touching.

The image below demonstrates this concept, but in 2D for easier comprehension. In the image there are 11 occupied blocks (represented by the filled gray circles), which would normally require 4 x 11 = 44 edges to represent (4 because it's a square, not a cube). But as you can see, you only really need to draw the edges when they're in contact with an empty square, which in this case, is only 8 edges.

enter image description here

If such a simple example in 2D managed to reduce 44 edges to 8 edges, imagine the gains in a large 3D world...

This is the approach described in this article, which I recommend you to read, despite being aimed at OpenGL. The concepts should be pretty universal though.

You could also probably use a geometry shader to generate the vertices on the fly on the GPU, eliminating the need to store them in memory, but I don't have experience with that, nor do I know how well it would perform for a large world.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Very interesting article. I am doing the edge optimization already, which certainly helps a lot. I'll give geometry shaders a try and see if that works out. \$\endgroup\$
    – Telanor
    Jun 26, 2012 at 10:00
  • \$\begingroup\$ Do you mean, use the geometry shader and the cross product to calculate the normals? I was thinking of using that, like create a separate program for flat surfaces (i'm not concerned with textures). \$\endgroup\$
    – dreta
    Jun 26, 2012 at 21:45
  • \$\begingroup\$ @dreta Not only that, but going to the point where you only submit the cube centroids as a point list, and all the vertices are emitted by the GS. Perhaps encode some neighbouring info into the points so that the shader only generates the faces that are really needed, though I havent thought how. \$\endgroup\$ Jun 27, 2012 at 2:09
  • \$\begingroup\$ If it's performance you want, don't use a geometry shader... Almost ever. They completely screw up the pipeline. If you want to generate geometry on the GPU, use a compute shader or openCL \$\endgroup\$ Dec 12, 2013 at 1:25

You must log in to answer this question.

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