solari/
solari.rs

1//! Demonstrates realtime dynamic raytraced lighting using Bevy Solari.
2
3#[path = "../helpers/camera_controller.rs"]
4mod camera_controller;
5
6use argh::FromArgs;
7use bevy::{
8    camera::CameraMainTextureUsages,
9    gltf::GltfMaterialName,
10    prelude::*,
11    render::render_resource::TextureUsages,
12    scene::SceneInstanceReady,
13    solari::{
14        pathtracer::{Pathtracer, PathtracingPlugin},
15        prelude::{RaytracingMesh3d, SolariLighting, SolariPlugins},
16    },
17};
18use camera_controller::{CameraController, CameraControllerPlugin};
19use std::f32::consts::PI;
20
21#[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
22use bevy::anti_alias::dlss::{
23    Dlss, DlssProjectId, DlssRayReconstructionFeature, DlssRayReconstructionSupported,
24};
25
26/// `bevy_solari` demo.
27#[derive(FromArgs, Resource, Clone, Copy)]
28struct Args {
29    /// use the reference pathtracer instead of the realtime lighting system.
30    #[argh(switch)]
31    pathtracer: Option<bool>,
32}
33
34fn main() {
35    let args: Args = argh::from_env();
36
37    let mut app = App::new();
38
39    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
40    app.insert_resource(DlssProjectId(bevy_asset::uuid::uuid!(
41        "5417916c-0291-4e3f-8f65-326c1858ab96" // Don't copy paste this - generate your own UUID!
42    )));
43
44    app.add_plugins((DefaultPlugins, SolariPlugins, CameraControllerPlugin))
45        .insert_resource(args)
46        .add_systems(Startup, setup);
47
48    if args.pathtracer == Some(true) {
49        app.add_plugins(PathtracingPlugin);
50    } else {
51        app.add_systems(Update, (pause_scene, toggle_lights, patrol_path));
52        app.add_systems(PostUpdate, update_text);
53    }
54
55    app.run();
56}
57
58fn setup(
59    mut commands: Commands,
60    asset_server: Res<AssetServer>,
61    args: Res<Args>,
62    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] dlss_rr_supported: Option<
63        Res<DlssRayReconstructionSupported>,
64    >,
65) {
66    commands
67        .spawn((
68            SceneRoot(
69                asset_server.load(
70                    GltfAssetLabel::Scene(0)
71                        .from_asset("https://github.com/bevyengine/bevy_asset_files/raw/2a5950295a8b6d9d051d59c0df69e87abcda58c3/pica_pica/mini_diorama_01.glb")
72                ),
73            ),
74            Transform::from_scale(Vec3::splat(10.0)),
75        ))
76        .observe(add_raytracing_meshes_on_scene_load);
77
78    commands
79        .spawn((
80            SceneRoot(asset_server.load(
81                GltfAssetLabel::Scene(0).from_asset("https://github.com/bevyengine/bevy_asset_files/raw/2a5950295a8b6d9d051d59c0df69e87abcda58c3/pica_pica/robot_01.glb")
82            )),
83            Transform::from_scale(Vec3::splat(2.0))
84                .with_translation(Vec3::new(-2.0, 0.05, -2.1))
85                .with_rotation(Quat::from_rotation_y(PI / 2.0)),
86            PatrolPath {
87                path: vec![
88                    (Vec3::new(-2.0, 0.05, -2.1), Quat::from_rotation_y(PI / 2.0)),
89                    (Vec3::new(2.2, 0.05, -2.1), Quat::from_rotation_y(0.0)),
90                    (
91                        Vec3::new(2.2, 0.05, 2.1),
92                        Quat::from_rotation_y(3.0 * PI / 2.0),
93                    ),
94                    (Vec3::new(-2.0, 0.05, 2.1), Quat::from_rotation_y(PI)),
95                ],
96                i: 0,
97            },
98        ))
99        .observe(add_raytracing_meshes_on_scene_load);
100
101    commands.spawn((
102        DirectionalLight {
103            illuminance: light_consts::lux::FULL_DAYLIGHT,
104            shadows_enabled: false, // Solari replaces shadow mapping
105            ..default()
106        },
107        Transform::from_rotation(Quat::from_xyzw(
108            -0.13334629,
109            -0.86597735,
110            -0.3586996,
111            0.3219264,
112        )),
113    ));
114
115    let mut camera = commands.spawn((
116        Camera3d::default(),
117        Camera {
118            clear_color: ClearColorConfig::Custom(Color::BLACK),
119            ..default()
120        },
121        CameraController {
122            walk_speed: 3.0,
123            run_speed: 10.0,
124            ..Default::default()
125        },
126        Transform::from_translation(Vec3::new(0.219417, 2.5764852, 6.9718704)).with_rotation(
127            Quat::from_xyzw(-0.1466768, 0.013738206, 0.002037309, 0.989087),
128        ),
129        // Msaa::Off and CameraMainTextureUsages with STORAGE_BINDING are required for Solari
130        CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING),
131        Msaa::Off,
132    ));
133
134    if args.pathtracer == Some(true) {
135        camera.insert(Pathtracer::default());
136    } else {
137        camera.insert(SolariLighting::default());
138    }
139
140    // Using DLSS Ray Reconstruction for denoising (and cheaper rendering via upscaling) is _highly_ recommended when using Solari
141    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
142    if dlss_rr_supported.is_some() {
143        camera.insert(Dlss::<DlssRayReconstructionFeature> {
144            perf_quality_mode: Default::default(),
145            reset: Default::default(),
146            _phantom_data: Default::default(),
147        });
148    }
149
150    commands.spawn((
151        Text::default(),
152        Node {
153            position_type: PositionType::Absolute,
154            bottom: Val::Px(12.0),
155            left: Val::Px(12.0),
156            ..default()
157        },
158    ));
159}
160
161fn add_raytracing_meshes_on_scene_load(
162    scene_ready: On<SceneInstanceReady>,
163    children: Query<&Children>,
164    mesh_query: Query<(
165        &Mesh3d,
166        &MeshMaterial3d<StandardMaterial>,
167        Option<&GltfMaterialName>,
168    )>,
169    mut meshes: ResMut<Assets<Mesh>>,
170    mut materials: ResMut<Assets<StandardMaterial>>,
171    mut commands: Commands,
172    args: Res<Args>,
173) {
174    for descendant in children.iter_descendants(scene_ready.entity) {
175        if let Ok((Mesh3d(mesh_handle), MeshMaterial3d(material_handle), material_name)) =
176            mesh_query.get(descendant)
177        {
178            // Add raytracing mesh component
179            commands
180                .entity(descendant)
181                .insert(RaytracingMesh3d(mesh_handle.clone()));
182
183            // Ensure meshes are Solari compatible
184            let mesh = meshes.get_mut(mesh_handle).unwrap();
185            if !mesh.contains_attribute(Mesh::ATTRIBUTE_UV_0) {
186                let vertex_count = mesh.count_vertices();
187                mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0]; vertex_count]);
188                mesh.insert_attribute(
189                    Mesh::ATTRIBUTE_TANGENT,
190                    vec![[0.0, 0.0, 0.0, 0.0]; vertex_count],
191                );
192            }
193            if !mesh.contains_attribute(Mesh::ATTRIBUTE_TANGENT) {
194                mesh.generate_tangents().unwrap();
195            }
196            if mesh.contains_attribute(Mesh::ATTRIBUTE_UV_1) {
197                mesh.remove_attribute(Mesh::ATTRIBUTE_UV_1);
198            }
199
200            // Prevent rasterization if using pathtracer
201            if args.pathtracer == Some(true) {
202                commands.entity(descendant).remove::<Mesh3d>();
203            }
204
205            // Adjust scene materials to better demo Solari features
206            if material_name.map(|s| s.0.as_str()) == Some("material") {
207                let material = materials.get_mut(material_handle).unwrap();
208                material.emissive = LinearRgba::BLACK;
209            }
210            if material_name.map(|s| s.0.as_str()) == Some("Lights") {
211                let material = materials.get_mut(material_handle).unwrap();
212                material.emissive =
213                    LinearRgba::from(Color::srgb(0.941, 0.714, 0.043)) * 1_000_000.0;
214                material.alpha_mode = AlphaMode::Opaque;
215                material.specular_transmission = 0.0;
216
217                commands.insert_resource(RobotLightMaterial(material_handle.clone()));
218            }
219            if material_name.map(|s| s.0.as_str()) == Some("Glass_Dark_01") {
220                let material = materials.get_mut(material_handle).unwrap();
221                material.alpha_mode = AlphaMode::Opaque;
222                material.specular_transmission = 0.0;
223            }
224        }
225    }
226}
227
228fn pause_scene(mut time: ResMut<Time<Virtual>>, key_input: Res<ButtonInput<KeyCode>>) {
229    if key_input.just_pressed(KeyCode::Space) {
230        if time.is_paused() {
231            time.unpause();
232        } else {
233            time.pause();
234        }
235    }
236}
237
238#[derive(Resource)]
239struct RobotLightMaterial(Handle<StandardMaterial>);
240
241fn toggle_lights(
242    key_input: Res<ButtonInput<KeyCode>>,
243    robot_light_material: Option<Res<RobotLightMaterial>>,
244    mut materials: ResMut<Assets<StandardMaterial>>,
245    directional_light: Query<Entity, With<DirectionalLight>>,
246    mut commands: Commands,
247) {
248    if key_input.just_pressed(KeyCode::Digit1) {
249        if let Ok(directional_light) = directional_light.single() {
250            commands.entity(directional_light).despawn();
251        } else {
252            commands.spawn((
253                DirectionalLight {
254                    illuminance: light_consts::lux::FULL_DAYLIGHT,
255                    shadows_enabled: false, // Solari replaces shadow mapping
256                    ..default()
257                },
258                Transform::from_rotation(Quat::from_xyzw(
259                    -0.13334629,
260                    -0.86597735,
261                    -0.3586996,
262                    0.3219264,
263                )),
264            ));
265        }
266    }
267
268    if key_input.just_pressed(KeyCode::Digit2)
269        && let Some(robot_light_material) = robot_light_material
270    {
271        let material = materials.get_mut(&robot_light_material.0).unwrap();
272        if material.emissive == LinearRgba::BLACK {
273            material.emissive = LinearRgba::from(Color::srgb(0.941, 0.714, 0.043)) * 1_000_000.0;
274        } else {
275            material.emissive = LinearRgba::BLACK;
276        }
277    }
278}
279
280#[derive(Component)]
281struct PatrolPath {
282    path: Vec<(Vec3, Quat)>,
283    i: usize,
284}
285
286fn patrol_path(mut query: Query<(&mut PatrolPath, &mut Transform)>, time: Res<Time<Virtual>>) {
287    for (mut path, mut transform) in query.iter_mut() {
288        let (mut target_position, mut target_rotation) = path.path[path.i];
289        let mut distance_to_target = transform.translation.distance(target_position);
290        if distance_to_target < 0.01 {
291            transform.translation = target_position;
292            transform.rotation = target_rotation;
293
294            path.i = (path.i + 1) % path.path.len();
295            (target_position, target_rotation) = path.path[path.i];
296            distance_to_target = transform.translation.distance(target_position);
297        }
298
299        let direction = (target_position - transform.translation).normalize();
300        let movement = direction * time.delta_secs();
301
302        if movement.length() > distance_to_target {
303            transform.translation = target_position;
304            transform.rotation = target_rotation;
305        } else {
306            transform.translation += movement;
307        }
308    }
309}
310
311fn update_text(
312    mut text: Single<&mut Text>,
313    robot_light_material: Option<Res<RobotLightMaterial>>,
314    materials: Res<Assets<StandardMaterial>>,
315    directional_light: Query<Entity, With<DirectionalLight>>,
316    time: Res<Time<Virtual>>,
317    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] dlss_rr_supported: Option<
318        Res<DlssRayReconstructionSupported>,
319    >,
320) {
321    text.0.clear();
322
323    if time.is_paused() {
324        text.0.push_str("(Space): Resume");
325    } else {
326        text.0.push_str("(Space): Pause");
327    }
328
329    if directional_light.single().is_ok() {
330        text.0.push_str("\n(1): Disable directional light");
331    } else {
332        text.0.push_str("\n(1): Enable directional light");
333    }
334
335    match robot_light_material.and_then(|m| materials.get(&m.0)) {
336        Some(robot_light_material) if robot_light_material.emissive != LinearRgba::BLACK => {
337            text.0.push_str("\n(2): Disable robot emissive light");
338        }
339        _ => {
340            text.0.push_str("\n(2): Enable robot emissive light");
341        }
342    }
343
344    #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))]
345    if dlss_rr_supported.is_some() {
346        text.0
347            .push_str("\nDenoising: DLSS Ray Reconstruction enabled");
348    } else {
349        text.0
350            .push_str("\nDenoising: DLSS Ray Reconstruction not supported");
351    }
352
353    #[cfg(any(not(feature = "dlss"), feature = "force_disable_dlss"))]
354    text.0
355        .push_str("\nDenoising: App not compiled with DLSS support");
356}