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

This commit is contained in:
pkuzic
2026-10-09 17:28:45 +01:00
6 changed files with 82 additions and 5 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ behind each constant.
| Lane | What you see | Main switch |
|---|---|---|
| Sun shadows, characters | real silhouettes for units instead of the oval blob | `characterShadows` |
| Sun shadows, world | trees, buildings and alpha-tested foliage cast; baked terrain shadows step aside; the static caster cache also refreshes when streamed geometry arrives, so a standing camera picks up late trees (`world_shadow.rs::refresh_streamed_shadows`, independent of volumetric fog) | `worldShadows` |
| Sun shadows, world | trees, buildings and alpha-tested foliage cast; baked terrain shadows step aside, and so does the MCSH 0.5 sun dim on models standing on them (`wow_model.wgsl` / `static_gx.wgsl`, terrain's gate and dusk weight); the static caster cache also refreshes when streamed geometry arrives, so a standing camera picks up late trees (`world_shadow.rs::refresh_streamed_shadows`, independent of volumetric fog) | `worldShadows` |
| Dynamic interiors | WMO rooms are lit per fragment by their own fixtures with a soft falloff, gated per room so light does not leak through walls | `interiorLight`, `interiorRoomGate` |
| Synthesised lights | torches, braziers, lanterns, campfires and lampposts emit a light in the colour of their flame, with flicker | `fireLightGain`, `fireFlicker` |
| Torch shadows | cube-map shadows from point lights onto buildings, models and terrain, with a contact-hardening penumbra and a shadow floor | `interiorShadows`, `exteriorShadows`, `torchShadowStrength`, `interiorShadowSoft` |
+16 -2
View File
@@ -716,7 +716,21 @@ fn pin_scene(
return;
};
debug.lighting.follow_server_time = false;
debug.lighting.manual_minute = scenario.minute;
// MONKEY (capture framing): `WOW_CAPTURE_MINUTE=<0..1440>`, `WOW_CAPTURE_EYE=x,y,z` and
// `WOW_CAPTURE_LOOK=x,y,z` (raw WoW coords) re-aim a scenario without a rebuild, for framing a
// reported spot; a scenario's baseline never sets them.
let triple = |var: &str| {
std::env::var(var).ok().and_then(|s| {
let v: Vec<f32> = s.split(',').filter_map(|x| x.trim().parse().ok()).collect();
(v.len() == 3).then(|| [v[0], v[1], v[2]])
})
};
debug.lighting.manual_minute = std::env::var("WOW_CAPTURE_MINUTE")
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(scenario.minute);
let scene_eye = triple("WOW_CAPTURE_EYE").unwrap_or(scenario.eye);
let scene_look = triple("WOW_CAPTURE_LOOK").unwrap_or(scenario.look);
// `WOW_MM_PROBE=x,y,z` (raw WoW coords) places an active player there, so the interior
// minimap renders server-less, with the camera above so the WMO streams in around it.
@@ -750,7 +764,7 @@ fn pin_scene(
* Quat::from_rotation_x(-req.el_deg.to_radians());
(look + orbit * (Vec3::Z * req.dist), look)
}
_ => (wow_to_bevy(scenario.eye), wow_to_bevy(scenario.look)),
_ => (wow_to_bevy(scene_eye), wow_to_bevy(scene_look)),
};
for mut t in &mut cam {
*t = Transform::from_translation(eye).looking_at(look, Vec3::Y);
@@ -1280,6 +1280,29 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
name: "redridge-lake", map: Some(MAP_AZEROTH),
eye: [-9350.0, -2340.0, 82.0], look: [-9500.0, -2650.0, 50.0], minute: 720, ui: None,
},
// MONKEY (gfx-1009 unlit): Lakeshire's report sites. The sunken `lionstatue.mdx` (MDDF uid
// 83242, (-9355.5, -2246.1, 47.9), ~8 yd under the 56.16 surface) seen from the water surface
// and from under it, and the gryphon master's two `gryphonroost01.mdx` (uids 325784/325785).
Scenario {
name: "lakeshire-statue", map: Some(MAP_AZEROTH),
eye: [-9318.0, -2238.0, 59.0], look: [-9356.0, -2247.0, 49.0], minute: 720, ui: None,
},
Scenario {
name: "lakeshire-statue-dusk", map: Some(MAP_AZEROTH),
eye: [-9318.0, -2238.0, 59.0], look: [-9356.0, -2247.0, 49.0], minute: 1170, ui: None,
},
Scenario {
name: "lakeshire-statue-under", map: Some(MAP_AZEROTH),
eye: [-9343.0, -2254.0, 52.0], look: [-9356.0, -2246.0, 48.5], minute: 720, ui: None,
},
Scenario {
name: "lakeshire-gryphons", map: Some(MAP_AZEROTH),
eye: [-9407.0, -2250.0, 72.5], look: [-9422.5, -2237.0, 69.5], minute: 720, ui: None,
},
Scenario {
name: "lakeshire-gryphons-dusk", map: Some(MAP_AZEROTH),
eye: [-9407.0, -2250.0, 72.5], look: [-9422.5, -2237.0, 69.5], minute: 1170, ui: None,
},
// Duskwood: the road into Darkshire, the zone's short dark fog.
Scenario {
name: "duskwood-road", map: Some(MAP_AZEROTH),
@@ -1417,7 +1417,17 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut {
let mat_shade = select(0.0, 1.0, m.sun_scale.x < 0.5);
let shade_t = max(mat_shade, inst_shade);
let mid_band = m.sun_scale.x >= 0.5 && m.sun_scale.x < 0.85;
let intensity = min(select(mix(2.5, 0.5, shade_t), 1.0, mid_band), 1.0);
let intensity_ref = min(select(mix(2.5, 0.5, shade_t), 1.0, mid_band), 1.0);
// MONKEY (mcsh model shade): the 0.5 band is the MCSH bit under the model, the same baked
// shadow `terrain.wgsl` drops while the realtime world lane is on (`sh_c16.w > 0.5`, weighted
// by the sun lane so the bake returns at night). Keeping it here left a doodad standing on its
// own baked shadow (Lakeshire's gryphon roosts) dimmed twice, by the bake AND the realtime map,
// on ground that no longer shows the bake. Only that band is < 1, so lit, matte, interior and
// rig lanes are untouched; with the lane off this is the reference value bit for bit.
let mcsh_suppress = select(0.0, shadow_hook::sun_shadow_w(wow_light.fog_params.z),
wow_light.sh_c16.w > 0.5);
let intensity = select(intensity_ref, mix(intensity_ref, 1.0, mcsh_suppress),
mcsh_suppress > 0.0);
// One `intensity` multiply covers every sun band (never I²); the c10 `.w` ambient does not
// scale. `pack_model_core_rows` packs the same closed form.
let sun_dc = wow_light.grade.yzw * intensity;
@@ -2891,6 +2891,30 @@ mod tests {
}
}
/// MONKEY (mcsh model shade): the model receivers drop the MCSH 0.5 sun band on exactly the
/// gate terrain drops its MCSH on (`sh_c16.w > 0.5`, weighted by the sun lane), so a doodad on
/// its own baked shadow is not dimmed twice once the realtime world lane has replaced the bake.
#[test]
fn model_receivers_drop_the_mcsh_band_on_terrains_gate() {
const TERRAIN: &str = include_str!("../../../benilla-assets/src/shaders/terrain.wgsl");
const MODEL: &str = include_str!("../../../benilla-assets/src/shaders/wow_model.wgsl");
const STATIC_GX: &str = include_str!("../shaders/static_gx.wgsl");
let squash = |s: &str| s.split_whitespace().collect::<String>();
assert!(squash(TERRAIN).contains("letworld_shadow_lane=wow_light.sh_c16.w>0.5;"));
let gate = "letmcsh_suppress=select(0.0,shadow_hook::sun_shadow_w(wow_light.fog_params.z),\
wow_light.sh_c16.w>0.5);";
let apply = "letintensity=select(intensity_ref,mix(intensity_ref,1.0,mcsh_suppress),\
mcsh_suppress>0.0);";
for (name, shader) in [("wow_model.wgsl", MODEL), ("static_gx.wgsl", STATIC_GX)] {
let s = squash(shader);
assert!(
s.contains(gate),
"{name} must gate the MCSH band like terrain"
);
assert!(s.contains(apply), "{name} must lift only through the gate");
}
}
/// MONKEY (enclosed day floor): `interiorDaylight` rides the FRACTION of the interior lane's
/// on/off word, and every existing decode of that word must be blind to it.
///
@@ -2033,10 +2033,16 @@ fn fragment(in: GxVsOut) -> @location(0) vec4<f32> {
// Deviation: the `min(I, 1)` cap, as in wow_model.wgsl, since lifting it takes sun-facing
// surfaces past 1.0; Matte keeps its own bit so it stays 1.0 if the cap goes.
let shade_t = select(1.0, 0.0, (in.word & WORD_SHADE_LIT) != 0u);
let intensity = min(
let intensity_ref = min(
select(mix(2.5, 0.5, shade_t), 1.0, (in.word & WORD_MATTE) != 0u),
1.0,
);
// MONKEY (mcsh model shade): the MCSH 0.5 steps aside with terrain's MCSH while the world
// shadow lane is on; see `wow_model.wgsl` (same expression, same gate).
let mcsh_suppress = select(0.0, shadow_hook::sun_shadow_w(wow_light.fog_params.z),
wow_light.sh_c16.w > 0.5);
let intensity = select(intensity_ref, mix(intensity_ref, 1.0, mcsh_suppress),
mcsh_suppress > 0.0);
let sun_dc = wow_light.grade.yzw * intensity;
let sun_lobe = vec3<f32>(
wow_light.sh_c10_r.w + sun_dc.x