mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
Add Order Independent Transparency (#14876)
# Objective - Alpha blending can easily fail in many situations and requires sorting on the cpu ## Solution - Implement order independent transparency (OIT) as an alternative to alpha blending - The implementation uses 2 passes - The first pass records all the fragments colors and position to a buffer that is the size of N layers * the render target resolution. - The second pass sorts the fragments, blends them and draws them to the screen. It also currently does manual depth testing because early-z fails in too many cases in the first pass. ## Testing - We've been using this implementation at foresight in production for many months now and we haven't had any issues related to OIT. --- ## Showcase ![image](https://github.com/user-attachments/assets/157f3e32-adaf-4782-b25b-c10313b9bc43) ![image](https://github.com/user-attachments/assets/bef23258-0c22-4b67-a0b8-48a9f571c44f) ## Future work - Add an example showing how to use OIT for a custom material - Next step would be to implement a per-pixel linked list to reduce memory use - I'd also like to investigate using a BinnedRenderPhase instead of a SortedRenderPhase. If it works, it would make the transparent pass significantly faster. --------- Co-authored-by: Kristoffer Søholm <k.soeholm@gmail.com> Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com> Co-authored-by: Charlotte McElwain <charlotte.c.mcelwain@gmail.com>
This commit is contained in:
parent
e7b83acadc
commit
4bf647ff3b
15 changed files with 1090 additions and 26 deletions
11
Cargo.toml
11
Cargo.toml
|
@ -973,6 +973,17 @@ description = "Demonstrates per-pixel motion blur"
|
|||
category = "3D Rendering"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "order_independent_transparency"
|
||||
path = "examples/3d/order_independent_transparency.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.order_independent_transparency]
|
||||
name = "Order Independent Transparency"
|
||||
description = "Demonstrates how to use OIT"
|
||||
category = "3D Rendering"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "tonemapping"
|
||||
path = "examples/3d/tonemapping.rs"
|
||||
|
|
|
@ -34,6 +34,7 @@ bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
|
|||
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.15.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
|
||||
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
bitflags = "2.3"
|
||||
|
|
|
@ -19,6 +19,7 @@ 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;
|
||||
|
@ -75,6 +76,8 @@ use crate::{
|
|||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::load_internal_asset;
|
||||
use bevy_render::prelude::Shader;
|
||||
#[cfg(not(feature = "webgl"))]
|
||||
use oit::OrderIndependentTransparencyPlugin;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CorePipelinePlugin;
|
||||
|
@ -107,6 +110,9 @@ impl Plugin for CorePipelinePlugin {
|
|||
DepthOfFieldPlugin,
|
||||
SmaaPlugin,
|
||||
PostProcessingPlugin,
|
||||
// DownlevelFlags::FRAGMENT_WRITABLE_STORAGE is required for OIT
|
||||
#[cfg(not(feature = "webgl"))]
|
||||
OrderIndependentTransparencyPlugin,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
283
crates/bevy_core_pipeline/src/oit/mod.rs
Normal file
283
crates/bevy_core_pipeline/src/oit/mod.rs
Normal file
|
@ -0,0 +1,283 @@
|
|||
//! Order Independent Transparency (OIT) for 3d rendering. See [`OrderIndependentTransparencyPlugin`] for more details.
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_math::UVec2;
|
||||
use bevy_render::{
|
||||
camera::{Camera, ExtractedCamera},
|
||||
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
||||
render_graph::{RenderGraphApp, ViewNodeRunner},
|
||||
render_resource::{BufferUsages, BufferVec, DynamicUniformBuffer, Shader, TextureUsages},
|
||||
renderer::{RenderDevice, RenderQueue},
|
||||
view::Msaa,
|
||||
Render, RenderApp, RenderSet,
|
||||
};
|
||||
use bevy_utils::{tracing::trace, HashSet, Instant};
|
||||
use bevy_window::PrimaryWindow;
|
||||
use resolve::{
|
||||
node::{OitResolveNode, OitResolvePass},
|
||||
OitResolvePlugin,
|
||||
};
|
||||
|
||||
use crate::core_3d::{
|
||||
graph::{Core3d, Node3d},
|
||||
Camera3d,
|
||||
};
|
||||
|
||||
/// Module that defines the necesasry 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> = Handle::weak_from_u128(4042527984320512);
|
||||
|
||||
/// 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.
|
||||
#[derive(Component, Clone, Copy, ExtractComponent)]
|
||||
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 16 is probably not worth it in the vast majority of cases
|
||||
pub layer_count: u8,
|
||||
}
|
||||
|
||||
impl Default for OrderIndependentTransparencySettings {
|
||||
fn default() -> Self {
|
||||
Self { layer_count: 8 }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
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.get_single().ok();
|
||||
let mut render_target_has_oit = HashSet::new();
|
||||
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 camaera 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 layers_count_uniforms: DynamicUniformBuffer<i32>,
|
||||
}
|
||||
|
||||
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 layers_count_uniforms = DynamicUniformBuffer::default();
|
||||
layers_count_uniforms.set_label(Some("oit_layers_count"));
|
||||
|
||||
Self {
|
||||
layers,
|
||||
layer_ids,
|
||||
layers_count_uniforms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct OitLayersCountOffset {
|
||||
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.
|
||||
#[allow(clippy::type_complexity)]
|
||||
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.layers_count_uniforms.get_writer(
|
||||
camera_oit_uniforms.iter().len(),
|
||||
&render_device,
|
||||
&render_queue,
|
||||
) {
|
||||
for (entity, settings) in &camera_oit_uniforms {
|
||||
let offset = writer.write(&(settings.layer_count as i32));
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(OitLayersCountOffset { offset });
|
||||
}
|
||||
}
|
||||
}
|
44
crates/bevy_core_pipeline/src/oit/oit_draw.wgsl
Normal file
44
crates/bevy_core_pipeline/src/oit/oit_draw.wgsl
Normal file
|
@ -0,0 +1,44 @@
|
|||
#define_import_path bevy_core_pipeline::oit
|
||||
|
||||
#import bevy_pbr::mesh_view_bindings::{view, oit_layers, oit_layer_ids, oit_layers_count}
|
||||
|
||||
#ifdef OIT_ENABLED
|
||||
// Add the fragment to the oit buffer
|
||||
fn oit_draw(position: vec4f, color: vec4f) -> vec4f {
|
||||
// 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_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_layers_count);
|
||||
// TODO for tail blending we should return the color here
|
||||
discard;
|
||||
}
|
||||
|
||||
// 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);
|
||||
discard;
|
||||
}
|
||||
#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));
|
||||
}
|
212
crates/bevy_core_pipeline/src/oit/resolve/mod.rs
Normal file
212
crates/bevy_core_pipeline/src/oit/resolve/mod.rs
Normal file
|
@ -0,0 +1,212 @@
|
|||
use bevy_app::Plugin;
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_derive::Deref;
|
||||
use bevy_ecs::{
|
||||
entity::{EntityHashMap, EntityHashSet},
|
||||
prelude::*,
|
||||
};
|
||||
use bevy_render::{
|
||||
render_resource::{
|
||||
binding_types::{storage_buffer_sized, texture_depth_2d, uniform_buffer},
|
||||
BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, BlendComponent,
|
||||
BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites, FragmentState,
|
||||
MultisampleState, PipelineCache, PrimitiveState, RenderPipelineDescriptor, Shader,
|
||||
ShaderStages, TextureFormat,
|
||||
},
|
||||
renderer::RenderDevice,
|
||||
texture::BevyDefault,
|
||||
view::{ExtractedView, ViewTarget, ViewUniform, ViewUniforms},
|
||||
Render, RenderApp, RenderSet,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
|
||||
oit::OrderIndependentTransparencySettings,
|
||||
};
|
||||
|
||||
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> = Handle::weak_from_u128(7698420424769536);
|
||||
|
||||
/// Contains the render node used to run the resolve pass.
|
||||
pub mod node;
|
||||
|
||||
/// 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
|
||||
);
|
||||
|
||||
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
||||
return;
|
||||
};
|
||||
|
||||
render_app.add_systems(
|
||||
Render,
|
||||
(
|
||||
queue_oit_resolve_pipeline.in_set(RenderSet::Queue),
|
||||
prepare_oit_resolve_bind_group.in_set(RenderSet::PrepareBindGroups),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut bevy_app::App) {
|
||||
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
||||
return;
|
||||
};
|
||||
|
||||
render_app.init_resource::<OitResolvePipeline>();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn queue_oit_resolve_pipeline(
|
||||
mut commands: Commands,
|
||||
pipeline_cache: Res<PipelineCache>,
|
||||
resolve_pipeline: Res<OitResolvePipeline>,
|
||||
views: Query<(Entity, &ExtractedView), 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) in &views {
|
||||
current_view_entities.insert(e);
|
||||
let key = OitResolvePipelineKey { hdr: view.hdr };
|
||||
|
||||
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![],
|
||||
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![],
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
78
crates/bevy_core_pipeline/src/oit/resolve/node.rs
Normal file
78
crates/bevy_core_pipeline/src/oit/resolve/node.rs
Normal 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(())
|
||||
}
|
||||
}
|
107
crates/bevy_core_pipeline/src/oit/resolve/oit_resolve.wgsl
Normal file
107
crates/bevy_core_pipeline/src/oit/resolve/oit_resolve.wgsl
Normal file
|
@ -0,0 +1,107 @@
|
|||
#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
|
||||
// TODO don't hardcode size
|
||||
var<private> fragment_list: array<OitFragment, 32>;
|
||||
|
||||
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);
|
||||
discard;
|
||||
} else {
|
||||
let result = sort(screen_index, buffer_size);
|
||||
reset_indices(screen_index);
|
||||
|
||||
// Manually do depth testing.
|
||||
// This is necessary because early z doesn't seem to trigger in the transparent pass.
|
||||
// Once we have a per pixel linked list it should be done much earlier
|
||||
let d = textureLoad(depth, vec2<i32>(in.position.xy), 0);
|
||||
if d > result.depth {
|
||||
discard;
|
||||
}
|
||||
|
||||
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) -> 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) {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
|
@ -10,6 +10,7 @@ use bevy_core_pipeline::{
|
|||
AlphaMask3d, Camera3d, Opaque3d, Opaque3dBinKey, ScreenSpaceTransmissionQuality,
|
||||
Transmissive3d, Transparent3d,
|
||||
},
|
||||
oit::OrderIndependentTransparencySettings,
|
||||
prepass::{
|
||||
DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass, OpaqueNoLightmap3dBinKey,
|
||||
},
|
||||
|
@ -620,6 +621,7 @@ pub fn queue_material_meshes<M: Material>(
|
|||
Has<RenderViewLightProbes<EnvironmentMapLight>>,
|
||||
Has<RenderViewLightProbes<IrradianceVolume>>,
|
||||
),
|
||||
Has<OrderIndependentTransparencySettings>,
|
||||
)>,
|
||||
) where
|
||||
M::Data: PartialEq + Eq + Hash + Clone,
|
||||
|
@ -638,6 +640,7 @@ pub fn queue_material_meshes<M: Material>(
|
|||
temporal_jitter,
|
||||
projection,
|
||||
(has_environment_maps, has_irradiance_volumes),
|
||||
has_oit,
|
||||
) in &views
|
||||
{
|
||||
let (
|
||||
|
@ -691,6 +694,10 @@ pub fn queue_material_meshes<M: Material>(
|
|||
view_key |= MeshPipelineKey::IRRADIANCE_VOLUME;
|
||||
}
|
||||
|
||||
if has_oit {
|
||||
view_key |= MeshPipelineKey::OIT_ENABLED;
|
||||
}
|
||||
|
||||
if let Some(projection) = projection {
|
||||
view_key |= match projection {
|
||||
Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE,
|
||||
|
|
|
@ -5,6 +5,7 @@ use bevy_asset::{load_internal_asset, AssetId};
|
|||
use bevy_core_pipeline::{
|
||||
core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT},
|
||||
deferred::{AlphaMask3dDeferred, Opaque3dDeferred},
|
||||
oit::{prepare_oit_buffers, OitLayersCountOffset},
|
||||
prepass::MotionVectorPrepass,
|
||||
};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
|
@ -47,6 +48,7 @@ use bevy_utils::{
|
|||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use nonmax::{NonMaxU16, NonMaxU32};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use static_assertions::const_assert_eq;
|
||||
|
||||
use crate::{
|
||||
|
@ -167,7 +169,9 @@ impl Plugin for MeshRenderPlugin {
|
|||
prepare_skins.in_set(RenderSet::PrepareResources),
|
||||
prepare_morphs.in_set(RenderSet::PrepareResources),
|
||||
prepare_mesh_bind_group.in_set(RenderSet::PrepareBindGroups),
|
||||
prepare_mesh_view_bind_groups.in_set(RenderSet::PrepareBindGroups),
|
||||
prepare_mesh_view_bind_groups
|
||||
.in_set(RenderSet::PrepareBindGroups)
|
||||
.after(prepare_oit_buffers),
|
||||
no_gpu_preprocessing::clear_batched_cpu_instance_buffers::<MeshPipeline>
|
||||
.in_set(RenderSet::Cleanup)
|
||||
.after(RenderSet::Render),
|
||||
|
@ -1490,6 +1494,7 @@ bitflags::bitflags! {
|
|||
const SCREEN_SPACE_REFLECTIONS = 1 << 16;
|
||||
const HAS_PREVIOUS_SKIN = 1 << 17;
|
||||
const HAS_PREVIOUS_MORPH = 1 << 18;
|
||||
const OIT_ENABLED = 1 << 18;
|
||||
const LAST_FLAG = Self::HAS_PREVIOUS_MORPH.bits();
|
||||
|
||||
// Bitfields
|
||||
|
@ -1507,8 +1512,8 @@ bitflags::bitflags! {
|
|||
const TONEMAP_METHOD_ACES_FITTED = 3 << Self::TONEMAP_METHOD_SHIFT_BITS;
|
||||
const TONEMAP_METHOD_AGX = 4 << Self::TONEMAP_METHOD_SHIFT_BITS;
|
||||
const TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM = 5 << Self::TONEMAP_METHOD_SHIFT_BITS;
|
||||
const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS;
|
||||
const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS;
|
||||
const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS;
|
||||
const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS;
|
||||
const SHADOW_FILTER_METHOD_RESERVED_BITS = Self::SHADOW_FILTER_METHOD_MASK_BITS << Self::SHADOW_FILTER_METHOD_SHIFT_BITS;
|
||||
const SHADOW_FILTER_METHOD_HARDWARE_2X2 = 0 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS;
|
||||
const SHADOW_FILTER_METHOD_GAUSSIAN = 1 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS;
|
||||
|
@ -1519,10 +1524,10 @@ bitflags::bitflags! {
|
|||
const VIEW_PROJECTION_ORTHOGRAPHIC = 2 << Self::VIEW_PROJECTION_SHIFT_BITS;
|
||||
const VIEW_PROJECTION_RESERVED = 3 << Self::VIEW_PROJECTION_SHIFT_BITS;
|
||||
const SCREEN_SPACE_SPECULAR_TRANSMISSION_RESERVED_BITS = Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_MASK_BITS << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS;
|
||||
const SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW = 0 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS;
|
||||
const SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW = 0 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS;
|
||||
const SCREEN_SPACE_SPECULAR_TRANSMISSION_MEDIUM = 1 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS;
|
||||
const SCREEN_SPACE_SPECULAR_TRANSMISSION_HIGH = 2 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS;
|
||||
const SCREEN_SPACE_SPECULAR_TRANSMISSION_ULTRA = 3 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS;
|
||||
const SCREEN_SPACE_SPECULAR_TRANSMISSION_HIGH = 2 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS;
|
||||
const SCREEN_SPACE_SPECULAR_TRANSMISSION_ULTRA = 3 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS;
|
||||
const ALL_RESERVED_BITS =
|
||||
Self::BLEND_RESERVED_BITS.bits() |
|
||||
Self::MSAA_RESERVED_BITS.bits() |
|
||||
|
@ -1751,7 +1756,15 @@ impl SpecializedMeshPipeline for MeshPipeline {
|
|||
let (label, blend, depth_write_enabled);
|
||||
let pass = key.intersection(MeshPipelineKey::BLEND_RESERVED_BITS);
|
||||
let (mut is_opaque, mut alpha_to_coverage_enabled) = (false, false);
|
||||
if pass == MeshPipelineKey::BLEND_ALPHA {
|
||||
if key.contains(MeshPipelineKey::OIT_ENABLED) && pass == MeshPipelineKey::BLEND_ALPHA {
|
||||
label = "oit_mesh_pipeline".into();
|
||||
// TODO tail blending would need alpha blending
|
||||
blend = None;
|
||||
shader_defs.push("OIT_ENABLED".into());
|
||||
// TODO it should be possible to use this to combine MSAA and OIT
|
||||
// alpha_to_coverage_enabled = true;
|
||||
depth_write_enabled = false;
|
||||
} else if pass == MeshPipelineKey::BLEND_ALPHA {
|
||||
label = "alpha_blend_mesh_pipeline".into();
|
||||
blend = Some(BlendState::ALPHA_BLENDING);
|
||||
// For the transparent pass, fragments that are closer will be alpha blended
|
||||
|
@ -2179,6 +2192,7 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshViewBindGroup<I>
|
|||
Read<ViewScreenSpaceReflectionsUniformOffset>,
|
||||
Read<ViewEnvironmentMapUniformOffset>,
|
||||
Read<MeshViewBindGroup>,
|
||||
Option<Read<OitLayersCountOffset>>,
|
||||
);
|
||||
type ItemQuery = ();
|
||||
|
||||
|
@ -2193,23 +2207,24 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetMeshViewBindGroup<I>
|
|||
view_ssr,
|
||||
view_environment_map,
|
||||
mesh_view_bind_group,
|
||||
maybe_oit_layers_count_offset,
|
||||
): ROQueryItem<'w, Self::ViewQuery>,
|
||||
_entity: Option<()>,
|
||||
_: SystemParamItem<'w, '_, Self::Param>,
|
||||
pass: &mut TrackedRenderPass<'w>,
|
||||
) -> RenderCommandResult {
|
||||
pass.set_bind_group(
|
||||
I,
|
||||
&mesh_view_bind_group.value,
|
||||
&[
|
||||
view_uniform.offset,
|
||||
view_lights.offset,
|
||||
view_fog.offset,
|
||||
**view_light_probes,
|
||||
**view_ssr,
|
||||
**view_environment_map,
|
||||
],
|
||||
);
|
||||
let mut offsets: SmallVec<[u32; 8]> = smallvec![
|
||||
view_uniform.offset,
|
||||
view_lights.offset,
|
||||
view_fog.offset,
|
||||
**view_light_probes,
|
||||
**view_ssr,
|
||||
**view_environment_map,
|
||||
];
|
||||
if let Some(layers_count_offset) = maybe_oit_layers_count_offset {
|
||||
offsets.push(layers_count_offset.offset);
|
||||
}
|
||||
pass.set_bind_group(I, &mesh_view_bind_group.value, &offsets);
|
||||
|
||||
RenderCommandResult::Success
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ use core::{array, num::NonZero};
|
|||
|
||||
use bevy_core_pipeline::{
|
||||
core_3d::ViewTransmissionTexture,
|
||||
oit::{OitBuffers, OrderIndependentTransparencySettings},
|
||||
prepass::ViewPrepassTextures,
|
||||
tonemapping::{
|
||||
get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts,
|
||||
|
@ -12,6 +13,7 @@ use bevy_derive::{Deref, DerefMut};
|
|||
use bevy_ecs::{
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
query::Has,
|
||||
system::{Commands, Query, Res, Resource},
|
||||
world::{FromWorld, World},
|
||||
};
|
||||
|
@ -71,6 +73,7 @@ bitflags::bitflags! {
|
|||
const NORMAL_PREPASS = 1 << 2;
|
||||
const MOTION_VECTOR_PREPASS = 1 << 3;
|
||||
const DEFERRED_PREPASS = 1 << 4;
|
||||
const OIT_ENABLED = 1 << 5;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,7 +86,7 @@ impl MeshPipelineViewLayoutKey {
|
|||
use MeshPipelineViewLayoutKey as Key;
|
||||
|
||||
format!(
|
||||
"mesh_view_layout{}{}{}{}{}",
|
||||
"mesh_view_layout{}{}{}{}{}{}",
|
||||
self.contains(Key::MULTISAMPLED)
|
||||
.then_some("_multisampled")
|
||||
.unwrap_or_default(),
|
||||
|
@ -99,6 +102,9 @@ impl MeshPipelineViewLayoutKey {
|
|||
self.contains(Key::DEFERRED_PREPASS)
|
||||
.then_some("_deferred")
|
||||
.unwrap_or_default(),
|
||||
self.contains(Key::OIT_ENABLED)
|
||||
.then_some("_oit")
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -122,6 +128,9 @@ impl From<MeshPipelineKey> for MeshPipelineViewLayoutKey {
|
|||
if value.contains(MeshPipelineKey::DEFERRED_PREPASS) {
|
||||
result |= MeshPipelineViewLayoutKey::DEFERRED_PREPASS;
|
||||
}
|
||||
if value.contains(MeshPipelineKey::OIT_ENABLED) {
|
||||
result |= MeshPipelineViewLayoutKey::OIT_ENABLED;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
@ -348,6 +357,18 @@ fn layout_entries(
|
|||
(30, sampler(SamplerBindingType::Filtering)),
|
||||
));
|
||||
|
||||
// OIT
|
||||
if cfg!(not(feature = "webgl")) && layout_key.contains(MeshPipelineViewLayoutKey::OIT_ENABLED) {
|
||||
entries = entries.extend_with_indices((
|
||||
// oit_layers
|
||||
(31, storage_buffer_sized(false, None)),
|
||||
// oit_layer_ids,
|
||||
(32, storage_buffer_sized(false, None)),
|
||||
// oit_layer_count
|
||||
(33, uniform_buffer::<i32>(true)),
|
||||
));
|
||||
}
|
||||
|
||||
entries.to_vec()
|
||||
}
|
||||
|
||||
|
@ -453,8 +474,7 @@ pub fn prepare_mesh_view_bind_groups(
|
|||
render_device: Res<RenderDevice>,
|
||||
mesh_pipeline: Res<MeshPipeline>,
|
||||
shadow_samplers: Res<ShadowSamplers>,
|
||||
light_meta: Res<LightMeta>,
|
||||
global_light_meta: Res<GlobalClusterableObjectMeta>,
|
||||
(light_meta, global_light_meta): (Res<LightMeta>, Res<GlobalClusterableObjectMeta>),
|
||||
fog_meta: Res<FogMeta>,
|
||||
(view_uniforms, environment_map_uniform): (Res<ViewUniforms>, Res<EnvironmentMapUniformBuffer>),
|
||||
views: Query<(
|
||||
|
@ -468,6 +488,7 @@ pub fn prepare_mesh_view_bind_groups(
|
|||
&Tonemapping,
|
||||
Option<&RenderViewLightProbes<EnvironmentMapLight>>,
|
||||
Option<&RenderViewLightProbes<IrradianceVolume>>,
|
||||
Has<OrderIndependentTransparencySettings>,
|
||||
)>,
|
||||
(images, mut fallback_images, fallback_image, fallback_image_zero): (
|
||||
Res<RenderAssets<GpuImage>>,
|
||||
|
@ -480,6 +501,7 @@ pub fn prepare_mesh_view_bind_groups(
|
|||
light_probes_buffer: Res<LightProbesBuffer>,
|
||||
visibility_ranges: Res<RenderVisibilityRanges>,
|
||||
ssr_buffer: Res<ScreenSpaceReflectionsBuffer>,
|
||||
oit_buffers: Res<OitBuffers>,
|
||||
) {
|
||||
if let (
|
||||
Some(view_binding),
|
||||
|
@ -513,6 +535,7 @@ pub fn prepare_mesh_view_bind_groups(
|
|||
tonemapping,
|
||||
render_view_environment_maps,
|
||||
render_view_irradiance_volumes,
|
||||
has_oit,
|
||||
) in &views
|
||||
{
|
||||
let fallback_ssao = fallback_images
|
||||
|
@ -523,10 +546,13 @@ pub fn prepare_mesh_view_bind_groups(
|
|||
.map(|t| &t.screen_space_ambient_occlusion_texture.default_view)
|
||||
.unwrap_or(&fallback_ssao);
|
||||
|
||||
let layout = &mesh_pipeline.get_view_layout(
|
||||
MeshPipelineViewLayoutKey::from(*msaa)
|
||||
| MeshPipelineViewLayoutKey::from(prepass_textures),
|
||||
);
|
||||
let mut layout_key = MeshPipelineViewLayoutKey::from(*msaa)
|
||||
| MeshPipelineViewLayoutKey::from(prepass_textures);
|
||||
if has_oit {
|
||||
layout_key |= MeshPipelineViewLayoutKey::OIT_ENABLED;
|
||||
}
|
||||
|
||||
let layout = &mesh_pipeline.get_view_layout(layout_key);
|
||||
|
||||
let mut entries = DynamicBindGroupEntries::new_with_indices((
|
||||
(0, view_binding.clone()),
|
||||
|
@ -645,6 +671,24 @@ pub fn prepare_mesh_view_bind_groups(
|
|||
entries =
|
||||
entries.extend_with_indices(((29, transmission_view), (30, transmission_sampler)));
|
||||
|
||||
if has_oit {
|
||||
if let (
|
||||
Some(oit_layers_binding),
|
||||
Some(oit_layer_ids_binding),
|
||||
Some(oit_layers_count_uniforms_binding),
|
||||
) = (
|
||||
oit_buffers.layers.binding(),
|
||||
oit_buffers.layer_ids.binding(),
|
||||
oit_buffers.layers_count_uniforms.binding(),
|
||||
) {
|
||||
entries = entries.extend_with_indices((
|
||||
(31, oit_layers_binding.clone()),
|
||||
(32, oit_layer_ids_binding.clone()),
|
||||
(33, oit_layers_count_uniforms_binding.clone()),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
commands.entity(entity).insert(MeshViewBindGroup {
|
||||
value: render_device.create_bind_group("mesh_view_bind_group", layout, &entries),
|
||||
});
|
||||
|
|
|
@ -101,3 +101,9 @@ const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u;
|
|||
|
||||
@group(0) @binding(29) var view_transmission_texture: texture_2d<f32>;
|
||||
@group(0) @binding(30) var view_transmission_sampler: sampler;
|
||||
|
||||
#ifdef OIT_ENABLED
|
||||
@group(0) @binding(31) var<storage, read_write> oit_layers: array<vec2<u32>>;
|
||||
@group(0) @binding(32) var<storage, read_write> oit_layer_ids: array<atomic<i32>>;
|
||||
@group(0) @binding(33) var<uniform> oit_layers_count: i32;
|
||||
#endif OIT_ENABLED
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#import bevy_pbr::{
|
||||
pbr_types,
|
||||
pbr_functions::alpha_discard,
|
||||
pbr_fragment::pbr_input_from_standard_material,
|
||||
}
|
||||
|
@ -21,6 +22,10 @@
|
|||
#import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output
|
||||
#endif
|
||||
|
||||
#ifdef OIT_ENABLED
|
||||
#import bevy_core_pipeline::oit::oit_draw
|
||||
#endif // OIT_ENABLED
|
||||
|
||||
@fragment
|
||||
fn fragment(
|
||||
#ifdef MESHLET_MESH_MATERIAL_PASS
|
||||
|
@ -65,5 +70,13 @@ fn fragment(
|
|||
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
|
||||
#endif
|
||||
|
||||
#ifdef OIT_ENABLED
|
||||
let alpha_mode = pbr_input.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS;
|
||||
if alpha_mode != pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE {
|
||||
// This will always return 0.0. The fragments will only be drawn during the oit resolve pass.
|
||||
out.color = oit_draw(in.position, out.color);
|
||||
}
|
||||
#endif // OIT_ENABLED
|
||||
|
||||
return out;
|
||||
}
|
||||
|
|
236
examples/3d/order_independent_transparency.rs
Normal file
236
examples/3d/order_independent_transparency.rs
Normal file
|
@ -0,0 +1,236 @@
|
|||
//! A simple 3D scene showing how alpha blending can break and how order independent transparency (OIT) can fix it.
|
||||
//!
|
||||
//! See [`OrderIndependentTransparencyPlugin`] for the trade-offs of using OIT.
|
||||
//!
|
||||
//! [`OrderIndependentTransparencyPlugin`]: bevy::render::pipeline::OrderIndependentTransparencyPlugin
|
||||
use bevy::{
|
||||
color::palettes::css::{BLUE, GREEN, RED},
|
||||
core_pipeline::oit::OrderIndependentTransparencySettings,
|
||||
prelude::*,
|
||||
render::view::RenderLayers,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, (toggle_oit, cycle_scenes))
|
||||
.run();
|
||||
}
|
||||
|
||||
/// set up a simple 3D scene
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
// camera
|
||||
commands
|
||||
.spawn((
|
||||
Camera3d::default(),
|
||||
Transform::from_xyz(0.0, 0.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||
// Add this component to this camera to render transparent meshes using OIT
|
||||
OrderIndependentTransparencySettings::default(),
|
||||
RenderLayers::layer(1),
|
||||
))
|
||||
.insert(
|
||||
// Msaa currently doesn't work with OIT
|
||||
Msaa::Off,
|
||||
);
|
||||
|
||||
// light
|
||||
commands.spawn((
|
||||
PointLight {
|
||||
shadows_enabled: false,
|
||||
..default()
|
||||
},
|
||||
Transform::from_xyz(4.0, 8.0, 4.0),
|
||||
RenderLayers::layer(1),
|
||||
));
|
||||
|
||||
// spawn help text
|
||||
commands.spawn((
|
||||
TextBundle::from_sections([
|
||||
TextSection::new("Press T to toggle OIT\n", TextStyle::default()),
|
||||
TextSection::new("OIT Enabled", TextStyle::default()),
|
||||
TextSection::new("\nPress C to cycle test scenes", TextStyle::default()),
|
||||
]),
|
||||
RenderLayers::layer(1),
|
||||
));
|
||||
|
||||
// spawn default scene
|
||||
spawn_spheres(&mut commands, &mut meshes, &mut materials);
|
||||
}
|
||||
|
||||
fn toggle_oit(
|
||||
mut commands: Commands,
|
||||
mut text: Query<&mut Text>,
|
||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||
q: Query<(Entity, Has<OrderIndependentTransparencySettings>), With<Camera3d>>,
|
||||
) {
|
||||
if keyboard_input.just_pressed(KeyCode::KeyT) {
|
||||
let (e, has_oit) = q.single();
|
||||
text.single_mut().sections[1].value = if has_oit {
|
||||
// Removing the component will completely disable OIT for this camera
|
||||
commands
|
||||
.entity(e)
|
||||
.remove::<OrderIndependentTransparencySettings>();
|
||||
"OIT disabled".to_string()
|
||||
} else {
|
||||
// Adding the component to the camera will render any transparent meshes
|
||||
// with OIT instead of alpha blending
|
||||
commands
|
||||
.entity(e)
|
||||
.insert(OrderIndependentTransparencySettings::default());
|
||||
"OIT enabled".to_string()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn cycle_scenes(
|
||||
mut commands: Commands,
|
||||
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
q: Query<Entity, With<Mesh3d>>,
|
||||
mut scene_id: Local<usize>,
|
||||
) {
|
||||
if keyboard_input.just_pressed(KeyCode::KeyC) {
|
||||
// depsawn current scene
|
||||
for e in &q {
|
||||
commands.entity(e).despawn_recursive();
|
||||
}
|
||||
// increment scene_id
|
||||
*scene_id = (*scene_id + 1) % 2;
|
||||
// spawn next scene
|
||||
match *scene_id {
|
||||
0 => spawn_spheres(&mut commands, &mut meshes, &mut materials),
|
||||
1 => spawn_occlusion_test(&mut commands, &mut meshes, &mut materials),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns 3 overlapping spheres
|
||||
/// Technically, when using `alpha_to_coverage` with MSAA this particular example wouldn't break,
|
||||
/// but it breaks when disabling MSAA and is enough to show the difference between OIT enabled vs disabled.
|
||||
fn spawn_spheres(
|
||||
commands: &mut Commands,
|
||||
meshes: &mut Assets<Mesh>,
|
||||
materials: &mut Assets<StandardMaterial>,
|
||||
) {
|
||||
let pos_a = Vec3::new(-1.0, 0.75, 0.0);
|
||||
let pos_b = Vec3::new(0.0, -0.75, 0.0);
|
||||
let pos_c = Vec3::new(1.0, 0.75, 0.0);
|
||||
|
||||
let offset = Vec3::new(0.0, 0.0, 0.0);
|
||||
|
||||
let sphere_handle = meshes.add(Sphere::new(2.0).mesh());
|
||||
|
||||
let alpha = 0.25;
|
||||
|
||||
let render_layers = RenderLayers::layer(1);
|
||||
|
||||
commands.spawn((
|
||||
Mesh3d(sphere_handle.clone()),
|
||||
MeshMaterial3d(materials.add(StandardMaterial {
|
||||
base_color: RED.with_alpha(alpha).into(),
|
||||
alpha_mode: AlphaMode::Blend,
|
||||
..default()
|
||||
})),
|
||||
Transform::from_translation(pos_a + offset),
|
||||
render_layers.clone(),
|
||||
));
|
||||
commands.spawn((
|
||||
Mesh3d(sphere_handle.clone()),
|
||||
MeshMaterial3d(materials.add(StandardMaterial {
|
||||
base_color: GREEN.with_alpha(alpha).into(),
|
||||
alpha_mode: AlphaMode::Blend,
|
||||
..default()
|
||||
})),
|
||||
Transform::from_translation(pos_b + offset),
|
||||
render_layers.clone(),
|
||||
));
|
||||
commands.spawn((
|
||||
Mesh3d(sphere_handle.clone()),
|
||||
MeshMaterial3d(materials.add(StandardMaterial {
|
||||
base_color: BLUE.with_alpha(alpha).into(),
|
||||
alpha_mode: AlphaMode::Blend,
|
||||
..default()
|
||||
})),
|
||||
Transform::from_translation(pos_c + offset),
|
||||
render_layers.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Spawn a combination of opaque cubes and transparent spheres.
|
||||
/// This is useful to make sure transparent meshes drawn with OIT
|
||||
/// are properly occluded by opaque meshes.
|
||||
fn spawn_occlusion_test(
|
||||
commands: &mut Commands,
|
||||
meshes: &mut Assets<Mesh>,
|
||||
materials: &mut Assets<StandardMaterial>,
|
||||
) {
|
||||
let sphere_handle = meshes.add(Sphere::new(1.0).mesh());
|
||||
let cube_handle = meshes.add(Cuboid::from_size(Vec3::ONE).mesh());
|
||||
let cube_material = materials.add(Color::srgb(0.8, 0.7, 0.6));
|
||||
|
||||
let render_layers = RenderLayers::layer(1);
|
||||
|
||||
// front
|
||||
let x = -2.5;
|
||||
commands.spawn((
|
||||
Mesh3d(cube_handle.clone()),
|
||||
MeshMaterial3d(cube_material.clone()),
|
||||
Transform::from_xyz(x, 0.0, 2.0),
|
||||
render_layers.clone(),
|
||||
));
|
||||
commands.spawn((
|
||||
Mesh3d(sphere_handle.clone()),
|
||||
MeshMaterial3d(materials.add(StandardMaterial {
|
||||
base_color: RED.with_alpha(0.5).into(),
|
||||
alpha_mode: AlphaMode::Blend,
|
||||
..default()
|
||||
})),
|
||||
Transform::from_xyz(x, 0., 0.),
|
||||
render_layers.clone(),
|
||||
));
|
||||
|
||||
// intersection
|
||||
commands.spawn((
|
||||
Mesh3d(cube_handle.clone()),
|
||||
MeshMaterial3d(cube_material.clone()),
|
||||
Transform::from_xyz(x, 0.0, 1.0),
|
||||
render_layers.clone(),
|
||||
));
|
||||
commands.spawn((
|
||||
Mesh3d(sphere_handle.clone()),
|
||||
MeshMaterial3d(materials.add(StandardMaterial {
|
||||
base_color: RED.with_alpha(0.5).into(),
|
||||
alpha_mode: AlphaMode::Blend,
|
||||
..default()
|
||||
})),
|
||||
Transform::from_xyz(0., 0., 0.),
|
||||
render_layers.clone(),
|
||||
));
|
||||
|
||||
// back
|
||||
let x = 2.5;
|
||||
commands.spawn((
|
||||
Mesh3d(cube_handle.clone()),
|
||||
MeshMaterial3d(cube_material.clone()),
|
||||
Transform::from_xyz(x, 0.0, -2.0),
|
||||
render_layers.clone(),
|
||||
));
|
||||
commands.spawn((
|
||||
Mesh3d(sphere_handle.clone()),
|
||||
MeshMaterial3d(materials.add(StandardMaterial {
|
||||
base_color: RED.with_alpha(0.5).into(),
|
||||
alpha_mode: AlphaMode::Blend,
|
||||
..default()
|
||||
})),
|
||||
Transform::from_xyz(x, 0., 0.),
|
||||
render_layers.clone(),
|
||||
));
|
||||
}
|
|
@ -158,6 +158,7 @@ Example | Description
|
|||
[Load glTF extras](../examples/3d/load_gltf_extras.rs) | Loads and renders a glTF file as a scene, including the gltf extras
|
||||
[Meshlet](../examples/3d/meshlet.rs) | Meshlet rendering for dense high-poly scenes (experimental)
|
||||
[Motion Blur](../examples/3d/motion_blur.rs) | Demonstrates per-pixel motion blur
|
||||
[Order Independent Transparency](../examples/3d/order_independent_transparency.rs) | Demonstrates how to use OIT
|
||||
[Orthographic View](../examples/3d/orthographic.rs) | Shows how to create a 3D orthographic view (for isometric-look in games or CAD applications)
|
||||
[Parallax Mapping](../examples/3d/parallax_mapping.rs) | Demonstrates use of a normal map and depth map for parallax mapping
|
||||
[Parenting](../examples/3d/parenting.rs) | Demonstrates parent->child relationships and relative transformations
|
||||
|
|
Loading…
Reference in a new issue