UI anti-aliasing fix (#16181)

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.

Multiply the distance by the scale factor in the `antialias` function so
that the edge radius stays constant in physical pixels.

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 François Mockers
parent 572f0c1a13
commit 988770ad99
8 changed files with 38 additions and 12 deletions

View file

@ -441,7 +441,10 @@ impl Camera {
#[inline]
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`].

View file

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

View file

@ -525,6 +525,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,
@ -552,7 +557,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;
}
@ -563,10 +568,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(
@ -577,6 +584,7 @@ pub fn extract_default_ui_camera_view(
0.0,
UI_CAMERA_FAR,
);
let default_camera_view = commands
.spawn((
ExtractedView {
@ -603,8 +611,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);
@ -782,6 +792,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)]
@ -832,13 +843,13 @@ pub fn queue_uinodes(
ui_pipeline: Res<UiPipeline>,
mut pipelines: ResMut<SpecializedRenderPipelines<UiPipeline>>,
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>,
draw_functions: Res<DrawFunctions<TransparentUi>>,
) {
let draw_function = draw_functions.read().id::<DrawUi>();
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;
};
@ -852,7 +863,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 {
@ -866,6 +877,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.),
});
}
}
@ -1136,6 +1148,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,
});
}
@ -1239,6 +1252,7 @@ pub fn prepare_uinodes(
border: [0.0; 4],
size: size.into(),
point: [0.0; 2],
inverse_scale_factor: item.inverse_scale_factor,
});
}

View file

@ -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 {

View file

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

View file

@ -22,6 +22,7 @@ struct VertexOutput {
// Position relative to the center of the rectangle.
@location(6) point: vec2<f32>,
@location(7) @interpolate(flat) scale_factor: f32,
@builtin(position) position: vec4<f32>,
};
@ -39,6 +40,7 @@ fn vertex(
@location(5) border: vec4<f32>,
@location(6) size: vec2<f32>,
@location(7) point: vec2<f32>,
@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<f32>, size: vec2<f32>, radius: vec4<f32>, 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<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
// 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<f32>) -> vec4<f32> {
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

View file

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

View file

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