Search code examples
c++sfml

How to fix vertical artifact lines in a vertex array in SFML, WITH pixel perfect zoom/move?


I have been working on a 2D top-down game solely in SFML and C++, that has a tilemap. I cannot rid the tilemap of vertical line artifacts when zooming in and out or moving the render view at different zoom levels. I attached an image below of the problem; circled in red.

Problem with gap between squares

[edit]

There are a lot of factors that make this bug inconsistent. If I use a tile_atlas from only one tile, there is no artifacts. If I map each texture to a tile a.k.a not using vertex arrays; I did not see any artifacts but it is anywhere from 10x to 15x slower with the same number of tiles on the screen. I have tried finding zoom levels that don't cause artifacts, and there are some. but the levels are almost random and makes zoom in and out, not smooth and choppy.

I have tried numerous tutorials and forum "fixes" that have not completely worked. I have completely rewrote the underlying tile engine 4 separate times to no avail.

https://en.sfml-dev.org/forums/index.php?topic=15747.0

topic=14504

topic=5952

topic=13637.15

https://www.sfml-dev.org/tutorials/2.5/graphics-view.php

https://www.binpress.com/creating-city-building-game-with-sfml/

https://www.sfml-dev.org/tutorials/2.5/graphics-vertex-array.php

[edit]

I have read the Terreria scaling issue, the fix to make extra large textures then scale, or multiple textures, one for each zoom level. seem exhaustive. I am looking for a programmatic way of achieving the scaling correctly.

This is post is my last attempt to fix the code, otherwise I will need to change languages.

I believe the main issue comes from the zoom in/out functions of the program. I have tried many, variations/attempt to get this to work +0.5f offset,+0.375f offset, not pixel perfect

if (sf::Keyboard::isKeyPressed(sf::Keyboard::E))
        {
            float zoom_in = 0.995f;

            float nz = last_sq * zoom_in;
            nz = std::floor(nz);
            float now = nz / last_sq;
            if (nz <= 10)
                continue;
            last_sq = nz;

            std::cout << now << std::endl;

            cam.zoom(now);
        }


        if (sf::Keyboard::isKeyPressed(sf::Keyboard::W))
        {
            cam.move(0.f, -0.02f);
            float x = cam.getCenter().x;
            float y = cam.getCenter().y;
            x = std::floor(x);
            y = std::floor(y);
            //std::cout << "x: " << x << "\ty: " << y << std::endl;
            cam.setCenter(x, y);
        }

Here is the entire code.

main.cpp


#include "chunk_map.h"
#include "tile_atlas.h"
#include <vector>
//#include "main.h"
#include "map.h"
#include <iostream>

#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/Graphics/View.hpp>
#include <SFML/Window/Event.hpp>
#include <SFML/Window/Keyboard.hpp>

#include "animation_handler.h"

int main(int argc, char* argv[])
{
    sf::RenderWindow app(sf::VideoMode(600, 600), "Tilemap Example");
    // Hard set fps to monitor refresh rate.

    // textures to load.
    /*text_mgr.loadTexture("grass", "grass.png");
    text_mgr.loadTexture("high_grass", "high_grass.png");

    Animation staticAnim(0, 0, 1.0f);
    tileAtlas["grass"] = Tile(32, 1, text_mgr.getRef("grass"),{ staticAnim },
            TileType::GRASS, 50, 0, 1);

    tileAtlas["high_grass"] = Tile(32, 1, text_mgr.getRef("high_grass"),{ staticAnim },
        TileType::HIGH_GRASS, 100, 0, 1);*/

    //Map map;

    //map.load(50, 50, tileAtlas);

    #ifdef NDEBUG
    app.setVerticalSyncEnabled(true);
    std::cout << "#################\n";
    #endif // DEBUG
    //app.setVerticalSyncEnabled(true);
    sf::View cam = app.getDefaultView();

    tile_atlas atlas = tile_atlas();

    std::vector<chunk_map> map;

    for (int x = 0; x < 5; x++)
    {
        for (int y = 0; y < 5; y++)
        {
            map.push_back(chunk_map());
            map.back().set_texture(atlas.get_atlas());
            map.back().set_position(10 * x, 10 * y, 10 * (x + 1), 10 * (y + 1));
            map.back().load_tiles();
        }
    }

    sf::Clock clock;
    int checked = 0;
    int last_sq = 600;

    while (app.isOpen())
    {
        //sf::Time elapsed = clock.restart();
        //float dt = elapsed.asSeconds();

        sf::Event eve;
        while (app.pollEvent(eve))
            if (eve.type == sf::Event::Closed)
                app.close();


        if (sf::Keyboard::isKeyPressed(sf::Keyboard::P))
            std::cout << "view x: " << cam.getSize().x << "\tview y: " << cam.getSize().y << std::endl;


        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Q))
        {
            float zoom_out = 1.005f;

            float nz = last_sq * zoom_out;
            nz = std::ceil(nz);
            float now = nz / last_sq;
            last_sq = nz;

            std::cout << now << std::endl;

            cam.zoom(now);
            //float x = cam.getCenter().x;
            //float y = cam.getCenter().y;
            //x = std::floor(x);
            //y = std::floor(y);
            ////std::cout << "x: " << x << "\ty: " << y << std::endl;
            //cam.setCenter(x, y);
        }


        if (sf::Keyboard::isKeyPressed(sf::Keyboard::E))
        {
            float zoom_in = 0.995f;

            float nz = last_sq * zoom_in;
            nz = std::floor(nz);
            float now = nz / last_sq;
            if (nz <= 10)
                continue;
            last_sq = nz;

            std::cout << now << std::endl;

            cam.zoom(now);
            //float x = cam.getCenter().x;
            //float y = cam.getCenter().y;
            //x = std::floor(x);
            //y = std::floor(y);
            ////std::cout << "x: " << x << "\ty: " << y << std::endl;
            //cam.setCenter(x, y);
        }


        if (sf::Keyboard::isKeyPressed(sf::Keyboard::W))
        {
            cam.move(0.f, -0.02f);
            float x = cam.getCenter().x;
            float y = cam.getCenter().y;
            x = std::floor(x);
            y = std::floor(y);
            //std::cout << "x: " << x << "\ty: " << y << std::endl;
            cam.setCenter(x, y);
        }


        if (sf::Keyboard::isKeyPressed(sf::Keyboard::A))
        {
            cam.move(-0.02f, 0.f);
            float x = cam.getCenter().x;
            float y = cam.getCenter().y;
            x = std::floor(x);
            y = std::floor(y);
            //std::cout << "x: " << x << "\ty: " << y << std::endl;
            cam.setCenter(x, y);
        }


        if (sf::Keyboard::isKeyPressed(sf::Keyboard::S))
        {
            cam.move(0.f, 0.02f);
            float x = cam.getCenter().x;
            float y = cam.getCenter().y;
            x = std::ceil(x);
            y = std::ceil(y);
            //std::cout << "x: " << x << "\ty: " << y << std::endl;
            cam.setCenter(x, y);
        }


        if (sf::Keyboard::isKeyPressed(sf::Keyboard::D))
        {
            cam.move(0.02f, 0.f);
            float x = cam.getCenter().x;
            float y = cam.getCenter().y;
            x = std::ceil(x);
            y = std::ceil(y);
            //std::cout << "x: " << x << "\ty: " << y << std::endl;
            cam.setCenter(x, y);
        }


        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right))
        {
            sf::Time elapsed = clock.getElapsedTime();
            float t = elapsed.asSeconds();
            int time = std::floor(t);

            if (checked < time)
            {
                checked = time;

                cam.move(0.01f, 0.f);
                float x = cam.getCenter().x;
                float y = cam.getCenter().y;
                x = std::ceil(x);
                y = std::ceil(y);
                std::cout << "x: " << x << "\ty: " << y << std::endl;
                cam.setCenter(x, y);
            }
        }


        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Escape))
            app.close();

        app.setView(cam);

    #ifdef _DEBUG
        app.clear();
    #endif // DEBUG
        //map.draw(app, dt);

        for (int i = 0; i < 25; i++)
        {
            app.draw(map.at(i));
        }

        app.display();
    }
}

chunk_map.h

#pragma once


#include <SFML/Graphics/Drawable.hpp>
#include <SFML/Graphics/Texture.hpp>
#include <SFML/Graphics/VertexArray.hpp>
#include <vector>


class chunk_map : public sf::Drawable
{
private:
    //change values of these to match your needs and improve performance
    enum { tilesize = 32, chunksize = 32};

