Skip to main content

solari/
solari.rs

1//! Demonstrates realtime dynamic raytraced lighting using Bevy Solari.
2
3use argh::FromArgs;
4use bevy::{
5    camera::CameraMainTextureUsages,
6    camera_controller::free_camera::{FreeCamera, FreeCameraPlugin},
7    diagnostic::{Diagnostic, DiagnosticPath, DiagnosticsStore},
8    gltf::GltfMaterialName,
9    image::{ImageAddressMode, ImageLoaderSettings},
10    mesh::{Indices, VertexAttributeValues},
11    post_process::bloom::Bloom,
12    prelude::*,
13    render::{diagnostic::RenderDiagnosticsPlugin, render_resource::TextureUsages},
14    solari::{
15        pathtracer::{Pathtracer, PathtracingPlugin},
16        prelude::{RaytracingMesh3d, SolariLighting, SolariPlugins},
17    },
18    world_serialization::WorldInstanceReady,
19};
20use chacha20::ChaCha8Rng;
21use rand::{RngExt, SeedableRng};
22use std::f32::consts::PI;
23
24#[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
25use bevy::anti_alias::dlss::{
26    Dlss, DlssProjectId, DlssRayReconstructionFeature, DlssRayReconstructionSupported,
27};
28
29/// `bevy_solari` demo.
30#[derive(FromArgs, Resource, Clone, Copy)]
31struct Args {
32    /// use the reference pathtracer instead of the realtime lighting system.
33    #[argh(switch)]
34    pathtracer: Option<bool>,
35    /// stress test a scene with many lights.
36    #[argh(switch)]
37    many_lights: Option<bool>,
38}
39
40fn main() {
41    let args: Args = argh::from_env();
42
43    let mut app = App::new();
44
45    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
46    app.insert_resource(DlssProjectId(bevy_asset::uuid::uuid!(
47        "5417916c-0291-4e3f-8f65-326c1858ab96" // Don't copy paste this - generate your own UUID!
48    )));
49
50    app.add_plugins((
51        DefaultPlugins,
52        SolariPlugins,
53        FreeCameraPlugin,
54        RenderDiagnosticsPlugin,
55    ))
56    .insert_resource(args);
57
58    if args.many_lights == Some(true) {
59        app.add_systems(Startup, setup_many_lights);
60    } else {
61        app.add_systems(Startup, setup_pica_pica);
62    }
63
64    if args.pathtracer == Some(true) {
65        app.add_plugins(PathtracingPlugin);
66    } else {
67        if args.many_lights != Some(true) {
68            app.add_systems(Update, (pause_scene, toggle_lights, patrol_path))
69                .add_systems(PostUpdate, update_control_text);
70        }
71        app.add_systems(PostUpdate, update_performance_text);
72    }
73
74    app.run();
75}
76
77fn setup_pica_pica(
78    mut commands: Commands,
79    asset_server: Res<AssetServer>,
80    args: Res<Args>,
81    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] dlss_rr_supported: Option<
82        Res<DlssRayReconstructionSupported>,
83    >,
84) {
85    commands
86        .spawn((
87            WorldAssetRoot(
88                asset_server.load(
89                    GltfAssetLabel::Scene(0)
90                        .from_asset("https://github.com/bevyengine/bevy_asset_files/raw/2a5950295a8b6d9d051d59c0df69e87abcda58c3/pica_pica/mini_diorama_01.glb")
91                ),
92            ),
93            Transform::from_scale(Vec3::splat(10.0)),
94        ))
95        .observe(add_raytracing_meshes_on_scene_load);
96
97    commands
98        .spawn((
99            WorldAssetRoot(asset_server.load(
100                GltfAssetLabel::Scene(0).from_asset("https://github.com/bevyengine/bevy_asset_files/raw/2a5950295a8b6d9d051d59c0df69e87abcda58c3/pica_pica/robot_01.glb")
101            )),
102            Transform::from_scale(Vec3::splat(2.0))
103                .with_translation(Vec3::new(-2.0, 0.05, -2.1))
104                .with_rotation(Quat::from_rotation_y(PI / 2.0)),
105            PatrolPath {
106                path: vec![
107                    (Vec3::new(-2.0, 0.05, -2.1), Quat::from_rotation_y(PI / 2.0)),
108                    (Vec3::new(2.2, 0.05, -2.1), Quat::from_rotation_y(0.0)),
109                    (
110                        Vec3::new(2.2, 0.05, 2.1),
111                        Quat::from_rotation_y(3.0 * PI / 2.0),
112                    ),
113                    (Vec3::new(-2.0, 0.05, 2.1), Quat::from_rotation_y(PI)),
114                ],
115                i: 0,
116            },
117        ))
118        .observe(add_raytracing_meshes_on_scene_load);
119
120    commands.spawn((
121        DirectionalLight {
122            illuminance: light_consts::lux::FULL_DAYLIGHT,
123            shadow_maps_enabled: false, // Solari replaces shadow mapping
124            ..default()
125        },
126        Transform::from_rotation(Quat::from_xyzw(
127            -0.13334629,
128            -0.86597735,
129            -0.3586996,
130            0.3219264,
131        )),
132    ));
133
134    let mut camera = commands.spawn((
135        Camera3d::default(),
136        Camera {
137            clear_color: ClearColorConfig::Custom(Color::BLACK),
138            ..default()
139        },
140        FreeCamera {
141            walk_speed: 3.0,
142            run_speed: 10.0,
143            ..Default::default()
144        },
145        Transform::from_translation(Vec3::new(0.219417, 2.5764852, 6.9718704)).with_rotation(
146            Quat::from_xyzw(-0.1466768, 0.013738206, 0.002037309, 0.989087),
147        ),
148        // Msaa::Off and CameraMainTextureUsages with STORAGE_BINDING are required for Solari
149        CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING),
150        Msaa::Off,
151    ));
152
153    if args.pathtracer == Some(true) {
154        camera.insert(Pathtracer::default());
155    } else {
156        camera.insert(SolariLighting::default());
157    }
158
159    // Using DLSS Ray Reconstruction for denoising (and cheaper rendering via upscaling) is _highly_ recommended when using Solari
160    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
161    if dlss_rr_supported.is_some() {
162        camera.insert(Dlss::<DlssRayReconstructionFeature> {
163            perf_quality_mode: Default::default(),
164            reset: Default::default(),
165            _phantom_data: Default::default(),
166        });
167    }
168
169    commands.spawn((
170        ControlText,
171        Text::default(),
172        Node {
173            position_type: PositionType::Absolute,
174            bottom: px(12.0),
175            left: px(12.0),
176            ..default()
177        },
178    ));
179
180    commands.spawn((
181        Node {
182            position_type: PositionType::Absolute,
183            right: px(0.0),
184            padding: px(4.0).all(),
185            border_radius: BorderRadius::bottom_left(px(4.0)),
186            ..default()
187        },
188        BackgroundColor(Color::srgba(0.10, 0.10, 0.10, 0.8)),
189        children![(
190            PerformanceText,
191            Text::default(),
192            TextFont {
193                font_size: FontSize::Px(8.0),
194                ..default()
195            },
196        )],
197    ));
198}
199
200fn setup_many_lights(
201    mut commands: Commands,
202    asset_server: Res<AssetServer>,
203    mut meshes: ResMut<Assets<Mesh>>,
204    mut materials: ResMut<Assets<StandardMaterial>>,
205    args: Res<Args>,
206    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] dlss_rr_supported: Option<
207        Res<DlssRayReconstructionSupported>,
208    >,
209) {
210    let mut rng = ChaCha8Rng::seed_from_u64(42);
211
212    let mut plane_mesh = Plane3d::default()
213        .mesh()
214        .size(400.0, 400.0)
215        .build()
216        .with_generated_tangents()
217        .unwrap();
218    match plane_mesh.attribute_mut(Mesh::ATTRIBUTE_UV_0).unwrap() {
219        VertexAttributeValues::Float32x2(items) => {
220            items.iter_mut().flatten().for_each(|x| *x *= 3.0);
221        }
222        _ => unreachable!(),
223    }
224    let plane_mesh = meshes.add(plane_mesh);
225    let cube_mesh = meshes.add(
226        Cuboid::default()
227            .mesh()
228            .build()
229            .with_generated_tangents()
230            .unwrap(),
231    );
232    let sphere_mesh = meshes.add(
233        Sphere::new(1.0)
234            .mesh()
235            .build()
236            .with_generated_tangents()
237            .unwrap(),
238    );
239
240    commands
241        .spawn((
242            RaytracingMesh3d(plane_mesh.clone()),
243            MeshMaterial3d(
244                materials.add(StandardMaterial {
245                    base_color_texture: Some(
246                        asset_server
247                            .load_builder()
248                            .with_settings::<ImageLoaderSettings>(|settings| {
249                                settings
250                                    .sampler
251                                    .get_or_init_descriptor()
252                                    .set_address_mode(ImageAddressMode::Repeat);
253                            })
254                            .load("textures/uv_checker_bw.png"),
255                    ),
256                    perceptual_roughness: 0.0,
257                    ..default()
258                }),
259            ),
260        ))
261        .insert_if(Mesh3d(plane_mesh), || args.pathtracer != Some(true));
262
263    for _ in 0..8000 {
264        commands
265            .spawn((
266                RaytracingMesh3d(cube_mesh.clone()),
267                MeshMaterial3d(materials.add(StandardMaterial {
268                    base_color: Color::srgb(rng.random(), rng.random(), rng.random()),
269                    perceptual_roughness: rng.random(),
270                    ..default()
271                })),
272                Transform::default()
273                    .with_scale(Vec3 {
274                        x: rng.random_range(0.2..=2.0),
275                        y: rng.random_range(0.2..=2.0),
276                        z: rng.random_range(0.2..=2.0),
277                    })
278                    .with_translation(Vec3::new(
279                        rng.random_range(-180.0..=180.0),
280                        0.2,
281                        rng.random_range(-180.0..=180.0),
282                    )),
283            ))
284            .insert_if(Mesh3d(cube_mesh.clone()), || args.pathtracer != Some(true));
285    }
286
287    for x in -10..=10 {
288        for y in -10..=10 {
289            commands
290                .spawn((
291                    RaytracingMesh3d(sphere_mesh.clone()),
292                    MeshMaterial3d(
293                        materials.add(StandardMaterial {
294                            emissive: Color::linear_rgb(
295                                rng.random::<f32>() * 60000.0,
296                                rng.random::<f32>() * 60000.0,
297                                rng.random::<f32>() * 60000.0,
298                            )
299                            .into(),
300                            ..default()
301                        }),
302                    ),
303                    Transform::default().with_translation(Vec3::new(
304                        (x * 20) as f32,
305                        7.0,
306                        (y * 20) as f32,
307                    )),
308                ))
309                .insert_if(Mesh3d(sphere_mesh.clone()), || {
310                    args.pathtracer != Some(true)
311                });
312        }
313    }
314
315    let mut camera = commands.spawn((
316        Camera3d::default(),
317        Camera {
318            clear_color: ClearColorConfig::Custom(Color::BLACK),
319            ..default()
320        },
321        FreeCamera {
322            walk_speed: 3.0,
323            run_speed: 10.0,
324            ..Default::default()
325        },
326        Transform::from_translation(Vec3::new(6.11329, 166.74896, 451.8226)).with_rotation(
327            Quat::from_xyzw(-0.183938, 0.009093744, 0.0017017953, 0.9828943),
328        ),
329        // Msaa::Off and CameraMainTextureUsages with STORAGE_BINDING are required for Solari
330        CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING),
331        Msaa::Off,
332        Bloom {
333            intensity: 0.1,
334            ..Bloom::NATURAL
335        },
336    ));
337
338    if args.pathtracer == Some(true) {
339        camera.insert(Pathtracer::default());
340    } else {
341        camera.insert(SolariLighting::default());
342    }
343
344    // Using DLSS Ray Reconstruction for denoising (and cheaper rendering via upscaling) is _highly_ recommended when using Solari
345    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
346    if dlss_rr_supported.is_some() {
347        camera.insert(Dlss::<DlssRayReconstructionFeature> {
348            perf_quality_mode: Default::default(),
349            reset: Default::default(),
350            _phantom_data: Default::default(),
351        });
352    }
353
354    commands.spawn((
355        Node {
356            position_type: PositionType::Absolute,
357            right: px(0.0),
358            padding: px(4.0).all(),
359            border_radius: BorderRadius::bottom_left(px(4.0)),
360            ..default()
361        },
362        BackgroundColor(Color::srgba(0.10, 0.10, 0.10, 0.8)),
363        children![(
364            PerformanceText,
365            Text::default(),
366            TextFont {
367                font_size: FontSize::Px(8.0),
368                ..default()
369            },
370        )],
371    ));
372}
373
374fn add_raytracing_meshes_on_scene_load(
375    scene_ready: On<WorldInstanceReady>,
376    children: Query<&Children>,
377    mesh_query: Query<(
378        &Mesh3d,
379        &MeshMaterial3d<StandardMaterial>,
380        Option<&GltfMaterialName>,
381    )>,
382    mut meshes: ResMut<Assets<Mesh>>,
383    mut materials: ResMut<Assets<StandardMaterial>>,
384    mut commands: Commands,
385    args: Res<Args>,
386) {
387    for descendant in children.iter_descendants(scene_ready.entity) {
388        if let Ok((Mesh3d(mesh_handle), MeshMaterial3d(material_handle), material_name)) =
389            mesh_query.get(descendant)
390        {
391            // Add raytracing mesh component
392            commands
393                .entity(descendant)
394                .insert(RaytracingMesh3d(mesh_handle.clone()));
395
396            // Ensure meshes are Solari compatible
397            let mut mesh = meshes.get_mut(mesh_handle).unwrap();
398            if !mesh.contains_attribute(Mesh::ATTRIBUTE_UV_0) {
399                let vertex_count = mesh.count_vertices();
400                mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0]; vertex_count]);
401                mesh.insert_attribute(
402                    Mesh::ATTRIBUTE_TANGENT,
403                    vec![[0.0, 0.0, 0.0, 0.0]; vertex_count],
404                );
405            }
406            if !mesh.contains_attribute(Mesh::ATTRIBUTE_TANGENT) {
407                mesh.generate_tangents().unwrap();
408            }
409            if mesh.contains_attribute(Mesh::ATTRIBUTE_UV_1) {
410                mesh.remove_attribute(Mesh::ATTRIBUTE_UV_1);
411            }
412            if let Some(indices) = mesh.indices_mut()
413                && let Indices::U16(_) = indices
414            {
415                *indices = Indices::U32(indices.iter().map(|i| i as u32).collect());
416            }
417
418            // Prevent rasterization if using pathtracer
419            if args.pathtracer == Some(true) {
420                commands.entity(descendant).remove::<Mesh3d>();
421            }
422
423            // Adjust scene materials to better demo Solari features
424            if material_name.map(|s| s.0.as_str()) == Some("material") {
425                let mut material = materials.get_mut(material_handle).unwrap();
426                material.emissive = LinearRgba::BLACK;
427            }
428            if material_name.map(|s| s.0.as_str()) == Some("Lights") {
429                let mut material = materials.get_mut(material_handle).unwrap();
430                material.emissive =
431                    LinearRgba::from(Color::srgb(0.941, 0.714, 0.043)) * 1_000_000.0;
432                material.alpha_mode = AlphaMode::Opaque;
433                material.specular_transmission = 0.0;
434
435                commands.insert_resource(RobotLightMaterial(material_handle.clone()));
436            }
437            if material_name.map(|s| s.0.as_str()) == Some("Glass_Dark_01") {
438                let mut material = materials.get_mut(material_handle).unwrap();
439                material.alpha_mode = AlphaMode::Opaque;
440                material.specular_transmission = 0.0;
441            }
442        }
443    }
444}
445
446fn pause_scene(mut time: ResMut<Time<Virtual>>, key_input: Res<ButtonInput<KeyCode>>) {
447    if key_input.just_pressed(KeyCode::Space) {
448        time.toggle();
449    }
450}
451
452#[derive(Resource)]
453struct RobotLightMaterial(Handle<StandardMaterial>);
454
455fn toggle_lights(
456    key_input: Res<ButtonInput<KeyCode>>,
457    robot_light_material: Option<Res<RobotLightMaterial>>,
458    mut materials: ResMut<Assets<StandardMaterial>>,
459    directional_light: Query<Entity, With<DirectionalLight>>,
460    mut commands: Commands,
461) {
462    if key_input.just_pressed(KeyCode::Digit1) {
463        if let Ok(directional_light) = directional_light.single() {
464            commands.entity(directional_light).despawn();
465        } else {
466            commands.spawn((
467                DirectionalLight {
468                    illuminance: light_consts::lux::FULL_DAYLIGHT,
469                    shadow_maps_enabled: false, // Solari replaces shadow mapping
470                    ..default()
471                },
472                Transform::from_rotation(Quat::from_xyzw(
473                    -0.13334629,
474                    -0.86597735,
475                    -0.3586996,
476                    0.3219264,
477                )),
478            ));
479        }
480    }
481
482    if key_input.just_pressed(KeyCode::Digit2)
483        && let Some(robot_light_material) = robot_light_material
484    {
485        let mut material = materials.get_mut(&robot_light_material.0).unwrap();
486        if material.emissive == LinearRgba::BLACK {
487            material.emissive = LinearRgba::from(Color::srgb(0.941, 0.714, 0.043)) * 1_000_000.0;
488        } else {
489            material.emissive = LinearRgba::BLACK;
490        }
491    }
492}
493
494#[derive(Component)]
495struct PatrolPath {
496    path: Vec<(Vec3, Quat)>,
497    i: usize,
498}
499
500fn patrol_path(mut query: Query<(&mut PatrolPath, &mut Transform)>, time: Res<Time<Virtual>>) {
501    for (mut path, mut transform) in query.iter_mut() {
502        let (mut target_position, mut target_rotation) = path.path[path.i];
503        let mut distance_to_target = transform.translation.distance(target_position);
504        if distance_to_target < 0.01 {
505            transform.translation = target_position;
506            transform.rotation = target_rotation;
507
508            path.i = (path.i + 1) % path.path.len();
509            (target_position, target_rotation) = path.path[path.i];
510            distance_to_target = transform.translation.distance(target_position);
511        }
512
513        let direction = (target_position - transform.translation).normalize();
514        let movement = direction * time.delta_secs();
515
516        if movement.length() > distance_to_target {
517            transform.translation = target_position;
518            transform.rotation = target_rotation;
519        } else {
520            transform.translation += movement;
521        }
522    }
523}
524
525#[derive(Component)]
526struct ControlText;
527
528fn update_control_text(
529    mut text: Single<&mut Text, With<ControlText>>,
530    robot_light_material: Option<Res<RobotLightMaterial>>,
531    materials: Res<Assets<StandardMaterial>>,
532    directional_light: Query<Entity, With<DirectionalLight>>,
533    time: Res<Time<Virtual>>,
534    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] dlss_rr_supported: Option<
535        Res<DlssRayReconstructionSupported>,
536    >,
537) {
538    text.0.clear();
539
540    if time.is_paused() {
541        text.0.push_str("(Space): Resume");
542    } else {
543        text.0.push_str("(Space): Pause");
544    }
545
546    if directional_light.single().is_ok() {
547        text.0.push_str("\n(1): Disable directional light");
548    } else {
549        text.0.push_str("\n(1): Enable directional light");
550    }
551
552    match robot_light_material.and_then(|m| materials.get(&m.0)) {
553        Some(robot_light_material) if robot_light_material.emissive != LinearRgba::BLACK => {
554            text.0.push_str("\n(2): Disable robot emissive light");
555        }
556        _ => {
557            text.0.push_str("\n(2): Enable robot emissive light");
558        }
559    }
560
561    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
562    if dlss_rr_supported.is_some() {
563        text.0
564            .push_str("\nDenoising: DLSS Ray Reconstruction enabled");
565    } else {
566        text.0
567            .push_str("\nDenoising: DLSS Ray Reconstruction not supported");
568    }
569
570    #[cfg(any(not(feature = "dlss"), feature = "force_disable_dlss"))]
571    text.0
572        .push_str("\nDenoising: App not compiled with DLSS support");
573}
574
575#[derive(Component)]
576struct PerformanceText;
577
578fn update_performance_text(
579    mut text: Single<&mut Text, With<PerformanceText>>,
580    diagnostics: Res<DiagnosticsStore>,
581) {
582    text.0.clear();
583
584    let mut total = 0.0;
585    let mut add_diagnostic = |name: &str, path: &'static str| {
586        let path = DiagnosticPath::new(path);
587        if let Some(value) = diagnostics.get(&path).and_then(Diagnostic::smoothed) {
588            text.push_str(&format!("{name:17}  {value:.2} ms\n"));
589            total += value;
590        }
591    };
592
593    (add_diagnostic)(
594        "Light tiles",
595        "render/solari_lighting/presample_light_tiles/elapsed_gpu",
596    );
597    (add_diagnostic)(
598        "World cache",
599        "render/solari_lighting/world_cache/elapsed_gpu",
600    );
601    (add_diagnostic)(
602        "Direct lighting",
603        "render/solari_lighting/direct_lighting/elapsed_gpu",
604    );
605    (add_diagnostic)(
606        "Diffuse indirect",
607        "render/solari_lighting/diffuse_indirect_lighting/elapsed_gpu",
608    );
609    (add_diagnostic)(
610        "Specular indirect",
611        "render/solari_lighting/specular_indirect_lighting/elapsed_gpu",
612    );
613    (add_diagnostic)("DLSS-RR", "render/dlss_ray_reconstruction/elapsed_gpu");
614    text.push_str(&format!("{:17}  {total:.2} ms\n", "Total"));
615
616    if let Some(world_cache_active_cells_count) = diagnostics
617        .get(&DiagnosticPath::new(
618            "render/solari_lighting/world_cache_active_cells_count",
619        ))
620        .and_then(Diagnostic::smoothed)
621    {
622        text.push_str(&format!(
623            "\nWorld cache cells {} ({:.0}%)",
624            world_cache_active_cells_count as u32,
625            (world_cache_active_cells_count * 100.0) / (2u64.pow(20) as f64)
626        ));
627    }
628}