Charger et se déplacer dans un univers 3D

Jeff Molofee (NeHe)

Traducteur

Mickaël Le Trocquer

Voici le tutoriel que vous attendez tous ! Ce tutoriel a été conçu par le programmeur nommé Lionel Brits. Dans cette leçon vous allez apprendre comment charger un monde 3D à partir d'un fichier, et comment se déplacer à travers ce monde 3D. Le code est basé sur celui de la leçon 1, toutefois, le tutoriel explique seulement les nouvelles parties de code, c'est-à-dire le passage pour charger la scène 3D, et celui pour se déplacer dans cet univers 3D. Téléchargez le code ci-dessous, et parcourez-le en parallèle de votre lecture de ce tutoriel. Les touches à essayer dans le programme sont les suivantes : B (Mélange), F (Filtre), L (Eclairage) [Les lumières ne bougeront pas avec la scène] ainsi que les touches Page Haut et Page Bas. J'espère que cette contribution de Lionel vous fera plaisir. ♪

- Titre : Charger et se déplacer dans un univers 3D

- Auteur : Jeff Molofee (NeHe)

- Traducteur : Mickaël Le Trocquer

- Parution : 23 mars 2007

- Mise à jour : 19 décembre 2012

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2007 Nehe Gamedev.net. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Contributions

- II. Tutoriel

- II-A. Structures de données

- II-B. Chargement de fichier

- II-C. Affichage de notre univers 3D

- III. Téléchargements

- IV. Remerciements

- V. Liens

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 grandit. 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

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

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

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

//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 chaine de caractères initialisée. Voici le code :

Fonction pour lire une ligne

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

NUMPOLLIES n

Voici le code pour lire le nombre de triangles :

Lecture du nombre de triangles

int numtriangles;                                    // Nombre de triangles dans le secteur
char oneline[255];                                   // Chaine 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

// 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;                       // Definit le nombre de triangles 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 caracteres 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

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 nôtre 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 a tourné). Déplacer le monde dans le sens opposé que la caméra (ce qui provoque aussi l'illusion que la caméra a 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

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 dû 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

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 2e point
y_m = sector1.triangle[loop_m].vertex[1].y;       // Coordonnee Y du 2e point
z_m = sector1.triangle[loop_m].vertex[1].z;       // Coordonnee Z du 2e point
u_m = sector1.triangle[loop_m].vertex[1].u;       // Coordonnee de texture U du 2e point
v_m = sector1.triangle[loop_m].vertex[1].v;       // Coordonnee de texture V du 2e 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 3e point
y_m = sector1.triangle[loop_m].vertex[2].y;       // Coordonnee Y du 3e point
z_m = sector1.triangle[loop_m].vertex[2].z;       // Coordonnee Z du 3e point
u_m = sector1.triangle[loop_m].vertex[2].u;       // Coordonnee de texture U du 3e point
v_m = sector1.triangle[loop_m].vertex[2].v;       // Coordonnee de texture V du 3e 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 voilà ! 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évu 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

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 :

- Borland C++ Builder 6(Conversion par Christian Kindahl)

- C#(Conversion par Brian Holley)

- Code Warrior 5.3(Conversion par Scott Lupton)

- CygWin(Conversion par Stephan Ferraro)

- D(Conversion par Familia Pineda Garcia)

- Delphi(Conversion par Michal Tucek)

- Dev C++(Conversion par Dan)

- Euphoria(Conversion par Evan Marshall)

- Game GLUT(Conversion par Milikas Anastasios)

- Irix(Conversion par Rob Fletcher)

- Java(Conversion par Jeff Kirby)

- Jedi-SDL(Conversion par Dominique Louis)

- JOGL(Conversion par Nicholas Campbell)

- LCC Win32(Conversion par Robert Wishlaw)

- Linux(Conversion par Richard Campbell)

- Linux GLX(Conversion par Mihael Vrbanec)

- Linux SDL(Conversion par Ti Leggett)

- LWJGL(Conversion par Mark Bernard)

- Mac OS(Conversion par Anthony Parker)

- Mac OS X/Cocoa(Conversion par Bryan Blackburn)

- MASM(Conversion par Nico (Scalp))

- VC++/OpenIL(Conversion par Denton Woods)

- Pelles C(Conversion par Pelle Orinius)

- Power Basic(Conversion par Angus Law)

- Python(Conversion par Ryan Showalter)

- Visual Basic(Conversion par Jarred Capellman)

- Visual Basic(Conversion par Ross Dawson)

- Visual Fortran(Conversion par Jean-Philippe Perois)

- Visual Studio

- Visual Studio NET(Conversion par Grant James)

IV - Remerciements

Merci à fearyourself et à jc_cornic pour leur relecture.

V - Liens

Sommaire

Tutoriel précédent : Déplacer un bitmap en 3D

Tutoriel suivant : Effet de drapeau