Article     Discussion     Modifier     Historique     Forums     Salon IRC

OpenGL:HeightMap

Un article de Games Creators Network.

Sommaire

[modifier] Heightmaps en C++ avec OpenGL

Dans cet article nous allons traiter d’un domaine phare de la programmation graphique, le rendu de terrain. Pour ce faire nous allons utiliser une méthode classique, celle dite des cartes de hauteur (ou Heightmaps). Cette méthode est particulièrement usitée dans les jeux, et propose une manière simple et intuitive d’approcher le problème.

[modifier] Principes des cartes de hauteur

Une carte de hauteur est en fait un simple fichier image, souvent en noir et blanc, qui représente le relief d’une région. Un exemple étant plus parlant qu’un long discours, contemplez la heightmap suivante :

image:Heightmap.gif

Sur cette carte, on peut distinguer des zones hautes (en blanc) et des zones basses (en noir). On peut donc reconnaître une vallée entourée de montagnes.

A partir de cet exemple, vous comprendrez qu’il serait pratique de disposer d’un tableau d’entiers à deux dimensions (largeur, longueur) qui contiendrait la hauteur (donc la couleur) de chaque point de notre carte. Il nous suffirait donc de matérialiser cette image sous forme d’un tableau.

[modifier] Choix d’un format

Le format le plus simple pour représenter notre carte de hauteur, est le format RAW (brut en anglais). Ce format est en fait le plus simple des formats graphiques possibles : Ces fichiers sont en fait une suite de nombres de 0 à 255. Le 0 représentant le noir, et 255 le blanc. Vous aurez remarquez que chacun de ces nombres peut être stocké sur 1 octet. Notre fichier sera donc une simple suite d’octets.

Cette représentation pose tout de même un petit problème, si nous disposons de 16 octets pour notre carte, celle-ci peut représenter une carte de 4x4, ou 2x8, 8x2 … par convention, nous utiliserons donc des cartes carrées, ainsi un fichier de taille 16384 octets représentera une carte de sqrt(16384) = 128 de côté (128x128 donc). Nous utiliserons aussi des cartes de dimensions en puissance de 2. (64, 128, 256, 512, 1024…)

Pour le côté pratique, la plupart des logiciels de graphisme reconnaissent le format RAW : Photoshop, Paintshop Pro …

[modifier] Intérêts et limites des cartes de hauteur

Les cartes de hauteur sont plus particulièrement adaptées aux rendus de terrain extérieurs. En effet, le but étant de modéliser des variations de relief du terrain, les cartes intérieures n’auraient que peu d’intérêt à utiliser ce type de représentations. Un autre désavantage des cartes de hauteur est qu’il n’est possible d’attribuer qu’une seule hauteur par point de la carte, autrement dit, il est impossible de modéliser des grottes, et toutes autres falaises avec une pente plus que verticale :

image:Vague.gif


[modifier] Définition de notre classe de Heightmap

Voici donc notre fichier GLHeightMap.h :

// *** ENTETES
#include "GLTexture.h"
#include "Struct.h"
 
// *** ERREURS
#define ERROR_FILE_MISSING 1              // heightmap introuvable
#define ERROR_FILE_READ 2                 // heightmap mauvais format
#define ERROR_TEXT_MAIN_MISSING 3         // texture main introuvable
#define ERROR_TEXT_DETAIL_MISSING 4       // texture détail introuvable
#define ERROR_TEXT_MAIN_UNKNOWFORMAT 5    // texture main mauvais format
#define ERROR_TEXT_DETAIL_UNKNOWFORMAT 6  // texture détail mauvais format
 
// *** SUCCES
#define SUCCESS 0
 
class GLHeightMap {
      private:
             // Attributs
             GLTexture text_main;  // texture principale
             GLTexture text_detail;// texture de détail
 
             GLuint width;         // longueur de la map
             GLuint height;        // hauteur de la map
 
             GLuint  precision;    // précision de la map
             GLfloat size_scale;   // facteur d'agrandissement
             GLfloat height_scale; // ration de la hauteur
             GLfloat detail_scale; // ratio de la texture de détail
 
             GLubyte* Vertex;      // pour stoquer les points de la map
             bool debugmode;       // log des erreurs ?
             bool wireframe;       // mode fil de fer ?
 
             GLuint MapList;       // la display list
             // Méthodes
             int Init(GLvoid);
      public:
             // Constructeurs / Destructeurs
             GLHeightMap();
             ~GLHeightMap();
             // Sets
             GLvoid SetDebugMode(bool debug);
             GLvoid SetWireFrame(bool wire);
             GLvoid SetPrecision(GLuint Precision);
             GLvoid SetSizeScale(GLfloat size);
             GLvoid SetHeightScale(GLfloat height);
             GLvoid SetPolygonMode(GLuint);
             GLvoid SetDetailScale(GLfloat Scale);
             // Gets
             GLubyte GetVertex(GLuint x, GLuint y);
             GLuint  GetPrecision(GLvoid);
             GLuint  GetHeight();
             GLuint  GetWidth();
             bool    GetWireFrame();
             // Méthodes
             int Load(GLuint Width,
                      GLuint Height,
                      char* filename,
                      char* main_textname,
                      char* detail_textname);
             int Draw(GLvoid);
};

Commençons maintenant les explications de cette classe.

// *** ENTETES
#include "GLTexture.h"
#include "Struct.h"

Ici, nous incluons à notre source les fichier GLTexture.h et Struct.h. GLTexture.h est notre classe servant à stocker des textures en mémoire. Ce fichier définit donc la classe GLTexture qui s’utilise comme suit :

GLTexture ma_texture ;
 
