Animation: clips, behaviour graphs and the runtime

The engine's own animation system (Havok Animation and Behavior are not used): the .anix clip format and its codecs, where a loaded clip lives and how events are pre-parsed, which characters can play a clip, the .bvr behaviour graphs, and the per-frame runtime from the behaviour state through clip trackers to the pose job on the animation worker threads.

The engine's own system

Animation is written by Monolith, not Havok. The executable carries run-time type information for exactly one Havok animation class, hkaSkeleton (kept for cloth deformation), and for no Havok Behavior class at all; the many hka* and hkb* names elsewhere in the file are Havok's serialization name table, which survives without the code. The engine's own animation sources are named in its strings: AnimNodeLink.cpp, ThreadAnimNodeLink.cpp, AnimationMgr.cpp, BehaviorData.cpp, CharacterBehaviorInterface.cpp, SimpleModel.cpp and FacialAnimationMgr.cpp.

Clips: the .anix format

header   'ANIX' | 0x2C | 0x30 | file size | track count | track table offset | block chunk offset |
         block chunk size | anim chunk offset | anim chunk size | 1 | 1
tracks   run-length runs {channel hash, run length, node hashes}: rotation 0x27436396,
         position 0x00CE66FC, events 0x210C8F5A
anim     +0x00 directory offset (0x10), +0x04 clip count, +0x0C the EVENT DICTIONARY offset;
chunk    directory {u32 name hash, u32 relative offset}; records are not in directory order
record   name[0x30] | +0x30 (flag << 16) | (0x7C + 16 * blocksets + 8 * named) | +0x34 blocksets |
         +0x36 named | +0x38 groups | +0x3A tracks | +0x3C root summary: start position, start
         rotation (x, y, z, w), end position, end rotation | +0x74 u32 | +0x78 duration (ms) |
         +0x7C codecs (4 characters per group), group of each track (u8), remap (u16 per track),
         blockset table {offset, size, t0, t1}, named table {hash, offset}, the KeyString blob
blockset u32 named count, u16 group offsets, u16 named offsets, align 4, u32 LITH 0x2B99DBC1
LITH     {keys, n2, n3, times[keys], 0}
Codec Content
SQ03 constant rotations: 42-bit smallest-three, three per 16 bytes
AQ03 animated rotations with their own key times (keys may skip frames where motion is smooth)
SV01 / AV01 constant / animated vectors
UATR position polylines: a u8 segment and a u8 weight per key, then a 4-aligned float3 palette
UAQT / UAVT keyed rotations / vectors
STRI event strings (offsets into the KeyString blob)

A loaded clip

resource table   [base+0x2A86758] + (hash(path) & 0x7FF) * 0x18; resource +0x08 = a 0x28-byte entry
                 {+0x00 handle vt_0x1DB4508, +0x08 the anim object, +0x18 3, +0x24 index}
anim object      vt_0x1E6F528 (constructor 0x57E4F4, 0x88 bytes); +0x30 the data object,
                 cached +0x38 anim chunk and +0x68 block chunk (0x57E5A8)
data object      0x28 bytes (loader 0x480AEC): +0x00 block chunk (16-aligned) +0x18 size,
                 +0x08 anim chunk +0x1C size, +0x10 node / track pairs +0x20 count
anim registry    [base+0x26FEFF8] + 8 + (hash(path) & 0x3FF) * 0x18, node = object + 0x10

The samplers (0x260740, 0x261360) re-read these pointers and the clip record on every sample, so a change to a clip's values in memory plays on the next frame. The track-to-bone binding is a vector per skeleton indexed by the file's track index and built at load, so a clip with a different track layout cannot replace a loaded one in place. A region change reloads the region's animation files.

Events are pre-parsed. When an anim object attaches, 0x4B9E58 parses the file's event dictionary (the anim chunk's +0x0C: a path, clip names, every event string, then each clip's events) into per-clip objects in an arena: anim object +0x40 arena / +0x48 size, +0x50 .. +0x58 a vector of {u32 hash(clip), object*}; a clip object holds +0x10 frames, +0x14 count, +0x18 duration (ms), +0x20 a u32 frame per event (+0x28 count), +0x30 the events as (string index << 24) | ms with 0xFF meaning no text (+0x38 count), and +0x40 the strings (+0x48 count). Playback reads these tables, never the STRI tracks.

