Knowledge base

Materials and shaders (REDengine 4, game 2.31)

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. The resource model, template catalogue, G-buffer layout, deferred BRDF and the skin SSS pipeline are consolidated from source, installed resources and compiled programs. Runtime evidence is partial: the first photo-mode session (25 September) showed the colour shift working, distinct coverage steps, close-up breakup of a skin-coincident plate and uniformly glossy flat finishes; the second (26 September, partial) confirmed that the 0.4 mm plate lift removes the breakup (see in-game test asks, experiment 017 and experiment 020).

This page explains how Cyberpunk 2077 materials work well enough to:

  • make the browser preview faithful;
  • design makeup finishes that have a realistic chance of rendering correctly in game at the first attempt.

Hair is covered only at overview level here. See hair shading for the in-depth study.

Per-family references. The systematic shader study keeps one evidence-level reference per template family, with every parameter, pass, program hash and grade: skin, hair, eye, decals, multilayered and metal and glass. The shader fact index maps each engine fact to the Studio preview materials and export compilers that rely on it, so a correction reaches all of them.

Evidence grades:

  • source: engine, framework or tool source, or disassembled compiled programs;
  • resource: installed game or mod resources;
  • wiki: Cyberpunk Modding Docs at commit be2f44eed8419342ec13f72ed9cab008e9f7b289;
  • runtime: observed in the running game;
  • hypothesis: not yet established.

Pinned sources:

SourceVersion
Installed game2.31 (GameVersion 2310 in serialized JSON)
shader_final.cacheSHA-256 339145…3ccfa
staticshader_final.cacheSHA-256 bff160…59ff
WolvenKitCLI 8.17.4; source 11720772
RED4ext.SDKad727771
ArchiveXL5474e34d
Cyberpunk Blender add-on2.1.0 at 7a4ee793
dxil-spirv / SPIRV-Cross (optional decompile)f2d1b554 / aa217aeb
Cyberpunk2077.exe / address library (§8)SHA-256 a7de82…0991 / linker map 68af45ea
RED4ext (§8)c52c8d80

Compiled-program hashes and the reproducible method are in the shader-system evidence note.


1. The material resource model

mesh (.mesh)
 ├─ appearances[k].chunkMaterials[submesh] ──name──► materialEntries[i] {name, isLocalInstance, index}
 │                                                      │ local                     │ external
 │                                   localMaterialBuffer.materials[index]   externalMaterials[index] (.mi path)
 │                                   (embedded CMaterialInstance)                   │
 └──────────────────────────────────────────────────────┴──► CMaterialInstance ── baseMaterial ──► (.mi …) ──► CMaterialTemplate (.mt/.remt)
                                                                  sparse name→value overrides          parameters, techniques/passes, materialType

1.1 Resource types

ExtensionRED classRoleGrade
.mt, .remtCMaterialTemplateThe shader: typed parameters with defaults, compiled techniques and passes, render states, lighting class. Both extensions are the same class; .remt is used mainly by engine\materials\* templates.source WolvenKit FileTypeHelper.cs:284,296
.miCMaterialInstancebaseMaterial (a template or another .mi), a sparse values list of name → Variant, and enableMask.source RED4ext N/CMaterialInstance.hpp:15-19; WolvenKit ClassesExt/Appendix/CMaterialInstance.cs:20-40
.hpCHairProfilegradientEntriesID and gradientEntriesRootToTip ({value, Color} stops), plus sampleCountsource G/CHairProfile.hpp:20-23
.spCSkinProfileblurSize, diffuse, falloff, roughness0, roughness1, lobeMixsource G/CSkinProfile.hpp:19-24
.gradientCGradientA list of colour stops, e.g. the eye IrisColorGradientsource G/CGradient.hpp:20
.mlsetupMultilayer_SetupAn ordered list of Multilayer_Layer, plus ratio and useNormalsource G/Multilayer_Setup.hpp:20-23
.mltemplateMultilayer_LayerTemplateColour/normal/roughness/metal textures, tilingMultiplier, and named override tablessource G/Multilayer_LayerTemplate.hpp:23-31
.mlmaskMultilayer_MaskPer-layer masks inside a render-resource blobsource G/Multilayer_Mask.hpp:20

1.2 Parameter types

Template parameters are CMaterialParameter* objects with parameterName and register source (G/CMaterialParameter.hpp:19-20).

ClassValueReferencesGrade
Scalarfloat (plus min/max)—source
VectorVector4—source
ColorColor (bytes); the program receives RGB sRGB-decoded and alpha as byte/255 (below)—source
Texture, TextureArray, Cube, DynamicTextureRef<ITexture>.xbm, .texarray, .cubemapsource
HairParametersRef<CHairProfile>.hpsource
SkinParametersRef<CSkinProfile>.spsource
MultilayerSetup, MultilayerMaskrefs.mlsetup, .mlmasksource
GradientRef<CGradient>.gradientsource
StructBuffernone reflectedruntime data (multilayer Layers/MaskTiles)source / resource

How values reach the program [source: the 2.31 executable, research/materials/shader-system/exe_hair.py params]. The material constant-buffer packer (RVA 0x4ff4d4, called once per stage, vertex then pixel, by 0x1eb374) walks a layout of 4-byte entries (data offset, parameter type, register) and writes one float4 per register. The type is what each CMaterialParameter* class reports (its vtable slot 28): Texture 1, Color 2, Cube 3, Vector 4, Scalar 5, TextureArray 7, SkinParameters 10, HairParameters 13, Gradient 16, and so on. Color (4 bytes) becomes (srgb(R), srgb(G), srgb(B), A/255) through the engine's 256-entry sRGB-to-linear table, the same table the skin-profile colours and the debug-draw vertex colours go through; Vector is copied and Scalar broadcast to all four lanes; the resource types (Texture, Cube, TextureArray, SkinParameters, HairParameters, Foliage, Gradient) write an integer looked up for the bound resource (a texture index, and, by their use in the programs, the skin-profile slot and the gradient row [hypothesis for the meaning]). So a template that raises a Color to 2.2 itself (eye_shadow's ShadowColor) applies that power on top of the decoding. This covers material parameters packed by this routine; runtime overrides through other paths (scripts, effects) were not read.

WolvenKit maps each class to its value field in MaterialExtractor.cs:626-646. There is no .mltemplate parameter type. A template is reached only through Multilayer_Layer.material source.

Stage slots. parameters, usedParameters and samplerStates are three-slot arrays indexed like EMaterialShaderTarget: [0] unused, [1] vertex, [2] pixel.

  • The indexing is source-supported: G/EMaterialShaderTarget.hpp:12-15, and WolvenKit reads only slot 2.
  • In every template inspected, vertex-animation parameters sit in [1] and all surface parameters in [2] resource.
  • usedParameters[2] gives the constant-buffer register the compiled pixel program uses (§3.1).

Wrong resource type or unresolved path renders black and shiny wiki (materials/materials-troubleshooting.md L17-25).

Inconsistent parameter names. The same concept has different names across templates (Albedo, BaseColor, DiffuseTexture, Color). Always read the template wiki (configuring-materials/README.md L82-88) resource.

1.3 How an instance chain resolves

RuleGrade
Defaults. The template's parameters supply the defaults.wiki materials/shaders/README.md L35-42
Leaf wins. Each .mi applies its values over its base; the mesh-local instance is applied last.wiki materials/shaders/README.md L35-42; source WolvenKit MaterialExtractor.cs:278-347 implements the same order
Undeclared names are inert. A value whose name the template does not declare has no effect.wiki shaders/README.md L19
Unused names are dropped on export. WolvenKit only exports template parameters present in usedParameters[2].source MaterialExtractor.cs:389-424
Cycles crash the game. A cyclic baseMaterial chain does this.wiki re-using-materials-.mi.md L29
Masking. Effective masking is template.canBeMasked && instance.enableMask. A masked draw uses the pass's stagePassNameDiscarded stage (…_discarded); the shader cache records it as Discarded. In the decal and multilayered families a Discarded program is the plain program plus a per-draw temporal-dither dissolve driven by MaterialModifiersConsts[0].z/.w, not an alpha test on the texture's alpha; multilayered.mt has such variants although it cannot be masked.source WolvenKit export logic MaterialExtractor.cs:313-314,386; the dissolve source (decal reference §2.3); which draws use it hypothesis
Shared templates. Never edit a shipped template or a shared .mi: every user of it changes. About half the player head skin .mi files are shared with NPCs.wiki configuring-materials/README.md L22-25; cheat-sheet-head/README.md L70-72
WolvenKit's material export applies this order for us. uncook … --mesh-export-type MeshOnly -gp <game> writes <mesh>.Material.json: every mesh-local material resolved through its .mi chain (Data holds the effective parameters), plus Appearances keyed <name><index> → chunk material names, and it decodes every texture those materials use. Example (2.31 female head, appearance default → 01_ca_pale): Albedo h0_000_pwa_c__basehead_d01.xbm and Normal h0_001_pwa_c__basehead_n01.xbm come from the mesh-local instance, Roughness h0_000_wa_c__basehead_rm01.xbm from default_female_head_01_ca_pale.mi two levels up; a manual .mi walk gave the same answer. The Studio's derived preview reads this file instead of re-walking chains (src/preview-core-materials.ts).resource 2.31 export, 25 Sep 2026, CLI 8.17.4 and 9.0.1; source MaterialExtractor.cs

1.4 Mesh-side binding

Name lookup. chunkMaterials[submesh] names a materialEntries entry. That entry's isLocalInstance and index choose between two lists source:

  • the local list, localMaterialBuffer.materials (embedded CR2W files) or preloadLocalMaterialInstances;
  • the external list, externalMaterials (async .mi refs) or preloadExternalMaterials.

ArchiveXL loads through exactly this pair (src/App/Extensions/Mesh/Extension.cpp:410-427).

Unnamed chunks. A chunk without a valid material name renders invisible source (WolvenKit CMeshPreProcessor.cs:144-162).

Preload lists. Do not mix preload and non-preload lists of the same kind wiki (3d-objects-.mesh-files/README.md L230-241):

  • mixing them makes non-preload materials render transparent on first use;
  • WolvenKit's export also collides on names when the lists are mixed source (MaterialExtractor.cs:223-271).

Dynamic materials (ArchiveXL) source (Extension.cpp, Dynamic.cpp):

  • Template entries. A chunk name prefix@tmpl binds to a template entry named @tmpl.
  • Soft references. Resource values and baseMaterial paths starting with * are soft and are substituted:
    • {material} is the prefix;
    • {material.N} is the Nth +-separated part;
    • other {attr} tokens come from context attributes.
  • Optional values. A trailing ? makes the value optional; if it is unresolved, the parameter is removed.
  • Base chains. Soft .mi base chains are cloned recursively.
  • @context. Local entry 0 named @context supplies override parameters and attributes.
  • Emulation gap. WolvenKit's Material.json export only emulates * and {material} source (ArchiveXlHelper.cs:14-61). Offline exports of CCXL-style chains can therefore silently fall back to defaults.

2. Rendering pipeline: what a material can and cannot express

2.1 Passes and render stages

Each template holds techniques[] → passes[]. A pass carries source (G/MaterialPass.hpp:21-30) resource:

  • stage names (stagePassNameRegular, stagePassNameDiscarded);
  • depth and stencil state;
  • the rasterizer: cull, winding and offsetMode, e.g. OFFSET_DecalBias;
  • per-target blend for up to 8 targets, with write masks;
  • enablePixelShader.

The depth test is GreaterEqual in G-buffer passes, i.e. a reversed-Z depth buffer resource.

Stage (renderstage_…)What it does, as evidencedExamples
gbuffer_regular / gbuffer_velbuff_regularOpaque G-buffer write, depth write on, no blendingskin, eye, metal_base, multilayered resource
post_gbufferAfter the opaque G-buffer. Alpha-blends 3 targets with depth writes off. This is where decals live.mesh_decal family, and metal_base has one too resource
skin_translucencyDepth-only pass (pixel shader off, front-face cull, R write mask) that feeds the SSS/translucency pipelineskin family resource
hair_alpha_accum, hair_basecolor_blend, hair_gbuffer_solidThe three-part hair path (§4.3)hair family resource
transparent, transparent_back_face, unlit, distortionForward passes after lighting, with their own lighting codeglass, eye_shadow, the clear-coat unlit pass resource source
highlightsOutline and highlight buffer. Its programs write constants or zeros and compute no lighting.nearly every template source particle-decal audit
cascade_regular, depth, depth_prepassShadow and depthresource

The full stage list, with compilation counts, is in the local material-index.json produced by the evidence tool.

2.2 The G-buffer

The layout below is decoded from the tiled deferred light (m_shaderLightsComputeGlobalLocalShadows_Clustered_*) and from the skin, metal_base and mesh_decal writers. All of it is source (compiled programs; see the evidence note).

TargetRGBA
GBuffer0sqrt(linear base colour). The light squares it.Class-specific payload (bits of the eye's second vector). Skin writes 1.
GBuffer1World normal n / max(|n|) × 0.5 + 0.5. The light uses normalize(rgb − 0.5).Class-specific: skin-profile slot high bits (skin); tangent-axis selector (hair); octahedral bits (eye). 0 for metal_base.
GBuffer2x = metalness, y = roughness (the light clamps it to [0.04, 1]), z = a transmission weight: neutral 1/3 for Standard materials, 0.4 + 0.6·(vertex colour G) for skin. The deferred light reads it only for Foliage; the translucency variant of the SSS setup reads it for skin (sun transmission through thin parts).Class-specific: skin-profile low bit, an emissive flag and 6 bits (skin); the eye's second vector (eye). Standard emissive writers (metal_base family, mesh_decal_emissive) store bit 7 = emissive flag and 7 bits = sqrt(EV/10).
Stencil (bits 5+)Lighting class = ERenderMaterialType: Standard 0, Subsurface 1, Cloth 2, Eye 3, Hair 4, Foliage 5. The value is set per material template (materialType).—

Grades for the less certain rows:

  • Stencil-class identity: the enum values match the light's class switch and the templates' materialType → source/resource match. The actual stencil write was not traced → hypothesis.
  • GBuffer2.z: the all-classes global light (…_11111111, 6606735909222169407) forms saturate((z − 1/3) × 1.5) (%85–%87) and uses it only in the Foliage branch (class 5, block %1051), as the weight of a back-lit transmission lobe added to diffuse (%1186–%1204); Eye forces it to 0. The Subsurface branch and the local-light path never read it (experiment 017). The SSS UseTranslucency setup (11387959167119825062) does: for Subsurface pixels it adds a sun transmission term weighted by the same saturate((z − 1/3) × 1.5) (for skin 0.1 + 0.9·G) and by a thickness falloff from a back-face depth target, before the blur source (skin reference §6.4). When that variant runs is hypothesis. The debug-view enum EEnvManagerModifier still lists a "Translucency" view (G/EEnvManagerModifier.hpp:24-40); whether that view shows this channel is hypothesis.

Which templates carry which class resource:

ClassTemplates
Subsurfaceskin, skin_blendable, skin_morph, blackwall_blendable_skin
Eyethe 6 eye templates (eye, eye_gradient, eye_blendable, eye_morph, blackwall variants)
Hairthe 6 hair templates
Foliagespeedtree and cloth_wind
Standardeverything else (351 of 373), including every decal

No template uses Cloth.

2.3 The deferred lighting model

Decoded from the global-light compute shader source:

TermEngine formulaBrowser implication
Base colour / F0diffuse colour = albedo·(1−metal). F0 = lerp(0.04, albedo, metal).Dielectric F0 is fixed at 0.04. No G-buffer channel sets IOR or specular level for non-metals.
DiffuseRenormalized Burley (Disney) diffuse: fd90 = 0.5·r + 2·LdotH²·r, energy factor 1/π·(1 − 0.338·r). This is Frostbite's formulation (Lagarde & de Rousiers 2014).Three.js MeshStandard/Physical uses Lambert. Rough skin and makeup darken or brighten differently at grazing angles.
SpecularGGX with α = roughness², height-correlated Smith visibility, Schlick Fresnel with F90 = 1Three uses the same α = r² convention. Roughness values transfer directly.
Sun / moonA disk light, using the representative-point methodA small area light, not a point. Highlights on glossy makeup are slightly larger than a point light's.
ClassesHair builds an anisotropic tangent frame. Eye decodes a second (iris) normal and has its own BRDF: Lambert diffuse on the iris normal, one GGX lobe on the cornea normal (GBuffer1) with its own visibility term and an exp2 Schlick Fresnel, no N·L on specular, a point sun, and ambient on the cornea normal; it switches off the GBuffer2.z term (eye rendering §3). Subsurface keeps diffuse and specular in separate outputs: albedo is set to 1 when metalness < 0.1 and a global flag is on, diffuse is Burley at the G-buffer roughness, and specular is two GGX lobes at roughness × roughness0 and × roughness1 from the pixel's skin-profile kernel (one of 8 slots, cb6 registers 4–11, SKernelDualSpecular), summed and scaled by (1 + lobeMix)/2. The default profile (roughness0 0.966, roughness1 1.597, lobeMix 1) gives skin up to twice a Standard lobe's specular. Local lights can also shift roughness per light: r′ = saturate(r + k·(byte/127.5 − 1)), from a byte of the light's parameters. source (…_11111111 %575–%751, %2766 onward)Skin roughness written by a decal does reach skin's specular. SSS is a separable screen-space blur of the diffuse irradiance (albedo 1), two passes (horizontal first is hypothesis), confined to Subsurface pixels: a neighbour counts only if it is class 1 and lit (red > 0), with no depth test, whole-pixel taps and per-channel renormalisation. The kernel is Jimenez et al.'s separable-SSS reference kernel over d'Eon and Luebke's skin profile, built on the CPU per profile: falloff sets each channel's width, blurSize scales the offsets, and the SubsurfaceScatteringQuality setting picks 11, 17 or 25 samples source (executable). The radius shrinks with depth; its absolute scale is hypothesis. The combine (7703933925853799832) lerps unblurred to blurred light per channel by the profile's diffuse colour, then multiplies by the pixel's albedo and adds specular unchanged, plus one term tinted by SubsurfaceSpecularTint. So skin detail and makeup colour stay sharp; only the shading softens. A pixel whose metalness exceeds 0.1 skips SSS entirely source (skin reference §6.3; the preview draws it the same way, §11). The global light has no probe term. The ambient (probe) composite is traced for the Eye class (eye reference §6.3) and for hair (hair reference §6.5); SSR and ray-traced reflections are not traced.

Consequence (the central fact for makeup). The G-buffer stores one surface per pixel: one base colour, one normal, one metalness and one roughness. There is no clear-coat, sheen, second normal (except Eye), anisotropy (except Hair) or specular-tint channel. Anything that needs a second reflection lobe must come from a separate forward pass drawn after lighting. The stock examples are multilayered_clear_coat's unlit pass (dual-source blend One/Src1Color) and eye_shadow's transparent_back_face pass (One/SrcAlpha) resource source.

2.4 What a post_gbuffer decal does to the pixel under it

All source/resource (mesh-decal contract, evidence note):

  1. What it blends, and in what space. It blends base colour, normal and metalness/roughness into GBuffer0–2 with SrcAlpha/InvSrcAlpha. Each target gets its own alpha (colour, normal and surface coverage). Colour is written as sqrt(colour), so decal colour blending happens in square-root (gamma-2) space, not linear.
  2. Write mask RGB, stencil disabled. The decal never changes the lighting class, skin-profile slot or .w payloads. Makeup on skin is still lit as Subsurface skin. Metalness ≥ 0.1 on a Subsurface pixel takes the class's non-SSS albedo path (§2.3).
  3. GBuffer2.z and skin translucency. The decal writes 1/3, so where surface coverage is non-zero it blends skin's 0.4 + 0.6·G toward 1/3. The deferred light ignores that channel for skin, but the SSS translucency setup weights its sun transmission by it, so a covering decal removes transmission under it source. The transmission needs thin geometry (a small front-to-back depth), so it matters on ears and nostrils, not under eye or lip makeup (skin reference §6.4).
  4. Opacity curve. Coverage is saturate((a−0.5)·tan((c+1)·π/4)+0.5)² · (1 − influence·secondaryMask). Each target's alpha multiplies it by DiffuseAlpha, NormalAlpha or RoughnessMetalnessAlpha. All three default to 0, so an instance must enable them.
  5. Placement and depth. Rasterizer state is front-face cull with CCW winding, which is equivalent to back-face cull with CW. The pass tests depth GreaterEqual without writing it, and offsetMode OFFSET_DecalBias (value 3 of PSODescRasterizerModeOffsetMode) adds a depth bias whose values are engine-side and unknown. DepthThreshold (default 0.5) discards decal pixels farther than that from the scene depth; it does not settle coincident surfaces source.
  6. Vanilla face decals do not rely on the bias. The 2.31 female eye-makeup, lip, freckle and pimple meshes are the head's vertices pushed 0.40 mm out along the head's normals (median residual 5 µm), and their morph targets keep the offset along each target's normal; they have one LOD like the head. The brow mesh is lifted 0.35 mm the same way (eyebrows). A plate coincident with the head broke into skin-coloured patches close up in photo mode, and was clean at medium distance resource runtime (first session, finish board results). XF Studio now lifts its packaged plate 0.4 mm the same way (experiment 017); in session 2 the 0 mm plate broke up again at an extreme close-up while the 0.1 mm and 0.4 mm lifts stayed clean runtime (experiment 020). The close-up-only mechanism (a depth bias that absorbs less world-space mismatch near the camera) is hypothesis.

3. Compiled shaders: finding and reading them

3.1 Where they are and how templates map to programs

CacheContentsGrade
PATH_TO_GAME/engine/shader_final.cacheAll material-template programs. Footer RDHS v10. Programs are DXBC containers holding DXIL. Each compilation record reads <template> CompiledTechnique [Index, Pass '<stage>', PassIndex, …, RenderStageContext: [ID, VF: <factory>; Discarded/Dismembered/PreSkinned]], followed by (vertex GUID, pixel GUID or none, material GUID).source WolvenKit ShaderCacheReader.cs, with the stage correction below
PATH_TO_GAME/engine/staticshader_final.cacheEngine programs: lighting, SSS, multilayer surface cache, hair resolve, post-FX, ray tracing. Footer RDHS v8, with m_* technique names.source reverse-engineered here
shaderps4/ps5/xboxone/xsx/vulkan*.cacheConsole and Vulkan buildsnot examined

Stage labels. WolvenKit labels the first GUID "pixel". In all 19,647 records it is the vertex program; 2,564 records have no pixel program source.

Template-to-program link. Templates connect to programs only through a lookup key: the hashed template name plus a hash of the permutation (vertex factory and flags, technique index, render stage, pass index); see §8.1. No template field stores a shader GUID source. The name is the template resource's own CMaterialTemplate.name, not its depot path (three vanilla templates' names differ from their file stems, and the cache keys follow the name): a mod's copy of mesh_decal.mt at another path (the legacy XF generator's mesh_decal__emp_front.mt, identical to vanilla except materialPriority) keeps name = mesh_decal resource and drew in game [runtime: the reference character wears the legacy build; no capture on file], so tools must identify a template by that name. The Studio's renderer does (head CC rendering §3). materialPriority is EMP_Normal (0) or EMP_Front (1); serializers omit the default [source: WolvenKit RED4 enums].

