use bevy_app::{App, Plugin};
use bevy_asset::{
embedded_asset, load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages,
};
use bevy_camera::Camera;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{QueryItem, With},
reflect::ReflectComponent,
resource::Resource,
schedule::IntoScheduleConfigs as _,
system::{lifetimeless::Read, Commands, Query, Res, ResMut},
world::World,
};
use bevy_image::{BevyDefault, Image};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
diagnostic::RecordDiagnostics,
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_asset::RenderAssets,
render_graph::{
NodeRunError, RenderGraphContext, RenderGraphExt as _, ViewNode, ViewNodeRunner,
},
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries,
CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d,
FilterMode, FragmentState, Operations, PipelineCache, RenderPassColorAttachment,
RenderPassDescriptor, RenderPipelineDescriptor, Sampler, SamplerBindingType,
SamplerDescriptor, ShaderStages, ShaderType, SpecializedRenderPipeline,
SpecializedRenderPipelines, TextureDimension, TextureFormat, TextureSampleType,
},
renderer::{RenderContext, RenderDevice, RenderQueue},
texture::GpuImage,
view::{ExtractedView, ViewTarget},
Render, RenderApp, RenderStartup, RenderSystems,
};
use bevy_shader::{load_shader_library, Shader};
use bevy_utils::prelude::default;
use bevy_core_pipeline::{
core_2d::graph::{Core2d, Node2d},
core_3d::graph::{Core3d, Node3d},
FullscreenShader,
};
const DEFAULT_CHROMATIC_ABERRATION_INTENSITY: f32 = 0.02;
const DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES: u32 = 8;
static DEFAULT_CHROMATIC_ABERRATION_LUT_DATA: [u8; 12] =
[255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255];
#[derive(Resource)]
struct DefaultChromaticAberrationLut(Handle<Image>);
#[derive(Default)]
pub struct EffectStackPlugin;
#[derive(Reflect, Component, Clone)]
#[reflect(Component, Default, Clone)]
pub struct ChromaticAberration {
pub color_lut: Option<Handle<Image>>,
pub intensity: f32,
pub max_samples: u32,
}
#[derive(Resource)]
pub struct PostProcessingPipeline {
bind_group_layout: BindGroupLayoutDescriptor,
source_sampler: Sampler,
chromatic_aberration_lut_sampler: Sampler,
fullscreen_shader: FullscreenShader,
fragment_shader: Handle<Shader>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct PostProcessingPipelineKey {
texture_format: TextureFormat,
}
#[derive(Component, Deref, DerefMut)]
pub struct PostProcessingPipelineId(CachedRenderPipelineId);
#[derive(ShaderType)]
pub struct ChromaticAberrationUniform {
intensity: f32,
max_samples: u32,
unused_1: u32,
unused_2: u32,
}
#[derive(Resource, Deref, DerefMut, Default)]
pub struct PostProcessingUniformBuffers {
chromatic_aberration: DynamicUniformBuffer<ChromaticAberrationUniform>,
}
#[derive(Component, Deref, DerefMut)]
pub struct PostProcessingUniformBufferOffsets {
chromatic_aberration: u32,
}
#[derive(Default)]
pub struct PostProcessingNode;
impl Plugin for EffectStackPlugin {
fn build(&self, app: &mut App) {
load_shader_library!(app, "chromatic_aberration.wgsl");
embedded_asset!(app, "post_process.wgsl");
let mut assets = app.world_mut().resource_mut::<Assets<_>>();
let default_lut = assets.add(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.add_plugins(ExtractComponentPlugin::<ChromaticAberration>::default());
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.insert_resource(DefaultChromaticAberrationLut(default_lut))
.init_resource::<SpecializedRenderPipelines<PostProcessingPipeline>>()
.init_resource::<PostProcessingUniformBuffers>()
.add_systems(RenderStartup, init_post_processing_pipeline)
.add_systems(
Render,
(
prepare_post_processing_pipelines,
prepare_post_processing_uniforms,
)
.in_set(RenderSystems::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),
);
}
}
impl Default for ChromaticAberration {
fn default() -> Self {
Self {
color_lut: None,
intensity: DEFAULT_CHROMATIC_ABERRATION_INTENSITY,
max_samples: DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES,
}
}
}
pub fn init_post_processing_pipeline(
mut commands: Commands,
render_device: Res<RenderDevice>,
fullscreen_shader: Res<FullscreenShader>,
asset_server: Res<AssetServer>,
) {
let bind_group_layout = BindGroupLayoutDescriptor::new(
"postprocessing bind group layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
uniform_buffer::<ChromaticAberrationUniform>(true),
),
),
);
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()
});
commands.insert_resource(PostProcessingPipeline {
bind_group_layout,
source_sampler,
chromatic_aberration_lut_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.texture_format,
blend: None,
write_mask: ColorWrites::ALL,
})],
..default()
}),
..default()
}
}
}
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>>();
let default_lut = world.resource::<DefaultChromaticAberrationLut>();
let Some(pipeline) = pipeline_cache.get_render_pipeline(**pipeline_id) else {
return Ok(());
};
let Some(chromatic_aberration_lut) = gpu_image_assets.get(
chromatic_aberration
.color_lut
.as_ref()
.unwrap_or(&default_lut.0),
) else {
return Ok(());
};
let Some(chromatic_aberration_uniform_buffer_binding) = post_processing_uniform_buffers
.chromatic_aberration
.binding()
else {
return Ok(());
};
let diagnostics = render_context.diagnostic_recorder();
let post_process = view_target.post_process_write();
let pass_descriptor = RenderPassDescriptor {
label: Some("postprocessing"),
color_attachments: &[Some(RenderPassColorAttachment {
view: post_process.destination,
depth_slice: None,
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"),
&pipeline_cache.get_bind_group_layout(&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);
let pass_span = diagnostics.pass_span(&mut render_pass, "postprocessing");
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);
pass_span.end(&mut render_pass);
Ok(())
}
}
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));
}
}
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();
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,
});
}
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> {
if chromatic_aberration.intensity > 0.0 {
Some(chromatic_aberration.clone())
} else {
None
}
}
}