ma_texture.Load("image.tga");
ma_texture.Load("image.bmp");
ma_texture.Load("image.raw", 128);
 
ma_texture.Use() ; // équivaut à un glBindTexture();

Cette classe vous donne aussi accès aux propriétés

ma_texture.width
ma_texture.height

Le fichier Struct.h définit lui deux structures primaires : Point2D et Point3D qui sont résumées comme suit :

struct Point2D {
       GLfloat x;
       GLfloat y;
 
       Point2D() ;
       Point2D operator++() ;
       Point2D operator--() ;
};
 
struct Point3D {
       GLfloat x;
       GLfloat y;
       GLfloat z;
       GLfloat h; // pour les coordonnées homogènes
 
       Point3D();
       Point3D(GLfloat xx, GLfloat yy, GLfloat zz);
 
       GLfloat   Length(); // norme pour un vecteur
       void      Normalize(); // normaliser un vecteur
 
       Point3D operator++(); // Incrémenter un point
       Point3D operator--(); // Décrémenter un point
       Point3D operator-(Point3D p); // Soustraction vectorielle
       Point3D operator+(Point3D p); // Addition vectorielle
       Point3D operator*(Point3D p); // Produit vectoriel
};

Nous définissons ensuite une petite panoplie de constantes préprocesseurs qui nous permettront de renvoyer différentes valeurs après le chargement de la map. La signification de ces constantes est décrite en commentaire.

[modifier] Attributs

La plupart des attributs sont décrits dans les commentaires, mais nous allons revenir ici sur les plus importants pour éclaircir leur rôle.

GLuint precision : cet attribut est fondamental, en fait, il représente le pas qui va nous servir à afficher notre carte. Concrètement, une fois que notre carte de hauteur sera stoquée dans un tableau, elle aura une précision extrèmement importante, la variable précision nous permettra de choisir de combien de points nous nous décalerons à chaque fois.

image:Precision.gif

Comme le montre le schéma, lorsque précision est à 1, alors on afficher 1 point pour chaque point de l'image, ce qui fait une précision énorme. Alors que pour précision=taille de la carte, on obtient un gros carré plat comme carte. Vous aurez compris que 1<= precision <= (width || height)

GLubyte* Vertex : cet attribut servira à conserver nos points de notre heightmap. Vous remarquerez que nous utiliserons un tableau dynamique à 1 dimension, car ils sont réputés plus rapides que les tableaux à 2 dimensions (sauf pour les tableaux statiques). Rappel : pour accéder à l'élément équivalent à tab[4][5] dans un tableau à 1 dimension, nous utiliserons tab[4*width+5]

image:Tab.gif

Ce schéma nous montre bien qu'un tableau tab[7][7] peut être représenté sous la forme tab[7*7], dans ce cas, l'élément tab[4][5] sera l'élément tab[4*7+5] donc tab[33].

Le type GLubyte (openGL Usunsigned BYTE) nous permet de stoquer des octets non signés (donc des nombres de 0 à 255)

GLuint MapList : cet attribut servira en fait à stocker le numéro de la display list contenant notre carte.

[modifier] Fonctions d'accès

Nous définissions aussi ce que nous appelons des fonctions d'accès, c'est à dire qu'au lieu de mettre tous nos attributs public, nous les mettont private, et nous utilisons des fonction
type_attribut Getattribut()
pour obtenir la valeur d'un attribut et
void Setattribut(type_attribut)
pour assigner une valeur à un attribut.

Cette encapsulation nous permettra d'avoir un bien meilleur controle sur nos attributs. Par exemple, notre méthode GetVertex pourra nous permettre de demander la valeur d'un point en utilisant GetVertex(x, z) et non par GetVertex(z*width+x) comme ce serait le cas si nous voulions utiliser directement l'attribut Vertex. Cela nous permettra par exemple aussi de faire un test d'intervalle sur x et z pour ne pas dépasser les valeurs du tableau.

[modifier] Initialisation de la carte

int Init(GLvoid);

Description : Cette fonction sert à créer la display list de notre map. Elle est elle même appelée a la fin du chargement par la fonction Load, ou lorsqu'on modifie la précision de la map avec SetPrecision().

[modifier] Chargement de la carte

int Load(GLuint Width,
         GLuint Height,
         char* filename,
         char* main_textname,
         char* detail_textname);

Arguments : Width - la longueur de la carte

Height - la largeur de la carte

filename - le fichier .raw de notre heightmap (par défaut maps/map.raw)

main_textname - le nom de fichier (tga ou bmp) de la texture à étaler sur toute la heightmap (par défaut maps/map_main.tga)

image:Text_main.gif

detail_textname - le nom de fichier (tga ou bmp) de la texture à de détail à plaquer en mozaïque sur la heightmap (par défaut maps/map_detail.tga). Voici quelques exemples de textures de détail appliquées sur une simple texture d'herbe :

image:Details.gif


Description : Cette fonction est chargée, comme son nom l'indique, de charger une carte de hauteur au format raw dans notre tableau Vertex, d'assigner des valeurs à width et height ...

[modifier] Affichage de la carte

int Draw(GLvoid);

Description : Cette fonction est simplement à appeler dans votre boucle d'affichage de votre programme. Elle se charge de rendre la carte avec options renseignées. Il est conseillé de l'utiliser comme suit

glPushMatrix();
   Map.Draw();
glPopMatrix();

[modifier] Implémentation de notre classe

Les fonctions d'accès Setattribut et Getattribut sont assez triviales pour ne pas être expliquées ici, notons quand même la fonction SetPrecision qui fait un test sur la valeur passée en paramètre, et réenclenche le processus de compilation de la display list.

GLvoid GLHeightMap::SetPrecision(GLuint Precision) {
    precision = Precision;
 
    if (precision<1)
       precision = 1;
 
    Init();
}

