Physics: Havok integration

How the engine embeds Havok 2016.2: the new-pipeline (hknp) world with legacy-pipeline ragdolls, the engine's handle to the world, the world's layout and gravity, the material palette and its half-precision fields, what is live and what ships unused, the Havok heap, the tagfiles that carry navigation meshes and collision shapes, and the surface types that decide footsteps, climbing, shooting through and impacts.

Which Havok

Physics is Havok Physics 2016.2 (hk2016_2_0_r1) in its new pipeline: the hknp* classes, which replaced the older hkp* API. The build is a hybrid: rigid bodies are hknp, but ragdoll constraints are still the legacy hkpRagdollConstraintData and hkpLimitedHingeConstraintData. Havok AI (hkai*) provides navigation meshes, avoidance and silhouettes, and Havok Cloth deforms cloth with hkaSkeleton; Havok Animation and Behavior are not used (see Animation). HavokScript runs the game's scripts (see Scripting).

Havok work runs on three HavokWorkerThreads (shared routine 0x11AFE58) and on the main thread's task queue: in play the main thread spends about 5% of its time in the physics step (TtPhysics, 0x6528B8) and about 3% in the Havok task queue, where hkaiAvoidanceSolverTask shows up.

The engine's handle

The world has exactly one construction path (its constructor 0x15464FC has one caller, 0x11B1504, whose only caller is 0x1187004), and the engine publishes its owner in a static slot:

[base+0x2A86760]   the physics owner (0x6860 bytes), published by 0x1186FB0
                   (1 write, 66 reads in 60 functions)
[base+0x2A86768]   u32 generation, incremented at each publish
  owner+0x20 -> hknpWorld (allocated from the Havok heap, 0xAA0 bytes)

The world is rebuilt on every world load.

The world

hknpWorld (vt_0x22EEA90). Sub-objects, named by their own run-time type information:

Offset Class
+0x018 a pointer to itself
+0x470 hknpMultithreadedSimulation
+0x478 hknpDynamicSpaceSplitter
+0x488 hknpHybridBroadPhase
+0x6C8 hknpStreamContactSolver
+0x6D0 hknpConstraintAtomSolver
+0x9C0 gravity, an aligned hkVector4 (0, -9.81, 0, 0): Y is up, SI units
+0x9D8 hknpMaterialLibrary (vt_0x22ED5C8; from the construction info, else a default made by 0x154AC7C)
+0x9E0 hknpMotionPropertiesLibrary (vt_0x22ED290; 8 motion properties)
+0x9E8 the body-quality library (0x620 bytes: 32 inline 0x30-byte hknpBodyQuality)

Arrays are Havok hkArrays ({T* data, u32 size, u32 capacity and flags}). The body arrays at +0x040, +0x050, +0x0C8 and +0x0D8 / +0x0F8 hold 15,360 entries each: the body capacity is 15,360, kept as parallel structure-of-arrays lanes. +0x0B8 is the active-body id list (254 of 15,364 in a combat scene); a body id packs index | generation << 24. +0x108 has capacity 1,024, matching the material palette.

This gravity drives the rigid-body solver only. Characters move through the engine's character controller, which the behaviour graph owns (see Animation).

Materials

Both libraries keep their entries in an hkArray at +0x30. The material palette is one contiguous array of 1,024 hknpMaterial entries (vt_0x1DAC040, stride 0x80):

Offset Type Field Default
+0x32 hkHalf dynamic friction 0.5
+0x34 hkHalf static friction 0.5
+0x36 hkHalf restitution 0.0
+0x3C f32 - 0.05
+0x40 f32 max contact impulse (-1.0 = an untouched slot) -1.0
+0x44 f32 - 1.0
+0x48 f32 - 1.0

hkHalf is not IEEE half precision: it stores the top 16 bits of a float32 (decode with float_from_bits(h << 16)). Only a handful of entries are configured - they carry a maximum contact impulse of FLT_MAX instead of the sentinel - and the number grows during play as surfaces register on demand: 13 and then 18 configured entries were seen in one world about an hour apart. Entries 0-7 are nearly uniform (friction 0.5, restitution 0, with one bouncier entry at 0.199 and one at friction 0.547 / restitution 0.05), so a handful of shared materials surfaces the whole world.

What is live, and what ships unused

A census of the live heap in a desert scene (169 live Havok classes; controls passing):

Class Live
hknpConvexPolytopeShape 6,673
hknpBoxShape 5,680
hknpCapsuleShape 3,062
hknpCompressedMeshShape (static world collision) 798
hknpTriangleShape 528
hknpCompoundShape 372
hknpSphereShape 264
hknpShapeMassProperties 7,009
hknpMaterial 1,024
hknpBodyQuality 32
hkpRagdollConstraintData 1,857
hkpLimitedHingeConstraintData 420
hkaiNavMeshInstance 464
hkaiNavMeshClearanceCache 5,300
hkaiPointCloudSilhouetteGenerator 3,124
hknpWorld 1

