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.
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. It skins fine anyway. It animates wrong.
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.
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 Blender export setting that
reproduces a clip the Kit reads correctly are two different questions, and knowing the first
does not answer the second. Export with primary_bone_axis='Y', secondary_bone_axis='X'.
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:
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.
Materials do not survive an FBX re-import
An FBX carries only the material slots it actually has. Anything assigned by hand in the editor exists only inside the compiled tpac, and a re-import silently loses it.
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.
Re-assign materials in the Kit after any re-import, and check every LOD.
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. An FBX
re-import rebuilds the bone definition but not this data.
A healthy creature skeleton has typed bodies and roughly one constraint per bone. For comparison:
| Skeleton | Bones | Usage | Ragdoll constraints |
|---|---|---|---|
human_skeleton |
28 | human |
|
horse_skeleton |
32 | horse |
|
skeleton_warg |
49 | horse |
48 |
elephant_skeleton |
60 | horse |
59 |
spider_skeleton |
62 | other |
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.
Next
Authoring the animation clips, or, if you took the reskin path and need no clips, straight to the XML.