Start a built-in postprocessing stack, and implement chromatic aberration in it. (#13695)

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.
This commit is contained in:
Patrick Walton 2024-07-15 06:59:02 -07:00 committed by GitHub
parent ed2b8e0f35
commit fcda67e894
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 843 additions and 4 deletions

View file

@ -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"

View file

@ -18,6 +18,7 @@ pub mod graph {
MainTransparentPass,
EndMainPass,
Bloom,
PostProcessing,
Tonemapping,
Fxaa,
Smaa,

View file

@ -30,6 +30,7 @@ pub mod graph {
Bloom,
AutoExposure,
DepthOfField,
PostProcessing,
Tonemapping,
Fxaa,
Smaa,

View file

@ -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,
));
}
}

View file

@ -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<f32>;
// 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<f32>;
// 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<uniform> chromatic_aberration_settings: ChromaticAberrationSettings;
fn chromatic_aberration(start_pos: vec2<f32>) -> vec3<f32> {
// Radial chromatic aberration implemented using the *Inside* technique:
//
// <https://github.com/playdeadgames/publications/blob/master/INSIDE/rendering_inside_gdc2016.pdf>
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<f32>(textureDimensions(chromatic_aberration_source_texture)));
let sample_count = min(u32(ceil(texel_length)), chromatic_aberration_settings.max_samples);
var color: vec3<f32>;
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;
}

View file

@ -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<Shader> = Handle::weak_from_u128(14675654334038973533);
/// The handle to the chromatic aberration shader `chromatic_aberration.wgsl`.
const CHROMATIC_ABERRATION_SHADER_HANDLE: Handle<Shader> =
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<Image> =
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<Image>,
/// 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<ChromaticAberrationUniform>,
}
/// 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<_>>();
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::<ChromaticAberration>();
app.add_plugins(ExtractComponentPlugin::<ChromaticAberration>::default());
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedRenderPipelines<PostProcessingPipeline>>()
.init_resource::<PostProcessingUniformBuffers>()
.add_systems(
Render,
(
prepare_post_processing_pipelines,
prepare_post_processing_uniforms,
)
.in_set(RenderSet::Prepare),
)
.add_render_graph_node::<ViewNodeRunner<PostProcessingNode>>(
Core3d,
Node3d::PostProcessing,
)
.add_render_graph_edges(
Core3d,
(
Node3d::DepthOfField,
Node3d::PostProcessing,
Node3d::Tonemapping,
),
)
.add_render_graph_node::<ViewNodeRunner<PostProcessingNode>>(
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::<PostProcessingPipeline>();
}
}
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::<RenderDevice>();
// 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::<ChromaticAberrationUniform>(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<ViewTarget>,
Read<PostProcessingPipelineId>,
Read<ChromaticAberration>,
Read<PostProcessingUniformBufferOffsets>,
);
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::<PipelineCache>();
let post_processing_pipeline = world.resource::<PostProcessingPipeline>();
let post_processing_uniform_buffers = world.resource::<PostProcessingUniformBuffers>();
let gpu_image_assets = world.resource::<RenderAssets<GpuImage>>();
// 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<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<PostProcessingPipeline>>,
post_processing_pipeline: Res<PostProcessingPipeline>,
views: Query<(Entity, &ExtractedView), With<ChromaticAberration>>,
) {
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<PostProcessingUniformBuffers>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
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<ChromaticAberration>;
type QueryFilter = With<Camera>;
type Out = ChromaticAberration;
fn extract_component(
chromatic_aberration: QueryItem<'_, Self::QueryData>,
) -> Option<Self::Out> {
// Skip the postprocessing phase entirely if the intensity is zero.
if chromatic_aberration.intensity > 0.0 {
Some(chromatic_aberration.clone())
} else {
None
}
}
}

View file

@ -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<f32> {
return vec4(chromatic_aberration(in.uv), 1.0);
}

View file

@ -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::<AppSettings>()
.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::<AppSettings>)
.after(handle_keyboard_input),
)
.run();
}
/// Creates the example scene and spawns the UI.
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, app_settings: Res<AppSettings>) {
// 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<AppSettings>, input: Res<ButtonInput<KeyCode>>) {
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<AppSettings>,
) {
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<AppSettings>) {
for mut text in text.iter_mut() {
*text = create_help_text(&app_settings);
}
}

View file

@ -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).