Afficher un triangle

Difficulté Facile
Note
Thématiques
Mis à jour le vendredi 21 juin 2013

Afficher un triangle

Maintenant que le blabla théorique est terminé, nous pouvons reprendre la programmation. :magicien:

Dans le chapitre précédent, nous avons appris que tous les modèles 3D présents dans un jeu étaient constitués de sommets (vertices), puis qu'il fallait les relier pour former une surface. Et si on assemble ces formes, ça donne nos modèles 3D.

Notre premier exercice est simple : afficher un triangle. Pour pouvoir afficher n'importe quel modèle 3D il faut déjà spécifier ses vertices que nous allons ensuite donner à OpenGL. En général, on définie nos vertices dans un seul tableau, on place chaque coordonnée de vertex à la chaine comme ceci :

float vertices = {X_vertex_1, Y_vertex_1, Z_vertex_1,   X_vertex_2, Y_vertex_2, Z_vertex_2,   ...};

Dans un triangle il y a trois sommets, nous aurons donc 3 vertices.

float vertex1[] = {-0.5, -0.5};
float vertex2[] = {0.0, 0.5};
float vertex3[] = {0.5, -0.5};

N'oubliez pas que nous sommes en 2D pour l'instant, nous n'avons donc que les coordonnées x et y à définir. Ce début de code est bien mais si on utilise 3 tableaux, ça ne fonctionnera pas. Nous devons combiner les trois tableaux pour en former un seul :

float vertices[] = {-0.5, -0.5,   0.0, 0.5,   0.5, -0.5};

Maintenant, il faut envoyer ces coordonnées à OpenGL. Pour envoyer des informations nous avons besoin d'un tableau appelé "Vertex Attribut". Pour ceux qui connaissent les "Vertex Arrays", c'est la même chose ;) . Sauf que maintenant ce n'est plus une optimisation mais belle et bien la méthode de base pour envoyer nos infos à OpenGL.

Ces "Vertex Attributs" sont déjà inclus dans l'API, il suffit de lui donner des valeurs (nos coordonnées) puis de l'activer. Voici le prototype de la fonction gérant les Vertex Attributs :

void glVertexAttribPointer(GLuint index, GLuint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer);
  • index : c'est le numéro du tableau, son identifiant.

  • size : c'est le nombre de coordonnées par vertex. En 2D ce sera 2 et en 3D ce sera 3.

  • type : c'est le type de données (float, int, ...).

  • normalized : booléen permettant à OpenGL de normaliser les données (comme les vecteurs).

  • stride : paramètre spécial, on le mettra à zéro tout le temps, nous ne l'utiliserons pas.

  • pointer : c'est le pointeur sur nos données, ici nos coordonnées.

Voyons ce que ça donne avec les données de notre triangle :

glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices);

Malheureusement, en l'état, notre tableau est inutilisable, il faut d'abord l'activer avec la fonction :

void glEnableVertexAttribArray(GLuint index);

Le paramètre index étant notre identifiant de tableau, ici ce sera 0.

Bien, maintenant OpenGL sait quels vertices il doit afficher, il ne manque plus qu'à lui dire ... ce qu'il doit faire de ces points. :p

Pour ça, on utilise une fonction (il y en a en fait deux, mais pour le début de ce tutoriel nous n'utiliserons que la première). Cette fonction permet de dire à OpenGL quelle forme afficher avec les points donnés précédemment. Voici le prototype de la fonction :

glDrawArrays(GLenum mode, GLint first, GLsizei count);
  • mode : C'est la forme finale (nous verrons cela juste après).

  • first : C'est l'indice de notre premier vertex à afficher(nous lui donnerons un int).

  • count : C'est le nombre de vertices à afficher depuis first (nous lui donnerons également un int).

Alors Attention ! :

  • Le paramètre first est un indice (comme un indice de tableau), si vous voulez utiliser le premier vertex vous lui donnerez la valeur "0" et pas "1".

  • Pour count, c'est le contraire. Lui il veut le nombre de vertices à utiliser depuis first. Si vous utilisez 4 vertices vous lui donnerez la valeur "4" et pas "3".

Exemple : J'ai 4 vertices et je veux afficher un carré. Le premier vertex sera le vertex 0 (le premier vertex), et la valeur de "count" sera 4 car j'ai besoin du vertex 0 + les 3 vertices qui le suivent.

Passons au paramètre le plus intéressant : mode.

Ce paramètre peut prendre plusieurs formes donc voici les principales :

Valeur

Définition

GL_TRIANGLES

Avec ce mode, chaque groupe de 3 vertices formera un triangle. C'est le mode le plus utilisé.

GL_POLYGON

Tous les vertices s'assemblent pour former un polygone de type convexe (Hexagone, Heptagone, ...).

GL_LINES

Chaque duo de vertices formera une ligne.

GL_TRIANGLE_STRIP

Ici les triangles s'assembleront. Les deux derniers vertices d'un triangle s'assembleront avec un 4ième vertex pour former un nouveau triangle.

Toutes les valeurs possibles ne sont pas représentées mais je vous expose ici les plus utilisées. ;)

