1 The whole pipeline
Everything that makes a Kenshi face or body passes through these stages. Each one is a function
in kenshi_x64.exe that was decompiled and re-implemented. The addresses are on the
method page.
editor_data_<race>.xml, which lists every slider..bod2 file or the randomiser. Pose sliders store weights, not values.softwareVertexPoseBlend adds the offsets to a private mesh copy at spawn.updateProportions scales 30 bones. Three sliders pick a time in posture clips..bod2, or the MSVC rand()
seeded from a hash of a string. That string is what limits the NPC faces (§9).2 Base assets
Each race and gender has one skinned mesh with the head built in. The morph targets
(OGRE "poses") are stored in the mesh and only touch head vertices. All 30 races share the same two
30-bone 3ds Max Biped skeletons (Bip01 …, no face bones). That's why body proportions
and posture work identically for a human, a Shek, a Hive drone or a skeleton robot.

| Asset | File | Notes |
|---|---|---|
| Body + head mesh | character/meshes/human/human_male.mesh | 15,504 vertices, 23 poses. The female has 16,614 vertices and 29 poses. |
| Skeleton | male_skeleton.skeleton | 30 bones, 174 animations, including the postures, neck set and shoulder set clips |
| Editor limits | editor/editor_data_human.xml | Slider list, ranges, pose targets and the skin ramp |
| Head textures | skins/human_male/*_head_*diffuse_HI.dds + normal + mask | 4 per gender for Greenlanders, weighted equally |
| Body textures | *_body_diffuse.dds, *_body_normal{,_muscular,_skinny}.dds | The normal map blends with physique |
| Hair, beards | character/hair/meshes/*.mesh + painted *BaseHead*.dds | ATTACHMENT records, slot 2 = hair, slot 13 = beard |
| Shader | materials/deferred/character.hlsl | main_fs |

All 30 races (sliders = face (morph targets) / body; per-gender counts)
| Race | Playable | Genders | Mesh | Editor limits | Sliders | Heads | Hairs | Beards | Colours |
|---|---|---|---|---|---|---|---|---|---|
| Alpha Fishman (rebirth.mod) | yes | one | fishman | editor_data_Fishman.xml | 23 (17) / 16 | 1 | 1 | 0 | 0 |
| Alpha Fishman (Newwworld Plus.mod) | yes | one | fishman | editor_data_Fishman.xml | 23 (17) / 16 | 1 | 1 | 0 | 0 |
| Fishman | yes | one | fishman | editor_data_Fishman.xml | 23 (17) / 16 | 1 | 1 | 0 | 0 |
| Flayed Man | yes | M/F | human_male | editor_data_human.xml | 23 (17) / 16 | 3/3 | 42/42 | 32 | 6 |
| Flayed Woman | yes | M/F | human_male | editor_data_human.xml | 23 (17) / 16 | 3/3 | 42/42 | 32 | 6 |
| Greenlander | yes | M/F | human_male | editor_data_human.xml | 23 (17) / 16 | 4/4 | 42/42 | 32 | 6 |
| Hive Prince | yes | one | stick_person | editor_data_stick_prince.xml | 13 (8) / 16 | 1 | 0 | 0 | 0 |
| Hive Soldier Drone | yes | one | stick_person3 | editor_data_stick_soldier.xml | 13 (8) / 16 | 1 | 0 | 0 | 0 |
| Hive Tall Head | yes | one | stick_person2 | editor_data_stick_people.xml | 13 (8) / 16 | 1 | 0 | 0 | 0 |
| Hive Worker Drone | yes | one | stick_person2 | editor_data_stick_people.xml | 13 (8) / 16 | 1 | 0 | 0 | 0 |
| King Fishman | yes | one | fishman | editor_data_Fishman.xml | 23 (17) / 16 | 1 | 1 | 0 | 0 |
| Scorchlander | yes | M/F | human_male | editor_data_sundemon.xml | 23 (17) / 16 | 1/1 | 42/42 | 26 | 1 |
| Shek | yes | M/F | bone_male | editor_data_bone_people.xml | 27 (21) / 16 | 3/3 | 1/1 | 0 | 2 |
| Skeleton | yes | one | robot | editor_data_human.xml | 23 (17) / 16 | 1 | 0 | 0 | 0 |
| Cannibal | M/F | human_male | editor_data_cannibal.xml | 19 (13) / 16 | 1/1 | 1/1 | 0 | 5 | |
| Cannibal Skav | M/F | human_male | editor_data_cannibal.xml | 19 (13) / 16 | 1/1 | 1/1 | 0 | 5 | |
| Deadhive Prince | one | stick_person | editor_data_stick_prince.xml | 13 (8) / 16 | 1 | 0 | 0 | 0 | |
| Deadhive Soldier | one | stick_person3 | editor_data_stick_soldier.xml | 13 (8) / 16 | 1 | 0 | 0 | 0 | |
| Deadhive Worker | one | stick_person2 | editor_data_stick_people.xml | 13 (8) / 16 | 1 | 0 | 0 | 0 | |
| Hive Prince South Hive | one | stick_person | editor_data_stick_prince.xml | 13 (8) / 16 | 1 | 0 | 0 | 0 | |
| Hive Queen | one | hive_queen | editor_data_stick_queen.xml | 13 (8) / 16 | 1 | 0 | 0 | 0 | |
| Hive Soldier Drone South Hive | one | stick_person3 | editor_data_stick_soldier.xml | 13 (8) / 16 | 1 | 0 | 0 | 0 | |
| Hive Worker Drone South Hive | one | stick_person2 | editor_data_stick_people.xml | 13 (8) / 16 | 1 | 0 | 0 | 0 | |
| P4 Unit | one | robot_variant02 | editor_data_human.xml | 23 (17) / 16 | 1 | 0 | 0 | 0 | |
| Screamer MkI | one | robot_variant01 | editor_data_human.xml | 23 (17) / 16 | 1 | 0 | 0 | 0 | |
| Skeleton Log-Head MKII | one | skeleton_mkii - log head | editor_data_mkii.xml | 5 (0) / 16 | 1 | 0 | 0 | 0 | |
| Skeleton MKII Screamer | one | skeleton_mkii - screamer | editor_data_mkii.xml | 5 (0) / 16 | 1 | 0 | 0 | 0 | |
| Skeleton No-Head MkII | one | skeleton_mkii - no head | editor_data_mkii.xml | 5 (0) / 16 | 1 | 0 | 0 | 0 | |
| Skeleton P4MkII | one | skeleton_mkii | editor_data_mkii.xml | 5 (0) / 16 | 1 | 0 | 0 | 0 | |
| Soldierbot | one | robot_variant03 | editor_data_human.xml | 23 (17) / 16 | 1 | 0 | 0 | 0 |
3 Sliders and what they drive
AppearanceManager::loadEditorXMLData reads each <Config gender> and
<Category> and sorts the entries into three kinds:
| XML | Kind | Stores | Example |
|---|---|---|---|
<Range> without target | bone slider | an integer, e.g. 90–110 | Head size, Neck, Jaw, Height, Chest… |
<Range target=… [target_opposite=…]> | morph slider | weights under the pose names | Nose tilt → tiltup_nose / tiltdown_nose |
<ColourRange> | colour ramp | an RGB colour | Skin Tone, 10 units per colour stop |
Missing attributes get defaults: mid = the rounded midpoint, and
random_variation = 40 % of the range (GLOBAL CONSTANTS
appearance random deviation percentage = 0.4). Many morph sliders don't centre on 0.
For example, Greenlander male Eyes height runs from −150 to 100 with mid −25.
// DataRangePose: slider value v (an integer percentage) → pose weights
if no opposite: w[target] = v × 0.01 // may be negative: the offset is subtracted
else: w[target] = max(v, 0) × 0.01
w[opposite] = max(−v, 0) × 0.01
4 Morphs are baked, not animated
When a character spawns, AppearanceManager::createMorphedMesh clones the base mesh and
calls Ogre::Mesh::softwareVertexPoseBlend(w, offsets) once per non-zero pose. That
adds w × offset to every vertex in the pose. Then it deletes the poses and switches the
entity to the copy. Nothing is morphed per frame.
Consequence: normals stay the same
The human poses have no normal offsets. A strongly morphed nose is lit with the base nose's normals. The game never recomputes them, so the demo doesn't either.
Consequence: meshes can be shared
A baked mesh has a name, and anyone with the same name reuses it. Generic NPCs exploit this: everyone with the same race, faction weirdness, gender and morph index shares one mesh (§9).
The live editor works on an unbaked _CHAREDIT_ copy and applies the
difference (new − old) each time a slider moves.
5 Bone sizes: the body sliders
Kenshi's OGRE fork adds two fields to every bone, and AppearanceHuman::updateProportions
sets both on all 30 bones in one pass:
Bone size
Multiplies only that bone's own derived scale. Children don't inherit it. Widening the chest doesn't widen the arms.
derivedScale(b) = boneSize(b) ⊙ localScale(b)Positional size
Scales the bone's offset from its parent, combined with the parent's own size along Y. That's how Neck length lifts the head and Chest pushes the arms outwards.
derivedPos(b) = derivedPos(p) + orient(p)·(size(p).y × posSize(b) ⊙ localPos(b))The function also folds in two physique factors that grow with the character's stats: muscle (up to ×1.27 on arms and legs) and thinness (down to ×0.4 on breasts). Both are 0 for a new character. An excerpt, all in float32:
hgt = Height/100 fr = Frame/100 leg = Leg length/100
size(Pelvis) = (hips·fr, hgt, hips·fr)
size(Spine2) = (((chest−1)·0.45+1)·fr, hgt, ((chest−1)·0.9+1)·fr)
size(UpperArm) = (ab·fr, hgt, ((ab−1)·1.5+1)·fr) ab = Arm bulk/100 · lerp(thin,1,0.7) · lerp(muscle,1,1.27)
pos(UpperArm) = (Shoulders/100 · s2x, 1, 1) ← the chest pushes the shoulders out
size(Neck) = (Neck width·s60·m13·fr, Neck length, Neck·s70·fr) / 100
size(Head) = (headFr·hs·Head shape/100, hs, headFr·hs) headFr = (fr−1)·0.25+1


6 Posture is an animation frame
Posture, Shoulder set and Neck position aren't scales.
AppearanceHuman::setPosture enables one skeleton animation per slider at weight 1 and
parks it at time = length × value / 100:
| Slider | Clip | Length | Keys | What the keys do |
|---|---|---|---|---|
| Posture | postures | 0.167 s | 6 | Upright → hunchback: bends the spine, neck and shoulders |
| Shoulder set | shoulder set | 0.067 s | 3 | Shoulders back ↔ forward |
| Neck position | neck set | 0.067 s | 3 | Head forward ↔ back |
Each clip has a track on all 30 bones. The key at 0.033 s is the identity. OGRE applies a track by adding its translation and post-multiplying its rotation, with keys interpolated linearly (nlerp). It's a neat trick: artists author a posture range as an animation, and a slider scrubs through it.
7 Skin, head textures and the shader
Everything about the surface happens in one pixel shader, character.hlsl: main_fs:
body = tex(diffuse, uv) + tex(headDiffuse, uv + (0,1)) // head island added on
mask = tex(mask, uv).r + tex(headMask, uv + (0,1)).r // r = skin (eyes and underwear excluded)
body.rgb *= 1 − (1 − skinTone) · mask // skin tinting
body.rgb = lerp(body.rgb, hairColour · hairTex, hairAlpha) // painted hair
body.rgb = lerp(body.rgb, hairColour · beardValue, beardValue) // stubble
glow = headMask.g // Scorchlander eyes
The skin tone is a 1-D ramp. The slider runs from 0 to (stops − 1) × 10, and every 10 units is one
colour stop, linearly interpolated (DataRangeVector::getValue).
Hair colour
Hair colour is HSB, stored as three 0–100 floats. It's shared by the painted hair layer and the hair mesh. The randomiser picks one of the race's weighted presets and converts it from ARGB:
8 Hair and beards
An ATTACHMENT record (slot 2 = hair, 13 = beard) has up to two parts:
A painted layer
head texture goes into the
body shader's hair or beard map, sampled in head UV space. One-hot channel selectors pick which RGBA
channel is the colour and which is the coverage. This gives stubble and shaved heads.
A mesh
Created as its own entity, attached to the body node,
and given shareSkeletonInstanceWith(body). Head, neck and jaw bone sizes move it,
but face morphs don't. A big jaw morph can push through a beard. Material:
alpha = dot(tex, alphaChannel); clip(alpha − rejection).

9 Randomisation and NPC faces
All randomness is MSVCR100's rand(), an LCG (s = s·214013 + 2531011, 15-bit
output). U(−1,1) = rand01()·2 − 1, and every randomised slider gets
round(mid + variation × deviation × U).
| Who | Face shape (morphs) | Everything else |
|---|---|---|
| Player, Randomise button | randomPoses(deviation = 1.0), fresh random state | Head, bone sliders, body, skin within ±40 % of mid, hair |
| Generic NPC | srand(FNV1a(name)) then randomPoses(deviation = faction "faces weirdness") | Per NPC: head texture, hair, beard, colour, head/neck/jaw bones, body. Skin is uniform over the whole ramp. |
NPC with a .bod2 | From the file | From the file |
At spawn a generic NPC gets morph index = int(rand01() × morph num), which is 0–4 for humans.
When its body is built, the game forms a mesh name, and only if no mesh of that name exists yet does it
seed the generator with that name and roll the face:
name = race.stringID + "_" + ⌊weirdness × 10⌋ + "_" + ("Male"|"Female") + morphIndex
e.g. 17-gamedata.quack_5_Male3
srand(FNV1a_32(name)); randomPoses(app, weirdness) → bake → cache under name

_5_Male0…4 and _5_Female0…4. These are the same in every game and every save.
_10_) and the deviation doubles.10 How much variety
| Greenlander, per gender | Options |
|---|---|
| Head textures | 4 |
| Hair styles | 42 |
| Beards (men) | 32 |
| Hair colour | HSB, 6 presets |
| Skin tone | 51 values |
| Face sliders (morph targets) | 23 (17) · F 26 (21) |
| Body sliders | 16 · F 19 |
| Generic NPC face shapes | 5 per weirdness |
Orders of magnitude contributed by each category. Integer sliders dominate: 39 sliders with ranges like −200…200 multiply to about 1078.
11 Quirks and limitations
- Normals ignore morphs. Strong morphs keep the base mesh's lighting.
- Five faces per NPC type. The face pool is tiny and fixed by the name hash. Variety comes from other channels.
- The fixed-appearance mesh cache uses a truncated hash. NPCs with a
.bod2outside the player faction share meshes by a hash of each weight cast to an integer. Typical weights (|w| < 1) all hash to 0, so whichever such NPC is built first can define the face for others in that session. - NPC skin is uniform over the whole ramp, while the editor's Randomise stays within ±40 % of the middle.
- Jaw is a bone slider for men and a morph pair for women.
- Robot races (Skeleton, P4 Unit, Screamer, Soldierbot) reuse
editor_data_human.xml, but their meshes have no poses, so 17 face sliders do nothing. - Wrong asset paths still work. Several HEAD records point at the wrong folders, and OGRE resolves textures by name anyway.
- Grouped morph sliders ignore deviation, although no vanilla human file uses
random_groupon poses.
