• {0} Facile|{1} Moyenne|{2} Difficile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 13/03/2017

Les textures

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Après le chapitre quelque peu compliqué sur les piles de matrices, nous allons passer aujourd'hui à un chapitre beaucoup plus concret (et plus intéressant :p ) qui concerne le Texturing avec OpenGL. Avec ceci, nous pourrons rendre notre scène 3D plus réaliste. Nous verrons comment charger des textures stockées sur votre disque dur (ou SSD) et comment les appliquer à une surface 3D.

Introduction

Introduction

Avant de commencer ce chapitre très intéressant, on va définir ensemble un mot que vous avez déjà probablement entendu quelque part : le mot Texture.

Une texture est simplement une sorte de papier peint que l'on va "coller" sur les modèles (2D ou 3D). Dans un jeu vidéo, toutes les surfaces que vous voyez (sols, herbe, murs, personnages, ...) sont constituées de simples images collées sur des formes géométriques. Pour un mur par exemple, il suffit de créer une surface carrée puis de coller une image représentant des briques.

Image utilisateur

L'objectif de ce chapitre sera d'apprendre à créer une texture OpenGL de A à Z. Pour ceux qui auraient suivi le cours de M@téo sur la SDL, vous devriez déjà avoir une petite idée sur la façon de charger les images. Vous vous apercevrez cependant que le chargement de texture est assez différent avec OpenGL. Mais il permet de faire des choses beaucoup plus avancées.

L'une des premières difficultés va concerner la taille des textures à afficher. En effet, OpenGL ne sait gérer que les textures dont les dimensions sont des puissances de 2 (64 pixels, 128, 256, 512, ...).

Alors bon, ce n'est pas une obligation car OpenGL redimensionne de toute façon les images par lui-même mais si vous ne voulez pas vous retrouver avec une texture déformée, il vaut mieux prendre l'habitude des dimensions en puissance de 2. Le mieux est encore de modifier la taille de vos images avec des logiciels spécialisés. ;)

Avant d'aller plus loin, je vais vous demander de télécharger la librairie SDL_image (ou plutôt SDL2_image) qui nous permettra de réaliser la première étape dans la création de nos textures. Elle va nous faire gagner du temps en chargeant tous les bits d'une image en mémoire. Remarquez une fois de plus que toutes les librairies que nous utilisons sont portables. :p

Télécharger : SDL_image - MinGW pour Windows
Télécharger : Code Source SDL_image + Librairies - Visual C++ et Linux

Pour MinGW sous Windows : fusionnez les dossiers bin, dll, include et lib avec ceux du dossier SDL-2.0 que vous avez placé au tout début du tutoriel (chez moi : C:\Program Files (x86)\CodeBlocks\MinGW\SDL-2.0).
Pensez à rajouter les nouvelles dll soit dans le dossier de chaque projet, soit dans le dossier bin de MinGW selon la méthode que vous avez choisie au début du tuto.

Pour Linux ça va être folklo. :lol: Vu que tout le monde n'a pas forcément la même distribution, il faudra que vous compiliez vous même SDL_image. Cependant, cette librairie fait appel à 5 autres librairies et il faudra également toutes les compiler !
Mais bon vous savez que je ne suis pas sadique (ah bon ?), je vais donc vous donner directement toutes les commandes à taper dans votre terminal.

Enfin, commencez par télécharger le code source de SDL_image et dézippez son contenu dans votre home. Ensuite, exécutez les commandes suivantes mais attention ! Si j'ai divisé les commandes en bloc ce n'est pas pour rien, je vous conseille d'exécuter les blocs un à un pour voir si tout compile normalement. Si vous copiez toute les commandes d'un coup vous risquez de zapper une erreur que vous regretterez plus tard. ;)

cd
cd SDL_image/tiff-4.0.3/
chmod +x configure
./configure
make
sudo make install


cd ../zlib-1.2.7/
chmod +x configure
./configure
make
sudo make install


cd ../libpng-1.5.13/
chmod +x configure
./configure
make
sudo make install


cd ../jpeg-8d/
chmod +x configure
./configure
make
sudo make install


cd ../libwebp-0.2.0/
chmod +x configure
./configure
make
sudo make install


cd ../SDL_image
chmod +x configure
./configure
make
sudo make install

On reprend au niveau des IDE pour tout le monde, il va falloir linker la librairie SDL_image avec vos projets. Voici un petit tableau avec le link à spécifier en fonction de votre IDE :

OS

Option

Code::Blocks Windows

SDL2_image

Code::Blocks Linux

SDL2_image

DevC++

-lSDL2_image

Visual Studio

SDL2_image.lib

Enfin pour terminer cette introduction, nous allons télécharger un pack de textures que l'on utilisera tout au long de ce tutoriel. Vous y verrez à l'intérieur plusieurs catégories d'images (sols, pierres, bois, ...). Je remercie au passage notre petit Kayl qui a fait découvrir ce pack dans son tuto. Je reprends le même vu qu'il est assez complet. :p

Télécharger : Pack de Textures Haute Résolution

Chargement

Les Objets OpenGL

La librairie SDL_image va nous faciliter grandement la tâche dans le chargement de texture. Elle va nous faire gagner beaucoup de temps car elle sait charger une multitude de formats d'image, nous n'aurons donc pas à charger nos images manuellement. Cependant, elle ne peut pas tout faire pour nous. Les images chargées avec SDL_image seront, si on les laisse comme ça, inutilisables avec OpenGL.

En effet, la librairie permet de charger les textures uniquement pour la SDL et non pour les autres API. Il nous faut donc configurer OpenGL pour qu'il puisse reconnaitre ces nouvelles textures. Dans cette partie nous allons voir pas mal de fonctions spécifiques à la librairie OpenGL, et vous verrez que vous retrouvez certaines d'entre elles tout au long du tutoriel. Je vous dirai celles qui sont importantes à retenir. ;)

Les objets OpenGL

Avant d'aller plus loin, j'aimerais que l'on développe un point important de ce chapitre : les objets OpenGL. Les objets OpenGL sont semblables aux objets en C++ (même s'ils sont différents dans le fond), on peut les représenter par le laboratoire que l'on voit dans le chapitre sur les objets de M@téo. Ce sont donc des sortes de laboratoires dont on ne connait pas le fonctionnement, et d'ailleurs on s'en moque à partir du moment où ils fonctionnent. :p

Pourquoi je vous parle de ça ? Et bien simplement parce qu'une texture est un objet OpenGL. Vous verrez que l'on va apprendre à initialiser la texture mais vous n'aurez aucune idée de ce qui se passe à l'intérieur de la carte graphique, tout comme le laboratoire. Nous donnerons à la texture des pixels à afficher et OpenGL se chargera du reste. Bon je schématise un peu mais vous avez compris l'idée.

Comment on crée un objet OpenGL ? C'est dur à faire ? :(

Non pas du tout c'est en réalité très simple ! En effet, pour créer ces objets on utilise la plupart du temps la même fonction. Et cette fonction nous renverra toujours la même chose : un ID représentant l'objet créé. Cet ID est une variable de type unsigned int et va permettra à OpenGL de savoir sur quel objet il doit travailler.

En ce qui concerne la configuration de ces objets, nous procéderons ainsi :

  • Chargement d'une image avec la librairie SDL_image

  • Création (ou plutôt de génération) de l'ID

  • Verrouillage de l'ID (nous allons voir ce que c’est dans un instant)

  • Configuration de l'objet

  • Déverrouillage de l'ID

Toutes ces parties se gèrent avec les mêmes fonctions pour la plupart des objets OpenGL (que ce soit une texture ou autre). Il n'y a que l'étape de la configuration qui va varier.

La classe Texture

On commence la partie programmation par le plus simple : la création d'une classe Texture. Mis à part le constructeur et le destructeur, cette classe ne contiendra que la méthode charger() qui s'occupera de charger la texture demandée. Elle retournera un booléen pour confirmer ou non le chargement :

bool charger();

La classe contiendra également 2 attributs :

  • GLuint m_id : Un unsigned int qui représentera le fameux ID

  • string m_fichierImage : Le chemin vers le fichier contenant l'image

Au niveau du code, ça donne ça :

// Attributs

GLuint m_id;
std::string m_fichierImage;

Le constructeur prendra en paramètre une string qui représentera le chemin vers le fichier image. L'ID quant à lui sera généré par OpenGL :

// Constructeur 

Texture(std::string fichierImage);

On rajoutera au passage un accesseur pour l'attribut m_id et un mutateur pour m_fichierImage au cas où devrions spécifier une image après déclaration de l'objet. L'accesseur sera important pour la suite :

GLuint getID() const;
void setFichierImage(const std::string &fichierImage);

Sans oublier les includes de la SDL, de SDL_image et quelques autres voici ce que ça nous donne :

#ifndef DEF_TEXTURE
#define DEF_TEXTURE


// Include

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

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

#endif

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


// Classe Texture

class Texture
{
    public:

    Texture(std::string fichierImage);
    ~Texture();
    bool charger();
    GLuint getID() const;
    void setFichierImage(const std::string &fichierImage);


    private:

    GLuint m_id;
    std::string m_fichierImage;
};

#endif

Pour l'implémentation de ce début de code vous savez comment faire : :p

#include "Texture.h"


// Constructeur

Texture::Texture(std::string fichierImage) : m_id(0), m_fichierImage(fichierImage)
{

}


// Destructeur

Texture::~Texture()
{

}


// Méthodes

bool Texture::charger()
{

}


GLuint Texture::getID() const
{
    return m_id;
}


void Texture::setFichierImage(const std::string &fichierImage)
{
    m_fichierImage = fichierImage;
}

La méthode charger

Chargement de l'image dans une SDL_Surface

Maintenant que l'on a un squelette de classe propre, nous pouvons nous lancer dans la création de texture. La première étape consiste à charger un fichier image en mémoire grâce à la librairie SDL_image. Pour cela rien de plus simple, il existe une et unique fonction pour charger plus d'une dizaine de formats d'image différents ! Que demande le peuple. ^^

La fonction est la suivante :

SDL_Surface *IMG_Load(const char *file)
  • file : Chemin du fichier image

La fonction renvoie une SDL_surface qui contiendra tous les pixels nécessaires.

Pour le chemin du fichier, nous donnerons l'attribut m_fichierImage, ou plutôt la chaine C de cet attribut car la fonction demande un tableau de caractère. ;)

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());
}

Attention cependant, la fonction peut renvoyer un pointeur sur 0. Il faut donc gérer cette erreur au cas où l'image n'existerait pas ou si le chemin donné contient une erreur. En cas de problème, on affiche alors un message d'erreur grâce à la fonction SDL_GetError() :

char* SDL_GetError(void);

Cette fonction permet de renvoyer la dernière erreur qu'a rencontrée la SDL (dans une chaine de char). Donc en cas d'erreur de chargement, on inclut le résultat de cette fonction dans un flux cout :

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());

    if(imageSDL == 0)
    {
        std::cout << "Erreur : " << SDL_GetError() << std::endl;
        return false;
    }
}
Génération de l'ID

On a vu tout à l'heure ce qu'étaient les objets OpenGL et on sait également que nous pouvons les gérer grâce à leur ID. Nous allons maintenant voir comment générer cet ID. Pour ce faire, il existe une fonction déjà toute prête dans OpenGL :

GLuint glGenTextures(GLsizei number,  GLuint *textures);
  • number : Le nombre d'ID à initialiser. Nous mettrons toujours la valeur 1

  • textures : Un tableau de type GLuint. On peut aussi mettre l'adresse d'une variable GLuint pour initialiser un seul ID de texture (et c'est ce qu'on fera)

