Files
healer-man/scripts/runewaker-pipeline/ORIGINAL_ANIMATION_IMPORT.md
T
2026-08-14 15:56:39 -04:00

309 lines
17 KiB
Markdown

# RuneWaker Original Animation Import
This is the portable, build-time pipeline used to bring original RuneWaker creature skeletons and animations into HealerMan. The shipped game loads ordinary GLBs from `public/assets/creatures/<dungeon>`; it does not load RuneWaker files, contact SQL Server, or require network access.
## Current status
Pasper's Shrine remains the compact direct-mesh reference, but the same pipeline
is applied across the complete recipe-referenced combat set. Directly
mesh-convertible actors ship with native skins and sampled RAS clips. Runtime
paperdoll containers are resolved separately into exact, image-specific
assembled actors from the portable appearance snapshot. Any genuinely
unsupported display remains an explicit pose-only exception. The inventory,
runtime-selector, and pose commands below derive their counts from recipes,
source graphs, manifests, appearance provenance, and shipping GLBs instead of
relying on a hand-maintained list.
The authoritative mapping is deliberately expressed at three levels:
- 436 runtime entity templates select 268 base actor configurations;
- per-dungeon namespacing and paperdoll appearances expand those configurations
to 322 runtime-referenced shipping variants; and
- 321 variants are expected to be native animated GLBs. Mantarick (Hall of
Survivors template `102452`, asset `judgement-light-pose`) is the single
documented pose-only variant.
`demon-stronghold-125` and `zurhidon-stronghold-124` are authoritative empty
copy zones. They must remain present in the 30-dungeon inventory with zero
templates and zero actor variants; they are not missing imports.
The v5 acceptance pass completed those gates:
- all 29 generic population recipes rebuilt green, and Forsaken Abbey's nine
actor GLBs passed Khronos validation with zero errors and zero warnings;
- the inventory is green at 30 dungeons, 436 templates, 268 base actor
configurations, and 322 runtime-referenced variants with zero blockers;
- runtime semantic near misses are zero. Mantarick remains the sole pose-only
exception and Sardo's Rune Warning Device remains the sole native
no-locomotion exception;
- the post-v5 Blender audit in
`artifacts/animation-audit/post-v5/summary.json` evaluated 44,305 poses across
all 322 variants: 282 numeric passes, 40 accepted-warning candidates, zero
errors, zero failures, and 634 flagged samples. Those totals exactly match the
visually reviewed v4 batches, so the v5 cast-semantics rebuild introduced no
numeric pose drift; and
- the live packaged/offline browser showed all 30 dungeons Ready and loaded the
Pasper's Shrine and Hall of Survivors environments. The only console message
was an unrelated library deprecation warning.
The 19 focused RuneWaker animation tests pass. The remaining repository-wide
build/test blockers belong to the unfinished RoM class catalog and missing
Manastorm fixtures, not this animation pipeline.
## Why the first imports were procedural
The original exporter loaded each ROS, flattened its meshes to OBJ, and let Blender create a static GLB. Geometry and textures survived, but the GR2 skeleton and RAS controls did not. HealerMan could only animate those static models procedurally.
GR2 actors must use the RuneWaker engine's `LoadAnimation_GR2()` path. A GR2 RAS header starts with the `CRuAnimation_GR2` identifier; passing it to the legacy keyframe reader treats that identifier as a huge keyframe count and crashes.
## Conversion flow
1. The native exporter loads the ROS and ACT-referenced RAS files through the original engine and Granny runtime.
2. It exports geometry and textures, selects the body GR2 hierarchy, obtains that hierarchy's source skeleton, and samples each ACT animation control at 30 Hz.
3. It writes `actor-rig.json` and `actor-rig.bin`. The JSON contains schema/version data, bones, clips, durations, and typed-array byte ranges. The binary contains skin influences and sampled translation/rotation/scale tracks.
4. Blender converts the OBJ and textures to a raw static GLB.
5. `inject-actor-rig.mjs` adds the joint hierarchy, inverse-bind matrices, `JOINTS_0`, `WEIGHTS_0`, skin binding, and glTF animation channels.
6. The injector copies influences onto vertices Blender duplicated at material or UV seams, normalizes weights, and bakes Blender's mesh basis into its accessors so skeleton and mesh share a coordinate system.
7. glTF Transform optimizes the asset while preserving the animated hierarchy.
8. The Khronos validator rejects structural errors and warnings.
9. The population generator records the clips and emits `animationMode: native`. At runtime Three.js chooses semantic `Stand`, `Walk`, `Run`, `Attack`, `Cast`, `Wound`, and `Death` clips.
## Runtime paperdoll actors
Some `model/character/.../*.ros` files are display containers rather than
triangle-bearing creatures. Their source ROS points at `paperdoll.ros`; the
actual head, hair, body, hands, feet, and equipment come from the template's
`ImageObjectDB` row. Treating that container as a failed static actor produced
the generic proxy bodies seen in the first Pasper/all-dungeon pass.
Paperdoll conversion is now split into a forensic refresh and a portable normal
build:
1. `npm run runewaker:paperdolls:refresh` discovers only image IDs used by
combat/boss templates, temporarily restores `ObjectEdit.bak` read-only,
exports those component/color rows, writes
`snapshots/runewaker-paperdoll-appearances.json`, and drops the temporary
database in `finally` cleanup.
2. Normal actor builds read only that checked-in JSON. They write a small TSV
assembly request for the native exporter, including reserved `@skin` and
`@hair` rows plus the ten supported component slots.
3. The preserved `CRuPaperdoll` path assembles the source components against the
container's original skeleton. The exporter then uses the same hierarchy
selection, ACT/RAS sampling, skin injection, optimization, and validation
gates as a direct actor.
4. The asset ID includes the source image ID, so different appearances that use
the same container cannot overwrite or alias one another.
Source composite masks can apply runtime color layers. The snapshot and actor
manifest preserve the layer values and `requiresColorLayerBake` flag. The
current exporter resolves the non-compmask base DDS so geometry, textures,
skeleton, and animation remain valid; exact mask-color baking still requires a
separate visual-fidelity pass wherever that flag is true.
## Body-rig selection
An ACT container can expose more than one `CRuACTEntity` or `CRuHierarchy_GR2`. In particular, some actors put a one-bone effects hierarchy before the actual body. Selecting the first GR2 hierarchy binds body vertex indices and motions to the wrong skeleton.
The exporter walks the complete entity tree and retains:
- the `CRuACTEntity` whose template has the largest motion library; and
- the `CRuHierarchy_GR2` with the largest subnode/bone count.
Skin indices, the source Granny skeleton, and sampled ACT controls are then
taken from that selected body hierarchy. The v4 cache generation introduced the
correct Z-reflection quaternion conversion. The current build recipe uses
`actorCacheVersion = gr2-native-animation-v5-z-reflection-complete-cast-semantics-30hz`:
v5 retains the v4 geometry repair and also invalidates actors for the complete
cast-semantics pass. Changing any hierarchy, transform, clip enumeration,
semantic naming, sampling, or rig-injection behavior requires another fresh
actor cache version.
## Coordinate-basis and quaternion conversion
RuneWaker positions are reflected across Z when they enter the HealerMan/Three
basis:
```text
S = diag(1, 1, -1)
(x, y, z) -> (x, y, -z)
```
A rotation cannot be converted by reflecting the quaternion's Z component.
The rotation matrix must receive the same basis change, `R' = S R S`, which is
represented by this quaternion mapping:
```text
(x, y, z, w) -> (-x, -y, z, w)
```
The earlier `(x, y, -z, w)` mapping left bind poses and GLB structure valid but
made animated limbs, armor, and weapons follow the wrong axes. Giant Assassin,
Giant Assassin Captain, and Blackhorn Silencer in Pasper's Shrine exposed the
problem most clearly. `CopyGrannyTransform` applies the corrected mapping to
both skeleton-local bind transforms and every sampled animation frame, then
normalizes quaternions and preserves hemisphere continuity.
## v5 complete cast semantics
The v5 contract is broader than finding one clip named `Cast`. Every source clip
exported from the selected ACT body must remain represented in the actor
manifest, and every cast-family clip intended for runtime combat must be
classified and reachable by the runtime selector. Attack, cast, and wound
one-shots rotate through all semantic matches in manifest order (or all matches
of the highest-priority numeric motion ID). A successful rebuild alone does not
prove this contract: `npm run runewaker:audit:runtime-clips` is the authority for
selector reachability and family gaps.
The completed v5 actor cache, manifests, runtime coverage, and evaluated-pose
summary are from the same rebuild. Runtime cast coverage is 321/322, semantic
near misses are zero, and the remaining non-native row is the documented
pose-only Mantarick asset rather than a missed cast export.
## OBJ vertex-remap workflow
The native exporter writes one named OBJ group for each exported render mesh and stores the source mesh's dense joint and weight arrays in the rig sidecar. Blender may reorder vertices or duplicate them at UV and material seams, so source and GLB accessor indices cannot be assumed to match.
The injector therefore:
1. reads the original `scene.obj` group positions;
2. converts OBJ Y-up coordinates to Blender's local accessor basis `[x, z, -y]`;
3. builds spatial buckets at a `1e-4` tolerance;
4. maps every Blender output vertex to the nearest accepted source vertex, including seam duplicates;
5. copies that source vertex's four joint indices and weights;
6. rejects unmapped vertices or joint indices outside the selected skeleton; and
7. normalizes weights before creating `JOINTS_0`, `WEIGHTS_0`, the skin, and inverse-bind matrices.
This position remap is intentionally strict. A failed remap is an extraction error to fix, not a reason to ship a partially skinned actor.
## Required invariants
- Keep `SkeletonUtils.clone()` in the creature runtime. Normal Three.js cloning does not independently rebind cloned skinned meshes.
- RuneWaker blend indices in these actors are full GR2 skeleton indices; do not remap them through a guessed palette.
- For the Z reflection, convert quaternions as `(-x, -y, z, w)`; do not negate quaternion Z.
- Preserve quaternion hemisphere continuity while sampling.
- Name the single-frame source `dead` clip `Terminal Pose - dead`; the active death action is `Death - death`.
- Optimize animated models with `--flatten false --join false --instance false`.
- Change `actorCacheVersion` in `build-population.mjs` whenever native export or rig-injection semantics change.
- Preserve every exported cast-family clip in manifest order and verify that the
runtime cast selector can reach it.
- Treat Sardo Castle template `105758` (`rune-device-red`, Rune Warning Device)
as a native stationary device. Its source has no locomotion clip by design;
`missing-exact-moving` is an approved exception for this asset only.
- Treat Mantarick template `102452` (`judgement-light-pose`) as the single
pose-only exception. Do not silently convert it into a generic animated proxy.
## Build commands
Run from `D:\Projects\HealerMan`.
Build the native exporter (development/extraction dependency only):
```powershell
powershell -ExecutionPolicy Bypass -File scripts/runewaker-pipeline/native/build-exporter.ps1 `
-RunewakerSourceRoot D:\Projects\rom-pvt-server-backups\Runewaker\SourceCodeVS2019
```
Rebuild Pasper's Shrine actors while iterating:
```powershell
node scripts/runewaker-pipeline/build-population.mjs `
--recipe scripts/runewaker-pipeline/recipes/paspers-shrine-population.json `
--actors-only
```
Rebuild the full portable Pasper package:
```powershell
node scripts/runewaker-pipeline/build-population.mjs `
--recipe scripts/runewaker-pipeline/recipes/paspers-shrine-population.json
```
Runtime outputs are under `public/assets/creatures/paspers-shrine/` and `src/game/generated/paspersShrinePopulation.generated.ts`.
Render a native actor's uncompressed diagnostic GLB before accepting a rig or
exporter change. Blender cannot import the meshopt-compressed shipping GLB, so
use `<actor>.animated.raw.glb` from `runewaker-export-work`:
```powershell
& $env:BLENDER_BIN --background --factory-startup `
--python scripts/runewaker-pipeline/blender/render-animated-actor.py -- `
runewaker-export-work/paspers-shrine-population/actors/fog-ferocity-03/prepared/fog-ferocity-03.animated.raw.glb `
artifacts/animation-diagnostics/fog-ferocity-03-runtime-clips `
"Bind Pose" "Idle - 1h_idle" "Run - run_forward" `
"Attack - 1h_attack01" "Attack - 1h_attack02"
```
The renderer writes the bind pose plus first, middle, and final frames for every
requested clip. Inspect the exact semantic clips the runtime will choose; clip
names, skins, finite transforms, and Khronos validation alone do not prove pose
quality.
Refresh the portable paperdoll appearance evidence only when the source backup
or combat-template image assignments change:
```powershell
npm run runewaker:paperdolls:refresh
```
This is an extraction command, not a game/build dependency. SQL Server may be
stopped or removed before every normal actor, population, test, build, and
offline browser command.
Regenerate all three machine-readable audit layers without rebuilding assets:
```powershell
npm run runewaker:audit:animation-inventory
npm run runewaker:audit:runtime-clips
npm run runewaker:audit:animation-readiness
npm run runewaker:audit:animation-readiness -- --json
```
The inventory writes `artifacts/animation-audit/inventory.json` and
`inventory.md`. Runtime selection writes
`artifacts/animation-audit/runtime-clip-coverage.json` and
`runtime-clip-coverage.md`. Readiness Markdown is written to stdout by default;
`--json` emits the same per-dungeon and per-actor findings as structured data.
The readiness audit exits nonzero only for shipping blockers; unresolved
optional source references and unused recipe actors remain visible as warnings.
The exact 322-variant Blender command and human-review workflow are in
[POSE_AUDIT.md](./POSE_AUDIT.md).
## Per-dungeon verification gate
1. Run the inventory and prove that all 436 entity templates map through 268
base actor configurations to exactly 322 runtime-referenced namespaced
variants. Keep both authoritative empty zones in the report.
2. Rebuild with a fresh cache version if exporter behavior changed.
3. Run the runtime selector audit. Confirm 321 variants have
`animationMode: native`, Mantarick alone is pose-only, all configured combat
clips are reachable, and the Sardo red rune device is the only approved
native no-locomotion exception.
4. Run the evaluated-pose audit against the exact 322 shipping paths. Blender
must evaluate bind plus first/middle/final samples for every action. Render
semantic-family contact sheets, visually review every variant, and expand
suspicious actors to `--render-mode all`. Reject collapsed silhouettes,
detached equipment, unexpected axis changes, or discontinuous poses.
5. Run the Khronos validator on every shipping GLB; accept zero errors and zero warnings.
6. Run:
```powershell
npx vitest run src/game/runewakerDungeonAnimation.test.ts src/game/mobAnimation.test.ts
npm test
npm run build
```
7. In a browser, observe idle/stand, locomotion where applicable, every
configured attack/cast family, wound, and death. Cover every mob/boss variant
over the per-dungeon pass, not only a representative rig. Check loading,
skeleton, WebGL, and animation errors. Record evidence and limitations in
`PLAYTEST_NOTES.md`.
8. Stop the development server and disable network access for an offline smoke test. Geometry, populations, skeletons, clips, and textures must load from the packaged build.
## Rebuilding or extending a dungeon
Use Pasper's Shrine as the compact reference and rebuild one recipe at a time. Do not label a new actor native merely because conversion completed: it needs actor accounting, GLB validation, animation-family coverage tests, and a browser playtest. Document missing/corrupt RAS clips per template and use procedural movement only for explicit pose-only or runtime/dynamic no-mesh exceptions.
Encounter mechanics are a separate layer from skeletal animation. Doors, summons, patrols, phases, hazards, and loot should consume the same packaged population IDs without introducing any runtime dependency on the original databases or engine.
Development inputs are the RuneWaker ROS/ACT/RAS files, textures, source, Granny runtime, and forensic database snapshot. Runtime inputs are packaged GLBs/textures, generated population data, and HealerMan's in-memory encounter state. Offline and online modes therefore use the same local dungeon package.