I. Contributions

Nous recherchons toujours des traducteurs pour finir ce projet de traduction des articles de NeHe. Votre aide serait appréciée ! Vous êtes intéressé ? Alors contactez-nous à l'adresse suivante : nehe@redaction-developpez.com .

II. Tutoriel

Ce tutoriel a été créé par Lionel Brits (ßetelgeuse). Cette leçon explique seulement les parties de code qui ont été rajoutées. En ajoutant seulement les lignes suivantes, le programme ne fonctionnera pas. Si vous êtes intéressé de savoir où est le bon emplacement des lignes de code suivantes, téléchargez le code source fourni, et parcourez le en même temps que vous lisez ce tutoriel.

Bienvenue dans cet infâme tutoriel 10. Dès à présent vous savez coder un cube qui tourne, ou des couples d'étoiles, et vous avez les bases pour la programmation 3D. Mais attendez ! Ne partez pas et ne commencez pas à coder Quake IV tout de suite. Les simples cubes qui vrillent ne feront pas de jolis adversaires pour les duels :-) De nos jours vous devez avoir un large, compliqué et dynamique univers 3D avec 6 angles de liberté et des effets sophistiqués comme les miroirs, les portails, les distorsions et bien sûr tout ceci avec un haut taux de rafraîchissement de l'image. Ce tutoriel explique la structure basique d'un monde 3D, et aussi comment bouger dedans.

II-A. Structures de données

Il est parfaitement compréhensible de vouloir coder un environnement 3D en se basant sur de longues suites de nombres, mais cela devient vite compliqué lorsque la complexité de l'environnement grandie. Pour cette raison, nous devons grouper nos données dans un modèle plus malléable. Au sommet de notre liste il y a les secteurs. Chaque monde 3D peut se percevoir comme une collection de secteurs. Un secteur peut être une pièce, un cube, ou n'importe quel volume fermé.

Définition du type SECTOR
Sélectionnez
typedef struct tagSECTOR // Construction de notre structure de donnees Sector
{
  int numtriangles;      // Nombre de triangles dans le secteur
  TRIANGLE* triangle;    // Pointeur sur le tableau de triangles
}SECTOR;                 // Nom de la structure : SECTOR

Un secteur contient une série de polygones, dans la prochaine catégorie ce sera le triangle. (Nous allons utiliser les triangles à partir de maintenant, ils faciliteront la mise en place du code.)

Définition du type TRIANGLE
Sélectionnez
typedef struct tagTRIANGLE  // Construction de notre structure de donnees Triangle
{
   VERTEX vertex[3];        // Tableau de trois sommets
}TRIANGLE;                  // Nom de la structure : TRIANGLE

Un triangle est simplement un polygone fait de trois sommets, ce qui nous conduit à notre dernière catégorie. Les sommets contiennent les vraies données utiles à OpenGL. Nous définissons chaque sommet des triangles par leurs positions dans l'espace 3D (x, y, z) ainsi que par leurs coordonnées de texture (u, v).

Définition du type VERTEX
Sélectionnez
typedef struct tagVERTEX  // Construction de notre structure de donnees Vertex
{
  float x, y, z;          // Coordonnees 3D
  float u, v;             // Coordonnees de texture
}VERTEX;                  // Nom de la structure : VERTEX

II-B. Chargement de fichier

Stocker les données de notre univers 3D dans notre programme le rend statique et vite ennuyeux. Charger le monde depuis un fichier rajoute une certaine flexibilité. En effet vous pouvez ainsi tester différents univers sans recompiler votre programme. Un autre avantage réside dans le fait que les utilisateurs peuvent échanger et modifier les univers sans connaître le fonctionnement interne de votre programme. Nous allons utiliser le format texte pour décrire les données de notre univers. Cela permettra de l'éditer facilement, et de produire moins de code. Nous nous occuperons du format binaire une autre fois.

Le problème est de savoir comment nous allons récupérer les données depuis notre fichier. Premièrement, nous créons une nouvelle fonction nommée SetupWorld(). Nous manipulerons notre fichier à travers la variable filein. Le fichier est ouvert en mode lecture seule. Nous devons également fermer le fichier lorsque nous avons fini. Voici ce que cela donne :

Fonction de lecture du fichier
Sélectionnez
//Precedente declaration : char* worldfile = "data\\world.txt";
void SetupWorld()                     // Creation de notre univers
{
   FILE *filein;                     // Descripteur de notre fichier contenant l'univers
   filein = fopen(worldfile, "rt");  // Ouverture du fichier
   ...
   (Lecture des données)
   ...
   fclose(filein);                   // Fermeture du fichier
   return;                           // Fin de la fonction
}

