Skip to content

Add VAT character crowd rendering scripts and example - #9123

Merged
mvaligursky merged 1 commit into
mainfrom
mv-vat-characters
Jul 27, 2026
Merged

Add VAT character crowd rendering scripts and example#9123
mvaligursky merged 1 commit into
mainfrom
mv-vat-characters

Conversation

@mvaligursky

@mvaligursky mvaligursky commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Adds vertex animation texture (VAT) support as a set of reusable ESM scripts plus an example.
Skinned animation is baked ahead of time into a texture, so a whole crowd renders as one
instanced draw call with no skeletons, bone matrices or per character animation evaluation.
There are no changes to the engine itself — only new files under scripts/esm/vat, a new
example, and four symbol registrations in the scripts TypeDoc config.

Changes:

  • New scripts/esm/vat script library: a runtime script, a glb converter, generic shader
    chunks and a simple shader material
  • The VAT chunks override transformCoreVS / normalCoreVS, so they work with a
    StandardMaterial as well as with a custom ShaderMaterial
  • New animation/vat-characters example with single character and crowd modes, a material
    switch, and an in-browser glb converter behind a file picker
  • New examples/assets/vat folder holding the converted lumberjack, its source glb and credits
  • Registered ContainerResource, Scene, VertexBuffer and Scene.ambientLight in
    typedoc.scripts.config.mjs so the new JSDoc links resolve

API Changes:

New, all additive — playcanvas/scripts/esm/vat/:

  • vat-characters.mjsVatCharacters script. Attributes: url, useStandardMaterial,
    castShadows, light, ambientSky, ambientGround. Properties: capacity, count,
    animations, characterBounds, ready. Methods: load, setData, getAnimationIndex,
    setAnimation, setSpeed, setLoop, setTime, getTime, setPosition, setRotation,
    setEulerAngles, setScale, setTransform
  • vat-converter.mjsconvertToVat, saveVat, VAT_MAX_TEXELS
  • vat-data.mjsparseVat, VatData, createVatTexture, createVatMesh,
    createVatAlbedoTexture, createWhiteTexture, octDecode, vatTextureSize,
    VAT_VERSION, VAT_MAGIC, VAT_TEXTURE_LIMIT
  • vat-chunks.mjsvatChunks, setupVatMaterial, VAT_FRAME_SEMANTIC,
    VAT_TRANSFORM_SEMANTICS
  • vat-material.mjscreateVatShaderMaterial

File format:

The converter emits a glb-style binary container (12 byte header with a magic number and a
version, then a JSON chunk and a BIN chunk). Like a glb it is stored uncompressed and leaves
compression to the transport — brotli over the raw payload is around 40% smaller than gzip
baked into the file, and pre-compressing would leave the transport nothing to work with.

The texture is RGBA16U, one texel per vertex per sampled frame: position quantized to 16 bits
per axis over the bounds of the animation in xyz, and an octahedrally encoded normal packed
into w. One fetch therefore supplies a whole vertex. Quantizing over the bounds is also more
accurate than a half float where it matters — near the top of a character fp16 has a step of
about 1.8 mm at human scale, against 28 µm for 16 bit.

Texels are addressed by the flat index vertex * frameCount + frame, wrapped at the power of
two texture width. This decouples the vertex count from a texture dimension: the limit is the
total texel count of a 4096x4096 texture, so roughly 218k vertices at 77 frames rather than
4096. 4096 is used because every WebGL 2 and WebGPU device supports at least that, so data
loads regardless of the machine it was authored on.

Examples:

  • animation/vat-characters — crowd of up to 10,000 walking characters by default, with a
    single character mode with an animation drop down, a Shader/Standard material switch, a
    shadow toggle, and a Convert GLB panel that converts and previews any skinned glb in browser

Performance:

  • One instanced draw call for the crowd; 6 draw calls total for the example scene at 10,000
    characters (about 31M vertices per frame), CPU 2.1 ms
  • Per instance data is 52 bytes — the three rows of the affine transform plus a fractional
    frame index. The fourth row is always (0,0,0,1) and the shader appends it
  • The VAT texture is 1.8 MiB for this character and is independent of the character count
  • Transforms are only rebuilt when a setter is called, so a static crowd is nearly free on
    the CPU
  • Verified on WebGL 2 and WebGPU, in both material modes
crowd.mov

Bakes skinned animation into a texture so a crowd renders as one instanced
draw call, with no skeletons or per character animation evaluation.

Adds a script library under scripts/esm/vat (runtime script, glb converter,
generic shader chunks usable by StandardMaterial as well as a custom
ShaderMaterial, and a simple shader material), plus an animation/vat-characters
example with single and crowd modes and an in-browser glb converter.

No engine changes - only new files and four symbol registrations in the
scripts TypeDoc config.
@github-actions

Copy link
Copy Markdown

Build size report

This PR does not change the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2326.5 KB — 598.5 KB — 465.5 KB —
playcanvas.min.mjs 2323.9 KB — 597.4 KB — 464.4 KB —

@mvaligursky
mvaligursky merged commit 93f511d into main Jul 27, 2026
10 checks passed
@mvaligursky
mvaligursky deleted the mv-vat-characters branch July 27, 2026 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant