Notes behind the recommended values#
Caveats that took rounds of review or in-game testing to settle. The fits live in shader_fit/.
Gamma blending#
ksDiffuseis a plain multiplier in the Gamma build. 0.225 is the neutral: under LCS it is the value wherepow(texture × k × 2 / 0.45, 2.2)becomes a pure sRGB decode (_gamma.fx:23-37), and it looks right in game.- On opaque surfaces, fresnel weights and highlights are physical amounts, as in commit
9140a9c:fresnelC≈ F0, and highlights are relative to a sunlit white surface, clipped at it, the fitted highlight as well as the target. Matching gamma-encoded pixel brightness instead inflates both on dark textures; in game that looked like a glass coat at grazing angles and made tyres and plastics grey. Clear-coated paint and glass are the exceptions (see Clear coat and Glass below). ksSpecularis relative to the 0.225 neutral and assumesextSpecularColoris the sun colour; the page’s rule for another neutral follows from that.
Opaque surfaces#
- The shader dims the whole lighting by
1 − fresnelbefore adding the reflection, the sun highlight included (utils_ps.fx:669-675). The reflector keeps only an eighth of its highlight, hencesunSpecular2.7. fresnelMaxLevelis a soft cap (smin,utils_ps.fx:550-554). WherefresnelC≥fresnelMaxLevelthe weight sits near the cap at every angle andfresnelEXPbarely matters.- AC’s Blinn highlight has no Fresnel or geometry term, so it cannot brighten toward grazing. Fitted lobes run
0.31–1.7× the clipped physical peak at 22.5° incidence and 0.03–0.59× at 72.5°; 17a, a smooth coat on
_NMDetail, reaches only 0.06× and 0.01×. - Exponents are chosen with overshoot counted, then intensities are fitted with the highlight clipped at white.
Choosing exponents with the clip too builds flat white discs from absurd intensities (
sunSpecularin the millions on rough metal), which Pure’s HDR would bloom. ksSpecularEXPalso picks the cubemap mip,6 × (1 − EXP / 255)²(utils_ps.fx:621-622, 701). Exponents fitted to the highlight made glossy plastic a mirror in game, soksSpecularEXPis255 × (1 − roughness^0.05): the mip CSP’s PBR shaders pick for the same roughness,6 × roughness^0.1(pbr.hlsl:349-351). That curve holds whenpbReflectionBlurEnvis 0, asMaterial_Glassdefines it (smGlass_ps.fx:88);nePBRmaterials set it per material (nePBR_MultiMap_NMDetail_ps.fx:80, 257).- With
ksSpecularEXPfixed, the fit sets its intensity and, on plain MultiMap, the sun lobe’s intensity and exponent._NMDetailandksPerPixelReflectionhave no sun lobe, so their highlights are wide and dim.ksTyreskeeps a fitted exponent: it always samples the blurriest mip. - Those mips, ≈5 and up, shimmer on metals and smooth plastic in game. The shimmer is accepted over mirror-like
reflections; sharper mips also show stripes on curved chrome (
utils_ps.fx:425-427). - The sun disc drawn into the cubemap isn’t modelled. Near-mirror materials (reflector, black gloss, glossy plastic) likely overshoot their highlight by its brightness.
Shader families#
ksPerPixelMultiMap_NMDetail,_AT,_AT_NMDetail,SimpleRefland the skinned variants have no sun lobe and skipextSunSpecularMult(NO_SUNSPEC_MAT,utils_ps.fx:1164).- A white
txMaps.grendersksSpecularEXP + 1on MultiMap (utils_ps.fx:886).ksPerPixelReflection,ksPerPixelNMandksTyresscalefresnelEXPbylerp(0.8, 1, fresnelMaxLevel); MultiMap usesmax(fresnelEXP, 1)(:595-599). The page’s conversion rule comes from these two lines. isAdditiveskips the diffuse dimming for any non-zero value onksPerPixelReflectionandksPerPixelNM, but only for1on MultiMap, where2changes the reflection blur divisor from 255 to 8 (:620-621, :670).ksPerPixelMultiMapand_NMDetailalways output alpha 1 (ksPerPixelMultiMap_ps.fx:58); they can’t be glass.
Materials#
No environment in the opaque values#
The opaque fresnel is fitted straight to the physical reflectance at each view angle (env_brdf, model.py), and
the highlight to the physical sun highlight, so neither sky nor environment brightness enters the opaque values.
The clear coats (1c, 3c, 17a) are the exception: their fresnel matches the on-screen look, which takes the sky and
environment levels in Runtime assumptions. Enclosure depends on where a surface sits, not on its material, so it
isn’t in them either.
Shader per material#
The Shader rows follow how the cars are built:
- Normal-mapped MultiMap materials (leather, vinyl, fabrics, rough plastic, rough paint) use
_NMDetail; smooth ones use plain MultiMap. - Chrome stays on
ksPerPixelReflection: badges don’t gettxMapsUV work. - Rough bare metal (sanded, blasted) has no grain, so no detail map: plain MultiMap, with its sun lobe. Powder coat is a polymer layer, F0 0.04, so it is paint (17b), not metal.
- Rubber isn’t put on MultiMap.
Grain in detail normal maps#
- Grain lives in
txNormalDetailatdetailNormalBlend1;txNormalon_NMDetailis a flat placeholder. The shader decodes the stored blue as z (utils_ps.fx:1128), so the decoded slopes are what render. - The lobe carries only what the map doesn’t:
α_finish² = α_total² − σ², with σ² the map’s slope variance (shader_fit/normal_maps.py). Rough plastic, powder coat and Alcantara use their samples innm_samples/. - The detail normal is weighted by
detailNormalBlend × (1 − txDiffuse.a)(utils_ps.fx:1112, 1136). The fits assume diffuse alpha 0; where it is higher, the grain fades and the grain-free lobe is too glossy. - Mips average the grain away, and nothing in
_NMDetailwidens the lobe to make up for it, so the grain-free roughness is right close up and too glossy far away. - Either leather map alone is rougher than a whole 0.40 leather, so the leathers keep their coat roughness (0.35, 0.47) instead of a subtracted one.
cloth.ddsstores blue as a constant 0.71, which decodes as z 0.42, so its weave alone is rougher than the fabric’s whole 0.90. It stays as-is by choice, and natural fabric keeps 0.90.
Metals#
ksDiffuse stays 0.225 and the texture carries the diffuse: near-black (albedo 0.02) plus dirt and oxide.
Clear coat#
3c, 17a and varnished wood (1c) assume a clear coat at roughness 0.08; powder coat (17b) has none. On 17a the coat
follows the grain, so the map keeps the grain and the lobe is the coat’s own roughness. The sun becomes a small spot
that clips to white, which only plain MultiMap’s sun lobe can draw: 3c and 1c get it, while 17a on _NMDetail draws
just 6% of the physical highlight.
ksSpecularEXP stays on the blur rule, 30, so reflections are mip 4.7: sharper than before, not mirror-like.
The clear coat’s fresnel matches the reflection’s on-screen look over the darkened paint (screen_cases,
model.py), not its physical amount: the gamma-space blend hides a physical 4% on dark paint. That is the glass coat
the other materials avoid, and on clear coat it is the real thing.
A clear layer darkens what is under it, as water does: about 60% of the light leaving the base reflects back down
off the layer’s underside, and the base absorbs part of it again (coated_diffuse_share, model.py, n 1.5). The
shader’s 1 − fresnel covers only the exit angle, so ksAmbient / ksDiffuse carry the rest: 0.40× the base for
black, 0.43× for paint (albedo 0.10), 0.46× for wood (0.20), 0.77× for white (0.80). Those are linear shares, and
ksDiffuse scales the texture before the 2.2 decode (Gamma blending above), so it takes the share to the 1/2.2:
0.225 × 0.43^(1/2.2) = 0.153 for paint. One multiplier per section takes a typical albedo, so lighter colours come
out a little dark; the extra saturation a coat gives isn’t in it.
It applies to the clear-coated sections and tyre shine (23a), for textures holding the bare colour. A texture taken
from the coated object already has the darkening, so it takes 0.225. Black gloss (22) is too dark to change.
Metal flakes#
Metallic paint is 3c or 17a plus aluminium flakes (reflectance 0.91) under the coat (flake_highlight,
model.py). Their tilt is assumed, about 10° (GGX α 0.18), since nothing gives it. Refraction widens their lobe by
roughly the coat’s index; the model takes that and the coat’s Fresnel terms at small angles, so grazing views are
approximate. ksSpecular is the flakes, 1.0 on both, fitted unclipped at full coverage: the txDetail alpha mask
scales it linearly in game (utils_ps.fx:1004, 885; :1144 on _NMDetail), as coverage does physically. At
ksSpecularEXP 30 it explains 80% of the flake highlight. The coat’s own ksSpecular is 0 on 3c; 17a’s 0.016 is
replaced by the flakes’ 1.0, so its coat highlight is dropped everywhere, a negligible loss.
- Pearl isn’t covered: mica pearl pigments reflect far less than aluminium, so 1.0 overstates them.
- The glitter renders white; real flakes glitter in the paint’s colour.
- Flakes take the pigment’s place, but
ksDiffusestays, so a high mask shows full pigment plus full flakes. ThetxDetailRGB multiplies the gamma-encoded paint colour where the mask applies (:1003, :1143), while the mask alpha is linear coverage, so removing the pigment takes RGB ≈ 255 × (1 − alpha)^(1/2.2): 186 at alpha 0.5. A non-white RGB tints or speckles the paint either way. - The detail needs
useDetailon (:1001) and only shows wheretxDiffusealpha is below 1: it’s weighted by1 − txDiffuse.a.
Glass#
Blending#
- Glass is alpha-blended: everything the shader outputs, reflection and highlight included, is scaled by the output
opacity (confirmed in game).
isAdditive0 raises the opacity tolerp(alpha, 1, fresnel)(utils_ps.fx:676-677). - A pane can’t be both clear and reflective: a reflection only shows by blocking what is behind it.
- So glass, unlike opaque surfaces, is fitted to the gamma-encoded brightness of the whole picture: background,
reflection and texture together. At
fresnelC≈ F0 a pane’s reflection would be invisible; the fits land at 0.23–0.67. - Shader values are shared by all tints of a group, clear or coloured; only the texture’s RGB and alpha change.
Colour needs alpha, which already hides the background, so coloured glass takes a weaker reflection (
fresnelC0.23 against 0.67 on a clear lens). - Each group takes the
isAdditivethat fits it better: 1 for clear lenses and prisms, 0 elsewhere; projector lenses are pinned at 1 (Projector lenses below).isAdditive0 makes the glass more opaque where it reflects, so the texture colour shows there even at alpha 0. Clear glass gets a grey texture either way, as on Kunos cars; it is lit like paint and fades at night, the reflections don’t. - A lamp interior is lit through its own lens, so its light crosses the lens twice. The cabin behind a window is lit by the car’s lighting and crosses once.
ksSpecularEXP20000 because 800 drew a giant blob in game. NoksSpecularmatches both the sun glint’s brightness and its size at that exponent, so it is set to clip to white across the sun’s 0.53° disc, taking a sunlit white surface as display white. It is set on a group’s clearest tint; the glint is scaled by opacity, so on more opaque tints it clips wider than the disc (red about 1.5× brighter than yellow).- A pane can’t transmit more than
(1 − R0) / (1 + R0), 0.918 at n 1.52;pane()rejects more. - A scratch is modelled as a frosted spot: it scatters 9.5% of the light back before the glass’s own absorption
(
FROSTING_REFLECTANCE, the diffuse reflectance of a surface at n 1.52). The page gives the texture at the scratch peak.
Projector lenses#
A projector lens is polished, but focused on the cutoff shield deep inside, so from outside the bowl blurs. AC can’t
blur, so alpha hides the bowl mesh behind the lens as far as the blur washes it out. Alpha is set by eye, not fitted:
0.75 for a heavily blurred low beam, 0.50 for a lightly blurred fog lamp. The texture is set by eye too, at 50 grey:
the lens carries the lamp’s emissive, ksEmissive × texture (utils_ps.fx:811, 817), and the near-black grey a
daylight fit picks (2–8) leaves the lit lens too dim to hide the bowl showing through it. isAdditive is pinned at 1:
the emissive is part of the lighting that isAdditive 0 dims by 1 − fresnel (:970, :670-673), which on the low
beam left a lit lens at 77% head-on and 20% at 85°, a dark rim. With those pinned the fit sets the reflection; it is
23% off for the low beam and 14% for the fog lamp.
ksWindscreen#
The inner window mesh has no fresnel parameters. Texture alpha below 0.5 only shows toward the sun
(windscreenBase.hlsl:66-96), and reflections come only from FLAG_MATERIAL_2, a fixed fresnelC 0.1.
Material_Glass#
- Reflections show at F: the shader divides the fresnel by the opacity (
smGlass_ps.fx:232). fresnelCis F0 of Schlick at roughness 0. Texture alpha becomes Beer–Lambert absorption throughextThicknessMult(:221-228). The diffuse is ×0.4, henceksDiffuse0.56.ksSpecularis unused; the shader’s own highlight is a GGX at roughness 1e-4.- The sunlit texture gets Burley diffuse, which halves toward grazing views at roughness 0 (
pbr.hlsl:240). - The fit’s path length uses n 1.52; the default
extIORis set in CSP.
Coloured prisms#
ksPerPixelNM takes alpha from the normal map’s alpha channel (ksPerPixelNM_ps.fx:17). Alpha blending can’t
filter colour, so a coloured tint hides most of the lamp interior behind a dark red, amber or yellow layer; the
coloured fit is 26% off. ReflectionsFX refraction isn’t modelled. The coloured prisms take 18b’s coloured values:
the fit sees the same lamp interior behind both.
Terrain#
ksMultilayer_fresnel_nmhas no cubemap: the fresnel block sets the highlight intensity from the view angle alone and replacesksSpecular(ksMultilayer_fresnel_nm_ps.fx:77-88).- Fitted freely, rougher terrain comes out with a sharper highlight to cut glare away from the sun. No type may be
sharper than new tarmac, so all sit at
ksSpecularEXP46. - The errors are 46–58%: the streak toward a low sun reaches 58–61% of the physical one, and the faint sheen away from the sun is lost.
- The glare isn’t clipped at white in the fit: every parameter is free, and with the clip new tarmac’s multiplier climbs from 2.0 to 5.0 for an error under 2% lower.
Tyres#
ksTyressamples the cubemap at the blurriest mip whateverksSpecularEXPis (utils_ps.fx:504-505), and fades the reflection bysaturate(normalW.y + 1)(ksTyres_ps.fx:93).- The diffuse alpha scales only
ksSpecular(ksTyres_ps.fx:83), so lettering renders with the tyre’s exponent and fresnel, and its alpha is fitted at the tyre’s exponent.
Runtime assumptions#
- Pure’s
extLambertGamma,extFresnelGamma,extSunSpecularMultandextSpecularColorrelative to the sun weren’t found in its code or in game; the fits assume 1 for each.extSpecColor, seen at 1, 1, 1, issmCarPaint’s per-material specular tint (smCarPaint_ps.fx:44, 143), not the globalextSpecularColor(cbuffers_common.fx:186). - The glass and clear-coat fits take sky light as 0.2 of a sunlit white surface and the reflected environment as 0.1–0.3; Pure sets that ratio.