Surfaces en OpenGL
Cet article a été rédigé par Amanga, webmaster du site http://studioamanga.free.fr/.
De tous les jeux, de toutes les applications ayant un rendu graphique, rares sont ceux n'utilisant pas la 3D, ce qui nous mène à la conclusion suivante : nous sommes envahis par les surfaces. Même la 2D les utilise. C'est bien simple, vous ne pouvez pas allumer votre ordinateur ou votre télévision sans en avoir plein l'écran. Impressionant, non ? Elles nous ont envahi. Mais que cachent-elles ? Comment les créer, les afficher ? Tâchons donc d'y répondre.
Je vais ici vous expliquer comment utiliser les surfaces avec OpenGL, parce qu'il s'agit de la bibliothèque graphique la plus performante et la plus utilisée (et surtout parce qu'elle est portable, ce qui n'est pas le cas de DirectX). Cela dit, ce tutoriel ne vous permet pas pour autant de faire un rendu, car sa vocation est de vous faire comprendre comment et pourquoi utiliser les surfaces.
Un peu de théorie...
Le rendu 3D a pour base le vertex, cet élément comparable à un point de l'espace, mais qui peut contenir beaucoup d'autres informations, telle que sa taille, sa couleur, les coordonées de sa normale, etc. Il est possible de rendre ces vertices (pluriel de 'vertex') tels des 'points' dans l'espace, ce qui donne au mieux une belle guirlande. La modélisation 3D a donc forcément besoin des surfaces pour espérer approcher d'un resultat photo-réaliste. La définition d'une surface est simple, il s'agit d'une liste de vertices qui forment les sommets d'une surface géometrique : la 'surface'.
Comme la surface la plus simple reste inscrite dans un plan, il va s'en dire que la meilleure façon de la définir est de donner 3 vertices, formant un triangle. Cependant, certaines bibliothèques acceptent les polygones, soit en les transformant en surface courbes (les célèbres 'courbes de Bézier') car les vertices n'appartiennent pas forcément au même plan, ce qui est très joli mais coûteux en calcul ; soit en séparant la surface en plusieurs triangles, en essayant d'avoir le rendu le plus approchant possible. Bref, n'utilisez que des triangles si possible, même si les carrés vous semblent plus pratiques. Récapitulons : une surface c'est, dans 99% des cas, une liste de 3 vertices.
Surfaces OpenGL
Pour que OpenGL affiche nos surfaces, nous pouvons utiliser les 'List'. Il s'agit d'un moyen pour mémoriser les surfaces, et ne pas avoir a redéfinir chacun des vertices a chaque appel de la surface. Il suffit de déclarer un entier qui stockera l'adresse de la surface. Cette adresse est retournée par la fonction glGenLists(...) qui prend comme paramètre le nombre de Lists que l'on veut générer. Mettons donc en place une fonction d'initialisation, qui sera appelée juste après le lancement d'OpenGL :
// Entier qui stockera l'adresse de la surface
int surf1;
// Fonction d'initialisation des surfaces
void InitSurf(void)
{
// Ne pas oublier de générer l'adresse !
surf1 = glGenLists(1);
// Déclaration de la surface
glNewList(surf1,GL_COMPILE);
glBegin(GL_TRIANGLE_STRIP);
glColor3d(0,0,1);
glVertex3f(0.,0.,0.);
glColor3d(0,1,1);
glVertex3f(0.,1.,0.);
glColor3d(1,1,0);
glVertex3f(1.,1.,0.);
glColor3d(1,0,1);
glVertex3f(1.,0.,0.);
glEnd();
glEndList();
}
On commence donc par appeler 'glNewList(...)', où le premier argument est l'entier qui correspond à l'adresse, et le second une constante OpenGL : ou bien la List est juste initialisée (GL_COMPILE), ou alors elle également affichée par la même occasion (GL_COMPILE_AND_EXECUTE). Lorsque cette List sera complète, on la verrouille avec 'glEndList()'. Vient la déclaration de la surface elle-même. La fonction 'glBegin()' commence par préciser l'agencement des vertices par son argument, qui renvoi à une constante OpenGL :
- Tous les vertices sont des points (GL_POINTS)
- Les vertices forment deux-à-deux des lignes (GL_LINES)
- Tous les vertices sont connectés au précédent pour former des lignes (GL_LINE_STRIP)
- Idem à GL_LINE_STRIP, mais avec le dernier vertex relié au premier par une ligne (GL_LINE_LOOP)
- Les vertices forment des triangles trois-à-trois (GL_TRIANGLES)
- Chaque vertex est relié aux deux précédents pour former des triangles (GL_TRIANGLE_STRIP)
- Le premier vertex sert de sommet, tous les autres y sont reliés pour former des triangles (GL_TRIANGLE_FAN)
- Les vertices forment des quadrilatères quatre-à-quatre (GL_QUADS)
- Les vertices forment des quadrilatères connectés (GL_QUAD_STRIP)
- Les vertices sont reliés pour former un polygone convexe (GL_POLYGON)
Vous voyez qu'OpenGL vous laisse libre de ranger vos vertices de la manière que vous souhaitez, mais je vous conseille vivement d'utiliser les GL_TRIANGLE_STRIP car il s'agit de la méthode la plus optimisée.
Afin que chaque vertex soit bien visible, nous allons leur affecter une couleur, par l'appel à 'glColor3d()' qui prend trois décimaux en argument, correspondants aux valeurs RGB (rouge, vert et bleu) sur une intervalle allant de 0 a 1. Reste a définir les coordonnées du vertex avec 'glVertex3f()' prenant les trois flottants x, y et z en argument. Enfin, on achève la surface par un appel à 'glEnd()'.
Voila comment la surface est créée et stockée par la List. Pour ce qui est de l'affichage, un simple 'glCallList()' avec l'entier de l'adresse de la surface pour argument suffira :
// A placer dans la fonction d'affichage
glCallList(surf1);
Un format pour tous les charger
Notre fonction d'affichage fonctionne maintenant très bien, mais il reste un détail : les objets ne sont que des variables globales, ce qui fait un peu désordre. L'idéal serait un format de fichier, à partir duquel seraient chargées les surface. Sitôt dit, sitôt fait : nous allons voir comment une classe peut gérer de belle manière notre problème d'objet.
Nous aurons besoin du nombre de surfaces du modèle, et donc d'un tableau d'entiers dynamique pour stocker chacune de leurs adresses. Ces éléments seront considérés comme privés, afin d'être sur qu'ils ne soient pas modifiés par une fonction extérieure à la class. Les méthodes publiques seront peu nombreuses : un constructeur pour l'initialisation et le chargement, une fonction d'affichage du modèle et bien sûr un destructeur. Cela nous permet déjà de coder le prototype :
// Classe des models
class cl_model
{
private:
// Nombre de surface
int nb_surface;
// Adresses des surfaces
int *adSurf;
public :
// Constructeur
cl_model(char * file);
// Destructeur
~cl_model(void);
// Fonction d'affichage
bool aff(void);
};
Nous allons maintenant définir la structure des fichiers contenants les modèles. La création de ces fichiers ne sera pas abordée ici, parce qu'il ne s'agirait plus du thème de ce tutorial, mais de simples appels à 'fwrite()' suffiront. Attardons nous donc sur les données. Il nous faut d'abord le nombre de surface (int), puis, pour chacune, le type de surface (char), que nous transposerons en triangles, le nombre de vertices (int) de celle-ci et donc les vertices eux-mêmes (3 float). Libre à vous de rajouter d'autres informations (url de la texture, type d'animation, etc.).
Le destructeur ne présente rien d'interessant, et la fonction d'affichage ne fait que reprendre les méthodes de dessin évoquées dans la partie précédente. Voilà donc comment se présentent nos déclarations :
// Constructeur
cl_model::cl_model(char * file)
{
this->nb_surface = 0;
int nbVertices = 0;
FILE * fOut=fopen(file,"r");
if (!FILE)
{
printf("Fichier inexistant !\n");
return;
}
fread(&(this->nb_surface),sizeof(int),1,fOut);
this->adSurf = new int[this->nb_surface];
for(int cur = 0; cur < this->nb_surface; cur++)
{
this->skin[cur] = glGenLists(1);
glNewList(this->skin[cur],GL_COMPILE);
char type;
fread(&type, sizeof(char), 1, fOut);
int nb_vert;
fread(&nb_vert, sizeof(int), 1, fOut);
// Série de valeur permettant de passer
// les éventuels 'QUADS' en 'TRIANGLES'
int quad = 0;
float x1, y1, z1, tx1, ty1;
float x3, y3, z3, tx3, ty3;
for(int y = 0; y < nb_vert; y++)
{
float x,y,z,tx,ty;
fread(&x, sizeof(float), 1, fOut);
fread(&y, sizeof(float), 1, fOut);
fread(&z, sizeof(float), 1, fOut);
nbVertices++;
glVertex3f(x,y,z);
quad++;
if (quad == 1)
{
x1 = x;
y1 = y;
z1 = z;
}
else if (quad == 3)
{
x3 = x;
y3 = y;
z3 = z;
}
else if (quad == 4 && type == 'Q')
{
glVertex3f(x1,y1,z1);
glVertex3f(x3,y3,z3);
quad=0;
}
}
glEnd();
glEndList();
printf("[OK] Model chargé (%d surface(s),"\
" %d vertices, file : '%s')\n",
this->nb_surface, nbVertices, file);
fclose (fOut);
}
}
// Destructeur
cl_model::~cl_model(void)
{
delete this->skin;
}
// Affichage
bool cl_model::aff(void)
{
for(int cur = 0; cur < this->nb_surface; cur++)
glCallList(this->skin[cur]);
return true;
}
Voilà notre classe de base établie, il ne vous reste plus qu'à l'adapter à vos besoins. L'utilisation en elle-même est très simple : le constructeur initialise avec l'url du fichier en argument, le destructeur doit juste être placé en fin de programme, et l'affichage se fait par la méthode 'aff()'.
Voila, vous en savez plus sur les surfaces et leur utilisation. Si vous utilisez une autre bibliothèque qu'OpenGL, il vous suffit de trouver les méthodes correspondantes, la forme changera mais pas le fond. Le début de format de modèle que nous avons fixé est pour vous la base d'une classe plus complète qui comprendrait de nouvelles données relatives aux surfaces : textures, effets, animation... Alors bonne prog' à tous ! ;)