Froxel volumetrics: the haze stops painting shadowed air the colour of sunlit air
`volumetricFog` gains tier 3, "Volume", with a "Shadowed Air" dial beside it; the Ultra rung of Lighting Quality takes it. A view-space 160x90x64 froxel grid — a fixed BUDGET, so every resolution costs the same — marched one compute thread per screen COLUMN, front to back, with a reprojected eight-frame history. Inject and integrate in one pass, because a froxel past the last shadow cascade has no evidence either way and the honest answer is the mean of what the rest of its column could see; a thread that owns the column can carry that and a froxel-per-invocation dispatch cannot. It publishes two things: - **the lit share** — how much of the air in front of each pixel the sun actually reaches, weighted by each slice's own contribution to what the eye sees through it. `volumetric_fog`'s haze multiplies its colour by it. The haze below this rung is the reference's: a zone colour mixed in by distance, the SAME colour whether the air it tints stands in open sun or a mountain's shadow, and no screen pass can tell it otherwise because a shaft march only ever ADDS light. On an Elwynn overlook at noon, 74% of the frame is air the sun does not fully reach. - **the lamps' in-scatter**, free once a volume exists: the phase lobe evaluated at every point of the air rather than once per ray at its closest approach, attenuated by the medium it crosses and strengthened when the medium is. `lampFog`'s analytic halos retire into this solve at tier 3, and that row still chooses which lamps and how many. The gain is derived, `0.9 / density(clear night)`, so the two lanes match on a clear night. At the Goldshire inn, isolating each by rendering lampFog 0 against 2 on both (the bases are byte-identical, which is what makes it a comparison): the volume's are 1.30x and wider. The sun's SHAFTS deliberately stay on `volumetric_light`'s per-pixel march, and that is a measurement. A volume bands in hard horizontal steps at 160x90, 320x180 and 534x300 alike, because one froxel column hands the same value to about a hundred pixels and a canopy's shadow is finer than any grid worth paying for. Spreading each column's samples across its own screen footprint (R2 down the column) turns the steps into a gradient and is worth knowing for any volume, but the march still wins on detail. docs/GRAPHICS.md has the sweep. Safety is one number: fully lit air multiplies by EXACTLY 1, so at Shadowed Air 0 the lane renders byte-identical to High over all 1,440,000 pixels with every pass still running. Getting that exact needed `1 - (1-floor)*(1-lit)` and not the `mix(floor, 1, lit)` it equals — `mix` is a fused multiply-add and lands a last ulp below 1.0, which moved five of nine tier 0-2 control pairs with the rung switched OFF. The shipped floor, 0.85, is deliberately half the depth of the first one tried, and the dial reaches twice it. Control: nine tier 0-2 pairs against the previous build, five byte-identical and four differing by one or two PIXELS out of 1,440,000 at 1-2 of 255 — every tier-0 pair identical, and tier 0 is the tier that switches the fog pass off, so the remainder is the fog shader's instruction schedule changing because the shader did. Cost is under the noise of both instruments: the frame probe puts Volume inside High's own run-to-run spread, and the GPU pass timer puts `gpu_other` at 0.75 -> 0.76 ms. Also recorded: `water-lake` leaves the deterministic row once shadows are forced on — it differs from a build of ITSELF by up to 88/255 on 0.03% of pixels. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -2362,8 +2362,14 @@ fn volumetric_fog_dropdown_is_localised_and_live() {
|
||||
.next()
|
||||
.unwrap();
|
||||
for (locale, title, labels) in [
|
||||
("enUS", "Volumetric Fog", ["Off", "Low", "High"]),
|
||||
("ruRU", "Объёмный туман", ["Выкл", "Низкое", "Высокое"]),
|
||||
// GFX (froxel volumetrics): the fourth tier is named, not numbered, and so is its
|
||||
// localisation.
|
||||
("enUS", "Volumetric Fog", ["Off", "Low", "High", "Volume"]),
|
||||
(
|
||||
"ruRU",
|
||||
"Объёмный туман",
|
||||
["Выкл", "Низкое", "Высокое", "Объём"],
|
||||
),
|
||||
] {
|
||||
let mut s = audio_harness();
|
||||
s.run(&format!("function GetLocale() return '{locale}' end"))
|
||||
@@ -2463,8 +2469,17 @@ fn ambient_occlusion_dropdown_is_localised_and_live() {
|
||||
.next()
|
||||
.unwrap();
|
||||
for (locale, title, labels) in [
|
||||
("enUS", "Ambient Occlusion", ["Off", "Low", "High"]),
|
||||
("ruRU", "Затенение окружения", ["Выкл", "Низкое", "Высокое"]),
|
||||
// GFX (ssilvb): the fourth tier is named, not numbered, and so is its localisation.
|
||||
(
|
||||
"enUS",
|
||||
"Ambient Occlusion",
|
||||
["Off", "Low", "High", "Indirect Light"],
|
||||
),
|
||||
(
|
||||
"ruRU",
|
||||
"Затенение окружения",
|
||||
["Выкл", "Низкое", "Высокое", "Непрямой свет"],
|
||||
),
|
||||
] {
|
||||
let mut s = audio_harness();
|
||||
s.run(&format!("function GetLocale() return '{locale}' end"))
|
||||
@@ -2603,18 +2618,26 @@ fn water_quality_writes_numeric_tiers_with_localised_labels() {
|
||||
// GFX (interior rig): the ten interior-lighting rows: 42 -> 52.
|
||||
// GFX (auto-exposure): the tier and its blend: 52 -> 54.
|
||||
// GFX (sharpening): the RCAS tier: 54 -> 55.
|
||||
// GFX (froxel volumetrics): the shadowed-air dial beside the haze tier: 55 -> 56.
|
||||
assert_eq!(
|
||||
s.eval::<usize>("return table.getn(OPTIONS_PAGE_ROWS.AdvancedGraphics)")
|
||||
.unwrap(),
|
||||
55
|
||||
56
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return OPTIONS_PAGE_ROWS.AdvancedGraphics[4]")
|
||||
.unwrap(),
|
||||
"RowWaterQuality"
|
||||
);
|
||||
// GFX (froxel volumetrics): the shadowed-air dial sits straight under its own tier, at
|
||||
// index 6, so every row after it moved one down — 23 -> 24.
|
||||
assert_eq!(
|
||||
s.eval::<String>("return OPTIONS_PAGE_ROWS.AdvancedGraphics[23]")
|
||||
s.eval::<String>("return OPTIONS_PAGE_ROWS.AdvancedGraphics[6]")
|
||||
.unwrap(),
|
||||
"RowVolumetricFogShadow"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return OPTIONS_PAGE_ROWS.AdvancedGraphics[24]")
|
||||
.unwrap(),
|
||||
"RowLavaGlow"
|
||||
);
|
||||
@@ -3587,6 +3610,11 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
|
||||
"BENILLA_TOOLTIP_VOLUMETRIC_FOG",
|
||||
"AdvancedGraphicsRowVolumetricFog",
|
||||
),
|
||||
// GFX (froxel volumetrics): the Volume rung's own depth dial.
|
||||
(
|
||||
"BENILLA_TOOLTIP_VOLUMETRIC_FOG_SHADOW",
|
||||
"AdvancedGraphicsRowVolumetricFogShadow",
|
||||
),
|
||||
// MONKEY (sky): the sky tier row owns its translated tooltip.
|
||||
(
|
||||
"BENILLA_TOOLTIP_SKY_QUALITY",
|
||||
@@ -3858,7 +3886,8 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
|
||||
// GFX (interior rig): the ten interior-lighting rows. 124 -> 134.
|
||||
// GFX (auto-exposure): the tier and its blend. 134 -> 136.
|
||||
// GFX (sharpening) / (field of view): one row each, on the two different pages. 136 -> 138.
|
||||
assert_eq!(checked, 138, "every tipped row carries a live key");
|
||||
// GFX (froxel volumetrics): the shadowed-air dial beside the haze tier. 138 -> 139.
|
||||
assert_eq!(checked, 139, "every tipped row carries a live key");
|
||||
assert_eq!(
|
||||
untipped,
|
||||
vec![
|
||||
@@ -3967,7 +3996,7 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() {
|
||||
// GFX (shadow rig): the five Advanced Graphics shadow rows. 119 -> 124.
|
||||
// GFX (interior rig): the ten interior-lighting rows. 124 -> 134.
|
||||
// GFX (auto-exposure): the tier and its blend. 134 -> 136.
|
||||
assert_eq!(raised, 138, "every row but Auto Loot raises a description");
|
||||
assert_eq!(raised, 139, "every row but Auto Loot raises a description");
|
||||
}
|
||||
|
||||
/// 1.12's AdvancedOptionsCombatText box as saved-global rows: a click writes the global, never a
|
||||
|
||||
Reference in New Issue
Block a user