//! Demonstrates realtime dynamic raytraced lighting using Bevy Solari. use argh::FromArgs; use bevy::{ camera::CameraMainTextureUsages, camera_controller::free_camera::{FreeCamera, FreeCameraPlugin}, diagnostic::{Diagnostic, DiagnosticPath, DiagnosticsStore}, gltf::GltfMaterialName, image::{ImageAddressMode, ImageLoaderSettings}, mesh::{Indices, VertexAttributeValues}, post_process::bloom::Bloom, prelude::*, render::{diagnostic::RenderDiagnosticsPlugin, render_resource::TextureUsages}, solari::{ pathtracer::{Pathtracer, PathtracingPlugin}, prelude::{RaytracingMesh3d, SolariLighting, SolariPlugins}, }, world_serialization::WorldInstanceReady, }; use chacha20::ChaCha8Rng; use rand::{RngExt, SeedableRng}; use std::f32::consts::PI; #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] use bevy::anti_alias::dlss::{ Dlss, DlssProjectId, DlssRayReconstructionFeature, DlssRayReconstructionSupported, }; /// `bevy_solari` demo. #[derive(FromArgs, Resource, Clone, Copy)] struct Args { /// use the reference pathtracer instead of the realtime lighting system. #[argh(switch)] pathtracer: Option, /// stress test a scene with many lights. #[argh(switch)] many_lights: Option, } fn main() { let args: Args = argh::from_env(); let mut app = App::new(); #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] app.insert_resource(DlssProjectId(bevy_asset::uuid::uuid!( "5417916c-0291-4e3f-8f65-326c1858ab96" // Don't copy paste this - generate your own UUID! ))); app.add_plugins(( DefaultPlugins, SolariPlugins, FreeCameraPlugin, RenderDiagnosticsPlugin, )) .insert_resource(args); if args.many_lights == Some(true) { app.add_systems(Startup, setup_many_lights); } else { app.add_systems(Startup, setup_pica_pica); } if args.pathtracer == Some(true) { app.add_plugins(PathtracingPlugin); } else { if args.many_lights != Some(true) { app.add_systems(Update, (pause_scene, toggle_lights, patrol_path)) .add_systems(PostUpdate, update_control_text); } app.add_systems(PostUpdate, update_performance_text); } app.run(); } fn setup_pica_pica( mut commands: Commands, asset_server: Res, args: Res, #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] dlss_rr_supported: Option< Res, >, ) { commands .spawn(( WorldAssetRoot( asset_server.load( GltfAssetLabel::Scene(0) .from_asset("https://github.com/bevyengine/bevy_asset_files/raw/2a5950295a8b6d9d051d59c0df69e87abcda58c3/pica_pica/mini_diorama_01.glb") ), ), Transform::from_scale(Vec3::splat(10.0)), )) .observe(add_raytracing_meshes_on_scene_load); commands .spawn(( WorldAssetRoot(asset_server.load( GltfAssetLabel::Scene(0).from_asset("https://github.com/bevyengine/bevy_asset_files/raw/2a5950295a8b6d9d051d59c0df69e87abcda58c3/pica_pica/robot_01.glb") )), Transform::from_scale(Vec3::splat(2.0)) .with_translation(Vec3::new(-2.0, 0.05, -2.1)) .with_rotation(Quat::from_rotation_y(PI / 2.0)), PatrolPath { path: vec![ (Vec3::new(-2.0, 0.05, -2.1), Quat::from_rotation_y(PI / 2.0)), (Vec3::new(2.2, 0.05, -2.1), Quat::from_rotation_y(0.0)), ( Vec3::new(2.2, 0.05, 2.1), Quat::from_rotation_y(3.0 * PI / 2.0), ), (Vec3::new(-2.0, 0.05, 2.1), Quat::from_rotation_y(PI)), ], i: 0, }, )) .observe(add_raytracing_meshes_on_scene_load); commands.spawn(( DirectionalLight { illuminance: light_consts::lux::FULL_DAYLIGHT, shadow_maps_enabled: false, // Solari replaces shadow mapping ..default() }, Transform::from_rotation(Quat::from_xyzw( -0.13334629, -0.86597735, -0.3586996, 0.3219264, )), )); let mut camera = commands.spawn(( Camera3d::default(), Camera { clear_color: ClearColorConfig::Custom(Color::BLACK), ..default() }, FreeCamera { walk_speed: 3.0, run_speed: 10.0, ..Default::default() }, Transform::from_translation(Vec3::new(0.219417, 2.5764852, 6.9718704)).with_rotation( Quat::from_xyzw(-0.1466768, 0.013738206, 0.002037309, 0.989087), ), // Msaa::Off and CameraMainTextureUsages with STORAGE_BINDING are required for Solari CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING), Msaa::Off, )); if args.pathtracer == Some(true) { camera.insert(Pathtracer::default()); } else { camera.insert(SolariLighting::default()); } // Using DLSS Ray Reconstruction for denoising (and cheaper rendering via upscaling) is _highly_ recommended when using Solari #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] if dlss_rr_supported.is_some() { camera.insert(Dlss:: { perf_quality_mode: Default::default(), reset: Default::default(), _phantom_data: Default::default(), }); } commands.spawn(( ControlText, Text::default(), Node { position_type: PositionType::Absolute, bottom: px(12.0), left: px(12.0), ..default() }, )); commands.spawn(( Node { position_type: PositionType::Absolute, right: px(0.0), padding: px(4.0).all(), border_radius: BorderRadius::bottom_left(px(4.0)), ..default() }, BackgroundColor(Color::srgba(0.10, 0.10, 0.10, 0.8)), children![( PerformanceText, Text::default(), TextFont { font_size: FontSize::Px(8.0), ..default() }, )], )); } fn setup_many_lights( mut commands: Commands, asset_server: Res, mut meshes: ResMut>, mut materials: ResMut>, args: Res, #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] dlss_rr_supported: Option< Res, >, ) { let mut rng = ChaCha8Rng::seed_from_u64(42); let mut plane_mesh = Plane3d::default() .mesh() .size(400.0, 400.0) .build() .with_generated_tangents() .unwrap(); match plane_mesh.attribute_mut(Mesh::ATTRIBUTE_UV_0).unwrap() { VertexAttributeValues::Float32x2(items) => { items.iter_mut().flatten().for_each(|x| *x *= 3.0); } _ => unreachable!(), } let plane_mesh = meshes.add(plane_mesh); let cube_mesh = meshes.add( Cuboid::default() .mesh() .build() .with_generated_tangents() .unwrap(), ); let sphere_mesh = meshes.add( Sphere::new(1.0) .mesh() .build() .with_generated_tangents() .unwrap(), ); commands .spawn(( RaytracingMesh3d(plane_mesh.clone()), MeshMaterial3d( materials.add(StandardMaterial { base_color_texture: Some( asset_server .load_builder() .with_settings::(|settings| { settings .sampler .get_or_init_descriptor() .set_address_mode(ImageAddressMode::Repeat); }) .load("textures/uv_checker_bw.png"), ), perceptual_roughness: 0.0, ..default() }), ), )) .insert_if(Mesh3d(plane_mesh), || args.pathtracer != Some(true)); for _ in 0..8000 { commands .spawn(( RaytracingMesh3d(cube_mesh.clone()), MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(rng.random(), rng.random(), rng.random()), perceptual_roughness: rng.random(), ..default() })), Transform::default() .with_scale(Vec3 { x: rng.random_range(0.2..=2.0), y: rng.random_range(0.2..=2.0), z: rng.random_range(0.2..=2.0), }) .with_translation(Vec3::new( rng.random_range(-180.0..=180.0), 0.2, rng.random_range(-180.0..=180.0), )), )) .insert_if(Mesh3d(cube_mesh.clone()), || args.pathtracer != Some(true)); } for x in -10..=10 { for y in -10..=10 { commands .spawn(( RaytracingMesh3d(sphere_mesh.clone()), MeshMaterial3d( materials.add(StandardMaterial { emissive: Color::linear_rgb( rng.random::() * 60000.0, rng.random::() * 60000.0, rng.random::() * 60000.0, ) .into(), ..default() }), ), Transform::default().with_translation(Vec3::new( (x * 20) as f32, 7.0, (y * 20) as f32, )), )) .insert_if(Mesh3d(sphere_mesh.clone()), || { args.pathtracer != Some(true) }); } } let mut camera = commands.spawn(( Camera3d::default(), Camera { clear_color: ClearColorConfig::Custom(Color::BLACK), ..default() }, FreeCamera { walk_speed: 3.0, run_speed: 10.0, ..Default::default() }, Transform::from_translation(Vec3::new(6.11329, 166.74896, 451.8226)).with_rotation( Quat::from_xyzw(-0.183938, 0.009093744, 0.0017017953, 0.9828943), ), // Msaa::Off and CameraMainTextureUsages with STORAGE_BINDING are required for Solari CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING), Msaa::Off, Bloom { intensity: 0.1, ..Bloom::NATURAL }, )); if args.pathtracer == Some(true) { camera.insert(Pathtracer::default()); } else { camera.insert(SolariLighting::default()); } // Using DLSS Ray Reconstruction for denoising (and cheaper rendering via upscaling) is _highly_ recommended when using Solari #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] if dlss_rr_supported.is_some() { camera.insert(Dlss:: { perf_quality_mode: Default::default(), reset: Default::default(), _phantom_data: Default::default(), }); } commands.spawn(( Node { position_type: PositionType::Absolute, right: px(0.0), padding: px(4.0).all(), border_radius: BorderRadius::bottom_left(px(4.0)), ..default() }, BackgroundColor(Color::srgba(0.10, 0.10, 0.10, 0.8)), children![( PerformanceText, Text::default(), TextFont { font_size: FontSize::Px(8.0), ..default() }, )], )); } fn add_raytracing_meshes_on_scene_load( scene_ready: On, children: Query<&Children>, mesh_query: Query<( &Mesh3d, &MeshMaterial3d, Option<&GltfMaterialName>, )>, mut meshes: ResMut>, mut materials: ResMut>, mut commands: Commands, args: Res, ) { for descendant in children.iter_descendants(scene_ready.entity) { if let Ok((Mesh3d(mesh_handle), MeshMaterial3d(material_handle), material_name)) = mesh_query.get(descendant) { // Add raytracing mesh component commands .entity(descendant) .insert(RaytracingMesh3d(mesh_handle.clone())); // Ensure meshes are Solari compatible let mut mesh = meshes.get_mut(mesh_handle).unwrap(); if !mesh.contains_attribute(Mesh::ATTRIBUTE_UV_0) { let vertex_count = mesh.count_vertices(); mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0]; vertex_count]); mesh.insert_attribute( Mesh::ATTRIBUTE_TANGENT, vec![[0.0, 0.0, 0.0, 0.0]; vertex_count], ); } if !mesh.contains_attribute(Mesh::ATTRIBUTE_TANGENT) { mesh.generate_tangents().unwrap(); } if mesh.contains_attribute(Mesh::ATTRIBUTE_UV_1) { mesh.remove_attribute(Mesh::ATTRIBUTE_UV_1); } if let Some(indices) = mesh.indices_mut() && let Indices::U16(_) = indices { *indices = Indices::U32(indices.iter().map(|i| i as u32).collect()); } // Prevent rasterization if using pathtracer if args.pathtracer == Some(true) { commands.entity(descendant).remove::(); } // Adjust scene materials to better demo Solari features if material_name.map(|s| s.0.as_str()) == Some("material") { let mut material = materials.get_mut(material_handle).unwrap(); material.emissive = LinearRgba::BLACK; } if material_name.map(|s| s.0.as_str()) == Some("Lights") { let mut material = materials.get_mut(material_handle).unwrap(); material.emissive = LinearRgba::from(Color::srgb(0.941, 0.714, 0.043)) * 1_000_000.0; material.alpha_mode = AlphaMode::Opaque; material.specular_transmission = 0.0; commands.insert_resource(RobotLightMaterial(material_handle.clone())); } if material_name.map(|s| s.0.as_str()) == Some("Glass_Dark_01") { let mut material = materials.get_mut(material_handle).unwrap(); material.alpha_mode = AlphaMode::Opaque; material.specular_transmission = 0.0; } } } } fn pause_scene(mut time: ResMut>, key_input: Res>) { if key_input.just_pressed(KeyCode::Space) { time.toggle(); } } #[derive(Resource)] struct RobotLightMaterial(Handle); fn toggle_lights( key_input: Res>, robot_light_material: Option>, mut materials: ResMut>, directional_light: Query>, mut commands: Commands, ) { if key_input.just_pressed(KeyCode::Digit1) { if let Ok(directional_light) = directional_light.single() { commands.entity(directional_light).despawn(); } else { commands.spawn(( DirectionalLight { illuminance: light_consts::lux::FULL_DAYLIGHT, shadow_maps_enabled: false, // Solari replaces shadow mapping ..default() }, Transform::from_rotation(Quat::from_xyzw( -0.13334629, -0.86597735, -0.3586996, 0.3219264, )), )); } } if key_input.just_pressed(KeyCode::Digit2) && let Some(robot_light_material) = robot_light_material { let mut material = materials.get_mut(&robot_light_material.0).unwrap(); if material.emissive == LinearRgba::BLACK { material.emissive = LinearRgba::from(Color::srgb(0.941, 0.714, 0.043)) * 1_000_000.0; } else { material.emissive = LinearRgba::BLACK; } } } #[derive(Component)] struct PatrolPath { path: Vec<(Vec3, Quat)>, i: usize, } fn patrol_path(mut query: Query<(&mut PatrolPath, &mut Transform)>, time: Res>) { for (mut path, mut transform) in query.iter_mut() { let (mut target_position, mut target_rotation) = path.path[path.i]; let mut distance_to_target = transform.translation.distance(target_position); if distance_to_target < 0.01 { transform.translation = target_position; transform.rotation = target_rotation; path.i = (path.i + 1) % path.path.len(); (target_position, target_rotation) = path.path[path.i]; distance_to_target = transform.translation.distance(target_position); } let direction = (target_position - transform.translation).normalize(); let movement = direction * time.delta_secs(); if movement.length() > distance_to_target { transform.translation = target_position; transform.rotation = target_rotation; } else { transform.translation += movement; } } } #[derive(Component)] struct ControlText; fn update_control_text( mut text: Single<&mut Text, With>, robot_light_material: Option>, materials: Res>, directional_light: Query>, time: Res>, #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] dlss_rr_supported: Option< Res, >, ) { text.0.clear(); if time.is_paused() { text.0.push_str("(Space): Resume"); } else { text.0.push_str("(Space): Pause"); } if directional_light.single().is_ok() { text.0.push_str("\n(1): Disable directional light"); } else { text.0.push_str("\n(1): Enable directional light"); } match robot_light_material.and_then(|m| materials.get(&m.0)) { Some(robot_light_material) if robot_light_material.emissive != LinearRgba::BLACK => { text.0.push_str("\n(2): Disable robot emissive light"); } _ => { text.0.push_str("\n(2): Enable robot emissive light"); } } #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] if dlss_rr_supported.is_some() { text.0 .push_str("\nDenoising: DLSS Ray Reconstruction enabled"); } else { text.0 .push_str("\nDenoising: DLSS Ray Reconstruction not supported"); } #[cfg(any(not(feature = "dlss"), feature = "force_disable_dlss"))] text.0 .push_str("\nDenoising: App not compiled with DLSS support"); } #[derive(Component)] struct PerformanceText; fn update_performance_text( mut text: Single<&mut Text, With>, diagnostics: Res, ) { text.0.clear(); let mut total = 0.0; let mut add_diagnostic = |name: &str, path: &'static str| { let path = DiagnosticPath::new(path); if let Some(value) = diagnostics.get(&path).and_then(Diagnostic::smoothed) { text.push_str(&format!("{name:17} {value:.2} ms\n")); total += value; } }; (add_diagnostic)( "Light tiles", "render/solari_lighting/presample_light_tiles/elapsed_gpu", ); (add_diagnostic)( "World cache", "render/solari_lighting/world_cache/elapsed_gpu", ); (add_diagnostic)( "Direct lighting", "render/solari_lighting/direct_lighting/elapsed_gpu", ); (add_diagnostic)( "Diffuse indirect", "render/solari_lighting/diffuse_indirect_lighting/elapsed_gpu", ); (add_diagnostic)( "Specular indirect", "render/solari_lighting/specular_indirect_lighting/elapsed_gpu", ); (add_diagnostic)("DLSS-RR", "render/dlss_ray_reconstruction/elapsed_gpu"); text.push_str(&format!("{:17} {total:.2} ms\n", "Total")); if let Some(world_cache_active_cells_count) = diagnostics .get(&DiagnosticPath::new( "render/solari_lighting/world_cache_active_cells_count", )) .and_then(Diagnostic::smoothed) { text.push_str(&format!( "\nWorld cache cells {} ({:.0}%)", world_cache_active_cells_count as u32, (world_cache_active_cells_count * 100.0) / (2u64.pow(20) as f64) )); } }