Skip to content

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 ​

Instanced Mesh inspector

Properties ​

PropertyTypeDescription
meshKindInstancedMeshKindGeometry used for every instance. One of "cube", "sphere", "plane", "cylinder", "capsule", "model".
modelPathstringSource URL or localasset:// path of the GLTF/GLB asset. Only read when meshKind === "model".
meshParametersMeshGeometryParametersGeometry dimensions (width/height/depth for box, radius/height for cylinder/capsule, radius for sphere, width/height for plane). Ignored when meshKind === "model".
castShadowbooleanWhether all instances cast shadows.
receiveShadowbooleanWhether all instances receive shadows.
instanceEntityIdsnumber[]Ordered list of entity IDs for the InstanceSlotComponent entities owned by this group. Each slot carries its own TransformComponent.
instancedMeshTHREE.Object3D | nullLive runtime object managed by InstancedMeshSystem. null until the system builds the mesh. Not serialized — do not persist or clone this field.

API reference ​

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:

typescript
// 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 ​

typescript
import { Behaviour } from "@relu-interactives/spatial-ecs";
import { InstancedMeshComponent } from "@relu-interactives/spatial-ecs/components";

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 ​

typescript
import { Behaviour } from "@relu-interactives/spatial-ecs";
import { InstancedMeshComponent } from "@relu-interactives/spatial-ecs/components";
import type { ToggleInput } from "@relu-interactives/spatial-ecs";

export default class ToggleShadows extends Behaviour {
  data = {
    castShadow: true as ToggleInput,
    receiveShadow: true as ToggleInput,
  };

  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.

typescript
import {
  Behaviour,
} from "@relu-interactives/spatial-ecs";
import {
  InstancedMeshComponent,
  TransformComponent,
} from "@relu-interactives/spatial-ecs/components";

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.getComponent(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.

typescript
import {
  Behaviour,
} from "@relu-interactives/spatial-ecs";
import {
  InstancedMeshComponent,
  TransformComponent,
} from "@relu-interactives/spatial-ecs/components";
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.getComponent(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.

typescript
import {
  Behaviour,
} from "@relu-interactives/spatial-ecs";
import {
  InstancedMeshComponent,
} from "@relu-interactives/spatial-ecs/components";

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.