    //tile size float
    float tile_size_float = 32.0f;

    // Draw chunk
    virtual void draw(sf::RenderTarget& target, sf::RenderStates states)const;

    // texture for chunk
    sf::Texture m_texture;

    // chunk dimensions
    int tiles_per_chunk_x;
    int tiles_per_chunk_y;

    //start x and y and ending x and y scaled to tile size. a.k.a. 
    // 1,1 = tile 1,1. 10,10, equals tile 10,10
    int chunk_start_x;
    int chunk_start_y;

    int chunk_end_x;
    int chunk_end_y;

    // Vertex array of positions of tiles in chunk
    std::vector<std::vector<sf::VertexArray> > m_chunks;

    // Append tiles.
    void append_tile(int gx, int gy, sf::VertexArray& garr);

public:
    chunk_map();
    ~chunk_map();
    void load_tiles();


    void set_texture(sf::Texture);
    void set_position(int chunk_start_x, int chunk_start_y,
        int chunk_end_x, int chunk_end_y);

};


chunk_map.cpp

#include "chunk_map.h"

#include <SFML/Graphics/RenderTarget.hpp>
#include <SFML/System/Vector2.hpp>
#include <SFML/Graphics/Vertex.hpp>

chunk_map::chunk_map()
{
}

chunk_map::~chunk_map()
{
}


void chunk_map::load_tiles()
{
    /*

        Tile loading this is were the tiles are added to the Quadrantics of the tilemap.
        this is the entire chunk_map loop

    */
    if ((chunk_end_x * chunk_end_y) == 0)//empty map - possibly forgotten to fill data struct
    {
        //to stop displaying at all after failed loading:
        tiles_per_chunk_x = 0;
        tiles_per_chunk_y = 0;
        m_chunks.clear();
        return;
    }

    chunk_map::tiles_per_chunk_x = (chunk_end_x / chunksize) + 1;
    chunk_map::tiles_per_chunk_y = (chunk_end_y / chunksize) + 1;
    m_chunks.assign(tiles_per_chunk_x, std::vector<sf::VertexArray>(tiles_per_chunk_y, sf::VertexArray(sf::Quads)));//ready up empty 2d arrays
    for (int iy = chunk_start_y; iy < chunk_end_y; ++iy)
    {
        for (int ix = chunk_start_x; ix < chunk_end_x; ++ix)
        {
            append_tile(ix, iy, m_chunks[ix / chunksize][iy / chunksize]);
        }
    }
}


void chunk_map::append_tile(int gx, int gy, sf::VertexArray& garr)
{
    /*

        This  is the specific tile vertex, broken from the other function to decrease complexitity.

    */

    int tile_selection_index_x = rand() % 2;
    int tile_selection_index_y = 0;

    float f_tx = tile_selection_index_x * tile_size_float;
    float f_ty = tile_selection_index_y * tile_size_float;


    sf::Vertex ver;

    //____________________________________________________________________________________________________________
    ver.position = sf::Vector2f(gx * tile_size_float, gy * tile_size_float);
    //texture in position of text atlas
    //top left corner
    //ver.texCoords = sf::Vector2f( 0.f, 0.f);
    ver.texCoords = sf::Vector2f(f_tx, f_ty);
    garr.append(ver);

    //____________________________________________________________________________________________________________
    ver.position = sf::Vector2f(gx * tile_size_float + tile_size_float, gy * tile_size_float);
    //texture in position of text atlas
    //top right corner
    //ver.texCoords = sf::Vector2f( tile_size_float, 0.f);
    ver.texCoords = sf::Vector2f(f_tx + tile_size_float, f_ty);
    garr.append(ver);

    //____________________________________________________________________________________________________________
    ver.position = sf::Vector2f(gx * tile_size_float + tile_size_float, gy * tile_size_float + tile_size_float);
    //texture in position of text atlas
    //bottom right corner
    //ver.texCoords = sf::Vector2f( tile_size_float,  tile_size_float);
    ver.texCoords = sf::Vector2f(f_tx + tile_size_float, f_ty + tile_size_float);
    garr.append(ver);

    //____________________________________________________________________________________________________________
    ver.position = sf::Vector2f(gx * tile_size_float, gy * tile_size_float + tile_size_float);
    //texture in position of text atlas
    //bottom left corner
    //ver.texCoords = sf::Vector2f( 0.f,  tile_size_float);
    ver.texCoords = sf::Vector2f(f_tx, f_ty + tile_size_float);
    garr.append(ver);
}


