MONKEY (rainshelter): receivers read the shelter map

wet_hook gains shelter_taps/shelter_cell/shelter_amount (bilinear over
four cells; sheltered when a covered top is >= ~1 yd above the fragment,
the roof's own surface stays open). terrain, wow_model and static_gx
mirror the region after monkey and scale wet exposure by 1 - shelter
(wet_puddles takes exposure, so no puddles under cover); enhanced_water
suppresses rain rings under bridges/roofs via its row view. The lookup
runs only while wetness > 0 and the header is active, so a dry frame is
unchanged.

Verified: the wet_hook + receiver/water lookup snippets composed and
validated with naga_oil 0.20 / naga 27 (scratch harness; unpack2x16float
avoided - it needs a float16 capability). Full receiver shaders were not
compiled here (no GPU, no game data).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8t89gSUuo1uvbXKdh1mJt
This commit is contained in:
Claude
2026-09-25 18:10:12 +00:00
parent a9409c4e38
commit 8b3c07fc11
5 changed files with 138 additions and 12 deletions
@@ -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<u32>(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>) -> 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<f32>,
@@ -256,13 +277,16 @@ fn ripple_hash(p: vec2<f32>) -> vec2<f32> {
q += dot(q, q.yzx + 33.33);
return fract((q.xx + q.yz) * q.zy);
}
fn rain_ripple_normal(world_xz: vec2<f32>, footprint: f32, distance: f32, room: bool) -> vec2<f32> {
fn rain_ripple_normal(world_pos: vec3<f32>, footprint: f32, distance: f32, room: bool) -> vec2<f32> {
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<f32>(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<f32>(0.0);
}
@@ -595,7 +619,7 @@ fn enhanced_water(in: WaterFragment, shallow: vec4<f32>, deep: vec4<f32>) -> 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<f32>(-surf_grad.x, 1.0, -surf_grad.y));
if dot(n, to_view) < 0.0 { n = -n; }
+18 -2
View File
@@ -64,6 +64,11 @@ struct WowLight {
points: array<vec4<f32>, 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<f32>,
shelter_cfg: vec4<f32>,
shelter: array<u32, 16384>,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(90) var<storage, read> wow_light: WowLight;
@@ -757,9 +762,20 @@ fn fragment(in: TerrainVsOut) -> @location(0) vec4<f32> {
// 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;
@@ -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<u32>,
frac: vec2<f32>,
live: f32,
}
fn shelter_taps(world_pos: vec3<f32>, hdr: vec4<f32>, cfg: vec4<f32>) -> ShelterTaps {
var t: ShelterTaps;
t.idx = vec4<u32>(0u);
t.frac = vec2<f32>(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<u32>(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>) -> 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<f32>,
// 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>) -> 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<f32>, world_pos: vec3<f32>, 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<f32>, world_pos: vec3<f32>, 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<f32>, world_pos: vec3<f32>, 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;
@@ -126,6 +126,11 @@ struct WowLight {
points: array<vec4<f32>, 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<f32>,
shelter_cfg: vec4<f32>,
shelter: array<u32, 16384>,
// 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<vec4<f32>, 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),
@@ -52,6 +52,11 @@ struct WowLight {
points: array<vec4<f32>, 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<f32>,
shelter_cfg: vec4<f32>,
shelter: array<u32, 16384>,
// lighting::prop_probes: 8192 slots of 7 rows; the buffer's later regions are not mirrored.
prop_probes: array<vec4<f32>, 57344>,
}
@@ -1993,10 +1998,21 @@ fn fragment(in: GxVsOut) -> @location(0) vec4<f32> {
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) {