use core::any::type_name;
use core::marker::PhantomData;
use crate::{core_2d::graph::Core2d, core_3d::graph::Core3d, FullscreenShader};
use bevy_app::{App, Plugin};
use bevy_asset::AssetServer;
use bevy_camera::{Camera2d, Camera3d};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{Added, Has, QueryItem},
resource::Resource,
system::{Commands, Res},
world::{FromWorld, World},
};
use bevy_image::BevyDefault;
use bevy_render::{
extract_component::{
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
UniformComponentPlugin,
},
render_graph::{
InternedRenderLabel, InternedRenderSubGraph, NodeRunError, RenderGraph, RenderGraphContext,
RenderGraphError, RenderGraphExt, RenderLabel, ViewNode, ViewNodeRunner,
},
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
encase::internal::WriteInto,
BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries,
CachedRenderPipelineId, ColorTargetState, ColorWrites, FragmentState, Operations,
PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor,
Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, TextureFormat,
TextureSampleType,
},
renderer::{RenderContext, RenderDevice},
view::ViewTarget,
ExtractSchedule, MainWorld, RenderApp, RenderStartup,
};
use bevy_shader::ShaderRef;
use bevy_utils::default;
use tracing::warn;
#[derive(Default)]
pub struct FullscreenMaterialPlugin<T: FullscreenMaterial> {
_marker: PhantomData<T>,
}
impl<T: FullscreenMaterial> Plugin for FullscreenMaterialPlugin<T> {
fn build(&self, app: &mut App) {
app.add_plugins((
ExtractComponentPlugin::<T>::default(),
UniformComponentPlugin::<T>::default(),
));
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.add_systems(RenderStartup, init_pipeline::<T>);
if let Some(sub_graph) = T::sub_graph() {
render_app.add_render_graph_node::<ViewNodeRunner<FullscreenMaterialNode<T>>>(
sub_graph,
T::node_label(),
);
if let Some(mut render_graph) = render_app.world_mut().get_resource_mut::<RenderGraph>()
&& let Some(graph) = render_graph.get_sub_graph_mut(sub_graph)
{
for window in T::node_edges().windows(2) {
let [a, b] = window else {
break;
};
let Err(err) = graph.try_add_node_edge(*a, *b) else {
continue;
};
match err {
RenderGraphError::EdgeAlreadyExists(_) => {}
_ => panic!("{err:?}"),
}
}
} else {
warn!("Failed to add edges for FullscreenMaterial");
};
} else {
render_app.add_systems(ExtractSchedule, extract_on_add::<T>);
}
}
}
fn extract_on_add<T: FullscreenMaterial>(world: &mut World) {
world.resource_scope::<MainWorld, ()>(|world, mut main_world| {
let mut query =
main_world.query_filtered::<(Entity, Has<Camera3d>, Has<Camera2d>), Added<T>>();
world.resource_scope::<RenderGraph, ()>(|world, mut render_graph| {
for (_entity, is_3d, is_2d) in query.iter(&main_world) {
let graph = if is_3d && let Some(graph) = render_graph.get_sub_graph_mut(Core3d) {
graph
} else if is_2d && let Some(graph) = render_graph.get_sub_graph_mut(Core2d) {
graph
} else {
warn!("FullscreenMaterial was added to an entity that isn't a camera");
continue;
};
let node = ViewNodeRunner::<FullscreenMaterialNode<T>>::from_world(world);
graph.add_node(T::node_label(), node);
for window in T::node_edges().windows(2) {
let [a, b] = window else {
break;
};
let Err(err) = graph.try_add_node_edge(*a, *b) else {
continue;
};
match err {
RenderGraphError::EdgeAlreadyExists(_) => {}
_ => panic!("{err:?}"),
}
}
}
});
});
}
pub trait FullscreenMaterial:
Component + ExtractComponent + Clone + Copy + ShaderType + WriteInto + Default
{
fn fragment_shader() -> ShaderRef;
fn node_edges() -> Vec<InternedRenderLabel>;
fn sub_graph() -> Option<InternedRenderSubGraph> {
None
}
fn node_label() -> impl RenderLabel {
FullscreenMaterialLabel(type_name::<Self>())
}
}
#[derive(Debug, Hash, PartialEq, Eq, Clone)]
struct FullscreenMaterialLabel(&'static str);
impl RenderLabel for FullscreenMaterialLabel
where
Self: 'static + Send + Sync + Clone + Eq + ::core::fmt::Debug + ::core::hash::Hash,
{
fn dyn_clone(&self) -> Box<dyn RenderLabel> {
Box::new(::core::clone::Clone::clone(self))
}
}
#[derive(Resource)]
struct FullscreenMaterialPipeline {
layout: BindGroupLayoutDescriptor,
sampler: Sampler,
pipeline_id: CachedRenderPipelineId,
pipeline_id_hdr: CachedRenderPipelineId,
}
fn init_pipeline<T: FullscreenMaterial>(
mut commands: Commands,
render_device: Res<RenderDevice>,
asset_server: Res<AssetServer>,
fullscreen_shader: Res<FullscreenShader>,
pipeline_cache: Res<PipelineCache>,
) {
let layout = BindGroupLayoutDescriptor::new(
"post_process_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
uniform_buffer::<T>(true),
),
),
);
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
let shader = match T::fragment_shader() {
ShaderRef::Default => {
unimplemented!(
"FullscreenMaterial::fragment_shader() must not return ShaderRef::Default"
)
}
ShaderRef::Handle(handle) => handle,
ShaderRef::Path(path) => asset_server.load(path),
};
let vertex_state = fullscreen_shader.to_vertex_state();
let mut desc = RenderPipelineDescriptor {
label: Some("post_process_pipeline".into()),
layout: vec![layout.clone()],
vertex: vertex_state,
fragment: Some(FragmentState {
shader,
targets: vec![Some(ColorTargetState {
format: TextureFormat::bevy_default(),
blend: None,
write_mask: ColorWrites::ALL,
})],
..default()
}),
..default()
};
let pipeline_id = pipeline_cache.queue_render_pipeline(desc.clone());
desc.fragment.as_mut().unwrap().targets[0]
.as_mut()
.unwrap()
.format = ViewTarget::TEXTURE_FORMAT_HDR;
let pipeline_id_hdr = pipeline_cache.queue_render_pipeline(desc);
commands.insert_resource(FullscreenMaterialPipeline {
layout,
sampler,
pipeline_id,
pipeline_id_hdr,
});
}
#[derive(Default)]
struct FullscreenMaterialNode<T: FullscreenMaterial> {
_marker: PhantomData<T>,
}
impl<T: FullscreenMaterial> ViewNode for FullscreenMaterialNode<T> {
type ViewQuery = (&'static ViewTarget, &'static DynamicUniformIndex<T>);
fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(view_target, settings_index): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let fullscreen_pipeline = world.resource::<FullscreenMaterialPipeline>();
let pipeline_cache = world.resource::<PipelineCache>();
let pipeline_id = if view_target.is_hdr() {
fullscreen_pipeline.pipeline_id_hdr
} else {
fullscreen_pipeline.pipeline_id
};
let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id) else {
return Ok(());
};
let data_uniforms = world.resource::<ComponentUniforms<T>>();
let Some(settings_binding) = data_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",
&pipeline_cache.get_bind_group_layout(&fullscreen_pipeline.layout),
&BindGroupEntries::sequential((
post_process.source,
&fullscreen_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,
depth_slice: None,
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(())
}
}