Introduction

Tous les développements d'applications 3D commencent par la rotation d'un cube sur un fond noir. Ce fond noir vous sort par les yeux ... vous pouvez toujours passer à une autre couleur, mais cela ne donnera pas beaucoup de réalisme à votre scène. Une autre solution simple serait de définir une image fixe en fond de votre application, mais le problème est que le ciel resterait fixe quel que soit le point de vue. Et si nous faisions un rendu de ciel ou de décor réaliste ?

Ce système de rendu, est souvent appelé "Skybox" (littéralement "Boite de ciel"). La Skybox est généralement un cube texturé avec un décor de ciel. Ce simple système peut poser de nombreux problèmes notamment dans la géométrie de ce cube.

Nous allons commencer par un peu de théorie pour présenter les différents concepts utilisés pour réaliser cette skybox le plus efficacement possible. Les thèmes abordés seront l'utilisation des cube-map et la contrainte sur le depth buffer. Nous continuerons par l'utilisation du logiciel "Terragen" qui permet de générer rapidement les textures de la skybox. Enfin, nous passerons à la pratique en appliquant les concepts en OpenGL.

II. Un peu de théorie ...

II-A. La géométrie : base d'une Skybox

Une Skybox est, comme son nom l'indique, une box ou plutôt une boite. Cette boite aura des largeur, longueur et hauteur identique. Toutes les faces du cube seront dirigées vers l'intérieur car toute la scène sera contenue dans celui-ci. Chacune des faces du cube correspondra aux vues du Nord, Sud, Est, Ouest de la caméra ainsi que le Haut et le Bas. La texture qui sera appliquée sur chacune d'elle correspondra à ce que l'on pourrait voir à une distance infinie, l'horizon.

Géométrie de la Skybox
Géométrie de la Skybox

Le cube sera positionné de part et d'autre de la camera dans toutes les directions. La boite aura donc une position fixe quel que soit la position de la camera. D'un point de vue de l'orientation, et contrairement à sa position, la boite sera alignée sur les trois axes X, Y et Z du monde. L'orientation sera, évidement, indépendante de la direction de la caméra. Nous placerons, par exemples, les faces Est et Ouest sur l'axe X, Haut et Bas sur l'axe Y et Nord et Sud sur l'axe Z. Ainsi, pour un cube de taille T, les faces opposées seront sur les positions -T/2 et T/2 sur l'axe d'alignement.

Le choix de la taille T interviendra un peu plus loin dans le tutoriel dans le chapitre "Contrainte sur le DepthBuffer"

II-B. La texture "CubeMap"

Maintenant que nous avons donné une forme à notre Skybox, il faut maintenant lui donner des couleurs. Pour cela, nous allons utiliser une texture. Celle-ci ne sera ni à une dimension, ni à deux ... ni à trois ... mais sera une texture cube appelée : "CubeMap" (pour plus de détail sur les textures 1D, 2D et 3D, je vous conseille de vous rendre sur des tutoriels de base sur le texturage). Une CubeMap, qu'est que c'est que cette bête là ??

On peut la placer à mi-chemin entre une texture 2D et une texture 3D. Elle peut se représenter, comme son nom l'indique, par un cube composé de 6 faces. Chacune des faces correspond à une direction par rapport au centre du cube soit les faces X positif, X négatif, Y positif, Y négatif, Z positif et enfin Z négatif. Ainsi, chacune d'elle sera une texture 2D. Donc une texture CubeMap correspond à six textures 2D mais contient moins d'information qu'une texture 3D.

Six Textures 2D
Six Textures 2D

L'avantage d'un tel type de texture est d'assigner une valeur (couleur par exemple) pour une direction donnée. Cela convient tout à fait à notre skybox car pour chaque direction donnée depuis la position de la camera, la texture assignera une couleur. Les coordonnées ne seront donc pas une position précise sur la texture comme ce le serait pour les textures "simples" mais une direction. Ainsi, pour notre géométrie, vue auparavant, alignée sur les axes, et centrée autours de (0;0;0), il est très facile de déterminer les coordonnées de textures. Sachant qu'il n'est pas nécessaire que le vecteur soit normalisé, nous pouvons, pour chaque sommet, passer directement la coordonnée du vertex.

