Génération de terrain (2/2)
Par Xavier Michelon


 
   
Implementation

Intéressons nous maintenant aux aspects pratiques de la question et regardons comment implémenter la gestion de l'éclairage dans le programme du precedent didacticiel. Le changement majeur est l'utilisation d'un tableau de type vertex pour calculer le terrain. Le type vertex contient 6 composantes : les cordonnées cartésiennes du sommet (x,y,z) ainsi que les coordonnées de la normale au terrain en ce sommet (nx,ny,nz). Vous remarquerez que le tableau utilisé est mono dimensionnel, car il est plus facile de le déclarer dynamiquement. Le vertex (i,j) du terrain est stocké dans T[i*(largeur du tableau)+j].

Le coeur du code est bien sûr la fonction creeTerrain() qui génère la liste d'affichage du terrain. Dans un premier temps, on alloue de la mémoire pour le tableau de 'vertex' T. Je vous rappelle que le programme permet de faire varier la densité du maillage, ce qui explique l'allocation dynamique de mémoire. On initialise également les normales des vertex à zéro, car nous allons utiliser une méthode incrémentale pour calculer ces normales.

T=(vertex *)malloc((nbSubdiv+1)*(nbSubdiv+1)*sizeof(vertex));

/* Initialisation des normales */
for (i=0;i<=nbSubdiv;i++)
  for (j=0;j<=nbSubdiv;j++) {
    T[i*(nbSubdiv+1)+j].nx=0.0;
    T[i*(nbSubdiv+1)+j].ny=0.0;
    T[i*(nbSubdiv+1)+j].nz=0.0;
  }

La méthode utilisée pour calculer la position des sommets et celle que nous avons déjà utilisée :

for (i=0;i<=nbSubdiv;i++)
  for (j=0;j<=nbSubdiv;j++) {
    T[i*(nbSubdiv+1)+j].x=-1.0+i*pas;
    T[i*(nbSubdiv+1)+j].y=-1.0+j*pas;
    T[i*(nbSubdiv+1)+j].z=elevation(i,j);
  }

Ensuite on passe au calcul des normales. Et nous allons nous permettre une petite astuce. Dans la méthode que je vous ai présentée, nous avons vu que pour chaque sommet, il fallait calculer la moyenne des normales de tous les polygones auxquels le sommet appartient. Mais tous les sommets ne font pas partie d'un même nombre de polygones. Aussi nous allons plutôt parcourir le maillage par patch carré (c'est-à-dire un couple de triangles). Pour chaque triangle, on calcule un vecteur normal (et normé !) qu'on ajoute à la composante  normale (nx,ny,nz) des sommets qui composent le triangle :

for (i=0;i<=nbSubdiv;i++)
  for (j=0;j<=nbSubdiv;j++) {
    T[i*(nbSubdiv+1)+j].x=-1.0+i*pas;
    T[i*(nbSubdiv+1)+j].y=-1.0+j*pas;
    T[i*(nbSubdiv+1)+j].z=elevation(i,j);
  }

for (i=0;i<nbSubdiv;i++)
  for (j=0;j<nbSubdiv;j++) {
    P1=&T[i*(nbSubdiv+1)+j];
    P2=&T[(i+1)*(nbSubdiv+1)+j];
    P3=&T[(i+1)*(nbSubdiv+1)+j+1];
    P4=&T[i*(nbSubdiv+1)+j+1];

    V1.x=P2->x-P1->x; V1.y=P2->y-P1->y; V1.z=P2->z-P1->z;
    V2.x=P3->x-P1->x; V2.y=P3->y-P1->y; V2.z=P3->z-P1->z;
    V3.x=P4->x-P1->x; V3.y=P4->y-P1->y; V3.z=P4->z-P1->z;

    incx=V2.y*V1.z-V1.y*V2.z;
    incy=V2.z*V1.x-V1.z*V2.x;
    incz=V2.x*V1.y-V1.x*V2.y;
    norme=sqrt(incx*incx+incy*incy+incz*incz);
    incx/=norme; incy/=norme; incz/=norme;
    P1->nx-=incx; P1->ny-=incy; P1->nz-=incz;
    P2->nx-=incx; P2->ny-=incy; P2->nz-=incz;
    P3->nx-=incx; P3->ny-=incy; P3->nz-=incz;

    incx=V3.y*V2.z-V2.y*V3.z;
    incy=V3.z*V2.x-V2.z*V3.x;
    incz=V3.x-V2.y-V2.x*V3.y;
    P1->nx-=incx; P1->ny-=incy; P1->nz-=incz;
    P3->nx-=incx; P3->ny-=incy; P3->nz-=incz;
    P4->nx-=incx; P4->ny-=incy; P4->nz-=incz;
  }

Comme je vous l'ai expliqué précédemment, il ne faut pas oublier de normer les vecteurs normaux moyens :

for (i=0;i<=nbSubdiv;i++)
  for (j=0;j<=nbSubdiv;j++) {
    P1=&T[i*(nbSubdiv+1)+j];
    norme=sqrt(P1->nx*P1->nx+P1->ny*P1->ny+P1->nz*P1->nz);
    P1->nx/=norme;
    P1->ny/=norme;
    P1->nz/=norme;
  }

L'essentiel du travail est fait : il ne reste plus qu'à générer la liste d'affichage terrain en utilisant le tableau que nous venons de créer :

glNewList(terrain,GL_COMPILE);
glColor3f(0.0,0.0,0.0);
glLineWidth(1.0);
for (i=0;i<nbSubdiv;i++)
  for (j=0;j<nbSubdiv;j++) {
    glBegin(GL_TRIANGLES);
    /* triangle 1 */
    glEdgeFlag(TRUE);
    drawVertex(i,j,T);
    drawVertex(i+1,j,T);
    if (!areteTransv)
glEdgeFlag(FALSE);
    drawVertex(i+1,j+1,T);
    /*triangle 2 */
    drawVertex(i,j,T);
    if (!areteTransv)
glEdgeFlag(TRUE);
    drawVertex(i+1,j+1,T);
    drawVertex(i,j+1,T);
    glEnd();
  }
glEndList();

La fonction drawVertex n'est utilisée que dans un but de lisibilité du code, elle se contente de déclarer un sommet et la normale qui lui est associée :

void drawVertex(int i,int j,vertex *T)
{
  glNormal3fv(&(T[i*(nbSubdiv+1)+j].nx));
  glVertex3fv(&(T[i*(nbSubdiv+1)+j].x));
}

Vous noterez que le programme vous propose quelques fonctionnalités nouvelles comme l'affichage des normales et des lampes. Ces fonctionnalités ne présentent aucune difficulté, et je vous laisse donc les découvrir par vous même.