2
\$\begingroup\$

I'm using the following code to render text in a letterboxed view.

#include <iostream>
#include <random>
#include <math.h>
#include <cstring>
#include <ostream>

#include <SFML/Graphics.hpp>

const float FIXED_UPDATE_TIME = 16.67f;//60fps

const unsigned int DEFAULT_CAMERA_WIDTH = 1280;
const unsigned int DEFAULT_CAMERA_HEIGHT = 720;

sf::View get_letterbox_view(sf::View view, int window_width, int window_height) {

    // Compares the aspect ratio of the window to the aspect ratio of the view,
    // and sets the view's viewport accordingly in order to archieve a letterbox effect.
    // A new view (with a new viewport set) is returned.

    float window_ratio = window_width / (float) window_height;
    float view_ratio = view.getSize().x / (float) view.getSize().y;
    float sizeX = 1;
    float sizeY = 1;
    float posX = 0;
    float posY = 0;

    bool horizontal_spacing = true;
    if (window_ratio < view_ratio)
        horizontal_spacing = false;

    // If horizontal_spacing is true, the black bars will appear on the left and right side.
    // Otherwise, the black bars will appear on the top and bottom.

    if (horizontal_spacing) {
        sizeX = view_ratio / window_ratio;
        posX = (1 - sizeX) / 2.f;
    }

    else {
        sizeY = window_ratio / view_ratio;
        posY = (1 - sizeY) / 2.f;
    }

    view.setViewport( sf::FloatRect(posX, posY, sizeX, sizeY) );

    return view;
}

int main() {
    sf::RenderWindow window(sf::VideoMode(DEFAULT_CAMERA_WIDTH, DEFAULT_CAMERA_HEIGHT), "Adventure of Jaggy Font Edges");
    window.setMouseCursorVisible(false);

    // Set up resource
    sf::Font fnt;
    fnt.loadFromFile("res/fnt/Ruda-Regular.ttf");

    // Set up text
    sf::Text text;
    text.setFont(fnt);
    text.setString("TEXT RENDERING");
    text.setCharacterSize(128);
    text.setFillColor(sf::Color::White);
    text.setOutlineColor(sf::Color::Black);
    text.setOutlineThickness(3.f);

    // Center text (in world space, I believe?)
    sf::FloatRect text_rect = text.getLocalBounds();
    text.setOrigin(
        floorf(text_rect.left + text_rect.width/2),
        floorf(text_rect.top + text_rect.height/2)
    );
    text.setPosition(0, 0);

    // Set up view, and center it on the text
    sf::View camera_view(sf::FloatRect(0.f, 0.f, DEFAULT_CAMERA_WIDTH, DEFAULT_CAMERA_HEIGHT));
    camera_view.setCenter(0, 0);

    sf::Clock clock;

    while (window.isOpen()) {
        // only update 60fps
        sf::Time elapsed_time = clock.getElapsedTime();
        if (elapsed_time.asMilliseconds() < FIXED_UPDATE_TIME) {
            continue;
        }

        // handle events
        sf::Event event;
        while (window.pollEvent(event)) {
            // window closed event
            if (event.type == sf::Event::Closed) {
                window.close();
            } // window resized event/*
            else if (event.type == sf::Event::Resized) {
                camera_view = get_letterbox_view(camera_view, event.size.width, event.size.height);
            }
        }
        window.clear(sf::Color(255, 255, 255, 255));

        window.setView(camera_view);
        window.draw(text);

        window.display();
    }
    return EXIT_SUCCESS;
}

At the default resolution, it looks like this: enter image description here

However, the moment I change the window size, we get artifacts: enter image description here

\$\endgroup\$
1
\$\begingroup\$

Welcome to the world of text rendering. The most common way to get rid of these artefacts is to always make sure that you're keeping font texture resolution 1:1 with the surface it's being rendered on.

Why this happens? SFML uses texture atlases to draw fonts, and when you resize the surface that the font is being drawn on, since font atlas is smaller in ratio than the new surface, texture interpolation is being applied in order to fill in places where we don't know what exact pixel is supposed to be drawn. But for pixels near the edges of the atlas, artifacts may occur with certain texture sampler configurations. This is called texture bleeding.

How to avoid this? You need to disable interpolation, or better yet, change font size every time you resize the window. You should try calculating new font size from the information you get at sf::Event::Resized event, by finding ratio between original resolution and new resolution, and then rescaling font size accordingly. You will probably need to take only height ratio into account.

As for interpolation, there is no official way to disable interpolation for fonts, as far as I know, but there is a well-known hack to achieve this through the use of setSmooth(bool) on a specific font-size atlas of the sf::Font.

