Enable/disable UI anti-aliasing (#15170)

# Objective

Currently, UI is always rendered with anti-aliasing. This makes bevy's
UI completely unsuitable for art-styles that demands hard pixelated
edges, such as retro-style games.

## Solution

Add a component for disabling anti-aliasing in UI.

## Testing

In
[`examples/ui/button.rs`](15e246eff8/examples/ui/button.rs),
add the component to the camera like this:

```rust
use bevy::{prelude::*, ui::prelude::*};

commands.spawn((Camera2dBundle::default(), UiAntiAlias::Off));
```

The rounded button will now render without anti-aliasing.

## Showcase

An example of a rounded UI node rendered without anti-aliasing, with and
without borders:


![image](https://github.com/user-attachments/assets/ea797e40-bdaa-4ede-a0d3-c9a7eab95b6e)
This commit is contained in:
patrickariel 2024-09-17 06:06:23 +07:00 committed by GitHub
parent 29c4c79342
commit 3efef59d83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 58 additions and 8 deletions

View file

@ -25,7 +25,7 @@ use ui_texture_slice_pipeline::UiTextureSlicerPlugin;
use crate::graph::{NodeUi, SubGraphUi};
use crate::{
BackgroundColor, BorderColor, CalculatedClip, DefaultUiCamera, Display, Node, Outline, Style,
TargetCamera, UiImage, UiScale, Val,
TargetCamera, UiAntiAlias, UiImage, UiScale, Val,
};
use bevy_app::prelude::*;
@ -608,13 +608,15 @@ pub fn extract_default_ui_camera_view(
mut commands: Commands,
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
ui_scale: Extract<Res<UiScale>>,
query: Extract<Query<(Entity, &Camera), Or<(With<Camera2d>, With<Camera3d>)>>>,
query: Extract<
Query<(Entity, &Camera, Option<&UiAntiAlias>), Or<(With<Camera2d>, With<Camera3d>)>>,
>,
mut live_entities: Local<EntityHashSet>,
) {
live_entities.clear();
let scale = ui_scale.0.recip();
for (entity, camera) in &query {
for (entity, camera, ui_anti_alias) in &query {
// ignore inactive cameras
if !camera.is_active {
continue;
@ -660,9 +662,12 @@ pub fn extract_default_ui_camera_view(
color_grading: Default::default(),
})
.id();
commands
let entity_commands = commands
.get_or_spawn(entity)
.insert(DefaultCameraView(default_camera_view));
if let Some(ui_anti_alias) = ui_anti_alias {
entity_commands.insert(*ui_anti_alias);
}
transparent_render_phases.insert_or_clear(entity);
live_entities.insert(entity);
@ -837,13 +842,14 @@ 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)>,
mut views: Query<(Entity, &ExtractedView, Option<&UiAntiAlias>)>,
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)) = views.get_mut(extracted_uinode.camera_entity) else {
let Ok((view_entity, view, ui_anti_alias)) = views.get_mut(extracted_uinode.camera_entity)
else {
continue;
};
@ -854,7 +860,10 @@ pub fn queue_uinodes(
let pipeline = pipelines.specialize(
&pipeline_cache,
&ui_pipeline,
UiPipelineKey { hdr: view.hdr },
UiPipelineKey {
hdr: view.hdr,
anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)),
},
);
transparent_phase.add(TransparentUi {
draw_function,

View file

@ -48,6 +48,7 @@ impl FromWorld for UiPipeline {
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct UiPipelineKey {
pub hdr: bool,
pub anti_alias: bool,
}
impl SpecializedRenderPipeline for UiPipeline {
@ -73,7 +74,11 @@ impl SpecializedRenderPipeline for UiPipeline {
VertexFormat::Float32x2,
],
);
let shader_defs = Vec::new();
let shader_defs = if key.anti_alias {
vec!["ANTI_ALIAS".into()]
} else {
Vec::new()
};
RenderPipelineDescriptor {
vertex: VertexState {

View file

@ -150,11 +150,15 @@ fn draw(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> {
// outside the outside edge, or inside the inner edge have positive signed distance.
let border_distance = max(external_distance, -internal_distance);
#ifdef ANTI_ALIAS
// At external edges with no border, `border_distance` is equal to zero.
// 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);
#else
let t = 1.0 - step(0.0, border_distance);
#endif
// Blend mode ALPHA_BLENDING is used for UI elements, so we don't premultiply alpha here.
return vec4(color.rgb, saturate(color.a * t));
@ -165,7 +169,13 @@ fn draw_background(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> {
// When drawing the background only draw the internal area and not the border.
let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border);
#ifdef ANTI_ALIAS
let t = antialias(internal_distance);
#else
let t = 1.0 - step(0.0, internal_distance);
#endif
return vec4(color.rgb, saturate(color.a * t));
}

View file

@ -2381,3 +2381,29 @@ impl<'w, 's> DefaultUiCamera<'w, 's> {
})
}
}
/// Marker for controlling whether Ui is rendered with or without anti-aliasing
/// in a camera. By default, Ui is always anti-aliased.
///
/// ```
/// use bevy_core_pipeline::prelude::*;
/// use bevy_ecs::prelude::*;
/// use bevy_ui::prelude::*;
///
/// fn spawn_camera(mut commands: Commands) {
/// commands.spawn((
/// Camera2dBundle::default(),
/// // This will cause all Ui in this camera to be rendered without
/// // anti-aliasing
/// UiAntiAlias::Off,
/// ));
/// }
/// ```
#[derive(Component, Clone, Copy, Default, Debug, Reflect, Eq, PartialEq)]
pub enum UiAntiAlias {
/// UI will render with anti-aliasing
#[default]
On,
/// UI will render without anti-aliasing
Off,
}