Before we can start generating terrain, we have to think about the most efficient way to render a single block – because each chunk in our world will have up-to 4096 of them, and given our map size, we’re going to be dealing with over 18 million blocks. If you remember our commitment in the previous post to having this run at 60fps (on mobile), it’s time to get serious about optimization.
Draw-Calls Are Costly
Simply put, a draw-call is an instruction to the graphics card, and the more of these you have each frame, the slower things get – because there’s a limited amount of bandwidth to pass data between the CPU and the GPU. On modern PC’s you can get away with hundreds or even thousands of draw-calls, but on mobile devices, we need to stick to around 60-100 per frame to avoid significant slow-down.
If we’re targetting 60fps, we only have 16.33 milliseconds to draw each frame. A very tight schedule! So how on Earth are we going to render all these blocks with less than 100 draw-calls?
Batching is a technique for combining renderable elements (3D models, sprites, or in our case, blocks) that share a common texture into a single draw-call. More accurately, changing the texture is a draw-call, and rendering an array of polygons is a draw-call – so if we can combine all the textures into one (Atlasing) and all neighboring blocks into the same 3D model, we can drastically reduce the number of instructions sent to the GPU per frame.
A Texture Atlas is just a big texture made-up of smaller textures, and they allow for more efficient batching of draw-calls, because everything that shares the same texture can be batched together. The only downside is that applying textures to our blocks is slightly more complex.
Each type of block now needs to know how to find the texture for each face in our atlas. We do this by dividing the UV coordinates by the number of columns, and calculating an offset with some modulo for wrapping.
double scale=1.0/columns; double offsu=(subtexture*scale) % 1.0; double offsv=Math.floor(subtexture/columns)*scale; u=(u*scale)+offsu; v=(v*scale)+offsv;
Some block-types will use one sub-texture for all faces (like the pink cube above), and some will have different sub-textures for the top/sides etc.. but we only need to calculate this once per block-type.