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.
I. Un peu de théorie▲
I-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 identiques. 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'elles correspondra à ce que l'on pourrait voir à une distance infinie, l'horizon.
Le cube sera positionné de part et d'autre de la caméra dans toutes les directions. La boite aura donc une position fixe, quelle que soit la position de la caméra. 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, évidemment, indépendante de la direction de la caméra. Nous placerons, par exemple, 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 ».
I-B. La texture « CubeMap »▲
Maintenant que nous avons donné une forme à notre Skybox, il faut 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étails 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-ce 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 six 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'elles sera une texture 2D. Donc une texture CubeMap correspond à six textures 2D, mais contient moins d'information qu'une texture 3D.
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 caméra, 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 autour 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.
I-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 1000 km, ce qui nous permet de simuler une boite à distance pseudoinfinie. Cela pose deux problèmes principaux :
- les objets se trouvant à 1001km seront derrière le ciel et donc invisibles ;
- le frustum doit être au minimum de la taille de la boite et englober les 1000 km ce qui perd complètement l'avantage du frustum (limitation de la quantité d'objets affichés).
Rappel : le Frustum définit le champ de vision de la caméra. Plus d'info (iciFAQ Frustum).
Ainsi, une taille pseudoinfinie ne convient pas. Pour résoudre le problème du frustum, prenons le cas extrême inverse, soit, choisissons une taille T de 1 m. 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 tous les objets rendus après soient 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èses 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.
II. Création des textures : Terragen▲
II-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 s'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 programmeurs amateurs, 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.
II-B. Configuration de la scène▲
Lorsque l'on crée un nouveau monde sous Terragen, il faut commencer par configurer tous les paramètres d'environnement. À gauche de la fenêtre principale se trouvent 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…
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 montagnes 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.
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…
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.
II-C. Position de la caméra 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.
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 caméra 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érer :
Texture générée |
Position de la caméra |
Target de la caméra |
Orientation de la caméra |
---|---|---|---|
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 |
- 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 s'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à une 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 textures relativement propres et invisibles.
III. Un peu de pratique▲
III-A. Initialisation 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 ses faces. Leur nom doit vous rappeler quelque chose…
#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 textures 2D par exemple) ou sortie de l'application ou toute 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…
// 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é.
III-B. Géométrie 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écisément, sa 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 autour 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 vient du fait que nous centrons notre cube autour 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écédemment sauvegardée.
// Taille du cube
float
t =
1.0
f;
// 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.0
f, 0.0
f, 0.0
f );
glRotatef( camera_yaw, 0.0
f, 1.0
f, 0.0
f );
// 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();
III-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és, réinitialiser la couleur principale et initialiser la caméra. On peut ainsi commencer le rendu de notre skybox. On définit 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.
// Initialisation de la scène
glClear (GL_COLOR_BUFFER_BIT |
GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
glColor3f( 1.0
f, 1.0
f, 1.0
f );
// Placement de la caméra
glTranslatef( 0.0
f, 0.0
f, -
8.0
f );
glRotatef( CAMERA_Pitch, 1.0
f, 0.0
f, 0.0
f );
glRotatef( CAMERA_Yaw, 0.0
f, 1.0
f, 0.0
f );
// 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 continue 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ée 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.
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.