VI. Texture cubique (cubemap)▲
Nous utilisons les textures 2D depuis quelque temps déjà, mais il existe d’autres types de texture que nous n’avons pas encore vu. Ce tutoriel explore un de ces autres types, qui est en réalité une combinaison de plusieurs textures assemblées en une : une texture cubique (cubemap).
Une texture cubique est une texture qui contient un ensemble de six textures 2D pouvant être appliqué sur chaque face du cube : un cube texturé. Vous pouvez vous demander à quoi cela sert : en effet, pourquoi s’ennuyer à assembler six textures en une seule entité alors qu’il est possible d’utiliser six textures séparément ? Eh bien, les textures de cube ont la particularité d’être indexables/échantillonnables grâce à un vecteur direction. Imaginez que nous avons une texture cubique de 1x1x1 avec comme origine un vecteur direction en son centre. L’échantillonnage d’une valeur de la texture cubique avec le vecteur orange donnera ceci :
La magnitude du vecteur direction n’est pas important. Tant que la direction est fournie, OpenGL récupère le texel que le vecteur direction atteint (in fine) et retourne la valeur de la texture appropriée.
Si nous imaginons un cube et que nous y attachons une texture cubique, le vecteur direction qui échantillonnera la texture sera similaire à une position (interpolée) sur le cube. De cette façon, nous pouvons échantillonner la texture cubique à l’aide de la position dans le cube tant que celui-ci est centré en son origine. Nous pouvons obtenir les coordonnées de texture de tous les sommets juste en utilisant la position du sommet du cube. Le résultat est une coordonnée de texture qui accède à la face correspondante dans la texture cubique.
VI-A. Créer une texture cubique▲
Une texture cubique est une texture comme toute autre, qu’il suffit de générer et de lier à la cible correspondante avant de faire plus d’opérations. Cette fois, nous lions au type GL_TEXTURE_CUBE_MAP :
unsigned
int
textureID;
glGenTextures
(
1
, &
textureID);
glBindTexture
(
GL_TEXTURE_CUBE_MAP, textureID);
Comme la texture cubique est composée de six textures, une par face, nous devons appeler six fois la fonction glTexImage2D() avec des paramètres similaires à ceux vus dans les précédents tutoriels. Cette fois, nous devons définir le paramètre de cible de la texture pour définir la face de la texture cubique. Cela indique à OpenGL sur quelle face nous souhaitons la texture que nous créons. Cela signifie que nous devons appeler glTexImage2D() pour chaque face de la texture cubique.
Comme nous avons six faces, OpenGL nous fournit six cibles :
Cible de texture |
Orientation |
GL_TEXTURE_CUBE_MAP_POSITIVE_X |
Droite |
GL_TEXTURE_CUBE_MAP_NEGATIVE_X |
Gauche |
GL_TEXTURE_CUBE_MAP_POSITIVE_Y |
Haut |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y |
Bas |
GL_TEXTURE_CUBE_MAP_POSITIVE_Z |
Arrière |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z |
Avant |
Comme pour la majorité des énumérations OpenGL, c’est un int dont toutes les valeurs se suivent. Si nous devions avoir un tableau pour contenir les emplacements des textures, nous pourrions le parcourir en commençant par GL_TEXTURE_CUBE_MAP_POSITIVE_X et incrémenter l’index de 1 afin de boucler sur toutes les cibles de textures.
int
width, height, nrChannels;
unsigned
char
*
data;
for
(
GLuint i =
0
; i <
textures_faces.size
(
); i++
)
{
data =
stbi_load
(
textures_faces[i].c_str
(
), &
width, &
height, &
nrChannels, 0
);
glTexImage2D
(
GL_TEXTURE_CUBE_MAP_POSITIVE_X +
i,
0
, GL_RGB, width, height, 0
, GL_RGB, GL_UNSIGNED_BYTE, data
);
}
Ici, nous avons un std::vector appelé textures_faces qui contient les emplacements de toutes les textures nécessaires pour la texture cubique. Cela génère une texture pour chaque face de la texture cubique liée.
Comme la texture cubique est une texture comme toute autre, nous devons aussi spécifier ses méthodes de filtrage :
glTexParameteri
(
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri
(
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri
(
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri
(
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri
(
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
Ne soyez pas impressionnés par le GL_TEXTURE_WRAP_R. Il définit simplement la méthode de dépassement pour la coordonnée R qui correspond à la troisième dimension de la texture (comme le z pour la position). Nous définissons la méthode de dépassement à GL_CLAMP_TO_EDGE comme les coordonnées de texture entre deux faces ne vont pas toujours atteindre une même face (à cause des limitations matérielles). Donc, en utilisant GL_CLAMP_TO_EDGE, OpenGL retournera toujours les valeurs de bordure lorsque nous échantillonnerons entre deux faces.
Avant de dessiner des objets utilisant notre texture cubique, nous devons activer l’unité de texture correspondante et lier la texture cubique. En réalité, c’est comme pour les textures 2D.
Dans le fragment shader, nous devons aussi utiliser un nouveau type d’échantillonneur samplerCube que nous utilisons conjointement avec la fonction texture(). Évidemment, les coordonnées seront dans un vecteur de type vec3 au lieu de vec2. Voici un exemple utilisant une texture cubique :
in vec3 textureDir; // vecteur direction représentant une coordonnée de texture 3D
uniform samplerCube cubemap; // échantillonneur pour une texture cubique
void
main
(
)
{
FragColor =
texture
(
cubemap, textureDir);
}
C’est bien, mais pourquoi s’en soucier ? Eh bien, il existe quelques techniques intéressantes qui sont plus faciles à implémenter avec un texture cubique. Une de ces techniques est la création d’une skybox.
VI-B. Skybox▲
Une skybox est un (grand) cube qui englobe l’intégralité de la scène et contient six images de l’environnement l’englobant. Ainsi, le joueur a l’illusion d’être dans un environnement bien plus grand que ce qu’il est vraiment. Certains exemples de skybox de jeux vidéos possèdent des montagnes, des nuages ou un ciel étoilé. Dans la capture suivante du troisième opus d’Elder Scrolls, vous pouvez y voir une skybox de ciel étoilé :
Vous le savez sûrement déjà, les textures cubiques sont la solution parfaite pour les skybox : nous avons un cube avec six faces qui a besoin d’une texture par face. Dans l’image précédente, les développeurs ont utilisé plusieurs images de ciel étoilé de nuit pour donner au joueur l’impression qu’il y a un univers bien plus grand que cette simple petite boite.
Il est facile de trouver des skybox en ligne. Ce site, par exemple, a un grand ensemble de skybox. Les images de skybox sont généralement conçues ainsi :
Si vous pliez ces six carrés, vous obtenez un cube complètement texturé simulant un grand paysage. Certains sites fournissent les skybox dans une image comme celle-ci et vous devez extraire manuellement les six faces, mais dans la majorité des cas, vous aurez six images.
Cette skybox (de haute qualité) est celle que nous allons utiliser dans notre scène et peut être téléchargée ici.
VI-B-1. Chargement d’une skybox▲
Comme la skybox n’est qu’une texture cubique, le chargement ne diffère pas de ce que nous avons vu précédemment. Pour charger la skybox, nous allons utiliser la fonction suivante qui accepte un std::vector des six emplacements de texture :
unsigned
int
loadCubemap
(
vector<
std::string>
faces)
{
unsigned
int
textureID;
glGenTextures
(
1
, &
textureID);
glBindTexture
(
GL_TEXTURE_CUBE_MAP, textureID);
int
width, height, nrChannels;
for
(
unsigned
int
i =
0
; i <
faces.size
(
); i++
)
{
unsigned
char
*
data =
stbi_load
(
faces[i].c_str
(
), &
width, &
height, &
nrChannels, 0
);
if
(
data)
{
glTexImage2D
(
GL_TEXTURE_CUBE_MAP_POSITIVE_X +
i,
0
, GL_RGB, width, height, 0
, GL_RGB, GL_UNSIGNED_BYTE, data
);
stbi_image_free
(
data);
}
else
{
std
:
:cout <<
"
Cubemap texture failed to load at path:
"
<<
faces[i] <<
std::endl;
stbi_image_free
(
data);
}
}
glTexParameteri
(
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri
(
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri
(
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri
(
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri
(
GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
return
textureID;
}
La fonction ne doit pas être très surprenante. C’est grossièrement le code de chargement d’une texture cubique que nous avons vu dans la section précédente, mais intégré dans une unique fonction.
Avant d’appeler cette fonction, nous devons charger les chemins des textures dans le std::vector dans l’ordre de l’énumération OpenGL :
vector<
std::string>
faces;
{
"
right.jpg
"
,
"
left.jpg
"
,
"
top.jpg
"
,
"
bottom.jpg
"
,
"
front.jpg
"
,
"
back.jpg
"
}
;
unsigned
int
cubemapTexture =
loadCubemap
(
faces);
Nous allons maintenant charger la skybox dans une texture cubique avec la fonction cubemapTexture(). Nous pouvons lier la texture à un cube et enfin remplacer la couleur de fond que nous utilisions depuis tout ce temps.
VI-B-2. Afficher une skybox▲
Comme la skybox est affichée sur un cube, nous devons créer d’autres VAO et VBO, ainsi qu’un nouvel ensemble de sommets. Vous pouvez obtenir les données des sommets ici.
Utiliser une texture cubique pour texturer un cube 3D peut s’effectuer en utilisant la position du cube comme coordonnées de texture. Lorsque le cube est centré en (0, 0, 0), chaque point est aussi une direction depuis son origine. La direction est exactement ce dont nous avons besoin pour obtenir les valeurs correspondantes de la texture en cette position du cube. Pour cette raison, nous ne fournissons que les positions et nous ne fournissons pas de coordonnées de texture.
Pour afficher la skybox, nous allons avoir besoin d’un nouvel ensemble de shaders qui ne sont pas très compliqués. Comme nous n’avons que la position des sommets comme attribut, c’est plutôt simple :
#
version
330
core
layout
(
location =
0
) in
vec3
aPos;
out
vec3
TexCoords;
uniform
mat4
projection;
uniform
mat4
view;
void
main
(
)
{
TexCoords =
aPos;
gl_Position
=
projection *
view *
vec4
(
aPos, 1
.0
);
}
La partie intéressante du shader est que les positions en entrée sont copiées en sortie pour devenir des coordonnées de texture pour le fragment shader. Ce dernier les utilise pour échantillonner un samplerCube:
#
version
330
core
out
vec4
FragColor;
in
vec3
TexCoords;
uniform
samplerCube
skybox;
void
main
(
)
{
FragColor =
texture
(
skybox, TexCoords);
}
Le fragment shader est évident. Nous prenons la position des sommets comme vecteur de direction pour la texture et les utilisons pour échantillonner les valeurs de la texture cubique.
Le rendu de la skybox est simple maintenant que nous avons la texture cubique. Nous lions simplement la texture et l’échantillonneur skybox est automatiquement connecté avec la texture cubique. Pour dessiner la skybox, nous allons la dessiner en premier et en désactivant les tests de profondeur. De cette façon, nous sommes certains que la skybox sera toujours au fond, derrière tous les autres objets.
glDepthMask
(
GL_FALSE);
skyboxShader.use
(
);
// ... définir la matrice de vue et projection
glBindVertexArray
(
skyboxVAO);
glBindTexture
(
GL_TEXTURE_CUBE_MAP, cubemapTexture);
glDrawArrays
(
GL_TRIANGLES, 0
, 36
);
glDepthMask
(
GL_TRUE);
// ... dessiner le reste de la scène
Si vous exécutez ce code en l’état, vous allez avoir des problèmes. Nous souhaitons avoir une skybox centrée autour du joueur, peu importe ses déplacements. La skybox ne s’approchera pas du joueur, ce qui donnera l’impression d’un environnement immense. La matrice de vue actuelle transforme les sommets de la skybox en les tournant, en les redimensionnant et en les déplaçant. Par conséquent, si le joueur se déplace, la skybox se déplace aussi ! Nous souhaitons supprimer le déplacement de la matrice de vue afin que le mouvement n’affecte pas la position de la skybox
Souvenez-vous du tutoriel d’éclairage basique, où nous apprenions que l’on peut supprimer la translation en ne gardant que la partie 3x3 supérieure gauche d’une matrice 4x4. Nous pouvons faire cela en convertissant la matrice de vue en une matrice 3x3 (supprimant ainsi la translation) et en la reconvertissant en matrice 4x4 :
glm
:
:mat4 view =
glm::mat4
(
glm::mat3
(
camera.GetViewMatrix
(
)));
Cela enlève toute translation, tout en gardant les autres transformations, ce qui permet ainsi au joueur de regarder autour de lui.
Le résultat rend la scène immense grâce à la skybox. Si vous naviguez autour du conteneur, vous avez une notion de sa taille, ce qui augmente le réalisme. Le résultat doit être celui-ci :
Essayez d’expérimenter avec différentes skybox et de voir leur impact sur l’aspect de la scène.
VI-B-3. Optimisation▲
Pour le moment, nous avons dessiné la skybox avant tous les autres objets de la scène. Cela fonctionne très bien, mais ce n’est pas efficace. Si nous dessinons la skybox en premier, nous exécutons le fragment shader pour chaque pixel de l’écran, même si certaines parties de la skybox seront cachées. Les fragments qui auraient pu être ignorés pourraient nous aider à gagner en bande passante.
Pour obtenir un léger gain de performance, nous allons donc dessiner la skybox en dernier. De cette façon, le tampon de profondeur est complètement repli avec les valeurs de profondeur de chaque objet. Ainsi, nous ne dessinerons que les pixels de la skybox qui seront réellement visibles, c’est-à-dire, ceux qui passent le test de profondeur. Le problème est que la skybox risque de ne pas s’afficher, car ce n’est qu’un cube 1x1x1 qui échouera à tous les tests de profondeur. Son affichage sans test de profondeur ne résout pas le problème, car la skybox recouvrera tous les objets de la scène. Nous devons tromper le tampon de profondeur afin de faire croire que la skybox a la profondeur maximale (soit 1,0), de telle sorte qu’elle échouera à tous les tests où la profondeur est différente, c’est-à-dire lorsqu’il y a un autre objet devant.
Dans le tutoriel sur les systèmes de coordonnées, nous avons expliqué que la division de perspective est exécutée après l’exécution du vertex shader. Elle divise les coordonnées xyz de gl_Position par la composante w. Nous savons aussi, grâce au tutoriel sur le test de profondeur, que la composante z résultant de la division est égale à la valeur de la profondeur du sommet. Grâce à cette information, nous pouvons définir la composante z de la position de sortie égale à la composante w. Ainsi, la division de w/z donne toujours 1,0, car la division devient w/w = 1,0.
void
main
(
)
{
TexCoords =
aPos;
vec4
pos =
projection *
view *
vec4
(
aPos, 1
.0
);
gl_Position
=
pos.xyww;
}
Le résultat obtenu en coordonnées normalisées sera toujours une valeur z égale à 1, soit la valeur de profondeur maximale. La skybox ne sera dessinée qu’aux emplacements où il n’y a pas d’objet visible (seuls ceux qui passent le test de profondeur, le reste du monde est devant la skybox).
Nous devons changer la fonction du test de profondeur pour le définir à GL_LEQUAL au lieu de GL_LESS. Le tampon de profondeur sera toujours rempli avec la valeur 1,0 pour la skybox, nous devons donc nous assurer que la skybox réussisse le test de profondeur pour les valeurs plus petites ou égales à celle dans le tampon de profondeur.
Vous pouvez trouver cette version optimisée ici.
VI-C. Application de l’environnement▲
Nous avons maintenant un environnement englobant grâce à l’application d’une simple texture. Nous pouvons aussi utiliser les textures cubiques pour d’autres effets. En particulier, elles nous permettent d’appliquer des propriétés de réflexion et de réfraction aux objets. Ces techniques qui utilisent une texture cubique d’environnement s’appellent « application de l’environnement » (environment mapping) et les deux plus courantes sont la réflexion et la réfraction.
VI-C-1. Réflexion▲
La propriété de réflexion fait qu’un objet (ou une partie d’un objet) reflète son environnement, c’est-à-dire, les couleurs de l’objet sont plus ou moins basées sur l’environnement l’englobant et l’angle du spectateur. Un miroir est un exemple d’objet reflétant : il reflète son environnement suivant la position du spectateur.
La réflexion de base n’est pas difficile. L’image suivante montre comment calculer un vecteur de réflexion et utilise ce vecteur pour échantillonner la texture cubique :
Nous calculons un vecteur réfléchissant kitxmlcodeinlinelatexdvp\color{green}{\bar{R}}finkitxmlcodeinlinelatexdvp grâce à la normale de l’objet kitxmlcodeinlinelatexdvp\color{red}{\bar{N}}finkitxmlcodeinlinelatexdvp et la direction du spectateur kitxmlcodeinlinelatexdvp\color{gray}{\bar{I}}finkitxmlcodeinlinelatexdvp. Nous calculons le vecteur de réflexion grâce à la fonction GLSL reflect(). Le vecteur kitxmlcodeinlinelatexdvp\color{green}{\bar{R}}finkitxmlcodeinlinelatexdvp résultant est utilisé comme direction pour indexer/échantillonner la texture cubique et ainsi obtenir la couleur de l’environnement. L’effet final est que l’objet semble réfléchir la skybox.
Comme nous avons déjà configuré la skybox pour notre scène, la création des reflets n’est pas très compliquée. Nous changeons le fragment shader utilisé par le conteneur pour lui donner des propriétés réfléchissantes :
#
version
330
core
out
vec4
FragColor;
in
vec3
Normal;
in
vec3
Position;
uniform
vec3
cameraPos;
uniform
samplerCube
skybox;
void
main
(
)
{
vec3
I =
normalize
(
Position -
cameraPos);
vec3
R =
reflect
(
I, normalize
(
Normal));
FragColor =
vec4
(
texture
(
skybox, R).rgb, 1
.0
);
}
D’abord, nous calculons le vecteur direction de la caméra I. Celui-ci permet d’obtenir le vecteur de réflexion R, qui est ensuite utilisé pour échantillonner la texture cubique de la skybox. Notez que les variables interpolées Normal et Position du fragment shader doivent être modifiées.
#
version
330
core
layout
(
location =
0
) in
vec3
aPos;
layout
(
location =
1
) in
vec3
aNormal;
out
vec3
Normal;
out
vec3
Position;
uniform
mat4
model;
uniform
mat4
view;
uniform
mat4
projection;
void
main
(
)
{
Normal =
mat3
(
transpose
(
inverse
(
model))) *
aNormal;
Position =
vec3
(
model *
vec4
(
aPos, 1
.0
));
gl_Position
=
projection *
view *
model *
vec4
(
aPos, 1
.0
);
}
Nous utilisons les normales, nous devons donc les transformer avec la matrice des normales. Le vecteur de sortie Position est un vecteur dans l’espace monde. Cette sortie est utilisée pour calculer la direction du spectateur dans le fragment shader.
Comme nous utilisons les normales, nous souhaitons mettre à jour les données des sommets et les pointeurs d’attributs. Assurez-vous de définir la variable uniforme cameraPos.
Ensuite, nous souhaitons lier la texture cubique avant d’afficher le conteneur :
glBindVertexArray
(
cubeVAO);
glBindTexture
(
GL_TEXTURE_CUBE_MAP, skyboxTexture);
glDrawArrays
(
GL_TRIANGLES, 0
, 36
);
Compilez et exécutez votre code afin d’obtenir un conteneur réfléchissant comme un miroir. La skybox est parfaitement reflétée sur le conteneur :
Vous pouvez trouver le code source ici.
Lorsque la réflexion est appliquée à l’intégralité d’un objet (comme le conteneur), l’objet semble être un matériau hautement réfléchissant comme de l’acier ou du chrome. Si nous chargeons le modèle nanosuit utilisé dans le tutoriel sur le chargement d’objet 3D, nous aurions un effet faisant croire que la combinaison est en chrome :
Cela est génial, mais, en réalité, la plupart des modèles ne sont pas complètement réfléchissants. Nous pouvons, par exemple, utiliser des textures de réflexion offrant des détails supplémentaires au modèle. Tout comme pour la texture diffuse et spéculaire, la texture de réflexion est une image qui est échantillonnée pour déterminer la propriété réfléchissante du fragment. Ainsi, nous pouvons déterminer quelle partie du modèle doit réfléchir l’environnement et avec quelle intensité. Dans les exercices de ce tutoriel, c’est à vous d’ajouter une texture de réflexion pour améliorer les détails de ce modèle
VI-C-2. Réfraction▲
Une autre technique d’application de l’environnement s’appelle la réfraction et s’apparente à la réflexion. La réfraction change la direction de la lumière, à cause du changement de matière traversée par celle-ci. La réfraction se voit généralement avec l’eau, où la lumière ne traverse pas le milieu aqueux en ligne droite. C’est comme regarder son bras lorsqu’il est à moitié dans l’eau.
La réfraction est décrite par la loi de Snell, qui ressemble à cela avec texture d’environnement :
Encore une fois, nous avons le vecteur du spectateur kitxmlcodeinlinelatexdvp\color{gray}{\bar{I}}finkitxmlcodeinlinelatexdvp, et une normale kitxmlcodeinlinelatexdvp\color{red}{\bar{N}}finkitxmlcodeinlinelatexdvp qui permettent d’obtenir un vecteur de réfraction kitxmlcodeinlinelatexdvp\color{green}{\bar{R}}finkitxmlcodeinlinelatexdvp. Comme vous pouvez le voir, la direction du vecteur du spectateur est légèrement déviée. Le vecteur dévié kitxmlcodeinlinelatexdvp\color{green}{\bar{R}}finkitxmlcodeinlinelatexdvp est utilisé pour échantillonner la texture cubique.
La réfraction peut être facilement implémentée grâce à la fonction GLSL refract(), qui prend une normale, la direction du spectateur et un ratio entre les deux indices de réfraction des matériaux.
L’indice de réfraction est la quantité de lumière déviée par le matériau. Chaque matériau a son propre indice de réfraction. Voici une liste des indices les plus communs :
Matériau |
Indice de réfraction |
Air |
1,00 |
Eau |
1,33 |
Glace |
1,309 |
Verre |
1,52 |
Diamant |
2,42 |
Nous utilisons ces indices pour calculer le ratio de lumière passant entre deux matériaux. Dans notre cas, la lumière/rayon du spectateur passe de l’air au verre (si nous prenons un conteneur en verre). Le ratio est donc :
kitxmlcodelatexdvp\frac{1.00}{1.52} = 0.658finkitxmlcodelatexdvpNous avons déjà la texture cubique liée, les données des sommets avec leurs normales et la définition de la caméra dans une variable uniforme. Il ne reste plus qu’à changer le fragment shader :
void
main
(
)
{
float
ratio =
1
.00
/
1
.52
;
vec3
I =
normalize
(
Position -
cameraPos);
vec3
R =
refract
(
I, normalize
(
Normal), ratio);
FragColor =
vec4
(
texture
(
skybox, R).rgb, 1
.0
);
}
En changeant l’indice de réfraction, vous pouvez créer des résultats très différents. La compilation et l’exécution de l’application n’a que peu d’intérêt, car nous n’utilisons qu’un simple conteneur. Il n’expose pas assez bien la réfraction qui n’agit que comme loupe. Pas contre, en utilisant les mêmes shaders et le modèle de nanosuit, nous obtenons bien un objet semblant être en verre.
Vous pouvez imaginer qu’avec la bonne combinaison de lumière, réflexion, réfraction ainsi qu’en déplaçant les sommets, vous pouvez créer un effet d’eau vraiment joli. Notez que, pour obtenir des résultats physiquement réalistes, nous devrions réfracter la lumière lorsqu’elle sort de l’objet. Pour le moment, nous utilisons juste une réfraction dans un seul sens, ce qui est suffisant pour la plupart des cas.
VI-C-3. Application de l’environnement dynamique▲
Jusqu’à présent, nous avons utilisé un ensemble d’images statiques pour la skybox. C’est bien, mais cela n’inclut pas les autres objets de la scène, qui peuvent se déplacer. Nous ne l’avons pas remarqué, car nous n’utilisons qu’un seul objet. Si nous avions plusieurs objets réfléchissant d’autres objets aux alentours, seule la skybox serait visible dans le miroir, comme si c’était l’unique objet de la scène.
Grâce au tampon d’image, il est possible de créer une texture de la scène avec les six différents angles aperçus de l’objet et de stocker cela dans une texture cubique à chaque itération. Nous pouvons ensuite l’utiliser (générée dynamiquement) pour créer des surfaces réfléchissantes et réfringentes qui incluent tous les objets. Cela s’appelle une texture d’environnement dynamique, car nous créons la texture cubique dynamiquement et nous l’utilisons comme texture d’environnement.
Même si c’est beau, cela a un gros désavantage : nous devons afficher la scène six fois par objet qui utilise une texture d’environnement. Les applications modernes essaient d’utiliser la skybox autant que possible et des textures cubiques pré-générées pour obtenir un effet similaire. Bien que les textures d’environnement dynamiques soient une bonne technique, cela nécessite beaucoup d’astuces et de hacks pour réussir à l’appliquer sans avoir de gros défaut de performances.
VI-D. Exercices▲
-
Essayez d’ajouter une texture de réflexion dans le chargement du modèle que nous avons créé au cours du tutoriel sur le chargement. Vous pouvez trouver un modèle de nanosuit à jour, avec texture de réflexion, ici. Il y a quelques points à noter :
- Assimp n’aime pas les textures de réflexion dans la plupart des formats de modèle 3D. Nous avons donc triché en l’implémentant comme une texture ambiante. Vous pouvez charger la texture de réflexion en spécifiant aiTextureType_AMBIENT comme type de texture lors du chargement des matériaux ;
- je me suis dépêché de créer les textures de réflexion à partir des textures spéculaire. Du coup, la texture de réflexion ne correspondra pas parfaitement au modèle :) ;
- comme le chargement du modèle prend déjà trois unités de texture dans le shader, vous devez charger la skybox dans une quatrième unité, car vous devez échantillonner la skybox dans un même shader que les autres textures.
- Si vous avez réussi, cela devrait vous donner ceci.
VI-E. Remerciements▲
Ce tutoriel est une traduction réalisée par Alexandre Laurent dont l’original a été écrit par Joey de Vries et qui est disponible sur le site Learn OpenGL.