bevy icon indicating copy to clipboard operation
bevy copied to clipboard

UI rounding is applied before UI scaling

Open eero-lehtinen opened this issue 1 year ago • 5 comments

Bevy version

main 7f658cabf7abd98d52bd94db8bd7fc788f49856a

What you did

Apply any UiScale or window scale factor override over 1, then move a UI element smoothly over the screen.

What went wrong

Here is an example of UiScale being 20 and then moving the white square smoothly across the screen. It jumps by 20 pixel increments. The blue square is there as a comparison to show what smooth movement looks like. GlobalTransform was modified directly to achieve that.

https://github.com/user-attachments/assets/f590e519-95dd-486d-b23d-4e88ffc639de

I would expect the white square position to be rounded after applying UiScale. E.g. Px(0.47) would first be scaled to Px(0.47*20) = Px(9.4), then rounded to Px(9). This would effectively round logical UI pixels to physical screen pixels, which sounds more correct to me than the current behavior.

The jittery movement is especially bad when trying to position scrolling lists or virtual cursors that need to look smooth.

Additional information

Example code used in the video.

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, update)
        .run();
}

#[derive(Component)]
struct Node1;
#[derive(Component)]
struct Node2;

#[derive(Resource)]
struct NodeAnimation((f32, f32));

fn setup(mut commands: Commands) {
    commands.spawn(Camera2dBundle::default());
    commands.spawn((
        NodeBundle {
            background_color: Color::WHITE.into(),
            style: Style {
                width: Val::Px(6.0),
                height: Val::Px(6.0),
                position_type: PositionType::Absolute,
                ..default()
            },
            ..default()
        },
        Node1,
    ));

    commands.spawn((
        NodeBundle {
            background_color: Srgba::BLUE.into(),
            style: Style {
                width: Val::Px(6.0),
                height: Val::Px(6.0),
                top: Val::Px(6.0),
                position_type: PositionType::Absolute,
                ..default()
            },
            ..default()
        },
        Node2,
    ));

    commands.insert_resource(UiScale(20.0));
    commands.insert_resource(NodeAnimation((0.0, 1.0)));
}

fn cubic_ease_in_out(factor: f32) -> f32 {
    if factor < 0.5 {
        4. * factor * factor * factor
    } else {
        1. - (-2. * factor + 2.).powf(3.) / 2.
    }
}

fn position(factor: f32) -> f32 {
    cubic_ease_in_out(factor) * 30.
}

fn update(
    mut node1_q: Query<&mut Style, With<Node1>>,
    mut node2_q: Query<&mut GlobalTransform, With<Node2>>,
    mut node_animation: ResMut<NodeAnimation>,
    time: Res<Time>,
) {
    let (factor, direction) = &mut node_animation.0;
    *factor += time.delta_seconds() * *direction * 0.3;
    if *factor > 1. {
        *direction = -1.;
    } else if *factor < 0. {
        *direction = 1.;
    }

    for mut style in node1_q.iter_mut() {
        style.left = Val::Px(position(*factor));
    }

    for mut t in node2_q.iter_mut() {
        let mut transform = t.compute_transform();
        transform.translation.x = position(*factor) + 3.;
        *t = transform.into();
    }
}

eero-lehtinen avatar Aug 11 '24 22:08 eero-lehtinen

Thanks for the great bug report: I think this explains the weirdness in #14183.

alice-i-cecile avatar Aug 11 '24 23:08 alice-i-cecile

related: https://github.com/bevyengine/bevy/issues/11207

rparrett avatar Aug 12 '24 00:08 rparrett

As discussed in #14779, this reverts a previous fix. We need to more carefully think about how and when we apply rounding.

alice-i-cecile avatar Aug 23 '24 12:08 alice-i-cecile

Rounding after scaling is the correct way to do things. That previous fix only works around the floating point imprecision of our rounding method.

We should really only do the rounding once after scaling to pixel coordinates and do the rest of the rendering in pixel coordinates. Now we round back and forth between pixel and logical coordinates and render in logical coordinates. According to discussion in https://github.com/bevyengine/bevy/issues/9754, we could just use Taffy's built in rounding.

Making this happen for all of our UI code is a lot of work though.

eero-lehtinen avatar Aug 23 '24 12:08 eero-lehtinen

In my own Bevy fork I disable all UI rounding to get perfect smoothness and I'm very happy with it. All this rounding business doesn't seem to be essential in all use cases.

eero-lehtinen avatar Aug 23 '24 12:08 eero-lehtinen

Great bug report. It seems like this fix isn't destined to be in 0.15 though :disappointed:

bas-ie avatar Sep 28 '24 07:09 bas-ie

This seems to be fixed by #16097 now 🤔

https://github.com/user-attachments/assets/7eed2b1e-c8a4-4ba8-a5d8-2dfed8396d67

Video lags a bit but both squares move very smoothly in person.

eero-lehtinen avatar Nov 03 '24 17:11 eero-lehtinen