Texture CubeMap
Texture CubeMap

II-C. Contrainte sur le DepthBuffer

Notre skybox est presque prête. Il ne reste qu'un seul problème qui va nous faire revenir sur la partie géométrique. Nous allons faire le choix de la taille de notre boite. Supposons que nous choisissions une taille T de 1000km, ce qui nous permet de simuler une boite à distance pseudo-infinie. Cela pose deux problèmes principaux :

  • les objets se trouvant à 1001km seront derrière le ciel et donc invisible
  • le frustum doit être au minimum de la taille de la boite et englober les 1000km ce qui perd complètement l'avantage du frustum ( limitation de la quantité d'objet affiché )

Rappel : le Frustum défini le champ de vision de la camera. Plus d'info (iciFAQ Frustum)

Ainsi, une taille pseudo-infinie ne convient pas. Pour résoudre le problème du frustum, prenons le cas extrême inverse, soit, choisissons une taille T de 1m. Ce choix ne convient pas non plus car il pose un problème majeur :

  • les objets se retrouvent plus facilement derrière la skybox

Il nous faut donc trouver une méthode pour résoudre l'ensemble de ces problèmes. Nous allons donc nous intéresser au DepthBuffer qui peut nous aider dans cette tâche.

Rappel : le DepthBuffer contient, pour chaque fragment (pixel), la valeur de profondeur de l'objet dessiné le plus proche. Plus d'info (iciFAQ Depth Buffer)

Nous savons que notre skybox doit se trouver toujours derrière l'ensemble des objets de la scène. Nous pouvons donc le rendre en premier, avant tout autre objet, pour qu'il se retrouve derrière. Malheureusement, nous conservons le problème des distances pour une boite trop petite ou trop grande. Nous allons donc utiliser une astuce de rendu pour considérer les faces de la boite comme se trouvant à l'infini même si elles ont des coordonnées proches. Cette astuce consiste à ne pas écrire dans le depth buffer lors du rendu de la skybox pour que tout les objets rendus après soit forcément considérés devant. Cela nous permet de choisir une boite qui peut être de taille 1 du moment qu'elle est rendue avant tout le reste de la scène. Le tableau suivant montre l'évolution du DepthBuffer en utilisant, ou pas, notre astuce. (les valeurs entre parenthèse sont les valeurs de profondeur des objets, 1 infini, 0 proche)

  Valeur initiale Skybox (0.5) Objet (0.7) Valeur finale utilisée
Avec écriture dans le DepthBuffer 1 0.5 0.5 Skybox
Sans écriture dans le DepthBuffer 1 1 0.7 Objet

On peut remarquer qu'un objet se trouvant derrière la skybox, sera quand même affiché devant.

III. Création des textures : Terragen

III-A. Par où commencer ?

Nous avons vu de quoi était composée l'intégralité de notre skybox. Maintenant, nous passons aux choses sérieuses, la pratique. J'en entends déjà dans le fond qui se demandent comment ils vont faire la texture de la skybox car ils ne se sentent pas du tout l'âme d'un artiste... Il y a toujours la possibilité d'en trouver gratuitement sur le web. Et si un outil le faisait pour nous ? Et si il nous créait notre skybox unique pour notre application ?

Ce formidable outil s'appelle Terragen. Il existe en deux versions. La première, gratuite et limitée et la seconde en version payante réservée à un usage commercial uniquement. En tant que programmeur amateur, nous pouvons nous contenter de la première version qui donne des résultats tout à fait acceptables. Cette version est disponible en téléchargement sur le site officiel du logiciel et pour plusieurs plateformes.

Site officiel de TerragenSite officiel de Terragen

Après le téléchargement et l'installation du logiciel, nous pouvons attaquer la génération de notre texture.

Interface générale de Terragen
Interface générale de Terragen

III-B. Configuration de la scène

Lorsque que l'on créé un nouveau monde sous Terragen, il faut commencer par configurer tous les paramètres d'environnement. A gauche de la fenêtre principale se trouve une série de boutons, qui permettent de configurer différentes caractéristiques de la scène comme les nuages, les couleurs, le soleil, le terrain, ...