Constant registers.

  • Material constants are cb4, one register per usedParameters[2] register ([1] for vertex programs).

    • Each program keeps a %ShaderSpecificConstants type with one field per register: i32 for Texture/TextureArray/Cube, float for Scalar, <4 x float> for Vector/Color.
    • Across one representative program per template, 314 of 314 checked templates agree, and no program reads a cb4 register that the template does not own source.
  • Textures are bindless. The program loads an index from that cb4 register and indexes t0, space1. Multilayer layer textures are the exception: their index comes from a structured-buffer load.

  • Engine buffers keep their struct names in the DXIL resource metadata; their member names do not survive, so individual registers are identified by use source:

    RegisterStruct
    cb0GlobalShaderConsts
    cb1CameraShaderConsts
    cb2ENV_PROBES
    cb3CustomPixelConsts / CustomVertexConsts
    cb5FrequentVertexConsts
    cb6COM / VER (static compute programs)
    cb7MaterialModifiersConsts
    cb12SharedPixelConsts
    cb13CSConstants

3.2 Repeatable method

research/materials/shader-system/ contains:

  • shader_cache.py (index / find / static / extract / summary);
  • template_summary.py (parameters, registers, passes and blend states for all 373 templates);
  • shader_annotate.py (annotate, optionally --decompile / search).

Recipe:

  1. Serialize the templates with WolvenKit CLI 8.17.4: unbundle memoryresident_1_general.archive -r "\.(mt|remt)$", then convert serialize.

  2. Find the compilation with shader_cache.py find <template> --info "<stage>'.*VF: MeshSkinned\]". The eye plate, head and eyes are MeshSkinned.

  3. Annotate with shader_annotate.py annotate <template> --guid <pixelGUID>. This extracts and disassembles with Windows SDK dxc -dumpbin, then writes readable pseudo-HLSL with names for:

    • template parameters and textures;
    • engine struct names;
    • the vertex inputs behind each interpolator;
    • G-buffer and blend roles for each target.

    shader_annotate.py search finds the programs that contain a code pattern. extract and summary remain for raw work.

  4. Read structured source if needed. --decompile adds <GUID>.decompiled.hlsl: dxil-spirv (DXIL to SPIR-V) and SPIRV-Cross (to HLSL), both built from source, with the same names applied.

    • It gives real loops, typed values and a named per-register cb4. HLSL output compiles with dxc for 244 of 253 templates.
    • It is HLSL for about three quarters of templates; forward programs with wave ops or float-granular cbuffer views fall back to GLSL.
    • Renaming was checked not to change the compiled program. The decompilers themselves are a second reading, not proof.
    • Its _N are not SSA numbers (annotation results).
  5. Check against the disassembly (<GUID>.annotated.ll) before relying on either listing. The lifted pseudo-HLSL folds expressions and keeps loops as goto; it is a reading aid, not compilable source.

  6. Write it down. Record the program GUID, the SHA-256 and SSA ranges. SSA numbers are only valid within that one program.

Older one-off extractors (projects/xf-studio/authoring/tools/inspect_shader_cache.ts, extract-brow-shader.ts, inspect-eye-skin-cache.ts, experiments/014-native-eye-gradient/inspect-cache.ts) cover the same v10 format for single studies.

3.3 Compiled-program index (MeshSkinned unless noted)

Template / passPixel GUIDDocumented in
skin gbuffer_regular pixel / vertex (wrinkle regions)12806642364631437234 / 7494393843130164436skin reference, saved-skin trace
eye gbuffer_regular14845425953312192161eye reference, eye/lip optics
eye_gradient gbuffer_regular / gbuffer_velbuff_regular10020572278408962832 / 776001972724306108eye reference, exp. 014, eye rendering evidence
mesh_decal post_gbuffer16098255505177109230decal contract
mesh_decal_blendable post_gbuffer / vertex (byte-identical to the recolour-blendable vertex)3004271728282315156 / 13171349059403238141decal reference §5.2, glossy feasibility
mesh_decal_double_diffuse post_gbuffer / its Discarded twin7624081209775720613 / 8834363738920290566decal reference §5.1
mesh_decal_gradientmap_recolor / _2 / _emissive post_gbuffer5232451138945967528 / 13292453303177655632 / 6714007623731301689decal reference §5.3
mesh_decal_parallax, _multitinted, _morph, _revealed, __blackbody post_gbuffer13887708964992876960, 12680699086337052733, 7449292646156817489, 6766005304427206345, 3236176651483732111decal reference §5.5
mesh_decal_wet_character post_gbuffer17388524518779931857glossy feasibility
mesh_decal_gradientmap_recolor_blendable post_gbuffer3456408455936683438colour-shift design, feasibility
static m_postfx_SubsurfaceScattering Setup / Setup_UseTranslucency / Blur_Horizontal / Blur_Vertical / Combine18323727242039837728 / 11387959167119825062 / 13638945895069409584 / 1061893983243070248 / 7703933925853799832skin reference §6, experiment 017
mesh_decal_gradientmap_recolor_blendable vertex (Fresnel distance fade)7066617541061519457 (DXBC SHA-256 77e887ec…cb3)colour-shift design
mesh_decal_particles post_gbuffer / highlights1688064767334184205 / 10861674281505605668particle-decal audit
mesh_decal_emissive post_gbuffer (target 2 is additive on B/A, so over skin it would add into GBuffer2.z/.w [hypothesis for the visible effect])9453098283293843067annotation results
mesh_decal_emissive_subsurface subsurface_emissive / highlights8986576764202126900 / 7960168020925993542decal reference §5.4, glint feasibility
metal_base_glitter gbuffer_regular15760075574186250120glitter investigation
metal_base gbuffer_regular Index 1 / Discarded (alpha test) / Index 2 (weather)4684655453878116559 / 6427363791120029564 / 1846801220589112223metal and glass §3, evidence note
metal_base post_gbuffer (decal mode)14174891702035234925metal and glass §3.5
glass_onesided transparent / distortion / transparent_mark_rt4256218839974653439 / 16640676567101803185 / 15548066001076791090metal and glass §4
static ambient and emissive composite (decodes the Standard emissive byte); the m_shaderLightIntegrate_NoEnvProbes technique (creator lighting §10.3)10393055107398307099metal and glass §3.6, eye reference §6.3
eye_shadow transparent_back_face14043594489545752539eye reference, evidence note, eye rendering evidence
multilayered_clear_coat unlit / gbuffer_regular (coat mask in GBuffer2.z)5403688829342147459 / 3911537533623547427multilayered reference §6, evidence note
multilayered_baked gbuffer_regular (virtual-texture surface cache)9875025106086992270multilayered reference §7
multilayered gbuffer_regular (MeshSkinned and MeshStatic)4792354088802328889multilayered evidence
static m_surfaceCache_GenerateMultilayer (compute)7318179378413828262multilayered evidence
hair alpha_accum / basecolor_blend / gbuffer_solid2782105832921211528 / 7571795766366002052 / 2903833597335136032hair reference, hair shading
static m_shaderLightsComputeGlobalLocalShadows_Clustered_00000001 / _11111111 (compute; _11111111 holds the local-light hair path)10862954502888615639 / 6606735909222169407evidence note, hair reference §6.2
static m_classifyMaterials (compute)13401747477113664336evidence note
static m_simpleTemporal (compute; PQ-space mean ± σ history clamp, 5 % blend)525406595650312948experiment 018
static FSR2 / XeSS reactivity and DLSS colour-bias masks (compute)881371692195232951 / 15039989337359667650 / 7261057323686950841experiment 018

