Knowledge base

Rendering every head character-creator option

Research in progress: some of this may be wrong. These notes record what XF Studio’s research has worked out so far about how Cyberpunk 2077 and its modding frameworks behave. Most of it comes from reading source code and game resources offline; not all of it has been confirmed in the running game, and claims graded hypothesis are unproven. If you spot a mistake or know more, please open an issue or suggest an edit.

Evidence grades

source
Read in engine/framework/tool source or decompiled data.
resource
Observed in extracted game or mod resources.
wiki
Documented in the Modding Docs, text or image.
runtime
Observed in the running game.
hypothesis
Not yet established.

Maturity: Draft. Consolidated from the vanilla 2.31 resources (female first, male where cheap), the generic resolver's output for four synthetic creator states and the reference save, one decompiled skin shader, the installed complexion mods of one MO2 profile, and an audit of the Studio preview on 25 September 2026, updated when steps P0 (brows, lashes and hair) and P1 (the head's skin) landed the same day when rank 2 (the face decals) and the piercings landed on 26 September, when the teeth landed on 27 September, and when multi-part hairstyles, hair accessories (the metal adapter) and the mouth interior's occlusion landed the same day; all of them render from the resolver's output (§6). The idle's breaths (PREV-147) were re-measured under the facial setup the preview now uses on 30 September, and the mouth interior's light follows the measured parting since the same day; the whole-creator coverage is scored in the render coverage audit. Runtime evidence is limited to the claims marked runtime (the close-up breakup of a skin-coincident plate and the legacy selectors drawing, §3, and the switcher check, open question 6); everything else is offline. Evidence grades follow the knowledge rules: source, resource, wiki, runtime, hypothesis; [community] marks values taken from a third-party tool. Provenance, hashes and the resolver runs are in the head CC render evidence.

This page answers, for each head option: which resources it resolves to, how the creator's choice selects them, which material template draws them and with which key parameters, whether the Studio preview draws it today, and what a save stores. It ends with a ranked render plan tied to the feature-module platform. The option catalogue itself (counts, links, groups, switchers) is in the CC file chain; shader internals are in materials and shaders and hair shading.

1. Every head option at a glance

"Selector" is how the chosen index reaches resources: S switcher (activates other options), A appearance option (definition name = .app appearance), M morph option, mA mesh appearance, mask chunk mask. Paths abbreviate base\characters\head\player_base_heads\ as …\. "Preview" is the state of projects/xf-studio/authoring/src on 26 September 2026. "Save" names the entry the save's CharacetrCustomization_Appearances node carries: (option, app hash, definition) in the listed groups, or (region, target) for morphs (save format).