sf::Texture& texture = const_cast<sf::Texture&>(font.getTexture(font_size));
texture.setSmooth(false);
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Changing the font size on the fly doesn't seem to improve matters, I tried float old_height = camera_view.getViewport().height; camera_view = get_letterbox_view(camera_view, event.size.width, event.size.height); float new_height = camera_view.getViewport().height; float new_character_size = roundf(text.getCharacterSize() * new_height/old_height); text.setCharacterSize(new_character_size); \$\endgroup\$ – floopfloop Jul 8 '19 at 8:13
  • \$\begingroup\$ @floopfloop using floor() instead of roundf() does not produce desired result too? \$\endgroup\$ – Ocelot Jul 8 '19 at 8:31
  • \$\begingroup\$ floor() shrinks the text because of truncation. Neither solve the artifacts. \$\endgroup\$ – floopfloop Jul 8 '19 at 9:37
  • \$\begingroup\$ @floopfloop, the goal is to make font size match the actual resolution of the rendered font texture, so no pixels would be interpolated. I honestly have no idea why it doesn't align right. Maybe the metrics are different? Have you tried turning smoothing off? Did that solve the issue? \$\endgroup\$ – Ocelot Jul 8 '19 at 10:10
  • \$\begingroup\$ what kind of metrics? (also, turning smoothing off 'fixes' the artifacts in exchange for pixelation. I understand the goal, but the method described doesn't produce it? \$\endgroup\$ – floopfloop Jul 8 '19 at 10:46
1
\$\begingroup\$

While this may not be the 'correct' solution for sharp text, I've rejigged the approach by rendering the text (and by implication, everything) onto a RenderTexture with smoothing enabled, which is subsequently stretched to fit the screen. This way everything on screen (sprites, text, etc) is rendered at a consistent resolution, which can be manipulated via the RenderTexture's dimensions.

#include <iostream>
#include <random>
#include <math.h>
#include <cstring>
#include <ostream>
#include <chrono>
#include <thread>

#include <SFML/Graphics.hpp>

using namespace std;

const float FIXED_UPDATE_TIME = 16.67f;//60fps

const unsigned int DEFAULT_CAMERA_WIDTH = 1280;
const unsigned int DEFAULT_CAMERA_HEIGHT = 720;

sf::View get_letterbox_view(sf::View view, int window_width, int window_height) {

    // Compares the aspect ratio of the window to the aspect ratio of the view,
    // and sets the view's viewport accordingly in order to archieve a letterbox effect.
    // A new view (with a new viewport set) is returned.

    float window_ratio = window_width / (float) window_height;
    float view_ratio = view.getSize().x / (float) view.getSize().y;
    float sizeX = 1;
    float sizeY = 1;
    float posX = 0;
    float posY = 0;

    bool horizontal_spacing = true;
    if (window_ratio < view_ratio)
        horizontal_spacing = false;

    // If horizontal_spacing is true, the black bars will appear on the left and right side.
    // Otherwise, the black bars will appear on the top and bottom.

    if (horizontal_spacing) {
        sizeX = view_ratio / window_ratio;
        posX = (1 - sizeX) / 2.f;
    }

    else {
        sizeY = window_ratio / view_ratio;
        posY = (1 - sizeY) / 2.f;
    }

    view.setViewport( sf::FloatRect(posX, posY, sizeX, sizeY) );

    return view;
}

int main() {
    sf::RenderWindow window(sf::VideoMode(DEFAULT_CAMERA_WIDTH, DEFAULT_CAMERA_HEIGHT), "Adventure of Jaggy Font Edges");
    window.setMouseCursorVisible(false);

    sf::RenderTexture render_texture;
    if (!render_texture.create(DEFAULT_CAMERA_WIDTH, DEFAULT_CAMERA_HEIGHT)) {
        exit(1);
    }
    render_texture.setSmooth(true);

    // Set up resource
    sf::Font fnt;
    fnt.loadFromFile("res/fnt/Ruda-Regular.ttf");

    // Set up text
    sf::Text text;
    text.setFont(fnt);
    text.setString("TEXT RENDERING");
    text.setCharacterSize(128);
    text.setFillColor(sf::Color::White);
    text.setOutlineColor(sf::Color::Black);
    text.setOutlineThickness(3.f);

    // Center text (in world space, I believe?)
    sf::FloatRect text_rect = text.getLocalBounds();
    text.setOrigin(
        floorf(text_rect.left + text_rect.width/2),
        floorf(text_rect.top + text_rect.height/2)
    );
    text.setPosition(0, 0);

    // Set up view, and center it on the text
    sf::View letterbox_view(sf::FloatRect(0.f, 0.f, DEFAULT_CAMERA_WIDTH, DEFAULT_CAMERA_HEIGHT));
    letterbox_view.setCenter(0, 0);

    sf::View camera_view(sf::FloatRect(0.f, 0.f, DEFAULT_CAMERA_WIDTH, DEFAULT_CAMERA_HEIGHT));
    camera_view.setCenter(0, 0);

    sf::Clock clock;

    while (window.isOpen()) {
        // only update 60fps
        sf::Time elapsed_time = clock.getElapsedTime();
        if (elapsed_time.asMilliseconds() < FIXED_UPDATE_TIME) {
            std::this_thread::sleep_for(std::chrono::milliseconds(17));
            continue;
        }

        // handle events
        sf::Event event;
        while (window.pollEvent(event)) {
            // window closed event
            if (event.type == sf::Event::Closed) {
                window.close();
            } // window resized event/*
            else if (event.type == sf::Event::Resized) {
                letterbox_view = get_letterbox_view(letterbox_view, event.size.width, event.size.height);
            }
        }
        window.clear(sf::Color(0, 0, 0, 255));

        render_texture.clear(sf::Color(255, 255, 255, 255));
        render_texture.setView(camera_view);
        render_texture.draw(text);
        render_texture.display();

        const sf::Texture& rt = render_texture.getTexture();
        window.setView(letterbox_view);
        sf::Sprite sprite(rt);

        sf::FloatRect sprite_rect = sprite.getLocalBounds();
        sprite.setOrigin(
            floorf(sprite_rect.left + sprite_rect.width/2),
            floorf(sprite_rect.top + sprite_rect.height/2)
        );
        sprite.setPosition(0, 0);

        window.draw(sprite);



        window.display();
    }
    return EXIT_SUCCESS;
}

Resizing the window produces the following (blurry) result: enter image description here

I would still like to know if there's a way to achieve sharp text at any resolution, preferably without needing an intermediate buffer, so I won't mark this as solved just yet.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.