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