bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
34.04k stars 3.32k forks source link

Text rasterization is based on world units instead of pixels #1890

Open janikrabe opened 3 years ago

janikrabe commented 3 years ago

Bevy version

Operating system & version

Linux/Wayland

What you did

Modify the camera in the text2d example to use a scale of 0.1:

use bevy::prelude::*;
use bevy::render::camera::{DepthCalculation, OrthographicProjection};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_startup_system(setup)
        .run();
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands
        .spawn_bundle(OrthographicCameraBundle::new_2d())
        .insert(OrthographicProjection {
            near: 0.0,
            far: 1000.0,
            scale: 0.1,
            depth_calculation: DepthCalculation::ZDifference,
            ..Default::default()
        });
    commands.spawn_bundle(Text2dBundle {
        text: Text::with_section(
            "Hello.",
            TextStyle {
                font: asset_server.load("fonts/FiraSans-Bold.ttf"),
                font_size: 25.0,
                color: Color::WHITE,
            },
            TextAlignment {
                vertical: VerticalAlign::Center,
                horizontal: HorizontalAlign::Center,
            },
        ),
        ..Default::default()
    });
}

What you expected to happen

The text should rasterize properly with no visible artifacts.

What actually happened

The text is rasterized to 10x10 pixel squares (1/scale).

raster

Additional information

This can also be seen with ScalingMode::FixedHorizontal when the scale does not match the window's width:

// ...
.insert(OrthographicProjection {
    near: 0.0,
    far: 1000.0,
    window_origin: WindowOrigin::Center,
    scaling_mode: ScalingMode::FixedHorizontal,
    scale: 100.0,
    depth_calculation: DepthCalculation::ZDifference,
    ..Default::default()
});
// ...

(The same applies to ScalingMode::FixedVertical, of course.)

benfrankel commented 1 year ago

I hit this issue in Bevy 0.10 during Bevy Game Jam 3. We made a 2D pixel art game with scaled up pixels via camera zoom, and then I tried to add nametags to the player / enemies. I ended up working around this issue by rendering the nametag text with an absurd font size (EDIT: This was actually unnecessary), then scaling the text entity down. This was good enough for the jam, but it was a rough hack, the text still had artifacts due to the scaling (EDIT: Wrong), and it was an extra performance cost (EDIT: Only because I incorrectly used a larger font size).

In retrospect we should have scaled our sprites up instead of zooming the camera in, but it's unfortunate that camera zoom is effectively incompatible with world-space text.

LLBlumire commented 11 months ago

This is effecting me on a current project in 0.11, I'd be happy to work on trying to fix it if someone with a bit more experience in the engine could point me in the right direction

rparrett commented 10 months ago

This was good enough for the jam, but it was a rough hack, the text still had artifacts due to the scaling, and it was an extra performance cost.

Can you elaborate on how this didn't work out for you / what artifacts you were experiencing?

font_size is the vertical height in pixels that the text is rasterized at. If you place that font texture into the world and zoom in 10x, it will appear pixelized. If you need the text to appear crisp at a height of 250 screen pixels, then it needs to be rasterized at 250px. There shouldn't be an "extra" performance cost.

(side note: font_size docs were improved recently in #9320 and #9524 is seeking to add viewport-based controls for font size, although I don't think that it would particularly help when the projection is scaled.)

I updated the repro for bevy 0.11 here: https://gist.github.com/rparrett/c9a485a313235d678dffa30f1bcba974

benfrankel commented 10 months ago

Can you elaborate on how this didn't work out for you / what artifacts you were experiencing?

The text gets pixelated first, and then scaled down via linear sampling, not pixel-perfect sampling. That's what causes the artifacts. The performance cost is due to choosing a huge font size to reduce (but not eliminate) the artifacts from linear scaling.

The above is totally wrong. The correct workaround is to simply have a system scale the text entity's transform by the inverse of the camera zoom (4x zoom => scale by 0.25).

rparrett commented 10 months ago

Font textures can be configured to use nearest sampling, although the easiest way to accomplish that is to set the default sampler globally. But that's probably the right call if you need it for text.

benfrankel commented 10 months ago

For some reason I remember there being more to the story at the time, but going back and looking at the actual font size rendered + the actual pixels on screen (visually comparing to the same font rendered in my browser on Google Fonts), everything seems fine. The only difference is Google Fonts does subpixel anti-aliasing whereas bevy just does normal anti-aliasing, which makes a big difference for smaller text, but that's not related to this issue.

TimJentzsch commented 5 months ago

Triage

This issue appears to persist with Bevy 0.12.

Reproduction code: https://github.com/TimJentzsch/bevy_triage/tree/main/issues/issue_1890

Font size 250 with camera projection scale 1.0: Crisp text

Font size 25 with camera projection scale 0.1: Extremely blurry text with pixelated artifacts

TimJentzsch commented 5 months ago

Notably, this issue only happens with Text2dBundle, not with TextBundle (from UI). The latter ignores the camera scale and uses UiScale instead, which handles this better.

jjyr commented 4 months ago

Workaround: Increase the font_size and set Transform#scale to a smaller value.

let mut bundle = Text2dBundle {
            transform: transform.with_scale(Vec3::splat(0.1)),
            ..Default::default()
        };
bundle.text.sections.push(TextSection {
        value: format!("test"),
        style: TextStyle {
            //...
            font_size: font_size * 10,
            ..Default::default(),
        },
    });
benfrankel commented 4 months ago

You only need to scale the text's transform by the inverse of the camera zoom. No need to touch the font size.