Files
2026-08-14 15:56:39 -04:00

17 KiB

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:

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:

(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 -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:

node scripts/runewaker-pipeline/build-population.mjs `
  --recipe scripts/runewaker-pipeline/recipes/paspers-shrine-population.json `
  --actors-only

Rebuild the full portable Pasper package:

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:

& $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:

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:

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.

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:

    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.