4. Template catalogue (characters and makeup)

Parameters and registers below are from the serialized 2.31 templates resource. Channel semantics are from compiled programs where cited source; everything else is marked. "Reg" is the cb4/usedParameters[2] register.

4.1 base\materials\skin.mt (and skin_blendable, skin_morph)

Class: Subsurface.

Passes: gbuffer_regular/velbuff (opaque), skin_translucency (depth only), cascade, highlights.

Vertex factories include MeshSkinned and the LightBlockers variants resource.

Every parameter, pass, program hash, the instance chains of the player head and body, and the full program walk-through are in the skin reference; the table below is the summary.

Parameter (reg)SemanticsGrade
Albedo (0)Base colour, UV0source
Normal (4), DetailNormal (5), DetailNormalInfluence (6)RG-packed tangent normals with Z reconstructed as sqrt(1−x²−y²). The detail normal (and the micro-detail) blend by slope-sum (partial-derivative) blending, lerped by influencesource
Roughness (8)R = base roughness. B gates a spatially varying bias between DetailRoughnessBiasMin/Max (9/10), giving saturate(R·(1+B·(bias−1))), which is written to GBuffer2.y. G is written to GBuffer2.x, the slot the light reads as metalness, and also makes the pixel non-porous when wet. Vanilla head G is zero; a skin texture with G above 0.1 turns metallic and loses SSS.source skin reference §4
MicroDetail (11), MicroDetailUVScale01/02 (12/13), MicroDetailInfluence (14)A 2:1 atlas of two micro-normal tiles at two frequencies, sampled with unwrapped derivatives; B of Roughness weights itsource
TintColorMask (15), TintColor (16), TintScale (17)Character-creator tone tint. Weight abs(TintScale)·Mask.R; a positive scale multiplies albedo by TintColor, a negative one overlays it. Mask G and B are microdetail selectors (tile blend and UV frequency), not tint. Per-tone values: head CC rendering §2source decompiled 12806642364631437234; wiki hair-and-skin-material-properties.md L28-38 (multiply only)
SecondaryAlbedo (1), …Influence (2), …TintColorInfluence (3)Overlay composited by Influence · SecondaryAlbedo.A, tinted toward the tone by TintColorInfluence; used by freckle and tattoo frameworkssource wiki
SkinProfile (24)Bound as a slot index 0–7 into a runtime table. The index is packed into GBuffer1.w and GBuffer2.w. The table holds each profile's dual lobe, its blur kernels (from falloff and blurSize) and its strength (diffuse); profile colours are sRGB-decoded on the CPUsource
EmissiveMask (18), EmissiveEV (19)Mask R × EV. If this exceeds 0.001, GBuffer2.w gets an emissive flag plus 6-bit intensity.source
CavityIntensity (7)Strength of a small additive darkening from Roughness.B, the micro-detail term and the detail normal's redsource
Detailmap_Stretch/Squash (20/21), Bloodflow (22), BloodColor (23)Wrinkle normals (RG) and a flush: BloodColor is added where Bloodflow.R × squash³ and Bloodflow.G × stretch³. The vertex program computes the two weights from UV-rectangle regions × animation float tracks (TextureRegionsCB, FloatTracksDataCB); both are 0 at rest. The regions are engine\materials\defaults\defaultfaceregions.regionset: 33 stretch or squash regions named after the facial setup's 33 wrinkle outputs (natural expressions §9.3)source; region set resource; that the engine feeds the solver's outputs through it hypothesis

Vertex colour. The wiki reads vertex colour as R = AO, G = SSS mask and B = "improved facial lighting" wiki (shader-docs.md L47-51). The skin writer stores GBuffer2.z = 0.4 + 0.6·G: its vertex program passes COLOR.y straight through to the interpolator the pixel program reads source (annotation results). The deferred light ignores GBuffer2.z for skin (only Foliage reads it, §2.2); the SSS translucency setup uses it as the weight of sun transmission, which is consistent with the wiki's "SSS mask" reading source.

Wetness. With rain (MaterialModifiersConsts[3]), the program darkens porous (rough) skin and makes smooth skin glossier, strongest on up-facing surfaces, with drops, streaks and drips inside 60, 30 and 15 m; dry skin is unaffected source (skin reference §5.7). Outdoor test captures should record the weather.

Roughness under eye makeup. On the pale default head the Roughness map's R channel reads about 0.57–0.66 at the vanilla eye-makeup vertices (h0_000_wa_c__basehead_rm01.xbm, before the detail bias) resource (experiment 017).

No specular antialiasing. Neither the skin writer nor mesh_decal derives roughness from normal-map variance. Decal textures are read with implicit-LOD Sample through one anisotropic, trilinear, wrapping sampler, so a texture's mip chain is the only filter a mod controls source (Glitter in game §1).

Gotcha. The Blender add-on maps Roughness G to Metallic and marks its microdetail masking as uncertain (material_types/skin.py:341,626). It uses Principled random-walk SSS, which is an approximation, not the engine path.

4.2 Eye family: eye.mt, eye_gradient.mt, eye_blendable.mt, eye_morph.mt

Class: Eye.

Passes: opaque G-buffer, velocity, cascade shadows (the eyeball casts sun shadows), depth, highlights and an editor pick pass; no translucency pass. eye.mt defaults: RoughnessScale 0.4934, RefractionIndex 0.97, EyeRadius 0.0152 resource. eye_blendable adds a braindance Fresnel rim to the colour and eye_morph a scripted wipe of the gradient; otherwise the four share one arithmetic source (eye reference §2).

ParameterSemanticsGrade
Albedo, NormalSampled at a computed, view- and eye-parameter-dependent UV. Normal is RG with reconstructed Z.source optics audit
RoughnessR × RoughnessScale, at the original UV, written to GBuffer2.y. The G channel is not used.source
NormalBubble, BubbleNormalTileCornea bulge normal (RG)source
IrisMask, IrisColorGradient (eye_gradient)The engine binds IrisColorGradient as a row index into a 512-row runtime gradient atlas, sampled at (mask R, (row + 0.5)/512): mask R is the position along the row. Mask A lerps from Albedo to the gradient colour.source exp. 014
RefractionIndex/Amount, IrisSize, EyeParallaxPlane, EyeHorizAngleLeft/Right, Egg*, IrisCoord*Parallax/refraction UV construction and the analytic cornea bulge. The index ratio 0.97 bends the view ray by about 1° at 30°, so the effect is nearly pure parallax 1.8 mm under a virtual 15.2 mm spheresource (eye reference §5.3)
SpecularityWritten to GBuffer2.x, the metalness slot; 0 in every vanilla and inspected mod materialsource
Blick, BlickScale, SubsurfaceFactor, AntiLightbleed*Read by no pixel program of the eye family in any pass or vertex factory, and by no static program. Blick is a cubemap. Inert in 2.31 unless a CPU-side consumer existssource (eye rendering §2.4)

Gotchas:

  • The Eye class stores a second vector, the iris normal, octahedral-encoded to 10 bits per axis in GBuffer2.zw plus two low bits each in GBuffer0.w and GBuffer1.w; GBuffer1.rgb holds the cornea normal. It disables the translucency term (§2.3). The full program, the 71 creator colours and the preview spec are on eye rendering.
  • IrisMask is a gamma texture, yet the gradient profiles are authored for its raw R range; the Rebecca eyes' normal map is a gamma texture too, and decoding its flat texels would tilt the iris normal 54° over the whole eye. Whether the engine decodes either is open (eye rendering §2.3).
  • Eye pixels are outside the SSS passes; the ambient pass multiplies their indirect light by 1 + cb6[9].w and can bypass their occlusion (eye reference §6.3–6.4).
  • The Blender add-on samples eye roughness G, contradicting the compiled R source. CCXL eye guides note the eye UVs are Y-flipped wiki (ccxl-eye-textures.md L267-271).

4.3 Hair (hair.mt family) — overview only

Class: Hair. There are three cooperating passes resource source:

  • hair_alpha_accum: alpha write mask only; alpha is accumulated atomically from Strand_Alpha R.
  • hair_basecolor_blend: additive; the two red-channel strand maps address a runtime gradient texture (the static cache names a HairProfilesGradients resource).
  • hair_gbuffer_solid: opaque, depth-writing.

The light builds an anisotropic tangent frame for the Hair class. The sun and every local light use the same Karis-style model; only the intensity triple differs (cb0[12] for the sun, cb0[13] for local lights), and neither reads the Scattering thickness in GBuffer2.z source. .hp profiles hold two gradient ramps (gradientEntriesID, gradientEntriesRootToTip).

See hair-shading.md for the detailed formula work, the hair reference for the template, passes, lashes, cap and what still blocks correct hair colour, and the lash follow-up and hair-profile resolution.

4.4 eye_shadow.mt / eye_shadow_blendable.mt — the eye's occlusion and tear shell, not cosmetic eyeshadow

Class: Standard, EMP_Front.

Pass: transparent_back_face, forward-lit, CULL_None, blend One/SrcAlpha, i.e. out = highlight + dst·alpha resource source.

Vertex factories: eye_shadow has MeshSkinned; eye_shadow_blendable has only MeshStatic and MeshExtSkinned resource. The blendable's pixel program is identical to eye_shadow's, and its Fresnel parameters are read by no program; it adds only a braindance glitch and fade on the vertex side source (eye reference §7.1).

Parameter (reg)Compiled semanticsGrade
Mask (4)R gives the shadow amount, saturate(Intensity·R^Exponent). G × WetnessRoughness gives a wet-highlight roughness in [0.04, 1]. B × WetnessStrength scales the lit highlight.source
ShadowColor (1), Exponent (2), Intensity (3)Colour is linearized with exponent 2.2. Alpha darkens the destination toward the colour's luminance.source
WetnessRoughness (5), WetnessStrength (6)Forward-lit specular added on top of already-lit pixelssource
AdditiveAlphaBlend (0), SubsurfaceBlur (7)Not read by this programsource

