FO4 Havok Packfile Format (hk_2014.1.0) - BadDogSkyrim/PyNifly GitHub Wiki

Fallout 4 stores collision geometry in bhkPhysicsSystem blocks as raw Havok packfile blobs. Each packfile is a self-contained binary containing one hknpPhysicsSystemData object with one or more rigid bodies and their collision shapes.

NIF Block Hierarchy

bhkNPCollisionObject
  ├─ dataID  → bhkPhysicsSystem     ← raw packfile bytes in .data
  └─ bodyID  → index into bodies array

Multiple NIF nodes can reference the same bhkPhysicsSystem, each selecting a different body by bodyID.

File Structure

All values are little-endian. Pointers are 8 bytes (64-bit packfile format).

0x000–0x03F: Global file header      (0x40 bytes)
0x040–0x07F: Section header 0        (__classnames__)
0x080–0x0BF: Section header 1        (__types__, usually empty)
0x0C0–0x0FF: Section header 2        (__data__)
0x100+:      Section data, then fixup tables

Global File Header (0x40 bytes)

Offset Size Field Value
0x00 8 magic 57 E0 E0 57 10 C0 C0 10
0x08 4 userTag 0
0x0C 4 fileVersion 11
0x10 4 layoutRules 08 01 00 01 (ptrSize=8, little-endian)
0x14 4 numSections 3
0x18 4 contentsSectionIndex 2 (data section)
0x1C 4 contentsSectionOffset 0
0x20 4 contentsClassNameIndex 0
0x24 4 contentsClassNameOffset offset of hknpPhysicsSystemData in classnames
0x28 18 contentsVersion hk_2014.1.0-r1\0\xFF
0x38 4 flags 0
0x3C 4 maxPredicate 21

Section Headers (0x40 bytes each)

Three section headers at offsets 0x40, 0x80, 0xC0:

Offset Size Field
0x00 20 sectionName (null-terminated, padded with 0xFF)
0x14 4 absStart (absolute file offset of section data)
0x18 4 localFixup (relative to absStart)
0x1C 4 globalFixup (relative to absStart)
0x20 4 virtualFixup (relative to absStart)
0x24 4 exports (= end of data, before fixups)
0x28 4 imports (same as exports)
0x2C 4 end (same as exports)
0x30 16 padding (0xFF)

Classnames Section

Sequence of entries, each:

u32  hash
u8   0x09          (type flag)
char name[]        (null-terminated)

Padded to 16-byte boundary with 0xFF.

Class entries vary by shape type. All packfiles include:

  • hkClass, hkClassMember, hkClassEnum, hkClassEnumItem
  • hknpPhysicsSystemData

Polytope adds: hknpConvexPolytopeShape, hkRefCountedProperties, hknpShapeMassProperties.

Compressed mesh adds: hknpCompressedMeshShape, hknpCompressedMeshShapeData, hknpBSMaterialProperties.

Sphere adds: hknpSphereShape.

Compound adds: hknpDynamicCompoundShape.

Data Section Layout

The data section always begins with an hknpPhysicsSystemData, followed by per-body arrays, then the shape objects. A typical single-polytope layout:

+0x0000: hknpPhysicsSystemData       (0x80 bytes)
+0x0080: body_props[N]               (0x110 bytes each)
         [dyn_motion, if dynamic]    (0x40 bytes)
         [dyn_inertia, if dynamic]   (0x40 bytes)
+0x....: BodyCInfo[N]                (0x60 bytes each)
+0x....: ShapeEntry[N]               (0x10 bytes each)
+0x....: shape objects (polytope, compressed mesh, sphere, etc.)
+0x....: hkRefCountedProperties      (0x20 bytes, polytope/CM only)
+0x....: hknpShapeMassProperties     (0x30 bytes, polytope/CM only)

hknpPhysicsSystemData (0x80 bytes)

Six hkArray slots (16 bytes each) plus 16 bytes padding:

Offset Array Count
0x00 (unused) 0
0x10 body_props num_bodies
0x20 dyn_motion 1 if dynamic, else 0
0x30 dyn_inertia 1 if dynamic, else 0
0x40 BodyCInfo num_bodies
0x50 (unused) 0
0x60 ShapeEntry num_bodies
0x70 (padding)

Each hkArray is 16 bytes:

+0x00: u64  pointer     (local fixup → array data)
+0x08: u32  size
+0x0C: u32  capacity    (= size | 0x80000000)

body_props (0x110 bytes per body)

Physics material and simulation parameters.

Offset Size Field Encoding
0x00 2 friction truncated float16
0x04 2 restitution truncated float16
0x48 4 gravityFactor float32
0x50 2 maxLinearVelocity truncated float16
0x54 2 maxAngularVelocity truncated float16
0x58 2 linearDamping truncated float16
0x5C 2 angularDamping truncated float16
0x84 4 inverseMass float32 (1.0/mass)
0x88 4 density float32 (mass/volume)
0x10A 1 collisionResponse 0=contact, 1=none

Truncated Float16 Encoding

The upper 16 bits of a 32-bit IEEE float, stored as a u16:

# Decode
value = struct.unpack('<f', struct.pack('<I', u16 << 16))[0]

# Encode
u16 = struct.unpack('<I', struct.pack('<f', value))[0] >> 16

This gives ~3 decimal digits of precision. Vanilla defaults: friction=0.5, restitution=0.4.

BodyCInfo (0x60 bytes per body)

Rigid body definition — shape pointer, position, and orientation.

