"Rebuilding a multi-material model loses submesh materials"
▸ 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:
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 takingMaterials[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.
- Restated MeshInfo rationale without unverified marker (undocumented API, no whitelist precedent).