OptionSelector → resourcesTemplate and key parametersPreview todaySave carries
Skin type (complexion)S skin_type → A skin_type_0N → …\appearances\head\h0_000__basehead(_d0N).app → h0_000_pwa__basehead.ent → h0_000_pwa__morphs.morphtarget → h0_000_pwa_c__basehead.mesh; mA <tone> / <tone>_d0N picks one of 60 local materials whose Albedo is h0_000_pwa_c__basehead_d0N.xbm (N = type)skin.mt (Subsurface): Albedo, RG Normal h0_001_pwa_c__basehead_n01, Roughness (R base, B detail gate), DetailNormal, MicroDetail atlas, wrinkle maps, TintColor/TintScale/TintColorMask, SkinProfile (template default default.sp) resourceYes (P1): the V's own type from the launch route's winning resources (albedo, normals, roughness, microdetail, secondary albedo, skin profile) through the skin.mt adapter; SSS approximatedskin_type_0N in TPP, TPP_photomode, character_customization; definition h0_000_pwa__basehead__<tone>
Skin toneA skin_color (colour-only controller, link skin color) → the same index on every follower: skin_type_0N, neck, proxies, facial tattoos, face cyberware, body and armsTone is only the .mi chain female_head_<tone>.mi → default_female_head_<tone> → default_head_<tone> → <tone>.mi: TintColor, TintScale, tint mask (§2) resourceYes (P1): the resolved chain's tint, multiply or overlay as the program does it; TintColor sRGB-decoded, as the engine packs every Color parameter [source; runtime-supported]Never stored directly; visible as the tone suffix of every linked definition
Face shape (eyes, nose, mouth, jaw, ears)M eyes, nose, mouth, jaw, ear → (target, region), applied to every morph component that carries that pair: head, eyes/lashes (eyes only), brows, teeth (mouth), all hx_ decals, earrings (ear or nose)–Yes for all five, from a save or any choice in the Character panel(region, target) in TPP, TPP_photomode, character_customization; None is not stored
Eye colourA eyes_color (71) → he_000__basehead.app → component he_000_pwa__basehead (mesh he_000_pwa_c__basehead.mesh, own he_000_pwa__morphs.morphtarget, 21 eyes targets); mA = colour (gradient_brown), mask …551614 hides chunk 0 (lashes)Chunk "eye" is one of three materials: 18 gradient colours on eye_gradient.mt (IrisMask + one of 9 IrisColorGradient profiles over a clear or bloodshot albedo), 16 texture colours on eye.mt, and 37 graphic designs on multilayered.mt (Standard class). Chunk "wetness": eye_shadow.mt via eyeshadow_base.mi for every colour. Refraction, cornea and iris normals, the Eye-class light and the shell are decoded in eye rendering resource sourceYes (eye plan ranks 1–5 and 7): any resolved vanilla or CCXL colour, from a save or the default V. Gradient irises and texture-only eyes follow the game's V-flipped sampling; the iris is refracted under the cornea and lit by the Eye-class light on its two normals, with the eye's own roughness; the wetness shell is a blended pass. Layered designs draw through the layered adapter (rank 7), the default eye only when one can't be drawn (limit eye-design). Heterochromia (rank 6) is open (eye rendering §6.6)eyes_color in TPP, TPP_photomode, character_customization
EyelashesA eyelash_color (35) → hel_000__basehead.app, the same eye component with mask …551609 (chunk 0 only), mA eyelashes__<colour>hair.mt via eyelashes_hq___<colour>.mi → eyelashes__default.mi: HairProfile <colour>.hp, Strand_Alpha hb1_01__lash_single_d, roughness scale 0 resourceYes (P0): the resolved component from a save or the default creator choice, vanilla chunk 0 or CCXL mesh, with its .hp through the decoded hair modeleyelash_color in the three head groups
EyebrowsS eyebrows (13 styles + none) → A eyebrows_colorN (35 colours, link eyebrows color) → …\eyebrows\heb_000__basehead_NN.app → heb_000_pwa__morphs.morphtarget (105 targets); mA <colour>__NNmesh_decal_double_diffuse.mt via eyebrows_grad__<colour>.mi → eyebrows_grad__default.mi: DiffuseTexture heb__base_dNN, SecondaryDiffuseAlpha heb_wa__base_dsNN (intensity 0.6), GradientMap hh_cap_grad__<colour> (the hair cap's file, intensity 2), normal at 0.4 with blending mode 1, roughmetal (≈ 0.50) at surface alpha 1; one mesh for every style, lifted 0.35 mm resource (eyebrows)Yes (P0): any resolved style and colour, vanilla or CCXL, through the double-diffuse adapter with the chunk's own parameters; its normal and roughness writes are not drawn (fixed roughness 0.8)eyebrows_colorN in the three head groups
HairS hairstyle (51; hairstyle_cyberware twin when face cyberware changes the scalp) → A hair_colorN (35, link hairstyle color) + hidden hair_color_fpp_N → …\hairs\hh_NNN_pwa__hairs_XXX.app → hair mesh(es), an *_shadow_npc mesh and a dangle animgraph; mA = colourOne mesh interleaves strand cards hair.mt (_master__long/short.mi: HairProfile, Strand_ID/Gradient/Alpha, Flow, roughness 0.15) and cap chunks mesh_decal_gradientmap_recolor.mt (GradientMap hh_cap_grad__<colour>, MaskTexture) resourceYes (P0): any resolved hairstyle and colour (strands and cap), highest LOD only; no physics. Everything the hairstyle controller draws is hair: every active option in the hairs group whatever its creator slot, so a multi-part hairstyle's parts (the Multicolored Hair framework's mch_hair_part_01…03) draw beside each other, and every drawing component of a hairstyle's appearance draws, its built-in accessories included (a CCXL hairstyle's earrings and hair pin on metal_base.remt, through the metal adapter)hair_colorN in hairs, character_customization; FPP twin in FPP_hairs; the reference save's tags also hold ash_brown and Short
Beard (male)S beard (12 + off) → S beardN (parts) → A beard_colorN_M (35) → …\facial_hairs\hb_000_pma__*.app: a stubble decal hb_000_pma_c__basehead_shadowbase_01.mesh plus a card mesh (chunk 0 hidden)Stubble: mesh_decal (beard_shadow.mi, alpha 0.75, noise secondary mask). Cards: hair.mt via <colour>__beard.mi → _master__beard.mi resourceStubble yes [offline]: the stubble decal takes the face-detail path (the beards group is a face consumer; beard 05, part 2, drawn on the default masculine V). Cards no: they wait for rank 5beard_colorN_M in character_customization, beards
Eye makeupS makeupEyes (36 + off) → A makeupEyes_NN (14 colours) → …\makeup_eyes\hx_000__basehead_makeup_eyes_NN.app; one shared mesh hx_000_pwa_c__basehead_makeup_eyes_01.mesh for every style; mA <colour>_NNmesh_decal.mt via makeup_NN_<colour>.mi → makeup_color__NN_<colour>.mi: DiffuseTexture hx_eyes_makeup_01_dNN (mask), DiffuseColor, DiffuseAlpha 1, SecondaryMask noise_decal_d01 ×30, RoughnessTexture roughmetal at surface alpha 1 resourceYes (rank 2), from a save, through the face-decal family, under the Studio's own authored plate (§3)makeupEyes_NN in character_customization, face
LipstickS makeupLips_type (off/regular/glossy/matte) → S makeupLips(_glossy/_matte) (38) → A style option (1–14 colours) → …\makeup_lips\hx_000__basehead_makeup_lips_NN(_02/_03).app; one shared mesh hx_000_pwa_c__basehead_makeup_lips_01.mesh; mA <colour>_NN(_02/_03)Style-dependent: 36 of the 114 style options use mesh_decal_double_diffuse (styles 05, 07, 17, 25, 27–29, 31, 33, 35, 37, 38 in each finish: mask + second alpha map and colour), the rest mesh_decal. Regular and early glossy styles write no roughness; most glossy styles from 21 on write a white metalness map at surface alpha 0.05, matte styles white roughness at 0.35–0.4 (a few named styles write their own roughness and metal maps) resourceYes (rank 2): both templates through the decal family; a style that writes no surface keeps the skin's roughnessstyle option in character_customization, face
Cheeks (freckles 1–4, blush 5–24)S makeupCheeks → A makeupCheeks_NN → …\makeup_freckles\hx_000__basehead_makeup_{freckles,cheeks}_NN.app; one shared 2-chunk mesh (face, nose) hx_000_pwa_c__basehead_makeup_freckles_01.meshmesh_decal: freckles alpha 0.3–1.0; blush DiffuseAlpha 0.5–4 (mostly 2 or 3) over faint alpha masks. Gold and silver (every style) and style 16 (every colour) write metalness and roughness, silver and styles 15/17 a flat normal: a vanilla metallic blush resource (face makeup §1)Yes (rank 2): both chunks (face, nose)option in character_customization, face
PimplesS makeupPimples → A makeupPimples_0N (6 colours) → …\pimples\hx_000__basehead_pimples_01.app; 2-chunk mesh, mask per stylemesh_decal, alpha 0.5–1, colour per definition resourceYes (rank 2)option in the three head groups
ScarsA scars (none + 13) → hx_000__scars.app; one 11-chunk mesh, mA scars_01, a different chunk mask per scarmesh_decal via face_scar_NN.mi: diffuse (alpha 0.1–0.45), secondary mask, scar normal (alpha 0.2–1, mode 1), roughness at surface alpha 0.02 (scar 3: 0.5) resource. 2.31 has no separate normal-only decal templateYes (rank 2), including the normal writescars in the three head groups
Facial tattoosS facial_tattoo (15 + off) → A facial_tattoo_NN (12 tones, link skin color) → …\tattoos\hx_000__tattoo_NN.app → one mesh per design; mA = tonemesh_decal via customization_tattoos_<tone>.mi: tone sets the ink tint and alpha (senna 216,204,191, dark 119,115,110, both at 0.6); meshes 06–10 override locally with alpha 0.7 resource (tattoos §4)Yes (rank 2), in the saved toneoption in the three head groups
Face cyberwareS cyberware (16 + off; also swaps hairstyle ↔ hairstyle_cyberware) → A cyberware_NN (12 tones) → hx_000__cyberware.app. Designs 1–7 have their own mesh; newer ones are appearances of other decal meshes (03 = freckles mesh cyberware_04; 12 = tattoo-06 mesh + freckles mesh cyberware_11_dark)mesh_decal with full colour, normal, roughness and metalness maps at surface alpha 1; some designs pick light/dark by tone resource. All 16 female designs resolve to mesh_decal onlyYes (rank 2); emissive parts would be reported (decal-template), none in vanillaoption in character_customization, face
PiercingsS piercings (14 + off female, 16 male; its choices name options on the creator slot piercings_color) → A piercings_NN (16 metals) → …\piercings\i0_000__earring_NN.app → up to three i1_ earring components, each with its own chunk mask; mA = metalmultilayered.mt with a per-metal .mlsetup and a per-mesh .mlmask resource; the layers combine front to back (materials §4.6)Yes (26 September): the V's own style and metal from the resolver, every drawing component with its own chunk mask, through the layered adapter (the .mlsetup stack baked once, lit as a standard metal/rough surface); a framework that replaces a style's .app resolves the same way; any creator style and colour can be tried in the viewportoption in character_customization, face
TeethA teeth (5: default, silver, gold, copper, pink) → ht_000__basehead.app → ht_000_pwa__morphs.morphtarget (21 mouth targets) → ht_000_pwa_c__basehead.mesh, one chunk (3,210 vertices, drawn in scene and shadows); mA per definition: teeth_001 (default), teeth_007__silver, teeth_003__gold, teeth_002__cooper, teeth_006__pink (8 appearances; teeth_004__metal, teeth_005__metal_rusty and default, which is metal_base.remt, are unreached) resourceteeth_001: skin.mt via teeth_base.mi: own albedo/normal/roughness (ht_001_ma_teeth_*), tint (61,37,0) × 0.9, tint mask, SkinProfile customisation_teeth.sp. The four finishes: multilayered.mt with GlobalNormal (the teeth normal), ht_000_base__<finish>.mlsetup and the shared ht_000_base__gold.mlmask resourceYes (27 September): the teeth slot, the V's own choice through its template's adapter: the default through the skin adapter with the teeth's own skin profile, the finishes through the layered adapter; it follows the mouth shapes and the idle's jaw, and takes the mouth interior's occlusion (§6, "Teeth"). The unreached default appearance draws through the metal adapter. An appearance that lists the component several times (a morph-additions mod on the reference profile lists it 16 times) draws it onceteeth in the three head groups
Always-on head partsEvery skin-type .app also adds the personal-link decal (hx_000_pwa_c__basehead_personal_slot_dec.mesh, mesh_decal, metal from white.xbm) and a neck seam-fix mesh; neck, proxies and face rigs are hidden linked optionsmesh_decal (metal white at surface alpha 0.5, normal at alpha 1); seam fix metal_base.remt resourcePersonal-link decal yes (rank 2), as a face detail; seam fix no, correctly: both its chunks are MCF_RenderInShadows only (open question 10); face rig via the prepared idlefollows the skin entries

Male differences resource: every pwa path has a pma twin; 20 targets per face region instead of 21; the eye mesh's chunk roles are swapped (male chunk 1 = wetness, chunk 2 = eye; female the reverse), so a renderer must take a chunk's role from its material, never from its index; mesh-appearance names differ in form (01_ca_pale__d05 male, 03_ca_senna_d03 female).

2. Skin: type, tone and the complexion texture set

