MONKEY: shadow extension hook (benilla::shadow_hook) so receivers stay untouched
Extract the realtime directional-shadow term (fetch + edge fade + night fade) into ONE importable WGSL library, benilla::shadow_hook, and route all three receivers through it. Adding or changing the realtime shadow now touches only shadow_hook.wgsl; terrain.wgsl / wow_model.wgsl / static_gx.wgsl (the shaders that blanked the terrain when hand-edited) are no longer edited per shadow feature. Confirmed in-game (rendering unchanged). - shadow_hook.wgsl (NEW): `realtime_shadow(sample_pos, normal, view_z, cam_dist, shadow_range, night) -> factor`. No-op whenever no shadow sun exists (the only Bevy DirectionalLight benilla spawns is the rig's, so n_directional_lights == 0 exactly when shadows are off) — no shader-def toggle needed. Registered via bevy `load_shader_library!` in benilla-assets so `#import benilla::shadow_hook` resolves across crates (verified: the model pipeline composes it cleanly). - terrain / wow_model: replace the inline fetch+fade with the hook call; keep their own downstream gating (MCSH suppression + spec gate; rig-skin sample point + interior exclusion + ambient-preserving apply). Behavior-preserving. - static_gx: routed through the hook too — tree/building surfaces now get the SAME edge + night fade terrain/models already had (they lacked it before). - The duplicated SHADOW_EDGE_BAND const now lives once, in the hook. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b2353797d7
commit
ab81ddfa06
@@ -35,9 +35,9 @@ use bevy::render::render_resource::{
|
||||
AsBindGroup, BlendComponent, BlendFactor, BlendOperation, BlendState, Buffer, ColorWrites,
|
||||
CompareFunction, RenderPipelineDescriptor, SpecializedMeshPipelineError,
|
||||
};
|
||||
use bevy::shader::ShaderRef;
|
||||
use bevy::shader::{load_shader_library, ShaderRef};
|
||||
|
||||
/// Compile the four WGSL files into the binary and register them under
|
||||
/// Compile the WGSL files into the binary and register them under
|
||||
/// `embedded://benilla_assets/shaders/…`. Call **after** Bevy's `AssetPlugin` (it fills the
|
||||
/// registry that plugin creates); [`crate::register_asset_loaders`] already does.
|
||||
pub fn register_shaders(app: &mut App) {
|
||||
@@ -45,6 +45,10 @@ pub fn register_shaders(app: &mut App) {
|
||||
bevy::asset::embedded_asset!(app, "shaders/wow_model.wgsl");
|
||||
bevy::asset::embedded_asset!(app, "shaders/wdl.wgsl");
|
||||
bevy::asset::embedded_asset!(app, "shaders/liquid.wgsl");
|
||||
// MONKEY (shadow hook): register the importable shadow-contribution library `benilla::shadow_hook`
|
||||
// (loaded eagerly so `#import benilla::shadow_hook` resolves in terrain/model/static_gx). This is
|
||||
// the ONE place the realtime directional-shadow term lives; the receivers just call into it.
|
||||
load_shader_library!(app, "shaders/shadow_hook.wgsl");
|
||||
}
|
||||
|
||||
/// The WDL far-band shader's source, for the law tests that live beside the renderer rather than
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#define_import_path benilla::shadow_hook
|
||||
|
||||
// MONKEY (shadow extension hook): the ONE place the realtime directional-shadow term is computed.
|
||||
//
|
||||
// benilla's receivers — `terrain.wgsl`, `wow_model.wgsl`, `static_gx.wgsl` — call `realtime_shadow`
|
||||
// instead of each hand-rolling the fetch + fades. Adding or changing the realtime shadow now touches
|
||||
// THIS file only; the three receiver shaders (the ones that blanked the terrain when edited by hand)
|
||||
// stay untouched. The receivers keep their own downstream gating (terrain's MCSH suppression + spec
|
||||
// gate, the model's interior/rig-skin exclusion + ambient-preserving apply, static_gx's ambient arm)
|
||||
// — those are lighting composition, not shadow sampling, so they belong to each receiver.
|
||||
//
|
||||
// It is a no-op whenever no shadow sun exists: the only Bevy `DirectionalLight` benilla ever spawns
|
||||
// is the shadow rig's (`benilla_app::shadow_core`), so `lights.n_directional_lights == 0` exactly
|
||||
// when shadows are off, and the loop below never runs. No shader-def toggle is needed.
|
||||
|
||||
#import bevy_pbr::{
|
||||
shadows,
|
||||
mesh_view_bindings::lights,
|
||||
}
|
||||
|
||||
// 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.
|
||||
const SHADOW_EDGE_BAND: f32 = 14.0;
|
||||
|
||||
// The realtime directional-shadow factor at `sample_pos`: 1.0 = lit, 0.0 = fully shadowed.
|
||||
//
|
||||
// - `sample_pos` world-space point to sample the shadow map at — a per-fragment position, or a
|
||||
// unit's nudged rig anchor (the caller chooses; that is the rig-skin decision).
|
||||
// - `normal` receiver normal, for the slope-aware depth bias.
|
||||
// - `view_z` view-space z, for cascade selection.
|
||||
// - `cam_dist` distance from the camera to the receiver fragment, for the EDGE fade.
|
||||
// - `shadow_range` the cascade's max distance (the `shadowDistance` slider) — where the edge fade ends.
|
||||
// - `night` the sun-height strength (0 at night .. 1 by day) — the NIGHT fade.
|
||||
//
|
||||
// Both fades lighten the result toward 1.0 (no shadow). The caller passes `shadow_range`/`night`
|
||||
// from its own light buffer (the receivers declare that buffer three incompatible ways, so the hook
|
||||
// takes the scalars as arguments rather than reading it).
|
||||
fn realtime_shadow(
|
||||
sample_pos: vec4<f32>,
|
||||
normal: vec3<f32>,
|
||||
view_z: f32,
|
||||
cam_dist: f32,
|
||||
shadow_range: f32,
|
||||
night: f32,
|
||||
) -> f32 {
|
||||
var shadow = 1.0;
|
||||
if (lights.n_directional_lights > 0u) {
|
||||
for (var light_id = 0u; light_id < lights.n_directional_lights; light_id = light_id + 1u) {
|
||||
if ((lights.directional_lights[light_id].flags & 1u) != 0u) {
|
||||
shadow = shadows::fetch_directional_shadow(light_id, sample_pos, normal, view_z);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let edge_fade = smoothstep(shadow_range - SHADOW_EDGE_BAND, shadow_range, cam_dist);
|
||||
return 1.0 - (1.0 - shadow) * night * (1.0 - edge_fade);
|
||||
}
|
||||
@@ -32,6 +32,8 @@
|
||||
mesh_view_bindings::{lights, view},
|
||||
shadows,
|
||||
}
|
||||
// MONKEY (shadow hook): the realtime directional-shadow term (fetch + edge/night fade) lives here.
|
||||
#import benilla::shadow_hook
|
||||
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(100) var layer_array: texture_2d_array<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(104) var alpha_array: texture_2d_array<f32>;
|
||||
@@ -41,11 +43,6 @@
|
||||
// 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 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
|
||||
// Rust `TerrainExtension` struct. Light + fog live in the shared global-light storage buffer (below);
|
||||
// what's left is just the per-tile layer tiling factor.
|
||||
@@ -302,31 +299,21 @@ fn fragment(in: TerrainVsOut) -> @location(0) vec4<f32> {
|
||||
// Keep the raw fetch for the specular gate below. The realtime map now contains CHARACTERS
|
||||
// only; MCSH contains the static world's baked blockers. They are independent occluders, so
|
||||
// the character map must never replace (and thereby brighten) the authored terrain bake.
|
||||
var world_shadow = 1.0;
|
||||
if (lights.n_directional_lights > 0u) {
|
||||
let view_z = (view.view_from_world * in.world_position).z;
|
||||
for (var light_id = 0u; light_id < lights.n_directional_lights; light_id = light_id + 1u) {
|
||||
if ((lights.directional_lights[light_id].flags & 1u) != 0u) {
|
||||
world_shadow = shadows::fetch_directional_shadow(
|
||||
light_id,
|
||||
in.world_position,
|
||||
normalize(in.world_normal),
|
||||
view_z,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// MONKEY: soften the realtime shadow before it darkens the ground — an EDGE fade toward the
|
||||
// cascade's max distance (no hard pop at the resolve boundary) and a NIGHT fade by the real sun
|
||||
// height (`fog_params.z`, 1 by day → 0 at night, so shadows vanish as the sun sets). Both
|
||||
// lighten `world_shadow` toward 1.0 (no shadow). `sun_shadow_strength` is reused for the MCSH
|
||||
// fade-back below.
|
||||
// MONKEY (shadow hook): the realtime shadow — fetch + edge fade (toward the `shadowDistance`
|
||||
// range in `_wmo_fog[1].z`) + night fade (`fog_params.z`) — is computed by `benilla::shadow_hook`.
|
||||
// Terrain keeps only its own MCSH suppression + spec gate below; `sun_shadow_strength` feeds the
|
||||
// MCSH fade-back.
|
||||
let view_z = (view.view_from_world * in.world_position).z;
|
||||
let cam_dist = distance(in.world_position.xyz, view.world_position.xyz);
|
||||
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 world_shadow = shadow_hook::realtime_shadow(
|
||||
in.world_position,
|
||||
normalize(in.world_normal),
|
||||
view_z,
|
||||
cam_dist,
|
||||
wow_light._wmo_fog[1].z,
|
||||
sun_shadow_strength,
|
||||
);
|
||||
let primary = in.primary;
|
||||
|
||||
// STEP 4: MCSH baked shadow. On the reference path (pixelShaders+specular) terrain is ONE pass
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
shadows,
|
||||
mesh_functions,
|
||||
}
|
||||
// MONKEY (shadow hook): the realtime directional-shadow term (fetch + edge/night fade) lives here.
|
||||
#import benilla::shadow_hook
|
||||
|
||||
// Our own fragment output, so the SKY lane can force the far depth. Bevy's `forward_io::FragmentOutput`
|
||||
// is `@location(0) color` and nothing else (bevy_pbr 0.18.1 `forward_io.wgsl`), so this is that struct
|
||||
@@ -115,11 +117,6 @@ 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 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
|
||||
// once/frame in place — replaces the per-material light/fog uniforms the old apply_wow_lighting re-pushed
|
||||
// each frame. The model reads rows 0-2 (ambient/diffuse/sun) + fog, plus rows 6-12: the disassembled
|
||||
@@ -981,53 +978,43 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut {
|
||||
// through `point_diffuse` below like any exterior entity.
|
||||
let is_rig = m.sun_scale.x >= 1.5;
|
||||
let lit = select(select(lit_exterior, lit_interior, is_interior), lit_m2_interior, is_rig);
|
||||
// MONKEY (shadow hook): the realtime shadow (fetch + edge/night fade) is computed by
|
||||
// `benilla::shadow_hook`. This lane keeps only the rig-skin SAMPLE-POINT choice + the
|
||||
// interior/rig exclusion + ambient-preserving apply below.
|
||||
var player_shadow = 1.0;
|
||||
if (lights.n_directional_lights > 0u) {
|
||||
let view_z = (view.view_from_world * in.world_position).z;
|
||||
for (var light_id = 0u; light_id < lights.n_directional_lights; light_id = light_id + 1u) {
|
||||
if ((lights.directional_lights[light_id].flags & 1u) != 0u) {
|
||||
#ifdef WOW_RIG_SKIN
|
||||
// A skinned UNIT samples the map ONCE at its rig origin (between the feet),
|
||||
// nudged 2.5 units TOWARD the sun, and the whole unit dims uniformly — the
|
||||
// reference's own per-unit response. Per-fragment sampling is wrong here: the
|
||||
// caster proxy holds a CPU-skinned copy of this very body, so a body fragment
|
||||
// reads its own limbs/weapon as occluders (self-shadow speckle no bias fixes on
|
||||
// animated silhouettes). Points along the sun ray share a shadow-map texel, so
|
||||
// the nudge only excludes occluders within ~2.5 units of the feet ALONG the ray
|
||||
// (a fence hugging the unit) — a building or canopy still lands. Gated on the
|
||||
// directional-enable flag so an interior-lit unit never takes an exterior dim.
|
||||
if (wow_light.light_sun.w > 0.5) {
|
||||
let sun_ray = normalize(wow_light.light_sun.xyz);
|
||||
let anchor = wow_light.rig_origin[(fade_tag >> 19u) & 0x7ffu].xyz
|
||||
- 2.5 * sun_ray;
|
||||
player_shadow = shadows::fetch_directional_shadow(
|
||||
light_id,
|
||||
vec4<f32>(anchor, 1.0),
|
||||
vec3<f32>(0.0, 1.0, 0.0),
|
||||
view_z,
|
||||
);
|
||||
}
|
||||
#else
|
||||
player_shadow = shadows::fetch_directional_shadow(
|
||||
light_id,
|
||||
in.world_position,
|
||||
wow_normalize(in.world_normal),
|
||||
view_z,
|
||||
);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// MONKEY: soften the realtime shadow like the terrain — an EDGE fade toward the cascade's max
|
||||
// distance + a NIGHT fade by the real sun height (`fog_params.z`, 1 by day → 0 at night). Both
|
||||
// lighten `player_shadow` toward 1.0 (no shadow), so shadows on models fade in at range and
|
||||
// vanish at night. No effect when unshadowed (player_shadow already 1.0) — keeps the
|
||||
// `worldShadows 0` path byte-identical.
|
||||
let view_z = (view.view_from_world * in.world_position).z;
|
||||
let shadow_cam_dist = distance(in.world_position.xyz, view.world_position.xyz);
|
||||
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);
|
||||
#ifdef WOW_RIG_SKIN
|
||||
// A skinned UNIT samples the map ONCE at its rig origin (between the feet), nudged 2.5 units
|
||||
// TOWARD the sun, and dims uniformly — the reference's per-unit response. Per-fragment sampling
|
||||
// is wrong here: the caster proxy holds a CPU-skinned copy of this very body, so a body fragment
|
||||
// reads its own limbs/weapon as occluders (self-shadow speckle). Gated on the directional-enable
|
||||
// flag so an interior-lit unit never takes an exterior dim (player_shadow stays 1.0).
|
||||
if (wow_light.light_sun.w > 0.5) {
|
||||
let sun_ray = normalize(wow_light.light_sun.xyz);
|
||||
let anchor = vec4<f32>(
|
||||
wow_light.rig_origin[(fade_tag >> 19u) & 0x7ffu].xyz - 2.5 * sun_ray,
|
||||
1.0,
|
||||
);
|
||||
player_shadow = shadow_hook::realtime_shadow(
|
||||
anchor,
|
||||
vec3<f32>(0.0, 1.0, 0.0),
|
||||
view_z,
|
||||
shadow_cam_dist,
|
||||
wow_light.wmo_fog_params.z,
|
||||
wow_light.fog_params.z,
|
||||
);
|
||||
}
|
||||
#else
|
||||
player_shadow = shadow_hook::realtime_shadow(
|
||||
in.world_position,
|
||||
wow_normalize(in.world_normal),
|
||||
view_z,
|
||||
shadow_cam_dist,
|
||||
wow_light.wmo_fog_params.z,
|
||||
wow_light.fog_params.z,
|
||||
);
|
||||
#endif
|
||||
// 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,
|
||||
// point-lit props, and shadow-side characters globally too dark.
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
mesh_view_bindings::{lights, view},
|
||||
shadows,
|
||||
}
|
||||
// MONKEY (shadow hook): the realtime directional-shadow term (fetch + edge/night fade) lives here.
|
||||
#import benilla::shadow_hook
|
||||
|
||||
// Group 0 is Bevy's standard mesh-view bind group (view matrices, directional-light records and
|
||||
// the shadow textures the retained pass reads).
|
||||
@@ -289,20 +291,21 @@ fn fragment(in: GxVsOut) -> @location(0) vec4<f32> {
|
||||
// The retained path uses the same Bevy directional shadow map as terrain and entity models.
|
||||
// `static_gx` used to have no mesh-view shadow bindings, which made every Stormwind WMO act
|
||||
// as if shadows were disabled even though the caster map was populated.
|
||||
// MONKEY (shadow hook): the realtime shadow (fetch + edge/night fade, via `benilla::shadow_hook`)
|
||||
// — now with the same edge/night fade as terrain + models. Interior batches (`WORD_INTERIOR`)
|
||||
// stay excluded (no sun reaches a sealed room); the hook is a no-op when no shadow sun exists.
|
||||
var world_shadow = 1.0;
|
||||
if (lights.n_directional_lights > 0u && (in.word & WORD_INTERIOR) == 0u) {
|
||||
if ((in.word & WORD_INTERIOR) == 0u) {
|
||||
let view_z = (view.view_from_world * in.world_position).z;
|
||||
for (var light_id = 0u; light_id < lights.n_directional_lights; light_id = light_id + 1u) {
|
||||
if ((lights.directional_lights[light_id].flags & 1u) != 0u) {
|
||||
world_shadow = shadows::fetch_directional_shadow(
|
||||
light_id,
|
||||
in.world_position,
|
||||
n_lit,
|
||||
view_z,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let cam_dist = distance(in.world_position.xyz, view.world_position.xyz);
|
||||
world_shadow = shadow_hook::realtime_shadow(
|
||||
in.world_position,
|
||||
n_lit,
|
||||
view_z,
|
||||
cam_dist,
|
||||
wow_light.wmo_fog_params.z,
|
||||
wow_light.fog_params.z,
|
||||
);
|
||||
}
|
||||
let shadow_term = mix(SHADOW_SUN_FLOOR, 1.0, world_shadow);
|
||||
// Keep ambient energy when the realtime map blocks the sun. The retained pass also carries
|
||||
|
||||
Reference in New Issue
Block a user