MONKEY (ao): diagnostic views, tuned gain/bias/strength, longer distance fade for outdoor contacts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pkuzic
2026-09-25 10:37:13 +01:00
co-authored by Claude Opus 5.5
parent dd0bb2bba9
commit 9eda0a738f
3 changed files with 54 additions and 11 deletions
+1 -1
View File
@@ -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 /
+12 -2
View File
@@ -13,6 +13,8 @@ struct Ao {
params: vec4<f32>,
// Distance fade start and end in yards, protection luma ramp start and end.
fade: vec4<f32>,
// x: debug view (0 off, 1 factor, 2 protection, 3 distance/50, 4 raw occlusion); y: gain.
debug: vec4<f32>,
}
const SKY_DIST: f32 = 60000.0;
@@ -116,7 +118,7 @@ fn ao_main(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
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<f32>) -> @location(0) vec4<f32> {
fn apply_main(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
let p = vec2<i32>(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<i32>(textureDimensions(source));
let hc = frag.xy * 0.5 - 0.5;
@@ -168,6 +174,10 @@ fn apply_main(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
}
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);
}
+41 -8
View File
@@ -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_<name>=<f32>`, read once per name; `None` keeps the tier value.
fn tuning(name: &'static str) -> Option<f32> {
static KNOBS: std::sync::Mutex<Vec<(&'static str, Option<f32>)>> = 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::<f32>().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<u8> = 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<bool> = 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 {