Le prochain problème que nous allons traiter est de pouvoir lire chaque ligne de texte dans une variable. Ceci peut être fait de plusieurs manières. Le problème réside dans le fait que toutes les lignes ne contiennent pas forcément des informations utiles. Les lignes blanches et les lignes de commentaires ne doivent pas être lues. Nous allons créer une fonction nommée readstr(). Cette fonction va lire une ligne utile de texte et la stocker une chaîne de caractères initialisée. Voici le code :

Fonction pour lire une ligne
Sélectionnez
void readstr(FILE *f,char *string)             // Lit une ligne et la stocke dans une chaine de caracteres
{
    do                                         // Debut de la boucle
    {
        fgets(string, 255, f);                 // Lit une ligne
    }while ((string[0] == '/') || (string[0] == '\n'));        // Si c'est un commentaire ou une ligne vide, on reboucle
    return;                                    // On quitte la fonction
}

Ensuite, nous devons lire les données concernant les secteurs. Cette leçon va se baser avec un seul secteur, mais il est facile de développer la solution fonctionnant pour plusieurs secteurs. Retournons à la fonction SetupWorld(). Notre programme doit savoir combien de triangles sont présents dans le secteur. Dans le fichier de données, nous allons stocker le nombre de triangles comme suit :

Contenu du fichier pour nombre de polygones
Sélectionnez
NUMPOLLIES n

Voici le code pour lire le nombre de triangles :

Lecture du nombre de triangle
Sélectionnez
int numtriangles;                                    // Nombre de triangles dans le secteur
char oneline[255];                                   // Chaîne de caracteres pour stocker les données
...
readstr(filein,oneline);                             // On lit la premiere ligne utile du fichier
sscanf(oneline, "NUMPOLLIES %d\n", &numtriangles);   // On recupere le nombre de triangles

Le reste de l'algorithme pour charger notre univers 3D se base sur le même procédé. Ensuite, nous initialisons notre secteur et nous stockons les données lues dedans :

Lecture d'un secteur
Sélectionnez
// Precedente declaration : SECTOR sector1;
char oneline[255];        // Chaine de caracteres contenant les informations lues
int numtriangles;         // Nombre de triangles du secteur
float x, y, z, u, v;      // Coordonnees 3D et de texture
...
sector1.triangle = new TRIANGLE[numtriangles];             // Allocation mémoire de numtriangles emplacements
sector1.numtriangles = numtriangles;                       // Definis le nombre de triangle de sector1
                                                           // On examine chacun des triangles du secteur
for (int triloop = 0; triloop < numtriangles; triloop++)   // On boucle sur tous les triangles
{
                                                           // On examine chacun des sommets du triangle
    for (int vertloop = 0; vertloop < 3; vertloop++)       // On boucle sur tous les sommets
    {
        readstr(filein,oneline);                           // On lit une chaine de caractere utile
                                                           // Convertit les donnees en coordonnees 3D et de texture
        sscanf(oneline, "%f %f %f %f %f", &x, &y, &z, &u, &v);
                                                           // Stocke les donnees dans le bon sommet
        sector1.triangle[triloop].vertex[vertloop].x = x;  // sector1, triangle n°triloop, sommet n°vertloop, valeur de x=x
        sector1.triangle[triloop].vertex[vertloop].y = y;  // sector1, triangle n°triloop, sommet n°vertloop, valeur de y=y
        sector1.triangle[triloop].vertex[vertloop].z = z;  // sector1, triangle n°triloop, sommet n°vertloop, valeur de z=z
        sector1.triangle[triloop].vertex[vertloop].u = u;  // sector1, triangle n°triloop, sommet n°vertloop, valeur de u=u
        sector1.triangle[triloop].vertex[vertloop].v = v;  // sector1, triangle n°triloop, sommet n°vertloop, valeur de v=v
    }
}

Chaque triangle dans notre fichier de données est déclaré comme ci-dessous :

Déclaration d'un triangle dans le fichier de données
Sélectionnez
X1 Y1 Z1 U1 V1
X2 Y2 Z2 U2 V2
X3 Y3 Z3 U3 V3

II-C. Affichage de notre univers 3D

Maintenant que nous avons chargé notre secteur en mémoire, nous devons l'afficher sur l'écran. Plus loin nous ferons quelques rotations et translations, mais notre caméra sera toujours centrée au point d'origine (0, 0, 0). Tous les bons moteurs 3D permettent à l'utilisateur de naviguer à travers leurs univers 3D, donc le notre permettra également de le faire. Une des façons de le faire est de bouger la caméra et de dessiner l'environnement 3D en fonction de la position de la caméra. Ceci est lent et dur à coder. Voici ce que nous allons faire : Tourner et translater la position de la caméra en fonction des touches pressées par l'utilisateur Tourner l'univers autour de l'origine dans le sens opposé de la rotation de la caméra (ce qui donne l'illusion que la caméra à tourné) Déplacer le monde dans le sens opposé que la caméra (ce qui provoque aussi l'illusion que la caméra à bougé) Ceci est relativement simple à implémenter. Commençons tout de suite avec la première partie (rotation et translation de la caméra).

