1use core::any::type_name;
8use core::marker::PhantomData;
9
10use crate::{schedule::Core3d, tonemapping::tonemapping, Core3dSystems, FullscreenShader};
11use bevy_app::{App, Plugin};
12use bevy_asset::AssetServer;
13use bevy_ecs::{
14 component::Component,
15 entity::Entity,
16 error::BevyError,
17 query::With,
18 resource::Resource,
19 schedule::{IntoScheduleConfigs, ScheduleConfigs, ScheduleLabel},
20 system::{BoxedSystem, Commands, Local, Query, Res, ResMut},
21};
22use bevy_render::{
23 camera::ExtractedCamera,
24 diagnostic::RecordDiagnostics,
25 extract_component::{
26 ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
27 UniformComponentPlugin,
28 },
29 render_resource::{
30 binding_types::{sampler, texture_2d, uniform_buffer},
31 encase::internal::WriteInto,
32 BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries,
33 CachedRenderPipelineId, Canonical, ColorTargetState, ColorWrites, FragmentState,
34 Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline,
35 RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages,
36 ShaderType, Specializer, SpecializerKey, TextureFormat, TextureSampleType, TextureView,
37 Variants,
38 },
39 renderer::{RenderContext, RenderDevice, ViewQuery},
40 view::{
41 ExtractedView, PostProcessBindGroupCache, PostProcessBindGroupCacheBuilder, ViewTarget,
42 },
43 Render, RenderApp, RenderStartup, RenderSystems,
44};
45use bevy_shader::ShaderRef;
46use bevy_utils::default;
47
48#[derive(Default)]
49pub struct FullscreenMaterialPlugin<T: FullscreenMaterial> {
50 _marker: PhantomData<T>,
51}
52
53impl<T: FullscreenMaterial> Plugin for FullscreenMaterialPlugin<T> {
54 fn build(&self, app: &mut App) {
55 app.add_plugins((
56 ExtractComponentPlugin::<T>::default(),
57 UniformComponentPlugin::<T>::default(),
58 ));
59
60 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
61 return;
62 };
63
64 render_app
65 .add_systems(RenderStartup, init_pipeline::<T>)
66 .add_systems(
67 Render,
68 (
69 prepare_fullscreen_material_pipelines::<T>.in_set(RenderSystems::Prepare),
70 prepare_bind_groups::<T>.in_set(RenderSystems::PrepareBindGroups),
71 ),
72 );
73
74 let system = T::schedule_configs(fullscreen_material_system::<T>.into_configs());
75 render_app.add_systems(T::schedule(), system);
76 }
77}
78
79pub trait FullscreenMaterial:
81 Component + ExtractComponent<RenderApp> + Clone + Copy + ShaderType + WriteInto + Default
82{
83 fn fragment_shader() -> ShaderRef;
85
86 fn schedule() -> impl ScheduleLabel + Clone {
90 Core3d
91 }
92
93 fn schedule_configs(system: ScheduleConfigs<BoxedSystem>) -> ScheduleConfigs<BoxedSystem> {
97 system
98 .in_set(Core3dSystems::PostProcess)
99 .before(tonemapping)
100 }
101}
102
103#[derive(Resource)]
104pub struct FullscreenMaterialPipeline<T: FullscreenMaterial> {
105 layout: BindGroupLayoutDescriptor,
106 sampler: Sampler,
107 variants: Variants<RenderPipeline, FullscreenMaterialPipelineSpecializer>,
108 _marker: PhantomData<T>,
109}
110
111struct FullscreenMaterialPipelineSpecializer;
112
113#[derive(PartialEq, Eq, Hash, Clone, Copy, SpecializerKey)]
114struct FullscreenMaterialPipelineKey {
115 target_format: TextureFormat,
116}
117
118impl Specializer<RenderPipeline> for FullscreenMaterialPipelineSpecializer {
119 type Key = FullscreenMaterialPipelineKey;
120
121 fn specialize(
122 &self,
123 key: Self::Key,
124 descriptor: &mut RenderPipelineDescriptor,
125 ) -> Result<Canonical<Self::Key>, BevyError> {
126 let fragment = descriptor.fragment_mut()?;
127 let color_target_state = ColorTargetState {
128 format: key.target_format,
129 blend: None,
130 write_mask: ColorWrites::ALL,
131 };
132 fragment.set_target(0, color_target_state);
133 Ok(key)
134 }
135}
136
137fn init_pipeline<T: FullscreenMaterial>(
138 mut commands: Commands,
139 render_device: Res<RenderDevice>,
140 asset_server: Res<AssetServer>,
141 fullscreen_shader: Res<FullscreenShader>,
142) {
143 let layout = BindGroupLayoutDescriptor::new(
144 "fullscreen_material_bind_group_layout",
145 &BindGroupLayoutEntries::sequential(
146 ShaderStages::FRAGMENT,
147 (
148 texture_2d(TextureSampleType::Float { filterable: true }),
149 sampler(SamplerBindingType::Filtering),
150 uniform_buffer::<T>(true),
151 ),
152 ),
153 );
154 let sampler = render_device.create_sampler(&SamplerDescriptor::default());
155 let shader = match T::fragment_shader() {
156 ShaderRef::Default => {
157 unimplemented!(
158 "FullscreenMaterial::fragment_shader() must not return ShaderRef::Default"
159 )
160 }
161 ShaderRef::Handle(handle) => handle,
162 ShaderRef::Path(path) => asset_server.load(path),
163 };
164
165 let vertex_state = fullscreen_shader.to_vertex_state();
166 let desc = RenderPipelineDescriptor {
167 label: Some(format!("fullscreen_material_pipeline<{}>", type_name::<T>()).into()),
168 layout: vec![layout.clone()],
169 vertex: vertex_state,
170 fragment: Some(FragmentState {
171 shader,
172 targets: vec![Some(ColorTargetState {
173 format: TextureFormat::Rgba8UnormSrgb,
174 blend: None,
175 write_mask: ColorWrites::ALL,
176 })],
177 ..default()
178 }),
179 ..default()
180 };
181
182 commands.insert_resource(FullscreenMaterialPipeline::<T> {
183 layout,
184 sampler,
185 variants: Variants::new(FullscreenMaterialPipelineSpecializer, desc),
186 _marker: PhantomData,
187 });
188}
189
190#[derive(Component)]
191pub struct FullscreenMaterialPipelineId<T: FullscreenMaterial>(
192 pub CachedRenderPipelineId,
193 PhantomData<T>,
194);
195
196fn prepare_fullscreen_material_pipelines<T: FullscreenMaterial>(
197 mut commands: Commands,
198 pipeline_cache: Res<PipelineCache>,
199 mut pipeline: ResMut<FullscreenMaterialPipeline<T>>,
200 views: Query<(Entity, &ExtractedView, Option<&T>), With<ExtractedCamera>>,
201) -> Result<(), BevyError> {
202 for (entity, view, material) in &views {
203 if material.is_none() {
204 commands
205 .entity(entity)
206 .remove::<FullscreenMaterialPipelineId<T>>();
207 continue;
208 }
209
210 let pipeline_key = FullscreenMaterialPipelineKey {
211 target_format: view.target_format,
212 };
213 let pipeline_id = pipeline
214 .variants
215 .specialize(&pipeline_cache, pipeline_key)?;
216
217 commands
218 .entity(entity)
219 .insert(FullscreenMaterialPipelineId::<T>(pipeline_id, PhantomData));
220 }
221
222 Ok(())
223}
224
225#[derive(Component)]
230pub struct FullscreenMaterialBindGroup<T: FullscreenMaterial> {
231 cache: PostProcessBindGroupCache,
232 _marker: PhantomData<T>,
234}
235
236fn prepare_bind_groups<T: FullscreenMaterial>(
238 mut commands: Commands,
239 mut view: Query<(
240 Entity,
241 &ViewTarget,
242 Option<&mut FullscreenMaterialBindGroup<T>>,
243 Option<&T>,
244 )>,
245 fullscreen_pipeline: Option<Res<FullscreenMaterialPipeline<T>>>,
246 pipeline_cache: Res<PipelineCache>,
247 data_uniforms: Res<ComponentUniforms<T>>,
248 render_device: Res<RenderDevice>,
249) {
250 let Some(fullscreen_pipeline) = fullscreen_pipeline else {
251 return;
252 };
253 let Some(settings_binding) = data_uniforms.uniforms().binding() else {
254 return;
255 };
256
257 for (entity, view_target, mut maybe_bind_groups, material) in &mut view {
258 if material.is_none() {
259 commands
260 .entity(entity)
261 .remove::<FullscreenMaterialBindGroup<T>>();
262 continue;
263 }
264
265 let builder = PostProcessBindGroupCacheBuilder::new(|texture: &TextureView| {
266 (
267 texture.id(),
268 render_device.create_bind_group(
269 "fullscreen_material_bind_group",
270 &pipeline_cache.get_bind_group_layout(&fullscreen_pipeline.layout),
271 &BindGroupEntries::sequential((
272 texture,
273 &fullscreen_pipeline.sampler,
274 settings_binding.clone(),
275 )),
276 ),
277 )
278 });
279
280 if let Some(bind_groups) = &mut maybe_bind_groups {
281 if bind_groups.cache.should_update(view_target) {
282 bind_groups.cache.update(view_target, builder);
283 }
284 } else {
285 commands.entity(entity).insert(FullscreenMaterialBindGroup {
286 cache: builder.generate_bind_groups(view_target),
287 _marker: PhantomData::<T>,
288 });
289 }
290 }
291}
292
293pub fn fullscreen_material_system<T: FullscreenMaterial>(
294 view: ViewQuery<(
295 &ViewTarget,
296 &DynamicUniformIndex<T>,
297 &FullscreenMaterialBindGroup<T>,
298 &FullscreenMaterialPipelineId<T>,
299 )>,
300 pipeline_cache: Res<PipelineCache>,
301 mut ctx: RenderContext,
302 mut pass_label: Local<String>,
303) {
304 let (view_target, settings_index, bind_groups, pipeline_id) = view.into_inner();
305
306 let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id.0) else {
307 return;
308 };
309
310 let post_process = view_target.post_process_write();
311 let source = post_process.source;
312 let destination = post_process.destination;
313
314 let bind_group = bind_groups.cache.get_current_bind_group(source);
315
316 if pass_label.is_empty() {
317 *pass_label = format!("fullscreen_material_pass<{}>", type_name::<T>());
318 }
319 let pass_descriptor = RenderPassDescriptor {
320 label: Some(&pass_label),
321 color_attachments: &[Some(RenderPassColorAttachment {
322 view: destination,
323 depth_slice: None,
324 resolve_target: None,
325 ops: Operations::default(),
326 })],
327 depth_stencil_attachment: None,
328 timestamp_writes: None,
329 occlusion_query_set: None,
330 multiview_mask: None,
331 };
332
333 {
334 let diagnostics = ctx.diagnostic_recorder();
335 let diagnostics = diagnostics.as_deref();
336
337 let mut render_pass = ctx.command_encoder().begin_render_pass(&pass_descriptor);
338 let pass_span = diagnostics.pass_span(&mut render_pass, pass_label.clone());
339
340 render_pass.set_pipeline(pipeline);
341 render_pass.set_bind_group(0, bind_group, &[settings_index.index()]);
342 render_pass.draw(0..3, 0..1);
343 pass_span.end(&mut render_pass);
344 }
345}