Merge branch 'gfx/1009-shadows' into gfx/1009-upstream

This commit is contained in:
pkuzic
2026-10-09 17:28:45 +01:00
5 changed files with 140 additions and 38 deletions
@@ -311,6 +311,55 @@ pub(super) const SUBJECT_INDOOR: [f32; 3] = [-9469.4, 31.9, 57.9];
/// Every other named viewpoint, capturable by name (`WOW_CAPTURE=<name>`) but not in the sweep.
pub(super) const ON_DEMAND: &[Scenario] = &[
// MONKEY (wmo surface lights): the portcullis/archery yard and lamp-lined gate ramp.
Scenario {
name: "stormwind-training-yard-day",
map: Some(MAP_AZEROTH),
eye: [-8728.0, 341.0, 104.0],
look: [-8517.4951, 792.4295, 147.57787],
minute: 720,
ui: None,
},
Scenario {
name: "stormwind-training-yard-dusk",
map: Some(MAP_AZEROTH),
eye: [-8728.0, 341.0, 104.0],
look: [-8517.4951, 792.4295, 147.57787],
minute: 1110,
ui: None,
},
Scenario {
name: "stormwind-training-yard-night",
map: Some(MAP_AZEROTH),
eye: [-8728.0, 341.0, 104.0],
look: [-8517.4951, 792.4295, 147.57787],
minute: 0,
ui: None,
},
Scenario {
name: "stormwind-gate-ramp-day",
map: Some(MAP_AZEROTH),
eye: [-9095.0, 412.0, 124.0],
look: [-8818.3066, 632.09216, -229.55338],
minute: 720,
ui: None,
},
Scenario {
name: "stormwind-gate-ramp-dusk",
map: Some(MAP_AZEROTH),
eye: [-9095.0, 412.0, 124.0],
look: [-8818.3066, 632.09216, -229.55338],
minute: 1110,
ui: None,
},
Scenario {
name: "stormwind-gate-ramp-night",
map: Some(MAP_AZEROTH),
eye: [-9095.0, 412.0, 124.0],
look: [-8818.3066, 632.09216, -229.55338],
minute: 0,
ui: None,
},
// MONKEY (volumetric fog): UI proof uses the same live page selection as a player.
Scenario {
name: "ui-options-advanced", map: Some(MAP_AZEROTH),
@@ -18,7 +18,9 @@ struct MonkeyFrame {
wind_a: vec4<f32>, // dir_x, dir_y (unit, world XZ: .x = world x, .y = world z), base_heading (rad, fixed profile heading), gust 0..1
wind_b: vec4<f32>, // travel (yd, integrated speed, wrapped at 4096), sway_strength, grass_strength, tree_strength
wet_a: vec4<f32>, // rain_rate 0..1, wetness 0..1, ripple_time_s, snow 0..1
misc: vec4<f32>, // bender_count, time_of_day 0..1 (packer), night 0..1 (packer), moon shadow confidence 0..1
// MONKEY (wmo surface lights): misc.x's integer is the bender count; its fraction is 0.5
// when enhanced exterior points are enabled, zero in Classic. Wind truncates to u32.
misc: vec4<f32>, // bender_count + exterior gate, time_of_day 0..1, night 0..1, moon shadow confidence
benders: array<vec4<f32>, 8>, // world xyz (Bevy space) + radius (yd); the first bender_count are live
// GFX (moonlight): unit direction toward the moon (Bevy space) + intensity (gamma; 0 = off).
moon: vec4<f32>,
@@ -1195,19 +1195,12 @@ fn vertex(vertex: WowVertex) -> WowVsOut {
// at its INSTANCE origin (wow-re wmo-surface-dynamic-light §6 — the receiving unit's own
// position, deliberately not the skinned per-vertex matrix).
if (m.model_flags.x > 0.5 && m.model_flags.z < 0.5) {
// MONKEY (wmo exterior points): an EXTERIOR-class WMO group (`model_flags.z` clear = MOGP
// `& 0x48` set) is a street, a courtyard, a porch — drawn by the exterior law, the same law
// the terrain beside it is drawn by, so it takes the exterior point term terrain takes. The
// §2 "zero on every WMO surface" finding was measured on abbey INTERIOR rooms and reading
// it as a blanket zero is what left Stormwind's Trade District torches lighting nothing but
// the NPC beside them. Mirrors `static_gx.wgsl`'s branch (which draws the vast majority of
// these batches) — keep the two in step.
// MONKEY (outdoor torch shadows): the pick is PUBLISHED so the fragment stage can shadow
// this same selection at night without re-ranking. One table walk still, not two -
// the sum was always `eval(pick(..))`, it is just no longer inlined.
let sel = wmo_exterior_pick(out.world_position.xyz);
out.ext_sel = sel;
// MONKEY (B035 per-fragment wmo points): summed per FRAGMENT now, as in `static_gx.wgsl`.
// MONKEY (wmo surface lights): Classic retains its MCNK vertex pick. Enhanced WMO
// picks once at the fragment instead; no per-vertex table walk on that lane.
out.ext_sel = EXT_SEL_NONE;
if (fract(wow_light.monkey.misc.x) == 0.0) {
out.ext_sel = wmo_exterior_pick(out.world_position.xyz);
}
} else if (m.model_flags.x > 0.5 || m.model_flags.z > 0.5) {
out.ext_sel = EXT_SEL_NONE;
} else {
@@ -1607,16 +1600,20 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut {
}
}
let sidn_e = m.sidn.rgb * (wow_light.grade.x * sidn_w);
// MONKEY (local-light consistency): all exterior receivers sum their committed lights
// per fragment. Sparse prop vertices must not spread the near-field peak across
// furniture while the adjacent terrain and WMO evaluate a different pool.
// Interior receivers publish EXT_SEL_NONE and use the separate room-light lane.
var point_diffuse = point_light_eval(in.ext_sel, in.world_position.xyz, n_lit);
// MONKEY (wmo surface lights): a city WMO spans many terrain cells. A flat vertex pick
// draws its lamp pool on whole triangles; a snapped cell pick moves the seam to the grid.
// Enhanced exterior WMO ranks at the receiving fragment, within the existing 48 yd reach.
// Props keep their placement pick, interior receivers their room lane, Classic its old pick.
var exterior_selection = in.ext_sel;
if (is_wmo && !is_interior && fract(wow_light.monkey.misc.x) > 0.0) {
exterior_selection = point_light_pick(in.world_position.xyz, 0.0);
}
var point_diffuse = point_light_eval(exterior_selection, in.world_position.xyz, n_lit);
let ext_night_w = select(0.0, clamp(1.0 - wow_light.fog_params.z, 0.0, 1.0), torch_ext_on());
if (ext_night_w > 0.0) {
point_diffuse = mix(
point_diffuse,
point_light_eval_shadowed(in.ext_sel, in.world_position.xyz, n_lit),
point_light_eval_shadowed(exterior_selection, in.world_position.xyz, n_lit),
ext_night_w,
);
}
@@ -756,10 +756,18 @@ fn pack_monkey_frame(
frame: Res<super::MonkeyFrame>,
clock: Res<super::GameClock>,
light: Res<WowLighting>,
fire_gain: Res<FireLightGain>,
spell_gain: Res<SpellLightGain>,
lava_gain: Option<Res<super::LavaLightGain>>,
mut data: ResMut<WowLightData>,
) {
let night = 1.0 - sun_shadow_strength(light.celestial_dir.y);
let packed = frame.pack(clock.minute as f32 / 1440.0, night);
let mut packed = frame.pack(clock.minute as f32 / 1440.0, night);
// MONKEY (wmo surface lights): wind truncates misc.x to u32, leaving its fraction free.
// All enhanced point gains zero (Classic) preserves the old pick and frame bits.
if fire_gain.0 > 0.0 || spell_gain.0 > 0.0 || lava_gain.is_some_and(|gain| gain.0 > 0.0) {
packed[7][0] += 0.5;
}
if data.0.monkey != packed {
data.0.monkey = packed;
}
@@ -2381,6 +2389,53 @@ mod tests {
assert_eq!(gpu.len(), ROOM_CLAIM_STRIDE, "2 head + 6 ids + 6 x 4 fade");
}
/// MONKEY (wmo surface lights): Classic keeps all frame bits, and the enhanced gate must
/// never alter the wind shader's integer bender count or any other programme row.
#[test]
fn the_wmo_surface_gate_preserves_classic_and_wind_counts() {
let mut app = App::new();
app.init_resource::<super::super::MonkeyFrame>()
.init_resource::<super::super::GameClock>()
.init_resource::<WowLighting>()
.init_resource::<WowLightData>()
.insert_resource(FireLightGain(0.0))
.insert_resource(SpellLightGain(0.0))
.insert_resource(super::super::LavaLightGain(0.0))
.add_systems(Update, pack_monkey_frame);
for count in 0..=8 {
app.world_mut()
.resource_mut::<super::super::MonkeyFrame>()
.bender_count = count;
let frame = app.world().resource::<super::super::MonkeyFrame>();
let classic = frame.pack(0.0, 1.0);
for gains in [
[0.0, 0.0, 0.0],
[0.25, 0.0, 0.0],
[0.0, 0.25, 0.0],
[0.0, 0.0, 0.25],
[1.0, 1.0, 1.0],
[0.0, 0.0, 0.0],
] {
app.world_mut().resource_mut::<FireLightGain>().0 = gains[0];
app.world_mut().resource_mut::<SpellLightGain>().0 = gains[1];
app.world_mut()
.resource_mut::<super::super::LavaLightGain>()
.0 = gains[2];
app.update();
let packed = app.world().resource::<WowLightData>().0.monkey;
assert_eq!(
packed[7][0] as u32, count,
"wind count must truncate unchanged"
);
let mut expected = classic;
if gains.iter().any(|gain| *gain > 0.0) {
expected[7][0] += 0.5;
}
assert_eq!(packed, expected, "only the enhanced gate may move");
}
}
}
/// GOLDEN — MONKEY (fire GO lights): `fireLightGain` scales SYNTHESISED sources and **only**
/// them, at pack time.
///
+16 -17
View File
@@ -1424,18 +1424,12 @@ fn vertex(v: GxVertex) -> GxVsOut {
// Gouraud: straight-edged wedges, and a torch mid-triangle lights nothing.
out.ext_sel = EXT_SEL_NONE;
} else if ((v.word & WORD_WMO) != 0u) {
// MONKEY (wmo exterior points): an EXTERIOR-class group (MOGP `& 0x48`) is a street, a
// courtyard, a porch — drawn by the exterior law, so it takes the exterior point term the
// terrain beside it takes. Its own anchor is the placement origin (one point for all of
// Stormwind), so `wmo_exterior_point_sum` re-anchors on the MCNK cell; see its comment.
// Selection stays per vertex; light falloff is evaluated per fragment.
// MONKEY (outdoor torch shadows): the pick is published so the fragment stage can shadow
// this same selection at night without re-ranking (see `EXT_SEL_NONE`). One table walk
// still, not two — the sum was always `eval(pick(...))`, it is just no longer inlined.
let sel = wmo_exterior_pick(world);
out.ext_sel = sel;
// MONKEY (B035 per-fragment wmo points): the SUM moved to the fragment stage (see there);
// only the pick stays per vertex, so this lane still walks the table once per vertex.
// MONKEY (wmo surface lights): Classic retains its MCNK vertex pick. Enhanced WMO
// picks once at the fragment instead; no per-vertex table walk on that lane.
out.ext_sel = EXT_SEL_NONE;
if (fract(wow_light.monkey.misc.x) == 0.0) {
out.ext_sel = wmo_exterior_pick(world);
}
} else {
// MONKEY (ext light k12): `box = 0` — an exterior doodad/MODD prop is its own draw unit and
// ranks from its baked PLACEMENT origin, exactly as it did before the widening.
@@ -1489,15 +1483,20 @@ fn fragment(in: GxVsOut) -> @location(0) vec4<f32> {
// Both faces light from the submitted normal (no GL_LIGHT_MODEL_TWO_SIDE in the reference);
// this pipeline never negates back faces, so it has no front-face select like wow_model.wgsl.
let n_lit = wow_normalize(in.world_normal);
// MONKEY (local-light consistency): the same per-fragment falloff for WMO and props.
// The vertex still ranks once and publishes its flat selection. Interior receivers
// publish EXT_SEL_NONE and keep their room-light lane.
var point_lit = point_light_eval(in.ext_sel, in.world_position.xyz, n_lit);
// MONKEY (wmo surface lights): a city WMO spans many terrain cells. A flat vertex pick
// draws its lamp pool on whole triangles; a snapped cell pick moves the seam to the grid.
// Enhanced exterior WMO ranks at the receiving fragment, within the existing 48 yd reach.
// Props keep their placement pick, interior receivers their room lane, Classic its old pick.
var exterior_selection = in.ext_sel;
if ((in.word & WORD_WMO) != 0u && (in.word & WORD_INTERIOR) == 0u && fract(wow_light.monkey.misc.x) > 0.0) {
exterior_selection = point_light_pick(in.world_position.xyz, 0.0);
}
var point_lit = point_light_eval(exterior_selection, in.world_position.xyz, n_lit);
let ext_night_w = select(0.0, clamp(1.0 - wow_light.fog_params.z, 0.0, 1.0), torch_ext_on());
if (ext_night_w > 0.0) {
point_lit = mix(
point_lit,
point_light_eval_shadowed(in.ext_sel, in.world_position.xyz, n_lit),
point_light_eval_shadowed(exterior_selection, in.world_position.xyz, n_lit),
ext_night_w,
);
}