Comme dernière fonction d'accès penchons nous aussi sur la fonction de gestion des points de notre map :

GLubyte GLHeightMap::GetVertex(GLuint x, GLuint y) {
    if (x>=width)
       x = width-1;
    if (y>=height)
       y = height-1;
    if (x<0)
       x=0;
    if (y<0)
       y=0;
 
    return Vertex[y*width + x];
}

Comme on le voit cette fonction nous permet d'accéder plus simplement à notre tableau (comme si il était à 2 dimensions), et elle permet aussi une petit vérification de l'accès aux limites du tableau.


Entrons maintenant dans le vif du sujet avec notre fonction Load qui va s'occuper de charger notre carte dans le tableau Vertex. comme vous le verrez, cette fonction est en fait extrèmement simple.


[modifier] Chargement de la carte

Commençons par déclarer notre fonction

int GLHeightMap::Load(GLuint Width,
                      GLuint Height,
                      char* filename,
                      char* main_textname,
                      char* detail_textname) {

La première chose à faire ensuite est de remplir les valeurs de width et height de notre map, par celles passées en paramètre à notre fonction :

height = Height;
width = Width;

Ensuite, nous pouvons charger les textures principales et de détail dans nos attributs prévus à cet effet :

text_main.Load(main_textname);
text_detail.Load(detail_textname);

Maintenant que nous connaissons la taille de notre carte, il nous suffit de réserver de l'espace mémoire pour nos points, ceci se fait assez simplement grace à l'opérateur new :

Vertex = new GLubyte[width*height];

cette instruction va réserver un espace mémoire de width*height objets de type GLubyte, et renvoyer un pointeur vers cet espace, qui sera stoqué dans Vertex.

Nous déclarons maintenant une variable nommée file, de type FILE* (fichier) qui nous servira à lire notre carte .raw

FILE *file = NULL;

Ensuite nous pouvons ouvrir le fichier en mode lecture binaire (read binary - "rb") et l'assigner à file :

file = fopen( filename, "rb");
Il ne nous reste donc plus maintenant qu'à lire width*height octets dans ce fichier, et à les transférer dans le tableau Vertex. Ceci peut se faire très simplement à l'aide de la fonction fread dont le prototype est :
size_t fread(void *ptr, size_t t1, size_t nbe, FILE *fic);

ptr - est un pointeur vers un tableau ou seront stoqués les informations lues. (dans notre cas Vertex)

t1 - est la taille des objets à lire (dans notre cas 1, ou sizeof(GLubyte))

nbe - est le nombre d'objets à lire dans le fichier (dans notre cas nous avons width*height objets à lire)

fic - est une variable de type FILE* (dans notre cas c'est file)


Nous utiliserons donc :

fread(Vertex, 1, width*height, file);

Il ne nous reste plus qu'à fermer notre fichier de carte :

fclose(file);

et à compiler notre display list :

Init();

Puis à indiquer que notre fonction a réussi :

return 0;
}

Ce que je vous ait présenté ici est la version minimale de la fonction Load. La version suivante, est améliorée puisqu'elle permet de détexter des éventuelles erreurs, et d'enregistrer dans un fichier les différentes étapes du chargement de la carte :

int GLHeightMap::Load(GLuint Width,
                      GLuint Height,
                      char* filename,
                      char* main_textname,
                      char* detail_textname) {
    // fichier pour les erreurs
    FILE* logfile;
    logfile = fopen("error.log", "w");
    // écrire les informations dans le fichier de log
    if (debugmode) {
       fprintf(logfile, "----- INFORMATIONS -----\n");
       fprintf(logfile, "-> Largeur : %i\n", Width);
       fprintf(logfile, "-> Heuteur : %i\n", Height);
       fprintf(logfile, "-> HeightMap : %s\n", filename);
       fprintf(logfile, "-> Texture principale : %s\n", main_textname);
       fprintf(logfile, "-> Texture détail : %s\n", detail_textname);
       fprintf(logfile, "----- CHARGEMENT -----\n");
    }
    // On remplit les données longueur/largeur de notre map
    height = Height;
    width = Width;
 
    if (debugmode)
       fprintf(logfile, "-> Donnée de taille et hauteur insérées\n");
 
    // On charge la texture principale et de détail
    text_main.Load(main_textname);
    text_detail.Load(detail_textname);
 
    if (debugmode)
       fprintf(logfile, "-> Textures chargées\n");
 
    // On créé un nouveau tableau de vertex de taille appropriée
    Vertex = new GLubyte[width*height];
 
    if (debugmode)
       fprintf(logfile, "-> Tableau de vertex inséré\n");
 
    FILE *file = NULL;
    // On ouvre le fichier en lecture binaire
    if ((file = fopen( filename, "rb")) == NULL) {
       if (debugmode)
          fprintf(logfile, "-> Erreur : le fichier de map n'existe pas (ERROR_FILE_MISSING retourné)\n");
       return ERROR_FILE_MISSING;
    }
    // On lit le contenu du fichier et le charge dans le tableau de vertex
    fread(Vertex, 1, width*height, file);
    // On recherche une éventuelle erreur
    if (ferror(file)) {
       if (debugmode)
          fprintf(logfile, "-> Erreur : le fichier de map est corrompu (ERROR_FILE_READ retourné)\n");
       return ERROR_FILE_READ;
    }
    // On ferme le fichier
    fclose(file);
 
    if (debugmode) {
          fprintf(logfile, "=> Succès !\n");
    }
    fclose(logfile);
 
    // On créé la display list
    Init();
 
    return SUCCESS;
}

[modifier] Compilation de la display list

Lorsqu'on désire afficher des objets à l'écran, et que l'affichage de ces objets nécessite des calculs, il est possible de demander à OpenGL de retenir un certain nombre d'opérations élémentaires, et de stoquer ces instructions dans une liste, que l'on pourra invoquer plus tard. Ces listes d'affichage (ou display lists) sont très utilisées car elles permettent de simplifier (on créé une fois la liste, et on la réutilise en 1 instruction) et d'accélérer le code (les instructions sont déjà "compilées" en mémoire).

Créer une display list est quelque chose de très simple, il suffit d'utiliser
GLuint un_entier;
glGenLists(un_entier);
pour générer un nouvel identifiant de liste qui n'est pas encore utilisé, puis d'invoquer
glNewList( un_entier, GL_COMPILE );
pour spécifier à OpenGL que les opérations à suivre devront être enregistrées dans la liste identifiée par un_entier, plutot qu'affichées à l'écran. Une fois votre rendu terminé, il vous suffira de clore la compilation de la liste par
glEndList();
et le tour est joué. Si à un moment donné vous souhaitez afficher le contenu de votre liste, alors vous aurez simplement à utiliser l'instruction
glCallList(un_entier);
pour que les opérations stoquées dans votre liste soient immédiatement affichées.

Vous l'aurez donc compris, le plus gros du travail va se trouver dans cette fonction Init() (pour preuve allez consulter la section "Rendu de la carte")

Nous savons que nous disposons de tous les points de notre carte dans notre tableau Vertex. Nous pouvons avoir à tout moment la hauteur de n'importe quel point de notre map en appelant GetVertex(x, z) - x représentant la longueur, z la largeur, et y la hauteur de chaque point.

Notre but sera donc de parcourir nos points, et de les liers les uns aux autres, de manière à obtenir un "maillage". Voici une représentation de notre terrain (les points en rouge, le maillage en noir)

image:heightmaillage.gif

Un algorithme basique consisterait donc à parcourir chaque point de notre liste, et à afficher les 2 triangles formant son maillage. Le schéma ci dessus illustre ce principe en coloriant en gris les 2 triangles du point (2,1). On se rend bien compte que tracer ces 2 triangles pour chaque point de notre liste dessinerai le maillage voulu.

Pourquoi ne pas utiliser des quadrangles (quads) à la place de 2 triangles ? En fait, il est fortement déconseillé d'utiliser des quads avec 4 points qui ne se situent pas sur un même plan.

La carte graphique de toute facon va transcrire les quadrangles en 2 triangles, en bout de chaine, mais il y a 2 diagonales à un rectangle, donc toujours 2 manières différentes de couper un ractangle en 2 triangles, et vous n'avez aucun controle sur laquelle des 2 diagonales va être choisie, ni même ni ce sera toujours la même :

image:triangles.gif

1 et 2 : un quadrangle avec 4 points coplanaires : la diagonale importe peu

3 et 4 : les 4 points ne sont pas coplanaires, la diagonale choisie change completement la figure

Nous choisirons arbitrairement de prendre la diagonale (x,z)->(x+1,z+1) pour tracer nos triangles. Le schéma de synthèse ci dessous devrait finir de vous éclairer sur la technique que nous allons mettre au point. Ici nous avons colorié les triangles du point (2,1).

image:maillage.gif

Nous pouvons donc maintenant nous plonger dans l'étude de notre fonction de création de la display list.

Tout d'abord, nous générons un nouvel identifiant de liste, et nous le stoquons dans notre variable MapList, puis nous indiquons à OpenGL que nous allons commencer à compiler la liste :

MapList = glGenLists(1);
glNewList( MapList, GL_COMPILE );

Le paramètre passé à glGenLists permet choisir le nombre d'identifiants à générer.

Nous allons maintenant activer ce qu'on apelle le backface culling. Le principe est assez simple, lorsque l'on dessine un triangle, ou plus généralement un polygone, dans l'espace, celui-ci possède 2 faces : une face avant, et une face arrière.

image:cull.gif

En règle générale, lorsque OpenGL affiche un polygone, il affiche sa face avant(front) et sa face arrière(back), mais il est possible que l'une des faces du polygone ne soit pas nécessaire. Prenez le cas d'un dé, celui-ci est composé de 6 facettes, matérialisées par 6 quads.

Si on regarde le cube depuis l'extérieur, il n'est pas utile de voir les faces arrières des quads, puisqu'elles se situent à l'intérieur du dé.

D'une autre manière, si on se situe dans le cube (comme avec une skybox), alors il n'est pas utile de dessiner les faces extérieures du cube, celles-ci nous seront invisibles de toute facon.

Dernièrement, si l'une des faces du cube est manquante (une boite ouverte), alors il est nécessaire d'afficher toutes les faces du cube, avant et arrière.

Dans notre cas, nous savons que les faces de nos triangles ne pourront être vues que d'un côté, il ne serait pas logique qu'on puisse voir notre terrain par en dessous, nous activerons donc le backface culling.

Pour reconnaitre les faces avant des faces arrières, OpenGL utilise un principe appelé le CCW - Counte ClockWise (contre le sens des aiguilles d'une montre). Le but est que lorsque les points d'un triangle sont énumérés, la face dont les points sont listés dans l'ordre contraire des aiguilles d'une montre est la face avant, l'autre est la face arrière. (PS : il est possible de configurer OpenGL en mode CW).

Commencons donc par spécifier à OpenGL que nous allons culler (ne pas afficher) certaines faces :

glEnable(GL_CULL_FACE);

Puis renseignons quelles faces nous allons cacher :

glCullFace(GL_BACK);

Maintenant, nous pouvons nous occuper des textures.

Commencons par sélectionner notre première texture : la 0

glActiveTextureARB( GL_TEXTURE0_ARB );

Maintenant nous activons les textures à 2 dimensions :

glEnable (GL_TEXTURE_2D);

Et enfin nous sélectionnons notre texture principale :

text_main.Use();

Faisons de même pour la texture 1 ce qui nous donne :

glActiveTextureARB( GL_TEXTURE1_ARB );
glEnable (GL_TEXTURE_2D);
text_detail.Use();

Mais n'oublions pas que nous allons plaquer cette texture de détail en mozaïque, pour cela, nous allons lui spécifier des paramètres de texture qui se répètent même après que l'intervalle [0;1] soit passé :

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

Ca y est, c'est le moment de passer à notre grande boucle qui va être chargée d'afficher tout notre maillage.

Nous dessinerons des triangles :

glBegin(GL_TRIANGLES);

Pour chaque ligne :

for (int x=0; x<height-precision; x+=precision) {

Pour chaque point de cette ligne :

for (int z=0; z<width-precision; z+=precision) {

Il nous faut calculer chacun des 4 point lié au point courant pour dessiner les 2 triangles, nous allons stocker ces points dans des structures prévues à cet effet :

Point3D triangle1;
Point3D triangle2;
Point3D triangle3;
Point3D triangle4;

Maintenant il nous faut assigner une valeur à chacun de ces points ::

triangle1.x = x;
triangle1.y = GetVertex(x, z);
triangle1.z = z;
 
triangle2.x = x+precision;
triangle2.y = GetVertex(x+precision, z);
triangle2.z = z;
 
triangle3.x = x+precision;
triangle3.y = GetVertex(x+precision, z+precision);
triangle3.z = z+precision;
 
triangle4.x = x;
triangle4.y = GetVertex(x, z+precision);
triangle4.z = z+precision;

Nous pouvons aussi nous servir de nos variables de controle de la carte que nous avons spécifiées au début, multiplions la largeur et la hauteur de chaque point par ces variable :

triangle1.x *= size_scale;
triangle1.y *= height_scale;
triangle1.z *= size_scale;
 
triangle2.x *= size_scale;
triangle2.y *= height_scale;
triangle2.z *= size_scale;
 
triangle3.x *= size_scale;
triangle3.y *= height_scale;
triangle3.z *= size_scale;
 
triangle4.x *= size_scale;
triangle4.y *= height_scale;
triangle4.z *= size_scale;

A présent, nous avons nos 4 points avec leurs coordonnées prets à l'emploi. Nous pouvons donc les afficher simplement dans cet ordre (voir plus haut) :

// Premier triangle
glVertex3f(triangle3.x, triangle3.y, triangle3.z);
glVertex3f(triangle2.x, triangle2.y, triangle2.z);
glVertex3f(triangle1.x, triangle1.y, triangle1.z);
 
// Deuxième triangle
glVertex3f(triangle4.x, triangle4.y, triangle4.z);
glVertex3f(triangle3.x, triangle3.y, triangle3.z);
glVertex3f(triangle1.x, triangle1.y, triangle1.z);

Sauf qu'ici nous n'avons pas texturé nos triangles, trouver les coordonnées de texture d'un des triangles est en fait très simple, pour la grande texture à étaler sur la map, nous ferons donc :

glMultiTexCoord2fARB( GL_TEXTURE0_ARB,
                      (float)x/(float)width,
                      (float)z/(float)height);

les types entre parenthèse sont des "convertions explicites", elles permettent de convertir les valeurs qui les suivent dans ce type, afin d'éviter les erreurs d'arrondis (sinon on aurait fait une division entre deux entiers, et la texture serait mal ajustée).

Pour la texture de détail, nous pouvons aussi trouver les coordonnées de texture comme suit :

glMultiTexCoord2fARB( GL_TEXTURE1_ARB,
                      (float)x/detail_scale,
                      (float)z/detail_scale);

Nous pouvons donc maintenant ajouter ces coordonnées de texture avant d'envoyer les coordonnées du point à la carte graphique (donc avant d'appeler glVertex3f. Ce qui donne :

// ================================== Premier triangle
// ---------------------------------- Point 3
glMultiTexCoord2fARB( GL_TEXTURE0_ARB,
                      (float)(x+precision)/(float)width,
                      (float)(z+precision)/(float)height);
glMultiTexCoord2fARB( GL_TEXTURE1_ARB,
                     (float)(x+precision)/detail_scale,
                     (float)(z+precision)/detail_scale);
glVertex3f(triangle3.x, triangle3.y, triangle3.z);
 
// ---------------------------------- Point 2
glMultiTexCoord2fARB( GL_TEXTURE0_ARB,
                     (float)(x+precision)/(float)width,
                     (float)(z)/(float)height);
glMultiTexCoord2fARB( GL_TEXTURE1_ARB,
                     (float)(x+precision)/detail_scale,
                     (float)(z)/detail_scale);
glVertex3f(triangle2.x, triangle2.y, triangle2.z); 
 
// ---------------------------------- Point 1 
glMultiTexCoord2fARB( GL_TEXTURE0_ARB,
                      (float)(x)/(float)width,
                      (float)(z)/(float)height);
glMultiTexCoord2fARB( GL_TEXTURE1_ARB,
                      (float)(x)/detail_scale,
                      (float)(z)/detail_scale);
glVertex3f(triangle1.x, triangle1.y, triangle1.z);
 
// ================================== Deuxième triangle
// ---------------------------------- Point 4
glMultiTexCoord2fARB( GL_TEXTURE0_ARB,
                      (float)(x)/(float)width,
                      (float)(z+precision)/(float)height);
glMultiTexCoord2fARB( GL_TEXTURE1_ARB,
                      (float)(x)/detail_scale,
                      (float)(z+precision)/detail_scale);
 
glVertex3f(triangle4.x, triangle4.y, triangle4.z);    
 
// ---------------------------------- Point 3                       
glMultiTexCoord2fARB( GL_TEXTURE0_ARB,
                      (float)(x+precision)/(float)width,
                      (float)(z+precision)/(float)height);
glMultiTexCoord2fARB( GL_TEXTURE1_ARB,
                      (float)(x+precision)/detail_scale,
                      (float)(z+precision)/detail_scale);
 
glVertex3f(triangle3.x, triangle3.y, triangle3.z);
 
// ---------------------------------- Point 1
glMultiTexCoord2fARB( GL_TEXTURE0_ARB,
                      (float)(x)/(float)width,
                      (float)(z)/(float)height);
glMultiTexCoord2fARB( GL_TEXTURE1_ARB,
                      (float)(x)/detail_scale,
                      (float)(z)/detail_scale);
glVertex3f(triangle1.x, triangle1.y, triangle1.z);

Nous pouvons maintenant terminer notre boucle et nos envois à la carte graphique (quoique nous compilions juste une display list dans notre cas)

}}
glEnd();

Nous allons maintenant desactiver ce que nous avions activé afin de retrouver l'environnement que nous avions au départ.

Commencons par les textures :

glActiveTextureARB( GL_TEXTURE0_ARB );
glDisable (GL_TEXTURE_2D);
 
glActiveTextureARB( GL_TEXTURE1_ARB );
glDisable (GL_TEXTURE_2D);

Puis le backface culling :

glDisable(GL_CULL_FACE);

Et enfin la compilation de notre liste :

glEndList();

Ca y est, tout le travail de remplissage de notre display liste est fait, je vous propose ci dessous une version plus élaborée de cette fonction, qui prends quelques raccourcis, et calcule la normale de chaque face. Cette fonction donne aussi un petit effet sympathique, elle dégrade vers le turquoise les points les plus bas de la carte, afin de donner une impression de profondeur à l'eau :

int GLHeightMap::Init(GLvoid) {
    // on génère un numéro pour notre liste d'affichage
    MapList = glGenLists(1);
    glNewList( MapList, GL_COMPILE );
    // On affiche pas les faces arrière
    glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);
    // On configure la texture principale
    glActiveTextureARB( GL_TEXTURE0_ARB );
    glEnable (GL_TEXTURE_2D);
    text_main.Use();
    // On configure la texture de détail
    glActiveTextureARB( GL_TEXTURE1_ARB );
    glEnable (GL_TEXTURE_2D);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    text_detail.Use();
 
    glBegin(GL_TRIANGLES);
    for (int x=0; x<height-precision; x+=precision) {
           for (int z=0; z<width-precision; z+=precision) {               
               //=============================================================== Points du triangle
               Point3D normale;
               //--------------------------------------------------------------- Premier point (X+1, Z)
               Point3D triangle1( (x+precision)*size_scale,
                                  (GLfloat)GetVertex(x+precision, z)/height_scale,
                                  z*size_scale);
               //--------------------------------------------------------------- Deuxième point (X, Z)
               Point3D triangle2( x*size_scale,
                                  (GLfloat)GetVertex(x, z)/height_scale,
                                  z*size_scale);
               //--------------------------------------------------------------- Troisième point (X+1, Z+1)
               Point3D triangle3( (x+precision)*size_scale,
                                  (GLfloat)GetVertex(x+precision, z+precision)/height_scale,
                                  (z+precision)*size_scale);
               //--------------------------------------------------------------- Quatrième point (X, Z+1)
               Point3D triangle4( x*size_scale,
                                  (GLfloat)GetVertex(x, z+precision)/height_scale,
                                  (z+precision)*size_scale);
 
               //=============================================================== Premier triangle
               //--------------------------------------------------------------- Normale
               // on calcule la normale du triangle (optionnel)
               normale = (triangle1-triangle2)*(triangle2-triangle3);
               glNormal3f(normale.x, normale.y, normale.z);
 
               //--------------------------------------------------------------- Point 1
               // cette petite conditionnelle permet d'afficher les points
               // les plus bas dans un dégradé de bleu (pour l'eau)
               // optionnel
               if (GetVertex(x+precision, z+precision)<=20)
                  glColor4f(0.25, 0.50, 1.00, 1.00);
               else if (GetVertex(x+precision, z+precision)<=50)
                  glColor4f(0.40, 0.70, 0.90, 1.00);
               else
                  glColor4f(1.00, 1.00, 100., 1.00);
 
               // coordonnée de texture pour le détail
               glMultiTexCoord2fARB( GL_TEXTURE1_ARB,
                                     (float)(x+precision)/detail_scale,
                                     (float)(z+precision)/detail_scale);
               // coordonnée de texture pour la texture principale
               glMultiTexCoord2fARB( GL_TEXTURE0_ARB,
                                     (float)(x+precision)/(float)width,
                                     (float)(z+precision)/(float)height);
               // affichage du point
               glVertex3f(triangle3.x, triangle3.y, triangle3.z);
 
               //--------------------------------------------------------------- Point 2
               if (GetVertex(x+precision, z)<=20)
                  glColor4f(0.25, 0.50, 1.00, 1.00);
               else if (GetVertex(x+precision, z)<=50)
                  glColor4f(0.40, 0.70, 0.90, 1.00);
               else
                  glColor4f(1.00, 1.00, 100., 1.00);
               glMultiTexCoord2fARB( GL_TEXTURE0_ARB,
                                     (float)(x+precision)/(float)width,
                                     (float)(z)/(float)height);
               glMultiTexCoord2fARB( GL_TEXTURE1_ARB,
                                     (float)(x+precision)/detail_scale,
                                     (float)(z)/detail_scale);
               glVertex3f(triangle1.x, triangle1.y, triangle1.z);  
 
               //--------------------------------------------------------------- Point 3 
               if (GetVertex(x, z)<=20)
                  glColor4f(0.25, 0.50, 1.00, 1.00);
               else if (GetVertex(x, z)<=50)
                  glColor4f(0.40, 0.70, 0.90, 1.00);
               else
                  glColor4f(1.00, 1.00, 100., 1.00);
               glMultiTexCoord2fARB( GL_TEXTURE0_ARB,
                                     (float)(x)/(float)width,
                                     (float)(z)/(float)height);
               glMultiTexCoord2fARB( GL_TEXTURE1_ARB,
                                     (float)(x)/detail_scale,
                                     (float)(z)/detail_scale);
               glVertex3f(triangle2.x, triangle2.y, triangle2.z);
 
               //=============================================================== Deuxième triangle
               //--------------------------------------------------------------- Normale
               normale = (triangle3-triangle2)*(triangle2-triangle4);
               glNormal3f(normale.x, normale.y, normale.z);
 
               //--------------------------------------------------------------- Point 1
               if (GetVertex(x, z+precision)<=20)
                  glColor4f(0.25, 0.50, 1.00, 1.00);
               else if (GetVertex(x, z+precision)<=50)
                  glColor4f(0.40, 0.70, 0.90, 1.00);
               else
                  glColor4f(1.00, 1.00, 100., 1.00);
               glMultiTexCoord2fARB( GL_TEXTURE1_ARB,
                                     (float)(x)/detail_scale,
                                     (float)(z+precision)/detail_scale);
               glMultiTexCoord2fARB( GL_TEXTURE0_ARB,
                                     (float)(x)/(float)width,
                                     (float)(z+precision)/(float)height);
               glVertex3f(triangle4.x, triangle4.y, triangle4.z);    
 
               //--------------------------------------------------------------- Point 2                       
               if (GetVertex(x+precision, z+precision)<=20)
                  glColor4f(0.25, 0.50, 1.00, 1.00);
               else if (GetVertex(x+precision, z+precision)<=50)
                  glColor4f(0.40, 0.70, 0.90, 1.00);
               else
                  glColor4f(1.00, 1.00, 100., 1.00);
               glMultiTexCoord2fARB( GL_TEXTURE1_ARB,
                                     (float)(x+precision)/detail_scale,
                                     (float)(z+precision)/detail_scale);
               glMultiTexCoord2fARB( GL_TEXTURE0_ARB,
                                     (float)(x+precision)/(float)width,
                                     (float)(z+precision)/(float)height);
               glVertex3f(triangle3.x, triangle3.y, triangle3.z);
 
               //--------------------------------------------------------------- Point 3
               if (GetVertex(x, z)<=20)
                  glColor4f(0.25, 0.50, 1.00, 1.00);
               else if (GetVertex(x, z)<=50)
                  glColor4f(0.40, 0.70, 0.90, 1.00);
               else
                  glColor4f(1.00, 1.00, 100., 1.00);
               glMultiTexCoord2fARB( GL_TEXTURE0_ARB,
                                     (float)(x)/(float)width,
                                     (float)(z)/(float)height);
               glMultiTexCoord2fARB( GL_TEXTURE1_ARB,
                                     (float)(x)/detail_scale,
                                     (float)(z)/detail_scale);
               glVertex3f(triangle2.x, triangle2.y, triangle2.z); 
           }
 
       }
    glEnd();
 
    glActiveTextureARB( GL_TEXTURE0_ARB );
    glDisable (GL_TEXTURE_2D);
    glActiveTextureARB( GL_TEXTURE1_ARB );
    glDisable (GL_TEXTURE_2D);
 
    glDisable(GL_CULL_FACE);
    glEndList();
}

[modifier] Rendu de la carte

Cette fonction est en fait des plus triviales, puisqu'elle se contente d'appeler la liste d'affichage ou est stoquée notre carte.

Auparavant, nous allons introduire une petite fonction pour afficher notre carte en fil de fer ou non grace à la fonction

glPolygonMode( GLenum face, GLenum mode );

face - les faces concernées (GL_FRONT, GL_BACK ou GL_FRONT_AND_BACK)

mode - le mode d'affichage de ces faces (GL_POINT en points, GL_LINE en lignes (fil de fer) ou GL_FILL en polygones)

Nous obtenons donc :

wireframe ? glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) :
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glCallList(MapList);

