Introduction
SFML provides simple classes to represent the most common 2D entities. While more complex entities can be easily created from these basic components, this is not always the most efficient solution. For example, if you draw a large number of sprites, you will quickly reach the limits of your graphics card. The reason is that performance depends heavily on the number of times the draw function is called. Indeed, each call involves setting a set of OpenGL states, resetting matrices, changing textures, etc. All of this is required even if only two triangles (sprites) are drawn. This is far from optimal for your graphics card: today’s GPUs are designed to handle large numbers of triangles, typically thousands to millions.
To fill this gap, SFML provides a lower-level mechanism for drawing things: vertex arrays. In fact, vertex arrays are used internally in all other SFML classes. They allow more flexibility in defining 2D solids, containing any number of triangles you need. They even allow drawing points or lines.
What are vertices and why are they always in arrays?
Vertices are the smallest graphical entities you can manipulate. In short, it’s a graphics point: it naturally has a 2D position (x,y), but also a color and a pair of texture coordinates. We’ll discuss the role of these properties later.
Vertices alone can’t do much. They are always combined into primitives: points (1 vertex), lines (2 vertices), triangles (3 vertices) or quadrilaterals (4 vertices). You can then combine multiple primitives together to create the final geometry of the solid.
Now you understand why we always talk about arrays of vertices, not individual vertices.
A simple vertex array
Now let’s look at the sf::Vertex class. It is just a container with three public members and no functions except the constructor. These constructors allow you to construct vertices from the set of properties you care about – you don’t always need to shade or texture entities.
// create a new vertex sf::Vertex vertex; // set its position vertex.position = sf::Vector2f(10.f, 50.f); // set its color vertex.color = sf::Color::Red; // set its texture coordinates vertex.texCoords = sf::Vector2f(100.f, 100.f);
…or, use the correct constructor:
sf::Vertex vertex(sf::Vector2f(10.f, 50.f), sf::Color::Red, sf::Vector2f(100.f, 100.f));
Now, let’s define a primitive. Remember that a primitive consists of multiple vertices, so we need an array of vertices. SFML provides a simple wrapper for this: sf::VertexArray. It provides array semantics similar to std::vector and also stores the primitive type for which its vertices are defined.
// create an array of 3 vertices that define a triangle primitive sf::VertexArray triangle(sf::Triangles, 3); // define the position of the triangle's points triangle[0].position = sf::Vector2f(10.f, 10.f); triangle[1].position = sf::Vector2f(100.f, 10.f); triangle[2].position = sf::Vector2f(100.f, 100.f); // define the color of the triangle's points triangle[0].color = sf::Color::Red; triangle[1].color = sf::Color::Blue; triangle[2].color = sf::Color::Green; // no texture coordinates here, we'll see that later
Your triangle is ready, now you can start drawing it. Drawing a vertex array is similar to drawing any other SFML entity, using the draw function:
window.draw(triangle);
As you can see, the colors of the vertices are interpolated to fill the primitives. This is a great way to create gradients.
Note that you don’t have to use the sf::VertexArray class, it’s just defined for convenience, it’s essentially just a std::vector
std::vector<sf::Vertex> vertices; vertices.push_back(sf::Vertex(...)); ... window.draw( & amp;vertices[0], vertices.size(), sf::Triangles);
sf::Vertex vertices[2] = {<!-- --> sf::Vertex(...), sf::Vertex(...) }; window.draw(vertices, 2, sf::Lines);
Character type
Let’s pause for a moment and see what types of basic geometry you can create. As mentioned above, you can define the most basic 2D primitives: points, lines, triangles, and quadrilaterals (the quadrilateral is just for convenience, the internal graphics card will split it into two triangles). There are also “chained” variants, which allow vertices to be shared between two consecutive primitives. The advantage of this is that consecutive primitives are usually connected to each other in some way.
Let’s take a look at the full list:
Character type | Description | Example |
---|---|---|
sf::Points | A set of disconnected points. These points have no thickness: they always occupy only one pixel, regardless of the current transformation and view. | |
sf ::Lines | A group of disconnected lines. These lines have no thickness: they are always only one pixel wide, regardless of the current transformation and view. | |
sf ::LineStrip | A set of connected lines. The end vertex of one segment is used as the starting vertex of the next segment. | |
sf ::Triangles | A set of disconnected triangles. | |
sf ::TriangleStrip | A set of connected triangles. Each triangle shares its last two vertices with the next triangle. | |
sf ::TriangleFan | A set of triangles connected to a center point. The first vertex is the center, then each new vertex defines a new triangle using the center and the previous vertex. | |
sf ::Quads (deprecated) | A set of unconnected quadrilaterals. The 4 points of each quad must be defined consistently, either in clockwise or counterclockwise order. |
Texture
Like other SFML entities, vertex arrays can be texture mapped. To do this, you need to manipulate the vertex’s texCoords attribute. This property defines which texel is mapped to the vertex.
// create a triangle strip sf::VertexArray triangleStrip(sf::TriangleStrip, 4); // define it as a rectangle, located at (10, 10) and with size 100x100 triangleStrip[0].position = sf::Vector2f(10.f, 10.f); triangleStrip[1].position = sf::Vector2f(10.f, 110.f); triangleStrip[2].position = sf::Vector2f(110.f, 10.f); triangleStrip[3].position = sf::Vector2f(110.f, 110.f); // define its texture area to be a 25x50 rectangle starting at (0, 0) triangleStrip[0].texCoords = sf::Vector2f(0.f, 0.f); triangleStrip[1].texCoords = sf::Vector2f(0.f, 50.f); triangleStrip[2].texCoords = sf::Vector2f(25.f, 0.f); triangleStrip[3].texCoords = sf::Vector2f(25.f, 50.f);
Texture coordinates are defined in pixels (just like textureRect for sprites and shapes). They are not normalized (between 0 and 1), which may surprise people used to OpenGL programming.
Vertex arrays are low-level entities that only handle geometric data and do not store other properties like textures. To draw a vertex array using a texture, it must be passed directly to the draw function:
sf::VertexArray vertices; sf::Texture texture; ... window.draw(vertices, & amp;texture);
Here’s the short version, if you need to pass additional render states (such as blend modes or transforms) you can use the explicit version, which accepts an sf::RenderStates object:
sf::VertexArray vertices; sf::Texture texture; ... sf::RenderStates states; states.texture = & amp;texture; window.draw(vertices, states);
Transform vertex array
Transforming is similar to textures in that the transformation information is not stored in the vertex array, you must pass it to the drawing function. When using the sf::Transform
class for transformation, it affects the entire vertex array and does not modify the original vertex data. Therefore, when drawing, you need to pass the transformation information to the drawing function through the sf::RenderStates
object to implement the transformation of the vertex array.
sf::VertexArray vertices; sf::Transform transform; ... window.draw(vertices, transform);
Or, if you need to pass other rendering state:
sf::VertexArray vertices; sf::Transform transform; ... sf::RenderStates states; states.transform = transform; window.draw(vertices, states);
If you want to learn more about transforms and the sf::Transform class, you can read the tutorial on transforming entities.
Create an SFML-like entity
Now that you know how to define your own texture/shading/transform entities, wouldn’t it be nice to wrap it in a class? Fortunately, SFML makes this easy by providing you with the sf::Drawable and sf::Transformable base classes. These two classes are the basis for the built-in SFML entities sf::Sprite, sf::Text, and sf::Shape.
sf::Drawable is an interface: it declares a pure virtual function with no members or concrete functions. Inheriting sf::Drawable allows you to draw instances of your class in the same way as SFML classes:
class MyEntity : public sf::Drawable {<!-- --> private: virtual void draw(sf::RenderTarget & amp; target, sf::RenderStates states) const; }; MyEntity entity; window.draw(entity); // internally calls entity.draw
Note that it is not mandatory to do this, you can also have a similar draw function in your class and simply call it using entity.draw(window). However, it is better and more consistent to use sf::Drawable as the base class. This also means that if you plan to store a set of drawables, you can do so without any extra effort since all drawables (SFML’s and yours) are derived from the same class.
Another base class, sf::Transformable, has no virtual functions. Inheriting from it will automatically add the same transformation functions to your class as other SFML classes (setPosition, setRotation, move, scale, etc.). You can learn more about this class in the tutorial on converting entities.
Using these two base classes and a vertex array (in this example, we’ll also add a texture), this is what a typical SFML-like graphics class would look like:
class MyEntity : public sf::Drawable, public sf::Transformable {<!-- --> public: // add functions to play with the entity's geometry / colors / texturing... private: virtual void draw(sf::RenderTarget & amp; target, sf::RenderStates states) const {<!-- --> // apply the entity's transform -- combine it with the one that was passed by the caller states.transform *= getTransform(); // getTransform() is defined by sf::Transformable // apply the texture states.texture = & amp;m_texture; // you may also override states.shader or states.blendMode if you want // draw the vertex array target.draw(m_vertices, states); } sf::VertexArray m_vertices; sf::Texture m_texture; };
You can then use this class just like the built-in SFML class:
MyEntity entity; // you can transform it entity.setPosition(10.f, 50.f); entity.setRotation(45.f); // you can draw it window.draw(entity);
Example: Tile Map
With what we have learned above, we can create a class that encapsulates the tile map. The entire map will be contained in a vertex array, so it will draw very quickly. Note that we can only apply this strategy if the entire collection of tiles can fit into one texture. Otherwise, we need to use at least one texture corresponding to one vertex array.
Here is the declaration for the tile map class:
class TileMap : public sf::Drawable, public sf::Transformable {<!-- --> public: bool load(const std::string & tileset, sf::Vector2u tileSize, const int* tiles, unsigned int width, unsigned int height) {<!-- --> // load the tileset texture if (!m_tileset.loadFromFile(tileset)) return false; // resize the vertex array to fit the level size m_vertices.setPrimitiveType(sf::Triangles); m_vertices.resize(width * height * 6); //populate the vertex array, with two triangles per tile for (unsigned int i = 0; i < width; + + i) for (unsigned int j = 0; j < height; + + j) {<!-- --> // get the current tile number int tileNumber = tiles[i + j * width]; // find its position in the tileset texture int tu = tileNumber % (m_tileset.getSize().x / tileSize.x); int tv = tileNumber / (m_tileset.getSize().x / tileSize.x); // get a pointer to the triangles' vertices of the current tile sf::Vertex* triangles = & amp;m_vertices[(i + j * width) * 6]; // define the 6 corners of the two triangles triangles[0].position = sf::Vector2f(i * tileSize.x, j * tileSize.y); triangles[1].position = sf::Vector2f((i + 1) * tileSize.x, j * tileSize.y); triangles[2].position = sf::Vector2f(i * tileSize.x, (j + 1) * tileSize.y); triangles[3].position = sf::Vector2f(i * tileSize.x, (j + 1) * tileSize.y); triangles[4].position = sf::Vector2f((i + 1) * tileSize.x, j * tileSize.y); triangles[5].position = sf::Vector2f((i + 1) * tileSize.x, (j + 1) * tileSize.y); // define the 6 matching texture coordinates triangles[0].texCoords = sf::Vector2f(tu * tileSize.x, tv * tileSize.y); triangles[1].texCoords = sf::Vector2f((tu + 1) * tileSize.x, tv * tileSize.y); triangles[2].texCoords = sf::Vector2f(tu * tileSize.x, (tv + 1) * tileSize.y); triangles[3].texCoords = sf::Vector2f(tu * tileSize.x, (tv + 1) * tileSize.y); triangles[4].texCoords = sf::Vector2f((tu + 1) * tileSize.x, tv * tileSize.y); triangles[5].texCoords = sf::Vector2f((tu + 1) * tileSize.x, (tv + 1) * tileSize.y); } return true; } private: virtual void draw(sf::RenderTarget & amp; target, sf::RenderStates states) const {<!-- --> // apply the transform states.transform *= getTransform(); // apply the tileset texture states.texture = &m_tileset; // draw the vertex array target.draw(m_vertices, states); } sf::VertexArray m_vertices; sf::Texture m_tileset; };
Now let’s look at an application that uses this class:
int main() {<!-- --> // create the window sf::RenderWindow window(sf::VideoMode(512, 256), "Tilemap"); // define the level with an array of tile indices const int level[] = {<!-- --> 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 0, 1, 0, 0, 2, 0, 3, 3, 3, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2, 0, 0, 0, 0, 1, 0, 3, 0, 2, 2, 0, 0, 1, 1, 1, 1, 2, 0, 2, 0, 1, 0, 3, 0, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 3, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1, }; // create the tilemap from the level definition TileMap map; if (!map.load("tileset.png", sf::Vector2u(32, 32), level, 16, 8)) return -1; // run the main loop while (window.isOpen()) {<!-- --> // handle events sf::Event event; while (window.pollEvent(event)) {<!-- --> if(event.type == sf::Event::Closed) window.close(); } // draw the map window.clear(); window.draw(map); window.display(); } return 0; }
You can download the tile set used for this tilemap example here.
Example: Particle System
This second example implements another common entity: a particle system. This one is very simple, no textures, and as few parameters as possible. It demonstrates the use of the sf::Points primitive type with dynamic vertex arrays that change in every frame.
class ParticleSystem : public sf::Drawable, public sf::Transformable {<!-- --> public: ParticleSystem(unsigned int count): m_particles(count), m_vertices(sf::Points, count), m_lifetime(sf::seconds(3.f)), m_emitter(0.f, 0.f) {<!-- --> } void setEmitter(sf::Vector2f position) {<!-- --> m_emitter = position; } void update(sf::Time elapsed) {<!-- --> for (std::size_t i = 0; i < m_particles.size(); + + i) {<!-- --> // update the particle lifetime Particle & p = m_particles[i]; p.lifetime -= elapsed; // if the particle is dead, respawn it if (p.lifetime <= sf::Time::Zero) resetParticle(i); // update the position of the corresponding vertex m_vertices[i].position + = p.velocity * elapsed.asSeconds(); // update the alpha (transparency) of the particle according to its lifetime float ratio = p.lifetime.asSeconds() / m_lifetime.asSeconds(); m_vertices[i].color.a = static_cast<sf::Uint8>(ratio * 255); } } private: virtual void draw(sf::RenderTarget & amp; target, sf::RenderStates states) const {<!-- --> // apply the transform states.transform *= getTransform(); // our particles don't use a texture states.texture = NULL; // draw the vertex array target.draw(m_vertices, states); } private: struct particles {<!-- --> sf::Vector2f velocity; sf::Time lifetime; }; void resetParticle(std::size_t index) {<!-- --> // give a random velocity and lifetime to the particle float angle = (std::rand() % 360) * 3.14f / 180.f; float speed = (std::rand() % 50) + 50.f; m_particles[index].velocity = sf::Vector2f(std::cos(angle) * speed, std::sin(angle) * speed); m_particles[index].lifetime = sf::milliseconds((std::rand() % 2000) + 1000); // reset the position of the corresponding vertex m_vertices[index].position = m_emitter; } std::vector<Particle> m_particles; sf::VertexArray m_vertices; sf::Time m_lifetime; sf::Vector2f m_emitter; };
There is also a small demo using it:
int main() {<!-- --> // create the window sf::RenderWindow window(sf::VideoMode(512, 256), "Particles"); // create the particle system ParticleSystem particles(1000); // create a clock to track the elapsed time sf::Clock clock; // run the main loop while (window.isOpen()) {<!-- --> // handle events sf::Event event; while (window.pollEvent(event)) {<!-- --> if(event.type == sf::Event::Closed) window.close(); } // make the particle system emitter follow the mouse sf::Vector2i mouse = sf::Mouse::getPosition(window); particles.setEmitter(window.mapPixelToCoords(mouse)); // update it sf::Time elapsed = clock.restart(); particles.update(elapsed); // draw it window.clear(); window.draw(particles); window.display(); } return 0; }