From cb64a4aa8ad6cc0d6398ca46507320c2d54639b2 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 10 Jul 2025 04:37:56 +0200 Subject: [PATCH 01/19] Refactor `3d_viewport_to_world` example with let chains (#20071) # Objective Make use of let chains to reduce LoC where we previously used let else to cut down on indentation. Best of both worlds. --- examples/3d/3d_viewport_to_world.rs | 49 +++++++++++------------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/examples/3d/3d_viewport_to_world.rs b/examples/3d/3d_viewport_to_world.rs index 9aabd6f629..883121779a 100644 --- a/examples/3d/3d_viewport_to_world.rs +++ b/examples/3d/3d_viewport_to_world.rs @@ -13,41 +13,30 @@ fn main() { fn draw_cursor( camera_query: Single<(&Camera, &GlobalTransform)>, ground: Single<&GlobalTransform, With>, - windows: Query<&Window>, + window: Single<&Window>, mut gizmos: Gizmos, ) { - let Ok(windows) = windows.single() else { - return; - }; - let (camera, camera_transform) = *camera_query; - let Some(cursor_position) = windows.cursor_position() else { - return; - }; + if let Some(cursor_position) = window.cursor_position() + // Calculate a ray pointing from the camera into the world based on the cursor's position. + && let Ok(ray) = camera.viewport_to_world(camera_transform, cursor_position) + // Calculate if and at what distance the ray is hitting the ground plane. + && let Some(distance) = + ray.intersect_plane(ground.translation(), InfinitePlane3d::new(ground.up())) + { + let point = ray.get_point(distance); - // Calculate a ray pointing from the camera into the world based on the cursor's position. - let Ok(ray) = camera.viewport_to_world(camera_transform, cursor_position) else { - return; - }; - - // Calculate if and where the ray is hitting the ground plane. - let Some(distance) = - ray.intersect_plane(ground.translation(), InfinitePlane3d::new(ground.up())) - else { - return; - }; - let point = ray.get_point(distance); - - // Draw a circle just above the ground plane at that position. - gizmos.circle( - Isometry3d::new( - point + ground.up() * 0.01, - Quat::from_rotation_arc(Vec3::Z, ground.up().as_vec3()), - ), - 0.2, - Color::WHITE, - ); + // Draw a circle just above the ground plane at that position. + gizmos.circle( + Isometry3d::new( + point + ground.up() * 0.01, + Quat::from_rotation_arc(Vec3::Z, ground.up().as_vec3()), + ), + 0.2, + Color::WHITE, + ); + } } #[derive(Component)] From 6792cebfd0d49a167aeb70fa03a136a512707e53 Mon Sep 17 00:00:00 2001 From: T772 Date: Thu, 10 Jul 2025 20:41:37 +0200 Subject: [PATCH 02/19] StandardMaterial docs: Make clear that lighting won't look correct if `is_srgb` is `true` (#20037) # Objective - Fix https://github.com/bevyengine/bevy/issues/12123 ## Solution - Add a code example found here: https://github.com/bevyengine/bevy/blob/main/examples/3d/parallax_mapping.rs#L209 to https://docs.rs/bevy/latest/bevy/pbr/struct.StandardMaterial.html ## Testing - This pull request if only changing docs. But I tested the formatting via ```cargo doc --no-deps --open```. --------- Co-authored-by: Alice Cecile Co-authored-by: BD103 <59022059+BD103@users.noreply.github.com> --- crates/bevy_pbr/src/pbr_material.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 0207a81ed0..d7baef3a9a 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -386,6 +386,23 @@ pub struct StandardMaterial { /// /// [`Mesh::generate_tangents`]: bevy_render::mesh::Mesh::generate_tangents /// [`Mesh::with_generated_tangents`]: bevy_render::mesh::Mesh::with_generated_tangents + /// + /// # Usage + /// + /// ``` + /// # use bevy_asset::{AssetServer, Handle}; + /// # use bevy_ecs::change_detection::Res; + /// # use bevy_image::{Image, ImageLoaderSettings}; + /// # + /// fn load_normal_map(asset_server: Res) { + /// let normal_handle: Handle = asset_server.load_with_settings( + /// "textures/parallax_example/cube_normal.png", + /// // The normal map texture is in linear color space. Lighting won't look correct + /// // if `is_srgb` is `true`, which is the default. + /// |settings: &mut ImageLoaderSettings| settings.is_srgb = false, + /// ); + /// } + /// ``` #[texture(9)] #[sampler(10)] #[dependency] From 0d6c591491925a701de7a74c4891258afc93e234 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Thu, 10 Jul 2025 21:54:53 -0700 Subject: [PATCH 03/19] Adjust specular_multiscatter to not take LightingInput (#20068) Small refactor for a future bevy_solari PR. Suggest reviewing while hiding the whitespace diff. --- crates/bevy_pbr/src/render/pbr_lighting.wgsl | 23 +++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/crates/bevy_pbr/src/render/pbr_lighting.wgsl b/crates/bevy_pbr/src/render/pbr_lighting.wgsl index 17cae13b92..6bc24d8af0 100644 --- a/crates/bevy_pbr/src/render/pbr_lighting.wgsl +++ b/crates/bevy_pbr/src/render/pbr_lighting.wgsl @@ -235,16 +235,13 @@ fn fresnel(f0: vec3, LdotH: f32) -> vec3 { // Multiscattering approximation: // fn specular_multiscatter( - input: ptr, D: f32, V: f32, F: vec3, + F0: vec3, + F_ab: vec2, specular_intensity: f32, ) -> vec3 { - // Unpack. - let F0 = (*input).F0_; - let F_ab = (*input).F_ab; - var Fr = (specular_intensity * D * V) * F; Fr *= 1.0 + F0 * (1.0 / F_ab.x - 1.0); return Fr; @@ -329,7 +326,7 @@ fn specular( let F = fresnel(F0, LdotH); // Calculate the specular light. - let Fr = specular_multiscatter(input, D, V, F, specular_intensity); + let Fr = specular_multiscatter(D, V, F, F0, (*input).F_ab, specular_intensity); return Fr; } @@ -397,7 +394,7 @@ fn specular_anisotropy( let Fa = fresnel(F0, LdotH); // Calculate the specular light. - let Fr = specular_multiscatter(input, Da, Va, Fa, specular_intensity); + let Fr = specular_multiscatter(Da, Va, Fa, F0, (*input).F_ab, specular_intensity); return Fr; } @@ -482,7 +479,7 @@ fn cubemap_uv(direction: vec3, cubemap_type: u32) -> vec2 { ), max_axis != abs_direction.x ); - + var face_uv: vec2; var divisor: f32; var corner_uv: vec2 = vec2(0, 0); @@ -500,12 +497,12 @@ fn cubemap_uv(direction: vec3, cubemap_type: u32) -> vec2 { face_uv = (face_uv / divisor) * 0.5 + 0.5; switch cubemap_type { - case CUBEMAP_TYPE_CROSS_VERTICAL: { - face_size = vec2(1.0/3.0, 1.0/4.0); + case CUBEMAP_TYPE_CROSS_VERTICAL: { + face_size = vec2(1.0/3.0, 1.0/4.0); corner_uv = vec2((0x111102u >> (4 * face_index)) & 0xFu, (0x132011u >> (4 * face_index)) & 0xFu); } - case CUBEMAP_TYPE_CROSS_HORIZONTAL: { - face_size = vec2(1.0/4.0, 1.0/3.0); + case CUBEMAP_TYPE_CROSS_HORIZONTAL: { + face_size = vec2(1.0/4.0, 1.0/3.0); corner_uv = vec2((0x131102u >> (4 * face_index)) & 0xFu, (0x112011u >> (4 * face_index)) & 0xFu); } case CUBEMAP_TYPE_SEQUENCE_HORIZONTAL: { @@ -765,7 +762,7 @@ fn directional_light( view_bindings::clustered_decal_sampler, decal_uv - floor(decal_uv), 0.0 - ).r; + ).r; } else { texture_sample = 0f; } From f8680135ed436d39ebaa2a648cd2c78ba2c64119 Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Fri, 11 Jul 2025 06:00:42 +0100 Subject: [PATCH 04/19] Fix crash on meshes with morphs + skins + motion blur when not using storage buffers (#20076) ## Objective Fixes #20058 ## Solution Fix the `dynamic_offsets` array being too small if a mesh has morphs and skins and motion blur, and the renderer isn't using storage buffers (i.e. WebGL2). The bug was introduced in #13572. ## Testing - Minimal repro: https://github.com/M4tsuri/bevy_reproduce. - Also examples `animated_mesh`, `morph_targets`, `test_invalid_skinned_meshes`. - As far as I can tell Bevy doesn't have any examples or tests that can repro the problem combination. Tested with WebGL and native, Win10/Chrome/Nvidia. --- crates/bevy_pbr/src/render/mesh.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 53b3b4129a..9795d442dd 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -3016,7 +3016,7 @@ impl RenderCommand

for SetMeshBindGroup { ); }; - let mut dynamic_offsets: [u32; 3] = Default::default(); + let mut dynamic_offsets: [u32; 5] = Default::default(); let mut offset_count = 0; if let PhaseItemExtraIndex::DynamicOffset(dynamic_offset) = item.extra_index() { dynamic_offsets[offset_count] = dynamic_offset; From 83305afa6628c44379ab5e1f402e864af79d74f0 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Thu, 10 Jul 2025 22:01:15 -0700 Subject: [PATCH 05/19] Fix SSAO specular occlusion roughness bug (#20067) Noticed that we're converting perceptual_roughness to roughness for SSAO specular occlusion up here, _but_ that happens _before_ we sample the metallic_roughness texture map. So we're using the wrong roughness. I assume this is a bug and was not intentional. Suggest reviewing while hiding the whitespace diff. --- crates/bevy_pbr/src/render/pbr_fragment.wgsl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index 779546f8bd..3c69c4405f 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -377,7 +377,6 @@ fn pbr_input_from_standard_material( var perceptual_roughness: f32 = pbr_bindings::material.perceptual_roughness; #endif // BINDLESS - let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); #ifdef VERTEX_UVS if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) { let metallic_roughness = @@ -627,7 +626,7 @@ fn pbr_input_from_standard_material( var specular_occlusion: f32 = 1.0; #ifdef VERTEX_UVS if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) { - diffuse_occlusion *= + diffuse_occlusion *= #ifdef MESHLET_MESH_MATERIAL_PASS textureSampleGrad( #else // MESHLET_MESH_MATERIAL_PASS @@ -660,7 +659,8 @@ fn pbr_input_from_standard_material( diffuse_occlusion = min(diffuse_occlusion, ssao_multibounce); // Use SSAO to estimate the specular occlusion. // Lagarde and Rousiers 2014, "Moving Frostbite to Physically Based Rendering" - specular_occlusion = saturate(pow(NdotV + ssao, exp2(-16.0 * roughness - 1.0)) - 1.0 + ssao); + let roughness = lighting::perceptualRoughnessToRoughness(pbr_input.material.perceptual_roughness); + specular_occlusion = saturate(pow(NdotV + ssao, exp2(-16.0 * roughness - 1.0)) - 1.0 + ssao); #endif pbr_input.diffuse_occlusion = diffuse_occlusion; pbr_input.specular_occlusion = specular_occlusion; From 63dd8b66526bc083b529bc9c8d562a439a69ff24 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 11 Jul 2025 06:02:15 +0100 Subject: [PATCH 06/19] `ColorStop` constructor functions (#20066) # Objective Add `px` and `percent` constructor functions for `ColorStop`. --- crates/bevy_ui/src/gradients.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/bevy_ui/src/gradients.rs b/crates/bevy_ui/src/gradients.rs index 7086b37416..87b7afc581 100644 --- a/crates/bevy_ui/src/gradients.rs +++ b/crates/bevy_ui/src/gradients.rs @@ -44,6 +44,24 @@ impl ColorStop { } } + /// A color stop with its position in logical pixels. + pub fn px(color: impl Into, px: f32) -> Self { + Self { + color: color.into(), + point: Val::Px(px), + hint: 0.5, + } + } + + /// A color stop with a percentage position. + pub fn percent(color: impl Into, percent: f32) -> Self { + Self { + color: color.into(), + point: Val::Percent(percent), + hint: 0.5, + } + } + // Set the interpolation midpoint between this and the following stop pub fn with_hint(mut self, hint: f32) -> Self { self.hint = hint; From df5dfcd2988fd5abd4fadb1c4ec0f8e2086f1ea9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 11 Jul 2025 06:02:36 +0100 Subject: [PATCH 07/19] `OverrideClip` interaction fix (#20064) # Objective Picking was changed in the UI transform PR to walk up the tree recursively to check if an interaction was on a clipped node. `OverrideClip` only affects a node's local clipping rect, so valid interactions can be ignored if a node has clipped ancestors. ## Solution Add a `Without` query filter to the picking systems' `child_of_query`s. ## Testing This modified `button` example can be used to test the change: ```rust //! This example illustrates how to create a button that changes color and text based on its //! interaction state. use bevy::{color::palettes::basic::*, input_focus::InputFocus, prelude::*, winit::WinitSettings}; fn main() { App::new() .add_plugins(DefaultPlugins) // Only run the app when there is user input. This will significantly reduce CPU/GPU use. .insert_resource(WinitSettings::desktop_app()) // `InputFocus` must be set for accessibility to recognize the button. .init_resource::() .add_systems(Startup, setup) .add_systems(Update, button_system) .run(); } const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); fn button_system( mut input_focus: ResMut, mut interaction_query: Query< ( Entity, &Interaction, &mut BackgroundColor, &mut BorderColor, &mut Button, &Children, ), Changed, >, mut text_query: Query<&mut Text>, ) { for (entity, interaction, mut color, mut border_color, mut button, children) in &mut interaction_query { let mut text = text_query.get_mut(children[0]).unwrap(); match *interaction { Interaction::Pressed => { input_focus.set(entity); **text = "Press".to_string(); *color = PRESSED_BUTTON.into(); *border_color = BorderColor::all(RED.into()); // The accessibility system's only update the button's state when the `Button` component is marked as changed. button.set_changed(); } Interaction::Hovered => { input_focus.set(entity); **text = "Hover".to_string(); *color = HOVERED_BUTTON.into(); *border_color = BorderColor::all(Color::WHITE); button.set_changed(); } Interaction::None => { input_focus.clear(); **text = "Button".to_string(); *color = NORMAL_BUTTON.into(); *border_color = BorderColor::all(Color::BLACK); } } } } fn setup(mut commands: Commands, assets: Res) { // ui camera commands.spawn(Camera2d); commands.spawn(button(&assets)); } fn button(asset_server: &AssetServer) -> impl Bundle + use<> { ( Node { width: Val::Percent(100.0), height: Val::Percent(100.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, ..default() }, children![( Node { width: Val::Px(0.), height: Val::Px(0.), overflow: Overflow::clip(), ..default() }, children![( //OverrideClip, Button, Node { position_type: PositionType::Absolute, width: Val::Px(150.0), height: Val::Px(65.0), border: UiRect::all(Val::Px(5.0)), // horizontally center child text justify_content: JustifyContent::Center, // vertically center child text align_items: AlignItems::Center, ..default() }, BorderColor::all(Color::WHITE), BorderRadius::MAX, BackgroundColor(Color::BLACK), children![( Text::new("Button"), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 33.0, ..default() }, TextColor(Color::srgb(0.9, 0.9, 0.9)), TextShadow::default(), )] )], )], ) } ``` On main the button ignores interactions, with this PR it should respond correctly. --------- Co-authored-by: Alice Cecile --- crates/bevy_ui/src/focus.rs | 11 +++++++---- crates/bevy_ui/src/picking_backend.rs | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 384661f4d1..32872f1447 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,10 +1,12 @@ -use crate::{ui_transform::UiGlobalTransform, ComputedNode, ComputedNodeTarget, Node, UiStack}; +use crate::{ + ui_transform::UiGlobalTransform, ComputedNode, ComputedNodeTarget, Node, OverrideClip, UiStack, +}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::{ContainsEntity, Entity}, hierarchy::ChildOf, prelude::{Component, With}, - query::QueryData, + query::{QueryData, Without}, reflect::ReflectComponent, system::{Local, Query, Res}, }; @@ -157,7 +159,7 @@ pub fn ui_focus_system( ui_stack: Res, mut node_query: Query, clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>, - child_of_query: Query<&ChildOf>, + child_of_query: Query<&ChildOf, Without>, ) { let primary_window = primary_window.iter().next(); @@ -325,11 +327,12 @@ pub fn ui_focus_system( } /// Walk up the tree child-to-parent checking that `point` is not clipped by any ancestor node. +/// If `entity` has an [`OverrideClip`] component it ignores any inherited clipping and returns true. pub fn clip_check_recursive( point: Vec2, entity: Entity, clipping_query: &Query<'_, '_, (&ComputedNode, &UiGlobalTransform, &Node)>, - child_of_query: &Query<&ChildOf>, + child_of_query: &Query<&ChildOf, Without>, ) -> bool { if let Ok(child_of) = child_of_query.get(entity) { let parent = child_of.0; diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index ccd61a3807..891aea7d35 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -109,7 +109,7 @@ pub fn ui_picking( node_query: Query, mut output: EventWriter, clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>, - child_of_query: Query<&ChildOf>, + child_of_query: Query<&ChildOf, Without>, ) { // For each camera, the pointer and its position let mut pointer_pos_by_camera = HashMap::>::default(); From cfb679a752ca5d8b440424a6516b4b87f61d9b85 Mon Sep 17 00:00:00 2001 From: atlv Date: Fri, 11 Jul 2025 03:34:06 -0400 Subject: [PATCH 08/19] Add a release note for scene types refactor (#20051) Its really barebones but I'm not sure what else to write. --------- Co-authored-by: Robert Swain --- release-content/release-notes/scene-type-crates.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 release-content/release-notes/scene-type-crates.md diff --git a/release-content/release-notes/scene-type-crates.md b/release-content/release-notes/scene-type-crates.md new file mode 100644 index 0000000000..875c2a5e92 --- /dev/null +++ b/release-content/release-notes/scene-type-crates.md @@ -0,0 +1,7 @@ +--- +title: Define scenes without depending on bevy_render +authors: ["@atlv24"] +pull_requests: [19997, 19991, 20000, 19949, 19943, 19953] +--- + +It is now possible to use cameras, lights, and meshes without depending on the Bevy renderer. This makes it possible for 3rd party custom renderers to be drop-in replacements for rendering existing scenes. From b3032e06bd8564cb4526548c5b07342a3821542c Mon Sep 17 00:00:00 2001 From: atlv Date: Fri, 11 Jul 2025 08:18:23 -0400 Subject: [PATCH 09/19] Fix adapter forcing breaking wasm builds (#20054) # Objective - Appease @mockersf ## Solution - Gate out enumerate_adapters usage on wasm and warn if `WGPU_FORCE_FALLBACK_ADAPTER` is somehow used. --- crates/bevy_render/src/renderer/mod.rs | 69 +++++++++++++++++--------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 7cb8023de1..52679002fa 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -7,7 +7,7 @@ use bevy_tasks::ComputeTaskPool; use bevy_utils::WgpuWrapper; pub use graph_runner::*; pub use render_device::*; -use tracing::{debug, error, info, info_span, trace, warn}; +use tracing::{debug, error, info, info_span, warn}; use crate::{ diagnostic::{internal::DiagnosticsRecorder, RecordDiagnostics}, @@ -145,6 +145,33 @@ const GPU_NOT_FOUND_ERROR_MESSAGE: &str = if cfg!(target_os = "linux") { "Unable to find a GPU! Make sure you have installed required drivers!" }; +#[cfg(not(target_family = "wasm"))] +fn find_adapter_by_name( + instance: &Instance, + options: &WgpuSettings, + compatible_surface: Option<&wgpu::Surface<'_>>, + adapter_name: &str, +) -> Option { + for adapter in + instance.enumerate_adapters(options.backends.expect( + "The `backends` field of `WgpuSettings` must be set to use a specific adapter.", + )) + { + tracing::trace!("Checking adapter: {:?}", adapter.get_info()); + let info = adapter.get_info(); + if let Some(surface) = compatible_surface { + if !adapter.is_surface_supported(surface) { + continue; + } + } + + if info.name.eq_ignore_ascii_case(adapter_name) { + return Some(adapter); + } + } + None +} + /// Initializes the renderer by retrieving and preparing the GPU instance, device and queue /// for the specified backend. pub async fn initialize_renderer( @@ -153,36 +180,30 @@ pub async fn initialize_renderer( request_adapter_options: &RequestAdapterOptions<'_, '_>, desired_adapter_name: Option, ) -> (RenderDevice, RenderQueue, RenderAdapterInfo, RenderAdapter) { + #[cfg(not(target_family = "wasm"))] + let mut selected_adapter = desired_adapter_name.and_then(|adapter_name| { + find_adapter_by_name( + instance, + options, + request_adapter_options.compatible_surface, + &adapter_name, + ) + }); + #[cfg(target_family = "wasm")] let mut selected_adapter = None; - if let Some(adapter_name) = &desired_adapter_name { - debug!("Searching for adapter with name: {}", adapter_name); - for adapter in instance.enumerate_adapters(options.backends.expect( - "The `backends` field of `WgpuSettings` must be set to use a specific adapter.", - )) { - trace!("Checking adapter: {:?}", adapter.get_info()); - let info = adapter.get_info(); - if let Some(surface) = request_adapter_options.compatible_surface { - if !adapter.is_surface_supported(surface) { - continue; - } - } - if info - .name - .to_lowercase() - .contains(&adapter_name.to_lowercase()) - { - selected_adapter = Some(adapter); - break; - } - } - } else { + #[cfg(target_family = "wasm")] + if desired_adapter_name.is_some() { + warn!("Choosing an adapter is not supported on wasm."); + } + + if selected_adapter.is_none() { debug!( "Searching for adapter with options: {:?}", request_adapter_options ); selected_adapter = instance.request_adapter(request_adapter_options).await.ok(); - }; + } let adapter = selected_adapter.expect(GPU_NOT_FOUND_ERROR_MESSAGE); let adapter_info = adapter.get_info(); From 20dfae9a2d07038bda2921f82af50ded6151c3de Mon Sep 17 00:00:00 2001 From: atlv Date: Fri, 11 Jul 2025 08:19:02 -0400 Subject: [PATCH 10/19] Factor out up-choice in shadow cubemap sampling orthonormalize (#20052) # Objective - Another step towards unifying our orthonormal basis construction #20050 - Preserve behavior but fix a bug. Unification will be a followup after these two PRs and will need more thorough testing. ## Solution - Make shadow cubemap sampling orthonormalize have the same function signature as the other orthonormal basis functions in bevy ## Testing - 3d_scene + lighting examples --- .../bevy_pbr/src/render/shadow_sampling.wgsl | 18 +++--------------- crates/bevy_render/src/maths.wgsl | 16 +++++++++------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/crates/bevy_pbr/src/render/shadow_sampling.wgsl b/crates/bevy_pbr/src/render/shadow_sampling.wgsl index c7f7253a63..2b35e57037 100644 --- a/crates/bevy_pbr/src/render/shadow_sampling.wgsl +++ b/crates/bevy_pbr/src/render/shadow_sampling.wgsl @@ -422,11 +422,7 @@ fn sample_shadow_cubemap_gaussian( ) -> f32 { // Create an orthonormal basis so we can apply a 2D sampling pattern to a // cubemap. - var up = vec3(0.0, 1.0, 0.0); - if (dot(up, normalize(light_local)) > 0.99) { - up = vec3(1.0, 0.0, 0.0); // Avoid creating a degenerate basis. - } - let basis = orthonormalize(light_local, up) * scale * distance_to_light; + let basis = orthonormalize(normalize(light_local)) * scale * distance_to_light; var sum: f32 = 0.0; sum += sample_shadow_cubemap_at_offset( @@ -469,11 +465,7 @@ fn sample_shadow_cubemap_jittered( ) -> f32 { // Create an orthonormal basis so we can apply a 2D sampling pattern to a // cubemap. - var up = vec3(0.0, 1.0, 0.0); - if (dot(up, normalize(light_local)) > 0.99) { - up = vec3(1.0, 0.0, 0.0); // Avoid creating a degenerate basis. - } - let basis = orthonormalize(light_local, up) * scale * distance_to_light; + let basis = orthonormalize(normalize(light_local)) * scale * distance_to_light; let rotation_matrix = random_rotation_matrix(vec2(1.0), temporal); @@ -553,11 +545,7 @@ fn search_for_blockers_in_shadow_cubemap( ) -> f32 { // Create an orthonormal basis so we can apply a 2D sampling pattern to a // cubemap. - var up = vec3(0.0, 1.0, 0.0); - if (dot(up, normalize(light_local)) > 0.99) { - up = vec3(1.0, 0.0, 0.0); // Avoid creating a degenerate basis. - } - let basis = orthonormalize(light_local, up) * scale * distance_to_light; + let basis = orthonormalize(normalize(light_local)) * scale * distance_to_light; var sum: vec2 = vec2(0.0); sum += search_for_blockers_in_shadow_cubemap_at_offset( diff --git a/crates/bevy_render/src/maths.wgsl b/crates/bevy_render/src/maths.wgsl index d1e35523dc..0f9a11076f 100644 --- a/crates/bevy_render/src/maths.wgsl +++ b/crates/bevy_render/src/maths.wgsl @@ -63,17 +63,19 @@ fn mat4x4_to_mat3x3(m: mat4x4) -> mat3x3 { return mat3x3(m[0].xyz, m[1].xyz, m[2].xyz); } -// Creates an orthonormal basis given a Z vector and an up vector (which becomes -// Y after orthonormalization). +// Creates an orthonormal basis given a normalized Z vector. // // The results are equivalent to the Gram-Schmidt process [1]. // // [1]: https://math.stackexchange.com/a/1849294 -fn orthonormalize(z_unnormalized: vec3, up: vec3) -> mat3x3 { - let z_basis = normalize(z_unnormalized); - let x_basis = normalize(cross(z_basis, up)); - let y_basis = cross(z_basis, x_basis); - return mat3x3(x_basis, y_basis, z_basis); +fn orthonormalize(z_normalized: vec3) -> mat3x3 { + var up = vec3(0.0, 1.0, 0.0); + if (abs(dot(up, z_normalized)) > 0.99) { + up = vec3(1.0, 0.0, 0.0); // Avoid creating a degenerate basis. + } + let x_basis = normalize(cross(z_normalized, up)); + let y_basis = cross(z_normalized, x_basis); + return mat3x3(x_basis, y_basis, z_normalized); } // Returns true if any part of a sphere is on the positive side of a plane. From f1eace62f04940224ee8f6a4a98cb57b1d3c8226 Mon Sep 17 00:00:00 2001 From: Christian Hughes <9044780+ItsDoot@users.noreply.github.com> Date: Fri, 11 Jul 2025 22:56:23 -0500 Subject: [PATCH 11/19] Thoroughly document `UninitializedId` semantics (#20080) # Objective Clean up documentation around `UninitializedId`, which has slightly confusing semantics. ## Solution Added documentation comments on `UninitializedId`. --------- Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com> --- crates/bevy_ecs/src/schedule/schedule.rs | 37 ++++++++++++++++++------ 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index ed40f21fbd..a9067b75b8 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -15,6 +15,7 @@ use bevy_utils::{default, prelude::DebugName, TypeIdMap}; use core::{ any::{Any, TypeId}, fmt::{Debug, Write}, + ops::Range, }; use fixedbitset::FixedBitSet; use log::{error, info, warn}; @@ -752,11 +753,31 @@ new_key_type! { pub struct SystemSetKey; } +/// A node in a [`ScheduleGraph`] with a system or conditions that have not been +/// initialized yet. +/// +/// We have to defer initialization of nodes in the graph until we have +/// `&mut World` access, so we store these in a list ([`ScheduleGraph::uninit`]) +/// until then. In most cases, initialization occurs upon the first run of the +/// schedule. enum UninitializedId { + /// A system and its conditions that have not been initialized yet. System(SystemKey), + /// A system set's conditions that have not been initialized yet. Set { key: SystemSetKey, - first_uninit_condition: usize, + /// The range of indices in [`SystemSets::conditions`] that correspond + /// to conditions that have not been initialized yet. + /// + /// [`SystemSets::conditions`] for a given set may be appended to + /// multiple times (e.g. when `configure_sets` is called multiple with + /// the same set), so we need to track which conditions in that list + /// are newly added and not yet initialized. + /// + /// Systems don't need this tracking because each `add_systems` call + /// creates separate nodes in the graph with their own conditions, + /// so all conditions are initialized together. + uninitialized_conditions: Range, }, } @@ -793,8 +814,8 @@ pub struct ScheduleGraph { pub system_conditions: SecondaryMap>, /// Data about system sets in the schedule system_sets: SystemSets, - /// Systems that have not been initialized yet; for system sets, we store the index of the first uninitialized condition - /// (all the conditions after that index still need to be initialized) + /// Systems, their conditions, and system set conditions that need to be + /// initialized before the schedule can be run. uninit: Vec, /// Directed acyclic graph of the hierarchy (which systems/sets are children of which sets) hierarchy: Dag, @@ -807,7 +828,6 @@ pub struct ScheduleGraph { anonymous_sets: usize, changed: bool, settings: ScheduleBuildSettings, - passes: BTreeMap>, } @@ -1101,9 +1121,10 @@ impl ScheduleGraph { // system init has to be deferred (need `&mut World`) let system_set_conditions = self.system_sets.conditions.entry(key).unwrap().or_default(); + let start = system_set_conditions.len(); self.uninit.push(UninitializedId::Set { key, - first_uninit_condition: system_set_conditions.len(), + uninitialized_conditions: start..(start + conditions.len()), }); system_set_conditions.extend(conditions.into_iter().map(ConditionWithAccess::new)); @@ -1189,11 +1210,9 @@ impl ScheduleGraph { } UninitializedId::Set { key, - first_uninit_condition, + uninitialized_conditions, } => { - for condition in self.system_sets.conditions[key] - .iter_mut() - .skip(first_uninit_condition) + for condition in &mut self.system_sets.conditions[key][uninitialized_conditions] { condition.access = condition.condition.initialize(world); } From 27fe2e88bd77a356c8be5db050a9021495c9039f Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sun, 13 Jul 2025 00:40:36 +0200 Subject: [PATCH 12/19] Update `sysinfo` version to `0.36.0` (#20084) Some bugfixes and new API additions. --- crates/bevy_diagnostic/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index 424ca67437..a1803151fc 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -71,14 +71,14 @@ log = { version = "0.4", default-features = false } # macOS [target.'cfg(all(target_os="macos"))'.dependencies] # Some features of sysinfo are not supported by apple. This will disable those features on apple devices -sysinfo = { version = "0.35.0", optional = true, default-features = false, features = [ +sysinfo = { version = "0.36.0", optional = true, default-features = false, features = [ "apple-app-store", "system", ] } # Only include when on linux/windows/android/freebsd [target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "android", target_os = "freebsd"))'.dependencies] -sysinfo = { version = "0.35.0", optional = true, default-features = false, features = [ +sysinfo = { version = "0.36.0", optional = true, default-features = false, features = [ "system", ] } From 1525dff7ada14714b35c8908381d534a7833faa3 Mon Sep 17 00:00:00 2001 From: onbjerg Date: Sun, 13 Jul 2025 01:00:38 +0200 Subject: [PATCH 13/19] Add `max_history_length` to `EntityCountDiagnosticsPlugin` (#20085) # Objective I was building out a diagnostics panel in egui when I noticed that I could specify the max history length for the `FrameTimeDiagnosticsPlugin`, but not for the `EntityCountDiagnosticsPlugin`. The objective was to harmonize the two, making the diagnostic history length configurable for both. ## Solution I added a `EntityCountDiagnosticsPlugin::new`, and a `Default` impl that matches `FrameTimeDiagnosticsPlugin`. This is a breaking change, given the plugin had no fields previously. ## Testing I did not test this. --- .../src/entity_count_diagnostics_plugin.rs | 29 +++++++++++++++---- examples/diagnostics/log_diagnostics.rs | 2 +- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs index b20a82bf6c..1de4f4c029 100644 --- a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs @@ -1,20 +1,39 @@ use bevy_app::prelude::*; use bevy_ecs::entity::Entities; -use crate::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic}; +use crate::{ + Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic, DEFAULT_MAX_HISTORY_LENGTH, +}; /// Adds "entity count" diagnostic to an App. /// /// # See also /// /// [`LogDiagnosticsPlugin`](crate::LogDiagnosticsPlugin) to output diagnostics to the console. -#[derive(Default)] -pub struct EntityCountDiagnosticsPlugin; +pub struct EntityCountDiagnosticsPlugin { + /// The total number of values to keep. + pub max_history_length: usize, +} + +impl Default for EntityCountDiagnosticsPlugin { + fn default() -> Self { + Self::new(DEFAULT_MAX_HISTORY_LENGTH) + } +} + +impl EntityCountDiagnosticsPlugin { + /// Creates a new `EntityCountDiagnosticsPlugin` with the specified `max_history_length`. + pub fn new(max_history_length: usize) -> Self { + Self { max_history_length } + } +} impl Plugin for EntityCountDiagnosticsPlugin { fn build(&self, app: &mut App) { - app.register_diagnostic(Diagnostic::new(Self::ENTITY_COUNT)) - .add_systems(Update, Self::diagnostic_system); + app.register_diagnostic( + Diagnostic::new(Self::ENTITY_COUNT).with_max_history_length(self.max_history_length), + ) + .add_systems(Update, Self::diagnostic_system); } } diff --git a/examples/diagnostics/log_diagnostics.rs b/examples/diagnostics/log_diagnostics.rs index 0e00e69ccd..1a36f9a1f2 100644 --- a/examples/diagnostics/log_diagnostics.rs +++ b/examples/diagnostics/log_diagnostics.rs @@ -33,7 +33,7 @@ fn main() { // Adds frame time, FPS and frame count diagnostics. FrameTimeDiagnosticsPlugin::default(), // Adds an entity count diagnostic. - EntityCountDiagnosticsPlugin, + EntityCountDiagnosticsPlugin::default(), // Adds cpu and memory usage diagnostics for systems and the entire game process. SystemInformationDiagnosticsPlugin, // Forwards various diagnostics from the render app to the main app. From e5aa94132c48788ea791321d2101e783d32dabc4 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Sun, 13 Jul 2025 10:23:38 -0700 Subject: [PATCH 14/19] Solari initial GI (#20020) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - Add 1-bounce RT GI ## Solution - Implement a very very basic version of ReSTIR GI https://d1qx31qr3h6wln.cloudfront.net/publications/ReSTIR%20GI.pdf - Pretty much a copy of the ReSTIR DI code, but adjusted for GI. - Didn't implement add more spatial samples, or do anything needed for better quality. - Didn't try to improve perf at all yet (it's actually faster than DI though, unfortunately 😅) - Didn't spend any time cleaning up the shared abstractions between DI/GI --- ## Showcase image --- crates/bevy_solari/src/lib.rs | 6 +- .../src/pathtracer/pathtracer.wgsl | 3 +- crates/bevy_solari/src/realtime/mod.rs | 5 + crates/bevy_solari/src/realtime/node.rs | 94 +++++- crates/bevy_solari/src/realtime/prepare.rs | 47 ++- .../bevy_solari/src/realtime/restir_di.wgsl | 43 +-- .../bevy_solari/src/realtime/restir_gi.wgsl | 310 ++++++++++++++++++ crates/bevy_solari/src/scene/sampling.wgsl | 31 +- crates/bevy_solari/src/scene/types.rs | 4 +- release-content/release-notes/bevy_solari.md | 6 +- 10 files changed, 486 insertions(+), 63 deletions(-) create mode 100644 crates/bevy_solari/src/realtime/restir_gi.wgsl diff --git a/crates/bevy_solari/src/lib.rs b/crates/bevy_solari/src/lib.rs index 686cfd3238..3847bc074a 100644 --- a/crates/bevy_solari/src/lib.rs +++ b/crates/bevy_solari/src/lib.rs @@ -26,11 +26,13 @@ use bevy_render::settings::WgpuFeatures; /// An experimental set of plugins for raytraced lighting. /// /// This plugin group provides: -/// * [`SolariLightingPlugin`] - Raytraced direct and indirect lighting (indirect lighting not yet implemented). +/// * [`SolariLightingPlugin`] - Raytraced direct and indirect lighting. /// * [`RaytracingScenePlugin`] - BLAS building, resource and lighting binding. +/// +/// There's also: /// * [`pathtracer::PathtracingPlugin`] - A non-realtime pathtracer for validation purposes (not added by default). /// -/// To get started, add `RaytracingMesh3d` and `MeshMaterial3d::` to your entities. +/// To get started, add this plugin to your app, and then add `RaytracingMesh3d` and `MeshMaterial3d::` to your entities. pub struct SolariPlugins; impl PluginGroup for SolariPlugins { diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl index c67b53e58e..be92b67bfa 100644 --- a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl +++ b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl @@ -47,7 +47,8 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { if ray_t_min == 0.0 { radiance = ray_hit.material.emissive; } // Sample direct lighting - radiance += throughput * diffuse_brdf * sample_random_light(ray_hit.world_position, ray_hit.world_normal, &rng); + let direct_lighting = sample_random_light(ray_hit.world_position, ray_hit.world_normal, &rng); + radiance += throughput * diffuse_brdf * direct_lighting.radiance * direct_lighting.inverse_pdf; // Sample new ray direction from the material BRDF for next bounce ray_direction = sample_cosine_hemisphere(ray_hit.world_normal, &rng); diff --git a/crates/bevy_solari/src/realtime/mod.rs b/crates/bevy_solari/src/realtime/mod.rs index 16e4f0ddbd..a2c17c7269 100644 --- a/crates/bevy_solari/src/realtime/mod.rs +++ b/crates/bevy_solari/src/realtime/mod.rs @@ -23,11 +23,16 @@ use node::SolariLightingNode; use prepare::prepare_solari_lighting_resources; use tracing::warn; +/// Raytraced direct and indirect lighting. +/// +/// When using this plugin, it's highly recommended to set `shadows_enabled: false` on all lights, as Solari replaces +/// traditional shadow mapping. pub struct SolariLightingPlugin; impl Plugin for SolariLightingPlugin { fn build(&self, app: &mut App) { embedded_asset!(app, "restir_di.wgsl"); + embedded_asset!(app, "restir_gi.wgsl"); app.register_type::() .insert_resource(DefaultOpaqueRendererMethod::deferred()); diff --git a/crates/bevy_solari/src/realtime/node.rs b/crates/bevy_solari/src/realtime/node.rs index 2fcc29b415..eaa432d8cb 100644 --- a/crates/bevy_solari/src/realtime/node.rs +++ b/crates/bevy_solari/src/realtime/node.rs @@ -36,8 +36,10 @@ pub mod graph { pub struct SolariLightingNode { bind_group_layout: BindGroupLayout, - initial_and_temporal_pipeline: CachedComputePipelineId, - spatial_and_shade_pipeline: CachedComputePipelineId, + di_initial_and_temporal_pipeline: CachedComputePipelineId, + di_spatial_and_shade_pipeline: CachedComputePipelineId, + gi_initial_and_temporal_pipeline: CachedComputePipelineId, + gi_spatial_and_shade_pipeline: CachedComputePipelineId, } impl ViewNode for SolariLightingNode { @@ -72,8 +74,10 @@ impl ViewNode for SolariLightingNode { let previous_view_uniforms = world.resource::(); let frame_count = world.resource::(); let ( - Some(initial_and_temporal_pipeline), - Some(spatial_and_shade_pipeline), + Some(di_initial_and_temporal_pipeline), + Some(di_spatial_and_shade_pipeline), + Some(gi_initial_and_temporal_pipeline), + Some(gi_spatial_and_shade_pipeline), Some(scene_bindings), Some(viewport), Some(gbuffer), @@ -82,8 +86,10 @@ impl ViewNode for SolariLightingNode { Some(view_uniforms), Some(previous_view_uniforms), ) = ( - pipeline_cache.get_compute_pipeline(self.initial_and_temporal_pipeline), - pipeline_cache.get_compute_pipeline(self.spatial_and_shade_pipeline), + pipeline_cache.get_compute_pipeline(self.di_initial_and_temporal_pipeline), + pipeline_cache.get_compute_pipeline(self.di_spatial_and_shade_pipeline), + pipeline_cache.get_compute_pipeline(self.gi_initial_and_temporal_pipeline), + pipeline_cache.get_compute_pipeline(self.gi_spatial_and_shade_pipeline), &scene_bindings.bind_group, camera.physical_viewport_size, view_prepass_textures.deferred_view(), @@ -101,8 +107,18 @@ impl ViewNode for SolariLightingNode { &self.bind_group_layout, &BindGroupEntries::sequential(( view_target.get_unsampled_color_attachment().view, - solari_lighting_resources.reservoirs_a.as_entire_binding(), - solari_lighting_resources.reservoirs_b.as_entire_binding(), + solari_lighting_resources + .di_reservoirs_a + .as_entire_binding(), + solari_lighting_resources + .di_reservoirs_b + .as_entire_binding(), + solari_lighting_resources + .gi_reservoirs_a + .as_entire_binding(), + solari_lighting_resources + .gi_reservoirs_b + .as_entire_binding(), gbuffer, depth_buffer, motion_vectors, @@ -135,14 +151,20 @@ impl ViewNode for SolariLightingNode { ], ); - pass.set_pipeline(initial_and_temporal_pipeline); + pass.set_pipeline(di_initial_and_temporal_pipeline); pass.set_push_constants( 0, bytemuck::cast_slice(&[frame_index, solari_lighting.reset as u32]), ); pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); - pass.set_pipeline(spatial_and_shade_pipeline); + pass.set_pipeline(di_spatial_and_shade_pipeline); + pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); + + pass.set_pipeline(gi_initial_and_temporal_pipeline); + pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); + + pass.set_pipeline(gi_spatial_and_shade_pipeline); pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); pass_span.end(&mut pass); @@ -189,10 +211,12 @@ impl FromWorld for SolariLightingNode { ( texture_storage_2d( ViewTarget::TEXTURE_FORMAT_HDR, - StorageTextureAccess::WriteOnly, + StorageTextureAccess::ReadWrite, ), storage_buffer_sized(false, None), storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), texture_2d(TextureSampleType::Uint), texture_depth_2d(), texture_2d(TextureSampleType::Float { filterable: true }), @@ -204,9 +228,9 @@ impl FromWorld for SolariLightingNode { ), ); - let initial_and_temporal_pipeline = + let di_initial_and_temporal_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { - label: Some("solari_lighting_initial_and_temporal_pipeline".into()), + label: Some("solari_lighting_di_initial_and_temporal_pipeline".into()), layout: vec![ scene_bindings.bind_group_layout.clone(), bind_group_layout.clone(), @@ -220,9 +244,9 @@ impl FromWorld for SolariLightingNode { ..default() }); - let spatial_and_shade_pipeline = + let di_spatial_and_shade_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { - label: Some("solari_lighting_spatial_and_shade_pipeline".into()), + label: Some("solari_lighting_di_spatial_and_shade_pipeline".into()), layout: vec![ scene_bindings.bind_group_layout.clone(), bind_group_layout.clone(), @@ -236,10 +260,46 @@ impl FromWorld for SolariLightingNode { ..default() }); + let gi_initial_and_temporal_pipeline = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("solari_lighting_gi_initial_and_temporal_pipeline".into()), + layout: vec![ + scene_bindings.bind_group_layout.clone(), + bind_group_layout.clone(), + ], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: load_embedded_asset!(world, "restir_gi.wgsl"), + shader_defs: vec![], + entry_point: Some("initial_and_temporal".into()), + zero_initialize_workgroup_memory: false, + }); + + let gi_spatial_and_shade_pipeline = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("solari_lighting_gi_spatial_and_shade_pipeline".into()), + layout: vec![ + scene_bindings.bind_group_layout.clone(), + bind_group_layout.clone(), + ], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: load_embedded_asset!(world, "restir_gi.wgsl"), + shader_defs: vec![], + entry_point: Some("spatial_and_shade".into()), + zero_initialize_workgroup_memory: false, + }); + Self { bind_group_layout, - initial_and_temporal_pipeline, - spatial_and_shade_pipeline, + di_initial_and_temporal_pipeline, + di_spatial_and_shade_pipeline, + gi_initial_and_temporal_pipeline, + gi_spatial_and_shade_pipeline, } } } diff --git a/crates/bevy_solari/src/realtime/prepare.rs b/crates/bevy_solari/src/realtime/prepare.rs index 992a75c451..46a94a3ca2 100644 --- a/crates/bevy_solari/src/realtime/prepare.rs +++ b/crates/bevy_solari/src/realtime/prepare.rs @@ -17,14 +17,19 @@ use bevy_render::{ renderer::RenderDevice, }; -/// Size of a Reservoir shader struct in bytes. -const RESERVOIR_STRUCT_SIZE: u64 = 32; +/// Size of a DI Reservoir shader struct in bytes. +const DI_RESERVOIR_STRUCT_SIZE: u64 = 32; + +/// Size of a GI Reservoir shader struct in bytes. +const GI_RESERVOIR_STRUCT_SIZE: u64 = 48; /// Internal rendering resources used for Solari lighting. #[derive(Component)] pub struct SolariLightingResources { - pub reservoirs_a: Buffer, - pub reservoirs_b: Buffer, + pub di_reservoirs_a: Buffer, + pub di_reservoirs_b: Buffer, + pub gi_reservoirs_a: Buffer, + pub gi_reservoirs_b: Buffer, pub previous_gbuffer: (Texture, TextureView), pub previous_depth: (Texture, TextureView), pub view_size: UVec2, @@ -47,18 +52,30 @@ pub fn prepare_solari_lighting_resources( continue; } - let size = (view_size.x * view_size.y) as u64 * RESERVOIR_STRUCT_SIZE; - - let reservoirs_a = render_device.create_buffer(&BufferDescriptor { - label: Some("solari_lighting_reservoirs_a"), - size, + let di_reservoirs_a = render_device.create_buffer(&BufferDescriptor { + label: Some("solari_lighting_di_reservoirs_a"), + size: (view_size.x * view_size.y) as u64 * DI_RESERVOIR_STRUCT_SIZE, usage: BufferUsages::STORAGE, mapped_at_creation: false, }); - let reservoirs_b = render_device.create_buffer(&BufferDescriptor { - label: Some("solari_lighting_reservoirs_b"), - size, + let di_reservoirs_b = render_device.create_buffer(&BufferDescriptor { + label: Some("solari_lighting_di_reservoirs_b"), + size: (view_size.x * view_size.y) as u64 * DI_RESERVOIR_STRUCT_SIZE, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + + let gi_reservoirs_a = render_device.create_buffer(&BufferDescriptor { + label: Some("solari_lighting_gi_reservoirs_a"), + size: (view_size.x * view_size.y) as u64 * GI_RESERVOIR_STRUCT_SIZE, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + + let gi_reservoirs_b = render_device.create_buffer(&BufferDescriptor { + label: Some("solari_lighting_gi_reservoirs_b"), + size: (view_size.x * view_size.y) as u64 * GI_RESERVOIR_STRUCT_SIZE, usage: BufferUsages::STORAGE, mapped_at_creation: false, }); @@ -88,8 +105,10 @@ pub fn prepare_solari_lighting_resources( let previous_depth_view = previous_depth.create_view(&TextureViewDescriptor::default()); commands.entity(entity).insert(SolariLightingResources { - reservoirs_a, - reservoirs_b, + di_reservoirs_a, + di_reservoirs_b, + gi_reservoirs_a, + gi_reservoirs_b, previous_gbuffer: (previous_gbuffer, previous_gbuffer_view), previous_depth: (previous_depth, previous_depth_view), view_size, diff --git a/crates/bevy_solari/src/realtime/restir_di.wgsl b/crates/bevy_solari/src/realtime/restir_di.wgsl index 70de4564cc..f56d80fb15 100644 --- a/crates/bevy_solari/src/realtime/restir_di.wgsl +++ b/crates/bevy_solari/src/realtime/restir_di.wgsl @@ -10,16 +10,16 @@ #import bevy_solari::sampling::{LightSample, generate_random_light_sample, calculate_light_contribution, trace_light_visibility, sample_disk} #import bevy_solari::scene_bindings::{previous_frame_light_id_translations, LIGHT_NOT_PRESENT_THIS_FRAME} -@group(1) @binding(0) var view_output: texture_storage_2d; -@group(1) @binding(1) var reservoirs_a: array; -@group(1) @binding(2) var reservoirs_b: array; -@group(1) @binding(3) var gbuffer: texture_2d; -@group(1) @binding(4) var depth_buffer: texture_depth_2d; -@group(1) @binding(5) var motion_vectors: texture_2d; -@group(1) @binding(6) var previous_gbuffer: texture_2d; -@group(1) @binding(7) var previous_depth_buffer: texture_depth_2d; -@group(1) @binding(8) var view: View; -@group(1) @binding(9) var previous_view: PreviousViewUniforms; +@group(1) @binding(0) var view_output: texture_storage_2d; +@group(1) @binding(1) var di_reservoirs_a: array; +@group(1) @binding(2) var di_reservoirs_b: array; +@group(1) @binding(5) var gbuffer: texture_2d; +@group(1) @binding(6) var depth_buffer: texture_depth_2d; +@group(1) @binding(7) var motion_vectors: texture_2d; +@group(1) @binding(8) var previous_gbuffer: texture_2d; +@group(1) @binding(9) var previous_depth_buffer: texture_depth_2d; +@group(1) @binding(10) var view: View; +@group(1) @binding(11) var previous_view: PreviousViewUniforms; struct PushConstants { frame_index: u32, reset: u32 } var constants: PushConstants; @@ -38,7 +38,7 @@ fn initial_and_temporal(@builtin(global_invocation_id) global_id: vec3) { let depth = textureLoad(depth_buffer, global_id.xy, 0); if depth == 0.0 { - reservoirs_b[pixel_index] = empty_reservoir(); + di_reservoirs_b[pixel_index] = empty_reservoir(); return; } let gpixel = textureLoad(gbuffer, global_id.xy, 0); @@ -49,9 +49,9 @@ fn initial_and_temporal(@builtin(global_invocation_id) global_id: vec3) { let initial_reservoir = generate_initial_reservoir(world_position, world_normal, diffuse_brdf, &rng); let temporal_reservoir = load_temporal_reservoir(global_id.xy, depth, world_position, world_normal); - let combined_reservoir = merge_reservoirs(initial_reservoir, temporal_reservoir, world_position, world_normal, diffuse_brdf, &rng); + let merge_result = merge_reservoirs(initial_reservoir, temporal_reservoir, world_position, world_normal, diffuse_brdf, &rng); - reservoirs_b[pixel_index] = combined_reservoir.merged_reservoir; + di_reservoirs_b[pixel_index] = merge_result.merged_reservoir; } @compute @workgroup_size(8, 8, 1) @@ -63,7 +63,7 @@ fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { let depth = textureLoad(depth_buffer, global_id.xy, 0); if depth == 0.0 { - reservoirs_a[pixel_index] = empty_reservoir(); + di_reservoirs_a[pixel_index] = empty_reservoir(); textureStore(view_output, global_id.xy, vec4(vec3(0.0), 1.0)); return; } @@ -74,12 +74,12 @@ fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { let diffuse_brdf = base_color / PI; let emissive = rgb9e5_to_vec3_(gpixel.g); - let input_reservoir = reservoirs_b[pixel_index]; + let input_reservoir = di_reservoirs_b[pixel_index]; let spatial_reservoir = load_spatial_reservoir(global_id.xy, depth, world_position, world_normal, &rng); let merge_result = merge_reservoirs(input_reservoir, spatial_reservoir, world_position, world_normal, diffuse_brdf, &rng); let combined_reservoir = merge_result.merged_reservoir; - reservoirs_a[pixel_index] = combined_reservoir; + di_reservoirs_a[pixel_index] = combined_reservoir; var pixel_color = merge_result.selected_sample_radiance * combined_reservoir.unbiased_contribution_weight * combined_reservoir.visibility; pixel_color *= view.exposure; @@ -123,10 +123,14 @@ fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3 let motion_vector = textureLoad(motion_vectors, pixel_id, 0).xy; let temporal_pixel_id_float = round(vec2(pixel_id) - (motion_vector * view.viewport.zw)); let temporal_pixel_id = vec2(temporal_pixel_id_float); + + // Check if the current pixel was off screen during the previous frame (current pixel is newly visible), + // or if all temporal history should assumed to be invalid if any(temporal_pixel_id_float < vec2(0.0)) || any(temporal_pixel_id_float >= view.viewport.zw) || bool(constants.reset) { return empty_reservoir(); } + // Check if the pixel features have changed heavily between the current and previous frame let temporal_depth = textureLoad(previous_depth_buffer, temporal_pixel_id, 0); let temporal_gpixel = textureLoad(previous_gbuffer, temporal_pixel_id, 0); let temporal_world_position = reconstruct_previous_world_position(temporal_pixel_id, temporal_depth); @@ -136,8 +140,9 @@ fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3 } let temporal_pixel_index = temporal_pixel_id.x + temporal_pixel_id.y * u32(view.viewport.z); - var temporal_reservoir = reservoirs_a[temporal_pixel_index]; + var temporal_reservoir = di_reservoirs_a[temporal_pixel_index]; + // Check if the light selected in the previous frame no longer exists in the current frame (e.g. entity despawned) temporal_reservoir.sample.light_id.x = previous_frame_light_id_translations[temporal_reservoir.sample.light_id.x]; if temporal_reservoir.sample.light_id.x == LIGHT_NOT_PRESENT_THIS_FRAME { return empty_reservoir(); @@ -160,7 +165,7 @@ fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3< } let spatial_pixel_index = spatial_pixel_id.x + spatial_pixel_id.y * u32(view.viewport.z); - var spatial_reservoir = reservoirs_b[spatial_pixel_index]; + var spatial_reservoir = di_reservoirs_b[spatial_pixel_index]; if reservoir_valid(spatial_reservoir) { spatial_reservoir.visibility = trace_light_visibility(spatial_reservoir.sample, world_position); @@ -209,7 +214,7 @@ fn depth_ndc_to_view_z(ndc_depth: f32) -> f32 { #endif } -// Don't adjust the size of this struct without also adjusting RESERVOIR_STRUCT_SIZE. +// Don't adjust the size of this struct without also adjusting DI_RESERVOIR_STRUCT_SIZE. struct Reservoir { sample: LightSample, weight_sum: f32, diff --git a/crates/bevy_solari/src/realtime/restir_gi.wgsl b/crates/bevy_solari/src/realtime/restir_gi.wgsl new file mode 100644 index 0000000000..13bb5d15eb --- /dev/null +++ b/crates/bevy_solari/src/realtime/restir_gi.wgsl @@ -0,0 +1,310 @@ +// https://intro-to-restir.cwyman.org/presentations/2023ReSTIR_Course_Notes.pdf + +#import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance +#import bevy_pbr::pbr_deferred_types::unpack_24bit_normal +#import bevy_pbr::prepass_bindings::PreviousViewUniforms +#import bevy_pbr::rgb9e5::rgb9e5_to_vec3_ +#import bevy_pbr::utils::{rand_f, octahedral_decode} +#import bevy_render::maths::{PI, PI_2} +#import bevy_render::view::View +#import bevy_solari::sampling::{sample_uniform_hemisphere, sample_random_light, sample_disk, trace_point_visibility} +#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX} + +@group(1) @binding(0) var view_output: texture_storage_2d; +@group(1) @binding(3) var gi_reservoirs_a: array; +@group(1) @binding(4) var gi_reservoirs_b: array; +@group(1) @binding(5) var gbuffer: texture_2d; +@group(1) @binding(6) var depth_buffer: texture_depth_2d; +@group(1) @binding(7) var motion_vectors: texture_2d; +@group(1) @binding(8) var previous_gbuffer: texture_2d; +@group(1) @binding(9) var previous_depth_buffer: texture_depth_2d; +@group(1) @binding(10) var view: View; +@group(1) @binding(11) var previous_view: PreviousViewUniforms; +struct PushConstants { frame_index: u32, reset: u32 } +var constants: PushConstants; + +const SPATIAL_REUSE_RADIUS_PIXELS = 30.0; +const CONFIDENCE_WEIGHT_CAP = 30.0; + +@compute @workgroup_size(8, 8, 1) +fn initial_and_temporal(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= vec2u(view.viewport.zw)) { return; } + + let pixel_index = global_id.x + global_id.y * u32(view.viewport.z); + var rng = pixel_index + constants.frame_index; + + let depth = textureLoad(depth_buffer, global_id.xy, 0); + if depth == 0.0 { + gi_reservoirs_b[pixel_index] = empty_reservoir(); + return; + } + let gpixel = textureLoad(gbuffer, global_id.xy, 0); + let world_position = reconstruct_world_position(global_id.xy, depth); + let world_normal = octahedral_decode(unpack_24bit_normal(gpixel.a)); + + let initial_reservoir = generate_initial_reservoir(world_position, world_normal, &rng); + let temporal_reservoir = load_temporal_reservoir(global_id.xy, depth, world_position, world_normal); + let merge_result = merge_reservoirs(initial_reservoir, temporal_reservoir, vec3(1.0), vec3(1.0), &rng); + + gi_reservoirs_b[pixel_index] = merge_result.merged_reservoir; +} + +@compute @workgroup_size(8, 8, 1) +fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= vec2u(view.viewport.zw)) { return; } + + let pixel_index = global_id.x + global_id.y * u32(view.viewport.z); + var rng = pixel_index + constants.frame_index; + + let depth = textureLoad(depth_buffer, global_id.xy, 0); + if depth == 0.0 { + gi_reservoirs_a[pixel_index] = empty_reservoir(); + return; + } + let gpixel = textureLoad(gbuffer, global_id.xy, 0); + let world_position = reconstruct_world_position(global_id.xy, depth); + let world_normal = octahedral_decode(unpack_24bit_normal(gpixel.a)); + let base_color = pow(unpack4x8unorm(gpixel.r).rgb, vec3(2.2)); + let diffuse_brdf = base_color / PI; + + let input_reservoir = gi_reservoirs_b[pixel_index]; + let spatial_reservoir = load_spatial_reservoir(global_id.xy, depth, world_position, world_normal, &rng); + + let input_factor = dot(normalize(input_reservoir.sample_point_world_position - world_position), world_normal) * diffuse_brdf; + let spatial_factor = dot(normalize(spatial_reservoir.sample_point_world_position - world_position), world_normal) * diffuse_brdf; + + let merge_result = merge_reservoirs(input_reservoir, spatial_reservoir, input_factor, spatial_factor, &rng); + let combined_reservoir = merge_result.merged_reservoir; + + gi_reservoirs_a[pixel_index] = combined_reservoir; + + var pixel_color = textureLoad(view_output, global_id.xy); + pixel_color += vec4(merge_result.selected_sample_radiance * combined_reservoir.unbiased_contribution_weight * view.exposure, 0.0); + textureStore(view_output, global_id.xy, pixel_color); +} + +fn generate_initial_reservoir(world_position: vec3, world_normal: vec3, rng: ptr) -> Reservoir{ + var reservoir = empty_reservoir(); + + let ray_direction = sample_uniform_hemisphere(world_normal, rng); + let ray_hit = trace_ray(world_position, ray_direction, RAY_T_MIN, RAY_T_MAX, RAY_FLAG_NONE); + + if ray_hit.kind == RAY_QUERY_INTERSECTION_NONE { + return reservoir; + } + + let sample_point = resolve_ray_hit_full(ray_hit); + + if all(sample_point.material.emissive != vec3(0.0)) { + return reservoir; + } + + reservoir.sample_point_world_position = sample_point.world_position; + reservoir.sample_point_world_normal = sample_point.world_normal; + reservoir.confidence_weight = 1.0; + + let sample_point_diffuse_brdf = sample_point.material.base_color / PI; + let direct_lighting = sample_random_light(sample_point.world_position, sample_point.world_normal, rng); + reservoir.radiance = direct_lighting.radiance * sample_point_diffuse_brdf; + + let inverse_uniform_hemisphere_pdf = PI_2; + reservoir.unbiased_contribution_weight = direct_lighting.inverse_pdf * inverse_uniform_hemisphere_pdf; + + return reservoir; +} + +fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3) -> Reservoir { + let motion_vector = textureLoad(motion_vectors, pixel_id, 0).xy; + let temporal_pixel_id_float = round(vec2(pixel_id) - (motion_vector * view.viewport.zw)); + let temporal_pixel_id = vec2(temporal_pixel_id_float); + + // Check if the current pixel was off screen during the previous frame (current pixel is newly visible), + // or if all temporal history should assumed to be invalid + if any(temporal_pixel_id_float < vec2(0.0)) || any(temporal_pixel_id_float >= view.viewport.zw) || bool(constants.reset) { + return empty_reservoir(); + } + + // Check if the pixel features have changed heavily between the current and previous frame + let temporal_depth = textureLoad(previous_depth_buffer, temporal_pixel_id, 0); + let temporal_gpixel = textureLoad(previous_gbuffer, temporal_pixel_id, 0); + let temporal_world_position = reconstruct_previous_world_position(temporal_pixel_id, temporal_depth); + let temporal_world_normal = octahedral_decode(unpack_24bit_normal(temporal_gpixel.a)); + if pixel_dissimilar(depth, world_position, temporal_world_position, world_normal, temporal_world_normal) { + return empty_reservoir(); + } + + let temporal_pixel_index = temporal_pixel_id.x + temporal_pixel_id.y * u32(view.viewport.z); + var temporal_reservoir = gi_reservoirs_a[temporal_pixel_index]; + + temporal_reservoir.confidence_weight = min(temporal_reservoir.confidence_weight, CONFIDENCE_WEIGHT_CAP); + + return temporal_reservoir; +} + +fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3, rng: ptr) -> Reservoir { + let spatial_pixel_id = get_neighbor_pixel_id(pixel_id, rng); + + let spatial_depth = textureLoad(depth_buffer, spatial_pixel_id, 0); + let spatial_gpixel = textureLoad(gbuffer, spatial_pixel_id, 0); + let spatial_world_position = reconstruct_world_position(spatial_pixel_id, spatial_depth); + let spatial_world_normal = octahedral_decode(unpack_24bit_normal(spatial_gpixel.a)); + if pixel_dissimilar(depth, world_position, spatial_world_position, world_normal, spatial_world_normal) { + return empty_reservoir(); + } + + let spatial_pixel_index = spatial_pixel_id.x + spatial_pixel_id.y * u32(view.viewport.z); + var spatial_reservoir = gi_reservoirs_b[spatial_pixel_index]; + + var jacobian = jacobian( + world_position, + spatial_world_position, + spatial_reservoir.sample_point_world_position, + spatial_reservoir.sample_point_world_normal + ); + if jacobian > 10.0 || jacobian < 0.1 { + return empty_reservoir(); + } + spatial_reservoir.unbiased_contribution_weight *= jacobian; + + spatial_reservoir.unbiased_contribution_weight *= trace_point_visibility(world_position, spatial_reservoir.sample_point_world_position); + + return spatial_reservoir; +} + +fn get_neighbor_pixel_id(center_pixel_id: vec2, rng: ptr) -> vec2 { + var spatial_id = vec2(center_pixel_id) + vec2(sample_disk(SPATIAL_REUSE_RADIUS_PIXELS, rng)); + spatial_id = clamp(spatial_id, vec2(0i), vec2(view.viewport.zw) - 1i); + return vec2(spatial_id); +} + +fn jacobian( + world_position: vec3, + spatial_world_position: vec3, + sample_point_world_position: vec3, + sample_point_world_normal: vec3, +) -> f32 { + let r = world_position - sample_point_world_position; + let q = spatial_world_position - sample_point_world_position; + let rl = length(r); + let ql = length(q); + let phi_r = saturate(dot(r / rl, sample_point_world_normal)); + let phi_q = saturate(dot(q / ql, sample_point_world_normal)); + let jacobian = (phi_r * ql * ql) / (phi_q * rl * rl); + return select(jacobian, 0.0, isinf(jacobian) || isnan(jacobian)); +} + +fn isinf(x: f32) -> bool { + return (bitcast(x) & 0x7fffffffu) == 0x7f800000u; +} + +fn isnan(x: f32) -> bool { + return (bitcast(x) & 0x7fffffffu) > 0x7f800000u; +} + +fn reconstruct_world_position(pixel_id: vec2, depth: f32) -> vec3 { + let uv = (vec2(pixel_id) + 0.5) / view.viewport.zw; + let xy_ndc = (uv - vec2(0.5)) * vec2(2.0, -2.0); + let world_pos = view.world_from_clip * vec4(xy_ndc, depth, 1.0); + return world_pos.xyz / world_pos.w; +} + +fn reconstruct_previous_world_position(pixel_id: vec2, depth: f32) -> vec3 { + let uv = (vec2(pixel_id) + 0.5) / view.viewport.zw; + let xy_ndc = (uv - vec2(0.5)) * vec2(2.0, -2.0); + let world_pos = previous_view.world_from_clip * vec4(xy_ndc, depth, 1.0); + return world_pos.xyz / world_pos.w; +} + +// Reject if tangent plane difference difference more than 0.3% or angle between normals more than 25 degrees +fn pixel_dissimilar(depth: f32, world_position: vec3, other_world_position: vec3, normal: vec3, other_normal: vec3) -> bool { + // https://developer.download.nvidia.com/video/gputechconf/gtc/2020/presentations/s22699-fast-denoising-with-self-stabilizing-recurrent-blurs.pdf#page=45 + let tangent_plane_distance = abs(dot(normal, other_world_position - world_position)); + let view_z = -depth_ndc_to_view_z(depth); + + return tangent_plane_distance / view_z > 0.003 || dot(normal, other_normal) < 0.906; +} + +fn depth_ndc_to_view_z(ndc_depth: f32) -> f32 { +#ifdef VIEW_PROJECTION_PERSPECTIVE + return -view.clip_from_view[3][2]() / ndc_depth; +#else ifdef VIEW_PROJECTION_ORTHOGRAPHIC + return -(view.clip_from_view[3][2] - ndc_depth) / view.clip_from_view[2][2]; +#else + let view_pos = view.view_from_clip * vec4(0.0, 0.0, ndc_depth, 1.0); + return view_pos.z / view_pos.w; +#endif +} + +// Don't adjust the size of this struct without also adjusting GI_RESERVOIR_STRUCT_SIZE. +struct Reservoir { + sample_point_world_position: vec3, + weight_sum: f32, + radiance: vec3, + confidence_weight: f32, + sample_point_world_normal: vec3, + unbiased_contribution_weight: f32, +} + +fn empty_reservoir() -> Reservoir { + return Reservoir( + vec3(0.0), + 0.0, + vec3(0.0), + 0.0, + vec3(0.0), + 0.0, + ); +} + +struct ReservoirMergeResult { + merged_reservoir: Reservoir, + selected_sample_radiance: vec3, +} + +fn merge_reservoirs( + canonical_reservoir: Reservoir, + other_reservoir: Reservoir, + canonical_factor: vec3, + other_factor: vec3, + rng: ptr, +) -> ReservoirMergeResult { + var combined_reservoir = empty_reservoir(); + combined_reservoir.confidence_weight = canonical_reservoir.confidence_weight + other_reservoir.confidence_weight; + + if combined_reservoir.confidence_weight == 0.0 { return ReservoirMergeResult(combined_reservoir, vec3(0.0)); } + + // TODO: Balance heuristic MIS weights + let mis_weight_denominator = 1.0 / combined_reservoir.confidence_weight; + + let canonical_mis_weight = canonical_reservoir.confidence_weight * mis_weight_denominator; + let canonical_radiance = canonical_reservoir.radiance * canonical_factor; + let canonical_target_function = luminance(canonical_radiance); + let canonical_resampling_weight = canonical_mis_weight * (canonical_target_function * canonical_reservoir.unbiased_contribution_weight); + + let other_mis_weight = other_reservoir.confidence_weight * mis_weight_denominator; + let other_radiance = other_reservoir.radiance * other_factor; + let other_target_function = luminance(other_radiance); + let other_resampling_weight = other_mis_weight * (other_target_function * other_reservoir.unbiased_contribution_weight); + + combined_reservoir.weight_sum = canonical_resampling_weight + other_resampling_weight; + + if rand_f(rng) < other_resampling_weight / combined_reservoir.weight_sum { + combined_reservoir.sample_point_world_position = other_reservoir.sample_point_world_position; + combined_reservoir.sample_point_world_normal = other_reservoir.sample_point_world_normal; + combined_reservoir.radiance = other_reservoir.radiance; + + let inverse_target_function = select(0.0, 1.0 / other_target_function, other_target_function > 0.0); + combined_reservoir.unbiased_contribution_weight = combined_reservoir.weight_sum * inverse_target_function; + + return ReservoirMergeResult(combined_reservoir, other_radiance); + } else { + combined_reservoir.sample_point_world_position = canonical_reservoir.sample_point_world_position; + combined_reservoir.sample_point_world_normal = canonical_reservoir.sample_point_world_normal; + combined_reservoir.radiance = canonical_reservoir.radiance; + + let inverse_target_function = select(0.0, 1.0 / canonical_target_function, canonical_target_function > 0.0); + combined_reservoir.unbiased_contribution_weight = combined_reservoir.weight_sum * inverse_target_function; + + return ReservoirMergeResult(combined_reservoir, canonical_radiance); + } +} diff --git a/crates/bevy_solari/src/scene/sampling.wgsl b/crates/bevy_solari/src/scene/sampling.wgsl index be709f0bc8..e1f67ac1ed 100644 --- a/crates/bevy_solari/src/scene/sampling.wgsl +++ b/crates/bevy_solari/src/scene/sampling.wgsl @@ -15,6 +15,17 @@ fn sample_cosine_hemisphere(normal: vec3, rng: ptr) -> vec3< return vec3(x, y, z); } +// https://www.pbr-book.org/3ed-2018/Monte_Carlo_Integration/2D_Sampling_with_Multidimensional_Transformations#UniformlySamplingaHemisphere +fn sample_uniform_hemisphere(normal: vec3, rng: ptr) -> vec3 { + let cos_theta = rand_f(rng); + let phi = PI_2 * rand_f(rng); + let sin_theta = sqrt(max(1.0 - cos_theta * cos_theta, 0.0)); + let x = sin_theta * cos(phi); + let y = sin_theta * sin(phi); + let z = cos_theta; + return build_orthonormal_basis(normal) * vec3(x, y, z); +} + // https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec19%3A294 fn sample_disk(disk_radius: f32, rng: ptr) -> vec2 { let ab = 2.0 * rand_vec2f(rng) - 1.0; @@ -37,11 +48,16 @@ fn sample_disk(disk_radius: f32, rng: ptr) -> vec2 { return vec2(x, y); } -fn sample_random_light(ray_origin: vec3, origin_world_normal: vec3, rng: ptr) -> vec3 { +struct SampleRandomLightResult { + radiance: vec3, + inverse_pdf: f32, +} + +fn sample_random_light(ray_origin: vec3, origin_world_normal: vec3, rng: ptr) -> SampleRandomLightResult { let light_sample = generate_random_light_sample(rng); let light_contribution = calculate_light_contribution(light_sample, ray_origin, origin_world_normal); let visibility = trace_light_visibility(light_sample, ray_origin); - return light_contribution.radiance * visibility * light_contribution.inverse_pdf; + return SampleRandomLightResult(light_contribution.radiance * visibility, light_contribution.inverse_pdf); } struct LightSample { @@ -171,10 +187,15 @@ fn trace_emissive_mesh_visibility(light_sample: LightSample, instance_id: u32, r let triangle_data = resolve_triangle_data_full(instance_id, triangle_id, barycentrics); - let light_distance = distance(ray_origin, triangle_data.world_position); - let ray_direction = (triangle_data.world_position - ray_origin) / light_distance; + return trace_point_visibility(ray_origin, triangle_data.world_position); +} - let ray_t_max = light_distance - RAY_T_MIN - RAY_T_MIN; +fn trace_point_visibility(ray_origin: vec3, point: vec3) -> f32 { + let ray = point - ray_origin; + let dist = length(ray); + let ray_direction = ray / dist; + + let ray_t_max = dist - RAY_T_MIN - RAY_T_MIN; if ray_t_max < RAY_T_MIN { return 0.0; } let ray_hit = trace_ray(ray_origin, ray_direction, RAY_T_MIN, ray_t_max, RAY_FLAG_TERMINATE_ON_FIRST_HIT); diff --git a/crates/bevy_solari/src/scene/types.rs b/crates/bevy_solari/src/scene/types.rs index 8ee33b31fc..7b4c164df8 100644 --- a/crates/bevy_solari/src/scene/types.rs +++ b/crates/bevy_solari/src/scene/types.rs @@ -10,8 +10,8 @@ use derive_more::derive::From; /// A mesh component used for raytracing. /// -/// The mesh used in this component must have [`bevy_render::mesh::Mesh::enable_raytracing`] set to true, -/// use the following set of vertex attributes: `{POSITION, NORMAL, UV_0, TANGENT}`, use [`bevy_render::render_resource::PrimitiveTopology::TriangleList`], +/// The mesh used in this component must have [`Mesh::enable_raytracing`] set to true, +/// use the following set of vertex attributes: `{POSITION, NORMAL, UV_0, TANGENT}`, use [`bevy_mesh::PrimitiveTopology::TriangleList`], /// and use [`bevy_mesh::Indices::U32`]. /// /// The material used for this entity must be [`MeshMaterial3d`]. diff --git a/release-content/release-notes/bevy_solari.md b/release-content/release-notes/bevy_solari.md index ac1b1abe1c..e6727deb47 100644 --- a/release-content/release-notes/bevy_solari.md +++ b/release-content/release-notes/bevy_solari.md @@ -1,7 +1,7 @@ --- title: Initial raytraced lighting progress (bevy_solari) authors: ["@JMS55"] -pull_requests: [19058, 19620, 19790] +pull_requests: [19058, 19620, 19790, 20020] --- (TODO: Embed solari example screenshot here) @@ -19,13 +19,13 @@ In Bevy, direct lighting comes from analytical light components (`DirectionalLig The problem with these methods is that they all have large downsides: * Emissive meshes do not cast light onto other objects, either direct or indirect. -* Shadow maps are very expensive to render and consume a lot of memory, so you're limited to using only a few shadow casting lights. Good quality can be difficult to obtain in large scenes. +* Shadow maps are very expensive to render and consume a lot of memory, so you're limited to using only a few shadow casting lights. Good shadow quality can be difficult to obtain in large scenes. * Baked lighting does not update in realtime as objects and lights move around, is low resolution/quality, and requires time to bake, slowing down game production. * Screen-space methods have low quality and do not capture off-screen geometry and light. Bevy Solari is intended as a completely alternate, high-end lighting solution for Bevy that uses GPU-accelerated raytracing to fix all of the above problems. Emissive meshes will properly cast light and shadows, you will be able to have hundreds of shadow casting lights, quality will be much better, it will require no baking time, and it will support _fully_ dynamic scenes! -While Bevy 0.17 adds the bevy_solari crate, it's intended as a long-term project. It is not yet usable by game developers. However, feel free to run the solari example (`cargo run --release --example solari --features bevy_solari` (realtime direct lighting, no denoising) or `cargo run --release --example solari --features bevy_solari -- --pathtracer` (non-realtime pathtracing)) to check out the progress we've made, and look forward to more work on Bevy Solari in future releases! +While Bevy 0.17 adds the bevy_solari crate, it's intended as a long-term project. It is not yet usable by game developers. However, feel free to run the solari example (`cargo run --release --example solari --features bevy_solari` (realtime direct and 1-bounce indirect lighting, no denoising) or `cargo run --release --example solari --features bevy_solari -- --pathtracer` (non-realtime pathtracing)) to check out the progress we've made, and look forward to more work on Bevy Solari in future releases! (TODO: Embed bevy_solari logo here, or somewhere else that looks good) From ace0114bdde973afb4e097f29c9e99cfa39a65d6 Mon Sep 17 00:00:00 2001 From: Talin Date: Sun, 13 Jul 2025 10:25:11 -0700 Subject: [PATCH 15/19] Changing the notification protocol for core_widgets. (#20086) Notifications now include the source entity. This is useful for callbacks that are responsible for more than one widget. Part of #19236 This is an incremental change only: I have not altered the fundamental nature of callbacks, as this is still in discussion. The only change here is to include the source entity id with the notification. The existing examples don't leverage this new field, but that will change when I work on the color sliders PR. I have been careful not to use the word "events" in describing the notification message structs because they are not capital-E `Events` at this time. That may change depending on the outcome of discussions. @alice-i-cecile --- crates/bevy_core_widgets/src/core_button.rs | 9 +-- crates/bevy_core_widgets/src/core_checkbox.rs | 12 +++- crates/bevy_core_widgets/src/core_radio.rs | 9 ++- crates/bevy_core_widgets/src/core_slider.rs | 36 ++++++++++-- crates/bevy_core_widgets/src/lib.rs | 14 +++++ crates/bevy_feathers/src/controls/button.rs | 6 +- crates/bevy_feathers/src/controls/checkbox.rs | 4 +- crates/bevy_feathers/src/controls/slider.rs | 4 +- .../src/controls/toggle_switch.rs | 4 +- examples/ui/core_widgets.rs | 33 ++++++----- examples/ui/core_widgets_observers.rs | 25 ++++---- examples/ui/feathers.rs | 57 ++++++++++++------- .../release-notes/headless-widgets.md | 2 +- 13 files changed, 141 insertions(+), 74 deletions(-) diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs index 8c4ec9b22e..5ef0d33ef0 100644 --- a/crates/bevy_core_widgets/src/core_button.rs +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -2,6 +2,7 @@ use accesskit::Role; use bevy_a11y::AccessibilityNode; use bevy_app::{App, Plugin}; use bevy_ecs::query::Has; +use bevy_ecs::system::In; use bevy_ecs::{ component::Component, entity::Entity, @@ -15,7 +16,7 @@ use bevy_input_focus::FocusedInput; use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; use bevy_ui::{InteractionDisabled, Pressed}; -use crate::{Callback, Notify}; +use crate::{Activate, Callback, Notify}; /// Headless button widget. This widget maintains a "pressed" state, which is used to /// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked` @@ -25,7 +26,7 @@ use crate::{Callback, Notify}; pub struct CoreButton { /// Callback to invoke when the button is clicked, or when the `Enter` or `Space` key /// is pressed while the button is focused. - pub on_activate: Callback, + pub on_activate: Callback>, } fn button_on_key_event( @@ -41,7 +42,7 @@ fn button_on_key_event( && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) { trigger.propagate(false); - commands.notify(&bstate.on_activate); + commands.notify_with(&bstate.on_activate, Activate(trigger.target())); } } } @@ -55,7 +56,7 @@ fn button_on_pointer_click( if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target()) { trigger.propagate(false); if pressed && !disabled { - commands.notify(&bstate.on_activate); + commands.notify_with(&bstate.on_activate, Activate(trigger.target())); } } } diff --git a/crates/bevy_core_widgets/src/core_checkbox.rs b/crates/bevy_core_widgets/src/core_checkbox.rs index 05edc53c44..01e3e61e49 100644 --- a/crates/bevy_core_widgets/src/core_checkbox.rs +++ b/crates/bevy_core_widgets/src/core_checkbox.rs @@ -15,7 +15,7 @@ use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; use bevy_picking::events::{Click, Pointer}; use bevy_ui::{Checkable, Checked, InteractionDisabled}; -use crate::{Callback, Notify as _}; +use crate::{Callback, Notify as _, ValueChange}; /// Headless widget implementation for checkboxes. The [`Checked`] component represents the current /// state of the checkbox. The `on_change` field is an optional system id that will be run when the @@ -34,7 +34,7 @@ pub struct CoreCheckbox { /// One-shot system that is run when the checkbox state needs to be changed. If this value is /// `Callback::Ignore`, then the checkbox will update it's own internal [`Checked`] state /// without notification. - pub on_change: Callback>, + pub on_change: Callback>>, } fn checkbox_on_key_input( @@ -162,7 +162,13 @@ fn set_checkbox_state( new_state: bool, ) { if !matches!(checkbox.on_change, Callback::Ignore) { - commands.notify_with(&checkbox.on_change, new_state); + commands.notify_with( + &checkbox.on_change, + ValueChange { + source: entity.into(), + value: new_state, + }, + ); } else if new_state { commands.entity(entity.into()).insert(Checked); } else { diff --git a/crates/bevy_core_widgets/src/core_radio.rs b/crates/bevy_core_widgets/src/core_radio.rs index a6c99a0d04..0aeebe9825 100644 --- a/crates/bevy_core_widgets/src/core_radio.rs +++ b/crates/bevy_core_widgets/src/core_radio.rs @@ -6,7 +6,6 @@ use bevy_ecs::query::Has; use bevy_ecs::system::In; use bevy_ecs::{ component::Component, - entity::Entity, observer::On, query::With, system::{Commands, Query}, @@ -17,7 +16,7 @@ use bevy_input_focus::FocusedInput; use bevy_picking::events::{Click, Pointer}; use bevy_ui::{Checkable, Checked, InteractionDisabled}; -use crate::{Callback, Notify}; +use crate::{Activate, Callback, Notify}; /// Headless widget implementation for a "radio button group". This component is used to group /// multiple [`CoreRadio`] components together, allowing them to behave as a single unit. It @@ -38,7 +37,7 @@ use crate::{Callback, Notify}; #[require(AccessibilityNode(accesskit::Node::new(Role::RadioGroup)))] pub struct CoreRadioGroup { /// Callback which is called when the selected radio button changes. - pub on_change: Callback>, + pub on_change: Callback>, } /// Headless widget implementation for radio buttons. These should be enclosed within a @@ -133,7 +132,7 @@ fn radio_group_on_key_input( let (next_id, _) = radio_buttons[next_index]; // Trigger the on_change event for the newly checked radio button - commands.notify_with(on_change, next_id); + commands.notify_with(on_change, Activate(next_id)); } } } @@ -201,7 +200,7 @@ fn radio_group_on_button_click( } // Trigger the on_change event for the newly checked radio button - commands.notify_with(on_change, radio_id); + commands.notify_with(on_change, Activate(radio_id)); } } diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index 07efc7e800..9f38065e37 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -23,7 +23,7 @@ use bevy_math::ops; use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; -use crate::{Callback, Notify}; +use crate::{Callback, Notify, ValueChange}; /// Defines how the slider should behave when you click on the track (not the thumb). #[derive(Debug, Default, PartialEq, Clone, Copy)] @@ -78,7 +78,7 @@ pub struct CoreSlider { /// Callback which is called when the slider is dragged or the value is changed via other user /// interaction. If this value is `Callback::Ignore`, then the slider will update it's own /// internal [`SliderValue`] state without notification. - pub on_change: Callback>, + pub on_change: Callback>>, /// Set the track-clicking behavior for this slider. pub track_click: TrackClick, // TODO: Think about whether we want a "vertical" option. @@ -298,7 +298,13 @@ pub(crate) fn slider_on_pointer_down( .entity(trigger.target()) .insert(SliderValue(new_value)); } else { - commands.notify_with(&slider.on_change, new_value); + commands.notify_with( + &slider.on_change, + ValueChange { + source: trigger.target(), + value: new_value, + }, + ); } } } @@ -370,7 +376,13 @@ pub(crate) fn slider_on_drag( .entity(trigger.target()) .insert(SliderValue(rounded_value)); } else { - commands.notify_with(&slider.on_change, rounded_value); + commands.notify_with( + &slider.on_change, + ValueChange { + source: trigger.target(), + value: rounded_value, + }, + ); } } } @@ -417,7 +429,13 @@ fn slider_on_key_input( .entity(trigger.target()) .insert(SliderValue(new_value)); } else { - commands.notify_with(&slider.on_change, new_value); + commands.notify_with( + &slider.on_change, + ValueChange { + source: trigger.target(), + value: new_value, + }, + ); } } } @@ -509,7 +527,13 @@ fn slider_on_set_value( .entity(trigger.target()) .insert(SliderValue(new_value)); } else { - commands.notify_with(&slider.on_change, new_value); + commands.notify_with( + &slider.on_change, + ValueChange { + source: trigger.target(), + value: new_value, + }, + ); } } } diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs index eb05a18ba6..9a20b59c13 100644 --- a/crates/bevy_core_widgets/src/lib.rs +++ b/crates/bevy_core_widgets/src/lib.rs @@ -23,6 +23,7 @@ mod core_slider; use bevy_app::{PluginGroup, PluginGroupBuilder}; +use bevy_ecs::entity::Entity; pub use callback::{Callback, Notify}; pub use core_button::{CoreButton, CoreButtonPlugin}; pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked}; @@ -50,3 +51,16 @@ impl PluginGroup for CoreWidgetsPlugins { .add(CoreSliderPlugin) } } + +/// Notification sent by a button or menu item. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Activate(pub Entity); + +/// Notification sent by a widget that edits a scalar value. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct ValueChange { + /// The id of the widget that produced this value. + pub source: Entity, + /// The new value. + pub value: T, +} diff --git a/crates/bevy_feathers/src/controls/button.rs b/crates/bevy_feathers/src/controls/button.rs index 5b6ad7117b..ad479f1ec5 100644 --- a/crates/bevy_feathers/src/controls/button.rs +++ b/crates/bevy_feathers/src/controls/button.rs @@ -1,5 +1,5 @@ use bevy_app::{Plugin, PreUpdate}; -use bevy_core_widgets::{Callback, CoreButton}; +use bevy_core_widgets::{Activate, Callback, CoreButton}; use bevy_ecs::{ bundle::Bundle, component::Component, @@ -9,7 +9,7 @@ use bevy_ecs::{ query::{Added, Changed, Has, Or}, schedule::IntoScheduleConfigs, spawn::{SpawnRelated, SpawnableList}, - system::{Commands, Query}, + system::{Commands, In, Query}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::{hover::Hovered, PickingSystems}; @@ -45,7 +45,7 @@ pub struct ButtonProps { /// Rounded corners options pub corners: RoundedCorners, /// Click handler - pub on_click: Callback, + pub on_click: Callback>, } /// Template function to spawn a button. diff --git a/crates/bevy_feathers/src/controls/checkbox.rs b/crates/bevy_feathers/src/controls/checkbox.rs index f81e357c21..db37f82623 100644 --- a/crates/bevy_feathers/src/controls/checkbox.rs +++ b/crates/bevy_feathers/src/controls/checkbox.rs @@ -1,5 +1,5 @@ use bevy_app::{Plugin, PreUpdate}; -use bevy_core_widgets::{Callback, CoreCheckbox}; +use bevy_core_widgets::{Callback, CoreCheckbox, ValueChange}; use bevy_ecs::{ bundle::Bundle, children, @@ -34,7 +34,7 @@ use crate::{ #[derive(Default)] pub struct CheckboxProps { /// Change handler - pub on_change: Callback>, + pub on_change: Callback>>, } /// Marker for the checkbox frame (contains both checkbox and label) diff --git a/crates/bevy_feathers/src/controls/slider.rs b/crates/bevy_feathers/src/controls/slider.rs index fa1978e06c..228801b85c 100644 --- a/crates/bevy_feathers/src/controls/slider.rs +++ b/crates/bevy_feathers/src/controls/slider.rs @@ -2,7 +2,7 @@ use core::f32::consts::PI; use bevy_app::{Plugin, PreUpdate}; use bevy_color::Color; -use bevy_core_widgets::{Callback, CoreSlider, SliderRange, SliderValue, TrackClick}; +use bevy_core_widgets::{Callback, CoreSlider, SliderRange, SliderValue, TrackClick, ValueChange}; use bevy_ecs::{ bundle::Bundle, children, @@ -42,7 +42,7 @@ pub struct SliderProps { /// Slider maximum value pub max: f32, /// On-change handler - pub on_change: Callback>, + pub on_change: Callback>>, } impl Default for SliderProps { diff --git a/crates/bevy_feathers/src/controls/toggle_switch.rs b/crates/bevy_feathers/src/controls/toggle_switch.rs index bc473d8d81..e3437a829d 100644 --- a/crates/bevy_feathers/src/controls/toggle_switch.rs +++ b/crates/bevy_feathers/src/controls/toggle_switch.rs @@ -1,7 +1,7 @@ use accesskit::Role; use bevy_a11y::AccessibilityNode; use bevy_app::{Plugin, PreUpdate}; -use bevy_core_widgets::{Callback, CoreCheckbox}; +use bevy_core_widgets::{Callback, CoreCheckbox, ValueChange}; use bevy_ecs::{ bundle::Bundle, children, @@ -30,7 +30,7 @@ use crate::{ #[derive(Default)] pub struct ToggleSwitchProps { /// Change handler - pub on_change: Callback>, + pub on_change: Callback>>, } /// Marker for the toggle switch outline diff --git a/examples/ui/core_widgets.rs b/examples/ui/core_widgets.rs index 7f99bdd848..5685b88283 100644 --- a/examples/ui/core_widgets.rs +++ b/examples/ui/core_widgets.rs @@ -3,9 +3,9 @@ use bevy::{ color::palettes::basic::*, core_widgets::{ - Callback, CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider, + Activate, Callback, CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider, CoreSliderDragState, CoreSliderThumb, CoreWidgetsPlugins, SliderRange, SliderValue, - TrackClick, + TrackClick, ValueChange, }, input_focus::{ tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin}, @@ -120,24 +120,24 @@ fn update_widget_values( fn setup(mut commands: Commands, assets: Res) { // System to print a value when the button is clicked. - let on_click = commands.register_system(|| { + let on_click = commands.register_system(|_: In| { info!("Button clicked!"); }); // System to update a resource when the slider value changes. Note that we could have // updated the slider value directly, but we want to demonstrate externalizing the state. let on_change_value = commands.register_system( - |value: In, mut widget_states: ResMut| { - widget_states.slider_value = *value; + |value: In>, mut widget_states: ResMut| { + widget_states.slider_value = value.0.value; }, ); // System to update a resource when the radio group changes. let on_change_radio = commands.register_system( - |value: In, + |value: In, mut widget_states: ResMut, q_radios: Query<&DemoRadio>| { - if let Ok(radio) = q_radios.get(*value) { + if let Ok(radio) = q_radios.get(value.0 .0) { widget_states.slider_click = radio.0; } }, @@ -155,9 +155,9 @@ fn setup(mut commands: Commands, assets: Res) { fn demo_root( asset_server: &AssetServer, - on_click: Callback, - on_change_value: Callback>, - on_change_radio: Callback>, + on_click: Callback>, + on_change_value: Callback>>, + on_change_radio: Callback>, ) -> impl Bundle { ( Node { @@ -181,7 +181,7 @@ fn demo_root( ) } -fn button(asset_server: &AssetServer, on_click: Callback) -> impl Bundle { +fn button(asset_server: &AssetServer, on_click: Callback>) -> impl Bundle { ( Node { width: Val::Px(150.0), @@ -324,7 +324,12 @@ fn set_button_style( } /// Create a demo slider -fn slider(min: f32, max: f32, value: f32, on_change: Callback>) -> impl Bundle { +fn slider( + min: f32, + max: f32, + value: f32, + on_change: Callback>>, +) -> impl Bundle { ( Node { display: Display::Flex, @@ -469,7 +474,7 @@ fn thumb_color(disabled: bool, hovered: bool) -> Color { fn checkbox( asset_server: &AssetServer, caption: &str, - on_change: Callback>, + on_change: Callback>>, ) -> impl Bundle { ( Node { @@ -662,7 +667,7 @@ fn set_checkbox_or_radio_style( } /// Create a demo radio group -fn radio_group(asset_server: &AssetServer, on_change: Callback>) -> impl Bundle { +fn radio_group(asset_server: &AssetServer, on_change: Callback>) -> impl Bundle { ( Node { display: Display::Flex, diff --git a/examples/ui/core_widgets_observers.rs b/examples/ui/core_widgets_observers.rs index c12edee08d..4c9d95097c 100644 --- a/examples/ui/core_widgets_observers.rs +++ b/examples/ui/core_widgets_observers.rs @@ -3,8 +3,8 @@ use bevy::{ color::palettes::basic::*, core_widgets::{ - Callback, CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb, CoreWidgetsPlugins, - SliderRange, SliderValue, + Activate, Callback, CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb, + CoreWidgetsPlugins, SliderRange, SliderValue, ValueChange, }, ecs::system::SystemId, input_focus::{ @@ -85,15 +85,15 @@ struct DemoWidgetStates { fn setup(mut commands: Commands, assets: Res) { // System to print a value when the button is clicked. - let on_click = commands.register_system(|| { + let on_click = commands.register_system(|_: In| { info!("Button clicked!"); }); // System to update a resource when the slider value changes. Note that we could have // updated the slider value directly, but we want to demonstrate externalizing the state. let on_change_value = commands.register_system( - |value: In, mut widget_states: ResMut| { - widget_states.slider_value = *value; + |value: In>, mut widget_states: ResMut| { + widget_states.slider_value = value.0.value; }, ); @@ -104,8 +104,8 @@ fn setup(mut commands: Commands, assets: Res) { fn demo_root( asset_server: &AssetServer, - on_click: SystemId, - on_change_value: SystemId>, + on_click: SystemId>, + on_change_value: SystemId>>, ) -> impl Bundle { ( Node { @@ -128,7 +128,7 @@ fn demo_root( ) } -fn button(asset_server: &AssetServer, on_click: Callback) -> impl Bundle { +fn button(asset_server: &AssetServer, on_click: Callback>) -> impl Bundle { ( Node { width: Val::Px(150.0), @@ -351,7 +351,12 @@ fn set_button_style( } /// Create a demo slider -fn slider(min: f32, max: f32, value: f32, on_change: Callback>) -> impl Bundle { +fn slider( + min: f32, + max: f32, + value: f32, + on_change: Callback>>, +) -> impl Bundle { ( Node { display: Display::Flex, @@ -517,7 +522,7 @@ fn thumb_color(disabled: bool, hovered: bool) -> Color { fn checkbox( asset_server: &AssetServer, caption: &str, - on_change: Callback>, + on_change: Callback>>, ) -> impl Bundle { ( Node { diff --git a/examples/ui/feathers.rs b/examples/ui/feathers.rs index 5b580483b6..2e8a68320e 100644 --- a/examples/ui/feathers.rs +++ b/examples/ui/feathers.rs @@ -2,7 +2,8 @@ use bevy::{ core_widgets::{ - Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision, SliderStep, + Activate, Callback, CoreRadio, CoreRadioGroup, CoreWidgetsPlugins, SliderPrecision, + SliderStep, }, feathers::{ controls::{ @@ -49,9 +50,9 @@ fn setup(mut commands: Commands) { fn demo_root(commands: &mut Commands) -> impl Bundle { // Update radio button states based on notification from radio group. let radio_exclusion = commands.register_system( - |ent: In, q_radio: Query>, mut commands: Commands| { + |ent: In, q_radio: Query>, mut commands: Commands| { for radio in q_radio.iter() { - if radio == *ent { + if radio == ent.0 .0 { commands.entity(radio).insert(Checked); } else { commands.entity(radio).remove::(); @@ -98,9 +99,11 @@ fn demo_root(commands: &mut Commands) -> impl Bundle { children![ button( ButtonProps { - on_click: Callback::System(commands.register_system(|| { - info!("Normal button clicked!"); - })), + on_click: Callback::System(commands.register_system( + |_: In| { + info!("Normal button clicked!"); + } + )), ..default() }, (), @@ -108,9 +111,11 @@ fn demo_root(commands: &mut Commands) -> impl Bundle { ), button( ButtonProps { - on_click: Callback::System(commands.register_system(|| { - info!("Disabled button clicked!"); - })), + on_click: Callback::System(commands.register_system( + |_: In| { + info!("Disabled button clicked!"); + } + )), ..default() }, InteractionDisabled, @@ -118,9 +123,11 @@ fn demo_root(commands: &mut Commands) -> impl Bundle { ), button( ButtonProps { - on_click: Callback::System(commands.register_system(|| { - info!("Primary button clicked!"); - })), + on_click: Callback::System(commands.register_system( + |_: In| { + info!("Primary button clicked!"); + } + )), variant: ButtonVariant::Primary, ..default() }, @@ -141,9 +148,11 @@ fn demo_root(commands: &mut Commands) -> impl Bundle { children![ button( ButtonProps { - on_click: Callback::System(commands.register_system(|| { - info!("Left button clicked!"); - })), + on_click: Callback::System(commands.register_system( + |_: In| { + info!("Left button clicked!"); + } + )), corners: RoundedCorners::Left, ..default() }, @@ -152,9 +161,11 @@ fn demo_root(commands: &mut Commands) -> impl Bundle { ), button( ButtonProps { - on_click: Callback::System(commands.register_system(|| { - info!("Center button clicked!"); - })), + on_click: Callback::System(commands.register_system( + |_: In| { + info!("Center button clicked!"); + } + )), corners: RoundedCorners::None, ..default() }, @@ -163,9 +174,11 @@ fn demo_root(commands: &mut Commands) -> impl Bundle { ), button( ButtonProps { - on_click: Callback::System(commands.register_system(|| { - info!("Right button clicked!"); - })), + on_click: Callback::System(commands.register_system( + |_: In| { + info!("Right button clicked!"); + } + )), variant: ButtonVariant::Primary, corners: RoundedCorners::Right, }, @@ -176,7 +189,7 @@ fn demo_root(commands: &mut Commands) -> impl Bundle { ), button( ButtonProps { - on_click: Callback::System(commands.register_system(|| { + on_click: Callback::System(commands.register_system(|_: In| { info!("Wide button clicked!"); })), ..default() diff --git a/release-content/release-notes/headless-widgets.md b/release-content/release-notes/headless-widgets.md index 68f3978fbc..b812385e5b 100644 --- a/release-content/release-notes/headless-widgets.md +++ b/release-content/release-notes/headless-widgets.md @@ -1,7 +1,7 @@ --- title: Headless Widgets authors: ["@viridia", "@ickshonpe", "@alice-i-cecile"] -pull_requests: [19366, 19584, 19665, 19778, 19803, 20032, 20036] +pull_requests: [19366, 19584, 19665, 19778, 19803, 20032, 20036, 20086] --- Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately From 7ae8b5392334f33d66616a063420f25822aa22ed Mon Sep 17 00:00:00 2001 From: Christian Hughes <9044780+ItsDoot@users.noreply.github.com> Date: Sun, 13 Jul 2025 12:25:46 -0500 Subject: [PATCH 16/19] Remove `SystemSetNode` (#20100) # Objective `SystemSetNode` doesn't really add much value beyond a couple helper functions for getting the name as a string and checking if its a `SystemTypeSet`. We can replace it entirely and use `InternedSystemSet` directly by inlining these helper functions' usages without sacrificing readability. ## Solution Remove it and replace it with direct `InternedSystemSet` usage. ## Testing Reusing current tests. --- crates/bevy_ecs/src/schedule/schedule.rs | 40 +++++------------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index a9067b75b8..0544956678 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -659,29 +659,6 @@ impl Dag { } } -/// A [`SystemSet`] with metadata, stored in a [`ScheduleGraph`]. -struct SystemSetNode { - inner: InternedSystemSet, -} - -impl SystemSetNode { - pub fn new(set: InternedSystemSet) -> Self { - Self { inner: set } - } - - pub fn name(&self) -> String { - format!("{:?}", &self.inner) - } - - pub fn is_system_type(&self) -> bool { - self.inner.system_type().is_some() - } - - pub fn is_anonymous(&self) -> bool { - self.inner.is_anonymous() - } -} - /// A [`SystemWithAccess`] stored in a [`ScheduleGraph`]. pub struct SystemNode { inner: Option, @@ -785,7 +762,7 @@ enum UninitializedId { #[derive(Default)] struct SystemSets { /// List of system sets in the schedule - sets: SlotMap, + sets: SlotMap, /// List of conditions for each system set, in the same order as `system_sets` conditions: SecondaryMap>, /// Map from system set to node id @@ -795,7 +772,7 @@ struct SystemSets { impl SystemSets { fn get_or_add_set(&mut self, set: InternedSystemSet) -> SystemSetKey { *self.ids.entry(set).or_insert_with(|| { - let key = self.sets.insert(SystemSetNode::new(set)); + let key = self.sets.insert(set); self.conditions.insert(key, Vec::new()); key }) @@ -875,7 +852,7 @@ impl ScheduleGraph { /// Returns the set at the given [`NodeId`], if it exists. pub fn get_set_at(&self, key: SystemSetKey) -> Option<&dyn SystemSet> { - self.system_sets.sets.get(key).map(|set| &*set.inner) + self.system_sets.sets.get(key).map(|set| &**set) } /// Returns the set at the given [`NodeId`]. @@ -917,10 +894,9 @@ impl ScheduleGraph { pub fn system_sets( &self, ) -> impl Iterator { - self.system_sets.sets.iter().filter_map(|(key, set_node)| { - let set = &*set_node.inner; + self.system_sets.sets.iter().filter_map(|(key, set)| { let conditions = self.system_sets.conditions.get(key)?.as_slice(); - Some((key, set, conditions)) + Some((key, &**set, conditions)) }) } @@ -1704,7 +1680,7 @@ impl ScheduleGraph { if set.is_anonymous() { self.anonymous_set_name(id) } else { - set.name() + format!("{set:?}") } } } @@ -1927,7 +1903,7 @@ impl ScheduleGraph { ) -> Result<(), ScheduleBuildError> { for (&key, systems) in set_systems { let set = &self.system_sets.sets[key]; - if set.is_system_type() { + if set.system_type().is_some() { let instances = systems.len(); let ambiguous_with = self.ambiguous_with.edges(NodeId::Set(key)); let before = self @@ -2033,7 +2009,7 @@ impl ScheduleGraph { fn names_of_sets_containing_node(&self, id: &NodeId) -> Vec { let mut sets = >::default(); self.traverse_sets_containing_node(*id, &mut |key| { - !self.system_sets.sets[key].is_system_type() && sets.insert(key) + self.system_sets.sets[key].system_type().is_none() && sets.insert(key) }); let mut sets: Vec<_> = sets .into_iter() From 84936cad5505cd5e4314673d4a9fb506417f035d Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Sun, 13 Jul 2025 12:33:09 -0700 Subject: [PATCH 17/19] Fix visibility (re)use in Solari DI (#20113) # Objective Fixes the re(use) of visibility in Solari's ReSTIR DI. The paper I based things off of didn't (seem) to use visibility in their resampling https://yusuketokuyoshi.com/papers/2024/Efficient_Visibility_Reuse_for_Real-time_ReSTIR_(Supplementary_Document).pdf, only shading, but factoring it into the resampling improves things a lot. --- ## Showcase Before: image After: image --- crates/bevy_solari/src/realtime/restir_di.wgsl | 7 +++---- crates/bevy_solari/src/realtime/restir_gi.wgsl | 4 ++-- release-content/release-notes/bevy_solari.md | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/bevy_solari/src/realtime/restir_di.wgsl b/crates/bevy_solari/src/realtime/restir_di.wgsl index f56d80fb15..b9a5bfa60c 100644 --- a/crates/bevy_solari/src/realtime/restir_di.wgsl +++ b/crates/bevy_solari/src/realtime/restir_di.wgsl @@ -112,7 +112,6 @@ fn generate_initial_reservoir(world_position: vec3, world_normal: vec3 reservoir.unbiased_contribution_weight = reservoir.weight_sum * inverse_target_function; reservoir.visibility = trace_light_visibility(reservoir.sample, world_position); - reservoir.unbiased_contribution_weight *= reservoir.visibility; } reservoir.confidence_weight = 1.0; @@ -175,8 +174,8 @@ fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3< } fn get_neighbor_pixel_id(center_pixel_id: vec2, rng: ptr) -> vec2 { - var spatial_id = vec2(center_pixel_id) + vec2(sample_disk(SPATIAL_REUSE_RADIUS_PIXELS, rng)); - spatial_id = clamp(spatial_id, vec2(0i), vec2(view.viewport.zw) - 1i); + var spatial_id = vec2(center_pixel_id) + sample_disk(SPATIAL_REUSE_RADIUS_PIXELS, rng); + spatial_id = clamp(spatial_id, vec2(0.0), view.viewport.zw - 1.0); return vec2(spatial_id); } @@ -288,7 +287,7 @@ fn merge_reservoirs( fn reservoir_target_function(reservoir: Reservoir, world_position: vec3, world_normal: vec3, diffuse_brdf: vec3) -> vec4 { if !reservoir_valid(reservoir) { return vec4(0.0); } - let light_contribution = calculate_light_contribution(reservoir.sample, world_position, world_normal).radiance; + let light_contribution = calculate_light_contribution(reservoir.sample, world_position, world_normal).radiance * reservoir.visibility; let target_function = luminance(light_contribution * diffuse_brdf); return vec4(light_contribution, target_function); } diff --git a/crates/bevy_solari/src/realtime/restir_gi.wgsl b/crates/bevy_solari/src/realtime/restir_gi.wgsl index 13bb5d15eb..2b0cff5de7 100644 --- a/crates/bevy_solari/src/realtime/restir_gi.wgsl +++ b/crates/bevy_solari/src/realtime/restir_gi.wgsl @@ -172,8 +172,8 @@ fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3< } fn get_neighbor_pixel_id(center_pixel_id: vec2, rng: ptr) -> vec2 { - var spatial_id = vec2(center_pixel_id) + vec2(sample_disk(SPATIAL_REUSE_RADIUS_PIXELS, rng)); - spatial_id = clamp(spatial_id, vec2(0i), vec2(view.viewport.zw) - 1i); + var spatial_id = vec2(center_pixel_id) + sample_disk(SPATIAL_REUSE_RADIUS_PIXELS, rng); + spatial_id = clamp(spatial_id, vec2(0.0), view.viewport.zw - 1.0); return vec2(spatial_id); } diff --git a/release-content/release-notes/bevy_solari.md b/release-content/release-notes/bevy_solari.md index e6727deb47..66f258eeb1 100644 --- a/release-content/release-notes/bevy_solari.md +++ b/release-content/release-notes/bevy_solari.md @@ -1,7 +1,7 @@ --- title: Initial raytraced lighting progress (bevy_solari) authors: ["@JMS55"] -pull_requests: [19058, 19620, 19790, 20020] +pull_requests: [19058, 19620, 19790, 20020, 20113] --- (TODO: Embed solari example screenshot here) From d83bae4417f6faf110ad9883596bd4b6ea238d91 Mon Sep 17 00:00:00 2001 From: Chamaloriz Date: Sun, 13 Jul 2025 19:44:40 +0000 Subject: [PATCH 18/19] FIX - RelativeCursorPosition Changed<> query filter (#20102) ## Problem This pseudocode was triggering constantly, even with the Changed<> query filter. ```rust pub fn check_mouse_movement_system( relative_cursor_positions: Query< &RelativeCursorPosition, (Changed, With