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