Revenons à notre code, nous avons désormais les différents modes d'affichage. Le but de ce chapitre est d'afficher un triangle, notre mode d'affichage sera donc : GL_TRIANGLES, ce qui donne :

glDrawArrays(GL_TRIANGLES, 0, 3);

La valeur "0" pour commencer l'affichage par le premier vertex, et le "3" pour utiliser les 3 vertices dont le premier sera le vertex "0".

Récapitulons tout ça :

// Vertices et coordonnées

float vertices[] = {-0.5, -0.5,   0.0, 0.5,   0.5, -0.5};


// Boucle principale

while(!terminer)
{
    // Gestion des évènements

    ....


    // On remplie puis on active le tableau Vertex Attrib 0

    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices);
    glEnableVertexAttribArray(0);


    // On affiche le triangle

    glDrawArrays(GL_TRIANGLES, 0, 3);


    // On désactive le tableau Vertex Attrib puisque l'on en a plus besoin

    glDisableVertexAttribArray(0);


    // Actualisation de la fenêtre

    ....
}

Vous avez dû remarquer la présence d'une nouvelle fonction : glDisableVertexAttribArray. Elle permet de désactiver le tableau Vertex Attrib utilisé, le paramètre est le même que celui de la fonction glEnableVertexAttribArray. On désactive le tableau juste après l'affichage des vertices.

Récapitulons tout avec le code SDL :

#ifdef WIN32
#include <GL/glew.h>

#else
#define GL3_PROTOTYPES 1
#include <GL3/gl3.h>

#endif

#include <SDL2/SDL.h>
#include <iostream>


int main(int argc, char **argv)
{	
    // Notre fenêtre
	
    SDL_Window* fenetre(0);
    SDL_GLContext contexteOpenGL(0);
	
    SDL_Event evenements;
    bool terminer(false);
	
	
    // Initialisation de la SDL
	
    if(SDL_Init(SDL_INIT_VIDEO) < 0)
    {
        std::cout << "Erreur lors de l'initialisation de la SDL : " << SDL_GetError() << std::endl;
        SDL_Quit();
		
        return -1;
    }
	
	
    // Version d'OpenGL
	
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
	
	
    // Double Buffer
	
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
	
	
    // Création de la fenêtre

    fenetre = SDL_CreateWindow("Test SDL 2.0", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600, SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL);

    if(fenetre == 0)
    {
        std::cout << "Erreur lors de la creation de la fenetre : " << SDL_GetError() << std::endl;
        SDL_Quit();

        return -1;
    }


    // Création du contexte OpenGL

    contexteOpenGL = SDL_GL_CreateContext(fenetre);

    if(contexteOpenGL == 0)
    {
        std::cout << SDL_GetError() << std::endl;
        SDL_DestroyWindow(fenetre);
        SDL_Quit();

        return -1;
    }


    #ifdef WIN32

        // On initialise GLEW

        GLenum initialisationGLEW( glewInit() );


        // Si l'initialisation a échouée :

        if(initialisationGLEW != GLEW_OK)
        {
            // On affiche l'erreur grâce à la fonction : glewGetErrorString(GLenum code)

            std::cout << "Erreur d'initialisation de GLEW : " << glewGetErrorString(initialisationGLEW) << std::endl;


            // On quitte la SDL

            SDL_GL_DeleteContext(contexteOpenGL);
            SDL_DestroyWindow(fenetre);
            SDL_Quit();

            return -1;
        }

    #endif


    // Vertices et coordonnées

    float vertices[] = {-0.5, -0.5,   0.0, 0.5,   0.5, -0.5};


    // Boucle principale

    while(!terminer)
    {
        // Gestion des évènements

        SDL_WaitEvent(&evenements);

        if(evenements.window.event == SDL_WINDOWEVENT_CLOSE)
            terminer = true;


        // Nettoyage de l'écran

        glClear(GL_COLOR_BUFFER_BIT);


        // On remplie puis on active le tableau Vertex Attrib 0

        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices);
        glEnableVertexAttribArray(0);


        // On affiche le triangle

        glDrawArrays(GL_TRIANGLES, 0, 3);


        // On désactive le tableau Vertex Attrib puisque l'on n'en a plus besoin

        glDisableVertexAttribArray(0);


        // Actualisation de la fenêtre

        SDL_GL_SwapWindow(fenetre);
    }


    // On quitte la SDL

    SDL_GL_DeleteContext(contexteOpenGL);
    SDL_DestroyWindow(fenetre);
    SDL_Quit();

    return 0;
}

