Skip to content

Custom Creature: the skeleton

Getting a creature's rig right, and why the rig you have in Blender is probably not the rig the engine will use.

Part of the Custom Creatures guide. If you only need to rig a human mesh, Armature/Skeleton already covers that and this page is not for you.

Version

Measured against Bannerlord v1.4.8, except the export facts in the danger box, the humanoid physics and the hit capsules, measured on v1.5.3.

The one rule

Author against the skeleton the engine will play the clip on. Never against a mesh FBX.

Here is why, and it is not obvious.

Skinning uses bind matrices, and bind matrices are roll-independent. A mesh FBX can carry arbitrary bone orientations and still deform perfectly in game. Rotations are not roll-independent.

So this inference is false:

The mesh works in game with vanilla animations, therefore its rig is correct.

It proves the bone positions and the weights are right. It says nothing at all about orientations. An animation authored on that rig can look flawless in Blender and come out twisted in the engine, and nothing in Blender will ever show you the problem.

TAOM lost a full day and six wrong diagnoses to this on a single head-butt clip.

What the difference actually looks like

horse_skeleton, read out of the engine's own asset:

32 bones, no *_nub_notused entries
horsepelvis -> horsespine1 -> horsespine2 -> horsespine3 -> horseneck1 -> horseneck2 -> horse_head

Compared against a mesh FBX that skins to that same skeleton and animates correctly in game:

Fact The engine The mesh FBX
horseneck1's parent horsespine3 horsetail3
Bone axis X along the bone Blender imports it Y along the bone
_nub_notused bones none 7, which the Kit drops on import

The mesh rig parents the neck to a tail bone on purpose. The engine lists horse_skeleton's neck last, after the rear legs and the tail. The Kit stores an animation's bone tracks in FBX node order, which is depth-first, and never remaps them by name, so the tail parent is what makes the two orders agree. Re-parent the neck to horsespine3 and its tracks land in the rear legs' slots. The mesh rig's bone frames are still wrong: see the export mapping.

Getting the real skeleton

Open Modules/Native/AssetPackages/skeletons.tpac in TpacTool. Every vanilla skeleton is in that one file, with bone names, parents and rest frames.

Do not give up on the first package that fails

The per-creature rig packages do not parse: pack_horse_customrig, animations_horse_and_rider, animations_movement_and_behaviour, pack_anim_cutscene, animation_clips.tpac and Assets.tpac all throw Frames not equal or capacity was less than the current size. A day was lost treating that as "the skeleton is unobtainable". skeletons.tpac holds them all and works.

The clip and master items inside are still readable from code. TAOM's optional read_anim_keyframes_tpac.ps1 loads animation_clips.tpac and animations.tpac through TpacTool.Lib, the library in TpacTool's bin folder, and with -ByClip resolves each clip name to its master. Its default paths are one machine's.

If you would rather script it, TAOM has a PowerShell wrapper over TpacTool.Lib that dumps a skeleton to JSON, which is convenient if you want to rebuild the rig in Blender programmatically:

pwsh dump_engine_skeleton.ps1 -List
pwsh dump_engine_skeleton.ps1 -Skeleton horse_skeleton -OutFile horse_skeleton.json

It also takes -TpacToolBin and -NativeDir, and its defaults are hardcoded to one machine, so set both to your own TpacTool bin folder and your Modules/Native folder.

You do not need any of this. TpacTool's own window shows you the same data.

Rest-frame maths, if you are rebuilding the rig in Blender

RestFrame is row-vector form: the rows are the basis vectors and M41..M43 is the offset. Blender is column-vector. Transpose the 3x3 and move the offset to the last column:

Matrix(((m[0], m[3], m[6], m[9]),
        (m[1], m[4], m[7], m[10]),
        (m[2], m[5], m[8], m[11]),
        (0.0,  0.0,  0.0,  1.0)))

Then accumulate down the hierarchy (world = parent_world @ local) and build each bone with head = world.translation, tail = head + world_X * length, eb.align_roll(world_Z).