Boutons de configuration
Boutons de configuration

Au niveau du terrain, par défaut, à la création d'un nouvel environnement, le terrain est plat. Cela nous convient très bien car nous allons nous contenter d'un fond sans relief. Il vous est tout à fait possible de générer des montages avec Terragen. De la même manière, nous n'avons pas besoin de mer ou de quelconque océan. Si vous voulez plus d'informations sur la génération avec du terrain ou avec de l'eau, je vous conseille de vous reporter aux différents tutoriels que l'on peut trouver au travers de la toile.

Une chose un peu plus intéressante à configurer est le paramétrage des nuages. Il est possible de configurer l'altitude des nuages, leur luminosité, leur taille, leur quantité, leur couleur... Je vous laisse faire quelques tests pour voir ce qui vous convient le mieux.

Configuration des nuages
Configuration des nuages

Il est également possible de configurer l'atmosphère de notre environnement à savoir les dégradés de couleurs du ciel. La configuration suivante consiste à paramétrer la lumière du soleil. Différents paramètres peuvent être réglés comme les angles et positions du soleil, la couleur de luminosité, les rayons d'éblouissement, ... Je vous laisse également faire vos propres essais...

Configuration du soleil
Configuration du soleil

Il peut être intéressant de faire correspondre la position du soleil de votre skybox et la position de la lumière principale dans votre scène 3D. Pour cela, notez sur un bout de papier les angles de positionnement.

III-C. Position de la camera et génération des textures

Maintenant, nous pouvons passer aux rendus de nos six textures. Grâce au tout premier bouton, nous pouvons afficher la fenêtre du contrôle du rendu. Cette fenêtre nous permet de positionner la caméra sur notre terrain, le niveau de détail du rendu et de prévisualiser celui-ci.

Configuration du rendu
Configuration du rendu

Il faut commencer par paramétrer notre caméra. Nous allons commencer par la placer au centre de notre scène soit en 128 * 128. Si vous avez modifié la taille du terrain, il faut ajuster ces coordonnées à votre terrain. Nous plaçons également la target de la camera vers le Nord de notre scène, à savoir en 128 * 256. Les angles sont calculés automatiquement en fonction des deux paramètres précédents. Nous plaçons la caméra et sa target à une hauteur de 1 pour se trouver juste au dessus de notre terrain plat. La dernière chose à paramètrer est le zoom de la caméra pour que chaque rendu corresponde à une vision de 90°. Dans la boite de dialogue qui apparait lorsque l'on clique sur le bouton "Camera Setting", nous pouvons configurer précisément le zoom à 1 Nous allons donc positionner la caméra de six manières différentes, une par texture à générée :

Texture générée Position de la caméra Target de la caméra Orientation de la camera
X négatif 128x128x1 128x256x1 0x0x0
X positif 128x128x1 128x0x1 180x0x0
Z négatif 128x128x1 0x128x1 -90x0x0
Z positif 128x128x1 256x128x1 90x0x0
Y négatif 128x128x1 128x128x-128 0x-90x0
Y positif 128x128x1 128x128x128 0x90x0
Image personnelle
  • Pour XN, ZN, XP, ZP : il faut appliquer une rotation de 180° et retourner l'image horizontalement.
  • Pour YN et YP : il faut appliquer une rotation de -90° et ensuite retourner l'image horizontalement.

Nous allons passer maintenant au rendu, proprement dit, des textures. Tout se situe dans la partie gauche de notre fenêtre. Nous avons, tout d'abord la possibilité de prévisualiser nos rendus. Il nous faut également configurer le niveau de détail de notre rendu. Je vous conseille de le mettre au maximum, même si il prend un peu de temps, cela donne des résultats tout à fait acceptables. Nous devons également configurer la taille de notre image finale. Nous la passons à 512x512 pixels ce qui est déjà un grosse texture. Nous sommes maintenant prêts pour rendre nos six textures

