My world is split into many (tens of thousands) of Chunks that each have a list of structs containing matrices for objects that should be GPU instanced when that Chunk is visible to the player (determined by mesh renderers on each Chunk). In order to save on draw calls, I have one universal GraphicsBuffer of these structs that I render with Graphics.RenderMeshIndirect.
I would like to efficiently add/remove each Chunk’s structs from the buffer possibly many times per second (as Chunks are loaded, unloaded, become visible, become invisible, etc.). What would be an efficient/performant way of doing this?
Here is some pseudocode:
public struct GPUInstancingStruct {
public float4x4 matrix;
}
// Many Chunks in world
public class Chunk {
public List<GPUInstancingStruct> structs;
}
// One GPUInstancer
public class GPUInstancer : MonoBehaviour {
GraphicsBuffer buffer;
private void Start() {
UpdateBuffer();
}
// Very inefficient
private void UpdateBuffer() {
// combine structs from all Chunks in world into one list
List<GPUInstancingStruct> allStructs = new();
foreach (Chunk chunk in World.Chunks) {
allStructs.AddRange(chunk.structs);
}
// is there a way to reuse the same buffer for a different amount of structs?
buffer?.Dispose();
buffer = new(GraphicsBuffer.Target.Structured, allStructs.Count, sizeof(float) * 16);
buffer.SetData(allStructs);
}
// Can be called many times per second
public void OnChunkAdded(Chunk chunk) {
// TODO: need some performant way of adding chunk's structs to buffer
// instead of recombining ALL chunks into one list again with UpdateBuffer
UpdateBuffer();
}
// Can be called many times per second
public void OnChunkRemoved(Chunk chunk) {
// TODO: need some performant way of removing chunk's structs from buffer
// instead of recombining ALL chunks into one list again with UpdateBuffer
UpdateBuffer();
}
private void Update() {
RenderMeshesUsingBuffer();
}
}
I thought of saving all structs in a list in GPUInstancer and having each Chunk store its index in the list and struct count, so adding and removing Chunks would look like this:
public void OnChunkAdded(Chunk chunk) {
chunk.index = allStructs.Count;
chunk.count = chunk.structs.Count;
allStructs.AddRange(chunk.structs);
CreateBuffer();
}
public void OnChunkRemoved(Chunk chunk) {
allStructs.RemoveRange(chunk.index, chunk.count);
// need to update chunk.index for every Chunk after this one...
UpdateChunkIndicesForChunksAfterThisOne(chunk);
CreateBuffer();
}
private void UpdateChunkIndicesForChunksAfterThisOne(Chunk chunkIn) {
foreach (Chunk chunk in World.Chunks) {
if (chunk.index > chunkIn.index) {
chunk.index -= chunkIn.count;
}
}
}
private void CreateBuffer() {
buffer?.Dispose();
buffer = new(GraphicsBuffer.Target.Structured, allStructs.Count, sizeof(float) * 16);
buffer.SetData(allStructs);
}
This still needs to recreate the buffer every time a Chunk is added/removed and iterate over every Chunk to update Chunk.index every time a Chunk is removed, so I don’t know if it is the most performant option. “allStructs.RemoveRange” also has to perform a copy for each struct after the end of the range of items, so it could be up to O(n).