[modifier] Construction de notre carte

Nous activons le mode debug (log dans un fichier texte "error.log" les opérations de chargement de la carte). On desactive le rendu en mode fil de fer, on spécifie une précision de 2 ... GLHeightMap::GLHeightMap () {

   debugmode = true;
   wireframe = false;
   precision = 2;
   size_scale = 1.;
   height_scale = 1.;
   detail_scale = 8.;

}

[modifier] Destruction de notre carte

Il suffit de libérer la mémoire occupée par notre liste de points. Ne pas oublier le [] après le delete pour spécifier qu'on détruit un tableau. GLHeightMap::~GLHeightMap () {

   delete [] Vertex;

}

[modifier] Annexe A : De l'utilisation du multitexturing

On l'a vu, notre application utilise une forme basique de multitexturing (texturer plusieurs fois un objet en une seule passe). Nous allons expliquer succintement comment manipuler cette extension d'OpenGL.

[modifier] Prérequis

Pour pouvoir utiliser les extensions d'OpenGL, vous devez inclure le fichier glext.h à vos codes sources, incluez donc la ligne suivante avec le reste de vos header :

#include <GL\glext.h>

[modifier] Initialisation du multitexturing

Pour utiliser le multitexturing, vous aurez besoin de 3 fonctions (plus souvent 2 en fait). Vous devrez donc déclarer ces 3 variables globales au début de votre code source principale (main.h) :

