## Dynamically Modifying 10,000 Cubes in 100 Draw Calls

October 11, 2014

In Sol 0 mineral resources are found by driving a rover and searching the Martian terrain for gypsum, clay, ore, methane, and subsurface ice. The game visualizes the resources that have been found with an overlay of cubes. As the rover drives, cubes are uncovered and the resulting color and height determines the mineral type and quantity. This looks nice, but was initially problematic as the number of cubes adds up quickly, and each cube was drawn individually. As the map size is 100 by 100 cubes, the resource overlay would add 10,000 draw calls to the game, slowing the framerate to a crawl.

In order to fix this problem groups of 10 by 10 cubes were combined into one mesh. This would lower the total draw calls to 100, much more acceptable than 10,000. This turned out to be fairly easy to accomplish in Unity. As the rover drives across the terrain, each time it uncovers a new cube the entire mesh of 10 x 10 cubes is redrawn and the positions and colors of the vertices recalculated. This is actually a very fast operation. Also, this does not happen every frame, just whenever the rover detects it is over a new cube.

When the level loads, the mesh vertices and colors are assigned to two arrays.

var mesh : Mesh = meshOf100Cubes.GetComponent(MeshFilter).mesh;
var vertices : Vector3[] = mesh.vertices;
var colors : Color[] = new Color[vertices.Length];

When the grid of 100 cubes needs to be updated, new heights and color values are calculated and then read out to the mesh. Each individual cube is made up of 24 vertices, as the cubes are really triangles. The code to update the height and color of an individual cube is:

for (var q = thisBlock*24; q < thisBlock*24+24; q++) {
colors[q] = thisColor;
if ( vertices[q].z > 0 ) {
vertices[q].z = thisHeight;
}
}

The variable thisBlock ranges from 0 to 99 and determines the individual cube that is being updated. The variables thisColor and thisHeight are obtained from other arrays that keep track of the mineral type and quantity at each point on the map. This section of code is looped 100 times in order to recalculate the color and height of each cube. Note that the cube height is only applied if the vertex position is greater than zero. This is because we always want the base of the cube to be anchored to the ground. Only the top of the cube should rise to the appropriate height. Once this code has been run for all the 100 cubes, the arrays 'vertices' and 'colors' now contain updated position and color values for the entire mesh. The last step is to update the mesh with the new values.

mesh.colors = colors;
mesh.vertices = vertices;
mesh.RecalculateBounds();

There are a few subtleties to this method. Note that we never read the mesh vertices or color values except when the level loads. When the rover uncovers a new cube we don't have to read in the mesh data, we just calculate new heights and colors and write to the mesh, resulting in a faster operation. It is also possible for the rover to uncover cubes from two different meshes at the same time, for example if the rover is driving straight up the border between two 100-cube meshes. To resolve this, before updating any meshes, the code checks the parent mesh of all the small cubes that the rover is uncovering, and only updates those meshes. The worst case scenario occurs when the rover is uncovering cubes at a four way intersection and four meshes need to be updated in one pass. However, this is still fast enough that no noticeable lag occurs.

The last possible issue: how do you know which vertices go with which cube? In other words, when you loop the above code 100 times, how do you know whether thisBlock=0 is the lower left cube, the upper right cube, or somewhere else? The cube meshes are made in Blender, starting with a single cube at the origin. This is cube 0, positioned at the lower left. An array modifier is applied in the x direction, resulting in cube 9 positioned at the lower right. Then another array modifier is applied in the y direction, resulting in cube 99 positioned at the upper right. Fortunately, using the array modifiers in this method results in the vertices aligning in the 1-dimensional mesh array as if you took the visual 2D array of cubes and lined up each row.

This change to the mineral system helped speed up the game and reduce the drawcalls from 10,000 to 100 when the mineral overlay is turned on. Pushing new vertex values and colors and updating the mesh is much faster than trying to draw every individual cube. This is probably obvious, but hopefully the details presented here in this specific application are useful.

comments powered by Disqus