Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

InstancedModel

InstancedModel draws a registered model once for each placement in a list, in one draw call for each mesh in the model. Use it for a forest, a field of coins or a scree of rocks: things that stand many times and do nothing on their own. It spawns no entity, so a copy has no behaviours, no collider and no transform a system moves. A thing that moves, collides or is picked up is an <Entity model>.

A placement is a plain transform, so any code can produce the list:

  • position: the point the model’s lowest point stands on. The model is seated on its own bounds, as the scatter kit is, so a model centred on its origin still stands on the ground.
  • turn: radians about y. It defaults to 0. InstancedModel draws the file unturned, so at 0 its front faces +z, where an <Entity model> turns it to face -z.
  • scale: a multiple of the model’s own size, one number for every axis or one for each. It defaults to 1.
  • tint: a colour the copy is multiplied by. Copies without one keep the model’s own colours.

Its props are in InstancedModelProps and a placement’s fields in InstancePlacement.

Unity’s Graphics.RenderMeshInstanced, Unreal’s Instanced Static Mesh and Godot’s MultiMeshInstance3D take the same input: one mesh and a list of transforms.

The copies are never culled one by one and have no level of detail: the whole batch draws whenever any of it is in view. A headless scene draws nothing and loads no model.

material changes what every copy wears, as an Entity’s material does: a function handed each of the file’s materials, which returns the one the copies wear in its place, asked once for each of the file’s materials and kept. Left out, the copies wear the material the model registered with, or the file’s own. A mesh drawn with several materials asks the function for each of them. Return a new material, such as a clone, since the original is the loaded file’s; what the function returns is the game’s to dispose. The copies of one batch are drawn as one, so a see-through material draws them in one pass, not sorted one by one. For a colour that differs per copy, give each placement a tint.

These firs turn to autumn. The engine’s tests mount this file:

packages/engine/test/outside/AutumnTreeline.tsx
import {
InstancedModel,
type InstancePlacement,
type ModelMaterial,
} from "@spawnite/engine";
import { Color, MeshStandardMaterial } from "three";
// A row of firs turned to autumn: every copy wears its file's material
// with its colour pulled toward orange.
const orange = new Color("#d9822b");
/** A copy of the file's material, since the original is the loaded
* file's, which every other draw of the file wears. Made once, outside the
* component: InstancedModel asks it once for each of the file's
* materials. */
const autumn: ModelMaterial = (original) => {
const turned = original.clone();
if (turned instanceof MeshStandardMaterial) turned.color.lerp(orange, 0.6);
return turned;
};
const firs: InstancePlacement[] = Array.from({ length: 50 }, (_, index) => ({
position: [index * 3, 0, -10],
turn: index,
}));
export function AutumnTreeline() {
return <InstancedModel model="fir" placements={firs} material={autumn} />;
}

Client. The copies only change how the world looks.

import { InstancedModel, registerModel } from "@spawnite/engine";
import fir from "./fir.glb?url";
registerModel("fir", fir);
const firs = Array.from({ length: 50 }, (_, index) => ({
position: [index * 3, 0, -10] as [number, number, number],
turn: index,
scale: 1 + (index % 3) * 0.2,
}));
export function Treeline() {
return <InstancedModel model="fir" placements={firs} />;
}