diff options
author | Silas Bartha <silas@exvacuum.dev> | 2024-04-23 13:20:51 -0400 |
---|---|---|
committer | Silas Bartha <silas@exvacuum.dev> | 2024-04-23 13:20:51 -0400 |
commit | 2e8780e8050e262db0917ef71304457dd65295ff (patch) | |
tree | a7b23226fb6e6becb463dc4255ec37c8553d41fd /src |
Initial Commit
Diffstat (limited to 'src')
-rw-r--r-- | src/components.rs | 9 | ||||
-rw-r--r-- | src/lib.rs | 42 | ||||
-rw-r--r-- | src/nodes.rs | 75 | ||||
-rw-r--r-- | src/resources.rs | 63 |
4 files changed, 189 insertions, 0 deletions
diff --git a/src/components.rs b/src/components.rs new file mode 100644 index 0000000..837ccd9 --- /dev/null +++ b/src/components.rs @@ -0,0 +1,9 @@ +use bevy::{prelude::*, render::{render_resource::ShaderType, extract_component::ExtractComponent}}; + +#[derive(Component, ShaderType, ExtractComponent, PartialEq, Clone, Default)] +pub struct OutlinePostProcessSettings { + pub weight: f32, + pub threshold: f32, + #[cfg(feature = "webgl2")] + _padding: Vec2, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f5c7120 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,42 @@ +use bevy::{prelude::*, render::{RenderApp, extract_component::{UniformComponentPlugin, ExtractComponentPlugin}, render_graph::{RenderGraphApp, ViewNodeRunner}}, asset::embedded_asset, core_pipeline::core_3d::graph::{Core3d, Node3d}}; + +pub struct OutlinePostProcessPlugin; + +pub mod components; +mod resources; +mod nodes; + +impl Plugin for OutlinePostProcessPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "../assets/shaders/outline_post_process.wgsl"); + + app.add_plugins(( + UniformComponentPlugin::<components::OutlinePostProcessSettings>::default(), + ExtractComponentPlugin::<components::OutlinePostProcessSettings>::default(), + )); + + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.add_render_graph_node::<ViewNodeRunner<nodes::OutlineRenderNode>>( + Core3d, + nodes::OutlineRenderLabel, + ).add_render_graph_edges( + Core3d, + ( + Node3d::Tonemapping, + nodes::OutlineRenderLabel, + Node3d::EndMainPassPostProcessing, + ), + ); + } + + fn finish(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.init_resource::<resources::OutlinePostProcessPipeline>(); + } +} diff --git a/src/nodes.rs b/src/nodes.rs new file mode 100644 index 0000000..56769e1 --- /dev/null +++ b/src/nodes.rs @@ -0,0 +1,75 @@ +use bevy::{prelude::*, render::{render_graph::{ViewNode, NodeRunError, RenderGraphContext, RenderLabel}, view::ViewTarget, renderer::RenderContext, render_resource::{PipelineCache, BindGroupEntries, RenderPassDescriptor, RenderPassColorAttachment, Operations}, extract_component::ComponentUniforms}, core_pipeline::prepass::ViewPrepassTextures, ecs::query::QueryItem}; + +use super::components; +use super::resources; + +#[derive(RenderLabel, Clone, Eq, PartialEq, Hash, Debug)] +pub struct OutlineRenderLabel; + +#[derive(Default)] +pub struct OutlineRenderNode; + +impl ViewNode for OutlineRenderNode { + type ViewQuery = ( + &'static ViewTarget, + &'static ViewPrepassTextures, + &'static components::OutlinePostProcessSettings, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (view_target, view_prepass_textures, _): QueryItem<Self::ViewQuery>, + world: &World, + ) -> Result<(), NodeRunError> { + let render_pipeline = world.resource::<resources::OutlinePostProcessPipeline>(); + let pipeline_cache = world.resource::<PipelineCache>(); + let Some(pipeline) = pipeline_cache.get_render_pipeline(render_pipeline.pipeline_id) else { + warn!("Failed to get render pipeline from cache, skipping..."); + return Ok(()); + }; + + let uniforms = world.resource::<ComponentUniforms<components::OutlinePostProcessSettings>>(); + let Some(uniform_binding) = uniforms.uniforms().binding() else { + error!("Failed to get settings uniform binding"); + return Ok(()); + }; + + let Some(normal_buffer_view) = view_prepass_textures.normal_view() else { + error!("Failed to get normal buffer view"); + return Ok(()); + }; + + let post_process = view_target.post_process_write(); + + let bind_group = render_context.render_device().create_bind_group( + "outline_post_process_bind_group", + &render_pipeline.layout, + &BindGroupEntries::sequential(( + post_process.source, + &render_pipeline.screen_sampler, + normal_buffer_view, + &render_pipeline.normal_sampler, + uniform_binding, + )), + ); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("outline_post_process_render_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: post_process.destination, + ops: Operations::default(), + resolve_target: None, + })], + timestamp_writes: None, + depth_stencil_attachment: None, + occlusion_query_set: None, + }); + + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group(0, &bind_group, &[]); + render_pass.draw(0..3, 0..1); + Ok(()) + } +} diff --git a/src/resources.rs b/src/resources.rs new file mode 100644 index 0000000..d755dc2 --- /dev/null +++ b/src/resources.rs @@ -0,0 +1,63 @@ +use bevy::{prelude::*, render::{render_resource::{Sampler, BindGroupLayout, BindGroupLayoutEntries, ShaderStages, binding_types::{texture_2d, sampler, uniform_buffer}, TextureSampleType, SamplerBindingType, SamplerDescriptor, PipelineCache, RenderPipelineDescriptor, CachedRenderPipelineId, PrimitiveState, MultisampleState, FragmentState, ColorTargetState, TextureFormat, ColorWrites}, renderer::RenderDevice, texture::BevyDefault}, core_pipeline::fullscreen_vertex_shader::fullscreen_shader_vertex_state}; + +use super::components; + +#[derive(Resource)] +pub struct OutlinePostProcessPipeline { + pub layout: BindGroupLayout, + pub screen_sampler: Sampler, + pub normal_sampler: Sampler, + pub pipeline_id: CachedRenderPipelineId, +} + +impl FromWorld for OutlinePostProcessPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::<RenderDevice>(); + + let layout = render_device.create_bind_group_layout( + "outline_post_process_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float{ filterable: true }), + sampler(SamplerBindingType::Filtering), + texture_2d(TextureSampleType::Float{ filterable: true }), + sampler(SamplerBindingType::Filtering), + uniform_buffer::<components::OutlinePostProcessSettings>(false), + ), + ), + ); + + let screen_sampler = render_device.create_sampler(&SamplerDescriptor::default()); + let normal_sampler = render_device.create_sampler(&SamplerDescriptor::default()); + + let shader = world.resource::<AssetServer>().load::<Shader>("embedded://grex_outline_post_process/../assets/shaders/outline_post_process.wgsl"); + + let pipeline_id = world.resource_mut::<PipelineCache>().queue_render_pipeline(RenderPipelineDescriptor { + label: Some("outline_post_process_render_pipeline".into()), + layout: vec![layout.clone()], + push_constant_ranges: vec![], + vertex: fullscreen_shader_vertex_state(), + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + shader, + shader_defs: vec![], + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: TextureFormat::bevy_default(), + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + }); + + Self { + layout, + screen_sampler, + normal_sampler, + pipeline_id, + } + } +} |