Three.js instanced meshes

Animate InstancedMesh and BatchedMesh slots one at a time through per-instance proxies.

getInstances(mesh) returns one proxy per slot. The proxies accept the same property names as a regular mesh.

import { animate, stagger } from 'animejs';
import { getInstances } from 'animejs/adapters/three';

const instances = getInstances(mesh);

animate(instances, {
  x: 100,
  scale: 2,
  delay: stagger(20),
});

getInstances

Returns the array of per-instance proxies for mesh. Pass the array, a slice of it, or a single proxy to animate() and utils.set().

const instances = getInstances(mesh);

Parameters

Name Accepts
mesh An InstancedMesh or BatchedMesh

Returns

An Array of per-instance proxies. Deleted BatchedMesh slots are null.

The array reference stays the same across mesh.count, addInstance(), and deleteInstance(), so animations bound to it keep working as the instance count changes.

commitChanges

Flushes pending matrix writes for mesh. The adapter calls this automatically before each render. Call it yourself only if you read mesh.instanceMatrix between an animation tick and the next render.

commitChanges(mesh);

Parameters

Name Accepts
mesh An InstancedMesh or BatchedMesh previously passed to getInstances()

Per-instance properties

Each proxy carries the full Object3D transform set, the skew and transformOrigin axes, plus a color routed through mesh.setColorAt().

Name Maps to
x / y / z per-instance position
rotateX / Y / Z per-instance rotation (degrees)
scaleX / Y / Z / scale per-instance scale
skewX / Y / Z per-instance skew (degrees)
transformOriginX / Y / Z per-instance origin shift
transformOrigin 'x y z' shorthand
color mesh.setColorAt(id, color)
visible mesh.setVisibleAt(id, value) (BatchedMesh only)

opacity writes the shared material, so it affects every instance. To fade individual slots, build an alpha channel into the material's shader and animate the per-instance color. visible is also a no-op on InstancedMesh, use scale = 0 to hide a single instance.

After getInstances() runs, mesh.onBeforeRender === fn identity checks will not match, but assigning to it still works.

Three.js instanced meshes code example

import { animate, createTimer, stagger, utils } from 'animejs';
import * as THREE from 'three';
import { getInstances } from 'animejs/adapters/three';

// Three.js setup

const [ $container ] = utils.$('.full-container');
const { width, height } = $container.getBoundingClientRect();

const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, preserveDrawingBuffer: true });
renderer.setSize(width, height);
renderer.setPixelRatio(window.devicePixelRatio);
$container.appendChild(renderer.domElement);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, width / height, 0.01, 100);
camera.position.set(0, 0, 1.5);
scene.add(camera);

scene.add(new THREE.AmbientLight(0xffffff, 0.35));
const light = new THREE.DirectionalLight(0xffffff, 2);
light.position.set(2, 3, 4);
scene.add(light);

const gridSize = 6; // cubes per axis
const cellSize = 2 / gridSize; // size of each cube
const spread = (gridSize - 1) / 2 * cellSize; // distance from center to the outer cubes
const geometry = new THREE.BoxGeometry(cellSize, cellSize, cellSize);
const material = new THREE.MeshLambertMaterial();
const mesh = new THREE.InstancedMesh(geometry, material, gridSize * gridSize * gridSize);
scene.add(mesh);

// Animation with Three.js adapter

const instances = getInstances(mesh);
const palette = ['red', 'orange', 'yellow', 'green', 'sky', 'purple', 'pink']
  .map(name => utils.get($container, `--hex-${name}-1`));
const gridAxis = (axis, span = spread) => stagger([-span, span], { grid: [gridSize, gridSize, gridSize], axis });

// Slowly rotate the whole mesh
animate(mesh, {
  rotateY: 360,
  rotateX: 360,
  duration: 24000,
  loop: true,
  ease: 'linear',
});

// Color, scale and spread each instance, staggered from the center
animate(instances, {
  color: palette,
  x: [gridAxis('x', spread * .25), gridAxis('x')],
  y: [gridAxis('y', spread * .25), gridAxis('y')],
  z: [gridAxis('z', spread * .25), gridAxis('z')],
  scale: [.1, .25, .1],
  delay: stagger([0, 3000], { grid: [gridSize, gridSize, gridSize], from: 'center', reversed: true }),
  duration: 2000,
  loopDelay: 500,
  loop: true,
  alternate: true,
  ease: 'inOutQuad',
});

createTimer({ onUpdate: () => renderer.render(scene, camera) });
<div class="full-container"></div>