mirror of
https://github.com/bevyengine/bevy
synced 2024-11-23 21:23:05 +00:00
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:
parent
c0fbadbc4c
commit
aab36f3951
9 changed files with 39 additions and 13 deletions
|
@ -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`].
|
||||
|
|
|
@ -371,6 +371,7 @@ pub fn queue_shadows(
|
|||
),
|
||||
batch_range: 0..0,
|
||||
extra_index: PhaseItemExtraIndex::NONE,
|
||||
inverse_scale_factor: 1.,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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;
|
||||
};
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -655,6 +655,7 @@ pub fn queue_ui_material_nodes<M: UiMaterial>(
|
|||
),
|
||||
batch_range: 0..0,
|
||||
extra_index: PhaseItemExtraIndex::NONE,
|
||||
inverse_scale_factor: 1.,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -372,6 +372,7 @@ pub fn queue_ui_slices(
|
|||
),
|
||||
batch_range: 0..0,
|
||||
extra_index: PhaseItemExtraIndex::NONE,
|
||||
inverse_scale_factor: 1.,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ fn main() {
|
|||
}
|
||||
|
||||
fn setup(mut commands: Commands) {
|
||||
commands.spawn((Camera2d, UiAntiAlias::Off));
|
||||
commands.spawn((Camera2d, UiAntiAlias::On));
|
||||
|
||||
commands
|
||||
.spawn((
|
||||
|
|
Loading…
Reference in a new issue