void chunk_map::set_texture(sf::Texture t)
{
    /*

        Sets the texture data for this chunk map from the texture atlas.

    */

    m_texture = t;

    // TODO test this feature
    // Attempt to optimize tearing on zooming to a different view.
    //m_texture.setSmooth(true);
}


void chunk_map::set_position(int chunk_start_x, int chunk_start_y,
    int chunk_end_x, int chunk_end_y)
{
    /*

        Initialize the accordinates of the start of the chunk_map to the end.

    */
    chunk_map::chunk_start_x = chunk_start_x;
    chunk_map::chunk_start_y = chunk_start_y;

    chunk_map::chunk_end_x = chunk_end_x;
    chunk_map::chunk_end_y = chunk_end_y;
}


void chunk_map::draw(sf::RenderTarget& target, sf::RenderStates states)const
{
    /*

        The actual draw call to this specific chunk_map

    */


    // position variables for this draw.
    int left = 0;
    int right = 0;
    int top = 0;
    int bottom = 0;


    //get top left point of view
    sf::Vector2f temp = target.getView().getCenter() - (target.getView().getSize() / 2.f);


    //get top left point of view
    left = static_cast<int>(temp.x / (chunksize * tilesize));
    top = static_cast<int>(temp.y / (chunksize * tilesize));


    //get bottom right point of view
    temp += target.getView().getSize();
    right = 1 + static_cast<int>(temp.x / (chunksize * tilesize));
    bottom = 1 + static_cast<int>(temp.y / (chunksize * tilesize));


    //clamp these to fit into array bounds:
    left = std::max(0, std::min(left, tiles_per_chunk_x));
    top = std::max(0, std::min(top, tiles_per_chunk_y));
    right = std::max(0, std::min(right, tiles_per_chunk_x));
    bottom = std::max(0, std::min(bottom, tiles_per_chunk_y));


    //set texture and draw visible chunks:
    states.texture = &m_texture;



    for (int ix = left; ix < right; ++ix)
    {
        for (int iy = top; iy < bottom; ++iy)
        {
            target.draw(m_chunks[ix][iy], states);
        }
    }
}

tile_atlas.h

#pragma once
#include <SFML/Graphics/Texture.hpp>
class tile_atlas
{
private:
    sf::Texture atlas_texture;
public:
    tile_atlas();
    ~tile_atlas();
    sf::Texture& get_atlas();
};

tile_atlas.cpp

#include "tile_atlas.h"
#include <iostream>
#include <string>


tile_atlas::tile_atlas()
{
    std::string file_string = "tilemap_test.png";
    if (!atlas_texture.loadFromFile(file_string))
    {
        std::cout << "Failed loading file: " << file_string << std::endl;
        exit(1);
    }
}

tile_atlas::~tile_atlas()
{
}

sf::Texture& tile_atlas::get_atlas()
{
    return atlas_texture;
}

I am trying to fix this code to remove vertical artifacts so the above image will always look like this no matter if moving the view or zooming in/out.

tilemap as expected



[code for answer]

Using @Mario's answer this is the code I wrote (at the bottom of main.cpp) that completely fixed the artifacts. here is a great link showing an example. https://www.sfml-dev.org/tutorials/2.5/graphics-draw.php#off-screen-drawing

    #ifdef _DEBUG
        app.clear();
    #endif // DEBUG
        //map.draw(app, dt);


        /*-----------------------------------------------------------*/
        // Draw the texture
        //rt.clear();

        rt.draw(map.at(0));
        rt.display();
        if (cam.getSize().x < 500)
        {
            rt.setSmooth(false);
        }
        else
        {
            rt.setSmooth(true);
        }
        //// get the target texture (where the stuff has been drawn)
        const sf::Texture& texture = rt.getTexture();

        sf::Sprite sprite(texture);

        app.draw(sprite);

        //app.draw(map.at(0));

        /*-----------------------------------------------------------*/

        app.display();

Solution

  • Simple, yet effective:

    Render your pixels 1:1 without scaling to a render texture and then upscale that instead.

    Might be a bit tricky to determine the correct position, zoom, etc. but it can be done.