MONKEY (daylight): terrain as a caster in exterior torch cube maps (torchTerrainShadows, default off)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6f30b5466b
commit
adc9d277e7
@@ -513,6 +513,7 @@
|
||||
interiorLight = "Dynamic Interior Lighting",
|
||||
interiorShadows = "Indoor Torch Shadows",
|
||||
exteriorShadows = "Outdoor Torch Shadows",
|
||||
torchTerrainShadows = "Terrain Blocks Torchlight",
|
||||
torchSoft = "Torch Shadow Softness",
|
||||
fireLight = "Fire Light Brightness",
|
||||
fireFlicker = "Fire Flicker",
|
||||
@@ -578,6 +579,13 @@
|
||||
"The same for fires out in the open: campfires, braziers and lampposts "
|
||||
.. "cast real shadows after dark.\n\n"
|
||||
.. "It does nothing by day and does not require Dynamic Interior Lighting.",
|
||||
TORCH_TERRAIN_SHADOWS =
|
||||
"Lets the ground itself take part in outdoor torch shadows: a hill or a "
|
||||
.. "bank between a fire and the slope behind it blocks the fire's light.
|
||||
|
||||
"
|
||||
.. "Adds the nearby terrain to each outdoor fire's shadow map when it is "
|
||||
.. "rebuilt; the per-frame cost is small. Needs Outdoor Torch Shadows.",
|
||||
TORCH_SOFT =
|
||||
"How soft the edge of a torch shadow is where the object meets the "
|
||||
.. "ground. Low is hard and crisp; high spreads the edge the way a real "
|
||||
@@ -637,6 +645,7 @@
|
||||
interiorLight = "Динамическое освещение помещений",
|
||||
interiorShadows = "Тени факелов в помещениях",
|
||||
exteriorShadows = "Тени огней под открытым небом",
|
||||
torchTerrainShadows = "Рельеф заслоняет огни",
|
||||
torchSoft = "Мягкость теней от огня",
|
||||
fireLight = "Яркость огня",
|
||||
fireFlicker = "Мерцание огня",
|
||||
@@ -710,6 +719,14 @@
|
||||
"То же для огней под открытым небом: костры, жаровни и фонари "
|
||||
.. "отбрасывают настоящие тени после наступления темноты.\n\n"
|
||||
.. "Днём не действует и не требует динамического освещения помещений.",
|
||||
TORCH_TERRAIN_SHADOWS =
|
||||
"Земля сама участвует в тенях от огней снаружи: холм или склон между "
|
||||
.. "костром и местностью за ним заслоняет свет огня.
|
||||
|
||||
"
|
||||
.. "Добавляет ближний рельеф в карту теней каждого уличного огня при её "
|
||||
.. "перестройке; затраты на кадр невелики. Требует теней огней под "
|
||||
.. "открытым небом.",
|
||||
TORCH_SOFT =
|
||||
"Насколько мягок край тени от огня там, где предмет касается пола. "
|
||||
.. "Низкое значение — резкий край, высокое — размытый, как от "
|
||||
@@ -798,7 +815,9 @@
|
||||
"RowLightingQuality", "RowWaterQuality", "RowVolumetricFog",
|
||||
"RowCharacterShadows", "RowWorldShadows", "RowShadowDistance",
|
||||
"RowShadowResolution", "RowMoonShadows",
|
||||
"RowInteriorLight", "RowInteriorShadows", "RowExteriorShadows", "RowTorchSoft",
|
||||
"RowInteriorLight", "RowInteriorShadows", "RowExteriorShadows",
|
||||
-- MONKEY (daylight: terrain torch casters)
|
||||
"RowTorchTerrainShadows", "RowTorchSoft",
|
||||
"RowFireLight", "RowFireFlicker", "RowSpellLights", "RowLavaGlow",
|
||||
"RowNightDarkness", "RowInteriorDarkness",
|
||||
},
|
||||
@@ -1315,6 +1334,9 @@
|
||||
local torch = (interior and GetCVar("interiorShadows") ~= "0")
|
||||
or GetCVar("exteriorShadows") ~= "0"
|
||||
OptionsRow_SetEnabled(getglobal(base .. "RowTorchSoft"), torch)
|
||||
-- MONKEY (daylight: terrain torch casters): the ground only casts for outdoor fires.
|
||||
OptionsRow_SetEnabled(getglobal(base .. "RowTorchTerrainShadows"),
|
||||
GetCVar("exteriorShadows") ~= "0")
|
||||
return
|
||||
end
|
||||
if key == "Controls" then
|
||||
@@ -2721,11 +2743,23 @@
|
||||
"BENILLA_TOOLTIP_EXTERIOR_SHADOWS")
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowTorchSoft" inherits="OptionsSliderRowTemplate">
|
||||
<!-- MONKEY (daylight: terrain torch casters) -->
|
||||
<Frame name="$parentRowTorchTerrainShadows" inherits="OptionsCheckboxRowTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentRowExteriorShadows" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentRowExteriorShadows" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts><OnLoad>
|
||||
OptionsRow_OnLoad(self, "torchTerrainShadows",
|
||||
BENILLA_ADVGFX.torchTerrainShadows,
|
||||
"BENILLA_TOOLTIP_TORCH_TERRAIN_SHADOWS")
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowTorchSoft" inherits="OptionsSliderRowTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentRowTorchTerrainShadows" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentRowTorchTerrainShadows" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts><OnLoad>
|
||||
-- 0.5..3, the CVar's own clamp verbatim: it is a
|
||||
-- SCALE on the PCF tap radius at a contact, so the
|
||||
|
||||
@@ -1070,6 +1070,12 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
"1",
|
||||
"benilla's own: outdoor fire lights (campfires, braziers, lampposts) cast real shadows at night; no effect by day",
|
||||
),
|
||||
// MONKEY (daylight: terrain torch casters): the ground as a torch caster.
|
||||
ours(
|
||||
"torchTerrainShadows",
|
||||
"0",
|
||||
"benilla's own: the ground casts into outdoor fire shadows (hills and banks block a fire's light); needs exteriorShadows",
|
||||
),
|
||||
// MONKEY (static torch cache): residency and per-frame work have separate live budgets.
|
||||
ours(
|
||||
"interiorShadowCasters",
|
||||
@@ -2898,7 +2904,7 @@ mod tests {
|
||||
let shadows = VideoConfig::default();
|
||||
let flag = |b: bool| if b { 1.0 } else { 0.0 };
|
||||
// MONKEY (volumetric fog): include the atmospheric tier in this fixed-size default table.
|
||||
let lighting: [(&str, f32); 33] = [
|
||||
let lighting: [(&str, f32); 34] = [
|
||||
("waterQuality", shadows.water_quality as f32),
|
||||
// MONKEY (volumetric fog): weld registry and renderer defaults.
|
||||
("volumetricFog", shadows.volumetric_fog as f32),
|
||||
@@ -2926,6 +2932,8 @@ mod tests {
|
||||
// MONKEY (torch shadows): the cube-map lane, indoors and out.
|
||||
("interiorShadows", flag(shadows.interior_shadows)),
|
||||
("exteriorShadows", flag(shadows.exterior_shadows)),
|
||||
// MONKEY (daylight: terrain torch casters)
|
||||
("torchTerrainShadows", flag(shadows.torch_terrain_shadows)),
|
||||
(
|
||||
"interiorShadowCasters",
|
||||
shadows.interior_shadow_casters as f32,
|
||||
@@ -2957,7 +2965,7 @@ mod tests {
|
||||
// row the client does not have; the length is the other half — 33 rows, 33 welds.
|
||||
let welded: std::collections::BTreeSet<&str> = lighting.iter().map(|(n, _)| *n).collect();
|
||||
// MONKEY (volumetric fog): the atmospheric tier joins the default-consumer weld.
|
||||
assert_eq!(welded.len(), 33, "the lighting lane welds 33 distinct rows");
|
||||
assert_eq!(welded.len(), 34, "the lighting lane welds 34 distinct rows");
|
||||
for name in &welded {
|
||||
assert!(
|
||||
REGISTERED.iter().any(|r| r.name == *name),
|
||||
|
||||
@@ -645,6 +645,23 @@ struct EntityCasters<'w, 's> {
|
||||
/// bundle rather than being a 17th system param because `update_torch_shadows` already sits at
|
||||
/// Bevy's 16-param ceiling — the same reason `distance` is here.
|
||||
lighting: Res<'w, WowLighting>,
|
||||
/// MONKEY (daylight: terrain torch casters): the resident ADT tiles, for the ground half of an
|
||||
/// exterior slot's static caster mesh (`torchTerrainShadows`). `Option` so a scene with no
|
||||
/// terrain streamer (the glue, a booth) is simply terrain-less.
|
||||
terrain: Option<Res<'w, benilla_world::terrain_stream::TerrainStreamer>>,
|
||||
adt_tiles: Option<Res<'w, Assets<benilla_assets::AdtTile>>>,
|
||||
}
|
||||
|
||||
/// MONKEY (daylight: terrain torch casters): does an EXTERIOR slot's static map include the ground?
|
||||
/// The cvar `torchTerrainShadows`, or `WOW_TORCH_TERRAIN=0|1` (read once) for hermetic captures.
|
||||
fn terrain_casters_on(video: &VideoConfig) -> bool {
|
||||
static ENV: std::sync::OnceLock<Option<bool>> = std::sync::OnceLock::new();
|
||||
ENV.get_or_init(|| match std::env::var("WOW_TORCH_TERRAIN").ok().as_deref() {
|
||||
Some("1") => Some(true),
|
||||
Some("0") => Some(false),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(video.torch_terrain_shadows)
|
||||
}
|
||||
|
||||
/// The INTERIOR lane is active when the dynamic-interior lane is on AND its shadow toggle is on.
|
||||
@@ -1123,9 +1140,22 @@ fn update_torch_shadows(
|
||||
// While this pair is unchanged the fixture-local source set cannot have changed either, so the
|
||||
// slot's walk is skipped outright — which is what turns "a full scene scan every frame,
|
||||
// forever, in a room where nothing ever moves" into "a scan the frame something does".
|
||||
// MONKEY (daylight: terrain torch casters): the resident terrain set joins the retained
|
||||
// scene's stamp, so a tile streaming in re-checks the exterior slots near it; the switch itself
|
||||
// is folded in so toggling the cvar re-keys every slot.
|
||||
let terrain_on = terrain_casters_on(&video);
|
||||
let terrain_gen = match (&ents.terrain, &ents.adt_tiles) {
|
||||
(Some(t), Some(a)) if terrain_on => {
|
||||
benilla_world::terrain_stream::terrain_torch_generation(t, a) ^ 0x7e44_a1d0
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
let census = (slot_count > 0)
|
||||
.then(|| PartCensus {
|
||||
gx: gx.as_ref().map_or(0, |gx| gx.torch_residency_generation()),
|
||||
gx: gx
|
||||
.as_ref()
|
||||
.map_or(0, |gx| gx.torch_residency_generation())
|
||||
.wrapping_add(terrain_gen),
|
||||
parts: static_part_census(&ents),
|
||||
})
|
||||
.unwrap_or_default();
|
||||
@@ -1210,6 +1240,12 @@ fn update_torch_shadows(
|
||||
entity_key = entity_key.wrapping_add(part_hash.finish());
|
||||
}
|
||||
entity_key.hash(&mut hash);
|
||||
// MONKEY (daylight: terrain torch casters): settled EXTERIOR slots only. An interior
|
||||
// fixture's room already occludes the ground, and a moving slot regathers every frame.
|
||||
let with_terrain = terrain_gen != 0 && slot.exterior && !moving;
|
||||
if with_terrain {
|
||||
terrain_gen.hash(&mut hash);
|
||||
}
|
||||
let key = hash.finish();
|
||||
let dirty = slot.geometry_key != Some(key);
|
||||
if !dirty {
|
||||
@@ -1235,6 +1271,15 @@ fn update_torch_shadows(
|
||||
});
|
||||
let (_, part_excluded) =
|
||||
collect_torch_entities(&ents, true, &gather, &mut positions, &mut indices);
|
||||
if with_terrain {
|
||||
if let (Some(t), Some(a)) = (&ents.terrain, &ents.adt_tiles) {
|
||||
let tris = benilla_world::terrain_stream::append_terrain_torch_triangles(
|
||||
t, a, slot.pos, range, &mut positions, &mut indices);
|
||||
if std::env::var_os("WOW_TORCH_TRACE").is_some() {
|
||||
info!("torch-terrain: slot {i} +{tris} ground tris within {range:.0} yd");
|
||||
}
|
||||
}
|
||||
}
|
||||
slot.trace_excluded = (gx_excluded, part_excluded);
|
||||
// MONKEY (moving fixture): a live slot MUTATES its mesh in place and keeps its asset
|
||||
// id; only a settled rebuild takes a fresh one. Two reasons. A new `Mesh` asset every
|
||||
|
||||
@@ -3051,6 +3051,11 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
|
||||
"BENILLA_TOOLTIP_EXTERIOR_SHADOWS",
|
||||
"AdvancedGraphicsRowExteriorShadows",
|
||||
),
|
||||
// MONKEY (daylight: terrain torch casters)
|
||||
(
|
||||
"BENILLA_TOOLTIP_TORCH_TERRAIN_SHADOWS",
|
||||
"AdvancedGraphicsRowTorchTerrainShadows",
|
||||
),
|
||||
("BENILLA_TOOLTIP_TORCH_SOFT", "AdvancedGraphicsRowTorchSoft"),
|
||||
("BENILLA_TOOLTIP_FIRE_LIGHT", "AdvancedGraphicsRowFireLight"),
|
||||
(
|
||||
|
||||
@@ -314,6 +314,11 @@ pub(crate) struct VideoConfig {
|
||||
/// It shares `interior_shadow_casters`' sixteen resident cube slots, capped at half of them
|
||||
/// (`torch_shadow::exterior_budget`) so a village square cannot evict an inn's candles.
|
||||
pub(crate) exterior_shadows: bool,
|
||||
/// MONKEY (daylight: terrain torch casters): whether the GROUND casts into an exterior torch's
|
||||
/// cube map (`torchTerrainShadows`, default off; High = on) — a hill or bank between a fire and
|
||||
/// the slope behind it blocks the fire. Only settled exterior slots gather it
|
||||
/// (`torch_shadow`); `0` leaves the torch lane exactly as it was.
|
||||
pub(crate) torch_terrain_shadows: bool,
|
||||
/// MONKEY (static torch cache): resident fixture budget (1..16, default 12). Static
|
||||
/// geometry renders only on promotion/residency changes; lowering this fades extra slots out.
|
||||
pub(crate) interior_shadow_casters: u32,
|
||||
@@ -439,6 +444,7 @@ impl Default for VideoConfig {
|
||||
// MONKEY (outdoor torch shadows): on — a night campfire with no shadow is the thing
|
||||
// this lane exists to fix, and it costs nothing whenever the sun is up.
|
||||
exterior_shadows: true,
|
||||
torch_terrain_shadows: false,
|
||||
// MONKEY (static torch cache): 12 resident maps, four moving-caster overlays.
|
||||
interior_shadow_casters: 12,
|
||||
interior_shadow_dynamic: 4,
|
||||
@@ -600,6 +606,8 @@ pub(crate) fn on_cvar(
|
||||
// reads `VideoConfig` every frame, so `0` fades the outdoor shadows out (the slots evict
|
||||
// through the same cross-fade a walked-away fixture does) and `1` fades them back in.
|
||||
"exteriorshadows" => cfg.exterior_shadows = ev.flag(),
|
||||
// MONKEY (daylight: terrain torch casters)
|
||||
"torchterrainshadows" => cfg.torch_terrain_shadows = ev.flag(),
|
||||
// MONKEY (torch caster selection): the working-set size and the PCF radius, clamped at the
|
||||
// edge like every other numeric row. `casters` floors at 1, not 0 — `interiorShadows 0` is
|
||||
// already the off switch, and a 0 here would be a second, confusing one.
|
||||
|
||||
@@ -34,6 +34,9 @@ mod queries;
|
||||
mod spawn;
|
||||
mod weld;
|
||||
pub(crate) mod window;
|
||||
// MONKEY (daylight: terrain torch casters): resident MCNK chunks as torch cube-map casters.
|
||||
mod torch_terrain;
|
||||
pub use torch_terrain::{append_terrain_torch_triangles, terrain_torch_generation};
|
||||
|
||||
use collider::{finish_colliders, impassable_wall_data, terrain_collider_data};
|
||||
use furnish::furnish_tile_cells;
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
//! MONKEY (daylight: terrain torch casters): terrain as a CASTER in the torch cube maps.
|
||||
//!
|
||||
//! The torch lane's static caster mesh is gathered from the retained scene (`StaticGx`) and the
|
||||
//! rigid model parts; the ground was never in it, so a hill or a bank between a campfire and the
|
||||
//! slope behind it never blocked the fire. This module hands the gather the resident MCNK chunks
|
||||
//! within a fixture's reach, as world-space triangles (hole-masked indices, draw space — the same
|
||||
//! vertices `collider::terrain_collider_data` welds, so the depth map and the drawn ground agree).
|
||||
//!
|
||||
//! Budget: one chunk is 145 vertices / up to 256 triangles; a 48 yd sphere touches at most ~16
|
||||
//! chunks, so a slot gathers at most ~2.3k vertices and ~4k triangles once, when its static map
|
||||
//! rebuilds. Chunks are culled by their own box against the sphere, then per triangle.
|
||||
|
||||
use benilla_assets::coords::wow_to_bevy;
|
||||
use benilla_assets::AdtTile;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use super::TerrainStreamer;
|
||||
|
||||
/// The terrain vertex bounds of one chunk in WoW axes. A chunk's positions are absolute world
|
||||
/// yards, so the box is found without the tile index.
|
||||
fn chunk_box(chunk: &benilla_formats::ChunkMesh) -> Option<(Vec3, Vec3)> {
|
||||
let mut it = chunk.positions.iter();
|
||||
let first = Vec3::from(*it.next()?);
|
||||
let (mut lo, mut hi) = (first, first);
|
||||
for p in it {
|
||||
let p = Vec3::from(*p);
|
||||
lo = lo.min(p);
|
||||
hi = hi.max(p);
|
||||
}
|
||||
Some((lo, hi))
|
||||
}
|
||||
|
||||
fn box_sphere_d2(lo: Vec3, hi: Vec3, c: Vec3) -> f32 {
|
||||
(c.clamp(lo, hi) - c).length_squared()
|
||||
}
|
||||
|
||||
/// Append every resident terrain triangle within `reach` of `center` (Bevy world space) to the
|
||||
/// caster buffers, and return how many triangles were added. A triangle is kept when its own
|
||||
/// box reaches the sphere — cheaper than an exact test and never drops a real occluder.
|
||||
pub fn append_terrain_torch_triangles(
|
||||
streamer: &TerrainStreamer,
|
||||
adt_tiles: &Assets<AdtTile>,
|
||||
center: Vec3,
|
||||
reach: f32,
|
||||
positions: &mut Vec<[f32; 3]>,
|
||||
indices: &mut Vec<u32>,
|
||||
) -> u32 {
|
||||
// Chunk boxes are in WoW axes, so test the sphere there (the conversion is a rigid swizzle).
|
||||
let c = Vec3::from(benilla_assets::coords::bevy_to_wow(center));
|
||||
let r2 = reach * reach;
|
||||
let mut added = 0u32;
|
||||
for ts in streamer.tiles.values() {
|
||||
let Some(adt) = adt_tiles.get(&ts.handle) else {
|
||||
continue;
|
||||
};
|
||||
for chunk in &adt.chunks {
|
||||
let Some((lo, hi)) = chunk_box(chunk) else {
|
||||
continue;
|
||||
};
|
||||
if box_sphere_d2(lo, hi, c) > r2 {
|
||||
continue;
|
||||
}
|
||||
let base = positions.len() as u32;
|
||||
let mut used = false;
|
||||
for tri in chunk.indices.as_chunks::<3>().0 {
|
||||
let (Some(a), Some(b), Some(d)) = (
|
||||
chunk.positions.get(tri[0] as usize),
|
||||
chunk.positions.get(tri[1] as usize),
|
||||
chunk.positions.get(tri[2] as usize),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let (a, b, d) = (Vec3::from(*a), Vec3::from(*b), Vec3::from(*d));
|
||||
if box_sphere_d2(a.min(b).min(d), a.max(b).max(d), c) > r2 {
|
||||
continue;
|
||||
}
|
||||
if !used {
|
||||
positions.extend(chunk.positions.iter().map(|p| wow_to_bevy(*p).to_array()));
|
||||
used = true;
|
||||
}
|
||||
indices.extend(tri.iter().map(|i| base + i));
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
added
|
||||
}
|
||||
|
||||
/// A cheap stamp of the resident terrain set: which tiles are loaded (decoded) right now. Folded
|
||||
/// into the torch lane's census and slot fingerprint so a tile streaming in or out re-gathers the
|
||||
/// fixtures near it. Order-independent.
|
||||
pub fn terrain_torch_generation(streamer: &TerrainStreamer, adt_tiles: &Assets<AdtTile>) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut sum = 0u64;
|
||||
for (key, ts) in &streamer.tiles {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
key.hash(&mut h);
|
||||
ts.handle.id().hash(&mut h);
|
||||
adt_tiles.contains(&ts.handle).hash(&mut h);
|
||||
sum = sum.wrapping_add(h.finish());
|
||||
}
|
||||
sum
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn box_sphere_distance_is_zero_inside_and_grows_outside() {
|
||||
let (lo, hi) = (Vec3::ZERO, Vec3::ONE);
|
||||
assert_eq!(box_sphere_d2(lo, hi, Vec3::splat(0.5)), 0.0);
|
||||
assert!((box_sphere_d2(lo, hi, Vec3::new(3.0, 0.5, 0.5)) - 4.0).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user