use bevy::{
core_pipeline::{schedule::Core3d, Core3dSystems, FullscreenShader},
prelude::*,
render::{
extract_component::{
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
UniformComponentPlugin,
},
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
*,
},
renderer::{RenderContext, RenderDevice, ViewQuery},
view::ViewTarget,
RenderApp, RenderStartup,
},
};
const SHADER_ASSET_PATH: &str = "shaders/post_processing.wgsl";
fn main() {
App::new()
.add_plugins((DefaultPlugins, PostProcessPlugin))
.add_systems(Startup, setup)
.add_systems(Update, (rotate, update_settings))
.run();
}
struct PostProcessPlugin;
impl Plugin for PostProcessPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
ExtractComponentPlugin::<PostProcessSettings>::default(),
UniformComponentPlugin::<PostProcessSettings>::default(),
));
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.add_systems(RenderStartup, init_post_process_pipeline);
render_app.add_systems(
Core3d,
post_process_system.in_set(Core3dSystems::PostProcess),
);
}
}
#[derive(Default)]
struct PostProcessBindGroupCache {
cached: Option<(TextureViewId, BindGroup)>,
}
fn post_process_system(
view: ViewQuery<(
&ViewTarget,
&PostProcessSettings,
&DynamicUniformIndex<PostProcessSettings>,
)>,
post_process_pipeline: Option<Res<PostProcessPipeline>>,
pipeline_cache: Res<PipelineCache>,
settings_uniforms: Res<ComponentUniforms<PostProcessSettings>>,
mut cache: Local<PostProcessBindGroupCache>,
mut ctx: RenderContext,
) {
let Some(post_process_pipeline) = post_process_pipeline else {
return;
};
let (view_target, _post_process_settings, settings_index) = view.into_inner();
let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id)
else {
return;
};
let Some(settings_binding) = settings_uniforms.uniforms().binding() else {
return;
};
let post_process = view_target.post_process_write();
let bind_group = match &mut cache.cached {
Some((texture_id, bind_group)) if post_process.source.id() == *texture_id => bind_group,
cached => {
let bind_group = ctx.render_device().create_bind_group(
"post_process_bind_group",
&pipeline_cache.get_bind_group_layout(&post_process_pipeline.layout),
&BindGroupEntries::sequential((
post_process.source,
&post_process_pipeline.sampler,
settings_binding.clone(),
)),
);
let (_, bind_group) = cached.insert((post_process.source.id(), bind_group));
bind_group
}
};
let mut render_pass = ctx
.command_encoder()
.begin_render_pass(&RenderPassDescriptor {
label: Some("post_process_pass"),
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,
multiview_mask: None,
});
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, bind_group, &[settings_index.index()]);
render_pass.draw(0..3, 0..1);
}
#[derive(Resource)]
struct PostProcessPipeline {
layout: BindGroupLayoutDescriptor,
sampler: Sampler,
pipeline_id: CachedRenderPipelineId,
}
fn init_post_process_pipeline(
mut commands: Commands,
render_device: Res<RenderDevice>,
asset_server: Res<AssetServer>,
fullscreen_shader: Res<FullscreenShader>,
pipeline_cache: Res<PipelineCache>,
) {
let layout = BindGroupLayoutDescriptor::new(
"post_process_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
uniform_buffer::<PostProcessSettings>(true),
),
),
);
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
let shader = asset_server.load(SHADER_ASSET_PATH);
let vertex_state = fullscreen_shader.to_vertex_state();
let pipeline_id = pipeline_cache
.queue_render_pipeline(RenderPipelineDescriptor {
label: Some("post_process_pipeline".into()),
layout: vec![layout.clone()],
vertex: vertex_state,
fragment: Some(FragmentState {
shader,
targets: vec![Some(ColorTargetState {
format: TextureFormat::Rgba8UnormSrgb,
blend: None,
write_mask: ColorWrites::ALL,
})],
..default()
}),
..default()
});
commands.insert_resource(PostProcessPipeline {
layout,
sampler,
pipeline_id,
});
}
#[derive(Component, Default, Clone, Copy, ExtractComponent, ShaderType)]
struct PostProcessSettings {
intensity: f32,
#[cfg(feature = "webgl2")]
_webgl2_padding: Vec3,
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
Camera3d::default(),
Transform::from_translation(Vec3::new(0.0, 0.0, 5.0)).looking_at(Vec3::default(), Vec3::Y),
Camera {
clear_color: Color::WHITE.into(),
..default()
},
PostProcessSettings {
intensity: 0.02,
..default()
},
));
commands.spawn((
Mesh3d(meshes.add(Cuboid::default())),
MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
Transform::from_xyz(0.0, 0.5, 0.0),
Rotates,
));
commands.spawn(DirectionalLight {
illuminance: 1_000.,
..default()
});
}
#[derive(Component)]
struct Rotates;
fn rotate(time: Res<Time>, mut query: Query<&mut Transform, With<Rotates>>) {
for mut transform in &mut query {
transform.rotate_x(0.55 * time.delta_secs());
transform.rotate_z(0.15 * time.delta_secs());
}
}
fn update_settings(mut settings: Query<&mut PostProcessSettings>, time: Res<Time>) {
for mut setting in &mut settings {
let mut intensity = ops::sin(time.elapsed_secs());
intensity = ops::sin(intensity);
intensity = intensity * 0.5 + 0.5;
intensity *= 0.015;
setting.intensity = intensity;
}
}