Naming gotcha. This template's name refers to the ocular occlusion/tear film. The Blender add-on reads Mask R as alpha and A as coat, which does not match the program. The vanilla user is the player eye mesh's wetness chunk eyeWetness_MAT (eyeshadow_base.mi: ShadowColor 125,58,58, Intensity 0.7, Exponent 0.8; female chunk 2, male chunk 1) resource. Only the luminance of ShadowColor is used, the highlight has no Fresnel, and the probe contribution is multiplied by zero, so the shell adds no environment reflection source (eye rendering §4).

4.5 mesh_decal family (post_gbuffer, all Standard class, MeshSkinned available)

Common parameters. Every member shares DiffuseTexture/DiffuseColor/DiffuseAlpha, UV*, SecondaryMask*, NormalTexture/NormalAlpha/NormalAlphaTex/UseNormalAlphaTex/NormalsBlendingMode(+Alpha), RoughnessTexture/MetalnessTexture with scale and bias, AlphaMaskContrast, RoughnessMetalnessAlpha, Animation* and DepthThreshold resource.

Compiled contract for mesh_decal source. Every program of the family, its passes and per-target blend states, and what each variant changes are in the decal reference:

  • Normal is RG with reconstructed Z.
  • Roughness and Metalness each read R × scale + bias, and they share one blend alpha (with GBuffer2.z): a decal cannot write roughness without also blending metalness towards its own value.
  • Texture UVs are transformed by UVScale/UVOffset/UVRotation (in half-turns) about 0.5 and then folded by sign·frac, so a UV window must keep the mesh inside one unit of the fold.
  • There are independent colour, normal and surface coverages. Colour and surface coverage are the squared contrast-adjusted colour-map alpha; normal alpha is not squared: NormalAlpha × (UseNormalAlphaTex ? NormalAlphaTex.R : colour-map alpha).
  • NormalsBlendingMode > 0.5 loads the existing G-buffer normal and composes the decal normal with it by reoriented normal mapping (Barré-Brisebois & Hill) in the plate's tangent frame, lerped by NormalsBlendingModeAlpha.R (white by default). Its normal alpha is also multiplied by saturate(50 − 50·z): a flat texel (z = 1) writes nothing, and full weight needs about 11.5° of tilt. Because the blend lerps the encoded normal, a partly faded facet keeps only about alpha × its tilt (a 5° facet keeps about 1°). NormalsBlendingModeAlpha below 1 lerps towards the decal normal mirrored in X and Y, so it must stay white. Mode 0 replaces the normal with the plate normal plus the map at the plain alpha. source (decompiled 16098255505177109230; see the Shimmer design and decal reference §4.5)
TemplateDistinguishing inputsRelevance
mesh_decalBase setCurrent makeup export route resource
mesh_decal_blendableAdds FresnelColor/Intensity/Exponent, VectorField, fade controlsmesh_decal plus the same Fresnel colour and distance fade as the recolour-blendable template (byte-identical vertex program), but with squared coverage, the UV transform and per-texel base colour: a better host for Colour-shifting than the current route (decal reference §5.2) source
mesh_decal_wet_characterBase setRain streaks: V scrolls with time. Writes surface alpha = 1.0 regardless of coverage. It would overwrite roughness across the whole plate. source
mesh_decal_double_diffuseGradientMap, UseGradientMap, SecondaryDiffuseAlpha/Color/IntensityBrows and 36 of the 114 lip style options. Same outputs and UV transform as mesh_decal; coverage (p + (1 − p)·s·I)²; colour = powder term + saturate(GradientMapIntensity · gradient) × texture RGB, so at intensity 2 gradient channels above 0.5 clip; DepthThreshold defaults to 0 (no depth test) [source: pixel 7624081209775720613; 8834363738920290566 is its Discarded twin]. See hair shading and eyebrows. resource
mesh_decal_gradientmap_recolor(_2)MaskTexture, GradientMap (or a .gradient in _2). DiffuseTexture is an ID map indexing the gradient.Recolour from one greyscale map: colour = DiffuseColor × gradient, linear, unsaturated coverage, no UV transform [source: 5232451138945967528]. _2 reads the engine's runtime gradient atlas (the eye's) and raises all four channels to 0.45 [source: 13292453303177655632]; add-on meshdecal.py:53
mesh_decal_gradientmap_recolor_blendableBase = DiffuseColor × GradientMap(DiffuseTexture.R, 0.5); Fresnel colour × intensity × w × saturate(|1 − N·V|^exponent) added before the square root. Colour coverage = DiffuseAlpha × gradient alpha × MaskTexture.R, linear (not squared). The vertex program sets w = max(1 + MaterialModifiersConsts[2].x − saturate((d − FadeOutOffset)/FadeOutDistance), 0), d = horizontal camera-to-object distance; the defaults remove the tint beyond 0.7 m.Colour-shifting export route, with the fade pushed to 1000 m source design
mesh_decal_parallaxHeightTexture, HeightStrengthA height-map ray search (linear march, then binary refinement) offsets every lookup: embossed depth for printed patterns; roughness and metalness raw R. The Blender add-on does not implement it. source
mesh_decal_emissiveDiffuseColor2, EmissiveEV, animation and scroll. Target 2 is additive (One/One, write mask BA).Glow accents, independent of light [source: 9453098283293843067]; over skin the additive write lands on the skin-profile and emissive bits, so the result there is not a clean emissive flag hypothesis (decal reference §5.4)
mesh_decal_emissive_subsurfaceEmissiveMask, EmissiveMaskChannel, EmissiveColor/EV. Stage subsurface_emissive.Emission only, no UV transform: colour = EmissiveEV × EmissiveColor (a plain multiplier, so EV 0 is black), opacity = MaterialModifiersConsts[2].x × mask, alpha-tested at AlphaThreshold [source: 8986576764202126900]. Whether that modifier is non-zero on a CCXL head component is hypothesis
mesh_decal_particlesFlipbook atlas (plain mesh_decal has the same time-driven flipbook controls)The mesh_decal arithmetic; not a glint model source
mesh_decal_morph, _revealed, _multitintedMorph, reveal/flow, 8 tintsScripted reveals driven by MaterialModifiersConsts[2].x, and eight per-vertex tints; all linear coverage source. Nothing for makeup

2.31 has no mesh_decal_normal template (the 16 compiled templates are mesh_decal plus __blackbody, _blendable, _double_diffuse, _emissive, _emissive_subsurface, _gradientmap_recolor(_2, _blendable, _emissive), _morph, _multitinted, _parallax, _particles, _revealed, _wet_character; the …_gradient name belongs to the braindance blackwall_blendable_mesh_decal_gradient); scars and face cyberware write their normals through plain mesh_decal [source: installed shader cache index]. Every vanilla female face decal uses mesh_decal or mesh_decal_double_diffuse resource.

4.6 Multilayered: engine\materials\multilayered.mt, multilayered_clear_coat.mt

multilayered.mt is Standard class. It is canBeMasked = 0, opaque and depth-writing, drawn in the depth prepass, with blending off on every target; its only transparency is the engine's whole-draw dissolve (§1.3) resource source. The game uses it for earrings and piercings, the 37 graphic eye designs, and most clothing, weapons and props. The family reference is the multilayered reference; line references are in the multilayered shader evidence.

Inputs:

  • MultilayerSetup (.mlsetup: an ordered list of layers, each naming a layer template, .mltemplate) and MultilayerMask (.mlmask: one mask per layer, stored as a BC4 tile atlas with tile tables) resource;
  • GlobalNormal with its intensity, UV scale and bias;
  • runtime-filled MaskAtlas, MaskTiles, Layers (StructBuffers), LayersStartIndex, SetupLayerMask and Mask* dimensions source.

Evaluated per pixel source. The G-buffer program (4792354088802328889, one program for MeshSkinned and MeshStatic) loops over the layers at every pixel; it does not sample a cache. The static m_surfaceCache_GenerateMultilayer compute program repeats the same arithmetic into cache pages, and the internal multilayered_baked.mt (9875025106086992270) reads them as a virtual texture (an indirection texture, 120-texel pages, a page-feedback buffer) source; when the engine switches a mesh to it is open. A variant pass adds rain and wetness on top at runtime; that is weather, not material. Layer textures are read with explicit gradients, and the normal and microblend gradients are scaled by NormalsTextureDDXYMultiplier / MicroblendsTextureDDXYMultiplier (default 1): a per-material mip bias for the maps that cause specular aliasing source (multilayered reference §5.4).

Layer values are name lookups. Each layer stores colorScale, normalStrength, rough/metalLevelsIn/Out as CNames that key into its template's override tables; its numeric fields are opacity, matTile, mbTile, microblend* and offsetU/V source (G/Multilayer_Layer.hpp:22-40). The tables hold {n, v} entries; colour names are <hex>_<hex> with null in either half, and null_null/null are ordinary entries present in every vanilla template resource. Each template also has a defaultOverrides selection and colorMaskLevelsIn/Out. The wiki gives up to 20 layers wiki (multilayered/README.md L27, L80).

How the layers combine [source unless marked]:

