|
| 1 | +//! A custom post processing effect, using two cameras, with one reusing the render texture of the first one. |
| 2 | +//! Here a chromatic aberration is applied to a 3d scene containting a rotating cube. |
| 3 | +//! This example is useful to implement your own post-processing effect such as |
| 4 | +//! edge detection, blur, pixelization, vignette... and countless others. |
| 5 | +
|
| 6 | +use bevy::{ |
| 7 | + core_pipeline::clear_color::ClearColorConfig, |
| 8 | + ecs::system::{lifetimeless::SRes, SystemParamItem}, |
| 9 | + prelude::*, |
| 10 | + reflect::TypeUuid, |
| 11 | + render::{ |
| 12 | + camera::{Camera, RenderTarget}, |
| 13 | + render_asset::{PrepareAssetError, RenderAsset, RenderAssets}, |
| 14 | + render_resource::{ |
| 15 | + BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, |
| 16 | + BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, |
| 17 | + Extent3d, SamplerBindingType, ShaderStages, TextureDescriptor, TextureDimension, |
| 18 | + TextureFormat, TextureSampleType, TextureUsages, TextureViewDimension, |
| 19 | + }, |
| 20 | + renderer::RenderDevice, |
| 21 | + view::RenderLayers, |
| 22 | + }, |
| 23 | + sprite::{Material2d, Material2dPipeline, Material2dPlugin, MaterialMesh2dBundle}, |
| 24 | +}; |
| 25 | + |
| 26 | +fn main() { |
| 27 | + let mut app = App::new(); |
| 28 | + app.add_plugins(DefaultPlugins) |
| 29 | + .add_plugin(Material2dPlugin::<PostProcessingMaterial>::default()) |
| 30 | + .add_startup_system(setup) |
| 31 | + .add_system(main_camera_cube_rotator_system); |
| 32 | + |
| 33 | + app.run(); |
| 34 | +} |
| 35 | + |
| 36 | +/// Marks the first camera cube (rendered to a texture.) |
| 37 | +#[derive(Component)] |
| 38 | +struct MainCube; |
| 39 | + |
| 40 | +fn setup( |
| 41 | + mut commands: Commands, |
| 42 | + mut windows: ResMut<Windows>, |
| 43 | + mut meshes: ResMut<Assets<Mesh>>, |
| 44 | + mut post_processing_materials: ResMut<Assets<PostProcessingMaterial>>, |
| 45 | + mut materials: ResMut<Assets<StandardMaterial>>, |
| 46 | + mut images: ResMut<Assets<Image>>, |
| 47 | +) { |
| 48 | + let window = windows.get_primary_mut().unwrap(); |
| 49 | + let size = Extent3d { |
| 50 | + width: window.physical_width(), |
| 51 | + height: window.physical_height(), |
| 52 | + ..default() |
| 53 | + }; |
| 54 | + |
| 55 | + // This is the texture that will be rendered to. |
| 56 | + let mut image = Image { |
| 57 | + texture_descriptor: TextureDescriptor { |
| 58 | + label: None, |
| 59 | + size, |
| 60 | + dimension: TextureDimension::D2, |
| 61 | + format: TextureFormat::Bgra8UnormSrgb, |
| 62 | + mip_level_count: 1, |
| 63 | + sample_count: 1, |
| 64 | + usage: TextureUsages::TEXTURE_BINDING |
| 65 | + | TextureUsages::COPY_DST |
| 66 | + | TextureUsages::RENDER_ATTACHMENT, |
| 67 | + }, |
| 68 | + ..default() |
| 69 | + }; |
| 70 | + |
| 71 | + // fill image.data with zeroes |
| 72 | + image.resize(size); |
| 73 | + |
| 74 | + let image_handle = images.add(image); |
| 75 | + |
| 76 | + let cube_handle = meshes.add(Mesh::from(shape::Cube { size: 4.0 })); |
| 77 | + let cube_material_handle = materials.add(StandardMaterial { |
| 78 | + base_color: Color::rgb(0.8, 0.7, 0.6), |
| 79 | + reflectance: 0.02, |
| 80 | + unlit: false, |
| 81 | + ..default() |
| 82 | + }); |
| 83 | + |
| 84 | + // The cube that will be rendered to the texture. |
| 85 | + commands |
| 86 | + .spawn_bundle(PbrBundle { |
| 87 | + mesh: cube_handle, |
| 88 | + material: cube_material_handle, |
| 89 | + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 1.0)), |
| 90 | + ..default() |
| 91 | + }) |
| 92 | + .insert(MainCube); |
| 93 | + |
| 94 | + // Light |
| 95 | + // NOTE: Currently lights are ignoring render layers - see https://github.com/bevyengine/bevy/issues/3462 |
| 96 | + commands.spawn_bundle(PointLightBundle { |
| 97 | + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)), |
| 98 | + ..default() |
| 99 | + }); |
| 100 | + |
| 101 | + // Main camera, first to render |
| 102 | + commands.spawn_bundle(Camera3dBundle { |
| 103 | + camera_3d: Camera3d { |
| 104 | + clear_color: ClearColorConfig::Custom(Color::WHITE), |
| 105 | + }, |
| 106 | + camera: Camera { |
| 107 | + target: RenderTarget::Image(image_handle.clone()), |
| 108 | + ..default() |
| 109 | + }, |
| 110 | + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0)) |
| 111 | + .looking_at(Vec3::default(), Vec3::Y), |
| 112 | + ..default() |
| 113 | + }); |
| 114 | + |
| 115 | + // This specifies the layer used for the post processing camera, which will be attached to the post processing camera and 2d quad. |
| 116 | + let post_processing_pass_layer = RenderLayers::layer((RenderLayers::TOTAL_LAYERS - 1) as u8); |
| 117 | + |
| 118 | + let quad_handle = meshes.add(Mesh::from(shape::Quad::new(Vec2::new( |
| 119 | + size.width as f32, |
| 120 | + size.height as f32, |
| 121 | + )))); |
| 122 | + |
| 123 | + // This material has the texture that has been rendered. |
| 124 | + let material_handle = post_processing_materials.add(PostProcessingMaterial { |
| 125 | + source_image: image_handle, |
| 126 | + }); |
| 127 | + |
| 128 | + // Post processing 2d quad, with material using the render texture done by the main camera, with a custom shader. |
| 129 | + commands |
| 130 | + .spawn_bundle(MaterialMesh2dBundle { |
| 131 | + mesh: quad_handle.into(), |
| 132 | + material: material_handle, |
| 133 | + transform: Transform { |
| 134 | + translation: Vec3::new(0.0, 0.0, 1.5), |
| 135 | + ..default() |
| 136 | + }, |
| 137 | + ..default() |
| 138 | + }) |
| 139 | + .insert(post_processing_pass_layer); |
| 140 | + |
| 141 | + // The post-processing pass camera. |
| 142 | + commands |
| 143 | + .spawn_bundle(Camera2dBundle { |
| 144 | + camera: Camera { |
| 145 | + // renders after the first main camera which has default value: 0. |
| 146 | + priority: 1, |
| 147 | + ..default() |
| 148 | + }, |
| 149 | + ..Camera2dBundle::default() |
| 150 | + }) |
| 151 | + .insert(post_processing_pass_layer); |
| 152 | +} |
| 153 | + |
| 154 | +/// Rotates the cube rendered by the main camera |
| 155 | +fn main_camera_cube_rotator_system( |
| 156 | + time: Res<Time>, |
| 157 | + mut query: Query<&mut Transform, With<MainCube>>, |
| 158 | +) { |
| 159 | + for mut transform in query.iter_mut() { |
| 160 | + transform.rotation *= Quat::from_rotation_x(0.55 * time.delta_seconds()); |
| 161 | + transform.rotation *= Quat::from_rotation_z(0.15 * time.delta_seconds()); |
| 162 | + } |
| 163 | +} |
| 164 | + |
| 165 | +// Region below declares of the custom material handling post processing effect |
| 166 | + |
| 167 | +/// Our custom post processing material |
| 168 | +#[derive(TypeUuid, Clone)] |
| 169 | +#[uuid = "bc2f08eb-a0fb-43f1-a908-54871ea597d5"] |
| 170 | +struct PostProcessingMaterial { |
| 171 | + /// In this example, this image will be the result of the main camera. |
| 172 | + source_image: Handle<Image>, |
| 173 | +} |
| 174 | + |
| 175 | +struct PostProcessingMaterialGPU { |
| 176 | + bind_group: BindGroup, |
| 177 | +} |
| 178 | + |
| 179 | +impl Material2d for PostProcessingMaterial { |
| 180 | + fn bind_group(material: &PostProcessingMaterialGPU) -> &BindGroup { |
| 181 | + &material.bind_group |
| 182 | + } |
| 183 | + |
| 184 | + fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout { |
| 185 | + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { |
| 186 | + label: None, |
| 187 | + entries: &[ |
| 188 | + BindGroupLayoutEntry { |
| 189 | + binding: 0, |
| 190 | + visibility: ShaderStages::FRAGMENT, |
| 191 | + ty: BindingType::Texture { |
| 192 | + multisampled: false, |
| 193 | + view_dimension: TextureViewDimension::D2, |
| 194 | + sample_type: TextureSampleType::Float { filterable: true }, |
| 195 | + }, |
| 196 | + count: None, |
| 197 | + }, |
| 198 | + BindGroupLayoutEntry { |
| 199 | + binding: 1, |
| 200 | + visibility: ShaderStages::FRAGMENT, |
| 201 | + ty: BindingType::Sampler(SamplerBindingType::Filtering), |
| 202 | + count: None, |
| 203 | + }, |
| 204 | + ], |
| 205 | + }) |
| 206 | + } |
| 207 | + |
| 208 | + fn fragment_shader(asset_server: &AssetServer) -> Option<Handle<Shader>> { |
| 209 | + asset_server.watch_for_changes().unwrap(); |
| 210 | + Some(asset_server.load("shaders/custom_material_chromatic_aberration.wgsl")) |
| 211 | + } |
| 212 | +} |
| 213 | + |
| 214 | +impl RenderAsset for PostProcessingMaterial { |
| 215 | + type ExtractedAsset = PostProcessingMaterial; |
| 216 | + type PreparedAsset = PostProcessingMaterialGPU; |
| 217 | + type Param = ( |
| 218 | + SRes<RenderDevice>, |
| 219 | + SRes<Material2dPipeline<PostProcessingMaterial>>, |
| 220 | + SRes<RenderAssets<Image>>, |
| 221 | + ); |
| 222 | + |
| 223 | + fn prepare_asset( |
| 224 | + extracted_asset: PostProcessingMaterial, |
| 225 | + (render_device, pipeline, images): &mut SystemParamItem<Self::Param>, |
| 226 | + ) -> Result<PostProcessingMaterialGPU, PrepareAssetError<PostProcessingMaterial>> { |
| 227 | + let (view, sampler) = if let Some(result) = pipeline |
| 228 | + .mesh2d_pipeline |
| 229 | + .get_image_texture(images, &Some(extracted_asset.source_image.clone())) |
| 230 | + { |
| 231 | + result |
| 232 | + } else { |
| 233 | + return Err(PrepareAssetError::RetryNextUpdate(extracted_asset)); |
| 234 | + }; |
| 235 | + |
| 236 | + let bind_group = render_device.create_bind_group(&BindGroupDescriptor { |
| 237 | + label: None, |
| 238 | + layout: &pipeline.material2d_layout, |
| 239 | + entries: &[ |
| 240 | + BindGroupEntry { |
| 241 | + binding: 0, |
| 242 | + resource: BindingResource::TextureView(view), |
| 243 | + }, |
| 244 | + BindGroupEntry { |
| 245 | + binding: 1, |
| 246 | + resource: BindingResource::Sampler(sampler), |
| 247 | + }, |
| 248 | + ], |
| 249 | + }); |
| 250 | + Ok(PostProcessingMaterialGPU { bind_group }) |
| 251 | + } |
| 252 | + |
| 253 | + fn extract_asset(&self) -> PostProcessingMaterial { |
| 254 | + self.clone() |
| 255 | + } |
| 256 | +} |
0 commit comments