diff --git a/LIGHTING.md b/LIGHTING.md index 6dd43bc3..af157726 100644 --- a/LIGHTING.md +++ b/LIGHTING.md @@ -24,7 +24,7 @@ behind each constant. | 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` | | Volumetric fog | near-field haze that converges on the zone fog colour (clear within 10 yd, full by 150 yd; mistier at dawn and in bad weather, faint indoors) and sun/moon light shafts through gaps, sampled from the shadow map; own fullscreen pass after the main pass (`benilla-app/src/volumetric_fog.rs`) | Advanced Graphics → Volumetric Fog (Off/Low/High), cvar `volumetricFog`, env `WOW_VOLFOG=0\|1\|2` | -| Ambient occlusion | soft contact shadows under props, in wall-floor corners and around feet; depth-only SSAO at half resolution (depth-reconstructed normals, 6/12 spiral taps, 4x4 depth-aware blur, bilateral upsample) multiplied into the opaque scene after the opaque pass and before the water copy, so water, particles and UI are untouched; fades out 35-80 yd, spares sky and bright pixels (`benilla-app/src/ssao.rs`, `benilla-app/src/shaders/ssao.wgsl`) | Advanced Graphics → Ambient Occlusion (Off/Low/High; default Off, High preset 2), cvar `ambientOcclusion`, env `WOW_AO=0\|1\|2`, `WOW_AO_DEBUG=1` shows the AO term alone | +| Ambient occlusion | soft contact shadows under props, in wall-floor corners and around feet; depth-only SSAO at half resolution (depth-reconstructed normals, 6/12 spiral taps, 4x4 depth-aware blur, bilateral upsample) multiplied into the opaque scene after the opaque pass and before the water copy, so water, particles and UI are untouched; fades out 45-90 yd (Low) / 60-120 yd (High), spares sky and bright pixels (`benilla-app/src/ssao.rs`, `benilla-app/src/shaders/ssao.wgsl`) | Advanced Graphics → Ambient Occlusion (Off/Low/High; default Off, High preset 2), cvar `ambientOcclusion`, env `WOW_AO=0\|1\|2`, `WOW_AO_DEBUG=1..4` diagnostic views (factor, protection, distance, raw occlusion), `WOW_AO_GAIN`/`_RADIUS`/`_BIAS`/`_STRENGTH` tuning | | Night and interior level | global dimming of the night sky term and of interior ambient | `nightGain`, `interiorGain`, `interiorBakeFloor` | Players reach all of it from **Options -> Advanced Graphics** (a Lighting Quality preset Off / Low / Medium / High plus the individual rows; Off is the original client look). The dev build has a panel for all of it: **Ctrl+Shift+D → Lighting & shadows**, with Dim / Default / diff --git a/crates/benilla-app/src/shaders/ssao.wgsl b/crates/benilla-app/src/shaders/ssao.wgsl index 2cc07f9e..c3459723 100644 --- a/crates/benilla-app/src/shaders/ssao.wgsl +++ b/crates/benilla-app/src/shaders/ssao.wgsl @@ -13,6 +13,8 @@ struct Ao { params: vec4, // Distance fade start and end in yards, protection luma ramp start and end. fade: vec4, + // x: debug view (0 off, 1 factor, 2 protection, 3 distance/50, 4 raw occlusion); y: gain. + debug: vec4, } const SKY_DIST: f32 = 60000.0; @@ -116,7 +118,7 @@ fn ao_main(@builtin(position) frag: vec4) -> @location(0) vec4 { let w = saturate(1.0 - vv / r2); occ += w * max(0.0, dot(v, n) * inverseSqrt(vv + 1e-6) - ao.params.w); } - occ = saturate(2.0 * occ / f32(count)) * fade; + occ = saturate(ao.debug.y * occ / f32(count)) * fade; return vec4(occ, dist, protect, 1.0); } #endif @@ -148,7 +150,11 @@ fn blur_main(@builtin(position) frag: vec4) -> @location(0) vec4 { fn apply_main(@builtin(position) frag: vec4) -> @location(0) vec4 { let p = vec2(frag.xy); let z = depth_at(p); - if (is_sky(z)) { return vec4(1.0); } + let mode = u32(ao.debug.x); + if (is_sky(z)) { + // Sky is magenta in the diagnostic views, so "depth reads as sky" is visible. + return select(vec4(1.0), vec4(1.0, 0.0, 1.0, 1.0), mode >= 2u); + } let dist = -view_at(p, z).z; let size = vec2(textureDimensions(source)); let hc = frag.xy * 0.5 - 0.5; @@ -168,6 +174,10 @@ fn apply_main(@builtin(position) frag: vec4) -> @location(0) vec4 { } occ /= weight; protect /= weight; + if (mode == 2u) { return vec4(protect, protect, protect, 1.0); } + // Red = positive view distance, green = negative (a sign error), both / 50 yd. + if (mode == 3u) { return vec4(saturate(dist / 50.0), saturate(-dist / 50.0), 0.0, 1.0); } + if (mode == 4u) { return vec4(1.0 - occ, 1.0 - occ, 1.0 - occ, 1.0); } let k = 1.0 - ao.params.y * occ * (1.0 - protect); return vec4(k, k, k, 1.0); } diff --git a/crates/benilla-app/src/ssao.rs b/crates/benilla-app/src/ssao.rs index a7c8c81d..d81d75e6 100644 --- a/crates/benilla-app/src/ssao.rs +++ b/crates/benilla-app/src/ssao.rs @@ -6,11 +6,12 @@ //! blur, then a joint-bilateral upsample MULTIPLIED into the main colour attachment (the MSAA //! attachment when multisampled, so the resolve and every later pass see it). It runs before //! the water depth/colour copy and before transparents, so water, particles and UI are -//! untouched and refraction sees the darkened bed. Sky, distant pixels (fade 35-80 yd) and -//! bright pixels (lit windows, flames, sunlit sand) are protected. +//! untouched and refraction sees the darkened bed. Sky, distant pixels and +//! bright pixels (lit windows, flames, sunlit sand) are protected; fade 45-90 yd (Low) / 60-120 yd (High). //! //! cvar `ambientOcclusion`: 0 Off (pass not scheduled, image unchanged), 1 Low, 2 High. -//! `WOW_AO=0|1|2` overrides it for the session; `WOW_AO_DEBUG=1` writes the AO term itself. +//! `WOW_AO=0|1|2` overrides it for the session; `WOW_AO_DEBUG=1..4` writes a diagnostic view +//! (factor, protection, distance, raw occlusion); `WOW_AO_GAIN/RADIUS/BIAS/STRENGTH` tune it. use crate::video::VideoConfig; use benilla_world::{liquid::WaterDepthLabel, view::WorldCamera}; use bevy::{ @@ -49,21 +50,53 @@ struct AoView { params: Vec4, /// Distance fade start and end (yd), bright-pixel protection ramp (gamma max channel). fade: Vec4, + /// x = debug view (0 off, 1 AO factor, 2 protection, 3 distance/50, 4 raw occlusion), + /// y = occlusion gain. + debug: Vec4, +} + +/// Dev tuning knob `WOW_AO_=`, read once per name; `None` keeps the tier value. +fn tuning(name: &'static str) -> Option { + static KNOBS: std::sync::Mutex)>> = std::sync::Mutex::new(Vec::new()); + let mut knobs = KNOBS.lock().unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some((_, v)) = knobs.iter().find(|(n, _)| *n == name) { + return *v; + } + let v = std::env::var(format!("WOW_AO_{name}")) + .ok() + .and_then(|v| v.parse::().ok()) + .filter(|v| v.is_finite() && *v >= 0.0); + knobs.push((name, v)); + v } impl AoView { fn tier(tier: u8) -> Self { - let (radius, strength, samples) = if tier >= 2 { (1.5, 0.55, 12.0) } else { (1.2, 0.5, 6.0) }; + let (radius, strength, samples) = if tier >= 2 { (1.5, 0.7, 12.0) } else { (1.2, 0.65, 6.0) }; + // Outdoor contacts (house bases, trunks) sit 30-60 yd out; Low stops sooner. + let fade = if tier >= 2 { (60.0, 120.0) } else { (45.0, 90.0) }; + let radius = tuning("RADIUS").unwrap_or(radius); + let strength = tuning("STRENGTH").unwrap_or(strength).min(1.0); + let bias = tuning("BIAS").unwrap_or(0.05); + let gain = tuning("GAIN").unwrap_or(6.0); Self { - params: Vec4::new(radius, strength, samples, 0.1), - fade: Vec4::new(35.0, 80.0, 0.7, 0.95), + params: Vec4::new(radius, strength, samples, bias), + fade: Vec4::new(fade.0, fade.1, 0.7, 0.95), + debug: Vec4::new(debug_mode() as f32, gain, 0.0, 0.0), } } } +/// `WOW_AO_DEBUG=1..4`: write a diagnostic term instead of darkening the scene. +fn debug_mode() -> u8 { + static MODE: std::sync::OnceLock = std::sync::OnceLock::new(); + *MODE.get_or_init(|| { + std::env::var("WOW_AO_DEBUG").ok().and_then(|v| v.parse().ok()).unwrap_or(0).min(4) + }) +} + fn debug_view() -> bool { - static ON: std::sync::OnceLock = std::sync::OnceLock::new(); - *ON.get_or_init(|| std::env::var("WOW_AO_DEBUG").as_deref() == Ok("1")) + debug_mode() != 0 } impl Plugin for AmbientOcclusionPlugin {