Si tout se passe bien, vous devriez avoir une belle fenêtre comme celle-ci :

Image utilisateur

Afficher plusieurs triangles

Les vertices

Comme nous l'avons vu dans le tableau précédemment, le paramètre GL_TRIANGLES dans la fonction glDrawArrays() permet d'afficher un triangle pour chaque triplet de vertices.
Pour le moment, nous n'utilisons que 3 vertices, donc nous n'avons au final qu'un seul triangle. Or si nous en utilisons 6 nous aurons alors deux triangles.
On peut même aller plus loin et prendre 60, 390, ou 3000 vertices pour en afficher plein ! C'est d'ailleurs ce que font les décors et les personnages dans les jeux-vidéo, ils ne savent utiliser que ça. :p

Enfin, prenons un exemple plus simple avant d'aller aussi loin. Si nous voulons afficher le rendu suivant ... :

Image utilisateur

... Nous devrons utiliser deux triplets de vertices.

Ce qu'il faut savoir, c'est qu'il ne faut surtout pas utiliser un tableau pour chaque triangle. On perdrait beaucoup trop de temps à tous les envoyer. A la place, nous devons inclure tous les vertices dans un seul et unique tableau :

// Vertices

float vertices[] = {0.0, 0.0,   0.5, 0.0,   0.0, 0.5,          // Triangle 1
                    -0.8, -0.8,   -0.3, -0.8,   -0.8, -0.3};   // Triangle 2

Toutes nos données sont regroupées dans un seul tableau. Ça ne nous fait qu'un seul envoi à faire c'est plus facile à gérer, ça arrange même OpenGL. ;)

L'affichage

Au niveau de l'affichage, le code reste identique à celui du triangle unique. C'est-à-dire qu'il faut utiliser les fonctions :

  • glVertexAttribPointer() : pour donner les vertices à OpenGL

  • glEnableVertexAttribArray() : pour activer le tableau Vertex Attrib

  • glDrawArrays() : pour afficher le tout

La seule différence notable va être la valeur du paramètre count (nombre de vertices à prendre en compte) de la fonction glDrawArrays(). Celui-ci était égal à 3 pour un seul triangle, nous la passerons désormais à 6 pour en afficher deux. L'appel à la fonction ressemblerait donc à ceci :

// Affichage des triangles

glDrawArrays(GL_TRIANGLES, 0, 6);

Ce qui donne le code source de la boucle principale suivant :

// Vertices

float vertices[] = {0.0, 0.0,   0.5, 0.0,   0.0, 0.5,          // Triangle 1
                    -0.8, -0.8,   -0.3, -0.8,   -0.8, -0.3};   // Triangle 2


// Boucle principale

while(!terminer)
{
    // Gestion des évènements

    SDL_WaitEvent(&evenements);

    if(evenements.window.event == SDL_WINDOWEVENT_CLOSE)
        terminer = true;


    // Nettoyage de l'écran

    glClear(GL_COLOR_BUFFER_BIT);


    // On remplie puis on active le tableau Vertex Attrib 0

    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices);
    glEnableVertexAttribArray(0);


    // On affiche des triangles

    glDrawArrays(GL_TRIANGLES, 0, 6);


    // On désactive le tableau Vertex Attrib puisque l'on n'en a plus besoin

    glDisableVertexAttribArray(0);


    // Actualisation de la fenêtre

    SDL_GL_SwapWindow(fenetre);
}

Si vous compilez ce code, vous devriez obtenir :

Image utilisateur

Le tour est joué. :)

L'auteur