Which characters can play a clip. A character plays a clip by name from its skeleton family's clip table (0x302540): [[[[char+0x110]+0x1C0]+0x50]+0xB0] (vt_0x1E6F3F8). A table takes clips only from files its member list names (+0x78 / +0x80, 0x38-byte entries: +0x20 the path, +0x28 the anim object while loaded); the orc list names 3,363 paths and the olog's 2,268. A file at a path the list does not name registers nowhere, even in a listed folder; a listed path that ships in no archive (2,107 of the orc list's) is an unused slot. Loading a bundle registers its clips, unloading unregisters them. The orc, Gondorian and Easterling families share many entries (one anim object for orc_deathtaunts.anix in all three); the olog's list is separate. Six tables served 47 spawned characters in one scene.

Armour shoulder-pad helper bones (L/R_SPadX) are keyed at half of the arm's swing (correlation 0.99 - 1.00 on five sprint clips), so they follow the arm by design data, not by a constraint.

Behaviour graphs (.bvr)

A .bvr is a GameDB database whose root type is BlendDB. Its categories are node types (BlendState, StateTransition, Animation.SimpleAnimation, Predicate.And, Modifier.* ...); its records are the graph's nodes, mostly named secondary records, with inline structures (PotentialFrame, AnimPoint, ValueModifier, AnchorData, TrajectoryPoint) as primaries. The player graph has 139,198 records in 270 categories: 46,400 structures, 22,229 state modifiers, 21,128 predicates, 15,704 transitions, 7,561 logic nodes, 6,454 camera modifiers, 3,750 states, 3,069 animations and more.

Record Holds
BehaviorSource the root: StartState, DefaultWeightset, ModifierTypes, PreviewModel, TrackingStates, reset-to-default states
BlendState one animation state: AnimationLink (the animation tree), Links (outgoing transitions) with a normalised-time window each and a minimum character LOD; Modifiers; Rate, DefaultBlendTime, BlendInterpolationMethod, Looping, Mirrored, gravity and foot / hand IK, movement / rotation / collision scale, NativeState, AIPosture, Priority
CameraBlendState a camera state: CameraModifiers and a predicate
StateTransition an edge: target node, predicate, blend time, window, sync, mirroring, interpolation

The 29 graphs hold 10,702 blend states and 73,448 transition links. The four player graphs (player_elf, el2, elt, bar) have about 2,050 states and 1,700 camera states each; the warg 431, the troll 218, the five orc class graphs 132-136, and smaller graphs serve the Easterlings, ologs, humans, the AI Talion, the fellbeast, Sauron's war form, the Nazgul, the Balrog, Gollum, goblins and spiders. 73,017 links resolve to a state, 431 have no target and none names a missing state. 5,300 states are reachable by transitions from the start state; the rest are entered by code (grabs, riding, ladders, mount deaths: the states that synchronised attacks and presentations force). All 17,606 clip references of the states resolve to a clip in some .anix (the .skel animation-set paths the nodes name ship nowhere, so the join goes through the clips' own names).

A BlendDB record is a native structure. Its header byte +0x01 is 0x1A (flag 0x10) on all 318,372 live records, so its fields sit at instance + SlotOffset(type, field) (SlotOffset 0x251C24, size step 0x251CAC) instead of the GameDB slot rule, and compiled code reads them as plain members; by-name getters divert such records to the reflected path (GetFloatByName 0x115B43C -> 0x5DA0FC). BlendState live layout: Mirrored +0x68, Priority +0x80, DefaultBlendTime +0x88, StartPct +0x98, Rate +0xA8, Gravity +0x110, Looping +0x118, Links +0x138 ({pointer, count}); editor-only fields take no bytes. Six fields read live equal the disk decode for 10,697 of 10,697 records.

Only two "play as" tiers are authored into the player graph: the tags pool (371 records) holds PlayAs_Spider and PlayAs_Gollum, and Client/PlayerMovement has exactly five records (PlayerMovement, PlayerMovement_Alt01, SpiderMovement, GollumMovement, DwarfMovement).

