Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: [Solved] Floating point view origin causes sprite rendering with shifted texture  (Read 2904 times)

0 Members and 1 Guest are viewing this topic.

LogicStuff

  • Newbie
  • *
  • Posts: 7
    • View Profile
    • Email
Hi, I've been making a tile-based 2D Game to pass time and bumped into one problem which is very obscure to me. The code has already maybe thousand lines so I won't be able to post it all here (I could give you the whole VS2013 solution though).

The behavior:
Before I've fixed that diagonal movement doesn't simply sum the horizontal and vertical movement, but divides these movements by square root of two, it worked just fine (the player was always having integral coordinates.
 
Now, when the coordinates are real numbers, as I move the player on the scene, sometimes I find a sensitive spot. If I manage to stay on one of these spots and not walk past it, all the tiles are not rendered properly - their texture rectangles appear to be shifted although I checked that they're fine. When I didn't focus the view on the sprite, I hadn't had this effect. From this, I deduced that this must be some problem with the sf::RenderTarget::draw that draws the tiles(sprites, I've also tried vertex array, but same thing).

(Look in the attachments)
Finally, here are some relevant parts to help your imagination:

TileSet class holds the texture of tiles and the vector of Tile class, which has sprite pointing to that texture:
// TileSet
class TileSet
{
        DECLARE_IMPL // my macro for declaring the struct for pimpl idiom

public:
        class Tile;
        using TileVec = std::vector<Tile>;

        // Tile
        class Tile
        {
                DECLARE_IMPL

        public:
                Tile(sf::Sprite const& sprite = sf::Sprite());

                Tile(Tile const&);
                ~Tile();
                Tile &operator=(Tile const&);

                // Get sprite
                sf::Sprite const& sprite() const;
        };

        TileSet(std::string const& filepath, uint tileSize);

        TileSet(TileSet const&);
        ~TileSet();
        TileSet &operator=(TileSet const&);

        // Load
        // Return true on success
        bool load(std::string const& filepath, uint tileSize);

        // Get tiles
        TileVec const& tiles() const;

        // Get tileSize
        uint tileSize() const;

        // Get texture
        sf::Texture const& texture() const;
};

// Impl
struct TileSet::Impl
{
        TileVec tiles_;
        uint tileSize_;
        sf::Texture texture_;
};
 

The TileSet::load function looks like this:
// Load
// Return true on success
bool TileSet::load(std::string const& filepath, uint tileSize)
{
        if(!impl_->texture_.loadFromFile(filepath))
                return false;

        uint tilecount_x = impl_->texture_.getSize().x / tileSize;
        uint tilecount_y = impl_->texture_.getSize().y / tileSize;

        impl_->tiles_.resize(tilecount_x * tilecount_y);

        for(uint y = 0; y < tilecount_y; ++y)
                for(uint x = 0; x < tilecount_x; ++x)
                        impl_->tiles_[tilecount_x * y + x] = Tile(sf::Sprite(impl_->texture_,
                                sf::IntRect(x * tileSize, y * tileSize, tileSize, tileSize)));

        impl_->tileSize_ = tileSize;
        return true;
}

TileMap class does the rendering:
// TileMap
class TileMap : public sf::Drawable, public sf::NonCopyable
{
        DECLARE_IMPL

public:
        using TileVec = std::vector<std::vector<Tile>>;

        TileMap();
        TileMap(std::shared_ptr<TileSet> const& tileSet);

        ~TileMap();

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

        // Get tiles
        TileVec &tiles();
        TileVec const& tiles() const;

        // Return width
        uint width() const;

        // Return height
        uint height() const;

        // Get tileSet
        Auxl::ThreadSafeReturn<std::shared_ptr<TileSet>> tileSet() const;

        // Set tiles
        void setTiles(TileVec const& tiles);

        // Set tileSet
        void setTileSet(std::shared_ptr<TileSet> const& tileSet);
};

// Impl
struct TileMap::Impl
{
        Impl(std::shared_ptr<TileSet> const& tileSet = nullptr);

        TileVec tiles_;
        std::shared_ptr<TileSet> tileSet_;
};
 

