diff --git a/crates/benilla-assets/src/shaders/enhanced_water.wgsl b/crates/benilla-assets/src/shaders/enhanced_water.wgsl index 411ffc0f..faf60bd9 100644 --- a/crates/benilla-assets/src/shaders/enhanced_water.wgsl +++ b/crates/benilla-assets/src/shaders/enhanced_water.wgsl @@ -25,6 +25,8 @@ #import bevy_pbr::mesh_view_bindings::{view, globals} // MONKEY (p0 MonkeyFrame): the programme block's struct, mirrored after the point table. #import benilla::monkey_frame +// MONKEY (rainshelter): the shelter-grid lookup the surface receivers share. +#import benilla::wet_hook // MONKEY (p0 fog hook): the one distance-fog law every receiver calls. #import benilla::fog_hook @@ -66,6 +68,25 @@ fn water_monkey() -> monkey_frame::MonkeyFrame { return m; } +// MONKEY (rainshelter): the rain-occlusion grid right after the MonkeyFrame block +// (`weather/shelter.rs`): two header rows, then the packed cell words, four to a row. +const SHELTER_ROW: u32 = 549u; +fn water_shelter_word(i: u32) -> u32 { + return bitcast(water_light[SHELTER_ROW + 2u + i / 4u][i % 4u]); +} +// 1 open .. 0 under a bridge or a roof. +fn water_rain_open(world_pos: vec3) -> f32 { + let hdr = water_light[SHELTER_ROW]; + let cfg = water_light[SHELTER_ROW + 1u]; + let st = wet_hook::shelter_taps(world_pos, hdr, cfg); + if (st.live <= 0.0) { + return 1.0; + } + return 1.0 - wet_hook::shelter_amount(st, water_shelter_word(st.idx.x), + water_shelter_word(st.idx.y), water_shelter_word(st.idx.z), water_shelter_word(st.idx.w), + world_pos.y, cfg); +} + // What the fragment stage hands over: the three varyings this module reads. struct WaterFragment { clip_position: vec4, @@ -256,13 +277,16 @@ fn ripple_hash(p: vec2) -> vec2 { q += dot(q, q.yzx + 33.33); return fract((q.xx + q.yz) * q.zy); } -fn rain_ripple_normal(world_xz: vec2, footprint: f32, distance: f32, room: bool) -> vec2 { +fn rain_ripple_normal(world_pos: vec3, footprint: f32, distance: f32, room: bool) -> vec2 { let wet_row = water_monkey().wet_a; let rain = clamp(wet_row.x, 0.0, 1.0); if rain <= 0.0 || water.lane.x > 1.5 || room { return vec2(0.0); } - let reach = (1.0 - smoothstep(35.0, 60.0, distance)) * (1.0 - smoothstep(0.05, 0.14, footprint)); + let world_xz = world_pos.xz; + // MONKEY (rainshelter): no rings under a bridge or a roof. + let reach = (1.0 - smoothstep(35.0, 60.0, distance)) * (1.0 - smoothstep(0.05, 0.14, footprint)) + * water_rain_open(world_pos); if reach <= 0.0 { return vec2(0.0); } @@ -595,7 +619,7 @@ fn enhanced_water(in: WaterFragment, shallow: vec4, deep: vec4) -> vec open_sea_fold = water_gerstner(p, t, shore).w; } // One surface gradient: the procedural bands and the shore break. - let surf_grad = wave.yz + shore_grad + rain_ripple_normal(in.world_position.xz, footprint, + let surf_grad = wave.yz + shore_grad + rain_ripple_normal(in.world_position.xyz, footprint, length(eye_pos), in.room_fog != 0u && water.lane.w > 0.5); var n = normalize(vec3(-surf_grad.x, 1.0, -surf_grad.y)); if dot(n, to_view) < 0.0 { n = -n; } diff --git a/crates/benilla-assets/src/shaders/terrain.wgsl b/crates/benilla-assets/src/shaders/terrain.wgsl index 9bc38773..4ea363e3 100644 --- a/crates/benilla-assets/src/shaders/terrain.wgsl +++ b/crates/benilla-assets/src/shaders/terrain.wgsl @@ -64,6 +64,11 @@ struct WowLight { points: array, 512>, // MONKEY (p0 MonkeyFrame): the programme block after the point table (monkey_frame.wgsl). monkey: monkey_frame::MonkeyFrame, + // MONKEY (rainshelter): the rain-occlusion grid (`weather/shelter.rs`): `[origin_x, origin_z, + // 1/cell, active]`, `[base_y, cells per side, 0, 0]`, then one packed word a cell (wet_hook.wgsl). + shelter_hdr: vec4, + shelter_cfg: vec4, + shelter: array, }; @group(#{MATERIAL_BIND_GROUP}) @binding(90) var wow_light: WowLight; @@ -757,9 +762,20 @@ fn fragment(in: TerrainVsOut) -> @location(0) vec4 { // specular = per-vertex sheen · gloss_mask · shadow → gated to ZERO in shadow (no sheen in shade) // (`tex·primary` is the MODULATE-1× diffuse; the sheen is added after, separate-specular.) Then // LDR-clamp; gamma/byte throughout; raw gamma out (GAMMA LANE, 0161). + // MONKEY (rainshelter): nothing gets wet under a roof, porch or bridge (the shelter grid). + var wet_open = 1.0; + if (wow_light.monkey.wet_a.y > 0.0) { + let wet_st = wet_hook::shelter_taps(in.world_position.xyz, wow_light.shelter_hdr, + wow_light.shelter_cfg); + if (wet_st.live > 0.0) { + wet_open = 1.0 - wet_hook::shelter_amount(wet_st, wow_light.shelter[wet_st.idx.x], + wow_light.shelter[wet_st.idx.y], wow_light.shelter[wet_st.idx.z], + wow_light.shelter[wet_st.idx.w], in.world_position.y, wow_light.shelter_cfg); + } + } // MONKEY (wet): rain-darkened ground + puddles (wet_hook.wgsl); a dry frame returns `color`. - let wet = wet_hook::wet_puddles(wet_hook::wet_surface(color, n_lit, in.world_position.xyz, 1.0, - wow_light.monkey), n_lit, in.world_position.xyz, wow_light.monkey); + let wet = wet_hook::wet_puddles(wet_hook::wet_surface(color, n_lit, in.world_position.xyz, + wet_open, wow_light.monkey), n_lit, in.world_position.xyz, wet_open, wow_light.monkey); color = wet.albedo; let diffuse_term = color * primary * (0.3 * shadow_lit_eff + 0.7) * character_shadow_term; let spec_term = in.specular * specmask * spec_gate; diff --git a/crates/benilla-assets/src/shaders/wet_hook.wgsl b/crates/benilla-assets/src/shaders/wet_hook.wgsl index 4bf3b73b..c09903e4 100644 --- a/crates/benilla-assets/src/shaders/wet_hook.wgsl +++ b/crates/benilla-assets/src/shaders/wet_hook.wgsl @@ -13,6 +13,57 @@ #import benilla::monkey_frame::MonkeyFrame +// MONKEY (rainshelter): the four cells around a fragment in the camera-centred shelter grid. +// `hdr` = `[origin_x, origin_z, 1 / cell, active]`, `cfg` = `[base_y, cells per side, 0, 0]`; +// `live` is 0 when the map is off or the fragment lies outside it (open sky). +struct ShelterTaps { + idx: vec4, + frac: vec2, + live: f32, +} + +fn shelter_taps(world_pos: vec3, hdr: vec4, cfg: vec4) -> ShelterTaps { + var t: ShelterTaps; + t.idx = vec4(0u); + t.frac = vec2(0.0); + t.live = 0.0; + if (hdr.w < 0.5) { + return t; + } + let g = (world_pos.xz - hdr.xy) * hdr.z - 0.5; + let gf = floor(g); + if (gf.x < 0.0 || gf.y < 0.0 || gf.x + 1.0 > cfg.y - 1.0 || gf.y + 1.0 > cfg.y - 1.0) { + return t; + } + let side = u32(cfg.y); + let first = u32(gf.y) * side + u32(gf.x); + t.idx = vec4(first, first + 1u, first + side, first + side + 1u); + t.frac = g - gf; + t.live = 1.0; + return t; +} + +// One cell: low half `(top − base_y) × 64 + 32768`, high half the soft cover as unorm16 +// (`shelter::pack_cell`). Sheltered when a covered top lies a yard or more above the fragment; the +// roof's own surface (at `top`) stays open. +fn shelter_cell(word: u32, y_rel: f32) -> f32 { + let top = (f32(word & 0xffffu) - 32768.0) / 64.0; + let cover = f32(word >> 16u) / 65535.0; + return cover * smoothstep(0.5, 1.5, top - y_rel); +} + +// How sheltered the fragment is, 0 open .. 1 under cover, bilinear over the four cells. +fn shelter_amount(t: ShelterTaps, wa: u32, wb: u32, wc: u32, wd: u32, world_y: f32, + cfg: vec4) -> f32 { + if (t.live <= 0.0) { + return 0.0; + } + let y_rel = world_y - cfg.x; + let near = mix(shelter_cell(wa, y_rel), shelter_cell(wb, y_rel), t.frac.x); + let far = mix(shelter_cell(wc, y_rel), shelter_cell(wd, y_rel), t.frac.x); + return clamp(mix(near, far, t.frac.y), 0.0, 1.0); +} + struct WetSurface { albedo: vec3, // Gloss weight for `wet_sheen`: 0 dry, ~1 a soaked top face, up to 2 in a terrain puddle (the @@ -61,11 +112,13 @@ fn wet_noise(p: vec2) -> f32 { } // Terrain bonus: puddle patches on flat ground once it is well soaked — darker, and a stronger -// gloss weight so they read as standing water catching the sky. -fn wet_puddles(s: WetSurface, n: vec3, world_pos: vec3, mf: MonkeyFrame) -> WetSurface { +// gloss weight so they read as standing water catching the sky. MONKEY (rainshelter): `exposure` +// as for `wet_surface`, so no puddle forms under cover. +fn wet_puddles(s: WetSurface, n: vec3, world_pos: vec3, exposure: f32, + mf: MonkeyFrame) -> WetSurface { let wet_row = mf.wet_a; var o = s; - if (wet_row.y <= 0.0) { + if (wet_row.y <= 0.0 || exposure <= 0.0) { return o; } let xz = world_pos.xz; @@ -73,7 +126,8 @@ fn wet_puddles(s: WetSurface, n: vec3, world_pos: vec3, mf: MonkeyFram let level = smoothstep(0.94, 0.99, n.y); // Puddles grow with the wetness: the threshold falls as the ground soaks. let edge = mix(0.8, 0.6, smoothstep(0.4, 1.0, wet_row.y)); - let puddle = smoothstep(edge, edge + 0.06, field) * level * smoothstep(0.35, 0.8, wet_row.y); + let puddle = smoothstep(edge, edge + 0.06, field) * level * smoothstep(0.35, 0.8, wet_row.y) + * clamp(exposure, 0.0, 1.0); o.albedo = o.albedo * (1.0 - 0.45 * puddle); o.boost = max(o.boost, 2.0 * puddle); return o; diff --git a/crates/benilla-assets/src/shaders/wow_model.wgsl b/crates/benilla-assets/src/shaders/wow_model.wgsl index d4810031..4a4a70a0 100644 --- a/crates/benilla-assets/src/shaders/wow_model.wgsl +++ b/crates/benilla-assets/src/shaders/wow_model.wgsl @@ -126,6 +126,11 @@ struct WowLight { points: array, 512>, // MONKEY (p0 MonkeyFrame): the programme block after the point table (monkey_frame.wgsl). monkey: monkey_frame::MonkeyFrame, + // MONKEY (rainshelter): the rain-occlusion grid (`weather/shelter.rs`): `[origin_x, origin_z, + // 1/cell, active]`, `[base_y, cells per side, 0, 0]`, then one packed word a cell (wet_hook.wgsl). + shelter_hdr: vec4, + shelter_cfg: vec4, + shelter: array, // The interior-prop SH probes, 7 rows per slot (`MAX_PROP_PROBES` = 8192 slots). Only this // shader declares this tail; the other shaders bind the same buffer by its prefix. prop_probes: array, 57344>, @@ -1740,8 +1745,19 @@ fn fragment(in: WowVsOut, @builtin(front_facing) is_front: bool) -> WowFragOut { // Mod/Mod2x, additive and dry = untouched. let wet_dry = is_interior || interior_fogged || is_rig || is_emissive || is_mod || is_mod2x || (u32(m.clutter_fade.z) & 4u) != 0u; - let wet = wet_hook::wet_surface(rgb, n_lit, in.world_position.xyz, select(1.0, 0.0, wet_dry), - wow_light.monkey); + // MONKEY (rainshelter): nothing gets wet under a roof, porch or bridge (the shelter grid). + var wet_open = 1.0; + if (wow_light.monkey.wet_a.y > 0.0) { + let wet_st = wet_hook::shelter_taps(in.world_position.xyz, wow_light.shelter_hdr, + wow_light.shelter_cfg); + if (wet_st.live > 0.0) { + wet_open = 1.0 - wet_hook::shelter_amount(wet_st, wow_light.shelter[wet_st.idx.x], + wow_light.shelter[wet_st.idx.y], wow_light.shelter[wet_st.idx.z], + wow_light.shelter[wet_st.idx.w], in.world_position.y, wow_light.shelter_cfg); + } + } + let wet = wet_hook::wet_surface(rgb, n_lit, in.world_position.xyz, + select(1.0, 0.0, wet_dry) * wet_open, wow_light.monkey); rgb = wet.albedo; if (wet.boost > 0.0) { rgb = min(rgb + wet_hook::wet_sheen(wet.boost, n_lit, normalize(view.world_position - in.world_position.xyz), diff --git a/crates/benilla-world/src/shaders/static_gx.wgsl b/crates/benilla-world/src/shaders/static_gx.wgsl index 9009163e..142c032e 100644 --- a/crates/benilla-world/src/shaders/static_gx.wgsl +++ b/crates/benilla-world/src/shaders/static_gx.wgsl @@ -52,6 +52,11 @@ struct WowLight { points: array, 512>, // MONKEY (p0 MonkeyFrame): the programme block after the point table (monkey_frame.wgsl). monkey: monkey_frame::MonkeyFrame, + // MONKEY (rainshelter): the rain-occlusion grid (`weather/shelter.rs`): `[origin_x, origin_z, + // 1/cell, active]`, `[base_y, cells per side, 0, 0]`, then one packed word a cell (wet_hook.wgsl). + shelter_hdr: vec4, + shelter_cfg: vec4, + shelter: array, // lighting::prop_probes: 8192 slots of 7 rows; the buffer's later regions are not mirrored. prop_probes: array, 57344>, } @@ -1993,10 +1998,21 @@ fn fragment(in: GxVsOut) -> @location(0) vec4 { if ((in.word & WORD_UNLIT) != 0u) { rgb = folded; } + // MONKEY (rainshelter): nothing gets wet under a roof, porch or bridge (the shelter grid). + var wet_open = 1.0; + if (wow_light.monkey.wet_a.y > 0.0) { + let wet_st = wet_hook::shelter_taps(in.world_position.xyz, wow_light.shelter_hdr, + wow_light.shelter_cfg); + if (wet_st.live > 0.0) { + wet_open = 1.0 - wet_hook::shelter_amount(wet_st, wow_light.shelter[wet_st.idx.x], + wow_light.shelter[wet_st.idx.y], wow_light.shelter[wet_st.idx.z], + wow_light.shelter[wet_st.idx.w], in.world_position.y, wow_light.shelter_cfg); + } + } // MONKEY (wet): rain on sky-exposed surfaces (wet_hook.wgsl); interior, unlit and dry = untouched. let wet = wet_hook::wet_surface(rgb, n_lit, in.world_position.xyz, // MONKEY (fix-wet): lit WMO windows (WORD_WINDOW) are not darkened by rain either. - select(1.0, 0.0, (in.word & (WORD_INTERIOR | WORD_UNLIT | WORD_WINDOW)) != 0u), + select(1.0, 0.0, (in.word & (WORD_INTERIOR | WORD_UNLIT | WORD_WINDOW)) != 0u) * wet_open, wow_light.monkey); rgb = wet.albedo; if (wet.boost > 0.0) {