vidar.games

Animated crowd instancing (VAT)

2026-11-07 · Discuss on X

Left: 300 creatures, each its own skinned mesh and skeleton, 300 draw calls.
Right: 5,000 of the same creature in 1 draw call, each wriggling on its own beat.

The problem

Instancing a static mesh is easy: one draw call, thousands of copies. Animated creatures are harder. A skinned mesh normally carries its own skeleton, the CPU poses every bone each frame, and each creature is its own draw call. So the CPU cost and the draw calls grow with every creature you add.

How it works

Bake the animation into a texture. Play the wriggle cycle once, record every vertex position at 32 steps, and store it as an image: one column per vertex, one row per step. Normals go in a second texture.

Then draw all creatures as one InstancedMesh. The vertex shader looks up its vertex in the texture, picks the row from a phase value and blends between two rows. No bones run at draw time.

Each instance only carries a transform (position and heading), a colour and one phase float. The phase is what breaks the lockstep: give every instance the same phase and 5,000 creatures wriggle in perfect sync, which reads as fake at once.

What I hit

My first skinned creature came out crumpled. I bound the skeleton before its bone matrices were updated, so the bind pose was wrong. Updating the world matrices first fixed it.

Baked normals: the skinned positions change the surface, so I recompute normals for each of the 32 frames before baking them.

Shadows: the shadow pass would need the same texture lookup in its depth shader. I skipped that and used soft blob shadows under each creature, on both halves.

My build

three.js (r187). The creature is my own tube mesh: 336 vertices, 648 triangles, 12 bones, animated by a travelling sine wave. The left half poses those 12 bones on the CPU for each of 300 SkinnedMeshes. The right half uses the same skinned mesh, baked into two 336 × 32 float textures.

The last shot adds steering on the CPU: separation within 0.28 m using a grid, a follow flow around the centre, and pushes away from 5 rocks. Each creature's phase advances with its ground speed, so faster ones wriggle faster.

Trade-offs

When to use it

Swarms, schools of fish, flocks, crowds in the distance, anything with many copies of one looping motion. Keep real skeletons for the few characters the camera gets close to.

New breakdowns go out on X first: @vidar_0208. If this one helped, you can buy me a coffee.

Source and credits

Topic credit: @Yokohara_h (UE5 short with a room full of crawling creatures). Only the crowd rendering technique is studied here; none of the scene, creatures or mood is reproduced.

Engine: three.js by @mrdoob and contributors, https://github.com/mrdoob/three.js, MIT license, branch dev at commit fa01dd6ad164c48554f1bb9456f6500db052ec4f. Used unchanged: SkinnedMesh, Skeleton, InstancedMesh, DataTexture, MeshStandardMaterial. The examples webgl_instancing_morph (per-instance animation offsets) and webgpu_skinning_instancing (instanced skinned mesh) were read for the approach.

My own code: the creature mesh and its 12-bone wave animation, the bake into position and normal textures, the vertex shader lookup with per-instance phase, the steering (separation, follow flow, rock avoidance), blob shadows, ground, rocks and camera. No external models or assets. No IA model.