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 <noreply@anthropic.com>
This commit is contained in:
pkuzic
2026-09-26 15:55:38 +01:00
co-authored by Claude Opus 5.5
parent 72dd5a0ce8
commit bcd3150086
3 changed files with 102 additions and 15 deletions
@@ -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
+77 -9
View File
@@ -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<VideoConfig>,
lighting: Res<WowLighting>,
interior: Res<CameraInteriorClaim>,
sky: Option<Res<CameraSkybox>>,
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::<ShaftView>();
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::<ShaftView>(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");
}
}
+15 -6
View File
@@ -4,6 +4,14 @@
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
struct Shafts {
sun: vec4<f32>,
color: vec4<f32>,
// MONKEY (leftovers): world-up of the view ray in NDC (xyz) and the sky threshold (w).
horizon: vec4<f32>,
ray: vec4<f32>,
};
#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<uniform> shafts: Shafts;
@fragment
fn fs_mask(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
@@ -19,7 +28,12 @@ fn fs_mask(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
// 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<f32>(p) + 0.5) / vec2<f32>(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<f32> {
@group(0) @binding(0) var scene: texture_2d<f32>;
@group(0) @binding(1) var scene_sampler: sampler;
@group(0) @binding(2) var mask: texture_2d<f32>;
struct Shafts {
sun: vec4<f32>,
color: vec4<f32>,
};
@group(0) @binding(3) var<uniform> shafts: Shafts;
@fragment