Pour générer un ID de texture, il suffit d'utiliser cette fonction en donnant en paramètre l'attribut m_id de notre classe Texture :

// Génération de l'ID

glGenTextures(1, &m_id);

On appelle cette fonction juste après avoir charger l'image en mémoire :

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());

    if(imageSDL == 0)
    {
        std::cout << "Erreur : " << SDL_GetError() << std::endl;
        return false;
    }

    // Génération de l'ID

    glGenTextures(1, &m_id);
}
Le verrouillage

Je vous ai parlé rapidement du verrouillage d'objet tout à l'heure, ceci permettait à OpenGL de verrouiller un objet pour travailler dessus. Tous les objets OpenGL doivent être verrouillés pour être configurés (et même pour être utilisés !) sinon vous ne pourrez rien faire avec.

On utilisera une fonction simple pour verrouiller nos objets. Ce sera d'ailleurs la même pour les déverrouiller. :p Voici son prototype :

void glBindTexture(GLenum target,  GLuint  texture);
  • target : C'est un paramètre que vous retrouvez souvent avec tous les objets, nous le verrons même plusieurs fois dans ce chapitre. Il correspond au type de l'objet que l'on veut créer, nous lui affecterons la valeur GL_TEXTURE_2D en ce qui concerne les textures.

  • texture : C'est l'ID de l'objet, nous lui donnerons la valeur de l'attribut m_id. La valeur de m_id et non un pointeur cette fois ci ! :)

Voici donc comment utiliser la fonction dans notre cas :

// Verrouillage

glBindTexture(GL_TEXTURE_2D, m_id);

Tiens au passage, vu que l'on a vu le verrouillage d'objets, nous allons voir maintenant le déverrouillage qui permet à OpenGL d’arrêter de se concentrer sur l'objet en cours, ce qui permet par extension d’empêcher les modifications.

Pour réaliser cette opération, on utilisera la même fonction mais avec le paramètre target non plus égal à la valeur de l'ID de la texture mais avec la valeur 0 (la valeur nulle quoi). En gros, on dit à OpenGL : "Verrouille l'objet possédant l'ID 0, soit rien du tout". :)

// Déverrouillage

glBindTexture(GL_TEXTURE_2D, 0);

Petit récap :

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());

    if(imageSDL == 0)
    {
        std::cout << "Erreur : " << SDL_GetError() << std::endl;
        return false;
    }


    // Génération de l'ID

    glGenTextures(1, &m_id);


    // Verrouillage

    glBindTexture(GL_TEXTURE_2D, m_id);


    // Déverrouillage

    glBindTexture(GL_TEXTURE_2D, 0);
}
Configuration de la texture

Notre texture a un ID généré, elle est également verrouillée, on peut maintenant passer à sa configuration. :D

Grossièrement parlant, pour avoir une texture dans OpenGL il suffit de copier les pixels d'une image dans la texture. C'est aussi simple que ça. Seulement voilà, il existe plusieurs formats d'image et certaines contiennent plus de données que d'autres, ...

Hein je croyais que la librairie SDL_image permettait justement de gérer tous ces formats ? :(

Et bien oui vous avez raison, c'est bien SDL_image qui gère les différents formats de l'image. Il existe cependant une chose qu'elle ne peut pas nous dire automatiquement.

Vous n'êtes pas sans savoir qu'un pixel est composé de 3 couleurs (rouge, vert et bleu) ... Les pixels d'une image n’échappent pas à cette règle, chacun d'entre eux est composé de ces 3 couleurs. Seulement voilà, il existe, pour certains formats, une quatrième composante qui s'appelle la composante Alpha. Cette composante permet de stocker le "niveau de transparence" d'une image.

Pour charger correctement une texture, il faut savoir si cette valeur alpha est présente ou non, et heureusement pour nous, la librairie SDL_image est capable de nous le dire. En effet, dans la structure SDL_Surface utilisée au début de la méthode charger(), il existe un champ BytesPerPixel qui permet de dire s'il y a 3 ou 4 couleurs. Nous devrons donc d'abord récupérer cette valeur avant de copier les pixels dans la texture.

Bien on arrête là pour la théorie, on passe au code.

On veut savoir si une image possède 3 ou 4 couleurs, on récupère donc le champ imageSDL->format->BytesPerPixel pour le vérifier puis on met le tout dans un bloc if. Si on a une valeur inconnue, on arrête le chargement de la texture pour éviter de se retrouver avec une grosse erreur puis on n'oublie pas de libérer la surface SDL avant de quitter la méthode :

// Détermination du nombre de composantes

if(imageSDL->format->BytesPerPixel == 3)
{

}

else if(imageSDL->format->BytesPerPixel == 4)
{

}


// Dans les autres cas, on arrête le chargement

else
{
    std::cout << "Erreur, format de l'image inconnu" << std::endl;
    SDL_FreeSurface(imageSDL);

    return false;
}

On sait maintenant qu'il faut faire attention au bidule alpha, mais qu'est qu'on met à l'intérieur des if ? Ils sont tout vide. :(

C'est normal, il manque encore quelque chose. Comme on l'a vu plus tôt, OpenGL a besoin de savoir si la composante alpha existe ou pas. Seulement si on lui donne la valeur 3 ou 4 ça ne va pas lui suffire, il faudra envoyer une autre valeur qui sera un peu comme le paramètre GL_TEXTURE_2D que l'on a vu plus haut. Avec ce paramètre, il comprendra mieux ce qu'on lui enverra.

Il y aura deux cas à gérer :

  • Soit l'image ne contiendra pas la composante alpha et dans ce cas on retiendra la constante GL_RGB

  • Soit l'image contiendra la composante alpha et dans ce cas on retiendra la constante GL_RGBA

Au niveau du code, on utilisera une variable de type GLenum pour retenir cette valeur. On l'appellera formatInterne, vous verrez pourquoi juste après :

// Détermination du nombre de composantes

GLenum formatInterne(0);

if(imageSDL->format->BytesPerPixel == 3)
{
    formatInterne = GL_RGB;
}

else if(imageSDL->format->BytesPerPixel == 4)
{    
    formatInterne = GL_RGBA;
}


// Dans les autres cas, on arrête le chargement

else
{
    std::cout << "Erreur, format interne de l'image inconnu" << std::endl;
    SDL_FreeSurface(imageSDL);

    return false;
}

Il ne manque plus qu'une chose à faire. Selon le système d'exploitation ou même les images que vous utiliserez, les pixels ne seront pas stockés dans le même ordre. Par exemple sous Windows, la plupart des formats stockent leurs pixels selon l'ordre Rouge Vert Bleu (RGB) sauf les images au format BMP. Ceux-ci voient leurs pixels stockés selon l'ordre Bleu Vert Rouge (BGR). C'est un problème que nous devons gérer car certains auront la belle surprise de voir leurs images avec des couleurs complétement inversées (Imaginez un Dark Vador en blanc :p ).

Il faut donc dire à OpenGL dans quel ordre les pixels sont stockés, et pour ça on va utiliser une autre variable de type GLenum que l'on appelera format :

// Détermination du format et du format interne

GLenum formatInterne(0);
GLenum format(0);

Pour connaitre l'ordre des pixels, nous devons utiliser un autre champ de la structure imageSDL. Ce champ sera imageSDL->format->Rmask.

Il existe 4 champs similaires Rmask, Gmask, Bmask et Amask qui représente chacun la position de sa couleur à l'aide d'une valeur hexadécimal. Nous utiliserons le premier champ (Rmask), même si nous pouvions utiliser n'importe lequel. Sauf le dernier car il se trouve toujours à la fin quelque soit le format d'image.

Nous devons donc tester cette valeur pour connaitre la position de la couleur rouge. Si sa valeur est égale à 0xff alors elle est placée au début, sinon c'est qu'elle se trouve à la fin :

// Format de l'image

GLenum formatInterne(0);
GLenum format(0);


// Détermination du format et du format interne

if(imageSDL->format->BytesPerPixel == 3)
{
    // Format interne

    formatInterne = GL_RGB;


    // Format

    if(imageSDL->format->Rmask == 0xff)
    {}

    else
    {}
}

else if(imageSDL->format->BytesPerPixel == 4)
{    
    // Format interne

    formatInterne = GL_RGBA;


    // Format

    if(imageSDL->format->Rmask == 0xff)
    {}

    else
    {}
}


// Dans les autres cas, on arrête le chargement

else
{
    std::cout << "Erreur, format interne de l'image inconnu" << std::endl;
    SDL_FreeSurface(imageSDL);

    return false;
}

Il faut maintenant affecter une valeur à la variable format. Il y a 4 cas à gérer :

  • Rouge en premier pour une image à 3 composantes

  • Rouge en dernier pour une image à 3 composantes

  • Rouge en premier pour une image à 4 composantes

  • Rouge en dernier pour une image à 4 composantes

Ces quatre cas seront représentés par les constantes suivantes :

  • GL_RGB

  • GL_BGR

  • GL_RGBA

  • GL_BGRA

Il n'y a plus qu'à affecter la bonne constante à la variable format :

// Format pour 3 couleurs

if(imageSDL->format->Rmask == 0xff)
    format = GL_RGB;

else
    format = GL_BGR;


....


// Format pour 4 couleurs

if(imageSDL->format->Rmask == 0xff)
    format = GL_RGBA;

else
    format = GL_BGRA;

Ce qui donne au final :

// Format de l'image

GLenum formatInterne(0);
GLenum format(0);


// Détermination du format et du format interne pour les images à 3 composantes

if(imageSDL->format->BytesPerPixel == 3)
{
    // Format interne

    formatInterne = GL_RGB;


    // Format

    if(imageSDL->format->Rmask == 0xff)
        format = GL_RGB;

    else
        format = GL_BGR;
}


// Détermination du format et du format interne pour les images à 4 composantes

else if(imageSDL->format->BytesPerPixel == 4)
{    
    // Format interne

    formatInterne = GL_RGBA;


    // Format

    if(imageSDL->format->Rmask == 0xff)
        format = GL_RGBA;

    else
        format = GL_BGRA;
}


// Dans les autres cas, on arrête le chargement

else
{
    std::cout << "Erreur, format interne de l'image inconnu" << std::endl;
    SDL_FreeSurface(imageSDL);

    return false;
}

Pfiou tout ce gourbi pour déterminer deux valeurs ! :lol:

On a fait le plus dur, il ne nous reste plus qu'à copier les fameux pixels dans la texture. Pour ça, on va utiliser la fonction suivante (ne soyez pas surpris du nombre de paramètres :p ) :

void glTexImage2D(GLenum target,  GLint level,  GLint internalFormat,  GLsizei width,  GLsizei height,  GLint border,  GLenum format,  GLenum type,  const GLvoid * data);
  • target : Comme on l'a vu précédemment, pour les textures on affectera toujours la valeur GL_TEXTURE_2D

  • level : Paramètre que nous n’utiliserons pas, on le mettra à 0

  • internalFormat : Tiens ! On vient de le déterminer juste avant celui-la

  • width : Largeur de l'image qui est contenue dans le champ imageSDL->w

  • height : Hauteur de l'image qui est contenue dans le champ imageSDL->h

  • border : Paramètre utile quand vous avez une bordure sur votre image. Nous donnerons la valeur 0 en général

  • format : Oh lui aussi on l'a trouvé !

  • type : Type de donnée des pixels (float, int, ...). Nous lui donnerons un type d'OpenGL : GL_UNSIGNED_BYTE

  • data : Ce sont les pixels de l'image, on lui donnera l'adresse du tableau imageSDL->pixels

Ça en fait des paramètres tout ça ! Mais au moins on n'utilise qu'une seule fonction pour remplir notre texture.

Voyons son implémentation dans le code :

// Copie des pixels

glTexImage2D(GL_TEXTURE_2D, 0, formatInterne, imageSDL->w, imageSDL->h, 0, format, GL_UNSIGNED_BYTE, imageSDL->pixels);

On refait un petit récap :

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());

    if(imageSDL == 0)
    {
        std::cout << "Erreur : " << SDL_GetError() << std::endl;
        return false;
    }


    // Génération de l'ID

    glGenTextures(1, &m_id);


    // Verrouillage

    glBindTexture(GL_TEXTURE_2D, m_id);


    // Format de l'image

    GLenum formatInterne(0);
    GLenum format(0);


    // Détermination du format et du format interne pour les images à 3 composantes

    if(imageSDL->format->BytesPerPixel == 3)
    {
        // Format interne

        formatInterne = GL_RGB;


        // Format

        if(imageSDL->format->Rmask == 0xff)
            format = GL_RGB;

        else
            format = GL_BGR;
    }


    // Détermination du format et du format interne pour les images à 4 composantes

    else if(imageSDL->format->BytesPerPixel == 4)
    {    
        // Format interne
        
        formatInterne = GL_RGBA;


        // Format

        if(imageSDL->format->Rmask == 0xff)
            format = GL_RGBA;

        else
            format = GL_BGRA;
    }


    // Dans les autres cas, on arrête le chargement

    else
    {
        std::cout << "Erreur, format interne de l'image inconnu" << std::endl;
        SDL_FreeSurface(imageSDL);

        return false;
    }


    // Copie des pixels

    glTexImage2D(GL_TEXTURE_2D, 0, formatInterne, imageSDL->w, imageSDL->h, 0, format, GL_UNSIGNED_BYTE, imageSDL->pixels);


    // Déverrouillage

    glBindTexture(GL_TEXTURE_2D, 0);
}