PFNGLACTIVETEXTUREARBPROC    glActiveTextureARB    = NULL;
PFNGLMULTITEXCOORD2FARBPROC  glMultiTexCoord2fARB  = NULL;
PFNGLMULTITEXCOORD1FARBPROC  glMultiTexCoord1fARB  = NULL;

Ces variables sont en fait des pointeurs vers des fonctions qui vous seront indispensables pour utiliser le multitexturing.

Si vous connaissez la notation hongroise : PFN <=> pointeur vers fonction.

Le suffixe ARB a la fin de ces fonction spécifie qu'elles ont étés testées et validées. Vous pouvez tester beaucoup d'extensions d'OpenGL qui auront chacune un suffixe propre à leur "niveau de comptabilité" (extensions spéciales nvidia, ati ...)

Maintenant que vous avez des pointeurs prets à stocker vos fonctions, il suffit de demander à Windows de trouver les adresses de ces fonctions :

glActiveTextureARB = (PFNGLCLIENTACTIVETEXTUREARBPROC)wglGetProcAddress("glActiveTextureARB");
glMultiTexCoord2fARB = (PFNGLMULTITEXCOORD2FARBPROC)wglGetProcAddress("glMultiTexCoord2fARB");
glMultiTexCoord1fARB = (PFNGLMULTITEXCOORD1FARBPROC)wglGetProcAddress("glMultiTexCoord1fARB");