Gestion de la caméra
Sélectionnez
if (keys[VK_RIGHT])                        // Est-ce que la touche Fleche Droite a ete pressee ?
{
    yrot -= 1.5f;                          // Tourne la scene vers la gauche
}
if (keys[VK_LEFT])                         // Est-ce que la touche Fleche Gauche a ete pressee ?
{
    yrot += 1.5f;                          // Tourne la scene vers la droite
}
if (keys[VK_UP])                           // Est-ce que la touche Fleche Haut a ete pressee ?
{
    xpos -= (float)sin(heading*piover180) * 0.05f;            // Bouge sur le plan des X dans la direction du joueur
    zpos -= (float)cos(heading*piover180) * 0.05f;            // Bouge sur le plan des Z dans la direction du joueur
    if (walkbiasangle >= 359.0f)             // Si walkbiasangle >= 359 ?
    {
        walkbiasangle = 0.0f;                // On met walkbiasangle a 0
    }
    else                                     // Autrement
    {
        walkbiasangle+= 10;                  // Si walkbiasangle < 359 on l'incremente de 10
    }
    walkbias = (float)sin(walkbiasangle * piover180)/20.0f;    // Le joueur "rebondit"
}
if (keys[VK_DOWN])                           // Est-ce que la touche Fleche Bas a ete pressee ?
{
    xpos += (float)sin(heading*piover180) * 0.05f;            // Bouge sur le plan des X dans la direction du joueur
    zpos += (float)cos(heading*piover180) * 0.05f;            // Bouge sur le plan des Z dans la direction du joueur
    if (walkbiasangle <= 1.0f)               // Si walkbiasangle <= 1 ?
    {
        walkbiasangle = 359.0f;              // On met walkbiasangle a 359
    }
    else                                     // Autrement
    {
        walkbiasangle-= 10;                  // Si walkbiasangle > 1 on le decremente de 10
    }
    walkbias = (float)sin(walkbiasangle * piover180)/20.0f;  // Le joueur "rebondit"
}

C'était assez simple. Quand l'une ou l'autre des touches gauche ou droite sont pressées, la variable de rotation yrot est incrémentée ou décrémentée respectivement. Lorsque les touches haut ou bas sont pressées, une nouvelle destination pour la caméra est calculée en utilisant les fonctions sinus et cosinus (quelques notions de trigonométrie sont requises). Piover180 est simplement une fonction de conversion pour passer des degrés aux radians.