Et voila ! La texture contient enfin ses propres pixels issus de notre fichier image.

Les filtres

Allez courage, il ne nous manque plus qu'une chose à faire.

Je vais vous parler rapidement d'une notion que je connais très mal : la notion de filtres. Je ne m'y connais pas assez pour tenir tout un pavé alors je ferai vite. :lol:

Un filtre permet de gérer la qualité d'affichage d'une texture. Il permet à OpenGL de savoir s'il doit afficher une image en mode pixelisé ou en mode lisse. On serait tenté de vouloir que toutes les textures soient lisses au moment de l'affichage mais sachez que ça joue sur la vitesse de votre programme. En effet, plus vous voudrez de belles textures à l'écran, plus votre matériel sera sollicité. Alors bon, aujourd'hui le problème est moins voyant qu'il y a 10 ans mais on est toujours aujourd'hui dans cette optique d'optimisation du rendu.

Pour vous éviter un gros bloc de théorie je vais résumer ma pensée en quelques lignes (en plus ça m'arrange, j'ai mal aux doigts :p ).

Il existe deux types d'affichage pour une texture :

  • Soit la texture est proche de l'écran et dans ce cas il vaut mieux la lisser, car le joueur a plus de chance de la voir

  • Soit la texture est éloignée de l'écran et dans ce cas on peut se permettre de "l'afficher à l'arrache", le joueur ne s'en rendre même pas compte

Ces deux cas vont correspondre à deux filtres que nous allons créer.

Pour créer un filtre, on utilise la fonction suivante :

void glTexParameteri(GLenum target,  GLenum pname,  GLint param);

Cette fonction permet d'envoyer des paramètres supplémentaires à nos textures, dans notre cas des filtres. Voici ses paramètres :

  • target : On ne le présente plus

  • pname : Type de paramètre à envoyer, ici le filtre

  • param : La valeur du paramètre

Avec cette fonction, nous pourrons créer deux filtres : un pour le cas où la texture est proche de l'écran et un autre pour le cas où elle en est éloignée.

Pour le premier filtre :

  • Le filtre permettant de gérer les "textures proches" s'appelle GL_TEXTURE_MIN_FILTER, ce sera donc la valeur du paramètre pname.

  • Nous voulons que les "textures proches" soient "lisses", nous donnerons donc la valeur GL_LINEAR au paramètre param

Code :

// Application des filtres

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

Pour le second filtre :

  • Le filtre permettant de gérer les "textures éloignées" s'appelle GL_TEXTURE_MAG_FILTER, ce sera donc la valeur du paramètre pname.

  • Nous voulons que les "textures éloignées" soient "pixelisées", nous donnerons donc la valeur GL_NEAREST au paramètre param

Code :

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

Grâce à ces filtres, nous pourrons avoir un bon équilibre entre érgonomie pour le joueur et rapidité pour le CPU et la carte graphique.

Petit récap :

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());

    if(imageSDL == 0)
    {
        std::cout << "Erreur : " << SDL_GetError() << std::endl;
        return false;
    }


    // Génération de l'ID

    glGenTextures(1, &m_id);


    // Verrouillage

    glBindTexture(GL_TEXTURE_2D, m_id);


    // Format de l'image

    GLenum formatInterne(0);
    GLenum format(0);


    // Détermination du format et du format interne pour les images à 3 composantes

    if(imageSDL->format->BytesPerPixel == 3)
    {
        // Format interne

        formatInterne = GL_RGB;


        // Format

        if(imageSDL->format->Rmask == 0xff)
            format = GL_RGB;

        else
            format = GL_BGR;
    }


    // Détermination du format et du format interne pour les images à 4 composantes

    else if(imageSDL->format->BytesPerPixel == 4)
    {    
        // Format interne
        
        formatInterne = GL_RGBA;


        // Format

        if(imageSDL->format->Rmask == 0xff)
            format = GL_RGBA;

        else
            format = GL_BGRA;
    }


    // Dans les autres cas, on arrête le chargement

    else
    {
        std::cout << "Erreur, format interne de l'image inconnu" << std::endl;
        SDL_FreeSurface(imageSDL);

        return false;
    }


    // Copie des pixels

    glTexImage2D(GL_TEXTURE_2D, 0, formatInterne, imageSDL->w, imageSDL->h, 0, format, GL_UNSIGNED_BYTE, imageSDL->pixels);


    // Application des filtres

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);


    // Déverrouillage

    glBindTexture(GL_TEXTURE_2D, 0);
}
Fin de la méthode

Pffffffiou, entre les ID, le verrouillage, les filtres et tout ça je n'en puis plus (comme dirait Hooper) !

Il est temps de terminer cette méthode. Il ne reste plus qu'à libérer la surface SDL qui contenait les pixels et à retourner la valeur true (car la méthode s'est normalement terminée sans erreur) :

// Fin de la méthode

SDL_FreeSurface(imageSDL);
return true;

Récap final :

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());

    if(imageSDL == 0)
    {
        std::cout << "Erreur : " << SDL_GetError() << std::endl;
        return false;
    }


    // Génération de l'ID

    glGenTextures(1, &m_id);


    // Verrouillage

    glBindTexture(GL_TEXTURE_2D, m_id);


    // Format de l'image

    GLenum formatInterne(0);
    GLenum format(0);


    // Détermination du format et du format interne pour les images à 3 composantes

    if(imageSDL->format->BytesPerPixel == 3)
    {
        // Format interne

        formatInterne = GL_RGB;


        // Format

        if(imageSDL->format->Rmask == 0xff)
            format = GL_RGB;

        else
            format = GL_BGR;
    }


    // Détermination du format et du format interne pour les images à 4 composantes

    else if(imageSDL->format->BytesPerPixel == 4)
    {    
        // Format interne
        
        formatInterne = GL_RGBA;


        // Format

        if(imageSDL->format->Rmask == 0xff)
            format = GL_RGBA;

        else
            format = GL_BGRA;
    }


    // Dans les autres cas, on arrête le chargement

    else
    {
        std::cout << "Erreur, format interne de l'image inconnu" << std::endl;
        SDL_FreeSurface(imageSDL);

        return false;
    }


    // Copie des pixels

    glTexImage2D(GL_TEXTURE_2D, 0, formatInterne, imageSDL->w, imageSDL->h, 0, format, GL_UNSIGNED_BYTE, imageSDL->pixels);


    // Application des filtres

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);


    // Déverrouillage

    glBindTexture(GL_TEXTURE_2D, 0);


    // Fin de la méthode

    SDL_FreeSurface(imageSDL);
    return true;
}

Enfin ! Nous avons une classe Texture qui permet de charger des fichiers images pour les afficher dans nos scènes 3D. Le petit truc que je ne vous ai pas dit, c'est que cette méthode est incomplète, il manque encore quelque chose mais nous verrons ça dans la dernière sous-partie de ce chapitre. Je ne vais pas vous assassiner plus pour l'instant. :lol: Nous avons ce qu'il nous faut pour afficher une texture sur un modèle 3D et c'est le principal pour le moment. Je veux que vous vous familiarisez avec l'affichage de texture avant d'aller plus loin. :)

Plaquage

Affichage d'une texture

Le plus dur est derrière nous, nous avons appris à déclarer et remplir un texture au sein d'OpenGL. On va maintenant apprendre à les afficher. Et la bonne nouvelle, c'est que vous savez déjà comment faire. :p Ça se passe de la même façon qu'avec les vertices ou les couleurs.

Pour débuter, on va se contenter d'afficher notre première texture sur un simple carré afin de se concentrer sur l'essentiel. Nous utiliserons ensuite notre classe Cube pour afficher un modèle un peu plus élaboré. D'ailleurs, voyons ensemble la première texture que nous serons capables de traiter :

Image utilisateur

Elle nous sera utile tout au long du chapitre, vous la trouverez dans l'archive que vous avez téléchargée au début sous le nom de crate13.jpg. Au pire des cas, faites un clique droit sur l'image ci-dessus pour la récupérer. Nous aurons l'occasion d'en utiliser d'autres aussi.

Coordonnées de Texture

Je ne sais pas si vous lisez toujours les sous-titres oranges et bleues, mais si vous lisez une ligne plus haut vous tomberez sur le terme "coordonnées de texture".

Oh ! C'est quoi ces coordonnées ? C'est similaire aux vertices et tout ça ?

Et bien oui ! On peut faire une analogie avec les vertices car, pour afficher une texture, il faut lui donner une sorte de position. Seulement attention, on ne parle pas ici de coordonnées spatiales avec un repère 3D, une matrice et tout ça ...

Je vais vous expliquer ça avec deux schéma.

Vous savez depuis un moment que les vertices sont composés de coordonnées spatiales (X, Y et Z) :

Image utilisateur

Avec ce système, vous pouvez donner n'importe quelle taille à une forme 3D comme le cube des chapitres précédents par exemple. Et bien les textures se comportent de la même façon. A une exception près : les coordonnées de texture sont toujours comprises entre 0 et 1 :

Image utilisateur

On peut voir sur le schéma le point d'origine de la texture en bas à gauche ainsi que 3 autres coordonnées. La plupart du temps, nous n'utiliserons que ces 4 points vu qu'ils correspondent aux coins de la texture. Mais sachez qu'il est tout à fait possible d'avoir des coordonnées du type (0.27; 0.38) pour ne récupérer qu'une partie de la texture.

Pour plaquer une texture sur une surface il faudra simplement faire correspondre les coordonnées de texture avec les bons vertices :

Image utilisateur

La chose se complexifie légèrement quand on n'oublie le carré et que l'on parle des deux triangles, mais le tout reste tout de même assez simple :

Image utilisateur

Simple n'est-ce pas ? :p

Vous savez maintenant ce que sont les coordonnées de texture : ce sont des points, correspondant généralement aux coins d'une texture, qui permettent de la plaquer sur une surface.

