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 1/8] 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 2/8] 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 3/8] 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 4/8] `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 5/8] `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 6/8] 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 7/8] 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 8/8] 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.