MONKEY (daylight): city rooms the portal graph connects to the sky take the interiorDaylight floor; capture knobs + torch/goldshire scenario; LIGHTING.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3f1637b521
commit
310bed8941
@@ -20,6 +20,8 @@ behind each constant.
|
||||
| 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` |
|
||||
| Daylight and doorways | calibrated fixtures at doors, windows and open boundaries carry daylight into a room, and doorways between rooms bleed light | `interiorDaylight`, env `WOW_DAYLIGHT`, `WOW_BLEED` |
|
||||
| Terrain in torch shadows | the ground casts into an outdoor fire's cube map, so a hill or bank blocks the fire (settled exterior slots only; `torch_terrain.rs` gathers the resident MCNK chunks inside the cube's 48 yd) | `torchTerrainShadows` (default 0, High 1), env `WOW_TORCH_TERRAIN=0\|1` |
|
||||
| City day floor | `interiorDaylight`'s room floor also reaches Stormwind/Ironforge rooms that the portal graph connects to the sky (an exterior-facing portal, or one hop from one; `district_sky_rooms` in `daylight.rs`); before this no city room could take it | `interiorDaylight` (default 0, suggested High 0.10) |
|
||||
| Spell and firework lights | fire, holy and fel effects light their surroundings for their lifetime; frost, nature, arcane and shadow do not | `spellLightGain`, env `WOW_SPELL_LIGHT=0` |
|
||||
| Moon shadows | at night the same shadow rig re-aims at the moon and casts a faint shadow; dims only the night sky term, never point lights | `moonShadowStrength` |
|
||||
| Ground-effect spells | Flamestrike, Rain of Fire, Consecration, Flare and fire traps light the ground for their duration; frost and nature areas stay dark | `spellLightGain` |
|
||||
@@ -47,6 +49,9 @@ is its own module, documented in `WATER.md`.
|
||||
`character_shadow.rs`, `world_shadow.rs`, `blob_shadow.rs`, `entities/carried_light.rs`,
|
||||
`entities/spell_fx/lifecycle.rs`, `dynamic_interior.rs`, `debug_panel/lighting_controls.rs`, and
|
||||
the cvars in `cvars.rs` / `video.rs`.
|
||||
- **Census**: `cargo test -p benilla-world --lib lighting::daylight::census -- --ignored --nocapture`
|
||||
(`WOW_CENSUS_WMO`, `WOW_CENSUS_PLACE=map,tx,ty,uid`) prints, per interior room of a city WMO,
|
||||
whether a daylight fixture, a bleed fixture or neither reaches it, and the sky-room count.
|
||||
- **Tools**: `benilla-extract <Data> wmolights <wmo> [--verts <group>]`, `wmolamps`, `m2firescan`
|
||||
print the inputs the system works from (groups, batch classes, portals, claims, flame emitters).
|
||||
|
||||
|
||||
@@ -1404,6 +1404,11 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
|
||||
Scenario {
|
||||
name: "daylight-if-gate", map: Some(MAP_AZEROTH),
|
||||
eye: [-4975.0, -895.0, 503.5], look: [-5005.0, -852.0, 506.0], minute: 720, ui: None,
|
||||
}, // Goldshire's lamps at midnight: terrain receiving (and, with `torchTerrainShadows`, casting)
|
||||
// torch cube shadows.
|
||||
Scenario {
|
||||
name: "daylight-torch-goldshire", map: Some(MAP_AZEROTH),
|
||||
eye: [-9460.0, 70.0, 58.0], look: [-9450.0, 20.0, 61.0], minute: 0, ui: None,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -104,7 +104,9 @@ fn bridge(
|
||||
// MONKEY (enclosed day floor): the daylight floor rides the same bridge, but is consumed in
|
||||
// the SHADER (it rides the packed `wmo_fog_params.w` fraction) rather than folded on the
|
||||
// CPU — the packer's only job is to put it in the lane.
|
||||
daylight: video.interior_daylight,
|
||||
// MONKEY (daylight): `WOW_INTERIOR_DAYLIGHT=<0..1>` overrides it in a hermetic capture,
|
||||
// which reads no config.toml (dev builds only; read once).
|
||||
daylight: capture_daylight().unwrap_or(video.interior_daylight),
|
||||
// MONKEY (bake floor): the bake floor rides the same bridge. Half CPU, half shader: the
|
||||
// packer folds `interiorGain` in and puts the product in the `sh_c16.w` fraction, and the
|
||||
// two interior lanes read it from there — so `SetCVar("interiorBakeFloor", 0)` restores
|
||||
@@ -116,6 +118,21 @@ fn bridge(
|
||||
}
|
||||
}
|
||||
|
||||
/// MONKEY (daylight): the capture-only `interiorDaylight` override (see the bridge above).
|
||||
fn capture_daylight() -> Option<f32> {
|
||||
static V: std::sync::OnceLock<Option<f32>> = std::sync::OnceLock::new();
|
||||
*V.get_or_init(|| {
|
||||
if !cfg!(feature = "dev") || std::env::var_os("WOW_CAPTURE").is_none() {
|
||||
return None;
|
||||
}
|
||||
std::env::var("WOW_INTERIOR_DAYLIGHT")
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<f32>().ok())
|
||||
.filter(|v| v.is_finite())
|
||||
.map(|v| v.clamp(0.0, 1.0))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -300,6 +300,74 @@ pub fn daylight_budget(groups: &[WmoGroupInfo]) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
/// MONKEY (daylight: district sky rooms): how many portal hops from an exterior-facing opening a
|
||||
/// DISTRICT room may sit and still count as lit by the day (see [`district_sky_rooms`]).
|
||||
const DISTRICT_SKY_HOPS: u8 = 1;
|
||||
|
||||
/// MONKEY (daylight: district sky rooms): **which rooms of a CITY-scale placement take the
|
||||
/// enclosed day floor** (`interiorDaylight`), as one flag per group; empty for a building.
|
||||
///
|
||||
/// MEASURED (`daylight::census`, 2026-09-25): the floor's own test,
|
||||
/// [`benilla_formats::room_claim::enclosed_by_building_shell`], is false for EVERY interior group
|
||||
/// of Stormwind (190) and Ironforge (103), because a district shell is refused as a building by
|
||||
/// design. So `interiorDaylight` never reached a city room at any value, while 35 Stormwind rooms
|
||||
/// are reached by no daylight or bleed fixture at all and many more only by a fixture whose 6-20 yd
|
||||
/// pool dies half-way into a 40 yd hall.
|
||||
///
|
||||
/// The rule is the portal graph's, not a box test: a room is SKY-CONNECTED when it authors an
|
||||
/// exterior-facing portal (the same relation the PORTAL seed stands the sun in) or sits within
|
||||
/// [`DISTRICT_SKY_HOPS`] interior portals of such a room. That lights the shops, taverns and their
|
||||
/// back rooms and leaves what the data says is deep inside alone: the Stormwind canal tunnels,
|
||||
/// the Slaughtered Lamb's cellar, and all of Ironforge except the gate halls (the city's ONE
|
||||
/// exterior portal is `p0`, into `g7`).
|
||||
pub fn district_sky_rooms(groups: &[WmoGroupInfo], portals: PortalGraph<'_>) -> Vec<bool> {
|
||||
if groups.iter().filter(|g| g.interior).count() <= DAYLIGHT_DISTRICT_ROOMS {
|
||||
return Vec::new();
|
||||
}
|
||||
// Every portal's sides, as the PORTAL seed collects them.
|
||||
let mut sides: HashMap<u16, Vec<u16>> = HashMap::new();
|
||||
for (gi, (start, count)) in portals.slices.iter().enumerate() {
|
||||
let (start, count) = (usize::from(*start), usize::from(*count));
|
||||
for r in portals.refs.get(start..start + count).unwrap_or(&[]) {
|
||||
for g in [gi as u16, r.group] {
|
||||
let e = sides.entry(r.portal).or_default();
|
||||
if !e.contains(&g) {
|
||||
e.push(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let interior = |g: u16| groups.get(usize::from(g)).is_some_and(|g| g.interior);
|
||||
let mut hop: Vec<Option<u8>> = vec![None; groups.len()];
|
||||
let mut edges: Vec<(u16, u16)> = Vec::new();
|
||||
for gs in sides.values() {
|
||||
let ins: Vec<u16> = gs.iter().copied().filter(|g| interior(*g)).collect();
|
||||
match ins.as_slice() {
|
||||
[g] if gs.len() == 1 || gs.iter().any(|o| !interior(*o)) => {
|
||||
if let Some(h) = hop.get_mut(usize::from(*g)) {
|
||||
*h = Some(0);
|
||||
}
|
||||
}
|
||||
[a, b] => edges.push((*a, *b)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for level in 0..DISTRICT_SKY_HOPS {
|
||||
let mut next = Vec::new();
|
||||
for &(a, b) in &edges {
|
||||
for (from, to) in [(a, b), (b, a)] {
|
||||
if hop[usize::from(from)] == Some(level) && hop[usize::from(to)].is_none() {
|
||||
next.push(to);
|
||||
}
|
||||
}
|
||||
}
|
||||
for g in next {
|
||||
hop[usize::from(g)] = Some(level + 1);
|
||||
}
|
||||
}
|
||||
hop.iter().map(Option::is_some).collect()
|
||||
}
|
||||
|
||||
/// The EFFECTIVE reach (yd) of an opening `diag` yards across — the radius `R` the shader actually
|
||||
/// windows the pool with: `clamp(1.5*diag + 4, 6, 20)`. The linear term says a wide doorway throws
|
||||
/// light further in than an arrow slit; the `+4` floor keeps even a small window's pool bigger than
|
||||
@@ -2053,6 +2121,39 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// MONKEY (daylight: district sky rooms): a building gets nothing; a district flags the room
|
||||
/// with an exterior portal and its one-hop neighbour, and not the room two hops in.
|
||||
#[test]
|
||||
fn district_sky_rooms_follow_the_portal_graph() {
|
||||
let lo = [0.0; 3];
|
||||
let hi = [1.0; 3];
|
||||
// g0 exterior shell, g1..=g3 a chain of rooms, padded with sealed rooms to district size.
|
||||
let mut groups = vec![group(false, lo, hi)];
|
||||
groups.extend((0..40).map(|_| group(true, lo, hi)));
|
||||
let vertices = [[0.0f32; 3]; 4];
|
||||
let infos: Vec<WmoPortalInfo> = (0..3)
|
||||
.map(|_| WmoPortalInfo { start_vertex: 0, count: 4, plane: [1.0, 0.0, 0.0, 0.0] })
|
||||
.collect();
|
||||
// p0: g1<->g0 (exterior), p1: g1<->g2, p2: g2<->g3.
|
||||
let refs = [
|
||||
WmoPortalRef { portal: 0, group: 0, side: 1 },
|
||||
WmoPortalRef { portal: 1, group: 2, side: 1 },
|
||||
WmoPortalRef { portal: 1, group: 1, side: -1 },
|
||||
WmoPortalRef { portal: 2, group: 3, side: 1 },
|
||||
WmoPortalRef { portal: 2, group: 2, side: -1 },
|
||||
];
|
||||
let mut slices = vec![(0u16, 0u16); groups.len()];
|
||||
slices[1] = (0, 2);
|
||||
slices[2] = (2, 2);
|
||||
slices[3] = (4, 1);
|
||||
let portals = PortalGraph { vertices: &vertices, infos: &infos, refs: &refs, slices: &slices };
|
||||
let sky = district_sky_rooms(&groups, portals);
|
||||
assert!(sky[1] && sky[2], "the doorway room and its neighbour");
|
||||
assert!(!sky[3], "two hops in stays dark");
|
||||
assert!(!sky[0], "the shell is not a room");
|
||||
assert!(district_sky_rooms(&groups[..5], portals).is_empty(), "a building is untouched");
|
||||
}
|
||||
|
||||
/// The reach formula: linear in the opening's diagonal, floored at 6 and capped at 20.
|
||||
#[test]
|
||||
fn reach_is_the_clamped_opening_width() {
|
||||
|
||||
@@ -192,6 +192,18 @@ fn census(path: &str) {
|
||||
);
|
||||
}
|
||||
println!("interior rooms: {n_day} reached by daylight, {n_bleed} by bleed only, {n_none} by neither");
|
||||
let sky = district_sky_rooms(&l.groups, graph);
|
||||
let enclosed = (0..l.groups.len() as u16)
|
||||
.filter(|g| benilla_formats::room_claim::enclosed_by_building_shell(&l.groups, *g))
|
||||
.count();
|
||||
let dark_sky = (0..l.groups.len())
|
||||
.filter(|g| sky.get(*g).copied().unwrap_or(false))
|
||||
.filter(|g| !day_claim.contains_key(&(*g as u16)))
|
||||
.count();
|
||||
println!(
|
||||
"day floor: {enclosed} rooms enclosed by a building shell; {} district sky rooms ({dark_sky} of them reached by no daylight fixture)",
|
||||
sky.iter().filter(|b| **b).count()
|
||||
);
|
||||
}
|
||||
|
||||
/// `WOW_CENSUS_PLACE=map,tx,ty,uid`: the MODF placement, for world coordinates in the table.
|
||||
|
||||
@@ -26,6 +26,8 @@ pub use blob::LightBlob;
|
||||
pub use daylight::{
|
||||
daylight_claims, daylight_intensity, daylight_lane, daylight_point_light, daylight_reach,
|
||||
daylight_rooms, daylight_seeds, daylight_target, bleed_seeds, placement_openings, BleedFixture,
|
||||
// MONKEY (daylight: district sky rooms)
|
||||
district_sky_rooms,
|
||||
BleedSeed,
|
||||
DaylightFixture, DaylightHow, DaylightSeed, BLEED_K, MAX_DAYLIGHT_PER_PLACEMENT,
|
||||
};
|
||||
|
||||
@@ -357,6 +357,9 @@ pub enum GxSite<'a> {
|
||||
instance: Entity,
|
||||
groups: &'a [u16],
|
||||
bounds: &'a [benilla_formats::WmoGroupInfo],
|
||||
/// MONKEY (daylight: district sky rooms): per ABSOLUTE group, a city room connected to
|
||||
/// the sky by the portal graph (`lighting::district_sky_rooms`); empty for a building.
|
||||
sky: &'a [bool],
|
||||
},
|
||||
/// A WMO doodad prop (B4, decision 1433 — 1418's lane 3, absorbed): the building's
|
||||
/// instance entity, the referrer set of rooms that name the prop, and the interior
|
||||
|
||||
@@ -322,6 +322,7 @@ pub fn spawn_model_entities(
|
||||
instance,
|
||||
groups,
|
||||
bounds,
|
||||
sky,
|
||||
} if is_wmo && class.merges() =>
|
||||
{
|
||||
groups.get(batch_idx).map(|&g| {
|
||||
@@ -346,7 +347,10 @@ pub fn spawn_model_entities(
|
||||
// in hand, and the answer rides to the shader as a record bit.
|
||||
enclosed: benilla_formats::room_claim::enclosed_by_building_shell(
|
||||
bounds, g,
|
||||
),
|
||||
)
|
||||
// MONKEY (daylight: district sky rooms): …or a city room
|
||||
// the portal graph connects to the sky.
|
||||
|| sky.get(usize::from(g)).copied().unwrap_or(false),
|
||||
class: sub.wmo_batch,
|
||||
sidn: sub.sidn,
|
||||
window: sub.window,
|
||||
|
||||
@@ -330,6 +330,17 @@ pub(super) fn spawn_loaded_placements(
|
||||
});
|
||||
// The portal-cull instance, spawned before the batches: the retained pass keys
|
||||
// its region on it, and the region lives as long as it does.
|
||||
// MONKEY (daylight: district sky rooms): which city rooms take the day floor.
|
||||
let sky_slices = fx::portal_slices(m);
|
||||
let sky = crate::lighting::district_sky_rooms(
|
||||
&m.group_bounds,
|
||||
benilla_formats::PortalGraph {
|
||||
vertices: &m.portal_vertices,
|
||||
infos: &m.portal_infos,
|
||||
refs: &m.portal_refs,
|
||||
slices: &sky_slices,
|
||||
},
|
||||
);
|
||||
let instance = (has_portals || m.wmo_id != 0).then(|| {
|
||||
commands
|
||||
.spawn(WmoPortalInstance {
|
||||
@@ -405,6 +416,7 @@ pub(super) fn spawn_loaded_placements(
|
||||
// MONKEY (ext-class night law): the MOGI table, so the
|
||||
// assembler can read each batch's group CLASS + BOX.
|
||||
bounds: &m.group_bounds,
|
||||
sky: &sky,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user