Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

View File

@@ -0,0 +1,193 @@
// Auto exposure
//
// This shader computes an auto exposure value for the current frame,
// which is then used as an exposure correction in the tone mapping shader.
//
// The auto exposure value is computed in two passes:
// * The compute_histogram pass calculates a histogram of the luminance values in the scene,
// taking into account the metering mask texture. The metering mask is a grayscale texture
// that defines the areas of the screen that should be given more weight when calculating
// the average luminance value. For example, the middle area of the screen might be more important
// than the edges.
// * The compute_average pass calculates the average luminance value of the scene, taking
// into account the low_percent and high_percent settings. These settings define the
// percentage of the histogram that should be excluded when calculating the average. This
// is useful to avoid overexposure when you have a lot of shadows, or underexposure when you
// have a lot of bright specular reflections.
//
// The final target_exposure is finally used to smoothly adjust the exposure value over time.
#import bevy_render::view::View
#import bevy_render::globals::Globals
// Constant to convert RGB to luminance, taken from Real Time Rendering, Vol 4 pg. 278, 4th edition
const RGB_TO_LUM = vec3<f32>(0.2125, 0.7154, 0.0721);
struct AutoExposure {
min_log_lum: f32,
inv_log_lum_range: f32,
log_lum_range: f32,
low_percent: f32,
high_percent: f32,
speed_up: f32,
speed_down: f32,
exponential_transition_distance: f32,
}
struct CompensationCurve {
min_log_lum: f32,
inv_log_lum_range: f32,
min_compensation: f32,
compensation_range: f32,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var<uniform> settings: AutoExposure;
@group(0) @binding(2) var tex_color: texture_2d<f32>;
@group(0) @binding(3) var tex_mask: texture_2d<f32>;
@group(0) @binding(4) var tex_compensation: texture_1d<f32>;
@group(0) @binding(5) var<uniform> compensation_curve: CompensationCurve;
@group(0) @binding(6) var<storage, read_write> histogram: array<atomic<u32>, 64>;
@group(0) @binding(7) var<storage, read_write> exposure: f32;
@group(0) @binding(8) var<storage, read_write> view: View;
var<workgroup> histogram_shared: array<atomic<u32>, 64>;
// For a given color, return the histogram bin index
fn color_to_bin(hdr: vec3<f32>) -> u32 {
// Convert color to luminance
let lum = dot(hdr, RGB_TO_LUM);
if lum < exp2(settings.min_log_lum) {
return 0u;
}
// Calculate the log_2 luminance and express it as a value in [0.0, 1.0]
// where 0.0 represents the minimum luminance, and 1.0 represents the max.
let log_lum = saturate((log2(lum) - settings.min_log_lum) * settings.inv_log_lum_range);
// Map [0, 1] to [1, 63]. The zeroth bin is handled by the epsilon check above.
return u32(log_lum * 62.0 + 1.0);
}
// Read the metering mask at the given UV coordinates, returning a weight for the histogram.
//
// Since the histogram is summed in the compute_average step, there is a limit to the amount of
// distinct values that can be represented. When using the chosen value of 16, the maximum
// amount of pixels that can be weighted and summed is 2^32 / 16 = 16384^2.
fn metering_weight(coords: vec2<f32>) -> u32 {
let pos = vec2<i32>(coords * vec2<f32>(textureDimensions(tex_mask)));
let mask = textureLoad(tex_mask, pos, 0).r;
return u32(mask * 16.0);
}
@compute @workgroup_size(16, 16, 1)
fn compute_histogram(
@builtin(global_invocation_id) global_invocation_id: vec3<u32>,
@builtin(local_invocation_index) local_invocation_index: u32
) {
// Clear the workgroup shared histogram
if local_invocation_index < 64 {
histogram_shared[local_invocation_index] = 0u;
}
// Wait for all workgroup threads to clear the shared histogram
workgroupBarrier();
let dim = vec2<u32>(textureDimensions(tex_color));
let uv = vec2<f32>(global_invocation_id.xy) / vec2<f32>(dim);
if global_invocation_id.x < dim.x && global_invocation_id.y < dim.y {
let col = textureLoad(tex_color, vec2<i32>(global_invocation_id.xy), 0).rgb;
let index = color_to_bin(col);
let weight = metering_weight(uv);
// Increment the shared histogram bin by the weight obtained from the metering mask
atomicAdd(&histogram_shared[index], weight);
}
// Wait for all workgroup threads to finish updating the workgroup histogram
workgroupBarrier();
// Accumulate the workgroup histogram into the global histogram.
// Note that the global histogram was not cleared at the beginning,
// as it will be cleared in compute_average.
atomicAdd(&histogram[local_invocation_index], histogram_shared[local_invocation_index]);
}
@compute @workgroup_size(1, 1, 1)
fn compute_average(@builtin(local_invocation_index) local_index: u32) {
var histogram_sum = 0u;
// Calculate the cumulative histogram and clear the histogram bins.
// Each bin in the cumulative histogram contains the sum of all bins up to that point.
// This way we can quickly exclude the portion of lowest and highest samples as required by
// the low_percent and high_percent settings.
for (var i=0u; i<64u; i+=1u) {
histogram_sum += histogram[i];
histogram_shared[i] = histogram_sum;
// Clear the histogram bin for the next frame
histogram[i] = 0u;
}
let first_index = u32(f32(histogram_sum) * settings.low_percent);
let last_index = u32(f32(histogram_sum) * settings.high_percent);
var count = 0u;
var sum = 0.0;
for (var i=1u; i<64u; i+=1u) {
// The number of pixels in the bin. The histogram values are clamped to
// first_index and last_index to exclude the lowest and highest samples.
let bin_count =
clamp(histogram_shared[i], first_index, last_index) -
clamp(histogram_shared[i - 1u], first_index, last_index);
sum += f32(bin_count) * f32(i);
count += bin_count;
}
var avg_lum = settings.min_log_lum;
if count > 0u {
// The average luminance of the included histogram samples.
avg_lum = sum / (f32(count) * 63.0)
* settings.log_lum_range
+ settings.min_log_lum;
}
// The position in the compensation curve texture to sample for avg_lum.
let u = (avg_lum - compensation_curve.min_log_lum) * compensation_curve.inv_log_lum_range;
// The target exposure is the negative of the average log luminance.
// The compensation value is added to the target exposure to adjust the exposure for
// artistic purposes.
let target_exposure = textureLoad(tex_compensation, i32(saturate(u) * 255.0), 0).r
* compensation_curve.compensation_range
+ compensation_curve.min_compensation
- avg_lum;
// Smoothly adjust the `exposure` towards the `target_exposure`
let delta = target_exposure - exposure;
if target_exposure > exposure {
let speed_down = settings.speed_down * globals.delta_time;
let exp_down = speed_down / settings.exponential_transition_distance;
exposure = exposure + min(speed_down, delta * exp_down);
} else {
let speed_up = settings.speed_up * globals.delta_time;
let exp_up = speed_up / settings.exponential_transition_distance;
exposure = exposure + max(-speed_up, delta * exp_up);
}
// Apply the exposure to the color grading settings, from where it will be used for the color
// grading pass.
view.color_grading.exposure += exposure;
}

View File

@@ -0,0 +1,87 @@
use bevy_ecs::prelude::*;
use bevy_platform::collections::{hash_map::Entry, HashMap};
use bevy_render::{
render_resource::{StorageBuffer, UniformBuffer},
renderer::{RenderDevice, RenderQueue},
sync_world::RenderEntity,
Extract,
};
use super::{pipeline::AutoExposureUniform, AutoExposure};
#[derive(Resource, Default)]
pub(super) struct AutoExposureBuffers {
pub(super) buffers: HashMap<Entity, AutoExposureBuffer>,
}
pub(super) struct AutoExposureBuffer {
pub(super) state: StorageBuffer<f32>,
pub(super) settings: UniformBuffer<AutoExposureUniform>,
}
#[derive(Resource)]
pub(super) struct ExtractedStateBuffers {
changed: Vec<(Entity, AutoExposure)>,
removed: Vec<Entity>,
}
pub(super) fn extract_buffers(
mut commands: Commands,
changed: Extract<Query<(RenderEntity, &AutoExposure), Changed<AutoExposure>>>,
mut removed: Extract<RemovedComponents<AutoExposure>>,
) {
commands.insert_resource(ExtractedStateBuffers {
changed: changed
.iter()
.map(|(entity, settings)| (entity, settings.clone()))
.collect(),
removed: removed.read().collect(),
});
}
pub(super) fn prepare_buffers(
device: Res<RenderDevice>,
queue: Res<RenderQueue>,
mut extracted: ResMut<ExtractedStateBuffers>,
mut buffers: ResMut<AutoExposureBuffers>,
) {
for (entity, settings) in extracted.changed.drain(..) {
let (min_log_lum, max_log_lum) = settings.range.into_inner();
let (low_percent, high_percent) = settings.filter.into_inner();
let initial_state = 0.0f32.clamp(min_log_lum, max_log_lum);
let settings = AutoExposureUniform {
min_log_lum,
inv_log_lum_range: 1.0 / (max_log_lum - min_log_lum),
log_lum_range: max_log_lum - min_log_lum,
low_percent,
high_percent,
speed_up: settings.speed_brighten,
speed_down: settings.speed_darken,
exponential_transition_distance: settings.exponential_transition_distance,
};
match buffers.buffers.entry(entity) {
Entry::Occupied(mut entry) => {
// Update the settings buffer, but skip updating the state buffer.
// The state buffer is skipped so that the animation stays continuous.
let value = entry.get_mut();
value.settings.set(settings);
value.settings.write_buffer(&device, &queue);
}
Entry::Vacant(entry) => {
let value = entry.insert(AutoExposureBuffer {
state: StorageBuffer::from(initial_state),
settings: UniformBuffer::from(settings),
});
value.state.write_buffer(&device, &queue);
value.settings.write_buffer(&device, &queue);
}
}
}
for entity in extracted.removed.drain(..) {
buffers.buffers.remove(&entity);
}
}

View File

@@ -0,0 +1,236 @@
use bevy_asset::prelude::*;
use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem};
use bevy_math::{cubic_splines::CubicGenerator, FloatExt, Vec2};
use bevy_reflect::prelude::*;
use bevy_render::{
render_asset::{RenderAsset, RenderAssetUsages},
render_resource::{
Extent3d, ShaderType, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
TextureView, UniformBuffer,
},
renderer::{RenderDevice, RenderQueue},
};
use thiserror::Error;
const LUT_SIZE: usize = 256;
/// An auto exposure compensation curve.
/// This curve is used to map the average log luminance of a scene to an
/// exposure compensation value, to allow for fine control over the final exposure.
#[derive(Asset, Reflect, Debug, Clone)]
#[reflect(Default, Clone)]
pub struct AutoExposureCompensationCurve {
/// The minimum log luminance value in the curve. (the x-axis)
min_log_lum: f32,
/// The maximum log luminance value in the curve. (the x-axis)
max_log_lum: f32,
/// The minimum exposure compensation value in the curve. (the y-axis)
min_compensation: f32,
/// The maximum exposure compensation value in the curve. (the y-axis)
max_compensation: f32,
/// The lookup table for the curve. Uploaded to the GPU as a 1D texture.
/// Each value in the LUT is a `u8` representing a normalized exposure compensation value:
/// * `0` maps to `min_compensation`
/// * `255` maps to `max_compensation`
///
/// The position in the LUT corresponds to the normalized log luminance value.
/// * `0` maps to `min_log_lum`
/// * `LUT_SIZE - 1` maps to `max_log_lum`
lut: [u8; LUT_SIZE],
}
/// Various errors that can occur when constructing an [`AutoExposureCompensationCurve`].
#[derive(Error, Debug)]
pub enum AutoExposureCompensationCurveError {
/// The curve couldn't be built in the first place.
#[error("curve could not be constructed from the given data")]
InvalidCurve,
/// A discontinuity was found in the curve.
#[error("discontinuity found between curve segments")]
DiscontinuityFound,
/// The curve is not monotonically increasing on the x-axis.
#[error("curve is not monotonically increasing on the x-axis")]
NotMonotonic,
}
impl Default for AutoExposureCompensationCurve {
fn default() -> Self {
Self {
min_log_lum: 0.0,
max_log_lum: 0.0,
min_compensation: 0.0,
max_compensation: 0.0,
lut: [0; LUT_SIZE],
}
}
}
impl AutoExposureCompensationCurve {
const SAMPLES_PER_SEGMENT: usize = 64;
/// Build an [`AutoExposureCompensationCurve`] from a [`CubicGenerator<Vec2>`], where:
/// - x represents the average log luminance of the scene in EV-100;
/// - y represents the exposure compensation value in F-stops.
///
/// # Errors
///
/// If the curve is not monotonically increasing on the x-axis,
/// returns [`AutoExposureCompensationCurveError::NotMonotonic`].
///
/// If a discontinuity is found between curve segments,
/// returns [`AutoExposureCompensationCurveError::DiscontinuityFound`].
///
/// # Example
///
/// ```
/// # use bevy_asset::prelude::*;
/// # use bevy_math::vec2;
/// # use bevy_math::cubic_splines::*;
/// # use bevy_core_pipeline::auto_exposure::AutoExposureCompensationCurve;
/// # let mut compensation_curves = Assets::<AutoExposureCompensationCurve>::default();
/// let curve: Handle<AutoExposureCompensationCurve> = compensation_curves.add(
/// AutoExposureCompensationCurve::from_curve(LinearSpline::new([
/// vec2(-4.0, -2.0),
/// vec2(0.0, 0.0),
/// vec2(2.0, 0.0),
/// vec2(4.0, 2.0),
/// ]))
/// .unwrap()
/// );
/// ```
pub fn from_curve<T>(curve: T) -> Result<Self, AutoExposureCompensationCurveError>
where
T: CubicGenerator<Vec2>,
{
let Ok(curve) = curve.to_curve() else {
return Err(AutoExposureCompensationCurveError::InvalidCurve);
};
let min_log_lum = curve.position(0.0).x;
let max_log_lum = curve.position(curve.segments().len() as f32).x;
let log_lum_range = max_log_lum - min_log_lum;
let mut lut = [0.0; LUT_SIZE];
let mut previous = curve.position(0.0);
let mut min_compensation = previous.y;
let mut max_compensation = previous.y;
for segment in curve {
if segment.position(0.0) != previous {
return Err(AutoExposureCompensationCurveError::DiscontinuityFound);
}
for i in 1..Self::SAMPLES_PER_SEGMENT {
let current = segment.position(i as f32 / (Self::SAMPLES_PER_SEGMENT - 1) as f32);
if current.x < previous.x {
return Err(AutoExposureCompensationCurveError::NotMonotonic);
}
// Find the range of LUT entries that this line segment covers.
let (lut_begin, lut_end) = (
((previous.x - min_log_lum) / log_lum_range) * (LUT_SIZE - 1) as f32,
((current.x - min_log_lum) / log_lum_range) * (LUT_SIZE - 1) as f32,
);
let lut_inv_range = 1.0 / (lut_end - lut_begin);
// Iterate over all LUT entries whose pixel centers fall within the current segment.
#[expect(
clippy::needless_range_loop,
reason = "This for-loop also uses `i` to calculate a value `t`."
)]
for i in lut_begin.ceil() as usize..=lut_end.floor() as usize {
let t = (i as f32 - lut_begin) * lut_inv_range;
lut[i] = previous.y.lerp(current.y, t);
min_compensation = min_compensation.min(lut[i]);
max_compensation = max_compensation.max(lut[i]);
}
previous = current;
}
}
let compensation_range = max_compensation - min_compensation;
Ok(Self {
min_log_lum,
max_log_lum,
min_compensation,
max_compensation,
lut: if compensation_range > 0.0 {
let scale = 255.0 / compensation_range;
lut.map(|f: f32| ((f - min_compensation) * scale) as u8)
} else {
[0; LUT_SIZE]
},
})
}
}
/// The GPU-representation of an [`AutoExposureCompensationCurve`].
/// Consists of a [`TextureView`] with the curve's data,
/// and a [`UniformBuffer`] with the curve's extents.
pub struct GpuAutoExposureCompensationCurve {
pub(super) texture_view: TextureView,
pub(super) extents: UniformBuffer<AutoExposureCompensationCurveUniform>,
}
#[derive(ShaderType, Clone, Copy)]
pub(super) struct AutoExposureCompensationCurveUniform {
min_log_lum: f32,
inv_log_lum_range: f32,
min_compensation: f32,
compensation_range: f32,
}
impl RenderAsset for GpuAutoExposureCompensationCurve {
type SourceAsset = AutoExposureCompensationCurve;
type Param = (SRes<RenderDevice>, SRes<RenderQueue>);
fn asset_usage(_: &Self::SourceAsset) -> RenderAssetUsages {
RenderAssetUsages::RENDER_WORLD
}
fn prepare_asset(
source: Self::SourceAsset,
_: AssetId<Self::SourceAsset>,
(render_device, render_queue): &mut SystemParamItem<Self::Param>,
) -> Result<Self, bevy_render::render_asset::PrepareAssetError<Self::SourceAsset>> {
let texture = render_device.create_texture_with_data(
render_queue,
&TextureDescriptor {
label: None,
size: Extent3d {
width: LUT_SIZE as u32,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D1,
format: TextureFormat::R8Unorm,
usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
view_formats: &[TextureFormat::R8Unorm],
},
Default::default(),
&source.lut,
);
let texture_view = texture.create_view(&Default::default());
let mut extents = UniformBuffer::from(AutoExposureCompensationCurveUniform {
min_log_lum: source.min_log_lum,
inv_log_lum_range: 1.0 / (source.max_log_lum - source.min_log_lum),
min_compensation: source.min_compensation,
compensation_range: source.max_compensation - source.min_compensation,
});
extents.write_buffer(render_device, render_queue);
Ok(GpuAutoExposureCompensationCurve {
texture_view,
extents,
})
}
}

View File

@@ -0,0 +1,131 @@
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle};
use bevy_ecs::prelude::*;
use bevy_render::{
extract_component::ExtractComponentPlugin,
render_asset::RenderAssetPlugin,
render_graph::RenderGraphApp,
render_resource::{
Buffer, BufferDescriptor, BufferUsages, PipelineCache, Shader, SpecializedComputePipelines,
},
renderer::RenderDevice,
ExtractSchedule, Render, RenderApp, RenderSet,
};
mod buffers;
mod compensation_curve;
mod node;
mod pipeline;
mod settings;
use buffers::{extract_buffers, prepare_buffers, AutoExposureBuffers};
pub use compensation_curve::{AutoExposureCompensationCurve, AutoExposureCompensationCurveError};
use node::AutoExposureNode;
use pipeline::{
AutoExposurePass, AutoExposurePipeline, ViewAutoExposurePipeline, METERING_SHADER_HANDLE,
};
pub use settings::AutoExposure;
use crate::{
auto_exposure::compensation_curve::GpuAutoExposureCompensationCurve,
core_3d::graph::{Core3d, Node3d},
};
/// Plugin for the auto exposure feature.
///
/// See [`AutoExposure`] for more details.
pub struct AutoExposurePlugin;
#[derive(Resource)]
struct AutoExposureResources {
histogram: Buffer,
}
impl Plugin for AutoExposurePlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
METERING_SHADER_HANDLE,
"auto_exposure.wgsl",
Shader::from_wgsl
);
app.add_plugins(RenderAssetPlugin::<GpuAutoExposureCompensationCurve>::default())
.register_type::<AutoExposureCompensationCurve>()
.init_asset::<AutoExposureCompensationCurve>()
.register_asset_reflect::<AutoExposureCompensationCurve>();
app.world_mut()
.resource_mut::<Assets<AutoExposureCompensationCurve>>()
.insert(&Handle::default(), AutoExposureCompensationCurve::default());
app.register_type::<AutoExposure>();
app.add_plugins(ExtractComponentPlugin::<AutoExposure>::default());
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedComputePipelines<AutoExposurePipeline>>()
.init_resource::<AutoExposureBuffers>()
.add_systems(ExtractSchedule, extract_buffers)
.add_systems(
Render,
(
prepare_buffers.in_set(RenderSet::Prepare),
queue_view_auto_exposure_pipelines.in_set(RenderSet::Queue),
),
)
.add_render_graph_node::<AutoExposureNode>(Core3d, node::AutoExposure)
.add_render_graph_edges(
Core3d,
(Node3d::EndMainPass, node::AutoExposure, Node3d::Tonemapping),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<AutoExposurePipeline>();
render_app.init_resource::<AutoExposureResources>();
}
}
impl FromWorld for AutoExposureResources {
fn from_world(world: &mut World) -> Self {
Self {
histogram: world
.resource::<RenderDevice>()
.create_buffer(&BufferDescriptor {
label: Some("histogram buffer"),
size: pipeline::HISTOGRAM_BIN_COUNT * 4,
usage: BufferUsages::STORAGE,
mapped_at_creation: false,
}),
}
}
}
fn queue_view_auto_exposure_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut compute_pipelines: ResMut<SpecializedComputePipelines<AutoExposurePipeline>>,
pipeline: Res<AutoExposurePipeline>,
view_targets: Query<(Entity, &AutoExposure)>,
) {
for (entity, auto_exposure) in view_targets.iter() {
let histogram_pipeline =
compute_pipelines.specialize(&pipeline_cache, &pipeline, AutoExposurePass::Histogram);
let average_pipeline =
compute_pipelines.specialize(&pipeline_cache, &pipeline, AutoExposurePass::Average);
commands.entity(entity).insert(ViewAutoExposurePipeline {
histogram_pipeline,
mean_luminance_pipeline: average_pipeline,
compensation_curve: auto_exposure.compensation_curve.clone(),
metering_mask: auto_exposure.metering_mask.clone(),
});
}
}

View File

@@ -0,0 +1,141 @@
use super::{
buffers::AutoExposureBuffers,
compensation_curve::GpuAutoExposureCompensationCurve,
pipeline::{AutoExposurePipeline, ViewAutoExposurePipeline},
AutoExposureResources,
};
use bevy_ecs::{
query::QueryState,
system::lifetimeless::Read,
world::{FromWorld, World},
};
use bevy_render::{
globals::GlobalsBuffer,
render_asset::RenderAssets,
render_graph::*,
render_resource::*,
renderer::RenderContext,
texture::{FallbackImage, GpuImage},
view::{ExtractedView, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms},
};
#[derive(RenderLabel, Debug, Clone, Hash, PartialEq, Eq)]
pub struct AutoExposure;
pub struct AutoExposureNode {
query: QueryState<(
Read<ViewUniformOffset>,
Read<ViewTarget>,
Read<ViewAutoExposurePipeline>,
Read<ExtractedView>,
)>,
}
impl FromWorld for AutoExposureNode {
fn from_world(world: &mut World) -> Self {
Self {
query: QueryState::new(world),
}
}
}
impl Node for AutoExposureNode {
fn update(&mut self, world: &mut World) {
self.query.update_archetypes(world);
}
fn run(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
world: &World,
) -> Result<(), NodeRunError> {
let view_entity = graph.view_entity();
let pipeline_cache = world.resource::<PipelineCache>();
let pipeline = world.resource::<AutoExposurePipeline>();
let resources = world.resource::<AutoExposureResources>();
let view_uniforms_resource = world.resource::<ViewUniforms>();
let view_uniforms = &view_uniforms_resource.uniforms;
let view_uniforms_buffer = view_uniforms.buffer().unwrap();
let globals_buffer = world.resource::<GlobalsBuffer>();
let auto_exposure_buffers = world.resource::<AutoExposureBuffers>();
let (
Ok((view_uniform_offset, view_target, auto_exposure, view)),
Some(auto_exposure_buffers),
) = (
self.query.get_manual(world, view_entity),
auto_exposure_buffers.buffers.get(&view_entity),
)
else {
return Ok(());
};
let (Some(histogram_pipeline), Some(average_pipeline)) = (
pipeline_cache.get_compute_pipeline(auto_exposure.histogram_pipeline),
pipeline_cache.get_compute_pipeline(auto_exposure.mean_luminance_pipeline),
) else {
return Ok(());
};
let source = view_target.main_texture_view();
let fallback = world.resource::<FallbackImage>();
let mask = world
.resource::<RenderAssets<GpuImage>>()
.get(&auto_exposure.metering_mask);
let mask = mask
.map(|i| &i.texture_view)
.unwrap_or(&fallback.d2.texture_view);
let Some(compensation_curve) = world
.resource::<RenderAssets<GpuAutoExposureCompensationCurve>>()
.get(&auto_exposure.compensation_curve)
else {
return Ok(());
};
let compute_bind_group = render_context.render_device().create_bind_group(
None,
&pipeline.histogram_layout,
&BindGroupEntries::sequential((
&globals_buffer.buffer,
&auto_exposure_buffers.settings,
source,
mask,
&compensation_curve.texture_view,
&compensation_curve.extents,
resources.histogram.as_entire_buffer_binding(),
&auto_exposure_buffers.state,
BufferBinding {
buffer: view_uniforms_buffer,
size: Some(ViewUniform::min_size()),
offset: 0,
},
)),
);
let mut compute_pass =
render_context
.command_encoder()
.begin_compute_pass(&ComputePassDescriptor {
label: Some("auto_exposure_pass"),
timestamp_writes: None,
});
compute_pass.set_bind_group(0, &compute_bind_group, &[view_uniform_offset.offset]);
compute_pass.set_pipeline(histogram_pipeline);
compute_pass.dispatch_workgroups(
view.viewport.z.div_ceil(16),
view.viewport.w.div_ceil(16),
1,
);
compute_pass.set_pipeline(average_pipeline);
compute_pass.dispatch_workgroups(1, 1, 1);
Ok(())
}
}

View File

@@ -0,0 +1,96 @@
use super::compensation_curve::{
AutoExposureCompensationCurve, AutoExposureCompensationCurveUniform,
};
use bevy_asset::{prelude::*, weak_handle};
use bevy_ecs::prelude::*;
use bevy_image::Image;
use bevy_render::{
globals::GlobalsUniform,
render_resource::{binding_types::*, *},
renderer::RenderDevice,
view::ViewUniform,
};
use core::num::NonZero;
#[derive(Resource)]
pub struct AutoExposurePipeline {
pub histogram_layout: BindGroupLayout,
pub histogram_shader: Handle<Shader>,
}
#[derive(Component)]
pub struct ViewAutoExposurePipeline {
pub histogram_pipeline: CachedComputePipelineId,
pub mean_luminance_pipeline: CachedComputePipelineId,
pub compensation_curve: Handle<AutoExposureCompensationCurve>,
pub metering_mask: Handle<Image>,
}
#[derive(ShaderType, Clone, Copy)]
pub struct AutoExposureUniform {
pub(super) min_log_lum: f32,
pub(super) inv_log_lum_range: f32,
pub(super) log_lum_range: f32,
pub(super) low_percent: f32,
pub(super) high_percent: f32,
pub(super) speed_up: f32,
pub(super) speed_down: f32,
pub(super) exponential_transition_distance: f32,
}
#[derive(PartialEq, Eq, Hash, Clone)]
pub enum AutoExposurePass {
Histogram,
Average,
}
pub const METERING_SHADER_HANDLE: Handle<Shader> =
weak_handle!("05c84384-afa4-41d9-844e-e9cd5e7609af");
pub const HISTOGRAM_BIN_COUNT: u64 = 64;
impl FromWorld for AutoExposurePipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
Self {
histogram_layout: render_device.create_bind_group_layout(
"compute histogram bind group",
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
uniform_buffer::<GlobalsUniform>(false),
uniform_buffer::<AutoExposureUniform>(false),
texture_2d(TextureSampleType::Float { filterable: false }),
texture_2d(TextureSampleType::Float { filterable: false }),
texture_1d(TextureSampleType::Float { filterable: false }),
uniform_buffer::<AutoExposureCompensationCurveUniform>(false),
storage_buffer_sized(false, NonZero::<u64>::new(HISTOGRAM_BIN_COUNT * 4)),
storage_buffer_sized(false, NonZero::<u64>::new(4)),
storage_buffer::<ViewUniform>(true),
),
),
),
histogram_shader: METERING_SHADER_HANDLE.clone(),
}
}
}
impl SpecializedComputePipeline for AutoExposurePipeline {
type Key = AutoExposurePass;
fn specialize(&self, pass: AutoExposurePass) -> ComputePipelineDescriptor {
ComputePipelineDescriptor {
label: Some("luminance compute pipeline".into()),
layout: vec![self.histogram_layout.clone()],
shader: self.histogram_shader.clone(),
shader_defs: vec![],
entry_point: match pass {
AutoExposurePass::Histogram => "compute_histogram".into(),
AutoExposurePass::Average => "compute_average".into(),
},
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
}
}
}

View File

@@ -0,0 +1,103 @@
use core::ops::RangeInclusive;
use super::compensation_curve::AutoExposureCompensationCurve;
use bevy_asset::Handle;
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_image::Image;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::extract_component::ExtractComponent;
use bevy_utils::default;
/// Component that enables auto exposure for an HDR-enabled 2d or 3d camera.
///
/// Auto exposure adjusts the exposure of the camera automatically to
/// simulate the human eye's ability to adapt to different lighting conditions.
///
/// Bevy's implementation builds a 64 bin histogram of the scene's luminance,
/// and then adjusts the exposure so that the average brightness of the final
/// render will be middle gray. Because it's using a histogram, some details can
/// be selectively ignored or emphasized. Outliers like shadows and specular
/// highlights can be ignored, and certain areas can be given more (or less)
/// weight based on a mask.
///
/// # Usage Notes
///
/// **Auto Exposure requires compute shaders and is not compatible with WebGL2.**
#[derive(Component, Clone, Reflect, ExtractComponent)]
#[reflect(Component, Default, Clone)]
pub struct AutoExposure {
/// The range of exposure values for the histogram.
///
/// Pixel values below this range will be ignored, and pixel values above this range will be
/// clamped in the sense that they will count towards the highest bin in the histogram.
/// The default value is `-8.0..=8.0`.
pub range: RangeInclusive<f32>,
/// The portion of the histogram to consider when metering.
///
/// By default, the darkest 10% and the brightest 10% of samples are ignored,
/// so the default value is `0.10..=0.90`.
pub filter: RangeInclusive<f32>,
/// The speed at which the exposure adapts from dark to bright scenes, in F-stops per second.
pub speed_brighten: f32,
/// The speed at which the exposure adapts from bright to dark scenes, in F-stops per second.
pub speed_darken: f32,
/// The distance in F-stops from the target exposure from where to transition from animating
/// in linear fashion to animating exponentially. This helps against jittering when the
/// target exposure keeps on changing slightly from frame to frame, while still maintaining
/// a relatively slow animation for big changes in scene brightness.
///
/// ```text
/// ev
/// ➔●┐
/// | ⬈ ├ exponential section
/// │ ⬈ ┘
/// │ ⬈ ┐
/// │ ⬈ ├ linear section
/// │⬈ ┘
/// ●───────────────────────── time
/// ```
///
/// The default value is 1.5.
pub exponential_transition_distance: f32,
/// The mask to apply when metering. The mask will cover the entire screen, where:
/// * `(0.0, 0.0)` is the top-left corner,
/// * `(1.0, 1.0)` is the bottom-right corner.
///
/// Only the red channel of the texture is used.
/// The sample at the current screen position will be used to weight the contribution
/// of each pixel to the histogram:
/// * 0.0 means the pixel will not contribute to the histogram,
/// * 1.0 means the pixel will contribute fully to the histogram.
///
/// The default value is a white image, so all pixels contribute equally.
///
/// # Usage Notes
///
/// The mask is quantized to 16 discrete levels because of limitations in the compute shader
/// implementation.
pub metering_mask: Handle<Image>,
/// Exposure compensation curve to apply after metering.
/// The default value is a flat line at 0.0.
/// For more information, see [`AutoExposureCompensationCurve`].
pub compensation_curve: Handle<AutoExposureCompensationCurve>,
}
impl Default for AutoExposure {
fn default() -> Self {
Self {
range: -8.0..=8.0,
filter: 0.10..=0.90,
speed_brighten: 3.0,
speed_darken: 1.0,
exponential_transition_distance: 1.5,
metering_mask: default(),
compensation_curve: default(),
}
}
}

View File

@@ -0,0 +1,9 @@
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
@group(0) @binding(0) var in_texture: texture_2d<f32>;
@group(0) @binding(1) var in_sampler: sampler;
@fragment
fn fs_main(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
return textureSample(in_texture, in_sampler, in.uv);
}

View File

@@ -0,0 +1,104 @@
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_ecs::prelude::*;
use bevy_render::{
render_resource::{
binding_types::{sampler, texture_2d},
*,
},
renderer::RenderDevice,
RenderApp,
};
use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state;
pub const BLIT_SHADER_HANDLE: Handle<Shader> = weak_handle!("59be3075-c34e-43e7-bf24-c8fe21a0192e");
/// Adds support for specialized "blit pipelines", which can be used to write one texture to another.
pub struct BlitPlugin;
impl Plugin for BlitPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, BLIT_SHADER_HANDLE, "blit.wgsl", Shader::from_wgsl);
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.allow_ambiguous_resource::<SpecializedRenderPipelines<BlitPipeline>>();
}
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<BlitPipeline>()
.init_resource::<SpecializedRenderPipelines<BlitPipeline>>();
}
}
#[derive(Resource)]
pub struct BlitPipeline {
pub texture_bind_group: BindGroupLayout,
pub sampler: Sampler,
}
impl FromWorld for BlitPipeline {
fn from_world(render_world: &mut World) -> Self {
let render_device = render_world.resource::<RenderDevice>();
let texture_bind_group = render_device.create_bind_group_layout(
"blit_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: false }),
sampler(SamplerBindingType::NonFiltering),
),
),
);
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
BlitPipeline {
texture_bind_group,
sampler,
}
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
pub struct BlitPipelineKey {
pub texture_format: TextureFormat,
pub blend_state: Option<BlendState>,
pub samples: u32,
}
impl SpecializedRenderPipeline for BlitPipeline {
type Key = BlitPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("blit pipeline".into()),
layout: vec![self.texture_bind_group.clone()],
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: BLIT_SHADER_HANDLE,
shader_defs: vec![],
entry_point: "fs_main".into(),
targets: vec![Some(ColorTargetState {
format: key.texture_format,
blend: key.blend_state,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState {
count: key.samples,
..Default::default()
},
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
}
}
}

View File

@@ -0,0 +1,187 @@
// Bloom works by creating an intermediate texture with a bunch of mip levels, each half the size of the previous.
// You then downsample each mip (starting with the original texture) to the lower resolution mip under it, going in order.
// You then upsample each mip (starting from the smallest mip) and blend with the higher resolution mip above it (ending on the original texture).
//
// References:
// * [COD] - Next Generation Post Processing in Call of Duty - http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare
// * [PBB] - Physically Based Bloom - https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom
struct BloomUniforms {
threshold_precomputations: vec4<f32>,
viewport: vec4<f32>,
scale: vec2<f32>,
aspect: f32,
};
@group(0) @binding(0) var input_texture: texture_2d<f32>;
@group(0) @binding(1) var s: sampler;
@group(0) @binding(2) var<uniform> uniforms: BloomUniforms;
#ifdef FIRST_DOWNSAMPLE
// https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4
fn soft_threshold(color: vec3<f32>) -> vec3<f32> {
let brightness = max(color.r, max(color.g, color.b));
var softness = brightness - uniforms.threshold_precomputations.y;
softness = clamp(softness, 0.0, uniforms.threshold_precomputations.z);
softness = softness * softness * uniforms.threshold_precomputations.w;
var contribution = max(brightness - uniforms.threshold_precomputations.x, softness);
contribution /= max(brightness, 0.00001); // Prevent division by 0
return color * contribution;
}
#endif
// luminance coefficients from Rec. 709.
// https://en.wikipedia.org/wiki/Rec._709
fn tonemapping_luminance(v: vec3<f32>) -> f32 {
return dot(v, vec3<f32>(0.2126, 0.7152, 0.0722));
}
fn rgb_to_srgb_simple(color: vec3<f32>) -> vec3<f32> {
return pow(color, vec3<f32>(1.0 / 2.2));
}
// http://graphicrants.blogspot.com/2013/12/tone-mapping.html
fn karis_average(color: vec3<f32>) -> f32 {
// Luminance calculated by gamma-correcting linear RGB to non-linear sRGB using pow(color, 1.0 / 2.2)
// and then calculating luminance based on Rec. 709 color primaries.
let luma = tonemapping_luminance(rgb_to_srgb_simple(color)) / 4.0;
return 1.0 / (1.0 + luma);
}
// [COD] slide 153
fn sample_input_13_tap(uv: vec2<f32>) -> vec3<f32> {
#ifdef UNIFORM_SCALE
// This is the fast path. When the bloom scale is uniform, the 13 tap sampling kernel can be
// expressed with constant offsets.
//
// It's possible that this isn't meaningfully faster than the "slow" path. However, because it
// is hard to test performance on all platforms, and uniform bloom is the most common case, this
// path was retained when adding non-uniform (anamorphic) bloom. This adds a small, but nonzero,
// cost to maintainability, but it does help me sleep at night.
let a = textureSample(input_texture, s, uv, vec2<i32>(-2, 2)).rgb;
let b = textureSample(input_texture, s, uv, vec2<i32>(0, 2)).rgb;
let c = textureSample(input_texture, s, uv, vec2<i32>(2, 2)).rgb;
let d = textureSample(input_texture, s, uv, vec2<i32>(-2, 0)).rgb;
let e = textureSample(input_texture, s, uv).rgb;
let f = textureSample(input_texture, s, uv, vec2<i32>(2, 0)).rgb;
let g = textureSample(input_texture, s, uv, vec2<i32>(-2, -2)).rgb;
let h = textureSample(input_texture, s, uv, vec2<i32>(0, -2)).rgb;
let i = textureSample(input_texture, s, uv, vec2<i32>(2, -2)).rgb;
let j = textureSample(input_texture, s, uv, vec2<i32>(-1, 1)).rgb;
let k = textureSample(input_texture, s, uv, vec2<i32>(1, 1)).rgb;
let l = textureSample(input_texture, s, uv, vec2<i32>(-1, -1)).rgb;
let m = textureSample(input_texture, s, uv, vec2<i32>(1, -1)).rgb;
#else
// This is the flexible, but potentially slower, path for non-uniform sampling. Because the
// sample is not a constant, and it can fall outside of the limits imposed on constant sample
// offsets (-8..8), we have to compute the pixel offset in uv coordinates using the size of the
// texture.
//
// It isn't clear if this is meaningfully slower than using the offset syntax, the spec doesn't
// mention it anywhere: https://www.w3.org/TR/WGSL/#texturesample, but the fact that the offset
// syntax uses a const-expr implies that it allows some compiler optimizations - maybe more
// impactful on mobile?
let scale = uniforms.scale;
let ps = scale / vec2<f32>(textureDimensions(input_texture));
let pl = 2.0 * ps;
let ns = -1.0 * ps;
let nl = -2.0 * ps;
let a = textureSample(input_texture, s, uv + vec2<f32>(nl.x, pl.y)).rgb;
let b = textureSample(input_texture, s, uv + vec2<f32>(0.00, pl.y)).rgb;
let c = textureSample(input_texture, s, uv + vec2<f32>(pl.x, pl.y)).rgb;
let d = textureSample(input_texture, s, uv + vec2<f32>(nl.x, 0.00)).rgb;
let e = textureSample(input_texture, s, uv).rgb;
let f = textureSample(input_texture, s, uv + vec2<f32>(pl.x, 0.00)).rgb;
let g = textureSample(input_texture, s, uv + vec2<f32>(nl.x, nl.y)).rgb;
let h = textureSample(input_texture, s, uv + vec2<f32>(0.00, nl.y)).rgb;
let i = textureSample(input_texture, s, uv + vec2<f32>(pl.x, nl.y)).rgb;
let j = textureSample(input_texture, s, uv + vec2<f32>(ns.x, ps.y)).rgb;
let k = textureSample(input_texture, s, uv + vec2<f32>(ps.x, ps.y)).rgb;
let l = textureSample(input_texture, s, uv + vec2<f32>(ns.x, ns.y)).rgb;
let m = textureSample(input_texture, s, uv + vec2<f32>(ps.x, ns.y)).rgb;
#endif
#ifdef FIRST_DOWNSAMPLE
// [COD] slide 168
//
// The first downsample pass reads from the rendered frame which may exhibit
// 'fireflies' (individual very bright pixels) that should not cause the bloom effect.
//
// The first downsample uses a firefly-reduction method proposed by Brian Karis
// which takes a weighted-average of the samples to limit their luma range to [0, 1].
// This implementation matches the LearnOpenGL article [PBB].
var group0 = (a + b + d + e) * (0.125f / 4.0f);
var group1 = (b + c + e + f) * (0.125f / 4.0f);
var group2 = (d + e + g + h) * (0.125f / 4.0f);
var group3 = (e + f + h + i) * (0.125f / 4.0f);
var group4 = (j + k + l + m) * (0.5f / 4.0f);
group0 *= karis_average(group0);
group1 *= karis_average(group1);
group2 *= karis_average(group2);
group3 *= karis_average(group3);
group4 *= karis_average(group4);
return group0 + group1 + group2 + group3 + group4;
#else
var sample = (a + c + g + i) * 0.03125;
sample += (b + d + f + h) * 0.0625;
sample += (e + j + k + l + m) * 0.125;
return sample;
#endif
}
// [COD] slide 162
fn sample_input_3x3_tent(uv: vec2<f32>) -> vec3<f32> {
// While this is probably technically incorrect, it makes nonuniform bloom smoother, without
// having any impact on uniform bloom, which simply evaluates to 1.0 here.
let frag_size = uniforms.scale / vec2<f32>(textureDimensions(input_texture));
let x = frag_size.x;
let y = frag_size.y;
let a = textureSample(input_texture, s, vec2<f32>(uv.x - x, uv.y + y)).rgb;
let b = textureSample(input_texture, s, vec2<f32>(uv.x, uv.y + y)).rgb;
let c = textureSample(input_texture, s, vec2<f32>(uv.x + x, uv.y + y)).rgb;
let d = textureSample(input_texture, s, vec2<f32>(uv.x - x, uv.y)).rgb;
let e = textureSample(input_texture, s, vec2<f32>(uv.x, uv.y)).rgb;
let f = textureSample(input_texture, s, vec2<f32>(uv.x + x, uv.y)).rgb;
let g = textureSample(input_texture, s, vec2<f32>(uv.x - x, uv.y - y)).rgb;
let h = textureSample(input_texture, s, vec2<f32>(uv.x, uv.y - y)).rgb;
let i = textureSample(input_texture, s, vec2<f32>(uv.x + x, uv.y - y)).rgb;
var sample = e * 0.25;
sample += (b + d + f + h) * 0.125;
sample += (a + c + g + i) * 0.0625;
return sample;
}
#ifdef FIRST_DOWNSAMPLE
@fragment
fn downsample_first(@location(0) output_uv: vec2<f32>) -> @location(0) vec4<f32> {
let sample_uv = uniforms.viewport.xy + output_uv * uniforms.viewport.zw;
var sample = sample_input_13_tap(sample_uv);
// Lower bound of 0.0001 is to avoid propagating multiplying by 0.0 through the
// downscaling and upscaling which would result in black boxes.
// The upper bound is to prevent NaNs.
// with f32::MAX (E+38) Chrome fails with ":value 340282346999999984391321947108527833088.0 cannot be represented as 'f32'"
sample = clamp(sample, vec3<f32>(0.0001), vec3<f32>(3.40282347E+37));
#ifdef USE_THRESHOLD
sample = soft_threshold(sample);
#endif
return vec4<f32>(sample, 1.0);
}
#endif
@fragment
fn downsample(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
return vec4<f32>(sample_input_13_tap(uv), 1.0);
}
@fragment
fn upsample(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
return vec4<f32>(sample_input_3x3_tent(uv), 1.0);
}

View File

@@ -0,0 +1,178 @@
use super::{Bloom, BLOOM_SHADER_HANDLE, BLOOM_TEXTURE_FORMAT};
use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state;
use bevy_ecs::{
prelude::{Component, Entity},
resource::Resource,
system::{Commands, Query, Res, ResMut},
world::{FromWorld, World},
};
use bevy_math::{Vec2, Vec4};
use bevy_render::{
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
*,
},
renderer::RenderDevice,
};
#[derive(Component)]
pub struct BloomDownsamplingPipelineIds {
pub main: CachedRenderPipelineId,
pub first: CachedRenderPipelineId,
}
#[derive(Resource)]
pub struct BloomDownsamplingPipeline {
/// Layout with a texture, a sampler, and uniforms
pub bind_group_layout: BindGroupLayout,
pub sampler: Sampler,
}
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct BloomDownsamplingPipelineKeys {
prefilter: bool,
first_downsample: bool,
uniform_scale: bool,
}
/// The uniform struct extracted from [`Bloom`] attached to a Camera.
/// Will be available for use in the Bloom shader.
#[derive(Component, ShaderType, Clone)]
pub struct BloomUniforms {
// Precomputed values used when thresholding, see https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4
pub threshold_precomputations: Vec4,
pub viewport: Vec4,
pub scale: Vec2,
pub aspect: f32,
}
impl FromWorld for BloomDownsamplingPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
// Bind group layout
let bind_group_layout = render_device.create_bind_group_layout(
"bloom_downsampling_bind_group_layout_with_settings",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
// Input texture binding
texture_2d(TextureSampleType::Float { filterable: true }),
// Sampler binding
sampler(SamplerBindingType::Filtering),
// Downsampling settings binding
uniform_buffer::<BloomUniforms>(true),
),
),
);
// Sampler
let sampler = render_device.create_sampler(&SamplerDescriptor {
min_filter: FilterMode::Linear,
mag_filter: FilterMode::Linear,
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
..Default::default()
});
BloomDownsamplingPipeline {
bind_group_layout,
sampler,
}
}
}
impl SpecializedRenderPipeline for BloomDownsamplingPipeline {
type Key = BloomDownsamplingPipelineKeys;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let layout = vec![self.bind_group_layout.clone()];
let entry_point = if key.first_downsample {
"downsample_first".into()
} else {
"downsample".into()
};
let mut shader_defs = vec![];
if key.first_downsample {
shader_defs.push("FIRST_DOWNSAMPLE".into());
}
if key.prefilter {
shader_defs.push("USE_THRESHOLD".into());
}
if key.uniform_scale {
shader_defs.push("UNIFORM_SCALE".into());
}
RenderPipelineDescriptor {
label: Some(
if key.first_downsample {
"bloom_downsampling_pipeline_first"
} else {
"bloom_downsampling_pipeline"
}
.into(),
),
layout,
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: BLOOM_SHADER_HANDLE,
shader_defs,
entry_point,
targets: vec![Some(ColorTargetState {
format: BLOOM_TEXTURE_FORMAT,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
}
}
}
pub fn prepare_downsampling_pipeline(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<BloomDownsamplingPipeline>>,
pipeline: Res<BloomDownsamplingPipeline>,
views: Query<(Entity, &Bloom)>,
) {
for (entity, bloom) in &views {
let prefilter = bloom.prefilter.threshold > 0.0;
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&pipeline,
BloomDownsamplingPipelineKeys {
prefilter,
first_downsample: false,
uniform_scale: bloom.scale == Vec2::ONE,
},
);
let pipeline_first_id = pipelines.specialize(
&pipeline_cache,
&pipeline,
BloomDownsamplingPipelineKeys {
prefilter,
first_downsample: true,
uniform_scale: bloom.scale == Vec2::ONE,
},
);
commands
.entity(entity)
.insert(BloomDownsamplingPipelineIds {
first: pipeline_first_id,
main: pipeline_id,
});
}
}

View File

@@ -0,0 +1,498 @@
mod downsampling_pipeline;
mod settings;
mod upsampling_pipeline;
pub use settings::{Bloom, BloomCompositeMode, BloomPrefilter};
use crate::{
core_2d::graph::{Core2d, Node2d},
core_3d::graph::{Core3d, Node3d},
};
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_color::{Gray, LinearRgba};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_math::{ops, UVec2};
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
extract_component::{
ComponentUniforms, DynamicUniformIndex, ExtractComponentPlugin, UniformComponentPlugin,
},
render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
render_resource::*,
renderer::{RenderContext, RenderDevice},
texture::{CachedTexture, TextureCache},
view::ViewTarget,
Render, RenderApp, RenderSet,
};
use downsampling_pipeline::{
prepare_downsampling_pipeline, BloomDownsamplingPipeline, BloomDownsamplingPipelineIds,
BloomUniforms,
};
#[cfg(feature = "trace")]
use tracing::info_span;
use upsampling_pipeline::{
prepare_upsampling_pipeline, BloomUpsamplingPipeline, UpsamplingPipelineIds,
};
const BLOOM_SHADER_HANDLE: Handle<Shader> = weak_handle!("c9190ddc-573b-4472-8b21-573cab502b73");
const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Ufloat;
pub struct BloomPlugin;
impl Plugin for BloomPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, BLOOM_SHADER_HANDLE, "bloom.wgsl", Shader::from_wgsl);
app.register_type::<Bloom>();
app.register_type::<BloomPrefilter>();
app.register_type::<BloomCompositeMode>();
app.add_plugins((
ExtractComponentPlugin::<Bloom>::default(),
UniformComponentPlugin::<BloomUniforms>::default(),
));
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedRenderPipelines<BloomDownsamplingPipeline>>()
.init_resource::<SpecializedRenderPipelines<BloomUpsamplingPipeline>>()
.add_systems(
Render,
(
prepare_downsampling_pipeline.in_set(RenderSet::Prepare),
prepare_upsampling_pipeline.in_set(RenderSet::Prepare),
prepare_bloom_textures.in_set(RenderSet::PrepareResources),
prepare_bloom_bind_groups.in_set(RenderSet::PrepareBindGroups),
),
)
// Add bloom to the 3d render graph
.add_render_graph_node::<ViewNodeRunner<BloomNode>>(Core3d, Node3d::Bloom)
.add_render_graph_edges(
Core3d,
(Node3d::EndMainPass, Node3d::Bloom, Node3d::Tonemapping),
)
// Add bloom to the 2d render graph
.add_render_graph_node::<ViewNodeRunner<BloomNode>>(Core2d, Node2d::Bloom)
.add_render_graph_edges(
Core2d,
(Node2d::EndMainPass, Node2d::Bloom, Node2d::Tonemapping),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<BloomDownsamplingPipeline>()
.init_resource::<BloomUpsamplingPipeline>();
}
}
#[derive(Default)]
struct BloomNode;
impl ViewNode for BloomNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ViewTarget,
&'static BloomTexture,
&'static BloomBindGroups,
&'static DynamicUniformIndex<BloomUniforms>,
&'static Bloom,
&'static UpsamplingPipelineIds,
&'static BloomDownsamplingPipelineIds,
);
// Atypically for a post-processing effect, we do not need to
// use a secondary texture normally provided by view_target.post_process_write(),
// instead we write into our own bloom texture and then directly back onto main.
fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(
camera,
view_target,
bloom_texture,
bind_groups,
uniform_index,
bloom_settings,
upsampling_pipeline_ids,
downsampling_pipeline_ids,
): QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
if bloom_settings.intensity == 0.0 {
return Ok(());
}
let downsampling_pipeline_res = world.resource::<BloomDownsamplingPipeline>();
let pipeline_cache = world.resource::<PipelineCache>();
let uniforms = world.resource::<ComponentUniforms<BloomUniforms>>();
let (
Some(uniforms),
Some(downsampling_first_pipeline),
Some(downsampling_pipeline),
Some(upsampling_pipeline),
Some(upsampling_final_pipeline),
) = (
uniforms.binding(),
pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.first),
pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.main),
pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_main),
pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_final),
)
else {
return Ok(());
};
let view_texture = view_target.main_texture_view();
let view_texture_unsampled = view_target.get_unsampled_color_attachment();
let diagnostics = render_context.diagnostic_recorder();
render_context.add_command_buffer_generation_task(move |render_device| {
#[cfg(feature = "trace")]
let _bloom_span = info_span!("bloom").entered();
let mut command_encoder =
render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("bloom_command_encoder"),
});
command_encoder.push_debug_group("bloom");
let time_span = diagnostics.time_span(&mut command_encoder, "bloom");
// First downsample pass
{
let downsampling_first_bind_group = render_device.create_bind_group(
"bloom_downsampling_first_bind_group",
&downsampling_pipeline_res.bind_group_layout,
&BindGroupEntries::sequential((
// Read from main texture directly
view_texture,
&bind_groups.sampler,
uniforms.clone(),
)),
);
let view = &bloom_texture.view(0);
let mut downsampling_first_pass =
command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("bloom_downsampling_first_pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view,
resolve_target: None,
ops: Operations::default(),
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
downsampling_first_pass.set_pipeline(downsampling_first_pipeline);
downsampling_first_pass.set_bind_group(
0,
&downsampling_first_bind_group,
&[uniform_index.index()],
);
downsampling_first_pass.draw(0..3, 0..1);
}
// Other downsample passes
for mip in 1..bloom_texture.mip_count {
let view = &bloom_texture.view(mip);
let mut downsampling_pass =
command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("bloom_downsampling_pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view,
resolve_target: None,
ops: Operations::default(),
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
downsampling_pass.set_pipeline(downsampling_pipeline);
downsampling_pass.set_bind_group(
0,
&bind_groups.downsampling_bind_groups[mip as usize - 1],
&[uniform_index.index()],
);
downsampling_pass.draw(0..3, 0..1);
}
// Upsample passes except the final one
for mip in (1..bloom_texture.mip_count).rev() {
let view = &bloom_texture.view(mip - 1);
let mut upsampling_pass =
command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("bloom_upsampling_pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view,
resolve_target: None,
ops: Operations {
load: LoadOp::Load,
store: StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
upsampling_pass.set_pipeline(upsampling_pipeline);
upsampling_pass.set_bind_group(
0,
&bind_groups.upsampling_bind_groups
[(bloom_texture.mip_count - mip - 1) as usize],
&[uniform_index.index()],
);
let blend = compute_blend_factor(
bloom_settings,
mip as f32,
(bloom_texture.mip_count - 1) as f32,
);
upsampling_pass.set_blend_constant(LinearRgba::gray(blend).into());
upsampling_pass.draw(0..3, 0..1);
}
// Final upsample pass
// This is very similar to the above upsampling passes with the only difference
// being the pipeline (which itself is barely different) and the color attachment
{
let mut upsampling_final_pass =
command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("bloom_upsampling_final_pass"),
color_attachments: &[Some(view_texture_unsampled)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
upsampling_final_pass.set_pipeline(upsampling_final_pipeline);
upsampling_final_pass.set_bind_group(
0,
&bind_groups.upsampling_bind_groups[(bloom_texture.mip_count - 1) as usize],
&[uniform_index.index()],
);
if let Some(viewport) = camera.viewport.as_ref() {
upsampling_final_pass.set_viewport(
viewport.physical_position.x as f32,
viewport.physical_position.y as f32,
viewport.physical_size.x as f32,
viewport.physical_size.y as f32,
viewport.depth.start,
viewport.depth.end,
);
}
let blend =
compute_blend_factor(bloom_settings, 0.0, (bloom_texture.mip_count - 1) as f32);
upsampling_final_pass.set_blend_constant(LinearRgba::gray(blend).into());
upsampling_final_pass.draw(0..3, 0..1);
}
time_span.end(&mut command_encoder);
command_encoder.pop_debug_group();
command_encoder.finish()
});
Ok(())
}
}
#[derive(Component)]
struct BloomTexture {
// First mip is half the screen resolution, successive mips are half the previous
#[cfg(any(
not(feature = "webgl"),
not(target_arch = "wasm32"),
feature = "webgpu"
))]
texture: CachedTexture,
// WebGL does not support binding specific mip levels for sampling, fallback to separate textures instead
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
texture: Vec<CachedTexture>,
mip_count: u32,
}
impl BloomTexture {
#[cfg(any(
not(feature = "webgl"),
not(target_arch = "wasm32"),
feature = "webgpu"
))]
fn view(&self, base_mip_level: u32) -> TextureView {
self.texture.texture.create_view(&TextureViewDescriptor {
base_mip_level,
mip_level_count: Some(1u32),
..Default::default()
})
}
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
fn view(&self, base_mip_level: u32) -> TextureView {
self.texture[base_mip_level as usize]
.texture
.create_view(&TextureViewDescriptor {
base_mip_level: 0,
mip_level_count: Some(1u32),
..Default::default()
})
}
}
fn prepare_bloom_textures(
mut commands: Commands,
mut texture_cache: ResMut<TextureCache>,
render_device: Res<RenderDevice>,
views: Query<(Entity, &ExtractedCamera, &Bloom)>,
) {
for (entity, camera, bloom) in &views {
if let Some(UVec2 {
x: width,
y: height,
}) = camera.physical_viewport_size
{
// How many times we can halve the resolution minus one so we don't go unnecessarily low
let mip_count = bloom.max_mip_dimension.ilog2().max(2) - 1;
let mip_height_ratio = if height != 0 {
bloom.max_mip_dimension as f32 / height as f32
} else {
0.
};
let texture_descriptor = TextureDescriptor {
label: Some("bloom_texture"),
size: Extent3d {
width: ((width as f32 * mip_height_ratio).round() as u32).max(1),
height: ((height as f32 * mip_height_ratio).round() as u32).max(1),
depth_or_array_layers: 1,
},
mip_level_count: mip_count,
sample_count: 1,
dimension: TextureDimension::D2,
format: BLOOM_TEXTURE_FORMAT,
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
};
#[cfg(any(
not(feature = "webgl"),
not(target_arch = "wasm32"),
feature = "webgpu"
))]
let texture = texture_cache.get(&render_device, texture_descriptor);
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
let texture: Vec<CachedTexture> = (0..mip_count)
.map(|mip| {
texture_cache.get(
&render_device,
TextureDescriptor {
size: Extent3d {
width: (texture_descriptor.size.width >> mip).max(1),
height: (texture_descriptor.size.height >> mip).max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
..texture_descriptor.clone()
},
)
})
.collect();
commands
.entity(entity)
.insert(BloomTexture { texture, mip_count });
}
}
}
#[derive(Component)]
struct BloomBindGroups {
downsampling_bind_groups: Box<[BindGroup]>,
upsampling_bind_groups: Box<[BindGroup]>,
sampler: Sampler,
}
fn prepare_bloom_bind_groups(
mut commands: Commands,
render_device: Res<RenderDevice>,
downsampling_pipeline: Res<BloomDownsamplingPipeline>,
upsampling_pipeline: Res<BloomUpsamplingPipeline>,
views: Query<(Entity, &BloomTexture)>,
uniforms: Res<ComponentUniforms<BloomUniforms>>,
) {
let sampler = &downsampling_pipeline.sampler;
for (entity, bloom_texture) in &views {
let bind_group_count = bloom_texture.mip_count as usize - 1;
let mut downsampling_bind_groups = Vec::with_capacity(bind_group_count);
for mip in 1..bloom_texture.mip_count {
downsampling_bind_groups.push(render_device.create_bind_group(
"bloom_downsampling_bind_group",
&downsampling_pipeline.bind_group_layout,
&BindGroupEntries::sequential((
&bloom_texture.view(mip - 1),
sampler,
uniforms.binding().unwrap(),
)),
));
}
let mut upsampling_bind_groups = Vec::with_capacity(bind_group_count);
for mip in (0..bloom_texture.mip_count).rev() {
upsampling_bind_groups.push(render_device.create_bind_group(
"bloom_upsampling_bind_group",
&upsampling_pipeline.bind_group_layout,
&BindGroupEntries::sequential((
&bloom_texture.view(mip),
sampler,
uniforms.binding().unwrap(),
)),
));
}
commands.entity(entity).insert(BloomBindGroups {
downsampling_bind_groups: downsampling_bind_groups.into_boxed_slice(),
upsampling_bind_groups: upsampling_bind_groups.into_boxed_slice(),
sampler: sampler.clone(),
});
}
}
/// Calculates blend intensities of blur pyramid levels
/// during the upsampling + compositing stage.
///
/// The function assumes all pyramid levels are upsampled and
/// blended into higher frequency ones using this function to
/// calculate blend levels every time. The final (highest frequency)
/// pyramid level in not blended into anything therefore this function
/// is not applied to it. As a result, the *mip* parameter of 0 indicates
/// the second-highest frequency pyramid level (in our case that is the
/// 0th mip of the bloom texture with the original image being the
/// actual highest frequency level).
///
/// Parameters:
/// * `mip` - the index of the lower frequency pyramid level (0 - `max_mip`, where 0 indicates highest frequency mip but not the highest frequency image).
/// * `max_mip` - the index of the lowest frequency pyramid level.
///
/// This function can be visually previewed for all values of *mip* (normalized) with tweakable
/// [`Bloom`] parameters on [Desmos graphing calculator](https://www.desmos.com/calculator/ncc8xbhzzl).
fn compute_blend_factor(bloom: &Bloom, mip: f32, max_mip: f32) -> f32 {
let mut lf_boost =
(1.0 - ops::powf(
1.0 - (mip / max_mip),
1.0 / (1.0 - bloom.low_frequency_boost_curvature),
)) * bloom.low_frequency_boost;
let high_pass_lq = 1.0
- (((mip / max_mip) - bloom.high_pass_frequency) / bloom.high_pass_frequency)
.clamp(0.0, 1.0);
lf_boost *= match bloom.composite_mode {
BloomCompositeMode::EnergyConserving => 1.0 - bloom.intensity,
BloomCompositeMode::Additive => 1.0,
};
(bloom.intensity + lf_boost) * high_pass_lq
}

View File

@@ -0,0 +1,261 @@
use super::downsampling_pipeline::BloomUniforms;
use bevy_ecs::{prelude::Component, query::QueryItem, reflect::ReflectComponent};
use bevy_math::{AspectRatio, URect, UVec4, Vec2, Vec4};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{extract_component::ExtractComponent, prelude::Camera};
/// Applies a bloom effect to an HDR-enabled 2d or 3d camera.
///
/// Bloom emulates an effect found in real cameras and the human eye,
/// causing halos to appear around very bright parts of the scene.
///
/// See also <https://en.wikipedia.org/wiki/Bloom_(shader_effect)>.
///
/// # Usage Notes
///
/// **Bloom is currently not compatible with WebGL2.**
///
/// Often used in conjunction with `bevy_pbr::StandardMaterial::emissive` for 3d meshes.
///
/// Bloom is best used alongside a tonemapping function that desaturates bright colors,
/// such as [`crate::tonemapping::Tonemapping::TonyMcMapface`].
///
/// Bevy's implementation uses a parametric curve to blend between a set of
/// blurred (lower frequency) images generated from the camera's view.
/// See <https://starlederer.github.io/bloom/> for a visualization of the parametric curve
/// used in Bevy as well as a visualization of the curve's respective scattering profile.
#[derive(Component, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
pub struct Bloom {
/// Controls the baseline of how much the image is scattered (default: 0.15).
///
/// This parameter should be used only to control the strength of the bloom
/// for the scene as a whole. Increasing it too much will make the scene appear
/// blurry and over-exposed.
///
/// To make a mesh glow brighter, rather than increase the bloom intensity,
/// you should increase the mesh's `emissive` value.
///
/// # In energy-conserving mode
/// The value represents how likely the light is to scatter.
///
/// The value should be between 0.0 and 1.0 where:
/// * 0.0 means no bloom
/// * 1.0 means the light is scattered as much as possible
///
/// # In additive mode
/// The value represents how much scattered light is added to
/// the image to create the glow effect.
///
/// In this configuration:
/// * 0.0 means no bloom
/// * Greater than 0.0 means a proportionate amount of scattered light is added
pub intensity: f32,
/// Low frequency contribution boost.
/// Controls how much more likely the light
/// is to scatter completely sideways (low frequency image).
///
/// Comparable to a low shelf boost on an equalizer.
///
/// # In energy-conserving mode
/// The value should be between 0.0 and 1.0 where:
/// * 0.0 means low frequency light uses base intensity for blend factor calculation
/// * 1.0 means low frequency light contributes at full power
///
/// # In additive mode
/// The value represents how much scattered light is added to
/// the image to create the glow effect.
///
/// In this configuration:
/// * 0.0 means no bloom
/// * Greater than 0.0 means a proportionate amount of scattered light is added
pub low_frequency_boost: f32,
/// Low frequency contribution boost curve.
/// Controls the curvature of the blend factor function
/// making frequencies next to the lowest ones contribute more.
///
/// Somewhat comparable to the Q factor of an equalizer node.
///
/// Valid range:
/// * 0.0 - base intensity and boosted intensity are linearly interpolated
/// * 1.0 - all frequencies below maximum are at boosted intensity level
pub low_frequency_boost_curvature: f32,
/// Tightens how much the light scatters (default: 1.0).
///
/// Valid range:
/// * 0.0 - maximum scattering angle is 0 degrees (no scattering)
/// * 1.0 - maximum scattering angle is 90 degrees
pub high_pass_frequency: f32,
/// Controls the threshold filter used for extracting the brightest regions from the input image
/// before blurring them and compositing back onto the original image.
///
/// Changing these settings creates a physically inaccurate image and makes it easy to make
/// the final result look worse. However, they can be useful when emulating the 1990s-2000s game look.
/// See [`BloomPrefilter`] for more information.
pub prefilter: BloomPrefilter,
/// Controls whether bloom textures
/// are blended between or added to each other. Useful
/// if image brightening is desired and a must-change
/// if `prefilter` is used.
///
/// # Recommendation
/// Set to [`BloomCompositeMode::Additive`] if `prefilter` is
/// configured in a non-energy-conserving way,
/// otherwise set to [`BloomCompositeMode::EnergyConserving`].
pub composite_mode: BloomCompositeMode,
/// Maximum size of each dimension for the largest mipchain texture used in downscaling/upscaling.
/// Only tweak if you are seeing visual artifacts.
pub max_mip_dimension: u32,
/// Amount to stretch the bloom on each axis. Artistic control, can be used to emulate
/// anamorphic blur by using a large x-value. For large values, you may need to increase
/// [`Bloom::max_mip_dimension`] to reduce sampling artifacts.
pub scale: Vec2,
}
impl Bloom {
const DEFAULT_MAX_MIP_DIMENSION: u32 = 512;
/// The default bloom preset.
///
/// This uses the [`EnergyConserving`](BloomCompositeMode::EnergyConserving) composite mode.
pub const NATURAL: Self = Self {
intensity: 0.15,
low_frequency_boost: 0.7,
low_frequency_boost_curvature: 0.95,
high_pass_frequency: 1.0,
prefilter: BloomPrefilter {
threshold: 0.0,
threshold_softness: 0.0,
},
composite_mode: BloomCompositeMode::EnergyConserving,
max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION,
scale: Vec2::ONE,
};
/// Emulates the look of stylized anamorphic bloom, stretched horizontally.
pub const ANAMORPHIC: Self = Self {
// The larger scale necessitates a larger resolution to reduce artifacts:
max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION * 2,
scale: Vec2::new(4.0, 1.0),
..Self::NATURAL
};
/// A preset that's similar to how older games did bloom.
pub const OLD_SCHOOL: Self = Self {
intensity: 0.05,
low_frequency_boost: 0.7,
low_frequency_boost_curvature: 0.95,
high_pass_frequency: 1.0,
prefilter: BloomPrefilter {
threshold: 0.6,
threshold_softness: 0.2,
},
composite_mode: BloomCompositeMode::Additive,
max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION,
scale: Vec2::ONE,
};
/// A preset that applies a very strong bloom, and blurs the whole screen.
pub const SCREEN_BLUR: Self = Self {
intensity: 1.0,
low_frequency_boost: 0.0,
low_frequency_boost_curvature: 0.0,
high_pass_frequency: 1.0 / 3.0,
prefilter: BloomPrefilter {
threshold: 0.0,
threshold_softness: 0.0,
},
composite_mode: BloomCompositeMode::EnergyConserving,
max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION,
scale: Vec2::ONE,
};
}
impl Default for Bloom {
fn default() -> Self {
Self::NATURAL
}
}
/// Applies a threshold filter to the input image to extract the brightest
/// regions before blurring them and compositing back onto the original image.
/// These settings are useful when emulating the 1990s-2000s game look.
///
/// # Considerations
/// * Changing these settings creates a physically inaccurate image
/// * Changing these settings makes it easy to make the final result look worse
/// * Non-default prefilter settings should be used in conjunction with [`BloomCompositeMode::Additive`]
#[derive(Default, Clone, Reflect)]
#[reflect(Clone, Default)]
pub struct BloomPrefilter {
/// Baseline of the quadratic threshold curve (default: 0.0).
///
/// RGB values under the threshold curve will not contribute to the effect.
pub threshold: f32,
/// Controls how much to blend between the thresholded and non-thresholded colors (default: 0.0).
///
/// 0.0 = Abrupt threshold, no blending
/// 1.0 = Fully soft threshold
///
/// Values outside of the range [0.0, 1.0] will be clamped.
pub threshold_softness: f32,
}
#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, Copy)]
#[reflect(Clone, Hash, PartialEq)]
pub enum BloomCompositeMode {
EnergyConserving,
Additive,
}
impl ExtractComponent for Bloom {
type QueryData = (&'static Self, &'static Camera);
type QueryFilter = ();
type Out = (Self, BloomUniforms);
fn extract_component((bloom, camera): QueryItem<'_, Self::QueryData>) -> Option<Self::Out> {
match (
camera.physical_viewport_rect(),
camera.physical_viewport_size(),
camera.physical_target_size(),
camera.is_active,
camera.hdr,
) {
(Some(URect { min: origin, .. }), Some(size), Some(target_size), true, true)
if size.x != 0 && size.y != 0 =>
{
let threshold = bloom.prefilter.threshold;
let threshold_softness = bloom.prefilter.threshold_softness;
let knee = threshold * threshold_softness.clamp(0.0, 1.0);
let uniform = BloomUniforms {
threshold_precomputations: Vec4::new(
threshold,
threshold - knee,
2.0 * knee,
0.25 / (knee + 0.00001),
),
viewport: UVec4::new(origin.x, origin.y, size.x, size.y).as_vec4()
/ UVec4::new(target_size.x, target_size.y, target_size.x, target_size.y)
.as_vec4(),
aspect: AspectRatio::try_from_pixels(size.x, size.y)
.expect("Valid screen size values for Bloom settings")
.ratio(),
scale: bloom.scale,
};
Some((bloom.clone(), uniform))
}
_ => None,
}
}
}

View File

@@ -0,0 +1,164 @@
use super::{
downsampling_pipeline::BloomUniforms, Bloom, BloomCompositeMode, BLOOM_SHADER_HANDLE,
BLOOM_TEXTURE_FORMAT,
};
use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state;
use bevy_ecs::{
prelude::{Component, Entity},
resource::Resource,
system::{Commands, Query, Res, ResMut},
world::{FromWorld, World},
};
use bevy_render::{
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
*,
},
renderer::RenderDevice,
view::ViewTarget,
};
#[derive(Component)]
pub struct UpsamplingPipelineIds {
pub id_main: CachedRenderPipelineId,
pub id_final: CachedRenderPipelineId,
}
#[derive(Resource)]
pub struct BloomUpsamplingPipeline {
pub bind_group_layout: BindGroupLayout,
}
#[derive(PartialEq, Eq, Hash, Clone)]
pub struct BloomUpsamplingPipelineKeys {
composite_mode: BloomCompositeMode,
final_pipeline: bool,
}
impl FromWorld for BloomUpsamplingPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let bind_group_layout = render_device.create_bind_group_layout(
"bloom_upsampling_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
// Input texture
texture_2d(TextureSampleType::Float { filterable: true }),
// Sampler
sampler(SamplerBindingType::Filtering),
// BloomUniforms
uniform_buffer::<BloomUniforms>(true),
),
),
);
BloomUpsamplingPipeline { bind_group_layout }
}
}
impl SpecializedRenderPipeline for BloomUpsamplingPipeline {
type Key = BloomUpsamplingPipelineKeys;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let texture_format = if key.final_pipeline {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
BLOOM_TEXTURE_FORMAT
};
let color_blend = match key.composite_mode {
BloomCompositeMode::EnergyConserving => {
// At the time of developing this we decided to blend our
// blur pyramid levels using native WGPU render pass blend
// constants. They are set in the bloom node's run function.
// This seemed like a good approach at the time which allowed
// us to perform complex calculations for blend levels on the CPU,
// however, we missed the fact that this prevented us from using
// textures to customize bloom appearance on individual parts
// of the screen and create effects such as lens dirt or
// screen blur behind certain UI elements.
//
// TODO: Use alpha instead of blend constants and move
// compute_blend_factor to the shader. The shader
// will likely need to know current mip number or
// mip "angle" (original texture is 0deg, max mip is 90deg)
// so make sure you give it that as a uniform.
// That does have to be provided per each pass unlike other
// uniforms that are set once.
BlendComponent {
src_factor: BlendFactor::Constant,
dst_factor: BlendFactor::OneMinusConstant,
operation: BlendOperation::Add,
}
}
BloomCompositeMode::Additive => BlendComponent {
src_factor: BlendFactor::Constant,
dst_factor: BlendFactor::One,
operation: BlendOperation::Add,
},
};
RenderPipelineDescriptor {
label: Some("bloom_upsampling_pipeline".into()),
layout: vec![self.bind_group_layout.clone()],
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: BLOOM_SHADER_HANDLE,
shader_defs: vec![],
entry_point: "upsample".into(),
targets: vec![Some(ColorTargetState {
format: texture_format,
blend: Some(BlendState {
color: color_blend,
alpha: BlendComponent {
src_factor: BlendFactor::Zero,
dst_factor: BlendFactor::One,
operation: BlendOperation::Add,
},
}),
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
}
}
}
pub fn prepare_upsampling_pipeline(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<BloomUpsamplingPipeline>>,
pipeline: Res<BloomUpsamplingPipeline>,
views: Query<(Entity, &Bloom)>,
) {
for (entity, bloom) in &views {
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&pipeline,
BloomUpsamplingPipelineKeys {
composite_mode: bloom.composite_mode,
final_pipeline: false,
},
);
let pipeline_final_id = pipelines.specialize(
&pipeline_cache,
&pipeline,
BloomUpsamplingPipelineKeys {
composite_mode: bloom.composite_mode,
final_pipeline: true,
},
);
commands.entity(entity).insert(UpsamplingPipelineIds {
id_main: pipeline_id,
id_final: pipeline_final_id,
});
}
}

View File

@@ -0,0 +1,272 @@
use crate::{
core_2d::graph::{Core2d, Node2d},
core_3d::graph::{Core3d, Node3d},
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
};
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_image::BevyDefault as _;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin},
prelude::Camera,
render_graph::RenderGraphApp,
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
*,
},
renderer::RenderDevice,
view::{ExtractedView, ViewTarget},
Render, RenderApp, RenderSet,
};
mod node;
pub use node::CasNode;
/// Applies a contrast adaptive sharpening (CAS) filter to the camera.
///
/// CAS is usually used in combination with shader based anti-aliasing methods
/// such as FXAA or TAA to regain some of the lost detail from the blurring that they introduce.
///
/// CAS is designed to adjust the amount of sharpening applied to different areas of an image
/// based on the local contrast. This can help avoid over-sharpening areas with high contrast
/// and under-sharpening areas with low contrast.
///
/// To use this, add the [`ContrastAdaptiveSharpening`] component to a 2D or 3D camera.
#[derive(Component, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
pub struct ContrastAdaptiveSharpening {
/// Enable or disable sharpening.
pub enabled: bool,
/// Adjusts sharpening strength. Higher values increase the amount of sharpening.
///
/// Clamped between 0.0 and 1.0.
///
/// The default value is 0.6.
pub sharpening_strength: f32,
/// Whether to try and avoid sharpening areas that are already noisy.
///
/// You probably shouldn't use this, and just leave it set to false.
/// You should generally apply any sort of film grain or similar effects after CAS
/// and upscaling to avoid artifacts.
pub denoise: bool,
}
impl Default for ContrastAdaptiveSharpening {
fn default() -> Self {
ContrastAdaptiveSharpening {
enabled: true,
sharpening_strength: 0.6,
denoise: false,
}
}
}
#[derive(Component, Default, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
pub struct DenoiseCas(bool);
/// The uniform struct extracted from [`ContrastAdaptiveSharpening`] attached to a [`Camera`].
/// Will be available for use in the CAS shader.
#[doc(hidden)]
#[derive(Component, ShaderType, Clone)]
pub struct CasUniform {
sharpness: f32,
}
impl ExtractComponent for ContrastAdaptiveSharpening {
type QueryData = &'static Self;
type QueryFilter = With<Camera>;
type Out = (DenoiseCas, CasUniform);
fn extract_component(item: QueryItem<Self::QueryData>) -> Option<Self::Out> {
if !item.enabled || item.sharpening_strength == 0.0 {
return None;
}
Some((
DenoiseCas(item.denoise),
CasUniform {
// above 1.0 causes extreme artifacts and fireflies
sharpness: item.sharpening_strength.clamp(0.0, 1.0),
},
))
}
}
const CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE: Handle<Shader> =
weak_handle!("ef83f0a5-51df-4b51-9ab7-b5fd1ae5a397");
/// Adds Support for Contrast Adaptive Sharpening (CAS).
pub struct CasPlugin;
impl Plugin for CasPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE,
"robust_contrast_adaptive_sharpening.wgsl",
Shader::from_wgsl
);
app.register_type::<ContrastAdaptiveSharpening>();
app.add_plugins((
ExtractComponentPlugin::<ContrastAdaptiveSharpening>::default(),
UniformComponentPlugin::<CasUniform>::default(),
));
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedRenderPipelines<CasPipeline>>()
.add_systems(Render, prepare_cas_pipelines.in_set(RenderSet::Prepare));
{
render_app
.add_render_graph_node::<CasNode>(Core3d, Node3d::ContrastAdaptiveSharpening)
.add_render_graph_edge(
Core3d,
Node3d::Tonemapping,
Node3d::ContrastAdaptiveSharpening,
)
.add_render_graph_edges(
Core3d,
(
Node3d::Fxaa,
Node3d::ContrastAdaptiveSharpening,
Node3d::EndMainPassPostProcessing,
),
);
}
{
render_app
.add_render_graph_node::<CasNode>(Core2d, Node2d::ContrastAdaptiveSharpening)
.add_render_graph_edge(
Core2d,
Node2d::Tonemapping,
Node2d::ContrastAdaptiveSharpening,
)
.add_render_graph_edges(
Core2d,
(
Node2d::Fxaa,
Node2d::ContrastAdaptiveSharpening,
Node2d::EndMainPassPostProcessing,
),
);
}
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<CasPipeline>();
}
}
#[derive(Resource)]
pub struct CasPipeline {
texture_bind_group: BindGroupLayout,
sampler: Sampler,
}
impl FromWorld for CasPipeline {
fn from_world(render_world: &mut World) -> Self {
let render_device = render_world.resource::<RenderDevice>();
let texture_bind_group = render_device.create_bind_group_layout(
"sharpening_texture_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
// CAS Settings
uniform_buffer::<CasUniform>(true),
),
),
);
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
CasPipeline {
texture_bind_group,
sampler,
}
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
pub struct CasPipelineKey {
texture_format: TextureFormat,
denoise: bool,
}
impl SpecializedRenderPipeline for CasPipeline {
type Key = CasPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let mut shader_defs = vec![];
if key.denoise {
shader_defs.push("RCAS_DENOISE".into());
}
RenderPipelineDescriptor {
label: Some("contrast_adaptive_sharpening".into()),
layout: vec![self.texture_bind_group.clone()],
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE,
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: key.texture_format,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
}
}
}
fn prepare_cas_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<CasPipeline>>,
sharpening_pipeline: Res<CasPipeline>,
views: Query<
(Entity, &ExtractedView, &DenoiseCas),
Or<(Added<CasUniform>, Changed<DenoiseCas>)>,
>,
mut removals: RemovedComponents<CasUniform>,
) {
for entity in removals.read() {
commands.entity(entity).remove::<ViewCasPipeline>();
}
for (entity, view, denoise_cas) in &views {
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&sharpening_pipeline,
CasPipelineKey {
denoise: denoise_cas.0,
texture_format: if view.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
},
);
commands.entity(entity).insert(ViewCasPipeline(pipeline_id));
}
}
#[derive(Component)]
pub struct ViewCasPipeline(CachedRenderPipelineId);

View File

@@ -0,0 +1,119 @@
use std::sync::Mutex;
use crate::contrast_adaptive_sharpening::ViewCasPipeline;
use bevy_ecs::prelude::*;
use bevy_render::{
extract_component::{ComponentUniforms, DynamicUniformIndex},
render_graph::{Node, NodeRunError, RenderGraphContext},
render_resource::{
BindGroup, BindGroupEntries, BufferId, Operations, PipelineCache,
RenderPassColorAttachment, RenderPassDescriptor, TextureViewId,
},
renderer::RenderContext,
view::{ExtractedView, ViewTarget},
};
use super::{CasPipeline, CasUniform};
pub struct CasNode {
query: QueryState<
(
&'static ViewTarget,
&'static ViewCasPipeline,
&'static DynamicUniformIndex<CasUniform>,
),
With<ExtractedView>,
>,
cached_bind_group: Mutex<Option<(BufferId, TextureViewId, BindGroup)>>,
}
impl FromWorld for CasNode {
fn from_world(world: &mut World) -> Self {
Self {
query: QueryState::new(world),
cached_bind_group: Mutex::new(None),
}
}
}
impl Node for CasNode {
fn update(&mut self, world: &mut World) {
self.query.update_archetypes(world);
}
fn run(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
world: &World,
) -> Result<(), NodeRunError> {
let view_entity = graph.view_entity();
let pipeline_cache = world.resource::<PipelineCache>();
let sharpening_pipeline = world.resource::<CasPipeline>();
let uniforms = world.resource::<ComponentUniforms<CasUniform>>();
let Ok((target, pipeline, uniform_index)) = self.query.get_manual(world, view_entity)
else {
return Ok(());
};
let uniforms_id = uniforms.buffer().unwrap().id();
let Some(uniforms) = uniforms.binding() else {
return Ok(());
};
let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline.0) else {
return Ok(());
};
let view_target = target.post_process_write();
let source = view_target.source;
let destination = view_target.destination;
let mut cached_bind_group = self.cached_bind_group.lock().unwrap();
let bind_group = match &mut *cached_bind_group {
Some((buffer_id, texture_id, bind_group))
if source.id() == *texture_id && uniforms_id == *buffer_id =>
{
bind_group
}
cached_bind_group => {
let bind_group = render_context.render_device().create_bind_group(
"cas_bind_group",
&sharpening_pipeline.texture_bind_group,
&BindGroupEntries::sequential((
view_target.source,
&sharpening_pipeline.sampler,
uniforms,
)),
);
let (_, _, bind_group) =
cached_bind_group.insert((uniforms_id, source.id(), bind_group));
bind_group
}
};
let pass_descriptor = RenderPassDescriptor {
label: Some("contrast_adaptive_sharpening"),
color_attachments: &[Some(RenderPassColorAttachment {
view: destination,
resolve_target: None,
ops: Operations::default(),
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
};
let mut render_pass = render_context
.command_encoder()
.begin_render_pass(&pass_descriptor);
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, bind_group, &[uniform_index.index()]);
render_pass.draw(0..3, 0..1);
Ok(())
}
}

View File

@@ -0,0 +1,95 @@
// Copyright (c) 2022 Advanced Micro Devices, Inc. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
struct CASUniforms {
sharpness: f32,
};
@group(0) @binding(0) var screenTexture: texture_2d<f32>;
@group(0) @binding(1) var samp: sampler;
@group(0) @binding(2) var<uniform> uniforms: CASUniforms;
// This is set at the limit of providing unnatural results for sharpening.
const FSR_RCAS_LIMIT = 0.1875;
// -4.0 instead of -1.0 to avoid issues with MSAA.
const peakC = vec2<f32>(10.0, -40.0);
// Robust Contrast Adaptive Sharpening (RCAS)
// Based on the following implementation:
// https://github.com/GPUOpen-Effects/FidelityFX-FSR2/blob/ea97a113b0f9cadf519fbcff315cc539915a3acd/src/ffx-fsr2-api/shaders/ffx_fsr1.h#L672
// RCAS is based on the following logic.
// RCAS uses a 5 tap filter in a cross pattern (same as CAS),
// W b
// W 1 W for taps d e f
// W h
// Where 'W' is the negative lobe weight.
// output = (W*(b+d+f+h)+e)/(4*W+1)
// RCAS solves for 'W' by seeing where the signal might clip out of the {0 to 1} input range,
// 0 == (W*(b+d+f+h)+e)/(4*W+1) -> W = -e/(b+d+f+h)
// 1 == (W*(b+d+f+h)+e)/(4*W+1) -> W = (1-e)/(b+d+f+h-4)
// Then chooses the 'W' which results in no clipping, limits 'W', and multiplies by the 'sharp' amount.
// This solution above has issues with MSAA input as the steps along the gradient cause edge detection issues.
// So RCAS uses 4x the maximum and 4x the minimum (depending on equation)in place of the individual taps.
// As well as switching from 'e' to either the minimum or maximum (depending on side), to help in energy conservation.
// This stabilizes RCAS.
// RCAS does a simple highpass which is normalized against the local contrast then shaped,
// 0.25
// 0.25 -1 0.25
// 0.25
// This is used as a noise detection filter, to reduce the effect of RCAS on grain, and focus on real edges.
// The CAS node runs after tonemapping, so the input will be in the range of 0 to 1.
@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
// Algorithm uses minimal 3x3 pixel neighborhood.
// b
// d e f
// h
let b = textureSample(screenTexture, samp, in.uv, vec2<i32>(0, -1)).rgb;
let d = textureSample(screenTexture, samp, in.uv, vec2<i32>(-1, 0)).rgb;
// We need the alpha value of the pixel we're working on for the output
let e = textureSample(screenTexture, samp, in.uv).rgba;
let f = textureSample(screenTexture, samp, in.uv, vec2<i32>(1, 0)).rgb;
let h = textureSample(screenTexture, samp, in.uv, vec2<i32>(0, 1)).rgb;
// Min and max of ring.
let mn4 = min(min(b, d), min(f, h));
let mx4 = max(max(b, d), max(f, h));
// Limiters
// 4.0 to avoid issues with MSAA.
let hitMin = mn4 / (4.0 * mx4);
let hitMax = (peakC.x - mx4) / (peakC.y + 4.0 * mn4);
let lobeRGB = max(-hitMin, hitMax);
var lobe = max(-FSR_RCAS_LIMIT, min(0.0, max(lobeRGB.r, max(lobeRGB.g, lobeRGB.b)))) * uniforms.sharpness;
#ifdef RCAS_DENOISE
// Luma times 2.
let bL = b.b * 0.5 + (b.r * 0.5 + b.g);
let dL = d.b * 0.5 + (d.r * 0.5 + d.g);
let eL = e.b * 0.5 + (e.r * 0.5 + e.g);
let fL = f.b * 0.5 + (f.r * 0.5 + f.g);
let hL = h.b * 0.5 + (h.r * 0.5 + h.g);
// Noise detection.
var noise = 0.25 * bL + 0.25 * dL + 0.25 * fL + 0.25 * hL - eL;;
noise = saturate(abs(noise) / (max(max(bL, dL), max(fL, hL)) - min(min(bL, dL), min(fL, hL))));
noise = 1.0 - 0.5 * noise;
// Apply noise removal.
lobe *= noise;
#endif
return vec4<f32>((lobe * b + lobe * d + lobe * f + lobe * h + e.rgb) / (4.0 * lobe + 1.0), e.w);
}

View File

@@ -0,0 +1,26 @@
use crate::{
core_2d::graph::Core2d,
tonemapping::{DebandDither, Tonemapping},
};
use bevy_ecs::prelude::*;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::{Camera, CameraProjection, CameraRenderGraph, OrthographicProjection, Projection},
extract_component::ExtractComponent,
primitives::Frustum,
};
use bevy_transform::prelude::{GlobalTransform, Transform};
/// A 2D camera component. Enables the 2D render graph for a [`Camera`].
#[derive(Component, Default, Reflect, Clone, ExtractComponent)]
#[extract_component_filter(With<Camera>)]
#[reflect(Component, Default, Clone)]
#[require(
Camera,
DebandDither,
CameraRenderGraph::new(Core2d),
Projection::Orthographic(OrthographicProjection::default_2d()),
Frustum = OrthographicProjection::default_2d().compute_frustum(&GlobalTransform::from(Transform::default())),
Tonemapping::None,
)]
pub struct Camera2d;

View File

@@ -0,0 +1,106 @@
use crate::core_2d::Opaque2d;
use bevy_ecs::{prelude::World, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::{TrackedRenderPass, ViewBinnedRenderPhases},
render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ExtractedView, ViewDepthTexture, ViewTarget},
};
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
use super::AlphaMask2d;
/// A [`bevy_render::render_graph::Node`] that runs the
/// [`Opaque2d`] [`ViewBinnedRenderPhases`] and [`AlphaMask2d`] [`ViewBinnedRenderPhases`]
#[derive(Default)]
pub struct MainOpaquePass2dNode;
impl ViewNode for MainOpaquePass2dNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewTarget,
&'static ViewDepthTexture,
);
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(camera, view, target, depth): QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let (Some(opaque_phases), Some(alpha_mask_phases)) = (
world.get_resource::<ViewBinnedRenderPhases<Opaque2d>>(),
world.get_resource::<ViewBinnedRenderPhases<AlphaMask2d>>(),
) else {
return Ok(());
};
let diagnostics = render_context.diagnostic_recorder();
let color_attachments = [Some(target.get_color_attachment())];
let depth_stencil_attachment = Some(depth.get_attachment(StoreOp::Store));
let view_entity = graph.view_entity();
let (Some(opaque_phase), Some(alpha_mask_phase)) = (
opaque_phases.get(&view.retained_view_entity),
alpha_mask_phases.get(&view.retained_view_entity),
) else {
return Ok(());
};
render_context.add_command_buffer_generation_task(move |render_device| {
#[cfg(feature = "trace")]
let _main_opaque_pass_2d_span = info_span!("main_opaque_pass_2d").entered();
// Command encoder setup
let mut command_encoder =
render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("main_opaque_pass_2d_command_encoder"),
});
// Render pass setup
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("main_opaque_pass_2d"),
color_attachments: &color_attachments,
depth_stencil_attachment,
timestamp_writes: None,
occlusion_query_set: None,
});
let mut render_pass = TrackedRenderPass::new(&render_device, render_pass);
let pass_span = diagnostics.pass_span(&mut render_pass, "main_opaque_pass_2d");
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
// Opaque draws
if !opaque_phase.is_empty() {
#[cfg(feature = "trace")]
let _opaque_main_pass_2d_span = info_span!("opaque_main_pass_2d").entered();
if let Err(err) = opaque_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the 2d opaque phase {err:?}");
}
}
// Alpha mask draws
if !alpha_mask_phase.is_empty() {
#[cfg(feature = "trace")]
let _alpha_mask_main_pass_2d_span = info_span!("alpha_mask_main_pass_2d").entered();
if let Err(err) = alpha_mask_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the 2d alpha mask phase {err:?}");
}
}
pass_span.end(&mut render_pass);
drop(render_pass);
command_encoder.finish()
});
Ok(())
}
}

View File

@@ -0,0 +1,120 @@
use crate::core_2d::Transparent2d;
use bevy_ecs::prelude::*;
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::{TrackedRenderPass, ViewSortedRenderPhases},
render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ExtractedView, ViewDepthTexture, ViewTarget},
};
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
#[derive(Default)]
pub struct MainTransparentPass2dNode {}
impl ViewNode for MainTransparentPass2dNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewTarget,
&'static ViewDepthTexture,
);
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(camera, view, target, depth): bevy_ecs::query::QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let Some(transparent_phases) =
world.get_resource::<ViewSortedRenderPhases<Transparent2d>>()
else {
return Ok(());
};
let view_entity = graph.view_entity();
let Some(transparent_phase) = transparent_phases.get(&view.retained_view_entity) else {
return Ok(());
};
let diagnostics = render_context.diagnostic_recorder();
let color_attachments = [Some(target.get_color_attachment())];
// NOTE: For the transparent pass we load the depth buffer. There should be no
// need to write to it, but store is set to `true` as a workaround for issue #3776,
// https://github.com/bevyengine/bevy/issues/3776
// so that wgpu does not clear the depth buffer.
// As the opaque and alpha mask passes run first, opaque meshes can occlude
// transparent ones.
let depth_stencil_attachment = Some(depth.get_attachment(StoreOp::Store));
render_context.add_command_buffer_generation_task(move |render_device| {
// Command encoder setup
let mut command_encoder =
render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("main_transparent_pass_2d_command_encoder"),
});
// This needs to run at least once to clear the background color, even if there are no items to render
{
#[cfg(feature = "trace")]
let _main_pass_2d = info_span!("main_transparent_pass_2d").entered();
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("main_transparent_pass_2d"),
color_attachments: &color_attachments,
depth_stencil_attachment,
timestamp_writes: None,
occlusion_query_set: None,
});
let mut render_pass = TrackedRenderPass::new(&render_device, render_pass);
let pass_span = diagnostics.pass_span(&mut render_pass, "main_transparent_pass_2d");
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
if !transparent_phase.items.is_empty() {
#[cfg(feature = "trace")]
let _transparent_main_pass_2d_span =
info_span!("transparent_main_pass_2d").entered();
if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity)
{
error!(
"Error encountered while rendering the transparent 2D phase {err:?}"
);
}
}
pass_span.end(&mut render_pass);
}
// WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't
// reset for the next render pass so add an empty render pass without a custom viewport
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
if camera.viewport.is_some() {
#[cfg(feature = "trace")]
let _reset_viewport_pass_2d = info_span!("reset_viewport_pass_2d").entered();
let pass_descriptor = RenderPassDescriptor {
label: Some("reset_viewport_pass_2d"),
color_attachments: &[Some(target.get_color_attachment())],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
};
command_encoder.begin_render_pass(&pass_descriptor);
}
command_encoder.finish()
});
Ok(())
}
}

View File

@@ -0,0 +1,503 @@
mod camera_2d;
mod main_opaque_pass_2d_node;
mod main_transparent_pass_2d_node;
pub mod graph {
use bevy_render::render_graph::{RenderLabel, RenderSubGraph};
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderSubGraph)]
pub struct Core2d;
pub mod input {
pub const VIEW_ENTITY: &str = "view_entity";
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
pub enum Node2d {
MsaaWriteback,
StartMainPass,
MainOpaquePass,
MainTransparentPass,
EndMainPass,
Wireframe,
Bloom,
PostProcessing,
Tonemapping,
Fxaa,
Smaa,
Upscaling,
ContrastAdaptiveSharpening,
EndMainPassPostProcessing,
}
}
use core::ops::Range;
use bevy_asset::UntypedAssetId;
use bevy_platform::collections::{HashMap, HashSet};
use bevy_render::{
batching::gpu_preprocessing::GpuPreprocessingMode,
render_phase::PhaseItemBatchSetKey,
view::{ExtractedView, RetainedViewEntity},
};
pub use camera_2d::*;
pub use main_opaque_pass_2d_node::*;
pub use main_transparent_pass_2d_node::*;
use crate::{tonemapping::TonemappingNode, upscaling::UpscalingNode};
use bevy_app::{App, Plugin};
use bevy_ecs::prelude::*;
use bevy_math::FloatOrd;
use bevy_render::{
camera::{Camera, ExtractedCamera},
extract_component::ExtractComponentPlugin,
render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner},
render_phase::{
sort_phase_system, BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId,
DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewBinnedRenderPhases,
ViewSortedRenderPhases,
},
render_resource::{
BindGroupId, CachedRenderPipelineId, Extent3d, TextureDescriptor, TextureDimension,
TextureFormat, TextureUsages,
},
renderer::RenderDevice,
sync_world::MainEntity,
texture::TextureCache,
view::{Msaa, ViewDepthTexture},
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use self::graph::{Core2d, Node2d};
pub const CORE_2D_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float;
pub struct Core2dPlugin;
impl Plugin for Core2dPlugin {
fn build(&self, app: &mut App) {
app.register_type::<Camera2d>()
.add_plugins(ExtractComponentPlugin::<Camera2d>::default());
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<DrawFunctions<Opaque2d>>()
.init_resource::<DrawFunctions<AlphaMask2d>>()
.init_resource::<DrawFunctions<Transparent2d>>()
.init_resource::<ViewSortedRenderPhases<Transparent2d>>()
.init_resource::<ViewBinnedRenderPhases<Opaque2d>>()
.init_resource::<ViewBinnedRenderPhases<AlphaMask2d>>()
.add_systems(ExtractSchedule, extract_core_2d_camera_phases)
.add_systems(
Render,
(
sort_phase_system::<Transparent2d>.in_set(RenderSet::PhaseSort),
prepare_core_2d_depth_textures.in_set(RenderSet::PrepareResources),
),
);
render_app
.add_render_sub_graph(Core2d)
.add_render_graph_node::<EmptyNode>(Core2d, Node2d::StartMainPass)
.add_render_graph_node::<ViewNodeRunner<MainOpaquePass2dNode>>(
Core2d,
Node2d::MainOpaquePass,
)
.add_render_graph_node::<ViewNodeRunner<MainTransparentPass2dNode>>(
Core2d,
Node2d::MainTransparentPass,
)
.add_render_graph_node::<EmptyNode>(Core2d, Node2d::EndMainPass)
.add_render_graph_node::<ViewNodeRunner<TonemappingNode>>(Core2d, Node2d::Tonemapping)
.add_render_graph_node::<EmptyNode>(Core2d, Node2d::EndMainPassPostProcessing)
.add_render_graph_node::<ViewNodeRunner<UpscalingNode>>(Core2d, Node2d::Upscaling)
.add_render_graph_edges(
Core2d,
(
Node2d::StartMainPass,
Node2d::MainOpaquePass,
Node2d::MainTransparentPass,
Node2d::EndMainPass,
Node2d::Tonemapping,
Node2d::EndMainPassPostProcessing,
Node2d::Upscaling,
),
);
}
}
/// Opaque 2D [`BinnedPhaseItem`]s.
pub struct Opaque2d {
/// Determines which objects can be placed into a *batch set*.
///
/// Objects in a single batch set can potentially be multi-drawn together,
/// if it's enabled and the current platform supports it.
pub batch_set_key: BatchSetKey2d,
/// The key, which determines which can be batched.
pub bin_key: Opaque2dBinKey,
/// An entity from which data will be fetched, including the mesh if
/// applicable.
pub representative_entity: (Entity, MainEntity),
/// The ranges of instances.
pub batch_range: Range<u32>,
/// An extra index, which is either a dynamic offset or an index in the
/// indirect parameters list.
pub extra_index: PhaseItemExtraIndex,
}
/// Data that must be identical in order to batch phase items together.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Opaque2dBinKey {
/// The identifier of the render pipeline.
pub pipeline: CachedRenderPipelineId,
/// The function used to draw.
pub draw_function: DrawFunctionId,
/// The asset that this phase item is associated with.
///
/// Normally, this is the ID of the mesh, but for non-mesh items it might be
/// the ID of another type of asset.
pub asset_id: UntypedAssetId,
/// The ID of a bind group specific to the material.
pub material_bind_group_id: Option<BindGroupId>,
}
impl PhaseItem for Opaque2d {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity.0
}
fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}
#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.bin_key.draw_function
}
#[inline]
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index.clone()
}
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl BinnedPhaseItem for Opaque2d {
// Since 2D meshes presently can't be multidrawn, the batch set key is
// irrelevant.
type BatchSetKey = BatchSetKey2d;
type BinKey = Opaque2dBinKey;
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Opaque2d {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
/// 2D meshes aren't currently multi-drawn together, so this batch set key only
/// stores whether the mesh is indexed.
#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub struct BatchSetKey2d {
/// True if the mesh is indexed.
pub indexed: bool,
}
impl PhaseItemBatchSetKey for BatchSetKey2d {
fn indexed(&self) -> bool {
self.indexed
}
}
impl CachedRenderPipelinePhaseItem for Opaque2d {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.bin_key.pipeline
}
}
/// Alpha mask 2D [`BinnedPhaseItem`]s.
pub struct AlphaMask2d {
/// Determines which objects can be placed into a *batch set*.
///
/// Objects in a single batch set can potentially be multi-drawn together,
/// if it's enabled and the current platform supports it.
pub batch_set_key: BatchSetKey2d,
/// The key, which determines which can be batched.
pub bin_key: AlphaMask2dBinKey,
/// An entity from which data will be fetched, including the mesh if
/// applicable.
pub representative_entity: (Entity, MainEntity),
/// The ranges of instances.
pub batch_range: Range<u32>,
/// An extra index, which is either a dynamic offset or an index in the
/// indirect parameters list.
pub extra_index: PhaseItemExtraIndex,
}
/// Data that must be identical in order to batch phase items together.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AlphaMask2dBinKey {
/// The identifier of the render pipeline.
pub pipeline: CachedRenderPipelineId,
/// The function used to draw.
pub draw_function: DrawFunctionId,
/// The asset that this phase item is associated with.
///
/// Normally, this is the ID of the mesh, but for non-mesh items it might be
/// the ID of another type of asset.
pub asset_id: UntypedAssetId,
/// The ID of a bind group specific to the material.
pub material_bind_group_id: Option<BindGroupId>,
}
impl PhaseItem for AlphaMask2d {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity.0
}
#[inline]
fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}
#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.bin_key.draw_function
}
#[inline]
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index.clone()
}
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl BinnedPhaseItem for AlphaMask2d {
// Since 2D meshes presently can't be multidrawn, the batch set key is
// irrelevant.
type BatchSetKey = BatchSetKey2d;
type BinKey = AlphaMask2dBinKey;
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
AlphaMask2d {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
impl CachedRenderPipelinePhaseItem for AlphaMask2d {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.bin_key.pipeline
}
}
/// Transparent 2D [`SortedPhaseItem`]s.
pub struct Transparent2d {
pub sort_key: FloatOrd,
pub entity: (Entity, MainEntity),
pub pipeline: CachedRenderPipelineId,
pub draw_function: DrawFunctionId,
pub batch_range: Range<u32>,
pub extracted_index: usize,
pub extra_index: PhaseItemExtraIndex,
/// Whether the mesh in question is indexed (uses an index buffer in
/// addition to its vertex buffer).
pub indexed: bool,
}
impl PhaseItem for Transparent2d {
#[inline]
fn entity(&self) -> Entity {
self.entity.0
}
#[inline]
fn main_entity(&self) -> MainEntity {
self.entity.1
}
#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.draw_function
}
#[inline]
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index.clone()
}
#[inline]
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl SortedPhaseItem for Transparent2d {
type SortKey = FloatOrd;
#[inline]
fn sort_key(&self) -> Self::SortKey {
self.sort_key
}
#[inline]
fn sort(items: &mut [Self]) {
// radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`.
radsort::sort_by_key(items, |item| item.sort_key().0);
}
fn indexed(&self) -> bool {
self.indexed
}
}
impl CachedRenderPipelinePhaseItem for Transparent2d {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.pipeline
}
}
pub fn extract_core_2d_camera_phases(
mut transparent_2d_phases: ResMut<ViewSortedRenderPhases<Transparent2d>>,
mut opaque_2d_phases: ResMut<ViewBinnedRenderPhases<Opaque2d>>,
mut alpha_mask_2d_phases: ResMut<ViewBinnedRenderPhases<AlphaMask2d>>,
cameras_2d: Extract<Query<(Entity, &Camera), With<Camera2d>>>,
mut live_entities: Local<HashSet<RetainedViewEntity>>,
) {
live_entities.clear();
for (main_entity, camera) in &cameras_2d {
if !camera.is_active {
continue;
}
// This is the main 2D camera, so we use the first subview index (0).
let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0);
transparent_2d_phases.insert_or_clear(retained_view_entity);
opaque_2d_phases.prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None);
alpha_mask_2d_phases
.prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None);
live_entities.insert(retained_view_entity);
}
// Clear out all dead views.
transparent_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
opaque_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
alpha_mask_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity));
}
pub fn prepare_core_2d_depth_textures(
mut commands: Commands,
mut texture_cache: ResMut<TextureCache>,
render_device: Res<RenderDevice>,
transparent_2d_phases: Res<ViewSortedRenderPhases<Transparent2d>>,
opaque_2d_phases: Res<ViewBinnedRenderPhases<Opaque2d>>,
views_2d: Query<(Entity, &ExtractedCamera, &ExtractedView, &Msaa), (With<Camera2d>,)>,
) {
let mut textures = <HashMap<_, _>>::default();
for (view, camera, extracted_view, msaa) in &views_2d {
if !opaque_2d_phases.contains_key(&extracted_view.retained_view_entity)
|| !transparent_2d_phases.contains_key(&extracted_view.retained_view_entity)
{
continue;
};
let Some(physical_target_size) = camera.physical_target_size else {
continue;
};
let cached_texture = textures
.entry(camera.target.clone())
.or_insert_with(|| {
// The size of the depth texture
let size = Extent3d {
depth_or_array_layers: 1,
width: physical_target_size.x,
height: physical_target_size.y,
};
let descriptor = TextureDescriptor {
label: Some("view_depth_texture"),
size,
mip_level_count: 1,
sample_count: msaa.samples(),
dimension: TextureDimension::D2,
format: CORE_2D_DEPTH_FORMAT,
usage: TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
};
texture_cache.get(&render_device, descriptor)
})
.clone();
commands
.entity(view)
.insert(ViewDepthTexture::new(cached_texture, Some(0.0)));
}
}

View File

@@ -0,0 +1,144 @@
use crate::{
core_3d::graph::Core3d,
tonemapping::{DebandDither, Tonemapping},
};
use bevy_ecs::prelude::*;
use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::{
camera::{Camera, CameraRenderGraph, Exposure, Projection},
extract_component::ExtractComponent,
render_resource::{LoadOp, TextureUsages},
view::ColorGrading,
};
use serde::{Deserialize, Serialize};
/// A 3D camera component. Enables the main 3D render graph for a [`Camera`].
///
/// The camera coordinate space is right-handed X-right, Y-up, Z-back.
/// This means "forward" is -Z.
#[derive(Component, Reflect, Clone, ExtractComponent)]
#[extract_component_filter(With<Camera>)]
#[reflect(Component, Default, Clone)]
#[require(
Camera,
DebandDither::Enabled,
CameraRenderGraph::new(Core3d),
Projection,
Tonemapping,
ColorGrading,
Exposure
)]
pub struct Camera3d {
/// The depth clear operation to perform for the main 3d pass.
pub depth_load_op: Camera3dDepthLoadOp,
/// The texture usages for the depth texture created for the main 3d pass.
pub depth_texture_usages: Camera3dDepthTextureUsage,
/// How many individual steps should be performed in the [`Transmissive3d`](crate::core_3d::Transmissive3d) pass.
///
/// Roughly corresponds to how many “layers of transparency” are rendered for screen space
/// specular transmissive objects. Each step requires making one additional
/// texture copy, so it's recommended to keep this number to a reasonably low value. Defaults to `1`.
///
/// ### Notes
///
/// - No copies will be performed if there are no transmissive materials currently being rendered,
/// regardless of this setting.
/// - Setting this to `0` disables the screen-space refraction effect entirely, and falls
/// back to refracting only the environment map light's texture.
/// - If set to more than `0`, any opaque [`clear_color`](Camera::clear_color) will obscure the environment
/// map light's texture, preventing it from being visible “through” transmissive materials. If you'd like
/// to still have the environment map show up in your refractions, you can set the clear color's alpha to `0.0`.
/// Keep in mind that depending on the platform and your window settings, this may cause the window to become
/// transparent.
pub screen_space_specular_transmission_steps: usize,
/// The quality of the screen space specular transmission blur effect, applied to whatever's “behind” transmissive
/// objects when their `roughness` is greater than `0.0`.
///
/// Higher qualities are more GPU-intensive.
///
/// **Note:** You can get better-looking results at any quality level by enabling TAA. See: [`TemporalAntiAliasPlugin`](crate::experimental::taa::TemporalAntiAliasPlugin).
pub screen_space_specular_transmission_quality: ScreenSpaceTransmissionQuality,
}
impl Default for Camera3d {
fn default() -> Self {
Self {
depth_load_op: Default::default(),
depth_texture_usages: TextureUsages::RENDER_ATTACHMENT.into(),
screen_space_specular_transmission_steps: 1,
screen_space_specular_transmission_quality: Default::default(),
}
}
}
#[derive(Clone, Copy, Reflect, Serialize, Deserialize)]
#[reflect(Serialize, Deserialize, Clone)]
pub struct Camera3dDepthTextureUsage(pub u32);
impl From<TextureUsages> for Camera3dDepthTextureUsage {
fn from(value: TextureUsages) -> Self {
Self(value.bits())
}
}
impl From<Camera3dDepthTextureUsage> for TextureUsages {
fn from(value: Camera3dDepthTextureUsage) -> Self {
Self::from_bits_truncate(value.0)
}
}
/// The depth clear operation to perform for the main 3d pass.
#[derive(Reflect, Serialize, Deserialize, Clone, Debug)]
#[reflect(Serialize, Deserialize, Clone, Default)]
pub enum Camera3dDepthLoadOp {
/// Clear with a specified value.
/// Note that 0.0 is the far plane due to bevy's use of reverse-z projections.
Clear(f32),
/// Load from memory.
Load,
}
impl Default for Camera3dDepthLoadOp {
fn default() -> Self {
Camera3dDepthLoadOp::Clear(0.0)
}
}
impl From<Camera3dDepthLoadOp> for LoadOp<f32> {
fn from(config: Camera3dDepthLoadOp) -> Self {
match config {
Camera3dDepthLoadOp::Clear(x) => LoadOp::Clear(x),
Camera3dDepthLoadOp::Load => LoadOp::Load,
}
}
}
/// The quality of the screen space transmission blur effect, applied to whatever's “behind” transmissive
/// objects when their `roughness` is greater than `0.0`.
///
/// Higher qualities are more GPU-intensive.
///
/// **Note:** You can get better-looking results at any quality level by enabling TAA. See: [`TemporalAntiAliasPlugin`](crate::experimental::taa::TemporalAntiAliasPlugin).
#[derive(Resource, Default, Clone, Copy, Reflect, PartialEq, PartialOrd, Debug)]
#[reflect(Resource, Default, Clone, Debug, PartialEq)]
pub enum ScreenSpaceTransmissionQuality {
/// Best performance at the cost of quality. Suitable for lower end GPUs. (e.g. Mobile)
///
/// `num_taps` = 4
Low,
/// A balanced option between quality and performance.
///
/// `num_taps` = 8
#[default]
Medium,
/// Better quality. Suitable for high end GPUs. (e.g. Desktop)
///
/// `num_taps` = 16
High,
/// The highest quality, suitable for non-realtime rendering. (e.g. Pre-rendered cinematics and photo mode)
///
/// `num_taps` = 32
Ultra,
}

View File

@@ -0,0 +1,137 @@
use crate::{
core_3d::Opaque3d,
skybox::{SkyboxBindGroup, SkyboxPipelineId},
};
use bevy_ecs::{prelude::World, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::{TrackedRenderPass, ViewBinnedRenderPhases},
render_resource::{CommandEncoderDescriptor, PipelineCache, RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ExtractedView, ViewDepthTexture, ViewTarget, ViewUniformOffset},
};
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
use super::AlphaMask3d;
/// A [`bevy_render::render_graph::Node`] that runs the [`Opaque3d`] and [`AlphaMask3d`]
/// [`ViewBinnedRenderPhases`]s.
#[derive(Default)]
pub struct MainOpaquePass3dNode;
impl ViewNode for MainOpaquePass3dNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewTarget,
&'static ViewDepthTexture,
Option<&'static SkyboxPipelineId>,
Option<&'static SkyboxBindGroup>,
&'static ViewUniformOffset,
);
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(
camera,
extracted_view,
target,
depth,
skybox_pipeline,
skybox_bind_group,
view_uniform_offset,
): QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let (Some(opaque_phases), Some(alpha_mask_phases)) = (
world.get_resource::<ViewBinnedRenderPhases<Opaque3d>>(),
world.get_resource::<ViewBinnedRenderPhases<AlphaMask3d>>(),
) else {
return Ok(());
};
let (Some(opaque_phase), Some(alpha_mask_phase)) = (
opaque_phases.get(&extracted_view.retained_view_entity),
alpha_mask_phases.get(&extracted_view.retained_view_entity),
) else {
return Ok(());
};
let diagnostics = render_context.diagnostic_recorder();
let color_attachments = [Some(target.get_color_attachment())];
let depth_stencil_attachment = Some(depth.get_attachment(StoreOp::Store));
let view_entity = graph.view_entity();
render_context.add_command_buffer_generation_task(move |render_device| {
#[cfg(feature = "trace")]
let _main_opaque_pass_3d_span = info_span!("main_opaque_pass_3d").entered();
// Command encoder setup
let mut command_encoder =
render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("main_opaque_pass_3d_command_encoder"),
});
// Render pass setup
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("main_opaque_pass_3d"),
color_attachments: &color_attachments,
depth_stencil_attachment,
timestamp_writes: None,
occlusion_query_set: None,
});
let mut render_pass = TrackedRenderPass::new(&render_device, render_pass);
let pass_span = diagnostics.pass_span(&mut render_pass, "main_opaque_pass_3d");
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
// Opaque draws
if !opaque_phase.is_empty() {
#[cfg(feature = "trace")]
let _opaque_main_pass_3d_span = info_span!("opaque_main_pass_3d").entered();
if let Err(err) = opaque_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the opaque phase {err:?}");
}
}
// Alpha draws
if !alpha_mask_phase.is_empty() {
#[cfg(feature = "trace")]
let _alpha_mask_main_pass_3d_span = info_span!("alpha_mask_main_pass_3d").entered();
if let Err(err) = alpha_mask_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the alpha mask phase {err:?}");
}
}
// Skybox draw using a fullscreen triangle
if let (Some(skybox_pipeline), Some(SkyboxBindGroup(skybox_bind_group))) =
(skybox_pipeline, skybox_bind_group)
{
let pipeline_cache = world.resource::<PipelineCache>();
if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_pipeline.0) {
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(
0,
&skybox_bind_group.0,
&[view_uniform_offset.offset, skybox_bind_group.1],
);
render_pass.draw(0..3, 0..1);
}
}
pass_span.end(&mut render_pass);
drop(render_pass);
command_encoder.finish()
});
Ok(())
}
}

View File

@@ -0,0 +1,152 @@
use super::{Camera3d, ViewTransmissionTexture};
use crate::core_3d::Transmissive3d;
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::ViewSortedRenderPhases,
render_resource::{Extent3d, RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ExtractedView, ViewDepthTexture, ViewTarget},
};
use core::ops::Range;
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
/// A [`bevy_render::render_graph::Node`] that runs the [`Transmissive3d`]
/// [`ViewSortedRenderPhases`].
#[derive(Default)]
pub struct MainTransmissivePass3dNode;
impl ViewNode for MainTransmissivePass3dNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static Camera3d,
&'static ViewTarget,
Option<&'static ViewTransmissionTexture>,
&'static ViewDepthTexture,
);
fn run(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(camera, view, camera_3d, target, transmission, depth): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let view_entity = graph.view_entity();
let Some(transmissive_phases) =
world.get_resource::<ViewSortedRenderPhases<Transmissive3d>>()
else {
return Ok(());
};
let Some(transmissive_phase) = transmissive_phases.get(&view.retained_view_entity) else {
return Ok(());
};
let physical_target_size = camera.physical_target_size.unwrap();
let render_pass_descriptor = RenderPassDescriptor {
label: Some("main_transmissive_pass_3d"),
color_attachments: &[Some(target.get_color_attachment())],
depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)),
timestamp_writes: None,
occlusion_query_set: None,
};
// Run the transmissive pass, sorted back-to-front
// NOTE: Scoped to drop the mutable borrow of render_context
#[cfg(feature = "trace")]
let _main_transmissive_pass_3d_span = info_span!("main_transmissive_pass_3d").entered();
if !transmissive_phase.items.is_empty() {
let screen_space_specular_transmission_steps =
camera_3d.screen_space_specular_transmission_steps;
if screen_space_specular_transmission_steps > 0 {
let transmission =
transmission.expect("`ViewTransmissionTexture` should exist at this point");
// `transmissive_phase.items` are depth sorted, so we split them into N = `screen_space_specular_transmission_steps`
// ranges, rendering them back-to-front in multiple steps, allowing multiple levels of transparency.
//
// Note: For the sake of simplicity, we currently split items evenly among steps. In the future, we
// might want to use a more sophisticated heuristic (e.g. based on view bounds, or with an exponential
// falloff so that nearby objects have more levels of transparency available to them)
for range in split_range(
0..transmissive_phase.items.len(),
screen_space_specular_transmission_steps,
) {
// Copy the main texture to the transmission texture, allowing to use the color output of the
// previous step (or of the `Opaque3d` phase, for the first step) as a transmissive color input
render_context.command_encoder().copy_texture_to_texture(
target.main_texture().as_image_copy(),
transmission.texture.as_image_copy(),
Extent3d {
width: physical_target_size.x,
height: physical_target_size.y,
depth_or_array_layers: 1,
},
);
let mut render_pass =
render_context.begin_tracked_render_pass(render_pass_descriptor.clone());
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
// render items in range
if let Err(err) =
transmissive_phase.render_range(&mut render_pass, world, view_entity, range)
{
error!("Error encountered while rendering the transmissive phase {err:?}");
}
}
} else {
let mut render_pass =
render_context.begin_tracked_render_pass(render_pass_descriptor);
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
if let Err(err) = transmissive_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the transmissive phase {err:?}");
}
}
}
Ok(())
}
}
/// Splits a [`Range`] into at most `max_num_splits` sub-ranges without overlaps
///
/// Properly takes into account remainders of inexact divisions (by adding extra
/// elements to the initial sub-ranges as needed)
fn split_range(range: Range<usize>, max_num_splits: usize) -> impl Iterator<Item = Range<usize>> {
let len = range.end - range.start;
assert!(len > 0, "to be split, a range must not be empty");
assert!(max_num_splits > 0, "max_num_splits must be at least 1");
let num_splits = max_num_splits.min(len);
let step = len / num_splits;
let mut rem = len % num_splits;
let mut start = range.start;
(0..num_splits).map(move |_| {
let extra = if rem > 0 {
rem -= 1;
1
} else {
0
};
let end = (start + step + extra).min(range.end);
let result = start..end;
start = end;
result
})
}

View File

@@ -0,0 +1,103 @@
use crate::core_3d::Transparent3d;
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::ViewSortedRenderPhases,
render_resource::{RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ExtractedView, ViewDepthTexture, ViewTarget},
};
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
/// A [`bevy_render::render_graph::Node`] that runs the [`Transparent3d`]
/// [`ViewSortedRenderPhases`].
#[derive(Default)]
pub struct MainTransparentPass3dNode;
impl ViewNode for MainTransparentPass3dNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewTarget,
&'static ViewDepthTexture,
);
fn run(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(camera, view, target, depth): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let view_entity = graph.view_entity();
let Some(transparent_phases) =
world.get_resource::<ViewSortedRenderPhases<Transparent3d>>()
else {
return Ok(());
};
let Some(transparent_phase) = transparent_phases.get(&view.retained_view_entity) else {
return Ok(());
};
if !transparent_phase.items.is_empty() {
// Run the transparent pass, sorted back-to-front
// NOTE: Scoped to drop the mutable borrow of render_context
#[cfg(feature = "trace")]
let _main_transparent_pass_3d_span = info_span!("main_transparent_pass_3d").entered();
let diagnostics = render_context.diagnostic_recorder();
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("main_transparent_pass_3d"),
color_attachments: &[Some(target.get_color_attachment())],
// NOTE: For the transparent pass we load the depth buffer. There should be no
// need to write to it, but store is set to `true` as a workaround for issue #3776,
// https://github.com/bevyengine/bevy/issues/3776
// so that wgpu does not clear the depth buffer.
// As the opaque and alpha mask passes run first, opaque meshes can occlude
// transparent ones.
depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)),
timestamp_writes: None,
occlusion_query_set: None,
});
let pass_span = diagnostics.pass_span(&mut render_pass, "main_transparent_pass_3d");
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the transparent phase {err:?}");
}
pass_span.end(&mut render_pass);
}
// WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't
// reset for the next render pass so add an empty render pass without a custom viewport
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
if camera.viewport.is_some() {
#[cfg(feature = "trace")]
let _reset_viewport_pass_3d = info_span!("reset_viewport_pass_3d").entered();
let pass_descriptor = RenderPassDescriptor {
label: Some("reset_viewport_pass_3d"),
color_attachments: &[Some(target.get_color_attachment())],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
};
render_context
.command_encoder()
.begin_render_pass(&pass_descriptor);
}
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
@group(0) @binding(0)
var material_id_texture: texture_2d<u32>;
struct FragmentOutput {
@builtin(frag_depth) frag_depth: f32,
}
@fragment
fn fragment(in: FullscreenVertexOutput) -> FragmentOutput {
var out: FragmentOutput;
// Depth is stored as unorm, so we are dividing the u8 by 255.0 here.
out.frag_depth = f32(textureLoad(material_id_texture, vec2<i32>(in.position.xy), 0).x) / 255.0;
return out;
}

View File

@@ -0,0 +1,210 @@
use crate::{
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
prepass::{DeferredPrepass, ViewPrepassTextures},
};
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_ecs::prelude::*;
use bevy_math::UVec2;
use bevy_render::{
camera::ExtractedCamera,
render_resource::{binding_types::texture_2d, *},
renderer::RenderDevice,
texture::{CachedTexture, TextureCache},
view::ViewTarget,
Render, RenderApp, RenderSet,
};
use bevy_ecs::query::QueryItem;
use bevy_render::{
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
renderer::RenderContext,
};
use super::DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT;
pub const COPY_DEFERRED_LIGHTING_ID_SHADER_HANDLE: Handle<Shader> =
weak_handle!("70d91342-1c43-4b20-973f-aa6ce93aa617");
pub struct CopyDeferredLightingIdPlugin;
impl Plugin for CopyDeferredLightingIdPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
COPY_DEFERRED_LIGHTING_ID_SHADER_HANDLE,
"copy_deferred_lighting_id.wgsl",
Shader::from_wgsl
);
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.add_systems(
Render,
(prepare_deferred_lighting_id_textures.in_set(RenderSet::PrepareResources),),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<CopyDeferredLightingIdPipeline>();
}
}
#[derive(Default)]
pub struct CopyDeferredLightingIdNode;
impl CopyDeferredLightingIdNode {
pub const NAME: &'static str = "copy_deferred_lighting_id";
}
impl ViewNode for CopyDeferredLightingIdNode {
type ViewQuery = (
&'static ViewTarget,
&'static ViewPrepassTextures,
&'static DeferredLightingIdDepthTexture,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(_view_target, view_prepass_textures, deferred_lighting_id_depth_texture): QueryItem<
Self::ViewQuery,
>,
world: &World,
) -> Result<(), NodeRunError> {
let copy_deferred_lighting_id_pipeline = world.resource::<CopyDeferredLightingIdPipeline>();
let pipeline_cache = world.resource::<PipelineCache>();
let Some(pipeline) =
pipeline_cache.get_render_pipeline(copy_deferred_lighting_id_pipeline.pipeline_id)
else {
return Ok(());
};
let Some(deferred_lighting_pass_id_texture) =
&view_prepass_textures.deferred_lighting_pass_id
else {
return Ok(());
};
let bind_group = render_context.render_device().create_bind_group(
"copy_deferred_lighting_id_bind_group",
&copy_deferred_lighting_id_pipeline.layout,
&BindGroupEntries::single(&deferred_lighting_pass_id_texture.texture.default_view),
);
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("copy_deferred_lighting_id_pass"),
color_attachments: &[],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &deferred_lighting_id_depth_texture.texture.default_view,
depth_ops: Some(Operations {
load: LoadOp::Clear(0.0),
store: StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: 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(())
}
}
#[derive(Resource)]
struct CopyDeferredLightingIdPipeline {
layout: BindGroupLayout,
pipeline_id: CachedRenderPipelineId,
}
impl FromWorld for CopyDeferredLightingIdPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let layout = render_device.create_bind_group_layout(
"copy_deferred_lighting_id_bind_group_layout",
&BindGroupLayoutEntries::single(
ShaderStages::FRAGMENT,
texture_2d(TextureSampleType::Uint),
),
);
let pipeline_id =
world
.resource_mut::<PipelineCache>()
.queue_render_pipeline(RenderPipelineDescriptor {
label: Some("copy_deferred_lighting_id_pipeline".into()),
layout: vec![layout.clone()],
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: COPY_DEFERRED_LIGHTING_ID_SHADER_HANDLE,
shader_defs: vec![],
entry_point: "fragment".into(),
targets: vec![],
}),
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT,
depth_write_enabled: true,
depth_compare: CompareFunction::Always,
stencil: StencilState::default(),
bias: DepthBiasState::default(),
}),
multisample: MultisampleState::default(),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
});
Self {
layout,
pipeline_id,
}
}
}
#[derive(Component)]
pub struct DeferredLightingIdDepthTexture {
pub texture: CachedTexture,
}
fn prepare_deferred_lighting_id_textures(
mut commands: Commands,
mut texture_cache: ResMut<TextureCache>,
render_device: Res<RenderDevice>,
views: Query<(Entity, &ExtractedCamera), With<DeferredPrepass>>,
) {
for (entity, camera) in &views {
if let Some(UVec2 {
x: width,
y: height,
}) = camera.physical_target_size
{
let texture_descriptor = TextureDescriptor {
label: Some("deferred_lighting_id_depth_texture_a"),
size: Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT,
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
view_formats: &[],
};
let texture = texture_cache.get(&render_device, texture_descriptor);
commands
.entity(entity)
.insert(DeferredLightingIdDepthTexture { texture });
}
}
}

View File

@@ -0,0 +1,186 @@
pub mod copy_lighting_id;
pub mod node;
use core::ops::Range;
use crate::prepass::{OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey};
use bevy_ecs::prelude::*;
use bevy_render::sync_world::MainEntity;
use bevy_render::{
render_phase::{
BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem,
PhaseItemExtraIndex,
},
render_resource::{CachedRenderPipelineId, TextureFormat},
};
pub const DEFERRED_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgba32Uint;
pub const DEFERRED_LIGHTING_PASS_ID_FORMAT: TextureFormat = TextureFormat::R8Uint;
pub const DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth16Unorm;
/// Opaque phase of the 3D Deferred pass.
///
/// Sorted by pipeline, then by mesh to improve batching.
///
/// Used to render all 3D meshes with materials that have no transparency.
#[derive(PartialEq, Eq, Hash)]
pub struct Opaque3dDeferred {
/// Determines which objects can be placed into a *batch set*.
///
/// Objects in a single batch set can potentially be multi-drawn together,
/// if it's enabled and the current platform supports it.
pub batch_set_key: OpaqueNoLightmap3dBatchSetKey,
/// Information that separates items into bins.
pub bin_key: OpaqueNoLightmap3dBinKey,
pub representative_entity: (Entity, MainEntity),
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
}
impl PhaseItem for Opaque3dDeferred {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity.0
}
fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}
#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.batch_set_key.draw_function
}
#[inline]
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index.clone()
}
#[inline]
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl BinnedPhaseItem for Opaque3dDeferred {
type BatchSetKey = OpaqueNoLightmap3dBatchSetKey;
type BinKey = OpaqueNoLightmap3dBinKey;
#[inline]
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Self {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
impl CachedRenderPipelinePhaseItem for Opaque3dDeferred {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.batch_set_key.pipeline
}
}
/// Alpha mask phase of the 3D Deferred pass.
///
/// Sorted by pipeline, then by mesh to improve batching.
///
/// Used to render all meshes with a material with an alpha mask.
pub struct AlphaMask3dDeferred {
/// Determines which objects can be placed into a *batch set*.
///
/// Objects in a single batch set can potentially be multi-drawn together,
/// if it's enabled and the current platform supports it.
pub batch_set_key: OpaqueNoLightmap3dBatchSetKey,
/// Information that separates items into bins.
pub bin_key: OpaqueNoLightmap3dBinKey,
pub representative_entity: (Entity, MainEntity),
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
}
impl PhaseItem for AlphaMask3dDeferred {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity.0
}
#[inline]
fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}
#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.batch_set_key.draw_function
}
#[inline]
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index.clone()
}
#[inline]
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl BinnedPhaseItem for AlphaMask3dDeferred {
type BatchSetKey = OpaqueNoLightmap3dBatchSetKey;
type BinKey = OpaqueNoLightmap3dBinKey;
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Self {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
impl CachedRenderPipelinePhaseItem for AlphaMask3dDeferred {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.batch_set_key.pipeline
}
}

View File

@@ -0,0 +1,262 @@
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::experimental::occlusion_culling::OcclusionCulling;
use bevy_render::render_graph::ViewNode;
use bevy_render::view::{ExtractedView, NoIndirectDrawing};
use bevy_render::{
camera::ExtractedCamera,
render_graph::{NodeRunError, RenderGraphContext},
render_phase::{TrackedRenderPass, ViewBinnedRenderPhases},
render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::ViewDepthTexture,
};
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
use crate::prepass::ViewPrepassTextures;
use super::{AlphaMask3dDeferred, Opaque3dDeferred};
/// The phase of the deferred prepass that draws meshes that were visible last
/// frame.
///
/// If occlusion culling isn't in use, this prepass simply draws all meshes.
///
/// Like all prepass nodes, this is inserted before the main pass in the render
/// graph.
#[derive(Default)]
pub struct EarlyDeferredGBufferPrepassNode;
impl ViewNode for EarlyDeferredGBufferPrepassNode {
type ViewQuery = <LateDeferredGBufferPrepassNode as ViewNode>::ViewQuery;
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
view_query: QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
run_deferred_prepass(
graph,
render_context,
view_query,
false,
world,
"early deferred prepass",
)
}
}
/// The phase of the prepass that runs after occlusion culling against the
/// meshes that were visible last frame.
///
/// If occlusion culling isn't in use, this is a no-op.
///
/// Like all prepass nodes, this is inserted before the main pass in the render
/// graph.
#[derive(Default)]
pub struct LateDeferredGBufferPrepassNode;
impl ViewNode for LateDeferredGBufferPrepassNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewDepthTexture,
&'static ViewPrepassTextures,
Has<OcclusionCulling>,
Has<NoIndirectDrawing>,
);
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
view_query: QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let (_, _, _, _, occlusion_culling, no_indirect_drawing) = view_query;
if !occlusion_culling || no_indirect_drawing {
return Ok(());
}
run_deferred_prepass(
graph,
render_context,
view_query,
true,
world,
"late deferred prepass",
)
}
}
/// Runs the deferred prepass that draws all meshes to the depth buffer and
/// G-buffers.
///
/// If occlusion culling isn't in use, and a prepass is enabled, then there's
/// only one prepass. If occlusion culling is in use, then any prepass is split
/// into two: an *early* prepass and a *late* prepass. The early prepass draws
/// what was visible last frame, and the last prepass performs occlusion culling
/// against a conservative hierarchical Z buffer before drawing unoccluded
/// meshes.
fn run_deferred_prepass<'w>(
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(camera, extracted_view, view_depth_texture, view_prepass_textures, _, _): QueryItem<
'w,
<LateDeferredGBufferPrepassNode as ViewNode>::ViewQuery,
>,
is_late: bool,
world: &'w World,
label: &'static str,
) -> Result<(), NodeRunError> {
let (Some(opaque_deferred_phases), Some(alpha_mask_deferred_phases)) = (
world.get_resource::<ViewBinnedRenderPhases<Opaque3dDeferred>>(),
world.get_resource::<ViewBinnedRenderPhases<AlphaMask3dDeferred>>(),
) else {
return Ok(());
};
let (Some(opaque_deferred_phase), Some(alpha_mask_deferred_phase)) = (
opaque_deferred_phases.get(&extracted_view.retained_view_entity),
alpha_mask_deferred_phases.get(&extracted_view.retained_view_entity),
) else {
return Ok(());
};
let mut color_attachments = vec![];
color_attachments.push(
view_prepass_textures
.normal
.as_ref()
.map(|normals_texture| normals_texture.get_attachment()),
);
color_attachments.push(
view_prepass_textures
.motion_vectors
.as_ref()
.map(|motion_vectors_texture| motion_vectors_texture.get_attachment()),
);
// If we clear the deferred texture with LoadOp::Clear(Default::default()) we get these errors:
// Chrome: GL_INVALID_OPERATION: No defined conversion between clear value and attachment format.
// Firefox: WebGL warning: clearBufferu?[fi]v: This attachment is of type FLOAT, but this function is of type UINT.
// Appears to be unsupported: https://registry.khronos.org/webgl/specs/latest/2.0/#3.7.9
// For webgl2 we fallback to manually clearing
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
if !is_late {
if let Some(deferred_texture) = &view_prepass_textures.deferred {
render_context.command_encoder().clear_texture(
&deferred_texture.texture.texture,
&bevy_render::render_resource::ImageSubresourceRange::default(),
);
}
}
color_attachments.push(
view_prepass_textures
.deferred
.as_ref()
.map(|deferred_texture| {
if is_late {
deferred_texture.get_attachment()
} else {
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
{
bevy_render::render_resource::RenderPassColorAttachment {
view: &deferred_texture.texture.default_view,
resolve_target: None,
ops: bevy_render::render_resource::Operations {
load: bevy_render::render_resource::LoadOp::Load,
store: StoreOp::Store,
},
}
}
#[cfg(any(
not(feature = "webgl"),
not(target_arch = "wasm32"),
feature = "webgpu"
))]
deferred_texture.get_attachment()
}
}),
);
color_attachments.push(
view_prepass_textures
.deferred_lighting_pass_id
.as_ref()
.map(|deferred_lighting_pass_id| deferred_lighting_pass_id.get_attachment()),
);
// If all color attachments are none: clear the color attachment list so that no fragment shader is required
if color_attachments.iter().all(Option::is_none) {
color_attachments.clear();
}
let depth_stencil_attachment = Some(view_depth_texture.get_attachment(StoreOp::Store));
let view_entity = graph.view_entity();
render_context.add_command_buffer_generation_task(move |render_device| {
#[cfg(feature = "trace")]
let _deferred_span = info_span!("deferred_prepass").entered();
// Command encoder setup
let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("deferred_prepass_command_encoder"),
});
// Render pass setup
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some(label),
color_attachments: &color_attachments,
depth_stencil_attachment,
timestamp_writes: None,
occlusion_query_set: None,
});
let mut render_pass = TrackedRenderPass::new(&render_device, render_pass);
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
// Opaque draws
if !opaque_deferred_phase.multidrawable_meshes.is_empty()
|| !opaque_deferred_phase.batchable_meshes.is_empty()
|| !opaque_deferred_phase.unbatchable_meshes.is_empty()
{
#[cfg(feature = "trace")]
let _opaque_prepass_span = info_span!("opaque_deferred_prepass").entered();
if let Err(err) = opaque_deferred_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the opaque deferred phase {err:?}");
}
}
// Alpha masked draws
if !alpha_mask_deferred_phase.is_empty() {
#[cfg(feature = "trace")]
let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred_prepass").entered();
if let Err(err) = alpha_mask_deferred_phase.render(&mut render_pass, world, view_entity)
{
error!("Error encountered while rendering the alpha mask deferred phase {err:?}");
}
}
drop(render_pass);
// After rendering to the view depth texture, copy it to the prepass depth texture
if let Some(prepass_depth_texture) = &view_prepass_textures.depth {
command_encoder.copy_texture_to_texture(
view_depth_texture.texture.as_image_copy(),
prepass_depth_texture.texture.texture.as_image_copy(),
view_prepass_textures.size,
);
}
command_encoder.finish()
});
Ok(())
}

View File

@@ -0,0 +1,301 @@
// Performs depth of field postprocessing, with both Gaussian and bokeh kernels.
//
// Gaussian blur is performed as a separable convolution: first blurring in the
// X direction, and then in the Y direction. This is asymptotically more
// efficient than performing a 2D convolution.
//
// The Bokeh blur uses a similar, but more complex, separable convolution
// technique. The algorithm is described in Colin Barré-Brisebois, "Hexagonal
// Bokeh Blur Revisited" [1]. It's motivated by the observation that we can use
// separable convolutions not only to produce boxes but to produce
// parallelograms. Thus, by performing three separable convolutions in sequence,
// we can produce a hexagonal shape. The first and second convolutions are done
// simultaneously using multiple render targets to cut the total number of
// passes down to two.
//
// [1]: https://colinbarrebrisebois.com/2017/04/18/hexagonal-bokeh-blur-revisited-part-2-improved-2-pass-version/
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
#import bevy_pbr::mesh_view_bindings::view
#import bevy_pbr::view_transformations::depth_ndc_to_view_z
#import bevy_render::view::View
// Parameters that control the depth of field effect. See
// `bevy_core_pipeline::dof::DepthOfFieldUniforms` for information on what these
// parameters mean.
struct DepthOfFieldParams {
/// The distance in meters to the location in focus.
focal_distance: f32,
/// The [focal length]. Physically speaking, this represents "the distance
/// from the center of the lens to the principal foci of the lens". The
/// default value, 50 mm, is considered representative of human eyesight.
/// Real-world lenses range from anywhere from 5 mm for "fisheye" lenses to
/// 2000 mm for "super-telephoto" lenses designed for very distant objects.
///
/// The higher the value, the more blurry objects not in focus will be.
///
/// [focal length]: https://en.wikipedia.org/wiki/Focal_length
focal_length: f32,
/// The premultiplied factor that we scale the circle of confusion by.
///
/// This is calculated as `focal_length² / (sensor_height * aperture_f_stops)`.
coc_scale_factor: f32,
/// The maximum diameter, in pixels, that we allow a circle of confusion to be.
///
/// A circle of confusion essentially describes the size of a blur.
///
/// This value is nonphysical but is useful for avoiding pathologically-slow
/// behavior.
max_circle_of_confusion_diameter: f32,
/// The depth value that we clamp distant objects to. See the comment in
/// [`DepthOfField`] for more information.
max_depth: f32,
/// Padding.
pad_a: u32,
/// Padding.
pad_b: u32,
/// Padding.
pad_c: u32,
}
// The first bokeh pass outputs to two render targets. We declare them here.
struct DualOutput {
// The vertical output.
@location(0) output_0: vec4<f32>,
// The diagonal output.
@location(1) output_1: vec4<f32>,
}
// @group(0) @binding(0) is `mesh_view_bindings::view`.
// The depth texture for the main view.
#ifdef MULTISAMPLED
@group(0) @binding(1) var depth_texture: texture_depth_multisampled_2d;
#else // MULTISAMPLED
@group(0) @binding(1) var depth_texture: texture_depth_2d;
#endif // MULTISAMPLED
// The main color texture.
@group(0) @binding(2) var color_texture_a: texture_2d<f32>;
// The auxiliary color texture that we're sampling from. This is only used as
// part of the second bokeh pass.
#ifdef DUAL_INPUT
@group(0) @binding(3) var color_texture_b: texture_2d<f32>;
#endif // DUAL_INPUT
// The global uniforms, representing data backed by buffers shared among all
// views in the scene.
// The parameters that control the depth of field effect.
@group(1) @binding(0) var<uniform> dof_params: DepthOfFieldParams;
// The sampler that's used to fetch texels from the source color buffer.
@group(1) @binding(1) var color_texture_sampler: sampler;
// cos(-30°), used for the bokeh blur.
const COS_NEG_FRAC_PI_6: f32 = 0.8660254037844387;
// sin(-30°), used for the bokeh blur.
const SIN_NEG_FRAC_PI_6: f32 = -0.5;
// cos(-150°), used for the bokeh blur.
const COS_NEG_FRAC_PI_5_6: f32 = -0.8660254037844387;
// sin(-150°), used for the bokeh blur.
const SIN_NEG_FRAC_PI_5_6: f32 = -0.5;
// Calculates and returns the diameter (not the radius) of the [circle of
// confusion].
//
// [circle of confusion]: https://en.wikipedia.org/wiki/Circle_of_confusion
fn calculate_circle_of_confusion(in_frag_coord: vec4<f32>) -> f32 {
// Unpack the depth of field parameters.
let focus = dof_params.focal_distance;
let f = dof_params.focal_length;
let scale = dof_params.coc_scale_factor;
let max_coc_diameter = dof_params.max_circle_of_confusion_diameter;
// Sample the depth.
let frag_coord = vec2<i32>(floor(in_frag_coord.xy));
let raw_depth = textureLoad(depth_texture, frag_coord, 0);
let depth = min(-depth_ndc_to_view_z(raw_depth), dof_params.max_depth);
// Calculate the circle of confusion.
//
// This is just the formula from Wikipedia [1].
//
// [1]: https://en.wikipedia.org/wiki/Circle_of_confusion#Determining_a_circle_of_confusion_diameter_from_the_object_field
let candidate_coc = scale * abs(depth - focus) / (depth * (focus - f));
let framebuffer_size = vec2<f32>(textureDimensions(color_texture_a));
return clamp(candidate_coc * framebuffer_size.y, 0.0, max_coc_diameter);
}
// Performs a single direction of the separable Gaussian blur kernel.
//
// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the
// `position` input to the fragment).
//
// * `coc` is the diameter (not the radius) of the circle of confusion for this
// fragment.
//
// * `frag_offset` is the vector, in screen-space units, from one sample to the
// next. For a horizontal blur this will be `vec2(1.0, 0.0)`; for a vertical
// blur this will be `vec2(0.0, 1.0)`.
//
// Returns the resulting color of the fragment.
fn gaussian_blur(frag_coord: vec4<f32>, coc: f32, frag_offset: vec2<f32>) -> vec4<f32> {
// Usually σ (the standard deviation) is half the radius, and the radius is
// half the CoC. So we multiply by 0.25.
let sigma = coc * 0.25;
// 1.5σ is a good, somewhat aggressive default for support—the number of
// texels on each side of the center that we process.
let support = i32(ceil(sigma * 1.5));
let uv = frag_coord.xy / vec2<f32>(textureDimensions(color_texture_a));
let offset = frag_offset / vec2<f32>(textureDimensions(color_texture_a));
// The probability density function of the Gaussian blur is (up to constant factors) `exp(-1 / 2σ² *
// x²). We precalculate the constant factor here to avoid having to
// calculate it in the inner loop.
let exp_factor = -1.0 / (2.0 * sigma * sigma);
// Accumulate samples on both sides of the current texel. Go two at a time,
// taking advantage of bilinear filtering.
var sum = textureSampleLevel(color_texture_a, color_texture_sampler, uv, 0.0).rgb;
var weight_sum = 1.0;
for (var i = 1; i <= support; i += 2) {
// This is a well-known trick to reduce the number of needed texture
// samples by a factor of two. We seek to accumulate two adjacent
// samples c₀ and c₁ with weights w₀ and w₁ respectively, with a single
// texture sample at a carefully chosen location. Observe that:
//
// k ⋅ lerp(c₀, c₁, t) = w₀⋅c₀ + w₁⋅c₁
//
// w₁
// if k = w₀ + w₁ and t = ───────
// w₀ + w₁
//
// Therefore, if we sample at a distance of t = w₁ / (w₀ + w₁) texels in
// between the two texel centers and scale by k = w₀ + w₁ afterward, we
// effectively evaluate w₀⋅c₀ + w₁⋅c₁ with a single texture lookup.
let w0 = exp(exp_factor * f32(i) * f32(i));
let w1 = exp(exp_factor * f32(i + 1) * f32(i + 1));
let uv_offset = offset * (f32(i) + w1 / (w0 + w1));
let weight = w0 + w1;
sum += (
textureSampleLevel(color_texture_a, color_texture_sampler, uv + uv_offset, 0.0).rgb +
textureSampleLevel(color_texture_a, color_texture_sampler, uv - uv_offset, 0.0).rgb
) * weight;
weight_sum += weight * 2.0;
}
return vec4(sum / weight_sum, 1.0);
}
// Performs a box blur in a single direction, sampling `color_texture_a`.
//
// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the
// `position` input to the fragment).
//
// * `coc` is the diameter (not the radius) of the circle of confusion for this
// fragment.
//
// * `frag_offset` is the vector, in screen-space units, from one sample to the
// next. This need not be horizontal or vertical.
fn box_blur_a(frag_coord: vec4<f32>, coc: f32, frag_offset: vec2<f32>) -> vec4<f32> {
let support = i32(round(coc * 0.5));
let uv = frag_coord.xy / vec2<f32>(textureDimensions(color_texture_a));
let offset = frag_offset / vec2<f32>(textureDimensions(color_texture_a));
// Accumulate samples in a single direction.
var sum = vec3(0.0);
for (var i = 0; i <= support; i += 1) {
sum += textureSampleLevel(
color_texture_a, color_texture_sampler, uv + offset * f32(i), 0.0).rgb;
}
return vec4(sum / vec3(1.0 + f32(support)), 1.0);
}
// Performs a box blur in a single direction, sampling `color_texture_b`.
//
// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the
// `position` input to the fragment).
//
// * `coc` is the diameter (not the radius) of the circle of confusion for this
// fragment.
//
// * `frag_offset` is the vector, in screen-space units, from one sample to the
// next. This need not be horizontal or vertical.
#ifdef DUAL_INPUT
fn box_blur_b(frag_coord: vec4<f32>, coc: f32, frag_offset: vec2<f32>) -> vec4<f32> {
let support = i32(round(coc * 0.5));
let uv = frag_coord.xy / vec2<f32>(textureDimensions(color_texture_b));
let offset = frag_offset / vec2<f32>(textureDimensions(color_texture_b));
// Accumulate samples in a single direction.
var sum = vec3(0.0);
for (var i = 0; i <= support; i += 1) {
sum += textureSampleLevel(
color_texture_b, color_texture_sampler, uv + offset * f32(i), 0.0).rgb;
}
return vec4(sum / vec3(1.0 + f32(support)), 1.0);
}
#endif
// Calculates the horizontal component of the separable Gaussian blur.
@fragment
fn gaussian_horizontal(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
let coc = calculate_circle_of_confusion(in.position);
return gaussian_blur(in.position, coc, vec2(1.0, 0.0));
}
// Calculates the vertical component of the separable Gaussian blur.
@fragment
fn gaussian_vertical(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
let coc = calculate_circle_of_confusion(in.position);
return gaussian_blur(in.position, coc, vec2(0.0, 1.0));
}
// Calculates the vertical and first diagonal components of the separable
// hexagonal bokeh blur.
//
//
//
// •
// │
// │
@fragment
fn bokeh_pass_0(in: FullscreenVertexOutput) -> DualOutput {
let coc = calculate_circle_of_confusion(in.position);
let vertical = box_blur_a(in.position, coc, vec2(0.0, 1.0));
let diagonal = box_blur_a(in.position, coc, vec2(COS_NEG_FRAC_PI_6, SIN_NEG_FRAC_PI_6));
// Note that the diagonal part is pre-mixed with the vertical component.
var output: DualOutput;
output.output_0 = vertical;
output.output_1 = mix(vertical, diagonal, 0.5);
return output;
}
// Calculates the second diagonal components of the separable hexagonal bokeh
// blur.
//
// ╲
// ╲
// •
#ifdef DUAL_INPUT
@fragment
fn bokeh_pass_1(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
let coc = calculate_circle_of_confusion(in.position);
let output_0 = box_blur_a(in.position, coc, vec2(COS_NEG_FRAC_PI_6, SIN_NEG_FRAC_PI_6));
let output_1 = box_blur_b(in.position, coc, vec2(COS_NEG_FRAC_PI_5_6, SIN_NEG_FRAC_PI_5_6));
return mix(output_0, output_1, 0.5);
}
#endif

923
vendor/bevy_core_pipeline/src/dof/mod.rs vendored Normal file
View File

@@ -0,0 +1,923 @@
//! Depth of field, a postprocessing effect that simulates camera focus.
//!
//! By default, Bevy renders all objects in full focus: regardless of depth, all
//! objects are rendered perfectly sharp (up to output resolution). Real lenses,
//! however, can only focus on objects at a specific distance. The distance
//! between the nearest and furthest objects that are in focus is known as
//! [depth of field], and this term is used more generally in computer graphics
//! to refer to the effect that simulates focus of lenses.
//!
//! Attaching [`DepthOfField`] to a camera causes Bevy to simulate the
//! focus of a camera lens. Generally, Bevy's implementation of depth of field
//! is optimized for speed instead of physical accuracy. Nevertheless, the depth
//! of field effect in Bevy is based on physical parameters.
//!
//! [Depth of field]: https://en.wikipedia.org/wiki/Depth_of_field
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{QueryItem, With},
reflect::ReflectComponent,
resource::Resource,
schedule::IntoScheduleConfigs as _,
system::{lifetimeless::Read, Commands, Query, Res, ResMut},
world::{FromWorld, World},
};
use bevy_image::BevyDefault as _;
use bevy_math::ops;
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use bevy_render::{
camera::{PhysicalCameraParameters, Projection},
extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin},
render_graph::{
NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner,
},
render_resource::{
binding_types::{
sampler, texture_2d, texture_depth_2d, texture_depth_2d_multisampled, uniform_buffer,
},
BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
CachedRenderPipelineId, ColorTargetState, ColorWrites, FilterMode, FragmentState, LoadOp,
Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor,
RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader,
ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, StoreOp,
TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages,
},
renderer::{RenderContext, RenderDevice},
sync_component::SyncComponentPlugin,
sync_world::RenderEntity,
texture::{CachedTexture, TextureCache},
view::{
prepare_view_targets, ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniform,
ViewUniformOffset, ViewUniforms,
},
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_utils::{default, once};
use smallvec::SmallVec;
use tracing::{info, warn};
use crate::{
core_3d::{
graph::{Core3d, Node3d},
Camera3d, DEPTH_TEXTURE_SAMPLING_SUPPORTED,
},
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
};
const DOF_SHADER_HANDLE: Handle<Shader> = weak_handle!("c3580ddc-2cbc-4535-a02b-9a2959066b52");
/// A plugin that adds support for the depth of field effect to Bevy.
pub struct DepthOfFieldPlugin;
/// A component that enables a [depth of field] postprocessing effect when attached to a [`Camera3d`],
/// simulating the focus of a camera lens.
///
/// [depth of field]: https://en.wikipedia.org/wiki/Depth_of_field
#[derive(Component, Clone, Copy, Reflect)]
#[reflect(Component, Clone, Default)]
pub struct DepthOfField {
/// The appearance of the effect.
pub mode: DepthOfFieldMode,
/// The distance in meters to the location in focus.
pub focal_distance: f32,
/// The height of the [image sensor format] in meters.
///
/// Focal length is derived from the FOV and this value. The default is
/// 18.66mm, matching the [Super 35] format, which is popular in cinema.
///
/// [image sensor format]: https://en.wikipedia.org/wiki/Image_sensor_format
///
/// [Super 35]: https://en.wikipedia.org/wiki/Super_35
pub sensor_height: f32,
/// Along with the focal length, controls how much objects not in focus are
/// blurred.
pub aperture_f_stops: f32,
/// The maximum diameter, in pixels, that we allow a circle of confusion to be.
///
/// A circle of confusion essentially describes the size of a blur.
///
/// This value is nonphysical but is useful for avoiding pathologically-slow
/// behavior.
pub max_circle_of_confusion_diameter: f32,
/// Objects are never considered to be farther away than this distance as
/// far as depth of field is concerned, even if they actually are.
///
/// This is primarily useful for skyboxes and background colors. The Bevy
/// renderer considers them to be infinitely far away. Without this value,
/// that would cause the circle of confusion to be infinitely large, capped
/// only by the `max_circle_of_confusion_diameter`. As that's unsightly,
/// this value can be used to essentially adjust how "far away" the skybox
/// or background are.
pub max_depth: f32,
}
/// Controls the appearance of the effect.
#[derive(Clone, Copy, Default, PartialEq, Debug, Reflect)]
#[reflect(Default, Clone, PartialEq)]
pub enum DepthOfFieldMode {
/// A more accurate simulation, in which circles of confusion generate
/// "spots" of light.
///
/// For more information, see [Wikipedia's article on *bokeh*].
///
/// This doesn't work on WebGPU.
///
/// [Wikipedia's article on *bokeh*]: https://en.wikipedia.org/wiki/Bokeh
Bokeh,
/// A faster simulation, in which out-of-focus areas are simply blurred.
///
/// This is less accurate to actual lens behavior and is generally less
/// aesthetically pleasing but requires less video memory bandwidth.
///
/// This is the default.
///
/// This works on native and WebGPU.
/// If targeting native platforms, consider using [`DepthOfFieldMode::Bokeh`] instead.
#[default]
Gaussian,
}
/// Data about the depth of field effect that's uploaded to the GPU.
#[derive(Clone, Copy, Component, ShaderType)]
pub struct DepthOfFieldUniform {
/// The distance in meters to the location in focus.
focal_distance: f32,
/// The focal length. See the comment in `DepthOfFieldParams` in `dof.wgsl`
/// for more information.
focal_length: f32,
/// The premultiplied factor that we scale the circle of confusion by.
///
/// This is calculated as `focal_length² / (sensor_height *
/// aperture_f_stops)`.
coc_scale_factor: f32,
/// The maximum circle of confusion diameter in pixels. See the comment in
/// [`DepthOfField`] for more information.
max_circle_of_confusion_diameter: f32,
/// The depth value that we clamp distant objects to. See the comment in
/// [`DepthOfField`] for more information.
max_depth: f32,
/// Padding.
pad_a: u32,
/// Padding.
pad_b: u32,
/// Padding.
pad_c: u32,
}
/// A key that uniquely identifies depth of field pipelines.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct DepthOfFieldPipelineKey {
/// Whether we're doing Gaussian or bokeh blur.
pass: DofPass,
/// Whether we're using HDR.
hdr: bool,
/// Whether the render target is multisampled.
multisample: bool,
}
/// Identifies a specific depth of field render pass.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
enum DofPass {
/// The first, horizontal, Gaussian blur pass.
GaussianHorizontal,
/// The second, vertical, Gaussian blur pass.
GaussianVertical,
/// The first bokeh pass: vertical and diagonal.
BokehPass0,
/// The second bokeh pass: two diagonals.
BokehPass1,
}
impl Plugin for DepthOfFieldPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, DOF_SHADER_HANDLE, "dof.wgsl", Shader::from_wgsl);
app.register_type::<DepthOfField>();
app.register_type::<DepthOfFieldMode>();
app.add_plugins(UniformComponentPlugin::<DepthOfFieldUniform>::default());
app.add_plugins(SyncComponentPlugin::<DepthOfField>::default());
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedRenderPipelines<DepthOfFieldPipeline>>()
.init_resource::<DepthOfFieldGlobalBindGroup>()
.add_systems(ExtractSchedule, extract_depth_of_field_settings)
.add_systems(
Render,
(
configure_depth_of_field_view_targets,
prepare_auxiliary_depth_of_field_textures,
)
.after(prepare_view_targets)
.in_set(RenderSet::ManageViews),
)
.add_systems(
Render,
(
prepare_depth_of_field_view_bind_group_layouts,
prepare_depth_of_field_pipelines,
)
.chain()
.in_set(RenderSet::Prepare),
)
.add_systems(
Render,
prepare_depth_of_field_global_bind_group.in_set(RenderSet::PrepareBindGroups),
)
.add_render_graph_node::<ViewNodeRunner<DepthOfFieldNode>>(Core3d, Node3d::DepthOfField)
.add_render_graph_edges(
Core3d,
(Node3d::Bloom, Node3d::DepthOfField, Node3d::Tonemapping),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<DepthOfFieldGlobalBindGroupLayout>();
}
}
/// The node in the render graph for depth of field.
#[derive(Default)]
pub struct DepthOfFieldNode;
/// The layout for the bind group shared among all invocations of the depth of
/// field shader.
#[derive(Resource, Clone)]
pub struct DepthOfFieldGlobalBindGroupLayout {
/// The layout.
layout: BindGroupLayout,
/// The sampler used to sample from the color buffer or buffers.
color_texture_sampler: Sampler,
}
/// The bind group shared among all invocations of the depth of field shader,
/// regardless of view.
#[derive(Resource, Default, Deref, DerefMut)]
pub struct DepthOfFieldGlobalBindGroup(Option<BindGroup>);
#[derive(Component)]
pub enum DepthOfFieldPipelines {
Gaussian {
horizontal: CachedRenderPipelineId,
vertical: CachedRenderPipelineId,
},
Bokeh {
pass_0: CachedRenderPipelineId,
pass_1: CachedRenderPipelineId,
},
}
struct DepthOfFieldPipelineRenderInfo {
pass_label: &'static str,
view_bind_group_label: &'static str,
pipeline: CachedRenderPipelineId,
is_dual_input: bool,
is_dual_output: bool,
}
/// The extra texture used as the second render target for the hexagonal bokeh
/// blur.
///
/// This is the same size and format as the main view target texture. It'll only
/// be present if bokeh is being used.
#[derive(Component, Deref, DerefMut)]
pub struct AuxiliaryDepthOfFieldTexture(CachedTexture);
/// Bind group layouts for depth of field specific to a single view.
#[derive(Component, Clone)]
pub struct ViewDepthOfFieldBindGroupLayouts {
/// The bind group layout for passes that take only one input.
single_input: BindGroupLayout,
/// The bind group layout for the second bokeh pass, which takes two inputs.
///
/// This will only be present if bokeh is in use.
dual_input: Option<BindGroupLayout>,
}
/// Information needed to specialize the pipeline corresponding to a pass of the
/// depth of field shader.
pub struct DepthOfFieldPipeline {
/// The bind group layouts specific to each view.
view_bind_group_layouts: ViewDepthOfFieldBindGroupLayouts,
/// The bind group layout shared among all invocations of the depth of field
/// shader.
global_bind_group_layout: BindGroupLayout,
}
impl ViewNode for DepthOfFieldNode {
type ViewQuery = (
Read<ViewUniformOffset>,
Read<ViewTarget>,
Read<ViewDepthTexture>,
Read<DepthOfFieldPipelines>,
Read<ViewDepthOfFieldBindGroupLayouts>,
Read<DynamicUniformIndex<DepthOfFieldUniform>>,
Option<Read<AuxiliaryDepthOfFieldTexture>>,
);
fn run<'w>(
&self,
_: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(
view_uniform_offset,
view_target,
view_depth_texture,
view_pipelines,
view_bind_group_layouts,
depth_of_field_uniform_index,
auxiliary_dof_texture,
): QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let pipeline_cache = world.resource::<PipelineCache>();
let view_uniforms = world.resource::<ViewUniforms>();
let global_bind_group = world.resource::<DepthOfFieldGlobalBindGroup>();
// We can be in either Gaussian blur or bokeh mode here. Both modes are
// similar, consisting of two passes each. We factor out the information
// specific to each pass into
// [`DepthOfFieldPipelines::pipeline_render_info`].
for pipeline_render_info in view_pipelines.pipeline_render_info().iter() {
let (Some(render_pipeline), Some(view_uniforms_binding), Some(global_bind_group)) = (
pipeline_cache.get_render_pipeline(pipeline_render_info.pipeline),
view_uniforms.uniforms.binding(),
&**global_bind_group,
) else {
return Ok(());
};
// We use most of the postprocess infrastructure here. However,
// because the bokeh pass has an additional render target, we have
// to manage a secondary *auxiliary* texture alongside the textures
// managed by the postprocessing logic.
let postprocess = view_target.post_process_write();
let view_bind_group = if pipeline_render_info.is_dual_input {
let (Some(auxiliary_dof_texture), Some(dual_input_bind_group_layout)) = (
auxiliary_dof_texture,
view_bind_group_layouts.dual_input.as_ref(),
) else {
once!(warn!(
"Should have created the auxiliary depth of field texture by now"
));
continue;
};
render_context.render_device().create_bind_group(
Some(pipeline_render_info.view_bind_group_label),
dual_input_bind_group_layout,
&BindGroupEntries::sequential((
view_uniforms_binding,
view_depth_texture.view(),
postprocess.source,
&auxiliary_dof_texture.default_view,
)),
)
} else {
render_context.render_device().create_bind_group(
Some(pipeline_render_info.view_bind_group_label),
&view_bind_group_layouts.single_input,
&BindGroupEntries::sequential((
view_uniforms_binding,
view_depth_texture.view(),
postprocess.source,
)),
)
};
// Push the first input attachment.
let mut color_attachments: SmallVec<[_; 2]> = SmallVec::new();
color_attachments.push(Some(RenderPassColorAttachment {
view: postprocess.destination,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(default()),
store: StoreOp::Store,
},
}));
// The first pass of the bokeh shader has two color outputs, not
// one. Handle this case by attaching the auxiliary texture, which
// should have been created by now in
// `prepare_auxiliary_depth_of_field_textures``.
if pipeline_render_info.is_dual_output {
let Some(auxiliary_dof_texture) = auxiliary_dof_texture else {
once!(warn!(
"Should have created the auxiliary depth of field texture by now"
));
continue;
};
color_attachments.push(Some(RenderPassColorAttachment {
view: &auxiliary_dof_texture.default_view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(default()),
store: StoreOp::Store,
},
}));
}
let render_pass_descriptor = RenderPassDescriptor {
label: Some(pipeline_render_info.pass_label),
color_attachments: &color_attachments,
..default()
};
let mut render_pass = render_context
.command_encoder()
.begin_render_pass(&render_pass_descriptor);
render_pass.set_pipeline(render_pipeline);
// Set the per-view bind group.
render_pass.set_bind_group(0, &view_bind_group, &[view_uniform_offset.offset]);
// Set the global bind group shared among all invocations of the shader.
render_pass.set_bind_group(
1,
global_bind_group,
&[depth_of_field_uniform_index.index()],
);
// Render the full-screen pass.
render_pass.draw(0..3, 0..1);
}
Ok(())
}
}
impl Default for DepthOfField {
fn default() -> Self {
let physical_camera_default = PhysicalCameraParameters::default();
Self {
focal_distance: 10.0,
aperture_f_stops: physical_camera_default.aperture_f_stops,
sensor_height: physical_camera_default.sensor_height,
max_circle_of_confusion_diameter: 64.0,
max_depth: f32::INFINITY,
mode: DepthOfFieldMode::Bokeh,
}
}
}
impl DepthOfField {
/// Initializes [`DepthOfField`] from a set of
/// [`PhysicalCameraParameters`].
///
/// By passing the same [`PhysicalCameraParameters`] object to this function
/// and to [`bevy_render::camera::Exposure::from_physical_camera`], matching
/// results for both the exposure and depth of field effects can be
/// obtained.
///
/// All fields of the returned [`DepthOfField`] other than
/// `focal_length` and `aperture_f_stops` are set to their default values.
pub fn from_physical_camera(camera: &PhysicalCameraParameters) -> DepthOfField {
DepthOfField {
sensor_height: camera.sensor_height,
aperture_f_stops: camera.aperture_f_stops,
..default()
}
}
}
impl FromWorld for DepthOfFieldGlobalBindGroupLayout {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
// Create the bind group layout that will be shared among all instances
// of the depth of field shader.
let layout = render_device.create_bind_group_layout(
Some("depth of field global bind group layout"),
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
// `dof_params`
uniform_buffer::<DepthOfFieldUniform>(true),
// `color_texture_sampler`
sampler(SamplerBindingType::Filtering),
),
),
);
// Create the color texture sampler.
let sampler = render_device.create_sampler(&SamplerDescriptor {
label: Some("depth of field sampler"),
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
..default()
});
DepthOfFieldGlobalBindGroupLayout {
color_texture_sampler: sampler,
layout,
}
}
}
/// Creates the bind group layouts for the depth of field effect that are
/// specific to each view.
pub fn prepare_depth_of_field_view_bind_group_layouts(
mut commands: Commands,
view_targets: Query<(Entity, &DepthOfField, &Msaa)>,
render_device: Res<RenderDevice>,
) {
for (view, depth_of_field, msaa) in view_targets.iter() {
// Create the bind group layout for the passes that take one input.
let single_input = render_device.create_bind_group_layout(
Some("depth of field bind group layout (single input)"),
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
uniform_buffer::<ViewUniform>(true),
if *msaa != Msaa::Off {
texture_depth_2d_multisampled()
} else {
texture_depth_2d()
},
texture_2d(TextureSampleType::Float { filterable: true }),
),
),
);
// If needed, create the bind group layout for the second bokeh pass,
// which takes two inputs. We only need to do this if bokeh is in use.
let dual_input = match depth_of_field.mode {
DepthOfFieldMode::Gaussian => None,
DepthOfFieldMode::Bokeh => Some(render_device.create_bind_group_layout(
Some("depth of field bind group layout (dual input)"),
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
uniform_buffer::<ViewUniform>(true),
if *msaa != Msaa::Off {
texture_depth_2d_multisampled()
} else {
texture_depth_2d()
},
texture_2d(TextureSampleType::Float { filterable: true }),
texture_2d(TextureSampleType::Float { filterable: true }),
),
),
)),
};
commands
.entity(view)
.insert(ViewDepthOfFieldBindGroupLayouts {
single_input,
dual_input,
});
}
}
/// Configures depth textures so that the depth of field shader can read from
/// them.
///
/// By default, the depth buffers that Bevy creates aren't able to be bound as
/// textures. The depth of field shader, however, needs to read from them. So we
/// need to set the appropriate flag to tell Bevy to make samplable depth
/// buffers.
pub fn configure_depth_of_field_view_targets(
mut view_targets: Query<&mut Camera3d, With<DepthOfField>>,
) {
for mut camera_3d in view_targets.iter_mut() {
let mut depth_texture_usages = TextureUsages::from(camera_3d.depth_texture_usages);
depth_texture_usages |= TextureUsages::TEXTURE_BINDING;
camera_3d.depth_texture_usages = depth_texture_usages.into();
}
}
/// Creates depth of field bind group 1, which is shared among all instances of
/// the depth of field shader.
pub fn prepare_depth_of_field_global_bind_group(
global_bind_group_layout: Res<DepthOfFieldGlobalBindGroupLayout>,
mut dof_bind_group: ResMut<DepthOfFieldGlobalBindGroup>,
depth_of_field_uniforms: Res<ComponentUniforms<DepthOfFieldUniform>>,
render_device: Res<RenderDevice>,
) {
let Some(depth_of_field_uniforms) = depth_of_field_uniforms.binding() else {
return;
};
**dof_bind_group = Some(render_device.create_bind_group(
Some("depth of field global bind group"),
&global_bind_group_layout.layout,
&BindGroupEntries::sequential((
depth_of_field_uniforms, // `dof_params`
&global_bind_group_layout.color_texture_sampler, // `color_texture_sampler`
)),
));
}
/// Creates the second render target texture that the first pass of the bokeh
/// effect needs.
pub fn prepare_auxiliary_depth_of_field_textures(
mut commands: Commands,
render_device: Res<RenderDevice>,
mut texture_cache: ResMut<TextureCache>,
mut view_targets: Query<(Entity, &ViewTarget, &DepthOfField)>,
) {
for (entity, view_target, depth_of_field) in view_targets.iter_mut() {
// An auxiliary texture is only needed for bokeh.
if depth_of_field.mode != DepthOfFieldMode::Bokeh {
continue;
}
// The texture matches the main view target texture.
let texture_descriptor = TextureDescriptor {
label: Some("depth of field auxiliary texture"),
size: view_target.main_texture().size(),
mip_level_count: 1,
sample_count: view_target.main_texture().sample_count(),
dimension: TextureDimension::D2,
format: view_target.main_texture_format(),
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
};
let texture = texture_cache.get(&render_device, texture_descriptor);
commands
.entity(entity)
.insert(AuxiliaryDepthOfFieldTexture(texture));
}
}
/// Specializes the depth of field pipelines specific to a view.
pub fn prepare_depth_of_field_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<DepthOfFieldPipeline>>,
global_bind_group_layout: Res<DepthOfFieldGlobalBindGroupLayout>,
view_targets: Query<(
Entity,
&ExtractedView,
&DepthOfField,
&ViewDepthOfFieldBindGroupLayouts,
&Msaa,
)>,
) {
for (entity, view, depth_of_field, view_bind_group_layouts, msaa) in view_targets.iter() {
let dof_pipeline = DepthOfFieldPipeline {
view_bind_group_layouts: view_bind_group_layouts.clone(),
global_bind_group_layout: global_bind_group_layout.layout.clone(),
};
// We'll need these two flags to create the `DepthOfFieldPipelineKey`s.
let (hdr, multisample) = (view.hdr, *msaa != Msaa::Off);
// Go ahead and specialize the pipelines.
match depth_of_field.mode {
DepthOfFieldMode::Gaussian => {
commands
.entity(entity)
.insert(DepthOfFieldPipelines::Gaussian {
horizontal: pipelines.specialize(
&pipeline_cache,
&dof_pipeline,
DepthOfFieldPipelineKey {
hdr,
multisample,
pass: DofPass::GaussianHorizontal,
},
),
vertical: pipelines.specialize(
&pipeline_cache,
&dof_pipeline,
DepthOfFieldPipelineKey {
hdr,
multisample,
pass: DofPass::GaussianVertical,
},
),
});
}
DepthOfFieldMode::Bokeh => {
commands
.entity(entity)
.insert(DepthOfFieldPipelines::Bokeh {
pass_0: pipelines.specialize(
&pipeline_cache,
&dof_pipeline,
DepthOfFieldPipelineKey {
hdr,
multisample,
pass: DofPass::BokehPass0,
},
),
pass_1: pipelines.specialize(
&pipeline_cache,
&dof_pipeline,
DepthOfFieldPipelineKey {
hdr,
multisample,
pass: DofPass::BokehPass1,
},
),
});
}
}
}
}
impl SpecializedRenderPipeline for DepthOfFieldPipeline {
type Key = DepthOfFieldPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
// Build up our pipeline layout.
let (mut layout, mut shader_defs) = (vec![], vec![]);
let mut targets = vec![Some(ColorTargetState {
format: if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
blend: None,
write_mask: ColorWrites::ALL,
})];
// Select bind group 0, the view-specific bind group.
match key.pass {
DofPass::GaussianHorizontal | DofPass::GaussianVertical => {
// Gaussian blurs take only a single input and output.
layout.push(self.view_bind_group_layouts.single_input.clone());
}
DofPass::BokehPass0 => {
// The first bokeh pass takes one input and produces two outputs.
layout.push(self.view_bind_group_layouts.single_input.clone());
targets.push(targets[0].clone());
}
DofPass::BokehPass1 => {
// The second bokeh pass takes the two outputs from the first
// bokeh pass and produces a single output.
let dual_input_bind_group_layout = self
.view_bind_group_layouts
.dual_input
.as_ref()
.expect("Dual-input depth of field bind group should have been created by now")
.clone();
layout.push(dual_input_bind_group_layout);
shader_defs.push("DUAL_INPUT".into());
}
}
// Add bind group 1, the global bind group.
layout.push(self.global_bind_group_layout.clone());
if key.multisample {
shader_defs.push("MULTISAMPLED".into());
}
RenderPipelineDescriptor {
label: Some("depth of field pipeline".into()),
layout,
push_constant_ranges: vec![],
vertex: fullscreen_shader_vertex_state(),
primitive: default(),
depth_stencil: None,
multisample: default(),
fragment: Some(FragmentState {
shader: DOF_SHADER_HANDLE,
shader_defs,
entry_point: match key.pass {
DofPass::GaussianHorizontal => "gaussian_horizontal".into(),
DofPass::GaussianVertical => "gaussian_vertical".into(),
DofPass::BokehPass0 => "bokeh_pass_0".into(),
DofPass::BokehPass1 => "bokeh_pass_1".into(),
},
targets,
}),
zero_initialize_workgroup_memory: false,
}
}
}
/// Extracts all [`DepthOfField`] components into the render world.
fn extract_depth_of_field_settings(
mut commands: Commands,
mut query: Extract<Query<(RenderEntity, &DepthOfField, &Projection)>>,
) {
if !DEPTH_TEXTURE_SAMPLING_SUPPORTED {
once!(info!(
"Disabling depth of field on this platform because depth textures aren't supported correctly"
));
return;
}
for (entity, depth_of_field, projection) in query.iter_mut() {
let mut entity_commands = commands
.get_entity(entity)
.expect("Depth of field entity wasn't synced.");
// Depth of field is nonsensical without a perspective projection.
let Projection::Perspective(ref perspective_projection) = *projection else {
// TODO: needs better strategy for cleaning up
entity_commands.remove::<(
DepthOfField,
DepthOfFieldUniform,
// components added in prepare systems (because `DepthOfFieldNode` does not query extracted components)
DepthOfFieldPipelines,
AuxiliaryDepthOfFieldTexture,
ViewDepthOfFieldBindGroupLayouts,
)>();
continue;
};
let focal_length =
calculate_focal_length(depth_of_field.sensor_height, perspective_projection.fov);
// Convert `DepthOfField` to `DepthOfFieldUniform`.
entity_commands.insert((
*depth_of_field,
DepthOfFieldUniform {
focal_distance: depth_of_field.focal_distance,
focal_length,
coc_scale_factor: focal_length * focal_length
/ (depth_of_field.sensor_height * depth_of_field.aperture_f_stops),
max_circle_of_confusion_diameter: depth_of_field.max_circle_of_confusion_diameter,
max_depth: depth_of_field.max_depth,
pad_a: 0,
pad_b: 0,
pad_c: 0,
},
));
}
}
/// Given the sensor height and the FOV, returns the focal length.
///
/// See <https://photo.stackexchange.com/a/97218>.
pub fn calculate_focal_length(sensor_height: f32, fov: f32) -> f32 {
0.5 * sensor_height / ops::tan(0.5 * fov)
}
impl DepthOfFieldPipelines {
/// Populates the information that the `DepthOfFieldNode` needs for the two
/// depth of field render passes.
fn pipeline_render_info(&self) -> [DepthOfFieldPipelineRenderInfo; 2] {
match *self {
DepthOfFieldPipelines::Gaussian {
horizontal: horizontal_pipeline,
vertical: vertical_pipeline,
} => [
DepthOfFieldPipelineRenderInfo {
pass_label: "depth of field pass (horizontal Gaussian)",
view_bind_group_label: "depth of field view bind group (horizontal Gaussian)",
pipeline: horizontal_pipeline,
is_dual_input: false,
is_dual_output: false,
},
DepthOfFieldPipelineRenderInfo {
pass_label: "depth of field pass (vertical Gaussian)",
view_bind_group_label: "depth of field view bind group (vertical Gaussian)",
pipeline: vertical_pipeline,
is_dual_input: false,
is_dual_output: false,
},
],
DepthOfFieldPipelines::Bokeh {
pass_0: pass_0_pipeline,
pass_1: pass_1_pipeline,
} => [
DepthOfFieldPipelineRenderInfo {
pass_label: "depth of field pass (bokeh pass 0)",
view_bind_group_label: "depth of field view bind group (bokeh pass 0)",
pipeline: pass_0_pipeline,
is_dual_input: false,
is_dual_output: true,
},
DepthOfFieldPipelineRenderInfo {
pass_label: "depth of field pass (bokeh pass 1)",
view_bind_group_label: "depth of field view bind group (bokeh pass 1)",
pipeline: pass_1_pipeline,
is_dual_input: true,
is_dual_output: false,
},
],
}
}
}

View File

@@ -0,0 +1,338 @@
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
@group(0) @binding(0) var mip_0: texture_storage_2d<r64uint, read>;
#else
#ifdef MESHLET
@group(0) @binding(0) var mip_0: texture_storage_2d<r32uint, read>;
#else // MESHLET
#ifdef MULTISAMPLE
@group(0) @binding(0) var mip_0: texture_depth_multisampled_2d;
#else // MULTISAMPLE
@group(0) @binding(0) var mip_0: texture_depth_2d;
#endif // MULTISAMPLE
#endif // MESHLET
#endif // MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
@group(0) @binding(1) var mip_1: texture_storage_2d<r32float, write>;
@group(0) @binding(2) var mip_2: texture_storage_2d<r32float, write>;
@group(0) @binding(3) var mip_3: texture_storage_2d<r32float, write>;
@group(0) @binding(4) var mip_4: texture_storage_2d<r32float, write>;
@group(0) @binding(5) var mip_5: texture_storage_2d<r32float, write>;
@group(0) @binding(6) var mip_6: texture_storage_2d<r32float, read_write>;
@group(0) @binding(7) var mip_7: texture_storage_2d<r32float, write>;
@group(0) @binding(8) var mip_8: texture_storage_2d<r32float, write>;
@group(0) @binding(9) var mip_9: texture_storage_2d<r32float, write>;
@group(0) @binding(10) var mip_10: texture_storage_2d<r32float, write>;
@group(0) @binding(11) var mip_11: texture_storage_2d<r32float, write>;
@group(0) @binding(12) var mip_12: texture_storage_2d<r32float, write>;
@group(0) @binding(13) var samplr: sampler;
struct Constants { max_mip_level: u32 }
var<push_constant> constants: Constants;
/// Generates a hierarchical depth buffer.
/// Based on FidelityFX SPD v2.1 https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/blob/d7531ae47d8b36a5d4025663e731a47a38be882f/sdk/include/FidelityFX/gpu/spd/ffx_spd.h#L528
// TODO:
// * Subgroup support
// * True single pass downsampling
var<workgroup> intermediate_memory: array<array<f32, 16>, 16>;
@compute
@workgroup_size(256, 1, 1)
fn downsample_depth_first(
@builtin(workgroup_id) workgroup_id: vec3u,
@builtin(local_invocation_index) local_invocation_index: u32,
) {
let sub_xy = remap_for_wave_reduction(local_invocation_index % 64u);
let x = sub_xy.x + 8u * ((local_invocation_index >> 6u) % 2u);
let y = sub_xy.y + 8u * (local_invocation_index >> 7u);
downsample_mips_0_and_1(x, y, workgroup_id.xy, local_invocation_index);
downsample_mips_2_to_5(x, y, workgroup_id.xy, local_invocation_index);
}
@compute
@workgroup_size(256, 1, 1)
fn downsample_depth_second(@builtin(local_invocation_index) local_invocation_index: u32) {
let sub_xy = remap_for_wave_reduction(local_invocation_index % 64u);
let x = sub_xy.x + 8u * ((local_invocation_index >> 6u) % 2u);
let y = sub_xy.y + 8u * (local_invocation_index >> 7u);
downsample_mips_6_and_7(x, y);
downsample_mips_8_to_11(x, y, local_invocation_index);
}
fn downsample_mips_0_and_1(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) {
var v: vec4f;
var tex = vec2(workgroup_id * 64u) + vec2(x * 2u, y * 2u);
var pix = vec2(workgroup_id * 32u) + vec2(x, y);
v[0] = reduce_load_mip_0(tex);
textureStore(mip_1, pix, vec4(v[0]));
tex = vec2(workgroup_id * 64u) + vec2(x * 2u + 32u, y * 2u);
pix = vec2(workgroup_id * 32u) + vec2(x + 16u, y);
v[1] = reduce_load_mip_0(tex);
textureStore(mip_1, pix, vec4(v[1]));
tex = vec2(workgroup_id * 64u) + vec2(x * 2u, y * 2u + 32u);
pix = vec2(workgroup_id * 32u) + vec2(x, y + 16u);
v[2] = reduce_load_mip_0(tex);
textureStore(mip_1, pix, vec4(v[2]));
tex = vec2(workgroup_id * 64u) + vec2(x * 2u + 32u, y * 2u + 32u);
pix = vec2(workgroup_id * 32u) + vec2(x + 16u, y + 16u);
v[3] = reduce_load_mip_0(tex);
textureStore(mip_1, pix, vec4(v[3]));
if constants.max_mip_level <= 1u { return; }
for (var i = 0u; i < 4u; i++) {
intermediate_memory[x][y] = v[i];
workgroupBarrier();
if local_invocation_index < 64u {
v[i] = reduce_4(vec4(
intermediate_memory[x * 2u + 0u][y * 2u + 0u],
intermediate_memory[x * 2u + 1u][y * 2u + 0u],
intermediate_memory[x * 2u + 0u][y * 2u + 1u],
intermediate_memory[x * 2u + 1u][y * 2u + 1u],
));
pix = (workgroup_id * 16u) + vec2(
x + (i % 2u) * 8u,
y + (i / 2u) * 8u,
);
textureStore(mip_2, pix, vec4(v[i]));
}
workgroupBarrier();
}
if local_invocation_index < 64u {
intermediate_memory[x + 0u][y + 0u] = v[0];
intermediate_memory[x + 8u][y + 0u] = v[1];
intermediate_memory[x + 0u][y + 8u] = v[2];
intermediate_memory[x + 8u][y + 8u] = v[3];
}
}
fn downsample_mips_2_to_5(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) {
if constants.max_mip_level <= 2u { return; }
workgroupBarrier();
downsample_mip_2(x, y, workgroup_id, local_invocation_index);
if constants.max_mip_level <= 3u { return; }
workgroupBarrier();
downsample_mip_3(x, y, workgroup_id, local_invocation_index);
if constants.max_mip_level <= 4u { return; }
workgroupBarrier();
downsample_mip_4(x, y, workgroup_id, local_invocation_index);
if constants.max_mip_level <= 5u { return; }
workgroupBarrier();
downsample_mip_5(workgroup_id, local_invocation_index);
}
fn downsample_mip_2(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) {
if local_invocation_index < 64u {
let v = reduce_4(vec4(
intermediate_memory[x * 2u + 0u][y * 2u + 0u],
intermediate_memory[x * 2u + 1u][y * 2u + 0u],
intermediate_memory[x * 2u + 0u][y * 2u + 1u],
intermediate_memory[x * 2u + 1u][y * 2u + 1u],
));
textureStore(mip_3, (workgroup_id * 8u) + vec2(x, y), vec4(v));
intermediate_memory[x * 2u + y % 2u][y * 2u] = v;
}
}
fn downsample_mip_3(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) {
if local_invocation_index < 16u {
let v = reduce_4(vec4(
intermediate_memory[x * 4u + 0u + 0u][y * 4u + 0u],
intermediate_memory[x * 4u + 2u + 0u][y * 4u + 0u],
intermediate_memory[x * 4u + 0u + 1u][y * 4u + 2u],
intermediate_memory[x * 4u + 2u + 1u][y * 4u + 2u],
));
textureStore(mip_4, (workgroup_id * 4u) + vec2(x, y), vec4(v));
intermediate_memory[x * 4u + y][y * 4u] = v;
}
}
fn downsample_mip_4(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) {
if local_invocation_index < 4u {
let v = reduce_4(vec4(
intermediate_memory[x * 8u + 0u + 0u + y * 2u][y * 8u + 0u],
intermediate_memory[x * 8u + 4u + 0u + y * 2u][y * 8u + 0u],
intermediate_memory[x * 8u + 0u + 1u + y * 2u][y * 8u + 4u],
intermediate_memory[x * 8u + 4u + 1u + y * 2u][y * 8u + 4u],
));
textureStore(mip_5, (workgroup_id * 2u) + vec2(x, y), vec4(v));
intermediate_memory[x + y * 2u][0u] = v;
}
}
fn downsample_mip_5(workgroup_id: vec2u, local_invocation_index: u32) {
if local_invocation_index < 1u {
let v = reduce_4(vec4(
intermediate_memory[0u][0u],
intermediate_memory[1u][0u],
intermediate_memory[2u][0u],
intermediate_memory[3u][0u],
));
textureStore(mip_6, workgroup_id, vec4(v));
}
}
fn downsample_mips_6_and_7(x: u32, y: u32) {
var v: vec4f;
var tex = vec2(x * 4u + 0u, y * 4u + 0u);
var pix = vec2(x * 2u + 0u, y * 2u + 0u);
v[0] = reduce_load_mip_6(tex);
textureStore(mip_7, pix, vec4(v[0]));
tex = vec2(x * 4u + 2u, y * 4u + 0u);
pix = vec2(x * 2u + 1u, y * 2u + 0u);
v[1] = reduce_load_mip_6(tex);
textureStore(mip_7, pix, vec4(v[1]));
tex = vec2(x * 4u + 0u, y * 4u + 2u);
pix = vec2(x * 2u + 0u, y * 2u + 1u);
v[2] = reduce_load_mip_6(tex);
textureStore(mip_7, pix, vec4(v[2]));
tex = vec2(x * 4u + 2u, y * 4u + 2u);
pix = vec2(x * 2u + 1u, y * 2u + 1u);
v[3] = reduce_load_mip_6(tex);
textureStore(mip_7, pix, vec4(v[3]));
if constants.max_mip_level <= 7u { return; }
let vr = reduce_4(v);
textureStore(mip_8, vec2(x, y), vec4(vr));
intermediate_memory[x][y] = vr;
}
fn downsample_mips_8_to_11(x: u32, y: u32, local_invocation_index: u32) {
if constants.max_mip_level <= 8u { return; }
workgroupBarrier();
downsample_mip_8(x, y, local_invocation_index);
if constants.max_mip_level <= 9u { return; }
workgroupBarrier();
downsample_mip_9(x, y, local_invocation_index);
if constants.max_mip_level <= 10u { return; }
workgroupBarrier();
downsample_mip_10(x, y, local_invocation_index);
if constants.max_mip_level <= 11u { return; }
workgroupBarrier();
downsample_mip_11(local_invocation_index);
}
fn downsample_mip_8(x: u32, y: u32, local_invocation_index: u32) {
if local_invocation_index < 64u {
let v = reduce_4(vec4(
intermediate_memory[x * 2u + 0u][y * 2u + 0u],
intermediate_memory[x * 2u + 1u][y * 2u + 0u],
intermediate_memory[x * 2u + 0u][y * 2u + 1u],
intermediate_memory[x * 2u + 1u][y * 2u + 1u],
));
textureStore(mip_9, vec2(x, y), vec4(v));
intermediate_memory[x * 2u + y % 2u][y * 2u] = v;
}
}
fn downsample_mip_9(x: u32, y: u32, local_invocation_index: u32) {
if local_invocation_index < 16u {
let v = reduce_4(vec4(
intermediate_memory[x * 4u + 0u + 0u][y * 4u + 0u],
intermediate_memory[x * 4u + 2u + 0u][y * 4u + 0u],
intermediate_memory[x * 4u + 0u + 1u][y * 4u + 2u],
intermediate_memory[x * 4u + 2u + 1u][y * 4u + 2u],
));
textureStore(mip_10, vec2(x, y), vec4(v));
intermediate_memory[x * 4u + y][y * 4u] = v;
}
}
fn downsample_mip_10(x: u32, y: u32, local_invocation_index: u32) {
if local_invocation_index < 4u {
let v = reduce_4(vec4(
intermediate_memory[x * 8u + 0u + 0u + y * 2u][y * 8u + 0u],
intermediate_memory[x * 8u + 4u + 0u + y * 2u][y * 8u + 0u],
intermediate_memory[x * 8u + 0u + 1u + y * 2u][y * 8u + 4u],
intermediate_memory[x * 8u + 4u + 1u + y * 2u][y * 8u + 4u],
));
textureStore(mip_11, vec2(x, y), vec4(v));
intermediate_memory[x + y * 2u][0u] = v;
}
}
fn downsample_mip_11(local_invocation_index: u32) {
if local_invocation_index < 1u {
let v = reduce_4(vec4(
intermediate_memory[0u][0u],
intermediate_memory[1u][0u],
intermediate_memory[2u][0u],
intermediate_memory[3u][0u],
));
textureStore(mip_12, vec2(0u, 0u), vec4(v));
}
}
fn remap_for_wave_reduction(a: u32) -> vec2u {
return vec2(
insertBits(extractBits(a, 2u, 3u), a, 0u, 1u),
insertBits(extractBits(a, 3u, 3u), extractBits(a, 1u, 2u), 0u, 2u),
);
}
fn reduce_load_mip_0(tex: vec2u) -> f32 {
let a = load_mip_0(tex.x, tex.y);
let b = load_mip_0(tex.x + 1u, tex.y);
let c = load_mip_0(tex.x, tex.y + 1u);
let d = load_mip_0(tex.x + 1u, tex.y + 1u);
return reduce_4(vec4(a, b, c, d));
}
fn reduce_load_mip_6(tex: vec2u) -> f32 {
return reduce_4(vec4(
textureLoad(mip_6, tex + vec2(0u, 0u)).r,
textureLoad(mip_6, tex + vec2(0u, 1u)).r,
textureLoad(mip_6, tex + vec2(1u, 0u)).r,
textureLoad(mip_6, tex + vec2(1u, 1u)).r,
));
}
fn load_mip_0(x: u32, y: u32) -> f32 {
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
let visibility = textureLoad(mip_0, vec2(x, y)).r;
return bitcast<f32>(u32(visibility >> 32u));
#else // MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
#ifdef MESHLET
let visibility = textureLoad(mip_0, vec2(x, y)).r;
return bitcast<f32>(visibility);
#else // MESHLET
// Downsample the top level.
#ifdef MULTISAMPLE
// The top level is multisampled, so we need to loop over all the samples
// and reduce them to 1.
var result = textureLoad(mip_0, vec2(x, y), 0);
let sample_count = i32(textureNumSamples(mip_0));
for (var sample = 1; sample < sample_count; sample += 1) {
result = min(result, textureLoad(mip_0, vec2(x, y), sample));
}
return result;
#else // MULTISAMPLE
return textureLoad(mip_0, vec2(x, y), 0);
#endif // MULTISAMPLE
#endif // MESHLET
#endif // MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
}
fn reduce_4(v: vec4f) -> f32 {
return min(min(v.x, v.y), min(v.z, v.w));
}

View File

@@ -0,0 +1,779 @@
//! Downsampling of textures to produce mipmap levels.
//!
//! Currently, this module only supports generation of hierarchical Z buffers
//! for occlusion culling. It's marked experimental because the shader is
//! designed only for power-of-two texture sizes and is slightly incorrect for
//! non-power-of-two depth buffer sizes.
use core::array;
use crate::core_3d::{
graph::{Core3d, Node3d},
prepare_core_3d_depth_textures,
};
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
prelude::{resource_exists, Without},
query::{Or, QueryState, With},
resource::Resource,
schedule::IntoScheduleConfigs as _,
system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut},
world::{FromWorld, World},
};
use bevy_math::{uvec2, UVec2, Vec4Swizzles as _};
use bevy_render::batching::gpu_preprocessing::GpuPreprocessingSupport;
use bevy_render::{
experimental::occlusion_culling::{
OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities,
},
render_graph::{Node, NodeRunError, RenderGraphApp, RenderGraphContext},
render_resource::{
binding_types::{sampler, texture_2d, texture_2d_multisampled, texture_storage_2d},
BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
CachedComputePipelineId, ComputePassDescriptor, ComputePipeline, ComputePipelineDescriptor,
Extent3d, IntoBinding, PipelineCache, PushConstantRange, Sampler, SamplerBindingType,
SamplerDescriptor, Shader, ShaderStages, SpecializedComputePipeline,
SpecializedComputePipelines, StorageTextureAccess, TextureAspect, TextureDescriptor,
TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureView,
TextureViewDescriptor, TextureViewDimension,
},
renderer::{RenderContext, RenderDevice},
texture::TextureCache,
view::{ExtractedView, NoIndirectDrawing, ViewDepthTexture},
Render, RenderApp, RenderSet,
};
use bitflags::bitflags;
use tracing::debug;
/// Identifies the `downsample_depth.wgsl` shader.
pub const DOWNSAMPLE_DEPTH_SHADER_HANDLE: Handle<Shader> =
weak_handle!("a09a149e-5922-4fa4-9170-3c1a13065364");
/// The maximum number of mip levels that we can produce.
///
/// 2^12 is 4096, so that's the maximum size of the depth buffer that we
/// support.
pub const DEPTH_PYRAMID_MIP_COUNT: usize = 12;
/// A plugin that allows Bevy to repeatedly downsample textures to create
/// mipmaps.
///
/// Currently, this is only used for hierarchical Z buffer generation for the
/// purposes of occlusion culling.
pub struct MipGenerationPlugin;
impl Plugin for MipGenerationPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
DOWNSAMPLE_DEPTH_SHADER_HANDLE,
"downsample_depth.wgsl",
Shader::from_wgsl
);
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedComputePipelines<DownsampleDepthPipeline>>()
.add_render_graph_node::<DownsampleDepthNode>(Core3d, Node3d::EarlyDownsampleDepth)
.add_render_graph_node::<DownsampleDepthNode>(Core3d, Node3d::LateDownsampleDepth)
.add_render_graph_edges(
Core3d,
(
Node3d::EarlyPrepass,
Node3d::EarlyDeferredPrepass,
Node3d::EarlyDownsampleDepth,
Node3d::LatePrepass,
Node3d::LateDeferredPrepass,
),
)
.add_render_graph_edges(
Core3d,
(
Node3d::EndMainPass,
Node3d::LateDownsampleDepth,
Node3d::EndMainPassPostProcessing,
),
)
.add_systems(
Render,
create_downsample_depth_pipelines.in_set(RenderSet::Prepare),
)
.add_systems(
Render,
(
prepare_view_depth_pyramids,
prepare_downsample_depth_view_bind_groups,
)
.chain()
.in_set(RenderSet::PrepareResources)
.run_if(resource_exists::<DownsampleDepthPipelines>)
.after(prepare_core_3d_depth_textures),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<DepthPyramidDummyTexture>();
}
}
/// The nodes that produce a hierarchical Z-buffer, also known as a depth
/// pyramid.
///
/// This runs the single-pass downsampling (SPD) shader with the *min* filter in
/// order to generate a series of mipmaps for the Z buffer. The resulting
/// hierarchical Z-buffer can be used for occlusion culling.
///
/// There are two instances of this node. The *early* downsample depth pass is
/// the first hierarchical Z-buffer stage, which runs after the early prepass
/// and before the late prepass. It prepares the Z-buffer for the bounding box
/// tests that the late mesh preprocessing stage will perform. The *late*
/// downsample depth pass runs at the end of the main phase. It prepares the
/// Z-buffer for the occlusion culling that the early mesh preprocessing phase
/// of the *next* frame will perform.
///
/// This node won't do anything if occlusion culling isn't on.
pub struct DownsampleDepthNode {
/// The query that we use to find views that need occlusion culling for
/// their Z-buffer.
main_view_query: QueryState<(
Read<ViewDepthPyramid>,
Read<ViewDownsampleDepthBindGroup>,
Read<ViewDepthTexture>,
Option<Read<OcclusionCullingSubviewEntities>>,
)>,
/// The query that we use to find shadow maps that need occlusion culling.
shadow_view_query: QueryState<(
Read<ViewDepthPyramid>,
Read<ViewDownsampleDepthBindGroup>,
Read<OcclusionCullingSubview>,
)>,
}
impl FromWorld for DownsampleDepthNode {
fn from_world(world: &mut World) -> Self {
Self {
main_view_query: QueryState::new(world),
shadow_view_query: QueryState::new(world),
}
}
}
impl Node for DownsampleDepthNode {
fn update(&mut self, world: &mut World) {
self.main_view_query.update_archetypes(world);
self.shadow_view_query.update_archetypes(world);
}
fn run<'w>(
&self,
render_graph_context: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
world: &'w World,
) -> Result<(), NodeRunError> {
let Ok((
view_depth_pyramid,
view_downsample_depth_bind_group,
view_depth_texture,
maybe_view_light_entities,
)) = self
.main_view_query
.get_manual(world, render_graph_context.view_entity())
else {
return Ok(());
};
// Downsample depth for the main Z-buffer.
downsample_depth(
render_graph_context,
render_context,
world,
view_depth_pyramid,
view_downsample_depth_bind_group,
uvec2(
view_depth_texture.texture.width(),
view_depth_texture.texture.height(),
),
view_depth_texture.texture.sample_count(),
)?;
// Downsample depth for shadow maps that have occlusion culling enabled.
if let Some(view_light_entities) = maybe_view_light_entities {
for &view_light_entity in &view_light_entities.0 {
let Ok((view_depth_pyramid, view_downsample_depth_bind_group, occlusion_culling)) =
self.shadow_view_query.get_manual(world, view_light_entity)
else {
continue;
};
downsample_depth(
render_graph_context,
render_context,
world,
view_depth_pyramid,
view_downsample_depth_bind_group,
UVec2::splat(occlusion_culling.depth_texture_size),
1,
)?;
}
}
Ok(())
}
}
/// Produces a depth pyramid from the current depth buffer for a single view.
/// The resulting depth pyramid can be used for occlusion testing.
fn downsample_depth<'w>(
render_graph_context: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
world: &'w World,
view_depth_pyramid: &ViewDepthPyramid,
view_downsample_depth_bind_group: &ViewDownsampleDepthBindGroup,
view_size: UVec2,
sample_count: u32,
) -> Result<(), NodeRunError> {
let downsample_depth_pipelines = world.resource::<DownsampleDepthPipelines>();
let pipeline_cache = world.resource::<PipelineCache>();
// Despite the name "single-pass downsampling", we actually need two
// passes because of the lack of `coherent` buffers in WGPU/WGSL.
// Between each pass, there's an implicit synchronization barrier.
// Fetch the appropriate pipeline ID, depending on whether the depth
// buffer is multisampled or not.
let (Some(first_downsample_depth_pipeline_id), Some(second_downsample_depth_pipeline_id)) =
(if sample_count > 1 {
(
downsample_depth_pipelines.first_multisample.pipeline_id,
downsample_depth_pipelines.second_multisample.pipeline_id,
)
} else {
(
downsample_depth_pipelines.first.pipeline_id,
downsample_depth_pipelines.second.pipeline_id,
)
})
else {
return Ok(());
};
// Fetch the pipelines for the two passes.
let (Some(first_downsample_depth_pipeline), Some(second_downsample_depth_pipeline)) = (
pipeline_cache.get_compute_pipeline(first_downsample_depth_pipeline_id),
pipeline_cache.get_compute_pipeline(second_downsample_depth_pipeline_id),
) else {
return Ok(());
};
// Run the depth downsampling.
view_depth_pyramid.downsample_depth(
&format!("{:?}", render_graph_context.label()),
render_context,
view_size,
view_downsample_depth_bind_group,
first_downsample_depth_pipeline,
second_downsample_depth_pipeline,
);
Ok(())
}
/// A single depth downsample pipeline.
#[derive(Resource)]
pub struct DownsampleDepthPipeline {
/// The bind group layout for this pipeline.
bind_group_layout: BindGroupLayout,
/// A handle that identifies the compiled shader.
pipeline_id: Option<CachedComputePipelineId>,
}
impl DownsampleDepthPipeline {
/// Creates a new [`DownsampleDepthPipeline`] from a bind group layout.
///
/// This doesn't actually specialize the pipeline; that must be done
/// afterward.
fn new(bind_group_layout: BindGroupLayout) -> DownsampleDepthPipeline {
DownsampleDepthPipeline {
bind_group_layout,
pipeline_id: None,
}
}
}
/// Stores all depth buffer downsampling pipelines.
#[derive(Resource)]
pub struct DownsampleDepthPipelines {
/// The first pass of the pipeline, when the depth buffer is *not*
/// multisampled.
first: DownsampleDepthPipeline,
/// The second pass of the pipeline, when the depth buffer is *not*
/// multisampled.
second: DownsampleDepthPipeline,
/// The first pass of the pipeline, when the depth buffer is multisampled.
first_multisample: DownsampleDepthPipeline,
/// The second pass of the pipeline, when the depth buffer is multisampled.
second_multisample: DownsampleDepthPipeline,
/// The sampler that the depth downsampling shader uses to sample the depth
/// buffer.
sampler: Sampler,
}
/// Creates the [`DownsampleDepthPipelines`] if downsampling is supported on the
/// current platform.
fn create_downsample_depth_pipelines(
mut commands: Commands,
render_device: Res<RenderDevice>,
pipeline_cache: Res<PipelineCache>,
mut specialized_compute_pipelines: ResMut<SpecializedComputePipelines<DownsampleDepthPipeline>>,
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
mut has_run: Local<bool>,
) {
// Only run once.
// We can't use a `resource_exists` or similar run condition here because
// this function might fail to create downsample depth pipelines if the
// current platform doesn't support compute shaders.
if *has_run {
return;
}
*has_run = true;
if !gpu_preprocessing_support.is_culling_supported() {
debug!("Downsample depth is not supported on this platform.");
return;
}
// Create the bind group layouts. The bind group layouts are identical
// between the first and second passes, so the only thing we need to
// treat specially is the type of the first mip level (non-multisampled
// or multisampled).
let standard_bind_group_layout =
create_downsample_depth_bind_group_layout(&render_device, false);
let multisampled_bind_group_layout =
create_downsample_depth_bind_group_layout(&render_device, true);
// Create the depth pyramid sampler. This is shared among all shaders.
let sampler = render_device.create_sampler(&SamplerDescriptor {
label: Some("depth pyramid sampler"),
..SamplerDescriptor::default()
});
// Initialize the pipelines.
let mut downsample_depth_pipelines = DownsampleDepthPipelines {
first: DownsampleDepthPipeline::new(standard_bind_group_layout.clone()),
second: DownsampleDepthPipeline::new(standard_bind_group_layout.clone()),
first_multisample: DownsampleDepthPipeline::new(multisampled_bind_group_layout.clone()),
second_multisample: DownsampleDepthPipeline::new(multisampled_bind_group_layout.clone()),
sampler,
};
// Specialize each pipeline with the appropriate
// `DownsampleDepthPipelineKey`.
downsample_depth_pipelines.first.pipeline_id = Some(specialized_compute_pipelines.specialize(
&pipeline_cache,
&downsample_depth_pipelines.first,
DownsampleDepthPipelineKey::empty(),
));
downsample_depth_pipelines.second.pipeline_id = Some(specialized_compute_pipelines.specialize(
&pipeline_cache,
&downsample_depth_pipelines.second,
DownsampleDepthPipelineKey::SECOND_PHASE,
));
downsample_depth_pipelines.first_multisample.pipeline_id =
Some(specialized_compute_pipelines.specialize(
&pipeline_cache,
&downsample_depth_pipelines.first_multisample,
DownsampleDepthPipelineKey::MULTISAMPLE,
));
downsample_depth_pipelines.second_multisample.pipeline_id =
Some(specialized_compute_pipelines.specialize(
&pipeline_cache,
&downsample_depth_pipelines.second_multisample,
DownsampleDepthPipelineKey::SECOND_PHASE | DownsampleDepthPipelineKey::MULTISAMPLE,
));
commands.insert_resource(downsample_depth_pipelines);
}
/// Creates a single bind group layout for the downsample depth pass.
fn create_downsample_depth_bind_group_layout(
render_device: &RenderDevice,
is_multisampled: bool,
) -> BindGroupLayout {
render_device.create_bind_group_layout(
if is_multisampled {
"downsample multisample depth bind group layout"
} else {
"downsample depth bind group layout"
},
&BindGroupLayoutEntries::sequential(
ShaderStages::COMPUTE,
(
// We only care about the multisample status of the depth buffer
// for the first mip level. After the first mip level is
// sampled, we drop to a single sample.
if is_multisampled {
texture_2d_multisampled(TextureSampleType::Depth)
} else {
texture_2d(TextureSampleType::Depth)
},
// All the mip levels follow:
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::ReadWrite),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly),
sampler(SamplerBindingType::NonFiltering),
),
),
)
}
bitflags! {
/// Uniquely identifies a configuration of the downsample depth shader.
///
/// Note that meshlets maintain their downsample depth shaders on their own
/// and don't use this infrastructure; thus there's no flag for meshlets in
/// here, even though the shader has defines for it.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct DownsampleDepthPipelineKey: u8 {
/// True if the depth buffer is multisampled.
const MULTISAMPLE = 1;
/// True if this shader is the second phase of the downsample depth
/// process; false if this shader is the first phase.
const SECOND_PHASE = 2;
}
}
impl SpecializedComputePipeline for DownsampleDepthPipeline {
type Key = DownsampleDepthPipelineKey;
fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor {
let mut shader_defs = vec![];
if key.contains(DownsampleDepthPipelineKey::MULTISAMPLE) {
shader_defs.push("MULTISAMPLE".into());
}
let label = format!(
"downsample depth{}{} pipeline",
if key.contains(DownsampleDepthPipelineKey::MULTISAMPLE) {
" multisample"
} else {
""
},
if key.contains(DownsampleDepthPipelineKey::SECOND_PHASE) {
" second phase"
} else {
" first phase"
}
)
.into();
ComputePipelineDescriptor {
label: Some(label),
layout: vec![self.bind_group_layout.clone()],
push_constant_ranges: vec![PushConstantRange {
stages: ShaderStages::COMPUTE,
range: 0..4,
}],
shader: DOWNSAMPLE_DEPTH_SHADER_HANDLE,
shader_defs,
entry_point: if key.contains(DownsampleDepthPipelineKey::SECOND_PHASE) {
"downsample_depth_second".into()
} else {
"downsample_depth_first".into()
},
zero_initialize_workgroup_memory: false,
}
}
}
/// Stores a placeholder texture that can be bound to a depth pyramid binding if
/// no depth pyramid is needed.
#[derive(Resource, Deref, DerefMut)]
pub struct DepthPyramidDummyTexture(TextureView);
impl FromWorld for DepthPyramidDummyTexture {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
DepthPyramidDummyTexture(create_depth_pyramid_dummy_texture(
render_device,
"depth pyramid dummy texture",
"depth pyramid dummy texture view",
))
}
}
/// Creates a placeholder texture that can be bound to a depth pyramid binding
/// if no depth pyramid is needed.
pub fn create_depth_pyramid_dummy_texture(
render_device: &RenderDevice,
texture_label: &'static str,
texture_view_label: &'static str,
) -> TextureView {
render_device
.create_texture(&TextureDescriptor {
label: Some(texture_label),
size: Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::R32Float,
usage: TextureUsages::STORAGE_BINDING,
view_formats: &[],
})
.create_view(&TextureViewDescriptor {
label: Some(texture_view_label),
format: Some(TextureFormat::R32Float),
dimension: Some(TextureViewDimension::D2),
usage: None,
aspect: TextureAspect::All,
base_mip_level: 0,
mip_level_count: Some(1),
base_array_layer: 0,
array_layer_count: Some(1),
})
}
/// Stores a hierarchical Z-buffer for a view, which is a series of mipmaps
/// useful for efficient occlusion culling.
///
/// This will only be present on a view when occlusion culling is enabled.
#[derive(Component)]
pub struct ViewDepthPyramid {
/// A texture view containing the entire depth texture.
pub all_mips: TextureView,
/// A series of texture views containing one mip level each.
pub mips: [TextureView; DEPTH_PYRAMID_MIP_COUNT],
/// The total number of mipmap levels.
///
/// This is the base-2 logarithm of the greatest dimension of the depth
/// buffer, rounded up.
pub mip_count: u32,
}
impl ViewDepthPyramid {
/// Allocates a new depth pyramid for a depth buffer with the given size.
pub fn new(
render_device: &RenderDevice,
texture_cache: &mut TextureCache,
depth_pyramid_dummy_texture: &TextureView,
size: UVec2,
texture_label: &'static str,
texture_view_label: &'static str,
) -> ViewDepthPyramid {
// Calculate the size of the depth pyramid.
let depth_pyramid_size = Extent3d {
width: size.x.div_ceil(2),
height: size.y.div_ceil(2),
depth_or_array_layers: 1,
};
// Calculate the number of mip levels we need.
let depth_pyramid_mip_count = depth_pyramid_size.max_mips(TextureDimension::D2);
// Create the depth pyramid.
let depth_pyramid = texture_cache.get(
render_device,
TextureDescriptor {
label: Some(texture_label),
size: depth_pyramid_size,
mip_level_count: depth_pyramid_mip_count,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::R32Float,
usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
);
// Create individual views for each level of the depth pyramid.
let depth_pyramid_mips = array::from_fn(|i| {
if (i as u32) < depth_pyramid_mip_count {
depth_pyramid.texture.create_view(&TextureViewDescriptor {
label: Some(texture_view_label),
format: Some(TextureFormat::R32Float),
dimension: Some(TextureViewDimension::D2),
usage: None,
aspect: TextureAspect::All,
base_mip_level: i as u32,
mip_level_count: Some(1),
base_array_layer: 0,
array_layer_count: Some(1),
})
} else {
(*depth_pyramid_dummy_texture).clone()
}
});
// Create the view for the depth pyramid as a whole.
let depth_pyramid_all_mips = depth_pyramid.default_view.clone();
Self {
all_mips: depth_pyramid_all_mips,
mips: depth_pyramid_mips,
mip_count: depth_pyramid_mip_count,
}
}
/// Creates a bind group that allows the depth buffer to be attached to the
/// `downsample_depth.wgsl` shader.
pub fn create_bind_group<'a, R>(
&'a self,
render_device: &RenderDevice,
label: &'static str,
bind_group_layout: &BindGroupLayout,
source_image: R,
sampler: &'a Sampler,
) -> BindGroup
where
R: IntoBinding<'a>,
{
render_device.create_bind_group(
label,
bind_group_layout,
&BindGroupEntries::sequential((
source_image,
&self.mips[0],
&self.mips[1],
&self.mips[2],
&self.mips[3],
&self.mips[4],
&self.mips[5],
&self.mips[6],
&self.mips[7],
&self.mips[8],
&self.mips[9],
&self.mips[10],
&self.mips[11],
sampler,
)),
)
}
/// Invokes the shaders to generate the hierarchical Z-buffer.
///
/// This is intended to be invoked as part of a render node.
pub fn downsample_depth(
&self,
label: &str,
render_context: &mut RenderContext,
view_size: UVec2,
downsample_depth_bind_group: &BindGroup,
downsample_depth_first_pipeline: &ComputePipeline,
downsample_depth_second_pipeline: &ComputePipeline,
) {
let command_encoder = render_context.command_encoder();
let mut downsample_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor {
label: Some(label),
timestamp_writes: None,
});
downsample_pass.set_pipeline(downsample_depth_first_pipeline);
// Pass the mip count as a push constant, for simplicity.
downsample_pass.set_push_constants(0, &self.mip_count.to_le_bytes());
downsample_pass.set_bind_group(0, downsample_depth_bind_group, &[]);
downsample_pass.dispatch_workgroups(view_size.x.div_ceil(64), view_size.y.div_ceil(64), 1);
if self.mip_count >= 7 {
downsample_pass.set_pipeline(downsample_depth_second_pipeline);
downsample_pass.dispatch_workgroups(1, 1, 1);
}
}
}
/// Creates depth pyramids for views that have occlusion culling enabled.
pub fn prepare_view_depth_pyramids(
mut commands: Commands,
render_device: Res<RenderDevice>,
mut texture_cache: ResMut<TextureCache>,
depth_pyramid_dummy_texture: Res<DepthPyramidDummyTexture>,
views: Query<(Entity, &ExtractedView), (With<OcclusionCulling>, Without<NoIndirectDrawing>)>,
) {
for (view_entity, view) in &views {
commands.entity(view_entity).insert(ViewDepthPyramid::new(
&render_device,
&mut texture_cache,
&depth_pyramid_dummy_texture,
view.viewport.zw(),
"view depth pyramid texture",
"view depth pyramid texture view",
));
}
}
/// The bind group that we use to attach the depth buffer and depth pyramid for
/// a view to the `downsample_depth.wgsl` shader.
///
/// This will only be present for a view if occlusion culling is enabled.
#[derive(Component, Deref, DerefMut)]
pub struct ViewDownsampleDepthBindGroup(BindGroup);
/// Creates the [`ViewDownsampleDepthBindGroup`]s for all views with occlusion
/// culling enabled.
fn prepare_downsample_depth_view_bind_groups(
mut commands: Commands,
render_device: Res<RenderDevice>,
downsample_depth_pipelines: Res<DownsampleDepthPipelines>,
view_depth_textures: Query<
(
Entity,
&ViewDepthPyramid,
Option<&ViewDepthTexture>,
Option<&OcclusionCullingSubview>,
),
Or<(With<ViewDepthTexture>, With<OcclusionCullingSubview>)>,
>,
) {
for (view_entity, view_depth_pyramid, view_depth_texture, shadow_occlusion_culling) in
&view_depth_textures
{
let is_multisampled = view_depth_texture
.is_some_and(|view_depth_texture| view_depth_texture.texture.sample_count() > 1);
commands
.entity(view_entity)
.insert(ViewDownsampleDepthBindGroup(
view_depth_pyramid.create_bind_group(
&render_device,
if is_multisampled {
"downsample multisample depth bind group"
} else {
"downsample depth bind group"
},
if is_multisampled {
&downsample_depth_pipelines
.first_multisample
.bind_group_layout
} else {
&downsample_depth_pipelines.first.bind_group_layout
},
match (view_depth_texture, shadow_occlusion_culling) {
(Some(view_depth_texture), _) => view_depth_texture.view(),
(None, Some(shadow_occlusion_culling)) => {
&shadow_occlusion_culling.depth_texture_view
}
(None, None) => panic!("Should never happen"),
},
&downsample_depth_pipelines.sampler,
),
));
}
}

View File

@@ -0,0 +1,11 @@
//! Experimental rendering features.
//!
//! Experimental features are features with known problems, missing features,
//! compatibility issues, low performance, and/or future breaking changes, but
//! are included nonetheless for testing purposes.
pub mod mip_generation;
pub mod taa {
pub use crate::taa::{TemporalAntiAliasNode, TemporalAntiAliasPlugin, TemporalAntiAliasing};
}

View File

@@ -0,0 +1,34 @@
#define_import_path bevy_core_pipeline::fullscreen_vertex_shader
struct FullscreenVertexOutput {
@builtin(position)
position: vec4<f32>,
@location(0)
uv: vec2<f32>,
};
// This vertex shader produces the following, when drawn using indices 0..3:
//
// 1 | 0-----x.....2
// 0 | | s | . ´
// -1 | x_____x´
// -2 | : .´
// -3 | 1´
// +---------------
// -1 0 1 2 3
//
// The axes are clip-space x and y. The region marked s is the visible region.
// The digits in the corners of the right-angled triangle are the vertex
// indices.
//
// The top-left has UV 0,0, the bottom-left has 0,2, and the top-right has 2,0.
// This means that the UV gets interpolated to 1,1 at the bottom-right corner
// of the clip-space rectangle that is at 1,-1 in clip space.
@vertex
fn fullscreen_vertex_shader(@builtin(vertex_index) vertex_index: u32) -> FullscreenVertexOutput {
// See the explanation above for how this works
let uv = vec2<f32>(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0;
let clip_position = vec4<f32>(uv * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);
return FullscreenVertexOutput(clip_position, uv);
}

View File

@@ -0,0 +1,25 @@
use bevy_asset::{weak_handle, Handle};
use bevy_render::{prelude::Shader, render_resource::VertexState};
pub const FULLSCREEN_SHADER_HANDLE: Handle<Shader> =
weak_handle!("481fb759-d0b1-4175-8319-c439acde30a2");
/// uses the [`FULLSCREEN_SHADER_HANDLE`] to output a
/// ```wgsl
/// struct FullscreenVertexOutput {
/// [[builtin(position)]]
/// position: vec4<f32>;
/// [[location(0)]]
/// uv: vec2<f32>;
/// };
/// ```
/// from the vertex shader.
/// The draw call should render one triangle: `render_pass.draw(0..3, 0..1);`
pub fn fullscreen_shader_vertex_state() -> VertexState {
VertexState {
shader: FULLSCREEN_SHADER_HANDLE,
shader_defs: Vec::new(),
entry_point: "fullscreen_vertex_shader".into(),
buffers: Vec::new(),
}
}

View File

@@ -0,0 +1,274 @@
// NVIDIA FXAA 3.11
// Original source code by TIMOTHY LOTTES
// https://gist.github.com/kosua20/0c506b81b3812ac900048059d2383126
//
// Cleaned version - https://github.com/kosua20/Rendu/blob/master/resources/common/shaders/screens/fxaa.frag
//
// Tweaks by mrDIMAS - https://github.com/FyroxEngine/Fyrox/blob/master/src/renderer/shaders/fxaa_fs.glsl
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
@group(0) @binding(0) var screenTexture: texture_2d<f32>;
@group(0) @binding(1) var samp: sampler;
// Trims the algorithm from processing darks.
#ifdef EDGE_THRESH_MIN_LOW
const EDGE_THRESHOLD_MIN: f32 = 0.0833;
#endif
#ifdef EDGE_THRESH_MIN_MEDIUM
const EDGE_THRESHOLD_MIN: f32 = 0.0625;
#endif
#ifdef EDGE_THRESH_MIN_HIGH
const EDGE_THRESHOLD_MIN: f32 = 0.0312;
#endif
#ifdef EDGE_THRESH_MIN_ULTRA
const EDGE_THRESHOLD_MIN: f32 = 0.0156;
#endif
#ifdef EDGE_THRESH_MIN_EXTREME
const EDGE_THRESHOLD_MIN: f32 = 0.0078;
#endif
// The minimum amount of local contrast required to apply algorithm.
#ifdef EDGE_THRESH_LOW
const EDGE_THRESHOLD_MAX: f32 = 0.250;
#endif
#ifdef EDGE_THRESH_MEDIUM
const EDGE_THRESHOLD_MAX: f32 = 0.166;
#endif
#ifdef EDGE_THRESH_HIGH
const EDGE_THRESHOLD_MAX: f32 = 0.125;
#endif
#ifdef EDGE_THRESH_ULTRA
const EDGE_THRESHOLD_MAX: f32 = 0.063;
#endif
#ifdef EDGE_THRESH_EXTREME
const EDGE_THRESHOLD_MAX: f32 = 0.031;
#endif
const ITERATIONS: i32 = 12; //default is 12
const SUBPIXEL_QUALITY: f32 = 0.75;
// #define QUALITY(q) ((q) < 5 ? 1.0 : ((q) > 5 ? ((q) < 10 ? 2.0 : ((q) < 11 ? 4.0 : 8.0)) : 1.5))
fn QUALITY(q: i32) -> f32 {
switch (q) {
//case 0, 1, 2, 3, 4: { return 1.0; }
default: { return 1.0; }
case 5: { return 1.5; }
case 6, 7, 8, 9: { return 2.0; }
case 10: { return 4.0; }
case 11: { return 8.0; }
}
}
fn rgb2luma(rgb: vec3<f32>) -> f32 {
return sqrt(dot(rgb, vec3<f32>(0.299, 0.587, 0.114)));
}
// Performs FXAA post-process anti-aliasing as described in the Nvidia FXAA white paper and the associated shader code.
@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
let resolution = vec2<f32>(textureDimensions(screenTexture));
let inverseScreenSize = 1.0 / resolution.xy;
let texCoord = in.position.xy * inverseScreenSize;
let centerSample = textureSampleLevel(screenTexture, samp, texCoord, 0.0);
let colorCenter = centerSample.rgb;
// Luma at the current fragment
let lumaCenter = rgb2luma(colorCenter);
// Luma at the four direct neighbors of the current fragment.
let lumaDown = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2<i32>(0, -1)).rgb);
let lumaUp = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2<i32>(0, 1)).rgb);
let lumaLeft = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2<i32>(-1, 0)).rgb);
let lumaRight = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2<i32>(1, 0)).rgb);
// Find the maximum and minimum luma around the current fragment.
let lumaMin = min(lumaCenter, min(min(lumaDown, lumaUp), min(lumaLeft, lumaRight)));
let lumaMax = max(lumaCenter, max(max(lumaDown, lumaUp), max(lumaLeft, lumaRight)));
// Compute the delta.
let lumaRange = lumaMax - lumaMin;
// If the luma variation is lower that a threshold (or if we are in a really dark area), we are not on an edge, don't perform any AA.
if (lumaRange < max(EDGE_THRESHOLD_MIN, lumaMax * EDGE_THRESHOLD_MAX)) {
return centerSample;
}
// Query the 4 remaining corners lumas.
let lumaDownLeft = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2<i32>(-1, -1)).rgb);
let lumaUpRight = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2<i32>(1, 1)).rgb);
let lumaUpLeft = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2<i32>(-1, 1)).rgb);
let lumaDownRight = rgb2luma(textureSampleLevel(screenTexture, samp, texCoord, 0.0, vec2<i32>(1, -1)).rgb);
// Combine the four edges lumas (using intermediary variables for future computations with the same values).
let lumaDownUp = lumaDown + lumaUp;
let lumaLeftRight = lumaLeft + lumaRight;
// Same for corners
let lumaLeftCorners = lumaDownLeft + lumaUpLeft;
let lumaDownCorners = lumaDownLeft + lumaDownRight;
let lumaRightCorners = lumaDownRight + lumaUpRight;
let lumaUpCorners = lumaUpRight + lumaUpLeft;
// Compute an estimation of the gradient along the horizontal and vertical axis.
let edgeHorizontal = abs(-2.0 * lumaLeft + lumaLeftCorners) +
abs(-2.0 * lumaCenter + lumaDownUp) * 2.0 +
abs(-2.0 * lumaRight + lumaRightCorners);
let edgeVertical = abs(-2.0 * lumaUp + lumaUpCorners) +
abs(-2.0 * lumaCenter + lumaLeftRight) * 2.0 +
abs(-2.0 * lumaDown + lumaDownCorners);
// Is the local edge horizontal or vertical ?
let isHorizontal = (edgeHorizontal >= edgeVertical);
// Choose the step size (one pixel) accordingly.
var stepLength = select(inverseScreenSize.x, inverseScreenSize.y, isHorizontal);
// Select the two neighboring texels lumas in the opposite direction to the local edge.
var luma1 = select(lumaLeft, lumaDown, isHorizontal);
var luma2 = select(lumaRight, lumaUp, isHorizontal);
// Compute gradients in this direction.
let gradient1 = luma1 - lumaCenter;
let gradient2 = luma2 - lumaCenter;
// Which direction is the steepest ?
let is1Steepest = abs(gradient1) >= abs(gradient2);
// Gradient in the corresponding direction, normalized.
let gradientScaled = 0.25 * max(abs(gradient1), abs(gradient2));
// Average luma in the correct direction.
var lumaLocalAverage = 0.0;
if (is1Steepest) {
// Switch the direction
stepLength = -stepLength;
lumaLocalAverage = 0.5 * (luma1 + lumaCenter);
} else {
lumaLocalAverage = 0.5 * (luma2 + lumaCenter);
}
// Shift UV in the correct direction by half a pixel.
// Compute offset (for each iteration step) in the right direction.
var currentUv = texCoord;
var offset = vec2<f32>(0.0, 0.0);
if (isHorizontal) {
currentUv.y = currentUv.y + stepLength * 0.5;
offset.x = inverseScreenSize.x;
} else {
currentUv.x = currentUv.x + stepLength * 0.5;
offset.y = inverseScreenSize.y;
}
// Compute UVs to explore on each side of the edge, orthogonally. The QUALITY allows us to step faster.
var uv1 = currentUv - offset; // * QUALITY(0); // (quality 0 is 1.0)
var uv2 = currentUv + offset; // * QUALITY(0); // (quality 0 is 1.0)
// Read the lumas at both current extremities of the exploration segment, and compute the delta wrt to the local average luma.
var lumaEnd1 = rgb2luma(textureSampleLevel(screenTexture, samp, uv1, 0.0).rgb);
var lumaEnd2 = rgb2luma(textureSampleLevel(screenTexture, samp, uv2, 0.0).rgb);
lumaEnd1 = lumaEnd1 - lumaLocalAverage;
lumaEnd2 = lumaEnd2 - lumaLocalAverage;
// If the luma deltas at the current extremities is larger than the local gradient, we have reached the side of the edge.
var reached1 = abs(lumaEnd1) >= gradientScaled;
var reached2 = abs(lumaEnd2) >= gradientScaled;
var reachedBoth = reached1 && reached2;
// If the side is not reached, we continue to explore in this direction.
uv1 = select(uv1 - offset, uv1, reached1); // * QUALITY(1); // (quality 1 is 1.0)
uv2 = select(uv2 - offset, uv2, reached2); // * QUALITY(1); // (quality 1 is 1.0)
// If both sides have not been reached, continue to explore.
if (!reachedBoth) {
for (var i: i32 = 2; i < ITERATIONS; i = i + 1) {
// If needed, read luma in 1st direction, compute delta.
if (!reached1) {
lumaEnd1 = rgb2luma(textureSampleLevel(screenTexture, samp, uv1, 0.0).rgb);
lumaEnd1 = lumaEnd1 - lumaLocalAverage;
}
// If needed, read luma in opposite direction, compute delta.
if (!reached2) {
lumaEnd2 = rgb2luma(textureSampleLevel(screenTexture, samp, uv2, 0.0).rgb);
lumaEnd2 = lumaEnd2 - lumaLocalAverage;
}
// If the luma deltas at the current extremities is larger than the local gradient, we have reached the side of the edge.
reached1 = abs(lumaEnd1) >= gradientScaled;
reached2 = abs(lumaEnd2) >= gradientScaled;
reachedBoth = reached1 && reached2;
// If the side is not reached, we continue to explore in this direction, with a variable quality.
if (!reached1) {
uv1 = uv1 - offset * QUALITY(i);
}
if (!reached2) {
uv2 = uv2 + offset * QUALITY(i);
}
// If both sides have been reached, stop the exploration.
if (reachedBoth) {
break;
}
}
}
// Compute the distances to each side edge of the edge (!).
var distance1 = select(texCoord.y - uv1.y, texCoord.x - uv1.x, isHorizontal);
var distance2 = select(uv2.y - texCoord.y, uv2.x - texCoord.x, isHorizontal);
// In which direction is the side of the edge closer ?
let isDirection1 = distance1 < distance2;
let distanceFinal = min(distance1, distance2);
// Thickness of the edge.
let edgeThickness = (distance1 + distance2);
// Is the luma at center smaller than the local average ?
let isLumaCenterSmaller = lumaCenter < lumaLocalAverage;
// If the luma at center is smaller than at its neighbor, the delta luma at each end should be positive (same variation).
let correctVariation1 = (lumaEnd1 < 0.0) != isLumaCenterSmaller;
let correctVariation2 = (lumaEnd2 < 0.0) != isLumaCenterSmaller;
// Only keep the result in the direction of the closer side of the edge.
var correctVariation = select(correctVariation2, correctVariation1, isDirection1);
// UV offset: read in the direction of the closest side of the edge.
let pixelOffset = - distanceFinal / edgeThickness + 0.5;
// If the luma variation is incorrect, do not offset.
var finalOffset = select(0.0, pixelOffset, correctVariation);
// Sub-pixel shifting
// Full weighted average of the luma over the 3x3 neighborhood.
let lumaAverage = (1.0 / 12.0) * (2.0 * (lumaDownUp + lumaLeftRight) + lumaLeftCorners + lumaRightCorners);
// Ratio of the delta between the global average and the center luma, over the luma range in the 3x3 neighborhood.
let subPixelOffset1 = clamp(abs(lumaAverage - lumaCenter) / lumaRange, 0.0, 1.0);
let subPixelOffset2 = (-2.0 * subPixelOffset1 + 3.0) * subPixelOffset1 * subPixelOffset1;
// Compute a sub-pixel offset based on this delta.
let subPixelOffsetFinal = subPixelOffset2 * subPixelOffset2 * SUBPIXEL_QUALITY;
// Pick the biggest of the two offsets.
finalOffset = max(finalOffset, subPixelOffsetFinal);
// Compute the final UV coordinates.
var finalUv = texCoord;
if (isHorizontal) {
finalUv.y = finalUv.y + finalOffset * stepLength;
} else {
finalUv.x = finalUv.x + finalOffset * stepLength;
}
// Read the color at the new UV coordinates, and use it.
var finalColor = textureSampleLevel(screenTexture, samp, finalUv, 0.0).rgb;
return vec4<f32>(finalColor, centerSample.a);
}

View File

@@ -0,0 +1,233 @@
use crate::{
core_2d::graph::{Core2d, Node2d},
core_3d::graph::{Core3d, Node3d},
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
};
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_ecs::prelude::*;
use bevy_image::BevyDefault as _;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
prelude::Camera,
render_graph::{RenderGraphApp, ViewNodeRunner},
render_resource::{
binding_types::{sampler, texture_2d},
*,
},
renderer::RenderDevice,
view::{ExtractedView, ViewTarget},
Render, RenderApp, RenderSet,
};
use bevy_utils::default;
mod node;
pub use node::FxaaNode;
#[derive(Debug, Reflect, Eq, PartialEq, Hash, Clone, Copy)]
#[reflect(PartialEq, Hash, Clone)]
pub enum Sensitivity {
Low,
Medium,
High,
Ultra,
Extreme,
}
impl Sensitivity {
pub fn get_str(&self) -> &str {
match self {
Sensitivity::Low => "LOW",
Sensitivity::Medium => "MEDIUM",
Sensitivity::High => "HIGH",
Sensitivity::Ultra => "ULTRA",
Sensitivity::Extreme => "EXTREME",
}
}
}
/// A component for enabling Fast Approximate Anti-Aliasing (FXAA)
/// for a [`bevy_render::camera::Camera`].
#[derive(Reflect, Component, Clone, ExtractComponent)]
#[reflect(Component, Default, Clone)]
#[extract_component_filter(With<Camera>)]
#[doc(alias = "FastApproximateAntiAliasing")]
pub struct Fxaa {
/// Enable render passes for FXAA.
pub enabled: bool,
/// Use lower sensitivity for a sharper, faster, result.
/// Use higher sensitivity for a slower, smoother, result.
/// [`Ultra`](`Sensitivity::Ultra`) and [`Extreme`](`Sensitivity::Extreme`)
/// settings can result in significant smearing and loss of detail.
///
/// The minimum amount of local contrast required to apply algorithm.
pub edge_threshold: Sensitivity,
/// Trims the algorithm from processing darks.
pub edge_threshold_min: Sensitivity,
}
impl Default for Fxaa {
fn default() -> Self {
Fxaa {
enabled: true,
edge_threshold: Sensitivity::High,
edge_threshold_min: Sensitivity::High,
}
}
}
const FXAA_SHADER_HANDLE: Handle<Shader> = weak_handle!("fc58c0a8-01c0-46e9-94cc-83a794bae7b0");
/// Adds support for Fast Approximate Anti-Aliasing (FXAA)
pub struct FxaaPlugin;
impl Plugin for FxaaPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, FXAA_SHADER_HANDLE, "fxaa.wgsl", Shader::from_wgsl);
app.register_type::<Fxaa>();
app.add_plugins(ExtractComponentPlugin::<Fxaa>::default());
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedRenderPipelines<FxaaPipeline>>()
.add_systems(Render, prepare_fxaa_pipelines.in_set(RenderSet::Prepare))
.add_render_graph_node::<ViewNodeRunner<FxaaNode>>(Core3d, Node3d::Fxaa)
.add_render_graph_edges(
Core3d,
(
Node3d::Tonemapping,
Node3d::Fxaa,
Node3d::EndMainPassPostProcessing,
),
)
.add_render_graph_node::<ViewNodeRunner<FxaaNode>>(Core2d, Node2d::Fxaa)
.add_render_graph_edges(
Core2d,
(
Node2d::Tonemapping,
Node2d::Fxaa,
Node2d::EndMainPassPostProcessing,
),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<FxaaPipeline>();
}
}
#[derive(Resource)]
pub struct FxaaPipeline {
texture_bind_group: BindGroupLayout,
sampler: Sampler,
}
impl FromWorld for FxaaPipeline {
fn from_world(render_world: &mut World) -> Self {
let render_device = render_world.resource::<RenderDevice>();
let texture_bind_group = render_device.create_bind_group_layout(
"fxaa_texture_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_2d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
),
),
);
let sampler = render_device.create_sampler(&SamplerDescriptor {
mipmap_filter: FilterMode::Linear,
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
..default()
});
FxaaPipeline {
texture_bind_group,
sampler,
}
}
}
#[derive(Component)]
pub struct CameraFxaaPipeline {
pub pipeline_id: CachedRenderPipelineId,
}
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
pub struct FxaaPipelineKey {
edge_threshold: Sensitivity,
edge_threshold_min: Sensitivity,
texture_format: TextureFormat,
}
impl SpecializedRenderPipeline for FxaaPipeline {
type Key = FxaaPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("fxaa".into()),
layout: vec![self.texture_bind_group.clone()],
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: FXAA_SHADER_HANDLE,
shader_defs: vec![
format!("EDGE_THRESH_{}", key.edge_threshold.get_str()).into(),
format!("EDGE_THRESH_MIN_{}", key.edge_threshold_min.get_str()).into(),
],
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: key.texture_format,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
}
}
}
pub fn prepare_fxaa_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<FxaaPipeline>>,
fxaa_pipeline: Res<FxaaPipeline>,
views: Query<(Entity, &ExtractedView, &Fxaa)>,
) {
for (entity, view, fxaa) in &views {
if !fxaa.enabled {
continue;
}
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&fxaa_pipeline,
FxaaPipelineKey {
edge_threshold: fxaa.edge_threshold,
edge_threshold_min: fxaa.edge_threshold_min,
texture_format: if view.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
},
);
commands
.entity(entity)
.insert(CameraFxaaPipeline { pipeline_id });
}
}

View File

@@ -0,0 +1,85 @@
use std::sync::Mutex;
use crate::fxaa::{CameraFxaaPipeline, Fxaa, FxaaPipeline};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_resource::{
BindGroup, BindGroupEntries, Operations, PipelineCache, RenderPassColorAttachment,
RenderPassDescriptor, TextureViewId,
},
renderer::RenderContext,
view::ViewTarget,
};
#[derive(Default)]
pub struct FxaaNode {
cached_texture_bind_group: Mutex<Option<(TextureViewId, BindGroup)>>,
}
impl ViewNode for FxaaNode {
type ViewQuery = (
&'static ViewTarget,
&'static CameraFxaaPipeline,
&'static Fxaa,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(target, pipeline, fxaa): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let pipeline_cache = world.resource::<PipelineCache>();
let fxaa_pipeline = world.resource::<FxaaPipeline>();
if !fxaa.enabled {
return Ok(());
};
let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline.pipeline_id) else {
return Ok(());
};
let post_process = target.post_process_write();
let source = post_process.source;
let destination = post_process.destination;
let mut cached_bind_group = self.cached_texture_bind_group.lock().unwrap();
let bind_group = match &mut *cached_bind_group {
Some((id, bind_group)) if source.id() == *id => bind_group,
cached_bind_group => {
let bind_group = render_context.render_device().create_bind_group(
None,
&fxaa_pipeline.texture_bind_group,
&BindGroupEntries::sequential((source, &fxaa_pipeline.sampler)),
);
let (_, bind_group) = cached_bind_group.insert((source.id(), bind_group));
bind_group
}
};
let pass_descriptor = RenderPassDescriptor {
label: Some("fxaa_pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: destination,
resolve_target: None,
ops: Operations::default(),
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
};
let mut render_pass = render_context
.command_encoder()
.begin_render_pass(&pass_descriptor);
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..3, 0..1);
Ok(())
}
}

98
vendor/bevy_core_pipeline/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,98 @@
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png"
)]
pub mod auto_exposure;
pub mod blit;
pub mod bloom;
pub mod contrast_adaptive_sharpening;
pub mod core_2d;
pub mod core_3d;
pub mod deferred;
pub mod dof;
pub mod experimental;
pub mod fullscreen_vertex_shader;
pub mod fxaa;
pub mod motion_blur;
pub mod msaa_writeback;
pub mod oit;
pub mod post_process;
pub mod prepass;
mod skybox;
pub mod smaa;
mod taa;
pub mod tonemapping;
pub mod upscaling;
pub use skybox::Skybox;
/// The core pipeline prelude.
///
/// This includes the most common types in this crate, re-exported for your convenience.
pub mod prelude {
#[doc(hidden)]
pub use crate::{core_2d::Camera2d, core_3d::Camera3d};
}
use crate::{
blit::BlitPlugin,
bloom::BloomPlugin,
contrast_adaptive_sharpening::CasPlugin,
core_2d::Core2dPlugin,
core_3d::Core3dPlugin,
deferred::copy_lighting_id::CopyDeferredLightingIdPlugin,
dof::DepthOfFieldPlugin,
experimental::mip_generation::MipGenerationPlugin,
fullscreen_vertex_shader::FULLSCREEN_SHADER_HANDLE,
fxaa::FxaaPlugin,
motion_blur::MotionBlurPlugin,
msaa_writeback::MsaaWritebackPlugin,
post_process::PostProcessingPlugin,
prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass},
smaa::SmaaPlugin,
tonemapping::TonemappingPlugin,
upscaling::UpscalingPlugin,
};
use bevy_app::{App, Plugin};
use bevy_asset::load_internal_asset;
use bevy_render::prelude::Shader;
use oit::OrderIndependentTransparencyPlugin;
#[derive(Default)]
pub struct CorePipelinePlugin;
impl Plugin for CorePipelinePlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
FULLSCREEN_SHADER_HANDLE,
"fullscreen_vertex_shader/fullscreen.wgsl",
Shader::from_wgsl
);
app.register_type::<DepthPrepass>()
.register_type::<NormalPrepass>()
.register_type::<MotionVectorPrepass>()
.register_type::<DeferredPrepass>()
.add_plugins((Core2dPlugin, Core3dPlugin, CopyDeferredLightingIdPlugin))
.add_plugins((
BlitPlugin,
MsaaWritebackPlugin,
TonemappingPlugin,
UpscalingPlugin,
BloomPlugin,
FxaaPlugin,
CasPlugin,
MotionBlurPlugin,
DepthOfFieldPlugin,
SmaaPlugin,
PostProcessingPlugin,
OrderIndependentTransparencyPlugin,
MipGenerationPlugin,
));
}
}

View File

@@ -0,0 +1,180 @@
//! Per-object, per-pixel motion blur.
//!
//! Add the [`MotionBlur`] component to a camera to enable motion blur.
use crate::{
core_3d::graph::{Core3d, Node3d},
prepass::{DepthPrepass, MotionVectorPrepass},
};
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_ecs::{
component::Component,
query::{QueryItem, With},
reflect::ReflectComponent,
schedule::IntoScheduleConfigs,
};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::Camera,
extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin},
render_graph::{RenderGraphApp, ViewNodeRunner},
render_resource::{Shader, ShaderType, SpecializedRenderPipelines},
Render, RenderApp, RenderSet,
};
pub mod node;
pub mod pipeline;
/// A component that enables and configures motion blur when added to a camera.
///
/// Motion blur is an effect that simulates how moving objects blur as they change position during
/// the exposure of film, a sensor, or an eyeball.
///
/// Because rendering simulates discrete steps in time, we use per-pixel motion vectors to estimate
/// the path of objects between frames. This kind of implementation has some artifacts:
/// - Fast moving objects in front of a stationary object or when in front of empty space, will not
/// have their edges blurred.
/// - Transparent objects do not write to depth or motion vectors, so they cannot be blurred.
///
/// Other approaches, such as *A Reconstruction Filter for Plausible Motion Blur* produce more
/// correct results, but are more expensive and complex, and have other kinds of artifacts. This
/// implementation is relatively inexpensive and effective.
///
/// # Usage
///
/// Add the [`MotionBlur`] component to a camera to enable and configure motion blur for that
/// camera.
///
/// ```
/// # use bevy_core_pipeline::{core_3d::Camera3d, motion_blur::MotionBlur};
/// # use bevy_ecs::prelude::*;
/// # fn test(mut commands: Commands) {
/// commands.spawn((
/// Camera3d::default(),
/// MotionBlur::default(),
/// ));
/// # }
/// ````
#[derive(Reflect, Component, Clone)]
#[reflect(Component, Default, Clone)]
#[require(DepthPrepass, MotionVectorPrepass)]
pub struct MotionBlur {
/// The strength of motion blur from `0.0` to `1.0`.
///
/// The shutter angle describes the fraction of a frame that a camera's shutter is open and
/// exposing the film/sensor. For 24fps cinematic film, a shutter angle of 0.5 (180 degrees) is
/// common. This means that the shutter was open for half of the frame, or 1/48th of a second.
/// The lower the shutter angle, the less exposure time and thus less blur.
///
/// A value greater than one is non-physical and results in an object's blur stretching further
/// than it traveled in that frame. This might be a desirable effect for artistic reasons, but
/// consider allowing users to opt out of this.
///
/// This value is intentionally tied to framerate to avoid the aforementioned non-physical
/// over-blurring. If you want to emulate a cinematic look, your options are:
/// - Framelimit your app to 24fps, and set the shutter angle to 0.5 (180 deg). Note that
/// depending on artistic intent or the action of a scene, it is common to set the shutter
/// angle between 0.125 (45 deg) and 0.5 (180 deg). This is the most faithful way to
/// reproduce the look of film.
/// - Set the shutter angle greater than one. For example, to emulate the blur strength of
/// film while rendering at 60fps, you would set the shutter angle to `60/24 * 0.5 = 1.25`.
/// Note that this will result in artifacts where the motion of objects will stretch further
/// than they moved between frames; users may find this distracting.
pub shutter_angle: f32,
/// The quality of motion blur, corresponding to the number of per-pixel samples taken in each
/// direction during blur.
///
/// Setting this to `1` results in each pixel being sampled once in the leading direction, once
/// in the trailing direction, and once in the middle, for a total of 3 samples (`1 * 2 + 1`).
/// Setting this to `3` will result in `3 * 2 + 1 = 7` samples. Setting this to `0` is
/// equivalent to disabling motion blur.
pub samples: u32,
}
impl Default for MotionBlur {
fn default() -> Self {
Self {
shutter_angle: 0.5,
samples: 1,
}
}
}
impl ExtractComponent for MotionBlur {
type QueryData = &'static Self;
type QueryFilter = With<Camera>;
type Out = MotionBlurUniform;
fn extract_component(item: QueryItem<Self::QueryData>) -> Option<Self::Out> {
Some(MotionBlurUniform {
shutter_angle: item.shutter_angle,
samples: item.samples,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_webgl2_padding: Default::default(),
})
}
}
#[doc(hidden)]
#[derive(Component, ShaderType, Clone)]
pub struct MotionBlurUniform {
shutter_angle: f32,
samples: u32,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
// WebGL2 structs must be 16 byte aligned.
_webgl2_padding: bevy_math::Vec2,
}
pub const MOTION_BLUR_SHADER_HANDLE: Handle<Shader> =
weak_handle!("d9ca74af-fa0a-4f11-b0f2-19613b618b93");
/// Adds support for per-object motion blur to the app. See [`MotionBlur`] for details.
pub struct MotionBlurPlugin;
impl Plugin for MotionBlurPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
MOTION_BLUR_SHADER_HANDLE,
"motion_blur.wgsl",
Shader::from_wgsl
);
app.add_plugins((
ExtractComponentPlugin::<MotionBlur>::default(),
UniformComponentPlugin::<MotionBlurUniform>::default(),
));
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedRenderPipelines<pipeline::MotionBlurPipeline>>()
.add_systems(
Render,
pipeline::prepare_motion_blur_pipelines.in_set(RenderSet::Prepare),
);
render_app
.add_render_graph_node::<ViewNodeRunner<node::MotionBlurNode>>(
Core3d,
Node3d::MotionBlur,
)
.add_render_graph_edges(
Core3d,
(
Node3d::EndMainPass,
Node3d::MotionBlur,
Node3d::Bloom, // we want blurred areas to bloom and tonemap properly.
),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<pipeline::MotionBlurPipeline>();
}
}

View File

@@ -0,0 +1,158 @@
#import bevy_pbr::prepass_utils
#import bevy_pbr::utils
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
#import bevy_render::globals::Globals
#ifdef MULTISAMPLED
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
@group(0) @binding(1) var motion_vectors: texture_multisampled_2d<f32>;
@group(0) @binding(2) var depth: texture_depth_multisampled_2d;
#else
@group(0) @binding(0) var screen_texture: texture_2d<f32>;
@group(0) @binding(1) var motion_vectors: texture_2d<f32>;
@group(0) @binding(2) var depth: texture_depth_2d;
#endif
@group(0) @binding(3) var texture_sampler: sampler;
struct MotionBlur {
shutter_angle: f32,
samples: u32,
#ifdef SIXTEEN_BYTE_ALIGNMENT
// WebGL2 structs must be 16 byte aligned.
_webgl2_padding: vec2<f32>
#endif
}
@group(0) @binding(4) var<uniform> settings: MotionBlur;
@group(0) @binding(5) var<uniform> globals: Globals;
@fragment
fn fragment(
#ifdef MULTISAMPLED
@builtin(sample_index) sample_index: u32,
#endif
in: FullscreenVertexOutput
) -> @location(0) vec4<f32> {
let texture_size = vec2<f32>(textureDimensions(screen_texture));
let frag_coords = vec2<i32>(in.uv * texture_size);
#ifdef MULTISAMPLED
let base_color = textureLoad(screen_texture, frag_coords, i32(sample_index));
#else
let base_color = textureSample(screen_texture, texture_sampler, in.uv);
#endif
let shutter_angle = settings.shutter_angle;
#ifdef MULTISAMPLED
let this_motion_vector = textureLoad(motion_vectors, frag_coords, i32(sample_index)).rg;
#else
let this_motion_vector = textureSample(motion_vectors, texture_sampler, in.uv).rg;
#endif
#ifdef NO_DEPTH_TEXTURE_SUPPORT
let this_depth = 0.0;
let depth_supported = false;
#else
let depth_supported = true;
#ifdef MULTISAMPLED
let this_depth = textureLoad(depth, frag_coords, i32(sample_index));
#else
let this_depth = textureSample(depth, texture_sampler, in.uv);
#endif
#endif
// The exposure vector is the distance that this fragment moved while the camera shutter was
// open. This is the motion vector (total distance traveled) multiplied by the shutter angle (a
// fraction). In film, the shutter angle is commonly 0.5 or "180 degrees" (out of 360 total).
// This means that for a frame time of 20ms, the shutter is only open for 10ms.
//
// Using a shutter angle larger than 1.0 is non-physical, objects would need to move further
// than they physically traveled during a frame, which is not possible. Note: we allow values
// larger than 1.0 because it may be desired for artistic reasons.
let exposure_vector = shutter_angle * this_motion_vector;
var accumulator: vec4<f32>;
var weight_total = 0.0;
let n_samples = i32(settings.samples);
let noise = utils::interleaved_gradient_noise(vec2<f32>(frag_coords), globals.frame_count); // 0 to 1
for (var i = -n_samples; i < n_samples; i++) {
// The current sample step vector, from in.uv
let step_vector = 0.5 * exposure_vector * (f32(i) + noise) / f32(n_samples);
var sample_uv = in.uv + step_vector;
// If the sample is off screen, skip it.
if sample_uv.x < 0.0 || sample_uv.x > 1.0 || sample_uv.y < 0.0 || sample_uv.y > 1.0 {
continue;
}
let sample_coords = vec2<i32>(sample_uv * texture_size);
#ifdef MULTISAMPLED
let sample_color = textureLoad(screen_texture, sample_coords, i32(sample_index));
#else
let sample_color = textureSample(screen_texture, texture_sampler, sample_uv);
#endif
#ifdef MULTISAMPLED
let sample_motion = textureLoad(motion_vectors, sample_coords, i32(sample_index)).rg;
#else
let sample_motion = textureSample(motion_vectors, texture_sampler, sample_uv).rg;
#endif
#ifdef NO_DEPTH_TEXTURE_SUPPORT
let sample_depth = 0.0;
#else
#ifdef MULTISAMPLED
let sample_depth = textureLoad(depth, sample_coords, i32(sample_index));
#else
let sample_depth = textureSample(depth, texture_sampler, sample_uv);
#endif
#endif
var weight = 1.0;
let is_sample_in_fg = !(depth_supported && sample_depth < this_depth && sample_depth > 0.0);
// If the depth is 0.0, this fragment has no depth written to it and we assume it is in the
// background. This ensures that things like skyboxes, which do not write to depth, are
// correctly sampled in motion blur.
if sample_depth != 0.0 && is_sample_in_fg {
// The following weight calculation is used to eliminate ghosting artifacts that are
// common in motion-vector-based motion blur implementations. While some resources
// recommend using depth, I've found that sampling the velocity results in significantly
// better results. Unlike a depth heuristic, this is not scale dependent.
//
// The most distracting artifacts occur when a stationary foreground object is
// incorrectly sampled while blurring a moving background object, causing the stationary
// object to blur when it should be sharp ("background bleeding"). This is most obvious
// when the camera is tracking a fast moving object. The tracked object should be sharp,
// and should not bleed into the motion blurred background.
//
// To attenuate these incorrect samples, we compare the motion of the fragment being
// blurred to the UV being sampled, to answer the question "is it possible that this
// sample was occluding the fragment?"
//
// Note to future maintainers: proceed with caution when making any changes here, and
// ensure you check all occlusion/disocclusion scenarios and fullscreen camera rotation
// blur for regressions.
let frag_speed = length(step_vector);
let sample_speed = length(sample_motion) / 2.0; // Halved because the sample is centered
let cos_angle = dot(step_vector, sample_motion) / (frag_speed * sample_speed * 2.0);
let motion_similarity = clamp(abs(cos_angle), 0.0, 1.0);
if sample_speed * motion_similarity < frag_speed {
// Project the sample's motion onto the frag's motion vector. If the sample did not
// cover enough distance to reach the original frag, there is no way it could have
// influenced this frag at all, and should be discarded.
weight = 0.0;
}
}
weight_total += weight;
accumulator += weight * sample_color;
}
let has_moved_less_than_a_pixel =
dot(this_motion_vector * texture_size, this_motion_vector * texture_size) < 1.0;
// In case no samples were accepted, fall back to base color.
// We also fall back if motion is small, to not break antialiasing.
if weight_total <= 0.0 || has_moved_less_than_a_pixel {
accumulator = base_color;
weight_total = 1.0;
}
return accumulator / weight_total;
}

View File

@@ -0,0 +1,101 @@
use bevy_ecs::{query::QueryItem, world::World};
use bevy_render::{
extract_component::ComponentUniforms,
globals::GlobalsBuffer,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_resource::{
BindGroupEntries, Operations, PipelineCache, RenderPassColorAttachment,
RenderPassDescriptor,
},
renderer::RenderContext,
view::{Msaa, ViewTarget},
};
use crate::prepass::ViewPrepassTextures;
use super::{
pipeline::{MotionBlurPipeline, MotionBlurPipelineId},
MotionBlurUniform,
};
#[derive(Default)]
pub struct MotionBlurNode;
impl ViewNode for MotionBlurNode {
type ViewQuery = (
&'static ViewTarget,
&'static MotionBlurPipelineId,
&'static ViewPrepassTextures,
&'static MotionBlurUniform,
&'static Msaa,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(view_target, pipeline_id, prepass_textures, motion_blur, msaa): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
if motion_blur.samples == 0 || motion_blur.shutter_angle <= 0.0 {
return Ok(()); // We can skip running motion blur in these cases.
}
let motion_blur_pipeline = world.resource::<MotionBlurPipeline>();
let pipeline_cache = world.resource::<PipelineCache>();
let settings_uniforms = world.resource::<ComponentUniforms<MotionBlurUniform>>();
let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id.0) else {
return Ok(());
};
let Some(settings_binding) = settings_uniforms.uniforms().binding() else {
return Ok(());
};
let (Some(prepass_motion_vectors_texture), Some(prepass_depth_texture)) =
(&prepass_textures.motion_vectors, &prepass_textures.depth)
else {
return Ok(());
};
let Some(globals_uniforms) = world.resource::<GlobalsBuffer>().buffer.binding() else {
return Ok(());
};
let post_process = view_target.post_process_write();
let layout = if msaa.samples() == 1 {
&motion_blur_pipeline.layout
} else {
&motion_blur_pipeline.layout_msaa
};
let bind_group = render_context.render_device().create_bind_group(
Some("motion_blur_bind_group"),
layout,
&BindGroupEntries::sequential((
post_process.source,
&prepass_motion_vectors_texture.texture.default_view,
&prepass_depth_texture.texture.default_view,
&motion_blur_pipeline.sampler,
settings_binding.clone(),
globals_uniforms.clone(),
)),
);
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("motion_blur_pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: post_process.destination,
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, &[]);
render_pass.draw(0..3, 0..1);
Ok(())
}
}

View File

@@ -0,0 +1,174 @@
use bevy_ecs::{
component::Component,
entity::Entity,
query::With,
resource::Resource,
system::{Commands, Query, Res, ResMut},
world::FromWorld,
};
use bevy_image::BevyDefault as _;
use bevy_render::{
globals::GlobalsUniform,
render_resource::{
binding_types::{
sampler, texture_2d, texture_2d_multisampled, texture_depth_2d,
texture_depth_2d_multisampled, uniform_buffer_sized,
},
BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState,
ColorWrites, FragmentState, MultisampleState, PipelineCache, PrimitiveState,
RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderDefVal,
ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines,
TextureFormat, TextureSampleType,
},
renderer::RenderDevice,
view::{ExtractedView, Msaa, ViewTarget},
};
use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state;
use super::{MotionBlurUniform, MOTION_BLUR_SHADER_HANDLE};
#[derive(Resource)]
pub struct MotionBlurPipeline {
pub(crate) sampler: Sampler,
pub(crate) layout: BindGroupLayout,
pub(crate) layout_msaa: BindGroupLayout,
}
impl MotionBlurPipeline {
pub(crate) fn new(render_device: &RenderDevice) -> Self {
let mb_layout = &BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
// View target (read)
texture_2d(TextureSampleType::Float { filterable: true }),
// Motion Vectors
texture_2d(TextureSampleType::Float { filterable: true }),
// Depth
texture_depth_2d(),
// Linear Sampler
sampler(SamplerBindingType::Filtering),
// Motion blur settings uniform input
uniform_buffer_sized(false, Some(MotionBlurUniform::min_size())),
// Globals uniform input
uniform_buffer_sized(false, Some(GlobalsUniform::min_size())),
),
);
let mb_layout_msaa = &BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
// View target (read)
texture_2d(TextureSampleType::Float { filterable: true }),
// Motion Vectors
texture_2d_multisampled(TextureSampleType::Float { filterable: false }),
// Depth
texture_depth_2d_multisampled(),
// Linear Sampler
sampler(SamplerBindingType::Filtering),
// Motion blur settings uniform input
uniform_buffer_sized(false, Some(MotionBlurUniform::min_size())),
// Globals uniform input
uniform_buffer_sized(false, Some(GlobalsUniform::min_size())),
),
);
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
let layout = render_device.create_bind_group_layout("motion_blur_layout", mb_layout);
let layout_msaa =
render_device.create_bind_group_layout("motion_blur_layout_msaa", mb_layout_msaa);
Self {
sampler,
layout,
layout_msaa,
}
}
}
impl FromWorld for MotionBlurPipeline {
fn from_world(render_world: &mut bevy_ecs::world::World) -> Self {
let render_device = render_world.resource::<RenderDevice>().clone();
MotionBlurPipeline::new(&render_device)
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
pub struct MotionBlurPipelineKey {
hdr: bool,
samples: u32,
}
impl SpecializedRenderPipeline for MotionBlurPipeline {
type Key = MotionBlurPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let layout = match key.samples {
1 => vec![self.layout.clone()],
_ => vec![self.layout_msaa.clone()],
};
let mut shader_defs = vec![];
if key.samples > 1 {
shader_defs.push(ShaderDefVal::from("MULTISAMPLED"));
}
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
{
shader_defs.push("NO_DEPTH_TEXTURE_SUPPORT".into());
shader_defs.push("SIXTEEN_BYTE_ALIGNMENT".into());
}
RenderPipelineDescriptor {
label: Some("motion_blur_pipeline".into()),
layout,
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: MOTION_BLUR_SHADER_HANDLE,
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
}
}
}
#[derive(Component)]
pub struct MotionBlurPipelineId(pub CachedRenderPipelineId);
pub(crate) fn prepare_motion_blur_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<MotionBlurPipeline>>,
pipeline: Res<MotionBlurPipeline>,
views: Query<(Entity, &ExtractedView, &Msaa), With<MotionBlurUniform>>,
) {
for (entity, view, msaa) in &views {
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&pipeline,
MotionBlurPipelineKey {
hdr: view.hdr,
samples: msaa.samples(),
},
);
commands
.entity(entity)
.insert(MotionBlurPipelineId(pipeline_id));
}
}

View File

@@ -0,0 +1,152 @@
use crate::{
blit::{BlitPipeline, BlitPipelineKey},
core_2d::graph::{Core2d, Node2d},
core_3d::graph::{Core3d, Node3d},
};
use bevy_app::{App, Plugin};
use bevy_color::LinearRgba;
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
render_resource::*,
renderer::RenderContext,
view::{Msaa, ViewTarget},
Render, RenderApp, RenderSet,
};
/// This enables "msaa writeback" support for the `core_2d` and `core_3d` pipelines, which can be enabled on cameras
/// using [`bevy_render::camera::Camera::msaa_writeback`]. See the docs on that field for more information.
pub struct MsaaWritebackPlugin;
impl Plugin for MsaaWritebackPlugin {
fn build(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.add_systems(
Render,
prepare_msaa_writeback_pipelines.in_set(RenderSet::Prepare),
);
{
render_app
.add_render_graph_node::<ViewNodeRunner<MsaaWritebackNode>>(
Core2d,
Node2d::MsaaWriteback,
)
.add_render_graph_edge(Core2d, Node2d::MsaaWriteback, Node2d::StartMainPass);
}
{
render_app
.add_render_graph_node::<ViewNodeRunner<MsaaWritebackNode>>(
Core3d,
Node3d::MsaaWriteback,
)
.add_render_graph_edge(Core3d, Node3d::MsaaWriteback, Node3d::StartMainPass);
}
}
}
#[derive(Default)]
pub struct MsaaWritebackNode;
impl ViewNode for MsaaWritebackNode {
type ViewQuery = (
&'static ViewTarget,
&'static MsaaWritebackBlitPipeline,
&'static Msaa,
);
fn run<'w>(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(target, blit_pipeline_id, msaa): QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
if *msaa == Msaa::Off {
return Ok(());
}
let blit_pipeline = world.resource::<BlitPipeline>();
let pipeline_cache = world.resource::<PipelineCache>();
let Some(pipeline) = pipeline_cache.get_render_pipeline(blit_pipeline_id.0) else {
return Ok(());
};
// The current "main texture" needs to be bound as an input resource, and we need the "other"
// unused target to be the "resolve target" for the MSAA write. Therefore this is the same
// as a post process write!
let post_process = target.post_process_write();
let pass_descriptor = RenderPassDescriptor {
label: Some("msaa_writeback"),
// The target's "resolve target" is the "destination" in post_process.
// We will indirectly write the results to the "destination" using
// the MSAA resolve step.
color_attachments: &[Some(RenderPassColorAttachment {
// If MSAA is enabled, then the sampled texture will always exist
view: target.sampled_main_texture_view().unwrap(),
resolve_target: Some(post_process.destination),
ops: Operations {
load: LoadOp::Clear(LinearRgba::BLACK.into()),
store: StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
};
let bind_group = render_context.render_device().create_bind_group(
None,
&blit_pipeline.texture_bind_group,
&BindGroupEntries::sequential((post_process.source, &blit_pipeline.sampler)),
);
let mut render_pass = render_context
.command_encoder()
.begin_render_pass(&pass_descriptor);
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, &bind_group, &[]);
render_pass.draw(0..3, 0..1);
Ok(())
}
}
#[derive(Component)]
pub struct MsaaWritebackBlitPipeline(CachedRenderPipelineId);
fn prepare_msaa_writeback_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<BlitPipeline>>,
blit_pipeline: Res<BlitPipeline>,
view_targets: Query<(Entity, &ViewTarget, &ExtractedCamera, &Msaa)>,
) {
for (entity, view_target, camera, msaa) in view_targets.iter() {
// only do writeback if writeback is enabled for the camera and this isn't the first camera in the target,
// as there is nothing to write back for the first camera.
if msaa.samples() > 1 && camera.msaa_writeback && camera.sorted_camera_index_for_target > 0
{
let key = BlitPipelineKey {
texture_format: view_target.main_texture_format(),
samples: msaa.samples(),
blend_state: None,
};
let pipeline = pipelines.specialize(&pipeline_cache, &blit_pipeline, key);
commands
.entity(entity)
.insert(MsaaWritebackBlitPipeline(pipeline));
} else {
// This isn't strictly necessary now, but if we move to retained render entity state I don't
// want this to silently break
commands
.entity(entity)
.remove::<MsaaWritebackBlitPipeline>();
}
}
}

318
vendor/bevy_core_pipeline/src/oit/mod.rs vendored Normal file
View File

@@ -0,0 +1,318 @@
//! Order Independent Transparency (OIT) for 3d rendering. See [`OrderIndependentTransparencyPlugin`] for more details.
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_ecs::{component::*, prelude::*};
use bevy_math::UVec2;
use bevy_platform::collections::HashSet;
use bevy_platform::time::Instant;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::{Camera, ExtractedCamera},
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_graph::{RenderGraphApp, ViewNodeRunner},
render_resource::{
BufferUsages, BufferVec, DynamicUniformBuffer, Shader, ShaderType, TextureUsages,
},
renderer::{RenderDevice, RenderQueue},
view::Msaa,
Render, RenderApp, RenderSet,
};
use bevy_window::PrimaryWindow;
use resolve::{
node::{OitResolveNode, OitResolvePass},
OitResolvePlugin,
};
use tracing::{trace, warn};
use crate::core_3d::{
graph::{Core3d, Node3d},
Camera3d,
};
/// Module that defines the necessary systems to resolve the OIT buffer and render it to the screen.
pub mod resolve;
/// Shader handle for the shader that draws the transparent meshes to the OIT layers buffer.
pub const OIT_DRAW_SHADER_HANDLE: Handle<Shader> =
weak_handle!("0cd3c764-39b8-437b-86b4-4e45635fc03d");
/// Used to identify which camera will use OIT to render transparent meshes
/// and to configure OIT.
// TODO consider supporting multiple OIT techniques like WBOIT, Moment Based OIT,
// depth peeling, stochastic transparency, ray tracing etc.
// This should probably be done by adding an enum to this component.
// We use the same struct to pass on the settings to the drawing shader.
#[derive(Clone, Copy, ExtractComponent, Reflect, ShaderType)]
#[reflect(Clone, Default)]
pub struct OrderIndependentTransparencySettings {
/// Controls how many layers will be used to compute the blending.
/// The more layers you use the more memory it will use but it will also give better results.
/// 8 is generally recommended, going above 32 is probably not worth it in the vast majority of cases
pub layer_count: i32,
/// Threshold for which fragments will be added to the blending layers.
/// This can be tweaked to optimize quality / layers count. Higher values will
/// allow lower number of layers and a better performance, compromising quality.
pub alpha_threshold: f32,
}
impl Default for OrderIndependentTransparencySettings {
fn default() -> Self {
Self {
layer_count: 8,
alpha_threshold: 0.0,
}
}
}
// OrderIndependentTransparencySettings is also a Component. We explicitly implement the trait so
// we can hook on_add to issue a warning in case `layer_count` is seemingly too high.
impl Component for OrderIndependentTransparencySettings {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
type Mutability = Mutable;
fn on_add() -> Option<ComponentHook> {
Some(|world, context| {
if let Some(value) = world.get::<OrderIndependentTransparencySettings>(context.entity) {
if value.layer_count > 32 {
warn!("{}OrderIndependentTransparencySettings layer_count set to {} might be too high.",
context.caller.map(|location|format!("{location}: ")).unwrap_or_default(),
value.layer_count
);
}
}
})
}
}
/// A plugin that adds support for Order Independent Transparency (OIT).
/// This can correctly render some scenes that would otherwise have artifacts due to alpha blending, but uses more memory.
///
/// To enable OIT for a camera you need to add the [`OrderIndependentTransparencySettings`] component to it.
///
/// If you want to use OIT for your custom material you need to call `oit_draw(position, color)` in your fragment shader.
/// You also need to make sure that your fragment shader doesn't output any colors.
///
/// # Implementation details
/// This implementation uses 2 passes.
///
/// The first pass writes the depth and color of all the fragments to a big buffer.
/// The buffer contains N layers for each pixel, where N can be set with [`OrderIndependentTransparencySettings::layer_count`].
/// This pass is essentially a forward pass.
///
/// The second pass is a single fullscreen triangle pass that sorts all the fragments then blends them together
/// and outputs the result to the screen.
pub struct OrderIndependentTransparencyPlugin;
impl Plugin for OrderIndependentTransparencyPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
OIT_DRAW_SHADER_HANDLE,
"oit_draw.wgsl",
Shader::from_wgsl
);
app.add_plugins((
ExtractComponentPlugin::<OrderIndependentTransparencySettings>::default(),
OitResolvePlugin,
))
.add_systems(Update, check_msaa)
.add_systems(Last, configure_depth_texture_usages)
.register_type::<OrderIndependentTransparencySettings>();
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.add_systems(
Render,
prepare_oit_buffers.in_set(RenderSet::PrepareResources),
);
render_app
.add_render_graph_node::<ViewNodeRunner<OitResolveNode>>(Core3d, OitResolvePass)
.add_render_graph_edges(
Core3d,
(
Node3d::MainTransparentPass,
OitResolvePass,
Node3d::EndMainPass,
),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<OitBuffers>();
}
}
// WARN This should only happen for cameras with the [`OrderIndependentTransparencySettings`] component
// but when multiple cameras are present on the same window
// bevy reuses the same depth texture so we need to set this on all cameras with the same render target.
fn configure_depth_texture_usages(
p: Query<Entity, With<PrimaryWindow>>,
cameras: Query<(&Camera, Has<OrderIndependentTransparencySettings>)>,
mut new_cameras: Query<(&mut Camera3d, &Camera), Added<Camera3d>>,
) {
if new_cameras.is_empty() {
return;
}
// Find all the render target that potentially uses OIT
let primary_window = p.single().ok();
let mut render_target_has_oit = <HashSet<_>>::default();
for (camera, has_oit) in &cameras {
if has_oit {
render_target_has_oit.insert(camera.target.normalize(primary_window));
}
}
// Update the depth texture usage for cameras with a render target that has OIT
for (mut camera_3d, camera) in &mut new_cameras {
if render_target_has_oit.contains(&camera.target.normalize(primary_window)) {
let mut usages = TextureUsages::from(camera_3d.depth_texture_usages);
usages |= TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING;
camera_3d.depth_texture_usages = usages.into();
}
}
}
fn check_msaa(cameras: Query<&Msaa, With<OrderIndependentTransparencySettings>>) {
for msaa in &cameras {
if msaa.samples() > 1 {
panic!("MSAA is not supported when using OrderIndependentTransparency");
}
}
}
/// Holds the buffers that contain the data of all OIT layers.
/// We use one big buffer for the entire app. Each camera will reuse it so it will
/// always be the size of the biggest OIT enabled camera.
#[derive(Resource)]
pub struct OitBuffers {
/// The OIT layers containing depth and color for each fragments.
/// This is essentially used as a 3d array where xy is the screen coordinate and z is
/// the list of fragments rendered with OIT.
pub layers: BufferVec<UVec2>,
/// Buffer containing the index of the last layer that was written for each fragment.
pub layer_ids: BufferVec<i32>,
pub settings: DynamicUniformBuffer<OrderIndependentTransparencySettings>,
}
impl FromWorld for OitBuffers {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let render_queue = world.resource::<RenderQueue>();
// initialize buffers with something so there's a valid binding
let mut layers = BufferVec::new(BufferUsages::COPY_DST | BufferUsages::STORAGE);
layers.set_label(Some("oit_layers"));
layers.reserve(1, render_device);
layers.write_buffer(render_device, render_queue);
let mut layer_ids = BufferVec::new(BufferUsages::COPY_DST | BufferUsages::STORAGE);
layer_ids.set_label(Some("oit_layer_ids"));
layer_ids.reserve(1, render_device);
layer_ids.write_buffer(render_device, render_queue);
let mut settings = DynamicUniformBuffer::default();
settings.set_label(Some("oit_settings"));
Self {
layers,
layer_ids,
settings,
}
}
}
#[derive(Component)]
pub struct OrderIndependentTransparencySettingsOffset {
pub offset: u32,
}
/// This creates or resizes the oit buffers for each camera.
/// It will always create one big buffer that's as big as the biggest buffer needed.
/// Cameras with smaller viewports or less layers will simply use the big buffer and ignore the rest.
pub fn prepare_oit_buffers(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
cameras: Query<
(&ExtractedCamera, &OrderIndependentTransparencySettings),
(
Changed<ExtractedCamera>,
Changed<OrderIndependentTransparencySettings>,
),
>,
camera_oit_uniforms: Query<(Entity, &OrderIndependentTransparencySettings)>,
mut buffers: ResMut<OitBuffers>,
) {
// Get the max buffer size for any OIT enabled camera
let mut max_layer_ids_size = usize::MIN;
let mut max_layers_size = usize::MIN;
for (camera, settings) in &cameras {
let Some(size) = camera.physical_target_size else {
continue;
};
let layer_count = settings.layer_count as usize;
let size = (size.x * size.y) as usize;
max_layer_ids_size = max_layer_ids_size.max(size);
max_layers_size = max_layers_size.max(size * layer_count);
}
// Create or update the layers buffer based on the max size
if buffers.layers.capacity() < max_layers_size {
let start = Instant::now();
buffers.layers.reserve(max_layers_size, &render_device);
let remaining = max_layers_size - buffers.layers.capacity();
for _ in 0..remaining {
buffers.layers.push(UVec2::ZERO);
}
buffers.layers.write_buffer(&render_device, &render_queue);
trace!(
"OIT layers buffer updated in {:.01}ms with total size {} MiB",
start.elapsed().as_millis(),
buffers.layers.capacity() * size_of::<UVec2>() / 1024 / 1024,
);
}
// Create or update the layer_ids buffer based on the max size
if buffers.layer_ids.capacity() < max_layer_ids_size {
let start = Instant::now();
buffers
.layer_ids
.reserve(max_layer_ids_size, &render_device);
let remaining = max_layer_ids_size - buffers.layer_ids.capacity();
for _ in 0..remaining {
buffers.layer_ids.push(0);
}
buffers
.layer_ids
.write_buffer(&render_device, &render_queue);
trace!(
"OIT layer ids buffer updated in {:.01}ms with total size {} MiB",
start.elapsed().as_millis(),
buffers.layer_ids.capacity() * size_of::<UVec2>() / 1024 / 1024,
);
}
if let Some(mut writer) = buffers.settings.get_writer(
camera_oit_uniforms.iter().len(),
&render_device,
&render_queue,
) {
for (entity, settings) in &camera_oit_uniforms {
let offset = writer.write(settings);
commands
.entity(entity)
.insert(OrderIndependentTransparencySettingsOffset { offset });
}
}
}

View File

@@ -0,0 +1,48 @@
#define_import_path bevy_core_pipeline::oit
#import bevy_pbr::mesh_view_bindings::{view, oit_layers, oit_layer_ids, oit_settings}
#ifdef OIT_ENABLED
// Add the fragment to the oit buffer
fn oit_draw(position: vec4f, color: vec4f) {
// Don't add fully transparent fragments to the list
// because we don't want to have to sort them in the resolve pass
if color.a < oit_settings.alpha_threshold {
return;
}
// get the index of the current fragment relative to the screen size
let screen_index = i32(floor(position.x) + floor(position.y) * view.viewport.z);
// get the size of the buffer.
// It's always the size of the screen
let buffer_size = i32(view.viewport.z * view.viewport.w);
// gets the layer index of the current fragment
var layer_id = atomicAdd(&oit_layer_ids[screen_index], 1);
// exit early if we've reached the maximum amount of fragments per layer
if layer_id >= oit_settings.layers_count {
// force to store the oit_layers_count to make sure we don't
// accidentally increase the index above the maximum value
atomicStore(&oit_layer_ids[screen_index], oit_settings.layers_count);
// TODO for tail blending we should return the color here
return;
}
// get the layer_index from the screen
let layer_index = screen_index + layer_id * buffer_size;
let rgb9e5_color = bevy_pbr::rgb9e5::vec3_to_rgb9e5_(color.rgb);
let depth_alpha = pack_24bit_depth_8bit_alpha(position.z, color.a);
oit_layers[layer_index] = vec2(rgb9e5_color, depth_alpha);
}
#endif // OIT_ENABLED
fn pack_24bit_depth_8bit_alpha(depth: f32, alpha: f32) -> u32 {
let depth_bits = u32(saturate(depth) * f32(0xFFFFFFu) + 0.5);
let alpha_bits = u32(saturate(alpha) * f32(0xFFu) + 0.5);
return (depth_bits & 0xFFFFFFu) | ((alpha_bits & 0xFFu) << 24u);
}
fn unpack_24bit_depth_8bit_alpha(packed: u32) -> vec2<f32> {
let depth_bits = packed & 0xFFFFFFu;
let alpha_bits = (packed >> 24u) & 0xFFu;
return vec2(f32(depth_bits) / f32(0xFFFFFFu), f32(alpha_bits) / f32(0xFFu));
}

View File

@@ -0,0 +1,262 @@
use crate::{
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
oit::OrderIndependentTransparencySettings,
};
use bevy_app::Plugin;
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_derive::Deref;
use bevy_ecs::{
entity::{EntityHashMap, EntityHashSet},
prelude::*,
};
use bevy_image::BevyDefault as _;
use bevy_render::{
render_resource::{
binding_types::{storage_buffer_sized, texture_depth_2d, uniform_buffer},
BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, BlendComponent,
BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites, DownlevelFlags,
FragmentState, MultisampleState, PipelineCache, PrimitiveState, RenderPipelineDescriptor,
Shader, ShaderDefVal, ShaderStages, TextureFormat,
},
renderer::{RenderAdapter, RenderDevice},
view::{ExtractedView, ViewTarget, ViewUniform, ViewUniforms},
Render, RenderApp, RenderSet,
};
use tracing::warn;
use super::OitBuffers;
/// Shader handle for the shader that sorts the OIT layers, blends the colors based on depth and renders them to the screen.
pub const OIT_RESOLVE_SHADER_HANDLE: Handle<Shader> =
weak_handle!("562d2917-eb06-444d-9ade-41de76b0f5ae");
/// Contains the render node used to run the resolve pass.
pub mod node;
/// Minimum required value of `wgpu::Limits::max_storage_buffers_per_shader_stage`.
pub const OIT_REQUIRED_STORAGE_BUFFERS: u32 = 2;
/// Plugin needed to resolve the Order Independent Transparency (OIT) buffer to the screen.
pub struct OitResolvePlugin;
impl Plugin for OitResolvePlugin {
fn build(&self, app: &mut bevy_app::App) {
load_internal_asset!(
app,
OIT_RESOLVE_SHADER_HANDLE,
"oit_resolve.wgsl",
Shader::from_wgsl
);
}
fn finish(&self, app: &mut bevy_app::App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
if !is_oit_supported(
render_app.world().resource::<RenderAdapter>(),
render_app.world().resource::<RenderDevice>(),
true,
) {
return;
}
render_app
.add_systems(
Render,
(
queue_oit_resolve_pipeline.in_set(RenderSet::Queue),
prepare_oit_resolve_bind_group.in_set(RenderSet::PrepareBindGroups),
),
)
.init_resource::<OitResolvePipeline>();
}
}
pub fn is_oit_supported(adapter: &RenderAdapter, device: &RenderDevice, warn: bool) -> bool {
if !adapter
.get_downlevel_capabilities()
.flags
.contains(DownlevelFlags::FRAGMENT_WRITABLE_STORAGE)
{
if warn {
warn!("OrderIndependentTransparencyPlugin not loaded. GPU lacks support: DownlevelFlags::FRAGMENT_WRITABLE_STORAGE.");
}
return false;
}
let max_storage_buffers_per_shader_stage = device.limits().max_storage_buffers_per_shader_stage;
if max_storage_buffers_per_shader_stage < OIT_REQUIRED_STORAGE_BUFFERS {
if warn {
warn!(
max_storage_buffers_per_shader_stage,
OIT_REQUIRED_STORAGE_BUFFERS,
"OrderIndependentTransparencyPlugin not loaded. RenderDevice lacks support: max_storage_buffers_per_shader_stage < OIT_REQUIRED_STORAGE_BUFFERS."
);
}
return false;
}
true
}
/// Bind group for the OIT resolve pass.
#[derive(Resource, Deref)]
pub struct OitResolveBindGroup(pub BindGroup);
/// Bind group layouts used for the OIT resolve pass.
#[derive(Resource)]
pub struct OitResolvePipeline {
/// View bind group layout.
pub view_bind_group_layout: BindGroupLayout,
/// Depth bind group layout.
pub oit_depth_bind_group_layout: BindGroupLayout,
}
impl FromWorld for OitResolvePipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let view_bind_group_layout = render_device.create_bind_group_layout(
"oit_resolve_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
uniform_buffer::<ViewUniform>(true),
// layers
storage_buffer_sized(false, None),
// layer ids
storage_buffer_sized(false, None),
),
),
);
let oit_depth_bind_group_layout = render_device.create_bind_group_layout(
"oit_depth_bind_group_layout",
&BindGroupLayoutEntries::single(ShaderStages::FRAGMENT, texture_depth_2d()),
);
OitResolvePipeline {
view_bind_group_layout,
oit_depth_bind_group_layout,
}
}
}
#[derive(Component, Deref, Clone, Copy)]
pub struct OitResolvePipelineId(pub CachedRenderPipelineId);
/// This key is used to cache the pipeline id and to specialize the render pipeline descriptor.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct OitResolvePipelineKey {
hdr: bool,
layer_count: i32,
}
pub fn queue_oit_resolve_pipeline(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
resolve_pipeline: Res<OitResolvePipeline>,
views: Query<
(
Entity,
&ExtractedView,
&OrderIndependentTransparencySettings,
),
With<OrderIndependentTransparencySettings>,
>,
// Store the key with the id to make the clean up logic easier.
// This also means it will always replace the entry if the key changes so nothing to clean up.
mut cached_pipeline_id: Local<EntityHashMap<(OitResolvePipelineKey, CachedRenderPipelineId)>>,
) {
let mut current_view_entities = EntityHashSet::default();
for (e, view, oit_settings) in &views {
current_view_entities.insert(e);
let key = OitResolvePipelineKey {
hdr: view.hdr,
layer_count: oit_settings.layer_count,
};
if let Some((cached_key, id)) = cached_pipeline_id.get(&e) {
if *cached_key == key {
commands.entity(e).insert(OitResolvePipelineId(*id));
continue;
}
}
let desc = specialize_oit_resolve_pipeline(key, &resolve_pipeline);
let pipeline_id = pipeline_cache.queue_render_pipeline(desc);
commands.entity(e).insert(OitResolvePipelineId(pipeline_id));
cached_pipeline_id.insert(e, (key, pipeline_id));
}
// Clear cache for views that don't exist anymore.
for e in cached_pipeline_id.keys().copied().collect::<Vec<_>>() {
if !current_view_entities.contains(&e) {
cached_pipeline_id.remove(&e);
}
}
}
fn specialize_oit_resolve_pipeline(
key: OitResolvePipelineKey,
resolve_pipeline: &OitResolvePipeline,
) -> RenderPipelineDescriptor {
let format = if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
};
RenderPipelineDescriptor {
label: Some("oit_resolve_pipeline".into()),
layout: vec![
resolve_pipeline.view_bind_group_layout.clone(),
resolve_pipeline.oit_depth_bind_group_layout.clone(),
],
fragment: Some(FragmentState {
entry_point: "fragment".into(),
shader: OIT_RESOLVE_SHADER_HANDLE,
shader_defs: vec![ShaderDefVal::UInt(
"LAYER_COUNT".into(),
key.layer_count as u32,
)],
targets: vec![Some(ColorTargetState {
format,
blend: Some(BlendState {
color: BlendComponent::OVER,
alpha: BlendComponent::OVER,
}),
write_mask: ColorWrites::ALL,
})],
}),
vertex: fullscreen_shader_vertex_state(),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
}
}
pub fn prepare_oit_resolve_bind_group(
mut commands: Commands,
resolve_pipeline: Res<OitResolvePipeline>,
render_device: Res<RenderDevice>,
view_uniforms: Res<ViewUniforms>,
buffers: Res<OitBuffers>,
) {
if let (Some(binding), Some(layers_binding), Some(layer_ids_binding)) = (
view_uniforms.uniforms.binding(),
buffers.layers.binding(),
buffers.layer_ids.binding(),
) {
let bind_group = render_device.create_bind_group(
"oit_resolve_bind_group",
&resolve_pipeline.view_bind_group_layout,
&BindGroupEntries::sequential((binding.clone(), layers_binding, layer_ids_binding)),
);
commands.insert_resource(OitResolveBindGroup(bind_group));
}
}

View File

@@ -0,0 +1,78 @@
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
render_graph::{NodeRunError, RenderGraphContext, RenderLabel, ViewNode},
render_resource::{BindGroupEntries, PipelineCache, RenderPassDescriptor},
renderer::RenderContext,
view::{ViewDepthTexture, ViewTarget, ViewUniformOffset},
};
use super::{OitResolveBindGroup, OitResolvePipeline, OitResolvePipelineId};
/// Render label for the OIT resolve pass.
#[derive(RenderLabel, Debug, Clone, Hash, PartialEq, Eq)]
pub struct OitResolvePass;
/// The node that executes the OIT resolve pass.
#[derive(Default)]
pub struct OitResolveNode;
impl ViewNode for OitResolveNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ViewTarget,
&'static ViewUniformOffset,
&'static OitResolvePipelineId,
&'static ViewDepthTexture,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(camera, view_target, view_uniform, oit_resolve_pipeline_id, depth): QueryItem<
Self::ViewQuery,
>,
world: &World,
) -> Result<(), NodeRunError> {
let Some(resolve_pipeline) = world.get_resource::<OitResolvePipeline>() else {
return Ok(());
};
// resolve oit
// sorts the layers and renders the final blended color to the screen
{
let pipeline_cache = world.resource::<PipelineCache>();
let bind_group = world.resource::<OitResolveBindGroup>();
let Some(pipeline) = pipeline_cache.get_render_pipeline(oit_resolve_pipeline_id.0)
else {
return Ok(());
};
let depth_bind_group = render_context.render_device().create_bind_group(
"oit_resolve_depth_bind_group",
&resolve_pipeline.oit_depth_bind_group_layout,
&BindGroupEntries::single(depth.view()),
);
let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("oit_resolve_pass"),
color_attachments: &[Some(view_target.get_color_attachment())],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(0, bind_group, &[view_uniform.offset]);
render_pass.set_bind_group(1, &depth_bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
Ok(())
}
}

View File

@@ -0,0 +1,117 @@
#import bevy_render::view::View
@group(0) @binding(0) var<uniform> view: View;
@group(0) @binding(1) var<storage, read_write> layers: array<vec2<u32>>;
@group(0) @binding(2) var<storage, read_write> layer_ids: array<atomic<i32>>;
@group(1) @binding(0) var depth: texture_depth_2d;
struct OitFragment {
color: vec3<f32>,
alpha: f32,
depth: f32,
}
// Contains all the colors and depth for this specific fragment
var<private> fragment_list: array<OitFragment, #{LAYER_COUNT}>;
struct FullscreenVertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
let buffer_size = i32(view.viewport.z * view.viewport.w);
let screen_index = i32(floor(in.position.x) + floor(in.position.y) * view.viewport.z);
let counter = atomicLoad(&layer_ids[screen_index]);
if counter == 0 {
reset_indices(screen_index);
// https://github.com/gfx-rs/wgpu/issues/4416
if true {
discard;
}
return vec4(0.0);
} else {
// Load depth for manual depth testing.
// This is necessary because early z doesn't seem to trigger in the transparent pass.
// This should be done during the draw pass so those fragments simply don't exist in the list,
// but this requires a bigger refactor
let d = textureLoad(depth, vec2<i32>(in.position.xy), 0);
let result = sort(screen_index, buffer_size, d);
reset_indices(screen_index);
return result.color;
}
}
// Resets all indices to 0.
// This means we don't have to clear the entire layers buffer
fn reset_indices(screen_index: i32) {
atomicStore(&layer_ids[screen_index], 0);
layers[screen_index] = vec2(0u);
}
struct SortResult {
color: vec4f,
depth: f32,
}
fn sort(screen_index: i32, buffer_size: i32, opaque_depth: f32) -> SortResult {
var counter = atomicLoad(&layer_ids[screen_index]);
// fill list
for (var i = 0; i < counter; i += 1) {
let fragment = layers[screen_index + buffer_size * i];
// unpack color/alpha/depth
let color = bevy_pbr::rgb9e5::rgb9e5_to_vec3_(fragment.x);
let depth_alpha = bevy_core_pipeline::oit::unpack_24bit_depth_8bit_alpha(fragment.y);
fragment_list[i].color = color;
fragment_list[i].alpha = depth_alpha.y;
fragment_list[i].depth = depth_alpha.x;
}
// bubble sort the list based on the depth
for (var i = counter; i >= 0; i -= 1) {
for (var j = 0; j < i; j += 1) {
if fragment_list[j].depth < fragment_list[j + 1].depth {
// swap
let temp = fragment_list[j + 1];
fragment_list[j + 1] = fragment_list[j];
fragment_list[j] = temp;
}
}
}
// resolve blend
var final_color = vec4(0.0);
for (var i = 0; i <= counter; i += 1) {
// depth testing
// This needs to happen here because we can only stop iterating if the fragment is
// occluded by something opaque and the fragments need to be sorted first
if fragment_list[i].depth < opaque_depth {
break;
}
let color = fragment_list[i].color;
let alpha = fragment_list[i].alpha;
var base_color = vec4(color.rgb * alpha, alpha);
final_color = blend(final_color, base_color);
if final_color.a == 1.0 {
break;
}
}
var result: SortResult;
result.color = final_color;
result.depth = fragment_list[0].depth;
return result;
}
// OVER operator using premultiplied alpha
// see: https://en.wikipedia.org/wiki/Alpha_compositing
fn blend(color_a: vec4<f32>, color_b: vec4<f32>) -> vec4<f32> {
let final_color = color_a.rgb + (1.0 - color_a.a) * color_b.rgb;
let alpha = color_a.a + (1.0 - color_a.a) * color_b.a;
return vec4(final_color.rgb, alpha);
}

View File

@@ -0,0 +1,92 @@
// The chromatic aberration postprocessing effect.
//
// This makes edges of objects turn into multicolored streaks.
#define_import_path bevy_core_pipeline::post_processing::chromatic_aberration
// See `bevy_core_pipeline::post_process::ChromaticAberration` for more
// information on these fields.
struct ChromaticAberrationSettings {
intensity: f32,
max_samples: u32,
unused_a: u32,
unused_b: u32,
}
// The source framebuffer texture.
@group(0) @binding(0) var chromatic_aberration_source_texture: texture_2d<f32>;
// The sampler used to sample the source framebuffer texture.
@group(0) @binding(1) var chromatic_aberration_source_sampler: sampler;
// The 1D lookup table for chromatic aberration.
@group(0) @binding(2) var chromatic_aberration_lut_texture: texture_2d<f32>;
// The sampler used to sample that lookup table.
@group(0) @binding(3) var chromatic_aberration_lut_sampler: sampler;
// The settings supplied by the developer.
@group(0) @binding(4) var<uniform> chromatic_aberration_settings: ChromaticAberrationSettings;
fn chromatic_aberration(start_pos: vec2<f32>) -> vec3<f32> {
// Radial chromatic aberration implemented using the *Inside* technique:
//
// <https://github.com/playdeadgames/publications/blob/master/INSIDE/rendering_inside_gdc2016.pdf>
let end_pos = mix(start_pos, vec2(0.5), chromatic_aberration_settings.intensity);
// Determine the number of samples. We aim for one sample per texel, unless
// that's higher than the developer-specified maximum number of samples, in
// which case we choose the maximum number of samples.
let texel_length = length((end_pos - start_pos) *
vec2<f32>(textureDimensions(chromatic_aberration_source_texture)));
let sample_count = min(u32(ceil(texel_length)), chromatic_aberration_settings.max_samples);
var color: vec3<f32>;
if (sample_count > 1u) {
// The LUT texture is in clamp-to-edge mode, so we start at 0.5 texels
// from the sides so that we have a nice gradient over the entire LUT
// range.
let lut_u_offset = 0.5 / f32(textureDimensions(chromatic_aberration_lut_texture).x);
var sample_sum = vec3(0.0);
var modulate_sum = vec3(0.0);
// Start accumulating samples.
for (var sample_index = 0u; sample_index < sample_count; sample_index += 1u) {
let t = (f32(sample_index) + 0.5) / f32(sample_count);
// Sample the framebuffer.
let sample_uv = mix(start_pos, end_pos, t);
let sample = textureSampleLevel(
chromatic_aberration_source_texture,
chromatic_aberration_source_sampler,
sample_uv,
0.0,
).rgb;
// Sample the LUT.
let lut_u = mix(lut_u_offset, 1.0 - lut_u_offset, t);
let modulate = textureSampleLevel(
chromatic_aberration_lut_texture,
chromatic_aberration_lut_sampler,
vec2(lut_u, 0.5),
0.0,
).rgb;
// Modulate the sample by the LUT value.
sample_sum += sample * modulate;
modulate_sum += modulate;
}
color = sample_sum / modulate_sum;
} else {
// If there's only one sample, don't do anything. If we don't do this,
// then this shader will apply whatever tint is in the center of the LUT
// texture to such pixels, which is wrong.
color = textureSampleLevel(
chromatic_aberration_source_texture,
chromatic_aberration_source_sampler,
start_pos,
0.0,
).rgb;
}
return color;
}

View File

@@ -0,0 +1,509 @@
//! Miscellaneous built-in postprocessing effects.
//!
//! Currently, this consists only of chromatic aberration.
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Assets, Handle};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{QueryItem, With},
reflect::ReflectComponent,
resource::Resource,
schedule::IntoScheduleConfigs as _,
system::{lifetimeless::Read, Commands, Query, Res, ResMut},
world::{FromWorld, World},
};
use bevy_image::{BevyDefault, Image};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::Camera,
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_asset::{RenderAssetUsages, RenderAssets},
render_graph::{
NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner,
},
render_resource::{
binding_types::{sampler, texture_2d, uniform_buffer},
BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId,
ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d, FilterMode, FragmentState,
Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor,
RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader,
ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines,
TextureDimension, TextureFormat, TextureSampleType,
},
renderer::{RenderContext, RenderDevice, RenderQueue},
texture::GpuImage,
view::{ExtractedView, ViewTarget},
Render, RenderApp, RenderSet,
};
use bevy_utils::prelude::default;
use crate::{
core_2d::graph::{Core2d, Node2d},
core_3d::graph::{Core3d, Node3d},
fullscreen_vertex_shader,
};
/// The handle to the built-in postprocessing shader `post_process.wgsl`.
const POST_PROCESSING_SHADER_HANDLE: Handle<Shader> =
weak_handle!("5e8e627a-7531-484d-a988-9a38acb34e52");
/// The handle to the chromatic aberration shader `chromatic_aberration.wgsl`.
const CHROMATIC_ABERRATION_SHADER_HANDLE: Handle<Shader> =
weak_handle!("e598550e-71c3-4f5a-ba29-aebc3f88c7b5");
/// The handle to the default chromatic aberration lookup texture.
///
/// This is just a 3x1 image consisting of one red pixel, one green pixel, and
/// one blue pixel, in that order.
const DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE: Handle<Image> =
weak_handle!("dc3e3307-40a1-49bb-be6d-e0634e8836b2");
/// The default chromatic aberration intensity amount, in a fraction of the
/// window size.
const DEFAULT_CHROMATIC_ABERRATION_INTENSITY: f32 = 0.02;
/// The default maximum number of samples for chromatic aberration.
const DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES: u32 = 8;
/// The raw RGBA data for the default chromatic aberration gradient.
///
/// This consists of one red pixel, one green pixel, and one blue pixel, in that
/// order.
static DEFAULT_CHROMATIC_ABERRATION_LUT_DATA: [u8; 12] =
[255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255];
/// A plugin that implements a built-in postprocessing stack with some common
/// effects.
///
/// Currently, this only consists of chromatic aberration.
pub struct PostProcessingPlugin;
/// Adds colored fringes to the edges of objects in the scene.
///
/// [Chromatic aberration] simulates the effect when lenses fail to focus all
/// colors of light toward a single point. It causes rainbow-colored streaks to
/// appear, which are especially apparent on the edges of objects. Chromatic
/// aberration is commonly used for collision effects, especially in horror
/// games.
///
/// Bevy's implementation is based on that of *Inside* ([Gjøl & Svendsen 2016]).
/// It's based on a customizable lookup texture, which allows for changing the
/// color pattern. By default, the color pattern is simply a 3×1 pixel texture
/// consisting of red, green, and blue, in that order, but you can change it to
/// any image in order to achieve different effects.
///
/// [Chromatic aberration]: https://en.wikipedia.org/wiki/Chromatic_aberration
///
/// [Gjøl & Svendsen 2016]: https://github.com/playdeadgames/publications/blob/master/INSIDE/rendering_inside_gdc2016.pdf
#[derive(Reflect, Component, Clone)]
#[reflect(Component, Default, Clone)]
pub struct ChromaticAberration {
/// The lookup texture that determines the color gradient.
///
/// By default, this is a 3×1 texel texture consisting of one red pixel, one
/// green pixel, and one blue texel, in that order. This recreates the most
/// typical chromatic aberration pattern. However, you can change it to
/// achieve different artistic effects.
///
/// The texture is always sampled in its vertical center, so it should
/// ordinarily have a height of 1 texel.
pub color_lut: Handle<Image>,
/// The size of the streaks around the edges of objects, as a fraction of
/// the window size.
///
/// The default value is 0.02.
pub intensity: f32,
/// A cap on the number of texture samples that will be performed.
///
/// Higher values result in smoother-looking streaks but are slower.
///
/// The default value is 8.
pub max_samples: u32,
}
/// GPU pipeline data for the built-in postprocessing stack.
///
/// This is stored in the render world.
#[derive(Resource)]
pub struct PostProcessingPipeline {
/// The layout of bind group 0, containing the source, LUT, and settings.
bind_group_layout: BindGroupLayout,
/// Specifies how to sample the source framebuffer texture.
source_sampler: Sampler,
/// Specifies how to sample the chromatic aberration gradient.
chromatic_aberration_lut_sampler: Sampler,
}
/// A key that uniquely identifies a built-in postprocessing pipeline.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct PostProcessingPipelineKey {
/// The format of the source and destination textures.
texture_format: TextureFormat,
}
/// A component attached to cameras in the render world that stores the
/// specialized pipeline ID for the built-in postprocessing stack.
#[derive(Component, Deref, DerefMut)]
pub struct PostProcessingPipelineId(CachedRenderPipelineId);
/// The on-GPU version of the [`ChromaticAberration`] settings.
///
/// See the documentation for [`ChromaticAberration`] for more information on
/// each of these fields.
#[derive(ShaderType)]
pub struct ChromaticAberrationUniform {
/// The intensity of the effect, in a fraction of the screen.
intensity: f32,
/// A cap on the number of samples of the source texture that the shader
/// will perform.
max_samples: u32,
/// Padding data.
unused_1: u32,
/// Padding data.
unused_2: u32,
}
/// A resource, part of the render world, that stores the
/// [`ChromaticAberrationUniform`]s for each view.
#[derive(Resource, Deref, DerefMut, Default)]
pub struct PostProcessingUniformBuffers {
chromatic_aberration: DynamicUniformBuffer<ChromaticAberrationUniform>,
}
/// A component, part of the render world, that stores the appropriate byte
/// offset within the [`PostProcessingUniformBuffers`] for the camera it's
/// attached to.
#[derive(Component, Deref, DerefMut)]
pub struct PostProcessingUniformBufferOffsets {
chromatic_aberration: u32,
}
/// The render node that runs the built-in postprocessing stack.
#[derive(Default)]
pub struct PostProcessingNode;
impl Plugin for PostProcessingPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
POST_PROCESSING_SHADER_HANDLE,
"post_process.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
CHROMATIC_ABERRATION_SHADER_HANDLE,
"chromatic_aberration.wgsl",
Shader::from_wgsl
);
// Load the default chromatic aberration LUT.
let mut assets = app.world_mut().resource_mut::<Assets<_>>();
assets.insert(
DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE.id(),
Image::new(
Extent3d {
width: 3,
height: 1,
depth_or_array_layers: 1,
},
TextureDimension::D2,
DEFAULT_CHROMATIC_ABERRATION_LUT_DATA.to_vec(),
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD,
),
);
app.register_type::<ChromaticAberration>();
app.add_plugins(ExtractComponentPlugin::<ChromaticAberration>::default());
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedRenderPipelines<PostProcessingPipeline>>()
.init_resource::<PostProcessingUniformBuffers>()
.add_systems(
Render,
(
prepare_post_processing_pipelines,
prepare_post_processing_uniforms,
)
.in_set(RenderSet::Prepare),
)
.add_render_graph_node::<ViewNodeRunner<PostProcessingNode>>(
Core3d,
Node3d::PostProcessing,
)
.add_render_graph_edges(
Core3d,
(
Node3d::DepthOfField,
Node3d::PostProcessing,
Node3d::Tonemapping,
),
)
.add_render_graph_node::<ViewNodeRunner<PostProcessingNode>>(
Core2d,
Node2d::PostProcessing,
)
.add_render_graph_edges(
Core2d,
(Node2d::Bloom, Node2d::PostProcessing, Node2d::Tonemapping),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<PostProcessingPipeline>();
}
}
impl Default for ChromaticAberration {
fn default() -> Self {
Self {
color_lut: DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE,
intensity: DEFAULT_CHROMATIC_ABERRATION_INTENSITY,
max_samples: DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES,
}
}
}
impl FromWorld for PostProcessingPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
// Create our single bind group layout.
let bind_group_layout = render_device.create_bind_group_layout(
Some("postprocessing bind group layout"),
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
// Chromatic aberration source:
texture_2d(TextureSampleType::Float { filterable: true }),
// Chromatic aberration source sampler:
sampler(SamplerBindingType::Filtering),
// Chromatic aberration LUT:
texture_2d(TextureSampleType::Float { filterable: true }),
// Chromatic aberration LUT sampler:
sampler(SamplerBindingType::Filtering),
// Chromatic aberration settings:
uniform_buffer::<ChromaticAberrationUniform>(true),
),
),
);
// Both source and chromatic aberration LUTs should be sampled
// bilinearly.
let source_sampler = render_device.create_sampler(&SamplerDescriptor {
mipmap_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mag_filter: FilterMode::Linear,
..default()
});
let chromatic_aberration_lut_sampler = render_device.create_sampler(&SamplerDescriptor {
mipmap_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mag_filter: FilterMode::Linear,
..default()
});
PostProcessingPipeline {
bind_group_layout,
source_sampler,
chromatic_aberration_lut_sampler,
}
}
}
impl SpecializedRenderPipeline for PostProcessingPipeline {
type Key = PostProcessingPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("postprocessing".into()),
layout: vec![self.bind_group_layout.clone()],
vertex: fullscreen_vertex_shader::fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: POST_PROCESSING_SHADER_HANDLE,
shader_defs: vec![],
entry_point: "fragment_main".into(),
targets: vec![Some(ColorTargetState {
format: key.texture_format,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: default(),
depth_stencil: None,
multisample: default(),
push_constant_ranges: vec![],
zero_initialize_workgroup_memory: false,
}
}
}
impl ViewNode for PostProcessingNode {
type ViewQuery = (
Read<ViewTarget>,
Read<PostProcessingPipelineId>,
Read<ChromaticAberration>,
Read<PostProcessingUniformBufferOffsets>,
);
fn run<'w>(
&self,
_: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let pipeline_cache = world.resource::<PipelineCache>();
let post_processing_pipeline = world.resource::<PostProcessingPipeline>();
let post_processing_uniform_buffers = world.resource::<PostProcessingUniformBuffers>();
let gpu_image_assets = world.resource::<RenderAssets<GpuImage>>();
// We need a render pipeline to be prepared.
let Some(pipeline) = pipeline_cache.get_render_pipeline(**pipeline_id) else {
return Ok(());
};
// We need the chromatic aberration LUT to be present.
let Some(chromatic_aberration_lut) = gpu_image_assets.get(&chromatic_aberration.color_lut)
else {
return Ok(());
};
// We need the postprocessing settings to be uploaded to the GPU.
let Some(chromatic_aberration_uniform_buffer_binding) = post_processing_uniform_buffers
.chromatic_aberration
.binding()
else {
return Ok(());
};
// Use the [`PostProcessWrite`] infrastructure, since this is a
// full-screen pass.
let post_process = view_target.post_process_write();
let pass_descriptor = RenderPassDescriptor {
label: Some("postprocessing pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: post_process.destination,
resolve_target: None,
ops: Operations::default(),
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
};
let bind_group = render_context.render_device().create_bind_group(
Some("postprocessing bind group"),
&post_processing_pipeline.bind_group_layout,
&BindGroupEntries::sequential((
post_process.source,
&post_processing_pipeline.source_sampler,
&chromatic_aberration_lut.texture_view,
&post_processing_pipeline.chromatic_aberration_lut_sampler,
chromatic_aberration_uniform_buffer_binding,
)),
);
let mut render_pass = render_context
.command_encoder()
.begin_render_pass(&pass_descriptor);
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, &bind_group, &[**post_processing_uniform_buffer_offsets]);
render_pass.draw(0..3, 0..1);
Ok(())
}
}
/// Specializes the built-in postprocessing pipeline for each applicable view.
pub fn prepare_post_processing_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<PostProcessingPipeline>>,
post_processing_pipeline: Res<PostProcessingPipeline>,
views: Query<(Entity, &ExtractedView), With<ChromaticAberration>>,
) {
for (entity, view) in views.iter() {
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&post_processing_pipeline,
PostProcessingPipelineKey {
texture_format: if view.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
},
);
commands
.entity(entity)
.insert(PostProcessingPipelineId(pipeline_id));
}
}
/// Gathers the built-in postprocessing settings for every view and uploads them
/// to the GPU.
pub fn prepare_post_processing_uniforms(
mut commands: Commands,
mut post_processing_uniform_buffers: ResMut<PostProcessingUniformBuffers>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut views: Query<(Entity, &ChromaticAberration)>,
) {
post_processing_uniform_buffers.clear();
// Gather up all the postprocessing settings.
for (view_entity, chromatic_aberration) in views.iter_mut() {
let chromatic_aberration_uniform_buffer_offset =
post_processing_uniform_buffers.push(&ChromaticAberrationUniform {
intensity: chromatic_aberration.intensity,
max_samples: chromatic_aberration.max_samples,
unused_1: 0,
unused_2: 0,
});
commands
.entity(view_entity)
.insert(PostProcessingUniformBufferOffsets {
chromatic_aberration: chromatic_aberration_uniform_buffer_offset,
});
}
// Upload to the GPU.
post_processing_uniform_buffers.write_buffer(&render_device, &render_queue);
}
impl ExtractComponent for ChromaticAberration {
type QueryData = Read<ChromaticAberration>;
type QueryFilter = With<Camera>;
type Out = ChromaticAberration;
fn extract_component(
chromatic_aberration: QueryItem<'_, Self::QueryData>,
) -> Option<Self::Out> {
// Skip the postprocessing phase entirely if the intensity is zero.
if chromatic_aberration.intensity > 0.0 {
Some(chromatic_aberration.clone())
} else {
None
}
}
}

View File

@@ -0,0 +1,9 @@
// Miscellaneous postprocessing effects, currently just chromatic aberration.
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
#import bevy_core_pipeline::post_processing::chromatic_aberration::chromatic_aberration
@fragment
fn fragment_main(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
return vec4(chromatic_aberration(in.uv), 1.0);
}

View File

@@ -0,0 +1,378 @@
//! Run a prepass before the main pass to generate depth, normals, and/or motion vectors textures, sometimes called a thin g-buffer.
//! These textures are useful for various screen-space effects and reducing overdraw in the main pass.
//!
//! The prepass only runs for opaque meshes or meshes with an alpha mask. Transparent meshes are ignored.
//!
//! To enable the prepass, you need to add a prepass component to a [`crate::prelude::Camera3d`].
//!
//! [`DepthPrepass`]
//! [`NormalPrepass`]
//! [`MotionVectorPrepass`]
//!
//! The textures are automatically added to the default mesh view bindings. You can also get the raw textures
//! by querying the [`ViewPrepassTextures`] component on any camera with a prepass component.
//!
//! The depth prepass will always run and generate the depth buffer as a side effect, but it won't copy it
//! to a separate texture unless the [`DepthPrepass`] is activated. This means that if any prepass component is present
//! it will always create a depth buffer that will be used by the main pass.
//!
//! When using the default mesh view bindings you should be able to use `prepass_depth()`,
//! `prepass_normal()`, and `prepass_motion_vector()` to load the related textures.
//! These functions are defined in `bevy_pbr::prepass_utils`. See the `shader_prepass` example that shows how to use them.
//!
//! The prepass runs for each `Material`. You can control if the prepass should run per-material by setting the `prepass_enabled`
//! flag on the `MaterialPlugin`.
//!
//! Currently only works for 3D.
pub mod node;
use core::ops::Range;
use crate::deferred::{DEFERRED_LIGHTING_PASS_ID_FORMAT, DEFERRED_PREPASS_FORMAT};
use bevy_asset::UntypedAssetId;
use bevy_ecs::prelude::*;
use bevy_math::Mat4;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::mesh::allocator::SlabId;
use bevy_render::render_phase::PhaseItemBatchSetKey;
use bevy_render::sync_world::MainEntity;
use bevy_render::{
render_phase::{
BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem,
PhaseItemExtraIndex,
},
render_resource::{
CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d,
ShaderType, TextureFormat, TextureView,
},
texture::ColorAttachment,
};
pub const NORMAL_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgb10a2Unorm;
pub const MOTION_VECTOR_PREPASS_FORMAT: TextureFormat = TextureFormat::Rg16Float;
/// If added to a [`crate::prelude::Camera3d`] then depth values will be copied to a separate texture available to the main pass.
#[derive(Component, Default, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
pub struct DepthPrepass;
/// If added to a [`crate::prelude::Camera3d`] then vertex world normals will be copied to a separate texture available to the main pass.
/// Normals will have normal map textures already applied.
#[derive(Component, Default, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
pub struct NormalPrepass;
/// If added to a [`crate::prelude::Camera3d`] then screen space motion vectors will be copied to a separate texture available to the main pass.
#[derive(Component, Default, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
pub struct MotionVectorPrepass;
/// If added to a [`crate::prelude::Camera3d`] then deferred materials will be rendered to the deferred gbuffer texture and will be available to subsequent passes.
/// Note the default deferred lighting plugin also requires `DepthPrepass` to work correctly.
#[derive(Component, Default, Reflect)]
#[reflect(Component, Default)]
pub struct DeferredPrepass;
#[derive(Component, ShaderType, Clone)]
pub struct PreviousViewData {
pub view_from_world: Mat4,
pub clip_from_world: Mat4,
pub clip_from_view: Mat4,
}
#[derive(Resource, Default)]
pub struct PreviousViewUniforms {
pub uniforms: DynamicUniformBuffer<PreviousViewData>,
}
#[derive(Component)]
pub struct PreviousViewUniformOffset {
pub offset: u32,
}
/// Textures that are written to by the prepass.
///
/// This component will only be present if any of the relevant prepass components are also present.
#[derive(Component)]
pub struct ViewPrepassTextures {
/// The depth texture generated by the prepass.
/// Exists only if [`DepthPrepass`] is added to the [`ViewTarget`](bevy_render::view::ViewTarget)
pub depth: Option<ColorAttachment>,
/// The normals texture generated by the prepass.
/// Exists only if [`NormalPrepass`] is added to the [`ViewTarget`](bevy_render::view::ViewTarget)
pub normal: Option<ColorAttachment>,
/// The motion vectors texture generated by the prepass.
/// Exists only if [`MotionVectorPrepass`] is added to the `ViewTarget`
pub motion_vectors: Option<ColorAttachment>,
/// The deferred gbuffer generated by the deferred pass.
/// Exists only if [`DeferredPrepass`] is added to the `ViewTarget`
pub deferred: Option<ColorAttachment>,
/// A texture that specifies the deferred lighting pass id for a material.
/// Exists only if [`DeferredPrepass`] is added to the `ViewTarget`
pub deferred_lighting_pass_id: Option<ColorAttachment>,
/// The size of the textures.
pub size: Extent3d,
}
impl ViewPrepassTextures {
pub fn depth_view(&self) -> Option<&TextureView> {
self.depth.as_ref().map(|t| &t.texture.default_view)
}
pub fn normal_view(&self) -> Option<&TextureView> {
self.normal.as_ref().map(|t| &t.texture.default_view)
}
pub fn motion_vectors_view(&self) -> Option<&TextureView> {
self.motion_vectors
.as_ref()
.map(|t| &t.texture.default_view)
}
pub fn deferred_view(&self) -> Option<&TextureView> {
self.deferred.as_ref().map(|t| &t.texture.default_view)
}
}
/// Opaque phase of the 3D prepass.
///
/// Sorted by pipeline, then by mesh to improve batching.
///
/// Used to render all 3D meshes with materials that have no transparency.
pub struct Opaque3dPrepass {
/// Determines which objects can be placed into a *batch set*.
///
/// Objects in a single batch set can potentially be multi-drawn together,
/// if it's enabled and the current platform supports it.
pub batch_set_key: OpaqueNoLightmap3dBatchSetKey,
/// Information that separates items into bins.
pub bin_key: OpaqueNoLightmap3dBinKey,
/// An entity from which Bevy fetches data common to all instances in this
/// batch, such as the mesh.
pub representative_entity: (Entity, MainEntity),
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
}
/// Information that must be identical in order to place opaque meshes in the
/// same *batch set* in the prepass and deferred pass.
///
/// A batch set is a set of batches that can be multi-drawn together, if
/// multi-draw is in use.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OpaqueNoLightmap3dBatchSetKey {
/// The ID of the GPU pipeline.
pub pipeline: CachedRenderPipelineId,
/// The function used to draw the mesh.
pub draw_function: DrawFunctionId,
/// The ID of a bind group specific to the material.
///
/// In the case of PBR, this is the `MaterialBindGroupIndex`.
pub material_bind_group_index: Option<u32>,
/// The ID of the slab of GPU memory that contains vertex data.
///
/// For non-mesh items, you can fill this with 0 if your items can be
/// multi-drawn, or with a unique value if they can't.
pub vertex_slab: SlabId,
/// The ID of the slab of GPU memory that contains index data, if present.
///
/// For non-mesh items, you can safely fill this with `None`.
pub index_slab: Option<SlabId>,
}
impl PhaseItemBatchSetKey for OpaqueNoLightmap3dBatchSetKey {
fn indexed(&self) -> bool {
self.index_slab.is_some()
}
}
// TODO: Try interning these.
/// The data used to bin each opaque 3D object in the prepass and deferred pass.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OpaqueNoLightmap3dBinKey {
/// The ID of the asset.
pub asset_id: UntypedAssetId,
}
impl PhaseItem for Opaque3dPrepass {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity.0
}
fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}
#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.batch_set_key.draw_function
}
#[inline]
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index.clone()
}
#[inline]
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl BinnedPhaseItem for Opaque3dPrepass {
type BatchSetKey = OpaqueNoLightmap3dBatchSetKey;
type BinKey = OpaqueNoLightmap3dBinKey;
#[inline]
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Opaque3dPrepass {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
impl CachedRenderPipelinePhaseItem for Opaque3dPrepass {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.batch_set_key.pipeline
}
}
/// Alpha mask phase of the 3D prepass.
///
/// Sorted by pipeline, then by mesh to improve batching.
///
/// Used to render all meshes with a material with an alpha mask.
pub struct AlphaMask3dPrepass {
/// Determines which objects can be placed into a *batch set*.
///
/// Objects in a single batch set can potentially be multi-drawn together,
/// if it's enabled and the current platform supports it.
pub batch_set_key: OpaqueNoLightmap3dBatchSetKey,
/// Information that separates items into bins.
pub bin_key: OpaqueNoLightmap3dBinKey,
pub representative_entity: (Entity, MainEntity),
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
}
impl PhaseItem for AlphaMask3dPrepass {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity.0
}
fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}
#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.batch_set_key.draw_function
}
#[inline]
fn batch_range(&self) -> &Range<u32> {
&self.batch_range
}
#[inline]
fn batch_range_mut(&mut self) -> &mut Range<u32> {
&mut self.batch_range
}
#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index.clone()
}
#[inline]
fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
(&mut self.batch_range, &mut self.extra_index)
}
}
impl BinnedPhaseItem for AlphaMask3dPrepass {
type BatchSetKey = OpaqueNoLightmap3dBatchSetKey;
type BinKey = OpaqueNoLightmap3dBinKey;
#[inline]
fn new(
batch_set_key: Self::BatchSetKey,
bin_key: Self::BinKey,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Self {
batch_set_key,
bin_key,
representative_entity,
batch_range,
extra_index,
}
}
}
impl CachedRenderPipelinePhaseItem for AlphaMask3dPrepass {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.batch_set_key.pipeline
}
}
pub fn prepass_target_descriptors(
normal_prepass: bool,
motion_vector_prepass: bool,
deferred_prepass: bool,
) -> Vec<Option<ColorTargetState>> {
vec![
normal_prepass.then_some(ColorTargetState {
format: NORMAL_PREPASS_FORMAT,
blend: None,
write_mask: ColorWrites::ALL,
}),
motion_vector_prepass.then_some(ColorTargetState {
format: MOTION_VECTOR_PREPASS_FORMAT,
blend: None,
write_mask: ColorWrites::ALL,
}),
deferred_prepass.then_some(ColorTargetState {
format: DEFERRED_PREPASS_FORMAT,
blend: None,
write_mask: ColorWrites::ALL,
}),
deferred_prepass.then_some(ColorTargetState {
format: DEFERRED_LIGHTING_PASS_ID_FORMAT,
blend: None,
write_mask: ColorWrites::ALL,
}),
]
}

View File

@@ -0,0 +1,248 @@
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::ExtractedCamera,
diagnostic::RecordDiagnostics,
experimental::occlusion_culling::OcclusionCulling,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::{TrackedRenderPass, ViewBinnedRenderPhases},
render_resource::{CommandEncoderDescriptor, PipelineCache, RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ExtractedView, NoIndirectDrawing, ViewDepthTexture, ViewUniformOffset},
};
use tracing::error;
#[cfg(feature = "trace")]
use tracing::info_span;
use crate::skybox::prepass::{RenderSkyboxPrepassPipeline, SkyboxPrepassBindGroup};
use super::{
AlphaMask3dPrepass, DeferredPrepass, Opaque3dPrepass, PreviousViewUniformOffset,
ViewPrepassTextures,
};
/// The phase of the prepass that draws meshes that were visible last frame.
///
/// If occlusion culling isn't in use, this prepass simply draws all meshes.
///
/// Like all prepass nodes, this is inserted before the main pass in the render
/// graph.
#[derive(Default)]
pub struct EarlyPrepassNode;
impl ViewNode for EarlyPrepassNode {
type ViewQuery = <LatePrepassNode as ViewNode>::ViewQuery;
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
view_query: QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
run_prepass(graph, render_context, view_query, world, "early prepass")
}
}
/// The phase of the prepass that runs after occlusion culling against the
/// meshes that were visible last frame.
///
/// If occlusion culling isn't in use, this is a no-op.
///
/// Like all prepass nodes, this is inserted before the main pass in the render
/// graph.
#[derive(Default)]
pub struct LatePrepassNode;
impl ViewNode for LatePrepassNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewDepthTexture,
&'static ViewPrepassTextures,
&'static ViewUniformOffset,
Option<&'static DeferredPrepass>,
Option<&'static RenderSkyboxPrepassPipeline>,
Option<&'static SkyboxPrepassBindGroup>,
Option<&'static PreviousViewUniformOffset>,
Has<OcclusionCulling>,
Has<NoIndirectDrawing>,
Has<DeferredPrepass>,
);
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
query: QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
// We only need a late prepass if we have occlusion culling and indirect
// drawing.
let (_, _, _, _, _, _, _, _, _, occlusion_culling, no_indirect_drawing, _) = query;
if !occlusion_culling || no_indirect_drawing {
return Ok(());
}
run_prepass(graph, render_context, query, world, "late prepass")
}
}
/// Runs a prepass that draws all meshes to the depth buffer, and possibly
/// normal and motion vector buffers as well.
///
/// If occlusion culling isn't in use, and a prepass is enabled, then there's
/// only one prepass. If occlusion culling is in use, then any prepass is split
/// into two: an *early* prepass and a *late* prepass. The early prepass draws
/// what was visible last frame, and the last prepass performs occlusion culling
/// against a conservative hierarchical Z buffer before drawing unoccluded
/// meshes.
fn run_prepass<'w>(
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(
camera,
extracted_view,
view_depth_texture,
view_prepass_textures,
view_uniform_offset,
deferred_prepass,
skybox_prepass_pipeline,
skybox_prepass_bind_group,
view_prev_uniform_offset,
_,
_,
has_deferred,
): QueryItem<'w, <LatePrepassNode as ViewNode>::ViewQuery>,
world: &'w World,
label: &'static str,
) -> Result<(), NodeRunError> {
// If we're using deferred rendering, there will be a deferred prepass
// instead of this one. Just bail out so we don't have to bother looking at
// the empty bins.
if has_deferred {
return Ok(());
}
let (Some(opaque_prepass_phases), Some(alpha_mask_prepass_phases)) = (
world.get_resource::<ViewBinnedRenderPhases<Opaque3dPrepass>>(),
world.get_resource::<ViewBinnedRenderPhases<AlphaMask3dPrepass>>(),
) else {
return Ok(());
};
let (Some(opaque_prepass_phase), Some(alpha_mask_prepass_phase)) = (
opaque_prepass_phases.get(&extracted_view.retained_view_entity),
alpha_mask_prepass_phases.get(&extracted_view.retained_view_entity),
) else {
return Ok(());
};
let diagnostics = render_context.diagnostic_recorder();
let mut color_attachments = vec![
view_prepass_textures
.normal
.as_ref()
.map(|normals_texture| normals_texture.get_attachment()),
view_prepass_textures
.motion_vectors
.as_ref()
.map(|motion_vectors_texture| motion_vectors_texture.get_attachment()),
// Use None in place of deferred attachments
None,
None,
];
// If all color attachments are none: clear the color attachment list so that no fragment shader is required
if color_attachments.iter().all(Option::is_none) {
color_attachments.clear();
}
let depth_stencil_attachment = Some(view_depth_texture.get_attachment(StoreOp::Store));
let view_entity = graph.view_entity();
render_context.add_command_buffer_generation_task(move |render_device| {
#[cfg(feature = "trace")]
let _prepass_span = info_span!("prepass").entered();
// Command encoder setup
let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("prepass_command_encoder"),
});
// Render pass setup
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some(label),
color_attachments: &color_attachments,
depth_stencil_attachment,
timestamp_writes: None,
occlusion_query_set: None,
});
let mut render_pass = TrackedRenderPass::new(&render_device, render_pass);
let pass_span = diagnostics.pass_span(&mut render_pass, label);
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
// Opaque draws
if !opaque_prepass_phase.is_empty() {
#[cfg(feature = "trace")]
let _opaque_prepass_span = info_span!("opaque_prepass").entered();
if let Err(err) = opaque_prepass_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the opaque prepass phase {err:?}");
}
}
// Alpha masked draws
if !alpha_mask_prepass_phase.is_empty() {
#[cfg(feature = "trace")]
let _alpha_mask_prepass_span = info_span!("alpha_mask_prepass").entered();
if let Err(err) = alpha_mask_prepass_phase.render(&mut render_pass, world, view_entity)
{
error!("Error encountered while rendering the alpha mask prepass phase {err:?}");
}
}
// Skybox draw using a fullscreen triangle
if let (
Some(skybox_prepass_pipeline),
Some(skybox_prepass_bind_group),
Some(view_prev_uniform_offset),
) = (
skybox_prepass_pipeline,
skybox_prepass_bind_group,
view_prev_uniform_offset,
) {
let pipeline_cache = world.resource::<PipelineCache>();
if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_prepass_pipeline.0) {
render_pass.set_render_pipeline(pipeline);
render_pass.set_bind_group(
0,
&skybox_prepass_bind_group.0,
&[view_uniform_offset.offset, view_prev_uniform_offset.offset],
);
render_pass.draw(0..3, 0..1);
}
}
pass_span.end(&mut render_pass);
drop(render_pass);
// After rendering to the view depth texture, copy it to the prepass depth texture if deferred isn't going to
if deferred_prepass.is_none() {
if let Some(prepass_depth_texture) = &view_prepass_textures.depth {
command_encoder.copy_texture_to_texture(
view_depth_texture.texture.as_image_copy(),
prepass_depth_texture.texture.texture.as_image_copy(),
view_prepass_textures.size,
);
}
}
command_encoder.finish()
});
Ok(())
}

View File

@@ -0,0 +1,307 @@
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_ecs::{
prelude::{Component, Entity},
query::{QueryItem, With},
reflect::ReflectComponent,
resource::Resource,
schedule::IntoScheduleConfigs,
system::{Commands, Query, Res, ResMut},
};
use bevy_image::{BevyDefault, Image};
use bevy_math::{Mat4, Quat};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::Exposure,
extract_component::{
ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin,
UniformComponentPlugin,
},
render_asset::RenderAssets,
render_resource::{
binding_types::{sampler, texture_cube, uniform_buffer},
*,
},
renderer::RenderDevice,
texture::GpuImage,
view::{ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniforms},
Render, RenderApp, RenderSet,
};
use bevy_transform::components::Transform;
use prepass::{SkyboxPrepassPipeline, SKYBOX_PREPASS_SHADER_HANDLE};
use crate::{core_3d::CORE_3D_DEPTH_FORMAT, prepass::PreviousViewUniforms};
const SKYBOX_SHADER_HANDLE: Handle<Shader> = weak_handle!("a66cf9cc-cab8-47f8-ac32-db82fdc4f29b");
pub mod prepass;
pub struct SkyboxPlugin;
impl Plugin for SkyboxPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, SKYBOX_SHADER_HANDLE, "skybox.wgsl", Shader::from_wgsl);
load_internal_asset!(
app,
SKYBOX_PREPASS_SHADER_HANDLE,
"skybox_prepass.wgsl",
Shader::from_wgsl
);
app.register_type::<Skybox>().add_plugins((
ExtractComponentPlugin::<Skybox>::default(),
UniformComponentPlugin::<SkyboxUniforms>::default(),
));
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedRenderPipelines<SkyboxPipeline>>()
.init_resource::<SpecializedRenderPipelines<SkyboxPrepassPipeline>>()
.init_resource::<PreviousViewUniforms>()
.add_systems(
Render,
(
prepare_skybox_pipelines.in_set(RenderSet::Prepare),
prepass::prepare_skybox_prepass_pipelines.in_set(RenderSet::Prepare),
prepare_skybox_bind_groups.in_set(RenderSet::PrepareBindGroups),
prepass::prepare_skybox_prepass_bind_groups
.in_set(RenderSet::PrepareBindGroups),
),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
let render_device = render_app.world().resource::<RenderDevice>().clone();
render_app
.insert_resource(SkyboxPipeline::new(&render_device))
.init_resource::<SkyboxPrepassPipeline>();
}
}
/// Adds a skybox to a 3D camera, based on a cubemap texture.
///
/// Note that this component does not (currently) affect the scene's lighting.
/// To do so, use `EnvironmentMapLight` alongside this component.
///
/// See also <https://en.wikipedia.org/wiki/Skybox_(video_games)>.
#[derive(Component, Clone, Reflect)]
#[reflect(Component, Default, Clone)]
pub struct Skybox {
pub image: Handle<Image>,
/// Scale factor applied to the skybox image.
/// After applying this multiplier to the image samples, the resulting values should
/// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre).
pub brightness: f32,
/// View space rotation applied to the skybox cubemap.
/// This is useful for users who require a different axis, such as the Z-axis, to serve
/// as the vertical axis.
pub rotation: Quat,
}
impl Default for Skybox {
fn default() -> Self {
Skybox {
image: Handle::default(),
brightness: 0.0,
rotation: Quat::IDENTITY,
}
}
}
impl ExtractComponent for Skybox {
type QueryData = (&'static Self, Option<&'static Exposure>);
type QueryFilter = ();
type Out = (Self, SkyboxUniforms);
fn extract_component((skybox, exposure): QueryItem<'_, Self::QueryData>) -> Option<Self::Out> {
let exposure = exposure
.map(Exposure::exposure)
.unwrap_or_else(|| Exposure::default().exposure());
Some((
skybox.clone(),
SkyboxUniforms {
brightness: skybox.brightness * exposure,
transform: Transform::from_rotation(skybox.rotation)
.compute_matrix()
.inverse(),
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_wasm_padding_8b: 0,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_wasm_padding_12b: 0,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_wasm_padding_16b: 0,
},
))
}
}
// TODO: Replace with a push constant once WebGPU gets support for that
#[derive(Component, ShaderType, Clone)]
pub struct SkyboxUniforms {
brightness: f32,
transform: Mat4,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_wasm_padding_8b: u32,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_wasm_padding_12b: u32,
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
_wasm_padding_16b: u32,
}
#[derive(Resource)]
struct SkyboxPipeline {
bind_group_layout: BindGroupLayout,
}
impl SkyboxPipeline {
fn new(render_device: &RenderDevice) -> Self {
Self {
bind_group_layout: render_device.create_bind_group_layout(
"skybox_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
texture_cube(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
uniform_buffer::<ViewUniform>(true)
.visibility(ShaderStages::VERTEX_FRAGMENT),
uniform_buffer::<SkyboxUniforms>(true),
),
),
),
}
}
}
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
struct SkyboxPipelineKey {
hdr: bool,
samples: u32,
depth_format: TextureFormat,
}
impl SpecializedRenderPipeline for SkyboxPipeline {
type Key = SkyboxPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("skybox_pipeline".into()),
layout: vec![self.bind_group_layout.clone()],
push_constant_ranges: Vec::new(),
vertex: VertexState {
shader: SKYBOX_SHADER_HANDLE,
shader_defs: Vec::new(),
entry_point: "skybox_vertex".into(),
buffers: Vec::new(),
},
primitive: PrimitiveState::default(),
depth_stencil: Some(DepthStencilState {
format: key.depth_format,
depth_write_enabled: false,
depth_compare: CompareFunction::GreaterEqual,
stencil: StencilState {
front: StencilFaceState::IGNORE,
back: StencilFaceState::IGNORE,
read_mask: 0,
write_mask: 0,
},
bias: DepthBiasState {
constant: 0,
slope_scale: 0.0,
clamp: 0.0,
},
}),
multisample: MultisampleState {
count: key.samples,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(FragmentState {
shader: SKYBOX_SHADER_HANDLE,
shader_defs: Vec::new(),
entry_point: "skybox_fragment".into(),
targets: vec![Some(ColorTargetState {
format: if key.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
// BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases.
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
zero_initialize_workgroup_memory: false,
}
}
}
#[derive(Component)]
pub struct SkyboxPipelineId(pub CachedRenderPipelineId);
fn prepare_skybox_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<SkyboxPipeline>>,
pipeline: Res<SkyboxPipeline>,
views: Query<(Entity, &ExtractedView, &Msaa), With<Skybox>>,
) {
for (entity, view, msaa) in &views {
let pipeline_id = pipelines.specialize(
&pipeline_cache,
&pipeline,
SkyboxPipelineKey {
hdr: view.hdr,
samples: msaa.samples(),
depth_format: CORE_3D_DEPTH_FORMAT,
},
);
commands
.entity(entity)
.insert(SkyboxPipelineId(pipeline_id));
}
}
#[derive(Component)]
pub struct SkyboxBindGroup(pub (BindGroup, u32));
fn prepare_skybox_bind_groups(
mut commands: Commands,
pipeline: Res<SkyboxPipeline>,
view_uniforms: Res<ViewUniforms>,
skybox_uniforms: Res<ComponentUniforms<SkyboxUniforms>>,
images: Res<RenderAssets<GpuImage>>,
render_device: Res<RenderDevice>,
views: Query<(Entity, &Skybox, &DynamicUniformIndex<SkyboxUniforms>)>,
) {
for (entity, skybox, skybox_uniform_index) in &views {
if let (Some(skybox), Some(view_uniforms), Some(skybox_uniforms)) = (
images.get(&skybox.image),
view_uniforms.uniforms.binding(),
skybox_uniforms.binding(),
) {
let bind_group = render_device.create_bind_group(
"skybox_bind_group",
&pipeline.bind_group_layout,
&BindGroupEntries::sequential((
&skybox.texture_view,
&skybox.sampler,
view_uniforms,
skybox_uniforms,
)),
);
commands
.entity(entity)
.insert(SkyboxBindGroup((bind_group, skybox_uniform_index.index())));
}
}
}

View File

@@ -0,0 +1,165 @@
//! Adds motion vector support to skyboxes. See [`SkyboxPrepassPipeline`] for details.
use bevy_asset::{weak_handle, Handle};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{Has, With},
resource::Resource,
system::{Commands, Query, Res, ResMut},
world::{FromWorld, World},
};
use bevy_render::{
render_resource::{
binding_types::uniform_buffer, BindGroup, BindGroupEntries, BindGroupLayout,
BindGroupLayoutEntries, CachedRenderPipelineId, CompareFunction, DepthStencilState,
FragmentState, MultisampleState, PipelineCache, RenderPipelineDescriptor, Shader,
ShaderStages, SpecializedRenderPipeline, SpecializedRenderPipelines,
},
renderer::RenderDevice,
view::{Msaa, ViewUniform, ViewUniforms},
};
use bevy_utils::prelude::default;
use crate::{
core_3d::CORE_3D_DEPTH_FORMAT,
prepass::{
prepass_target_descriptors, MotionVectorPrepass, NormalPrepass, PreviousViewData,
PreviousViewUniforms,
},
Skybox,
};
pub const SKYBOX_PREPASS_SHADER_HANDLE: Handle<Shader> =
weak_handle!("7a292435-bfe6-4ed9-8d30-73bf7aa673b0");
/// This pipeline writes motion vectors to the prepass for all [`Skybox`]es.
///
/// This allows features like motion blur and TAA to work correctly on the skybox. Without this, for
/// example, motion blur would not be applied to the skybox when the camera is rotated and motion
/// blur is enabled.
#[derive(Resource)]
pub struct SkyboxPrepassPipeline {
bind_group_layout: BindGroupLayout,
}
/// Used to specialize the [`SkyboxPrepassPipeline`].
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
pub struct SkyboxPrepassPipelineKey {
samples: u32,
normal_prepass: bool,
}
/// Stores the ID for a camera's specialized pipeline, so it can be retrieved from the
/// [`PipelineCache`].
#[derive(Component)]
pub struct RenderSkyboxPrepassPipeline(pub CachedRenderPipelineId);
/// Stores the [`SkyboxPrepassPipeline`] bind group for a camera. This is later used by the prepass
/// render graph node to add this binding to the prepass's render pass.
#[derive(Component)]
pub struct SkyboxPrepassBindGroup(pub BindGroup);
impl FromWorld for SkyboxPrepassPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
Self {
bind_group_layout: render_device.create_bind_group_layout(
"skybox_prepass_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
uniform_buffer::<ViewUniform>(true),
uniform_buffer::<PreviousViewData>(true),
),
),
),
}
}
}
impl SpecializedRenderPipeline for SkyboxPrepassPipeline {
type Key = SkyboxPrepassPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
label: Some("skybox_prepass_pipeline".into()),
layout: vec![self.bind_group_layout.clone()],
push_constant_ranges: vec![],
vertex: crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state(),
primitive: default(),
depth_stencil: Some(DepthStencilState {
format: CORE_3D_DEPTH_FORMAT,
depth_write_enabled: false,
depth_compare: CompareFunction::GreaterEqual,
stencil: default(),
bias: default(),
}),
multisample: MultisampleState {
count: key.samples,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(FragmentState {
shader: SKYBOX_PREPASS_SHADER_HANDLE,
shader_defs: vec![],
entry_point: "fragment".into(),
targets: prepass_target_descriptors(key.normal_prepass, true, false),
}),
zero_initialize_workgroup_memory: false,
}
}
}
/// Specialize and cache the [`SkyboxPrepassPipeline`] for each camera with a [`Skybox`].
pub fn prepare_skybox_prepass_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<SkyboxPrepassPipeline>>,
pipeline: Res<SkyboxPrepassPipeline>,
views: Query<(Entity, Has<NormalPrepass>, &Msaa), (With<Skybox>, With<MotionVectorPrepass>)>,
) {
for (entity, normal_prepass, msaa) in &views {
let pipeline_key = SkyboxPrepassPipelineKey {
samples: msaa.samples(),
normal_prepass,
};
let render_skybox_prepass_pipeline =
pipelines.specialize(&pipeline_cache, &pipeline, pipeline_key);
commands
.entity(entity)
.insert(RenderSkyboxPrepassPipeline(render_skybox_prepass_pipeline));
}
}
/// Creates the required bind groups for the [`SkyboxPrepassPipeline`]. This binds the view uniforms
/// from the CPU for access in the prepass shader on the GPU, allowing us to compute camera motion
/// between frames. This is then stored in the [`SkyboxPrepassBindGroup`] component on the camera.
pub fn prepare_skybox_prepass_bind_groups(
mut commands: Commands,
pipeline: Res<SkyboxPrepassPipeline>,
view_uniforms: Res<ViewUniforms>,
prev_view_uniforms: Res<PreviousViewUniforms>,
render_device: Res<RenderDevice>,
views: Query<Entity, (With<Skybox>, With<MotionVectorPrepass>)>,
) {
for entity in &views {
let (Some(view_uniforms), Some(prev_view_uniforms)) = (
view_uniforms.uniforms.binding(),
prev_view_uniforms.uniforms.binding(),
) else {
continue;
};
let bind_group = render_device.create_bind_group(
"skybox_prepass_bind_group",
&pipeline.bind_group_layout,
&BindGroupEntries::sequential((view_uniforms, prev_view_uniforms)),
);
commands
.entity(entity)
.insert(SkyboxPrepassBindGroup(bind_group));
}
}

View File

@@ -0,0 +1,81 @@
#import bevy_render::view::View
#import bevy_pbr::utils::coords_to_viewport_uv
struct SkyboxUniforms {
brightness: f32,
transform: mat4x4<f32>,
#ifdef SIXTEEN_BYTE_ALIGNMENT
_wasm_padding_8b: u32,
_wasm_padding_12b: u32,
_wasm_padding_16b: u32,
#endif
}
@group(0) @binding(0) var skybox: texture_cube<f32>;
@group(0) @binding(1) var skybox_sampler: sampler;
@group(0) @binding(2) var<uniform> view: View;
@group(0) @binding(3) var<uniform> uniforms: SkyboxUniforms;
fn coords_to_ray_direction(position: vec2<f32>, viewport: vec4<f32>) -> vec3<f32> {
// Using world positions of the fragment and camera to calculate a ray direction
// breaks down at large translations. This code only needs to know the ray direction.
// The ray direction is along the direction from the camera to the fragment position.
// In view space, the camera is at the origin, so the view space ray direction is
// along the direction of the fragment position - (0,0,0) which is just the
// fragment position.
// Use the position on the near clipping plane to avoid -inf world position
// because the far plane of an infinite reverse projection is at infinity.
let view_position_homogeneous = view.view_from_clip * vec4(
coords_to_viewport_uv(position, viewport) * vec2(2.0, -2.0) + vec2(-1.0, 1.0),
1.0,
1.0,
);
// Transforming the view space ray direction by the skybox transform matrix, it is
// equivalent to rotating the skybox itself.
var view_ray_direction = view_position_homogeneous.xyz / view_position_homogeneous.w;
view_ray_direction = (view.world_from_view * vec4(view_ray_direction, 0.0)).xyz;
// Transforming the view space ray direction by the view matrix, transforms the
// direction to world space. Note that the w element is set to 0.0, as this is a
// vector direction, not a position, That causes the matrix multiplication to ignore
// the translations from the view matrix.
let ray_direction = (uniforms.transform * vec4(view_ray_direction, 0.0)).xyz;
return normalize(ray_direction);
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
};
// 3 | 2.
// 2 | : `.
// 1 | x-----x.
// 0 | | s | `.
// -1 | 0-----x.....1
// +---------------
// -1 0 1 2 3
//
// The axes are clip-space x and y. The region marked s is the visible region.
// The digits in the corners of the right-angled triangle are the vertex
// indices.
@vertex
fn skybox_vertex(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
// See the explanation above for how this works.
let clip_position = vec2(
f32(vertex_index & 1u),
f32((vertex_index >> 1u) & 1u),
) * 4.0 - vec2(1.0);
return VertexOutput(vec4(clip_position, 0.0, 1.0));
}
@fragment
fn skybox_fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let ray_direction = coords_to_ray_direction(in.position.xy, view.viewport);
// Cube maps are left-handed so we negate the z coordinate.
let out = textureSample(skybox, skybox_sampler, ray_direction * vec3(1.0, 1.0, -1.0));
return vec4(out.rgb * uniforms.brightness, out.a);
}

View File

@@ -0,0 +1,21 @@
#import bevy_render::view::View
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
#import bevy_pbr::view_transformations::uv_to_ndc
struct PreviousViewUniforms {
view_from_world: mat4x4<f32>,
clip_from_world: mat4x4<f32>,
}
@group(0) @binding(0) var<uniform> view: View;
@group(0) @binding(1) var<uniform> previous_view: PreviousViewUniforms;
@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(1) vec4<f32> {
let clip_pos = uv_to_ndc(in.uv); // Convert from uv to clip space
let world_pos = view.world_from_clip * vec4(clip_pos, 0.0, 1.0);
let prev_clip_pos = (previous_view.clip_from_world * world_pos).xy;
let velocity = (clip_pos - prev_clip_pos) * vec2(0.5, -0.5); // Copied from mesh motion vectors
return vec4(velocity.x, velocity.y, 0.0, 1.0);
}

Binary file not shown.

Binary file not shown.

1086
vendor/bevy_core_pipeline/src/smaa/mod.rs vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

494
vendor/bevy_core_pipeline/src/taa/mod.rs vendored Normal file
View File

@@ -0,0 +1,494 @@
use crate::{
core_3d::graph::{Core3d, Node3d},
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
prelude::Camera3d,
prepass::{DepthPrepass, MotionVectorPrepass, ViewPrepassTextures},
};
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_diagnostic::FrameCount;
use bevy_ecs::{
prelude::{Component, Entity, ReflectComponent},
query::{QueryItem, With},
resource::Resource,
schedule::IntoScheduleConfigs,
system::{Commands, Query, Res, ResMut},
world::{FromWorld, World},
};
use bevy_image::BevyDefault as _;
use bevy_math::vec2;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::{ExtractedCamera, MipBias, TemporalJitter},
prelude::{Camera, Projection},
render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
render_resource::{
binding_types::{sampler, texture_2d, texture_depth_2d},
BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId,
ColorTargetState, ColorWrites, Extent3d, FilterMode, FragmentState, MultisampleState,
Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor,
RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader,
ShaderStages, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureDescriptor,
TextureDimension, TextureFormat, TextureSampleType, TextureUsages,
},
renderer::{RenderContext, RenderDevice},
sync_component::SyncComponentPlugin,
sync_world::RenderEntity,
texture::{CachedTexture, TextureCache},
view::{ExtractedView, Msaa, ViewTarget},
ExtractSchedule, MainWorld, Render, RenderApp, RenderSet,
};
use tracing::warn;
const TAA_SHADER_HANDLE: Handle<Shader> = weak_handle!("fea20d50-86b6-4069-aa32-374346aec00c");
/// Plugin for temporal anti-aliasing.
///
/// See [`TemporalAntiAliasing`] for more details.
pub struct TemporalAntiAliasPlugin;
impl Plugin for TemporalAntiAliasPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, TAA_SHADER_HANDLE, "taa.wgsl", Shader::from_wgsl);
app.register_type::<TemporalAntiAliasing>();
app.add_plugins(SyncComponentPlugin::<TemporalAntiAliasing>::default());
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedRenderPipelines<TaaPipeline>>()
.add_systems(ExtractSchedule, extract_taa_settings)
.add_systems(
Render,
(
prepare_taa_jitter_and_mip_bias.in_set(RenderSet::ManageViews),
prepare_taa_pipelines.in_set(RenderSet::Prepare),
prepare_taa_history_textures.in_set(RenderSet::PrepareResources),
),
)
.add_render_graph_node::<ViewNodeRunner<TemporalAntiAliasNode>>(Core3d, Node3d::Taa)
.add_render_graph_edges(
Core3d,
(
Node3d::EndMainPass,
Node3d::MotionBlur, // Running before TAA reduces edge artifacts and noise
Node3d::Taa,
Node3d::Bloom,
Node3d::Tonemapping,
),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<TaaPipeline>();
}
}
/// Component to apply temporal anti-aliasing to a 3D perspective camera.
///
/// Temporal anti-aliasing (TAA) is a form of image smoothing/filtering, like
/// multisample anti-aliasing (MSAA), or fast approximate anti-aliasing (FXAA).
/// TAA works by blending (averaging) each frame with the past few frames.
///
/// # Tradeoffs
///
/// Pros:
/// * Filters more types of aliasing than MSAA, such as textures and singular bright pixels (specular aliasing)
/// * Cost scales with screen/view resolution, unlike MSAA which scales with number of triangles
/// * Greatly increases the quality of stochastic rendering techniques such as SSAO, certain shadow map sampling methods, etc
///
/// Cons:
/// * Chance of "ghosting" - ghostly trails left behind moving objects
/// * Thin geometry, lighting detail, or texture lines may flicker noisily or disappear
///
/// Because TAA blends past frames with the current frame, when the frames differ too much
/// (such as with fast moving objects or camera cuts), ghosting artifacts may occur.
///
/// Artifacts tend to be reduced at higher framerates and rendering resolution.
///
/// # Usage Notes
///
/// The [`TemporalAntiAliasPlugin`] must be added to your app.
/// Any camera with this component must also disable [`Msaa`] by setting it to [`Msaa::Off`].
///
/// [Currently](https://github.com/bevyengine/bevy/issues/8423), TAA cannot be used with [`bevy_render::camera::OrthographicProjection`].
///
/// TAA also does not work well with alpha-blended meshes, as it requires depth writing to determine motion.
///
/// It is very important that correct motion vectors are written for everything on screen.
/// Failure to do so will lead to ghosting artifacts. For instance, if particle effects
/// are added using a third party library, the library must either:
///
/// 1. Write particle motion vectors to the motion vectors prepass texture
/// 2. Render particles after TAA
///
/// If no [`MipBias`] component is attached to the camera, TAA will add a `MipBias(-1.0)` component.
#[derive(Component, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
#[require(TemporalJitter, DepthPrepass, MotionVectorPrepass)]
#[doc(alias = "Taa")]
pub struct TemporalAntiAliasing {
/// Set to true to delete the saved temporal history (past frames).
///
/// Useful for preventing ghosting when the history is no longer
/// representative of the current frame, such as in sudden camera cuts.
///
/// After setting this to true, it will automatically be toggled
/// back to false at the end of the frame.
pub reset: bool,
}
impl Default for TemporalAntiAliasing {
fn default() -> Self {
Self { reset: true }
}
}
/// Render [`bevy_render::render_graph::Node`] used by temporal anti-aliasing.
#[derive(Default)]
pub struct TemporalAntiAliasNode;
impl ViewNode for TemporalAntiAliasNode {
type ViewQuery = (
&'static ExtractedCamera,
&'static ViewTarget,
&'static TemporalAntiAliasHistoryTextures,
&'static ViewPrepassTextures,
&'static TemporalAntiAliasPipelineId,
&'static Msaa,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(camera, view_target, taa_history_textures, prepass_textures, taa_pipeline_id, msaa): QueryItem<
Self::ViewQuery,
>,
world: &World,
) -> Result<(), NodeRunError> {
if *msaa != Msaa::Off {
warn!("Temporal anti-aliasing requires MSAA to be disabled");
return Ok(());
}
let (Some(pipelines), Some(pipeline_cache)) = (
world.get_resource::<TaaPipeline>(),
world.get_resource::<PipelineCache>(),
) else {
return Ok(());
};
let (Some(taa_pipeline), Some(prepass_motion_vectors_texture), Some(prepass_depth_texture)) = (
pipeline_cache.get_render_pipeline(taa_pipeline_id.0),
&prepass_textures.motion_vectors,
&prepass_textures.depth,
) else {
return Ok(());
};
let view_target = view_target.post_process_write();
let taa_bind_group = render_context.render_device().create_bind_group(
"taa_bind_group",
&pipelines.taa_bind_group_layout,
&BindGroupEntries::sequential((
view_target.source,
&taa_history_textures.read.default_view,
&prepass_motion_vectors_texture.texture.default_view,
&prepass_depth_texture.texture.default_view,
&pipelines.nearest_sampler,
&pipelines.linear_sampler,
)),
);
{
let mut taa_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
label: Some("taa_pass"),
color_attachments: &[
Some(RenderPassColorAttachment {
view: view_target.destination,
resolve_target: None,
ops: Operations::default(),
}),
Some(RenderPassColorAttachment {
view: &taa_history_textures.write.default_view,
resolve_target: None,
ops: Operations::default(),
}),
],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
taa_pass.set_render_pipeline(taa_pipeline);
taa_pass.set_bind_group(0, &taa_bind_group, &[]);
if let Some(viewport) = camera.viewport.as_ref() {
taa_pass.set_camera_viewport(viewport);
}
taa_pass.draw(0..3, 0..1);
}
Ok(())
}
}
#[derive(Resource)]
struct TaaPipeline {
taa_bind_group_layout: BindGroupLayout,
nearest_sampler: Sampler,
linear_sampler: Sampler,
}
impl FromWorld for TaaPipeline {
fn from_world(world: &mut World) -> Self {
let render_device = world.resource::<RenderDevice>();
let nearest_sampler = render_device.create_sampler(&SamplerDescriptor {
label: Some("taa_nearest_sampler"),
mag_filter: FilterMode::Nearest,
min_filter: FilterMode::Nearest,
..SamplerDescriptor::default()
});
let linear_sampler = render_device.create_sampler(&SamplerDescriptor {
label: Some("taa_linear_sampler"),
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
..SamplerDescriptor::default()
});
let taa_bind_group_layout = render_device.create_bind_group_layout(
"taa_bind_group_layout",
&BindGroupLayoutEntries::sequential(
ShaderStages::FRAGMENT,
(
// View target (read)
texture_2d(TextureSampleType::Float { filterable: true }),
// TAA History (read)
texture_2d(TextureSampleType::Float { filterable: true }),
// Motion Vectors
texture_2d(TextureSampleType::Float { filterable: true }),
// Depth
texture_depth_2d(),
// Nearest sampler
sampler(SamplerBindingType::NonFiltering),
// Linear sampler
sampler(SamplerBindingType::Filtering),
),
),
);
TaaPipeline {
taa_bind_group_layout,
nearest_sampler,
linear_sampler,
}
}
}
#[derive(PartialEq, Eq, Hash, Clone)]
struct TaaPipelineKey {
hdr: bool,
reset: bool,
}
impl SpecializedRenderPipeline for TaaPipeline {
type Key = TaaPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let mut shader_defs = vec![];
let format = if key.hdr {
shader_defs.push("TONEMAP".into());
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
};
if key.reset {
shader_defs.push("RESET".into());
}
RenderPipelineDescriptor {
label: Some("taa_pipeline".into()),
layout: vec![self.taa_bind_group_layout.clone()],
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: TAA_SHADER_HANDLE,
shader_defs,
entry_point: "taa".into(),
targets: vec![
Some(ColorTargetState {
format,
blend: None,
write_mask: ColorWrites::ALL,
}),
Some(ColorTargetState {
format,
blend: None,
write_mask: ColorWrites::ALL,
}),
],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
}
}
}
fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut<MainWorld>) {
let mut cameras_3d = main_world.query_filtered::<(
RenderEntity,
&Camera,
&Projection,
&mut TemporalAntiAliasing,
), (
With<Camera3d>,
With<TemporalJitter>,
With<DepthPrepass>,
With<MotionVectorPrepass>,
)>();
for (entity, camera, camera_projection, mut taa_settings) in
cameras_3d.iter_mut(&mut main_world)
{
let has_perspective_projection = matches!(camera_projection, Projection::Perspective(_));
let mut entity_commands = commands
.get_entity(entity)
.expect("Camera entity wasn't synced.");
if camera.is_active && has_perspective_projection {
entity_commands.insert(taa_settings.clone());
taa_settings.reset = false;
} else {
// TODO: needs better strategy for cleaning up
entity_commands.remove::<(
TemporalAntiAliasing,
// components added in prepare systems (because `TemporalAntiAliasNode` does not query extracted components)
TemporalAntiAliasHistoryTextures,
TemporalAntiAliasPipelineId,
)>();
}
}
}
fn prepare_taa_jitter_and_mip_bias(
frame_count: Res<FrameCount>,
mut query: Query<(Entity, &mut TemporalJitter, Option<&MipBias>), With<TemporalAntiAliasing>>,
mut commands: Commands,
) {
// Halton sequence (2, 3) - 0.5, skipping i = 0
let halton_sequence = [
vec2(0.0, -0.16666666),
vec2(-0.25, 0.16666669),
vec2(0.25, -0.3888889),
vec2(-0.375, -0.055555552),
vec2(0.125, 0.2777778),
vec2(-0.125, -0.2777778),
vec2(0.375, 0.055555582),
vec2(-0.4375, 0.3888889),
];
let offset = halton_sequence[frame_count.0 as usize % halton_sequence.len()];
for (entity, mut jitter, mip_bias) in &mut query {
jitter.offset = offset;
if mip_bias.is_none() {
commands.entity(entity).insert(MipBias(-1.0));
}
}
}
#[derive(Component)]
pub struct TemporalAntiAliasHistoryTextures {
write: CachedTexture,
read: CachedTexture,
}
fn prepare_taa_history_textures(
mut commands: Commands,
mut texture_cache: ResMut<TextureCache>,
render_device: Res<RenderDevice>,
frame_count: Res<FrameCount>,
views: Query<(Entity, &ExtractedCamera, &ExtractedView), With<TemporalAntiAliasing>>,
) {
for (entity, camera, view) in &views {
if let Some(physical_target_size) = camera.physical_target_size {
let mut texture_descriptor = TextureDescriptor {
label: None,
size: Extent3d {
depth_or_array_layers: 1,
width: physical_target_size.x,
height: physical_target_size.y,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: if view.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
},
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
};
texture_descriptor.label = Some("taa_history_1_texture");
let history_1_texture = texture_cache.get(&render_device, texture_descriptor.clone());
texture_descriptor.label = Some("taa_history_2_texture");
let history_2_texture = texture_cache.get(&render_device, texture_descriptor);
let textures = if frame_count.0 % 2 == 0 {
TemporalAntiAliasHistoryTextures {
write: history_1_texture,
read: history_2_texture,
}
} else {
TemporalAntiAliasHistoryTextures {
write: history_2_texture,
read: history_1_texture,
}
};
commands.entity(entity).insert(textures);
}
}
}
#[derive(Component)]
pub struct TemporalAntiAliasPipelineId(CachedRenderPipelineId);
fn prepare_taa_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<TaaPipeline>>,
pipeline: Res<TaaPipeline>,
views: Query<(Entity, &ExtractedView, &TemporalAntiAliasing)>,
) {
for (entity, view, taa_settings) in &views {
let mut pipeline_key = TaaPipelineKey {
hdr: view.hdr,
reset: taa_settings.reset,
};
let pipeline_id = pipelines.specialize(&pipeline_cache, &pipeline, pipeline_key.clone());
// Prepare non-reset pipeline anyways - it will be necessary next frame
if pipeline_key.reset {
pipeline_key.reset = false;
pipelines.specialize(&pipeline_cache, &pipeline, pipeline_key);
}
commands
.entity(entity)
.insert(TemporalAntiAliasPipelineId(pipeline_id));
}
}

View File

@@ -0,0 +1,201 @@
// References:
// https://www.elopezr.com/temporal-aa-and-the-quest-for-the-holy-trail
// http://behindthepixels.io/assets/files/TemporalAA.pdf
// http://leiy.cc/publications/TAA/TAA_EG2020_Talk.pdf
// https://advances.realtimerendering.com/s2014/index.html#_HIGH-QUALITY_TEMPORAL_SUPERSAMPLING
// Controls how much to blend between the current and past samples
// Lower numbers = less of the current sample and more of the past sample = more smoothing
// Values chosen empirically
const DEFAULT_HISTORY_BLEND_RATE: f32 = 0.1; // Default blend rate to use when no confidence in history
const MIN_HISTORY_BLEND_RATE: f32 = 0.015; // Minimum blend rate allowed, to ensure at least some of the current sample is used
@group(0) @binding(0) var view_target: texture_2d<f32>;
@group(0) @binding(1) var history: texture_2d<f32>;
@group(0) @binding(2) var motion_vectors: texture_2d<f32>;
@group(0) @binding(3) var depth: texture_depth_2d;
@group(0) @binding(4) var nearest_sampler: sampler;
@group(0) @binding(5) var linear_sampler: sampler;
struct Output {
@location(0) view_target: vec4<f32>,
@location(1) history: vec4<f32>,
};
// TAA is ideally applied after tonemapping, but before post processing
// Post processing wants to go before tonemapping, which conflicts
// Solution: Put TAA before tonemapping, tonemap TAA input, apply TAA, invert-tonemap TAA output
// https://advances.realtimerendering.com/s2014/index.html#_HIGH-QUALITY_TEMPORAL_SUPERSAMPLING, slide 20
// https://gpuopen.com/learn/optimized-reversible-tonemapper-for-resolve
fn rcp(x: f32) -> f32 { return 1.0 / x; }
fn max3(x: vec3<f32>) -> f32 { return max(x.r, max(x.g, x.b)); }
fn tonemap(color: vec3<f32>) -> vec3<f32> { return color * rcp(max3(color) + 1.0); }
fn reverse_tonemap(color: vec3<f32>) -> vec3<f32> { return color * rcp(1.0 - max3(color)); }
// The following 3 functions are from Playdead (MIT-licensed)
// https://github.com/playdeadgames/temporal/blob/master/Assets/Shaders/TemporalReprojection.shader
fn RGB_to_YCoCg(rgb: vec3<f32>) -> vec3<f32> {
let y = (rgb.r / 4.0) + (rgb.g / 2.0) + (rgb.b / 4.0);
let co = (rgb.r / 2.0) - (rgb.b / 2.0);
let cg = (-rgb.r / 4.0) + (rgb.g / 2.0) - (rgb.b / 4.0);
return vec3(y, co, cg);
}
fn YCoCg_to_RGB(ycocg: vec3<f32>) -> vec3<f32> {
let r = ycocg.x + ycocg.y - ycocg.z;
let g = ycocg.x + ycocg.z;
let b = ycocg.x - ycocg.y - ycocg.z;
return saturate(vec3(r, g, b));
}
fn clip_towards_aabb_center(history_color: vec3<f32>, current_color: vec3<f32>, aabb_min: vec3<f32>, aabb_max: vec3<f32>) -> vec3<f32> {
let p_clip = 0.5 * (aabb_max + aabb_min);
let e_clip = 0.5 * (aabb_max - aabb_min) + 0.00000001;
let v_clip = history_color - p_clip;
let v_unit = v_clip / e_clip;
let a_unit = abs(v_unit);
let ma_unit = max3(a_unit);
if ma_unit > 1.0 {
return p_clip + (v_clip / ma_unit);
} else {
return history_color;
}
}
fn sample_history(u: f32, v: f32) -> vec3<f32> {
return textureSample(history, linear_sampler, vec2(u, v)).rgb;
}
fn sample_view_target(uv: vec2<f32>) -> vec3<f32> {
var sample = textureSample(view_target, nearest_sampler, uv).rgb;
#ifdef TONEMAP
sample = tonemap(sample);
#endif
return RGB_to_YCoCg(sample);
}
@fragment
fn taa(@location(0) uv: vec2<f32>) -> Output {
let texture_size = vec2<f32>(textureDimensions(view_target));
let texel_size = 1.0 / texture_size;
// Fetch the current sample
let original_color = textureSample(view_target, nearest_sampler, uv);
var current_color = original_color.rgb;
#ifdef TONEMAP
current_color = tonemap(current_color);
#endif
#ifndef RESET
// Pick the closest motion_vector from 5 samples (reduces aliasing on the edges of moving entities)
// https://advances.realtimerendering.com/s2014/index.html#_HIGH-QUALITY_TEMPORAL_SUPERSAMPLING, slide 27
let offset = texel_size * 2.0;
let d_uv_tl = uv + vec2(-offset.x, offset.y);
let d_uv_tr = uv + vec2(offset.x, offset.y);
let d_uv_bl = uv + vec2(-offset.x, -offset.y);
let d_uv_br = uv + vec2(offset.x, -offset.y);
var closest_uv = uv;
let d_tl = textureSample(depth, nearest_sampler, d_uv_tl);
let d_tr = textureSample(depth, nearest_sampler, d_uv_tr);
var closest_depth = textureSample(depth, nearest_sampler, uv);
let d_bl = textureSample(depth, nearest_sampler, d_uv_bl);
let d_br = textureSample(depth, nearest_sampler, d_uv_br);
if d_tl > closest_depth {
closest_uv = d_uv_tl;
closest_depth = d_tl;
}
if d_tr > closest_depth {
closest_uv = d_uv_tr;
closest_depth = d_tr;
}
if d_bl > closest_depth {
closest_uv = d_uv_bl;
closest_depth = d_bl;
}
if d_br > closest_depth {
closest_uv = d_uv_br;
}
let closest_motion_vector = textureSample(motion_vectors, nearest_sampler, closest_uv).rg;
// Reproject to find the equivalent sample from the past
// Uses 5-sample Catmull-Rom filtering (reduces blurriness)
// Catmull-Rom filtering: https://gist.github.com/TheRealMJP/c83b8c0f46b63f3a88a5986f4fa982b1
// Ignoring corners: https://www.activision.com/cdn/research/Dynamic_Temporal_Antialiasing_and_Upsampling_in_Call_of_Duty_v4.pdf#page=68
// Technically we should renormalize the weights since we're skipping the corners, but it's basically the same result
let history_uv = uv - closest_motion_vector;
let sample_position = history_uv * texture_size;
let texel_center = floor(sample_position - 0.5) + 0.5;
let f = sample_position - texel_center;
let w0 = f * (-0.5 + f * (1.0 - 0.5 * f));
let w1 = 1.0 + f * f * (-2.5 + 1.5 * f);
let w2 = f * (0.5 + f * (2.0 - 1.5 * f));
let w3 = f * f * (-0.5 + 0.5 * f);
let w12 = w1 + w2;
let texel_position_0 = (texel_center - 1.0) * texel_size;
let texel_position_3 = (texel_center + 2.0) * texel_size;
let texel_position_12 = (texel_center + (w2 / w12)) * texel_size;
var history_color = sample_history(texel_position_12.x, texel_position_0.y) * w12.x * w0.y;
history_color += sample_history(texel_position_0.x, texel_position_12.y) * w0.x * w12.y;
history_color += sample_history(texel_position_12.x, texel_position_12.y) * w12.x * w12.y;
history_color += sample_history(texel_position_3.x, texel_position_12.y) * w3.x * w12.y;
history_color += sample_history(texel_position_12.x, texel_position_3.y) * w12.x * w3.y;
// Constrain past sample with 3x3 YCoCg variance clipping (reduces ghosting)
// YCoCg: https://advances.realtimerendering.com/s2014/index.html#_HIGH-QUALITY_TEMPORAL_SUPERSAMPLING, slide 33
// Variance clipping: https://developer.download.nvidia.com/gameworks/events/GDC2016/msalvi_temporal_supersampling.pdf
let s_tl = sample_view_target(uv + vec2(-texel_size.x, texel_size.y));
let s_tm = sample_view_target(uv + vec2( 0.0, texel_size.y));
let s_tr = sample_view_target(uv + vec2( texel_size.x, texel_size.y));
let s_ml = sample_view_target(uv + vec2(-texel_size.x, 0.0));
let s_mm = RGB_to_YCoCg(current_color);
let s_mr = sample_view_target(uv + vec2( texel_size.x, 0.0));
let s_bl = sample_view_target(uv + vec2(-texel_size.x, -texel_size.y));
let s_bm = sample_view_target(uv + vec2( 0.0, -texel_size.y));
let s_br = sample_view_target(uv + vec2( texel_size.x, -texel_size.y));
let moment_1 = s_tl + s_tm + s_tr + s_ml + s_mm + s_mr + s_bl + s_bm + s_br;
let moment_2 = (s_tl * s_tl) + (s_tm * s_tm) + (s_tr * s_tr) + (s_ml * s_ml) + (s_mm * s_mm) + (s_mr * s_mr) + (s_bl * s_bl) + (s_bm * s_bm) + (s_br * s_br);
let mean = moment_1 / 9.0;
let variance = (moment_2 / 9.0) - (mean * mean);
let std_deviation = sqrt(max(variance, vec3(0.0)));
history_color = RGB_to_YCoCg(history_color);
history_color = clip_towards_aabb_center(history_color, s_mm, mean - std_deviation, mean + std_deviation);
history_color = YCoCg_to_RGB(history_color);
// How confident we are that the history is representative of the current frame
var history_confidence = textureSample(history, nearest_sampler, uv).a;
let pixel_motion_vector = abs(closest_motion_vector) * texture_size;
if pixel_motion_vector.x < 0.01 && pixel_motion_vector.y < 0.01 {
// Increment when pixels are not moving
history_confidence += 10.0;
} else {
// Else reset
history_confidence = 1.0;
}
// Blend current and past sample
// Use more of the history if we're confident in it (reduces noise when there is no motion)
// https://hhoppe.com/supersample.pdf, section 4.1
var current_color_factor = clamp(1.0 / history_confidence, MIN_HISTORY_BLEND_RATE, DEFAULT_HISTORY_BLEND_RATE);
// Reject history when motion vectors point off screen
if any(saturate(history_uv) != history_uv) {
current_color_factor = 1.0;
history_confidence = 1.0;
}
current_color = mix(history_color, current_color, current_color_factor);
#endif // #ifndef RESET
// Write output to history and view target
var out: Output;
#ifdef RESET
let history_confidence = 1.0 / MIN_HISTORY_BLEND_RATE;
#endif
out.history = vec4(current_color, history_confidence);
#ifdef TONEMAP
current_color = reverse_tonemap(current_color);
#endif
out.view_target = vec4(current_color, original_color.a);
return out;
}

View File

@@ -0,0 +1,5 @@
#define_import_path bevy_core_pipeline::tonemapping_lut_bindings
@group(0) @binding(#TONEMAPPING_LUT_TEXTURE_BINDING_INDEX) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(#TONEMAPPING_LUT_SAMPLER_BINDING_INDEX) var dt_lut_sampler: sampler;

Binary file not shown.

View File

@@ -0,0 +1,22 @@
--- Process for recreating AgX-default_contrast.ktx2 ---
Download:
https://github.com/MrLixm/AgXc/blob/898198e0490b0551ed81412a0c22e0b72fffb7cd/obs/obs-script/AgX-default_contrast.lut.png
Convert to vertical strip exr with:
https://gist.github.com/DGriffin91/fc8e0cfd55aaa175ac10199403bc19b8
Convert exr to 3D ktx2 with:
https://gist.github.com/DGriffin91/49401c43378b58bce32059291097d4ca
--- Process for recreating tony_mc_mapface.ktx2 ---
Download:
https://github.com/h3r2tic/tony-mc-mapface/blob/909e51c8a74251fd828770248476cb084081e08c/tony_mc_mapface.dds
Convert dds to 3D ktx2 with:
https://gist.github.com/DGriffin91/49401c43378b58bce32059291097d4ca
--- Process for recreating Blender_-11_12.ktx2 ---
Create LUT stimulus with:
https://gist.github.com/DGriffin91/e119bf32b520e219f6e102a6eba4a0cf
Open LUT image in Blender's image editor and make sure color space is set to linear.
Export from Blender as 32bit EXR, override color space to Filmic sRGB.
Import EXR back into blender set color space to sRGB, then export as 32bit EXR override color space to linear.
Convert exr to 3D ktx2 with:
https://gist.github.com/DGriffin91/49401c43378b58bce32059291097d4ca

Binary file not shown.

View File

@@ -0,0 +1,487 @@
use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state;
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, weak_handle, Assets, Handle};
use bevy_ecs::prelude::*;
use bevy_image::{CompressedImageFormats, Image, ImageSampler, ImageType};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::Camera,
extract_component::{ExtractComponent, ExtractComponentPlugin},
extract_resource::{ExtractResource, ExtractResourcePlugin},
render_asset::{RenderAssetUsages, RenderAssets},
render_resource::{
binding_types::{sampler, texture_2d, texture_3d, uniform_buffer},
*,
},
renderer::RenderDevice,
texture::{FallbackImage, GpuImage},
view::{ExtractedView, ViewTarget, ViewUniform},
Render, RenderApp, RenderSet,
};
use bitflags::bitflags;
#[cfg(not(feature = "tonemapping_luts"))]
use tracing::error;
mod node;
use bevy_utils::default;
pub use node::TonemappingNode;
const TONEMAPPING_SHADER_HANDLE: Handle<Shader> =
weak_handle!("e239c010-c25c-42a1-b4e8-08818764d667");
const TONEMAPPING_SHARED_SHADER_HANDLE: Handle<Shader> =
weak_handle!("61dbc544-4b30-4ca9-83bd-4751b5cfb1b1");
const TONEMAPPING_LUT_BINDINGS_SHADER_HANDLE: Handle<Shader> =
weak_handle!("d50e3a70-c85e-4725-a81e-72fc83281145");
/// 3D LUT (look up table) textures used for tonemapping
#[derive(Resource, Clone, ExtractResource)]
pub struct TonemappingLuts {
blender_filmic: Handle<Image>,
agx: Handle<Image>,
tony_mc_mapface: Handle<Image>,
}
pub struct TonemappingPlugin;
impl Plugin for TonemappingPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
TONEMAPPING_SHADER_HANDLE,
"tonemapping.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
TONEMAPPING_SHARED_SHADER_HANDLE,
"tonemapping_shared.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
TONEMAPPING_LUT_BINDINGS_SHADER_HANDLE,
"lut_bindings.wgsl",
Shader::from_wgsl
);
if !app.world().is_resource_added::<TonemappingLuts>() {
let mut images = app.world_mut().resource_mut::<Assets<Image>>();
#[cfg(feature = "tonemapping_luts")]
let tonemapping_luts = {
TonemappingLuts {
blender_filmic: images.add(setup_tonemapping_lut_image(
include_bytes!("luts/Blender_-11_12.ktx2"),
ImageType::Extension("ktx2"),
)),
agx: images.add(setup_tonemapping_lut_image(
include_bytes!("luts/AgX-default_contrast.ktx2"),
ImageType::Extension("ktx2"),
)),
tony_mc_mapface: images.add(setup_tonemapping_lut_image(
include_bytes!("luts/tony_mc_mapface.ktx2"),
ImageType::Extension("ktx2"),
)),
}
};
#[cfg(not(feature = "tonemapping_luts"))]
let tonemapping_luts = {
let placeholder = images.add(lut_placeholder());
TonemappingLuts {
blender_filmic: placeholder.clone(),
agx: placeholder.clone(),
tony_mc_mapface: placeholder,
}
};
app.insert_resource(tonemapping_luts);
}
app.add_plugins(ExtractResourcePlugin::<TonemappingLuts>::default());
app.register_type::<Tonemapping>();
app.register_type::<DebandDither>();
app.add_plugins((
ExtractComponentPlugin::<Tonemapping>::default(),
ExtractComponentPlugin::<DebandDither>::default(),
));
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app
.init_resource::<SpecializedRenderPipelines<TonemappingPipeline>>()
.add_systems(
Render,
prepare_view_tonemapping_pipelines.in_set(RenderSet::Prepare),
);
}
fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.init_resource::<TonemappingPipeline>();
}
}
#[derive(Resource)]
pub struct TonemappingPipeline {
texture_bind_group: BindGroupLayout,
sampler: Sampler,
}
/// Optionally enables a tonemapping shader that attempts to map linear input stimulus into a perceptually uniform image for a given [`Camera`] entity.
#[derive(
Component, Debug, Hash, Clone, Copy, Reflect, Default, ExtractComponent, PartialEq, Eq,
)]
#[extract_component_filter(With<Camera>)]
#[reflect(Component, Debug, Hash, Default, PartialEq)]
pub enum Tonemapping {
/// Bypass tonemapping.
None,
/// Suffers from lots hue shifting, brights don't desaturate naturally.
/// Bright primaries and secondaries don't desaturate at all.
Reinhard,
/// Suffers from hue shifting. Brights don't desaturate much at all across the spectrum.
ReinhardLuminance,
/// Same base implementation that Godot 4.0 uses for Tonemap ACES.
/// <https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl>
/// Not neutral, has a very specific aesthetic, intentional and dramatic hue shifting.
/// Bright greens and reds turn orange. Bright blues turn magenta.
/// Significantly increased contrast. Brights desaturate across the spectrum.
AcesFitted,
/// By Troy Sobotka
/// <https://github.com/sobotka/AgX>
/// Very neutral. Image is somewhat desaturated when compared to other tonemappers.
/// Little to no hue shifting. Subtle [Abney shifting](https://en.wikipedia.org/wiki/Abney_effect).
/// NOTE: Requires the `tonemapping_luts` cargo feature.
AgX,
/// By Tomasz Stachowiak
/// Has little hue shifting in the darks and mids, but lots in the brights. Brights desaturate across the spectrum.
/// Is sort of between Reinhard and `ReinhardLuminance`. Conceptually similar to reinhard-jodie.
/// Designed as a compromise if you want e.g. decent skin tones in low light, but can't afford to re-do your
/// VFX to look good without hue shifting.
SomewhatBoringDisplayTransform,
/// Current Bevy default.
/// By Tomasz Stachowiak
/// <https://github.com/h3r2tic/tony-mc-mapface>
/// Very neutral. Subtle but intentional hue shifting. Brights desaturate across the spectrum.
/// Comment from author:
/// Tony is a display transform intended for real-time applications such as games.
/// It is intentionally boring, does not increase contrast or saturation, and stays close to the
/// input stimulus where compression isn't necessary.
/// Brightness-equivalent luminance of the input stimulus is compressed. The non-linearity resembles Reinhard.
/// Color hues are preserved during compression, except for a deliberate [BezoldBrücke shift](https://en.wikipedia.org/wiki/Bezold%E2%80%93Br%C3%BCcke_shift).
/// To avoid posterization, selective desaturation is employed, with care to avoid the [Abney effect](https://en.wikipedia.org/wiki/Abney_effect).
/// NOTE: Requires the `tonemapping_luts` cargo feature.
#[default]
TonyMcMapface,
/// Default Filmic Display Transform from blender.
/// Somewhat neutral. Suffers from hue shifting. Brights desaturate across the spectrum.
/// NOTE: Requires the `tonemapping_luts` cargo feature.
BlenderFilmic,
}
impl Tonemapping {
pub fn is_enabled(&self) -> bool {
*self != Tonemapping::None
}
}
bitflags! {
/// Various flags describing what tonemapping needs to do.
///
/// This allows the shader to skip unneeded steps.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct TonemappingPipelineKeyFlags: u8 {
/// The hue needs to be changed.
const HUE_ROTATE = 0x01;
/// The white balance needs to be adjusted.
const WHITE_BALANCE = 0x02;
/// Saturation/contrast/gamma/gain/lift for one or more sections
/// (shadows, midtones, highlights) need to be adjusted.
const SECTIONAL_COLOR_GRADING = 0x04;
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TonemappingPipelineKey {
deband_dither: DebandDither,
tonemapping: Tonemapping,
flags: TonemappingPipelineKeyFlags,
}
impl SpecializedRenderPipeline for TonemappingPipeline {
type Key = TonemappingPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let mut shader_defs = Vec::new();
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(),
3,
));
shader_defs.push(ShaderDefVal::UInt(
"TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(),
4,
));
if let DebandDither::Enabled = key.deband_dither {
shader_defs.push("DEBAND_DITHER".into());
}
// Define shader flags depending on the color grading options in use.
if key.flags.contains(TonemappingPipelineKeyFlags::HUE_ROTATE) {
shader_defs.push("HUE_ROTATE".into());
}
if key
.flags
.contains(TonemappingPipelineKeyFlags::WHITE_BALANCE)
{
shader_defs.push("WHITE_BALANCE".into());
}
if key
.flags
.contains(TonemappingPipelineKeyFlags::SECTIONAL_COLOR_GRADING)
{
shader_defs.push("SECTIONAL_COLOR_GRADING".into());
}
match key.tonemapping {
Tonemapping::None => shader_defs.push("TONEMAP_METHOD_NONE".into()),
Tonemapping::Reinhard => shader_defs.push("TONEMAP_METHOD_REINHARD".into()),
Tonemapping::ReinhardLuminance => {
shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into());
}
Tonemapping::AcesFitted => shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()),
Tonemapping::AgX => {
#[cfg(not(feature = "tonemapping_luts"))]
error!(
"AgX tonemapping requires the `tonemapping_luts` feature.
Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended),
or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`."
);
shader_defs.push("TONEMAP_METHOD_AGX".into());
}
Tonemapping::SomewhatBoringDisplayTransform => {
shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into());
}
Tonemapping::TonyMcMapface => {
#[cfg(not(feature = "tonemapping_luts"))]
error!(
"TonyMcMapFace tonemapping requires the `tonemapping_luts` feature.
Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended),
or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`."
);
shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into());
}
Tonemapping::BlenderFilmic => {
#[cfg(not(feature = "tonemapping_luts"))]
error!(
"BlenderFilmic tonemapping requires the `tonemapping_luts` feature.
Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended),
or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`."
);
shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into());
}
}
RenderPipelineDescriptor {
label: Some("tonemapping pipeline".into()),
layout: vec![self.texture_bind_group.clone()],
vertex: fullscreen_shader_vertex_state(),
fragment: Some(FragmentState {
shader: TONEMAPPING_SHADER_HANDLE,
shader_defs,
entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState {
format: ViewTarget::TEXTURE_FORMAT_HDR,
blend: None,
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState::default(),
depth_stencil: None,
multisample: MultisampleState::default(),
push_constant_ranges: Vec::new(),
zero_initialize_workgroup_memory: false,
}
}
}
impl FromWorld for TonemappingPipeline {
fn from_world(render_world: &mut World) -> Self {
let mut entries = DynamicBindGroupLayoutEntries::new_with_indices(
ShaderStages::FRAGMENT,
(
(0, uniform_buffer::<ViewUniform>(true)),
(
1,
texture_2d(TextureSampleType::Float { filterable: false }),
),
(2, sampler(SamplerBindingType::NonFiltering)),
),
);
let lut_layout_entries = get_lut_bind_group_layout_entries();
entries =
entries.extend_with_indices(((3, lut_layout_entries[0]), (4, lut_layout_entries[1])));
let render_device = render_world.resource::<RenderDevice>();
let tonemap_texture_bind_group = render_device
.create_bind_group_layout("tonemapping_hdr_texture_bind_group_layout", &entries);
let sampler = render_device.create_sampler(&SamplerDescriptor::default());
TonemappingPipeline {
texture_bind_group: tonemap_texture_bind_group,
sampler,
}
}
}
#[derive(Component)]
pub struct ViewTonemappingPipeline(CachedRenderPipelineId);
pub fn prepare_view_tonemapping_pipelines(
mut commands: Commands,
pipeline_cache: Res<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<TonemappingPipeline>>,
upscaling_pipeline: Res<TonemappingPipeline>,
view_targets: Query<
(
Entity,
&ExtractedView,
Option<&Tonemapping>,
Option<&DebandDither>,
),
With<ViewTarget>,
>,
) {
for (entity, view, tonemapping, dither) in view_targets.iter() {
// As an optimization, we omit parts of the shader that are unneeded.
let mut flags = TonemappingPipelineKeyFlags::empty();
flags.set(
TonemappingPipelineKeyFlags::HUE_ROTATE,
view.color_grading.global.hue != 0.0,
);
flags.set(
TonemappingPipelineKeyFlags::WHITE_BALANCE,
view.color_grading.global.temperature != 0.0 || view.color_grading.global.tint != 0.0,
);
flags.set(
TonemappingPipelineKeyFlags::SECTIONAL_COLOR_GRADING,
view.color_grading
.all_sections()
.any(|section| *section != default()),
);
let key = TonemappingPipelineKey {
deband_dither: *dither.unwrap_or(&DebandDither::Disabled),
tonemapping: *tonemapping.unwrap_or(&Tonemapping::None),
flags,
};
let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key);
commands
.entity(entity)
.insert(ViewTonemappingPipeline(pipeline));
}
}
/// Enables a debanding shader that applies dithering to mitigate color banding in the final image for a given [`Camera`] entity.
#[derive(
Component, Debug, Hash, Clone, Copy, Reflect, Default, ExtractComponent, PartialEq, Eq,
)]
#[extract_component_filter(With<Camera>)]
#[reflect(Component, Debug, Hash, Default, PartialEq)]
pub enum DebandDither {
#[default]
Disabled,
Enabled,
}
pub fn get_lut_bindings<'a>(
images: &'a RenderAssets<GpuImage>,
tonemapping_luts: &'a TonemappingLuts,
tonemapping: &Tonemapping,
fallback_image: &'a FallbackImage,
) -> (&'a TextureView, &'a Sampler) {
let image = match tonemapping {
// AgX lut texture used when tonemapping doesn't need a texture since it's very small (32x32x32)
Tonemapping::None
| Tonemapping::Reinhard
| Tonemapping::ReinhardLuminance
| Tonemapping::AcesFitted
| Tonemapping::AgX
| Tonemapping::SomewhatBoringDisplayTransform => &tonemapping_luts.agx,
Tonemapping::TonyMcMapface => &tonemapping_luts.tony_mc_mapface,
Tonemapping::BlenderFilmic => &tonemapping_luts.blender_filmic,
};
let lut_image = images.get(image).unwrap_or(&fallback_image.d3);
(&lut_image.texture_view, &lut_image.sampler)
}
pub fn get_lut_bind_group_layout_entries() -> [BindGroupLayoutEntryBuilder; 2] {
[
texture_3d(TextureSampleType::Float { filterable: true }),
sampler(SamplerBindingType::Filtering),
]
}
#[expect(clippy::allow_attributes, reason = "`dead_code` is not always linted.")]
#[allow(
dead_code,
reason = "There is unused code when the `tonemapping_luts` feature is disabled."
)]
fn setup_tonemapping_lut_image(bytes: &[u8], image_type: ImageType) -> Image {
let image_sampler = ImageSampler::Descriptor(bevy_image::ImageSamplerDescriptor {
label: Some("Tonemapping LUT sampler".to_string()),
address_mode_u: bevy_image::ImageAddressMode::ClampToEdge,
address_mode_v: bevy_image::ImageAddressMode::ClampToEdge,
address_mode_w: bevy_image::ImageAddressMode::ClampToEdge,
mag_filter: bevy_image::ImageFilterMode::Linear,
min_filter: bevy_image::ImageFilterMode::Linear,
mipmap_filter: bevy_image::ImageFilterMode::Linear,
..default()
});
Image::from_buffer(
#[cfg(all(debug_assertions, feature = "dds"))]
"Tonemapping LUT sampler".to_string(),
bytes,
image_type,
CompressedImageFormats::NONE,
false,
image_sampler,
RenderAssetUsages::RENDER_WORLD,
)
.unwrap()
}
pub fn lut_placeholder() -> Image {
let format = TextureFormat::Rgba8Unorm;
let data = vec![255, 0, 255, 255];
Image {
data: Some(data),
texture_descriptor: TextureDescriptor {
size: Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
format,
dimension: TextureDimension::D3,
label: None,
mip_level_count: 1,
sample_count: 1,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
},
sampler: ImageSampler::Default,
texture_view_descriptor: None,
asset_usage: RenderAssetUsages::RENDER_WORLD,
}
}

View File

@@ -0,0 +1,141 @@
use std::sync::Mutex;
use crate::tonemapping::{TonemappingLuts, TonemappingPipeline, ViewTonemappingPipeline};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
render_asset::RenderAssets,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_resource::{
BindGroup, BindGroupEntries, BufferId, LoadOp, Operations, PipelineCache,
RenderPassColorAttachment, RenderPassDescriptor, StoreOp, TextureViewId,
},
renderer::RenderContext,
texture::{FallbackImage, GpuImage},
view::{ViewTarget, ViewUniformOffset, ViewUniforms},
};
use super::{get_lut_bindings, Tonemapping};
#[derive(Default)]
pub struct TonemappingNode {
cached_bind_group: Mutex<Option<(BufferId, TextureViewId, TextureViewId, BindGroup)>>,
last_tonemapping: Mutex<Option<Tonemapping>>,
}
impl ViewNode for TonemappingNode {
type ViewQuery = (
&'static ViewUniformOffset,
&'static ViewTarget,
&'static ViewTonemappingPipeline,
&'static Tonemapping,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(view_uniform_offset, target, view_tonemapping_pipeline, tonemapping): QueryItem<
Self::ViewQuery,
>,
world: &World,
) -> Result<(), NodeRunError> {
let pipeline_cache = world.resource::<PipelineCache>();
let tonemapping_pipeline = world.resource::<TonemappingPipeline>();
let gpu_images = world.get_resource::<RenderAssets<GpuImage>>().unwrap();
let fallback_image = world.resource::<FallbackImage>();
let view_uniforms_resource = world.resource::<ViewUniforms>();
let view_uniforms = &view_uniforms_resource.uniforms;
let view_uniforms_id = view_uniforms.buffer().unwrap().id();
if *tonemapping == Tonemapping::None {
return Ok(());
}
if !target.is_hdr() {
return Ok(());
}
let Some(pipeline) = pipeline_cache.get_render_pipeline(view_tonemapping_pipeline.0) else {
return Ok(());
};
let post_process = target.post_process_write();
let source = post_process.source;
let destination = post_process.destination;
let mut last_tonemapping = self.last_tonemapping.lock().unwrap();
let tonemapping_changed = if let Some(last_tonemapping) = &*last_tonemapping {
tonemapping != last_tonemapping
} else {
true
};
if tonemapping_changed {
*last_tonemapping = Some(*tonemapping);
}
let mut cached_bind_group = self.cached_bind_group.lock().unwrap();
let bind_group = match &mut *cached_bind_group {
Some((buffer_id, texture_id, lut_id, bind_group))
if view_uniforms_id == *buffer_id
&& source.id() == *texture_id
&& *lut_id != fallback_image.d3.texture_view.id()
&& !tonemapping_changed =>
{
bind_group
}
cached_bind_group => {
let tonemapping_luts = world.resource::<TonemappingLuts>();
let lut_bindings =
get_lut_bindings(gpu_images, tonemapping_luts, tonemapping, fallback_image);
let bind_group = render_context.render_device().create_bind_group(
None,
&tonemapping_pipeline.texture_bind_group,
&BindGroupEntries::sequential((
view_uniforms,
source,
&tonemapping_pipeline.sampler,
lut_bindings.0,
lut_bindings.1,
)),
);
let (_, _, _, bind_group) = cached_bind_group.insert((
view_uniforms_id,
source.id(),
lut_bindings.0.id(),
bind_group,
));
bind_group
}
};
let pass_descriptor = RenderPassDescriptor {
label: Some("tonemapping_pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: destination,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(Default::default()), // TODO shouldn't need to be cleared
store: StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
};
let mut render_pass = render_context
.command_encoder()
.begin_render_pass(&pass_descriptor);
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, bind_group, &[view_uniform_offset.offset]);
render_pass.draw(0..3, 0..1);
Ok(())
}
}

View File

@@ -0,0 +1,34 @@
#define TONEMAPPING_PASS
#import bevy_render::{
view::View,
maths::powsafe,
}
#import bevy_core_pipeline::{
fullscreen_vertex_shader::FullscreenVertexOutput,
tonemapping::{tone_mapping, screen_space_dither},
}
@group(0) @binding(0) var<uniform> view: View;
@group(0) @binding(1) var hdr_texture: texture_2d<f32>;
@group(0) @binding(2) var hdr_sampler: sampler;
@group(0) @binding(3) var dt_lut_texture: texture_3d<f32>;
@group(0) @binding(4) var dt_lut_sampler: sampler;
@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
let hdr_color = textureSample(hdr_texture, hdr_sampler, in.uv);
var output_rgb = tone_mapping(hdr_color, view.color_grading).rgb;
#ifdef DEBAND_DITHER
output_rgb = powsafe(output_rgb.rgb, 1.0 / 2.2);
output_rgb = output_rgb + screen_space_dither(in.position.xy);
// This conversion back to linear space is required because our output texture format is
// SRGB; the GPU will assume our output is linear and will apply an SRGB conversion.
output_rgb = powsafe(output_rgb.rgb, 2.2);
#endif
return vec4<f32>(output_rgb, hdr_color.a);
}

View File

@@ -0,0 +1,405 @@
#define_import_path bevy_core_pipeline::tonemapping
#import bevy_render::{
view::ColorGrading,
color_operations::{hsv_to_rgb, rgb_to_hsv},
maths::{PI_2, powsafe},
}
#import bevy_core_pipeline::tonemapping_lut_bindings::{
dt_lut_texture,
dt_lut_sampler,
}
// Half the size of the crossfade region between shadows and midtones and
// between midtones and highlights. This value, 0.1, corresponds to 10% of the
// gamut on either side of the cutoff point.
const LEVEL_MARGIN: f32 = 0.1;
// The inverse reciprocal of twice the above, used when scaling the midtone
// region.
const LEVEL_MARGIN_DIV: f32 = 0.5 / LEVEL_MARGIN;
fn sample_current_lut(p: vec3<f32>) -> vec3<f32> {
// Don't include code that will try to sample from LUTs if tonemap method doesn't require it
// Allows this file to be imported without necessarily needing the lut texture bindings
#ifdef TONEMAP_METHOD_AGX
return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb;
#else ifdef TONEMAP_METHOD_TONY_MC_MAPFACE
return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb;
#else ifdef TONEMAP_METHOD_BLENDER_FILMIC
return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb;
#else
return vec3(1.0, 0.0, 1.0);
#endif
}
// --------------------------------------
// --- SomewhatBoringDisplayTransform ---
// --------------------------------------
// By Tomasz Stachowiak
fn rgb_to_ycbcr(col: vec3<f32>) -> vec3<f32> {
let m = mat3x3<f32>(
0.2126, 0.7152, 0.0722,
-0.1146, -0.3854, 0.5,
0.5, -0.4542, -0.0458
);
return col * m;
}
fn ycbcr_to_rgb(col: vec3<f32>) -> vec3<f32> {
let m = mat3x3<f32>(
1.0, 0.0, 1.5748,
1.0, -0.1873, -0.4681,
1.0, 1.8556, 0.0
);
return max(vec3(0.0), col * m);
}
fn tonemap_curve(v: f32) -> f32 {
#ifdef 0
// Large linear part in the lows, but compresses highs.
float c = v + v * v + 0.5 * v * v * v;
return c / (1.0 + c);
#else
return 1.0 - exp(-v);
#endif
}
fn tonemap_curve3_(v: vec3<f32>) -> vec3<f32> {
return vec3(tonemap_curve(v.r), tonemap_curve(v.g), tonemap_curve(v.b));
}
fn somewhat_boring_display_transform(col: vec3<f32>) -> vec3<f32> {
var boring_color = col;
let ycbcr = rgb_to_ycbcr(boring_color);
let bt = tonemap_curve(length(ycbcr.yz) * 2.4);
var desat = max((bt - 0.7) * 0.8, 0.0);
desat *= desat;
let desat_col = mix(boring_color.rgb, ycbcr.xxx, desat);
let tm_luma = tonemap_curve(ycbcr.x);
let tm0 = boring_color.rgb * max(0.0, tm_luma / max(1e-5, tonemapping_luminance(boring_color.rgb)));
let final_mult = 0.97;
let tm1 = tonemap_curve3_(desat_col);
boring_color = mix(tm0, tm1, bt * bt);
return boring_color * final_mult;
}
// ------------------------------------------
// ------------- Tony McMapface -------------
// ------------------------------------------
// By Tomasz Stachowiak
// https://github.com/h3r2tic/tony-mc-mapface
const TONY_MC_MAPFACE_LUT_DIMS: f32 = 48.0;
fn sample_tony_mc_mapface_lut(stimulus: vec3<f32>) -> vec3<f32> {
var uv = (stimulus / (stimulus + 1.0)) * (f32(TONY_MC_MAPFACE_LUT_DIMS - 1.0) / f32(TONY_MC_MAPFACE_LUT_DIMS)) + 0.5 / f32(TONY_MC_MAPFACE_LUT_DIMS);
return sample_current_lut(saturate(uv)).rgb;
}
// ---------------------------------
// ---------- ACES Fitted ----------
// ---------------------------------
// Same base implementation that Godot 4.0 uses for Tonemap ACES.
// https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl
// The code in this file was originally written by Stephen Hill (@self_shadow), who deserves all
// credit for coming up with this fit and implementing it. Buy him a beer next time you see him. :)
fn RRTAndODTFit(v: vec3<f32>) -> vec3<f32> {
let a = v * (v + 0.0245786) - 0.000090537;
let b = v * (0.983729 * v + 0.4329510) + 0.238081;
return a / b;
}
fn ACESFitted(color: vec3<f32>) -> vec3<f32> {
var fitted_color = color;
// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT
let rgb_to_rrt = mat3x3<f32>(
vec3(0.59719, 0.35458, 0.04823),
vec3(0.07600, 0.90834, 0.01566),
vec3(0.02840, 0.13383, 0.83777)
);
// ODT_SAT => XYZ => D60_2_D65 => sRGB
let odt_to_rgb = mat3x3<f32>(
vec3(1.60475, -0.53108, -0.07367),
vec3(-0.10208, 1.10813, -0.00605),
vec3(-0.00327, -0.07276, 1.07602)
);
fitted_color *= rgb_to_rrt;
// Apply RRT and ODT
fitted_color = RRTAndODTFit(fitted_color);
fitted_color *= odt_to_rgb;
// Clamp to [0, 1]
fitted_color = saturate(fitted_color);
return fitted_color;
}
// -------------------------------
// ------------- AgX -------------
// -------------------------------
// By Troy Sobotka
// https://github.com/MrLixm/AgXc
// https://github.com/sobotka/AgX
/*
Increase color saturation of the given color data.
:param color: expected sRGB primaries input
:param saturationAmount: expected 0-1 range with 1=neutral, 0=no saturation.
-- ref[2] [4]
*/
fn saturation(color: vec3<f32>, saturationAmount: f32) -> vec3<f32> {
let luma = tonemapping_luminance(color);
return mix(vec3(luma), color, vec3(saturationAmount));
}
/*
Output log domain encoded data.
Similar to OCIO lg2 AllocationTransform.
ref[0]
*/
fn convertOpenDomainToNormalizedLog2_(color: vec3<f32>, minimum_ev: f32, maximum_ev: f32) -> vec3<f32> {
let in_midgray = 0.18;
// remove negative before log transform
var normalized_color = max(vec3(0.0), color);
// avoid infinite issue with log -- ref[1]
normalized_color = select(normalized_color, 0.00001525878 + normalized_color, normalized_color < vec3<f32>(0.00003051757));
normalized_color = clamp(
log2(normalized_color / in_midgray),
vec3(minimum_ev),
vec3(maximum_ev)
);
let total_exposure = maximum_ev - minimum_ev;
return (normalized_color - minimum_ev) / total_exposure;
}
// Inverse of above
fn convertNormalizedLog2ToOpenDomain(color: vec3<f32>, minimum_ev: f32, maximum_ev: f32) -> vec3<f32> {
var open_color = color;
let in_midgray = 0.18;
let total_exposure = maximum_ev - minimum_ev;
open_color = (open_color * total_exposure) + minimum_ev;
open_color = pow(vec3(2.0), open_color);
open_color = open_color * in_midgray;
return open_color;
}
/*=================
Main processes
=================*/
// Prepare the data for display encoding. Converted to log domain.
fn applyAgXLog(Image: vec3<f32>) -> vec3<f32> {
var prepared_image = max(vec3(0.0), Image); // clamp negatives
let r = dot(prepared_image, vec3(0.84247906, 0.0784336, 0.07922375));
let g = dot(prepared_image, vec3(0.04232824, 0.87846864, 0.07916613));
let b = dot(prepared_image, vec3(0.04237565, 0.0784336, 0.87914297));
prepared_image = vec3(r, g, b);
prepared_image = convertOpenDomainToNormalizedLog2_(prepared_image, -10.0, 6.5);
prepared_image = clamp(prepared_image, vec3(0.0), vec3(1.0));
return prepared_image;
}
fn applyLUT3D(Image: vec3<f32>, block_size: f32) -> vec3<f32> {
return sample_current_lut(Image * ((block_size - 1.0) / block_size) + 0.5 / block_size).rgb;
}
// -------------------------
// -------------------------
// -------------------------
fn sample_blender_filmic_lut(stimulus: vec3<f32>) -> vec3<f32> {
let block_size = 64.0;
let normalized = saturate(convertOpenDomainToNormalizedLog2_(stimulus, -11.0, 12.0));
return applyLUT3D(normalized, block_size);
}
// from https://64.github.io/tonemapping/
// reinhard on RGB oversaturates colors
fn tonemapping_reinhard(color: vec3<f32>) -> vec3<f32> {
return color / (1.0 + color);
}
fn tonemapping_reinhard_extended(color: vec3<f32>, max_white: f32) -> vec3<f32> {
let numerator = color * (1.0 + (color / vec3<f32>(max_white * max_white)));
return numerator / (1.0 + color);
}
// luminance coefficients from Rec. 709.
// https://en.wikipedia.org/wiki/Rec._709
fn tonemapping_luminance(v: vec3<f32>) -> f32 {
return dot(v, vec3<f32>(0.2126, 0.7152, 0.0722));
}
fn tonemapping_change_luminance(c_in: vec3<f32>, l_out: f32) -> vec3<f32> {
let l_in = tonemapping_luminance(c_in);
return c_in * (l_out / l_in);
}
fn tonemapping_reinhard_luminance(color: vec3<f32>) -> vec3<f32> {
let l_old = tonemapping_luminance(color);
let l_new = l_old / (1.0 + l_old);
return tonemapping_change_luminance(color, l_new);
}
fn rgb_to_srgb_simple(color: vec3<f32>) -> vec3<f32> {
return pow(color, vec3<f32>(1.0 / 2.2));
}
// Source: Advanced VR Rendering, GDC 2015, Alex Vlachos, Valve, Slide 49
// https://media.steampowered.com/apps/valve/2015/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf
fn screen_space_dither(frag_coord: vec2<f32>) -> vec3<f32> {
var dither = vec3<f32>(dot(vec2<f32>(171.0, 231.0), frag_coord)).xxx;
dither = fract(dither.rgb / vec3<f32>(103.0, 71.0, 97.0));
return (dither - 0.5) / 255.0;
}
// Performs the "sectional" color grading: i.e. the color grading that applies
// individually to shadows, midtones, and highlights.
fn sectional_color_grading(
in: vec3<f32>,
color_grading: ptr<function, ColorGrading>,
) -> vec3<f32> {
var color = in;
// Determine whether the color is a shadow, midtone, or highlight. Colors
// close to the edges are considered a mix of both, to avoid sharp
// discontinuities. The formulas are taken from Blender's compositor.
let level = (color.r + color.g + color.b) / 3.0;
// Determine whether this color is a shadow, midtone, or highlight. If close
// to the cutoff points, blend between the two to avoid sharp color
// discontinuities.
var levels = vec3(0.0);
let midtone_range = (*color_grading).midtone_range;
if (level < midtone_range.x - LEVEL_MARGIN) {
levels.x = 1.0;
} else if (level < midtone_range.x + LEVEL_MARGIN) {
levels.y = ((level - midtone_range.x) * LEVEL_MARGIN_DIV) + 0.5;
levels.z = 1.0 - levels.y;
} else if (level < midtone_range.y - LEVEL_MARGIN) {
levels.y = 1.0;
} else if (level < midtone_range.y + LEVEL_MARGIN) {
levels.z = ((level - midtone_range.y) * LEVEL_MARGIN_DIV) + 0.5;
levels.y = 1.0 - levels.z;
} else {
levels.z = 1.0;
}
// Calculate contrast/saturation/gamma/gain/lift.
let contrast = dot(levels, (*color_grading).contrast);
let saturation = dot(levels, (*color_grading).saturation);
let gamma = dot(levels, (*color_grading).gamma);
let gain = dot(levels, (*color_grading).gain);
let lift = dot(levels, (*color_grading).lift);
// Adjust saturation and contrast.
let luma = tonemapping_luminance(color);
color = luma + saturation * (color - luma);
color = 0.5 + (color - 0.5) * contrast;
// The [ASC CDL] formula for color correction. Given *i*, an input color, we
// have:
//
// out = (i × s + o)ⁿ
//
// Following the normal photographic naming convention, *gain* is the *s*
// factor, *lift* is the *o* term, and the inverse of *gamma* is the *n*
// exponent.
//
// [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function
color = powsafe(color * gain + lift, 1.0 / gamma);
// Account for exposure.
color = color * powsafe(vec3(2.0), (*color_grading).exposure);
return max(color, vec3(0.0));
}
fn tone_mapping(in: vec4<f32>, in_color_grading: ColorGrading) -> vec4<f32> {
var color = max(in.rgb, vec3(0.0));
var color_grading = in_color_grading; // So we can take pointers to it.
// Rotate hue if needed, by converting to and from HSV. Remember that hue is
// an angle, so it needs to be modulo 2π.
#ifdef HUE_ROTATE
var hsv = rgb_to_hsv(color);
hsv.r = (hsv.r + color_grading.hue) % PI_2;
color = hsv_to_rgb(hsv);
#endif
// Perform white balance correction. Conveniently, this is a linear
// transform. The matrix was pre-calculated from the temperature and tint
// values on the CPU.
#ifdef WHITE_BALANCE
color = max(color_grading.balance * color, vec3(0.0));
#endif
// Perform the "sectional" color grading: i.e. the color grading that
// applies individually to shadows, midtones, and highlights.
#ifdef SECTIONAL_COLOR_GRADING
color = sectional_color_grading(color, &color_grading);
#else
// If we're not doing sectional color grading, the exposure might still need
// to be applied, for example when using auto exposure.
color = color * powsafe(vec3(2.0), color_grading.exposure);
#endif
// tone_mapping
#ifdef TONEMAP_METHOD_NONE
color = color;
#else ifdef TONEMAP_METHOD_REINHARD
color = tonemapping_reinhard(color.rgb);
#else ifdef TONEMAP_METHOD_REINHARD_LUMINANCE
color = tonemapping_reinhard_luminance(color.rgb);
#else ifdef TONEMAP_METHOD_ACES_FITTED
color = ACESFitted(color.rgb);
#else ifdef TONEMAP_METHOD_AGX
color = applyAgXLog(color);
color = applyLUT3D(color, 32.0);
#else ifdef TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM
color = somewhat_boring_display_transform(color.rgb);
#else ifdef TONEMAP_METHOD_TONY_MC_MAPFACE
color = sample_tony_mc_mapface_lut(color);
#else ifdef TONEMAP_METHOD_BLENDER_FILMIC
color = sample_blender_filmic_lut(color.rgb);
#endif
// Perceptual post tonemapping grading
color = saturation(color, color_grading.post_saturation);
return vec4(color, in.a);
}
// This is an **incredibly crude** approximation of the inverse of the tone mapping function.
// We assume here that there's a simple linear relationship between the input and output
// which is not true at all, but useful to at least preserve the overall luminance of colors
// when sampling from an already tonemapped image. (e.g. for transmissive materials when HDR is off)
fn approximate_inverse_tone_mapping(in: vec4<f32>, color_grading: ColorGrading) -> vec4<f32> {
let out = tone_mapping(in, color_grading);
let approximate_ratio = length(in.rgb) / length(out.rgb);
return vec4(in.rgb * approximate_ratio, in.a);
}

View File

@@ -0,0 +1,90 @@
use crate::blit::{BlitPipeline, BlitPipelineKey};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_platform::collections::HashSet;
use bevy_render::{
camera::{CameraOutputMode, ExtractedCamera},
render_resource::*,
view::ViewTarget,
Render, RenderApp, RenderSet,
};
mod node;
pub use node::UpscalingNode;
pub struct UpscalingPlugin;
impl Plugin for UpscalingPlugin {
fn build(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.add_systems(
Render,
// This system should probably technically be run *after* all of the other systems
// that might modify `PipelineCache` via interior mutability, but for now,
// we've chosen to simply ignore the ambiguities out of a desire for a better refactor
// and aversion to extensive and intrusive system ordering.
// See https://github.com/bevyengine/bevy/issues/14770 for more context.
prepare_view_upscaling_pipelines
.in_set(RenderSet::Prepare)
.ambiguous_with_all(),
);
}
}
}
#[derive(Component)]
pub struct ViewUpscalingPipeline(CachedRenderPipelineId);
fn prepare_view_upscaling_pipelines(
mut commands: Commands,
mut pipeline_cache: ResMut<PipelineCache>,
mut pipelines: ResMut<SpecializedRenderPipelines<BlitPipeline>>,
blit_pipeline: Res<BlitPipeline>,
view_targets: Query<(Entity, &ViewTarget, Option<&ExtractedCamera>)>,
) {
let mut output_textures = <HashSet<_>>::default();
for (entity, view_target, camera) in view_targets.iter() {
let out_texture_id = view_target.out_texture().id();
let blend_state = if let Some(extracted_camera) = camera {
match extracted_camera.output_mode {
CameraOutputMode::Skip => None,
CameraOutputMode::Write { blend_state, .. } => {
let already_seen = output_textures.contains(&out_texture_id);
output_textures.insert(out_texture_id);
match blend_state {
None => {
// If we've already seen this output for a camera and it doesn't have an output blend
// mode configured, default to alpha blend so that we don't accidentally overwrite
// the output texture
if already_seen {
Some(BlendState::ALPHA_BLENDING)
} else {
None
}
}
_ => blend_state,
}
}
}
} else {
output_textures.insert(out_texture_id);
None
};
let key = BlitPipelineKey {
texture_format: view_target.out_texture_format(),
blend_state,
samples: 1,
};
let pipeline = pipelines.specialize(&pipeline_cache, &blit_pipeline, key);
// Ensure the pipeline is loaded before continuing the frame to prevent frames without any GPU work submitted
pipeline_cache.block_on_render_pipeline(pipeline);
commands
.entity(entity)
.insert(ViewUpscalingPipeline(pipeline));
}
}

View File

@@ -0,0 +1,100 @@
use crate::{blit::BlitPipeline, upscaling::ViewUpscalingPipeline};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::{
camera::{CameraOutputMode, ClearColor, ClearColorConfig, ExtractedCamera},
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_resource::{
BindGroup, BindGroupEntries, PipelineCache, RenderPassDescriptor, TextureViewId,
},
renderer::RenderContext,
view::ViewTarget,
};
use std::sync::Mutex;
#[derive(Default)]
pub struct UpscalingNode {
cached_texture_bind_group: Mutex<Option<(TextureViewId, BindGroup)>>,
}
impl ViewNode for UpscalingNode {
type ViewQuery = (
&'static ViewTarget,
&'static ViewUpscalingPipeline,
Option<&'static ExtractedCamera>,
);
fn run(
&self,
_graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(target, upscaling_target, camera): QueryItem<Self::ViewQuery>,
world: &World,
) -> Result<(), NodeRunError> {
let pipeline_cache = world.get_resource::<PipelineCache>().unwrap();
let blit_pipeline = world.get_resource::<BlitPipeline>().unwrap();
let clear_color_global = world.get_resource::<ClearColor>().unwrap();
let clear_color = if let Some(camera) = camera {
match camera.output_mode {
CameraOutputMode::Write { clear_color, .. } => clear_color,
CameraOutputMode::Skip => return Ok(()),
}
} else {
ClearColorConfig::Default
};
let clear_color = match clear_color {
ClearColorConfig::Default => Some(clear_color_global.0),
ClearColorConfig::Custom(color) => Some(color),
ClearColorConfig::None => None,
};
let converted_clear_color = clear_color.map(Into::into);
let upscaled_texture = target.main_texture_view();
let mut cached_bind_group = self.cached_texture_bind_group.lock().unwrap();
let bind_group = match &mut *cached_bind_group {
Some((id, bind_group)) if upscaled_texture.id() == *id => bind_group,
cached_bind_group => {
let bind_group = render_context.render_device().create_bind_group(
None,
&blit_pipeline.texture_bind_group,
&BindGroupEntries::sequential((upscaled_texture, &blit_pipeline.sampler)),
);
let (_, bind_group) = cached_bind_group.insert((upscaled_texture.id(), bind_group));
bind_group
}
};
let Some(pipeline) = pipeline_cache.get_render_pipeline(upscaling_target.0) else {
return Ok(());
};
let pass_descriptor = RenderPassDescriptor {
label: Some("upscaling_pass"),
color_attachments: &[Some(
target.out_texture_color_attachment(converted_clear_color),
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
};
let mut render_pass = render_context
.command_encoder()
.begin_render_pass(&pass_descriptor);
if let Some(camera) = camera {
if let Some(viewport) = &camera.viewport {
let size = viewport.physical_size;
let position = viewport.physical_position;
render_pass.set_scissor_rect(position.x, position.y, size.x, size.y);
}
}
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..3, 0..1);
Ok(())
}
}