Affichage d'une texture

Nous avons toutes les cartes en main pour afficher notre première texture. Nous n'avons plus qu'à instancier la classe Texture et à utiliser ses coordonnées.

On reprend donc notre bonne vielle classe SceneOpenGL pour déclarer un objet de type Texture (n'oubliez pas d'inclure le header) qu'on appellera simplement texture, puis on la chargera en mémoire. Pensez également à effacer tout le code qui concernait le cube des chapitres précédents :

void SceneOpenGL::bouclePrincipale()
{
    // Variables

    unsigned int frameRate (1000 / 50);
    Uint32 debutBoucle(0), finBoucle(0), tempsEcoule(0);


    // Matrices

    mat4 projection;
    mat4 modelview;

    projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
    modelview = mat4(1.0);


    // Texture

    Texture texture("Textures/Caisse.jpg");
    texture.charger();


    // Boucle principale

    while(!m_input.terminer())
    {
        /* *** Boucle principale *** */
    }
}

Je vous redonne au passage le code de base pour la boucle principale :

// Boucle principale

while(!m_input.terminer())
{
    // On définit le temps de début de boucle

    debutBoucle = SDL_GetTicks();


    // Gestion des évènements

    m_input.updateEvenements();

    if(m_input.getTouche(SDL_SCANCODE_ESCAPE))
       break;


    // Nettoyage de l'écran

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


    // Placement de la caméra

    modelview = lookAt(vec3(0, 0, 2), vec3(0, 0, 0), vec3(0, 1, 0));


    
    /* *** Rendu *** */



    // Actualisation de la fenêtre

    SDL_GL_SwapWindow(m_fenetre);


    // Calcul du temps écoulé

    finBoucle = SDL_GetTicks();
    tempsEcoule = finBoucle - debutBoucle;


    // Si nécessaire, on met en pause le programme

    if(tempsEcoule < frameRate)
        SDL_Delay(frameRate - tempsEcoule);
}

Bien, avec ce code propre on va pouvoir bosser tranquillement.

Notre objectif depuis le début de ce chapitre est l'affichage d'une texture sur une surface, et de préférence un carré. On va commencer par créer nos coordonnées pour les vertices. D'ailleurs, vous savez déjà depuis un moment comment faire un carré avec OpenGL. Je vais donc vous demander de me coder vous-même les vertices pour un carré avec des arrêtes mesurant 4 unités de longueur :

Image utilisateur

Cependant, je souhaite attirer votre attention sur deux points :

  • Faites votre carré en 3 dimensions, il faut donc intégrer la coordonnée Z

  • Faites attention au repère qui se trouve au milieu de la surface et non en bas à gauche.

Voila pour les explications, en gros faites un carré à l'ancienne avec 3 dimensions. ^^

...

Fini ? Voici la solution :

 

// Vertices

float vertices[] = {-2, -2, -2,   2, -2, -2,   2, 2, -2,   // Triangle 1
                    -2, -2, -2,   -2, 2, -2,   2, 2, -2};  // Triangle 2

Aaaah, faire un carré c'est tout de même plus simple que de devoir faire un cube complet.

L'étape suivante consisterait normalement à colorier notre nouvelle forme. Cependant, nous n'en avons plus besoin vu que nous avons une texture qui va s'appliquer dessus.
Dans notre code, au lieu de déclarer un tableau de couleurs nous allons déclarer un tableau de coordonnées de texture. Et comme je suis sadique, je vais vous demander de me coder ce tableau vous-même comme des grands. :p N’ayez pas peur c'est exactement le même principe qu'avec les vertices et les couleurs.

Pour vous aider dans cette tâche je vais vous donner deux petites schémas avec les informations nécessaires :

Image utilisateur
Image utilisateur

Rien de bien compliqué, vous devez simplement faire comme s'il s'agissait de vertices. Vous avez toutes les coordonnées (x, y) et vous devez en faire un tableau. Faites attention cependant à leur ordre à l'intérieur du tableau car celles-ci sont liées à vos vertices (au même titre que les couleurs).

... Pause café ...

Fini. Allez on passe à la correction.

 

// Coordonnées de texture

float coordTexture[] = {0, 0,   1, 0,   1, 1,     // Triangle 1
                        0, 0,   0, 1,   1, 1};    // Triangle 2

On a maintenant un carré, une texture et des coordonnées permettant de la plaquer dessus. On n'a plus qu'à donner tout ça à OpenGL et le tour est joué. :)

Ah mais justement : comment on envoie les coordonnées à OpenGL ?

Bonne question, je vous ai dit tout à l'heure que les coordonnées de texture se géraient de la même façon que les vertices et les couleurs. On va donc utiliser donc un Vertex Array pour envoyer toutes les données à OpenGL.

Vous vous souvenez des Vertex Array quand même ? Ce sont des tableaux qui font le lien entre les coordonnées et OpenGL. Le tableau Vertex Attrib 0 permet d'envoyer les vertices et le tableau Vertex Attrib 1 permet d'envoyer les couleurs. Maintenant nous allons utiliser le tableau Vertex Attrib 2 qui va nous permettre d'envoyer nos coordonnées de texture.

Dans notre code, il nous faut appeler la fonction glVertexAttribPointer() avec l'indice 2 (et non 1). Il faut également donner la valeur 2 au paramètre size car nos coordonnées de texture ne sont constituées que de couple de valeurs (x, y) et non de triplet (x, y, z). On pensera à donner le tableau coordTexture en dernier paramètres sinon on ne risque pas d'envoyer grand chose à OpenGL. :lol:

Voici à quoi ressemblerait l'appel à glVertexAttribPointer() dans notre cas :

// Envoi des coordonnées de texture

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

Puisque nous envoyons des données à travers un tableau Vertex Attrib, il faut penser à l'activer grâce à la fonction glEnableVertexAttribArray(). On appelle donc celle-ci avec la valeur 2 pour activer le tableau d'indice 2 :

// Envoi des coordonnées de texture

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

On place ce bout de code au même endroit où on envoyait les couleurs, c'est-à-dire entre les deux appels à la fonction glUseProgram() :

// Activation du shader

glUseProgram(shader.getProgramID());


    // Envoi des vertices

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


    // Envoi des coordonnées de texture

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


    // Envoi des matrices et rendu

    ....



    // Désactivation des tableaux

    glDisableVertexAttribArray(2);
    glDisableVertexAttribArray(0);


// Désactivation du shader

glUseProgram(0);

Tiens en parlant de shader, nous allons devoir modifier les codes sources (le petits fichiers dans le dossier Shaders) qu'il utilise afin de pouvoir intégrer l'affichage de texture. En effet, ceux que nous avons utilisés jusqu'à maintenant ne gèrent que la couleur, nous en avons donc besoin de nouveaux pour gérer notre nouvelle façon d'afficher.

Ces nouveaux codes sources se nomment texture.vert et texture.frag et devraient être présents dans votre dossier Shaders :

// Shader gérant les texture

Shader shaderTexture("Shaders/texture.vert", "Shaders/texture.frag");
shaderTexture.charger();

Faisons un petit récap (sans la boucle principale) pour voir où nous en sommes pour le moment :

void SceneOpenGL::bouclePrincipale()
{
    // Variables

    unsigned int frameRate (1000 / 50);
    Uint32 debutBoucle(0), finBoucle(0), tempsEcoule(0);


    // Matrices

    mat4 projection;
    mat4 modelview;

    projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
    modelview = mat4(1.0);


    // Vertices

    float vertices[] = {-2, -2, -2,   2, -2, -2,   2, 2, -2,   // Triangle 1
                        -2, -2, -2,   -2, 2, -2,   2, 2, -2};  // Triangle 2


    // Coordonnées de texture

    float coordTexture[] = {0, 0,   1, 0,   1, 1,     // Triangle 1
                            0, 0,   0, 1,   1, 1};    // Triangle 2


    // Texture

    Texture texture("Textures/Caisse.jpg");
    texture.charger();


    // Shader

    Shader shaderTexture("Shaders/texture.vert", "Shaders/texture.frag");
    shaderTexture.charger();


    // Boucle principale

    while(!m_input.terminer())
    {
        /* *** Rendu *** */
    }
}

Nous ferons un petit récap de la boucle principale dans un instant. Concentrons-nous d'abord sur le code d'affichage.

Celui-ci va commencer par l'activation du shader (qui gère les textures maintenant) suivie de l'envoi des vertices, des coordonnées de texture et des matrices ainsi que de l'appel à la fonction de rendu glDrawArrays() :

// Activation du shader

glUseProgram(shaderTexture.getProgramID());


    // Envoi des vertices

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


    // Envoi des coordonnées de texture

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


    // Envoi des matrices

    glUniformMatrix4fv(glGetUniformLocation(shaderTexture.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));
    glUniformMatrix4fv(glGetUniformLocation(shaderTexture.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));


    // Rendu

    glDrawArrays(GL_TRIANGLES, 0, 6);


    // Désactivation des tableaux

    glDisableVertexAttribArray(2);
    glDisableVertexAttribArray(0);


// Désactivation du shader

glUseProgram(0);

Ah encore une petite chose, normalement je devrais faire le sadique en vous demandant de compiler votre code maintenant. Seulement si vous le faites vous aurez au mieux un écran noir. :p

Il manque donc encore une dernière chose à faire. Pour le moment, nous nous contentons d'envoyer les coordonnées de texture à OpenGL, mais à aucun moment nous lui disons quelle texture il doit afficher. Et comme OpenGL n'est pas devin, il va nous envoyer balader et afficher un carré noir.

Pour éviter cela, il faut faire une opération que vous savez déjà faire : le verrouillage de texture. En effet, grâce à ça, OpenGL saura ce qu'il doit afficher exactement comme au moment de la configuration où il savait sur quelle texture travailler.

Pour effectuer cette opération, nous allons réutiliser la fonction glBindTexture() avec le même paramètre que d'habitude soit l'ID de la texture. D'où l'utilité de la méthode getID() que je vous avais demandée de coder précédemment.

En définitif, nous devons appeler la fonction glBindTexture() deux fois avec la méthode getID() en tant que paramètre pour le premier appel et la valeur 0 pour le second :

// Verrouillage de la texture

glBindTexture(GL_TEXTURE_2D, texture.getID());


// Rendu

glDrawArrays(GL_TRIANGLES, 0, 6);


// Déverrouillage de la texture

glBindTexture(GL_TEXTURE_2D, 0);

Et si on veut afficher deux textures en même temps sur une surface on fait comment ? On les verrouille toutes les deux ?

Ah là on touche au domaine du multi-texturing, vaste sujet passionnant que nous ne verrons pas maintenant. Nous verrons cela quand nous saurons manipuler les shaders. ;)

On revient au code avec un petit récap de la boucle principale :

// Boucle principale

while(!m_input.terminer())
{
    // On définit le temps de début de boucle

    debutBoucle = SDL_GetTicks();


    // Gestion des évènements

    m_input.updateEvenements();

    if(m_input.getTouche(SDL_SCANCODE_ESCAPE))
       break;


    // Nettoyage de l'écran

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


    // Placement de la caméra

    modelview = lookAt(vec3(0, 0, 2), vec3(0, 0, 0), vec3(0, 1, 0));


    // Activation du shader

    glUseProgram(shaderTexture.getProgramID());


        // Envoi des vertices

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


        // Envoi des coordonnées de texture

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


        // Envoi des matrices

        glUniformMatrix4fv(glGetUniformLocation(shaderTexture.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));
        glUniformMatrix4fv(glGetUniformLocation(shaderTexture.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));


        // Verrouillage de la texture

        glBindTexture(GL_TEXTURE_2D, texture.getID());


        // Rendu

        glDrawArrays(GL_TRIANGLES, 0, 6);


        // Déverrouillage de la texture

        glBindTexture(GL_TEXTURE_2D, 0);


        // Désactivation des tableaux

        glDisableVertexAttribArray(2);
        glDisableVertexAttribArray(0);


    // Désactivation du shader

    glUseProgram(0);


    // Actualisation de la fenêtre

    SDL_GL_SwapWindow(m_fenetre);


    // Calcul du temps écoulé

    finBoucle = SDL_GetTicks();
    tempsEcoule = finBoucle - debutBoucle;


    // Si nécessaire, on met en pause le programme

    if(tempsEcoule < frameRate)
        SDL_Delay(frameRate - tempsEcoule);
}

