From bcd3150086d8c1d8a0937efb4bd008d1e9d726db Mon Sep 17 00:00:00 2001
From: pkuzic
Date: Sat, 26 Sep 2026 15:55:38 +0100
Subject: [PATCH] MONKEY (leftovers): sun shafts - skybox 0x8 forces them; the
sky mask ignores depth-0 below the horizon
0x8: the sky's weight lifts the daylight fade so a low/just-set sun keeps full shafts.
Mask: water writes no depth, so the sea under the horizon read as sky and got a shaft lift
(Westfall sunset: up to 9.7 levels on the water, 0 above the horizon after). The mask pass now
binds the view uniform and keeps depth-0 only where the view ray is above -0.01 elevation.
Adds the leftovers-sea-sunset capture scenario.
Co-Authored-By: Claude Opus 5.5
---
crates/benilla-app/src/capture/scenarios.rs | 10 +++
crates/benilla-app/src/post/sun_shafts.rs | 86 ++++++++++++++++++---
crates/benilla-app/src/post/sun_shafts.wgsl | 21 +++--
3 files changed, 102 insertions(+), 15 deletions(-)
diff --git a/crates/benilla-app/src/capture/scenarios.rs b/crates/benilla-app/src/capture/scenarios.rs
index 8aa90ad1..7b5fdd20 100644
--- a/crates/benilla-app/src/capture/scenarios.rs
+++ b/crates/benilla-app/src/capture/scenarios.rs
@@ -1808,6 +1808,16 @@ pub(super) const ON_DEMAND: &[Scenario] = &[
minute: 0,
ui: None,
},
+ // MONKEY (leftovers): the Westfall sea with the visible sun ~5° up on its fixed 45° bearing
+ // (minute 1200), framed so the sun sits above the open water: the sun-shaft sky mask test.
+ Scenario {
+ name: "leftovers-sea-sunset",
+ map: Some(MAP_AZEROTH),
+ eye: [-10500.0, 2112.0, 6.0],
+ look: [-10420.0, 2192.0, 8.0],
+ minute: 1200,
+ ui: None,
+ },
];
/// MONKEY (p0 baseline): the Elwynn programme view, above the canopy east of Goldshire looking
diff --git a/crates/benilla-app/src/post/sun_shafts.rs b/crates/benilla-app/src/post/sun_shafts.rs
index 77887be3..fdf382f2 100644
--- a/crates/benilla-app/src/post/sun_shafts.rs
+++ b/crates/benilla-app/src/post/sun_shafts.rs
@@ -6,6 +6,8 @@
use super::bloom::BloomLabel;
use crate::video::VideoConfig;
use benilla_world::{lighting::WowLighting, view::WorldCamera, wmo_portal::CameraInteriorClaim};
+// MONKEY (leftovers): LightSkybox flag 0x8.
+use benilla_world::skybox::CameraSkybox;
use bevy::{
core_pipeline::{
core_3d::graph::{Core3d, Node3d},
@@ -36,6 +38,12 @@ struct ShaftView {
sun: Vec4,
// Gamma-space sun colour; the framebuffer is still gamma-space here.
color: Vec4,
+ // MONKEY (leftovers): the view ray's world height is `dot(horizon.xyz, (ndc.x, ndc.y, 1))`
+ // over `length((ndc.x * ray.x, ndc.y * ray.y, 1))`; the mask counts a depth-0 pixel as sky
+ // only above `horizon.w` (sin elevation). Water writes no depth, so the sea read as sky.
+ horizon: Vec4,
+ // xy = tan half-FOV per axis (1/P00, 1/P11); zw unused.
+ ray: Vec4,
}
impl Plugin for SunShaftsPlugin {
@@ -79,6 +87,7 @@ fn update_views(
video: Res,
lighting: Res,
interior: Res,
+ sky: Option>,
mut cameras: Query<
(
Entity,
@@ -92,6 +101,11 @@ fn update_views(
>,
) {
let to_sun = lighting.celestial_dir().normalize_or_zero();
+ // MONKEY (leftovers): a `0x8` sky forces the shafts: its weight lifts the daylight ramp, so
+ // a low or just-set sun keeps full shafts (a sun far below the horizon still ends them).
+ let force = sky.map_or(0.0, |s| {
+ s.flag_weight(benilla_formats::SKYBOX_FORCE_SUN_SHAFTS)
+ });
for (entity, camera, transform, projection, mut camera3d, old) in &mut cameras {
let disabled =
!video.sun_shafts || !camera.is_active || interior.0.is_some() || to_sun == Vec3::ZERO;
@@ -121,18 +135,22 @@ fn update_views(
)
.max_element();
let edge_fade = 1.0 - smoothstep(0.0, 0.18, outside);
- let daylight = smoothstep(-0.02, 0.16, to_sun.y);
+ let daylight = smoothstep(-0.02, 0.16, to_sun.y)
+ .max(force.clamp(0.0, 1.0) * smoothstep(-0.12, -0.02, to_sun.y));
let strength = edge_fade * daylight;
if strength <= 0.001 {
commands.entity(entity).remove::();
continue;
}
camera3d.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();
+ let (horizon, ray) = horizon_row(transform, projection);
let next = ShaftView {
sun: uv.extend(strength).extend(28.0),
color: Vec3::from_array(lighting.diffuse)
.lerp(Vec3::ONE, 0.18)
.extend(0.0),
+ horizon,
+ ray,
};
if old != Some(&next) {
commands.entity(entity).insert(next);
@@ -140,6 +158,22 @@ fn update_views(
}
}
+/// MONKEY (leftovers): the view ray's world-up component as a linear form in NDC, plus the per-axis
+/// ray scale to normalise it. A pixel is sky only above `-0.01` (≈ 0.6° below the horizon): a sun
+/// just under the horizon still finds sky next to it, the sea below it never does.
+fn horizon_row(transform: &GlobalTransform, projection: &Projection) -> (Vec4, Vec4) {
+ let Projection::Perspective(p) = projection else {
+ // No horizon test: `(0, 0, 1)` is always "up".
+ return (Vec4::new(0.0, 0.0, 1.0, -2.0), Vec4::new(0.0, 0.0, 0.0, 0.0));
+ };
+ let ty = (p.fov * 0.5).tan();
+ let tx = ty * p.aspect_ratio;
+ let rot = transform.affine().matrix3;
+ // View dir = right·x·tx + up·y·ty − back; only its world Y matters.
+ let row = Vec3::new(rot.x_axis.y * tx, rot.y_axis.y * ty, -rot.z_axis.y);
+ (row.extend(-0.01), Vec4::new(tx, ty, 0.0, 0.0))
+}
+
fn smoothstep(a: f32, b: f32, value: f32) -> f32 {
let t = ((value - a) / (b - a)).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
@@ -197,13 +231,17 @@ fn init_pipeline(
let mask_layouts = [false, true].map(|multisampled| {
BindGroupLayoutDescriptor::new(
"post_sun_shafts_mask_layout",
- &BindGroupLayoutEntries::single(
+ // MONKEY (leftovers): + the view uniform, for the horizon row.
+ &BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
- if multisampled {
- texture_depth_2d_multisampled()
- } else {
- texture_depth_2d()
- },
+ (
+ if multisampled {
+ texture_depth_2d_multisampled()
+ } else {
+ texture_depth_2d()
+ },
+ uniform_buffer::(false),
+ ),
),
)
});
@@ -302,7 +340,12 @@ fn prepare_masks(
let Some(size) = camera.physical_viewport_size else {
continue;
};
- let rows = [shaft.sun.to_array(), shaft.color.to_array()];
+ let rows = [
+ shaft.sun.to_array(),
+ shaft.color.to_array(),
+ shaft.horizon.to_array(),
+ shaft.ray.to_array(),
+ ];
match uniform {
Some(uniform) => queue.write_buffer(&uniform.0, 0, bytemuck::cast_slice(&rows)),
None => {
@@ -376,7 +419,7 @@ impl ViewNode for ShaftNode {
let mask_bind = device.create_bind_group(
"post_sun_shafts_mask",
&cache.get_bind_group_layout(&settings.mask_layouts[multisampled]),
- &BindGroupEntries::single(depth.view()),
+ &BindGroupEntries::sequential((depth.view(), uniform.0.as_entire_binding())),
);
let out = target.post_process_write();
let bind = device.create_bind_group(
@@ -444,4 +487,29 @@ mod tests {
assert_eq!(smoothstep(0.0, 1.0, 2.0), 1.0);
assert!((smoothstep(0.0, 1.0, 0.5) - 0.5).abs() < 1.0e-6);
}
+
+ /// MONKEY (leftovers): the shader's horizon form, evaluated on the CPU, is the view ray's
+ /// world-up sine: the screen centre of a level camera sits on the horizon, a pitched-down one
+ /// sees below it, and the top/bottom edges sit at ±half the vertical FOV.
+ #[test]
+ fn horizon_row_is_the_view_ray_elevation() {
+ let p = PerspectiveProjection {
+ fov: 1.0,
+ aspect_ratio: 16.0 / 9.0,
+ ..default()
+ };
+ let proj = Projection::Perspective(p.clone());
+ let up = |t: &Transform, ndc: Vec2| {
+ let (h, r) = horizon_row(&GlobalTransform::from(*t), &proj);
+ h.truncate().dot(ndc.extend(1.0)) / (ndc * r.truncate().truncate()).extend(1.0).length()
+ };
+ let level = Transform::default().looking_to(Vec3::X, Vec3::Y);
+ assert!(up(&level, Vec2::ZERO).abs() < 1e-5);
+ assert!((up(&level, Vec2::new(0.0, 1.0)) - 0.5f32.sin()).abs() < 1e-4);
+ assert!((up(&level, Vec2::new(0.0, -1.0)) + 0.5f32.sin()).abs() < 1e-4);
+ let down = Transform::default().looking_to(Vec3::new(1.0, -0.3, 0.0), Vec3::Y);
+ let dir = Vec3::new(1.0, -0.3, 0.0).normalize();
+ assert!((up(&down, Vec2::ZERO) - dir.y).abs() < 1e-5);
+ assert!(up(&down, Vec2::ZERO) < -0.01, "sea below the horizon is not sky");
+ }
}
diff --git a/crates/benilla-app/src/post/sun_shafts.wgsl b/crates/benilla-app/src/post/sun_shafts.wgsl
index f7dec61f..845a3a88 100644
--- a/crates/benilla-app/src/post/sun_shafts.wgsl
+++ b/crates/benilla-app/src/post/sun_shafts.wgsl
@@ -4,6 +4,14 @@
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
+struct Shafts {
+ sun: vec4,
+ color: vec4,
+ // MONKEY (leftovers): world-up of the view ray in NDC (xyz) and the sky threshold (w).
+ horizon: vec4,
+ ray: vec4,
+};
+
#ifdef SHAFT_MASK
#ifdef MULTISAMPLED
@@ -11,6 +19,7 @@
#else
@group(0) @binding(0) var depth: texture_depth_2d;
#endif
+@group(0) @binding(1) var shafts: Shafts;
@fragment
fn fs_mask(in: FullscreenVertexOutput) -> @location(0) vec4 {
@@ -19,7 +28,12 @@ fn fs_mask(in: FullscreenVertexOutput) -> @location(0) vec4 {
// MONKEY (fix-post): one sample, not a max over all of them: the march is a blur.
// Only the cleared / sky-pinned depth (exactly 0 on infinite reverse-Z) is sky; a ramp on
// `near / z` would move with the live `nearclip` cvar and pour shafts through far terrain.
- let sky = select(0.0, 1.0, textureLoad(depth, p, 0) <= 1.0e-7);
+ // MONKEY (leftovers): water writes no depth, so depth 0 under the horizon is the sea (or the
+ // far-clipped ground), never sky.
+ let ndc = (vec2(p) + 0.5) / vec2(dims) * vec2(2.0, -2.0) + vec2(-1.0, 1.0);
+ let up = dot(shafts.horizon.xyz, vec3(ndc, 1.0))
+ / length(vec3(ndc * shafts.ray.xy, 1.0));
+ let sky = select(0.0, 1.0, textureLoad(depth, p, 0) <= 1.0e-7 && up > shafts.horizon.w);
return vec4(sky, 0.0, 0.0, 1.0);
}
@@ -28,11 +42,6 @@ fn fs_mask(in: FullscreenVertexOutput) -> @location(0) vec4 {
@group(0) @binding(0) var scene: texture_2d;
@group(0) @binding(1) var scene_sampler: sampler;
@group(0) @binding(2) var mask: texture_2d;
-
-struct Shafts {
- sun: vec4,
- color: vec4,
-};
@group(0) @binding(3) var shafts: Shafts;
@fragment