Ca y est, le multitexturing est possible dans votre application.

Attention tout de même, une fois le multitexturing activé, vous devez l'utiliser dans votre application, c'est à dire que vous devrez ajouter

glActiveTextureARB( GL_TEXTUREx_ARB );

avant d'utiliser une texture (remplacez x par un nombre compris entre 0 et le maximum de texture que peut supporter votre implémentation d'OpenGL). Vous devrez aussi utiliser

glMultiTexCoordARB(GL_TEXTUREx_ARB, u, v);

à la place de vos anciens

glTexCoord(u, v);

Dernier conseil, si vous voulez utiliser le multitexturing dans des fichiers sources autres que votre main.cpp, n'oubliez pas de spécifier :

extern const PFNGLACTIVETEXTUREARBPROC glActiveTextureARB;
extern const PFNGLMULTITEXCOORD2FARBPROC glMultiTexCoord2fARB;

[modifier] Annexe B : Petit aide mémoire sur l'application

[modifier] Les fonctions a retenir

Si vous ne connaissez pas l'API Win32, ne vous laissez pas noyer, cela parait lourd et complexe au début, mais faites simplement abstraction de ce que vous ne connaissez par, voici les seules fonctions que vous avez besoin de regarder :

ReSizeGLScene : cette fonction est appelée lorsque la fenêtre est redimensionnée