Cette fois-ci, notre code est complet. :D Affichons enfin notre première texture !

Image utilisateur

Heeeeein ! o_O o_O o_O

Pourquoi la texture est inversée ? C'est pourtant les bonnes coordonnées qu'on a envoyées non ? Qu'est-ce qui s'passe ? :(

C'est tout à fait normal ne vous inquiétez pas. :p Tout à l'heure, je vous ai dit qu'on n'en arrêtait là avec le code de la classe Texture, et que du coup elle serait incomplète. En effet, la librairie SDL_image n'a pas le même repère que nous pour charger les images. Son repère à elle lui se trouve en haut à gauche alors que le notre se situe en bas à droite :

Image utilisateur
Image utilisateur

Les textures se retrouvent donc la tête en bas pour nous. Pour corriger ce problème, il nous suffit d'inverser tous les pixels d'une image au moment de son chargement. C'est le fameux dernier point qu'il nous manquait dans notre méthode charger(). Nous allons régler ça dans un moment.

Mais avant cela, je vous conseille de vous entrainer à afficher quelques textures sur plusieurs carrés pour vous familiariser avec l'affichage de texture. ;)

Améliorations

Si vous n'êtes pas trop fatigués on va pouvoir passer à la dernière partie de ce chapitre. :p

Les objectifs de cette dernière partie sont :

  • De de terminer l'implémentation de la méthode charger() pour afficher nos textures dans le bon sens.

  • D'améliorer la classe Texture en y ajoutant des méthodes comme le constructeur de copie, l'opérateur =, ...

L'inversion des pixels

Théorie

Commençons tout de suite par nous n'occuper du premier point : l'inversion des pixels. Vous avez remarqué (avec effroi) que nos textures s'affichent la tête inversée. Il y a deux façons de régler ce problème :

  • Soit on inverse l’ordre de toutes les coordonnées de texture pour afficher l'image à l'envers (mais qui du coup sera dans le bon sens)

  • Soit on inverse les pixels de l'image avant de la charger coté OpenGL

Comme vous le savez déjà, on va opter pour la seconde solution.

En effet, la première pourrait fonctionner un temps mais une fois que nous chargerons des modèles depuis des fichiers externes il ne sera plus possible d'inverser les coordonnées de texture. Imaginez-vous en train de modifier 20 000 coordonnées de texture à la main. :lol:

L'avantage d'inverser les pixels au moment du chargement c'est qu'il nous suffit d'une méthode pour inverser toutes nos textures. Avouez quand même que c’est un gain de temps énorme, si ce n'est astronomique !

Au niveau de la théorie, il faut juste de créer une copie conforme de la première surface SDL (celle qui contient l'image) puis d'inverser ses pixels. C'est grâce à une boucle que nous pourrons effectuer cette opération.

Cette boucle devra être capable de copier chaque ligne de la texture et d'inverser la position de celle-ci par rapport à l'axe des ordonnées. Et quand je dis copier chaque ligne, je parle de copier TOUS les pixels de la ligne SANS CHANGER L'ORDRE. En effet, les pixels présents sur l'axe des abscisses sont, eux, dans le bon ordre, il n'y que sur l'axe des ordonnées que ça ne va pas. Il faut conserver l'ordre des pixels sur chaque ligne mais inverser ces fameuses lignes par rapport à l'axe des ordonnées :

Image utilisateur

Essayez de bien comprendre cette notion. Revoyez le rendu de texture de la sous-partie précédente et vous verrez que sur l'axe des abscisses les pixels sont dans le bon ordre mais pas sur l'axe des ordonnées.

La méthode inverserPixels

Pour régler notre problème de texture, nous avons donc choisi d'inverser les pixels d'une image avant son chargement du coté OpenGL.

Pour faire ça proprement, on va déclarer une méthode inverserPixels() dans laquelle nous placerons notre code d’inversement. Cette méthode devra prendre en paramètre la surface SDL à inverser et elle renverra une autre surface SDL contenant l'image inversée.

SDL_Surface* inverserPixels(SDL_Surface *imageSource) const;

L'implémentation de cette méthode va être rapide, on commence par créer une copie conforme de la surface SDL envoyée en paramètre. Pour créer les surfaces avec la SDL, on va utiliser une fonction que vous devez déjà connaitre si vous avez suivi le tuto de M@téo dessus : la fonction SDL_CreateRGBSurface() :

SDL_Surface* SDL_CreateRGBSurface(Uint32 flags, int width, int  height, int depth, Uint32 Rmask, Uint32 Gmask, Uint32 Bmask, Uint32 Amask)

Houla ! Ça en fait des paramètres tout ça. Si vous connaissez la SDL vous devriez au moins connaitre les 4 premiers. :p Faisons tout de même le point autour des paramètres :

  • flags : Option permettant notamment de stocker l'image dans la carte graphique. On lui donnera la valeur 0. Avec la SDL 1.2, nous aurions pu lui donner la valeur SDL_HWSURFACE, mais celle-ci n'est plus utilisée avec la version 2.0

  • width : Largeur de l'image. On lui donnera la valeur du champ w

  • height : Hauteur de l'image. On lui donnera la valeur du champ h

  • depth : Profondeur de l'image, on lui donnera la valeur du champ BitsPerPixel, qui n'est pas à confondre avec le champ BytesPerPixel attention !

  • Rmask, Gmask, Bmask, Amask : Paramètres un peu spéciaux qui concerne le masque des couleurs. On leur donnera les champs respectifs de la structure SDL_Surface.

Avec cette fonction, nous disposerons d'une nouvelle surface identique à la première, excepté le fait qu'elle ne contiendra encore aucun pixel.

Si on implémente cette fonction, ça nous donne :

SDL_Surface* Texture::inverserPixels(SDL_Surface *imageSource) const
{
    // Copie conforme de l'image source sans les pixels

    SDL_Surface *imageInversee = SDL_CreateRGBSurface(0, imageSource->w, imageSource->h, imageSource->format->BitsPerPixel, imageSource->format->Rmask, 
                                                         imageSource->format->Gmask, imageSource->format->Bmask, imageSource->format->Amask);
}

On a vu dans la partie théorique que l'objectif de cette méthode est de pouvoir copier chaque ligne d'une texture afin d'inverser leur position sur l'axe des ordonnées, ceci à l'aide d'une boucle. En code "grossier" ça va nous donner ça :

for(int i(0); i < hauteur; i++)
{
    for(int j(0); j < largeur; j++)
        imageInversee->pixels[hauteur - i][j] = imageSource->pixels[i][j];
}

Pas de panique je vous explique. :)

On remarque que grâce à cette boucle, chaque pixel présent sur la largeur de la texture (à l'indice j) sera copié exactement au même endroit dans l'image finale à l'indice j (vu qu'ils sont dans le bon ordre sur la largeur). Par exemple, un pixel présent en j = 10ième position dans l'image source sera copié au même endroit dans l'image finale en j = 10ième position.

En revanche, toute la ligne se retrouve inversée par rapport à sa position initiale : la ligne qui était présente en position i sur l'image source se retrouvera en position hauteur - i. Par exemple prenons une image 256x256, la ligne qui se trouvait en 5ième position se retrouvera en [256 - 5] = 251ième position, la ligne s'est retrouvé en haut de l'image.

Si nous pouvions écrire directement cette boucle dans notre code ça serait le rêve. Cependant il existe un gros problème : le champ pixels de la structure SDL_Surface n'est pas un tableau à deux dimensions, il est donc impossible d'utiliser les doubles cases [i][j]. :(

Néanmoins, il existe une façon de contrer cela dans les tableaux à une dimension. En effet :

  • Un pixel du type [i][j] peut être remplacé par un indice du type [(largeur * i) + j] et

  • Le pixel inverse de type [hauteur - i][j] peut être remplacé par [(largeur * (hauteur - i)) + j]

Par exemple, si on veut inverser un pixel situé à la position [5][10] sur l'image 256x256, on devra :

  • Récupérer l'indice source en position [(256 * 5) + j] = [1280 + 10] = [1290]

  • Que l'on copiera dans l'image finale en position [(256 * (256 - 5)) + 10] = [(256 * 251) + 10] = [64256] = [64266]

Oui oui, ces chiffres donnent le tournis mais on a bien inversé notre pixel dans le bon sens. :p

Avec cette méthode, on récupère bien notre pixel même si on est en présence d'un tableau à une dimension. :D Du coup, la boucle précédente devient :

for(int i(0); i < hauteur; i++)
{
    for(int j(0); j < largeur; j++)
        imageInversee->pixels[(largeur * i) + j] = imageSource->pixels[(largeur * (hauteur - i)) + j];
}

On est maintenant prêt pour coder notre méthode. :D Dernier petit détail cependant, je vous rappelle qu'en C++ il est malheureusement interdit d'utiliser un tableau contenu dans un pointeur (exemple : imageSource->pixels[56]), il va donc falloir passer par des tableaux intermédiaires pour manipuler les pixels :

SDL_Surface* Texture::inverserPixels(SDL_Surface *imageSource) const
{
    // Copie conforme de l'image source sans les pixels

    SDL_Surface *imageInversee = SDL_CreateRGBSurface(0, imageSource->w, imageSource->h, imageSource->format->BitsPerPixel, imageSource->format->Rmask, 
                                                         imageSource->format->Gmask, imageSource->format->Bmask, imageSource->format->Amask);


    // Tableau intermédiaires permettant de manipuler les pixels

    unsigned char* pixelsSources = (unsigned char*) imageSource->pixels;
    unsigned char* pixelsInverses = (unsigned char*) imageInversee->pixels;
}

Reprenons le grossier code de la boucle pour la convertir en code fonctionnel :

// Inversion des pixels

for(int i = 0; i < imageSource->h; i++)
{
    for(int j = 0; j < imageSource->w; j++)
        pixelsInverses[(imageSource->w * (imageSource->h - 1 - i)) + j] = pixelsSources[(imageSource->w * i) + j];
}

Remarquez le -1 dans l'indice de l'image inversée [(image->w * (imageSource->h - 1 - i)) + j], je vous rappelle que l'on travaille avec des tableaux donc les indices partent de 0 pour arriver à hauteur- 1. Je n'en parle que maintenant car je ne voulais pas surcharger la boucle précédente. Déjà que la notion n'est pas évidente à intégrer. :)

Nous avons pratiquement terminé notre boucle, il ne manque plus qu'une seule chose : jusqu'à maintenant, cette boucle permettait d'échanger des pixels entre eux, mais je vous rappelle qu'un pixel est composé de 3 couleurs. Pour le moment, nous n'échangeons qu'un tiers de la texture, et encore cet échange est complétement buggé !

Pour pallier à ce problème, il suffit juste de multiplier par 3 ou par 4 (pour la composante alpha) la boucle qui parcourt la ligne, car chaque ligne du tableau fait en réalité 3 ou 4 fois la longueur de l'image :

Image utilisateur
Image utilisateur

Dans notre code, il suffit de rajouter un "* imageSource->format->BytesPerPixel" partout où vous trouvez le champ imageSource->w (y compris dans la boucle), soit 3 fois normalement :

// Inversion des pixels

for(int i = 0; i < imageSource->h; i++)
{
    for(int j = 0; j < imageSource->w * imageSource->format->BytesPerPixel; j++)
        pixelsInverses[(imageSource->w * imageSource->format->BytesPerPixel * (imageSource->h - 1 - i)) + j] = pixelsSources[(imageSource->w * imageSource->format->BytesPerPixel * i) + j];
}

Avec cette boucle, nous pourrons inverser tous les pixels d'une image et ainsi les afficher à l'endroit dans nos scènes 3D. ;)

Pour terminer la méthode, il ne nous manque plus qu'à retourner la nouvelle surface SDL :

// Retour de l'image inversée

return imageInversee;

Ce qui nous donne au final :

SDL_Surface* Texture::inverserPixels(SDL_Surface *imageSource) const
{
    // Copie conforme de l'image source sans les pixels

    SDL_Surface *imageInversee = SDL_CreateRGBSurface(0, imageSource->w, imageSource->h, imageSource->format->BitsPerPixel, imageSource->format->Rmask, 
                                                         imageSource->format->Gmask, imageSource->format->Bmask, imageSource->format->Amask);


    // Tableau intermédiaires permettant de manipuler les pixels

    unsigned char* pixelsSources = (unsigned char*) imageSource->pixels;
    unsigned char* pixelsInverses = (unsigned char*) imageInversee->pixels;


    // Inversion des pixels

    for(int i = 0; i < imageSource->h; i++)
    {
        for(int j = 0; j < imageSource->w * imageSource->format->BytesPerPixel; j++)
            pixelsInverses[(imageSource->w * imageSource->format->BytesPerPixel * (imageSource->h - 1 - i)) + j] = pixelsSources[(imageSource->w * imageSource->format->BytesPerPixel * i) + j];
    }


    // Retour de l'image inversée

    return imageInversee;
}
Adaptation dans la méthode charger

Maintenant que nous sommes capables d'inverser le sens d'une image, nous pouvons intégrer la nouvelle méthode dans le chargement de texture. Il nous suffit d'appeler la méthode inverserPixels() juste après avoir chargé la surface SDL. Bien évidemment, on pense à détruire l'ancienne surface qui ne sert plus rien :

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());

    if(imageSDL == 0)
    {
        printf("IMG_Load: %s\n", IMG_GetError());
        return false;
    }


    // Inversion de l'image

    SDL_Surface *imageInversee = inverserPixels(imageSDL);
    SDL_FreeSurface(imageSDL);


    // ....
}

