Tutoriels iOS

| Abdallah DIB | Alexandre Laurent |

Traducteur

Jérôme Marsaguet

Cet article regroupe une série de tutoriels OpenGL ES 2.0 pour iPhone. En lisant ceux-ci, vous apprendrez à charger des modèles, appliquer un bump mapping, appliquer des effets de lumière et plus encore.

Commentez

- Titre : Tutoriels iOS

- Auteurs : Abdallah DIB, Alexandre Laurent

- Traducteur : Jérôme Marsaguet

- Parution : 1 mars 2013

- Mise à jour : 1 mars 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Abdallah DIB. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Téléchargement

- III. Tutoriel 1. Importer des assets

- III-A. Code de la procédure

- IV. Tutoriel 2. Placage de relief

- IV-A. Code de la procédure

- V. Tutoriel 3. Animation de personnages (skinning) sur GPU

- V-A. Code de la procédure

- VI. Tutoriel 4. Mouvement de la caméra quaternion

- VII. Tutoriel 5. SkyBox

- VIII. Tutoriel 6. Réflexion de l'eau

- VIII-A. Code de la procédure

- IX. Tutoriel 7. Éclairage avec relief

- IX-A. Code de la procédure

- X. Tutoriel 8. Réflexion par cube mapping

- X-A. Code de la procédure

- XI. Remerciements

I - Introduction