InitGL : cette fonction est appelée une seule fois au chargement du programme, initialisez vos variables et chargez vos maps dans cette fonction

DeInitGL : cette fonction est appelée lorsque l'application est prête à se terminer. Libérez votre mémoire dans cette fonction.

DrawGLScene : c'est la boucle principale de l'application, c'est cette fonction qui se charge de l'affichage et des calculs.

WndProc : cette fonction Windows se charge de gérer les messages. des messages sont en fait des événements qui se produisent durant l'exécution du programme, exemple : WM_KEYDOWN(touche pressée), WM_KEYUP(touche relachée), WM_MOUSEWHEEL(molette de la souris tournée), WM_MOUSEMOVE(mouvement de la souris).

[modifier] Variables et constantes

Vous disposez d'une variable globales keys[] qui est un tableau de booléens contenant l'état des touches à n'importe quel moment de l'application.

Les touches principales à connaitre sont VK_SPACE(espace), VK_UP(fl_che haut), VK_DOWN(flèche bas), VK_LEFT(flèche gauche), VK_RIGHT(flèche droite), VK_SUBSTRACT(touche - pavé numérique), VK_ADD(touche + pavé numérique), VK_ESCAPE(touche echap), VK_Fx (touche F1 à F12).

Accédez à l'état d'une touche avec keys[VK_touche].