Ensuite vous allez me demander : Qu'est-ce que walkbias ? C'est en quelque sorte pour compenser l'effet qui apparaît lorsqu'une personne se promène (avec un déplacement vertical de la tête dû au mouvement du corps ; en effet, une personne qui marche n'arrive généralement pas à garder sa tête immobile). Cela permet d'ajuster la position en Y de la caméra grâce à la fonction sinus. J'ai du rajouter ceci dans le code, sinon bouger en avant ou en arrière ne rendait pas terrible.

Maintenant que nous avons ces variables, nous pouvons passer aux étapes deux et trois. Cela va se faire en mettant en place une boucle d'affichage des secteurs, en effet le programme n'est pas suffisamment compliqué pour mettre l'affichage d'un secteur dans une fonction séparée.

Fonction d'affichage
Sélectionnez
int DrawGLScene(GLvoid)                                    // Dessin de la scene OpenGL
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);    // Efface l'ecran et le tampon de profondeur
    glLoadIdentity();                                      // Reinitialise la matrice d'affichage courante
    GLfloat x_m, y_m, z_m, u_m, v_m;                       // Declaration des coordonnees 3D et de textures
    GLfloat xtrans = -xpos;                                // Utilise pour la translation du joueur sur l'axe des X
    GLfloat ztrans = -zpos;                                // Utilise pour la translation du joueur sur l'axe des Z
    GLfloat ytrans = -walkbias-0.25f;                      // Utilise pour les mouvements vers le haut et vers le bas
    GLfloat sceneroty = 360.0f - yrot;                     // Un angle de 360 degres pour la direction du joueur
    int numtriangles;                                      // Entier pour stocker le nombre de triangles
    glRotatef(lookupdown,1.0f,0,0);                        // Tourne vers le haut et le bas comme le regard
    glRotatef(sceneroty,0,1.0f,0);                         // La rotation dépend vers ou le joueur regarde
    glTranslatef(xtrans, ytrans, ztrans);                  // Deplace la scene en fonction de la position du joueur
    glBindTexture(GL_TEXTURE_2D, texture[filter]);         // Applique une texture en fonction de "filter"
    numtriangles = sector1.numtriangles;                   // Recupere le nombre de triangles du secteur 1
                                                           // On dessine chaque triangle
    for (int loop_m = 0; loop_m < numtriangles; loop_m++)  // Boucle autour de tous les triangles
    {
        glBegin(GL_TRIANGLES);                             // Commence a dessiner les triangles
            glNormal3f( 0.0f, 0.0f, 1.0f);                   // La normale pointe vers devant
            x_m = sector1.triangle[loop_m].vertex[0].x;       // Coordonnee X du 1er point
            y_m = sector1.triangle[loop_m].vertex[0].y;       // Coordonnee Y du 1er point
            z_m = sector1.triangle[loop_m].vertex[0].z;       // Coordonnee Z du 1er point
            u_m = sector1.triangle[loop_m].vertex[0].u;       // Coordonnee de texture U du 1er point
            v_m = sector1.triangle[loop_m].vertex[0].v;       // Coordonnee de texture V du 1er point
            glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);// Place les coordonnees de texture et le sommet
            x_m = sector1.triangle[loop_m].vertex[1].x;       // Coordonnee X du 2eme point
            y_m = sector1.triangle[loop_m].vertex[1].y;       // Coordonnee Y du 2eme point
            z_m = sector1.triangle[loop_m].vertex[1].z;       // Coordonnee Z du 2eme point
            u_m = sector1.triangle[loop_m].vertex[1].u;       // Coordonnee de texture U du 2eme point
            v_m = sector1.triangle[loop_m].vertex[1].v;       // Coordonnee de texture V du 2eme point
            glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);// Place les coordonnees de texture et le sommet
            x_m = sector1.triangle[loop_m].vertex[2].x;       // Coordonnee X du 3eme point
            y_m = sector1.triangle[loop_m].vertex[2].y;       // Coordonnee Y du 3eme point
            z_m = sector1.triangle[loop_m].vertex[2].z;       // Coordonnee Z du 3eme point
            u_m = sector1.triangle[loop_m].vertex[2].u;       // Coordonnee de texture U du 3eme point
            v_m = sector1.triangle[loop_m].vertex[2].v;       // Coordonnee de texture V du 3eme point
            glTexCoord2f(u_m,v_m); glVertex3f(x_m,y_m,z_m);// Place les coordonnees de texture et le sommet
        glEnd();                                           // Fin du dessin du triangle
    }
    return TRUE;                                           // Fin de la fonction
}

Et voila ! Nous avons fini de dessiner notre première image. Ce n'est pas exactement Quake mais nous ne sommes pas encore comme Carmack ou Abrash. Pendant que le programme fonctionne, vous pouvez appuyer sur F, B, PageHaut et PageBas pour voir ce que cela fait. Page Haut/Page Bas incline simplement la caméra vers le haut ou le bas (la même chose que si l'on faisait un panoramique coté par coté). La texture utilisée est juste texture de boue avec le logo de mon école appliquée en bumpmap. Enfin si NeHe décide de les garder :-).

Donc maintenant vous pensez probablement à ce que vous allez faire ensuite. Ne pensez pas qu'à partir de ce code vous pourrez faire un moteur 3D ultra complet, en effet il n'a pas été prévue pour. Vous voulez sans doute plus qu'un unique secteur dans votre jeu, surtout si vous allez implémenter un système de portail. Vous allez sans doute vouloir avoir des polygones de plus de trois sommets, ce qui est essentiel pour les portails. Je continue de développer le code, maintenant on peut avoir plusieurs secteurs, et il gère les faces cachées (ne pas dessiner les polygones qui ne font pas face à la caméra). Je réécrirais un tutoriel prochainement, mais comme il y aura besoin de notions mathématiques plus complexes, j'écrirais un tutoriel sur les matrices avant.

NeHe (05/01/00):

J'ai entièrement commenté chaque ligne de code listée dans ce tutoriel. On comprend mieux ainsi. Quelques lignes seulement étaient commentées avant, maintenant elles le sont toutes.

Lionel Brits ( ßetelgeuse )

Enfin, voici une petite image de ce que vous devez voir :

Image de l'application
Image de l'application

Jeff Molofee ( NeHe )

III. Téléchargements

Compte tenu du nombre de versions de codes sources pour les tutoriels nehe, nous les laissons en anglais. En principe, si vous avez compris le code présenté dans ce tutoriel (et les tutoriels antérieurs), vous n'aurez pas de mal à le comprendre :

IV. Remerciements

Merci à fearyourself et à jc_cornic pour leur relecture.

V. Liens