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,hkClassEnumItemhknpPhysicsSystemData
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:
penetrationDepthmotionSystemdeactivatorTypequalityType
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