Skip to main content

order_independent_transparency/
order_independent_transparency.rs

1//! A series of simple 3D scenes 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
6
7use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
8use bevy::{
9    color::palettes::css::{BLUE, GREEN, RED, YELLOW},
10    core_pipeline::{oit::OrderIndependentTransparencySettings, prepass::DepthPrepass},
11    ecs::system::SystemParam,
12    feathers::{dark_theme::create_dark_theme, theme::UiTheme, FeathersPlugins},
13    pbr::{ExtendedMaterial, MaterialExtension},
14    picking::window::update_window_hits,
15    prelude::*,
16    render::render_resource::AsBindGroup,
17    shader::ShaderRef,
18    ui::Checked,
19    ui_widgets::{RadioButton, RadioGroup, ValueChange},
20    window::{CursorIcon, PrimaryWindow, SystemCursorIcon},
21};
22
23#[path = "../helpers/radio.rs"]
24mod radio;
25
26/// Scene construction functions
27const SCENES: &[(&str, &str, fn(&mut Commands, &mut SceneResources))] = &[
28    ("1", "Three balls", spawn_spheres),
29    ("2", "Stacked quads", spawn_quads),
30    ("3", "Opaque occlusion test", spawn_occlusion_test),
31    ("4", "Auto instancing test", spawn_auto_instancing_test),
32    ("5", "Custom material demo", spawn_custom_material),
33];
34
35/// Application state
36#[derive(Resource)]
37struct AppState {
38    /// Current OIT settings
39    oit_settings: OrderIndependentTransparencySettings,
40    /// Whether to use OIT or standard mesh sorting
41    use_oit: bool,
42    /// Using a depth prepass helps cull transparent fragment against opaque ones earlier
43    use_depth_prepass: bool,
44    /// The current scene being displayed
45    current_scene_id: usize,
46}
47
48impl Default for AppState {
49    fn default() -> Self {
50        Self {
51            oit_settings: Default::default(),
52            use_oit: true,
53            use_depth_prepass: true,
54            current_scene_id: 0,
55        }
56    }
57}
58
59/// Tweakable settings
60#[derive(Clone, PartialEq)]
61enum AppSetting {
62    /// Change whether OIT is used or not
63    EnableOIT(bool),
64    /// Change whether `DepthPrepass` is used or not
65    UseDepthPrepass(bool),
66    /// Change the displayed scene
67    ChangeScene(usize),
68}
69
70/// impl Default so that `AppSetting` can be more easily used in bsn!
71impl Default for AppSetting {
72    fn default() -> Self {
73        Self::EnableOIT(true)
74    }
75}
76
77/// Marker components to make it easier to update the `Selected` of each radio group.
78#[derive(Component, Clone, Default, PartialEq)]
79enum RadioGroupSetting {
80    #[default]
81    EnableOIT,
82    UseDepthPrepass,
83    ChangeScene,
84}
85
86/// This system param bundles up the resources used by the scene creation functions.
87#[derive(SystemParam)]
88struct SceneResources<'w> {
89    meshes: ResMut<'w, Assets<Mesh>>,
90    materials: ResMut<'w, Assets<StandardMaterial>>,
91    extended_materials:
92        ResMut<'w, Assets<ExtendedMaterial<StandardMaterial, CheckeredMaterialExtension>>>,
93    custom_materials: ResMut<'w, Assets<NoisyOpacityMaterial>>,
94    asset_server: Res<'w, AssetServer>,
95}
96
97fn main() {
98    App::new()
99        .add_plugins((DefaultPlugins, FeathersPlugins))
100        .add_plugins(MaterialPlugin::<NoisyOpacityMaterial>::default())
101        .add_plugins(MaterialPlugin::<
102            ExtendedMaterial<StandardMaterial, CheckeredMaterialExtension>,
103        >::default())
104        .insert_resource(UiTheme(create_dark_theme()))
105        .init_resource::<AppState>()
106        .add_systems(Startup, setup)
107        .add_systems(Update, handle_keyboard_shortcuts)
108        .add_systems(Update, scene_change_watcher)
109        .add_systems(Update, update_window_hits)
110        .add_observer(on_radio_value_change)
111        .run();
112}
113
114/// Component used to memorize the camera transform when initiating a Drag event
115#[derive(Component)]
116struct BaseTransform(Transform);
117
118/// Sets up the base scene
119fn setup(
120    mut commands: Commands,
121    mut resources: SceneResources,
122    app_state: Res<AppState>,
123    window: Single<Entity, With<PrimaryWindow>>,
124) {
125    // Spawn the main UI
126    spawn_ui(&mut commands, &app_state);
127
128    // Drag events handling on the window, tied to the camera rotation
129    commands
130        .entity(*window)
131        .observe(
132            |event: On<PointerDrag>,
133             mut commands: Commands,
134             mut camera_transforms: Single<
135                (&mut Transform, &BaseTransform),
136                With<Camera3d>,
137            >| {
138                commands
139                    .entity(event.entity)
140                    .insert(CursorIcon::System(SystemCursorIcon::Grabbing));
141
142                // During drag the additional rotation is relative to the transform stored when Drag was initiated
143                let (ref mut transform, base_transform) = *camera_transforms;
144                **transform = base_transform.0;
145
146                const RADIANS_PER_PIXEL: f32 = -std::f32::consts::PI / 600.0;
147                let angle = event.distance.x * RADIANS_PER_PIXEL;
148                transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(angle));
149            },
150        )
151        .observe(
152            |_: On<PointerDragStart>,
153             mut commands: Commands,
154             camera: Single<(Entity, &Transform), With<Camera3d>>| {
155                let (camera, transform) = *camera;
156                // Memorize the current transform
157                commands
158                    .entity(camera)
159                    .insert(BaseTransform(*transform));
160            },
161        )
162        .observe(
163            |_: On<PointerDragEnd>,
164             mut commands: Commands,
165             window: Single<Entity, With<PrimaryWindow>>| {
166                commands
167                    .entity(*window)
168                    .insert(CursorIcon::System(SystemCursorIcon::Default));
169            },
170        );
171
172    // Camera configuration
173    let mut camera = commands.spawn((
174        Camera3d::default(),
175        Transform::from_xyz(0.0, 0.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
176        // Msaa currently doesn't work with OIT
177        Msaa::Off,
178    ));
179
180    if app_state.use_oit {
181        // Add this component to the camera to render transparent meshes using OIT
182        camera.insert(OrderIndependentTransparencySettings {
183            ..app_state.oit_settings
184        });
185    }
186
187    if app_state.use_depth_prepass {
188        // Optional: depth prepass can help OIT filter out fragments occluded by opaque objects
189        camera.insert(DepthPrepass);
190    }
191
192    // Light
193    commands.spawn((
194        PointLight {
195            shadow_maps_enabled: false,
196            ..default()
197        },
198        Transform::from_xyz(4.0, 8.0, 4.0),
199    ));
200
201    // Spawn the default scene
202    SCENES[0].2(&mut commands, &mut resources); //&mut meshes, &mut materials);
203}
204
205/// Watches for key presses and changes the app accordingly.
206fn handle_keyboard_shortcuts(
207    keyboard_input: Res<ButtonInput<KeyCode>>,
208    mut app_state: ResMut<AppState>,
209    camera: Single<(Entity, Has<OrderIndependentTransparencySettings>), With<Camera3d>>,
210    radio_group_q: Query<(&Children, &RadioGroupSetting), With<RadioGroup>>,
211    setting_q: Query<(Entity, &RadioButtonOptionValue<AppSetting>), With<RadioButton>>,
212    mut commands: Commands,
213) {
214    let new_setting = if keyboard_input.just_pressed(KeyCode::ArrowRight)
215        || keyboard_input.just_pressed(KeyCode::ArrowLeft)
216    {
217        let n = app_state.current_scene_id + SCENES.len();
218        if keyboard_input.pressed(KeyCode::ArrowLeft) {
219            AppSetting::ChangeScene((n - 1) % SCENES.len())
220        } else {
221            AppSetting::ChangeScene((n + 1) % SCENES.len())
222        }
223    } else if keyboard_input.just_pressed(KeyCode::KeyT) {
224        AppSetting::EnableOIT(!app_state.use_oit)
225    } else if keyboard_input.just_pressed(KeyCode::KeyD) {
226        AppSetting::UseDepthPrepass(!app_state.use_depth_prepass)
227    } else {
228        return;
229    };
230
231    change_setting(
232        &new_setting,
233        &mut app_state,
234        camera,
235        radio_group_q,
236        setting_q,
237        &mut commands,
238    );
239}
240
241fn spawn_ui(commands: &mut Commands, app_state: &AppState) {
242    // Invite to interact
243    commands.spawn((
244        Node {
245            position_type: PositionType::Absolute,
246            top: px(12.0),
247            left: px(0.0),
248            right: px(0.0),
249            justify_content: JustifyContent::Center,
250            ..default()
251        },
252        children![(
253            Text::new("Drag view to spin around"),
254            TextFont {
255                font_size: FontSize::Px(18.0),
256                ..default()
257            },
258        )],
259    ));
260
261    // Buttons
262    commands.spawn_scene(bsn! {
263        @main_ui_node_scene()
264        // Prevent the event from bubble up so that view drag does not initiate when interacting with the UI
265        on (|mut event: On<PointerDrag>| {
266            event.propagate(false);
267        })
268        Children [
269            RadioGroupSetting::ChangeScene
270            @feathers_option_buttons(
271                "Scene ([←] or [→])",
272                &(SCENES
273                    .iter()
274                    .enumerate()
275                    .map(|(i, scene)| (AppSetting::ChangeScene(i), scene.1))
276                    .collect::<Vec<_>>()),
277                app_state.current_scene_id,
278            )
279            --
280            RadioGroupSetting::EnableOIT
281            @feathers_option_buttons(
282                "Order Independent [T]ransparency",
283                &[
284                    (AppSetting::EnableOIT(true), "On"),
285                    (AppSetting::EnableOIT(false), "Off")
286                ],
287                if app_state.use_oit {
288                    0
289                } else {
290                    1
291                }
292            )
293            --
294            RadioGroupSetting::UseDepthPrepass
295            @feathers_option_buttons(
296                "[D]epth Prepass",
297                &[
298                    (AppSetting::UseDepthPrepass(true), "On"),
299                    (AppSetting::UseDepthPrepass(false), "Off")
300                ],
301                if app_state.use_depth_prepass {
302                    0
303                } else {
304                    1
305                }
306            )
307        ]
308    });
309}
310
311/// Handles setting changes via the radio buttons
312fn on_radio_value_change(
313    event: On<ValueChange<Entity>>,
314    new_value_q: Query<&RadioButtonOptionValue<AppSetting>>,
315    mut commands: Commands,
316    mut app_state: ResMut<AppState>,
317    camera: Single<(Entity, Has<OrderIndependentTransparencySettings>), With<Camera3d>>,
318    radio_group_q: Query<(&Children, &RadioGroupSetting), With<RadioGroup>>,
319    setting_q: Query<(Entity, &RadioButtonOptionValue<AppSetting>), With<RadioButton>>,
320) {
321    let Ok(RadioButtonOptionValue(setting)) = new_value_q.get(event.value) else {
322        return;
323    };
324
325    change_setting(
326        setting,
327        &mut app_state,
328        camera,
329        radio_group_q,
330        setting_q,
331        &mut commands,
332    );
333}
334
335/// Helper that updates the `app_state`, what is shown on the screen,
336/// and the radio button selections.
337fn change_setting(
338    new_app_setting: &AppSetting,
339    app_state: &mut AppState,
340    camera: Single<(Entity, Has<OrderIndependentTransparencySettings>), With<Camera3d>>,
341    radio_group_q: Query<(&Children, &RadioGroupSetting), With<RadioGroup>>,
342    setting_q: Query<(Entity, &RadioButtonOptionValue<AppSetting>), With<RadioButton>>,
343    commands: &mut Commands,
344) {
345    let radio_group_setting = match *new_app_setting {
346        AppSetting::EnableOIT(value) => {
347            app_state.use_oit = value;
348            if app_state.use_oit {
349                commands.entity(camera.0).insert(app_state.oit_settings);
350            } else {
351                commands
352                    .entity(camera.0)
353                    .remove::<OrderIndependentTransparencySettings>();
354            }
355            RadioGroupSetting::EnableOIT
356        }
357        AppSetting::UseDepthPrepass(value) => {
358            app_state.use_depth_prepass = value;
359
360            if app_state.use_depth_prepass {
361                commands.entity(camera.0).insert(DepthPrepass);
362            } else {
363                commands.entity(camera.0).remove::<DepthPrepass>();
364            }
365            RadioGroupSetting::UseDepthPrepass
366        }
367        AppSetting::ChangeScene(id) => {
368            if id != app_state.current_scene_id {
369                app_state.current_scene_id = id;
370            }
371            RadioGroupSetting::ChangeScene
372            // The actual scene change is handled by scene_change_watcher()
373        }
374    };
375
376    // Update the radio group
377    for (children, group_setting) in radio_group_q.iter() {
378        if *group_setting != radio_group_setting {
379            continue;
380        }
381        for (entity, setting) in setting_q.iter_many(children).matched() {
382            if setting.0 == *new_app_setting {
383                commands.entity(entity).insert(Checked);
384            } else {
385                commands.entity(entity).remove::<Checked>();
386            }
387        }
388    }
389}
390
391/// Watches changes on the `AppState` and loads the appropriate scene
392fn scene_change_watcher(
393    app_state: Res<AppState>,
394    mut prev_scene_id: Local<usize>,
395    entities: Query<Entity, With<Mesh3d>>,
396    mut resources: SceneResources<'_>,
397    mut commands: Commands,
398) {
399    if app_state.is_changed() && *prev_scene_id != app_state.current_scene_id {
400        // Despawn the current scene
401        for e in &entities {
402            commands.entity(e).despawn();
403        }
404        SCENES[app_state.current_scene_id].2(&mut commands, &mut resources);
405        *prev_scene_id = app_state.current_scene_id;
406    }
407}
408
409/// Spawns 3 overlapping spheres
410/// Technically, when using `alpha_to_coverage` with MSAA this particular example wouldn't break,
411/// but it breaks when disabling MSAA and is enough to show the difference between OIT enabled vs disabled.
412fn spawn_spheres(commands: &mut Commands, resources: &mut SceneResources) {
413    let meshes = &mut resources.meshes;
414    let materials = &mut resources.materials;
415
416    let pos_a = Vec3::new(-1.0, 0.75, 0.0);
417    let pos_b = Vec3::new(0.0, -0.75, 0.0);
418    let pos_c = Vec3::new(1.0, 0.75, 0.0);
419
420    let offset = Vec3::new(0.0, 0.0, 0.0);
421
422    let sphere_handle = meshes.add(Sphere::new(2.0).mesh());
423
424    let alpha = 0.25;
425
426    commands.spawn((
427        Mesh3d(sphere_handle.clone()),
428        MeshMaterial3d(materials.add(StandardMaterial {
429            base_color: RED.with_alpha(alpha).into(),
430            alpha_mode: AlphaMode::Blend,
431            ..default()
432        })),
433        Transform::from_translation(pos_a + offset),
434    ));
435    commands.spawn((
436        Mesh3d(sphere_handle.clone()),
437        MeshMaterial3d(materials.add(StandardMaterial {
438            base_color: GREEN.with_alpha(alpha).into(),
439            alpha_mode: AlphaMode::Premultiplied,
440            ..default()
441        })),
442        Transform::from_translation(pos_b + offset),
443    ));
444    commands.spawn((
445        Mesh3d(sphere_handle.clone()),
446        MeshMaterial3d(materials.add(StandardMaterial {
447            base_color: BLUE.with_alpha(alpha).into(),
448            alpha_mode: AlphaMode::Add,
449            ..default()
450        })),
451        Transform::from_translation(pos_c + offset),
452    ));
453}
454
455/// Spawns a stack of transparent quads, which better illustrates the shortcomings
456/// of the standard mesh sorted transparency.
457fn spawn_quads(commands: &mut Commands, resources: &mut SceneResources) {
458    let meshes = &mut resources.meshes;
459    let materials = &mut resources.materials;
460
461    let quad_handle = meshes.add(Rectangle::new(3.0, 3.0).mesh());
462    let xform = |x, y, z| {
463        Transform::from_rotation(Quat::from_rotation_y(0.5))
464            .mul_transform(Transform::from_xyz(x, y, z))
465    };
466
467    // Make the quads double-sided
468    let common_params = StandardMaterial {
469        alpha_mode: AlphaMode::Blend,
470        cull_mode: None,
471        ..default()
472    };
473
474    commands.spawn((
475        Mesh3d(quad_handle.clone()),
476        MeshMaterial3d(materials.add(StandardMaterial {
477            base_color: RED.with_alpha(0.5).into(),
478            ..common_params.clone()
479        })),
480        xform(1.0, -0.1, 0.),
481    ));
482    commands.spawn((
483        Mesh3d(quad_handle.clone()),
484        MeshMaterial3d(materials.add(StandardMaterial {
485            base_color: BLUE.with_alpha(0.8).into(),
486            ..common_params.clone()
487        })),
488        xform(0.5, 0.2, -0.5),
489    ));
490    commands.spawn((
491        Mesh3d(quad_handle.clone()),
492        MeshMaterial3d(materials.add(StandardMaterial {
493            base_color: GREEN.with_green(1.0).with_alpha(0.5).into(),
494            ..common_params.clone()
495        })),
496        xform(0.0, 0.4, -1.),
497    ));
498    commands.spawn((
499        Mesh3d(quad_handle.clone()),
500        MeshMaterial3d(materials.add(StandardMaterial {
501            base_color: YELLOW.with_alpha(0.3).into(),
502            ..common_params.clone()
503        })),
504        xform(-0.5, 0.6, -1.1),
505    ));
506    commands.spawn((
507        Mesh3d(quad_handle.clone()),
508        MeshMaterial3d(materials.add(StandardMaterial {
509            base_color: BLUE.with_alpha(0.2).into(),
510            ..common_params
511        })),
512        xform(-0.8, 0.8, -1.2),
513    ));
514}
515
516/// Spawns a combination of opaque cubes and transparent spheres.
517/// This is useful to make sure transparent meshes drawn with OIT
518/// are properly occluded by opaque meshes.
519fn spawn_occlusion_test(commands: &mut Commands, resources: &mut SceneResources) {
520    let meshes = &mut resources.meshes;
521    let materials = &mut resources.materials;
522
523    let sphere_handle = meshes.add(Sphere::new(1.0).mesh());
524    let cube_handle = meshes.add(Cuboid::from_size(Vec3::ONE).mesh());
525    let cube_material = materials.add(Color::srgb(0.8, 0.7, 0.6));
526
527    // front
528    let x = -2.5;
529    commands.spawn((
530        Mesh3d(cube_handle.clone()),
531        MeshMaterial3d(cube_material.clone()),
532        Transform::from_xyz(x, 0.0, 2.0),
533    ));
534    commands.spawn((
535        Mesh3d(sphere_handle.clone()),
536        MeshMaterial3d(materials.add(StandardMaterial {
537            base_color: RED.with_alpha(0.5).into(),
538            alpha_mode: AlphaMode::Blend,
539            ..default()
540        })),
541        Transform::from_xyz(x, 0., 0.),
542    ));
543
544    // intersection
545    commands.spawn((
546        Mesh3d(cube_handle.clone()),
547        MeshMaterial3d(cube_material.clone()),
548        Transform::from_xyz(x, 0.0, 1.0),
549    ));
550    commands.spawn((
551        Mesh3d(sphere_handle.clone()),
552        MeshMaterial3d(materials.add(StandardMaterial {
553            base_color: RED.with_alpha(0.5).into(),
554            alpha_mode: AlphaMode::Blend,
555            ..default()
556        })),
557        Transform::from_xyz(0., 0., 0.),
558    ));
559
560    // back
561    let x = 2.5;
562    commands.spawn((
563        Mesh3d(cube_handle.clone()),
564        MeshMaterial3d(cube_material.clone()),
565        Transform::from_xyz(x, 0.0, -2.0),
566    ));
567    commands.spawn((
568        Mesh3d(sphere_handle.clone()),
569        MeshMaterial3d(materials.add(StandardMaterial {
570            base_color: RED.with_alpha(0.5).into(),
571            alpha_mode: AlphaMode::Blend,
572            ..default()
573        })),
574        Transform::from_xyz(x, 0., 0.),
575    ));
576}
577
578/// Spawns multiple entities with the same Mesh+Material. They should automatically be drawn using
579/// instancing (when GPU preprocessing is not active) or `MultiDrawIndirect` (when it is).
580fn spawn_auto_instancing_test(commands: &mut Commands, resources: &mut SceneResources) {
581    let meshes = &mut resources.meshes;
582    let materials = &mut resources.materials;
583    let asset_server = &mut resources.asset_server;
584
585    let cube = meshes.add(Cuboid::new(1.0, 1.0, 1.0));
586    let material_handle = materials.add(StandardMaterial {
587        alpha_mode: AlphaMode::Blend,
588        base_color_texture: Some(asset_server.load("textures/slice_square.png")),
589        ..Default::default()
590    });
591    let mut bundles = Vec::with_capacity(3 * 3 * 3);
592
593    for z in -1..=1 {
594        for y in -1..=1 {
595            for x in -1..=1 {
596                bundles.push((
597                    Mesh3d(cube.clone()),
598                    MeshMaterial3d(material_handle.clone()),
599                    Transform::from_xyz(x as f32 * 2.0, y as f32 * 2.0, z as f32 * 2.0),
600                ));
601            }
602        }
603    }
604    commands.spawn_batch(bundles);
605}
606
607const EXTENDED_MATERIAL_SHADER_ASSET_PATH: &str = "shaders/oit_compatible_extended_material.wesl";
608
609/// Material extension that defines the extra data that will be passed to your shader
610/// Used as `ExtendedMaterial<StandardMaterial, CheckeredMaterialExtension>`
611#[derive(Asset, AsBindGroup, Reflect, Debug, Clone, Default)]
612struct CheckeredMaterialExtension {
613    #[uniform(100)]
614    color_1: LinearRgba,
615    #[uniform(100)]
616    color_2: LinearRgba,
617}
618
619impl MaterialExtension for CheckeredMaterialExtension {
620    fn fragment_shader() -> ShaderRef {
621        EXTENDED_MATERIAL_SHADER_ASSET_PATH.into()
622    }
623
624    fn alpha_mode() -> Option<AlphaMode> {
625        Some(AlphaMode::Blend)
626    }
627}
628
629const CUSTOM_MATERIAL_SHADER_ASSET_PATH: &str = "shaders/oit_compatible_custom_material.wesl";
630
631/// A custom material
632#[derive(Asset, AsBindGroup, Reflect, Debug, Clone, Default)]
633struct NoisyOpacityMaterial {
634    #[uniform(0)]
635    color: LinearRgba,
636}
637
638impl Material for NoisyOpacityMaterial {
639    fn fragment_shader() -> ShaderRef {
640        CUSTOM_MATERIAL_SHADER_ASSET_PATH.into()
641    }
642
643    /// For simplicity this material is always transparent
644    fn alpha_mode(&self) -> AlphaMode {
645        AlphaMode::Blend
646    }
647
648    /// Optional: specialize the pipeline to deactivate back face culling
649    fn specialize(
650        _pipeline: &bevy::pbr::MaterialPipeline,
651        descriptor: &mut bevy_render::render_resource::RenderPipelineDescriptor,
652        _layout: &bevy::mesh::MeshVertexBufferLayoutRef,
653        _key: bevy::pbr::MaterialPipelineKey<Self>,
654    ) -> Result<(), bevy_render::render_resource::SpecializedMeshPipelineError> {
655        descriptor.primitive.cull_mode = None;
656        Ok(())
657    }
658}
659/// This scene demonstrates the integration of OIT into extended/custom materials
660fn spawn_custom_material(commands: &mut Commands, resources: &mut SceneResources) {
661    let meshes = &mut resources.meshes;
662    let ext_materials = &mut resources.extended_materials;
663    let custom_materials = &mut resources.custom_materials;
664    let materials = &mut resources.materials;
665
666    let torus = meshes.add(Torus::new(2.0, 3.0));
667
668    // Spawn a torus with an ExtendedMaterial
669    commands.spawn((
670        Mesh3d(torus.clone()),
671        MeshMaterial3d(ext_materials.add(ExtendedMaterial {
672            base: StandardMaterial {
673                cull_mode: None,
674                ..default()
675            },
676            extension: CheckeredMaterialExtension {
677                color_1: LinearRgba::new(0.9, 0.1, 0.2, 0.4),
678                color_2: LinearRgba::new(0.2, 0.1, 0.9, 0.7),
679            },
680        })),
681        Transform::from_rotation(Quat::from_rotation_z(0.4)),
682    ));
683
684    // Spawn a torus with an custom material
685    commands.spawn((
686        Mesh3d(torus.clone()),
687        MeshMaterial3d(custom_materials.add(NoisyOpacityMaterial {
688            color: LinearRgba::new(0.9, 0.6, 0.0, 0.5),
689        })),
690        Transform::from_rotation(Quat::from_rotation_z(1.0)),
691    ));
692
693    // Spawn a torus with a StandardMaterial
694    commands.spawn((
695        Mesh3d(torus.clone()),
696        MeshMaterial3d(materials.add(StandardMaterial {
697            base_color: LinearRgba::new(0.3, 1.0, 0.1, 0.5).into(),
698            alpha_mode: AlphaMode::Blend,
699            cull_mode: None,
700            ..default()
701        })),
702        Transform::from_rotation(Quat::from_rotation_x(1.0)),
703    ));
704}