From 4bf647ff3b0ca7c8ca47496db9cfe03702328473 Mon Sep 17 00:00:00 2001 From: IceSentry Date: Mon, 7 Oct 2024 19:50:28 -0400 Subject: [PATCH] Add Order Independent Transparency (#14876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 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 Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com> Co-authored-by: Charlotte McElwain --- Cargo.toml | 11 + crates/bevy_core_pipeline/Cargo.toml | 1 + crates/bevy_core_pipeline/src/lib.rs | 6 + crates/bevy_core_pipeline/src/oit/mod.rs | 283 ++++++++++++++++++ .../bevy_core_pipeline/src/oit/oit_draw.wgsl | 44 +++ .../bevy_core_pipeline/src/oit/resolve/mod.rs | 212 +++++++++++++ .../src/oit/resolve/node.rs | 78 +++++ .../src/oit/resolve/oit_resolve.wgsl | 107 +++++++ crates/bevy_pbr/src/material.rs | 7 + crates/bevy_pbr/src/render/mesh.rs | 53 ++-- .../bevy_pbr/src/render/mesh_view_bindings.rs | 58 +++- .../src/render/mesh_view_bindings.wgsl | 6 + crates/bevy_pbr/src/render/pbr.wgsl | 13 + examples/3d/order_independent_transparency.rs | 236 +++++++++++++++ examples/README.md | 1 + 15 files changed, 1090 insertions(+), 26 deletions(-) create mode 100644 crates/bevy_core_pipeline/src/oit/mod.rs create mode 100644 crates/bevy_core_pipeline/src/oit/oit_draw.wgsl create mode 100644 crates/bevy_core_pipeline/src/oit/resolve/mod.rs create mode 100644 crates/bevy_core_pipeline/src/oit/resolve/node.rs create mode 100644 crates/bevy_core_pipeline/src/oit/resolve/oit_resolve.wgsl create mode 100644 examples/3d/order_independent_transparency.rs diff --git a/Cargo.toml b/Cargo.toml index ecc1efedab575..cbe7d9c79d6b1 100644 --- a/Cargo.toml +++ b/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" diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index fbf070f4cbb61..e134f702caade 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -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" diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index ac91fca61dfcb..c7a65a3e1f1c9 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -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, )); } } diff --git a/crates/bevy_core_pipeline/src/oit/mod.rs b/crates/bevy_core_pipeline/src/oit/mod.rs new file mode 100644 index 0000000000000..4d3a3286071f8 --- /dev/null +++ b/crates/bevy_core_pipeline/src/oit/mod.rs @@ -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 = 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::::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::>(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::(); + } +} + +// 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>, + cameras: Query<(&Camera, Has)>, + mut new_cameras: Query<(&mut Camera3d, &Camera), Added>, +) { + 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>) { + 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, + /// Buffer containing the index of the last layer that was written for each fragment. + pub layer_ids: BufferVec, + pub layers_count_uniforms: DynamicUniformBuffer, +} + +impl FromWorld for OitBuffers { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let render_queue = world.resource::(); + + // 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, + render_queue: Res, + cameras: Query< + (&ExtractedCamera, &OrderIndependentTransparencySettings), + ( + Changed, + Changed, + ), + >, + camera_oit_uniforms: Query<(Entity, &OrderIndependentTransparencySettings)>, + mut buffers: ResMut, +) { + // 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::() / 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::() / 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 }); + } + } +} diff --git a/crates/bevy_core_pipeline/src/oit/oit_draw.wgsl b/crates/bevy_core_pipeline/src/oit/oit_draw.wgsl new file mode 100644 index 0000000000000..b6138cc87fdd5 --- /dev/null +++ b/crates/bevy_core_pipeline/src/oit/oit_draw.wgsl @@ -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 { + let depth_bits = packed & 0xFFFFFFu; + let alpha_bits = (packed >> 24u) & 0xFFu; + return vec2(f32(depth_bits) / f32(0xFFFFFFu), f32(alpha_bits) / f32(0xFFu)); +} diff --git a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs new file mode 100644 index 0000000000000..e728ec004aa2b --- /dev/null +++ b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs @@ -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 = 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::(); + } +} + +/// 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::(); + + let view_bind_group_layout = render_device.create_bind_group_layout( + "oit_resolve_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + uniform_buffer::(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, + resolve_pipeline: Res, + views: Query<(Entity, &ExtractedView), With>, + // 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>, +) { + 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::>() { + 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, + render_device: Res, + view_uniforms: Res, + buffers: Res, +) { + 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)); + } +} diff --git a/crates/bevy_core_pipeline/src/oit/resolve/node.rs b/crates/bevy_core_pipeline/src/oit/resolve/node.rs new file mode 100644 index 0000000000000..14d42235f12a9 --- /dev/null +++ b/crates/bevy_core_pipeline/src/oit/resolve/node.rs @@ -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::() else { + return Ok(()); + }; + + // resolve oit + // sorts the layers and renders the final blended color to the screen + { + let pipeline_cache = world.resource::(); + let bind_group = world.resource::(); + 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(()) + } +} diff --git a/crates/bevy_core_pipeline/src/oit/resolve/oit_resolve.wgsl b/crates/bevy_core_pipeline/src/oit/resolve/oit_resolve.wgsl new file mode 100644 index 0000000000000..513000be03e35 --- /dev/null +++ b/crates/bevy_core_pipeline/src/oit/resolve/oit_resolve.wgsl @@ -0,0 +1,107 @@ +#import bevy_render::view::View + +@group(0) @binding(0) var view: View; +@group(0) @binding(1) var layers: array>; +@group(0) @binding(2) var layer_ids: array>; + +@group(1) @binding(0) var depth: texture_depth_2d; + +struct OitFragment { + color: vec3, + alpha: f32, + depth: f32, +} +// Contains all the colors and depth for this specific fragment +// TODO don't hardcode size +var fragment_list: array; + +struct FullscreenVertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, +}; + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + 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(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, color_b: vec4) -> vec4 { + 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); +} diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 29a3a85b342dd..05292f7fce846 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -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( Has>, Has>, ), + Has, )>, ) where M::Data: PartialEq + Eq + Hash + Clone, @@ -638,6 +640,7 @@ pub fn queue_material_meshes( temporal_jitter, projection, (has_environment_maps, has_irradiance_volumes), + has_oit, ) in &views { let ( @@ -691,6 +694,10 @@ pub fn queue_material_meshes( 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, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 1ef92d63c1921..1f8836ff87b2f 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -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:: .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 RenderCommand

for SetMeshViewBindGroup Read, Read, Read, + Option>, ); type ItemQuery = (); @@ -2193,23 +2207,24 @@ impl RenderCommand

