Mesh Instancing in Polyphase Engine
Overview
Polyphase Engine supports mesh instancing through two distinct mechanisms:
- Asset-level sharing - Multiple scene nodes reference a single
StaticMeshasset in memory (always active, all platforms). - GPU hardware instancing - A single draw call renders many copies of the same mesh with per-instance transforms (Vulkan only, via
InstancedMesh3D).
On platforms that lack hardware instancing support (GameCube, Wii, 3DS), the engine uses an unrolling fallback that merges instance geometry into batched static meshes at load time.
Asset Sharing (All Platforms)
Every StaticMesh is a shared asset managed by the AssetManager. When you place the same mesh 100 times using individual StaticMesh3D nodes, the mesh data is loaded into memory once. Each node holds an AssetRef (a lightweight smart-pointer wrapper defined in Engine/Source/Engine/AssetRef.h) that points back to the single shared StaticMesh asset.
What this means in practice:
- Importing one building mesh and placing it 50 times does not duplicate the vertex/index data 50 times in RAM or VRAM.
- The GPU-side mesh resource (StaticMeshResource) is created once and bound for each draw call that uses it.
- This is a clear win over importing 50 copies of the geometry from Blender/Maya as a single merged blob, because the single-asset approach uses far less memory.
However, each individual StaticMesh3D node still generates its own draw call. There is no automatic cross-node draw call batching - 50 separate StaticMesh3D nodes using the same mesh = 50 separate vkCmdDrawIndexed calls (each with instanceCount = 1). On modern desktop GPUs this is usually fine, but on constrained hardware the draw call overhead adds up.
InstancedMesh3D Node (Hardware Instancing)
InstancedMesh3D (defined in Engine/Source/Engine/Nodes/3D/InstancedMesh3d.h) is a specialized node designed for rendering many copies of the same mesh efficiently. It extends StaticMesh3D and adds:
- A vector of
MeshInstanceData, each containing per-instanceposition,rotation, andscale. - An
InstancedMeshCompResourcethat holds GPU-side instance data buffers. - Support for per-instance vertex colors (baked lighting).
How It Works
Instead of creating 100 separate nodes, you create one InstancedMesh3D node, assign it a StaticMesh, and populate its instance data array:
MeshInstanceData data;
data.mPosition = {10.0f, 0.0f, 5.0f};
data.mRotation = {0.0f, 45.0f, 0.0f};
data.mScale = {1.0f, 1.0f, 1.0f};
instancedMesh->AddInstanceData(data);
On Vulkan, this results in a single vkCmdDrawIndexed call with instanceCount set to the number of instances. The GPU vertex shader reads per-instance transform data from a storage buffer to position each copy. This is true hardware instancing - the GPU does all the heavy lifting.
Per-Instance Colors
InstancedMesh3D supports baked per-instance vertex colors for static lighting. The vertex type is selected automatically based on what data is available:
| Mesh Has Vertex Color | Instance Colors Present | Vertex Type Used |
|---|---|---|
| No | No | Vertex |
| Yes | No | VertexColor |
| No | Yes | VertexInstanceColor |
| Yes | Yes | VertexColorInstanceColor |
Platform-Specific Behavior
Vulkan (Windows / Linux / Android)
- Full hardware instancing support.
InstancedMesh3Drenders all instances in a singlevkCmdDrawIndexedcall withinstanceCount = N.- Per-instance transforms are uploaded to a storage buffer (
mInstanceDataBuffer) and read by the vertex shader. - The instance data buffer is rebuilt when marked dirty (instances added/removed/modified).
ShouldUnroll()returnsfalseon Windows/Linux/Android - instances stay as true GPU instances.
Relevant code: Engine/Source/Graphics/Vulkan/VulkanUtils.cpp - DrawInstancedMeshComp() (line ~1905).
GameCube / Wii (GX)
- No hardware instancing support.
GFX_DrawInstancedMeshComp()is an empty stub inEngine/Source/Graphics/GX/Graphics_GX.cpp(line ~842).ShouldUnroll()returnstruefor these platforms (any platform that isn't Windows/Linux/Android).- On load,
InstancedMesh3D::Unroll()is called automatically, which merges instance geometry into batched static meshes.
How unrolling works:
- The engine calculates a spatial grid over all instance positions using
mUnrolledCellSize(default 25 units). - Instances are bucketed into grid cells based on their XZ position.
- For each non-empty cell, vertex data from the source mesh is duplicated and pre-transformed by each instance's transform (position, rotation, scale baked into vertex positions and normals).
- A new transient
StaticMeshasset is created per cell containing the merged geometry. - A child
StaticMesh3Dnode is spawned for each cell, with optionalmUnrolledCullDistancefor distance-based culling. - The parent
InstancedMesh3Dstops rendering itself (mUnrolled = trueskips theRender()call).
Implication: On GCN/Wii, instancing saves you from manually merging geometry in your DCC tool, but at runtime the geometry is duplicated per instance in VRAM (just like importing a merged scene from Blender). The benefit is spatial cell-based culling and simplified authoring - the engine handles the merge at load time, and cells outside the cull distance can be skipped entirely.
Limitation: Unrolling does not support meshes with vertex colors.
Relevant code: Engine/Source/Engine/Nodes/3D/InstancedMesh3d.cpp - Unroll() (line ~455).
3DS (Citro3D)
- No hardware instancing support.
GFX_DrawInstancedMeshComp()is an empty stub inEngine/Source/Graphics/C3D/Graphics_C3D.cpp(line ~1061).- Same unrolling behavior as GCN/Wii -
ShouldUnroll()returnstrue. - Geometry is pre-merged into spatial cells at load time.
Summary Table
| Feature | Vulkan (Win/Linux) | GX (GameCube/Wii) | C3D (3DS) |
|---|---|---|---|
| Asset sharing (1 mesh in memory) | Yes | Yes | Yes |
| Hardware instanced draw calls | Yes | No | No |
InstancedMesh3D support |
Full (GPU instancing) | Unroll fallback | Unroll fallback |
| Per-instance transforms | Storage buffer (GPU) | Baked into vertices | Baked into vertices |
| Draw calls for 100 instances | 1 | ~N cells (spatial grid) | ~N cells (spatial grid) |
| Per-instance vertex colors | Yes | No (unroll limitation) | No (unroll limitation) |
Practical Guidance: Instancing vs. Merged Geometry
Should you import one mesh and instance it, or merge everything in Blender/Maya?
Use InstancedMesh3D (one mesh, many instances) when:
- You have many copies of the same mesh (trees, buildings, rocks, props).
- On Vulkan: massive draw call savings (100 instances = 1 draw call).
- On GCN/Wii/3DS: the engine handles the merge for you with spatial culling built in, so you get the same end result as a manual merge but with less manual work and better culling.
- You want the flexibility to adjust instance placement without re-exporting from your DCC tool.
Use merged geometry (single large mesh from Blender/Maya) when: - The objects are all unique (no repeated meshes). - You need vertex colors on GCN/Wii/3DS (unrolling doesn't support them). - You want full control over the final mesh topology and optimization.
Use individual StaticMesh3D nodes when:
- You have a small number of unique objects that each need independent behavior (animation, physics, scripting).
- Draw call count isn't a concern (few objects on screen at once).
The Bottom Line
On Vulkan targets, using InstancedMesh3D is a significant performance win - it's real GPU instancing with a single draw call. Importing one building mesh and instancing it 50 times is dramatically cheaper than 50 separate StaticMesh3D nodes (1 draw call vs 50), and uses the same amount of VRAM either way since asset data is always shared.
On GCN/Wii/3DS targets, InstancedMesh3D is a convenience and culling feature, not a draw call reduction tool. The geometry gets pre-merged at load time (like importing a merged scene from Blender), but it's split into spatial cells with distance culling. This is roughly equivalent to manually merging geometry in your DCC tool and splitting it into chunks for LOD culling - but the engine does it automatically. Memory usage is the same as a manual merge (vertices are duplicated per instance), but you gain spatial culling that you wouldn't get from a single monolithic merged mesh.
TL;DR: Always prefer InstancedMesh3D over placing many individual StaticMesh3D nodes for repeated geometry. On Vulkan you get true GPU instancing. On console exports you get automatic spatial cell merging with distance culling. Either way it's better than both "50 separate nodes" and "one giant blob from Blender" approaches.
Configurable Properties
InstancedMesh3D exposes these properties in the editor:
| Property | Type | Default | Description |
|---|---|---|---|
| Unrolled Cull Distance | Float | 0.0 | Distance at which unrolled cells are culled (0 = no culling) |
| Unrolled Cell Size | Float | 25.0 | Size of spatial grid cells (in world units) used when unrolling |
| Always Unroll | Bool | false | Force unrolling even on platforms that support hardware instancing |
These only affect behavior when the InstancedMesh3D is unrolled (console platforms, or when Always Unroll is enabled).