The runtime

The behaviour state of a character

CCharacter offset Meaning
+0x120 the behaviour-interface base subobject (constructor 0x45520C; vt_0x1F96C68 on the player, vt_0x1FB3AA8 on NPCs)
+0x140 char* the .bvr path (behaviors\player\player_elf.bvr)
+0x578 the AnimationMgr (= [GM+0x6CB0])
+0x1198 / +0x11A0 / +0x11A8 / +0x11B0 four BlendState record pointers; +0x11B0 is the current state
+0x1258 the graph's BehaviorSource record
+0x1970 the AnimNodeLink (vt_0x1DCC0C0, constructor 0x456F14, destructor 0x4555B8)
+0x1980 / +0x1988 a shared pointer to one manager-wide object (AnimationMgr+0x65A0)

The state slots point into the graph's records, not into per-character copies: every value is a record of one of the loaded BlendState categories (28 loaded graphs, 21,394 records), so all characters on one graph share them, and a change to a BlendState record reaches every character using that graph. In play the current state changes as characters move (60 kinds of transition in 20 s on 80 characters: Idle -> IdleToRun, IdleToRun -> RunToIdle, Run -> Stumble -> Run, Idle -> Wpn_Sheathe, Idle <-> AutoPicker_Fidget ...). +0x11A8 is either null or equal to the current state (29,520 of 29,520 samples, and 35,882 of 35,882 in a second run); +0x11A0 is set only on the player (its second machine: button and stick states). +0x1198 is not a previous state: right after 402 state changes it held the old state 0 times, the new state 334 times and was null 61 times. It is null on exactly the samples where +0x11A8 is null, and otherwise equals the current state, except on the player, where it can hold the second machine's state: in 164 of 164 samples where it differed from the current state, the character was the player and the value equalled +0x11A0 (for example A_Button_JustPressed). The null is passing: 41 of 55 characters showed both null and set within 40 s.

The AnimNodeLink: +0x98 the AnimationMgr, +0xC8 the parsed name of a forced animation, +0xF0 / +0xF8 a BlendState pair (+0xF0 equals char+0x11A8), +0x230 a serial from the counter 0x2702008, +0x270 a shared pointer to the manager object. The AnimationMgr [GM+0x6CB0] has no vtable: +0x4158 / +0x4160 count and 0x28-byte handles of the registered characters (+0x20 the character; every spawned character is registered), and +0x6720 / +0x6728 a pool of 256 objects (vt_0x1FD99F8, about 0xE0B8 bytes each).

Clip trackers

M = [[char+0x110]+0x1C0] is the character's model animation instance (vt_0x1DA9700; the model object is vt_0x1F6F950). M+0x08 is a vector of 0xA0-byte trackers, one per active clip; M+0x50 the skeleton resource; M+0xC8 the pose-job descriptor and M+0xD8 its 20-byte blend entries (one per tracker); M+0x118 the time source; M+0x128 job submitted, +0x12A needs evaluation.

