June 27, 2025
My Terrain System
My engine has a 3D terrain system. This system splits up my world into chunks, and for each, uses Marching Cubes to generate a terrain mesh for each chunk. This chunk-based system lets me divide up my terrain into smaller jobs that could be processed asynchronously, letting me concurrently load and unload parts of the world as the player moves around.
This system gives me a lot of flexibility, but poses a bit of a problem when it comes to rendering. At the end of my terrain update, I’m left with thousands of meshes that together, make up my terrain. If I were to issue one draw call for each terrain mesh, I would need several tens of thousands of draw calls! This invokes a lot of overhead and is very inefficient.
So, I need a way to render my terrain without invoking this high degree of overhead. I’m also constrained by the following needs:
- I can’t simply combine all of my chunk meshes into one large mesh, as this will unnecessarily invoke the vertex shader many times for vertices that are clearly outside of the view.
- If I had a finite number of mesh configurations, I could use instancing, but I don’t. My Marching Cubes algorithm linearly interpolates between the corners of the Marching Cube, so no two Marching Cubes generate the same mesh.
- I’m running D3D11, meaning I cannot utilize
DrawMultiIndirect
or other indirect rendering methods.
Vertex Pulling
The solution I opted for to solve this problem is called vertex pulling.
Traditionally, when you issue a draw call, you (generally) do the following:
- Bind the shaders / shader resources
- Bind the vertex / index buffers that represent the mesh
- Issue a draw call
However, the vertex shader doesn’t have to get its vertex data from the vertex buffer! Instead, we can have it pull vertex data from shader resources!
This doesn’t mean much by itself, but is very useful when combined with some other D3D11 capabilities. Introducing:
- The
SV_VertexID
semantic, a system generated value that is automatically assigned to each vertex. - The
SV_InstanceID
semantic, a system generated value that is automatically assigned to each instance. - The
StructuredBuffer
, a shader resource which lets us upload arrays of structures to the GPU without worrying about alignment.
So here’s what we can do. Suppose we have a chunk with 4 triangles. Here’s how we can render this chunk:
- Issue a draw call for 4 triangles, with the
SV_VertexID
semantic. This will pass vertices with IDs into the pipeline. - For each vertex ID, index a
StructuredBuffer
containing their positions / normals. - Use these positions / normals in whatever other shader calculations we have.
To render this chunk, we’re just issuing a draw call for arbitrary vertices, and pull their data from a resource in the GPU. Because these vertices are arbitrary, if we knew that every terrain chunk had no more than vertices, then we could use instancing to render every chunk in one draw call!
As the most bare-bones example, define the following two StructuredBuffers
:
sb_chunks
: Elements of buffer 1 containvertex_start
,vertex_count
which tell us what vertices in buffer 2 belong to this chunk.sb_vertices
: Elements of buffer 2 contain vertex data, such as position / normals.
Then, letting every chunk correspond to one instance, we can issue one instanced draw call that does the following:
- Let be the number of chunks we have. Populate our structured buffers with the chunk data.
- Suppose each chunk has no more than triangles. Issue an instanced draw call for instances and triangles per instance.
- In the vertex shader:
- Use
SV_InstanceID
to indexsb_chunks
and figure our what vertices insb_vertices
to pull data from.
chunk = sb_chunks[instance_id]
- If
SV_VertexID
is less thanchunk.vertex_count
, useSV_VertexID
to indexsb_vertices[chunk.vertex_start + vertex_id]
and get the vertex data.
// Option 1 vertex_data = sb_vertices[chunk.vertex_start + vertex_id]
- Otherwise, return NaN. This has the effect of creating a degenerate triangle that won’t be visible.
// Option 2 // If vertex_id >= chunk.vertex_count (no more // vertices in the chunk to render) vertex_data = NaN
- Use
This lets us render our terrain in one call! Furthermore, because the chunks we render only depend on the chunk vertex_start
/ vertex_offset
that are in sb_chunks
, we can easily perform culling before rendering! Simply update the chunks in sb_chunks
before making the same draw call sb_vertices
can stay the same.