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:

<img width="127" alt="edgg"
src="https://github.com/user-attachments/assets/7c852030-c0e8-4aef-8d3e-768cb2464cab">

With this PR the edges are sharp and smooth at all scale factors: 

<img width="127" alt="edge"
src="https://github.com/user-attachments/assets/b3231140-1bbc-4a4f-a1d3-dde21f287988">
This commit is contained in:
ickshonpe 2024-11-13 21:41:02 +00:00 committed by GitHub
parent c0fbadbc4c
commit aab36f3951
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 39 additions and 13 deletions

View file

@ -441,7 +441,10 @@ impl Camera {
#[inline] #[inline]
pub fn target_scaling_factor(&self) -> Option<f32> { pub fn target_scaling_factor(&self) -> Option<f32> {
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`]. /// The projection matrix computed using this camera's [`CameraProjection`].

View file

@ -371,6 +371,7 @@ pub fn queue_shadows(
), ),
batch_range: 0..0, batch_range: 0..0,
extra_index: PhaseItemExtraIndex::NONE, extra_index: PhaseItemExtraIndex::NONE,
inverse_scale_factor: 1.,
}); });
} }
} }

View file

@ -528,6 +528,11 @@ const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1;
#[derive(Component)] #[derive(Component)]
pub struct DefaultCameraView(pub Entity); 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. /// Extracts all UI elements associated with a camera into the render world.
pub fn extract_default_ui_camera_view( pub fn extract_default_ui_camera_view(
mut commands: Commands, mut commands: Commands,
@ -555,7 +560,7 @@ pub fn extract_default_ui_camera_view(
commands commands
.get_entity(entity) .get_entity(entity)
.expect("Camera entity wasn't synced.") .expect("Camera entity wasn't synced.")
.remove::<(DefaultCameraView, UiAntiAlias, UiBoxShadowSamples)>(); .remove::<(DefaultCameraView, ExtractedAA, UiBoxShadowSamples)>();
continue; continue;
} }
@ -566,10 +571,12 @@ pub fn extract_default_ui_camera_view(
.. ..
}), }),
Some(physical_size), Some(physical_size),
Some(scale_factor),
) = ( ) = (
camera.logical_viewport_size(), camera.logical_viewport_size(),
camera.physical_viewport_rect(), camera.physical_viewport_rect(),
camera.physical_viewport_size(), 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 // 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( let projection_matrix = Mat4::orthographic_rh(
@ -580,6 +587,7 @@ pub fn extract_default_ui_camera_view(
0.0, 0.0,
UI_CAMERA_FAR, UI_CAMERA_FAR,
); );
let default_camera_view = commands let default_camera_view = commands
.spawn(( .spawn((
ExtractedView { ExtractedView {
@ -606,8 +614,10 @@ pub fn extract_default_ui_camera_view(
.get_entity(entity) .get_entity(entity)
.expect("Camera entity wasn't synced."); .expect("Camera entity wasn't synced.");
entity_commands.insert(DefaultCameraView(default_camera_view)); entity_commands.insert(DefaultCameraView(default_camera_view));
if let Some(ui_anti_alias) = ui_anti_alias { if ui_anti_alias != Some(&UiAntiAlias::Off) {
entity_commands.insert(*ui_anti_alias); entity_commands.insert(ExtractedAA {
scale_factor: (scale_factor * ui_scale.0),
});
} }
if let Some(shadow_samples) = shadow_samples { if let Some(shadow_samples) = shadow_samples {
entity_commands.insert(*shadow_samples); entity_commands.insert(*shadow_samples);
@ -785,6 +795,7 @@ struct UiVertex {
pub size: [f32; 2], pub size: [f32; 2],
/// Position relative to the center of the UI node. /// Position relative to the center of the UI node.
pub point: [f32; 2], pub point: [f32; 2],
pub inverse_scale_factor: f32,
} }
#[derive(Resource)] #[derive(Resource)]
@ -835,13 +846,13 @@ pub fn queue_uinodes(
ui_pipeline: Res<UiPipeline>, ui_pipeline: Res<UiPipeline>,
mut pipelines: ResMut<SpecializedRenderPipelines<UiPipeline>>, mut pipelines: ResMut<SpecializedRenderPipelines<UiPipeline>>,
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>, mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
mut views: Query<(Entity, &ExtractedView, Option<&UiAntiAlias>)>, mut views: Query<(Entity, &ExtractedView, Option<&ExtractedAA>)>,
pipeline_cache: Res<PipelineCache>, pipeline_cache: Res<PipelineCache>,
draw_functions: Res<DrawFunctions<TransparentUi>>, draw_functions: Res<DrawFunctions<TransparentUi>>,
) { ) {
let draw_function = draw_functions.read().id::<DrawUi>(); let draw_function = draw_functions.read().id::<DrawUi>();
for (entity, extracted_uinode) in extracted_uinodes.uinodes.iter() { 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 { else {
continue; continue;
}; };
@ -855,7 +866,7 @@ pub fn queue_uinodes(
&ui_pipeline, &ui_pipeline,
UiPipelineKey { UiPipelineKey {
hdr: view.hdr, hdr: view.hdr,
anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)), anti_alias: extracted_aa.is_some(),
}, },
); );
transparent_phase.add(TransparentUi { transparent_phase.add(TransparentUi {
@ -869,6 +880,7 @@ pub fn queue_uinodes(
// batch_range will be calculated in prepare_uinodes // batch_range will be calculated in prepare_uinodes
batch_range: 0..0, batch_range: 0..0,
extra_index: PhaseItemExtraIndex::NONE, 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], border: [border.left, border.top, border.right, border.bottom],
size: rect_size.xy().into(), size: rect_size.xy().into(),
point: points[i].into(), point: points[i].into(),
inverse_scale_factor: item.inverse_scale_factor,
}); });
} }
@ -1242,6 +1255,7 @@ pub fn prepare_uinodes(
border: [0.0; 4], border: [0.0; 4],
size: size.into(), size: size.into(),
point: [0.0; 2], point: [0.0; 2],
inverse_scale_factor: item.inverse_scale_factor,
}); });
} }

View file

@ -74,6 +74,8 @@ impl SpecializedRenderPipeline for UiPipeline {
VertexFormat::Float32x2, VertexFormat::Float32x2,
// position relative to the center // position relative to the center
VertexFormat::Float32x2, VertexFormat::Float32x2,
// inverse scale factor
VertexFormat::Float32,
], ],
); );
let shader_defs = if key.anti_alias { let shader_defs = if key.anti_alias {

View file

@ -97,6 +97,7 @@ pub struct TransparentUi {
pub draw_function: DrawFunctionId, pub draw_function: DrawFunctionId,
pub batch_range: Range<u32>, pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex, pub extra_index: PhaseItemExtraIndex,
pub inverse_scale_factor: f32,
} }
impl PhaseItem for TransparentUi { impl PhaseItem for TransparentUi {
@ -206,6 +207,7 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetUiTextureBindGroup<I>
RenderCommandResult::Success RenderCommandResult::Success
} }
} }
pub struct DrawUiNode; pub struct DrawUiNode;
impl<P: PhaseItem> RenderCommand<P> for DrawUiNode { impl<P: PhaseItem> RenderCommand<P> for DrawUiNode {
type Param = SRes<UiMeta>; type Param = SRes<UiMeta>;

View file

@ -22,6 +22,7 @@ struct VertexOutput {
// Position relative to the center of the rectangle. // Position relative to the center of the rectangle.
@location(6) point: vec2<f32>, @location(6) point: vec2<f32>,
@location(7) @interpolate(flat) scale_factor: f32,
@builtin(position) position: vec4<f32>, @builtin(position) position: vec4<f32>,
}; };
@ -39,6 +40,7 @@ fn vertex(
@location(5) border: vec4<f32>, @location(5) border: vec4<f32>,
@location(6) size: vec2<f32>, @location(6) size: vec2<f32>,
@location(7) point: vec2<f32>, @location(7) point: vec2<f32>,
@location(8) scale_factor: f32,
) -> VertexOutput { ) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
out.uv = vertex_uv; out.uv = vertex_uv;
@ -49,6 +51,7 @@ fn vertex(
out.size = size; out.size = size;
out.border = border; out.border = border;
out.point = point; out.point = point;
out.scale_factor = scale_factor;
return out; return out;
} }
@ -115,10 +118,9 @@ fn sd_inset_rounded_box(point: vec2<f32>, size: vec2<f32>, radius: vec4<f32>, in
} }
// get alpha for antialiasing for sdf // 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. // 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 - scale_factor * distance));
return clamp(0.0, 1.0, 0.5 - 2.0 * distance);
} }
fn draw(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> { fn draw(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> {
@ -149,7 +151,7 @@ fn draw(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> {
// This select statement ensures we only perform anti-aliasing where a non-zero width border // 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 // is present, otherwise an outline about the external boundary would be drawn even without
// a border. // 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 #else
let t = 1.0 - step(0.0, border_distance); let t = 1.0 - step(0.0, border_distance);
#endif #endif
@ -165,7 +167,7 @@ fn draw_background(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> {
let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border);
#ifdef ANTI_ALIAS #ifdef ANTI_ALIAS
let t = antialias(internal_distance); let t = antialias(internal_distance, in.scale_factor);
#else #else
let t = 1.0 - step(0.0, internal_distance); let t = 1.0 - step(0.0, internal_distance);
#endif #endif

View file

@ -655,6 +655,7 @@ pub fn queue_ui_material_nodes<M: UiMaterial>(
), ),
batch_range: 0..0, batch_range: 0..0,
extra_index: PhaseItemExtraIndex::NONE, extra_index: PhaseItemExtraIndex::NONE,
inverse_scale_factor: 1.,
}); });
} }
} }

View file

@ -372,6 +372,7 @@ pub fn queue_ui_slices(
), ),
batch_range: 0..0, batch_range: 0..0,
extra_index: PhaseItemExtraIndex::NONE, extra_index: PhaseItemExtraIndex::NONE,
inverse_scale_factor: 1.,
}); });
} }
} }

View file

@ -18,7 +18,7 @@ fn main() {
} }
fn setup(mut commands: Commands) { fn setup(mut commands: Commands) {
commands.spawn((Camera2d, UiAntiAlias::Off)); commands.spawn((Camera2d, UiAntiAlias::On));
commands commands
.spawn(( .spawn((