The default modifier set (hknpDefaultModifierSet) instantiates the six per-contact modifiers: trigger, mass changer, restitution, soft contact, surface velocity and animated-shape surface velocity.

Shipped in the executable with their vtables but never instantiated: the complete Havok vehicle suite (hknpVehicleInstance with its engine, transmission, steering, suspension, brake, aerodynamics, velocity damper, tyre marks and ray-cast wheels), Havok's demo guns (hknpBallGun, hknpGravityGun, hknpProjectileGun, hknpMountedBallGun, hknpFirstPersonGun, hknpFirstPersonCharacter), the spring / unary / binary actions, and the linear and circular surface velocity objects.

The Havok heap

Havok allocates its whole heap in one call (0x11870E4); its size is the u32 at 0x262AC80. The world, both libraries, the material palette and the motion array all live in that single region, which is the largest single allocation in the process (512 MB with a raised setting).

Tagfiles: navigation meshes and collision shapes

Havok data ships as tagfiles (root hkRootLevelContainer) that carry their own type table, so every class layout is read from the file itself. Every carrier parses exactly: 923 standalone .hkai files (185 types), 1,176 HKAI sections inside bundles, and 5,148 physics files in world-object entries plus 4,808 inside bundles (196 types).

Surfaces

What a character, a projectile or a sound does when it meets geometry is decided by the surface it touched: one of the 78 Surfaces/Surfaces records (type Surface.Surface).

Field Records with it set Read by
CanSeeThrough 21 0x274AA0 (0x272E48 tests it before CanShootThrough)
CanShootThrough 26 0x405CC8 (four call sites)
CanPlayerRangedTargetThrough 23 0x739490, which also requires CanShootThrough
CanDoFootsteps 55 footstep code
FootPrintFX / FootPrintLifetime 0x10FBA8, 0x2DA0F8 (the feet: 0x2DA16C, FrontRightFoot ...); a list of _StructuresSurfaceFootstepData
WeaponFX 59 link one 0x15B0DC; Surfaces/WeaponFX (13: default, Wood, Stone, Metal, Dirt, Water ...)
OcclusionFactor 0x405A64, through the audio occlusion query 0x383104
CollisionProperty 41 link one 0x1B6D1D8; Collisions/CollisionProperty
CanWallClimb 7 0x1C71B44 (six call sites, 0x123E24 and 0x371708 among them)
IsSlippery 14 0x652048, after a world query in 0x6518E4
CanWargJump 60 0x1BE5ABC, in the warg's navigation jump test (FoundNavMesh_JumpUp, 0x449F90)
IsFireProp 12 0x405B28
CanSpiderCrawl 73 nothing
CanMeleeThrough 10 nothing
FXContextList a string of effect contexts (stone ...)

Every field but two has an accessor read by game code; CanSpiderCrawl and CanMeleeThrough are registered in the schema and filled in the data, but no code reads them. The sets are telling: wall-climbing is allowed only on Air, AxeClimb, Can_Scramble, PlayerBlocker_Dust and the three No_WallRunPlusSlip* surfaces; the 14 slippery surfaces are the *_No_WallRun* family, the player blockers, Perimeter, Wood_Tree and Slippery; the 12 fire props are the wood and cloth surfaces; warg jumps are refused on 18 surfaces, the five Drake_* collision surfaces among them, which are also the only five that refuse spider crawling.

Surface ids. Collision data does not name a surface by index but by a 16-bit id folded from the P919 hash of its record name: id = (h + (h >> 16)) & 0xFFFF (78 of 78 surfaces, no two sharing an id). A table of 16-byte entries {u16 id, ..., +0x08 the record} between [base+0x277C9E0] and [base+0x277C9E8] (78 entries, in the category's record order) maps an id back to its record: 0x2748F8 compares the id with the "no surface" value at 0x2A868F8 (0) and searches the table. A query result carries a surface reference {u16 id, u8 valid}, resolved by 0x2748E4 (null when the valid byte is 0). A world object can also carry its own surface: the u16 at +0xCE (0x274B5C; 0x274B68 returns the u32 at +0xCC, whose high half it is). In one loaded world 107 of 105,842 placed objects set it (106 metal_cage, one water_shallow); every other object holds 0, no surface.

Impacts and collision properties. Surfaces/Impacts has 65 records, one per material and size (Small, Medium, Large, Ambient, Explosion, with _OlogHai variants, Fist_*, Nazgul_Projectile); Surfaces/SurfaceFXType names the eight impact kinds. Collisions/CollisionProperty has 52 records, of which the surfaces use nine (Wood 16 times, Metal 6, water 5, Cloth 4, Stone 3, Footstep 2, Sand 2, Flesh 2, Orc_head 1); the table also carries props of a contemporary setting that nothing in this game's surfaces refers to (Office_Chair, Keyboard, ComputerCase, Radio, Deli_SignChains, CeilingTile, Duct, PropaneTank ...).

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.