The TileMap::draw function:
// Draw
void TileMap::draw(sf::RenderTarget &target, sf::RenderStates states) const
{
        if(impl_->tileSet_->tiles().empty())
                return;

        sf::Vector2f topLeft            = target.getView().getCenter() - target.getView().getSize() / 2.0f;
        sf::Vector2f bottomRight        = topLeft + target.getView().getSize();

        int start_x = topLeft.x         / impl_->tileSet_->tileSize();
        int start_y = topLeft.y         / impl_->tileSet_->tileSize();
        float end_x = bottomRight.x     / impl_->tileSet_->tileSize();
        float end_y = bottomRight.y     / impl_->tileSet_->tileSize();

        if(start_x < 0)
                start_x = 0;

        if(start_y < 0)
                start_y = 0;

        if(end_y > static_cast<int>(impl_->tiles_.size()))
                end_y = impl_->tiles_.size();

        for(uint y = start_y; y < end_y; ++y)
        {
                float current_end_x = end_x;

                if(current_end_x > impl_->tiles_[y].size())
                        current_end_x = impl_->tiles_[y].size();

                for(uint x = start_x; x < current_end_x; ++x)
                {
                        int tile_id = impl_->tiles_[y][x].id();

                        if(tile_id != Tile::ID_EMPTY)
                        {
                                sf::Sprite sprite = impl_->tileSet_->tiles()[tile_id].sprite();
                                sprite.setPosition(
                                        x * impl_->tileSet_->tileSize(),
                                        y * impl_->tileSet_->tileSize());
                                target.draw(sprite, states);
                        }
                }
        }
}

Now, this is where I'm drawing the player (white square) moving the view:
// Render
virtual void render(
        Object const& object,
        sf::RenderTarget &target,
        sf::RenderStates states) const override
{
        sf::View view = target.getView();
        view.setCenter(bodyComponent_->x() + bodyComponent_->width() / 2.0f,
                                   bodyComponent_->y() + bodyComponent_->height() / 2.0f);
        target.setView(view);
        sf::RectangleShape rect(
                sf::Vector2f(bodyComponent_->width(), bodyComponent_->height()));
        rect.move(bodyComponent_->x(), bodyComponent_->y());
        target.draw(rect, states);
}

Below, you can see an example of the effect, along with the tile set, where you can see that the tile's texture isn't right in its place but contains a bit of adjacent tile's texture.
« Last Edit: August 03, 2015, 10:21:56 pm by LogicStuff »

Hapax

  • Hero Member
  • *****
  • Posts: 3379
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
It sounds like you're getting the common problem of what I'd probably call Tile Edging, where tile get odd lines etc. on their edges when the combination of tile's position, origin and the view's position is not an integer.

By far far the simplest, most accurate and probably the most common solution to this is to render all of the tiles to a render texture - all at integer co-ordinates - and then display the render texture at non-integer co-ordinates.
« Last Edit: August 03, 2015, 03:06:54 am by Hapax »
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*

LogicStuff

  • Newbie
  • *
  • Posts: 7
    • View Profile
    • Email
Thanks, that solves it! BTW, is there any original thread about that "Tile Edging" and other possible solutions?

zsbzsb

  • Hero Member
  • *****
  • Posts: 1409
  • Active Maintainer of CSFML/SFML.NET
    • View Profile
    • My little corner...
    • Email
A note on coordinates and undistorted rendering:
By default, SFML (or more exactly, OpenGL) may interpolate drawable objects such as sprites or texts when rendering. While this allows transitions like slow movements or rotations to appear smoothly, it can lead to unwanted results in some cases, for example blurred or distorted objects. In order to render a sf::Drawable object pixel-perfectly, make sure the involved coordinates allow a 1:1 mapping of pixels in the window to texels (pixels in the texture). More specifically, this means:
  • The object's position, origin and scale have no fractional part
  • The object's and the view's rotation are a multiple of 90 degrees
  • The view's center and size have no fractional part
Motion / MotionNET - Complete video / audio playback for SFML / SFML.NET

NetEXT - An SFML.NET Extension Library based on Thor

Hapax

  • Hero Member
  • *****
  • Posts: 3379
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
Thanks, that solves it!
You're welcome  :)

is there any original thread about that "Tile Edging"
The subject (probably not called tile edging) has come up a number of times on the forum but is usually a small problem amongst more serious problems so I don't think the subject has a dedicated thread as such. I guess this one counts now?  ;D

The most recent instance of this popping up that I have noticed would be here. Note that it's very subtle and doesn't detract from how awesome the game actually looks  ;)

[...]
The view's center and size have no fractional part
Technically, I would say that the sentence is not particularly accurate. The size doesn't need to avoid fractional values, rather it should avoid fractional ratios.
e.g. a window width of 800 and a view width of 801. Both values are integer but ratio is not.
It could be possibly worded thusly:
"The view's center and size-to-window-size ratio have no fraction part."
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*