Skip the transpose and every bone offset reads zero, every bone stacks on the root, and the rig collapses to a point. That bug survived a while because the only check being run was an fcurve count, never a visual one.

Knowing the engine stores bones X-along does NOT mean exporting with primary_bone_axis='X'

This inference is natural, it was made, and it was tested. It produced the worst result of the entire session: the creature folded in on itself.

The convention the engine stores rest frames in and the export setting that reproduces a clip are two different questions. Export with primary_bone_axis='Y', secondary_bone_axis='X', from an armature whose bone frames equal the engine's, and bake a 180 degree turn about world Z into the keyed pose. The X-along construction above is for viewing the rig; how to build the export rig is in the export mapping.

Bone limits

There is exactly one, and it is not the one you may have read:

Skeleton.MaxBoneCount = 64. It is a cap on TOTAL bones per skeleton, not per mesh. Author 63 or fewer for margin.

There is no per-mesh bone palette limit

A widely-repeated "~40 bones per mesh / per draw call" figure does not exist. TAOM published it internally for months and it was wrong. Two in-game measurements killed it: a war elephant renders as one mesh skinned to 59 active bones, and a two-horse chariot renders as one mesh skinned to 54.

This matters because the false limit is used to justify splitting a creature body into several meshes, and splitting causes its own problems. A fully disjoint body mesh in <AdditionalMeshes> may not render at all.

Keep the whole body in one mesh. Split only for a genuinely separate sub-mesh with a different simulation need. The warg splits its fur so the fur can cloth-simulate independently, which is cloth-driven, not bone-driven.

Exporting the rig

Skeleton and meshes go in one FBX. A mesh-only export (armature deselected) drops the skin weights entirely, because Blender stores skin in the FBX armature deformer. Re-import such a file and you will find zero vertex groups.

object_types={'ARMATURE', 'MESH'}
primary_bone_axis='Y', secondary_bone_axis='X'
axis_forward='-Y', axis_up='Z'
add_leaf_bones=False
bake_anim=False

For a mesh export the armature keeps its real name. (The <skeleton>_notused rename is for animation-only exports, and is covered on the animation page.)

primary_bone_axis is not a preference, and the obvious test will not catch it

At X/Y the bone heads and the bounding box stay correct to 1e-06 while the tails drift over half a metre, putting a hip bone 88% of its reference motion out of place. A validation gate that checks head positions or bounding boxes passes the broken file.

Check tails.

Also note primary_bone_axis='X' force-aligns every bone to world X and destroys mirror symmetry on a symmetric rig.

After import, set the skeleton's Type in the Kit. A fresh import comes in as other; quadruped mounts want horse.

Two things in the Kit that look like failures and are not

A duplicate name error is the protection working. If you import a second FBX containing an armature the module already has, you get:

Unable to import skeleton_warg(Skeleton). Item with same name already exists in
Warg_Rig_V5_geo. Asset names are required to be unique within the same module.

That is correct. The Kit enforces per-module name uniqueness, skips the duplicate, imports the meshes, and lets them bind to the skeleton already present. No workaround is needed. Renaming the armature to dodge it produces a tpac with no Skeleton item, which merely looks like success.

The bone-octahedron display is cosmetic. With add_leaf_bones=False the Kit computes bone tails itself and draws them scrambled, even on data that is perfectly symmetric. Bannerlord uses only each bone's origin and rotation for skinning; it does not use tails. Judge by the model viewer, never by the skeleton editor.

Package the skeleton with the mesh

Every working creature bundles its Skeleton item inside the same .tpac as its meshes.

Do not ship a standalone skeleton-only tpac

It is an unproven structure and it crashed the game: a recursive worker-thread native access violation reading null, on spawn. That is strictly worse than the problem it was trying to fix.

If a mesh re-export silently drops the Skeleton item, the symptom is quiet: the action_set still declares skeleton="<name>", that now resolves to nothing, agent-skeleton creation returns null, and the rider spawns with no mount at all. No crash, no error. Easy to miss for days.

Check after every mesh re-export that the Skeleton item is still in the live tpac, and compare against a known-good creature rather than against your own expectations.