for SetMeshViewBindGroup 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 } diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 0973b6513609c..2ad6ad8499e77 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -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 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::(true)), + )); + } + entries.to_vec() } @@ -453,8 +474,7 @@ pub fn prepare_mesh_view_bind_groups( render_device: Res, mesh_pipeline: Res, shadow_samplers: Res, - light_meta: Res, - global_light_meta: Res, + (light_meta, global_light_meta): (Res, Res), fog_meta: Res, (view_uniforms, environment_map_uniform): (Res, Res), views: Query<( @@ -468,6 +488,7 @@ pub fn prepare_mesh_view_bind_groups( &Tonemapping, Option<&RenderViewLightProbes>, Option<&RenderViewLightProbes>, + Has, )>, (images, mut fallback_images, fallback_image, fallback_image_zero): ( Res>, @@ -480,6 +501,7 @@ pub fn prepare_mesh_view_bind_groups( light_probes_buffer: Res, visibility_ranges: Res, ssr_buffer: Res, + oit_buffers: Res, ) { 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), }); diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index dfda3a576b317..e777b20358330 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -101,3 +101,9 @@ const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u; @group(0) @binding(29) var view_transmission_texture: texture_2d; @group(0) @binding(30) var view_transmission_sampler: sampler; + +#ifdef OIT_ENABLED +@group(0) @binding(31) var oit_layers: array>; +@group(0) @binding(32) var oit_layer_ids: array>; +@group(0) @binding(33) var oit_layers_count: i32; +#endif OIT_ENABLED diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index 7b94f3cdf801c..336c1724c548f 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -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; } diff --git a/examples/3d/order_independent_transparency.rs b/examples/3d/order_independent_transparency.rs new file mode 100644 index 0000000000000..79520d7431b87 --- /dev/null +++ b/examples/3d/order_independent_transparency.rs @@ -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>, + mut materials: ResMut>, +) { + // 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>, + q: Query<(Entity, Has), With>, +) { + 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::(); + "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>, + mut meshes: ResMut>, + mut materials: ResMut>, + q: Query>, + mut scene_id: Local, +) { + 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, + materials: &mut Assets, +) { + 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, + materials: &mut Assets, +) { + 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(), + )); +} diff --git a/examples/README.md b/examples/README.md index 1744816a8c914..700bc6f01720d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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