Tracker offset Meaning
+0x02 flags: bit 0 advanced by the engine clock, bit 1 loop, bit 3 cleared on wrap
+0x04 u16 delay counter (0xFFFF = none)
+0x08 the clip name's hash
+0x0C current key index
+0x10 rate (time += (dt + remainder) x rate)
+0x14 weight
+0x18 sub-millisecond remainder
+0x1C playback position in ms
+0x24 weight-set index (-1 = whole body)
+0x44 key count
+0x4C duration in ms (equal to the clip record's +0x78 in 100 of 100)

Tracker 0 is always the clip base (the model's base pose). Clips layer per body part rather than cross-fade: a player at a fidget state carried base plus PL_Base_Idle_B (pl_fidgets.anix) and two pairs of fist idle clips, all at weight 1.0 with different weight sets. Most trackers have rate 0 and are not engine-advanced: the graph writes their time. In play, tracker time advanced at the world clock's rate (median 1.0 ms per world ms over 42,123 consecutive same-clip pairs; 731 loops or restarts).

Per frame

main thread   CGameClientShell vf1 0x1A864C -> model manager vt_0x2301178 vf1 0x1A83AC -> vf14 0x1A8490
              -> per model and phase 0x187A38 -> 0x1760A8:
                1. 0x1760F0 advance: per tracker 0x17620C (skip unless flag bit 0 and rate != 0);
                   0x17674C wraps a looping clip or holds the last key; dt from M+0x118 or the
                   world clock
                2. 0x176348 submit: 0x175F04 copies weight, weight set, time and clip of each tracker
                   into M+0xD8, then queues the job on ThreadSystem_A [base+0x26FF000] (vf2)
workers       EngineAnimationJobThread1-3 (routine 0x114CBA8) take jobs from
              AnimJobThreadSystem [base+0x26FF018]+8 -> 0x1B0E60 -> the pose job 0x1B0EE4;
              the main thread drains the same queue too (0x7042FC -> 0x1AF430)
pose job      0x1B0EE4 walks the blend entries: the first non-zero weight -> op 0 SAMPLE
              0x264E8C -> 0x260740 -> 0x261360; each later one -> op 1 BLEND 0x182980 by its weight;
              no clip -> op 2 REFERENCE POSE 0x11B517C; end -> op 3 OUTPUT 0x1B12A4

The operations live in a 16 x 0x20 table at 0x2A86C60 filled by 0x1188844; the dispatcher 0x1B1230 asserts "Animation Operation not registered" for a missing one. In play the pose job also shows up on the main thread (2.7% of its samples).

Console entry points

AnimLink.PlayAnim (handler 0x1BCF20C, CharacterBehaviorInterface.cpp) targets the character named in its first argument or the player and calls 0x1BDEC34(link = [char+0x1970], name), which parses the name (0x41758C, 0x41A1E0), stores it at link+0xC8 / +0xD0 and calls 0x41A148. AnimLink.Dump.History (0x1BDEE0C) finds a registered character by name through 0x1B32410.

Ragdolls

Ragdolls are a death effect. Every character model carries a physics ragdoll, but the powered ragdoll, its brain and the joint tuning from the database are started only by the dead-body step 0x2F8460: the slot claim (0x65C214), the brain start (0x65C274) and the joint setup (0x64D4A4) each have that one caller. Knockdowns and other reactions of living characters are animations.

The dead-body step

The NPC Update runs its dead-body branch, slot 121 (base implementation 0x175020), once the combat object's dead byte [char+0x26A8]+0x4A0 and the byte char+0x225E are set (see Threads and the frame); on the frame of death the AI update itself calls the step once. 0x2F8460(state = char+0x1AF0, character):

  1. Reads Combat/Category.RagdollReaction of the killing attack (0x65E990; 11 of 2,559 categories set one) and, when a ragdoll is pending, its InitalVelocityScale and ForceValue.
  2. Claims a ring slot, then, when the character's model ([char+0x2B30]) names a Character/Models.RagdollConfig, applies that config's joints, enables the ragdoll and starts its brain; otherwise it starts the controller with no config and no brain.
  3. Fires Combat/Buffs.DeathProjectiles, switches the character to RigidBody, plays the clip named by Character/Models/Global.RagdollDeathAnimation (base) on the model and posts the audio hook CharacterSwitchesToRagdoll.
  4. On every later frame: runs the brain (0x30F6D4), then tests for rest. Counting from the state's time stamp and leaving out the brain's running time, a body is at rest after 15 s, or after 3 s once its speed falls below Character/Models/BodyCleanup.AtRestSpeed (15 cm/s without a record). A resting body that is not the player goes to the body cleanup 0x6C00A0 (Lifetime, SinkDistance); otherwise 0x145218 relaxes the hit reaction (RelaxTime, start and end friction and tau factor).

The thresholds are initialised data ([base+0x26736A4] 15.0, [base+0x26736AC] 15.0, [base+0x26736B0] 3.0, enable bytes [base+0x26736A8] and [base+0x26736C0]), each read only by this step. The two BodyCleanup records, Troll (15 models) and Fellbeast (2), set AtRestSpeed to 1,000,000, so those bodies rest at once; the other 264 models use the default.

The per-character body settings are copied from Character/Models/Global when the character is created (0xFCFB4, from its slot 16), into char+0x1E50: +0x239 BodyRagdoll, +0x218 BodyDeathDirMinY, +0x238 BodyDeathForce, +0x21C BodyDeathForceScale, +0x234 BodyDirDeathForce, +0x220 BodyDirDeathForceScale, +0x228 BodyStickDist, +0x22C BodyStickTime, +0x230 BodyDeathScaleVelocity and +0x224 BodyDeathClampVelocityMax (shipped: true, 0.4, true, 0.5, true, 1.5, 400, 0.3, 1.0, 800). A later edit of the record reaches only characters created afterwards.

Offset from the character Content
+0x110 -> +0x1C8 the physics ragdoll (below)
+0x1AF0 dead-body state: +0x00 phase (0 while alive), +0x308 the killing attack, +0x310 the BodyCleanup record, +0x320 a time stamp (f64), +0x350-+0x35D flags
+0x1AF8 the ragdoll controller
+0x1E50 body settings (above)
+0x2C17 u8, set on a corpse

The physics ragdoll

[model+0x1C8] (26 of 26 characters read):

Offset Content
+0x00 pointer to the shared definition of the skeleton's ragdoll (one per body family)
+0x30 the character
+0x38 the model
+0x78, +0x88 per-body arrays (count at +0x80, +0x90)
+0x98 joint objects (count at +0xA0)

Every ragdoll has one body more than joints: orcs, Olog-hai and cave trolls 20 bodies and 19 joints, humans and wargs 21 and 20. A joint object (vt_0x1DB4980) keeps the powered-angle limits of its pitch, yaw and roll axes in radians at +0x0C-+0x20 (slots 10-12 convert from degrees), mirror bytes from +0x24 (slots 13-15) and a motor-disabled byte at +0x28 (slot 8).

The joint setup (entered at 0x64D4A4, which reaches its body 0x64D4B8(ragdoll, config) by a conditional tail jump) walks the config's Ragdoll/Constraints: joint [+0x98][Index], motor disabled unless Powered, then each axis's ConstraintAxisData MinPoweredAngle / MaxPoweredAngle (default -180 / 180) and Mirror. On the 11 corpses read whose powered ragdoll started, 19 of 19 joints hold exactly the database limits; living characters hold unconfigured joints (±565.49, 180 × π). The database tuning therefore reaches the physics only at death.

Ragdoll has two configs, Orc_Base and OlogHai_Base, each a brain and 19 joints (indexes 0-18, torso to feet). Character/Models.RagdollConfig names Orc_Base on 113 models (humans included, whose 20th joint is never tuned), OlogHai_Base on one and nothing on 167; models without a config, such as wargs, still take a ring slot but get no brain.

The ring of eight

The engine runs at most eight powered ragdolls. [base+0x26FFB90] holds eight controller pointers and [base+0x26FFB88] a cursor. The claim releases the controller in the cursor's slot if there is one (0x65C528), stores the new controller, writes the slot index into controller+0x2EC and advances the cursor modulo 8. A released controller keeps +0x2EC at -1 and its joints keep their tuning. In one read 15 corpses lay in the world and two held slots.

The brain

The controller at char+0x1AF8:

Offset Content
+0x08 the character
+0x10 the brain (Ragdoll/Brains)
+0x18 the config
+0x20, +0x28 the current and the prior behaviour controller
+0x48 eight 0x50-byte players
+0x2C8, +0x2CC, +0x2D0 height, speed, average body speed
+0x2D4-+0x2E8 time stationary, moving, on ground, in air, in the current behaviour, in total
+0x2EC ring slot, -1 for none

The brain step 0x30F6D4 runs only while the controller holds a slot. It updates the samples and the timers (the brain's OnGround and Stationary float ranges decide which timers run), then walks Brain.BehaviorControllers in order and takes the first controller whose conditions all pass: float ranges on height, velocity, average body velocity, time on ground, in air, stationary, moving, in the current behaviour and in total, spine upright, head severed, include and exclude hints, the current and prior controller, and the combat state. On a change it records the prior controller, resets the behaviour timer, picks a BehaviorSet whose RequiredRagdollConfig is the controller's config, picks one of its behaviours and starts the behaviour's Sequences.

Orc_Base lists ten controllers (DieHeadSeveredController, DieController, FallingBraceImpactController, TuckAndRollController, FallForwardBraceController, WritheOnGroundController, PanicLostGroundController, the two leg-kicking controllers and FallbackController); OlogHai_Base three. A behaviour (Ragdoll/Behaviors, 18) sets strength and stiffness ranges, joint masks and scalar mappings; a sequence (Ragdoll/Sequences, 16) plays timed target poses (Clamp, PingPong), and a target pose sets per-joint pitch, yaw and roll. In one read of eleven corpses with a brain, three ran DieHeadSeveredController, four DieController and four WritheOnGroundController (one entered from FallForwardBraceController).

Clip keywords

Two keyword tables name the ragdoll controls a clip can carry: at 0x1E658B0 (RAGDOLL, DESTROY, RESUMERAGDOLL, DISALLOWRAGDOLL, ALLOWRAGDOLL, DELAYRAGDOLL, COLLISION_CHECK_NONE, COLLISION_CHECK_WORLD_ONLY, COLLISION_CHECK_ALL, GROUNDCHECK, RAGDOLL_STIFFNESS, NEVER_RAGDOLL, FREEZERAGDOLL, BEHAVIOR_HINT) and at 0x1F9FC40 for sync actions (SYNCACTION_ALLOWDEATH, SYNCACTION_DELAYDEATH, RESUMERAGDOLL, ALLOWRAGDOLL, RAGDOLL, DELAYRAGDOLL, STOP_ALIGN), each entry a 32-bit hash and the name (the second table has no terminator; its seven entries run into a string). The hash is the multiply-by-31 string hash over the upper-cased, sign-extended bytes: 0x569FF8(rest of the string, seed) computes seed * 31 + c per character, recursively, and its callers seed it with the upper-cased first character, so a keyword matches in any case (17 of 17 table entries reproduce). The same hash has 18 callers, among them the slot-19 method of four classes.

The keywords are not turned into flags when a clip loads: the dead-body step asks the clip at run time. 0x75BB34(character, attack) copies the RAGDOLL and RESUMERAGDOLL entries to the stack and 0x2C5DD8(character, attack) uses the DESTROY entry; both pass the key set to 0x4CAB0C, which walks the events of the clip the character is playing (when [[char+0x26A8]+0x114] is not -1) and compares each event name's hash with the keys, or else to 0x2C5E10, which checks the attack's own animation: the clip named by AnimationData.Name (looked up in the character's set by 0x4C8A0C) or, without one, the AnimationData.BehaviorNode, where each key is tested by name (0x1BDEA50). The dead-body step 0x2F8460 is the only caller of 0x75BB34 and one of two callers of 0x2C5DD8 (the other is 0x19B6D1C). It calls 0x75BB34 only while the state byte +0x35A is set, then clears that byte without testing the result. The DESTROY query gates the step's flags: while +0x353 is set and the killing attack shows no DESTROY, the step can enter phase 5 (after further checks, among them the attack's AttackPostData.SuppressDeathAnimGroundCheck) and sets +0x356 when a second query, on the record 0x190E80 returns (a wrapper of the Combat/Attack.MultiHitRecords accessor), finds DESTROY; in another branch, no DESTROY sets +0x35C and +0x35D. The other entries of both tables have no direct reader in the code (checked for four of them).

In a sample of 40 clip files (302 clips, 4,192 events) the ragdoll events are DELAYRAGDOLL 23, RAGDOLL 22 (one with argument 0.25), ALLOWRAGDOLL 15 and RESUMERAGDOLL 14, spelled in any case. Combat/Sync records can start the source or the target with a delayed ragdoll (StartWithDelayRagdollSource 1 of 1,507, StartWithDelayRagdollTarget 98).

Open questions

Shadow of War (Steam), game version 1.21. An unofficial fan research project, not affiliated with or endorsed by Warner Bros. Games, Monolith Productions or Middle-earth Enterprises. Middle-earth: Shadow of War belongs to its owners.