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 : .
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é.
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.)
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).
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 :
//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 :
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 :
NUMPOLLIES n
Voici le code pour lire le 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 :
// 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 :
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).
if
(keys[VK_RIGHT]) // Est-ce que la touche Fleche Droite a ete pressee ?
{
yrot -=
1.5
f; // Tourne la scene vers la gauche
}
if
(keys[VK_LEFT]) // Est-ce que la touche Fleche Gauche a ete pressee ?
{
yrot +=
1.5
f; // 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.05
f; // Bouge sur le plan des X dans la direction du joueur
zpos -=
(float
)cos(heading*
piover180) *
0.05
f; // Bouge sur le plan des Z dans la direction du joueur
if
(walkbiasangle >=
359.0
f) // Si walkbiasangle >= 359 ?
{
walkbiasangle =
0.0
f; // On met walkbiasangle a 0
}
else
// Autrement
{
walkbiasangle+=
10
; // Si walkbiasangle < 359 on l'incremente de 10
}
walkbias =
(float
)sin(walkbiasangle *
piover180)/
20.0
f; // Le joueur "rebondit"
}
if
(keys[VK_DOWN]) // Est-ce que la touche Fleche Bas a ete pressee ?
{
xpos +=
(float
)sin(heading*
piover180) *
0.05
f; // Bouge sur le plan des X dans la direction du joueur
zpos +=
(float
)cos(heading*
piover180) *
0.05
f; // Bouge sur le plan des Z dans la direction du joueur
if
(walkbiasangle <=
1.0
f) // Si walkbiasangle <= 1 ?
{
walkbiasangle =
359.0
f; // On met walkbiasangle a 359
}
else
// Autrement
{
walkbiasangle-=
10
; // Si walkbiasangle > 1 on le decremente de 10
}
walkbias =
(float
)sin(walkbiasangle *
piover180)/
20.0
f; // 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.
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.25
f; // Utilise pour les mouvements vers le haut et vers le bas
GLfloat sceneroty =
360.0
f -
yrot; // Un angle de 360 degres pour la direction du joueur
int
numtriangles; // Entier pour stocker le nombre de triangles
glRotatef(lookupdown,1.0
f,0
,0
); // Tourne vers le haut et le bas comme le regard
glRotatef(sceneroty,0
,1.0
f,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.0
f, 0.0
f, 1.0
f); // 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 :
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.