Il ne faut pas modifier la scène (terrain, nuage, ...) entre deux rendus de texture car la scène complète sera modifiée et il vous faudra recommencer toutes les textures précédemment rendues. Cela permet de conserver des jointures entre texture relativement propre et invisible.

IV. Un peu de pratique ...

IV-A. Intialisation de la Skybox : Création de la texture CubeMap

Nous avons maintenant tous les éléments nécessaires pour attaquer la vraie partie pratique de programmation. La première chose à faire est d'initialiser notre texture CubeMap. Ce type de texture n'est pas supporté par défaut par OpenGL. Son utilisation se fait par l'extension ARB "GL_ARB_texture_cube_map". Celle-ci est disponible sur toutes les cartes graphiques depuis 1999 environ donc il est très peu probable de trouver une carte ne la supportant pas. Mais pour faire cela propre, nous allons quand même vérifier son support dans notre carte graphique au moyen de la méthode glGetString(GL_EXTENSIONS). Si le nom de l'extension se trouve dans la chaîne de caractères, les textures CubeMap sont supportées. Dans notre cas, seulement sept constantes vont nous être utiles. Celles-ci permettent de définir le type de texture ainsi que chacune de ces faces. Leurs noms doit vous rappeler quelque-chose...

Spécification de l'extension GL_ARB_texture_cube_mapSpecification de l'exstension GL_ARB_texture_cube_map

Extension OpenGL pour les CubeMap
Sélectionnez

#define GL_TEXTURE_CUBE_MAP_ARB             0x8513
#define GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB  0x8515
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_X_ARB  0x8516
#define GL_TEXTURE_CUBE_MAP_POSITIVE_Y_ARB  0x8517
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y_ARB  0x8518
#define GL_TEXTURE_CUBE_MAP_POSITIVE_Z_ARB  0x8519
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z_ARB  0x851A
 
// Test de l'extension GL_ARB_texture_cube_map
char* extensions = (char*) glGetString(GL_EXTENSIONS); 
if(strstr(extensions, "GL_ARB_texture_cube_map") != NULL)
{
	// Initialisation de la CubeMap
} 
else
{
	// Autre système ( six texture 2D par exemple ) ou sortie de l'application ou tout autre gestion d'erreur
}

Nous pouvons maintenant procéder à la création de notre texture. Nous allons donc utiliser nos six textures BMP générées par Terragen. Nous commençons par définir l'ordre de déclaration des textures. Nous chargeons ensuite les textures en mémoire grâce à la méthode LoadBMP vue dans les tutoriels de Nehe.

Tutoriel Nehe sur les texturesTutoriel Nehe sur les textures

Nous demandons ensuite à OpenGL de nous générer une texture de type CubeMap dont un identifiant nous est retourné. Nous ajoutons ensuite nos textures une à une. Nous pouvons terminer par la configuration de la texture avec des éventuels mipmap, ...

Initialisation de la Skybox
Sélectionnez

// Liste des faces successives pour la création des textures de CubeMap
GLenum cube_map_target[6] = {           
	GL_TEXTURE_CUBE_MAP_NEGATIVE_X_ARB,
	GL_TEXTURE_CUBE_MAP_POSITIVE_X_ARB,
	GL_TEXTURE_CUBE_MAP_NEGATIVE_Y_ARB,
	GL_TEXTURE_CUBE_MAP_POSITIVE_Y_ARB,
	GL_TEXTURE_CUBE_MAP_NEGATIVE_Z_ARB,
	GL_TEXTURE_CUBE_MAP_POSITIVE_Z_ARB
};
 
// Chargement des six textures
AUX_RGBImageRec * texture_image[6];
texture_image[0] = LoadBMP( "Skybox/XN.bmp" );
texture_image[1] = LoadBMP( "Skybox/XP.bmp" );
texture_image[2] = LoadBMP( "Skybox/YN.bmp" );
texture_image[3] = LoadBMP( "Skybox/YP.bmp" );
texture_image[4] = LoadBMP( "Skybox/ZN.bmp" );
texture_image[5] = LoadBMP( "Skybox/ZP.bmp" );
 
// Génération d'une texture CubeMap
GLuint cube_map_texture_ID;
glGenTextures(1, &cube_map_texture_ID);
 
