diff --git a/CHANGELOG.md b/CHANGELOG.md index 7183b9c..034ec92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Pixelation shader for retro effect +- (Optional) pixelation shader for retro effect +- (Optional) chromatic aberration shader ## [0.0.10] - 2023-10-04 diff --git a/src/camera.rs b/src/camera.rs index 36eda16..d36066f 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,14 +1,18 @@ use bevy::prelude::*; #[allow(unused_imports)] -use crate::shader::{PixelateSettings, ShaderPlugin}; +use crate::shaders::{ + chromatic_aberration::{ChromaticAberrationPlugin, ChromaticAberrationSettings}, + pixelate::{PixelatePlugin, PixelateSettings}, +}; use crate::{ship::Ship, state::AppState}; pub struct CameraPlugin; impl Plugin for CameraPlugin { fn build(&self, app: &mut App) { - // app.add_plugins(ShaderPlugin); + // app.add_plugins(PixelatePlugin); + // app.add_plugins(ChromaticAberrationPlugin); app.add_systems(Startup, setup); app.add_systems(Update, follow_player.run_if(in_state(AppState::Active))); } @@ -21,6 +25,10 @@ fn setup(mut commands: Commands) { block_size: 3.25, ..default() }, + ChromaticAberrationSettings { + intensity: 0.001, + ..default() + }, Name::new("Main Camera"), )); } diff --git a/src/main.rs b/src/main.rs index 37e414a..4ddc632 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ mod orbit; mod pause; mod planet; mod planetary_system; -mod shader; +mod shaders; mod ship; mod star; mod state; diff --git a/src/shaders/chromatic_aberration.rs b/src/shaders/chromatic_aberration.rs new file mode 100644 index 0000000..6405781 --- /dev/null +++ b/src/shaders/chromatic_aberration.rs @@ -0,0 +1,317 @@ +/* Ported from: https://github.com/bevyengine/bevy/blob/main/examples/shader/post_processing.rs */ + +use bevy::{ + core_pipeline::{core_2d, fullscreen_vertex_shader::fullscreen_shader_vertex_state}, + ecs::query::QueryItem, + prelude::*, + render::{ + extract_component::{ + ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, + }, + render_graph::{ + NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner, + }, + render_resource::{ + BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingResource, BindingType, CachedRenderPipelineId, + ColorTargetState, ColorWrites, FragmentState, MultisampleState, Operations, + PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor, + RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, + ShaderType, TextureFormat, TextureSampleType, TextureViewDimension, + }, + renderer::{RenderContext, RenderDevice}, + texture::BevyDefault, + view::ViewTarget, + RenderApp, + }, +}; + +pub struct ChromaticAberrationPlugin; +impl Plugin for ChromaticAberrationPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(PostProcessPlugin); + } +} + +/// It is generally encouraged to set up post processing effects as a plugin +struct PostProcessPlugin; + +impl Plugin for PostProcessPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + // The settings will be a component that lives in the main world but will + // be extracted to the render world every frame. + // This makes it possible to control the effect from the main world. + // This plugin will take care of extracting it automatically. + // It's important to derive [`ExtractComponent`] on [`PostProcessingSettings`] + // for this plugin to work correctly. + ExtractComponentPlugin::::default(), + // The settings will also be the data used in the shader. + // This plugin will prepare the component for the GPU by creating a uniform buffer + // and writing the data to that buffer every frame. + UniformComponentPlugin::::default(), + )); + + // We need to get the render app from the main app + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + // Bevy's renderer uses a render graph which is a collection of nodes in a directed acyclic graph. + // It currently runs on each view/camera and executes each node in the specified order. + // It will make sure that any node that needs a dependency from another node + // only runs when that dependency is done. + // + // Each node can execute arbitrary work, but it generally runs at least one render pass. + // A node only has access to the render world, so if you need data from the main world + // you need to extract it manually or with the plugin like above. + // Add a [`Node`] to the [`RenderGraph`] + // The Node needs to impl FromWorld + // + // The [`ViewNodeRunner`] is a special [`Node`] that will automatically run the node for each view + // matching the [`ViewQuery`] + .add_render_graph_node::>( + // Specify the name of the graph, in this case we want the graph for 3d + core_2d::graph::NAME, + // It also needs the name of the node + PostProcessNode::NAME, + ) + .add_render_graph_edges( + core_2d::graph::NAME, + // Specify the node ordering. + // This will automatically create all required node edges to enforce the given ordering. + &[ + core_2d::graph::node::TONEMAPPING, + PostProcessNode::NAME, + core_2d::graph::node::END_MAIN_PASS_POST_PROCESSING, + ], + ); + } + + fn finish(&self, app: &mut App) { + // We need to get the render app from the main app + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + // Initialize the pipeline + .init_resource::(); + } +} + +// The post process node used for the render graph +#[derive(Default)] +struct PostProcessNode; +impl PostProcessNode { + pub const NAME: &str = "post_process"; +} + +// The ViewNode trait is required by the ViewNodeRunner +impl ViewNode for PostProcessNode { + // The node needs a query to gather data from the ECS in order to do its rendering, + // but it's not a normal system so we need to define it manually. + // + // This query will only run on the view entity + type ViewQuery = &'static ViewTarget; + + // Runs the node logic + // This is where you encode draw commands. + // + // This will run on every view on which the graph is running. + // If you don't want your effect to run on every camera, + // you'll need to make sure you have a marker component as part of [`ViewQuery`] + // to identify which camera(s) should run the effect. + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + view_target: QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + // Get the pipeline resource that contains the global data we need + // to create the render pipeline + let post_process_pipeline = world.resource::(); + + // The pipeline cache is a cache of all previously created pipelines. + // It is required to avoid creating a new pipeline each frame, + // which is expensive due to shader compilation. + let pipeline_cache = world.resource::(); + + // Get the pipeline from the cache + let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id) + else { + return Ok(()); + }; + + // Get the settings uniform binding + let settings_uniforms = world.resource::>(); + let Some(settings_binding) = settings_uniforms.uniforms().binding() else { + return Ok(()); + }; + + // This will start a new "post process write", obtaining two texture + // views from the view target - a `source` and a `destination`. + // `source` is the "current" main texture and you _must_ write into + // `destination` because calling `post_process_write()` on the + // [`ViewTarget`] will internally flip the [`ViewTarget`]'s main + // texture to the `destination` texture. Failing to do so will cause + // the current main texture information to be lost. + let post_process = view_target.post_process_write(); + + // The bind_group gets created each frame. + // + // Normally, you would create a bind_group in the Queue set, + // but this doesn't work with the post_process_write(). + // The reason it doesn't work is because each post_process_write will alternate the source/destination. + // The only way to have the correct source/destination for the bind_group + // is to make sure you get it during the node execution. + let bind_group = render_context + .render_device() + .create_bind_group(&BindGroupDescriptor { + label: Some("post_process_bind_group"), + layout: &post_process_pipeline.layout, + // It's important for this to match the BindGroupLayout defined in the PostProcessPipeline + entries: &[ + BindGroupEntry { + binding: 0, + // Make sure to use the source view + resource: BindingResource::TextureView(post_process.source), + }, + BindGroupEntry { + binding: 1, + // Use the sampler created for the pipeline + resource: BindingResource::Sampler(&post_process_pipeline.sampler), + }, + BindGroupEntry { + binding: 2, + // Set the settings binding + resource: settings_binding.clone(), + }, + ], + }); + + // Begin the render pass + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("post_process_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + // We need to specify the post process destination view here + // to make sure we write to the appropriate texture. + view: post_process.destination, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + }); + + // This is mostly just wgpu boilerplate for drawing a fullscreen triangle, + // using the pipeline/bind_group created above + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group(0, &bind_group, &[]); + render_pass.draw(0..3, 0..1); + + Ok(()) + } +} + +// This contains global data used by the render pipeline. This will be created once on startup. +#[derive(Resource)] +struct PostProcessPipeline { + layout: BindGroupLayout, + sampler: Sampler, + pipeline_id: CachedRenderPipelineId, +} + +impl FromWorld for PostProcessPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + // We need to define the bind group layout used for our pipeline + let layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("post_process_bind_group_layout"), + entries: &[ + // The screen texture + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + // The sampler that will be used to sample the screen texture + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::Filtering), + count: None, + }, + // The settings uniform that will control the effect + BindGroupLayoutEntry { + binding: 2, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: bevy::render::render_resource::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(ChromaticAberrationSettings::min_size()), + }, + count: None, + }, + ], + }); + + // We can create the sampler here since it won't change at runtime and doesn't depend on the view + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); + + // Get the shader handle + let shader = world + .resource::() + .load("shaders/chromatic_aberration.wgsl"); + + let pipeline_id = world + .resource_mut::() + // This will add the pipeline to the cache and queue it's creation + .queue_render_pipeline(RenderPipelineDescriptor { + label: Some("post_process_pipeline".into()), + layout: vec![layout.clone()], + // This will setup a fullscreen triangle for the vertex state + vertex: fullscreen_shader_vertex_state(), + fragment: Some(FragmentState { + shader, + shader_defs: vec![], + // Make sure this matches the entry point of your shader. + // It can be anything as long as it matches here and in the shader. + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: TextureFormat::bevy_default(), + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + // All of the following properties are not important for this effect so just use the default values. + // This struct doesn't have the Default trait implemented because not all field can have a default value. + primitive: PrimitiveState::default(), + depth_stencil: None, + multisample: MultisampleState::default(), + push_constant_ranges: vec![], + }); + + Self { + layout, + sampler, + pipeline_id, + } + } +} + +// This is the component that will get passed to the shader +#[derive(Component, Default, Clone, Copy, ExtractComponent, ShaderType)] +pub struct ChromaticAberrationSettings { + pub intensity: f32, + // WebGL2 structs must be 16 byte aligned. + #[cfg(feature = "webgl2")] + _webgl2_padding: Vec3, +} diff --git a/src/shaders/mod.rs b/src/shaders/mod.rs new file mode 100644 index 0000000..15fc5d5 --- /dev/null +++ b/src/shaders/mod.rs @@ -0,0 +1,2 @@ +pub mod chromatic_aberration; +pub mod pixelate; diff --git a/src/shader.rs b/src/shaders/pixelate.rs similarity index 99% rename from src/shader.rs rename to src/shaders/pixelate.rs index 971ba88..358c9af 100644 --- a/src/shader.rs +++ b/src/shaders/pixelate.rs @@ -26,8 +26,8 @@ use bevy::{ }, }; -pub struct ShaderPlugin; -impl Plugin for ShaderPlugin { +pub struct PixelatePlugin; +impl Plugin for PixelatePlugin { fn build(&self, app: &mut App) { app.add_plugins(PostProcessPlugin); }