Skip to content

Commit

Permalink
add chromatic aberration shader
Browse files Browse the repository at this point in the history
  • Loading branch information
thombruce committed Oct 4, 2023
1 parent 8b8fec7 commit 7f124cc
Show file tree
Hide file tree
Showing 6 changed files with 334 additions and 6 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 10 additions & 2 deletions src/camera.rs
Original file line number Diff line number Diff line change
@@ -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)));
}
Expand All @@ -21,6 +25,10 @@ fn setup(mut commands: Commands) {
block_size: 3.25,
..default()
},
ChromaticAberrationSettings {
intensity: 0.001,
..default()
},
Name::new("Main Camera"),
));
}
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mod orbit;
mod pause;
mod planet;
mod planetary_system;
mod shader;
mod shaders;
mod ship;
mod star;
mod state;
Expand Down
317 changes: 317 additions & 0 deletions src/shaders/chromatic_aberration.rs
Original file line number Diff line number Diff line change
@@ -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::<ChromaticAberrationSettings>::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::<ChromaticAberrationSettings>::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::<ViewNodeRunner<PostProcessNode>>(
// 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::<PostProcessPipeline>();
}
}

// 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<Self::ViewQuery>,
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::<PostProcessPipeline>();

// 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::<PipelineCache>();

// 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::<ComponentUniforms<ChromaticAberrationSettings>>();
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::<RenderDevice>();

// 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::<AssetServer>()
.load("shaders/chromatic_aberration.wgsl");

let pipeline_id = world
.resource_mut::<PipelineCache>()
// 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,
}
2 changes: 2 additions & 0 deletions src/shaders/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod chromatic_aberration;
pub mod pixelate;
4 changes: 2 additions & 2 deletions src/shader.rs → src/shaders/pixelate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down

0 comments on commit 7f124cc

Please sign in to comment.