Il ne reste plus qu'à remplacer toutes les occurrences du pointeur imageSDL par le pointeur imageSDLInversee (Vous devriez trouver 9 occurrences à remplacer). Ce qui donne au final :

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());

    if(imageSDL == 0)
    {
        std::cout << "Erreur : " << SDL_GetError() << std::endl;
        return false;
    }


    // Inversion de l'image

    SDL_Surface *imageInversee = inverserPixels(imageSDL);
    SDL_FreeSurface(imageSDL);


    // Génération de l'ID

    glGenTextures(1, &m_id);


    // Verrouillage

    glBindTexture(GL_TEXTURE_2D, m_id);


    // Format de l'image

    GLenum formatInterne(0);
    GLenum format(0);


    // Détermination du format et du format interne pour les images à 3 composantes

    if(imageInversee->format->BytesPerPixel == 3)
    {
        // Format interne

        formatInterne = GL_RGB;


        // Format

        if(imageInversee->format->Rmask == 0xff)
            format = GL_RGB;

        else
            format = GL_BGR;
    }


    // Détermination du format et du format interne pour les images à 4 composantes

    else if(imageInversee->format->BytesPerPixel == 4)
    {
        // Format interne

        formatInterne = GL_RGBA;


        // Format

        if(imageInversee->format->Rmask == 0xff)
            format = GL_RGBA;

        else
            format = GL_BGRA;
    }


    // Dans les autres cas, on arrête le chargement

    else
    {
        std::cout << "Erreur, format interne de l'image inconnu" << std::endl;
        SDL_FreeSurface(imageInversee);

        return false;
    }


    // Copie des pixels

    glTexImage2D(GL_TEXTURE_2D, 0, formatInterne, imageInversee->w, imageInversee->h, 0, format, GL_UNSIGNED_BYTE, imageInversee->pixels);


    // Application des filtres

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);


    // Déverrouillage

    glBindTexture(GL_TEXTURE_2D, 0);


    // Fin de la méthode

    SDL_FreeSurface(imageInversee);
    return true;
}

Et voila nous pouvons enfin charger des textures dans le bon ordre. :D Essayez avec la texture de la caisse de missiles, vous verrez qu'elle s'affiche désormais à l'endroit :

Image utilisateur

Les méthodes additionnelles

On va terminer tranquillement ce chapitre par quelques méthodes importantes qu'il faut implémenter, à savoir : le constructeur par défaut, celui de copie, le destructeur et la surcharge de l'opérateur =.

Ces méthodes sont importantes pour assurer le bon cycle de vie de la classe Texture. Le constructeur par défaut par exemple est utile lorsque vous souhaitez initialiser tout un tableau de textures, l'opérateur = quant à lui sert à copier une texture facilement en une ligne de code, etc ...

Le constructeur par défaut et le destructeur

On va commencer tout de suite par implémenter le constructeur par défaut et le destructeur.

Je vous rappelle que le constructeur par défaut est appelé lorsque vous ne donnez aucun paramètre à votre objet. Son prototype est le suivant :

Texture();

Son rôle ne lui permet que d'initialiser les deux attributs de la classe. Il va affecter l'ID 0 à l'attribut m_id et une chaine vide à m_fichierImage :

Texture::Texture() : m_id(0), m_fichierImage("")
{

}

Occupons maintenant du destructeur. Cette "méthode" est appelée à chaque fois qu'un objet est détruit, c'est notamment ici que nous devrons détruire nos textures. Car oui, même les objets OpenGL doivent être détruits pour libérer la mémoire qu'ils occupent.

Les objets OpenGL sont alloués dynamiquement dans la carte graphique par la fonction glGenTextures(), il va nous falloir libérer l'espace occupé pour éviter les fuites de mémoire. Pour ça, OpenGL nous fournit une fonction qui reprend exactement les mêmes paramètres que glGenTextures(). Voici son prototype :

void glDeleteTextures(GLsizei number, const GLuint *textures);
  • number : Le nombre d'ID à initialiser. Comme pour la génération, nous lui donnerons la valeur 1

  • textures : Un tableau de type GLuint ou une adresse d'ID. Nous lui donnerons l'adresse de l'ID à détruire

Nous utiliserons cette fonction dans notre destructeur dont j'espère que vous connaissez son utilité. :p

Il est appelé au moment de la destruction d'un objet pour permettre au développeur de libérer toute la mémoire qu'il avait prise. D'ailleurs, nous avions déjà déclaré cette "méthode" précédemment, il ne nous reste donc plus qu'à utiliser la fonction glDeleteTextures() à l'intérieur :

Texture::~Texture()
{
    // Destruction de la texture

    glDeleteTextures(1, &m_id);
}

Et voila, nos textures seront maintenant détruites proprement lorsqu'elles ne seront plus utilisées. :)

Copier une texture

On passe maintenant à la copie de textures. Mais avant ça, je vais d'abord vous faire une petite parenthèse sur le chargement des textures dans un vrai jeux-vidéo.

Dans un jeu-vidéo lambda, le moteur 3D doit être capable de charger toutes les textures dont il a besoin dans un espèce de gros tableau qui doit être accessible par toutes les classes :

Image utilisateur

A partir de là, chaque modèle 3D (comme un personnage ou un arbre) va récupérer un pointeur sur la texture dont il aura besoin, ce n'est donc pas lui qui va charger sa propre texture. Et de plus, si vous avez une dizaine d'ennemis qui doit s'afficher à l'écran, vous n'allez pas charger 10 fois les mêmes textures pour les 10 ennemis. En théorie, chaque personnage devrait récupérer un pointeur sur la texture dont il a besoin dans le gros tableau pour l'utiliser. De cette façon, les textures ne sont chargées qu'une seule fois et ça économise pas mal de ressources.

Je vous parle de ça maintenant car c'est ce que nous "devrions faire" en réalité. Cependant nos scènes étant encore assez simples nous pouvons nous permettre de charger une texture plusieurs fois sans se préoccuper de créer le gros tableau. :)

Bien, ceci étant dit on va pouvoir retourner à nos méthodes de copie.

Il existe grosso-modo deux façons de copier une texture : soit on utilise le constructeur de copie, soit on surcharge l'opérateur =. La différence entre les deux est que si vous initialisez une texture à partir d'une autre texture, alors ce sera le constructeur de copie qui sera appelé. En revanche, si vous le faites après sa déclaration se sera l'opérateur = qui sera sollicité.

On va commencer l'implémentation de ces deux méthodes par le constructeur de copie dont voici le prototype :

Texture(Texture const &textureACopier);

Le constructeur prend en paramètre une référence constante sur l'objet à copier, dans notre cas une texture.

Petit point important : si nous avons besoin de coder nous-même ce constructeur c'est parce qu'il y a, en général, un problème au niveau des pointeurs lors de la copie d'un objet. En effet, si on dispose de deux copies d'une même texture alors elles partagerons le même ID. Or, si on détruit l'une des deux textures, la deuxième se retrouvera avec un ID invalide et donc une erreur d'affichage.

Alors bon, il est vrai que dans notre classe l'ID n'est qu'une simple variable. Mais pour votre carte graphique cet ID représente un pointeur pointant sur la texture chargée.

Par conséquent, pour copier proprement deux textures il va falloir créer un nouvel ID indépendant du premier. De cette façon, même si un objet est détruit, la copie ne sera pas affectée.

Dans la classe Texture, nous devrons donc :

  • Copier l'attribut m_fichierImage qui contient le chemin vers le fichier image

  • Recharger la texture pour avoir un nouvel ID indépendant du premier

Le constructeur de copie ressemblera donc à ceci :

Texture::Texture(Texture const &textureACopier)
{
    // Copie de la texture

    m_fichierImage = textureACopier.m_fichierImage;
    charger();
}

Rien de plus facile isn't it ? :lol:

D'ailleurs on va s'occuper maintenant de coder la surcharge de l'opérateur = car le code ne change pas vraiment. Le prototype de cette surcharge sera le suivant :

Texture& operator=(Texture const &textureACopier);

Pour cette méthode, on copie simplement le même code que le constructeur de copie puis on renvoie le pointeur *this :

Texture& Texture::operator=(Texture const &textureACopier)
{
    // Copie de la texture

    m_fichierImage = textureACopier.m_fichierImage;
    charger();


    // Retour du pointeur *this

    return *this;
}

Il manque encore un tout petit détail à cette méthode - un détail qui s'étend même au chargement en général. Imaginez qu'une texture soit déjà chargée et que l'on copie une autre texture dans celle-ci, que se passerait-il ?

Et bien le premier ID qui a été initialisé sera perdu, et du coup la texture qui a été chargée en mémoire sera elle aussi perdue. C'est encore une fuite. Pour régler ce petit problème, il faut simplement détruire l'ancien ID de la même façon qu'avec le destructeur. Dans le pire des cas, même si la texture n'était pas chargée avant, ce sera l'ID 0 qu'OpenGL essaiera de détruire. Mais vu que c'est impossible il passera à autre chose sans nous renvoyer d'erreur.