StepArithmetic
OrderFront to back: from the highest layer index down; the bottom layer (index 0) ignores its mask. A layer with no mask data at a pixel is skipped
CoverageMask m (bilinear, at frac(uv) with V flipped), crossfaded with the layer's microblend: mp = saturate(k + (m − k)·c), k = 1 − microblend.a; a = mp · opacity. c = 1 / microblendContrast [community + game capture; the CPU upload is unread]: a low contrast steepens the mask about k (the in-game ring eye designs need it, eye rendering §6.6)
ShareEach layer takes w = min(remaining, a) of the coverage still remaining. Not a lerp stack: two half-covering layers over a base give 0.5 / 0.5 / 0. Coverage left over is black, with zero roughness and metalness
MapsRead at offset + matTile · frac(uv) (U times the setup's ratio) in the game's own texture rows; microblend likewise with mbTile
Roughness, metalnessThe map's R through its levels as a clamped scale/bias chain: saturate(saturate(x·in₀ + in₁)·out₀ + out₁). The stored pairs are scale/bias (for example the input range 30…220 as (1.342, −0.158)) resource
Colourcolour map · lerp(1, colorScale, cm), where cm is the same chain on the roughness map through the template's colour-mask levels. Templates storing colorMaskLevelsOut = (0, 0) (most, including every earring template) must tint everywhere for vanilla gold and paint to look right; the CPU mapping is unread hypothesis
NormalsLayer normals add up by share; microblend normals blend in at the mask's edges (e = saturate(saturate(√(1 − 2·abs(mp − ½)))·opacity − Σa)); the two mix by the strongest microblend weight and are laid over GlobalNormal (strengthened by its intensity) with reoriented normal mapping
OutputGBuffer0 sqrt(albedo), GBuffer1 the normal, GBuffer2 (metalness, roughness, ⅓); roughness is clamped to [0.04, 1] only by the deferred light

Colour maps are sRGB (isGamma), roughness, metalness, normals and microblends linear resource. How community tools differ (the Blender add-on stacks bottom-up with a lerp, ignores the colour mask, and writes the program's contrast form as saturate((m − (1 − c)(1 − a))/c), the reciprocal reading; WolvenKit's preview paints flat colours) is recorded in the evidence note [community].

What the Studio draws. The preview's layered adapter (layered-material.ts) bakes this arithmetic once per material over the mesh's own UV range, accumulating in half floats, into two packed 8-bit maps (sRGB colour with roughness in alpha; the tangent normal with metalness in alpha) and lights them as a standard metal/rough surface. A bake is sized by texel density on the part's surface (about 20 texels a millimetre, 256–2048: a stud is 256², the eyeball 2048² as its UVs span several tiles), chunks with identical stacks and UV ranges share one bake, and a hidden slot is baked when first shown; five piercing chunks keep about 2 MiB of maps where they kept 160 MiB. Real-GPU tests match the bake to its CPU reference within one byte for constant and for tiled, offset, masked sRGB maps. It draws layered chunks on any slot whose render mask draws them in the scene: the V's piercings and the graphic eye designs so far (head CC rendering). The host clamps a setup's and a template's stored numbers to plausible ranges (a paint template's colour table holds up to 906 entries). A layer whose microblend is absent or unreadable is drawn over the values of base\surfaces\microblends\default.xbm (RGB (126, 127, 255), alpha 104/255) and noted, not over alpha 1: under the reciprocal contrast, alpha 1 let a low-contrast layer cover every texel above its contrast [offline; what the engine does with a layer that names no microblend is unread]. The rendered look is hypothesis until an in-game comparison.

multilayered_clear_coat.mt resource source:

  • The G-buffer pass is opaque, depth-writing and Standard class. It also stores a per-pixel coat amount, the summed shares of the layers in [CoatLayerMin, CoatLayerMax], as GBuffer2.z = 1/3 + ⅔·coat [source: 3911537533623547427].
  • A second unlit forward pass, depth test Equal, blends One/Src1Color (dual source): coat weight saturate((GBuffer2.z − 1/3)·1.5) × Opacity, adding a coat reflection (its own roughness ≥ 0.04 and Fresnel) and attenuating the base per channel [source: 5403688829342147459]. The coat therefore lands only on its own base pass's pixels (multilayered reference §6).
  • Parameters: CoatTintFwd/Side, CoatTintFresnelBias, CoatSpecularColor, CoatRoughnessBase, CoatReflectionPower, CoatFresnelBias, CoatNormalStrength, CoatLayerMin/Max, Opacity.
  • A community test shows angle-dependent tint on a garment wiki (multilayered-material-properties.md L121-133, image clearcoat_1.png/clearcoat_2.png, discovered by Rebecca).
  • Not an overlay: its base pass replaces the surface, and skin's Subsurface class with it.

Multilayered and makeup. On a face plate, any multilayered template would be an opaque Standard surface replacing the skin: no soft coverage, no SSS or skin lobes, no skin normal, black where no layer covers. Mixed finishes are already expressible per texel in a decal, which keeps the skin, and both are limited to one surface per pixel; the coat's second lobe cannot sit over skin. The makeup export therefore stays on the decal family source (multilayered reference §8).

4.7 engine\materials\metal_base.remt and glass_onesided.mt

Full reference: metal and glass. Both appear on arm cyberware (Gorilla Arms, Mantis Blades), the teeth mesh's unused default appearance, the accessories some CCXL hairstyles carry (earrings, hair pins) and many clothing items. The preview draws metal_base since 27 September (src/metal-base-material.ts, without the alpha test yet); glass_onesided has no adapter.

metal_base.remt is Standard class: a depth prepass, an opaque G-buffer pass (Index 1; Index 2 adds rain), and a post_gbuffer decal mode resource source.

  • Inputs source: saturate(BaseColor.rgb × BaseColorScale.rgb); metalness and roughness each from the R channel of their own map through saturate(x·scale + bias); an RG normal with Z reconstructed and XY × NormalStrength; every UV × LayerTile except Emissive. Defaults: grey colour, metalness 0, roughness 1, flat normal.
  • Output source: the plain Standard G-buffer (sqrt(colour), the normal with .w 0, metalness, roughness, 1/3), lit by the Standard deferred light and ambient composite (§2.3). Nothing about metal is special to the template: it is metalness and roughness.
  • Alpha test source: the Discarded variant discards where BaseColor.a < AlphaThreshold (0.38) when an engine flag is set; the cyberware decal chunks set enableMask 1 resource, and the wiki ties transparency to enableMask wiki (textured-material-properties.md L25); the link between them is hypothesis.
  • Emission source: only when EmissiveEV > 0: the colour lerps toward EmissiveColor × BaseColor by the emissive mask, and GBuffer2.w stores a flag and sqrt(EV/10) in 7 bits, EV = max(log2(mask) + EmissiveEV, 0); the ambient composite adds the albedo times a per-frame function of it (2^EV if its constant is 10·ln 2 hypothesis).

glass_onesided.mt writes no G-buffer. It is a forward transparent pass with dual-source blending, out = radiance + background × T per channel, plus a distortion pass and a transparent_mark_rt pass resource source:

  • Transmittance T = lerp(1, tint′·(1 − mask), Opacity), where tint′ is GlassTint × TintColor darkened and saturated toward grazing angles. It ignores the reflection's Fresnel source.
  • Reflection: F0 comes from FresnelBias (0 → 0.25, 1 (default) → 0.08, 2 → 0.04), not from IOR, which only the distortion pass reads. Roughness is saturate(GlassRoughnessBias + Roughness.R) (default 0). The environment term is Karis's analytic environment BRDF over box-projected probes; the direct term is a sun-only GGX lobe. The program has no local-light loop, so point, spot and photo-mode lights do not reflect in it source.
  • Distortion: active only when IOR > 1 or BlurRadius > 0. It adds a screen-space refraction offset (Snell's law 50 units along the ray, or a slab of RefractionDepth) and a blur radius source.
  • Gorilla Arms glass sets GlassSpecularColor to black: it reflects nothing, only tints (TintColor 240, 235, 228), refracts (IOR 1.32) and blurs (BlurRadius 1) the mechanism behind it resource.
  • glass.mt is the same with a back-face pass first (OpacityBackFace, BackFacesReflectionPower); its parameters are documented in wiki glass-material-properties.md.

Relatives:

  • pbr_simple.mt has scalar Color, Roughness and Metalness only resource.
  • metal_base_glitter.mt is noise/time emissive, not a glint BRDF source glitter investigation.
  • metal_base_blackbody.mt drives the Gorilla Arms thermal glow chunks (EmissiveEV 10) resource; not read.

5. Texture conventions

ConventionEvidence
Normal maps are two-channel (RG) with Z reconstructed in skin, eye, decal and multilayer global normals. Blue is ignored, and a DirectX orientation is expected. The compiled programs apply no Y flip, so orientation lives in the texture data.source (compiled RG/Z paths); wiki normal-maps-in-cyberpunk.md L18-32, shader-docs.md L37-43
Roughness and metalness are separate single-channel (R) maps in decals, with the usual meaning: white rough, white metal. Browser packed maps (G/B) must be split for export.source decal contract; wiki textures/README.md L84-102
Skin and eye roughness maps are multi-channel with template-specific meanings (skin R/G/B, eye R only). Three.js's default G sampling is wrong for both.source
Colour is stored as sqrt(linear) in the G-buffer. Whether a texture sample is sRGB-decoded depends on the XBM's isGamma / format. Diffuse maps use isGamma; normal and roughness maps are linear. Masks vary: the skin tint masks (h0_000_base__tintcolormask.xbm and a texture framework's replacement) are gamma textures, so their R weight and G/B selector steps are sampled sRGB-decoded; the preview's skin adapter follows each resource's flag.source (sqrt); wiki textures/README.md L60-62, ccxl-eye-textures.md L252
Textures are stored vertically flipped relative to PNG; WolvenKit's VFlip handles this.wiki materials/textures/README.md L28-29
Player head defaults. The female head mesh's own default appearance is the pale tone 01_ca_pale (D01 albedo). The eye mesh's default appearance is a metal_base.remt placeholder on all three chunks; the real eye colour is an appearance the character creator selects (e.g. gradient_brown, whose albedo is he_000_base_d02.xbm and whose colour comes from IrisColorGradient).resource 2.31 h0_000_pwa_c__basehead.mesh, he_000_pwa_c__basehead.mesh
Gradient and profile ramps (.gradient, .hp) are baked at runtime into gradient textures that the shaders address by row. The .hp bake is decoded from the executable: stops sorted and rescaled to span 0–1, sampled at k/N, interpolated on the 8-bit values and truncated, then sRGB-decoded into float32 rows read unfiltered (hair shading §3). The .gradient rows' bytes and filtering are unrecovered.source exp. 014, hair reference §7

6. Makeup finish implications

"Plate" means the owned morph-skinned eye-makeup mesh drawn over the head. Every decal route keeps the underlying Subsurface class (§2.4).

The Studio exports each preset as one decal draw, so per-texel inputs can vary within a preset but material constants cannot. The implemented routes are in finish-export.ts; each finish has a design page. None has been seen in game; the finish board is the prepared test.

FinishExport route (current)Why / limitsGradeSingle confirming in-game test
MatteFlat mesh_decal (@preset): roughness 1.0 (0.88 until 27 September), metal 0, colour and surface alpha on, normal offReplaces skin roughness under the mark (bare lid skin ≈ 0.6); skin SSS and skin's dual-lobe specular still apply. Its lobes are dimmer than bare skin's (they were at 0.88 too), so its written roughness cannot make it read glossier under direct light; vanilla matte lipstick writes the same 1.0, at 35–40 % weight.source route; runtime read glossy on the first board (with close-up breakup)Experiment 017 Gloss A–D
SatinSame, roughness 0.38Glossier than bare lid skin (≈ 0.6) and than vanilla eye makeup (0.50), and skin doubles the lobe; a calibrated 0.50 is on the next candidatesource; look hypothesisExperiment 017 Gloss D against Gloss A
Metallic / foilSame, roughness 0.27, metal 0.65At metal ≥ 0.1 the Subsurface class leaves its SSS albedo path. Reflections come from probes/SSR/RT, so metallic looks dark in dim or probe-poor scenes. Losing SSS also makes the skin under it read hard.source; runtime part of the uniformly glossy first boardExperiment 017 Gloss A–D (right outer stripe); Metal ramp · lifted for the 0.1 seam
Glossy / wetExperimental. Flat mesh_decal, one dielectric lobe at roughness 0.12 (game-matched model only)One G-buffer lobe: no clear coat, same F0 as skin, only sharper. The eye_shadow forward shell (Intensity 0, wet highlight from Mask G/B) remains the only stock way to add a second lobe; not built.source; look hypothesis; runtime not distinct from Satin on the first boardExperiment 017 Gloss A against Gloss D (0.24)
Shimmer / pearlExperimental. mesh_decal in NormalsBlendingMode 1 (@faceted): a uniform surface (roughness 0.32, metalness 0.3, a pigment-tinted lobe) and a one-texel grain of tilted normals (about a quarter of texels at 12° or more, the rest flat), roughness mips widened by the grain variance lost to averaging (Shimmer design)Every tilted grain clears the ≈ 11.5° mode-1 gate; flat texels leave the skin normal. Nothing static varies between texels, so any pattern must move with the light. At face framing grains are sub-pixel and the chain turns them into a broader, brighter lobe; close up they glint individually. Random azimuths make the untested green sign irrelevant statistically.source; [offline] experiment 030; runtime the facet bake it replaced read as a diffuse gloss by default and, at Shimmer · strong, as a static regular grid of oversized dots (per-facet metalness in 1.2–3.5 mm discs on a UV-cell lattice); the grain has not been in gameShimmer grain check: Shimmer · strong rebuilt with the grain
GlitterNone (preview only). Proposed: resolved glint flakes in mesh_decal through a plate-local UV window (4096×1024), with nested flake mips; fallback adds an emissive accent chunk (Glitter in game)No stock glint BRDF; one normal per pixel, no specular antialiasing, and a temporal clamp that removes one-pixel glints, so flakes must stay ≥ 2 texels wide at every mip. Board 2's coarse stripe was ~5.6 mm facets (sequins), not a glitter test. The accent's template multiplies its colour by EmissiveEV directly (0 is black; the diagnostic board writes 1 since 27 September) and gates it by an engine modifier (decal reference §5.4).source; route measured [offline]; look hypothesisExperiment 018 Glitter A and B
Colour-shiftingExperimental. Per-preset mesh_decal_gradientmap_recolor_blendable (@fresnel_<preset>): uniform base-colour gradient, linear coverage mask, roughness 0.32 and metal 0.08 via bias (0.25 until 27 September), FresnelColor from the chosen shift colour, exponent 2, distance fade pushed awayOne additive secondary hue by view angle, lit like base colour; not thin-film or multichrome. One constant per draw, so a colour-shift preset must be one pigment. At metalness 0.08 the covered skin keeps its SSS at any coverage (0.25 crossed the 0.1 switch at 40 %, Metallic's hardness); the tint itself needs no metalness. FresnelColor is written sRGB-encoded, since the engine decodes Color parameters (§1.2); MaterialModifiersConsts[2].x is unconfirmed.source; look hypothesisBoards 3 and 4: teal at the lid edges that follows the camera, not the light

Plate depth. Every route draws on the plate lifted 0.4 mm off the skin, like the vanilla face decals (§2.4 item 6).

Plate texel density and the UV window. The plate keeps the head's UV0, so a head-atlas texture spends only a 0.453 × 0.145 rectangle on it: at 1024 a texel is about 0.56 × 0.40 mm on the lids (569 and 405 mm per UV unit, area-weighted medians). The vanilla eye-makeup mesh has its own UV layout at about 66 mm per UV unit, so its 1024 texels are 0.065 mm resource (experiment 018). mesh_decal transforms every texture UV by UVScale/UVOffset from the UV as stored in the mesh source, so Build now maps just the plate's rectangle onto a 2048 × 512 texture for the flat and faceted routes: about 0.13 × 0.12 mm per texel at today's texel count and memory [offline]. The V sign follows from two offline facts: the Studio authors in glTF UV (1 − stored V), and WolvenKit's XBM import stores image rows bottom to top; so UVOffsetY is negative (−1.66 for the built-in plate) and the verifier checks the stored row order on every build. The Fresnel route's gradient-recolour template has no UV transform and stays on the 1024 head atlas. The window's placement has not been seen in game (experiment 019). Shimmer's grain is one texel of this window (0.13 × 0.12 mm).

Calibration against skin. Finish roughness sits on top of skin, not a Standard surface: skin's own roughness under the lids is about 0.6, vanilla eye makeup writes 0.50 at full surface alpha (and breaks its coverage with a ×30 noise mask), and the Subsurface class sums two GGX lobes at 0.97× and 1.6× the written roughness. On the first in-game board, Matte, Satin, Glossy and Metallic read uniformly glossy; experiment 017 separates "values too glossy" (Gloss D: all +0.12) from "written roughness not what you see" (Gloss B: surface alpha 0; Gloss C: roughness forced to 1) [hypothesis until that session]. CDPR's own makeup gives the calibration points resource: eye makeup 0.50 at full surface weight with noise-broken coverage, matte lipstick roughness 1.0 at 35–40 % weight, glossy lipstick keeping the skin's roughness (plus metalness 1 at 5 %), gold and silver blush metalness 1 at roughness ≈ 0.51 (if its sRGB-flagged roughness texture is read raw; ≈ 0.22 if decoded, face makeup open question 1). None of them writes one constant roughness at full weight over a whole mark, as the flat route does; that uniformity, which replaces the skin's pore-level roughness variation, is the decal-side suspect for the "plastic" read (decal reference §9.1) hypothesis.

What the preview predicts for Board 1 [offline; approximation of the light]. Since the authored plate is lit once with the skin's light (head CC rendering §3), Board 1 in the preview (default V, reference profile, both lighting presets, three studio key angles) reads much closer to the in-game report than before. Metallic becomes a bright, pale, sheet-like reflection as bright as the bare lid skin at the same pixels (0.89–1.12 of its median luminance, from 0.37–0.41) with 2.7–4.7 times the skin's highlight: at metalness 0.65 the reflection takes F0 from the pigment through skin's two summed lobes, and the SSS stand-in is off. Glossy shows 1.1–4.9 times the skin's highlight (0.5–2.3 before), and Satin about the skin's own (1.0–1.2), so it does not read as satin. Matte does not read glossy (at or below the skin's highlight), so the light alone does not explain the in-game report for Matte; the close-up breakup and the photo-mode per-light roughness shift remain the suspects there. With Gloss D's values (all +0.12) the preview lowers highlight peaks by 15–30 % on Satin and Glossy and Metallic's highlight by about a quarter, but moves the broad sheen by under 10 % and leaves Metallic as bright: D alone is not predicted to make the board read matte and satin, and for Metallic the metalness is the larger lever. Statistics are per-stripe percentiles of private captures (tools/plate-light-look.ts); the in-game Gloss A–D frames decide.

Cross-cutting guidance for the browser preview:

  1. Blend decal colour in sqrt space, and apply the decal's squared coverage curve. The preview does, for the V's own face decals and for the authored plate, whose layers blend as the export's one merged decal (head CC rendering §3).
  2. Use F0 = 0.04 for non-metals, and treat roughness as perceptual (α = r²).
  3. Light makeup on skin with the skin's light: renormalised Burley diffuse and the profile's two lobes, once, on the blended surface. The preview does for the skin, the face decals (per pass) and the authored plate (once, on the blended surface; head CC rendering §3).
  4. Present Glossy as the one lobe the game can draw (the game-matched model does); a clear coat would need a second, forward pass.
  5. Sample skin roughness from R, with B gating the bias, and eye roughness from R × scale.

7. Open questions

These become backlog items.

  1. GBuffer2.z for skin. Answered source: the SSS translucency setup (11387959167119825062) weights a sun transmission term by it; the static index's other GUID under that name (2220067148481019988) is a sun-light program. Still open: which setting enables the translucency variant, and what its camera vector and per-pixel factor are (skin reference §6.4).
  2. Stencil write. Confirm that the engine writes materialType into stencil bits 5–7, via a runtime capture or the pass stencil setup code.
  3. Skin-profile table. The table holds 8 slots per frame; roughness0/1 and lobeMix form the dual-lobe specular kernel (§2.3), and the CPU builds each slot's blur kernel and strength from blurSize, diffuse and falloff (answered source: skin reference §6.3.2). Open: the kernel's absolute screen scale; whether the separable or the stochastic blur runs at a given setting; what happens with more than 8 distinct .sp on screen; which lights set the per-light roughness byte, and to what (photo-mode lights in particular).
  4. Metal ≥ 0.1 threshold. The SSS blur and combine skip pixels whose blended metalness exceeds 0.1 source; the flag controlling the light's albedo = 1 path is unknown. Does metallic makeup over skin show a visible SSS seam at soft edges (the lifted metal ramp on the next candidate)? The preview switches its SSS stand-in off on the authored plate's blended metalness, so Metallic crosses it at about 15 % coverage over dielectric skin; the albedo = 1 path is not modelled.
  5. Eye second vector. Answered source: it is the iris normal from the Normal map at the refracted coordinate; the light uses it for Lambert diffuse and the specular horizon, and the cornea normal for specular and ambient. No program reads Blick, SubsurfaceFactor or AntiLightbleed*. Remaining eye questions are on eye rendering.
  6. eye_shadow as a gloss shell. Is it usable as a glossy-makeup shell (sorting, transparent_back_face stage semantics, blink, cost)? (Its vanilla user is the eye mesh's wetness chunk.) Known from the programs: only eye_shadow itself compiles for MeshSkinned; with Intensity 0 it adds a direct-light-only GGX highlight with no Fresnel and no environment reflection, so a wet look would show only under direct lights (eye reference §7.2).
  7. Runtime gradient atlases. Recover the .gradient atlas construction (row assignment, colour space, filtering, and whether stops are rescaled like .hp). This affects the gradient eyes and mesh_decal_gradientmap_recolor_2. The .hp bake is decoded (hair reference §7); brows sample an ordinary gradient texture, not an atlas (eyebrows). A clue: mesh_decal_gradientmap_recolor_2 reads the eye's atlas and raises all four channels, alpha included, to 0.45, which suggests the atlas stores its channels with a 2.2 power (decal reference §5.3).
  8. Unmapped debris. Recover the G-buffer render-target formats (8-bit vs 10-bit precision affects the sqrt encoding) and the meaning of the low 5 stencil bits.
  9. Ray-tracing libraries. Parse the ray-tracing libraries in staticshader_final.cache. Their names survive (ShadeSurfaceWithLightSample*, GBuffer0..2), which could confirm the BRDF and G-buffer semantics independently.
  10. Multilayer levels. Answered source: the pairs feed a clamped scale/bias chain (saturate(saturate(x·a + b)·c + d)), identical in the G-buffer and surface-cache programs. For multilayered, game captures of the ring eye designs now support two CPU mappings: microblendContrast enters as its reciprocal, and colorMaskLevelsOut = (0, 0) tints (the ring's red comes from a template storing (0, 0)) [game capture, 27 September 2026, eye rendering §6.6]. Still open: whether colorScale is linearised and matTile multiplied by tilingMultiplier, and when the surface cache is used (evidence).
  11. Color parameter encoding. Answered: sRGB-decoded RGB, linear alpha [source; runtime-supported]. The executable's material constant-buffer packer converts every Color parameter's RGB through the engine's sRGB-to-linear table and its alpha as byte/255 (§1.2). Session 4's skin-tone frames agree: senna amber and limestone against warm ivory darken the skin more than byte/255 can at full mask weight, and limestone's blue-to-green ratio rises where byte/255 must lower it; the sRGB fit holds even with a free tone-curve exponent per region [runtime, provisional: Ultra+'s RT skin tuning was on and the frames are display-referred; experiment 029 §4.2]. The preview decodes every Color it reads this way (skin and teeth TintColor, decal and brow colours, the eye shell's ShadowColor before its own 2.2 power), and the Colour-shifting export writes FresnelColor sRGB-encoded. Still open from the old question: what is MaterialModifiersConsts[2].x on the player head? It also gates the opacity of mesh_decal_emissive_subsurface (the glitter accent), and [0].z/.w drive the Discarded dissolve.
  12. Debug views. Can the engine's EEnvManagerModifier debug views (G-buffer, Roughness, Metalness, Translucency, MaterialID) be enabled from CET or RED4ext? That would turn most questions here into one capture session.
  13. Decal depth bias. What depth bias does OFFSET_DecalBias apply, and does it explain why a coincident decal failed only close up?
  14. Temporal filtering and upscalers. Which program is the main anti-aliasing? Does the engine apply a negative texture LOD bias under DLSS, FSR3 or XeSS? How do the upscalers treat stable 2–3 pixel glints? Tracked in Glitter in game.
  15. Decal normal source. A mode-1 decal composes with a copy of the G-buffer normal (t74). Is it refreshed between decal draws, so that overlapping normal-writing decals compose with each other rather than with the skin (decal reference §4.5)?
  16. Glass and local lights. The glass_onesided transparent program reflects only the sun and the probes; it has no local-light loop source. Confirm with a photo-mode light moved across the Gorilla Arms window, and settle what enables metal_base's alpha-test variant and its post_gbuffer pass (metal and glass §8–9).

8. Custom shaders: from template to pipeline state

How the engine turns a material template into GPU work, for the XF Shaders track (our own material shaders). Everything here is offline evidence from the 2.31 executable, CD PROJEKT RED's address library and the cache file; the full trace with addresses, IDs and grades is the pipeline trace, and exe_shaders.py re-proves it. Nothing is confirmed in game yet; the probe plan is the test.

8.1 How a technique is found source

  • One cache, one lookup key. shader_final.cache is opened once at start-up; only its RDHS magic is checked on load. A compiled technique is found by a 64-bit key: fold32(FNV-1a-64(template name)) << 32 | permutation, where the permutation is FNV-1a-32 over the vertex factory and its Discarded/PreSkinned/Dismembered flags, the technique index, the render-stage name and the pass index. The recipe reproduces all 19,647 records.
  • The name is the template's name CName, not its file. A copy of mesh_decal.mt that keeps name = mesh_decal reaches mesh_decal's programs; a copy with a new name finds none.
  • Programs are per template. Each of the 19,037 programs belongs to exactly one template name, even when bytes are identical elsewhere. A record names a vertex program and an optional pixel program by GUID; each program names the engine modifier slots it reads (its parameter set).
  • All programs are vs_6_0/ps_6_0, signed, with no embedded root signature.

8.2 From technique to pipeline state source

  1. A draw binds a material for a pass; the render template keeps its own technique map and compiles on a miss (key lookup, then the pixel and vertex programs).
  2. Each program becomes a GpuApi shader, deduplicated by an FNV-1a-32 hash of its bytecode. The bytecode is referenced, not copied.
  3. The command list collects the shaders and the pass's states into a D3D12_GRAPHICS_PIPELINE_STATE_DESC. At draw time it hashes the state fields (not the bytecode), seeded by the shaders' IDs, and resolves a PSO from an 8,192-entry cache.
  4. A missing PSO is loaded from an ID3D12PipelineLibrary by a name derived from the shader hashes, else created with ID3D12Device::CreateGraphicsPipelineState, synchronously or as a CompilePSOAsync job.
  5. One global graphics root signature, built at start-up, serves every material draw; bindless textures sit in t0, space1.

8.3 What this means for custom shaders

  • The cleanest detour is the cache reader's two lookups (find technique, get program). A copied template with its own name is recognised by its key; the plugin answers with the original's record and its own pixel program under its own GUID, and everything downstream (shader, PSO, pipeline library) separates by itself. Vanilla users of the same template never see it [source for the path; the design is a proposal].
  • A replacement pixel program must match the original's input and output signatures, stay within the global root signature's bindings, read material constants from cb4 at the template's registers, reuse the original's parameter set, and be signed SM 6.0 DXIL.
  • Shipping shaders natively is not possible additively: there is one cache file and no mod path. A plugin-read side cache in the same format is the practical form of "a package ships compiled shaders".
  • Every function on the path has an address-library ID, so hooks need no pattern scanning, as with ArchiveXL; object offsets remain version-bound and must be gated.

In-game test asks

Batch these into one prepared session with fixed camera, FOV and light, and record game and framework versions.

  1. Finish board (first photo-mode session held 25 September). The finish board packaged asks 1–3 as six selector presets through the production pipeline, with its own test card: flat finishes and single-lobe Glossy (Board 1), faceted Shimmer and a coarse glitter proxy (Board 2), and the Fresnel colour shift with its strength-0 control (Boards 3 and 4).
    • Capture a camera orbit under fixed light, then a light sweep under a fixed camera, at close and face framing, with a blink.
    • The eye_shadow gloss shell (Glossy route 2) is not on the board; it needs a second component.
  2. Sqrt-space blend check. Board 5: black Matte patches at 25/50/75 % coverage under the eyes, contrast 0, captured in photo mode with fixed exposure.
    • Compare measured luminance with linear and sqrt-space predictions.
  3. Metal threshold edge. Board 6: a metalness ramp 0 → 0.3 and five steps (0.05–0.3) over Satin.
    • Look for a visible seam at 0.1.
  4. Plate depth and gloss (partly run). Experiment 017 packages the depth comparison (0, 0.1, 0.2 and 0.4 mm lifts), the gloss controls (surface off, roughness forced to 1, roughness +0.12) and a stronger Shimmer, with its own test card. Session 2 (26 September) settled the depth ask: at an extreme close-up 0 mm breaks up while 0.1 and 0.4 mm stay clean, so 0.4 mm stays the default runtime (experiment 020 results). Under a controlled photo-mode light (session 4, 28 September) runtime: Gloss C (roughness forced to 1) is clearly the flattest, so the written roughness is what shows. A, B and D look wet, A the most, and their stripes don't separate at face framing. Shimmer · strong reads as a regular grid of dots far too large ("at the scale you'd see under a microscope"), so it needs randomised, much finer facets or a sheen-first design; the Metal ramp's stripes are its own test pattern (experiment 029 §3.2). It replaced an earlier backlit-translucency ask for the eyelids, which stays unnecessary: the translucency term needs thin geometry, which eyelids over the eyeball are not.
  5. Back-lit ear. Answered in session 4 runtime: translucency runs. A photo-mode light behind and beside the head makes the whole ear glow red-orange, redder than the directly lit neck (ear R/G 1.42 against 1.04), and the no-light frames are neutral. This was with Ultra+'s Character Subsurface Translucency toggle on, so that toggle is a dependency. The light was a local photo-mode light at midnight, while the decoded translucency setup has only a sun term, so local lights reach a transmission path too [hypothesis for which] (skin reference §6.4, experiment 029 §4.6).
  6. Debug views. If a debug-view toggle is found (open question 12), capture Albedo, Roughness, Metalness, Translucency and MaterialID views of the plate once.

Related: Glitter in game · mesh-decal contract · glossy feasibility · colour-shift feasibility · multilayered assessment · multilayered evidence · glint feasibility · finish taxonomy · skin trace · eye/lip optics · backlog. Community sources are acknowledged in the community credits.

Corrections and credit

This page is generated from knowledge/materials-and-shaders.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.