1use 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#[derive(FromArgs, Resource, Clone, Copy)]
31struct Args {
32 #[argh(switch)]
34 pathtracer: Option<bool>,
35 #[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" )));
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, ..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 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 #[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 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 #[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 commands
393 .entity(descendant)
394 .insert(RaytracingMesh3d(mesh_handle.clone()));
395
396 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 if args.pathtracer == Some(true) {
420 commands.entity(descendant).remove::<Mesh3d>();
421 }
422
423 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, ..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}