-
Notifications
You must be signed in to change notification settings - Fork 2
Animation System
V79 environments animate in four distinct ways. Each is decoded from the OPA scene and ported to a matching V205 mechanism. This page explains all four and the bugs that were recently fixed.
An official V79 home ships an entity graph (content/scenes/basescene/<fbx>.entity) that wires
components onto nodes. The ones that matter for animation:
| Component | Meaning |
|---|---|
CoModel + CoModelAnimation
|
A model whose .sanim drives node transforms and material params. |
CoModelAnimation.MaterialTargets |
The materials the sanim animates (UV / tint). |
CoSkeleton + CoSkinnedMesh
|
A skeleton (.skel + .anim) skinning child meshes (cloth, flags, creatures). |
The .sanim stores per-node Translation / Rotation / Scale tracks (windmill blades, trains,
snow instancers, floating props). The loader composes each node's animated world matrix by walking
the parent chain (evalAnimNodes). The cook ports it as:
-
spin/sway in place → a
getTime()Rodrigues rotation shader (axis + ω or amplitude, about the node pivot); - translating nodes (cars/trains) → a 1-joint RIGID-HZANIM clip carrying the exact per-frame world matrix (so arbitrary paths port faithfully).
The .sanim also carries UVTransform channels — a per-frame 2×3 UV affine
[a,b,c, d,e,f] (6 floats/frame). This is how flipbook fire, drifting smoke, scrolling aurora and
waterfalls animate, authored inside the node sanim (not a separate .mat.sanim.opa).
Bug (fixed): the sanim parser handled only Translation/Rotation/Scale and silently dropped
UVTransform, so every such material rendered static. Worse, hasAnimation() gated on
animMaxFrames>1 which was computed from node T/R/S only — so a UV-only env (rockquarry) had
hasAnimation()==false and the entire animation path was skipped. Fix: parse UVTransform into
matUVAnim, and fold UV/tint track lengths into animMaxFrames.
Two cook targets for a UV track:
-
Continuous scroll (waterfall/water/foam: identity
2×2+ a small constant per-frame translate) → the smoothuv += rate*timeshader. Continuous, REPEAT-wrapped, seamless. -
Flipbook / complex (sprite-cell atlas, or a track with
2×2scale + tint-fade + motion) → the per-frame matrix-replay fragment shader (FLIPBOOK-UVMATRIX).
Bug (fixed): every UV-animated mesh was being sent to matrix-replay, which snaps each
frame → choppy, and the hard per-frame offset jump shows a cell border/seam on a scroll.
Waterfalls now detect as pure scrolls and take the smooth path (HSR_NOUVSCROLLFAST forces the old
replay). Verify with hsr_dump <idx> → [COOK-ANIM] => UV-SCROLL(smooth uv+=rate*time).
CoSkinnedMesh geometry (.skin.opa) is stored in model-local space; the world placement lives
in the skeleton's bind pose. Linear blend skinning:
vertex_world(f) = Σ wᵢ · clipJointWorld(f)[cjᵢ] · invBind[bᵢ] · localVert (matches libshell's
skinning vertex shader).
Bug (fixed): banners/stringlights collapsed to the origin. The loader bound each skin to a clip
via a last-writer-wins joint-name map. A full-scene skeleton (rootnode) duplicates the
banner/stringlight joint names of the dedicated sub-skeletons (banners_VFX, strings_lights),
so it overwrote the binding → skins bound to the wrong (superset) clip → collapsed to (0,0,0). Fix:
bind each skin to the clip that best-matches its own joint set (max coverage, fewest extras) —
which reproduces the entity graph's CoSkinnedMesh → CoSkeleton pairing. Global for any env with
overlapping skeleton joint names. Large skins auto-split (HSR_SKINCAP) sharing one skeleton/clip.
Some vista meshes animate via a VAT: an R16G16B16A16_SFLOAT texture, width=vertexCount × height=frameCount, each texel = that vertex's offset for that frame. The device vatunlitbasecolor
shader samples it by (vertexIndex, time) and does pos += offset. Cooked with a full half-float
mip chain (the device expects it) — see File Formats.
hsr_dump <meshIdx> prints [COOK-ANIM] — the exact device animation/shader that mesh cooks to:
UV-SCROLL · FLIPBOOK-UVMATRIX · HZANIM-SKINNED · RIGID-HZANIM · SCALE-getTime ·
POSE-TRANSLATE · STATIC. It reads the same extractors the real cook uses, so it's authoritative.
Related: Architecture · File Formats · Roadmap.