// Configuration de la texture
glBindTexture(GL_TEXTURE_CUBE_MAP_ARB, cube_map_texture_ID);
 
for (int i = 0; i < 6; i++)
{
	glTexImage2D(cube_map_target[i], 0, 3, texture_image[i]->sizeX, texture_image[i]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, texture_image[i]->data);
 
	if (texture_image[i])				
	{
		if (texture_image[i]->data)	
		{
			free(texture_image[i]->data);	
		}
		free(texture_image[i]);	
	}
}
 
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_T, GL_CLAMP);

Il est possible que vous deviez tourner vos textures de 180° si vous utilisez un format BMP, car c'est un format inversé.

IV-B. Géometrie de la Skybox

Nous allons maintenant définir une fonction ou méthode DrawSkyBox() qui sera appelée à chaque frame lors du rendu de notre scène. Celle-ci s'occupera de dessiner l'ensemble de notre géométrie. Pour simplifier les choses, nous allons utiliser un TRIANGLE_STRIP ("bande") par face mais cela peut très facilement être optimisé. Nous commençons par définir la taille de notre cube, ou plus précisement, ça demi-taille. Comme nous l'avons vu précédemment, sa valeur n'est pas très importante pour le rendu, il suffit qu'elle se situe dans le frustum d'affichage. Nous continuons en indiquant que nous utilisons la texture créée plus haut. L'étape suivante consiste au positionnement général de la SkyBox. Pour cela, nous réinitialisons la matrice ModelView et nous déplaçons le point de vue uniquement en orientation. Ainsi, la Skybox se trouve centrée autours de la caméra. Enfin, nous définissons chacune de nos faces. Nous pouvons remarquer que les coordonnées de textures correspondent exactement au vertex du cube. Cela viens du fait que nous centrons notre cube autours de la caméra (soit (0;0;0)) et que chaque coordonnée de vertex correspond à sa direction depuis son centre. On termine en rétablissant la matrice ModelView précedement sauvegardée.

Rendu du cube
Sélectionnez

// Taille du cube
float t = 1.0f;
 
// Utilisation de la texture CubeMap
glBindTexture(GL_TEXTURE_CUBE_MAP_ARB, cube_map_texture_ID);
 
// Réglage de l'orientation
glPushMatrix();
glLoadIdentity();
glRotatef( camera_pitch, 1.0f, 0.0f, 0.0f );
glRotatef( camera_yaw, 0.0f, 1.0f, 0.0f );	
 
 
// Rendu de la géométrie
glBegin(GL_TRIANGLE_STRIP);			// X Négatif		
	glTexCoord3f(-t,-t,-t); glVertex3f(-t,-t,-t); 	
	glTexCoord3f(-t,t,-t); glVertex3f(-t,t,-t);
	glTexCoord3f(-t,-t,t); glVertex3f(-t,-t,t);
	glTexCoord3f(-t,t,t); glVertex3f(-t,t,t);
glEnd();
 
glBegin(GL_TRIANGLE_STRIP);			// X Positif
	glTexCoord3f(t, -t,-t); glVertex3f(t,-t,-t);
	glTexCoord3f(t,-t,t); glVertex3f(t,-t,t);
	glTexCoord3f(t,t,-t); glVertex3f(t,t,-t); 
	glTexCoord3f(t,t,t); glVertex3f(t,t,t); 	
glEnd();
 
glBegin(GL_TRIANGLE_STRIP);			// Y Négatif	
	glTexCoord3f(-t,-t,-t); glVertex3f(-t,-t,-t);
	glTexCoord3f(-t,-t,t); glVertex3f(-t,-t,t);
	glTexCoord3f(t, -t,-t); glVertex3f(t,-t,-t);
	glTexCoord3f(t,-t,t); glVertex3f(t,-t,t);
glEnd();
 
glBegin(GL_TRIANGLE_STRIP);			// Y Positif		
	glTexCoord3f(-t,t,-t); glVertex3f(-t,t,-t);
	glTexCoord3f(t,t,-t); glVertex3f(t,t,-t); 
	glTexCoord3f(-t,t,t); glVertex3f(-t,t,t);
	glTexCoord3f(t,t,t); glVertex3f(t,t,t); 	
