use bevy::{
core_pipeline::{
core_3d::graph::{Core3d, Node3d},
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
},
ecs::query::QueryItem,
prelude::*,
render::{
extract_component::{
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
UniformComponentPlugin,
},
render_graph::{
NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel, ViewNode, ViewNodeRunner,
},
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
*,
},
renderer::{RenderContext, RenderDevice},
view::ViewTarget,
RenderApp,
},
};
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_render_graph_node::<ViewNodeRunner<PostProcessNode>>(
Core3d,
PostProcessLabel,
)
.add_render_graph_edges(
Core3d,
(
Node3d::Tonemapping,
PostProcessLabel,
Node3d::EndMainPassPostProcessing,
),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<PostProcessPipeline>();
}
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
struct PostProcessLabel;
#[derive(Default)]
struct PostProcessNode;
impl ViewNode for PostProcessNode {
type ViewQuery = (
&'static ViewTarget,
&'static PostProcessSettings,
&'static DynamicUniformIndex<PostProcessSettings>,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(view_target, _post_process_settings, settings_index): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let post_process_pipeline = world.resource::<PostProcessPipeline>();
let pipeline_cache = world.resource::<PipelineCache>();
let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id)
else {
return Ok(());
};
let settings_uniforms = world.resource::<ComponentUniforms<PostProcessSettings>>();
let Some(settings_binding) = settings_uniforms.uniforms().binding() else {
return Ok(());
};
let post_process = view_target.post_process_write();
let bind_group = render_context.render_device().create_bind_group(
"post_process_bind_group",
&post_process_pipeline.layout,
&BindGroupEntries::sequential((
post_process.source,
&post_process_pipeline.sampler,
settings_binding.clone(),
)),
);
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("post_process_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,
});
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(0, &bind_group, &[settings_index.index()]);
render_pass.draw(0..3, 0..1);
Ok(())
}
}
#[derive(Resource)]
struct PostProcessPipeline {
layout: BindGroupLayout,
sampler: Sampler,
pipeline_id: CachedRenderPipelineId,
}
impl FromWorld for PostProcessPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let layout = render_device.create_bind_group_layout(
"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 = world.load_asset(SHADER_ASSET_PATH);
let pipeline_id = world
.resource_mut::<PipelineCache>()
.queue_render_pipeline(RenderPipelineDescriptor {
label: Some("post_process_pipeline".into()),
layout: vec![layout.clone()],
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader,
shader_defs: vec![],
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: TextureFormat::bevy_default(),
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
});
Self {
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;
}
}