diff --git a/crates/bevy_post_process/src/effect_stack/lens_distortion.rs b/crates/bevy_post_process/src/effect_stack/lens_distortion.rs index e676da36b5c3e..dbb2c951ebef3 100644 --- a/crates/bevy_post_process/src/effect_stack/lens_distortion.rs +++ b/crates/bevy_post_process/src/effect_stack/lens_distortion.rs @@ -104,9 +104,9 @@ impl ExtractComponent for LensDistortion { #[derive(ShaderType, Default)] pub struct LensDistortionUniform { pub(super) intensity: f32, - pub(super) scale: f32, + pub(super) inv_scale: f32, pub(super) multiplier: Vec2, pub(super) center: Vec2, - pub(super) edge_curvature: f32, + pub(super) edge_intensity: f32, pub(super) unused: u32, } diff --git a/crates/bevy_post_process/src/effect_stack/lens_distortion.wgsl b/crates/bevy_post_process/src/effect_stack/lens_distortion.wgsl index f05a69e112037..327d17a2080f9 100644 --- a/crates/bevy_post_process/src/effect_stack/lens_distortion.wgsl +++ b/crates/bevy_post_process/src/effect_stack/lens_distortion.wgsl @@ -6,10 +6,10 @@ // information on these fields. struct LensDistortionSettings { intensity: f32, - scale: f32, + inv_scale: f32, multiplier: vec2, center: vec2, - edge_curvature: f32, + edge_intensity: f32, unused: u32, } @@ -24,7 +24,6 @@ fn lens_distortion(uv: vec2) -> vec2 { if (abs(intensity) < VISUAL_THRESHOLD) { return uv; } - let multiplier = lens_distortion_settings.multiplier; let center = lens_distortion_settings.center; let uv_centered = uv - center; @@ -32,20 +31,20 @@ fn lens_distortion(uv: vec2) -> vec2 { let radius = max(length(uv_centered), MATH_EPSILON); let direction = uv_centered / radius; - let adjust = dot(abs(direction), multiplier); + let adjust = dot(abs(direction), lens_distortion_settings.multiplier); // Maintains the correlation between k2 and k1, while ensuring the sign of k2 // is determined solely by `edge_curvature` rather than being influenced by intensity. let k1 = intensity * adjust; - let k2 = k1 * intensity * lens_distortion_settings.edge_curvature; - + let k2 = k1 * lens_distortion_settings.edge_intensity; + let r2 = radius * radius; let r_distorted = radius * (1.0 + (k1 + k2 * r2) * r2); let uv_distorted = direction * r_distorted + center; // Compensates for the distortion pushing pixels outside the [0,1] UV bounds. - let uv_scaled = (uv_distorted - center) / lens_distortion_settings.scale + center; + let uv_scaled = (uv_distorted - center) * lens_distortion_settings.inv_scale + center; // Discard out-of-bounds pixels to prevent edge bleeding artifacts. let uv_safe = clamp(uv_scaled, vec2(0.0), vec2(1.0)); diff --git a/crates/bevy_post_process/src/effect_stack/mod.rs b/crates/bevy_post_process/src/effect_stack/mod.rs index 7c847d8ddf2d0..b05ed87f9e31f 100644 --- a/crates/bevy_post_process/src/effect_stack/mod.rs +++ b/crates/bevy_post_process/src/effect_stack/mod.rs @@ -8,22 +8,24 @@ mod chromatic_aberration; mod lens_distortion; +mod pipeline; mod vignette; -use bevy_color::ColorToComponents; pub use chromatic_aberration::{ChromaticAberration, ChromaticAberrationUniform}; pub use lens_distortion::{LensDistortion, LensDistortionUniform}; pub use vignette::{Vignette, VignetteUniform}; -use crate::effect_stack::chromatic_aberration::{ - DefaultChromaticAberrationLut, DEFAULT_CHROMATIC_ABERRATION_LUT_DATA, +use crate::effect_stack::{ + chromatic_aberration::{DefaultChromaticAberrationLut, DEFAULT_CHROMATIC_ABERRATION_LUT_DATA}, + pipeline::{ + init_post_processing_pipeline, prepare_post_processing_pipelines, PostProcessingPipeline, + PostProcessingPipelineId, + }, }; use bevy_app::{App, Plugin}; -use bevy_asset::{ - embedded_asset, load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages, -}; -use bevy_derive::{Deref, DerefMut}; +use bevy_asset::{embedded_asset, Assets, RenderAssetUsages}; +use bevy_color::ColorToComponents; use bevy_ecs::{ component::Component, entity::Entity, @@ -33,33 +35,27 @@ use bevy_ecs::{ system::{Commands, Query, Res, ResMut}, }; use bevy_image::Image; +use bevy_math::Vec2; use bevy_render::{ - camera::ExtractedCamera, diagnostic::RecordDiagnostics, extract_component::ExtractComponentPlugin, render_asset::RenderAssets, render_resource::{ - binding_types::{sampler, texture_2d, uniform_buffer}, - BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries, - CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d, - FilterMode, FragmentState, MipmapFilterMode, Operations, PipelineCache, - RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, Sampler, - SamplerBindingType, SamplerDescriptor, ShaderStages, SpecializedRenderPipeline, - SpecializedRenderPipelines, TextureDimension, TextureFormat, TextureSampleType, + BindGroupEntries, DynamicUniformBuffer, Extent3d, Operations, PipelineCache, + RenderPassColorAttachment, RenderPassDescriptor, SpecializedRenderPipelines, + TextureDimension, TextureFormat, }, renderer::{RenderContext, RenderDevice, RenderQueue, ViewQuery}, texture::GpuImage, view::{ExtractedView, ViewTarget}, GpuResourceAppExt, Render, RenderApp, RenderStartup, RenderSystems, }; -use bevy_shader::{load_shader_library, Shader}; -use bevy_utils::prelude::default; +use bevy_shader::load_shader_library; use crate::{bloom::bloom, dof::depth_of_field}; use bevy_core_pipeline::{ schedule::{Core2d, Core3d}, tonemapping::tonemapping, - FullscreenShader, }; /// A plugin that implements a built-in postprocessing stack with some common @@ -73,38 +69,10 @@ use bevy_core_pipeline::{ #[derive(Default)] pub struct EffectStackPlugin; -/// GPU pipeline data for the built-in postprocessing stack. -/// -/// This is stored in the render world. -#[derive(Resource)] -pub struct PostProcessingPipeline { - /// The layout of bind group 0, containing the source, LUT, and settings. - bind_group_layout: BindGroupLayoutDescriptor, - /// A shared sampler used to sample both the source framebuffer texture and the LUT texture. - common_sampler: Sampler, - /// The asset handle for the fullscreen vertex shader. - fullscreen_shader: FullscreenShader, - /// The fragment shader asset handle. - fragment_shader: Handle, -} - -/// A key that uniquely identifies a built-in postprocessing pipeline. -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct PostProcessingPipelineKey { - /// The format of the source and destination textures. - target_format: TextureFormat, -} - -/// A component attached to cameras in the render world that stores the -/// specialized pipeline ID for the built-in postprocessing stack. -#[derive(Component, Deref, DerefMut)] -pub struct PostProcessingPipelineId(CachedRenderPipelineId); - /// A resource, part of the render world, that stores the uniform buffers for /// post-processing effects. /// -/// This currently holds buffers for [`ChromaticAberrationUniform`] and -/// [`VignetteUniform`], allowing them to be uploaded to the GPU efficiently. +/// This currently holds buffers, allowing them to be uploaded to the GPU efficiently. #[derive(Resource, Default)] pub struct PostProcessingUniformBuffers { chromatic_aberration: DynamicUniformBuffer, @@ -173,74 +141,7 @@ impl Plugin for EffectStackPlugin { } } -pub(crate) fn init_post_processing_pipeline( - mut commands: Commands, - render_device: Res, - fullscreen_shader: Res, - asset_server: Res, -) { - // Create our single bind group layout. - let bind_group_layout = BindGroupLayoutDescriptor::new( - "postprocessing bind group layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::FRAGMENT, - ( - // Common source: - texture_2d(TextureSampleType::Float { filterable: true }), - // Common sampler: - sampler(SamplerBindingType::Filtering), - // Chromatic aberration LUT: - texture_2d(TextureSampleType::Float { filterable: true }), - // Chromatic aberration settings: - uniform_buffer::(true), - // Vignette settings: - uniform_buffer::(true), - // Lens Distortion settings: - uniform_buffer::(true), - ), - ), - ); - - // Both source and chromatic aberration LUTs should be sampled - // bilinearly. - let common_sampler = render_device.create_sampler(&SamplerDescriptor { - mipmap_filter: MipmapFilterMode::Linear, - min_filter: FilterMode::Linear, - mag_filter: FilterMode::Linear, - ..default() - }); - - commands.insert_resource(PostProcessingPipeline { - bind_group_layout, - common_sampler, - fullscreen_shader: fullscreen_shader.clone(), - fragment_shader: load_embedded_asset!(asset_server.as_ref(), "post_process.wgsl"), - }); -} - -impl SpecializedRenderPipeline for PostProcessingPipeline { - type Key = PostProcessingPipelineKey; - - fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { - RenderPipelineDescriptor { - label: Some("postprocessing".into()), - layout: vec![self.bind_group_layout.clone()], - vertex: self.fullscreen_shader.to_vertex_state(), - fragment: Some(FragmentState { - shader: self.fragment_shader.clone(), - targets: vec![Some(ColorTargetState { - format: key.target_format, - blend: None, - write_mask: ColorWrites::ALL, - })], - ..default() - }), - ..default() - } - } -} - -pub(crate) fn post_processing( +fn post_processing( view: ViewQuery<( &ViewTarget, &PostProcessingPipelineId, @@ -350,40 +251,9 @@ pub(crate) fn post_processing( pass_span.end(&mut render_pass); } -/// Specializes the built-in postprocessing pipeline for each applicable view. -pub(crate) fn prepare_post_processing_pipelines( - mut commands: Commands, - pipeline_cache: Res, - mut pipelines: ResMut>, - post_processing_pipeline: Res, - cameras: Query< - (Entity, &ExtractedView), - Or<( - With, - With, - With, - With, - )>, - >, -) { - for (entity, view) in cameras.iter() { - let pipeline_id = pipelines.specialize( - &pipeline_cache, - &post_processing_pipeline, - PostProcessingPipelineKey { - target_format: view.target_format, - }, - ); - - commands - .entity(entity) - .insert(PostProcessingPipelineId(pipeline_id)); - } -} - /// Gathers the built-in postprocessing settings for every view and uploads them /// to the GPU. -pub(crate) fn prepare_post_processing_uniforms( +fn prepare_post_processing_uniforms( mut commands: Commands, mut post_processing_uniform_buffers: ResMut, render_device: Res, @@ -391,6 +261,7 @@ pub(crate) fn prepare_post_processing_uniforms( mut views: Query< ( Entity, + &ExtractedView, Option<&ChromaticAberration>, Option<&Vignette>, Option<&LensDistortion>, @@ -407,7 +278,7 @@ pub(crate) fn prepare_post_processing_uniforms( post_processing_uniform_buffers.lens_distortion.clear(); // Gather up all the postprocessing settings. - for (view_entity, maybe_chromatic_aberration, maybe_vignette, maybe_lens_distortion) in + for (view_entity, view, maybe_chromatic_aberration, maybe_vignette, maybe_lens_distortion) in views.iter_mut() { let chromatic_aberration_uniform_buffer_offset = @@ -426,17 +297,44 @@ pub(crate) fn prepare_post_processing_uniforms( .push(&ChromaticAberrationUniform::default()) }; - let vignette_uniform_buffer_offset = if let Some(vignette) = maybe_vignette { + let vignette_uniform_buffer_offset = if let (Some(vignette), view_size) = + (maybe_vignette, view.viewport) + { + let width = view_size.z as f32; + let height = view_size.w as f32; + + let screen_aspect = width / height; + let aspect_ratio_vec = Vec2::new(1.0, height / width); + let uv_offset = (vignette.center - Vec2::new(0.5, 0.5)) * aspect_ratio_vec; + + let min_dim = width.min(height); + let norm_aspect_ratio = Vec2::new(width / min_dim, height / min_dim); + let base_scale = norm_aspect_ratio + * Vec2::new(1.0, 1.0 / vignette.roundness.clamp(1e-6, 2.0 - 1e-6)); + // e1 * (1.0 - e3) + e2 * e3, where e1 = 1.0 + let edge_factor = if screen_aspect >= 1.0 { + Vec2::new( + 1.0 - vignette.edge_compensation + + screen_aspect.recip() * vignette.edge_compensation, + 1.0, + ) + } else { + Vec2::new( + 1.0, + 1.0 - vignette.edge_compensation + screen_aspect * vignette.edge_compensation, + ) + }; + let uv_scale = base_scale * edge_factor; + post_processing_uniform_buffers .vignette .push(&VignetteUniform { intensity: vignette.intensity.min(1.0), - radius: vignette.radius.max(1e-6), + inv_radius: 1.0 / vignette.radius.max(1e-6), smoothness: vignette.smoothness.max(0.0), - roundness: vignette.roundness.clamp(1e-6, 2.0 - 1e-6), - center: vignette.center, - edge_compensation: vignette.edge_compensation, unused: 0, + uv_offset, + uv_scale, color: vignette.color.to_srgba().to_vec4(), }) } else { @@ -451,10 +349,10 @@ pub(crate) fn prepare_post_processing_uniforms( .lens_distortion .push(&LensDistortionUniform { intensity: lens_distortion.intensity, - scale: lens_distortion.scale.max(1e-6), + inv_scale: 1.0 / lens_distortion.scale.max(1e-6), multiplier: lens_distortion.multiplier, center: lens_distortion.center, - edge_curvature: lens_distortion.edge_curvature, + edge_intensity: lens_distortion.intensity * lens_distortion.edge_curvature, unused: 0, }) } else { diff --git a/crates/bevy_post_process/src/effect_stack/pipeline.rs b/crates/bevy_post_process/src/effect_stack/pipeline.rs new file mode 100644 index 0000000000000..24ae5a4199beb --- /dev/null +++ b/crates/bevy_post_process/src/effect_stack/pipeline.rs @@ -0,0 +1,154 @@ +use bevy_asset::{load_embedded_asset, AssetServer, Handle}; +use bevy_core_pipeline::FullscreenShader; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{Or, With}, + resource::Resource, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_render::{ + camera::ExtractedCamera, + render_resource::{ + binding_types::{sampler, texture_2d, uniform_buffer}, + BindGroupLayoutDescriptor, BindGroupLayoutEntries, CachedRenderPipelineId, + ColorTargetState, ColorWrites, FilterMode, FragmentState, MipmapFilterMode, PipelineCache, + RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, + SpecializedRenderPipeline, SpecializedRenderPipelines, TextureFormat, TextureSampleType, + }, + renderer::RenderDevice, + view::ExtractedView, +}; +use bevy_shader::Shader; +use bevy_utils::default; + +use crate::effect_stack::{ + ChromaticAberration, ChromaticAberrationUniform, LensDistortion, LensDistortionUniform, + Vignette, VignetteUniform, +}; + +/// GPU pipeline data for the built-in postprocessing stack. +/// +/// This is stored in the render world. +#[derive(Resource)] +pub struct PostProcessingPipeline { + /// The layout of bind group 0, containing the source, LUT, and settings. + pub bind_group_layout: BindGroupLayoutDescriptor, + /// A shared sampler used to sample both the source framebuffer texture and the LUT texture. + pub common_sampler: Sampler, + /// The asset handle for the fullscreen vertex shader. + pub fullscreen_shader: FullscreenShader, + /// The fragment shader asset handle. + pub fragment_shader: Handle, +} + +/// A key that uniquely identifies a built-in postprocessing pipeline. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct PostProcessingPipelineKey { + /// The format of the source and destination textures. + pub target_format: TextureFormat, +} + +/// A component attached to cameras in the render world that stores the +/// specialized pipeline ID for the built-in postprocessing stack. +#[derive(Component, Deref, DerefMut)] +pub struct PostProcessingPipelineId(pub CachedRenderPipelineId); + +pub fn init_post_processing_pipeline( + mut commands: Commands, + render_device: Res, + fullscreen_shader: Res, + asset_server: Res, +) { + // Create our single bind group layout. + let bind_group_layout = BindGroupLayoutDescriptor::new( + "postprocessing bind group layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + // Common source: + texture_2d(TextureSampleType::Float { filterable: true }), + // Common sampler: + sampler(SamplerBindingType::Filtering), + // Chromatic aberration LUT: + texture_2d(TextureSampleType::Float { filterable: true }), + // Chromatic aberration settings: + uniform_buffer::(true), + // Vignette settings: + uniform_buffer::(true), + // Lens Distortion settings: + uniform_buffer::(true), + ), + ), + ); + + // Both source and chromatic aberration LUTs should be sampled + // bilinearly. + let common_sampler = render_device.create_sampler(&SamplerDescriptor { + mipmap_filter: MipmapFilterMode::Linear, + min_filter: FilterMode::Linear, + mag_filter: FilterMode::Linear, + ..default() + }); + + commands.insert_resource(PostProcessingPipeline { + bind_group_layout, + common_sampler, + fullscreen_shader: fullscreen_shader.clone(), + fragment_shader: load_embedded_asset!(asset_server.as_ref(), "post_process.wgsl"), + }); +} + +impl SpecializedRenderPipeline for PostProcessingPipeline { + type Key = PostProcessingPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + RenderPipelineDescriptor { + label: Some("postprocessing".into()), + layout: vec![self.bind_group_layout.clone()], + vertex: self.fullscreen_shader.to_vertex_state(), + fragment: Some(FragmentState { + shader: self.fragment_shader.clone(), + targets: vec![Some(ColorTargetState { + format: key.target_format, + blend: None, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + ..default() + } + } +} + +/// Specializes the built-in postprocessing pipeline for each applicable view. +pub(crate) fn prepare_post_processing_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + post_processing_pipeline: Res, + cameras: Query< + (Entity, &ExtractedView), + Or<( + With, + With, + With, + With, + )>, + >, +) { + for (entity, view) in cameras.iter() { + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &post_processing_pipeline, + PostProcessingPipelineKey { + target_format: view.target_format, + }, + ); + + commands + .entity(entity) + .insert(PostProcessingPipelineId(pipeline_id)); + } +} diff --git a/crates/bevy_post_process/src/effect_stack/vignette.rs b/crates/bevy_post_process/src/effect_stack/vignette.rs index 1ebc913a30741..4b7d8ac1e4ba6 100644 --- a/crates/bevy_post_process/src/effect_stack/vignette.rs +++ b/crates/bevy_post_process/src/effect_stack/vignette.rs @@ -113,11 +113,10 @@ impl ExtractComponent for Vignette { #[derive(ShaderType, Default)] pub struct VignetteUniform { pub(super) intensity: f32, - pub(super) radius: f32, + pub(super) inv_radius: f32, pub(super) smoothness: f32, - pub(super) roundness: f32, - pub(super) center: Vec2, - pub(super) edge_compensation: f32, pub(super) unused: u32, + pub(super) uv_offset: Vec2, + pub(super) uv_scale: Vec2, pub(super) color: Vec4, } diff --git a/crates/bevy_post_process/src/effect_stack/vignette.wgsl b/crates/bevy_post_process/src/effect_stack/vignette.wgsl index f76fb0907a5f3..2b0643ba2d490 100644 --- a/crates/bevy_post_process/src/effect_stack/vignette.wgsl +++ b/crates/bevy_post_process/src/effect_stack/vignette.wgsl @@ -8,12 +8,11 @@ // information on these fields. struct VignetteSettings { intensity: f32, - radius: f32, + inv_radius: f32, smoothness: f32, roundness: f32, - center: vec2, - edge_compensation: f32, - unused: u32, + uv_offset: vec2, + uv_scale: vec2, color: vec4 } @@ -27,52 +26,18 @@ fn vignette(uv: vec2, color: vec3) -> vec3 { if (intensity < VISUAL_THRESHOLD) { return color; } - let radius = vignette_settings.radius; - let smoothness = vignette_settings.smoothness; - let roundness = vignette_settings.roundness; - let edge_comp = vignette_settings.edge_compensation; - // Get the screen resolution. - let dims = textureDimensions(source_texture); - let resolution = vec2(dims.xy); - let screen_aspect = resolution.x / resolution.y; - - // Calculate the aspect ratio. - // - // We divide by the smallest dimension to normalize the scale. - // This will be used later to force the vignette to be circular, not oval. - let aspect_ratio = resolution / min(resolution.x, resolution.y); - - // Center the UV coordinates at (0,0). let centered_uv = uv - 0.5; - - // Calculate the normalized offset from the center. - // - // (vignette_settings.center - 0.5) maps the 0.0-1.0 input to -0.5-0.5. - // Multiplying by (1.0, y/x) compensates for the screen's aspect ratio. - // This ensures that a movement of 0.1 looks the same distance horizontally and vertically. - let offset = (vignette_settings.center - 0.5) * vec2(1.0, resolution.y / resolution.x); - - let uv_from_center = centered_uv - offset; - var scale_vec = aspect_ratio * vec2(1.0, 1.0 / roundness); - - // Apply edge compensation to make the vignette fit the screen better. - if (screen_aspect >= 1.0) { - let compensation_factor = mix(1.0, 1.0 / screen_aspect, edge_comp); - scale_vec.x *= compensation_factor; - } else { - let compensation_factor = mix(1.0, screen_aspect, edge_comp); - scale_vec.y *= compensation_factor; - } - - let final_uv = uv_from_center * scale_vec; + let uv_from_center = centered_uv - vignette_settings.uv_offset; + let final_uv = uv_from_center * vignette_settings.uv_scale; // Calculate distance from center. - let dist = length(final_uv) * (1.0 / radius); + let dist = length(final_uv) * vignette_settings.inv_radius; + + // Create a smooth radial gradient: 1.0 at center, fading to 0.0 at the edges let base_curve = 1.0 - (dist * dist); let clamped_factor = clamp(base_curve, 0.0, 1.0); - let factor = pow(clamped_factor, smoothness); + let factor = pow(clamped_factor, vignette_settings.smoothness); - // Blend the original color with the vignette color. return mix(color, vignette_settings.color.rgb, (1.0 - factor) * intensity); }