Battle actor rendering
How a combatant gets on screen: which mesh it draws, which clip poses it, what colour and blend the draw takes, and the trails a swing leaves. Party members are not stored as finished models - each is assembled per fight from equipment sections - and every clip an actor plays goes through one staged byte.
At a glance
- Loader
FUN_800513F0registers backdrop, party blobs and monster meshes intoDAT_8007C018[]and seeds the animation rate- Party meshes
- Assembled from the player battle files; see battle data pack and character mesh
- Monster meshes
- Archive record
+0x04mesh and+0x08texture pool; poses in monster animation - Clip channel
- Staged
actor[+0x1DA], playing+0x1D9, camera key+0x1DB, commit flags+0x1DC - Rate
actor[+0x21D], normal8- Colour
- Tint lanes
+0x04, blend weight+0x0C, mode word+0x08(top byte = semi-transparency) - Port
actor-formats(assembly),engine-core::world::battle::clip_ladder,engine-battle-vm::battle_actor_tint,battle_trail; hostsengine-shellandweb-play::play_battle_render
The draw pipeline
One staged clip byte
Which clip an actor plays is a single byte, actor[+0x1DA]. Every producer writes it and the last writer wins; the commit FUN_8004AD80 snaps the playing id to it. A hit reaction is not a mode the actor is in - it is only the current value of the staged byte, and the next thing staged replaces it.
| Producer | What it stages |
|---|---|
| Action SM, approach | literal 1, the walk entry |
| Action SM, strike loop | the queue byte: 0x0C..0x0F swings, art ids |
| Damage arm, flinch | actor[+0x1EF], the tag-2 entry |
| Damage arm, knockdown | actor[+0x1F1], the tag-4 entry |
| Cast module | caster stage literals; the victim’s knockdown |
Flinch or knockdown is decided in the damage arm: flinch only when the actor has no get-up entry and the damage is survivable. Every player battle file carries a get-up entry, so a party member takes the knockdown arm on any hit.
When a staged clip commits. Mid-clip, commit-flag bit 0 commits at once and bit 1 commits once the cursor is past the entry’s gate frame. Otherwise the staged byte waits for the natural end. A clip left staged behind itself re-commits from frame one, which is how a loop loops.
The clip-tag ladder
A monster’s death runs through an earlier arm of the same routine: the knockdown is held on its last frame, the spoils run, and the actor takes the capture / defeat fade. With a Seru staged for capture the monster is stood back up for the absorb.
Slow motion and after-images
actor[+0x21D] scales how far the animation cursor moves per frame: 8 normal, 4 half speed, 2 quarter, 0 a freeze. Every animation-driven edge - swing pacing, root motion, the strike loop’s gate - stretches with it. A Stone or Numb body on its idle takes no step at all: the animation tick skips it and the body holds the keyframe it stood on.
| Staged id | Effect on the rate |
|---|---|
any commit while the rate is not 8 | rate 4 |
0x1A, the special starter | every slot 0, the acting actor 2 |
0x1B and up, an art | every slot 2 or 4 |
The mesh after-images behind an art are two ghost copies drawn from the actor’s pose-history rings (FUN_80049348), additive, only while the ring id marks an art.
Tint and blend
The tint pass packs the actor’s colour lanes into the render node and the draw pass stages them as the GTE far colour and IR0. The prim’s modulation colour becomes
baked + (tint - baked) * blend / 0x1000
and the GPU still multiplies the texel through it. With no blend weight set, the same word is derived from depth instead: the distance fade. A colour word of zero is the summon-hide’s “not drawn” value, not a black tint.
- The capture / defeat fade ORs additive blending into the node’s mode word, so the fading body is never an opaque black silhouette. It runs after the near-camera ghost pass, which clears the same bits, so the fade’s OR is what the draw reads.
- The near-camera ghost pass (
FUN_8004DC68) makes a body that crowds the lens translucent. - Shading itself is on shading and palettes.
Trails and streaks
| Trail | Primitive | Driven by | Routine |
|---|---|---|---|
| Weapon trail | untextured gouraud band, semi-transparent | the committed clip’s identity byte +0x77 | FUN_8005112C |
| Move-FX afterimage | one textured billboard | streak counter high, party | FUN_801E1AB0 |
| Move-FX ribbon | chained textured strip | counter low, or any monster | FUN_801E1D98 |
| Mesh after-image | the actor mesh, additive | history ring id marks an art | FUN_80049348 |
Where the weapon trail lands. The sweep is sampled off the pose-history ring: step k is the pose 2k frames ago, retail’s rewind at a constant rate. Each control point is turned by the body’s facing and set on its slot’s ground base with retail’s integer arithmetic (products round toward zero, no base height), then placed the way the hosts place a battle body - the frame flip and the 4x world scale - and projected through the battle view-projection.
Both hosts call one kernel for that, engine-world-views::screen_passes::weapon_trail_prims, and hand it nothing but their battle view-projection, so a trail cannot land in one place on the page and another in the window. Tests pin the native composition to the page’s matrix and a trail point onto the body’s own drawn point under both. PROT 0904’s Theeder beam projects through the same point kernel (render-kernels::battle_trail::project_battle_point). The bands composite over the scene; retail’s ordering-table interleave with scene depth is not reproduced.
See also
The assembly and relocation detail, the monster texture pool, every rate writer with its address, the distance fade and both hosts’ render bridges are in docs/subsystems/battle-actor-rendering.md.