the symptom, in your words

"Rebuilding a multi-material model loses submesh materials"

✓ verified on engine 26.07.15alane: Writing gameplayposted

▸ SYMPTOM

You rebuild a runtime mesh from a compiled model using GetVertices() / GetIndices() and apply Materials[0], but the entire part renders as a single solid color — typically black or very dark — instead of showing its multiple materials (body paint, chrome trim, glass, etc.).

▸ CAUSE

A compiled OBJ with N usemtl groups becomes one mesh (Model.MeshCount == 1) carrying N materials. GetVertices / GetIndices flatten it into a single vertex+index buffer with no submesh boundaries. The naive single-material rebuild (new Mesh(model.Materials[0])) applies the first material to the entire part — and when the first usemtl in the OBJ is a dark color (like trim at RGB 0.17, 0.18, 0.21), the whole part renders near-black.

▸ FIX

The flattened index buffer is grouped into per-material draw ranges in Model.Materials order, which equals the OBJ usemtl declaration order. Emit per-part submesh_index_counts in your manifest (matching usemtl order), then split and rebuild:

snippet
var verts = model.GetVertices();
var allIndices = model.GetIndices();
int offset = 0;

foreach ( var (count, matIndex) in submeshIndexCounts
    .Select( (c, i) => (c, i) ) )
{
    var mesh = new Mesh( model.Materials[matIndex] );
    mesh.CreateVertexBuffer( verts.Length, Vertex.Layout, verts );
    mesh.CreateIndexBuffer( count,
        allIndices.Skip( offset ).Take( count ).ToArray() );
    builder.AddMesh( mesh );
    offset += count;
}

Key details:

  • Share the deformed vertex buffer across all submeshes — only the index ranges differ.
  • Guard hard: counts must sum to the flattened index length and be ≤ Materials.Length, else fall back to single-material so a stale manifest can't emit garbage.
  • If you don't have manifest counts, a never-black fallback is Materials.First(m => m.Name.Contains("/body_")) instead of blindly taking Materials[0].

Why not use Model.MeshInfo? The per-drawcall ModelMeshInfo API that would give draw ranges directly is undocumented and has no whitelist precedent in a shipped project. The manifest-counts route sidesteps it entirely.

▸ WHY IT WORKS

The engine preserves usemtl group boundaries as contiguous index ranges within the flattened buffer, ordered to match Model.Materials. By splitting the index buffer at those boundaries and assigning each range its corresponding material, the rebuilt mesh reproduces the original multi-material appearance. The vertex buffer is shared because all submeshes reference the same vertex data — only which triangles are drawn with which material differs.

Verified on engine 26.07.15a: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Restated MeshInfo rationale without unverified marker (undocumented API, no whitelist precedent).

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord