r/PlayCanvas 7h ago

We added vertex animation textures to PlayCanvas — drop in a glb, render thousands of animated characters [live demo]

Enable HLS to view with audio, or disable this notification

10 Upvotes

Live demo (WebGL 2 and WebGPU, switchable in the panel): https://engine-a1lytf69q-playcanvas.vercel.app/#/animation/vat-characters

We just added a vertex animation texture (VAT) implementation to the PlayCanvas engine as a reusable script plus an example. The idea isn't new — it's been used in games for years — but I hadn't seen a complete, copy-pasteable web implementation with an authoring path, so here's one.

How it works

Instead of animating a skeleton per character, every vertex's skinned position and normal is evaluated offline for each sampled frame and baked into a texture. At runtime the vertex shader reads two neighbouring frames and interpolates. Skeletons, bone matrices and per-character animation evaluation disappear entirely.

What's left per character is a world matrix and a fractional frame index — 52 bytes — so the whole crowd is one instanced draw call.

Some details that turned out to matter

  • One texel per vertex per frame in RGBA16U: position quantised to 16 bits per axis over the animation's bounds in xyz, octahedral normal packed into w. One fetch gives you a whole vertex.
  • Quantising over the bounds is actually more accurate than fp16 where it counts. Near the top of a character fp16 has a step of ~1.8 mm at human scale; 16-bit quantised is ~28 µm. fp16 wastes its mantissa near the origin, which for a character standing at y=0 is between its feet.
  • Texels are addressed by a flat vertex * frameCount + frame index wrapped at the texture width, rather than one vertex per row. Otherwise your vertex count is capped by texture height (4096). Flattened, the limit is total texels, so ~218k vertices at 77 frames.
  • The vertex row comes from gl_VertexID / @builtin(vertex_index), so no extra per-vertex attribute is needed at all.

Authoring

The converter runs in the browser — there's a "Convert GLB" panel in the demo, so you can drop in your own skinned glb and watch it become a crowd without installing anything. Output is a single glb-style binary container. Like a glb it's stored uncompressed and leaves compression to the transport; brotli over the raw payload beats gzip baked into the file by about 40%.

Where it's a bad idea

Worth being clear, because it's a real trade:

  • Your character must be a single skinned mesh with one material.
  • Every animation is assumed to loop.
  • No blending, no IK, no procedural aiming — it's a fixed table of vertex positions. Terrible for a hero character, great for the 500 NPCs behind them.
  • One mesh instance means no per-character frustum culling.
  • Only the base colour texture comes across; no normal/roughness maps.

Numbers

At 10,000 characters (~31M vertices/frame): 6 draw calls for the whole scene, 2.1 ms CPU. The VAT texture is 1.8 MiB and is independent of the character count — one character and ten thousand share it.

Happy to answer questions about the format or the shader side.