glEnd();
 
glBegin(GL_TRIANGLE_STRIP);			// Z Négatif		
	glTexCoord3f(-t,-t,-t); glVertex3f(-t,-t,-t);
	glTexCoord3f(t, -t,-t); glVertex3f(t,-t,-t);
	glTexCoord3f(-t,t,-t); glVertex3f(-t,t,-t);
	glTexCoord3f(t,t,-t); glVertex3f(t,t,-t); 
glEnd();
 
glBegin(GL_TRIANGLE_STRIP);			// Z Positif	
	glTexCoord3f(-t,-t,t); glVertex3f(-t,-t,t);
	glTexCoord3f(-t,t,t); glVertex3f(-t,t,t);
	glTexCoord3f(t,-t,t); glVertex3f(t,-t,t);
	glTexCoord3f(t,t,t); glVertex3f(t,t,t); 	
glEnd();
 
// Réinitialisation de la matrice ModelView
glPopMatrix();

IV-C. Rendu principal

Nous pouvons passer enfin au rendu principal de notre application. On commence, comme tout rendu, par vider les différents buffers utilisé, réinitialiser la couleur principale et initialiser la caméra. On peut ainsi commencer le rendu de notre skybox. On défini ainsi que l'on souhaite utiliser le tampon de profondeur, que l'on active l'extension des textures CubeMap et que l'on désactive la lumière. Ce dernier cas nous sert à éviter les problèmes liés à l'ombre des faces sur notre cube et on garde ainsi un rendu continu entre les faces. La fonction glDepthMask() nous sert à définir un masque d'écriture dans le DepthBuffer. Lorsque l'on passe GL_TRUE, on active l'écriture alors que lorsque l'on passe GL_FALSE, on la désactive. On peut enfin dessiner notre skybox et réinitialiser les états du rendu.

Rendu de la SkyBox
Sélectionnez

// Initialisation de la scene
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);	
glLoadIdentity();
glColor3f( 1.0f, 1.0f, 1.0f );
 
// Placement de la camera	
glTranslatef( 0.0f, 0.0f, -8.0f );
glRotatef( CAMERA_Pitch, 1.0f, 0.0f, 0.0f );
glRotatef( CAMERA_Yaw, 0.0f, 1.0f, 0.0f );
 
// Configuration des états OpenGL
glEnable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_CUBE_MAP_ARB); 
glDisable(GL_LIGHTING);
 
// Désactivation de l'écriture dans le DepthBuffer
glDepthMask(GL_FALSE);
 
// Rendu de la SkyBox
DrawSkyBox( camera_yaw, camera_pitch );
 
// Réactivation de l'écriture dans le DepthBuffer
glDepthMask(GL_TRUE);
 
// Réinitialisation des états OpenGL
glDisable(GL_TEXTURE_CUBE_MAP_ARB); 
glEnable(GL_LIGHTING);
 
// .... on continu par le rendu de la scène

Conclusion

Lors de ce tutoriel, nous avons vu comment créer un fond réaliste statique pour votre application 3D. Cela nous a permis d'aborder quelques notions très intéressantes comme les textures CubeMap ou l'utilisation avancé du DepthBuffer. Nous obtenons un résultat tout à fait satisfaisant du moment que la texture utilisée est générée correctement. Dans un prochain tutoriel, nous aborderons les différentes améliorations que l'on peut opérer sur cette Skybox pour la rendre encore plus efficace.

Pour toutes questions, n'hésitez pas à demander directement sur le forum OpenGLPour toutes questions, n'hésitez pas à demander directement sur le forum OpenGL

Rendu final
Rendu final
Rendu final (wireframe)
Rendu final (wireframe)

Sources

Un exemple simple d'utilisation de notre skybox est disponible :

Remerciements

Je tiens particulièrement à remercier LokaLoka et RideKickRideKick pour leur relecture et leur soutien. Je remercie également YnoYno. Et je remercie milenamilena pour ses remarques sur l'orientation des images pour résoudre les problèmes de jointures.