Dans cette page, vous trouverez une liste de tutoriels sur OpenGL ES 2.0. Vous devez connaître le C++ et avoir de solides bases en OpenGL/GLSL (transformations, VBO, VAO, FBO et l'écriture de shaders). Tous les tutoriels utilisent le framework commun. Ce framework se trouve dans le dossier « engine » du fichier zip. Il est tiré de mon propre moteur appelé Virtual Vision.

Virtual Vision fut initialement écrit pour PC et a été porté plus tard sur iOS. J'ai décidé ensuite de faire quelque chose de plus intéressant avec le moteur. J'ai donc commencé à écrire ma première application pour iPhone qui est maintenant disponible sur l'Apple Store nommée Camera Art Effects qui permet d'appliquer des effets de posttraitement en temps réel sur l'image de la caméra. J'ai ensuite développé un jeu avec ce même moteur qui est aussi disponible sur l'Apple Store, Jumper Buggy, un jeu de simulation et d'action en 3D. Je vous recommande de lire cet article si vous souhaitez écrire votre propre moteur.

Restez à l'écoute des futurs tutoriels. Ci-dessous la feuille de route (par ordre de priorité) :

- mapping de l'environnement (FAIT) ;

- éclairage par pixel avancé (FAIT) ;

- mapping d'ombres (EN COURS) ;

- sprite 2D ;

- rendu différé (alors qu'il est toujours possible d'effectuer un rendu sur le G-buffer avec la version 2.0 d'OpenGL ES, ce qui nécessite plusieurs passes pour stocker la géométrie, les matériaux et les normales, cela pourrait être réuni dans un seul passage sur les appareils compatibles avec la future version 3.0 d'OpenGL ES [grâce au rendu sur cibles multiples uniquement. La prochaine version permettra aux pixel shaders d'écrire dans plusieurs tampons]). Les futurs jeux sur appareils mobiles devraient donc remplacer leurs modules de rendu par des plus modernes ;

- occlusion culling (uniquement avec les appareils compatibles avec la version 3.0 d'OpenGL ES).

Tous les tutoriels ont le même point d'entrée situé dans les fichiers Tutorial.h/Tutorial.cpp. Il y a deux fonctions dans cette classe, la première nommée « Deploy » qui charge les shaders et les ressources du disque pour le tutoriel et la seconde nommée Frame() qui affiche les images à l'écran à une fréquence de 60 images/seconde. Cette fonction est appelée par le viewControler de l'application.

Les tutoriels ne couvrent pas tout le code source, ils traitent seulement des parties principales, il vous reste à regarder en détail le code source. Le code est documenté et facile à parcourir si vous connaissez bien le C++.

Pour les questions ou rapports de bogues, retrouvez-moi ici :

- e-mail : abdallah.dib(replace_with_at)virtual-vision.net

- Twitter : @abdallah_dib

II - Téléchargement

Le framework et les exemples de code sont distribués sous licence LGPL.

Cliquez ici pour télécharger le code source pour le framework et les exemples de code.

III - Tutoriel 1. Importer des assets

Ce tutoriel montre comment utiliser Assimp pour importer et effectuer le rendu d'assets. Assimp est une bibliothèque open source capable de charger une variété de modèles 3D (3ds, obj, X, modèles quake et doom). En plus des positions des vertices, des normales, des coordonnées de texture, des tangentes, des os et leur poids (utilisés pour l'animation de personnages dans un prochain tutoriel), les matériaux et textures diffuses sont extraits du modèle et stockés dans un CMeshBuffer. Le CMeshBuffer contient tous les groupes et matériaux du mesh. Les groupes peuvent partager des matériaux. Lors du rendu du modèle, nous parcourons chaque groupe du buffer mesh, trouvons le matériau correspondant à chaque groupe, nous associons les propriétés du matériau au shader et dessinons finalement le groupe.

III-A - Code de la procédure

Nous utilisons la classe CAssimpMesh pour charger le mesh dans le tableau OpenGL comme expliqué dans la section précédente. Pour faire le rendu du CMeshBuffer, nous parcourons tous les groupes du buffer mesh, obtenons le matériau correspondant et envoyons les bonnes informations du matériau au shader et dessinons le groupe. Le code ci-dessous montre la façon dont le CMeshEntity effectue le rendu de son buffer mesh.

// Récupère le buffer mesh
CMeshBuffer* buffer = m_pMesh->GetMeshBuffer();

// Parcours de tous les groupes du buffer mesh
for(uint32 g = 0; g < buffer->GroupsCount(); ++g)
{
// Rendu de chaque groupe
CMeshGroup* grp = buffer->GroupAtIndex(g);

// Récupération du matériau correspondant à chaque groupe
CMaterial* material = buffer->MaterialForGroup(grp);

// Association des propriétés du matériau au shader (nous associons seulement la texture diffuse car il n'y a pas d'éclairage)
if(material != NULL )
{
if( material->diffuseTexture != NULL && shader->texture0 != -1)
{
material->diffuseTexture->ActivateAndBind(GL_TEXTURE0);
glUniform1i(shader->texture0, 0);
}
}

// Activation du Vertex Array Object (VAO)
grp->MapToGPU(0);

// Dessin de chaque groupe
glDrawElements(grp->GetDrawingMode(), grp->GetIndices().size(), GL_UNSIGNED_SHORT, 0);
}

IV - Tutoriel 2. Placage de relief

Ce tutoriel montre comment effectuer un placage de relief simple et efficace sur iOS. Cette technique est largement utilisée dans les jeux pour ajouter des détails aux modèles sans avoir besoin d'un modèle complexe ou comportant un nombre élevé de polygones. Nous avons besoin d'une texture contenant les normales en plus de la diffuse. La position de l'éclairage est envoyée au shader dans le système de coordonnées espace monde puis convertie en espace tangent pour effectuer les calculs de lumière. Dans le pixel shader, nous récupérons la normale depuis la normal map pour chaque pixel, menant ainsi à un calcul non uniforme de la lumière sur la surface du modèle. Sans la normal map nous aurions un éclairage uniforme sur une surface plane. La capture d'écran ci-dessous montre le résultat de cette technique appliquée à une simple caisse. (Modèle 3D de turbosquid.com.)

IV-A - Code de la procédure

Le shader a besoin d'une texture normale définissant le vecteur normal pour chaque pixel. Nous fournissons aussi la position de la lumière dans l'espace modèle, ce qui peut être fait en multipliant la position de la lumière par l'inverse de la matrice de transformation du modèle comme suit :

// Cette partie sert à animer et déplacer la lumière sur une trajectoire circulaire
static float y = 0.f;
static float x = 0.f;
x = -80.f * sinf(time);
y = -80.f * cosf(time);
// Conversion de la position de la lumière dans l'espace modèle, de sorte que le calcul de l'éclairage puisse être effectué dans l'espace modèle au sein du shader
// Pour ce faire, nous multiplions la position de la lumière par l'inverse de la matrice de transformation du modèle
vec4f lightPos = modelBumpMeshMat.inverse() * vec4f(x, y, 0, 1.f);

// Envoi de la position de la lumière au shader
m_pShaderBump->SetUniform3fv("LightPosModel", 1, &lightPos.x);

Après, dans le vertex shader, nous construisons la matrice Tangent Bitangent Normal (TBN). Cette matrice effectue le passage du système de coordonnées modèle au système tangent. Nous multiplions ainsi le vecteur direction de la lumière par cette matrice pour le convertir dans l'espace tangent. En fait nous avons besoin d'un système de coordonnées commun pour effectuer le calcul de l'éclairage. Les normales et tangentes étant données dans l'espace tangent, nous devons faire passer le vecteur lumière dans le même système de coordonnées pour que cela ait un sens.

// Création de la matrice d'espace tangent
mat3 tangentSpaceXform = mat3(tangent, bitangent, normal);

// Conversion du vecteur direction de la lumière depuis l'espace modèle vers l'espace tangent
v_lightVec = lightDirection * tangentSpaceXform;

Dans le vexter shadex, nous créons la normale par pixel à partir de la texture normale, puis calculons l'intensité de la lumière en faisant le produit scalaire entre le vecteur de la normale et le vecteur direction de la lumière.

// Normale par pixel
vec3 normal = texture2D(textureBump, v_texCoord).rgb * 2.0 - 1.0;
// L'intensité lumineuse est le produit scalaire du vecteur direction de la lumière par la normale par pixel
float lightIntensity = dot(v_lightVec, normal);

V - Tutoriel 3. Animation de personnages (skinning) sur GPU

Ce tutoriel montre comment réaliser du skinning sur le GPU en utilisant le vertex du pipeline programmable pour soulager le CPU. Nous utilisons Assimp pour importer le modèle animé. Un modèle animé est composé d'un maillage lié à un squelette articulé. Le squelette est composé d'une hiérarchie d'os où chaque os est représenté par une matrice de transformation. Chaque position de vertex est affectée par N os. Les indices des os influencés, ainsi que leur poids respectif, sont stockés dans les attributs du vertex. Dans le vertex shader, nous calculons la position finale du vertex en additionnant les combinaisons des différentes contributions des os. Par exemple : un vertex 'vert' est influencé par l'os 'bone0' avec le poids 'weight0' et l'os 'bone1' avec le poids 'weight1'. La position finale de 'vert' est calculée comme suit :

vert.finalpos = weight0 * (bone0.matrix * vert.pos) + weight1 * (bone1.matrix * vert.pos);

V-A - Code de la procédure

Dans le vertex shader, nous trouvons les matrices de transformation des os influencés pour le vertex actuel avec leur poids respectif comme suit :

// skinningMatrix contient les matrices de transformation de tous les os. bones.x/y/z/w contient les indices des os qui affectent le vertex
vec4 p0 = skinningMatrix[ int(bones.x) ] * position;
vec4 p1 = skinningMatrix[ int(bones.y) ] * position;
vec4 p2 = skinningMatrix[ int(bones.z) ] * position;
vec4 p3 = skinningMatrix[ int(bones.w) ] * position;

La position finale du vertex est la somme des quatre vertices, chacun multiplié par le poids de l'os (la somme des poids d'un os doit être égale à 1) :

// Position interpolée
vec4 interpolatedPosition = p0 * weights.x + p1 * weights.y + p2 * weights.z + p3 * weights.w;

VI - Tutoriel 4. Mouvement de la caméra quaternion

Ce tutoriel implémente une simple caméra quaternion que nous pouvons déplacer dans un monde 3D. La classe Camera n'est pas optimisée, donc si vous prévoyez de l'utiliser dans une application en temps réel, envisagez de faire quelques optimisations. J'ai ajouté un brouillard exponentiel au pixel shader. Pour une application en temps réel, il est recommandé de calculer le facteur du brouillard pour chaque vertex et d'écrire le résultat dans une variable varying qui peut être utilisée par le pixel shader. Le tutoriel emploie le multitexturing pour ajouter quelques détails au terrain.

Dans la fonction UpdateControls(const CControlPad& pad) du fichier Tutorial.cpp, pour faire pivoter la camera, nous obtenons premièrement la représentation actuelle du quaternion et la multiplions par la rotation quaternion adéquate selon le type de contrôle reçu (rotation gauche/droite ou haut/bas). Pour faire bouger la caméra, nous prenons sa position actuelle et le vecteur forward et nous changeons la position de la caméra suivant le vecteur forward.

const float step = 0.6f;// Mouvement et vitesse de la vue

// Obtention de la caméra quaternion
const Quatf& q = m_pCamera->GetQuaternionRepresentation();

if(pad.keyUp) // Touche haut
{
Quatf rot = Quatf::fromAxisRot(vec3i(1, 0, 0), -step);
Quatf qf = rot * q; // Cela va faire tourner la caméra quaternion autour de l'axe x (regarder vers le haut)
m_pCamera->SetQuaternion(qf);// Définit la nouvelle caméra quaternion
}

if(pad.keyDown)
{
Quatf rot = Quatf::fromAxisRot(vec3i(1, 0, 0), step);
Quatf qf  = rot * q  ; // Cela va faire tourner la caméra quaternion autour de l'axe x (regarder vers le bas)
m_pCamera->SetQuaternion(qf);
}

if(pad.keyLeft)
{
Quatf rotX = Quatf::fromAxisRot(vec3i(0, 0, 1), -step);
Quatf qf  = q * rotX;// Rotation de la caméra autour de l'axe des z, axe vertical
m_pCamera->SetQuaternion(qf);
}

if(pad.keyRight)
{
Quatf rotX = Quatf::fromAxisRot(vec3i(0, 0, 1), step);
Quatf qf  = q * rotX;// Rotation de la caméra autour de l'axe des z, axe vertical
m_pCamera->SetQuaternion(qf);
}

if(pad.keyMoveUp)
{
vec3f pos = m_pCamera->GetPosition();// Position de la caméra
vec3f forward = m_pCamera->GetForwardVector();// Vecteur forward de la caméra
pos += forward * step;// Déplacement de la caméra selon le vecteur forward
m_pCamera->SetPosition(pos);// Application de la nouvelle position

}

else if(pad.keyMoveDown)
{
vec3f pos = m_pCamera->GetPosition();// Position de la caméra
vec3f forward = m_pCamera->GetForwardVector();// Vecteur forward de la caméra
pos -= forward * step;// Déplacement de la caméra selon le vecteur forward
m_pCamera->SetPosition(pos);// Application de la nouvelle position
}

VII - Tutoriel 5. SkyBox

Ce tutoriel montre comment charger et afficher une skybox en utilisant une texture cube map dans OpenGL. La texture cube map est composée de six textures 2D, chacune représentant une face du cube map. Pour allouer les six textures, nous appelons la fonction glTexImage2D six fois pour chaque face. Le paramètre target de la fonction renseignera le programme sur la face à allouer parmi les six. Par exemple, pour transmettre les données depuis la mémoire centrale à la texture cube map sur l'axe X positif :

glTexImage2D (GL_TEXTURE_CUBE_MAP_POSITIVE_X,
mip_level,
internal_format,
width,
height,
border,
host_data_format,
host_data_type,
host_data_xp)

Où le premier paramètre indique à OpenGL que nous voulons envoyer les données à l'axe X positif du cube map. Les deux derniers paramètres spécifient le type et l'adresse mémoire des données de l'hôte à transférer au GPU.

Le CCacheResourceManager situé dans le framework charge et retourne la texture cube map prête à l'emploi. Notez que les six faces de la texture cube map doivent être nommées ainsi

(Nom de base + underscore + vecteur direction) :

- skyboxName_xn.jpg (ou png) ;

- skyboxName_xp.jpg ;

- skyboxName_yn.jpg ;

- skyboxName_yp.jpg ;

- skyboxName_zn.jpg ;

- skyboxName_zp.jpg.

Pour charger la skybox, donnez seulement le nom de base au gestionnaire de ressources :

CTextureCubeMap *textureCubeMap = crm.LoadTextureCube("skyboxName.jpg");

La fonction LoadTextureCube charge et transfère chaque image à la face correspondante de la texture cube map. La fonction retourne un objet texture cube map qui peut être utilisé et échantillonné par le pixel shader.

VIII - Tutoriel 6. Réflexion de l'eau

Ce tutoriel montre comment on peut implémenter la réflexion de surface en temps réel. La partie la plus importante est le rendu de la texture de réflexion basé sur la position de la caméra. Nous utilisons un Frame Buffer Object (FBO) pour effectuer un rendu offscreen du reflet de la scène dans une texture. Cette texture est passée à un shader pour simuler la surface de l'eau.

Pour trouver la position de la caméra miroir, nous avons à partir du schéma ci-dessus :

Zc = Zb + 2 * x Eq 1)
where x = H - Zb

En substituant x dans l'équation (Eq 1), la hauteur finale de la position du reflet est égale à :

Zb = 2H - Zc

VIII-A - Code de la procédure

Dans CwaterEntity, nous créons une cible de rendu offscreen avec une texture couleur et un buffer de rendu de profondeur. La fonction PrepareReflectionPass active le frame buffer object pour le rendu offscreen, et nous retrouvons les positions de la caméra miroir et de la cible comme expliqué avant :

// Activation du FBO pour le rendu offscreen
m_pOffscreenRT->Enable();

// Envoi de la hauteur de l'eau au shader et activation du rejet, pour couper tous les objets sous l'eau
shader->SetUniform1i("enableDiscardScene",1) ;
shader->SetUniform1f("waterHeight", m_fWaterHeight);

// Effacement du contexte
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

vec3f camPos = camera->GetPosition(); // Position de la caméra
vec3f camTarget = camPos + camera->GetForwardVector();// Cible

// Si la caméra est au-dessus de l'eau, inverser sa position
if(camPos.z >= m_fWaterHeight)
{

// Position de la caméra miroir
camPos.z = 2.0f * m_fWaterHeight - camPos.z;
camTarget.z = 2.0f * m_fWaterHeight - camTarget.z;

vec3f forwardVector = camTarget - camPos; // Trouver le vecteur forward à partir des nouvelles positions
vec3f sideVector = camera->GetRigthVector(); // Obtention du vecteur right
vec3f reflectionCamUp = sideVector.crossProduct(forwardVector); //Obtention du vecteur up

// Mise en place de la matrice vue miroir
mat4f m;
m = mat4f::createLookAt(camPos, camTarget, reflectionCamUp);
m_mLookAt =  m_mMirror * m;
}

Maintenant que nous avons la texture de réflexion, la prochaine étape est de l'utiliser pour effectuer le rendu de la surface de l'eau. La surface de l'eau est composée d'un simple rectangle auquel nous appliquons la texture de la réflexion. Pour troubler la texture de la réflexion, nous utilisons une texture normale sur différentes directions u,v.

Dans le pixel shader, nous utilisons deux coordonnées de texture avec un facteur de répétition pour créer la texture normale. Les coordonnées de la texture résultante sont combinées avec les coordonnées du fragment pour créer la texture miroir :

// Définition du nombre de tuiles de la surface d'eau (répétition de la texture)
const float tile_factor = 20.0;
const float noise_factor = 0.03;

vec2 texCoordNormal0 = v_texCoord * tile_factor;
texCoordNormal0 += time ;

vec2 texCoordNormal1 = v_texCoord * tile_factor;
texCoordNormal1.s -= time ;
texCoordNormal1.t += time ;

// Coordonnées tex utilisées pour troubler le vecteur texCoordReflection
vec3 normal0 = texture2D(normal, texCoordNormal0).rgb * 2.0 - 1.0;
vec3 normal1 = texture2D(normal, texCoordNormal1).rgb * 2.0 - 1.0;
vec3 normal = normalize(normal0 + normal1);

// Ajustement des coordonnées de la texture selon la taille de l'écran
vec2 texCoordReflection = vec2(gl_FragCoord.x * screenWidth, gl_FragCoord.y * screenHeight);

// Les coordonnées de la texture finale sont troublées par la normale multipliée par un facteur de bruit
vec2 texCoordFinal = texCoordReflection.xy + noise_factor * normal.xy;

gl_FragColor =  texture2D( texture0, texCoordFinal);

Dans ce tutoriel nous ne traitons pas le cas où la caméra se situe en dessous de la surface, situation dans laquelle nous avons besoin d'une passe supplémentaire pour faire la capture de la texture de réfraction. En réalité, sous l'eau nous pouvons toujours voir le monde réfracté, cette texture additionnelle est utilisée pour simuler l'effet de réfraction. La couleur finale est obtenue par le mélange des textures de réflexion et réfraction selon la formule de Fresnel.

Le but du tutoriel était de présenter une simulation simple et rapide de réflexion d'eau sur mobile. Vous pourrez trouver des simulations de surfaces d'eau plus avancées sur le net.

Pour optimiser, nous pouvons réduire la taille de la texture de réflexion en réduisant la taille du frame buffer object de sortie dans le CwaterEntity. La taille actuelle est fixée à 256, essayez différentes valeurs pour entrevoir l'impact sur la qualité/performance.

m_pOffscreenRT->Initialize(kCONTEXT_COLOR | kCONTEXT_DEPTH,  256);

Une autre optimisation consiste à remplacer l'opération de rejet du pixel shader lors du rendu du terrain. Cette opération est utilisée pour ne pas prendre en compte les pixels situés au-dessus de l'eau lors de la capture de la texture de réflexion. Ce qui pourrait être effectué en ajustant les plans de coupe de la matrice de projection.

Voir le SDK de Imagination Technologies (exemple de réflexion d'eau).

IX - Tutoriel 7. Éclairage avec relief

Ce tutoriel suppose que vous connaissiez les rudiments d'éclairage en OpenGL, ce que lumière ambiante, diffuse et spéculaire veulent dire et les mathématiques derrière. Il ne couvre pas la théorie de l'éclairage Phong, ni comment les facteurs ambiant, diffus et spéculaire sont calculés. Il y a un grand nombre d'articles et de documentations sur le Net, suivez ce lien pour plus de détails.

Ce tutoriel montre comment réaliser un éclairage par point sur un shader avec les reliefs. Le bump mapping ajoute des détails à la scène en perturbant les normales de la surface des objets, que l'on utilise ensuite dans le pixel shader pour le calcul de l'éclairage. Il en résulte une surface irrégulière. La capture d'écran ci-dessous montre la même scène rendue avec et sans bumps (dans l'exemple de code fourni, vous pouvez basculer entre les deux scènes en utilisant le bouton switch).

IX-A - Code de la procédure

Du point de vue CPU, les positions de la lumière et de la caméra sont envoyées au shader dans le système de coordonnées monde.

Tout d'abord, nous obtenons l'inverse de la matrice modèle de l'objet comme suit :

//Obtention de la matrice modèle
mat4f &modelBumpMeshMat =  mesh->GetTransfromationMatrix();
mat4f modelInv = modelBumpMeshMat.inverse();

Puis nous multiplions la position de la lumière par l'inverse de la matrice modèle pour obtenir sa position dans l'espace modèle :

//Position de la lumière dans l'espace monde
vec4f lightPosition = modelInv * lightPos;

// Envoi des uniformes au shader
m_pActiveShader->SetUniform3fv("lightPosModel", 1, &lightPosition.x);

En ce qui concerne la position de la caméra, elle se situe toujours à l'origine (0, 0, 0). La matrice de vue transforme chaque objet depuis l'espace monde vers l'espace caméra, donc si nous multiplions la position de la caméra située à (0, 0, 0, 1) par l'inverse de la matrice de vue, nous obtenons la position de la caméra dans l'espace monde. Puis nous multiplions cette position par l'inverse de la matrice modèle pour obtenir la position de la caméra dans l'espace objet :

vec4f cameraPos = inv( modelMatrix ) * (inv(ViewMatrix) * vec4(0, 0, 0, 1)

Pour une expression compacte :

vec4f cameraPos= modelInv * m_pCamera->GetPosition();

m_pCamera->GetPosition() calcule l'inverse de la matrice de vue, que nous multiplions par le vecteur homogène vec4f(0, 0, 0, 1.f). Notez qu'effectuer la multiplication d'une matrice « M » 4x4 par un vecteur « v » homogène 4x1 extrait la partie translation de la matrice (M[12] M[13] M[14]).

Maintenant que nous avons la position de la caméra dans l'espace monde, nous l'envoyons au shader.

m_pActiveShader->SetUniform3fv("camPosModel", 1, &cameraPos.x);

De plus, nous devons envoyer les paramètres de la lumière et les propriétés du matériau définis par les vecteurs ambiant, diffus et spéculaire.

Pour les paramètres de la lumière :

m_pActiveShader->SetUniform4fv("lightColorAmbient", 1, &lightAmbient.x);
m_pActiveShader->SetUniform4fv("lightColorDiffuse", 1, &lightDiffuse.x);
m_pActiveShader->SetUniform4fv("lightColorSpecular", 1, &lightSpecular.x);

Pour les propriétés du matériau, l'envoi se fait dans la fonction Render de l'objet CMeshEntity comme suit :

// Envoi du paramètre ambiant
if(shader->matColorAmbient != -1)
{
glUniform4fv(shader->matColorAmbient, 1, material->ambient);
}

// Envoi du paramètre diffus
if(shader->matColorDiffuse != -1)
{
glUniform4fv(shader->matColorDiffuse, 1, material->diffuse);
}

// Envoi du paramètre spéculaire
if(shader->matColorSpecular != -1)
{
glUniform4fv(shader->matColorSpecular, 1, material->specular);
}

// Envoi du paramètre de brillance
if(shader->matShininess != -1)
{
glUniform1f(shader->matShininess, material->shininess);
}

Passons ensuite au vertex shader. Nous commençons par construire la matrice tangente utilisée pour passer tous les vecteurs dans le même système de coordonnées tangent (espace tangent). Le vecteur direction de la lumière et demi-angle sont convertis vers l'espace tangent.

// Calcul du vecteur bitangent  (ceci peut être fait sur le CPU)
vec3 bitangent = cross(normal, tangent);
// Création de la matrice de l'espace tangent
mat3 tangentSpace = mat3(tangent, bitangent, normal);

// Obtention du vecteur direction de la lumière du vertex actuel
v_lightVector =  lightPosModel - position.xyz  ;
// Conversion du vecteur direction de la lumière vers l'espace tangent
v_lightVector = v_lightVector * tangentSpace;
// Normalisation
v_lightVector = normalize(v_lightVector);

// Obtention de la direction de la caméra du vertex actuel
v_halfVector = camPosModel - position.xyz ;
// Conversion vers l'espace tangent
v_halfVector = v_halfVector * tangentSpace;
// Normalisation
v_halfVector = normalize(v_halfVector);
// Calcul du vecteur demi-angle
v_halfVector = (v_halfVector + v_lightVector) /2.0;
// Normalisation
v_halfVector = normalize(v_halfVector) ;

Dans le pixel shader, nous créons une normale par pixel, perturbée à partir de la normal map. Puis nous calculons la couleur finale du pixel par la somme des contributions ambiante, diffuse et spéculaire de la lumière.

// Obtention de la couleur diffuse par vertex
vec4 color  = texture2D(texture0, texCoord) ;

// Obtention de la normale par pixel
vec3 bump = texture2D(textureBump, texCoord).rgb * 2.0 - 1.0;
// Inversion de la normale pour les faces arrière (si le face culling est activé, cette étape peut être supprimée)
if (!gl_FrontFacing)
bump = - bump;

// Calcul de la contribution de la lumière
// 1- lamber ou facteur diffus
float lamber = max(0.0, dot(normalize(v_lightVector), bump) );

//2- facteur spéculaire
float specular = 0.0;
if (dot(bump, v_lightVector) < 0.0)
specular = 0.0;
else
specular = max(0.0, pow(dot(normalize(v_halfVector), bump), matShininess)) ;

// Obtention des couleurs finales ambiantes, diffuses et spéculaires
vec4 finalAmbientContrib  = lightColorAmbient  * color /** matColorAmbient.xyz*/;
vec4 finalDiffuseContrib  = lightColorDiffuse  * color *  lamber * matColorDiffuse;
vec4 finalSpecularContrib = lightColorSpecular  * specular * matColorSpecular;

// La couleur finale est la somme des composantes ambiante, diffuse et spéculaire
gl_FragColor = finalAmbientContrib + (finalDiffuseContrib + finalSpecularContrib)  ;

Scène éclairée sans relief

Scène éclairée avec relief

X - Tutoriel 8. Réflexion par cube mapping

Aussi connu sous le nom d'environment mapping, ce tutoriel montre comment réaliser la réflexion d'un environnement sur un objet. Cette technique est utilisée pour simuler une surface réfléchissante dans une scène. Nous utilisons une texture cube map comme texture de réflexion. Cette texture est découpée en fragments dans le pixel shader pour simuler cette réflexion sur l'objet.

X-A - Code de la procédure

Pour commencer, nous chargeons la texture cube map qui sera utilisée comme texture de réflexion :

CTextureCubeMap *textureCubeMap = crm.LoadTextureCube("cm.jpg");

Puis nous envoyons la position de la caméra au shader dans l'espace modèle, position qui sera utilisée pour déterminer le vecteur de réflexion sur la surface normale.

/* Pour trouver la position de la caméra dans l'espace modèle :

la caméra (ou œil) est située en v0(0, 0, 0), nous multiplions v0 par l'inverse de la matrice de vue (V) puis par l'inverse de la matrice modèle (M). camPos = inv(M) * inv(V) *                     vec4(0, 0, 0, 1); */

vec4f camera_pos = ( m_pCamera->GetViewMatrix() * model ).inverse() * vec4f(0, 0, 0, 1.f);

// Envoi de la position de la caméra au shader
m_pShaderMesh->SetUniform4fv("camPosModel", 1, &camera_pos.x);

Le vecteur de réflexion calculé dans le vexter shader doit être multiplié par le vecteur de rotation de l'objet pour obtenir une réflexion correcte. Donc nous devons envoyer la partie rotation de la matrice modèle au vexter shader pour reprojeter le vecteur réflexion dans l'espace monde.

// Extraction de la matrice de rotation
mat3f model_mat3 = mat3f(model);

// Envoi au shader
m_pShaderMesh->SetUniformMatrix3x3fv("matModelToWorld", 1, GL_FALSE, &model_mat3[0]);

En ce qui concerne le GPU, nous calculons au sein du vertex shader la direction de la caméra dans l'espace modèle en soustrayant la position de la caméra à la position du vertex.

// Obtention de la direction de la caméra dans l'espace modèle
vec4 eyeDir = normalize(position - camPosModel);

Nous calculons ensuite le vecteur de réflexion :

reflect(eyeDir.xyz, normal);

Nous avons besoin de ramener le vecteur de réflexion vers l'espace monde :

/* Retrouver le vecteur réflexion entre la direction de la caméra et la normale du vertex, multiplier ce vecteur par la matrice de rotation du modèle (conversion vers l'espace monde) */
v_reflectionVector = matModelToWorld * reflect(eyeDir.xyz, normal);

Finalement, au sein du pixel shader, nous échantillonnons la texture cube map en utilisant le vecteur réflexion interpolé renvoyé par le vertex shader.

gl_FragColor = textureCube(textureCubeMap, v_reflectionVector);

La capture d'écran ci-dessous montre la réflexion sur une théière. Pour effectuer le rendu du skybox, décommentez la ligne suivante du fichier Tutorial.cpp :

//#define RENDER_SKYBOX

XI - Remerciements

Cette liste d'articles est une traduction autorisée d'articles parus sur www.virtual-vision.net.

Je tiens à remercier LittleWhite pour sa relecture lors de la traduction de cet article, ainsi que ClaudeLELOUP et mumen pour leurs relectures orthographiques.