Si j'ai dit que ce problème s'étendait aussi au chargement de texture en général, c'est parce que nous avons exactement le même problème lorsque nous utilisons la méthode charger(). En effet, si nous utilisons cette méthode deux fois sur le même objet, alors le premier chargement qui a été fait sera perdu en mémoire.

Pour régler ce problème, nous allons rajouter un petit bout de code dans la méthode charger() juste avant de générer l'identifiant. Ce code sera constitué d'une condition if qui permettra de savoir si une texture a déjà été chargée. Si oui, alors il faudra la détruire avant de la recharger.

La fonction qui permet de savoir ceci s'appelle glIsTexture() :

GLboolean glIsTexture(GLuint texture);

Elle ne prend en paramètre que l'identifiant de la texture à vérifier. Elle renvoie la valeur GL_TRUE si elle a déjà été chargée et GL_FALSE dans le cas contraire.

Nous devons donc appeler cette fonction avant de générer l'ID. Si elle renvoie GL_TRUE alors il faudra détruire la texture à l'aide de glDeleteTextures() :

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());

    if(imageSDL == 0)
    {
        std::cout << "Erreur : " << SDL_GetError() << std::endl;
        return false;
    }


    // Inversion de l'image

    SDL_Surface *imageInversee = inverserPixels(imageSDL);
    SDL_FreeSurface(imageSDL);


    // Destruction d'une éventuelle ancienne texture

    if(glIsTexture(m_id) == GL_TRUE)
        glDeleteTextures(1, &m_id);


    .....

Et voila, nos méthodes de copie sont maintenant complètes et prêtes à l'emploi. :D

Aller plus loin

Répéter une texture

Fonctionnement

Maintenant que l'on peut afficher une texture dans le bon sens, je vais vous montrer la technique utilisée pour répéter une texture plusieurs fois. Si vous avez déjà joué à un jeu-vidéo, vous avez peut-être remarqué que les textures au sol se répétait en boucle.

On peut prendre l'exemple d'un sol composé d'herbe :

Image utilisateur

Si vous vous concentrez sur cette image, vous remarquerez qu'elle est composée d'une seule petite texture qui se répète en boucle.

Si un jeu-vidéo devait afficher ce sol et qu'il s'amuserait à charger la même texture pour chaque carré d'herbe, alors il deviendrait totalement injouable et serait beaucoup trop lent. A la place, il se contentera de la charger une seule fois puis il utilisera des coordonnées de texture 'spéciales' pour la répéter :

Image utilisateur
Image utilisateur

Cette technique est utilisée par tous les jeux-vidéo. Sans elle, il n'y en aurait pas beaucoup d'ailleurs car ils seraient tous terriblement lents à l'affichage. :p

Bon en réalité les coordonnées utilisées n'ont rien de spéciales, elles ne sont juste pas comprises entre 0 et 1.

Hein ? Mais tu as dit que les coordonnées de texture devaient toujours être comprises entre 0 et 1 ?

Oui et c'est totalement vrai !

Si vous allez au-delà de 1 (2 par exemple) ou en dessous de 0 alors vous répéterez la même texture :

Image utilisateur

Les coordonnées sont toujours comprises entre 0 et 1 mais au lieu de renvoyer une erreur, OpenGL recopiera la texture. Bien évidemment, plus vous fournissez des valeurs importantes, plus votre texture sera répétée :

Image utilisateur

Ce n'est absolument pas un bug puisque c'est prévu par OpenGL et c'est même LA technique à utiliser pour répéter vos textures que ce soit sur les sols, les murs, ... Nous ferons comme ça dans le futur pour afficher de grands espaces. :)

Le seul point à prendre en compte c'est qu'il faut que votre texture soit prévue pour la répétition, si elle ne l'est pas vous aurez un rendu assez moche et pas continu. En général, ce point concerne surtout le graphiste et non le développeur (une bonne raison de lui râler dessus si ça ne va pas :p )

Vous pouvez essayer de voir ce que ça donne avec la texture que l'on utilise dans ce chapitre. Remplacez les coordonnées par celles-ci pour l'affiche 12 fois !

// Coordonnées de texture

float coordTexture[] = {0, 0,   4, 0,   4, 4,     // Triangle 1
                        0, 0,   0, 4,   4, 4};    // Triangle 2
Image utilisateur
Exercice

La répétition de texture est une occasion parfaite pour moi de vous proposer un petit exercice à faire.

En reprenant ce qu'on vient de voir, essayez de recréer le sol constitué d'herbe que je vous ai montré au-dessus. Les consignes sont les suivantes :

  • Vous ne devez utiliser que 4 vertices (fournis dans le schéma ci-dessous)

  • Répéter la texture 49 fois (7 fois en largeur et en hauteur)

Vous pouvez télécharger l'image à afficher un peu plus haut ou si vous avez téléchargé l’archive, vous la retrouverez sous le nom de "veg005.jpg".

Voici le schéma explicatif :

Image utilisateur

Et voici ce que vous devriez obtenir (en plaçant votre caméra un peu différemment pour mieux voir) :

Image utilisateur

Bonne chance. ;) (et n’oubliez pas de verrouiller votre texture !)

Solution

La solution de cette exercice est assez simple, nous n'avons que quelques lignes de code à modifier par rapport à l'exemple que l'on a vu.

En premier, il fallait définir les vertices. Nous faisons des carrés depuis un moment maintenant il n'aurait pas dû y avoir de problèmes :

// Vertices

float vertices[] = {-10, 0, -10,   10, 0, -10,   10, 0, 10,    // Triangle 1
                    -10, 0, -10,   -10, 0, 10,   10, 0, 10};   // Triangle 2

N'oubliez pas qu'il fallait afficher un sol, les vertices étaient donc tous à la hauteur 0.

Ensuite, il fallait définir les coordonnées de texture. Nous avons vu que si on allait plus que 1 dans leurs valeurs alors on attaquait un autre affichage de la même texture. Il fallait l'afficher 7 fois en largeur et en hauteur, donc les coordonnées maximales à utiliser étaient respectivement [7 ; 0] et [0 ; 7].

On peut représenter la situation avec le schéma suivant :

Image utilisateur

Le tableau coordTexture à utiliser était donc le suivant :

// Coordonnées de texture

float coordTexture[] = {0, 0,   7, 0,   7, 7,     // Triangle 1
                        0, 0,   0, 7,   7, 7};    // Triangle 2

Une fois les données définies, il ne manquait plus qu'à déclarer un objet Texture qui permettait de charger l'image en mémoire :

// Vertices et coordonnées de texture

....


// Texture

Texture texture("Textures/Herbe.jpg");
texture.charger();

Au niveau du code d'affichage, il fallait juste reprendre celui que l'on utilisait précédemment en faisant attention de bien verrouiller la texture au moment d'appeler la fonction glDrawArrays(). Le reste du code permettait, entre autres, d'envoyer les vertices, les coordonnées de texture et les matrices à OpenGL :

// Activation du shader

glUseProgram(shaderTexture.getProgramID());


    // Envoi des vertices

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


    // Envoi des coordonnées de texture

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


    // Envoi des matrices

    glUniformMatrix4fv(glGetUniformLocation(shaderTexture.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));
    glUniformMatrix4fv(glGetUniformLocation(shaderTexture.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));


    // Verrouillage de la texture

    glBindTexture(GL_TEXTURE_2D, texture.getID());


    // Rendu

    glDrawArrays(GL_TRIANGLES, 0, 6);


    // Déverrouillage de la texture

    glBindTexture(GL_TEXTURE_2D, 0);


    // Désactivation des tableaux

    glDisableVertexAttribArray(2);
    glDisableVertexAttribArray(0);


// Désactivation du shader

glUseProgram(0);

Afficher une caisse

Pour clore ce chapitre d'une façon intéressante, nous allons appliquer ce que l'on a vu sur les textures non plus sur une forme simple 2D mais bien sur un modèle 3D comme un cube. Vu que nous avons utilisé une texture de caisse jusqu'ici, nous pouvons légitimement l'utiliser pour transformer notre cube coloré à l'ancienne en super caisse de missiles. :D

Notre objectif final est d'être capables d'avoir ce rendu :

Image utilisateur

Pour arriver à cela, nous allons coder une classe Caisse qui contiendra tout ce qu'il faut pour afficher un cube et le texturer. Pour nous faciliter la tâche, nous la ferons hériter de la classe Cube afin de ne pas avoir à refaire l'initialisation des vertices et du shader. ;)

L'héritage de la classe Cube

Avant de se lancer dans la classe Caisse, nous allons devoir régler un petit problème d'héritage au niveau du cube qui nous empêche de profiter de ses attributs. En effet, si on regarde son header, on remarque que ces derniers sont tous privés à cause de l'utilisation du mot-clef private :

#ifndef DEF_CUBE
#define DEF_CUBE


// Includes 

....


// Classe Cube

class Cube
{
    public:

    Cube(float taille, std::string const vertexShader, std::string const fragmentShader);
    ~Cube();


    private:

    Shader m_shader;
    float m_vertices[108];
    float m_couleurs[108];
};

#endif

Pour prétendre à un héritage, il faut changer ce mot-clef en le remplaçant par son confrère protected :

#ifndef DEF_CUBE
#define DEF_CUBE


// Includes 

....


// Classe Cube

class Cube
{
    public:

    Cube(float taille, std::string const vertexShader, std::string const fragmentShader);
    ~Cube();


    protected:

    Shader m_shader;
    float m_vertices[108];
    float m_couleurs[108];
};

#endif

De cette façon, nous pouvons utiliser les trois attributs présents ici dans les classes filles.

Le gros avantage pour nous, c'est que le shader et surtout les vertices seront déjà initialisés sans même que nous ayons à faire quoi que ce soit. Il ne nous restera plus qu'à gérer les nouveaux attributs. Entre parenthèses, le tableau de couleurs ne sera pas utile pour la suite mais mieux vaut le garder sous la main au cas où.

Le header de la classe Caisse

Comme toute implémentation de classe, nous devons commencer par le header. Celui de la classe Caisse sera assez similaire à celui du Cube puisqu'il comportera les mêmes méthodes ainsi qu'un tableau à initialiser dans le constructeur.

Le début du fichier, que nous appellerons Caisse.h, contiendra la définition de la classe accompagnée du code ":publicCube" qui lui permettra de profiter de l'héritage :

#ifndef DEF_CAISSE
#define DEF_CAISSE


// Includes

#include "Cube.h"


// Classe Caisse

class Caisse : public Cube
{
    public:


    private:
};

#endif

Pour agrémenter un peu ce code, nous allons ajouter les attributs nécessaires à la réalisation de notre caisse. Sachant que les vertices et le shader sont déjà fournis, nous n'en avons donc besoin que de deux supplémentaires :

  • Un objet Texture qui représentera l'image à plaquer

  • Un tableau de float qui représentera les coordonnées à associer aux vertices

Petit précision pour le tableau, celui-ci contiendra 72 cases car il faut associer 2 coordonnées à chaque vertex, ce qui donne 36 vertices x 2 coordonnées = 72 cases.

// Attributs

Texture m_texture;
float m_coordTexture[72];

Passons maintenant aux méthodes dont nous aurons besoin. Hormis celle nécessaire pour afficher le rendu final, nous n'aurons besoin que du constructeur et du destructeur.

Le premier reprendra les mêmes paramètres que celui de la classe Cube afin de pouvoir lui donner les valeurs qu'il attend au moment de l'initialisation. Ces paramètres représentaient la taille désirée ainsi que les codes sources du shader à utiliser.

