From fcda67e8945775c59a7835b5f4936cb7fe9fb117 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Mon, 15 Jul 2024 06:59:02 -0700 Subject: [PATCH] Start a built-in postprocessing stack, and implement chromatic aberration in it. (#13695) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit creates a new built-in postprocessing shader that's designed to hold miscellaneous postprocessing effects, and starts it off with chromatic aberration. Possible future effects include vignette, film grain, and lens distortion. [Chromatic aberration] is a common postprocessing effect that simulates lenses that fail to focus all colors of light to a single point. It's often used for impact effects and/or horror games. This patch uses the technique from *Inside* ([Gjøl & Svendsen 2016]), which allows the developer to customize the particular color pattern to achieve different effects. Unity HDRP uses the same technique, while Unreal has a hard-wired fixed color pattern. A new example, `post_processing`, has been added, in order to demonstrate the technique. The existing `post_processing` shader has been renamed to `custom_post_processing`, for clarity. [Chromatic aberration]: https://en.wikipedia.org/wiki/Chromatic_aberration [Gjøl & Svendsen 2016]: https://github.com/playdeadgames/publications/blob/master/INSIDE/rendering_inside_gdc2016.pdf ![Screenshot 2024-06-04 180304](https://github.com/bevyengine/bevy/assets/157897/3631c64f-a615-44fe-91ca-7f04df0a54b2) ![Screenshot 2024-06-04 180743](https://github.com/bevyengine/bevy/assets/157897/ee055cbf-4314-49c5-8bfa-8d8a17bd52bb) ## Changelog ### Added * Chromatic aberration is now available as a built-in postprocessing effect. To use it, add `ChromaticAberration` to your camera. --- Cargo.toml | 17 +- crates/bevy_core_pipeline/src/core_2d/mod.rs | 1 + crates/bevy_core_pipeline/src/core_3d/mod.rs | 1 + crates/bevy_core_pipeline/src/lib.rs | 3 + .../post_process/chromatic_aberration.wgsl | 92 ++++ .../src/post_process/mod.rs | 505 ++++++++++++++++++ .../src/post_process/post_process.wgsl | 9 + examples/3d/post_processing.rs | 216 ++++++++ examples/README.md | 3 +- ...rocessing.rs => custom_post_processing.rs} | 0 10 files changed, 843 insertions(+), 4 deletions(-) create mode 100644 crates/bevy_core_pipeline/src/post_process/chromatic_aberration.wgsl create mode 100644 crates/bevy_core_pipeline/src/post_process/mod.rs create mode 100644 crates/bevy_core_pipeline/src/post_process/post_process.wgsl create mode 100644 examples/3d/post_processing.rs rename examples/shader/{post_processing.rs => custom_post_processing.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index 9a39bb09e5..02424d3217 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2237,11 +2237,11 @@ category = "Shaders" wasm = true [[example]] -name = "post_processing" -path = "examples/shader/post_processing.rs" +name = "custom_post_processing" +path = "examples/shader/custom_post_processing.rs" doc-scrape-examples = true -[package.metadata.example.post_processing] +[package.metadata.example.custom_post_processing] name = "Post Processing - Custom Render Pass" description = "A custom post processing effect, using a custom render pass that runs after the main pass" category = "Shaders" @@ -3284,6 +3284,17 @@ description = "Handles input, physics, and rendering in an industry-standard way category = "Movement" wasm = true +[[example]] +name = "post_processing" +path = "examples/3d/post_processing.rs" +doc-scrape-examples = true + +[package.metadata.example.post_processing] +name = "Built-in postprocessing" +description = "Demonstrates the built-in postprocessing features" +category = "3D Rendering" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 869356c502..f04287081d 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -18,6 +18,7 @@ pub mod graph { MainTransparentPass, EndMainPass, Bloom, + PostProcessing, Tonemapping, Fxaa, Smaa, diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index d2ddc76982..ae82cd58fb 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -30,6 +30,7 @@ pub mod graph { Bloom, AutoExposure, DepthOfField, + PostProcessing, Tonemapping, Fxaa, Smaa, diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index fec38e432b..9118afc8d4 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -19,6 +19,7 @@ pub mod fullscreen_vertex_shader; pub mod fxaa; pub mod motion_blur; pub mod msaa_writeback; +pub mod post_process; pub mod prepass; mod skybox; pub mod smaa; @@ -60,6 +61,7 @@ use crate::{ fxaa::FxaaPlugin, motion_blur::MotionBlurPlugin, msaa_writeback::MsaaWritebackPlugin, + post_process::PostProcessingPlugin, prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, smaa::SmaaPlugin, tonemapping::TonemappingPlugin, @@ -99,6 +101,7 @@ impl Plugin for CorePipelinePlugin { MotionBlurPlugin, DepthOfFieldPlugin, SmaaPlugin, + PostProcessingPlugin, )); } } diff --git a/crates/bevy_core_pipeline/src/post_process/chromatic_aberration.wgsl b/crates/bevy_core_pipeline/src/post_process/chromatic_aberration.wgsl new file mode 100644 index 0000000000..905b11d7e6 --- /dev/null +++ b/crates/bevy_core_pipeline/src/post_process/chromatic_aberration.wgsl @@ -0,0 +1,92 @@ +// The chromatic aberration postprocessing effect. +// +// This makes edges of objects turn into multicolored streaks. + +#define_import_path bevy_core_pipeline::post_processing::chromatic_aberration + +// See `bevy_core_pipeline::post_process::ChromaticAberration` for more +// information on these fields. +struct ChromaticAberrationSettings { + intensity: f32, + max_samples: u32, + unused_a: u32, + unused_b: u32, +} + +// The source framebuffer texture. +@group(0) @binding(0) var chromatic_aberration_source_texture: texture_2d; +// The sampler used to sample the source framebuffer texture. +@group(0) @binding(1) var chromatic_aberration_source_sampler: sampler; +// The 1D lookup table for chromatic aberration. +@group(0) @binding(2) var chromatic_aberration_lut_texture: texture_2d; +// The sampler used to sample that lookup table. +@group(0) @binding(3) var chromatic_aberration_lut_sampler: sampler; +// The settings supplied by the developer. +@group(0) @binding(4) var chromatic_aberration_settings: ChromaticAberrationSettings; + +fn chromatic_aberration(start_pos: vec2) -> vec3 { + // Radial chromatic aberration implemented using the *Inside* technique: + // + // + + let end_pos = mix(start_pos, vec2(0.5), chromatic_aberration_settings.intensity); + + // Determine the number of samples. We aim for one sample per texel, unless + // that's higher than the developer-specified maximum number of samples, in + // which case we choose the maximum number of samples. + let texel_length = length((end_pos - start_pos) * + vec2(textureDimensions(chromatic_aberration_source_texture))); + let sample_count = min(u32(ceil(texel_length)), chromatic_aberration_settings.max_samples); + + var color: vec3; + if (sample_count > 1u) { + // The LUT texture is in clamp-to-edge mode, so we start at 0.5 texels + // from the sides so that we have a nice gradient over the entire LUT + // range. + let lut_u_offset = 0.5 / f32(textureDimensions(chromatic_aberration_lut_texture).x); + + var sample_sum = vec3(0.0); + var modulate_sum = vec3(0.0); + + // Start accumulating samples. + for (var sample_index = 0u; sample_index < sample_count; sample_index += 1u) { + let t = (f32(sample_index) + 0.5) / f32(sample_count); + + // Sample the framebuffer. + let sample_uv = mix(start_pos, end_pos, t); + let sample = textureSampleLevel( + chromatic_aberration_source_texture, + chromatic_aberration_source_sampler, + sample_uv, + 0.0, + ).rgb; + + // Sample the LUT. + let lut_u = mix(lut_u_offset, 1.0 - lut_u_offset, t); + let modulate = textureSampleLevel( + chromatic_aberration_lut_texture, + chromatic_aberration_lut_sampler, + vec2(lut_u, 0.5), + 0.0, + ).rgb; + + // Modulate the sample by the LUT value. + sample_sum += sample * modulate; + modulate_sum += modulate; + } + + color = sample_sum / modulate_sum; + } else { + // If there's only one sample, don't do anything. If we don't do this, + // then this shader will apply whatever tint is in the center of the LUT + // texture to such pixels, which is wrong. + color = textureSampleLevel( + chromatic_aberration_source_texture, + chromatic_aberration_source_sampler, + start_pos, + 0.0, + ).rgb; + } + + return color; +} diff --git a/crates/bevy_core_pipeline/src/post_process/mod.rs b/crates/bevy_core_pipeline/src/post_process/mod.rs new file mode 100644 index 0000000000..79c41f990b --- /dev/null +++ b/crates/bevy_core_pipeline/src/post_process/mod.rs @@ -0,0 +1,505 @@ +//! Miscellaneous built-in postprocessing effects. +//! +//! Currently, this consists only of chromatic aberration. + +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, Assets, Handle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{QueryItem, With}, + reflect::ReflectComponent, + schedule::IntoSystemConfigs as _, + system::{lifetimeless::Read, Commands, Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + camera::Camera, + extract_component::{ExtractComponent, ExtractComponentPlugin}, + render_asset::{RenderAssetUsages, RenderAssets}, + render_graph::{ + NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner, + }, + render_resource::{ + binding_types::{sampler, texture_2d, uniform_buffer}, + BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, + ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d, FilterMode, FragmentState, + Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, + RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader, + ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, + TextureDimension, TextureFormat, TextureSampleType, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::{BevyDefault, GpuImage, Image}, + view::{ExtractedView, ViewTarget}, + Render, RenderApp, RenderSet, +}; +use bevy_utils::prelude::default; + +use crate::{ + core_2d::graph::{Core2d, Node2d}, + core_3d::graph::{Core3d, Node3d}, + fullscreen_vertex_shader, +}; + +/// The handle to the built-in postprocessing shader `post_process.wgsl`. +const POST_PROCESSING_SHADER_HANDLE: Handle = Handle::weak_from_u128(14675654334038973533); +/// The handle to the chromatic aberration shader `chromatic_aberration.wgsl`. +const CHROMATIC_ABERRATION_SHADER_HANDLE: Handle = + Handle::weak_from_u128(10969893303667163833); + +/// The handle to the default chromatic aberration lookup texture. +/// +/// This is just a 3x1 image consisting of one red pixel, one green pixel, and +/// one blue pixel, in that order. +const DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE: Handle = + Handle::weak_from_u128(2199972955136579180); + +/// The default chromatic aberration intensity amount, in a fraction of the +/// window size. +const DEFAULT_CHROMATIC_ABERRATION_INTENSITY: f32 = 0.02; + +/// The default maximum number of samples for chromatic aberration. +const DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES: u32 = 8; + +/// The raw RGBA data for the default chromatic aberration gradient. +/// +/// This consists of one red pixel, one green pixel, and one blue pixel, in that +/// order. +static DEFAULT_CHROMATIC_ABERRATION_LUT_DATA: [u8; 12] = + [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]; + +/// A plugin that implements a built-in postprocessing stack with some common +/// effects. +/// +/// Currently, this only consists of chromatic aberration. +pub struct PostProcessingPlugin; + +/// Adds colored fringes to the edges of objects in the scene. +/// +/// [Chromatic aberration] simulates the effect when lenses fail to focus all +/// colors of light toward a single point. It causes rainbow-colored streaks to +/// appear, which are especially apparent on the edges of objects. Chromatic +/// aberration is commonly used for collision effects, especially in horror +/// games. +/// +/// Bevy's implementation is based on that of *Inside* ([Gjøl & Svendsen 2016]). +/// It's based on a customizable lookup texture, which allows for changing the +/// color pattern. By default, the color pattern is simply a 3×1 pixel texture +/// consisting of red, green, and blue, in that order, but you can change it to +/// any image in order to achieve different effects. +/// +/// [Chromatic aberration]: https://en.wikipedia.org/wiki/Chromatic_aberration +/// +/// [Gjøl & Svendsen 2016]: https://github.com/playdeadgames/publications/blob/master/INSIDE/rendering_inside_gdc2016.pdf +#[derive(Reflect, Component, Clone)] +#[reflect(Component, Default)] +pub struct ChromaticAberration { + /// The lookup texture that determines the color gradient. + /// + /// By default, this is a 3×1 texel texture consisting of one red pixel, one + /// green pixel, and one blue texel, in that order. This recreates the most + /// typical chromatic aberration pattern. However, you can change it to + /// achieve different artistic effects. + /// + /// The texture is always sampled in its vertical center, so it should + /// ordinarily have a height of 1 texel. + pub color_lut: Handle, + + /// The size of the streaks around the edges of objects, as a fraction of + /// the window size. + /// + /// The default value is 0.2. + pub intensity: f32, + + /// A cap on the number of texture samples that will be performed. + /// + /// Higher values result in smoother-looking streaks but are slower. + /// + /// The default value is 8. + pub max_samples: u32, +} + +/// 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: BindGroupLayout, + /// Specifies how to sample the source framebuffer texture. + source_sampler: Sampler, + /// Specifies how to sample the chromatic aberration gradient. + chromatic_aberration_lut_sampler: Sampler, +} + +/// 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. + texture_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); + +/// The on-GPU version of the [`ChromaticAberration`] settings. +/// +/// See the documentation for [`ChromaticAberration`] for more information on +/// each of these fields. +#[derive(ShaderType)] +pub struct ChromaticAberrationUniform { + /// The intensity of the effect, in a fraction of the screen. + intensity: f32, + /// A cap on the number of samples of the source texture that the shader + /// will perform. + max_samples: u32, + /// Padding data. + unused_1: u32, + /// Padding data. + unused_2: u32, +} + +/// A resource, part of the render world, that stores the +/// [`ChromaticAberrationUniform`]s for each view. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct PostProcessingUniformBuffers { + chromatic_aberration: DynamicUniformBuffer, +} + +/// A component, part of the render world, that stores the appropriate byte +/// offset within the [`PostProcessingUniformBuffers`] for the camera it's +/// attached to. +#[derive(Component, Deref, DerefMut)] +pub struct PostProcessingUniformBufferOffsets { + chromatic_aberration: u32, +} + +/// The render node that runs the built-in postprocessing stack. +#[derive(Default)] +pub struct PostProcessingNode; + +impl Plugin for PostProcessingPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + POST_PROCESSING_SHADER_HANDLE, + "post_process.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + CHROMATIC_ABERRATION_SHADER_HANDLE, + "chromatic_aberration.wgsl", + Shader::from_wgsl + ); + + // Load the default chromatic aberration LUT. + let mut assets = app.world_mut().resource_mut::>(); + assets.insert( + DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE.id(), + Image::new( + Extent3d { + width: 3, + height: 1, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + DEFAULT_CHROMATIC_ABERRATION_LUT_DATA.to_vec(), + TextureFormat::Rgba8UnormSrgb, + RenderAssetUsages::RENDER_WORLD, + ), + ); + + app.register_type::(); + app.add_plugins(ExtractComponentPlugin::::default()); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::>() + .init_resource::() + .add_systems( + Render, + ( + prepare_post_processing_pipelines, + prepare_post_processing_uniforms, + ) + .in_set(RenderSet::Prepare), + ) + .add_render_graph_node::>( + Core3d, + Node3d::PostProcessing, + ) + .add_render_graph_edges( + Core3d, + ( + Node3d::DepthOfField, + Node3d::PostProcessing, + Node3d::Tonemapping, + ), + ) + .add_render_graph_node::>( + Core2d, + Node2d::PostProcessing, + ) + .add_render_graph_edges( + Core2d, + (Node2d::Bloom, Node2d::PostProcessing, Node2d::Tonemapping), + ); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app.init_resource::(); + } +} + +impl Default for ChromaticAberration { + fn default() -> Self { + Self { + color_lut: DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE, + intensity: DEFAULT_CHROMATIC_ABERRATION_INTENSITY, + max_samples: DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES, + } + } +} + +impl FromWorld for PostProcessingPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + // Create our single bind group layout. + let bind_group_layout = render_device.create_bind_group_layout( + Some("postprocessing bind group layout"), + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + // Chromatic aberration source: + texture_2d(TextureSampleType::Float { filterable: true }), + // Chromatic aberration source sampler: + sampler(SamplerBindingType::Filtering), + // Chromatic aberration LUT: + texture_2d(TextureSampleType::Float { filterable: true }), + // Chromatic aberration LUT sampler: + sampler(SamplerBindingType::Filtering), + // Chromatic aberration settings: + uniform_buffer::(true), + ), + ), + ); + + // Both source and chromatic aberration LUTs should be sampled + // bilinearly. + + let source_sampler = render_device.create_sampler(&SamplerDescriptor { + mipmap_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mag_filter: FilterMode::Linear, + ..default() + }); + + let chromatic_aberration_lut_sampler = render_device.create_sampler(&SamplerDescriptor { + mipmap_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mag_filter: FilterMode::Linear, + ..default() + }); + + PostProcessingPipeline { + bind_group_layout, + source_sampler, + chromatic_aberration_lut_sampler, + } + } +} + +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: fullscreen_vertex_shader::fullscreen_shader_vertex_state(), + fragment: Some(FragmentState { + shader: POST_PROCESSING_SHADER_HANDLE, + shader_defs: vec![], + entry_point: "fragment_main".into(), + targets: vec![Some(ColorTargetState { + format: key.texture_format, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + primitive: default(), + depth_stencil: None, + multisample: default(), + push_constant_ranges: vec![], + } + } +} + +impl ViewNode for PostProcessingNode { + type ViewQuery = ( + Read, + Read, + Read, + Read, + ); + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + (view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let post_processing_pipeline = world.resource::(); + let post_processing_uniform_buffers = world.resource::(); + let gpu_image_assets = world.resource::>(); + + // We need a render pipeline to be prepared. + let Some(pipeline) = pipeline_cache.get_render_pipeline(**pipeline_id) else { + return Ok(()); + }; + + // We need the chromatic aberration LUT to be present. + let Some(chromatic_aberration_lut) = gpu_image_assets.get(&chromatic_aberration.color_lut) + else { + return Ok(()); + }; + + // We need the postprocessing settings to be uploaded to the GPU. + let Some(chromatic_aberration_uniform_buffer_binding) = post_processing_uniform_buffers + .chromatic_aberration + .binding() + else { + return Ok(()); + }; + + // Use the [`PostProcessWrite`] infrastructure, since this is a + // full-screen pass. + let post_process = view_target.post_process_write(); + + let pass_descriptor = RenderPassDescriptor { + label: Some("postprocessing pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: post_process.destination, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let bind_group = render_context.render_device().create_bind_group( + Some("postprocessing bind group"), + &post_processing_pipeline.bind_group_layout, + &BindGroupEntries::sequential(( + post_process.source, + &post_processing_pipeline.source_sampler, + &chromatic_aberration_lut.texture_view, + &post_processing_pipeline.chromatic_aberration_lut_sampler, + chromatic_aberration_uniform_buffer_binding, + )), + ); + + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group(0, &bind_group, &[**post_processing_uniform_buffer_offsets]); + render_pass.draw(0..3, 0..1); + + Ok(()) + } +} + +/// Specializes the built-in postprocessing pipeline for each applicable view. +pub fn prepare_post_processing_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + post_processing_pipeline: Res, + views: Query<(Entity, &ExtractedView), With>, +) { + for (entity, view) in views.iter() { + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &post_processing_pipeline, + PostProcessingPipelineKey { + texture_format: if view.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + }, + ); + + commands + .entity(entity) + .insert(PostProcessingPipelineId(pipeline_id)); + } +} + +/// Gathers the built-in postprocessing settings for every view and uploads them +/// to the GPU. +pub fn prepare_post_processing_uniforms( + mut commands: Commands, + mut post_processing_uniform_buffers: ResMut, + render_device: Res, + render_queue: Res, + mut views: Query<(Entity, &ChromaticAberration)>, +) { + post_processing_uniform_buffers.clear(); + + // Gather up all the postprocessing settings. + for (view_entity, chromatic_aberration) in views.iter_mut() { + let chromatic_aberration_uniform_buffer_offset = + post_processing_uniform_buffers.push(&ChromaticAberrationUniform { + intensity: chromatic_aberration.intensity, + max_samples: chromatic_aberration.max_samples, + unused_1: 0, + unused_2: 0, + }); + commands + .entity(view_entity) + .insert(PostProcessingUniformBufferOffsets { + chromatic_aberration: chromatic_aberration_uniform_buffer_offset, + }); + } + + // Upload to the GPU. + post_processing_uniform_buffers.write_buffer(&render_device, &render_queue); +} + +impl ExtractComponent for ChromaticAberration { + type QueryData = Read; + + type QueryFilter = With; + + type Out = ChromaticAberration; + + fn extract_component( + chromatic_aberration: QueryItem<'_, Self::QueryData>, + ) -> Option { + // Skip the postprocessing phase entirely if the intensity is zero. + if chromatic_aberration.intensity > 0.0 { + Some(chromatic_aberration.clone()) + } else { + None + } + } +} diff --git a/crates/bevy_core_pipeline/src/post_process/post_process.wgsl b/crates/bevy_core_pipeline/src/post_process/post_process.wgsl new file mode 100644 index 0000000000..6251ba6688 --- /dev/null +++ b/crates/bevy_core_pipeline/src/post_process/post_process.wgsl @@ -0,0 +1,9 @@ +// Miscellaneous postprocessing effects, currently just chromatic aberration. + +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_core_pipeline::post_processing::chromatic_aberration::chromatic_aberration + +@fragment +fn fragment_main(in: FullscreenVertexOutput) -> @location(0) vec4 { + return vec4(chromatic_aberration(in.uv), 1.0); +} diff --git a/examples/3d/post_processing.rs b/examples/3d/post_processing.rs new file mode 100644 index 0000000000..cb582c81fe --- /dev/null +++ b/examples/3d/post_processing.rs @@ -0,0 +1,216 @@ +//! Demonstrates Bevy's built-in postprocessing features. +//! +//! Currently, this simply consists of chromatic aberration. + +use std::f32::consts::PI; + +use bevy::{ + core_pipeline::post_process::ChromaticAberration, pbr::CascadeShadowConfigBuilder, prelude::*, +}; + +/// The number of units per frame to add to or subtract from intensity when the +/// arrow keys are held. +const CHROMATIC_ABERRATION_INTENSITY_ADJUSTMENT_SPEED: f32 = 0.002; + +/// The maximum supported chromatic aberration intensity level. +const MAX_CHROMATIC_ABERRATION_INTENSITY: f32 = 0.4; + +/// The settings that the user can control. +#[derive(Resource)] +struct AppSettings { + /// The intensity of the chromatic aberration effect. + chromatic_aberration_intensity: f32, +} + +/// The entry point. +fn main() { + App::new() + .init_resource::() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Chromatic Aberration Example".into(), + ..default() + }), + ..default() + })) + .add_systems(Startup, setup) + .add_systems(Update, handle_keyboard_input) + .add_systems( + Update, + (update_chromatic_aberration_settings, update_help_text) + .run_if(resource_changed::) + .after(handle_keyboard_input), + ) + .run(); +} + +/// Creates the example scene and spawns the UI. +fn setup(mut commands: Commands, asset_server: Res, app_settings: Res) { + // Spawn the camera. + spawn_camera(&mut commands, &asset_server); + + // Create the scene. + spawn_scene(&mut commands, &asset_server); + + // Spawn the help text. + spawn_text(&mut commands, &app_settings); +} + +/// Spawns the camera, including the [`ChromaticAberration`] component. +fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) { + commands.spawn(( + Camera3dBundle { + camera: Camera { + hdr: true, + ..default() + }, + transform: Transform::from_xyz(0.7, 0.7, 1.0) + .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + ..default() + }, + FogSettings { + color: Color::srgb_u8(43, 44, 47), + falloff: FogFalloff::Linear { + start: 1.0, + end: 8.0, + }, + ..default() + }, + EnvironmentMapLight { + diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), + specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), + intensity: 2000.0, + }, + // Include the `ChromaticAberration` component. + ChromaticAberration::default(), + )); +} + +/// Spawns the scene. +/// +/// This is just the tonemapping test scene, chosen for the fact that it uses a +/// variety of colors. +fn spawn_scene(commands: &mut Commands, asset_server: &AssetServer) { + // Spawn the main scene. + commands.spawn(SceneBundle { + scene: asset_server.load( + GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"), + ), + ..default() + }); + + // Spawn the flight helmet. + commands.spawn(SceneBundle { + scene: asset_server + .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")), + transform: Transform::from_xyz(0.5, 0.0, -0.5) + .with_rotation(Quat::from_rotation_y(-0.15 * PI)), + ..default() + }); + + // Spawn the light. + commands.spawn(DirectionalLightBundle { + directional_light: DirectionalLight { + illuminance: 15000.0, + shadows_enabled: true, + ..default() + }, + transform: Transform::from_rotation(Quat::from_euler( + EulerRot::ZYX, + 0.0, + PI * -0.15, + PI * -0.15, + )), + cascade_shadow_config: CascadeShadowConfigBuilder { + maximum_distance: 3.0, + first_cascade_far_bound: 0.9, + ..default() + } + .into(), + ..default() + }); +} + +/// Spawns the help text at the bottom of the screen. +fn spawn_text(commands: &mut Commands, app_settings: &AppSettings) { + commands.spawn( + TextBundle { + text: create_help_text(app_settings), + ..default() + } + .with_style(Style { + position_type: PositionType::Absolute, + bottom: Val::Px(12.0), + left: Val::Px(12.0), + ..default() + }), + ); +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + chromatic_aberration_intensity: ChromaticAberration::default().intensity, + } + } +} + +/// Creates help text at the bottom of the screen. +fn create_help_text(app_settings: &AppSettings) -> Text { + Text::from_section( + format!( + "Chromatic aberration intensity: {} (Press Left or Right to change)", + app_settings.chromatic_aberration_intensity + ), + TextStyle::default(), + ) +} + +/// Handles requests from the user to change the chromatic aberration intensity. +fn handle_keyboard_input(mut app_settings: ResMut, input: Res>) { + let mut delta = 0.0; + if input.pressed(KeyCode::ArrowLeft) { + delta -= CHROMATIC_ABERRATION_INTENSITY_ADJUSTMENT_SPEED; + } else if input.pressed(KeyCode::ArrowRight) { + delta += CHROMATIC_ABERRATION_INTENSITY_ADJUSTMENT_SPEED; + } + + // If no arrow key was pressed, just bail out. + if delta == 0.0 { + return; + } + + app_settings.chromatic_aberration_intensity = (app_settings.chromatic_aberration_intensity + + delta) + .clamp(0.0, MAX_CHROMATIC_ABERRATION_INTENSITY); +} + +/// Updates the [`ChromaticAberration`] settings per the [`AppSettings`]. +fn update_chromatic_aberration_settings( + mut chromatic_aberration_settings: Query<&mut ChromaticAberration>, + app_settings: Res, +) { + let intensity = app_settings.chromatic_aberration_intensity; + + // Pick a reasonable maximum sample size for the intensity to avoid an + // artifact whereby the individual samples appear instead of producing + // smooth streaks of color. + // + // Don't take this formula too seriously; it hasn't been heavily tuned. + let max_samples = ((intensity - 0.02) / (0.20 - 0.02) * 56.0 + 8.0) + .clamp(8.0, 64.0) + .round() as u32; + + for mut chromatic_aberration_settings in &mut chromatic_aberration_settings { + chromatic_aberration_settings.intensity = intensity; + chromatic_aberration_settings.max_samples = max_samples; + } +} + +/// Updates the help text at the bottom of the screen to reflect the current +/// [`AppSettings`]. +fn update_help_text(mut text: Query<&mut Text>, app_settings: Res) { + for mut text in text.iter_mut() { + *text = create_help_text(&app_settings); + } +} diff --git a/examples/README.md b/examples/README.md index b83ea77123..4e5985f87b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -135,6 +135,7 @@ Example | Description [Atmospheric Fog](../examples/3d/atmospheric_fog.rs) | A scene showcasing the atmospheric fog effect [Auto Exposure](../examples/3d/auto_exposure.rs) | A scene showcasing auto exposure [Blend Modes](../examples/3d/blend_modes.rs) | Showcases different blend modes +[Built-in postprocessing](../examples/3d/post_processing.rs) | Demonstrates the built-in postprocessing features [Clearcoat](../examples/3d/clearcoat.rs) | Demonstrates the clearcoat PBR feature [Color grading](../examples/3d/color_grading.rs) | Demonstrates color grading [Deferred Rendering](../examples/3d/deferred_rendering.rs) | Renders meshes with both forward and deferred pipelines @@ -386,7 +387,7 @@ Example | Description [Material - GLSL](../examples/shader/shader_material_glsl.rs) | A shader that uses the GLSL shading language [Material - Screenspace Texture](../examples/shader/shader_material_screenspace_texture.rs) | A shader that samples a texture with view-independent UV coordinates [Material Prepass](../examples/shader/shader_prepass.rs) | A shader that uses the various textures generated by the prepass -[Post Processing - Custom Render Pass](../examples/shader/post_processing.rs) | A custom post processing effect, using a custom render pass that runs after the main pass +[Post Processing - Custom Render Pass](../examples/shader/custom_post_processing.rs) | A custom post processing effect, using a custom render pass that runs after the main pass [Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader) [Texture Binding Array (Bindless Textures)](../examples/shader/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures). diff --git a/examples/shader/post_processing.rs b/examples/shader/custom_post_processing.rs similarity index 100% rename from examples/shader/post_processing.rs rename to examples/shader/custom_post_processing.rs