Offset Size Field
0x00 8 shape pointer (global fixup → shape object)
0x08 8 flags (7FFFFFFF 7FFFFFFF)
0x10 8 type/count fields
0x18 24 (zeros)
0x30 12 position (x, y, z) float32, Havok space
0x3C 4 (padding)
0x40 16 quaternion (x, y, z, w) float32, identity = (0,0,0,1)
0x50 16 (zeros)

Body Transform Semantics

  • Non-identity rotation: body transform = NIF node's world transform. Apply to vertices to get world-space positions.
  • Identity rotation: position is centre-of-mass, not node position. Vertices are in node-local space. Don't apply transform to verts.
  • Sphere shapes: position is baked into vertex offsets (no vertex transform needed). The body position records the sphere center.

ShapeEntry (0x10 bytes per body)

+0x00: u64  shape pointer (global fixup → shape object)
+0x08: u64  (reserved, zeros)

dyn_motion (0x40 bytes, dynamic bodies only)

Engine defaults for dynamic simulation. Usually constant across all FO4 NIFs.

Offset Size Field Default
0x08 4 gravityFactor 1.0
0x10 4 maxLinearVelocity 104.375
0x14 4 maxAngularVelocity 31.57
0x18 4 linearDamping 0.1
0x1C 4 angularDamping 0.05

dyn_inertia (0x40 bytes, dynamic bodies only)

Per-body mass and inertia tensor.

Offset Size Field
0x04 4 inverseMass (float32, 1/mass)
0x08 4 density (float32, mass/volume)
0x20 4 inertia_xx (float32)
0x24 4 inertia_yy (float32)
0x28 4 inertia_zz (float32)

Shape Objects

hknpConvexPolytopeShape (variable size)

Convex hull defined by vertices, face planes, and a face-vertex-index array.

+0x00: 0x30-byte fixed header
+0x30: u16 numVertices, u16 verticesOffset (=0x20), 12 bytes zeros
+0x40: u16 numVerts2, u16 planesOff, u16 numPlanes,
       u16 facesOff, u16 numFVI, u16 fviOff, 4 bytes zeros
+0x50: vertices[]     (numVerts × 16 bytes)
       planes[]       (numPlanes × 16 bytes)
       28-byte gap
       faces[]        (numPlanes × 4 bytes)
       4-byte gap
       fvi[]          (numFVI × 1 byte)
       [padded to 8-byte boundary]

Vertex (16 bytes): float x, y, z; u32 w where w = 0x3F000000 | (index & 0xFF).

Face (4 bytes): u16 firstFVI; u8 numVtx; u8 flags.

FVI: flat array of vertex indices, one byte each.

The fixed header at +0x14 contains the convex_radius as a float32.

hknpCompressedMeshShape (0xC0 bytes) + ShapeData

Two objects: a shape header (0xC0) pointing to a ShapeData (0xA0+).

ShapeData arrays:

Offset Array Contents
0x20 aabb_min vec4 (min corner of bounding box)
0x30 aabb_max vec4 (max corner of bounding box)
0x50 sections Section structs (0x60 each)
0x60 quadIndices Packed quad/triangle indices
0x80 packedVertices 11-11-10 bit packed vertices

Packed vertex encoding (u32):

qx = (v >>  0) & 0x7FF     (11 bits)
qy = (v >> 11) & 0x7FF     (11 bits)
qz = (v >> 22) & 0x3FF     (10 bits)

x = section.base_x + qx * section.scale_x
y = section.base_y + qy * section.scale_y
z = section.base_z + qz * section.scale_z

hknpSphereShape (0x50 bytes)

Offset Size Field
0x10 4 flags (0x01000111)
0x14 4 radius (float32, Havok space)
0x30 4 flags2 (0x00100004)
0x4C 4 value (0.5)

The sphere center is stored in the BodyCInfo position field, not in the shape object itself.

hknpDynamicCompoundShape

Container for multiple child shapes with per-instance transforms. Each instance references a child shape (typically hknpConvexPolytopeShape) and carries a position/rotation transform.

Fixup Tables

Three fixup tables follow the data section objects, in order.

Local Fixups

Resolve pointers within the same section (e.g. hkArray → its data).

u32 src_offset    (offset in data section where pointer lives)
u32 dst_offset    (offset in data section the pointer targets)

Terminated by FFFFFFFF FFFFFFFF.

Global Fixups

Resolve pointers between sections (e.g. BodyCInfo shape_ptr → shape object, or hkRefCountedProperties → classnames).

u32 src_offset    (offset in data section)
u32 section_idx   (0=classnames, 1=types, 2=data)
u32 dst_offset    (offset in target section)

Terminated by FFFFFFFF FFFFFFFF FFFFFFFF.

Virtual Fixups

Associate objects with their class names (for type identification).

u32 obj_offset    (offset in data section where object starts)
u32 section_idx   (always 0 = classnames)
u32 name_offset   (offset in classnames section)

Terminated by FFFFFFFF FFFFFFFF FFFFFFFF.

Fields NOT in the Packfile

These bhkRigidBody properties from Skyrim's collision system have no equivalent in the FO4 packfile format:

  • penetrationDepth
  • motionSystem
  • deactivatorType
  • qualityType

References

  • PyNifly parser: io_scene_nifly/pyn/bhk_autounpack.py
  • PyNifly packer: io_scene_nifly/pyn/bhk_autopack.py
  • PyNifly collision import/export: io_scene_nifly/nif/collision.py