Skip to main content

order_independent_transparency/
order_independent_transparency.rs

1//! A simple 3D scene showing how alpha blending can break and how order independent transparency (OIT) can fix it.
2//!
3//! See [`OrderIndependentTransparencyPlugin`] for the trade-offs of using OIT.
4//!
5//! [`OrderIndependentTransparencyPlugin`]: bevy::core_pipeline::oit::OrderIndependentTransparencyPlugin
6use bevy::{
7    camera::visibility::RenderLayers,
8    color::palettes::css::{BLUE, GREEN, RED, YELLOW},
9    core_pipeline::{oit::OrderIndependentTransparencySettings, prepass::DepthPrepass},
10    prelude::*,
11};
12
13fn main() {
14    App::new()
15        .add_plugins(DefaultPlugins)
16        .add_systems(Startup, setup)
17        .add_systems(Update, (toggle_oit, cycle_scenes))
18        .run();
19}
20
21/// set up a simple 3D scene
22fn setup(
23    mut commands: Commands,
24    mut meshes: ResMut<Assets<Mesh>>,
25    mut materials: ResMut<Assets<StandardMaterial>>,
26) {
27    // camera
28    commands.spawn((
29        Camera3d::default(),
30        Transform::from_xyz(0.0, 0.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
31        // Add this component to this camera to render transparent meshes using OIT
32        OrderIndependentTransparencySettings::default(),
33        RenderLayers::layer(1),
34        // Msaa currently doesn't work with OIT
35        Msaa::Off,
36        // Optional: depth prepass can help OIT filter out fragments occluded by opaque objects
37        DepthPrepass,
38    ));
39
40    // light
41    commands.spawn((
42        PointLight {
43            shadow_maps_enabled: false,
44            ..default()
45        },
46        Transform::from_xyz(4.0, 8.0, 4.0),
47        RenderLayers::layer(1),
48    ));
49
50    // spawn help text
51    commands.spawn((
52        Text::default(),
53        Node {
54            position_type: PositionType::Absolute,
55            top: px(12),
56            left: px(12),
57            ..default()
58        },
59        RenderLayers::layer(1),
60        children![
61            TextSpan::new("Press T to toggle OIT\n"),
62            TextSpan::new("OIT Enabled"),
63            TextSpan::new("\nPress C to cycle test scenes"),
64        ],
65    ));
66
67    // spawn default scene
68    spawn_spheres(&mut commands, &mut meshes, &mut materials);
69}
70
71fn toggle_oit(
72    mut commands: Commands,
73    text: Single<Entity, With<Text>>,
74    keyboard_input: Res<ButtonInput<KeyCode>>,
75    q: Single<(Entity, Has<OrderIndependentTransparencySettings>), With<Camera3d>>,
76    mut text_writer: TextUiWriter,
77) {
78    if keyboard_input.just_pressed(KeyCode::KeyT) {
79        let (e, has_oit) = *q;
80        *text_writer.text(*text, 2) = if has_oit {
81            // Removing the component will completely disable OIT for this camera
82            commands
83                .entity(e)
84                .remove::<OrderIndependentTransparencySettings>();
85            "OIT disabled".to_string()
86        } else {
87            // Adding the component to the camera will render any transparent meshes
88            // with OIT instead of alpha blending
89            commands
90                .entity(e)
91                .insert(OrderIndependentTransparencySettings::default());
92            "OIT enabled".to_string()
93        };
94    }
95}
96
97fn cycle_scenes(
98    mut commands: Commands,
99    keyboard_input: Res<ButtonInput<KeyCode>>,
100    mut meshes: ResMut<Assets<Mesh>>,
101    mut materials: ResMut<Assets<StandardMaterial>>,
102    q: Query<Entity, With<Mesh3d>>,
103    mut scene_id: Local<usize>,
104    asset_server: Res<AssetServer>,
105) {
106    if keyboard_input.just_pressed(KeyCode::KeyC) {
107        // despawn current scene
108        for e in &q {
109            commands.entity(e).despawn();
110        }
111        // increment scene_id
112        *scene_id = (*scene_id + 1) % 4;
113        // spawn next scene
114        match *scene_id {
115            0 => spawn_spheres(&mut commands, &mut meshes, &mut materials),
116            1 => spawn_quads(&mut commands, &mut meshes, &mut materials),
117            2 => spawn_occlusion_test(&mut commands, &mut meshes, &mut materials),
118            3 => {
119                spawn_auto_instancing_test(
120                    &mut commands,
121                    &mut meshes,
122                    &mut materials,
123                    asset_server,
124                );
125            }
126            _ => unreachable!(),
127        }
128    }
129}
130
131/// Spawns 3 overlapping spheres
132/// Technically, when using `alpha_to_coverage` with MSAA this particular example wouldn't break,
133/// but it breaks when disabling MSAA and is enough to show the difference between OIT enabled vs disabled.
134fn spawn_spheres(
135    commands: &mut Commands,
136    meshes: &mut Assets<Mesh>,
137    materials: &mut Assets<StandardMaterial>,
138) {
139    let pos_a = Vec3::new(-1.0, 0.75, 0.0);
140    let pos_b = Vec3::new(0.0, -0.75, 0.0);
141    let pos_c = Vec3::new(1.0, 0.75, 0.0);
142
143    let offset = Vec3::new(0.0, 0.0, 0.0);
144
145    let sphere_handle = meshes.add(Sphere::new(2.0).mesh());
146
147    let alpha = 0.25;
148
149    let render_layers = RenderLayers::layer(1);
150
151    commands.spawn((
152        Mesh3d(sphere_handle.clone()),
153        MeshMaterial3d(materials.add(StandardMaterial {
154            base_color: RED.with_alpha(alpha).into(),
155            alpha_mode: AlphaMode::Blend,
156            ..default()
157        })),
158        Transform::from_translation(pos_a + offset),
159        render_layers.clone(),
160    ));
161    commands.spawn((
162        Mesh3d(sphere_handle.clone()),
163        MeshMaterial3d(materials.add(StandardMaterial {
164            base_color: GREEN.with_alpha(alpha).into(),
165            alpha_mode: AlphaMode::Blend,
166            ..default()
167        })),
168        Transform::from_translation(pos_b + offset),
169        render_layers.clone(),
170    ));
171    commands.spawn((
172        Mesh3d(sphere_handle.clone()),
173        MeshMaterial3d(materials.add(StandardMaterial {
174            base_color: BLUE.with_alpha(alpha).into(),
175            alpha_mode: AlphaMode::Blend,
176            ..default()
177        })),
178        Transform::from_translation(pos_c + offset),
179        render_layers.clone(),
180    ));
181}
182
183fn spawn_quads(
184    commands: &mut Commands,
185    meshes: &mut Assets<Mesh>,
186    materials: &mut Assets<StandardMaterial>,
187) {
188    let quad_handle = meshes.add(Rectangle::new(3.0, 3.0).mesh());
189    let render_layers = RenderLayers::layer(1);
190    let xform = |x, y, z| {
191        Transform::from_rotation(Quat::from_rotation_y(0.5))
192            .mul_transform(Transform::from_xyz(x, y, z))
193    };
194    commands.spawn((
195        Mesh3d(quad_handle.clone()),
196        MeshMaterial3d(materials.add(StandardMaterial {
197            base_color: RED.with_alpha(0.5).into(),
198            alpha_mode: AlphaMode::Blend,
199            ..default()
200        })),
201        xform(1.0, -0.1, 0.),
202        render_layers.clone(),
203    ));
204    commands.spawn((
205        Mesh3d(quad_handle.clone()),
206        MeshMaterial3d(materials.add(StandardMaterial {
207            base_color: BLUE.with_alpha(0.8).into(),
208            alpha_mode: AlphaMode::Blend,
209            ..default()
210        })),
211        xform(0.5, 0.2, -0.5),
212        render_layers.clone(),
213    ));
214    commands.spawn((
215        Mesh3d(quad_handle.clone()),
216        MeshMaterial3d(materials.add(StandardMaterial {
217            base_color: GREEN.with_green(1.0).with_alpha(0.5).into(),
218            alpha_mode: AlphaMode::Blend,
219            ..default()
220        })),
221        xform(0.0, 0.4, -1.),
222        render_layers.clone(),
223    ));
224    commands.spawn((
225        Mesh3d(quad_handle.clone()),
226        MeshMaterial3d(materials.add(StandardMaterial {
227            base_color: YELLOW.with_alpha(0.3).into(),
228            alpha_mode: AlphaMode::Blend,
229            ..default()
230        })),
231        xform(-0.5, 0.6, -1.1),
232        render_layers.clone(),
233    ));
234    commands.spawn((
235        Mesh3d(quad_handle.clone()),
236        MeshMaterial3d(materials.add(StandardMaterial {
237            base_color: BLUE.with_alpha(0.2).into(),
238            alpha_mode: AlphaMode::Blend,
239            ..default()
240        })),
241        xform(-0.8, 0.8, -1.2),
242        render_layers.clone(),
243    ));
244}
245
246/// Spawn a combination of opaque cubes and transparent spheres.
247/// This is useful to make sure transparent meshes drawn with OIT
248/// are properly occluded by opaque meshes.
249fn spawn_occlusion_test(
250    commands: &mut Commands,
251    meshes: &mut Assets<Mesh>,
252    materials: &mut Assets<StandardMaterial>,
253) {
254    let sphere_handle = meshes.add(Sphere::new(1.0).mesh());
255    let cube_handle = meshes.add(Cuboid::from_size(Vec3::ONE).mesh());
256    let cube_material = materials.add(Color::srgb(0.8, 0.7, 0.6));
257
258    let render_layers = RenderLayers::layer(1);
259
260    // front
261    let x = -2.5;
262    commands.spawn((
263        Mesh3d(cube_handle.clone()),
264        MeshMaterial3d(cube_material.clone()),
265        Transform::from_xyz(x, 0.0, 2.0),
266        render_layers.clone(),
267    ));
268    commands.spawn((
269        Mesh3d(sphere_handle.clone()),
270        MeshMaterial3d(materials.add(StandardMaterial {
271            base_color: RED.with_alpha(0.5).into(),
272            alpha_mode: AlphaMode::Blend,
273            ..default()
274        })),
275        Transform::from_xyz(x, 0., 0.),
276        render_layers.clone(),
277    ));
278
279    // intersection
280    commands.spawn((
281        Mesh3d(cube_handle.clone()),
282        MeshMaterial3d(cube_material.clone()),
283        Transform::from_xyz(x, 0.0, 1.0),
284        render_layers.clone(),
285    ));
286    commands.spawn((
287        Mesh3d(sphere_handle.clone()),
288        MeshMaterial3d(materials.add(StandardMaterial {
289            base_color: RED.with_alpha(0.5).into(),
290            alpha_mode: AlphaMode::Blend,
291            ..default()
292        })),
293        Transform::from_xyz(0., 0., 0.),
294        render_layers.clone(),
295    ));
296
297    // back
298    let x = 2.5;
299    commands.spawn((
300        Mesh3d(cube_handle.clone()),
301        MeshMaterial3d(cube_material.clone()),
302        Transform::from_xyz(x, 0.0, -2.0),
303        render_layers.clone(),
304    ));
305    commands.spawn((
306        Mesh3d(sphere_handle.clone()),
307        MeshMaterial3d(materials.add(StandardMaterial {
308            base_color: RED.with_alpha(0.5).into(),
309            alpha_mode: AlphaMode::Blend,
310            ..default()
311        })),
312        Transform::from_xyz(x, 0., 0.),
313        render_layers.clone(),
314    ));
315}
316
317fn spawn_auto_instancing_test(
318    commands: &mut Commands,
319    meshes: &mut Assets<Mesh>,
320    materials: &mut Assets<StandardMaterial>,
321    asset_server: Res<AssetServer>,
322) {
323    let render_layers = RenderLayers::layer(1);
324
325    let cube = meshes.add(Cuboid::new(1.0, 1.0, 1.0));
326    let material_handle = materials.add(StandardMaterial {
327        alpha_mode: AlphaMode::Blend,
328        base_color_texture: Some(asset_server.load("textures/slice_square.png")),
329        ..Default::default()
330    });
331    let mut bundles = Vec::with_capacity(3 * 3 * 3);
332
333    for z in -1..=1 {
334        for y in -1..=1 {
335            for x in -1..=1 {
336                bundles.push((
337                    Mesh3d(cube.clone()),
338                    MeshMaterial3d(material_handle.clone()),
339                    Transform::from_xyz(x as f32 * 2.0, y as f32 * 2.0, z as f32 * 2.0),
340                    render_layers.clone(),
341                ));
342            }
343        }
344    }
345    commands.spawn_batch(bundles);
346}