Skip to content

Commit

Permalink
UI anti-aliasing fix (#16181)
Browse files Browse the repository at this point in the history
# 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">
  • Loading branch information
ickshonpe authored Nov 13, 2024
1 parent c0fbadb commit aab36f3
Show file tree
Hide file tree
Showing 9 changed files with 39 additions and 13 deletions.
5 changes: 4 additions & 1 deletion crates/bevy_render/src/camera/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ui/src/render/box_shadow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ pub fn queue_shadows(
),
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::NONE,
inverse_scale_factor: 1.,
});
}
}
Expand Down
26 changes: 20 additions & 6 deletions crates/bevy_ui/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}

Expand All @@ -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(
Expand All @@ -580,6 +587,7 @@ pub fn extract_default_ui_camera_view(
0.0,
UI_CAMERA_FAR,
);

let default_camera_view = commands
.spawn((
ExtractedView {
Expand All @@ -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);
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -835,13 +846,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;
};
Expand All @@ -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 {
Expand All @@ -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.),
});
}
}
Expand Down Expand Up @@ -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,
});
}

Expand Down Expand Up @@ -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,
});
}

Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_ui/src/render/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_ui/src/render/render_pass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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>;
Expand Down
12 changes: 7 additions & 5 deletions crates/bevy_ui/src/render/ui.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
};

Expand All @@ -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;
Expand All @@ -49,6 +51,7 @@ fn vertex(
out.size = size;
out.border = border;
out.point = point;
out.scale_factor = scale_factor;

return out;
}
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ui/src/render/ui_material_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,7 @@ pub fn queue_ui_material_nodes<M: UiMaterial>(
),
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::NONE,
inverse_scale_factor: 1.,
});
}
}
1 change: 1 addition & 0 deletions crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ pub fn queue_ui_slices(
),
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::NONE,
inverse_scale_factor: 1.,
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/testbed/ui_layout_rounding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ fn main() {
}

fn setup(mut commands: Commands) {
commands.spawn((Camera2d, UiAntiAlias::Off));
commands.spawn((Camera2d, UiAntiAlias::On));

commands
.spawn((
Expand Down

0 comments on commit aab36f3

Please sign in to comment.