bevy/examples/3d/order_independent_transparency.rs
IceSentry 4bf647ff3b
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>
2024-10-07 23:50:28 +00:00

236 lines
7.4 KiB
Rust

//! 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(),
));
}