From 8a3a8b5cfba13df6dda087ce2e69189b2661c5e8 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 22 Nov 2024 00:45:07 +0000 Subject: [PATCH] Only use physical coords internally in `bevy_ui` (#16375) # Objective We switch back and forwards between logical and physical coordinates all over the place. Systems have to query for cameras and the UiScale when they shouldn't need to. It's confusing and fragile and new scale factor bugs get found constantly. ## Solution * Use physical coordinates whereever possible in `bevy_ui`. * Store physical coords in `ComputedNode` and tear out all the unneeded scale factor calculations and queries. * Add an `inverse_scale_factor` field to `ComputedNode` and set nodes changed when their scale factor changes. ## Migration Guide `ComputedNode`'s fields and methods now use physical coordinates. `ComputedNode` has a new field `inverse_scale_factor`. Multiplying the physical coordinates by the `inverse_scale_factor` will give the logical values. --------- Co-authored-by: atlv --- crates/bevy_ui/src/focus.rs | 13 +-- crates/bevy_ui/src/layout/mod.rs | 62 ++++++------ crates/bevy_ui/src/picking_backend.rs | 11 +-- crates/bevy_ui/src/render/box_shadow.rs | 53 +++++----- crates/bevy_ui/src/render/mod.rs | 97 +++++-------------- crates/bevy_ui/src/render/pipeline.rs | 2 - crates/bevy_ui/src/render/render_pass.rs | 1 - crates/bevy_ui/src/render/ui.wgsl | 11 +-- .../src/render/ui_material_pipeline.rs | 1 - .../src/render/ui_texture_slice_pipeline.rs | 5 +- crates/bevy_ui/src/ui_node.rs | 87 ++++++++++++----- crates/bevy_ui/src/update.rs | 3 +- crates/bevy_ui/src/widget/text.rs | 46 +-------- examples/ui/overflow_debug.rs | 8 +- examples/ui/ui_texture_slice_flip_and_tile.rs | 38 ++++---- 15 files changed, 195 insertions(+), 243 deletions(-) diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 763c6e00f0..a2eb37ee31 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,6 +1,5 @@ use crate::{ - CalculatedClip, ComputedNode, DefaultUiCamera, ResolvedBorderRadius, TargetCamera, UiScale, - UiStack, + CalculatedClip, ComputedNode, DefaultUiCamera, ResolvedBorderRadius, TargetCamera, UiStack, }; use bevy_ecs::{ change_detection::DetectChangesMut, @@ -158,7 +157,6 @@ pub fn ui_focus_system( windows: Query<&Window>, mouse_button_input: Res>, touches_input: Res, - ui_scale: Res, ui_stack: Res, mut node_query: Query, ) { @@ -201,19 +199,16 @@ pub fn ui_focus_system( }; let viewport_position = camera - .logical_viewport_rect() - .map(|rect| rect.min) + .physical_viewport_rect() + .map(|rect| rect.min.as_vec2()) .unwrap_or_default(); windows .get(window_ref.entity()) .ok() - .and_then(Window::cursor_position) + .and_then(Window::physical_cursor_position) .or_else(|| touches_input.first_pressed_position()) .map(|cursor_position| (entity, cursor_position - viewport_position)) }) - // The cursor position returned by `Window` only takes into account the window scale factor and not `UiScale`. - // To convert the cursor position to logical UI viewport coordinates we have to divide it by `UiScale`. - .map(|(entity, cursor_position)| (entity, cursor_position / ui_scale.0)) .collect(); // prepare an iterator that contains all the nodes that have the cursor in their rect, diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index aaa3f47050..ca077a9e56 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1,7 +1,7 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, BorderRadius, ComputedNode, ContentSize, DefaultUiCamera, Display, Node, Outline, OverflowAxis, - ScrollPosition, TargetCamera, UiScale, + ScrollPosition, TargetCamera, UiScale, Val, }; use bevy_ecs::{ change_detection::{DetectChanges, DetectChangesMut}, @@ -343,31 +343,33 @@ with UI components as a child of an entity without UI components, your UI layout maybe_scroll_position, )) = node_transform_query.get_mut(entity) { - let Ok((layout, unrounded_unscaled_size)) = ui_surface.get_layout(entity) else { + let Ok((layout, unrounded_size)) = ui_surface.get_layout(entity) else { return; }; - let layout_size = - inverse_target_scale_factor * Vec2::new(layout.size.width, layout.size.height); - let unrounded_size = inverse_target_scale_factor * unrounded_unscaled_size; - let layout_location = - inverse_target_scale_factor * Vec2::new(layout.location.x, layout.location.y); + let layout_size = Vec2::new(layout.size.width, layout.size.height); + + let layout_location = Vec2::new(layout.location.x, layout.location.y); // The position of the center of the node, stored in the node's transform let node_center = layout_location - parent_scroll_position + 0.5 * (layout_size - parent_size); // only trigger change detection when the new values are different - if node.size != layout_size || node.unrounded_size != unrounded_size { + if node.size != layout_size + || node.unrounded_size != unrounded_size + || node.inverse_scale_factor != inverse_target_scale_factor + { node.size = layout_size; node.unrounded_size = unrounded_size; + node.inverse_scale_factor = inverse_target_scale_factor; } let taffy_rect_to_border_rect = |rect: taffy::Rect| BorderRect { - left: rect.left * inverse_target_scale_factor, - right: rect.right * inverse_target_scale_factor, - top: rect.top * inverse_target_scale_factor, - bottom: rect.bottom * inverse_target_scale_factor, + left: rect.left, + right: rect.right, + top: rect.top, + bottom: rect.bottom, }; node.bypass_change_detection().border = taffy_rect_to_border_rect(layout.border); @@ -377,28 +379,35 @@ with UI components as a child of an entity without UI components, your UI layout if let Some(border_radius) = maybe_border_radius { // We don't trigger change detection for changes to border radius - node.bypass_change_detection().border_radius = - border_radius.resolve(node.size, viewport_size); + node.bypass_change_detection().border_radius = border_radius.resolve( + node.size, + viewport_size, + inverse_target_scale_factor.recip(), + ); } if let Some(outline) = maybe_outline { // don't trigger change detection when only outlines are changed let node = node.bypass_change_detection(); node.outline_width = if style.display != Display::None { - outline - .width - .resolve(node.size().x, viewport_size) - .unwrap_or(0.) - .max(0.) + match outline.width { + Val::Px(w) => Val::Px(w / inverse_target_scale_factor), + width => width, + } + .resolve(node.size().x, viewport_size) + .unwrap_or(0.) + .max(0.) } else { 0. }; - node.outline_offset = outline - .offset - .resolve(node.size().x, viewport_size) - .unwrap_or(0.) - .max(0.); + node.outline_offset = match outline.offset { + Val::Px(offset) => Val::Px(offset / inverse_target_scale_factor), + offset => offset, + } + .resolve(node.size().x, viewport_size) + .unwrap_or(0.) + .max(0.); } if transform.translation.truncate() != node_center { @@ -422,8 +431,7 @@ with UI components as a child of an entity without UI components, your UI layout }) .unwrap_or_default(); - let content_size = Vec2::new(layout.content_size.width, layout.content_size.height) - * inverse_target_scale_factor; + let content_size = Vec2::new(layout.content_size.width, layout.content_size.height); let max_possible_offset = (content_size - layout_size).max(Vec2::ZERO); let clamped_scroll_position = scroll_position.clamp(Vec2::ZERO, max_possible_offset); @@ -1131,7 +1139,7 @@ mod tests { .sum(); let parent_width = world.get::(parent).unwrap().size.x; assert!((width_sum - parent_width).abs() < 0.001); - assert!((width_sum - 320.).abs() <= 1.); + assert!((width_sum - 320. * s).abs() <= 1.); s += r; } } diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 93c14e35b4..fb43cd08c4 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -65,7 +65,6 @@ pub fn ui_picking( camera_query: Query<(Entity, &Camera, Has)>, default_ui_camera: DefaultUiCamera, primary_window: Query>, - ui_scale: Res, ui_stack: Res, node_query: Query, mut output: EventWriter, @@ -95,15 +94,15 @@ pub fn ui_picking( let Ok((_, camera_data, _)) = camera_query.get(camera) else { continue; }; - let mut pointer_pos = pointer_location.position; - if let Some(viewport) = camera_data.logical_viewport_rect() { - pointer_pos -= viewport.min; + let mut pointer_pos = + pointer_location.position * camera_data.target_scaling_factor().unwrap_or(1.); + if let Some(viewport) = camera_data.physical_viewport_rect() { + pointer_pos -= viewport.min.as_vec2(); } - let scaled_pointer_pos = pointer_pos / **ui_scale; pointer_pos_by_camera .entry(camera) .or_default() - .insert(pointer_id, scaled_pointer_pos); + .insert(pointer_id, pointer_pos); } } diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index 4be209f1ed..51536ad981 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -4,7 +4,7 @@ use core::{hash::Hash, ops::Range}; use crate::{ BoxShadow, CalculatedClip, ComputedNode, DefaultUiCamera, RenderUiSystem, ResolvedBorderRadius, - TargetCamera, TransparentUi, UiBoxShadowSamples, UiScale, Val, + TargetCamera, TransparentUi, UiBoxShadowSamples, Val, }; use bevy_app::prelude::*; use bevy_asset::*; @@ -237,7 +237,6 @@ pub fn extract_shadows( mut commands: Commands, mut extracted_box_shadows: ResMut, default_ui_camera: Extract, - ui_scale: Extract>, camera_query: Extract>, box_shadow_query: Extract< Query<( @@ -268,37 +267,36 @@ pub fn extract_shadows( continue; } - let ui_logical_viewport_size = camera_query + let ui_physical_viewport_size = camera_query .get(camera_entity) .ok() - .and_then(|(_, c)| c.logical_viewport_size()) - .unwrap_or(Vec2::ZERO) - // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, - // so we have to divide by `UiScale` to get the size of the UI viewport. - / ui_scale.0; + .and_then(|(_, c)| { + c.physical_viewport_size() + .map(|size| Vec2::new(size.x as f32, size.y as f32)) + }) + .unwrap_or(Vec2::ZERO); - let resolve_val = |val, base| match val { + let scale_factor = uinode.inverse_scale_factor.recip(); + + let resolve_val = |val, base, scale_factor| match val { Val::Auto => 0., - Val::Px(px) => px, + Val::Px(px) => px * scale_factor, Val::Percent(percent) => percent / 100. * base, - Val::Vw(percent) => percent / 100. * ui_logical_viewport_size.x, - Val::Vh(percent) => percent / 100. * ui_logical_viewport_size.y, - Val::VMin(percent) => percent / 100. * ui_logical_viewport_size.min_element(), - Val::VMax(percent) => percent / 100. * ui_logical_viewport_size.max_element(), + Val::Vw(percent) => percent / 100. * ui_physical_viewport_size.x, + Val::Vh(percent) => percent / 100. * ui_physical_viewport_size.y, + Val::VMin(percent) => percent / 100. * ui_physical_viewport_size.min_element(), + Val::VMax(percent) => percent / 100. * ui_physical_viewport_size.max_element(), }; - let spread_x = resolve_val(box_shadow.spread_radius, uinode.size().x); - let spread_ratio_x = (spread_x + uinode.size().x) / uinode.size().x; + let spread_x = resolve_val(box_shadow.spread_radius, uinode.size().x, scale_factor); + let spread_ratio = (spread_x + uinode.size().x) / uinode.size().x; - let spread = vec2( - spread_x, - (spread_ratio_x * uinode.size().y) - uinode.size().y, - ); + let spread = vec2(spread_x, uinode.size().y * spread_ratio - uinode.size().y); - let blur_radius = resolve_val(box_shadow.blur_radius, uinode.size().x); + let blur_radius = resolve_val(box_shadow.blur_radius, uinode.size().x, scale_factor); let offset = vec2( - resolve_val(box_shadow.x_offset, uinode.size().x), - resolve_val(box_shadow.y_offset, uinode.size().y), + resolve_val(box_shadow.x_offset, uinode.size().x, scale_factor), + resolve_val(box_shadow.y_offset, uinode.size().y, scale_factor), ); let shadow_size = uinode.size() + spread; @@ -307,10 +305,10 @@ pub fn extract_shadows( } let radius = ResolvedBorderRadius { - top_left: uinode.border_radius.top_left * spread_ratio_x, - top_right: uinode.border_radius.top_right * spread_ratio_x, - bottom_left: uinode.border_radius.bottom_left * spread_ratio_x, - bottom_right: uinode.border_radius.bottom_right * spread_ratio_x, + top_left: uinode.border_radius.top_left * spread_ratio, + top_right: uinode.border_radius.top_right * spread_ratio, + bottom_left: uinode.border_radius.bottom_left * spread_ratio, + bottom_right: uinode.border_radius.bottom_right * spread_ratio, }; extracted_box_shadows.box_shadows.insert( @@ -373,7 +371,6 @@ pub fn queue_shadows( ), batch_range: 0..0, extra_index: PhaseItemExtraIndex::NONE, - inverse_scale_factor: 1., }); } } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index b8bdb40d5e..8badc68771 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -8,7 +8,6 @@ use crate::widget::ImageNode; use crate::{ experimental::UiChildren, BackgroundColor, BorderColor, CalculatedClip, ComputedNode, DefaultUiCamera, Outline, ResolvedBorderRadius, TargetCamera, UiAntiAlias, UiBoxShadowSamples, - UiScale, }; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle}; @@ -19,7 +18,7 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_ecs::entity::{EntityHashMap, EntityHashSet}; use bevy_ecs::prelude::*; use bevy_image::Image; -use bevy_math::{FloatOrd, Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles}; +use bevy_math::{FloatOrd, Mat4, Rect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles}; use bevy_render::render_phase::ViewSortedRenderPhases; use bevy_render::sync_world::MainEntity; use bevy_render::texture::TRANSPARENT_IMAGE_HANDLE; @@ -528,16 +527,10 @@ const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1; #[derive(Component)] pub struct DefaultCameraView(pub Entity); -#[derive(Component)] -pub struct ExtractedAA { - pub scale_factor: f32, -} - /// Extracts all UI elements associated with a camera into the render world. pub fn extract_default_ui_camera_view( mut commands: Commands, mut transparent_render_phases: ResMut>, - ui_scale: Extract>, query: Extract< Query< ( @@ -553,41 +546,26 @@ pub fn extract_default_ui_camera_view( ) { live_entities.clear(); - let scale = ui_scale.0.recip(); for (entity, camera, ui_anti_alias, shadow_samples) in &query { // ignore inactive cameras if !camera.is_active { commands .get_entity(entity) .expect("Camera entity wasn't synced.") - .remove::<(DefaultCameraView, ExtractedAA, UiBoxShadowSamples)>(); + .remove::<(DefaultCameraView, UiAntiAlias, UiBoxShadowSamples)>(); continue; } - if let ( - Some(logical_size), - Some(URect { - min: physical_origin, - .. - }), - Some(physical_size), - Some(scale_factor), - ) = ( - camera.logical_viewport_size(), - camera.physical_viewport_rect(), - camera.physical_viewport_size(), - camera.target_scaling_factor(), - ) { + if let Some(physical_viewport_rect) = camera.physical_viewport_rect() { // use a projection matrix with the origin in the top left instead of the bottom left that comes with OrthographicProjection let projection_matrix = Mat4::orthographic_rh( 0.0, - logical_size.x * scale, - logical_size.y * scale, + physical_viewport_rect.width() as f32, + physical_viewport_rect.height() as f32, 0.0, 0.0, UI_CAMERA_FAR, ); - let default_camera_view = commands .spawn(( ExtractedView { @@ -599,12 +577,10 @@ pub fn extract_default_ui_camera_view( ), clip_from_world: None, hdr: camera.hdr, - viewport: UVec4::new( - physical_origin.x, - physical_origin.y, - physical_size.x, - physical_size.y, - ), + viewport: UVec4::from(( + physical_viewport_rect.min, + physical_viewport_rect.size(), + )), color_grading: Default::default(), }, TemporaryRenderEntity, @@ -614,10 +590,8 @@ pub fn extract_default_ui_camera_view( .get_entity(entity) .expect("Camera entity wasn't synced."); entity_commands.insert(DefaultCameraView(default_camera_view)); - if ui_anti_alias != Some(&UiAntiAlias::Off) { - entity_commands.insert(ExtractedAA { - scale_factor: (scale_factor * ui_scale.0), - }); + if let Some(ui_anti_alias) = ui_anti_alias { + entity_commands.insert(*ui_anti_alias); } if let Some(shadow_samples) = shadow_samples { entity_commands.insert(*shadow_samples); @@ -635,10 +609,8 @@ pub fn extract_default_ui_camera_view( pub fn extract_text_sections( mut commands: Commands, mut extracted_uinodes: ResMut, - camera_query: Extract>, default_ui_camera: Extract, texture_atlases: Extract>>, - ui_scale: Extract>, uinode_query: Extract< Query<( Entity, @@ -678,32 +650,18 @@ pub fn extract_text_sections( continue; } - let scale_factor = camera_query - .get(camera_entity) - .ok() - .and_then(Camera::target_scaling_factor) - .unwrap_or(1.0) - * ui_scale.0; - let inverse_scale_factor = scale_factor.recip(); - let Ok(&render_camera_entity) = mapping.get(camera_entity) else { continue; }; - // Align the text to the nearest physical pixel: + + // Align the text to the nearest pixel: // * Translate by minus the text node's half-size // (The transform translates to the center of the node but the text coordinates are relative to the node's top left corner) - // * Multiply the logical coordinates by the scale factor to get its position in physical coordinates - // * Round the physical position to the nearest physical pixel - // * Multiply by the rounded physical position by the inverse scale factor to return to logical coordinates - - let logical_top_left = -0.5 * uinode.size(); + // * Round the position to the nearest physical pixel let mut transform = global_transform.affine() - * bevy_math::Affine3A::from_translation(logical_top_left.extend(0.)); - - transform.translation *= scale_factor; + * bevy_math::Affine3A::from_translation((-0.5 * uinode.size()).extend(0.)); transform.translation = transform.translation.round(); - transform.translation *= inverse_scale_factor; let mut color = LinearRgba::WHITE; let mut current_span = usize::MAX; @@ -730,15 +688,14 @@ pub fn extract_text_sections( .unwrap_or_default(); current_span = *span_index; } - let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - - let mut rect = atlas.textures[atlas_info.location.glyph_index].as_rect(); - rect.min *= inverse_scale_factor; - rect.max *= inverse_scale_factor; + let rect = texture_atlases + .get(&atlas_info.texture_atlas) + .unwrap() + .textures[atlas_info.location.glyph_index] + .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform - * Mat4::from_translation(position.extend(0.) * inverse_scale_factor), + transform: transform * Mat4::from_translation(position.extend(0.)), rect, }); @@ -762,7 +719,7 @@ pub fn extract_text_sections( camera_entity: render_camera_entity.id(), rect, item: ExtractedUiItem::Glyphs { - atlas_scaling: Vec2::splat(inverse_scale_factor), + atlas_scaling: Vec2::ONE, range: start..end, }, main_entity: entity.into(), @@ -795,7 +752,6 @@ struct UiVertex { pub size: [f32; 2], /// Position relative to the center of the UI node. pub point: [f32; 2], - pub inverse_scale_factor: f32, } #[derive(Resource)] @@ -846,13 +802,13 @@ pub fn queue_uinodes( ui_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, - mut views: Query<(Entity, &ExtractedView, Option<&ExtractedAA>)>, + mut views: Query<(Entity, &ExtractedView, Option<&UiAntiAlias>)>, pipeline_cache: Res, draw_functions: Res>, ) { let draw_function = draw_functions.read().id::(); for (entity, extracted_uinode) in extracted_uinodes.uinodes.iter() { - let Ok((view_entity, view, extracted_aa)) = views.get_mut(extracted_uinode.camera_entity) + let Ok((view_entity, view, ui_anti_alias)) = views.get_mut(extracted_uinode.camera_entity) else { continue; }; @@ -866,7 +822,7 @@ pub fn queue_uinodes( &ui_pipeline, UiPipelineKey { hdr: view.hdr, - anti_alias: extracted_aa.is_some(), + anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)), }, ); transparent_phase.add(TransparentUi { @@ -880,7 +836,6 @@ pub fn queue_uinodes( // batch_range will be calculated in prepare_uinodes batch_range: 0..0, extra_index: PhaseItemExtraIndex::NONE, - inverse_scale_factor: extracted_aa.map(|aa| aa.scale_factor).unwrap_or(1.), }); } } @@ -1151,7 +1106,6 @@ pub fn prepare_uinodes( border: [border.left, border.top, border.right, border.bottom], size: rect_size.xy().into(), point: points[i].into(), - inverse_scale_factor: item.inverse_scale_factor, }); } @@ -1255,7 +1209,6 @@ pub fn prepare_uinodes( border: [0.0; 4], size: size.into(), point: [0.0; 2], - inverse_scale_factor: item.inverse_scale_factor, }); } diff --git a/crates/bevy_ui/src/render/pipeline.rs b/crates/bevy_ui/src/render/pipeline.rs index caa3e804dc..dd465515c5 100644 --- a/crates/bevy_ui/src/render/pipeline.rs +++ b/crates/bevy_ui/src/render/pipeline.rs @@ -74,8 +74,6 @@ impl SpecializedRenderPipeline for UiPipeline { VertexFormat::Float32x2, // position relative to the center VertexFormat::Float32x2, - // inverse scale factor - VertexFormat::Float32, ], ); let shader_defs = if key.anti_alias { diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index cbae1204db..29b2328c2f 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -97,7 +97,6 @@ pub struct TransparentUi { pub draw_function: DrawFunctionId, pub batch_range: Range, pub extra_index: PhaseItemExtraIndex, - pub inverse_scale_factor: f32, } impl PhaseItem for TransparentUi { diff --git a/crates/bevy_ui/src/render/ui.wgsl b/crates/bevy_ui/src/render/ui.wgsl index 940bdbfed6..342fd4d380 100644 --- a/crates/bevy_ui/src/render/ui.wgsl +++ b/crates/bevy_ui/src/render/ui.wgsl @@ -22,7 +22,6 @@ struct VertexOutput { // Position relative to the center of the rectangle. @location(6) point: vec2, - @location(7) @interpolate(flat) scale_factor: f32, @builtin(position) position: vec4, }; @@ -40,7 +39,6 @@ fn vertex( @location(5) border: vec4, @location(6) size: vec2, @location(7) point: vec2, - @location(8) scale_factor: f32, ) -> VertexOutput { var out: VertexOutput; out.uv = vertex_uv; @@ -51,7 +49,6 @@ fn vertex( out.size = size; out.border = border; out.point = point; - out.scale_factor = scale_factor; return out; } @@ -118,9 +115,9 @@ fn sd_inset_rounded_box(point: vec2, size: vec2, radius: vec4, in } // get alpha for antialiasing for sdf -fn antialias(distance: f32, scale_factor: f32) -> f32 { +fn antialias(distance: f32) -> f32 { // Using the fwidth(distance) was causing artifacts, so just use the distance. - return clamp(0.0, 1.0, (0.5 - scale_factor * distance)); + return clamp(0.0, 1.0, (0.5 - distance)); } fn draw(in: VertexOutput, texture_color: vec4) -> vec4 { @@ -151,7 +148,7 @@ fn draw(in: VertexOutput, texture_color: vec4) -> vec4 { // This select statement ensures we only perform anti-aliasing where a non-zero width border // is present, otherwise an outline about the external boundary would be drawn even without // a border. - let t = select(1.0 - step(0.0, border_distance), antialias(border_distance, in.scale_factor), external_distance < internal_distance); + let t = select(1.0 - step(0.0, border_distance), antialias(border_distance), external_distance < internal_distance); #else let t = 1.0 - step(0.0, border_distance); #endif @@ -167,7 +164,7 @@ fn draw_background(in: VertexOutput, texture_color: vec4) -> vec4 { let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); #ifdef ANTI_ALIAS - let t = antialias(internal_distance, in.scale_factor); + let t = antialias(internal_distance); #else let t = 1.0 - step(0.0, internal_distance); #endif diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index c0bd6a0673..23be50063c 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -655,7 +655,6 @@ pub fn queue_ui_material_nodes( ), batch_range: 0..0, extra_index: PhaseItemExtraIndex::NONE, - inverse_scale_factor: 1., }); } } diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 423a57bd1c..dc19fd830b 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -237,6 +237,7 @@ pub struct ExtractedUiTextureSlice { pub image_scale_mode: SpriteImageMode, pub flip_x: bool, pub flip_y: bool, + pub inverse_scale_factor: f32, pub main_entity: MainEntity, } @@ -331,6 +332,7 @@ pub fn extract_ui_texture_slices( atlas_rect, flip_x: image.flip_x, flip_y: image.flip_y, + inverse_scale_factor: uinode.inverse_scale_factor, main_entity: entity.into(), }, ); @@ -372,7 +374,6 @@ pub fn queue_ui_slices( ), batch_range: 0..0, extra_index: PhaseItemExtraIndex::NONE, - inverse_scale_factor: 1., }); } } @@ -609,7 +610,7 @@ pub fn prepare_ui_slices( let [slices, border, repeat] = compute_texture_slices( image_size, - uinode_rect.size(), + uinode_rect.size() * texture_slices.inverse_scale_factor, &texture_slices.image_scale_mode, ); diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 05fb3ca4d3..4a867b90ff 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -22,7 +22,7 @@ pub struct ComputedNode { /// The order of the node in the UI layout. /// Nodes with a higher stack index are drawn on top of and receive interactions before nodes with lower stack indices. pub(crate) stack_index: u32, - /// The size of the node as width and height in logical pixels + /// The size of the node as width and height in physical pixels /// /// automatically calculated by [`super::layout::ui_layout_system`] pub(crate) size: Vec2, @@ -37,29 +37,34 @@ pub struct ComputedNode { /// /// Automatically calculated by [`super::layout::ui_layout_system`]. pub(crate) outline_offset: f32, - /// The unrounded size of the node as width and height in logical pixels. + /// The unrounded size of the node as width and height in physical pixels. /// /// Automatically calculated by [`super::layout::ui_layout_system`]. pub(crate) unrounded_size: Vec2, - /// Resolved border values in logical pixels + /// Resolved border values in physical pixels /// Border updates bypass change detection. /// /// Automatically calculated by [`super::layout::ui_layout_system`]. pub(crate) border: BorderRect, - /// Resolved border radius values in logical pixels. + /// Resolved border radius values in physical pixels. /// Border radius updates bypass change detection. /// /// Automatically calculated by [`super::layout::ui_layout_system`]. pub(crate) border_radius: ResolvedBorderRadius, - /// Resolved padding values in logical pixels + /// Resolved padding values in physical pixels /// Padding updates bypass change detection. /// /// Automatically calculated by [`super::layout::ui_layout_system`]. pub(crate) padding: BorderRect, + /// Inverse scale factor for this Node. + /// Multiply physical coordinates by the inverse scale factor to give logical coordinates. + /// + /// Automatically calculated by [`super::layout::ui_layout_system`]. + pub(crate) inverse_scale_factor: f32, } impl ComputedNode { - /// The calculated node size as width and height in logical pixels. + /// The calculated node size as width and height in physical pixels. /// /// Automatically calculated by [`super::layout::ui_layout_system`]. #[inline] @@ -82,7 +87,7 @@ impl ComputedNode { self.stack_index } - /// The calculated node size as width and height in logical pixels before rounding. + /// The calculated node size as width and height in physical pixels before rounding. /// /// Automatically calculated by [`super::layout::ui_layout_system`]. #[inline] @@ -90,7 +95,7 @@ impl ComputedNode { self.unrounded_size } - /// Returns the thickness of the UI node's outline in logical pixels. + /// Returns the thickness of the UI node's outline in physical pixels. /// If this value is negative or `0.` then no outline will be rendered. /// /// Automatically calculated by [`super::layout::ui_layout_system`]. @@ -99,7 +104,7 @@ impl ComputedNode { self.outline_width } - /// Returns the amount of space between the outline and the edge of the node in logical pixels. + /// Returns the amount of space between the outline and the edge of the node in physical pixels. /// /// Automatically calculated by [`super::layout::ui_layout_system`]. #[inline] @@ -139,7 +144,7 @@ impl ComputedNode { } } - /// Returns the thickness of the node's border on each edge in logical pixels. + /// Returns the thickness of the node's border on each edge in physical pixels. /// /// Automatically calculated by [`super::layout::ui_layout_system`]. #[inline] @@ -147,7 +152,7 @@ impl ComputedNode { self.border } - /// Returns the border radius for each of the node's corners in logical pixels. + /// Returns the border radius for each of the node's corners in physical pixels. /// /// Automatically calculated by [`super::layout::ui_layout_system`]. #[inline] @@ -155,7 +160,7 @@ impl ComputedNode { self.border_radius } - /// Returns the inner border radius for each of the node's corners in logical pixels. + /// Returns the inner border radius for each of the node's corners in physical pixels. pub fn inner_radius(&self) -> ResolvedBorderRadius { fn clamp_corner(r: f32, size: Vec2, offset: Vec2) -> f32 { let s = 0.5 * size + offset; @@ -177,7 +182,7 @@ impl ComputedNode { } } - /// Returns the thickness of the node's padding on each edge in logical pixels. + /// Returns the thickness of the node's padding on each edge in physical pixels. /// /// Automatically calculated by [`super::layout::ui_layout_system`]. #[inline] @@ -185,7 +190,7 @@ impl ComputedNode { self.padding } - /// Returns the combined inset on each edge including both padding and border thickness in logical pixels. + /// Returns the combined inset on each edge including both padding and border thickness in physical pixels. #[inline] pub const fn content_inset(&self) -> BorderRect { BorderRect { @@ -195,6 +200,13 @@ impl ComputedNode { bottom: self.border.bottom + self.padding.bottom, } } + + /// Returns the inverse of the scale factor for this node. + /// To convert from physical coordinates to logical coordinates multiply by this value. + #[inline] + pub const fn inverse_scale_factor(&self) -> f32 { + self.inverse_scale_factor + } } impl ComputedNode { @@ -207,6 +219,7 @@ impl ComputedNode { border_radius: ResolvedBorderRadius::ZERO, border: BorderRect::ZERO, padding: BorderRect::ZERO, + inverse_scale_factor: 1., }; } @@ -2330,10 +2343,15 @@ impl BorderRadius { } /// Compute the logical border radius for a single corner from the given values - pub fn resolve_single_corner(radius: Val, node_size: Vec2, viewport_size: Vec2) -> f32 { + pub fn resolve_single_corner( + radius: Val, + node_size: Vec2, + viewport_size: Vec2, + scale_factor: f32, + ) -> f32 { match radius { Val::Auto => 0., - Val::Px(px) => px, + Val::Px(px) => px * scale_factor, Val::Percent(percent) => node_size.min_element() * percent / 100., Val::Vw(percent) => viewport_size.x * percent / 100., Val::Vh(percent) => viewport_size.y * percent / 100., @@ -2343,19 +2361,44 @@ impl BorderRadius { .clamp(0., 0.5 * node_size.min_element()) } - pub fn resolve(&self, node_size: Vec2, viewport_size: Vec2) -> ResolvedBorderRadius { + pub fn resolve( + &self, + node_size: Vec2, + viewport_size: Vec2, + scale_factor: f32, + ) -> ResolvedBorderRadius { ResolvedBorderRadius { - top_left: Self::resolve_single_corner(self.top_left, node_size, viewport_size), - top_right: Self::resolve_single_corner(self.top_right, node_size, viewport_size), - bottom_left: Self::resolve_single_corner(self.bottom_left, node_size, viewport_size), - bottom_right: Self::resolve_single_corner(self.bottom_right, node_size, viewport_size), + top_left: Self::resolve_single_corner( + self.top_left, + node_size, + viewport_size, + scale_factor, + ), + top_right: Self::resolve_single_corner( + self.top_right, + node_size, + viewport_size, + scale_factor, + ), + bottom_left: Self::resolve_single_corner( + self.bottom_left, + node_size, + viewport_size, + scale_factor, + ), + bottom_right: Self::resolve_single_corner( + self.bottom_right, + node_size, + viewport_size, + scale_factor, + ), } } } /// Represents the resolved border radius values for a UI node. /// -/// The values are in logical pixels. +/// The values are in physical pixels. #[derive(Copy, Clone, Debug, Default, PartialEq, Reflect)] pub struct ResolvedBorderRadius { pub top_left: f32, diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 428f0a6c30..049c797ea1 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -115,7 +115,8 @@ fn update_clipping( clip_rect.max.x -= clip_inset.right; clip_rect.max.y -= clip_inset.bottom; - clip_rect = clip_rect.inflate(node.overflow_clip_margin.margin.max(0.)); + clip_rect = clip_rect + .inflate(node.overflow_clip_margin.margin.max(0.) / computed_node.inverse_scale_factor); if node.overflow.x == OverflowAxis::Visible { clip_rect.min.x = -f32::INFINITY; diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 1007a4cc78..9d13270aaa 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -350,10 +350,7 @@ fn queue_text( TextBounds::UNBOUNDED } else { // `scale_factor` is already multiplied by `UiScale` - TextBounds::new( - node.unrounded_size.x * scale_factor, - node.unrounded_size.y * scale_factor, - ) + TextBounds::new(node.unrounded_size.x, node.unrounded_size.y) }; let text_layout_info = text_layout_info.into_inner(); @@ -398,12 +395,7 @@ fn queue_text( #[allow(clippy::too_many_arguments)] pub fn text_system( mut textures: ResMut>, - mut scale_factors_buffer: Local>, - mut last_scale_factors: Local>, fonts: Res>, - camera_query: Query<(Entity, &Camera)>, - default_ui_camera: DefaultUiCamera, - ui_scale: Res, mut texture_atlases: ResMut>, mut font_atlas_sets: ResMut, mut text_pipeline: ResMut, @@ -414,40 +406,13 @@ pub fn text_system( &mut TextLayoutInfo, &mut TextNodeFlags, &mut ComputedTextBlock, - Option<&TargetCamera>, )>, mut text_reader: TextUiReader, mut font_system: ResMut, mut swash_cache: ResMut, ) { - scale_factors_buffer.clear(); - - for (entity, node, block, text_layout_info, text_flags, mut computed, maybe_camera) in - &mut text_query - { - let Some(camera_entity) = maybe_camera - .map(TargetCamera::entity) - .or(default_ui_camera.get()) - else { - continue; - }; - let scale_factor = match scale_factors_buffer.entry(camera_entity) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => *entry.insert( - camera_query - .get(camera_entity) - .ok() - .and_then(|(_, c)| c.target_scaling_factor()) - .unwrap_or(1.0) - * ui_scale.0, - ), - }; - let inverse_scale_factor = scale_factor.recip(); - - if last_scale_factors.get(&camera_entity) != Some(&scale_factor) - || node.is_changed() - || text_flags.needs_recompute - { + for (entity, node, block, text_layout_info, text_flags, mut computed) in &mut text_query { + if node.is_changed() || text_flags.needs_recompute { queue_text( entity, &fonts, @@ -455,8 +420,8 @@ pub fn text_system( &mut font_atlas_sets, &mut texture_atlases, &mut textures, - scale_factor, - inverse_scale_factor, + node.inverse_scale_factor.recip(), + node.inverse_scale_factor, block, node, text_flags, @@ -468,5 +433,4 @@ pub fn text_system( ); } } - core::mem::swap(&mut *last_scale_factors, &mut *scale_factors_buffer); } diff --git a/examples/ui/overflow_debug.rs b/examples/ui/overflow_debug.rs index 13f2c362a2..cfa1fb4348 100644 --- a/examples/ui/overflow_debug.rs +++ b/examples/ui/overflow_debug.rs @@ -233,13 +233,13 @@ fn update_animation( fn update_transform( animation: Res, - mut containers: Query<(&mut Transform, &mut Node, &T)>, + mut containers: Query<(&mut Transform, &mut Node, &ComputedNode, &T)>, ) { - for (mut transform, mut node, update_transform) in &mut containers { + for (mut transform, mut node, computed_node, update_transform) in &mut containers { update_transform.update(animation.t, &mut transform); - node.left = Val::Px(transform.translation.x); - node.top = Val::Px(transform.translation.y); + node.left = Val::Px(transform.translation.x * computed_node.inverse_scale_factor()); + node.top = Val::Px(transform.translation.y * computed_node.inverse_scale_factor()); } } diff --git a/examples/ui/ui_texture_slice_flip_and_tile.rs b/examples/ui/ui_texture_slice_flip_and_tile.rs index ccf9e7d174..7b8153965a 100644 --- a/examples/ui/ui_texture_slice_flip_and_tile.rs +++ b/examples/ui/ui_texture_slice_flip_and_tile.rs @@ -51,26 +51,24 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }) .with_children(|parent| { - for ([width, height], flip_x, flip_y) in [ - ([160., 160.], false, false), - ([320., 160.], false, true), - ([320., 160.], true, false), - ([160., 160.], true, true), - ] { - parent.spawn(( - ImageNode { - image: image.clone(), - flip_x, - flip_y, - image_mode: NodeImageMode::Sliced(slicer.clone()), - ..default() - }, - Node { - width: Val::Px(width), - height: Val::Px(height), - ..default() - }, - )); + for [columns, rows] in [[3., 3.], [4., 4.], [5., 4.], [4., 5.], [5., 5.]] { + for (flip_x, flip_y) in [(false, false), (false, true), (true, false), (true, true)] + { + parent.spawn(( + ImageNode { + image: image.clone(), + flip_x, + flip_y, + image_mode: NodeImageMode::Sliced(slicer.clone()), + ..default() + }, + Node { + width: Val::Px(16. * columns), + height: Val::Px(16. * rows), + ..default() + }, + )); + } } }); }