use bevy::{
core_pipeline::{
core_2d::graph::{Core2d, Node2d},
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
},
ecs::query::QueryItem,
prelude::*,
render::{
extract_component::{
ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin,
},
render_graph::{
NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel, ViewNode, ViewNodeRunner,
},
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId,
ColorTargetState, ColorWrites, FragmentState, MultisampleState, Operations,
PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor,
RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages,
ShaderType, TextureFormat, TextureSampleType,
},
renderer::{RenderContext, RenderDevice},
texture::BevyDefault,
view::ViewTarget,
RenderApp,
},
};
pub struct ChromaticAberrationPlugin;
impl Plugin for ChromaticAberrationPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
ExtractComponentPlugin::<ChromaticAberattionSettings>::default(),
UniformComponentPlugin::<ChromaticAberattionSettings>::default(),
));
// Get render app
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.add_render_graph_node::<ViewNodeRunner<ChromaticAberrationNode>>(
Core2d,
ChromaticAberrationLabel,
)
.add_render_graph_edges(
Core2d,
(
Node2d::Tonemapping,
ChromaticAberrationLabel,
Node2d::EndMainPassPostProcessing,
),
);
}
fn finish(&self, app: &mut App) {
// We need to get the render app from the main app
let Ok(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
// Initialize the pipeline
.init_resource::<ChromaticAberrationPipeline>();
}
}
#[derive(Component, Clone, Copy, ExtractComponent, ShaderType)]
pub struct ChromaticAberattionSettings {
pub intensity: f32,
// NOTE all offsets are currently normalized by the shader.
// so they should avoid being 0? a non-zero unit vector should do fine.
pub red_offset: Vec2,
pub green_offset: Vec2,
pub blue_offset: Vec2,
// WebGL2 must be 16-byte aligned
pub _webgl_padding: f32,
}
impl Default for ChromaticAberattionSettings {
fn default() -> Self {
Self {
intensity: default(),
red_offset: Vec2::NEG_Y + Vec2::X,
green_offset: Vec2::NEG_X,
blue_offset: Vec2::Y,
_webgl_padding: default(),
}
}
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
struct ChromaticAberrationLabel;
// The post process node used for the render graph
#[derive(Default)]
struct ChromaticAberrationNode;
impl ViewNode for ChromaticAberrationNode {
type ViewQuery = (&'static ViewTarget, &'static ChromaticAberattionSettings);
fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(view_target, _settings): QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let chromatic_aberration_pipeline = world.resource::<ChromaticAberrationPipeline>();
let pipeline_cache = world.resource::<PipelineCache>();
let Some(pipeline) =
pipeline_cache.get_render_pipeline(chromatic_aberration_pipeline.pipeline_id)
else {
return Ok(());
};
let settings_uniform = world.resource::<ComponentUniforms<ChromaticAberattionSettings>>();
let Some(settings_binding) = settings_uniform.uniforms().binding() else {
return Ok(());
};
// Do the post processing
let post_process = view_target.post_process_write();
// The bind_group gets created each frame.
//
// Normally, you would create a bind_group in the Queue set,
// but this doesn't work with the post_process_write().
// The reason it doesn't work is because each post_process_write will alternate the source/destination.
// The only way to have the correct source/destination for the bind_group
// is to make sure you get it during the node execution.
let bind_group = render_context.render_device().create_bind_group(
"chromatic_aberration_bind_group",
&chromatic_aberration_pipeline.layout,
// It's important for this to match the BindGroupLayout defined in the PostProcessPipeline
&BindGroupEntries::sequential((
// Make sure to use the source view
post_process.source,
// Use the sampler created for the pipeline
&chromatic_aberration_pipeline.sampler,
// Set the settings binding
settings_binding.clone(),
)),
);
// Begin the render pass
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("post_process_pass"),
color_attachments: &[Some(RenderPassColorAttachment {
// We need to specify the post process destination view here
// to make sure we write to the appropriate texture.
view: post_process.destination,
resolve_target: None,
ops: Operations::default(),
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
// This is mostly just wgpu boilerplate for drawing a fullscreen triangle,
// using the pipeline/bind_group created above
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(0, &bind_group, &[]);
render_pass.draw(0..3, 0..1);
Ok(())
}
}
// This contains global data used by the render pipeline. This will be created once on startup.
#[derive(Resource)]
struct ChromaticAberrationPipeline {
layout: BindGroupLayout,
sampler: Sampler,
pipeline_id: CachedRenderPipelineId,
}
impl FromWorld for ChromaticAberrationPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let layout = render_device.create_bind_group_layout(
"chromatic_aberration_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
// The screen texture
texture_2d(TextureSampleType::Float { filterable: true }),
// The sampler that will be used to sample the screen texture
sampler(SamplerBindingType::Filtering),
// The settings uniform that will control the effect
uniform_buffer::<ChromaticAberattionSettings>(false),
),
),
);
// We can create the sampler here since it won't change at runtime and doesn't depend on the view
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
// Get the shader handle
let shader = world
.resource::<AssetServer>()
.load("shaders/chromatic_aberration.wgsl");
let pipeline_id = world
.resource_mut::<PipelineCache>()
// This will add the pipeline to the cache and queue it's creation
.queue_render_pipeline(RenderPipelineDescriptor {
label: Some("chromatic_aberration_pipeline".into()),
layout: vec![layout.clone()],
// This will setup a fullscreen triangle for the vertex state
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader,
shader_defs: vec![],
// Make sure this matches the entry point of your shader.
// It can be anything as long as it matches here and in the shader.
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: TextureFormat::bevy_default(),
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
// All of the following properties are not important for this effect so just use the default values.
// This struct doesn't have the Default trait implemented because not all field can have a default value.
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: vec![],
});
Self {
layout,
sampler,
pipeline_id,
}
}
}