Préambule
nouveau : shadercc pour compiler vos shaders directement depuis un éditeur de texte.
Téléchargez et installez les nouvelles
versions (débuguées et complétées) des
librairies objtoy et sdlkit fournies en
Annexes.
Vérifiez le bon fonctionnement du sdlkit_testmain et de glsl_testmain (dans l'archive tests).
Téléchargez les objets :
bigguy.obj et
ground.obj
Documentation :
à ne pas manquer :
Rappel / Partie 1 : Texture et shaders
La création et l'utilisation de textures en openGL sont décrites dans le
chapitre 10 du Programming Guide.
Résumé : création d'une texture RGB "standard"
glGenTextures(1, &texName); // crée un objet texture
glBindTexture(GL_TEXTURE_2D, texName); // active une texture 2D pour l'objet
// fixe les paramètres par défaut : une seule application de la texture, pas de répétition
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
// fixe les paramètres par défaut : échantillonnage filtré (interpolation bi-linéaire)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
// remplit la texture et fixe le format des données (couleurs, combien, etc.)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texture_largeur, texture_hauteur, 0,
// la ligne du dessus décrit le format interne de la texture,
// la ligne du dessous décrit le format des données fournies par l'application,
GL_RGB, GL_UNSIGNED_BYTE, texture_data);
// si elles ne correspondent pas, openGL fera la conversion.
// texture_largeur / hauteur précise les dimensions de la texture, !! attention : ce sont des puissances de 2 !!
// texture_data est un tableau de 3 unsigned char par pixel (largeur * hauteur)
Résumé : utilisation d'une texture pour afficher un quad
// active l'unite 0 de gestion de texture, nécessaire pour l'utilisation dans un shader
glActiveTexture(GL_TEXTURE0);
// active l'utilisation des textures 2D
glEnable(GL_TEXTURE_2D);
// indique comment "mélanger" la couleur de la texture et la couleur de l'objet
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
// indique quelle texture 2D utiliser
glBindTexture(GL_TEXTURE_2D, texName);
// dessiner un quad
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0);
glVertex3f(-2.0, -1.0, 0.0); // chaque sommet est associé à une coordonnée dans la texture
glTexCoord2f(0.0, 1.0);
glVertex3f(-2.0, 1.0, 0.0);
glTexCoord2f(1.0, 1.0);
glVertex3f(0.0, 1.0, 0.0);
glTexCoord2f(1.0, 0.0);
glVertex3f(0.0, -1.0, 0.0);
glEnd();
La seule 'subtilité' consiste à
indiquer quelle unité de texture traite la texture. Le fragment
shader utilise directement le numéro de l'unite pour
accéder à la texture.
Résumé : lire une texture dans un fragment shader
// fragment shader
uniform sampler2D texture;
void main(void)
{
// lire la texture aux coordonnées s, t
gl_FragColor.rgb= texture2D(texture, vec2(s, t));
gl_FragColord.a= 1.f;
}
Résumé : fournir l'unité de texture au shader
glsl_init()
GLuint program= glsl_init_program( ... )
GLuint texture_location= glGetUniformLocation(program, "texture");
glUniformi(texture_location, 0);
glActiveTexture(GL_TEXTURE0);
glEnable(GL_TEXTURE_2D);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
glBindTexture(GL_TEXTURE_2D, texName);
// dessiner un quad, shader activé
glUseProgram(program);
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0);
glVertex3f(-2.0, -1.0, 0.0);
glTexCoord2f(0.0, 1.0);
glVertex3f(-2.0, 1.0, 0.0);
glTexCoord2f(1.0, 1.0);
glVertex3f(0.0, 1.0, 0.0);
glTexCoord2f(1.0, 0.0);
glVertex3f(0.0, -1.0, 0.0);
glEnd();
Modifiez vos shaders du tp précédent et ajoutez une texture pour modifier l'aspect de bigguy.
Partie 2 : Shadow map
Comment projetter une ombre ? Le pipeline graphique
traite les vertex et les fragments indépendemment les uns des
autres, il n'est donc pas possible de savoir directement si un objet se
trouve entre la source de lumière et l'objet en cours
d'affichage.
La solution standard consiste à afficher deux
fois la scène : une fois vue de la source de lumière afin
de stocker les fragments éclairés (et leur distance
à la source), et une autre fois depuis la caméra. Lors de
l'affichage depuis la caméra, il suffit de recalculer la
position de chaque sommet dans le repère (projectif) de la
source de lumière et de comparer cette distance à celle
stockée dans la première étape. Si la distance est
inférieure ou égale à celle stockée : le
sommet est visible de la source, il est donc éclairé,
dans l'autre cas, le sommet n'est pas le plus proche de la source et il
est donc à l'ombre.
Voici un schema pour illustrer le principe :
Une shadow map est donc le zbuffer de la
scène vue par la source de lumière. Lors de la
deuxième étape, le vertex shader calcule deux
transformations par sommet traité : une classique
utilisée pour afficher les objets vus par la caméra et
l'autre pour retrouver la position du sommet dans le repère de
la source de lumière. Les deux résultats sont transmis au
fragment shader qui termine la projection dans le repère de la
source de lumière et compare la profondeur stockée dans
la shadow map à celle du fragment. La comparaison se fait
automatiquement en accédant à une texture de type
shadow2D.
L'accès à une texture se fait en utilisant des
coordonnées de textures ( !! ), donc le plus simple consiste
à transmettre les coordonnées du sommet (dans le
repère projectif de la source de lumière) en utilisant
les varying
gl_TexCoord[0].stpq dans le vertex shader.
En résumé, voici les différentes étapes :
1. placer la source de lumière dans le monde,
2. afficher la scène vue par la source de lumière,
3. stocker le zbuffer dans une texture (la shadow map),
4. activer la texture shadow_map,
5. afficher la scène vue par la camera,
6. le vertex shader calcule la
transformation classique des sommets dans le repère projectif de
la caméra,
7. le vertex shader calcule
également la transformation des sommets dans le repère
projectif de la camèra utilisée dans l'étape 2 et
stocke le sommet transformé dans gl_TexCoord[0],
8. le fragment shader
récupère les coordonnées, termine la projection et
accède à la texture pour comparer les profondeurs
du point stocké dans la shadow map et du fragment,
9. le fragment shader calcule la
couleur du fragment en fonction du résultat : fragment
éclairé ou à l'ombre.
Remarques : une texture contenant des profondeurs n'est pas manipulée de la même manière qu'une texture de couleur :
1. elle se déclare
uniform sampler2DShadow texture;
2. elle est utilisé avec
shadow2D(texture, vec3(s, t, p)) ou
shadow2DProj(texture, vec4(s, t, p, q))
3. il faut activer le type de comparaison (dans l'application, après le
glBind()):
résultat vrai si p < au texel (ou p / q pour shadow2DProj)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
ou vrai si p > au texel (ou p / q pour shadow2DProj)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_GEQUAL);
4. les coordonnées de texture
sont des réels entre 0 et 1, le résultat de la projection
du sommet est entre -1 et 1, comment corriger ce dernier changement de
repère ??
5. pour dessiner directement dans la texture à l'étape 2, parcourez l'annexe
'utilisation des framebuffer objects'.
Partie Subsidiaire : Ambient occlusion,
ou "comment pré-calculer ça ?"
Il suffit de calculer une shadow map par source et
de comptabiliser les texels qui sont éclairés. Au lieu de
dessiner bigguy en 3D avec des coordonnées de texture 2D comme
paramètres, il suffit de faire le contraire. C'est à dire,
dessiner la surface en 2D et d'utiliser les coordonnées 3D des
sommets comme attributs des sommets 2D.
Partie Subsidiaire : Ombres douces et pénombres.
La méthode de base décrite dans la
première partie du sujet ne 'fonctionne' correctement qu'avec
des sources ponctuelles, ce qui produit des ombres 'dures' : deux
pixels voisins dans l'image peuvent être l'un à l'ombre alors que l'autre est
en pleine lumière.
Une méthode simple pour des ombres douces correctes est décrit dans le sdk nvidia, exemple
'Soft Shadows'
Partie Subsidaire : Anti-aliasing,
ou "mes ombres, elles sont toutes carrées ..."
Une méthode filtrage simple et élégante est décrite dans
'Variance Shadow Maps'.
Partie Subsidiaire : construction de la caméra associée à la source de lumière
ou "mes ombres, elles sont toutes carrées, mais pas tout le temps !!"
C'est encore lié à un problème
d'échantillonnage entre le rendu camera et le rendu depuis la
source de lumière. Une méthode interressante pour limiter
le problème :
'Parallel Split Shadow Maps'.
Autre technique simple pour améliorer la
qualité des shadow maps et limiter les problèmes de
précisions du zbuffer :
'Linear Zbuffer'.
Annexes : utilisation des frame buffer objects
Les frame buffer objects permettent de créer des ensembles de
textures et de les faire remplir lors du rendu de la scène
à la place du buffer d'affichage de la fenêtre.
render_target.c (fourni avec la nouvelle version de sdlkit) crée
un buffer contenant deux textures : une couleur et une de profondeur.
exemples d'utilisation (pas d'ordre particulier) :
#include "render_target.h"
// avant toute utilisation
render_target_init();
// à la fin du programme
render_target_quit();
// creation d'un frame buffer object de dimensions largeur x hauteur
RENDER_TARGET *buffer= render_target_create(largeur, hauteur);
// les rendu suivants rempliront 'buffer'
render_target_bind(buffer);
// retour à la normale : remplissage du buffer d'affichage de la fenêtre
render_target_disable();
// active la texture couleur du buffer pour l'unite de texture 0
glActiveTexture(GL_TEXTURE0);
render_target_bind_texture(buffer);
// active la texture de profondeur du buffer pour l'unite de texture 1
glActiveTexture(GL_TEXTURE1); // on peut bien sur utiliser n'importe quelle unite de texture
render_target_bind_depth_texture(buffer);
// detruit le buffer
render_target_delete(buffer);
// recuperer les dimensions du buffer
int viewport[4];
render_target_get_viewport(buffer, viewport);
// c'est la même convention qu'openGL
// viewport[0] = xmin, viewport[1]= ymin
// viewport[2]= largeur, viewport[3]= hauteur
Annexe : utilitaires (nouvelles versions)
Installation de sdlkit + render_target + glsl2 + shadercc
Téléchargez
sdlkit + render_target + glsl2 + shadercc
créez un répertoire et dézippez l'archive dedans
cd <répertoire>
tar -zxvf sdlkit_latest.tar.gz
make
make install
si vous souhaitez installer les exemples :
créez un répertoire tests dans le répertoire sdlkit et dézippez l'archive dedans
cd tests
tar -zxvf sdlkit_tests_latest.tar.gz
make
shadercc
Pour compiler vos shaders et vérifier qu'il
n'y a pas d'erreurs de syntaxe et de casts avant
d'éxécuter votre programme complet :
~/local/bin/shadercc -c shader.vsl
Pensez à ajouter
#version 110 au début du source des shaders pour détecter le maximum d'erreurs / warnings.
shadercc
affiche également la ligne du source correspondant à
l'erreur détectée par le driver et en utilisant le format
d'erreur de
gcc, résultat, il s'intègre facilement à un éditeur ...
Démonstration :
Intégration de shadercc dans SciTE
Ouvrez les propriétés utilisateur : menu '
Options', option '
Open User Options File', copiez - collez les lignes suivantes :
# extensions de fichiers reconnues
file.patterns.cpp=*.c;*.cpp;*.cs;*.h;*.hpp;*.fsl;*.vsl
# compilation des shaders glsl
shadercc=shadercc -c $(FileNameExt)
command.compile.*.vsl=$(shadercc)
command.compile.*.fsl=$(shadercc)
Enregistrez le fichier, fermez SciTE, rouvrez-le et
éditez un shader. Dans le menu Tools, l'option Compile est
maintenant disponible (Ctrl-F7). Appuyez ensuite sur F4 pour visualiser
les différentes erreurs (éventuelles, bien sur !!).
Installation de objtoy
Téléchargez
objtoy
créer un répertoire et dézippez l'archive dedans
cd <répertoire>