Instanced Mesh
Groups one or more instance-slot entities into a single GPU-instanced draw call. Each instance's transform comes from its corresponding InstanceSlotComponent entity; InstancedMeshSystem reads those transforms every frame and writes the instance matrices into the underlying THREE.InstancedMesh.
Use instanced meshes when you need tens, hundreds, or thousands of identical objects (trees, tiles, particles, projectiles, obstacles) without paying individual draw-call cost per object.
Managed by:
InstancedMeshSystem
Editor inspector

Properties
| Property | Type | Description |
|---|---|---|
meshKind | InstancedMeshKind | Geometry used for every instance. One of "cube", "sphere", "plane", "cylinder", "capsule", "model". |
modelPath | string | Source URL or localasset:// path of the GLTF/GLB asset. Only read when meshKind === "model". |
meshParameters | MeshGeometryParameters | Geometry dimensions (width/height/depth for box, radius/height for cylinder/capsule, radius for sphere, width/height for plane). Ignored when meshKind === "model". |
castShadow | boolean | Whether all instances cast shadows. |
receiveShadow | boolean | Whether all instances receive shadows. |
instanceEntityIds | number[] | Ordered list of entity IDs for the InstanceSlotComponent entities owned by this group. Each slot carries its own TransformComponent. |
instancedMesh | THREE.Object3D | null | Live runtime object managed by InstancedMeshSystem. null until the system builds the mesh. Not serialized — do not persist or clone this field. |
API reference
- Class:
InstancedMeshComponent - Source:
core/components/InstancedMesh.ts
Scripting examples
Component access patterns
Use this.getComponent(X) to access a component on the entity this script is attached to. Use this.world.getEntityByName to find another entity by name, then this.world.getComponent to read its component:
// Component on the entity this script is on
const instanced = this.getComponent(InstancedMeshComponent);
// Component on another named entity
const other = this.world.getEntityByName("Tree Group");
if (other) {
const instanced = this.world.getComponent(other.entityId, InstancedMeshComponent);
}Read instance count
import { Behaviour, InstancedMeshComponent } from "@relu-interactives/spatial-ecs";
export default class InstanceCounter extends Behaviour {
protected init() {
const instanced = this.getComponent(InstancedMeshComponent);
if (instanced) {
console.log(`This group has ${instanced.instanceEntityIds.length} instances`);
}
}
}Toggle shadows on all instances at runtime
import { Behaviour, InstancedMeshComponent } from "@relu-interactives/spatial-ecs";
import type { BooleanInput } from "@relu-interactives/spatial-ecs";
export default class ToggleShadows extends Behaviour {
data = {
castShadow: true as BooleanInput,
receiveShadow: true as BooleanInput,
};
protected init() {
const instanced = this.getComponent(InstancedMeshComponent);
if (!instanced) return;
instanced.castShadow = this.data.castShadow;
instanced.receiveShadow = this.data.receiveShadow;
}
}Animate every instance's Y position
Move each slot entity up and down using a sine wave. InstancedMeshSystem reads the TransformComponent of each slot every frame, so mutating the transform is all that is needed.
import {
Behaviour,
InstancedMeshComponent,
TransformComponent,
} from "@relu-interactives/spatial-ecs";
export default class WaveInstances extends Behaviour {
private elapsed = 0;
protected onUpdate() {
const instanced = this.getComponent(InstancedMeshComponent);
if (!instanced) return;
this.elapsed += this.deltaTime;
for (let i = 0; i < instanced.instanceEntityIds.length; i++) {
const slotId = instanced.instanceEntityIds[i];
const transform = this.world.getComponentById(slotId, TransformComponent);
if (transform) {
transform.position.y = Math.sin(this.elapsed + i * 0.5) * 2;
}
}
}
}Accept a group reference via ComponentInput
Use a ComponentInput field so the inspector can wire any InstancedMeshComponent entity to this script — useful when the script lives on a different entity than the group.
import {
Behaviour,
InstancedMeshComponent,
TransformComponent,
} from "@relu-interactives/spatial-ecs";
import type { ComponentInput } from "@relu-interactives/spatial-ecs";
export default class ScatterInstances extends Behaviour {
data = {
group: {
type: "component",
componentKind: "InstancedMesh",
value: null,
} as unknown as ComponentInput<InstancedMeshComponent>,
};
protected init() {
const instanced = this.data.group.value as InstancedMeshComponent | null;
if (!instanced) return;
// Scatter each instance randomly within a 10-unit area
for (const slotId of instanced.instanceEntityIds) {
const transform = this.world.getComponentById(slotId, TransformComponent);
if (transform) {
transform.position.x = (Math.random() - 0.5) * 10;
transform.position.z = (Math.random() - 0.5) * 10;
}
}
}
}Spawn a new instance slot at runtime
Add a new slot entity to an existing group from a script. The new slot's transform is registered with the group and InstancedMeshSystem picks it up on the next frame.
import {
Behaviour,
InstancedMeshComponent,
} from "@relu-interactives/spatial-ecs";
export default class DynamicSpawner extends Behaviour {
protected init() {
const instanced = this.getComponent(InstancedMeshComponent);
if (!instanced) return;
const groupId = this.entityId;
const objects = this.world.getObjectManagementSystem();
objects?.requestCreate(
"instanceSlot",
{
name: `Instance ${instanced.instanceEntityIds.length + 1}`,
transform: { position: { x: 2, y: 0, z: 0 } },
options: {
groupEntityId: groupId,
slotIndex: instanced.instanceEntityIds.length,
},
},
(slotId) => {
instanced.instanceEntityIds.push(slotId);
},
);
}
}See the Spawning Instanced Meshes guide for the full requestCreate API reference.

