Efficiently adding/removing vertices from a runtime mesh?

Hi!

We are looking into dynamically modifying mesh geometry at runtime, in particular cases where vertices and indices may be added or removed frequently.

MeshAccessor looks useful for editing existing mesh data, especially since it abstracts away the actual vertex buffer layout and formats. However, as far as we understand, its buffers have a fixed size once allocated with AllocateBuffer(). The underlying BytesContainer/DataContainer does not appear to be designed for dynamic resizing either.

This raises a question about the recommended approach when the topology itself changes frequently.

For example, suppose we have a mesh containing N vertices and some indices, and over time we need to add or remove vertices:

1000 vertices
   ↓
1050 vertices
   ↓
1100 vertices
   ↓
1030 vertices
   ↓
...

Reallocating the buffers to exactly match the new vertex/index count every time seems potentially wasteful, especially if those changes happen frequently.

While looking at the implementation, we also noticed that MeshAccessor::UpdateMesh() calls MeshBase::Init(), which in turn creates the vertex/index GPUBuffer objects through GPUDevice::CreateBuffer().

So, unless we are missing another code path, it looks like updating a mesh through MeshAccessor::UpdateMesh() creates new GPU buffer objects rather than updating the existing ones in place.

This surprised us a little, particularly because the documentation recommends MeshAccessor when “performing real-time mesh editing”. We assume there may be implementation details or another intended update path that we are overlooking.

We have considered a few approaches:

  1. Allocate some spare capacity

    We could use MeshAccessor::AllocateBuffer() to allocate buffers larger than currently required, similarly to the capacity of a dynamic array.

    Conceptually:

    Vertex capacity: 2048
    Vertices in use: 1350
    

    We could then add/remove vertices without reallocating until the capacity is exceeded.

    However, we haven’t found a way to tell a Mesh that only the first N vertices/indices of those buffers are currently in use. Also, since MeshAccessor::UpdateMesh() goes through MeshBase::Init(), we are not sure that over-allocating the accessor buffers would help with reusing the GPU buffers anyway.

  2. Split the geometry into multiple meshes

    Another possibility would be to use several smaller meshes, so that a local topology change only requires rebuilding one of them instead of reallocating one large mesh.

    Depending on the geometry, hardware geometry instancing might also be useful when some of those meshes share the same geometry, although that obviously does not apply to arbitrary topology changes.

So our questions are:

  • What is the recommended Flax approach for a runtime mesh whose vertex/index count changes frequently?
  • Is MeshAccessor::UpdateMesh() expected to recreate the GPU buffer objects on each update, including for real-time mesh editing, or is there another intended path for updating their contents in place?
  • Is there a supported way to allocate a Mesh/GPU buffer with some spare capacity while specifying a smaller number of vertices/indices to actually use for rendering?
  • If such spare capacity can be allocated, can the existing vertex/index GPU buffers then be updated and reused until that capacity is exceeded?
  • Or is splitting dynamic geometry into multiple smaller meshes generally the preferred approach in this situation?

We are mainly trying to understand the intended Flax architecture before building our own solution around it.

Thanks!

Small update after looking further into the engine source.

We found DynamicBuffer and its specialized DynamicVertexBuffer and DynamicIndexBuffer classes, and they seem very relevant to what we were trying to achieve.

In particular, DynamicBuffer::Flush() does this:

const uint32 size = Data.Count();

if (_buffer == nullptr)
    _buffer = GPUDevice::Instance->CreateBuffer(_name);

if (_buffer->GetSize() < size || _buffer->GetDescription().Usage != Usage)
{
    const int32 numElements = Math::AlignUp<int32>(
        static_cast<int32>((size / _stride) * 1.3f),
        32
    );

    GPUBufferDescription desc;
    InitDesc(desc, numElements);
    desc.Usage = Usage;
    _buffer->Init(desc);
}

context->UpdateBuffer(_buffer, Data.Get(), size);

So this appears to already implement the kind of capacity management we were looking for:

  • the amount of data currently used is tracked separately from the GPU buffer capacity;
  • the GPU buffer is only resized when the existing allocation is too small;
  • when it is resized, some spare capacity is allocated (1.3f, aligned to 32 elements);
  • otherwise, the existing GPU buffer is updated with UpdateBuffer().

We also found several uses of these classes in the engine. TextRender, for example, uses both a DynamicVertexBuffer and a DynamicIndexBuffer, rebuilds their CPU-side data when the text layout changes, and calls Flush() before rendering when the buffers are dirty.

This looks very close to the behavior we were looking for when asking about meshes whose vertex/index counts change frequently.

So at least regarding buffer allocation, it seems that Flax already has an established mechanism for this use case. We are continuing to investigate how this mechanism is intended to fit with the higher-level Mesh/Model APIs.