Nous en rajouterons un dernier, spécifique au constructeur Caisse(), qui représentera l'image à plaquer sur les faces de la caisse et qui permettra surtout d'initialiser l'attribut m_texture.

Le prototype du constructeur au final est le suivant :

Caisse(float taille, std::string const vertexShader, std::string const fragmentShader, std::string const texture);

On en profite au passage pour déclarer le destructeur (qui est quand même plus simple à faire :p ) :

~Caisse();

Si on réunit tout ça dans le header, on devrait avoir :

#ifndef DEF_CAISSE
#define DEF_CAISSE


// Includes

#include "Cube.h"
#include "Texture.h"
#include <string>


// Classe Caisse

class Caisse : public Cube
{
    public:

    Caisse(float taille, std::string const vertexShader, std::string const fragmentShader, std::string const texture);
    ~Caisse();


    private:

    Texture m_texture;
    float m_coordTexture[72];
};

#endif
Le constructeur

Le constructeur va être un poil différent de ceux que nous avons l'habitude d'implémenter. En effet, avant d’initialiser n'importe quel attribut, nous devons faire appel au constructeur parent de la classe Caisse comme nous le demande le C++.

Nous l'appelons donc juste après les deux points ":" en lui passant les trois premiers paramètres reçus (la taille et les codes sources shader) :

Caisse::Caisse(float taille, std::string const vertexShader, std::string const fragmentShader, std::string const texture) : Cube(taille, vertexShader, fragmentShader)
{

}

Maintenant que la classe-mère est prête, nous pouvons nous occuper de nos attributs. Le seul que l'on peut initialiser après les deux points est la texture m_texture, l'autre étant un tableau il doit être rempli entre les accolades.

Nous donnerons à m_texture la string reçue en paramètre pour lui spécifier l'image à charger. Nous appellerons également sa méthode charger() sinon elle risque d'être un peu vide dans notre rendu. :p

Caisse::Caisse(float taille, std::string const vertexShader, std::string const fragmentShader, std::string const texture) : Cube(taille, vertexShader, fragmentShader),
                                                                                                                            m_texture(texture)
{
    // Chargement de la texture

    m_texture.charger();
}

Pour initialiser le second attribut, m_coordTexture, nous allons reprendre le principe que nous avons utilisé lors de la création du tableau de couleurs. C'est-à-dire que nous devrons faire correspondre un couple de coordonnées de texture à chaque vertex comme nous l'avons fait précédemment.

Pour la première face de la caisse par exemple, nous pouvons reprendre celles que nous avons utilisées pour le carré :

// Coordonnées de texture

float coordTextureTmp[] = {0, 0,   1, 0,   1, 1,     // Face 1
                           0, 0,   0, 1,   1, 1};    // Face 1

L'avantage d'un cube, c'est que toutes ses faces ne sont en fait que des carrés. Du coup, nous pouvons reprendre les coordonnées de texture de la première face pour les assigner à toutes les autres. Nous n'avons pas besoin de les déterminer manuellement. Le tableau temporaire devient donc :

// Coordonnées de texture temporaires

float coordTextureTmp[] = {0, 0,   1, 0,   1, 1,     // Face 1
                           0, 0,   0, 1,   1, 1,     // Face 1

                           0, 0,   1, 0,   1, 1,     // Face 2
                           0, 0,   0, 1,   1, 1,     // Face 2

                           0, 0,   1, 0,   1, 1,     // Face 3
                           0, 0,   0, 1,   1, 1,     // Face 3

                           0, 0,   1, 0,   1, 1,     // Face 4
                           0, 0,   0, 1,   1, 1,     // Face 4

                           0, 0,   1, 0,   1, 1,     // Face 5
                           0, 0,   0, 1,   1, 1,     // Face 5

                           0, 0,   1, 0,   1, 1,     // Face 6
                           0, 0,   0, 1,   1, 1};    // Face 6

On a l'impression de travailler avec des couleurs tellement que ça y ressemble. ;)

Une fois les coordonnées définies, il ne manque plus qu'à les copier dans l'attribut m_coordTexture à l'aide d'une boucle for :

// Copie des valeurs dans le tableau final

for(int i (0); i < 72; i++)
    m_coordTexture[i] = coordTextureTmp[i];

Si on résume tout ça :

Caisse::Caisse(float taille, std::string const vertexShader, std::string const fragmentShader, std::string const texture) : Cube(taille, vertexShader, fragmentShader),
                                                                                                                            m_texture(texture)
{
    // Chargement de la texture

    m_texture.charger();


    // Coordonnées de texture temporaires

    float coordTextureTmp[] = {0, 0,   1, 0,   1, 1,     // Face 1
                               0, 0,   0, 1,   1, 1,     // Face 1

                               0, 0,   1, 0,   1, 1,     // Face 2
                               0, 0,   0, 1,   1, 1,     // Face 2

                               0, 0,   1, 0,   1, 1,     // Face 3
                               0, 0,   0, 1,   1, 1,     // Face 3

                               0, 0,   1, 0,   1, 1,     // Face 4
                               0, 0,   0, 1,   1, 1,     // Face 4

                               0, 0,   1, 0,   1, 1,     // Face 5
                               0, 0,   0, 1,   1, 1,     // Face 5

                               0, 0,   1, 0,   1, 1,     // Face 6
                               0, 0,   0, 1,   1, 1};    // Face 6


    // Copie des valeurs dans le tableau final

    for(int i (0); i < 72; i++)
        m_coordTexture[i] = coordTextureTmp[i];
}

Le constructeur initialise maintenant tous nos attributs, même ceux de sa classe-mère puisqu'il fait appel aussi à son constructeur.

Le destructeur

On passe rapidement sur le destructeur qui restera vide au même titre que celui du cube :

Caisse::~Caisse()
{

}
La méthode afficher()

La méthode afficher() va faire exactement la même chose que sa méthode "parente" à savoir afficher notre caisse à l'écran. Elle aura besoin des matrices projection et modelview en paramètres afin de pouvoir les envoyer à son shader :

void afficher(glm::mat4 &projection, glm::mat4 &modelview);

Petit détail : Nous faisons ici ce qu'on appelle le masquage de méthode, c'est une notion dont M@téo parle dans son tuto sur le C++. ;)

Son implémentation ne va pas nous poser de problème puisqu'il suffit de recopier le code que nous avons utilisé pour afficher le carré. Nous avons juste une petite modification à faire au niveau de la fonction glDrawArrays(). Celle-ci ne prendra pas en compte 6 mais 36 vertices, soit le nombre requis pour un cube comme nous avons eu l'occasion de le voir.

Il faut également changer le nom des variables utilisées comme :

  • Le shader qui devient m_shader

  • Les tableaux qui deviennent m_vertices et m_coordTexture

  • La texture qui devient m_texture

Le code de base à recopier est le suivant :

// Activation du shader

glUseProgram(shaderTexture.getProgramID());


    // Envoi des vertices

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


    // Envoi des coordonnées de texture

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


    // Envoi des matrices

    glUniformMatrix4fv(glGetUniformLocation(shaderTexture.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));
    glUniformMatrix4fv(glGetUniformLocation(shaderTexture.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));


    // Verrouillage de la texture

    glBindTexture(GL_TEXTURE_2D, m_texture.getID());


    // Rendu

    glDrawArrays(GL_TRIANGLES, 0, 36);


    // Déverrouillage de la texture

    glBindTexture(GL_TEXTURE_2D, 0);


    // Désactivation des tableaux

    glDisableVertexAttribArray(2);
    glDisableVertexAttribArray(0);


// Désactivation du shader

glUseProgram(0);

Une fois remanié et intégré dans la méthode afficher(), il ressemble à ceci :

void Caisse::afficher(glm::mat4 &projection, glm::mat4 &modelview)
{
    // Activation du shader

    glUseProgram(m_shader.getProgramID());


        // Envoi des vertices

        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, m_vertices);
        glEnableVertexAttribArray(0);


        // Envoi des coordonnées de texture

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


        // Envoi des matrices

        glUniformMatrix4fv(glGetUniformLocation(m_shader.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));
        glUniformMatrix4fv(glGetUniformLocation(m_shader.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));


        // Verrouillage de la texture

        glBindTexture(GL_TEXTURE_2D, m_texture.getID());


        // Rendu

        glDrawArrays(GL_TRIANGLES, 0, 36);


        // Déverrouillage de la texture

        glBindTexture(GL_TEXTURE_2D, 0);


        // Désactivation des tableaux

        glDisableVertexAttribArray(2);
        glDisableVertexAttribArray(0);


    // Désactivation du shader

    glUseProgram(0);
}

La classe Caisse est maintenant terminée. Nous n'avons plus qu'à la tester.

Afficher une caisse

Le plus dur est derrière nous maintenant, on peut facilement afficher un cube texturé en seulement 2 lignes de code. :p

Nous devons premièrement déclarer un objet Caisse dans la méthode bouclePrincipale(), pensez à inclure l'en-tête "Caisse.h". Nous lui donnerons en paramètre une taille de 2.0, le chemin vers les fichiers texture.vert et texture.frag ainsi vers la texture Caisse.jpg :

// Objet Caisse

Caisse caisse(2.0, "Shaders/texture.vert", "Shaders/texture.frag", "Textures/Caisse.jpg");

Ensuite, nous appelons sa méthode afficher() dans la boucle while en n'oubliant pas de lui donner les matrices projection et modelview :

// Boucle principale

while(!terminer)
{
    // Gestion des évènements, nettoyage de l'écran, ...

    ....


    // Affichage de la caisse

    caisse.afficher(projection, modelview);


    // Actualisation de la fenêtre

    ....
}

Si vous compilez votre projet, vous devriez avoir :

Image utilisateur

Vu que nous avons superbement bien coder notre classe ( ;) ), nous pouvons changer l'aspect de notre caisse en ne modifiant qu'un seul paramètre lors de sa création. Par exemple, si vous voulez avoir une caisse avec la texture suivante (crate12.jpg dans l'archive) :

Image utilisateur

Il vous suffit juste de copier cette image dans votre dossier "Textures" puis de l'utiliser dans votre déclaration d'objet :

// Objet Caisse

Caisse caisse(2.0, "Shaders/texture.vert", "Shaders/texture.frag", "Textures/Caisse2.jpg");
Image utilisateur

Vous pouvez même combiner la répétition de texture et notre caisse pour voir ce que ça donne. Vous devrez cependant faire translater votre cube pour ne pas qu'il transperce l'herbe. La valeur de la translation dépend de la taille de votre cube, celle-ci doit en faire la moitié. Si vous avez donné une taille de 2.0 unités alors la valeur sera de 1.0 :

// Sauvegarde de la matrice modelview

mat4 sauvegardeModelview = modelview;


    // Translation pour positionner le cube

    modelview = translate(modelview, vec3(0, 1, 0));


    // Affichage du cube

    caisse.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;
Image utilisateur

Télécharger : Code Source C++ du chapitre 10

Pfiou il était long ce chapitre. Je me demande encore si je n'aurais pas dû le couper en deux. :lol:

Au moins ça nous a permis d'en apprendre pas mal sur les textures car nous savons maintenant charger des images depuis le disque dur et les afficher dans nos scènes 3D. Nous allons abandonner petit à petit les couleurs au profit de ces nouvelles textures, cela nous permettra d'avoir un monde plus réaliste.

Nous avons également vu dans ce chapitre comment manipuler des objets OpenGL. Je vous ai mis un commentaire en vert en dessous de chaque fonction que nous reverrons plus tard dans le tuto. Vous verrez que ces objets se manipulent tous de la même façon. ;)

Exemple de certificat de réussite
Exemple de certificat de réussite