Vous disposez de 3 constantes préprocesseur, leur nom parle d'elles mêmes :

RESO_X - la résolution en X (1280)

RESO_Y - la résolution en Y (1024)

TITLE - le titre de l'application


[modifier] Annexe C : Sources et exécutables

image:Screenshot_height.jpg

Vous pourrez trouver une version de ce tutorial complet contenant les sources, le projet DevCpp 4.9 et les exécutables à cette adresse :

[1]

Vous trouverez une version améliorée et un début d'éditeur de carte à cette adresse (source + binaires + exécutable) :

[2]



Je vous remercie d'avoir lu ce tutorial, n'hésitez pas à le corriger, ou à poser vos questions sur le forum.


--NewbiZ 20 fév 2006 à 02:47 (CET)

 

Rechercher
Installer l'extension de recherche Plus d'informations

 

Comprendre
Tu me dis, j'oublie. Tu m'enseignes, je me souviens. Tu m'impliques, j'apprends. - Benjamin Franklin

 

Partager
La connaissance est la seule chose qui s'accroit lorsqu'on la partage. - Sacha Boudjema

 

Créer
L'imagination est plus importante que la connaissance. - Albert Einstein

 

 

Le wiki en images Le wiki en images Image du mois: «Snowball: un prototype de jeu développé avec NeL.