From aab36f39513bc30e148d106f6d2f11f4e85264b5 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 13 Nov 2024 21:41:02 +0000 Subject: [PATCH] UI anti-aliasing fix (#16181) # Objective UI Anti-aliasing is incorrectly implemented. It always uses an edge radius of 0.25 logical pixels, and ignores the physical resolution. For low dpi screens 0.25 is is too low and on higher dpi screens the physical edge radius is much too large, resulting in visual artifacts. ## Solution Multiply the distance by the scale factor in the `antialias` function so that the edge radius stays constant in physical pixels. ## Testing To see the problem really clearly run the button example with `UiScale` set really high. With `UiScale(25.)` on main if you examine the button's border you can see a thick gradient fading away from the edges: edgg With this PR the edges are sharp and smooth at all scale factors: edge --- crates/bevy_render/src/camera/camera.rs | 5 +++- crates/bevy_ui/src/render/box_shadow.rs | 1 + crates/bevy_ui/src/render/mod.rs | 26 ++++++++++++++----- crates/bevy_ui/src/render/pipeline.rs | 2 ++ crates/bevy_ui/src/render/render_pass.rs | 2 ++ crates/bevy_ui/src/render/ui.wgsl | 12 +++++---- .../src/render/ui_material_pipeline.rs | 1 + .../src/render/ui_texture_slice_pipeline.rs | 1 + examples/testbed/ui_layout_rounding.rs | 2 +- 9 files changed, 39 insertions(+), 13 deletions(-) diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 7e7036d1e8..d6d2d869d3 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -441,7 +441,10 @@ impl Camera { #[inline] pub fn target_scaling_factor(&self) -> Option { - self.computed.target_info.as_ref().map(|t| t.scale_factor) + self.computed + .target_info + .as_ref() + .map(|t: &RenderTargetInfo| t.scale_factor) } /// The projection matrix computed using this camera's [`CameraProjection`]. diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index eb7c8169d4..fc19dde8bf 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -371,6 +371,7 @@ 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 27068346e8..b8bdb40d5e 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -528,6 +528,11 @@ 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, @@ -555,7 +560,7 @@ pub fn extract_default_ui_camera_view( commands .get_entity(entity) .expect("Camera entity wasn't synced.") - .remove::<(DefaultCameraView, UiAntiAlias, UiBoxShadowSamples)>(); + .remove::<(DefaultCameraView, ExtractedAA, UiBoxShadowSamples)>(); continue; } @@ -566,10 +571,12 @@ pub fn extract_default_ui_camera_view( .. }), Some(physical_size), + Some(scale_factor), ) = ( camera.logical_viewport_size(), camera.physical_viewport_rect(), camera.physical_viewport_size(), + camera.target_scaling_factor(), ) { // 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( @@ -580,6 +587,7 @@ pub fn extract_default_ui_camera_view( 0.0, UI_CAMERA_FAR, ); + let default_camera_view = commands .spawn(( ExtractedView { @@ -606,8 +614,10 @@ pub fn extract_default_ui_camera_view( .get_entity(entity) .expect("Camera entity wasn't synced."); entity_commands.insert(DefaultCameraView(default_camera_view)); - if let Some(ui_anti_alias) = ui_anti_alias { - entity_commands.insert(*ui_anti_alias); + if ui_anti_alias != Some(&UiAntiAlias::Off) { + entity_commands.insert(ExtractedAA { + scale_factor: (scale_factor * ui_scale.0), + }); } if let Some(shadow_samples) = shadow_samples { entity_commands.insert(*shadow_samples); @@ -785,6 +795,7 @@ 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)] @@ -835,13 +846,13 @@ pub fn queue_uinodes( ui_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, - mut views: Query<(Entity, &ExtractedView, Option<&UiAntiAlias>)>, + mut views: Query<(Entity, &ExtractedView, Option<&ExtractedAA>)>, 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, ui_anti_alias)) = views.get_mut(extracted_uinode.camera_entity) + let Ok((view_entity, view, extracted_aa)) = views.get_mut(extracted_uinode.camera_entity) else { continue; }; @@ -855,7 +866,7 @@ pub fn queue_uinodes( &ui_pipeline, UiPipelineKey { hdr: view.hdr, - anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)), + anti_alias: extracted_aa.is_some(), }, ); transparent_phase.add(TransparentUi { @@ -869,6 +880,7 @@ 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.), }); } } @@ -1139,6 +1151,7 @@ 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, }); } @@ -1242,6 +1255,7 @@ 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 dd465515c5..caa3e804dc 100644 --- a/crates/bevy_ui/src/render/pipeline.rs +++ b/crates/bevy_ui/src/render/pipeline.rs @@ -74,6 +74,8 @@ 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 c673c8229b..cbae1204db 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -97,6 +97,7 @@ pub struct TransparentUi { pub draw_function: DrawFunctionId, pub batch_range: Range, pub extra_index: PhaseItemExtraIndex, + pub inverse_scale_factor: f32, } impl PhaseItem for TransparentUi { @@ -206,6 +207,7 @@ impl RenderCommand

for SetUiTextureBindGroup RenderCommandResult::Success } } + pub struct DrawUiNode; impl RenderCommand

for DrawUiNode { type Param = SRes; diff --git a/crates/bevy_ui/src/render/ui.wgsl b/crates/bevy_ui/src/render/ui.wgsl index d73e7bd929..940bdbfed6 100644 --- a/crates/bevy_ui/src/render/ui.wgsl +++ b/crates/bevy_ui/src/render/ui.wgsl @@ -22,6 +22,7 @@ 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, }; @@ -39,6 +40,7 @@ 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; @@ -49,6 +51,7 @@ fn vertex( out.size = size; out.border = border; out.point = point; + out.scale_factor = scale_factor; return out; } @@ -115,10 +118,9 @@ fn sd_inset_rounded_box(point: vec2, size: vec2, radius: vec4, in } // get alpha for antialiasing for sdf -fn antialias(distance: f32) -> f32 { +fn antialias(distance: f32, scale_factor: f32) -> f32 { // Using the fwidth(distance) was causing artifacts, so just use the distance. - // This antialiases between the distance values of 0.25 and -0.25 - return clamp(0.0, 1.0, 0.5 - 2.0 * distance); + return clamp(0.0, 1.0, (0.5 - scale_factor * distance)); } fn draw(in: VertexOutput, texture_color: vec4) -> vec4 { @@ -149,7 +151,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), external_distance < internal_distance); + let t = select(1.0 - step(0.0, border_distance), antialias(border_distance, in.scale_factor), external_distance < internal_distance); #else let t = 1.0 - step(0.0, border_distance); #endif @@ -165,7 +167,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); + let t = antialias(internal_distance, in.scale_factor); #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 23be50063c..c0bd6a0673 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -655,6 +655,7 @@ 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 67f9b327ae..423a57bd1c 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -372,6 +372,7 @@ pub fn queue_ui_slices( ), batch_range: 0..0, extra_index: PhaseItemExtraIndex::NONE, + inverse_scale_factor: 1., }); } } diff --git a/examples/testbed/ui_layout_rounding.rs b/examples/testbed/ui_layout_rounding.rs index fc59e7c6cb..be653039d0 100644 --- a/examples/testbed/ui_layout_rounding.rs +++ b/examples/testbed/ui_layout_rounding.rs @@ -18,7 +18,7 @@ fn main() { } fn setup(mut commands: Commands) { - commands.spawn((Camera2d, UiAntiAlias::Off)); + commands.spawn((Camera2d, UiAntiAlias::On)); commands .spawn((