Attention: cette page se réfère à une ancienne version de SFML. Cliquez ici pour passer à la dernière version.

Utiliser OpenGL dans une fenêtre SFML

Introduction

Ce tutoriel ne parle pas d'OpenGL, mais plutôt de la manière d'utiliser SFML comme interface à OpenGL, ainsi que la façon de les mélanger.

Comme vous le savez très certainement, l'un des plus importants atouts d'OpenGL est sa portabilité. Mais OpenGL tout seul ne sera pas suffisant pour créer un programme complet : vous aurez également besoin d'une fenêtre, d'un contexte de rendu, des entrées utilisateur, etc. Et pour ça vous n'avez pas d'autre choix que d'écrire du code spécifique à l'OS. C'est là que le module sfml-window intervient ; voyons comment il vous permet de jouer avec OpenGL.

Inclure et lier OpenGL à votre application

Le nom des en-têtes OpenGL est malheureusement différent pour chaque OS. Pas de panique, SFML fournit un en-tête "abstrait" qui s'occupe d'inclure le bon fichier pour vous.

#include <SFML/OpenGL.hpp>

Cet en-tête inclut les fonctions OpenGL et GLU, et rien d'autre. Certains utilisateurs pensent à tort que SFML inclut automatiquement GLEW (une bibliothèque qui gère les extensions OpenGL) parce qu'elle l'utilise en interne, mais ce n'est qu'un détail d'implémentation. Du point de vue de l'utilisateur, GLEW doit être gérée comme n'importe quelle autre bibliothèque.

Vous aurez ensuite besoin de lier votre programme à OpenGL. Contrairement à ce qu'elle fait avec les en-têtes, SFML ne peut pas fournir de moyen unifié de lier OpenGL. Ainsi, vous devrez savoir quelle bibliothèque lier selon l'OS que vous utilisez ("opengl" sous Windows, "GL" sous Linux, etc.). Pareil pour GLU, au cas où vous l'utiliseriez également : "glu32" sous Windows, "GLU" sous Linux, etc.

Les fonctions OpenGL sont préfixées par "gl", les fonctions GLU par "glu". Souvenez-vous en lorsque vous aurez de bêtes erreurs d'édition de liens, cela vous aidera à trouver quelle bibliothèque vous avez oublié de lier.

Créer une fenêtre OpenGL

Comme SFML est construite par dessus OpenGL, ses fenêtres sont déjà prêtes pour vos appels OpenGL, sans effort supplémentaire.

sf::Window window(sf::VideoMode(800, 600), "OpenGL");

// ça marche directement
glEnable(GL_TEXTURE_2D);
...

Si vous pensez que c'est trop automatique, rassurez-vous : le constructeur de sf::Window accepte un paramètre supplémentaire qui vous permet de changer les options du contexte OpenGL de la fenêtre. Ce paramètre est une instance de la structure sf::ContextSettings, qui définit les champs suivants :

sf::ContextSettings settings;
settings.depthBits = 24;
settings.stencilBits = 8;
settings.antialiasingLevel = 4;
settings.majorVersion = 3;
settings.minorVersion = 0;

sf::Window window(sf::VideoMode(800, 600), "OpenGL", sf::Style::Default, settings);

Si l'un (ou plusieurs) de ces paramètres n'est pas supporté par la carte graphique, SFML essaye de trouver la plus proche valeur qui soit valide. Par exemple, si un anti-crénelage x4 n'est pas supporté, SFML va essayer x2 puis en dernier recours se rabattre sur 0.
Dans ce cas de figure, vous pouvez savoir ce que SFML a choisi avec la fonction getSettings :

sf::ContextSettings settings = window.getSettings();

std::cout << "depth bits:" << settings.depthBits << std::endl;
std::cout << "stencil bits:" << settings.stencilBits << std::endl;
std::cout << "antialiasing level:" << settings.antialiasingLevel << std::endl;
std::cout << "version:" << settings.majorVersion << "." << settings.minorVersion << std::endl;

Les versions d'OpenGL supérieures à 3.0 sont supportées par SFML (pour autant que votre driver graphique puisse suivre), mais vous ne pouvez pas jouer avec les flags pour le moment. Cela signifie que vous ne pourrez pas créer de contexte debug ou forward compatible ; en fait SFML crée automatiquement des contextes possédant le flag de compatibilité, car elle utilise encore quelques fonctions qui sont marquées "obsolètes" dans les dernières versions d'OpenGL. Cela devrait être amélioré prochainement, de sorte que les flags de contexte puissent être choisis par l'utilisateur.

Un programme OpenGL typique, à la sauce SFML

Voici à quoi pourrait ressembler un programme OpenGL complet avec SFML :

#include <SFML/Window.hpp>
#include <SFML/OpenGL.hpp>

int main()
{
    // crée la fenêtre
    sf::Window window(sf::VideoMode(800, 600), "OpenGL", sf::Style::Default, sf::ContextSettings(32));
    window.setVerticalSyncEnabled(true);

    // chargement des ressources, initialisation des états OpenGL, ...

    // la boucle principale
    bool running = true;
    while (running)
    {
        // gestion des évènements
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed)
            {
                // on stoppe le programme
                running = false;
            }
            else if (event.type == sf::Event::Resized)
            {
                // on ajuste le viewport lorsque la fenêtre est redimensionnée
                glViewport(0, 0, event.size.width, event.size.height);
            }
        }

        // effacement les tampons de couleur/profondeur
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // dessin...

        // termine la trame courante (en interne, échange les deux tampons de rendu)
        window.display();
    }

    // libération des ressources...

    return 0;
}

