MONKEY: shadow render-distance slider (shadowDistance)
Make the 80 yd realtime-shadow range a runtime slider (Options -> Graphics -> Shadow Distance, 40-200 yd, default 80). Confirmed in-game. - shadowDistance cvar -> VideoConfig.shadow_distance, clamped to shadow_core::SHADOW_DISTANCE_RANGE. VideoConfig drops Eq (f32 field). - shadow_core reads it: the cascade maximum_distance + the caster-collection reaches. The CascadeShadowConfig is re-fit only when the slider changes (not per frame, which would defeat the texel snap and crawl every edge). - Bridged to a benilla-world ShadowDistance resource; global_light packs it into the free row-19 light lane so the receiver EDGE FADE fades at the cascade's actual max distance, whatever the slider is set to (terrain _wmo_fog[1].z / model wmo_fog_params.z). The fixed SHADOW_RANGE consts are gone. - Options: a deferred slider row (commits on Apply so a drag doesn't re-fit the cascade + recollect casters every tick). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bb610744c7
commit
68f8e31c48
@@ -687,6 +687,10 @@
|
||||
.. "following the sun, and switches off the baked terrain shadows they replace. "
|
||||
.. "Independent of Real Character Shadows. This uses additional GPU time and memory."
|
||||
|
||||
BENILLA_TOOLTIP_SHADOW_DISTANCE =
|
||||
"How far realtime shadows are drawn, in yards. Higher covers more ground but spreads "
|
||||
.. "the shadow map thinner (softer edges) and costs more. Applies on Okay/Apply."
|
||||
|
||||
OPTIONS_PAGE_ROWS = {
|
||||
Controls = {
|
||||
"RowStickyTarget", "RowBlockTrades", "RowAutoLoot", "RowInvertMouse",
|
||||
@@ -699,7 +703,7 @@
|
||||
},
|
||||
Graphics = {
|
||||
"RowUiScale", "RowFarclip", "RowWorldDetail",
|
||||
"RowDisplayMode", "RowVerticalSync", "RowCharacterShadows", "RowWorldShadows", "RowRenderScale",
|
||||
"RowDisplayMode", "RowVerticalSync", "RowCharacterShadows", "RowWorldShadows", "RowShadowDistance", "RowRenderScale",
|
||||
},
|
||||
Nameplates = {
|
||||
"RowEnemyPlates", "RowFriendlyPlates",
|
||||
@@ -2547,11 +2551,28 @@
|
||||
"BENILLA_TOOLTIP_WORLD_SHADOWS")
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowRenderScale" inherits="OptionsSliderRowTemplate">
|
||||
<Frame name="$parentRowShadowDistance" inherits="OptionsSliderRowTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentRowWorldShadows" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentRowWorldShadows" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts><OnLoad>
|
||||
-- Realtime-shadow render distance (yd): the cascade
|
||||
-- range + caster reach. 40-200, matching the CVar's
|
||||
-- own clamp (SHADOW_DISTANCE_RANGE). DEFERRED to
|
||||
-- Apply so a drag doesn't re-fit the cascade + recollect
|
||||
-- casters on every tick (one rebuild on commit instead).
|
||||
OptionsRow_OnLoad(self, "shadowDistance", "Shadow Distance",
|
||||
"BENILLA_TOOLTIP_SHADOW_DISTANCE")
|
||||
OptionsSliderRow_Setup(self, 40, 200, 10, "yards")
|
||||
self.deferred = 1
|
||||
</OnLoad></Scripts>
|
||||
</Frame>
|
||||
<Frame name="$parentRowRenderScale" inherits="OptionsSliderRowTemplate">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentRowShadowDistance" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentRowShadowDistance" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
<Scripts><OnLoad>
|
||||
-- Render Scale (decision 1639) -- benilla's own
|
||||
-- row, no 1.12 counterpart: the reference renders
|
||||
|
||||
@@ -54,6 +54,7 @@ use crate::ui_loot::LootConfig;
|
||||
use crate::ui_script::UiScaleCvar;
|
||||
use crate::video::VideoConfig;
|
||||
use crate::vplates::VPlateMode;
|
||||
use crate::shadow_core::SHADOW_DISTANCE_RANGE;
|
||||
use crate::world_backdrop::{RenderScale, RENDER_SCALE_RANGE};
|
||||
use benilla_ui::script::UiScript;
|
||||
use benilla_ui::widget::MINIMAP_ZOOM_LEVELS;
|
||||
@@ -591,6 +592,14 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
// players/NPCs/creatures/mounts cast realtime silhouettes instead of the legacy oval blob.
|
||||
same("worldShadows", "0"),
|
||||
same("characterShadows", "0"),
|
||||
// Realtime-shadow render distance in yards (the shadow-map cascade range + caster reach).
|
||||
// benilla's own — the reference has no realtime shadow to size. Clamped to SHADOW_DISTANCE_RANGE.
|
||||
ours(
|
||||
"shadowDistance",
|
||||
"80",
|
||||
"1900: benilla's own realtime-shadow render-distance slider; the reference bakes MCSH and \
|
||||
has no cascade to size",
|
||||
),
|
||||
// **Display mode** (decisions 1627, 1650) — 1.12's own `gxWindow`, worn since 1650 as modern
|
||||
// Classic's two-entry *Display Mode* dropdown rather than 1.12's *Windowed Mode* checkbox: the
|
||||
// two states 1627 settled on ARE that client's two (its own `Graphics.lua` builds the list from
|
||||
@@ -1072,6 +1081,10 @@ fn apply_to_knobs(name: &str, value: &str, knobs: &mut Knobs) -> bool {
|
||||
"gxvsync" => knobs.video.vsync = v != 0.0,
|
||||
"worldshadows" => knobs.video.world_shadows = v != 0.0,
|
||||
"charactershadows" => knobs.video.character_shadows = v != 0.0,
|
||||
"shadowdistance" => {
|
||||
knobs.video.shadow_distance =
|
||||
v.clamp(*SHADOW_DISTANCE_RANGE.start(), *SHADOW_DISTANCE_RANGE.end());
|
||||
}
|
||||
// Display mode (1627) — a flag like every other checkbox here, and the reference's own
|
||||
// polarity: `1` is WINDOWED (the row is "Windowed Mode"). `video::apply_window_mode`
|
||||
// watches the value and pushes it to the window; nothing else reads it.
|
||||
@@ -1374,7 +1387,7 @@ fn sync_cvars(
|
||||
.collect(),
|
||||
);
|
||||
let flag = |b: bool| if b { "1" } else { "0" }.to_string();
|
||||
let session: [(&str, String); 46] = [
|
||||
let session: [(&str, String); 47] = [
|
||||
("MasterVolume", sound.master.to_string()),
|
||||
("SoundVolume", sound.sfx.to_string()),
|
||||
("MusicVolume", sound.music.to_string()),
|
||||
@@ -1422,6 +1435,7 @@ fn sync_cvars(
|
||||
("gxVSync", flag(video.vsync)),
|
||||
("worldShadows", flag(video.world_shadows)),
|
||||
("characterShadows", flag(video.character_shadows)),
|
||||
("shadowDistance", video.shadow_distance.to_string()),
|
||||
// The reference's polarity: the CVar is `gxWindow`, so `1` is the WINDOWED state.
|
||||
(
|
||||
"gxWindow",
|
||||
|
||||
@@ -44,21 +44,27 @@ use benilla_assets::materials::WowModelMaterial;
|
||||
use benilla_formats::ModelBlend;
|
||||
use benilla_world::billboard::BillboardCard;
|
||||
use benilla_world::interact::PickMesh;
|
||||
use benilla_world::lighting::WowLighting;
|
||||
use benilla_world::lighting::{ShadowDistance, WowLighting};
|
||||
use benilla_world::model_render::{ModelKind, ModelPart, ShadowOccluder};
|
||||
use benilla_world::rig_palette::{RigPalettes, RigPart, RigSkin};
|
||||
use benilla_world::view::WorldCamera;
|
||||
|
||||
use crate::char_select::ClientState;
|
||||
use crate::video::VideoConfig;
|
||||
|
||||
/// Layer 31 is deliberately private to this feature. Bevy supports 32 render layers; the normal
|
||||
/// world is layer 0 and the UI/portrait layers occupy the low numbered slots.
|
||||
pub(crate) const PLAYER_SHADOW_LAYER: usize = 31;
|
||||
|
||||
/// How far from the camera the shadow map RESOLVES (the single cascade's `maximum_distance`).
|
||||
/// Collection reaches farther than this: a caster's own body may stand outside the cascade
|
||||
/// while its shadow lands inside it — so an out-of-frustum tree still darkens ground in view.
|
||||
const CASTER_RANGE: f32 = 80.0;
|
||||
/// Shipped realtime-shadow render distance in yards — the `shadowDistance` default. How far from the
|
||||
/// camera the shadow map RESOLVES (the single cascade's `maximum_distance`). Collection reaches
|
||||
/// farther than this (a caster's body may stand outside the cascade while its shadow lands inside).
|
||||
pub(crate) const DEFAULT_SHADOW_DISTANCE: f32 = 80.0;
|
||||
|
||||
/// The `shadowDistance` slider range (yd). Min keeps a useful near shadow; max is bounded because
|
||||
/// the fixed 4096 shadow map spreads thinner (softer) over a larger area and the caster-collection
|
||||
/// cost grows with distance.
|
||||
pub(crate) const SHADOW_DISTANCE_RANGE: std::ops::RangeInclusive<f32> = 40.0..=200.0;
|
||||
|
||||
/// How far the camera may drift before a cached caster-collection pass must refresh. The collection
|
||||
/// reach carries this as margin so coverage holds everywhere between refreshes.
|
||||
@@ -98,8 +104,8 @@ fn shadow_reach_extension(sun_travel: Vec3) -> f32 {
|
||||
|
||||
/// The collection law for a lane that can contribute a TALL caster (a doodad/WMO exile, or the whole
|
||||
/// static world): resolve range + rebuild margin + the sun-dependent shadow reach.
|
||||
pub(crate) fn static_collection_reach(sun_travel: Vec3) -> f32 {
|
||||
CASTER_RANGE + STATIC_REBUILD_STEP + shadow_reach_extension(sun_travel)
|
||||
pub(crate) fn static_collection_reach(sun_travel: Vec3, distance: f32) -> f32 {
|
||||
distance + STATIC_REBUILD_STEP + shadow_reach_extension(sun_travel)
|
||||
}
|
||||
|
||||
/// `WOW_SHADOW_TRACE=1` — log the caster population when it CHANGES, to attribute a vanished shadow
|
||||
@@ -190,6 +196,10 @@ struct ShadowRigState {
|
||||
sun_written: Option<Vec3>,
|
||||
/// The invisible proxy material shared by every lane's SOLID caster.
|
||||
material: Option<Handle<ShadowCasterMaterial>>,
|
||||
/// The cascade's currently-applied `maximum_distance` (the `shadowDistance` slider). The
|
||||
/// `CascadeShadowConfig` is only re-inserted when this changes — rebuilding it every frame would
|
||||
/// re-fit the cascade and defeat the texel snap (crawling every shadow edge).
|
||||
cascade_distance: Option<f32>,
|
||||
}
|
||||
|
||||
pub(crate) struct ShadowCorePlugin;
|
||||
@@ -234,10 +244,14 @@ impl Plugin for ShadowCorePlugin {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn manage_rig(
|
||||
state: Res<State<ClientState>>,
|
||||
video: Res<VideoConfig>,
|
||||
lighting: Res<WowLighting>,
|
||||
mut demand: ResMut<ShadowDemand>,
|
||||
mut frame: ResMut<ShadowFrame>,
|
||||
mut rig: ResMut<ShadowRigState>,
|
||||
// MONKEY (distance slider): bridge the app-side `shadowDistance` to benilla-world so
|
||||
// `global_light` can pack it for the receivers' edge fade (which must fade at THIS distance).
|
||||
mut shadow_distance_out: ResMut<ShadowDistance>,
|
||||
mut commands: Commands,
|
||||
mut materials: ResMut<Assets<ShadowCasterMaterial>>,
|
||||
mut cameras: Query<
|
||||
@@ -248,6 +262,12 @@ fn manage_rig(
|
||||
cameras_for_position: Query<&GlobalTransform, With<WorldCamera>>,
|
||||
) {
|
||||
let in_world = *state.get() == ClientState::InWorld;
|
||||
// The `shadowDistance` slider (already clamped by the cvar). Publish it to benilla-world every
|
||||
// frame so the receiver edge fade tracks it.
|
||||
let distance = video.shadow_distance;
|
||||
if shadow_distance_out.0 != distance {
|
||||
shadow_distance_out.0 = distance;
|
||||
}
|
||||
// Read the demand the lanes accumulated LAST frame, then reset so they re-accumulate this frame
|
||||
// (they run after us, in ShadowSet::Lanes). One-frame lag on rig spawn — imperceptible.
|
||||
let wanted = demand.0 && in_world;
|
||||
@@ -296,7 +316,7 @@ fn manage_rig(
|
||||
CascadeShadowConfigBuilder {
|
||||
num_cascades: 1,
|
||||
minimum_distance: 0.1,
|
||||
maximum_distance: CASTER_RANGE,
|
||||
maximum_distance: distance,
|
||||
..default()
|
||||
}
|
||||
.build(),
|
||||
@@ -308,6 +328,23 @@ fn manage_rig(
|
||||
.id();
|
||||
rig.sun = Some(sun);
|
||||
rig.sun_written = None;
|
||||
rig.cascade_distance = Some(distance);
|
||||
}
|
||||
// Re-fit the cascade only when the slider changed (not every frame — that would defeat the
|
||||
// texel snap and crawl every shadow edge).
|
||||
if rig.cascade_distance != Some(distance) {
|
||||
if let Some(sun) = rig.sun {
|
||||
commands.entity(sun).insert(
|
||||
CascadeShadowConfigBuilder {
|
||||
num_cascades: 1,
|
||||
minimum_distance: 0.1,
|
||||
maximum_distance: distance,
|
||||
..default()
|
||||
}
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
rig.cascade_distance = Some(distance);
|
||||
}
|
||||
|
||||
for (entity, layers, marker) in &mut cameras {
|
||||
@@ -336,8 +373,8 @@ fn manage_rig(
|
||||
|
||||
// MONKEY (moving sun): aim at the VISIBLE celestial sun (rises/sets), clamped in elevation.
|
||||
let sun_direction = shadow_sun_travel(lighting.celestial_dir());
|
||||
let entity_reach = CASTER_RANGE + STATIC_REBUILD_STEP;
|
||||
let tall_reach = static_collection_reach(sun_direction);
|
||||
let entity_reach = distance + STATIC_REBUILD_STEP;
|
||||
let tall_reach = static_collection_reach(sun_direction, distance);
|
||||
|
||||
if let Some(sun) = rig.sun {
|
||||
// Only the rotation matters; QUANTISED to hold the cascade texel snap (see SUN_SNAP_RADIANS).
|
||||
@@ -569,7 +606,7 @@ fn append_skinned(
|
||||
mod tests {
|
||||
use super::{
|
||||
append_triangles, casts_realtime_shadow, shadow_reach_extension, shadow_sun_travel,
|
||||
static_collection_reach, sun_snap_due, CASTER_RANGE, MAX_CASTER_HEIGHT,
|
||||
static_collection_reach, sun_snap_due, DEFAULT_SHADOW_DISTANCE, MAX_CASTER_HEIGHT,
|
||||
MAX_SHADOW_SUN_ELEVATION, MIN_SHADOW_SUN_ELEVATION, SHADOW_REACH_CAP, STATIC_REBUILD_STEP,
|
||||
SUN_SNAP_RADIANS,
|
||||
};
|
||||
@@ -636,7 +673,10 @@ mod tests {
|
||||
assert!((shadow_reach_extension(diagonal) - MAX_CASTER_HEIGHT).abs() < 1e-3);
|
||||
assert_eq!(shadow_reach_extension(Vec3::NEG_Y), 0.0);
|
||||
assert_eq!(shadow_reach_extension(Vec3::NEG_Z), SHADOW_REACH_CAP);
|
||||
assert!(static_collection_reach(Vec3::NEG_Y) >= CASTER_RANGE + STATIC_REBUILD_STEP);
|
||||
assert!(
|
||||
static_collection_reach(Vec3::NEG_Y, DEFAULT_SHADOW_DISTANCE)
|
||||
>= DEFAULT_SHADOW_DISTANCE + STATIC_REBUILD_STEP
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2186,6 +2186,7 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
|
||||
),
|
||||
("BENILLA_TOOLTIP_CHARACTER_SHADOWS", "GraphicsRowCharacterShadows"),
|
||||
("BENILLA_TOOLTIP_WORLD_SHADOWS", "GraphicsRowWorldShadows"),
|
||||
("BENILLA_TOOLTIP_SHADOW_DISTANCE", "GraphicsRowShadowDistance"),
|
||||
];
|
||||
if let Some((_, want_row)) = BENILLA_OWNED.iter().find(|(k, _)| *k == key) {
|
||||
assert_eq!(row, *want_row, "{row}: not this row's string");
|
||||
@@ -2231,8 +2232,9 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() {
|
||||
// Enable Sound in Background (1847), the Audio page's fifth checkbox and the one row on that
|
||||
// page the reference never made settable.
|
||||
// MONKEY (world shadows): +1 for splitting the old single shadow row into Real Character
|
||||
// Shadows + World Shadows (both benilla-owned keys, carved out in BENILLA_OWNED above).
|
||||
assert_eq!(checked, 60, "every tipped row carries a live key");
|
||||
// Shadows + World Shadows, +1 for the Shadow Distance slider (all benilla-owned keys, carved
|
||||
// out in BENILLA_OWNED above).
|
||||
assert_eq!(checked, 61, "every tipped row carries a live key");
|
||||
assert_eq!(
|
||||
untipped,
|
||||
vec![
|
||||
|
||||
@@ -212,7 +212,8 @@ pub(crate) fn boot_windowed_size() -> UVec2 {
|
||||
/// since 0294): the window literal resolves env-then-file for itself, `load_config` applies the
|
||||
/// file to this resource at `Startup`, and because both read the same key the reconcile is a no-op
|
||||
/// rather than a mode change one frame into the run.
|
||||
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug)]
|
||||
// NB: no `Eq` — `shadow_distance` is an f32 (only `PartialEq` is needed, for `!=` change detection).
|
||||
#[derive(Resource, Clone, Copy, PartialEq, Debug)]
|
||||
pub(crate) struct VideoConfig {
|
||||
pub(crate) vsync: bool,
|
||||
/// Whether the STATIC WORLD (trees, buildings, foliage) casts realtime shadows and baked MCSH
|
||||
@@ -222,6 +223,9 @@ pub(crate) struct VideoConfig {
|
||||
/// Whether CHARACTERS (players, NPCs, creatures, mounts) cast realtime silhouettes instead of
|
||||
/// the legacy oval blob. Independent of [`Self::world_shadows`].
|
||||
pub(crate) character_shadows: bool,
|
||||
/// Realtime-shadow render distance in yards (the `shadowDistance` slider) — the shadow-map
|
||||
/// cascade range + caster reach. Clamped to `shadow_core::SHADOW_DISTANCE_RANGE`.
|
||||
pub(crate) shadow_distance: f32,
|
||||
pub(crate) display: DisplayMode,
|
||||
/// The windowed size, `gxResolution`. Kept while fullscreen so leaving it can restore it.
|
||||
pub(crate) windowed: UVec2,
|
||||
@@ -233,6 +237,7 @@ impl Default for VideoConfig {
|
||||
vsync: !novsync_env(),
|
||||
world_shadows: false,
|
||||
character_shadows: false,
|
||||
shadow_distance: crate::shadow_core::DEFAULT_SHADOW_DISTANCE,
|
||||
display: if windowed_env() {
|
||||
DisplayMode::Windowed
|
||||
} else {
|
||||
@@ -545,6 +550,7 @@ mod tests {
|
||||
vsync: true,
|
||||
world_shadows: false,
|
||||
character_shadows: false,
|
||||
shadow_distance: 80.0,
|
||||
display: DisplayMode::Fullscreen,
|
||||
windowed: UVec2::new(1024, 768),
|
||||
})
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
// A fully covered character shadow retains 45% of the authored terrain colour.
|
||||
const SHADOW_SUN_FLOOR: f32 = 0.45;
|
||||
|
||||
// MONKEY (edge fade): the realtime shadow lightens toward the cascade's max distance so it fades in
|
||||
// rather than popping at the resolve boundary. SHADOW_RANGE must match `shadow_core::CASTER_RANGE`.
|
||||
const SHADOW_RANGE: f32 = 80.0;
|
||||
// MONKEY (edge fade): the realtime shadow lightens over the last SHADOW_EDGE_BAND yards of the
|
||||
// cascade's max distance so it fades in rather than popping at the resolve boundary. The distance
|
||||
// is the `shadowDistance` slider, read live from `_wmo_fog[1].z` (packed by build_light_data).
|
||||
const SHADOW_EDGE_BAND: f32 = 14.0;
|
||||
|
||||
// Per-tile Vec4 uniforms packed into ONE buffer (binding 106) — the field order here MUST match the
|
||||
@@ -323,7 +323,8 @@ fn fragment(in: TerrainVsOut) -> @location(0) vec4<f32> {
|
||||
// lighten `world_shadow` toward 1.0 (no shadow). `sun_shadow_strength` is reused for the MCSH
|
||||
// fade-back below.
|
||||
let cam_dist = distance(in.world_position.xyz, view.world_position.xyz);
|
||||
let edge_fade = smoothstep(SHADOW_RANGE - SHADOW_EDGE_BAND, SHADOW_RANGE, cam_dist);
|
||||
let shadow_range = wow_light._wmo_fog[1].z; // the `shadowDistance` slider (yd)
|
||||
let edge_fade = smoothstep(shadow_range - SHADOW_EDGE_BAND, shadow_range, cam_dist);
|
||||
let sun_shadow_strength = wow_light.fog_params.z;
|
||||
world_shadow = 1.0 - (1.0 - world_shadow) * sun_shadow_strength * (1.0 - edge_fade);
|
||||
let primary = in.primary;
|
||||
|
||||
@@ -115,9 +115,9 @@ struct ModelParams {
|
||||
// A fully covered character shadow retains 45% of the authored model lighting.
|
||||
const SHADOW_SUN_FLOOR: f32 = 0.45;
|
||||
|
||||
// MONKEY (edge fade): the realtime shadow lightens toward the cascade's max distance so it fades in
|
||||
// rather than popping at the resolve boundary. SHADOW_RANGE must match `shadow_core::CASTER_RANGE`.
|
||||
const SHADOW_RANGE: f32 = 80.0;
|
||||
// MONKEY (edge fade): the realtime shadow lightens over the last SHADOW_EDGE_BAND yards of the
|
||||
// cascade's max distance so it fades in rather than popping. The distance is the `shadowDistance`
|
||||
// slider, read live from `wmo_fog_params.z` (packed by build_light_data).
|
||||
const SHADOW_EDGE_BAND: f32 = 14.0;
|
||||
|
||||
// The shared global light (lighting::global_light): ONE storage buffer every material reads, updated
|
||||
@@ -1025,7 +1025,8 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut {
|
||||
// vanish at night. No effect when unshadowed (player_shadow already 1.0) — keeps the
|
||||
// `worldShadows 0` path byte-identical.
|
||||
let shadow_cam_dist = distance(in.world_position.xyz, view.world_position.xyz);
|
||||
let shadow_edge_fade = smoothstep(SHADOW_RANGE - SHADOW_EDGE_BAND, SHADOW_RANGE, shadow_cam_dist);
|
||||
let shadow_range = wow_light.wmo_fog_params.z; // the `shadowDistance` slider (yd)
|
||||
let shadow_edge_fade = smoothstep(shadow_range - SHADOW_EDGE_BAND, shadow_range, shadow_cam_dist);
|
||||
player_shadow = 1.0 - (1.0 - player_shadow) * wow_light.fog_params.z * (1.0 - shadow_edge_fade);
|
||||
// The realtime map blocks only the directional sun. Preserve the authored ambient/probe
|
||||
// contribution instead of multiplying the whole lighting result; the latter makes interiors,
|
||||
|
||||
@@ -184,6 +184,19 @@ pub struct LightRooms(pub(crate) crate::wmo_portal::WmoGroupVis);
|
||||
#[derive(Resource, Clone, Copy, Default)]
|
||||
pub struct WorldShadowActive(pub bool);
|
||||
|
||||
/// MONKEY (distance slider): the realtime-shadow render distance in yards (the `shadowDistance`
|
||||
/// slider), bridged from benilla-app's shadow rig. [`build_light_data`] packs it into a free light
|
||||
/// lane so the receivers (`terrain.wgsl`/`wow_model.wgsl`) fade the realtime shadow at THIS distance
|
||||
/// rather than a fixed one — the fade must track the cascade's actual `maximum_distance`.
|
||||
#[derive(Resource, Clone, Copy)]
|
||||
pub struct ShadowDistance(pub f32);
|
||||
|
||||
impl Default for ShadowDistance {
|
||||
fn default() -> Self {
|
||||
Self(80.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Main-world resource holding the packed light for this frame; extracted into the render world where
|
||||
/// [`upload_light`] writes it. Rebuilt every frame by [`build_light_data`] (cheap — one std430 pack).
|
||||
#[derive(Resource, Clone, Copy, ExtractResource)]
|
||||
@@ -211,6 +224,7 @@ pub struct SharedLightBuffer(pub Buffer);
|
||||
pub(super) fn register(app: &mut App) {
|
||||
app.init_resource::<WowLightData>()
|
||||
.init_resource::<WorldShadowActive>()
|
||||
.init_resource::<ShadowDistance>()
|
||||
.init_resource::<super::prop_probes::PropProbeExtract>()
|
||||
.add_plugins(ExtractResourcePlugin::<WowLightData>::default())
|
||||
.add_plugins(ExtractResourcePlugin::<SharedLightBuffer>::default())
|
||||
@@ -296,6 +310,8 @@ fn build_light_data(
|
||||
time: Res<Time>,
|
||||
// MONKEY (world shadows): the `worldShadows` lane flag, packed into `sh_c16.w` for the MCSH gate.
|
||||
world_shadow: Res<WorldShadowActive>,
|
||||
// MONKEY (distance slider): the realtime-shadow render distance, packed for the edge fade.
|
||||
shadow_distance: Res<ShadowDistance>,
|
||||
mut last_dump: Local<f64>,
|
||||
mut last_rows_dump: Local<f64>,
|
||||
) {
|
||||
@@ -340,6 +356,10 @@ fn build_light_data(
|
||||
// so shadows soften and vanish at night; the shadow basis is separately clamped to 18° so a low
|
||||
// sun still casts the right DIRECTION.
|
||||
rows[5][2] = sun_shadow_strength(l.celestial_dir.y);
|
||||
// MONKEY (distance slider): the realtime-shadow render distance (yd), packed into the free
|
||||
// `_wmo_fog[1].z` / `wmo_fog_params.z` lane (row 19). The receivers' edge fade reads it so the
|
||||
// shadow fades at the cascade's actual `maximum_distance`, whatever the slider is set to.
|
||||
rows[19][2] = shadow_distance.0;
|
||||
// The dynamic point-light table (decision 0278): every spawned point light within
|
||||
// [`POINT_PACK_RADIUS`] of the camera, nearest-first when over capacity — the VERTEX stages of
|
||||
// `terrain.wgsl`/`wow_model.wgsl` walk it for the Gouraud point term (bevy's clusterable buffer
|
||||
|
||||
@@ -17,7 +17,9 @@ mod prop_probes; // the per-instance interior-prop SH probe table (slot ↔ Mesh
|
||||
mod resolve; // the per-frame time-of-day sample into WowLighting + the WMO interior-fog crossfade
|
||||
mod sh; // the model SH light-probe coefficient math
|
||||
pub use blob::LightBlob;
|
||||
pub use global_light::{new_shared_light_buffer, LightRooms, SharedLightBuffer, WorldShadowActive};
|
||||
pub use global_light::{
|
||||
new_shared_light_buffer, LightRooms, SharedLightBuffer, ShadowDistance, WorldShadowActive,
|
||||
};
|
||||
pub use prop_probes::{PropProbeSlot, PropProbes, MAX_PROP_PROBES};
|
||||
// The std430 layout itself — row indices, byte sizes, region offsets and the folds that fill them
|
||||
// — stays in the crate. Off-world producers state values through `LightBlob` and never a row index
|
||||
|
||||
Reference in New Issue
Block a user