1use core::any::type_name;
8use core::marker::PhantomData;
9
10use crate::{core_2d::graph::Core2d, core_3d::graph::Core3d, FullscreenShader};
11use bevy_app::{App, Plugin};
12use bevy_asset::AssetServer;
13use bevy_camera::{Camera2d, Camera3d};
14use bevy_ecs::{
15 component::Component,
16 entity::Entity,
17 query::{Added, Has, QueryItem},
18 resource::Resource,
19 system::{Commands, Res},
20 world::{FromWorld, World},
21};
22use bevy_image::BevyDefault;
23use bevy_render::{
24 extract_component::{
25 ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
26 UniformComponentPlugin,
27 },
28 render_graph::{
29 InternedRenderLabel, InternedRenderSubGraph, NodeRunError, RenderGraph, RenderGraphContext,
30 RenderGraphError, RenderGraphExt, RenderLabel, ViewNode, ViewNodeRunner,
31 },
32 render_resource::{
33 binding_types::{sampler, texture_2d, uniform_buffer},
34 encase::internal::WriteInto,
35 BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries,
36 CachedRenderPipelineId, ColorTargetState, ColorWrites, FragmentState, Operations,
37 PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor,
38 Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, TextureFormat,
39 TextureSampleType,
40 },
41 renderer::{RenderContext, RenderDevice},
42 view::ViewTarget,
43 ExtractSchedule, MainWorld, RenderApp, RenderStartup,
44};
45use bevy_shader::ShaderRef;
46use bevy_utils::default;
47use tracing::warn;
48
49#[derive(Default)]
50pub struct FullscreenMaterialPlugin<T: FullscreenMaterial> {
51 _marker: PhantomData<T>,
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.add_systems(RenderStartup, init_pipeline::<T>);
65
66 if let Some(sub_graph) = T::sub_graph() {
67 render_app.add_render_graph_node::<ViewNodeRunner<FullscreenMaterialNode<T>>>(
68 sub_graph,
69 T::node_label(),
70 );
71
72 if let Some(mut render_graph) = render_app.world_mut().get_resource_mut::<RenderGraph>()
74 && let Some(graph) = render_graph.get_sub_graph_mut(sub_graph)
75 {
76 for window in T::node_edges().windows(2) {
77 let [a, b] = window else {
78 break;
79 };
80 let Err(err) = graph.try_add_node_edge(*a, *b) else {
81 continue;
82 };
83 match err {
84 RenderGraphError::EdgeAlreadyExists(_) => {}
87 _ => panic!("{err:?}"),
88 }
89 }
90 } else {
91 warn!("Failed to add edges for FullscreenMaterial");
92 };
93 } else {
94 render_app.add_systems(ExtractSchedule, extract_on_add::<T>);
97 }
98 }
99}
100
101fn extract_on_add<T: FullscreenMaterial>(world: &mut World) {
102 world.resource_scope::<MainWorld, ()>(|world, mut main_world| {
103 let mut query =
105 main_world.query_filtered::<(Entity, Has<Camera3d>, Has<Camera2d>), Added<T>>();
106
107 world.resource_scope::<RenderGraph, ()>(|world, mut render_graph| {
109 for (_entity, is_3d, is_2d) in query.iter(&main_world) {
110 let graph = if is_3d && let Some(graph) = render_graph.get_sub_graph_mut(Core3d) {
111 graph
112 } else if is_2d && let Some(graph) = render_graph.get_sub_graph_mut(Core2d) {
113 graph
114 } else {
115 warn!("FullscreenMaterial was added to an entity that isn't a camera");
116 continue;
117 };
118
119 let node = ViewNodeRunner::<FullscreenMaterialNode<T>>::from_world(world);
120 graph.add_node(T::node_label(), node);
121
122 for window in T::node_edges().windows(2) {
123 let [a, b] = window else {
124 break;
125 };
126 let Err(err) = graph.try_add_node_edge(*a, *b) else {
127 continue;
128 };
129 match err {
130 RenderGraphError::EdgeAlreadyExists(_) => {}
133 _ => panic!("{err:?}"),
134 }
135 }
136 }
137 });
138 });
139}
140
141pub trait FullscreenMaterial:
143 Component + ExtractComponent + Clone + Copy + ShaderType + WriteInto + Default
144{
145 fn fragment_shader() -> ShaderRef;
147
148 fn node_edges() -> Vec<InternedRenderLabel>;
164
165 fn sub_graph() -> Option<InternedRenderSubGraph> {
170 None
171 }
172
173 fn node_label() -> impl RenderLabel {
175 FullscreenMaterialLabel(type_name::<Self>())
176 }
177}
178
179#[derive(Debug, Hash, PartialEq, Eq, Clone)]
180struct FullscreenMaterialLabel(&'static str);
181
182impl RenderLabel for FullscreenMaterialLabel
183where
184 Self: 'static + Send + Sync + Clone + Eq + ::core::fmt::Debug + ::core::hash::Hash,
185{
186 fn dyn_clone(&self) -> Box<dyn RenderLabel> {
187 Box::new(::core::clone::Clone::clone(self))
188 }
189}
190
191#[derive(Resource)]
192struct FullscreenMaterialPipeline {
193 layout: BindGroupLayoutDescriptor,
194 sampler: Sampler,
195 pipeline_id: CachedRenderPipelineId,
196 pipeline_id_hdr: CachedRenderPipelineId,
197}
198
199fn init_pipeline<T: FullscreenMaterial>(
200 mut commands: Commands,
201 render_device: Res<RenderDevice>,
202 asset_server: Res<AssetServer>,
203 fullscreen_shader: Res<FullscreenShader>,
204 pipeline_cache: Res<PipelineCache>,
205) {
206 let layout = BindGroupLayoutDescriptor::new(
207 "post_process_bind_group_layout",
208 &BindGroupLayoutEntries::sequential(
209 ShaderStages::FRAGMENT,
210 (
211 texture_2d(TextureSampleType::Float { filterable: true }),
213 sampler(SamplerBindingType::Filtering),
215 uniform_buffer::<T>(true),
218 ),
219 ),
220 );
221 let sampler = render_device.create_sampler(&SamplerDescriptor::default());
222 let shader = match T::fragment_shader() {
223 ShaderRef::Default => {
224 unimplemented!(
227 "FullscreenMaterial::fragment_shader() must not return ShaderRef::Default"
228 )
229 }
230 ShaderRef::Handle(handle) => handle,
231 ShaderRef::Path(path) => asset_server.load(path),
232 };
233 let vertex_state = fullscreen_shader.to_vertex_state();
235 let mut desc = RenderPipelineDescriptor {
236 label: Some("post_process_pipeline".into()),
237 layout: vec![layout.clone()],
238 vertex: vertex_state,
239 fragment: Some(FragmentState {
240 shader,
241 targets: vec![Some(ColorTargetState {
242 format: TextureFormat::bevy_default(),
243 blend: None,
244 write_mask: ColorWrites::ALL,
245 })],
246 ..default()
247 }),
248 ..default()
249 };
250 let pipeline_id = pipeline_cache.queue_render_pipeline(desc.clone());
251 desc.fragment.as_mut().unwrap().targets[0]
252 .as_mut()
253 .unwrap()
254 .format = ViewTarget::TEXTURE_FORMAT_HDR;
255 let pipeline_id_hdr = pipeline_cache.queue_render_pipeline(desc);
256 commands.insert_resource(FullscreenMaterialPipeline {
257 layout,
258 sampler,
259 pipeline_id,
260 pipeline_id_hdr,
261 });
262}
263
264#[derive(Default)]
265struct FullscreenMaterialNode<T: FullscreenMaterial> {
266 _marker: PhantomData<T>,
267}
268
269impl<T: FullscreenMaterial> ViewNode for FullscreenMaterialNode<T> {
270 type ViewQuery = (&'static ViewTarget, &'static DynamicUniformIndex<T>);
272
273 fn run<'w>(
274 &self,
275 _graph: &mut RenderGraphContext,
276 render_context: &mut RenderContext,
277 (view_target, settings_index): QueryItem<Self::ViewQuery>,
278 world: &World,
279 ) -> Result<(), NodeRunError> {
280 let fullscreen_pipeline = world.resource::<FullscreenMaterialPipeline>();
281
282 let pipeline_cache = world.resource::<PipelineCache>();
283 let pipeline_id = if view_target.is_hdr() {
284 fullscreen_pipeline.pipeline_id_hdr
285 } else {
286 fullscreen_pipeline.pipeline_id
287 };
288
289 let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id) else {
290 return Ok(());
291 };
292
293 let data_uniforms = world.resource::<ComponentUniforms<T>>();
294 let Some(settings_binding) = data_uniforms.uniforms().binding() else {
295 return Ok(());
296 };
297
298 let post_process = view_target.post_process_write();
299
300 let bind_group = render_context.render_device().create_bind_group(
301 "post_process_bind_group",
302 &pipeline_cache.get_bind_group_layout(&fullscreen_pipeline.layout),
303 &BindGroupEntries::sequential((
304 post_process.source,
305 &fullscreen_pipeline.sampler,
306 settings_binding.clone(),
307 )),
308 );
309
310 let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
311 label: Some("post_process_pass"),
312 color_attachments: &[Some(RenderPassColorAttachment {
313 view: post_process.destination,
314 depth_slice: None,
315 resolve_target: None,
316 ops: Operations::default(),
317 })],
318 depth_stencil_attachment: None,
319 timestamp_writes: None,
320 occlusion_query_set: None,
321 });
322
323 render_pass.set_render_pipeline(pipeline);
324 render_pass.set_bind_group(0, &bind_group, &[settings_index.index()]);
325 render_pass.draw(0..3, 0..1);
326
327 Ok(())
328 }
329}