Ici nous n'utilisons pas window.isOpen() comme condition pour la boucle principale, car nous avons besoin que la fenêtre reste ouverte jusqu'à la fin du programme, de façon à garder un contexte OpenGL valide pour la dernière itération de la boucle ainsi que le code de libération des ressources à la fin.

N'hésitez pas à jeter un oeil aux exemples "OpenGL" et "Window" du SDK de SFML si vous avez des difficultés, ils sont plus complets et possèdent très probablement des réponses à vos questions.

Gérer plusieurs fenêtres OpenGL

La gestion de plusieurs fenêtres OpenGL n'est pas plus compliquée que la gestion d'une seule, il y a juste certaines choses à savoir.

Les appels OpenGL affectent le contexte actif (et donc la fenêtre active). Ainsi, si vous voulez dessiner sur deux fenêtres différentes au sein du même programme, il vous faudra choisir quelle fenêtre est active avant de dessiner quoique ce soit. Pour ce faire, vous devez utiliser la fonction setActive :

// activation de la première fenêtre
window1.setActive(true);

// dessin sur la première fenêtre...

// activation de la seconde fenêtre
window2.setActive(true);

// dessin sur la seconde fenêtre...

Un seul contexte (fenêtre) peut être actif dans chaque thread, vous n'avez donc pas besoin de désactiver la fenêtre active avant d'en activer une autre, c'est fait implicitement. C'est comme ça que fonctionne OpenGL.

Une autre chose à savoir est que tous les contextes OpenGL créés par SFML partagent leurs ressources. Cela signifie que vous pouvez créer une texture ou un vertex buffer avec n'importe quel contexte actif, et l'utiliser avec n'importe quel autre. Cela signifie aussi que vous n'aurez pas besoin de recharger toutes vos ressources OpenGL si vous recréez votre fenêtre.

OpenGL sans fenêtre

Il est parfois nécessaire d'appeler des fonctions OpenGL alors qu'il n'y a aucune fenêtre, et donc aucun contexte OpenGL. Par exemple, lorsque vous chargez des textures depuis un thread, ou bien simplement avant que la première fenêtre ne soit créée. SFML vous permet de créer des contextes sans fenêtre avec la classe sf::Context. Tout ce que vous avez à faire est de l'instancier pour obtenir un contexte valide.

int main()
{
    sf::Context context;

    // chargement de ressources OpenGL...

    sf::Window window(sf::VideoMode(800, 600), "OpenGL");

    ...

    return 0;
}

Dessiner depuis un thread

Une configuration typique pour un programme multi-threadé est de gérer la fenêtre et ses évènements depuis un thread (le principal), et le rendu depuis un autre. Si vous êtes dans ce cas de figure, il y a une règle importante à garder en tête : vous ne pouvez pas activer un contexte (une fenêtre) s'il est actif dans un autre thread. En pratique, cela signifie que vous devez désactiver votre fenêtre avant de lancer le thread de rendu.

void renderingThread(sf::Window* window)
{
    // activation du contexte de la fenêtre
    window->setActive(true);

    // la boucle de rendu
    while (window->isOpen())
    {
        // dessin...

        // fin de la trame -- ceci est une fonction de rendu (elle a besoin d'activer le contexte)
        window->display();
    }
}

int main()
{
    // création de la fenêtre (souvenez-vous: créer la fenêtre dans le thread principal est plus sûr, du fait des limitations de l'OS)
    sf::Window window(sf::VideoMode(800, 600), "OpenGL");

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

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

    // la boucle d'évènements/logique/etc.
    while (window.isOpen())
    {
        ...
    }

    return 0;
}

Utiliser SFML avec le module graphique de SFML

Pour l'instant ce tutoriel a parlé de l'utilisation d'OpenGL avec sfml-window, ce qui est relativement simple étant donné que c'est le seul but de ce module. Mixer OpenGL avec le module graphique est un tout petit peu plus compliqué : sfml-graphics utilise aussi OpenGL, il faut donc prendre certaines précautions de sorte que les états OpenGL de l'utilisateur et ceux de SFML n'entrent pas en conflit.

Si vous ne connaissez pas encore le module graphique, tout ce que vous avez à savoir c'est que la classe sf::Window est remplacée par sf::RenderWindow, qui hérite de toutes ses fonctions et en ajoute quelques autres qui servent à dessiner des entités SFML.

Le seul moyen d'éviter les conflits entre SFML et vos propres états OpenGL, est de les sauvegarder/restaurer à chaque fois que vous passez d'OpenGL à SFML.

- dessin avec OpenGL

- sauvegarde des états OpenGL

- dessin avec SFML

- restauration des états OpenGL

- dessin avec OpenGL

...

La solution la plus simple est de laisser SFML le faire pour vous, avec les fonctions pushGLStates/popGLStates :

glDraw...

window.pushGLStates();

window.draw(...);

window.popGLStates();

glDraw...

Comme elle ne connaît rien de votre code OpenGL, SFML ne peut pas optimiser ces étapes et en conséquence elle sauvegarde/restaure absolument tous les états et matrices OpenGL. C'est négligeable pour des petits projets, mais cela peut se révéler trop lent sur des programmes conséquents qui requierent des performances maximales. Dans ce cas, vous pouvez gérer vous-même la sauvegarde et la restauration des états OpenGL, avec glPushAttrib/glPopAttrib, glPushMatrix/glPopMatrix, etc.
Vous devrez cependant toujours restaurer les états de SFML avant dessiner avec elle, avec la fonction resetGLStates.

glDraw...

glPush...
window.resetGLStates();

window.draw(...);

glPop...

glDraw...

En gérant vous-même la sauvegarde des états OpenGL, vous pouvez vous occuper uniquement de ceux qui vous intéressent, et épargner quelques appels inutiles.