SFML2.6 graphics module–designing entities with vertex arrays

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 with a sf::PrimitiveType. If you need more flexibility or a static array, you can use your own storage method. Then, you must use an overloaded form of the draw function, which accepts pointers to vertices, vertex numbers, and primitive types.

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;
}