Hi, I use a tileset texture with multiple tiles on it and sometimes - when the view is zoomed - a line from the next tile is drawn. So I expect the tex coords to be broken somehow ... but why?
Minimal example code:
#include <iostream>
#include <SFML/Graphics.hpp>
void add(sf::VertexArray& array, sf::Vector2u const & world_pos, unsigned int tile_offset, unsigned int tile_size) {
sf::Vertex tl, tr, br, bl; // [t]op[r]ight etc.
// setup positions
tl.position = sf::Vector2f( world_pos.x * tile_size, world_pos.y * tile_size);
tr.position = sf::Vector2f((1 + world_pos.x) * tile_size, world_pos.y * tile_size);
br.position = sf::Vector2f((1 + world_pos.x) * tile_size, (1 + world_pos.y) * tile_size);
bl.position = sf::Vector2f( world_pos.x * tile_size, (1 + world_pos.y) * tile_size);
// setup texture coords
tl.texCoords = sf::Vector2f(0.f, tile_offset * tile_size);
tr.texCoords = sf::Vector2f(tile_size, tile_offset * tile_size);
br.texCoords = sf::Vector2f(tile_size, (tile_offset + 1) * tile_size);
bl.texCoords = sf::Vector2f(0.f, (tile_offset + 1) * tile_size);
// add tiles
array.append(tl);
array.append(tr);
array.append(br);
array.append(bl);
}
int main() {
sf::RenderWindow window{{800u, 600u}, "test"};
sf::Texture tileset;
tileset.loadFromFile("tileset.png");
unsigned int tile_size{16u};
sf::VertexArray array{sf::Quads};
for (auto y = 0u; y < 20; ++y) {
for (auto x = 0u; x < 20; ++x) {
unsigned int offset{0u};
if (y == 0 || y == 19 || x == 0 || x == 19) {
offset = 1u;
}
add(array, {x, y}, offset, tile_size);
}
}
auto view = window.getDefaultView();
float zoom{1.f};
while (window.isOpen()) {
sf::Event event;
while (window.pollEvent(event)) {
if (event.type == sf::Event::Closed) {
window.close();
}
}
window.clear(sf::Color::Black);
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Up)) { view.move(0, -1); }
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Down)) { view.move(0, 1); }
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) { view.move(-1, 0); }
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) { view.move(1, 0); }
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Q)) { view.zoom(1.01f); zoom *= 1.01f; std::cout << "zoom: " << zoom << "\n"; }
if (sf::Keyboard::isKeyPressed(sf::Keyboard::E)) { view.zoom(0.99f); zoom *= 0.99f; std::cout << "zoom: " << zoom << "\n"; }
window.setView(view);
window.draw(array, &tileset);
window.display();
}
}
Tileset png and screenshots are attached. Any ideas what's wrong with my implementation? (btw using SFML 2.3 from ubuntu wily repos)
By changing the size by less than a half pixel? ???
If the current resolution is 800x600 and the zoom factor is 1.013525, the resulting size is 810.82x608.115; rounding results to 811x608. gcd(800,600)=200, hence 800/200=4, 600/200=3, thus 4:3 ratio. gcd(811,608)=1, hence 811:608 ratio. Doing this repeated will break the ratio more and more.
I tried the following:
float zoom(sf::View& view, float factor) {
if (factor == 1.f) {
return 1.f;
}
auto size = sf::Vector2i{view.getSize()};
auto prev = size;
int step = factor > 1.f ? 1 : -1;
// calculate ratio and reduce fraction
auto ratio = size;
auto gcd = boost::math::gcd(ratio.x, ratio.y);
ratio.x /= gcd;
ratio.y /= gcd;
// resize until factor exceeded
float done{1.f};
do {
size += ratio * step;
done = (1.f * size.x) / prev.x;
} while ((factor > 1.f && done < factor) ||
(factor < 1.f && done > factor));
// fix size
if (size.x <= 0 || size.y <= 0) {
size = ratio;
}
// apply size
view.setSize(sf::Vector2f{size});
return done;
}
But this doesn't fix the "render feature"... any thoughts about that?
/EDIT: Optimized version for larger zooming steps (without loop)
float zoom(sf::View& view, float factor) {
if (factor == 1.f) {
return 1.f;
}
auto size = sf::Vector2i{view.getSize()};
auto prev = size;
int step = factor > 1.f ? 1 : -1;
// calculate ratio and reduce fraction
auto ratio = size;
auto gcd = boost::math::gcd(ratio.x, ratio.y);
ratio.x /= gcd;
ratio.y /= gcd;
// resize until factor exceeded
auto n = static_cast<int>(std::abs(size.x * factor - size.x)) / ratio.x;
size += ratio * step * (n+1);
// fix size
if (size.x <= 0 || size.y <= 0) {
size = ratio;
}
// apply size
view.setSize(sf::Vector2f{size});
return (1.f * size.x) / prev.x;
}
/EDIT: Updated program code that will zoom to a factor which causes the lines on my machine
#include <iostream>
#include <boost/math/common_factor_rt.hpp>
#include <SFML/Graphics.hpp>
void add(sf::VertexArray& array, sf::Vector2u const & world_pos, unsigned int tile_offset, unsigned int tile_size) {
sf::Vertex tl, tr, br, bl; // [t]op[r]ight etc.
// setup positions
tl.position = sf::Vector2f( world_pos.x * tile_size, world_pos.y * tile_size);
tr.position = sf::Vector2f((1 + world_pos.x) * tile_size, world_pos.y * tile_size);
br.position = sf::Vector2f((1 + world_pos.x) * tile_size, (1 + world_pos.y) * tile_size);
bl.position = sf::Vector2f( world_pos.x * tile_size, (1 + world_pos.y) * tile_size);
// setup texture coords
tl.texCoords = sf::Vector2f(0.f, tile_offset * tile_size);
tr.texCoords = sf::Vector2f(tile_size, tile_offset * tile_size);
br.texCoords = sf::Vector2f(tile_size, (tile_offset + 1) * tile_size);
bl.texCoords = sf::Vector2f(0.f, (tile_offset + 1) * tile_size);
// add tiles
array.append(tl);
array.append(tr);
array.append(br);
array.append(bl);
}
float zoom(sf::View& view, float factor) {
if (factor == 1.f) {
return 1.f;
}
auto size = sf::Vector2i{view.getSize()};
auto prev = size;
int step = factor > 1.f ? 1 : -1;
// calculate ratio and reduce fraction
auto ratio = size;
auto gcd = boost::math::gcd(ratio.x, ratio.y);
ratio.x /= gcd;
ratio.y /= gcd;
// resize until factor exceeded
auto n = static_cast<int>(std::abs(size.x * factor - size.x)) / ratio.x;
size += ratio * step * (n+1);
// fix size
if (size.x <= 0 || size.y <= 0) {
size = ratio;
}
// apply size
view.setSize(sf::Vector2f{size});
return (1.f * size.x) / prev.x;
}
int main() {
sf::RenderWindow window{{800u, 600u}, "test"};
sf::Texture tileset;
tileset.loadFromFile("tileset.png");
unsigned int tile_size{16u};
sf::VertexArray array{sf::Quads};
for (auto y = 0u; y < 20; ++y) {
for (auto x = 0u; x < 20; ++x) {
unsigned int offset{0u};
if (y == 0 || y == 19 || x == 0 || x == 19) {
offset = 1u;
}
add(array, {x, y}, offset, tile_size);
}
}
auto view = window.getDefaultView();
float z{1.f};
z *= zoom(view, 0.160001f); // causes "feature"
while (window.isOpen()) {
sf::Event event;
while (window.pollEvent(event)) {
if (event.type == sf::Event::Closed) {
window.close();
}
}
window.clear(sf::Color::Black);
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Up)) { view.move(0, -1); }
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Down)) { view.move(0, 1); }
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) { view.move(-1, 0); }
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) { view.move(1, 0); }
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Q)) { z *= zoom(view, 1.001f); std::cout << z << "\n"; }
if (sf::Keyboard::isKeyPressed(sf::Keyboard::E)) { z *= zoom(view, 0.999f); std::cout << z << "\n"; }
window.setView(view);
window.draw(array, &tileset);
window.display();
}
}
Inspired by the fact, that google let this problem seem a general one, I thought about that "adding pixels to each tile"-solution and came up with this: let the game add the pixels when loading the tileset. It seems to work perfectly ;D
Also the code for preparing the tile changed. Here's my solution:
void scale(sf::Vector2f& v, sf::Vector2u const & tile_size) {
v.x *= tile_size.x;
v.y *= tile_size.y;
}
void prepare(sf::Vector2f& tl, sf::Vector2f& tr, sf::Vector2f& br, sf::Vector2f& bl, sf::Vector2u const & offset, sf::Vector2u const & tile_size) {
tl = sf::Vector2f(offset.x, offset.y);
tr = sf::Vector2f(offset.x + 1.f, offset.y);
br = sf::Vector2f(offset.x + 1.f, offset.y + 1.f);
bl = sf::Vector2f(offset.x, offset.y + 1.f);
scale(tl, tile_size);
scale(tr, tile_size);
scale(br, tile_size);
scale(bl, tile_size);
}
void add(sf::VertexArray& array, sf::Vector2u const & world_pos, sf::Vector2u const & tile_offset, sf::Vector2u const & tile_size) {
sf::Vertex tl, tr, br, bl; // [t]op[r]ight etc.
// setup positions and texture coords
prepare(tl.position, tr.position, br.position, bl.position, world_pos, tile_size);
prepare(tl.texCoords, tr.texCoords, br.texCoords, bl.texCoords, tile_offset, tile_size);
// fix tex coords to suit the modified atlas
sf::Vector2f delta{1.f, 1.f};
delta.x += tile_offset.x * 2;
delta.y += tile_offset.y * 2;
tl.texCoords += delta;
tr.texCoords += delta;
br.texCoords += delta;
bl.texCoords += delta;
// add tiles
array.append(tl);
array.append(tr);
array.append(br);
array.append(bl);
}
sf::Image rebuildAtlas(sf::Image const & source, sf::Vector2u const & tilesize) {
// determine new size
auto size = source.getSize();
assert(size.x % tilesize.x == 0);
assert(size.y % tilesize.y == 0);
auto num_x = size.x / tilesize.x;
auto num_y = size.y / tilesize.y;
size.x += num_x * 2;
size.y += num_y * 2;
sf::Image atlas;
atlas.create(size.x, size.y);
// create atlas
sf::Vector2u offset;
for (offset.y = 0u; offset.y < num_y; ++offset.y) {
for (offset.x = 0u; offset.x < num_x; ++offset.x) {
// copy frame
auto destX = 1 + offset.x * (tilesize.x + 2);
auto destY = 1 + offset.y * (tilesize.y + 2);
sf::IntRect sourceRect;
sourceRect.left = offset.x * tilesize.x;
sourceRect.top = offset.y * tilesize.y;
sourceRect.width = tilesize.x;
sourceRect.height = tilesize.y;
atlas.copy(source, destX, destY, sourceRect);
// create left border
--destX;
sourceRect.width = 1;
atlas.copy(source, destX, destY, sourceRect);
// create right border
destX += tilesize.x + 1;
sourceRect.left += tilesize.x - 1;
atlas.copy(source, destX, destY, sourceRect);
// create top border (copying from source to source!)
destX -= tilesize.x + 1;
sourceRect.left = destX;
sourceRect.top = destY;
sourceRect.width = tilesize.x + 2;
sourceRect.height = 1;
--destY;
atlas.copy(atlas, destX, destY, sourceRect);
// create bottom border (copying from source to source!)
destY += tilesize.x;
sourceRect.top = destY;
++destY;
atlas.copy(atlas, destX, destY, sourceRect);
}
}
return atlas;
};