Type and tone are separate axes resource. The head mesh holds 60 local materials, one per (tone, type). Each overrides only Albedo (the type's d0N) and Normal; everything else, including the tone, is inherited through the four-level tone chain. default_female_head_<tone>.mi holds the shared maps, default_head_<tone>.mi the microdetail scales 20/8 and the tint mask, and <tone>.mi the tint and detail values. So "skin type" swaps one albedo texture, and "skin tone" changes three numbers:

ToneTintColorTintScaleToneTintColorTintScale
01_ca_pale171,155,150003_ca_senna_01_honey181,141,290.67
01_ca_pale_00_warm_ivory255,245,181−0.1503_ca_senna_02_band17,62,110.57
02_ca_limestone131,149,830.3804_ca_almond189,180,1731.0
02_ca_limestone_00_beige172,166,310.5804_ca_almond_00_umber160,113,700.716
03_ca_senna202,177,1530.7005_bl_espresso117,98,960.80
03_ca_senna_00_amber199,116,1120.5206_bl_dark63,51,390.845

What the shader does with them [source: skin G-buffer program 12806642364631437234, decompiled; see the evidence]:

StepArithmetic
Tint weightw = abs(TintScale) · TintColorMask.R
TintTintScale ≥ 0: multiply, saturate(TintColor · albedo); TintScale < 0: overlay blend with TintColor (so warm ivory brightens rather than darkens)
Base colourlerp(albedo + cavity, tint, w); cavity darkening comes from Roughness.B × CavityIntensity
Blood flowBloodflow-masked blend toward BloodColor, driven by two per-vertex values cubed (an animation-driven flush) [hypothesis for the driver]
Secondary albedoComposited over the result by SecondaryAlbedoInfluence · SecondaryAlbedo.A, tinted by w · SecondaryAlbedoTintColorInfluence (freckle and tattoo frameworks use this)
MicrodetailMicroDetail is a 2:1 atlas of two RG normal tiles; TintColorMask.B (5 steps) raises both UV frequencies, TintColorMask.G (6 steps) blends between the tiles, Roughness.B · MicroDetailInfluence weights the result. The wiki's microdetail image shows the same two-tile atlas [wiki: hair-and-skin-material-properties.md, skin_shader_microdetail_scale.png]
OutputGBuffer0 = sqrt(colour); roughness and the RG normal as in materials §4.1

TintColorMask is therefore three masks in one: R = where the tone applies, G and B = microdetail region selectors. The tint masks are gamma textures: the base game's h0_000_base__tintcolormask.xbm and the texture framework's replacement both have isGamma set, so the engine's sampler sRGB-decodes them (and the G/B selector steps) before the arithmetic above resource. TintColor reaches the program sRGB-decoded (RGB through the engine's sRGB-to-linear table, like every Color parameter; materials §1.2) source; the tone table above is in bytes. Session 4's tone frames agree [runtime, provisional: Ultra+ on, display-referred; test ask 8]. The encoding sets every tone's strength: at senna's 0.70 a mid-grey albedo keeps 71/60/52 % per channel, where a byte/255 reading would keep 85/78/72 %. Warm ivory's overlay also lowers blue slightly under sRGB decoding (its decoded blue, 0.46, is below the overlay's neutral 0.5), where byte/255 would raise it.

Where the tone does not reach resource. TintScale 0 (pale) leaves the albedo untouched, so type and tone only look different through the albedo. With a complexion replacer that ships one fair albedo per type (the reference profile's), every tone therefore starts from fair skin; senna, for example, darkens it only by its multiply.

Global render options also shape skin [community]. The engine exposes runtime GameOptions for characters: Editor/Characters/Skin (SkinAmbientIntensity_Factor 0.4, SkinAmbientMix_Factor 1.0, SubsurfaceSpecularTintWeight 0.3 with tint 0.21/0.26/0.29), Editor/Characters/RimEnhancement (GlobalCharacterFresnel 3.0, LightBlockerInfluence 0.7), Editor/Characters/Eyes (DiffuseBoost 0.1), plus feature toggles for character SSS, rim enhancement and contact shadows. Values are the "Vanilla" preset of Arkhe's Character Rendering Editor; the names exist in the 2.31 executable. CET scripts can change them at runtime, and the reference profile runs such a script, so a capture must record its state. SubsurfaceSpecularTint and its weight feed the SSS combine's tinted term, and the SubsurfaceScatteringQuality graphics setting picks the kernel's sample count (skin reference §6.3.3, §6.3.5) source. How the other skin, rim and eye options enter the lighting programs is not decoded; register pairings are proposed hypothesis (open question 1). The hair options are read from the executable (hair shading §5).

3. The head decal family

Everything drawn on the skin, brows included, is a morph-skinned decal mesh over the head with a post_gbuffer decal template resource. They share the head's topology region, carry all 105 face targets, and draw in the decal stage after the skin (materials §2.4).

What the renderer needsEvidence
One geometry path: export each resolved morph-target component to a bound GLB with its face targets, apply the saved (target, region) pairs, hide chunks by maskMakeup, lips, cheeks, pimples, scars, tattoos, cyberware, stubble and brows all use entMorphTargetSkinnedMeshComponent resource
Shared meshes, many looks: the eye-makeup, lip and freckle meshes each serve every style through mesh appearances; newer cyberware designs are extra appearances on the freckle and tattoo meshesresource; the head cheat sheet's warning that 2.2 cyberware appearances are missing from the meshes does not hold for the 2.31 player meshes
Four decal templates: mesh_decal (makeup, cheeks, pimples, scars, tattoos, cyberware, stubble, personal link), mesh_decal_double_diffuse (brows, many lip styles), mesh_decal_gradientmap_recolor (hair caps), and eye_shadow (the eye's wetness shell, forward pass)resource
Per-target alphas matter: DiffuseAlpha above 1 (blush 0.5–4), RoughnessMetalnessAlpha 0 on most makeup (roughness stays skin), 1 on eye makeup and cyberware, 0.4 on matte lipsresource; coverage curve and sqrt-space blend source
Decals sit 0.40 mm above the skin: the female eye-makeup, lip, freckle and pimple meshes are the head's vertices pushed 0.40 mm out along the head's normals, one LOD like the head, and their morph targets keep the offset along each target's normal; the brows sit lower, at 0.35 mm (eyebrows §2). They do not rely on the decal pass's depth bias; a coincident plate broke up close up in gameresource runtime; experiment 017
Eye makeup's surface write: roughness 0.50 (roughmetal.xbm) at RoughnessMetalnessAlpha 1, with coverage broken by noise_decal_d01 at ×30 UV and influence 1; bare lid skin reads about 0.6resource
Draw order between decals: materialPriority first (EMP_Normal 0, EMP_Front 1; every vanilla face decal is EMP_Normal, the legacy XF generator shipped an EMP_Front copy); the order within a priority is unknownEnum [source: WolvenKit RED4 EMaterialPriority]; values resource; ordering within a priority hypothesis
Template identity is the template's own name: the engine links a template to its compiled programs by name, and a copy of mesh_decal.mt at another path keeps name = mesh_decal (the legacy XF selectors' mesh_decal__emp_front.mt differs from vanilla only in materialPriority). Such copies rendered in game, so a renderer must choose its adapter by that name, not by the depot pathsource (§3.1 of materials); resource the two templates compared field by field; runtime the legacy mod drew in game on the reference character (no capture on file)
Vanilla face templates: the 222 female face choices (eye makeup, 114 lip style options, cheeks, pimples, scars, tattoos, cyberware) use only mesh_decal (224 chunks) and mesh_decal_double_diffuse (36 lip chunks)resource

The player heads have unwelded seams resource measured. Around the eyes the 2.31 heads store some vertices twice at one position: 16 such pairs in the female head's eye-plate region, 23 in the male head's. Each pair has identical normal, tangent, skin indices and weights and identical position, normal and tangent deltas in every morph target, so the two copies move together under skinning and every face shape; the triangles on either side use different copies, which leaves a zero-width slit in the index buffer. A cut of the head therefore shows extra boundary loops (and a pinched vertex where two slits meet) that are not holes in the surface. XF Eye Artistry's masculine plate declares them and its verifier welds only such identical copies before judging the plate's shape (male V plan §4.1).

Vanilla lip finishes are mostly colour resource. For style 05, the regular and glossy finishes differ only in colours and alpha, and neither writes roughness (template default surface alpha 0), so the lips keep the skin's own roughness. Only matte writes roughness (white at 0.4 coverage). The reference save's glossy style 08 also writes none. The Studio's own Glossy finish therefore already does more than vanilla glossy lipstick; this is a point of comparison, not a defect.

How the preview draws the face decals (rank 2) [source for the arithmetic; approximation for the forward pass]. Selection, record and renderer:

  • Selection is structural: every head choice in a third-person face group (TPP, face, beards) whose option no other detail claims (brows, lashes, hair, eyes and skin keep their creator slots), drawn by the decal-family chunks of its components; the skin type's own decal parts (the personal-link port) join it. Piercings (layered), the teeth (their own slot) and the face rig are not decals and stay out; a choice with no appearance in the installation draws nothing. CCXL makeup options qualify the same way whatever their slot: the reference save's three legacy XF selectors, whose material derives from a copied mesh_decal template at front priority, resolve and draw.
  • Record (xfs/render-detail-4): the face slot, components in draw order, and per chunk the template's own name and priority beside its scalars, colours and raw textures. The served GLB holds only the drawn chunks, with sparse facial deltas (a scar mesh exports all eleven scars with dense deltas for 105 targets, 45.6 MB, for one 273-vertex chunk); see §6.
  • Material: one family (face-decal-material.ts) with the decompiled mesh_decal and double-diffuse arithmetic: transformed UV, contrast-adjusted squared coverage with the secondary mask, three target alphas clamped at 1 (so a blush DiffuseAlpha above 1 saturates), roughness and metalness as R × scale + bias, the RG normal with mode 1's fade of flat texels. The gradient-recolour member uses the blendable variant's colour and linear coverage without its Fresnel term [hypothesis for the plain template]. Color parameters are sRGB-decoded, as the engine packs them source (materials open question 11, answered).
  • Forward approximation: the decal is drawn over the skin as one blended pass lit by the skin's own light (profile lobes and subsurface wrap; metal above 0.1 skips the wrap as the engine skips SSS). The skin's colour, effective roughness and metalness (its Roughness map's G) under each decal vertex (read on the drawn head) let the pass solve the "over" colour that reproduces the square-root-space blend exactly per channel, raise its alpha to the surface or normal write where those exceed the colour, and keep the skin's roughness and metalness where the decal writes none. The solve is in linear light, and both lighting presets blend in the display's scene-linear target (src/linear-display.ts), so it holds under both; a GPU without a renderable half-float buffer draws the Studio stage straight to the canvas, where it is approximate. Limits: the skin under a decal is known per vertex, overlapping decals each blend against the skin rather than the decal below, and a surface-only or normal-only write softens the fine skin texture by its coverage.
  • Order: opaque skin, then the face decals by priority and then by the creator resource's option order (merged CCO, so CCXL options follow vanilla; the fallback for the unknown order), then the Studio's authored plate (the look being authored is always on top), the eye's wetness shell, brows and lashes. The decal meshes carry their own lift, so no depth offset is added: 0.40–0.41 mm from the drawn head's vertices for the eye-makeup, lip, freckle, pimple and cyberware meshes, 0.52 mm for the personal link, 0.31 mm for the scar mesh and about 0.2 mm for the facial tattoos (evidence, tattoos §2).
  • Limits by code: a decal template the family does not draw yet (the emissive, parallax and other 2.31 family members) is recorded, hidden and reported as decal-template; the presentation words it.

How the preview draws the Studio's authored plate [source for the arithmetic; approximation for the forward pass]. The export merges a preset's exportable layers into one mesh_decal texture: square-root colour premultiplied by coverage, in layer order, with √coverage in the alpha so the template's squared coverage is the merged coverage again (preset-compiler.ts). The game then blends that one decal over the skin in the G-buffer (colour in square-root space, roughness and metalness linearly at that coverage, a faceted preset's normal in mode 1), and the deferred light shades the blended pixel once, as skin (materials §2.3–2.4). The preview draws exactly that decal and lights it once (src/engines/layered-makeup/render/plate-blend.ts):

  • Which layers. The export's own plan (planPresetExport) over the drawn layers picks the layers the one decal carries and its route (flat, faceted, or Fresnel for a one-pigment Colour-shifting preset).
  • Composite (src/engines/layered-makeup/render/plate-composite.ts). Small UV-space passes per change draw those layers, in order, as premultiplied "over" into an accumulation target over the plate's UV rectangle at the masks' texel density (half-float where the GPU can render it): √colour, roughness, metalness, the Shimmer facet normal, and coverage. The per-layer preview facet maps carry the mode-1 fade already, so the pass undoes it (to within their 8-bit steps) and the plate fades once. A resolve pass then writes the merged decal as the export stores it: the merged normal per texel (rounded to the export's bytes, flat where nothing covers) and its squared length, and the GPU builds that target's mips once (box means, which are the export's: coverage-weighted colour, roughness and metalness, plain means of the normal and its moment). Last, one draw per level writes the export's faceted roughness chain (facetedMipChain): level 0 unwidened, lower levels widened by the facet variance their averaging lost, (r⁴ + variance)^¼. Nothing runs per frame while idle; the skin is read once per head or skin change, when a layer first needs it.
  • One lit plate. One plate mesh reads the composite and the skin under each plate vertex (colour, roughness and metalness: the face decals' underlay, read on the drawn head) and forms the blended G-buffer surface G: colour in square-root space, roughness and metalness at the coverage, the facet normal at √coverage × saturate(50 − 50z), and the decal's roughness sampled from the export's roughness chain, as the game samples the exported map (so the widening follows the mip level and never appears at level 0, where the game has none). Colour-shifting adds its Fresnel tint to the decal colour before the square root. The plate lights G with the skin's own light when a resolved skin is drawn (the profile's two lobes and the subsurface stand-in, which the blended metalness above 0.1 switches off, as the engine's SSS combine skips such pixels source); without a resolved skin it uses the standard light, as the face decals do. It lights the skin under the vertex, S, the same way and writes Y at alpha A with A·Y + (1 − A)·L(S) = L(G), so what shows is the blended surface's light plus the skin's own texel detail at 1 − A. A is the colour solve's alpha (the face decals' forwardDecal), raised only where Y would go negative.
  • Why once, not per layer. Light is not linear in roughness or metalness. Layers lit separately and mixed afterwards show two highlights where the G-buffer holds one roughness in between, and they cannot place the SSS switch on the blended metalness. The earlier preview drew one plate per layer with the standard light.
  • Measured on a real GPU (tests/webgl-display.test.ts, headless Chrome, ANGLE D3D11): Board 5's black Matte steps leave 0.562, 0.249 and 0.064 of the skin, against (1 − a)² = 0.561, 0.248 and 0.063; plum at 60 % under pale gold at 50 % lands within 0.2 % of the export's merged decal over the same skin. Under the skin's light with a key light and the room environment, the plate matches the blended surface drawn as one opaque skin-lit quad within 0.2 % for stacked Matte and Satin, Glossy, Metallic at 12 % and 30 % coverage (blended metalness 0.08 and 0.20, either side of the SSS switch) and black Matte at 50 %. For a surface as rough as the skin, the plate and the face decals' pass give the same pixel; for Glossy at 36 % coverage the face decals' per-pass light is 3–5 % darker than the lit blend, and the plate is within 0.1 % of it. The composite's faceted chain (tests/webgl-plate-composite.test.ts, Shimmer at 30–100 % over Glossy at 50 % on a 16-texel grid) equals facetedMipChain of the compiler's bytes for the same texels at levels 0 and 2 within 0.6 of a byte step for roughness and coverage and 1.1 for the normal (8-bit targets: within 2); at the texel with Shimmer 50 % over Glossy 50 % the export's roughness is 0.353 and the composite's 0.354, where the earlier composite, which kept each layer's facet moment, widened it to 0.444 at level 0.
  • In the app (experiment 016's Board 1, default V on the reference profile, tools/plate-light-look.ts, private captures): under the skin light, Metallic turns from a dim tinted stripe (median luminance 0.37–0.41 of the bare skin at the same pixels) into a bright pale sheet as bright as the skin (0.89–1.12) with 2.7–4.7 times its highlight; Glossy's highlight rises from 0.5–2.3 to 1.1–4.9 times the skin's (most under the creator rig); Satin's is about the skin's (1.0–1.2); Matte stays at or below the skin's. What this predicts for the in-game Board 1 reading is under materials §6.
  • Limits. The skin is known per plate vertex (its texel colour detail passes through at 1 − A, exact for black), and under the covered part the skin's own normal detail gives way to the plate's geometric normal. The V's face decals below the plate are not part of what it blends over. Colour and metalness are filtered premultiplied, so between texel centres at coverage edges they weigh by coverage where the game filters the stored colour. Preview-only models (Glitter, the earlier Glossy clear coat and thin-film study) and Colour-shifting layers the export leaves out of a mixed preset keep their own plates with the ordinary linear blend and standard light; one that sits between exported layers draws above all of them. Without a renderable half-float buffer the composite is 8-bit (its roughness chain within 2 byte steps of the export's) and the studio stage's blend is approximate, as for the face decals, and without the skin under the plate every layer falls back to its own plate and a linear blend.
  • Open. The face decals still light each pass with a surface interpolated between the skin's and the decal's; they could take the same residual light, which matters only where a decal writes a roughness or metalness different from the skin (vanilla eye makeup's 0.50 and matte lipstick's 0.4, for example).

4. Eyes, lashes, brows, hair and beard

  • Eyes: one mesh, three chunks: lashes (hair.mt, used only by the lash option), eye (eye_gradient.mt, eye.mt or multilayered.mt), wetness shell (eye_shadow.mt) resource. The eye's G-buffer program projects the iris from a refracted view ray onto a plane inside the eye, writes a cornea normal and a second iris normal, and the Eye class lights them with its own Lambert-plus-GGX model; the shell darkens the eye towards the lids and adds the tear line in a forward pass. All of it, with the mod-eye chain and the preview spec, is on eye rendering.
  • Lashes, brows, hair, beard cards: hair.mt plus .hp profiles and mesh_decal_double_diffuse / mesh_decal_gradientmap_recolor for brows and caps. The colour model is decoded in hair shading and drives the preview for any resolved choice since P0: the planner selects components by the creator slot (eyebrows_color, eyelash_color, shared by vanilla and CCXL options resource) and the third-person consumer groups (TPP, hairs), and each chunk is drawn by the adapter for its template. Hair is chosen by its consumer alone: every active head option the hairstyle controller's group (hairs) lists, on hair_color or on a slot of its own (a multi-part hairstyle's parts), so no list of slot names is needed; the hair label then names each part by its own creator label ("hair base: brown liquorice, face frame: ash brown"). Each hair mesh repeats its parts per level of detail (render-chunk lodMask); the preview draws the highest level only resource.
  • Shadow-only parts: each hairstyle .app adds a *_shadow_npc mesh, and CCXL hairs add their own shadow component. The render blob says which chunks draw: each render chunk's renderMask (EMeshChunkFlags) has MCF_RenderInScene for a drawn chunk, and every vanilla hair *_shadow mesh (like a body's seam-fix proxy and some mantis-blade parts) has MCF_RenderInShadows alone [resource: all 955 chunks in the reference installation's resolver cache carry the field]. The preview plans only in-scene chunks, on every slot; isShadowMesh is not used (the visible beard-stubble mesh sets it too). A CCXL hair's hair_shadow mesh (glass.mt and multilayered.mt) is two one-triangle chunks marked in-scene but with lodMask 0, in no level of detail; the preview reads an explicit 0 as never drawn and keeps the hair's other parts [resource: the values; hypothesis: that the engine skips a chunk with lodMask 0]. The only other chunks with lodMask 0 are small morph additions in one framework archive.

5. How skin and complexion mods take effect

A complexion mod changes what the head looks like through two file mechanisms and, sometimes, a runtime one. The installed example in the reference profile uses all three resource (scan):

MechanismWhat it changesWhat the generic resolver must follow
Same-path replacement (legacy archive override)A mod archive ships the vanilla depot path: the five female head albedos …\h0_000_pwa_c__basehead_d01…d05.xbm, the head normal h0_001_pwa_c__basehead_n01.xbm, and global resources used by every skin.mt user: microdetail_n.xbm and the template-default skin profile engine\materials\defaults\default.spR1 archive precedence for every resource the material chain reaches, including textures and profiles, not just .app/.mesh; texture export must read the winning archive. Tone .mi files were untouched, so tones keep vanilla tints over replaced albedos
ArchiveXL mesh patchA texture framework's .xl patches the head mesh with a donor mesh whose appearances replace vanilla ones; its local materials become the material source, add a SecondaryAlbedo overlay, an emissive mask and a tint mask, and point Roughness and DetailNormal at new paths that only the donor referencesR3/R10: resource.patch of meshes, patch-mesh materials as the source, and resolving the new paths through the same mount set. Already implemented and shown on the reference save (validation)
Runtime render optionsA CET script sets the character GameOptions of §2Not in any file. Record as capture context; offer the vanilla values as the preview default

Consequences: no per-mod code is needed, but the preview must (1) take the head material from the resolver's effective chain, (2) decode textures without assuming vanilla resolution (the replacement albedos are 4096² against 1024² base, per the saved-skin trace), and (3) treat global replacements (default.sp, microdetail) as affecting every skin.mt part that does not set its own value (the teeth set their own skin profile, so only the microdetail reaches them). Male heads are untouched by this particular package, which is female-only. Since P1 the preview does all three: on the reference profile the default V, the reference save (type 5, warm ivory) and a new-game save (type 3, senna) all resolve the replaced albedo, normal and microdetail, the replaced default.sp (lobeMix 0.6, blurSize 2.5, falloff 255/155/119 against the base game's 1, 1.4 and 255/178/165), and the framework's donor material with its secondary albedo, tint mask and new roughness and detail-normal paths. The texture framework's emissive mask carries EmissiveEV 2 but is black in this install, so nothing would glow resource; the preview has no emissive path yet and says so only when a mask would light up.

6. Render plan, ranked by visual gain per effort

Effort is relative: S about a day of agent work, M a few days, L a week or more. All items plug into the platform design: the host resolves (CharacterContextService + resolver host), a DetailLoader loads each drawing component, and a MaterialAdapter per template turns resolved parameters into a Three material (platform §5).

P0 (prerequisite): done 25 September 2026 for brows, lashes and hair. The render record has a version 2 (xfs/render-detail-2, src/render-detail.ts): the character record carries, per resolved drawing component, its geometry (the resource whose blob the game draws, a patch or copy source when ArchiveXL supplies it, exported to GLB by the generic exporter and cached by depot hash and the winning archive's fingerprint), its visible highest-LOD chunks, and per chunk {template, scalars, colours, textures, profiles}: effective values from the instance chain, then the template's own defaults (for example Strand_ID = grey.xbm on the vanilla lashes resource), and each texture as its raw decoded channels plus the resource's isGamma. The exporter never interprets channels; the renderer's material adapters do (src/character-material-adapters.ts). Version 1 stays the core head. Both hosts resolve the shown V (the loaded save, or the default female creator state) against the launch route Build uses and serve the content-addressed record (src/character-detail-*.ts); loading another save replaces the details completely. Grades: selection by slot and group resource; highest-LOD choice resource; template defaults resource; archive precedence as in R1 [source, native order unread]; the rendered look [hypothesis until an in-game comparison]. Checked in the browser with the reference save, a new-game save and the default V; a cold first preparation takes about 2–3 minutes (WolvenKit reads the creator resources), later ones seconds. The two resolver defects this study found are fixed: a switcher's default target no longer stays active beside another choice, and a None (Off) definition emits nothing (evidence). Still on the old path after P0: the core head, skin and eyes (base-game files, PREV-03) and piercings (their manifests, UI-02).

P1: done 25 September 2026 for the head and its skin. The character record gained a skin slot: the skin-type option (uiSlot skin_type, shared by vanilla and CCXL options resource) brings the head morph component, whose one visible chunk is the skin.mt material of the chosen (tone, type), including an ArchiveXL appearance patch's donor material when one applies. The record carries its geometry (the winning morph target, exported with its facial shapes) and per chunk the scalars, colours, raw textures and a new skinProfiles map with the resolved CSkinProfile values (roughness0/1, lobeMix, blurSize, diffuse and falloff colours). The adapter (src/skin-material.ts) reproduces the decompiled G-buffer program's surface arithmetic: RG normals with reconstructed Z, detail-normal and microdetail blends, the tint-mask selectors for the two-tile microdetail atlas, the cavity term, the tone tint (multiply or overlay), the secondary albedo, the roughness detail bias, and Roughness.G as metalness source. Every input is sampled through its resource's own isGamma flag, as the engine's sampler does. Lighting is an approximation: two GGX lobes at roughness × roughness0 and × roughness1, weighted (1 + lobeMix)/2, over the engine's fixed F0 of 0.04 source; renormalised Burley diffuse source; and, standing in for the screen-space subsurface blur, a per-channel wrap of the diffuse term from the profile's falloff colour and blur size [hypothesis; chosen by eye, not decoded]. Not drawn: the wrinkle maps and blood flow (animation-driven, neutral at rest) and the emissive mask. The renderer compares the resolved head with the core head (positions, UVs, triangles and every facial morph, over all of the skin component's chunks; src/head-skin-placement.ts). When a single chunk matches, the skin is drawn on the core head, which keeps the eye plate and the idle binding. Otherwise the resolved chunks are drawn and the core head is hidden, so the two never overlap; when the shape differs, the renderer reports a head-shape limit and the app says in one plain line that the eye-makeup area still follows the original shape. Decals over the skin (brows) blend against the resolved skin's toned colour, read on whichever head is drawn and only at the texels under the decal's vertices. Loading another save replaces the skin with the rest of the character; the core head returns to its fixed default in between. Grades: selection resource; surface arithmetic source; tint encoding [source; runtime-supported, provisional] (sRGB-decoded since 29 September; byte/255 until then); lighting and SSS stand-in hypothesis; the rendered look [hypothesis until an in-game comparison]. Checked in the browser (?verify=1) on the reference MO2 profile with the default V (pale, type 1), the reference save (warm ivory, type 5) and a new-game save (senna, type 3): all three resolve the complexion replacer's albedos, the replaced skin profile and the framework's donor material; the launch route's head (its morph target comes from a geometry-neutral fix mod) is the same surface as the core head, so the skin is drawn on the core head; switching A → B → A restores A's exact record. A direct-route (base game only) default V resolves the vanilla chain (1024² albedo, base-game default.sp). Qualitatively, all three read as fair skin (the replacer's albedos are fair) with visible pores and microdetail; B is warmer and freckled; no in-game comparison yet. The skin was checked under both lighting presets: the Studio stage (with its image-based light, taken through the two lobes) and the Character creator preset (spot rig only, no environment, so only the direct-light path draws), in light and dark app themes. Still on the old path: the eye plate cut (base-game files). Piercings moved to the resolver on 26 September.

Rank 2: done 26 September 2026 for the face decals. The V's own eye makeup, lipstick, cheeks and freckles, blemishes, scars, facial tattoos, face cyberware and personal-link port render from the resolver's output through one decal family (§3, "How the preview draws the face decals"). Grades: selection and template identity resource; arithmetic source; forward composite and lighting [approximation]; Color encoding source; draw order within a priority hypothesis; the rendered look [hypothesis until an in-game comparison]. Checked in the browser (?verify=1) on the reference MO2 profile: the default V shows only the personal-link port; the reference save shows red glossy lipstick at 0.4 (keeping the skin's gloss), red blush over cheeks and nose tip, and the three legacy XF eye-makeup layers (dark liner and lid at front priority, a teal crease) under the Studio's authored plate; save B shows light-brown freckles over nose and cheeks and brown blemishes; a test V built in memory from save B adds eye makeup 05 black, scar 3, tattoo 2 and face cyberware 3. Switching default → reference → B → reference restores the reference record byte for byte. Checked under both lighting presets and both app themes. Still open: male stubble (male V), the order between same-priority decals (test ask 3), and a visibility toggle for the V's own scars, tattoos and cyberware (the Character panel's Hide my V's own makeup already turns every makeup row Off). Served geometry (26 September): the host serves each component's GLB with only its drawn chunks and sparse facial deltas, so a V with a scar, a tattoo and face cyberware fetches 72.9 MiB instead of 151.2 MiB of the loader's 256 MiB budget (face slot 13.1 instead of 71.3 MiB), and renders the same (creator-preset captures identical to before). The Studio stage now blends the decals in the same scene-linear target as the creator preset, so their colours agree between presets; the reference save's blush and lipstick read lighter than before under the Studio stage, which had over-darkened them.

Piercings: done 26 September 2026. The record moved to xfs/render-detail-5 with a piercings slot, selected like the other slots by the creator slot (piercings_color) and consumed by the face groups (TPP, face, beards). Each drawing component of the resolved appearance is recorded with its chunk mask's visible chunks, and each multilayered.mt chunk with its layer stack: the winning .mlsetup and each layer's .mltemplate read through the resolver (values from the template's own tables by the layer's names), their colour, normal, roughness, metalness and microblend maps, and the .mlmask exported as one raw image per mask layer. The layered adapter bakes the stack once per material in WebGL 2, as the decompiled program combines it (front-to-back coverage shares, scale/bias levels, colour mask, microblend contrast and edge normals, reoriented normal over GlobalNormal), and lights the baked maps as a standard metal/rough surface. A framework that replaces a style's .app at its vanilla path works by archive precedence alone: on the reference profile, style 12 shows the framework's kept vanilla part, a nose stud and two nostril rings filled from item archives, all changing metal together (the stud's second chunk stays silver through its own material). A part whose exported mesh has no skin (one ring is a rigid linked mesh) is bound whole to one bone and stays where the export placed it, and the slot carries the limit rigid-part: the component's parentTransform and skinning bindings name the entity's root animated component, not a head bone, in every vanilla and framework piercing .app resource, so the data gives no bone to follow; how the engine moves such a part during the idle is a hypothesis for an in-game check. Any creator choice the viewer makes in the Character panel (the character context: a piercing style is the Piercings row) is sent with the V and derived by the host with the whole creator state through rule R5, so every target of a switcher choice and each linked follower's colour follow the creator, while the save's own descriptors stay for everything the choice doesn't change. The host re-plans a change from what it already resolved (only what changed is resolved and exported; a cached change takes 0.1–0.2 s end to end on the reference profile), and the browser keeps the V on screen and swaps in only the parts that changed. The PRC-specific intake, manifests, aggregation and UI notes are gone. Grades: selection and chunk masks resource; layer arithmetic source, with the colour-mask and contrast mappings hypothesis; lighting [approximation]; the rendered look [hypothesis until an in-game comparison]. Checked in the browser (?verify=1) on the reference MO2 profile: the default V and the reference save have no piercings; a new-game save shows style 09 black (brow and lip studs, ear bars and a lobe spinner in glossy black plastic) under both lighting presets; trying style 12 gold on it shows the framework's parts; switching reference → save → reference restores the reference record exactly and leaves no piercing behind.

Teeth: done 27 September 2026. The record moved to xfs/render-detail-11 with a teeth slot: the choice on the creator slot teeth, consumed by the third-person head (TPP), drawn from its own component (a morph target over the teeth mesh, so it follows the mouth shapes, and skinned to the head's bones, so the idle's jaw moves it). Nothing about the slot is teeth-specific apart from its label: the chunk's template picks the adapter as everywhere else. The default choice's teeth_001 is skin.mt, so the skin adapter draws it with the teeth's own customisation_teeth.sp; the four finishes are multilayered.mt and draw through the layered adapter. The teeth never stand in for the head's skin: a lip decal reads the skin under it on the head, never on the teeth. An appearance that lists one component name several times draws its first drawable one, and identical parts draw once, as the body's do [hypothesis: one component per name in an entity]. On the reference profile a morph-additions mod's teeth .app lists ht_000_pwa__basehead 16 times, once over the vanilla morph target and 15 times over hash-only copies whose chunk carries LOD mask 0, so the highest-LOD rule already leaves those out resource. The unreached default appearance (metal_base.remt) draws through the metal adapter; a chunk with no adapter would be left out and said in one plain line. Checked in the browser (?verify=1) on the reference save: the slot shows "natural", the upper teeth appear when the idle parts the lips, and nothing shows through the closed lips in the bind pose. Mouth occlusion (27 September). The teeth first read bright and forward, as if clenched, whenever the idle parted the lips. Three causes were checked against the game's data:

  • The jaw is not it [resource; source]. The lower teeth and tongue are skinned to mid_J_jaw_JNT (the upper teeth to Head), the idle's face clip drives that joint, and in the browser every teeth bone is bound to the idle (none unmapped). But the idle opens the jaw by at most 1.45° (about 2.4 mm at the incisors), while it turns the lip joints (*_J_mug_lip_*) by up to 11°, so the lips part over a nearly closed bite in the game's own clip.
  • The placement is not it resource. At rest the teeth sit where the game's meshes put them: the upper incisors about 13 mm behind the lip surface on the midline, with a closed bite (the upper and lower incisors overlap vertically).
  • The light is [hypothesis for the game's side]. The preview drew no shadows and no ambient occlusion, so the teeth were lit as if outside the head. In game, whatever light reaches them comes through the parting: shadows, screen-space occlusion, contact shadows and the subsurface blur across the lips darken the interior (none of those passes is decoded).

The teeth slot's chunks take a stand-in whatever their template (src/mouth-occlusion.ts): each vertex keeps the share of the outside it would see through the lips' parting from its depth behind the drawn head (the frontmost head surface straight in front of it: (h/2)/√((h/2)² + d²)), never less than a floor, on every light term (direct and ambient, diffuse and specular). Since 30 September h is the lips' real aperture each frame (PREV-147 step A): the depth is baked per vertex, the renderer measures the parting on the posed head's lip joints every drawn frame (src/mouth-aperture.ts, platform/scene/mouth-aperture-sampler.ts) and the interior's program reads it from one scene uniform, with a floor of 0.05. The lips closed keep only the floor; the idle's breaths (2.75–2.91 mm) give the front teeth about 0.11; the teeth page's 15 mm about 0.5; a wide smile more [offline arithmetic]. The joints are chosen from the facial setup's own data, never by name: its JointRegions (mouth 2, jaw 3), the jaw's ancestry (the lower lip's joints ride on the jaw) and the rest heights near the midline; on the player face that picks [lr]_J_mug_lip_up_0_JNT and [lr]_J_mug_lip_dn_0_JNT in the Head joint's frame, and reproduces experiment 034's by-name breaths to 0.01 mm [offline] (experiment 034 §3). The host's face records carry the regions for it (FaceMotionRest.regions). Where they can't be read (a developer's prepared idle), the stand-in keeps its earlier fixed 10 mm parting. The teeth's own tint mask (teeth_base.mi: (61, 37, 0) × 0.9, masked) already darkens the gums and tooth edges as the game does. Grades: selection and templates resource; jaw and placement resource; the aperture reading [offline]; the stand-in's light model and its floor [hypothesis until capture C1]; the rendered look [hypothesis until captures C1 and C2].

The edge-to-edge bite in the idle's breaths (PREV-147, open). A later report showed V "gritting her teeth": lips slightly parted, upper and lower teeth both visible and meeting edge to edge, which the game is not seen to do. Measured on the reference V in the browser (8-influence skinning computed on the CPU, rays from the front along the midline, the bite in the Head joint's frame so the body's motion drops out) and offline with the solver [measured]:

  • Skinning, morphs and placement match the game's data. Upper teeth on Head (1,448 vertices), lower teeth on mid_J_jaw_JNT (1,561), tongue on its own joints; the teeth mesh's Head and jaw binds equal the face rig's rest (0.1 mm, same axes), and both sets of four influences are drawn. The teeth take the V's mouth shape (h053) as the head does; the teeth morph target has no jaw targets, as in the game files. At rest the bite is closed: the upper incisors overlap the lower by 1.9 mm with 4.7 mm overjet (2.0 mm and 4.0 mm without the mouth shape), and the lips meet at the incisal edges. The body idle leaves the bite unchanged.
  • The jaw moves exactly as the solver says. The face driver's jaw turn equals the solver's at every sampled time (0.31°, 0.65°, 0.88°, 1.21° at 6.0, 2.4, 14.4, 18.4 s); jaw_mid_open 1.0 is 20°.
  • The look is the clip's two breaths. Around 1.5–3.0 s and 13.4–15.0 s of ui_closeup_shot the clip funnels the lips (lips_[lr]_funnel about 0.2, lips_suck_up 0.15) with jaw_mid_open about 0.04: the funnel parts the lip joints by about 2.2 mm, the jaw 0.7–1.0 mm, 3.4–3.5 mm in all, while the jaw's 0.65–0.88° leaves 0.7–1.0 mm of overlap. The window through the lips is 3.75–4 mm tall on the midline, centred on the incisal edges, so both rows show about equally. The clip's sticky-lip tracks (lips_*_sticky_cutScene, up to 1.0 elsewhere in the loop) are zero at both breaths and move no joint in the solver.

So nothing in the preview's geometry departs from the game's clip, meshes and solver. What could differ at runtime, untested: the engine's solve of these controls (sticky lips, or a jaw that opens further than the external solver's), and how dark the mouth reads (the stand-in above now follows the measured parting). Test ask 15 now includes the breath (in-game test asks).

Re-measured on 30 September under the setup the preview now uses (experiment 034) [offline]:

  • The male player setup parts the lips less. Since 29 September the face is solved with the setup the face rig names (facial animation §3); the measurement above used the female head's own. Under it the breaths part the midline lip joints 2.75–2.91 mm (outer) and 3.5–3.6 mm (inner), against 3.36–3.51 and 4.9–5.0 mm under the female setup; the jaw turns identically (0.65–0.88°). The window is about 0.6 mm shorter and still centred on the bite, so both rows still show.
  • The solver's open readings don't change it. Each of the specification's alternatives E1, E2, C1 and I1 (facial solver spec §7) gives the same breaths to 0.01 mm.
  • The lips part by 1 mm or more for 6.7 s of each 22 s loop, so the mouth interior is on screen often.
  • The stand-in was three times too bright for these partings source. Until 30 September it assumed a fixed 10 mm parting and kept at least 0.3 of every light, so the front teeth kept 0.36 at the breaths where its own formula gives about 0.11: two bright incisal edges against a dark slit read as a clenched bite. Step A of the plan (the pose-aware parting above) removes that; whether the game's interior is that dark, and whether per-light interior shadowing (step B) is also needed, waits for capture C1 (render gap plans §1).

Eyes (rank 4, sub-ranks 1–3): done 25 September 2026. The record moved to xfs/render-detail-3 with an eyes slot (uiSlot eyes_color), per-chunk gradients and the morph target's baseTexture rule. Every vanilla and CCXL eye colour resolves on the launch route: on the reference profile that includes the saved Kala eye from a hash-only archive, which the exporter now reads by hash, and a new-game save's gradient_light_blue, which the old manifest left unresolved. The eyeball adapter samples colour and mask V-flipped and roughness at the raw UV, and bakes the iris ramp from the gradient stops, reading the mask's R raw by default. The wetness shell draws as a One/SrcAlpha forward pass. The core eye remains only as the fallback while a V's details prepare, or when a layered design can't be drawn (limit eye-design); since 26 September the 37 layered designs draw through the layered adapter (rank 7 of the eye plan). Refraction and the two-normal Eye light (ranks 4–5) were built on 27 September and layered designs (rank 7) on 26 September; heterochromia (rank 6) is open (eye plan). Grades: selection and chunk roles resource; sampling rule and shell arithmetic source; mask encoding and ramp bake hypothesis; the rendered look [hypothesis until test asks 9–12].

RankWorkVisual gainEffortNeeds
1Done (P1). Skin adapter from the resolved chain: winning albedo, RG normal, roughness R/B, detail normal, microdetail atlas with the tint-mask selectors, tint (multiply/overlay), secondary albedo; SSS: the game's screen-space scatter since 27 September (skin reference §11), a per-channel wrap as the fallbackWhole face: correct type, tone and installed complexion mod at once; fixes the "base game only" headMRecord: resolved skin params and raw textures. Shader: onBeforeCompile extension of MeshPhysicalMaterial with the §2 arithmetic. Resolver: nothing new (already resolves the chain)
2Done (26 September). One head-decal path for every hx_ detail: eye/lip/cheek makeup, pimples, scars, tattoos, cyberware, stubble, personal linkLips and blush appear (the reference V wears both); vanilla makeup becomes visible next to XF makeup for comparisonMRecord: decal components. Shader: mesh_decal and mesh_decal_double_diffuse adapters with the squared coverage curve, sqrt-space colour blend and per-target alphas (decal contract); draw order by priority then a documented fallback. Reuses the existing plate compositing ideas in makeup-stack.ts
3Done (26 September; teeth 27 September). The render coverage audit ranks every remaining gap. Creator controls for all shape and skin options: nose, mouth, jaw and ear morph selectors; skin type and tone; teethAny V, not just a saved one; cheap once 1 lands, because type and tone only choose another resolved materialSResolver: descriptorsFromUiState over the merged CCO (labels from the CCO, never mesh order). Record: the teeth slot, a morph component drawn by its own template's adapter
4Sub-ranks 1–5 and 7 done (record, iris colour, wetness shell, two-normal light, refraction/parallax UV, graphic designs); 6 (heterochromia) open. Full eye chain: gradient iris, the eye template's texture colours, the two-normal Eye light with refraction, and the eye_shadow wetness shell as a forward blended pass; any of the 71 colours or CCXL eyes. Spec and its own ranked sub-plan: eye rendering §6Eyes read as eyes; the most looked-at detail near the makeupM (37 multilayer designs: L, with the multilayer adapter)Record: the eye component with raw UV0, all three chunks with roles from templates, a gradients map, the morph baseTexture rule. Shader: eye adapter (§2–3 of that page) and eye_shadow adapter (One/SrcAlpha)
5Beard, and the rest of hair: male beards (stubble decal and cards) and hair physics; brows, lashes and hair themselves are done in P0Male V's beard; moving hairMRecord: beard components (the planner already takes any slot). Physics: the dangle components and a solver of their graphs (dangle physics, plan). Open: which shadow components to skip (§4)

The body below the head renders since 26 September through the same record, adapters and rig (body rendering). A masculine V draws since 27 September on his own core head through the same resolver, record and adapters (pma resources, the eyeball on chunk 2, 20 targets per region), body included; his XF Eye Artistry export is built (28 September); his face motion (idle, blink, expressions) and the beard cards, which the face planner leaves out because it draws face options only through their decal chunks, remain in the masculine V plan. After these: the character GameOptions (rim Fresnel, SSS tint) as a lighting preset; face-cyberware emissive and metal details. Priorities 1–3 together replace the derived preview's special-case head recipe with resolver-driven parts; piercings already moved (the PRC-specific code is retired; file chain §8).

Open questions

  1. How does each character GameOption (skin ambient mix, SSS specular tint, rim Fresnel, eye diffuse boost) enter the lighting programs, and are the third-party "Vanilla" values the engine defaults? Partly answered: the hair options' defaults and registers are read from the executable (hair shading §5); register pairings for skin ambient (skin reference §6.3) and the eye's DiffuseBoost/UseAOOnEyes (eye reference §6.3) are proposed as hypothesis. The same executable reading could settle the skin, rim and eye defaults offline.
  2. What decides the draw order of several head decals (eye makeup, lipstick, blush, tattoos, cyberware) within one materialPriority? (The preview's fallback is the creator option order.)
  3. Which components are shadow-only? Answered from data resource: the chunk's renderMask says it (MCF_RenderInShadows without MCF_RenderInScene is shadow-only; every vanilla hair shadow mesh), not isShadowMesh (also set on the visible stubble). Still open: that a chunk with lodMask 0 (the CCXL hair_shadow proxy's two triangles) never draws hypothesis; an in-game look at a CCXL hair's silhouette settles it.
  4. What drives the skin blood-flow blend (the two cubed vertex interpolants), and is it active in the creator?
  5. Is TintColor delivered to the skin program as byte/255 or sRGB-decoded? Answered: sRGB-decoded [source; runtime-supported] (§2). A confirming frame with Ultra+ off beside the preview remains in test ask 8.
  6. Does the running game deactivate a switcher's previous target on a choice change? Answered [runtime, 25 September]: a new-game save with skin type 3, brows 3, hairstyle 5 and scars Off lists only skin_type_03, eyebrows_color3, hair_color5 (and its FPP twin hair_color_fpp_05) across the TPP, TPP_photomode, hairs, FPP_hairs and character_customization groups, with no default targets and no scars entry, exactly as rule R5 predicts. switchVisibility is not the cause (it is 0 on skin_type).
  7. Does the renderer's SecondaryAlbedo path from a texture framework's donor mesh (overlay, emissive mask) actually draw in game with ArchiveXL 1.27.3, as the resolver expects?
  8. How do Color parameters of decal templates reach the program? Answered: sRGB-decoded, like every Color parameter source (materials open question 11); the decal family already decoded them so.
  9. What does a masculine V need? Planned in the masculine V plan. The resolver side is gender-neutral, and since phase 1 so is the preview: a masculine V gets his own core head, and his resolved skin matches that head's surface (it draws on the core, as the feminine skin does). The masculine head keeps the feminine plate's UVs (all 1,620 plate vertices have an exact UV twin; 3,010 triangles span them), but it sits about 5 mm away at the same UV, so XF Eye Artistry needs a masculine plate cut under the same skin-byte and 0.4 mm lift rules. The two plates can share every texture [resource; hypothesis that the looks land identically until an in-game check].
  10. Is the seam-fix part shadow-only? Answered resource: yes. The skin type's .app brings t0_000_pwa_base__full_seamfix.mesh, whose two chunks (neck and arms) carry MCF_RenderInShadows without MCF_RenderInScene, so the render-mask rule of §4 skips it whatever its material (metal_base.remt) (render coverage §8).

In-game test asks

Batch into one prepared session; record game, ArchiveXL, CET and the Character Rendering Editor preset in use.

  1. Tone overlay versus multiply. In the creator, skin type 1, compare tone 2 (warm ivory, negative scale) with tone 1 (pale, zero scale) under fixed light: warm ivory should look warmer and slightly brighter, not darker.
  2. Complexion replacement. With the reference profile, photograph the face at skin type 5, tone 2; then disable only the Realistic Complexion III mod and repeat. A visible change confirms mod-over-base for the albedo; a change after disabling only the texture framework confirms the ArchiveXL mesh patch.
  3. Decal order. Choose eye makeup style 5 black, blush 10 and facial tattoo 2 together; capture where they overlap near the cheekbone.
  4. Lip finish roughness. Lipstick style 5 regular, glossy and matte in turn under a moving light: expect regular and glossy to share the skin's highlight and matte to dull it.
  5. Render options. Toggle the Character Rendering Editor between "Vanilla" and the installed preset once, same frame, to size how much runtime tuning changes the reference portrait.
  6. Switcher exclusivity and Off. Done 25 September; result under open question 6. New game, female V: skin type 3, brows 3, hairstyle 5, scars Off, everything else default. Make a manual save straight after the creator and hand it over. Expected from the save's appearance list: skin_type_03, eyebrows_color3 and hair_color5 only, with no skin_type_01, eyebrows_color1, hair_color1 and no scars entry. One face with no z-fighting at the hairline and brows is the visual cross-check.
  7. Replaced hair profiles. With the reference profile's hair-colour mod enabled, the resolver binds its brown_liquorice.hp and blonde_platinum.hp over the base game's (R1). The preview therefore shows the default V's hair as pale yellow at the roots fading to saturated brown at the tips, and "blonde platinum" as a warm tan-blonde with dark roots and tips. In the creator, photograph hairstyle 1 in brown liquorice and hairstyle 5 in blonde platinum, then disable only that mod and repeat. A change confirms mod-over-base for .hp profiles in game. NPC hairdos in brown liquorice already show the yellow-to-brown look in game, anecdotally (yellow-hair investigation).
  8. Tone strength (tint encoding). With the reference profile, creator skin page, skin type 1: photograph tone 1 (pale, no tint) and tone 5 (senna, multiply 0.70) under the same light, then type 3 in senna. Also record the same pair in the preview. If the game's senna darkens a mid-tone skin by about a fifth, TintColor is byte/255 (the preview's choice); if by about two fifths, it is sRGB-decoded. The executable has since settled materials open question 11: Color parameters are sRGB-decoded source, and the preview draws tones so; this ask is now the runtime confirmation. Session 4 ran a variant, because the tone numbers were applied as zero-based indices: warm ivory, senna amber and limestone on type 1. Fitted with the shader's arithmetic, the frames lean to sRGB decoding (amber darkens the skin 32–44 % against warm ivory). They were display-referred and carried Ultra+'s RT skin tuning, so the result is provisional (experiment 029 §4.2). Still to take: pale against senna (the values 01_ca_pale and 03_ca_senna, not label numbers) with Ultra+ off, beside the preview's render of the same tones.
  9. Iris gradient coordinate. Creator eyes page, one frame each: gradient red, gradient light blue and gradient brown, then blood gradient red. Bright red-orange and mid sky-blue irises mean the iris mask is read raw; near-black red and dark slate blue mean it is sRGB-decoded (eye rendering §2.3). This also bears on how every gamma-flagged mask is sampled, the skin tint mask included. Add one frame of eye colour 54 (Rebecca blue) beside gradient blue with the key light from the side: a lopsided sclera or a catch light missing on one side means gamma-flagged textures are decoded, since the Rebecca normal map is one (eye reference §4). Answered in session 3: the mask is read raw, and Rebecca's sclera is even with both catch lights (eye rendering §2.3).
  10. Iris axis, orientation and depth. Same page, the V looking straight at the camera: one close-up of both eyes with gradient light blue (a well-defined limbus). The preview will render the same frame with the iris axis turned 5° outward (its default) and 5° inward; the matching one fixes the sign of the per-eye axis. Then one close-up of a texture eye whose iris is not round-symmetric (one of the graphic texture eyes, creator options 62–71), straight on: the preview draws the iris the same way up as the rest of the eye, and the literal reading of the program would mirror it top to bottom (eye rendering §2.2). Last, the same eye from the side (photo mode, about 45°): the iris should sit visibly under the cornea and slide against the limbus rather than look painted on.
  11. Wetness shell. Photo mode, close-up of one eye, a single light moved slowly from above the head to below the chin: the tear-line highlight should appear along the lower lid margin only near the mirror position, and the inner and outer corners should stay darker than the middle of the sclera throughout.
  12. Cornea against iris. Same framing, rotate the light around the head horizontally: the catch light should stay sharp and small across the iris and sclera, while the iris darkens smoothly (matte) as the light moves to the side.
  13. Face decals against the preview. Load the reference save; in photo mode, one frontal close-up of the lips and one of the left cheek under a single fixed light, then the same with the light moved 45° up. Compare with the preview's reference-save frames: the lipstick's colour and strength (it keeps the skin's gloss), the blush's extent on the cheek and nose tip, and whether the legacy XF layers sit over the vanilla makeup. This settles open question 8 for decals and anchors test ask 3.
  14. Layered parts against the preview. Creator face page, one close-up each under the same light: piercing style 9 in black, style 1 in silver and gold (vanilla metals), eye colour 24 (the heart design), and eye colours 19 (arasaka) and 36 (target) beside the preview's frames, which checks the reciprocal microblend-contrast reading on designs not yet captured in game (eye rendering §6.6). Compare with the preview's frames: black plastic should keep a narrow highlight, silver and gold read as brushed metal of their tint, and the heart sit the right way up. A gold that shows grey, or plastic without its colour, answers the colour-mask question (materials open question 10) against the preview's "tint everywhere" choice. Session 3 answered the arasaka and target part: both draw crisp and in place, as in the preview. Session 4 took the metals and the heart runtime: gold shows gold, black plastic keeps its colour and a narrow highlight, and the heart sits upright, as in the preview. The silver ring's highlight cores are neutral (experiment 029 §4.1).
  15. Teeth. Creator teeth page (UI_Teeth camera, its ui_expose_teeth loop), one frame for each of the five choices under the same light, and one photo-mode smile: the default's colour against the preview's, the metal finishes' highlight, and whether the gums and tongue read inside the mouth. Expected: the default a warm off-white; compare its brightness inside a slightly parted mouth with the preview's mouth occlusion (§6, "Teeth": the front teeth at about 0.11 of their light at the breaths, 0.5 on the teeth page), which a brighter game mouth would say is too strong and a darker one too weak. The breath (PREV-147): the creator's head close-up, zoomed on the mouth, recorded from entering the page for 16 s (the clip's breaths come at about 2 s and 14 s): do the lips part, and are teeth visible through the parting, upper only, both rows edge to edge, or only darkness? With the runtime bridge, the mid_J_jaw_JNT turn at the same moments (the external solver gives 0.65–0.88°).

CC file chain · Body rendering · Materials and shaders · Hair shading · Eye rendering · Eyebrows · Face makeup · Tattoos · Mod loading · Head CC render evidence · Feature-module platform · Preview fidelity backlog

Corrections and credit

This page is generated from knowledge/head-cc-rendering.md in the repository. If something here is wrong or missing, please open an issue or suggest an edit; links to sources, captures or tests help most.

It builds on the Cyberpunk 2077 modding community’s work. The inline citations link to the wiki pages, mods, tools and repositories each finding came from, and the credits acknowledge everyone we learned from. Citing a source does not mean its authors endorse these notes.