-
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 — full-length, no frame caps, no joint pruning (official V205 content ships multi-hundred-KB clips; artificial "budgets" were a misdiagnosis and are gone).
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, floating props, sliding doors, 2D screens). The loader composes each node's animated world
matrix by walking the parent chain. The cook ports it as:
-
spin/sway in place → a
getTime()Rodrigues rotation shader (axis + ω or amplitude, about the node pivot), or an exact ROTREPLAY (per-frame quaternion replay) when the motion isn't a clean spin — with hemisphere continuity so quaternion sign flips never cause shaking; - anything else (paths, scales, combined channels) → a 1-joint RIGID-HZANIM clip carrying the exact per-frame node matrix. Clips keep an inclusive endpoint and wrap-lerp the seam (no "jumpy reset" at the loop), and the bind carries unit scale (clean ACL compression).
- glTF-sourced envs (
.gltf.ovrscene) go through the same rigid port — dropped T+R+S channels are re-attached, normalized-3×3 gates catch non-rigid matrices.
The .sanim and .mat.sanim.opa carry UVTransform channels — a per-frame 2×3 UV affine
([a,b,c,d,e,f]). Two cook targets:
-
Continuous scroll (waterfall/foam: identity 2×2 + a constant per-frame translate) → the smooth
uv += rate*timeshader, REPEAT-wrapped, seamless. Scroll detection uses the full track window with an oscillation gate — a track that swings 0 → x → 0 (pond color swirls) is NOT a scroll and routes to the exact replay instead of cooking static. -
Flipbook / complex (sprite-cell atlases, 2×2 scale, dust) → the per-frame matrix replay
fragment shader: the verbatim source matrices are baked into the generated SPIR-V and selected
frame-snapped by
getTime()— the device plays exactly what the desktop plays, at the authored speed (never a generic one).
Spritesheet fidelity (the borders/seams class of bug):
- the texture's mip chain is capped per cell so coarse mips never average adjacent frames ("borders around the animated card at distance");
- the replay shader clamps the sampled UV to the current frame's cell (the base-UV rect pushed through the same frame matrix, minus an ASTC-block pad) — edge-touching samples get pinned instead of bleeding the neighbour frame in, and the frame interior stays pixel-exact. (An earlier attempt inset the mesh UVs instead — that rescaled the whole frame and visibly squeezed narrow cells; if you saw a "squished" animated card, update.)
- MaterialTint RGBA cycles (fireworks flashes, color-changing lights) chain a tint replay onto the same shader — evaluated in the vertex stage (full curve, no compression; fragment-stage tint tables break the Adreno driver).
- Opacity fade curves layer on top (fog puffs fade out exactly when their node teleports back).
CoSkinnedMesh geometry (.skin.opa) is model-local; world placement lives in the skeleton bind.
Linear-blend skinning matches libshell's vertex shader. Ported as HZAN:SKEL + ACL-compressed
HZAN:ANIM + a skinned RENDMESH (weights/indices on the device's vertex-stream semantics), with
large skins auto-split (~16k verts/piece) sharing one skeleton/clip.
Hard-won correctness notes that are now permanent:
- each skin binds to the clip that best-matches its own joint set (a last-writer-wins name map collapsed banners to the origin);
- byte-identical skeletons dedup, but animation groups pair per (skeleton, clip) — not per file;
- Maya-exported glTF rigs can bake unit conversion into the inverse-bind matrices only — the
residual is folded into the rest verts (
[HZIBM]in the cook log), else meshes skin ~70× too big; -
HSR_ACLVERIFY=1roundtrips every compressed clip and prints the error.
An R16G16B16A16_SFLOAT texture, width = vertexCount, height = frameCount, each texel = that
vertex's offset for that frame; the device vatunlitbasecolor shader samples by (vertexIndex, time) and offsets the position. Cooked with the full half-float mip chain (the device expects it).
Used both for authored VAT vistas and as the port target for complex OPA vertex animation.
The device fract-wraps a clip only for WrapMode ∈ {0,1,2} — the cook normalizes to a looping mode
so nothing freezes on the last frame. Desktop and device share the same phase math
(fract(time/loop)), so the timeline scrub (at=<sec> / HSR_ANIMTIME) previews the exact device
pose at any time.
hsr_dump <meshIdx> (MCP) or dump=<idx> (HTTP) prints [COOK-ANIM] — the exact device
animation/shader that mesh cooks to: UV-SCROLL · FLIPBOOK-UVMATRIX · HZANIM-SKINNED ·
RIGID-HZANIM · ROTREPLAY · VAT · STATIC. It reads the same extractors the real cook uses, so
it's authoritative. Pin poses with HSR_ANIMTIME=<sec> and compare source vs cooked numerically.
Related: Architecture · File Formats · Roadmap.