Источники Альфы 🔗
ksPerPixelNM+AlphaTest:txNormalksPerPixel_AT:txDiffuseksPerPixelMultiMap:txDiffuse, альфаtxDetailигнорируется
ksFlags 🔗
Флаги используют UV Map для анимации. Чтобы это работало правильно, углы в развёртке должны соответствовать углам в меше. Левая грань развёртки всегда будет считаться стороной флагштока. Флипы работать не будут, т.к. AC всегда использует развёртку Left -> Right, Top -> Bottom для анимации флагов.
Такая развёртка будет работать верно:
TODO
Такая развёртка будет работать неверно:
TODO
Darkening via ksAmbient / ksDiffuse 🔗
They do different things, and choosing the right one matters.
For AO, 0.26 -> 0.23 -> 0.19, depending on how heavy the baked AO is. For materials where baked AO is common (interiors, detailed mechanical parts, anything with geometry that creates crevices), I’d knock ksAmbient and ksDiffuse down by about 0.02–0.04 from my recommendations. For flat surfaces where AO is minimal or absent (road surfaces, flat panels, glass), the values are fine as-is.
-
Lowering
ksDiffusedarkens the material in direct sunlight but leaves it unchanged in shadow/ambient. The material would look dark in the sun but still bright-grey in shaded areas or indirect light. -
Lowering
ksAmbientdarkens the material in ambient/indirect light but leaves the sun-lit contribution unchanged. The material would look dark in shadow but still bright in direct sun.
For a material that’s too bright overall, you’d lower both equally. A texture that appears bright-grey at ksAmbient = ksDiffuse = 0.22 means the texture itself is authored too bright. Dropping both to something like 0.16–0.18 would darken it uniformly across all lighting conditions.
However, there’s a better conceptual framing: rather than thinking of these as “brightness knobs,” think of them as controlling how much the material responds to each type of light:
-
A material with
ksAmbient > ksDiffuselooks relatively bright in shadow compared to its sunlit side — it has a “flat” look with less contrast between lit and unlit faces. This suits materials like fabric or foliage that scatter light into shadow via subsurface or multi-bounce. -
A material with
ksDiffuse > ksAmbienthas higher contrast between sun and shadow — it “pops” more in direct light but goes darker in shade. This suits hard, opaque surfaces like metal or hard plastic. -
A material with
ksAmbient ≈ ksDiffusehas neutral light response — the balance between sun and shadow is determined purely by the scene lighting.
For tyres specifically, if the texture is too bright, I’d recommend:
| Approach | ksAmbient |
ksDiffuse |
Effect |
|---|---|---|---|
| Darken uniformly | 0.17 | 0.17 | Tyre is darker everywhere, neutral contrast |
| Darken + more contrast | 0.15 | 0.18 | Darker overall, but “pops” slightly in direct sun — good for the slight rubbery sheen |
| Fix the texture | 0.22 | 0.22 | Darken the texture itself in your image editor to 30-50% brightness |
The third option is arguably the cleanest — the 0.22 neutral point exists so that a properly-authored texture (where the albedo values represent actual material reflectance) converts cleanly to linear space. If your tyre texture is a photograph that hasn’t been adjusted for this pipeline, it’s probably stored too bright because it includes baked lighting, or because the original sRGB values are higher than the actual albedo of rubber (~0.02–0.05 for black rubber). Darkening the texture to represent the true material albedo and keeping the coefficients at neutral gives you the most predictable behavior when lighting conditions change.
ksPerPixelSimpleRefl vs ksPerPixelReflection 🔗
Both use the same vertex shader, which handles standard transforms, shadow coords, AO, fog, and optional rain vertex modifications.
Key Differences 🔗
| Feature | ksPerPixelSimpleRefl |
ksPerPixelReflection |
|---|---|---|
| Complexity | ~27 lines, lightweight | ~103 lines, full-featured |
| Cbuffer layout | CARPAINT_NM (includes nmObjectSpace) |
CARPAINT_SIMPLE (includes extBounceBack) |
| Reflection source | Sky color gradient (useSkyColor = true) — cheap |
Cubemap sampling — higher quality, more expensive |
| Reflection color tinting | None (getReflParamsBase) |
Yes, via specular color (getReflParams) |
| Specular from texture | No | Yes (GET_SPEC_COLOR → L.txSpecularValue) |
| Bounce-back lighting | No | Yes (includes lightingBounceBack.hlsl, extBounceBack param) |
| Rain FX | None | Full: RAINFX_WET, RAINFX_SHINY, RAINFX_REFLECTIVE, RAINFX_WATER |
| Dither fading | No | Yes (SUPPORTS_DITHER_FADING) |
| AO in lighting | Basic (extraShadow.y) |
Full (extraShadow.y * AO_LIGHTING) |
In Short 🔗
ksPerPixelSimpleRefl is the budget version — it fakes reflections using a sky color gradient, skips rain effects, bounce-back lighting, and specular color tinting. Good for surfaces where accurate environment reflections aren’t needed.
ksPerPixelReflection is the full-featured version — it samples the actual cubemap, supports colored specular reflections, has complete rain FX integration (wet darkening, shiny specular boost, puddle reflections), bounce-back lighting for thin/translucent surfaces, and dither-based LOD fading.
The reflection pipeline itself (Fresnel calculation, blur from ksSpecularEXP, CPL filter) is shared — the difference is just whether the reflection color comes from a sky gradient or the cubemap, and whether reflection params include specular color tinting.
When to use ksPerPixelReflection 🔗
Close-up, important surfaces.
Intended for objects where you actually need to see the environment reflected:
- Car glass (windscreens, windows) — it’s in the “wipers” rain list
- Headlight/taillight glass and reflectors if not baking emissives
- Chrome and metal trim — supports colored reflections via specular texture tinting
- Painted metal signs, barriers with metallic surfaces
- Any surface the camera gets close to where accurate reflections matter
It samples the actual cubemap/environment probe, supports full rain FX (wet darkening, drops, puddles, wiper traces), bounce-back lighting for translucent surfaces, and dither fading for LOD transitions. It also applies energy conservation (darkening diffuse as fresnel increases).
When to use ksPerPixelSimpleRefl 🔗
Distant/secondary scenery.
Intended for objects where reflections are “nice to have” but don’t need accuracy:
- Track-side barriers and fencing
- Distant buildings and scenery objects
- Poles, posts, and secondary track furniture
- Any surface where a faint sky-colored sheen at grazing angles is sufficient
It fakes reflections using a sky color gradient instead of the cubemap — much cheaper, but you’ll never see surrounding geometry reflected. It has no rain FX, no bounce-back lighting, no dither fading, and no specular color tinting of reflections. It’s also excluded from the ColorMask compilation pass, confirming it’s not meant for glass-like surfaces.
The practical rule of thumb 🔗
If the player will notice the quality of the reflections → use ksPerPixelReflection. If the object is far enough away or unimportant enough that a sky-tinted sheen is indistinguishable from a real cubemap reflection → use ksPerPixelSimpleRefl to save GPU cost.
Note: there’s also ksPerPixelMultiMapSimpleRefl, which is a middle ground — it uses the same cheap sky-color reflections but adds normal mapping and rain FX support, making it suitable for detailed track surfaces that need weathering but not accurate environment reflections.
ksPerPixelReflection 🔗
All values below assume CSP’s gamma fix is active, which is the standard modern rendering path. The shader applies these transforms internally before use:
| Input Parameter | Internal Transform | “Neutral” Input |
|---|---|---|
ksDiffuse / ksAmbient |
pow(texture * value * 4.44, 2.2) |
~0.22 (gives multiplier ≈1.0) |
ksSpecular |
Linear × 1.6 via GAMMA_BLINNPHONG_ADJ |
— |
ksSpecularEXP |
Specular: × 1.6. Reflection blur: raw (1 - EXP/255)²×6 |
— |
fresnelC |
pow(value, 2) — squared |
— |
fresnelEXP |
value × 2.2 |
— |
fresnelMaxLevel |
pow(value, 2) — squared |
— |
ksPerPixelMultiMap and ksPerPixelMultiMap_NMDetail 🔗
These shaders share the same ksAmbient/ksDiffuse gamma pipeline (applyGamma with the same × 4.44 then pow(2.2) transform), so the diffuse/ambient recommendations are identical to ksPerPixelReflection.
The fresnelC/fresnelEXP/fresnelMaxLevel values pass through the same calculateReflection() function with the same gamma transforms. The difference is that txMaps.y multiplies ksSpecularEXP (controlling per-pixel reflection blur) and txMaps.z multiplies reflection intensity (finalMult). The base cbuffer values are the maximum, and txMaps scales them down per-pixel.
Key differences in specular handling:
| Aspect | ksPerPixelReflection, ksPerPixelNM, ksTree |
ksPerPixelMultiMap, ksPerPixelMultiMap_NMDetail |
|---|---|---|
| Specular function | reflectanceModel() with GAMMA_BLINNPHONG_EXP (×1.6) and GAMMA_BLINNPHONG_ADJ (×1.6) |
Raw pow(), no gamma adj. |
| Specular exp meaning | ksSpecularEXP × 1.6 effective |
txMaps.g × ksSpecularEXP + 1 effective (no 1.6× boost) |
| Sun specular | Not available | Only ksPerPixelMultiMap: second specular lobe with sunSpecular/sunSpecularEXP. Not available in NMDetail variant. |
| txMaps modulation | None (no txMaps) | specularValue *= txMaps.r, specularExp = txMaps.g * exp + 1 |
The ×1.6 boost applies to shaders that use calculateLighting_spec() → reflectanceModel(). This covers CARPAINT_SIMPLE (ksPerPixelReflection), CARPAINT_NM (ksPerPixelNM), NO_CARPAINT (ksTree, ksPerPixel), and CARPAINT_GLASS. The multimap shaders instead use calculateMapsLighting which calls raw pow().
In practice, to get the same visual highlight sharpness, the multimap shaders need ~1.6× higher ksSpecularEXP values. However, since txMaps.g (typically 0–1) multiplies the exponent, the cbuffer value acts as the maximum — so overshoot is fine and expected.
Each material table below lists two values: ksSpecular / ksSpecularEXP for ksPerPixelReflection (with ×1.6 boost) and for ksPerPixelMultiMap (raw, pre-txMaps).
Sun specular (sunSpecular/sunSpecularEXP) is only available in ksPerPixelMultiMap (non-NMDetail). It creates a second Blinn-Phong specular lobe that’s added on top of the base specular. The two lobes are combined as:
specular = (baseSpec * ksSpecular + sunSpec * sunSpecular * extSunSpecularMult) * shadow
Where baseSpec uses ksSpecularEXP and sunSpec uses sunSpecularEXP. After applyTxMaps, the sun specular value becomes txMaps.z × txMaps.y × sunSpecular and the sun exponent becomes txMaps.y × sunSpecularEXP + 1. This allows a tight, bright sun highlight (high sunSpecularEXP) on top of a broader, softer base specular (lower ksSpecularEXP). Neither exponent gets the GAMMA_BLINNPHONG_EXP ×1.6 boost — both are raw pow().
Multilayer fresnel (roads, terrain) 🔗
Since this shader uses fresnel for specular intensity rather than cubemap reflections, and ksSpecular is bypassed entirely, the parameter roles are different from ksPerPixelReflection:
ksAmbient/ksDiffuse— same role as before, same gamma-fix behaviorksSpecular— unused, set to 0 or any value (doesn’t matter)ksSpecularEXP— controls Blinn-Phong highlight shape (with 1.6x gamma adjustment). Also still has theGAMMA_BLINNPHONG_EXPmultiplierfresnelC— base specular level when viewed head-on. Gamma-corrected withpow(value, 1.75)(not 2.0)fresnelEXP— steepness of specular increase toward grazing angles. Multiplied by 2.2fresnelMaxLevel— maximum specular intensity cap. SquaredtarmacSpecularMultiplier— linear scale on the angle-dependent fresnel term (applied before thefresnelCaddition). This is the primary “how shiny overall” knobmagicMult— overall diffuse brightness after layer blending. This absorbs the role thatksDiffuse/ksAmbientwould partially play in simpler shaders
Important: txDiffuseValue.a masks the specular. If your diffuse texture has alpha = 0 somewhere, that area will have zero specular regardless of other parameters.
| Parameter | Purpose |
|---|---|
multR/G/B |
World-space UV scale for each detail texture layer (float) |
multA |
World-space UV scale for the 4th detail layer (float2) |
magicMult |
Global brightness multiplier on blended diffuse |
tarmacSpecularMultiplier |
Overall scale on the fresnel-driven specular intensity |
detailNMMult |
UV scale for the detail normal map (float2) |
extBounceBack |
Bounce-back lighting intensity per mask channel (float4) |
ksTree 🔗
No fresnelC/fresnelEXP/fresnelMaxLevel/sunSpecular/sunSpecularEXP — the ksTree shader has no cubemap reflections and no fresnel system.
The ksTree shader is a specialized foliage shader with a simplified ambient model: AMBIENT_SIMPLE_FN(x) = saturate(normalY * 0.4 + 0.6), meaning ambient contribution is always at least 60% of maximum regardless of normal direction. It also uses NO_EXTAMBIENT (no IBL ambient sampling). This permanently bright ambient baseline means ksAmbient/ksDiffuse must be well below the 0.22 “neutral” point — at 0.18, the effective pre-gamma multiplier is 0.18 × 4.44 = 0.80, which combined with the 60–100% ambient floor gives a reasonable brightness range without blowing out. At 0.22 or above, the combination of the high ambient floor and the gamma pipeline overexposes foliage, especially sunlit canopies.
ksSpecular must be set to 0 because billboard trees represent the aggregate optical behavior of thousands of randomly-oriented leaves at once. Any coherent specular highlight makes flat billboard geometry visually read as a glossy plastic sheet rather than a mass of foliage. The specular from reflectanceModel() (with its ×1.6 gamma boost) is especially harsh on flat geometry. Setting specular to zero forces the material to be purely diffuse, which is the correct look for distant foliage.
Gotcha: Cubemap Blur and fresnelMaxLevel on Rough Materials 🔗
The double-squaring problem 🔗
fresnelMaxLevel is squared inside calculateReflection (utils_ps.fx line 587):
P.fresnelMaxLevel = pow(saturate(P.fresnelMaxLevel), 2);
The value you set in the .ini is squared before it caps the fresnel term. A value of 0.28 in the .ini produces an effective cap of pow(0.28, 2) = 0.078. The recommendations in this document account for this — the ini values listed are what you type into the material file, and the “effective” values in the comments are what the shader actually applies.
Why even small effective values cause visible tinting in shade 🔗
On rough materials (reflBlur > ~3), the cubemap degrades to averaged sky color — a blue-ish tint. Two compounding factors make this visible even with low effective fresnel values:
1. The cubemap doesn’t dim in shade. The cubemap always sees the full sky regardless of whether the surface is in shadow. In direct sun, diffuse is bright and a small cubemap contribution is imperceptible. In shade, diffuse drops dramatically but the cubemap tint remains constant — it becomes the dominant term.
2. Energy conservation amplifies it. The shader darkens the diffuse to “make room” for the reflection (utils_ps.fx line 673):
fixBase = sqrt(fresnel);
finalColor *= saturate(1 - fixBase * cplMult);
The sqrt() means the diffuse dimming is disproportionately strong for small fresnel values. At fresnel = 0.078: sqrt(0.078) = 0.28 → 28% diffuse dimming. At fresnel = 0.006: sqrt(0.006) = 0.078 → only 8% dimming. In shade where diffuse is already dim, this 28% reduction makes the sky-color tint dominate the final output.
The practical result: for rough materials, the effective fresnelMaxLevel cap needs to be in the 0.0004–0.015 range, not 0.02–0.08 as previously documented.
Reflblur reference 🔗
reflBlurBase = saturate(1 - ksSpecularEXP / 255)
reflBlur = pow(reflBlurBase, 2) * 6
| Effective ksSpecularEXP | reflBlur | Cubemap quality | Max recommended effective fresnelMaxLevel |
|---|---|---|---|
| 200+ | < 0.3 | Near-mirror | 0.40+ (use physical values) |
| 120–200 | 0.3–1.6 | Recognizable but soft | 0.10–0.16 |
| 50–120 | 1.6–3.2 | Blurry, transitional | 0.01–0.05 |
| 20–50 | 3.2–4.8 | Averages to sky color | 0.002–0.01 |
| < 20 | 4.8–6.0 | Pure sky-color tint | < 0.001 |
Note on the 120–200 range: Even though the cubemap shows recognizable environmental shapes at these blur levels, the energy conservation dimming still causes problems in shade. At effective fresnelMaxLevel 0.16 (the upper end of this range), sqrt(0.16) = 0.40 → the shader dims the surface by 40% at grazing angles and replaces that light with cubemap content. In shade, where diffuse is already low, this produces a visible environmental tint. The previous recommendation of 0.10–0.40 was too permissive — keep the effective cap at or below 0.16 for materials in this range.
Special case: FORCE_BLURREST_REFLECTIONS 🔗
The ksTyres shader forces cubemap sampling at level 15 (maximum blur) regardless of ksSpecularEXP. The cubemap is always a uniform sky-color tint. Apply the same principle — set fresnelMaxLevel to the former “effective” value. See §23. Tyre Rubber for specific values.
Special case: car txMaps textures 🔗
On car materials using ksPerPixelMultiMap, the txMaps.b channel feeds into R.finalMult, which scales the entire reflection contribution. In most car mods, txMaps.b is a copy of the AO map — this attenuates reflections in crevices (dark AO) but provides no attenuation on flat, exposed surfaces (AO = 1.0). The fresnelMaxLevel recommendations assume no attenuation from txMaps on exposed surfaces.
isAdditive and energy conservation 🔗
Setting isAdditive = 1 (for non-car-paint shaders, where IS_ADDITIVE_VAR reads the cbuffer value directly) disables the energy conservation term — the diffuse is not darkened under the reflection. This is physically correct for glass: a window reflecting the sky should not darken the interior behind it. For opaque materials, isAdditive = 0 is correct — the surface does lose some diffuse energy to reflected light. All glass sections (§18a, §18b, §19, §20) use isAdditive = 1 — including frosted glass, which is still transmissive despite its roughened surface.