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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user