Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
61a1e0c
Implement per-pixel linked list for OIT
beicause Nov 14, 2025
8a3c399
update
beicause Nov 14, 2025
7ebd383
Fix
beicause Nov 14, 2025
9d0dbc3
make use of depth prepass to filter out fragments
beicause Nov 16, 2025
c414d2e
Fix corrupted linked list on startup
beicause Nov 16, 2025
395acf0
change OIT default value
beicause Nov 26, 2025
a2583f6
Sort in desc order and fix early termination in blending
beicause Nov 26, 2025
cd963ea
Merge remote-tracking branch 'upstream' into oit-opt
beicause Nov 26, 2025
6398884
format
beicause Nov 26, 2025
a8028b5
fmt
beicause Dec 6, 2025
ae57011
Merge remote-tracking branch 'upstream' into oit-opt
beicause Dec 6, 2025
1fbeaa7
Merge remote-tracking branch 'upstream' into oit-opt
beicause Jan 9, 2026
2babad2
Rename header -> head
beicause Jan 9, 2026
0290b64
Add some methods and optimizations in buffer vec
beicause Jan 10, 2026
c07054a
Avoid writing oit heads buffer by offseting values by 1
beicause Jan 10, 2026
41fb34a
Save local memory for sorted frags
beicause Jan 12, 2026
fe52562
Merge remote-tracking branch 'upstream/main' into oit-opt
beicause Jan 12, 2026
545d99a
Store packed depth and alpha to save local memory
goodartistscopy Jan 12, 2026
77c783d
Swap depth/alpha in packed repr. to make it monotonic with depth
goodartistscopy Jan 12, 2026
47b9642
Finalize previous commit
goodartistscopy Jan 12, 2026
706529f
Merge branch 'oit-depth-opt' into oit-opt
beicause Jan 12, 2026
1c68b0d
beicause Jan 12, 2026
e98bee5
Merge remote-tracking branch 'upstream/main' into oit-opt
beicause Jan 29, 2026
a97aa85
Fix merging
beicause Jan 29, 2026
ab1e342
Merge remote-tracking branch 'upstream/main' into oit-opt
beicause Feb 5, 2026
e955608
docs, rm unnecessary clone
beicause Feb 5, 2026
4c7c38e
Bind oit nodes buffer capacity
beicause Feb 5, 2026
23feaa2
Release oit buffers if no camera enables OIT
beicause Feb 5, 2026
2b3dd5f
Resize oit buffers instead of reserve when changed
beicause Feb 5, 2026
d455ee5
CI
beicause Feb 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use crate::core_3d::Transparent3d;
use crate::{
core_3d::Transparent3d,
oit::{resolve::OitResolvePipelineId, OrderIndependentTransparencySettings},
};
use bevy_camera::{MainPassResolutionOverride, Viewport};
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},
render_resource::{PipelineCache, RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ExtractedView, ViewDepthTexture, ViewTarget},
};
Expand All @@ -26,12 +29,14 @@ impl ViewNode for MainTransparentPass3dNode {
&'static ViewTarget,
&'static ViewDepthTexture,
Option<&'static MainPassResolutionOverride>,
Has<OrderIndependentTransparencySettings>,
Option<&'static OitResolvePipelineId>,
);
fn run(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext,
(camera, view, target, depth, resolution_override): QueryItem<Self::ViewQuery>,
(camera, view, target, depth, resolution_override, has_oit,oit_resolve_pipeline_id): QueryItem<Self::ViewQuery>,
Comment thread
beicause marked this conversation as resolved.
Outdated
world: &World,
) -> Result<(), NodeRunError> {
let view_entity = graph.view_entity();
Expand All @@ -47,6 +52,22 @@ impl ViewNode for MainTransparentPass3dNode {
};

if !transparent_phase.items.is_empty() {
if has_oit {
// We can't run transparent phase if OitResolvePipelineId is not ready
// Otherwise we will write to `oit_atomic_counter` and `oit_headers` buffer without resetting them
// which causes corrupted linked list(can have circular references) on the next pass
let Some(oit_resolve_pipeline_id) = oit_resolve_pipeline_id else {
return Ok(());
};
let pipeline_cache = world.resource::<PipelineCache>();
if pipeline_cache
.get_render_pipeline(oit_resolve_pipeline_id.0)
.is_none()
{
return Ok(());
}
}

// Run the transparent pass, sorted back-to-front
// NOTE: Scoped to drop the mutable borrow of render_context
#[cfg(feature = "trace")]
Expand Down
188 changes: 80 additions & 108 deletions crates/bevy_core_pipeline/src/oit/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
//! Order Independent Transparency (OIT) for 3d rendering. See [`OrderIndependentTransparencyPlugin`] for more details.

use bevy_app::prelude::*;
use bevy_camera::{Camera, Camera3d};
use bevy_ecs::{component::*, lifecycle::ComponentHook, prelude::*};
use bevy_camera::Camera3d;
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::{
Expand All @@ -17,12 +16,11 @@ use bevy_render::{
Render, RenderApp, RenderStartup, RenderSystems,
};
use bevy_shader::load_shader_library;
use bevy_window::PrimaryWindow;
use resolve::{
node::{OitResolveNode, OitResolvePass},
OitResolvePlugin,
};
use tracing::{trace, warn};
use tracing::trace;

use crate::core_3d::graph::{Core3d, Node3d};

Expand All @@ -35,13 +33,16 @@ pub mod resolve;
// 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)]
#[derive(Clone, Copy, ExtractComponent, Reflect, ShaderType, Component)]
#[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,
/// Controls how many fragments will be exactly sorted.
/// If the scene has more fragments than this, they will be merged approximately.
/// More sorted fragments is more accurate but will be slower.
pub sorted_fragment_max_count: u32,
/// The average fragments per pixel stored in the buffer. This should be bigger enough otherwise the fragments will be discarded.
/// Higher values increase memory usage.
pub fragments_per_pixel_average: f32,
/// 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.
Expand All @@ -51,32 +52,13 @@ pub struct OrderIndependentTransparencySettings {
impl Default for OrderIndependentTransparencySettings {
fn default() -> Self {
Self {
layer_count: 8,
sorted_fragment_max_count: 8,
fragments_per_pixel_average: 4.0,
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)
&& 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.
///
Expand All @@ -88,8 +70,8 @@ impl Component for OrderIndependentTransparencySettings {
/// # 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`].
/// The first pass constructs a linked list which stores depth and color of all fragments in a big buffer.
/// The linked list capacity can be set with [`OrderIndependentTransparencySettings::fragments_per_pixel_average`].
/// 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
Expand All @@ -103,8 +85,7 @@ impl Plugin for OrderIndependentTransparencyPlugin {
ExtractComponentPlugin::<OrderIndependentTransparencySettings>::default(),
OitResolvePlugin,
))
.add_systems(Update, check_msaa)
.add_systems(Last, configure_depth_texture_usages);
.add_systems(Update, check_msaa);

let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
Expand All @@ -114,7 +95,10 @@ impl Plugin for OrderIndependentTransparencyPlugin {
.add_systems(RenderStartup, init_oit_buffers)
.add_systems(
Render,
prepare_oit_buffers.in_set(RenderSystems::PrepareResources),
(
configure_camera_depth_usages.in_set(RenderSystems::ManageViews),
prepare_oit_buffers.in_set(RenderSystems::PrepareResources),
),
);

render_app
Expand All @@ -130,34 +114,17 @@ impl Plugin for OrderIndependentTransparencyPlugin {
}
}

// 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>>,
fn configure_camera_depth_usages(
mut cameras: Query<
&mut Camera3d,
(
Changed<Camera3d>,
With<OrderIndependentTransparencySettings>,
),
>,
) {
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();
}
for mut camera in &mut cameras {
camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits();
}
}

Expand All @@ -169,18 +136,25 @@ fn check_msaa(cameras: Query<&Msaa, With<OrderIndependentTransparencySettings>>)
}
}

#[derive(Clone, Copy, ShaderType)]
pub struct OitFragmentNode {
pub color: u32,
pub depth_alpha: u32,
pub next: u32,
}

/// 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.
pub settings: DynamicUniformBuffer<OrderIndependentTransparencySettings>,
/// The OIT buffers containing color, depth and linked next node 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>,
pub nodes: BufferVec<OitFragmentNode>,
pub headers: BufferVec<u32>,
pub atomic_counter: BufferVec<u32>,
}

pub fn init_oit_buffers(
Expand All @@ -190,22 +164,27 @@ pub fn init_oit_buffers(
) {
// 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 nodes = BufferVec::new(BufferUsages::COPY_DST | BufferUsages::STORAGE);
nodes.set_label(Some("oit_nodes"));
nodes.reserve(1, &render_device);

let mut headers = BufferVec::new(BufferUsages::COPY_DST | BufferUsages::STORAGE);
headers.set_label(Some("oit_headers"));
headers.push(u32::MAX);
headers.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 atomic_counter = BufferVec::new(BufferUsages::COPY_DST | BufferUsages::STORAGE);
atomic_counter.set_label(Some("oit_atomic_counter"));
atomic_counter.push(0);
atomic_counter.write_buffer(&render_device, &render_queue);

let mut settings = DynamicUniformBuffer::default();
settings.set_label(Some("oit_settings"));

commands.insert_resource(OitBuffers {
layers,
layer_ids,
nodes,
headers,
atomic_counter,
settings,
});
}
Expand Down Expand Up @@ -233,52 +212,45 @@ pub fn prepare_oit_buffers(
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;
let mut max_size = UVec2::new(0, 0);
let mut fragments_per_pixel_average = 0f32;
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);
max_size = max_size.max(size);
fragments_per_pixel_average =
fragments_per_pixel_average.max(settings.fragments_per_pixel_average);
}

// Create or update the layers buffer based on the max size
if buffers.layers.capacity() < max_layers_size {
// Create or update the headers texture based on the max size
let headers_size = (max_size.x * max_size.y) as usize;
if buffers.headers.capacity() < headers_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.headers.clear();
buffers.headers.reserve(headers_size, &render_device);
Comment thread
beicause marked this conversation as resolved.
Outdated
for _ in 0..headers_size {
buffers.headers.push(u32::MAX);
}
buffers.layers.write_buffer(&render_device, &render_queue);
buffers.headers.write_buffer(&render_device, &render_queue);
trace!(
"OIT layers buffer updated in {:.01}ms with total size {} MiB",
"OIT headers buffer updated in {:.01}ms with capacity {}, total size {} MiB",
start.elapsed().as_millis(),
buffers.layers.capacity() * size_of::<UVec2>() / 1024 / 1024,
buffers.headers.capacity(),
buffers.headers.capacity() * size_of::<u32>() / 1024 / 1024,
);
}

// Create or update the layer_ids buffer based on the max size
if buffers.layer_ids.capacity() < max_layer_ids_size {
// Create or update the nodes buffer based on the max size
let nodes_size = ((max_size.x * max_size.y) as f32 * fragments_per_pixel_average) as usize;
if buffers.nodes.capacity() < nodes_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);
buffers.nodes.reserve(nodes_size, &render_device);
trace!(
"OIT layer ids buffer updated in {:.01}ms with total size {} MiB",
"OIT nodes buffer updated in {:.01}ms with capacity {}, total size {} MiB",
start.elapsed().as_millis(),
buffers.layer_ids.capacity() * size_of::<UVec2>() / 1024 / 1024,
buffers.nodes.capacity(),
buffers.nodes.capacity() * size_of::<OitFragmentNode>() / 1024 / 1024,
);
}

Expand Down
Loading