Textures: four rules the Kit does not enforce loudly

None of these produces a clear error message.

Dimensions must be divisible by 4, and should be a power of two. Block compression works on 4x4 blocks. Eight maps at 5689x5689 (odd, so not divisible by 4) silently fell back to uncompressed R8G8B8A8_UNORM at 164 MiB each. Six of them were essentially the entire 940 MB runtime cache payload for one creature. The same map at 2048 with DXT5 and mips is 5.33 MiB.

The formula worth memorising:

bytes = width x height x bpp x 4/3

where 4/3 is the mip chain and bpp is 4.0 uncompressed, 1.0 for BC3/DXT5 and BC5, 0.5 for BC1 and BC4. Halving the dimension quarters the memory.

Palette-mode PNGs compile to single-channel BC4. A single-channel normal map is worthless. Re-save as RGB. Watch out that a resize pass will silently skip files already at the target size, so they keep their palette: the de-palette has to be a separate forced re-encode.

Uppercase in a texture filename can crash the editor when the texture is assigned to a material. Mesh names are lowercased automatically on import (SK_GD_Fellwarg becomes sk_gd_fellwarg) while texture names keep their case, and that asymmetry is exactly the shape that produces a lookup miss. Lowercase is safe either way.

Force colour management off on data maps. Normal, AO and metallic are data, not colour. Set colorspace_settings.name = 'Non-Color' and the view transform to Standard at gamma 1.0 before saving, or you get a creature with subtly wrong lighting that nobody can explain later.

Check materials after every FBX re-import

The Kit binds each imported mesh to the material its FBX names, lowercased, looked up across the whole module. No match logs Unable to find material and leaves the mesh unbound. An older Kit material with the same name wins silently: that is how TAOM's new hill troll came out wearing the old model's textures. A re-import may also reset bindings assigned by hand in the editor.

One TAOM rig carried three material slots for five meshes; four more had been editor-assigned by the original author. After re-import the creature had every XML row, every material asset and every clip correct, and still did not appear in battle.

A missing material binding does not present as a missing material

It presents as the wrong colour, or as nothing at all. Bindings sit one per LOD level, so a creature can render correctly in a close-up UI preview and be invisible in the world, where it draws at a lower LOD.

After every re-import, check each mesh at every LOD. Fix a name in Blender's material panel or with TAOM's optional fbx_remap_materials.py.

Physics: bodies and ragdoll joints

A freshly imported skeleton comes in with Usage = 'other', every collision body empty (body_type='none', mass 0, radii at an unset sentinel) and zero joint constraints. A re-import that left the skeleton unchanged kept this data byte for byte in TAOM's checks; one that rebuilds the skeleton empties it. Compare decompressed skeleton segments, not file bytes: the Kit's LZ4 output differs on every compile.

A healthy creature skeleton has typed bodies and roughly one constraint per bone. For comparison:

Skeleton Bones Usage Ragdoll constraints
human_skeleton 28 human 34 (15 d6, 19 ik)
horse_skeleton 32 horse
skeleton_warg 49 horse 48
elephant_skeleton 60 horse 59
spider_skeleton 62 horse
troll_skeleton_a (a humanoid race) 28 human 34

The human's ik joints are ragdoll constraints, not animation IK. TAOM's spider skeleton launch-crashed as a raw other import with no constraints; as horse it fights. A race on its own skeleton carries the human's physics across.

Zero constraints and every body typed none means the data was dropped. Setting it by hand in the Kit is real work: for a 58-bone creature that is 57 joints times roughly 12 fields each.

Hit capsules are what weapons strike

Each body has a hit capsule, which weapons and arrows strike (read from the field names and body zones; no in-game hit test yet), and a ragdoll capsule, which only moves the corpse. Other agents walk into the Monster's body_capsule instead. The Kit's default hit capsule is a rod whose radius is about a ninth of its length: TAOM's war elephant had 41 of 60 like that, with under half its skin inside any hit capsule. See big creatures in battle.

Next

Authoring the animation clips, or, if you took the reskin path and need no clips, straight to the XML.