Dessiner avec SFML

Introduction

Comme vous l'avez vu dans les tutoriels précédents, le module de fenêtrage de SFML fournit tout ce qu'il faut pour créer une fenêtre OpenGL et gérer ses évènements, mais n'est d'aucune aide pour dessiner quoique ce soit. La seule option qu'il vous offre est d'utiliser OpenGL, qui est certes très puissante mais tout aussi complexe.

Heureusement, SFML fournit un module graphique avec plein d'entités 2D, beaucoup plus simples à manipuler qu'OpenGL.

La fenêtre de dessin

Afin de dessiner les entités fournies par le module graphique, vous devez utiliser une classe de fenêtre spécialisée : sf::RenderWindow. Celle-ci dérive de sf::Window et hérite de toutes ses fonctions. Tout ce que vous avez appris à propos de sf::Window (création, gestion des évènements, contrôle du rafraîchissement, mélange avec OpenGL, etc.) est toujours valable avec sf::RenderWindow.

Par dessus cela, sf::RenderWindow ajoute des fonctions de plus haut-niveau pour vous aider à dessiner plus facilement. Ce tutoriel se concentre sur deux de ces fonctions : clear et draw. Elles sont aussi simples que leur nom le suggère : clear efface la fenêtre toute entière avec la couleur choisie, et draw y dessine l'objet que vous lui passez en paramètre.

Voici une boucle principale typique avec une fenêtre de dessin :

#include <SFML/Graphics.hpp>

int main()
{
    // création de la fenêtre
    sf::RenderWindow window(sf::VideoMode(800, 600), "My window");

    // on fait tourner le programme tant que la fenêtre n'a pas été fermée
    while (window.isOpen())
    {
        // on traite tous les évènements de la fenêtre qui ont été générés depuis la dernière itération de la boucle
        sf::Event event;
        while (window.pollEvent(event))
        {
            // fermeture de la fenêtre lorsque l'utilisateur le souhaite
            if (event.type == sf::Event::Closed)
                window.close();
        }

        // effacement de la fenêtre en noir
        window.clear(sf::Color::Black);

        // c'est ici qu'on dessine tout
        // window.draw(...);

        // fin de la frame courante, affichage de tout ce qu'on a dessiné
        window.display();
    }

    return 0;
}

Appeler clear avant de dessiner quoique ce soit est obligatoire, sinon vous pourriez vous retrouver avec des pixels aléatoires ou bien de la frame précédente. La seule exception est le cas où vous couvrez la totalité de la fenêtre avec ce que vous dessinez, de sorte que tous les pixels soient écrasés ; dans ce cas vous pouvez ne pas appeler clear (bien que cela ne fasse pas de grande différence au niveau des performances).

Appeler display est tout aussi obligatoire, cela a pour effet d'afficher dans la fenêtre tout ce qui a été dessiné depuis l'appel précédent à display. En effet, les entités ne sont pas dessinées directement dans la fenêtre, mais plutôt dans une surface cachée. Cette surface est ensuite copiée vers la fenêtre lors de l'appel à display -- c'est ce qu'on appelle le double buffering.

Ce cycle clear/draw/display est la seule bonne manière de dessiner. N'essayez pas d'autres stratégies, telles que garder certains pixels de la frame précédente, "effacer" des pixels, ou bien encore dessiner une seule fois et appeler display plusieurs fois. Vous obtiendrez des résultats bizarres à cause du double buffering.
Les puces et les APIs graphiques modernes sont vraiment faites pour des cycles clear/draw/display répétés, où tout est complètement rafraîchi à chaque itération de la boucle de dessin. Ne soyez pas effrayés de dessiner 1000 sprites 60 fois par seconde, vous êtes encore loin des millions de triangles que votre machine peut gérer.

Qu'est-ce que je peux dessiner maintenant ?

Maintenant que vous avez une boucle principale qui est prête à dessiner, voyons ce que vous pouvez y dessiner, et de quelle manière.

SFML fournit quatre types d'entités dessinables : trois d'entre elles sont prêtes à l'emploi (sprites, textes et formes), la dernière est la brique de base qui vous aidera à créer vos propres entités dessinables (les tableaux de vertex).

Bien que ces entités partagent pas mal d'attributs communs, elles ont chacune leurs spécificités et méritent leur propre tutoriel :

Dessin hors-écran

SFML fournit aussi un moyen de dessiner sur une texture plutôt que directement sur la fenêtre. Pour ce faire, il faut utiliser la classe sf::RenderTexture au lieu de sf::RenderWindow. Elle possède les mêmes fonctions de dessin, héritées de leur base commune sf::RenderTarget.

// on crée une texture de dessin de 500x500
sf::RenderTexture renderTexture;
if (!renderTexture.create(500, 500))
{
    // erreur...
}

// pour dessiner, on utilise les mêmes fonctions
renderTexture.clear();
renderTexture.draw(sprite); // ou n'importe quel autre objet dessinable
renderTexture.display();

// on récupère la texture (sur laquelle on vient de dessiner)
const sf::Texture& texture = renderTexture.getTexture();

// on la dessine dans la fenêtre
sf::Sprite sprite(texture);
window.draw(sprite);

La fonction getTexture renvoie une texture en lecture seule, ce qui signifie que vous ne pouvez que l'utiliser, pas la modifier. Si vous avez besoin de la manipuler avant de l'utiliser, vous pouvez la copier dans votre propre instance de sf::Texture.

sf::RenderTexture déclare les mêmes fonctions que sf::RenderWindow pour gérer les vues et OpenGL (voir les tutoriels correspondant pour plus de détails). Si vous utilisez OpenGL pour dessiner sur une texture, vous pouvez activer un tampon de profondeur (depth buffer) en utilisant le troisième paramètre optionnel de la fonction create.

renderTexture.create(500, 500, true); // activation du tampon de profondeur

Dessiner depuis un thread

SFML supporte le rendu multi-threadé, et vous n'avez même pas besoin de faire quoique ce soit pour que cela fonctionne. La seule chose à se rappeler est de désactiver une fenêtre avant de l'utiliser dans un autre thread ; une fenêtre (plus précisément son contexte OpenGL) ne peut en effet pas être active dans plusieurs threads en même temps.

void renderingThread(sf::RenderWindow* window)
{
    // activation du contexte de la fenêtre
    window->setActive(true);
    
    // la boucle de rendu
    while (window->isOpen())
    {
        // dessin...

        // fin de la frame
        window->display();
    }
}

int main()
{
    // création de la fenêtre
    // (rappelez-vous : il est plus prudent de le faire dans le thread principal à cause des limitations de l'OS)
    sf::RenderWindow window(sf::VideoMode(800, 600), "OpenGL");

    // désactivation de son contexte OpenGL
    window.setActive(false);

    // lancement du thread de dessin
    sf::Thread thread(&renderingThread, &window);
    thread.launch();

    // la boucle d'évènements/logique/ce que vous voulez...
    while (window.isOpen())
    {
        ...
    }

    return 0;
}

Comme vous pouvez le voir, vous n'avez même pas besoin d'activer la fenêtre dans le thread de dessin, SFML le fait automatiquement pour vous dès que nécessaire.

Souvenez-vous : il faut toujours créer la fenêtre et gérer ses évènements dans le thread principal, pour un maximum de portabilité, comme expliqué dans le tutoriel sur les fenêtres.