MONKEY (reviewfix-a): zone grading visible in captures; follows the rendered clock
Found by the first runtime capture of the grade: a capture has no avatar, so CurrentArea stayed None and no zone was ever graded; and the day/night weight read WorldTime (the server clock, noon offline), so a manual/capture night graded with the day LUT. Adds CaptureCameraArea (area under the camera, only under WOW_CAPTURE, never read by the area authority) and grades by the rendered GameClock minute. LIGHTING.md notes the preset/capture changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ddc001c849
commit
ac9a1ef390
+11
-1
@@ -31,7 +31,7 @@ behind each constant.
|
||||
| Sky quality | Enhanced: the five Light.dbc sky stops through a smooth monotone curve in linear light (no bands at the rings), a soft sun glow tinted by sun and fog colour (fades at night and under cloud), a procedural star field with twinkle and a faint Milky Way over the stock `Stars.m2`. High adds domain-warped cloud detail and sun-lit clouds (self-shadow, silver lining; technique from WarcraftXL, see `THIRD-PARTY.md`). Classic is the reference sky unchanged | Advanced Graphics → Sky Quality, cvar `skyQuality` 0 Classic / 1 Enhanced / 2 High, env `WOW_SKY_QUALITY` |
|
||||
| Modern fog | MONKEY (fog): radial distance fog with a gradual exponential curve, a daylight sun lobe, and an end colour shared by the world, WDL hull, sky horizon and volumetric haze. Each zone keeps its authored fog end up to the reference view-distance limit; optional `LightFogBand.dbc` rows provide height, sun and end-fog controls. Interior WMO fog stays classic | Advanced Graphics → Modern Fog, cvar `fogModel` 0 Classic (default) / 1 Modern (High = 1), env `WOW_FOGMODEL=0\|1`; `fog_hook.wgsl`, `lighting/fog_model.rs`, `light/fog_band.rs` |
|
||||
| Rain on surfaces | MONKEY (wet): rain gradually darkens and saturates exposed terrain and models, adds puddles and a sky/sun sheen, and produces rings on exterior Enhanced water. Wetness rises over roughly 90 seconds and dries over roughly four minutes; MONKEY (rainshelter): a camera-centred rain-occlusion map (128² cells of 0.75 yd, filled by vertical collision rays) keeps ground, walls and water under roofs, porches and bridges dry, with a ~1.5 yd drip margin and a soft edge; tree canopies without colliders do not shelter | Advanced Graphics → Wet Surfaces in Rain, cvar `rainSurfaces` 0/1 (default and High 1), env `WOW_RAIN_SURFACES`, `WOW_WETNESS`, `WOW_WET_T`; `weather/wetness.rs`, `weather/shelter.rs`, `wet_hook.wgsl` |
|
||||
| Foliage wind | one weather-fed gust/veer field drives grass and classified tree/bush foliage; grass also parts around the player and nearby units | Advanced Graphics → Foliage Wind, cvar `foliageWind` 0 Off / 1 Grass / 2 Grass + Trees (High = 2), capture override `WOW_FOLIAGE_WIND`; field and benders in `benilla-world/src/wind/`. The wave phase is the CPU-integrated travel (speed integrated over time, wrapped seamlessly), and the waves' spatial term uses the fixed profile heading; the veer turns only the bend. Tree exile copies sway through material marker bit 14 (`FOLIAGE_WIND_MARKER`). Capture offset `WOW_CAPTURE_WIND_T` (s). Known: shadow casters (world-shadow proxies, `torch_depth.wgsl`) do not apply the sway, so leaves sample a static shadow; a future world-camera DepthPrepass must call the same offsets |
|
||||
| Foliage wind | one weather-fed gust/veer field drives grass and classified tree/bush foliage; grass also parts around the player and nearby units | Advanced Graphics → Foliage Wind, cvar `foliageWind` 0 Off / 1 Grass / 2 Grass + Trees (High = 2), capture override `WOW_FOLIAGE_WIND` (dev build + `WOW_CAPTURE` only); field and benders in `benilla-world/src/wind/`. The wave phase is the CPU-integrated travel (speed integrated over time, wrapped seamlessly), and the waves' spatial term uses the fixed profile heading; the veer turns only the bend. Tree exile copies sway through material marker bit 14 (`FOLIAGE_WIND_MARKER`). Capture offset `WOW_CAPTURE_WIND_T` (s). Known: shadow casters (world-shadow proxies, `torch_depth.wgsl`) do not apply the sway, so leaves sample a static shadow; a future world-camera DepthPrepass must call the same offsets |
|
||||
| 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; on alpha-tested foliage (a dense window of two-sided depth spikes) the blur widens its depth tolerance and the AO drops 40 %, which removes the leaf grain while planes, silhouettes and contact creases keep full AO (`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..5` diagnostic views (factor, protection, distance, raw occlusion, foliage mask), `WOW_AO_GAIN`/`_RADIUS`/`_BIAS`/`_STRENGTH` tuning |
|
||||
| Lamp fog | MONKEY (lampfog): the fog fullscreen pass selects the nearest resolved exterior and interior point-table sources and analytically integrates their softened inverse-square light over the camera-to-scene-depth ray. A near-field haze floor makes lamps visible inside the ordinary volumetric haze's 10 yd clear zone; it is zero by day, stronger in rain/fog, and deliberately unshadowed. The table colour already includes intensity, flame flicker and `fireLightGain`. Halo output uses destination headroom and stays below 1.0, so a later bloom pass cannot blow it out | Advanced Graphics → Lamp Fog, cvar `lampFog` 0 Off (default, unchanged) / 1 Low (16 lamps) / 2 High (32 lamps; Graphics preset High = 2), env `WOW_LAMPFOG=0\|1\|2`; `benilla-app/src/volumetric_fog.rs` |
|
||||
| City window daylight | MONKEY (fix-daylight/integration): a district WMO's oversized window batch (e.g. the Cathedral of Light's stained-glass walls) is split into window-sized daylight apertures, so the nave takes daylight pane by pane. Applies to WMOs loaded after a change | Advanced Graphics → City Window Daylight, cvar `daylightWindowSplit` 0/1 (default 1, the merged behaviour); `benilla-world/src/lighting/daylight.rs` (`set_window_split`), bridge `benilla-app/src/dynamic_interior.rs` |
|
||||
@@ -76,6 +76,16 @@ and High volumetric fog. No rung decides `shadowDistance` or `interiorShadowSoft
|
||||
column at startup (`Cvars::seed_graphics_preset`) on every row the file does not carry; the rows'
|
||||
registered defaults stay their per-lane ones (`farclip` the reference's 350), and captures and
|
||||
tests, which never seed, keep them. The Advanced page's Defaults button lands on High too.
|
||||
The seeded boot value that leaves the reference (`farclip` 777 against 350) is declared in
|
||||
`cvars::SEEDED_DEVIATIONS`, and a test walks the whole High column against the reference rows.
|
||||
Session-owned rows (env levers such as `WOW_FARCLIP`) are skipped when both labels are derived, so
|
||||
an env override never persists `Custom` (MONKEY (reviewfix-a)).
|
||||
|
||||
**Capture fixtures** (MONKEY (reviewfix-a)). A capture reads CVars only from
|
||||
`WOW_CAPTURE_CVARS=<config.toml path>` (read-only; nothing is saved), never from `BENILLA_HOME`.
|
||||
`WOW_FOLIAGE_WIND` is honoured only in a dev build under `WOW_CAPTURE`. A capture has no avatar,
|
||||
so zone grading reads `terrain_stream::CaptureCameraArea` (the area under the camera, written only
|
||||
under `WOW_CAPTURE`) and the grade's day/night weight follows the rendered `GameClock` minute.
|
||||
|
||||
**Saved presets and new rows** (MONKEY (followups)). On load, `Cvars::reapply_saved_presets`
|
||||
writes the saved `graphicsQuality` rung (and the saved `lightingQuality` rung, else the Graphics
|
||||
|
||||
@@ -282,14 +282,19 @@ fn update_views(
|
||||
video: Res<VideoConfig>,
|
||||
catalog: Option<Res<GradeCatalog>>,
|
||||
area: Res<CurrentArea>,
|
||||
// MONKEY (reviewfix-a): a capture has no avatar; it grades by the area under the camera.
|
||||
capture_area: Option<Res<benilla_world::terrain_stream::CaptureCameraArea>>,
|
||||
areas: Option<Res<crate::area::AreaTableRes>>,
|
||||
time: Res<WorldTime>,
|
||||
// MONKEY (reviewfix-a): the minute the lighting actually rendered (server, or the manual /
|
||||
// capture clock); `WorldTime` alone is the server's and stays at its noon default offline.
|
||||
rendered: Option<Res<benilla_world::lighting::GameClock>>,
|
||||
clock: Res<Time>,
|
||||
mut blend: ResMut<GradeBlend>,
|
||||
cameras: Query<(Entity, &Camera, Option<&GradeView>), With<WorldCamera>>,
|
||||
) {
|
||||
let selected = catalog.as_ref().and_then(|catalog| {
|
||||
let leaf = area.0?;
|
||||
let leaf = area.0.or_else(|| capture_area.as_ref().and_then(|a| a.0))?;
|
||||
let zone = areas
|
||||
.as_ref()
|
||||
.and_then(|areas| areas.0.top_zone(leaf))
|
||||
@@ -313,6 +318,7 @@ fn update_views(
|
||||
})
|
||||
});
|
||||
blend.step(selected, clock.delta_secs(), !video.color_grading);
|
||||
let minute = rendered_minute(&time, rendered.as_deref());
|
||||
let selected = catalog.as_ref().and_then(|catalog| {
|
||||
if blend.current.is_none() && blend.previous.is_none() {
|
||||
return None;
|
||||
@@ -332,7 +338,7 @@ fn update_views(
|
||||
prev_night,
|
||||
control: Vec4::new(
|
||||
strength,
|
||||
night_weight(time.minute_f),
|
||||
night_weight(minute),
|
||||
blend.fade,
|
||||
prev_strength,
|
||||
),
|
||||
@@ -354,6 +360,15 @@ fn update_views(
|
||||
}
|
||||
}
|
||||
|
||||
/// MONKEY (reviewfix-a): the server's fractional minute while it is the minute being rendered,
|
||||
/// else the rendered (manual or capture) minute.
|
||||
fn rendered_minute(time: &WorldTime, rendered: Option<&benilla_world::lighting::GameClock>) -> f32 {
|
||||
match rendered {
|
||||
Some(c) if time.minute_f.floor() as u32 != c.minute => c.minute as f32,
|
||||
_ => time.minute_f,
|
||||
}
|
||||
}
|
||||
|
||||
fn night_weight(minute: f32) -> f32 {
|
||||
let dawn = smoothstep(330.0, 450.0, minute);
|
||||
let dusk = smoothstep(1110.0, 1230.0, minute);
|
||||
@@ -538,6 +553,25 @@ impl ViewNode for GradeNode {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// MONKEY (reviewfix-a): offline (capture / manual clock) the grade follows the rendered
|
||||
/// minute, not the server's noon default; live it keeps the server's fraction.
|
||||
#[test]
|
||||
fn grade_night_weight_follows_the_rendered_minute() {
|
||||
let server = WorldTime::default();
|
||||
let manual = benilla_world::lighting::GameClock {
|
||||
minute: 1260,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(rendered_minute(&server, Some(&manual)), 1260.0);
|
||||
assert_eq!(night_weight(rendered_minute(&server, Some(&manual))), 1.0);
|
||||
let live = WorldTime {
|
||||
minute_f: 1260.5,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(rendered_minute(&live, Some(&manual)), 1260.5);
|
||||
assert_eq!(rendered_minute(&live, None), 1260.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_volume_has_exact_axes() {
|
||||
let cube = identity_cube();
|
||||
|
||||
@@ -59,7 +59,7 @@ pub use spawn::{
|
||||
use queries::update_current_area;
|
||||
pub use queries::{
|
||||
area_id_under, doodad_ground_shade, ground_effect_under, terrain_height_under,
|
||||
terrain_height_under_cached, AreaAuthoritySet, CurrentArea, ShadeResolve,
|
||||
terrain_height_under_cached, AreaAuthoritySet, CaptureCameraArea, CurrentArea, ShadeResolve,
|
||||
};
|
||||
|
||||
/// Wall-clock per frame for spawning streamed tiles and placements before the rest waits a frame;
|
||||
@@ -389,6 +389,8 @@ impl Plugin for TerrainPlugin {
|
||||
.init_resource::<StaticMerge>()
|
||||
.init_resource::<crate::model_forms::ModelForms>()
|
||||
.init_resource::<CurrentArea>()
|
||||
// MONKEY (reviewfix-a): the capture-only camera area (zone grading in captures).
|
||||
.init_resource::<CaptureCameraArea>()
|
||||
// In `WorldStage::Stream`, between the teleport snap (Input) and the loading cover
|
||||
// (Present), so a swap never renders uncovered. `finish_colliders` heads the chain so
|
||||
// the collider queue read downstream is this frame's.
|
||||
@@ -444,7 +446,8 @@ impl Plugin for TerrainPlugin {
|
||||
update_current_area
|
||||
.after(crate::wmo_portal::WmoPvsSet)
|
||||
.in_set(AreaAuthoritySet),
|
||||
);
|
||||
)
|
||||
.add_systems(Update, queries::update_capture_camera_area.in_set(AreaAuthoritySet));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,43 @@ use super::TerrainStreamer;
|
||||
#[derive(Resource, Default, PartialEq, Eq)]
|
||||
pub struct CurrentArea(pub Option<u32>);
|
||||
|
||||
/// MONKEY (reviewfix-a): the area under the CAMERA, written only under `WOW_CAPTURE` while there
|
||||
/// is no avatar. A deterministic capture has no body, so [`CurrentArea`] stays `None`; area-keyed
|
||||
/// look lanes (zone grading) read this as their capture fallback. The area authority (music,
|
||||
/// channels, names) never reads it.
|
||||
#[derive(Resource, Default, PartialEq, Eq)]
|
||||
pub struct CaptureCameraArea(pub Option<u32>);
|
||||
|
||||
/// MONKEY (reviewfix-a): see [`CaptureCameraArea`].
|
||||
pub(super) fn update_capture_camera_area(
|
||||
mut out: ResMut<CaptureCameraArea>,
|
||||
focus: Res<crate::terrain_stream::ViewFocus>,
|
||||
streamer: Res<TerrainStreamer>,
|
||||
adt_tiles: Res<Assets<AdtTile>>,
|
||||
camera: Query<&GlobalTransform, With<crate::view::WorldCamera>>,
|
||||
mut capture: Local<Option<bool>>,
|
||||
) {
|
||||
if !*capture.get_or_insert_with(|| std::env::var_os("WOW_CAPTURE").is_some())
|
||||
|| focus.body_pos().is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(eye) = camera.iter().next().map(GlobalTransform::translation) else {
|
||||
return;
|
||||
};
|
||||
let wow = bevy_to_wow(eye);
|
||||
let (tx, ty) = world_to_tile(wow[0], wow[1]);
|
||||
let found = streamer
|
||||
.tiles
|
||||
.get(&(tx as i32, ty as i32))
|
||||
.and_then(|ts| adt_tiles.get(&ts.handle))
|
||||
.and_then(|adt| benilla_formats::area_id_at(&adt.chunks, wow))
|
||||
.filter(|&id| id != 0);
|
||||
if found.is_some() && out.0 != found {
|
||||
out.0 = found;
|
||||
}
|
||||
}
|
||||
|
||||
/// The set of [`update_current_area`]. Every consumer that acts on the area orders after it, so
|
||||
/// leaf, indoor bit and names come from one frame (the reference resolves them in one pass).
|
||||
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
|
||||
Reference in New Issue
Block a user