mirror of
https://github.com/bevyengine/bevy.git
synced 2026-06-30 15:55:32 -04:00
082c44ea4c
# Objective - Clean up our texture format handling. - Fix #23732 - Get us closer to #22563 - It makes no sense for views to talk about being hdr or not. They have a texture format, that's it. What does HDR shadows even mean lol - Same for compositing_space ## Solution - Remove ExtractedView::hdr - Add ExtractedView::texture_format - Move ExtractedView::compositing_space to ExtractedCamera::compositing_space - Add texture_format to a bunch of specialization keys instead of hdr bool - Convert VolumetricFogPipelineKey to not use flags and just use bool and texture format - Remove BevyDefault TextureFormat - Remove ViewTarget TEXTURE_FORMAT_HDR ## Testing - Pretty extensively test at this point This has a migration guide. --------- Co-authored-by: Willow Black <wmcblack@gmail.com> Co-authored-by: Máté Homolya <mate.homolya@gmail.com> Co-authored-by: Luo Zhihao <luo_zhihao@outlook.com> Co-authored-by: IceSentry <IceSentry@users.noreply.github.com>
304 lines
12 KiB
Rust
304 lines
12 KiB
Rust
//! This example shows how to create a custom post-processing effect that runs after the main pass
|
|
//! and reads the texture generated by the main pass.
|
|
//!
|
|
//! The example shader is a very simple implementation of chromatic aberration.
|
|
//! To adapt this example for 2D, replace all instances of 3D structures (such as `Core3d`, etc.) with their corresponding 2D counterparts.
|
|
//!
|
|
//! This is a fairly low level example and assumes some familiarity with rendering concepts and wgpu.
|
|
|
|
use bevy::{
|
|
core_pipeline::{schedule::Core3d, Core3dSystems, FullscreenShader},
|
|
prelude::*,
|
|
render::{
|
|
extract_component::{
|
|
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
|
|
UniformComponentPlugin,
|
|
},
|
|
render_resource::{
|
|
binding_types::{sampler, texture_2d, uniform_buffer},
|
|
*,
|
|
},
|
|
renderer::{RenderContext, RenderDevice, ViewQuery},
|
|
view::ViewTarget,
|
|
RenderApp, RenderStartup,
|
|
},
|
|
};
|
|
|
|
/// This example uses a shader source file from the assets subdirectory
|
|
const SHADER_ASSET_PATH: &str = "shaders/post_processing.wgsl";
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins((DefaultPlugins, PostProcessPlugin))
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, (rotate, update_settings))
|
|
.run();
|
|
}
|
|
|
|
/// 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 [`PostProcessSettings`]
|
|
// for this plugin to work correctly.
|
|
ExtractComponentPlugin::<PostProcessSettings>::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::<PostProcessSettings>::default(),
|
|
));
|
|
|
|
// We need to get the render app from the main app
|
|
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
|
|
render_app.add_systems(RenderStartup, init_post_process_pipeline);
|
|
render_app.add_systems(
|
|
Core3d,
|
|
post_process_system.in_set(Core3dSystems::PostProcess),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct PostProcessBindGroupCache {
|
|
cached: Option<(TextureViewId, BindGroup)>,
|
|
}
|
|
|
|
fn post_process_system(
|
|
view: ViewQuery<(
|
|
&ViewTarget,
|
|
&PostProcessSettings,
|
|
&DynamicUniformIndex<PostProcessSettings>,
|
|
)>,
|
|
post_process_pipeline: Option<Res<PostProcessPipeline>>,
|
|
pipeline_cache: Res<PipelineCache>,
|
|
settings_uniforms: Res<ComponentUniforms<PostProcessSettings>>,
|
|
mut cache: Local<PostProcessBindGroupCache>,
|
|
mut ctx: RenderContext,
|
|
) {
|
|
let Some(post_process_pipeline) = post_process_pipeline else {
|
|
return;
|
|
};
|
|
|
|
let (view_target, _post_process_settings, settings_index) = view.into_inner();
|
|
|
|
let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let Some(settings_binding) = settings_uniforms.uniforms().binding() else {
|
|
return;
|
|
};
|
|
|
|
// 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();
|
|
|
|
let bind_group = match &mut cache.cached {
|
|
Some((texture_id, bind_group)) if post_process.source.id() == *texture_id => bind_group,
|
|
cached => {
|
|
// 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 = ctx.render_device().create_bind_group(
|
|
"post_process_bind_group",
|
|
&pipeline_cache.get_bind_group_layout(&post_process_pipeline.layout),
|
|
// It's important for this to match the BindGroupLayout defined in the PostProcessPipeline
|
|
&BindGroupEntries::sequential((
|
|
// Make sure to use the source view
|
|
post_process.source,
|
|
// Use the sampler created for the pipeline
|
|
&post_process_pipeline.sampler,
|
|
// Set the settings binding
|
|
settings_binding.clone(),
|
|
)),
|
|
);
|
|
|
|
let (_, bind_group) = cached.insert((post_process.source.id(), bind_group));
|
|
bind_group
|
|
}
|
|
};
|
|
|
|
let mut render_pass = ctx
|
|
.command_encoder()
|
|
.begin_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,
|
|
depth_slice: None,
|
|
resolve_target: None,
|
|
ops: Operations::default(),
|
|
})],
|
|
depth_stencil_attachment: None,
|
|
timestamp_writes: None,
|
|
occlusion_query_set: None,
|
|
multiview_mask: None,
|
|
});
|
|
|
|
render_pass.set_pipeline(pipeline);
|
|
// By passing in the index of the post process settings on this view, we ensure
|
|
// that in the event that multiple settings were sent to the GPU (as would be the
|
|
// case with multiple cameras), we use the correct one.
|
|
render_pass.set_bind_group(0, bind_group, &[settings_index.index()]);
|
|
render_pass.draw(0..3, 0..1);
|
|
}
|
|
|
|
// This contains global data used by the render pipeline. This will be created once on startup.
|
|
#[derive(Resource)]
|
|
struct PostProcessPipeline {
|
|
layout: BindGroupLayoutDescriptor,
|
|
sampler: Sampler,
|
|
pipeline_id: CachedRenderPipelineId,
|
|
}
|
|
|
|
fn init_post_process_pipeline(
|
|
mut commands: Commands,
|
|
render_device: Res<RenderDevice>,
|
|
asset_server: Res<AssetServer>,
|
|
fullscreen_shader: Res<FullscreenShader>,
|
|
pipeline_cache: Res<PipelineCache>,
|
|
) {
|
|
// We need to define the bind group layout used for our pipeline
|
|
let layout = BindGroupLayoutDescriptor::new(
|
|
"post_process_bind_group_layout",
|
|
&BindGroupLayoutEntries::sequential(
|
|
// The layout entries will only be visible in the fragment stage
|
|
ShaderStages::FRAGMENT,
|
|
(
|
|
// The screen texture
|
|
texture_2d(TextureSampleType::Float { filterable: true }),
|
|
// The sampler that will be used to sample the screen texture
|
|
sampler(SamplerBindingType::Filtering),
|
|
// The settings uniform that will control the effect
|
|
uniform_buffer::<PostProcessSettings>(true),
|
|
),
|
|
),
|
|
);
|
|
// 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 = asset_server.load(SHADER_ASSET_PATH);
|
|
// This will setup a fullscreen triangle for the vertex state.
|
|
let vertex_state = fullscreen_shader.to_vertex_state();
|
|
let pipeline_id = pipeline_cache
|
|
// This will add the pipeline to the cache and queue its creation
|
|
.queue_render_pipeline(RenderPipelineDescriptor {
|
|
label: Some("post_process_pipeline".into()),
|
|
layout: vec![layout.clone()],
|
|
vertex: vertex_state,
|
|
fragment: Some(FragmentState {
|
|
shader,
|
|
// Make sure this matches the entry point of your shader.
|
|
// It can be anything as long as it matches here and in the shader.
|
|
targets: vec![Some(ColorTargetState {
|
|
format: TextureFormat::Rgba8UnormSrgb,
|
|
blend: None,
|
|
write_mask: ColorWrites::ALL,
|
|
})],
|
|
..default()
|
|
}),
|
|
..default()
|
|
});
|
|
commands.insert_resource(PostProcessPipeline {
|
|
layout,
|
|
sampler,
|
|
pipeline_id,
|
|
});
|
|
}
|
|
|
|
// This is the component that will get passed to the shader
|
|
#[derive(Component, Default, Clone, Copy, ExtractComponent, ShaderType)]
|
|
struct PostProcessSettings {
|
|
intensity: f32,
|
|
// WebGL2 structs must be 16 byte aligned.
|
|
#[cfg(feature = "webgl2")]
|
|
_webgl2_padding: Vec3,
|
|
}
|
|
|
|
/// Set up a simple 3D scene
|
|
fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) {
|
|
// camera
|
|
// Make sure you change the TextureFormat of the ColorTargetState
|
|
// if you enable Hdr directly or through features like Bloom.
|
|
commands.spawn((
|
|
Camera3d::default(),
|
|
Transform::from_translation(Vec3::new(0.0, 0.0, 5.0)).looking_at(Vec3::default(), Vec3::Y),
|
|
Camera {
|
|
clear_color: Color::WHITE.into(),
|
|
..default()
|
|
},
|
|
// Add the setting to the camera.
|
|
// This component is also used to determine on which camera to run the post processing effect.
|
|
PostProcessSettings {
|
|
intensity: 0.02,
|
|
..default()
|
|
},
|
|
));
|
|
|
|
// cube
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Cuboid::default())),
|
|
MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
|
|
Transform::from_xyz(0.0, 0.5, 0.0),
|
|
Rotates,
|
|
));
|
|
// light
|
|
commands.spawn(DirectionalLight {
|
|
illuminance: 1_000.,
|
|
..default()
|
|
});
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct Rotates;
|
|
|
|
/// Rotates any entity around the x and y axis
|
|
fn rotate(time: Res<Time>, mut query: Query<&mut Transform, With<Rotates>>) {
|
|
for mut transform in &mut query {
|
|
transform.rotate_x(0.55 * time.delta_secs());
|
|
transform.rotate_z(0.15 * time.delta_secs());
|
|
}
|
|
}
|
|
|
|
// Change the intensity over time to show that the effect is controlled from the main world
|
|
fn update_settings(mut settings: Query<&mut PostProcessSettings>, time: Res<Time>) {
|
|
for mut setting in &mut settings {
|
|
let mut intensity = ops::sin(time.elapsed_secs());
|
|
// Make it loop periodically
|
|
intensity = ops::sin(intensity);
|
|
// Remap it to 0..1 because the intensity can't be negative
|
|
intensity = intensity * 0.5 + 0.5;
|
|
// Scale it to a more reasonable level
|
|
intensity *= 0.015;
|
|
|
|
// Set the intensity.
|
|
// This will then be extracted to the render world and uploaded to the GPU automatically by the [`UniformComponentPlugin`]
|
|
setting.intensity = intensity;
|
|
}
|
|
}
|