MONKEY (polish): sun shafts march a half-resolution sky mask

The radial blur read the scene depth 28 times per full-resolution pixel. A
new mask pre-pass (`fs_mask`, R8Unorm at ceil(w/2) x ceil(h/2)) loads the
depth once per half-res texel and thresholds it with the same sky test
(depth <= 1e-7 on infinite reverse-Z); the blur then takes 28 clamp-to-edge
bilinear taps of the mask. Only the mask stage is MSAA-specialised (single
and multisampled depth layouts, one pipeline each); the blur pipeline is
keyed by the target format alone.

Verified: cargo check -p benilla; all four WGSL variants (blur, blur +
MULTISAMPLED, SHAFT_MASK, SHAFT_MASK + MULTISAMPLED) compose and validate
under naga_oil 0.20 / naga 27 offline. No GPU or WoW data in this
environment, so the image is not captured; built on Linux only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6yUWgPosSakvL23gtgvpH
This commit is contained in:
Claude
2026-09-25 17:42:29 +00:00
parent 9f874e33e6
commit bc43ed063e
2 changed files with 172 additions and 59 deletions
+144 -38
View File
@@ -1,4 +1,7 @@
//! MONKEY (post): screen-space sun shafts from the visible sky depth.
//!
//! MONKEY (polish): the depth is read once per half-resolution texel into an R8 sky mask
//! (`fs_mask`, the only MSAA-specialised stage); the 28-tap radial blur then samples the mask.
use super::bloom::BloomLabel;
use crate::video::VideoConfig;
@@ -11,6 +14,7 @@ use bevy::{
ecs::query::QueryItem,
prelude::*,
render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_graph::{
@@ -18,7 +22,8 @@ use bevy::{
},
render_resource::{binding_types::*, *},
renderer::{RenderContext, RenderDevice, RenderQueue},
view::{Msaa, ViewDepthTexture, ViewTarget},
texture::{CachedTexture, TextureCache},
view::{ViewDepthTexture, ViewTarget},
Render, RenderApp, RenderStartup, RenderSystems,
},
};
@@ -54,7 +59,13 @@ impl Plugin for SunShaftsPlugin {
.insert_resource(ShaftShader(shader))
.init_resource::<SpecializedRenderPipelines<ShaftPipeline>>()
.add_systems(RenderStartup, init_pipeline)
.add_systems(Render, prepare_pipelines.in_set(RenderSystems::Prepare))
.add_systems(
Render,
(
prepare_pipelines.in_set(RenderSystems::Prepare),
prepare_masks.in_set(RenderSystems::PrepareResources),
),
)
.add_render_graph_node::<ViewNodeRunner<ShaftNode>>(Core3d, ShaftLabel)
.add_render_graph_edges(
Core3d,
@@ -139,12 +150,21 @@ struct ShaftShader(Handle<Shader>);
#[derive(Resource)]
struct ShaftPipeline {
layouts: [BindGroupLayoutDescriptor; 2],
/// The blur's layout; the mask's per MSAA (`[single, multisampled]`).
layout: BindGroupLayoutDescriptor,
mask_layouts: [BindGroupLayoutDescriptor; 2],
masks: [CachedRenderPipelineId; 2],
shader: Handle<Shader>,
fullscreen: FullscreenShader,
sampler: Sampler,
}
/// The half-resolution sky mask the blur marches through.
const MASK_FORMAT: TextureFormat = TextureFormat::R8Unorm;
#[derive(Component)]
struct ShaftMask(CachedTexture);
#[derive(Component)]
struct ViewShaftPipeline(CachedRenderPipelineId);
@@ -154,29 +174,61 @@ struct ShaftLabel;
fn init_pipeline(
mut commands: Commands,
device: Res<RenderDevice>,
cache: Res<PipelineCache>,
shader: Res<ShaftShader>,
fullscreen: Res<FullscreenShader>,
) {
let layouts = [false, true].map(|multisampled| {
let layout = BindGroupLayoutDescriptor::new(
"post_sun_shafts_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
texture_2d(TextureSampleType::Float { filterable: true }),
uniform_buffer::<ShaftView>(false),
),
),
);
let mask_layouts = [false, true].map(|multisampled| {
BindGroupLayoutDescriptor::new(
"post_sun_shafts_layout",
&BindGroupLayoutEntries::sequential(
"post_sun_shafts_mask_layout",
&BindGroupLayoutEntries::single(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
if multisampled {
texture_depth_2d_multisampled()
} else {
texture_depth_2d()
},
uniform_buffer::<ShaftView>(false),
),
if multisampled {
texture_depth_2d_multisampled()
} else {
texture_depth_2d()
},
),
)
});
let masks = [false, true].map(|multisampled| {
let mut shader_defs = vec!["SHAFT_MASK".into()];
if multisampled {
shader_defs.push("MULTISAMPLED".into());
}
cache.queue_render_pipeline(RenderPipelineDescriptor {
label: Some("post_sun_shafts_mask".into()),
layout: vec![mask_layouts[multisampled as usize].clone()],
vertex: fullscreen.to_vertex_state(),
fragment: Some(FragmentState {
shader: shader.0.clone(),
shader_defs,
entry_point: Some("fs_mask".into()),
targets: vec![Some(ColorTargetState {
format: MASK_FORMAT,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
..default()
})
});
commands.insert_resource(ShaftPipeline {
layouts,
layout,
mask_layouts,
masks,
shader: shader.0.clone(),
fullscreen: fullscreen.clone(),
sampler: device.create_sampler(&SamplerDescriptor {
@@ -190,19 +242,16 @@ fn init_pipeline(
}
impl SpecializedRenderPipeline for ShaftPipeline {
type Key = (TextureFormat, bool);
type Key = TextureFormat;
fn specialize(&self, (format, multisampled): Self::Key) -> RenderPipelineDescriptor {
fn specialize(&self, format: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("post_sun_shafts".into()),
layout: vec![self.layouts[multisampled as usize].clone()],
layout: vec![self.layout.clone()],
vertex: self.fullscreen.to_vertex_state(),
fragment: Some(FragmentState {
shader: self.shader.clone(),
shader_defs: multisampled
.then(|| "MULTISAMPLED".into())
.into_iter()
.collect(),
shader_defs: vec![],
entry_point: Some("fragment".into()),
targets: vec![Some(ColorTargetState {
format,
@@ -220,18 +269,46 @@ fn prepare_pipelines(
cache: Res<PipelineCache>,
pipeline: Res<ShaftPipeline>,
mut specialized: ResMut<SpecializedRenderPipelines<ShaftPipeline>>,
views: Query<(Entity, &ViewTarget, &Msaa), With<ShaftView>>,
views: Query<(Entity, &ViewTarget), With<ShaftView>>,
) {
for (entity, target, msaa) in &views {
let id = specialized.specialize(
&cache,
&pipeline,
(target.main_texture_format(), msaa.samples() > 1),
);
for (entity, target) in &views {
let id = specialized.specialize(&cache, &pipeline, target.main_texture_format());
commands.entity(entity).insert(ViewShaftPipeline(id));
}
}
/// The half-size mask target, rounded up so an odd edge column still has a texel.
fn prepare_masks(
mut commands: Commands,
mut textures: ResMut<TextureCache>,
device: Res<RenderDevice>,
views: Query<(Entity, &ExtractedCamera), With<ShaftView>>,
) {
for (entity, camera) in &views {
let Some(size) = camera.physical_viewport_size else {
continue;
};
let mask = textures.get(
&device,
TextureDescriptor {
label: Some("post_sun_shafts_mask"),
size: Extent3d {
width: size.x.div_ceil(2).max(1),
height: size.y.div_ceil(2).max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: MASK_FORMAT,
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
);
commands.entity(entity).insert(ShaftMask(mask));
}
}
#[derive(Default)]
struct ShaftNode;
@@ -241,17 +318,23 @@ impl ViewNode for ShaftNode {
&'static ViewDepthTexture,
&'static ShaftView,
&'static ViewShaftPipeline,
&'static ShaftMask,
);
fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
context: &mut RenderContext<'w>,
(target, depth, shaft, id): QueryItem<'w, '_, Self::ViewQuery>,
(target, depth, shaft, id, mask): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let cache = world.resource::<PipelineCache>();
let Some(pipeline) = cache.get_render_pipeline(id.0) else {
let settings = world.resource::<ShaftPipeline>();
let multisampled = (depth.texture.sample_count() > 1) as usize;
let (Some(pipeline), Some(mask_pipeline)) = (
cache.get_render_pipeline(id.0),
cache.get_render_pipeline(settings.masks[multisampled]),
) else {
return Ok(());
};
if !depth
@@ -261,24 +344,47 @@ impl ViewNode for ShaftNode {
{
return Ok(());
}
let settings = world.resource::<ShaftPipeline>();
let device = context.render_device();
let mask_bind = device.create_bind_group(
"post_sun_shafts_mask",
&cache.get_bind_group_layout(&settings.mask_layouts[multisampled]),
&BindGroupEntries::single(depth.view()),
);
let mut uniform = UniformBuffer::from(*shaft);
uniform.write_buffer(device, world.resource::<RenderQueue>());
let out = target.post_process_write();
let bind = device.create_bind_group(
"post_sun_shafts",
&cache.get_bind_group_layout(
&settings.layouts[(depth.texture.sample_count() > 1) as usize],
),
&cache.get_bind_group_layout(&settings.layout),
&BindGroupEntries::sequential((
out.source,
&settings.sampler,
depth.view(),
&mask.0.default_view,
uniform.binding().unwrap(),
)),
);
let diagnostics = context.diagnostic_recorder();
{
let mut pass = context
.command_encoder()
.begin_render_pass(&RenderPassDescriptor {
label: Some("post_sun_shafts_mask"),
color_attachments: &[Some(RenderPassColorAttachment {
view: &mask.0.default_view,
depth_slice: None,
resolve_target: None,
ops: Operations::default(),
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
let span = diagnostics.pass_span(&mut pass, "post_sun_shafts_mask");
pass.set_pipeline(mask_pipeline);
pass.set_bind_group(0, &mask_bind, &[]);
pass.draw(0..3, 0..1);
span.end(&mut pass);
}
let mut pass = context
.command_encoder()
.begin_render_pass(&RenderPassDescriptor {
+28 -21
View File
@@ -1,14 +1,33 @@
// MONKEY (post): screen-space radial blur through sky pixels in the scene depth.
// MONKEY (polish): two passes. `SHAFT_MASK` reads the depth once per half-resolution texel into
// a sky mask; the blur then samples that mask 28 times instead of the full-size depth.
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
#ifdef SHAFT_MASK
#ifdef MULTISAMPLED
@group(0) @binding(0) var depth: texture_depth_multisampled_2d;
#else
@group(0) @binding(0) var depth: texture_depth_2d;
#endif
@fragment
fn fs_mask(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
let dims = vec2<i32>(textureDimensions(depth));
let p = min(vec2<i32>(in.position.xy) * 2, dims - vec2<i32>(1));
// 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);
return vec4(sky, 0.0, 0.0, 1.0);
}
#else
@group(0) @binding(0) var scene: texture_2d<f32>;
@group(0) @binding(1) var scene_sampler: sampler;
#ifdef MULTISAMPLED
@group(0) @binding(2) var depth: texture_depth_multisampled_2d;
#else
@group(0) @binding(2) var depth: texture_depth_2d;
#endif
@group(0) @binding(2) var mask: texture_2d<f32>;
struct Shafts {
sun: vec4<f32>,
@@ -16,18 +35,6 @@ struct Shafts {
};
@group(0) @binding(3) var<uniform> shafts: Shafts;
fn scene_depth(uv: vec2<f32>) -> f32 {
let dims = textureDimensions(depth);
let p = vec2<i32>(clamp(uv, vec2(0.0), vec2(0.999999)) * vec2<f32>(dims));
#ifdef MULTISAMPLED
// MONKEY (fix-post): one sample, not a max over all of them: the march is a blur, and the
// per-sample loop cost 28 x N loads per pixel (224 at 8x).
return textureLoad(depth, p, 0);
#else
return textureLoad(depth, p, 0);
#endif
}
@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
let base = textureSample(scene, scene_sampler, in.uv);
@@ -41,10 +48,8 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
if (i >= count) { break; }
uv += step_uv;
decay *= 0.965;
// MONKEY (fix-post): only the cleared / sky-pinned depth (exactly 0 on infinite reverse-Z)
// is sky. The old ramp read d = near / z, so it moved with the live `nearclip` cvar and
// let shafts pour through terrain beyond ~125 yd at nearclip 0.01.
let sky = select(0.0, 1.0, scene_depth(uv) <= 1.0e-7);
// The clamp-to-edge bilinear tap is the old clamped depth read, pre-thresholded.
let sky = textureSampleLevel(mask, scene_sampler, uv, 0.0).r;
light += sky * decay;
norm += decay;
}
@@ -56,3 +61,5 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
let rays = light / max(norm, 0.0001) * radial * radial;
return vec4(base.rgb + shafts.color.rgb * rays * shafts.sun.z * 0.16, base.a);
}
#endif