diff --git a/crates/benilla-app/src/post/sun_shafts.rs b/crates/benilla-app/src/post/sun_shafts.rs index f509fd48..04228740 100644 --- a/crates/benilla-app/src/post/sun_shafts.rs +++ b/crates/benilla-app/src/post/sun_shafts.rs @@ -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::>() .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::>(Core3d, ShaftLabel) .add_render_graph_edges( Core3d, @@ -139,12 +150,21 @@ struct ShaftShader(Handle); #[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, 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, + cache: Res, shader: Res, fullscreen: Res, ) { - 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::(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::(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, pipeline: Res, mut specialized: ResMut>, - views: Query<(Entity, &ViewTarget, &Msaa), With>, + views: Query<(Entity, &ViewTarget), With>, ) { - 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, + device: Res, + views: Query<(Entity, &ExtractedCamera), With>, +) { + 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::(); - let Some(pipeline) = cache.get_render_pipeline(id.0) else { + let settings = world.resource::(); + 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::(); 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::()); 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 { diff --git a/crates/benilla-app/src/post/sun_shafts.wgsl b/crates/benilla-app/src/post/sun_shafts.wgsl index 2ddc1834..f7dec61f 100644 --- a/crates/benilla-app/src/post/sun_shafts.wgsl +++ b/crates/benilla-app/src/post/sun_shafts.wgsl @@ -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 { + let dims = vec2(textureDimensions(depth)); + let p = min(vec2(in.position.xy) * 2, dims - vec2(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; @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; struct Shafts { sun: vec4, @@ -16,18 +35,6 @@ struct Shafts { }; @group(0) @binding(3) var shafts: Shafts; -fn scene_depth(uv: vec2) -> f32 { - let dims = textureDimensions(depth); - let p = vec2(clamp(uv, vec2(0.0), vec2(0.999999)) * vec2(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 { let base = textureSample(scene, scene_sampler, in.uv); @@ -41,10 +48,8 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { 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 { 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