Skip to main content

transmission/
transmission.rs

1//! This example showcases light transmission
2//!
3//! ## Controls
4//!
5//! | Key Binding        | Action                                               |
6//! |:-------------------|:-----------------------------------------------------|
7//! | `J`/`K`/`L`/`;`    | Change Screen Space Transmission Quality             |
8//! | `O` / `P`          | Decrease / Increase Screen Space Transmission Steps  |
9//! | `1` / `2`          | Decrease / Increase Diffuse Transmission             |
10//! | `Q` / `W`          | Decrease / Increase Specular Transmission            |
11//! | `A` / `S`          | Decrease / Increase Thickness                        |
12//! | `Z` / `X`          | Decrease / Increase IOR                              |
13//! | `E` / `R`          | Decrease / Increase Perceptual Roughness             |
14//! | `U` / `I`          | Decrease / Increase Reflectance                      |
15//! | Arrow Keys         | Control Camera                                       |
16//! | `C`                | Randomize Colors                                     |
17//! | `H`                | Toggle HDR + Bloom                                   |
18//! | `D`                | Toggle Depth Prepass                                 |
19//! | `T`                | Toggle TAA                                           |
20
21use std::f32::consts::PI;
22
23use bevy::{
24    camera::{Exposure, Hdr},
25    color::palettes::css::*,
26    core_pipeline::{prepass::DepthPrepass, tonemapping::Tonemapping},
27    light::{NotShadowCaster, PointLightShadowMap, TransmittedShadowReceiver},
28    math::ops,
29    pbr::{ScreenSpaceTransmission, ScreenSpaceTransmissionQuality},
30    post_process::bloom::Bloom,
31    prelude::*,
32    render::{
33        camera::TemporalJitter,
34        view::{ColorGrading, ColorGradingGlobal},
35    },
36};
37
38// *Note:* TAA is not _required_ for specular transmission, but
39// it _greatly enhances_ the look of the resulting blur effects.
40// Sadly, it's not available under WebGL.
41#[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))]
42use bevy::anti_alias::taa::TemporalAntiAliasing;
43
44use rand::random;
45
46fn main() {
47    App::new()
48        .add_plugins(DefaultPlugins)
49        .insert_resource(ClearColor(Color::BLACK))
50        .insert_resource(PointLightShadowMap { size: 2048 })
51        .insert_resource(GlobalAmbientLight {
52            brightness: 0.0,
53            ..default()
54        })
55        .add_systems(Startup, setup)
56        .add_systems(Update, (example_control_system, flicker_system))
57        .run();
58}
59
60/// set up a simple 3D scene
61fn setup(
62    mut commands: Commands,
63    mut meshes: ResMut<Assets<Mesh>>,
64    mut materials: ResMut<Assets<StandardMaterial>>,
65    asset_server: Res<AssetServer>,
66) {
67    let icosphere_mesh = meshes.add(Sphere::new(0.9).mesh().ico(7).unwrap());
68    let cube_mesh = meshes.add(Cuboid::new(0.7, 0.7, 0.7));
69    let plane_mesh = meshes.add(Plane3d::default().mesh().size(2.0, 2.0));
70    let cylinder_mesh = meshes.add(Cylinder::new(0.5, 2.0).mesh().resolution(50));
71
72    // Cube #1
73    commands.spawn((
74        Mesh3d(cube_mesh.clone()),
75        MeshMaterial3d(materials.add(StandardMaterial::default())),
76        Transform::from_xyz(0.25, 0.5, -2.0).with_rotation(Quat::from_euler(
77            EulerRot::XYZ,
78            1.4,
79            3.7,
80            21.3,
81        )),
82        ExampleControls {
83            color: true,
84            specular_transmission: false,
85            diffuse_transmission: false,
86        },
87    ));
88
89    // Cube #2
90    commands.spawn((
91        Mesh3d(cube_mesh),
92        MeshMaterial3d(materials.add(StandardMaterial::default())),
93        Transform::from_xyz(-0.75, 0.7, -2.0).with_rotation(Quat::from_euler(
94            EulerRot::XYZ,
95            0.4,
96            2.3,
97            4.7,
98        )),
99        ExampleControls {
100            color: true,
101            specular_transmission: false,
102            diffuse_transmission: false,
103        },
104    ));
105
106    // Candle
107    commands.spawn((
108        Mesh3d(cylinder_mesh),
109        MeshMaterial3d(materials.add(StandardMaterial {
110            base_color: Color::srgb(0.9, 0.2, 0.3),
111            diffuse_transmission: 0.7,
112            perceptual_roughness: 0.32,
113            thickness: 0.2,
114            ..default()
115        })),
116        Transform::from_xyz(-1.0, 0.0, 0.0),
117        ExampleControls {
118            color: true,
119            specular_transmission: false,
120            diffuse_transmission: true,
121        },
122    ));
123
124    // Candle Flame
125    let scaled_white = LinearRgba::from(ANTIQUE_WHITE) * 20.;
126    let scaled_orange = LinearRgba::from(ORANGE_RED) * 4.;
127    let emissive = LinearRgba {
128        red: scaled_white.red + scaled_orange.red,
129        green: scaled_white.green + scaled_orange.green,
130        blue: scaled_white.blue + scaled_orange.blue,
131        alpha: 1.0,
132    };
133
134    commands.spawn((
135        Mesh3d(icosphere_mesh.clone()),
136        MeshMaterial3d(materials.add(StandardMaterial {
137            emissive,
138            diffuse_transmission: 1.0,
139            ..default()
140        })),
141        Transform::from_xyz(-1.0, 1.15, 0.0).with_scale(Vec3::new(0.1, 0.2, 0.1)),
142        Flicker,
143        NotShadowCaster,
144    ));
145
146    // Glass Sphere
147    commands.spawn((
148        Mesh3d(icosphere_mesh.clone()),
149        MeshMaterial3d(materials.add(StandardMaterial {
150            base_color: Color::WHITE,
151            specular_transmission: 0.9,
152            diffuse_transmission: 1.0,
153            thickness: 1.8,
154            ior: 1.5,
155            perceptual_roughness: 0.12,
156            ..default()
157        })),
158        Transform::from_xyz(1.0, 0.0, 0.0),
159        ExampleControls {
160            color: true,
161            specular_transmission: true,
162            diffuse_transmission: false,
163        },
164    ));
165
166    // R Sphere
167    commands.spawn((
168        Mesh3d(icosphere_mesh.clone()),
169        MeshMaterial3d(materials.add(StandardMaterial {
170            base_color: RED.into(),
171            specular_transmission: 0.9,
172            diffuse_transmission: 1.0,
173            thickness: 1.8,
174            ior: 1.5,
175            perceptual_roughness: 0.12,
176            ..default()
177        })),
178        Transform::from_xyz(1.0, -0.5, 2.0).with_scale(Vec3::splat(0.5)),
179        ExampleControls {
180            color: true,
181            specular_transmission: true,
182            diffuse_transmission: false,
183        },
184    ));
185
186    // G Sphere
187    commands.spawn((
188        Mesh3d(icosphere_mesh.clone()),
189        MeshMaterial3d(materials.add(StandardMaterial {
190            base_color: LIME.into(),
191            specular_transmission: 0.9,
192            diffuse_transmission: 1.0,
193            thickness: 1.8,
194            ior: 1.5,
195            perceptual_roughness: 0.12,
196            ..default()
197        })),
198        Transform::from_xyz(0.0, -0.5, 2.0).with_scale(Vec3::splat(0.5)),
199        ExampleControls {
200            color: true,
201            specular_transmission: true,
202            diffuse_transmission: false,
203        },
204    ));
205
206    // B Sphere
207    commands.spawn((
208        Mesh3d(icosphere_mesh),
209        MeshMaterial3d(materials.add(StandardMaterial {
210            base_color: BLUE.into(),
211            specular_transmission: 0.9,
212            diffuse_transmission: 1.0,
213            thickness: 1.8,
214            ior: 1.5,
215            perceptual_roughness: 0.12,
216            ..default()
217        })),
218        Transform::from_xyz(-1.0, -0.5, 2.0).with_scale(Vec3::splat(0.5)),
219        ExampleControls {
220            color: true,
221            specular_transmission: true,
222            diffuse_transmission: false,
223        },
224    ));
225
226    // Chessboard Plane
227    let black_material = materials.add(StandardMaterial {
228        base_color: Color::BLACK,
229        reflectance: 0.3,
230        perceptual_roughness: 0.8,
231        ..default()
232    });
233
234    let white_material = materials.add(StandardMaterial {
235        base_color: Color::WHITE,
236        reflectance: 0.3,
237        perceptual_roughness: 0.8,
238        ..default()
239    });
240
241    for x in -3..4 {
242        for z in -3..4 {
243            commands.spawn((
244                Mesh3d(plane_mesh.clone()),
245                MeshMaterial3d(if (x + z) % 2 == 0 {
246                    black_material.clone()
247                } else {
248                    white_material.clone()
249                }),
250                Transform::from_xyz(x as f32 * 2.0, -1.0, z as f32 * 2.0),
251                ExampleControls {
252                    color: true,
253                    specular_transmission: false,
254                    diffuse_transmission: false,
255                },
256            ));
257        }
258    }
259
260    // Paper
261    commands.spawn((
262        Mesh3d(plane_mesh),
263        MeshMaterial3d(materials.add(StandardMaterial {
264            base_color: Color::WHITE,
265            diffuse_transmission: 0.6,
266            perceptual_roughness: 0.8,
267            reflectance: 1.0,
268            double_sided: true,
269            cull_mode: None,
270            ..default()
271        })),
272        Transform::from_xyz(0.0, 0.5, -3.0)
273            .with_scale(Vec3::new(2.0, 1.0, 1.0))
274            .with_rotation(Quat::from_euler(EulerRot::XYZ, PI / 2.0, 0.0, 0.0)),
275        TransmittedShadowReceiver,
276        ExampleControls {
277            specular_transmission: false,
278            color: false,
279            diffuse_transmission: true,
280        },
281    ));
282
283    // Candle Light
284    commands.spawn((
285        Transform::from_xyz(-1.0, 1.7, 0.0),
286        PointLight {
287            color: Color::from(
288                LinearRgba::from(ANTIQUE_WHITE).mix(&LinearRgba::from(ORANGE_RED), 0.2),
289            ),
290            intensity: 4_000.0,
291            radius: 0.2,
292            range: 5.0,
293            shadow_maps_enabled: true,
294            ..default()
295        },
296        Flicker,
297    ));
298
299    // Camera
300    commands.spawn((
301        Camera3d::default(),
302        Transform::from_xyz(1.0, 1.8, 7.0).looking_at(Vec3::ZERO, Vec3::Y),
303        ColorGrading {
304            global: ColorGradingGlobal {
305                post_saturation: 1.2,
306                ..default()
307            },
308            ..default()
309        },
310        Tonemapping::TonyMcMapface,
311        Exposure { ev100: 6.0 },
312        #[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))]
313        Msaa::Off,
314        #[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))]
315        TemporalAntiAliasing::default(),
316        EnvironmentMapLight {
317            intensity: 25.0,
318            diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
319            specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
320            ..default()
321        },
322        Bloom::default(),
323    ));
324
325    // Controls Text
326    commands.spawn((
327        Text::default(),
328        Node {
329            position_type: PositionType::Absolute,
330            top: px(12),
331            left: px(12),
332            ..default()
333        },
334        ExampleDisplay,
335    ));
336}
337
338#[derive(Component)]
339struct Flicker;
340
341#[derive(Component)]
342struct ExampleControls {
343    diffuse_transmission: bool,
344    specular_transmission: bool,
345    color: bool,
346}
347
348struct ExampleState {
349    diffuse_transmission: f32,
350    specular_transmission: f32,
351    thickness: f32,
352    ior: f32,
353    perceptual_roughness: f32,
354    reflectance: f32,
355    auto_camera: bool,
356}
357
358#[derive(Component)]
359struct ExampleDisplay;
360
361impl Default for ExampleState {
362    fn default() -> Self {
363        ExampleState {
364            diffuse_transmission: 0.5,
365            specular_transmission: 0.9,
366            thickness: 1.8,
367            ior: 1.5,
368            perceptual_roughness: 0.12,
369            reflectance: 0.5,
370            auto_camera: true,
371        }
372    }
373}
374
375fn example_control_system(
376    mut commands: Commands,
377    mut materials: ResMut<Assets<StandardMaterial>>,
378    controllable: Query<(&MeshMaterial3d<StandardMaterial>, &ExampleControls)>,
379    camera: Single<
380        (
381            Entity,
382            &mut ScreenSpaceTransmission,
383            &mut Transform,
384            Option<&DepthPrepass>,
385            Option<&TemporalJitter>,
386            Has<Hdr>,
387        ),
388        With<Camera3d>,
389    >,
390    mut display: Single<&mut Text, With<ExampleDisplay>>,
391    mut state: Local<ExampleState>,
392    time: Res<Time>,
393    input: Res<ButtonInput<KeyCode>>,
394) {
395    if input.pressed(KeyCode::Digit2) {
396        state.diffuse_transmission = (state.diffuse_transmission + time.delta_secs()).min(1.0);
397    } else if input.pressed(KeyCode::Digit1) {
398        state.diffuse_transmission = (state.diffuse_transmission - time.delta_secs()).max(0.0);
399    }
400
401    if input.pressed(KeyCode::KeyW) {
402        state.specular_transmission = (state.specular_transmission + time.delta_secs()).min(1.0);
403    } else if input.pressed(KeyCode::KeyQ) {
404        state.specular_transmission = (state.specular_transmission - time.delta_secs()).max(0.0);
405    }
406
407    if input.pressed(KeyCode::KeyS) {
408        state.thickness = (state.thickness + time.delta_secs()).min(5.0);
409    } else if input.pressed(KeyCode::KeyA) {
410        state.thickness = (state.thickness - time.delta_secs()).max(0.0);
411    }
412
413    if input.pressed(KeyCode::KeyX) {
414        state.ior = (state.ior + time.delta_secs()).min(3.0);
415    } else if input.pressed(KeyCode::KeyZ) {
416        state.ior = (state.ior - time.delta_secs()).max(1.0);
417    }
418
419    if input.pressed(KeyCode::KeyI) {
420        state.reflectance = (state.reflectance + time.delta_secs()).min(1.0);
421    } else if input.pressed(KeyCode::KeyU) {
422        state.reflectance = (state.reflectance - time.delta_secs()).max(0.0);
423    }
424
425    if input.pressed(KeyCode::KeyR) {
426        state.perceptual_roughness = (state.perceptual_roughness + time.delta_secs()).min(1.0);
427    } else if input.pressed(KeyCode::KeyE) {
428        state.perceptual_roughness = (state.perceptual_roughness - time.delta_secs()).max(0.0);
429    }
430
431    let randomize_colors = input.just_pressed(KeyCode::KeyC);
432
433    for (material_handle, controls) in &controllable {
434        let mut material = materials.get_mut(material_handle).unwrap();
435        if controls.specular_transmission {
436            material.specular_transmission = state.specular_transmission;
437            material.thickness = state.thickness;
438            material.ior = state.ior;
439            material.perceptual_roughness = state.perceptual_roughness;
440            material.reflectance = state.reflectance;
441        }
442
443        if controls.diffuse_transmission {
444            material.diffuse_transmission = state.diffuse_transmission;
445        }
446
447        if controls.color && randomize_colors {
448            material.base_color =
449                Color::srgba(random(), random(), random(), material.base_color.alpha());
450        }
451    }
452
453    let (
454        camera_entity,
455        mut transmission,
456        mut camera_transform,
457        depth_prepass,
458        temporal_jitter,
459        hdr,
460    ) = camera.into_inner();
461
462    if input.just_pressed(KeyCode::KeyH) {
463        if hdr {
464            commands.entity(camera_entity).remove::<Hdr>();
465        } else {
466            commands.entity(camera_entity).insert(Hdr);
467        }
468    }
469
470    #[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))]
471    if input.just_pressed(KeyCode::KeyD) {
472        if depth_prepass.is_none() {
473            commands.entity(camera_entity).insert(DepthPrepass);
474        } else {
475            commands.entity(camera_entity).remove::<DepthPrepass>();
476        }
477    }
478
479    #[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))]
480    if input.just_pressed(KeyCode::KeyT) {
481        if temporal_jitter.is_none() {
482            commands
483                .entity(camera_entity)
484                .insert((TemporalJitter::default(), TemporalAntiAliasing::default()));
485        } else {
486            commands
487                .entity(camera_entity)
488                .remove::<(TemporalJitter, TemporalAntiAliasing)>();
489        }
490    }
491
492    if input.just_pressed(KeyCode::KeyO) && transmission.steps > 0 {
493        transmission.steps -= 1;
494    }
495
496    if input.just_pressed(KeyCode::KeyP) && transmission.steps < 4 {
497        transmission.steps += 1;
498    }
499
500    if input.just_pressed(KeyCode::KeyJ) {
501        transmission.quality = ScreenSpaceTransmissionQuality::Low;
502    }
503
504    if input.just_pressed(KeyCode::KeyK) {
505        transmission.quality = ScreenSpaceTransmissionQuality::Medium;
506    }
507
508    if input.just_pressed(KeyCode::KeyL) {
509        transmission.quality = ScreenSpaceTransmissionQuality::High;
510    }
511
512    if input.just_pressed(KeyCode::Semicolon) {
513        transmission.quality = ScreenSpaceTransmissionQuality::Ultra;
514    }
515
516    let rotation = if input.pressed(KeyCode::ArrowRight) {
517        state.auto_camera = false;
518        time.delta_secs()
519    } else if input.pressed(KeyCode::ArrowLeft) {
520        state.auto_camera = false;
521        -time.delta_secs()
522    } else if state.auto_camera {
523        time.delta_secs() * 0.25
524    } else {
525        0.0
526    };
527
528    let distance_change =
529        if input.pressed(KeyCode::ArrowDown) && camera_transform.translation.length() < 25.0 {
530            time.delta_secs()
531        } else if input.pressed(KeyCode::ArrowUp) && camera_transform.translation.length() > 2.0 {
532            -time.delta_secs()
533        } else {
534            0.0
535        };
536
537    camera_transform.translation *= ops::exp(distance_change);
538
539    camera_transform.rotate_around(
540        Vec3::ZERO,
541        Quat::from_euler(EulerRot::XYZ, 0.0, rotation, 0.0),
542    );
543
544    display.0 = format!(
545        concat!(
546            " J / K / L / ;  Screen Space Specular Transmissive Quality: {:?}\n",
547            "         O / P  Screen Space Specular Transmissive Steps: {}\n",
548            "         1 / 2  Diffuse Transmission: {:.2}\n",
549            "         Q / W  Specular Transmission: {:.2}\n",
550            "         A / S  Thickness: {:.2}\n",
551            "         Z / X  IOR: {:.2}\n",
552            "         E / R  Perceptual Roughness: {:.2}\n",
553            "         U / I  Reflectance: {:.2}\n",
554            "    Arrow Keys  Control Camera\n",
555            "             C  Randomize Colors\n",
556            "             H  HDR + Bloom: {}\n",
557            "             D  Depth Prepass: {}\n",
558            "             T  TAA: {}\n",
559        ),
560        transmission.quality,
561        transmission.steps,
562        state.diffuse_transmission,
563        state.specular_transmission,
564        state.thickness,
565        state.ior,
566        state.perceptual_roughness,
567        state.reflectance,
568        if hdr { "ON " } else { "OFF" },
569        if cfg!(any(feature = "webgpu", not(target_arch = "wasm32"))) {
570            if depth_prepass.is_some() {
571                "ON "
572            } else {
573                "OFF"
574            }
575        } else {
576            "N/A (WebGL)"
577        },
578        if cfg!(any(feature = "webgpu", not(target_arch = "wasm32"))) {
579            if temporal_jitter.is_some() {
580                if depth_prepass.is_some() {
581                    "ON "
582                } else {
583                    "N/A (Needs Depth Prepass)"
584                }
585            } else {
586                "OFF"
587            }
588        } else {
589            "N/A (WebGL)"
590        },
591    );
592}
593
594fn flicker_system(
595    mut flame: Single<&mut Transform, (With<Flicker>, With<Mesh3d>)>,
596    light: Single<(&mut PointLight, &mut Transform), (With<Flicker>, Without<Mesh3d>)>,
597    time: Res<Time>,
598) {
599    let s = time.elapsed_secs();
600    let a = ops::cos(s * 6.0) * 0.0125 + ops::cos(s * 4.0) * 0.025;
601    let b = ops::cos(s * 5.0) * 0.0125 + ops::cos(s * 3.0) * 0.025;
602    let c = ops::cos(s * 7.0) * 0.0125 + ops::cos(s * 2.0) * 0.025;
603    let (mut light, mut light_transform) = light.into_inner();
604    light.intensity = 4_000.0 + 3000.0 * (a + b + c);
605    flame.translation = Vec3::new(-1.0, 1.23, 0.0);
606    flame.look_at(Vec3::new(-1.0 - c, 1.7 - b, 0.0 - a), Vec3::X);
607    flame.rotate(Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, PI / 2.0));
608    light_transform.translation = Vec3::new(-1.0 - c, 1.7, 0.0 - a);
609    flame.translation = Vec3::new(-1.0 - c, 1.23, 0.0 - a);
610}