1 The whole pipeline
cm_body / cf_body, resets every slider, applies the race grid and the figure.rand_float. Clamped to ≥ 0.0001.ir_sr3pcskinfull: composite × shader ball × inferred-lighting buffer.2 Where the data lives
Everything is in packfiles/pc/cache/*.vpp_pc (version 6 packfiles, zlib-compressed). Tables are
XML (.xtbl), and names are stored as a Volition CRC: the reflected CRC-32 table with init 0 and no
final xor, over the lower-cased string.
| Packfile | Contents |
|---|---|
misc_tables.vpp_pc | All tables: player_creation.xtbl (sliders), presets, skin and hair colours, customization_items.xtbl, customization_compositing.xtbl, NPC definitions and palettes |
customize_item.vpp_pc | The player bodies and all 1151 item meshes, each with its _pc.cmorph_pc morph file and textures |
customize_player.vpp_pc | Composite layers, shader balls, body diffuse, mask and normal maps |
characters.vpp_pc | One container per NPC: body mesh, head morph, head material library |
shaders.vpp_pc | DXBC shaders, decompiled to GLSL for this study |
patch_uncompressed.vpp_pc | The creator UI as plain Lua: store_character.lua, vdo_triangle_select.lua |

3 Sliders, keys and morph weights
player_creation.xtbl holds 110 sliders in 12 sets. A slider doesn't map to one shape. It has
keys, and each key names a morph target and three numbers. FUN_009f65c0 turns slider value
s into a weight per key:
t = clamp((s − Nill) / (Full − Nill), 0, 1)
t2 = clamp((s − Nill) / (Neg − Nill), 0, 1) // only if Neg_Value is present
if t2 > t: t = −t2 // the sign flip at 0x9f6758 (missing from Ghidra's decompile)
w = Morph_Scalar · t
Sliders per set
Applying the weights
For each slider in table order, each key and each record of that key's target, the deltas are accumulated in exactly this form:
pos[v] = f32((double)q / 32767.0 · w · A + pos[v]) // SSE2, per axis
nrm[v] += ((byte/255)·2 − 1) · w · B
The creator keeps old and new values per slider and applies only the difference. The same
weights go to every worn hair and item mesh, because each has its own .cmorph_pc with the
same target CRCs. A target has one block per vertex buffer of its mesh. Getting that wrong is what made
the demo's headset throw a vertex across the screen.
4 Presets, the body triangle and Randomise
The Gender, Race and Figure buttons call pcr_apply_preset, which resets every slider to its
Initial_Value, applies the race grid (e.g. male_asian: asian = 1, body fat = 1)
and the figure, then sets skin, hair and default layers. Race is just another morph target, set to 1.

Randomise
The creator's Randomise is plain Lua: for each face set it calls
pcr_apply_morph_value(set, id, rand_float(0.5 − Rand_Range, 0.5 + Rand_Range)), then picks a random
eye layer and some face features. Every random draw in the game reads from one engine-wide table of 8192
MT19937 outputs, refilled from a clock seed at start-up. So a session can't be replayed, but a given seed
is fully deterministic. The demo uses a seed you type in.
5 Normal maps follow the body
Skin detail (muscle definition, fat folds, age) isn't geometry. player_creation_normal_maps.xtbl
defines weights for Base, Skinny, Fat, Muscle and Age as products of piecewise curves over the body sliders.
The blend pass then sets fat = fat^¼ and base = max(0, 1 − skinny − fat − muscle). It
mixes five race- and gender-specific DXT5nm maps into one render target with the
cust_normal_map_blend pixel shader:
n.xy = Σ −wᵢ · ((2a−1)(2r−1), 2g−1)ᵢ // decode each DXT5nm map
if |n.xy| > 1: normalise; n.z = √(1 − |n.xy|²); out = (n + 1) / 2
For female_black + average: base 0.25, skinny 0.25, fat 0.5 (0.0625¼), muscle 0.
6 Skin and the composite texture
The body's diffuse texture is rebuilt into a 2048×1024 render target whenever the look changes
(FUN_009fa8e0), in two passes:
M_ideal_d.tga and PC_body_standardmask.tga, in body UV spaceLayer order, at its x/y, tinted if Colorizable, with alpha × opacity.
55 skin colours
Each entry is an HSB offset, not a colour. These swatches apply each offset to the median skin texel of the base diffuse, so they're approximate. Lighting and the shader ball change the final look a lot.
Layers
Layers come from customization_compositing.xtbl: 321 selectable entries, with
at most one per slot. Colourable layers take a colour from the 117-entry makeup pool.
Tattoos use the makeup pool too: tattoo_color_pool exists but isn't read on this path.
The skin shader
n = tangent-space normal from the blended Normal_Map (2nd UV set)
uv_sb = view-space n.xy · 0.5 + 0.5
matcap = mix(Sphere_Map_1(uv_sb), Sphere_Map_2(uv_sb), Blend_Map(uv0).r)
out = L_diffuse · matcap · (composite · Diffuse_Color) · 2 + L_specular · Specular_Alpha · Specular_Color

7 Hair
Hair is an ordinary clothing item in the head hair slot (51 styles), with its own morph targets so
it follows the head. Colour isn't a texture tint. ir_sr3pchair builds it per pixel from the Dob map
(B = strand brightness b, G = opacity, R = specular) and four parameters:
sat = sat_dark + (sat_light − sat_dark) · b
val = b · Brightness + bias
rgb = HSV(Hue / 60, sat, val) + specular(bright ↔ dark by b)
35 hair colours. Each swatch is HSV(Hue, Saturation_light, Brightness), the colour of the brightest strand (b = 1) before lighting.
Most hair styles have a second wear option (cm_pulledintohat and similar) whose required flag is
hat active. Put on a cap and the hair switches to a squashed mesh.
8 Clothing and accessories
Items per slot (both genders; 33 are DLC).
Every item has male and female meshes, each carrying the body's morph targets, and up to three dye colours from the 126-colour clothing pool. It also has three mechanisms that let items coexist:
- Obscured slots. A full suit hides the bra, underwear, gloves, hat and so on.
- Obscured VIDs. Long sleeves hide the body's arm submeshes, so skin can't poke through.
- Wear options with flags. An item sets flags (
hat active,hood down,glasses on face…), and other items pick the first option whose required flags are set.
Dyeing: three colours through a pattern mask
P = Pattern_Map(uv1 · (TilingU, TilingV)) // second UV set, often a tiny tiling mask
colour = lerp(lerp(lerp(1, primary, P.b), secondary, P.g), tertiary, P.r) · Diffuse_Map(uv0) · Diffuse_Color


9 NPCs: a different system
Ambient NPCs don't use sliders, presets or the composite. Each character_definitions.xtbl entry is a
hand-made body mesh plus heads. A head is the shared npc_basehead (1426 vertices) plus one
morph target, named after the head, at weight 1. Per spawn (FUN_00950660, in this RNG order):
- Palette:
rand_inta primary colour from the character's palette tree, then a secondary under it, then a tertiary. - Props: for each prop group, roll against its chance, then pick an item and a sub-item. A prop is usually a VID of the NPC's own mesh: hood up or down, hair, an iPod.
- Head:
rand_intover the definition's heads. - Hair tint:
rand_intover the head's list of hair shader balls. Hair is painted into the head texture and coloured through the matcap.


223 spawnable NPC types, from 183 definitions. Which type spawns where (spawn groups, districts, time of day) isn't part of this study. There's no random height: Variation is 0 and Height 1.0 for every shipped NPC.
10 How much variety
| Category | Options |
|---|---|
| Gender × race presets, figures | 8 · 6 per gender |
| Morph sliders | 110, continuous |
| Skin colours | 55 |
| Hair styles × colours | 51 (+ bald) × 35 |
| Eyes · eyebrows · facial hair | 14 · 12 · 27 (× 117 colours) |
| Face features · face items · lips · makeup slots | 25 · 7 · 2 · 11, 8, 6, 9 (× 117 colours, opacity) |
| Tattoos | 199 in 18 areas (opacity) |
| Clothing and accessories | 572 items in 21 slots, 3 dyes each from 126 colours |
| Ambient NPC types | 223 |
The top bar ignores the sliders (≈10208 discrete combinations, matching the demo's Variety tab). The bottom bar adds the sliders at their UI step size. Clothing dyes alone are 126³ ≈ 2 million colourings per item.
11 Known gaps in the re-implementation
- Lighting. The demo replaces the inferred-lighting buffer with a key light, a fill light and ambient. The material maths follows the shaders.
- Eyes, lashes, hair specular are approximated, not ported.
- Clothing normal maps, decals, cloth simulation and glow animations aren't done.
- Two hair textures are drawn opaque. "Off The Shoulder" and
M_PlayerMediumhave a copy of the brightness in the opacity channel. Read literally, they'd be see-through. - Wear-option resolution is my reading of the flags. The game function isn't decompiled yet.
- Handedness. The game is left-handed (Direct3D), so the demo mirrors X so that text on clothing reads correctly.
- Not yet compared side by side with the running game.
