Un exemple d'utilisation des register combiners: shadow mapping
Ce tutorial sera compose de 4 parties:
-
Rappel de l'algorithme
Application en OpenGL
Configuration des combiners
Conclusion
Ce tutorial est une implémentation d'un algorithme introduit par Wolfgang Heidrich dans sa thèse.
Rappel de l'algorithme du shadow mapping
L'idée de cet
algorithme est de considérer la lampe comme une
caméra. La méthode consiste pour chaque point
rasterisé dans le repère de la caméra à le
regarder du point de vue de la lampe ce point, et vérifier s'il
n'est pas caché par un autre point de la scène. Ce
mécanisme est iliustré sur la figure ci-dessous. En
rouge, le point vu de la caméra en cours de rasterisation. En
bleu ce même point exprimé dans le repère de la
lampe. En jaune, le point dans le repère de la lampe qui cache le point courant pour la lampe.
Application en OpenGL
Le problème de cet algorithme
est qu' il requiert souvent un materiel très spécifique
pour être appliqué en une seule passe (ie une Reality
Engine ou une GeForce 3). En effet, comment
effectuer le test d'occlusion? En fait, il faut avoir tout d'abord
effectué un rendu du point de vue de la lampe, et
recuperé le ZBuffer. On obtient donc la carte d'occlusions du point
de vue de la lampe.
OpenGL equivalent:
Rendu du point de vue de la lampe
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glMultMatrixf(light->getProjectionMatrix());
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glMultMatrixf(light->getModelviewMatrix());
RenderScene();
glFlush();
Recuperation de la carte de profondeur de la lampe (Depth Map)
glPixelStorei(GL_UNPACK_ALIGNMENT,1);
glActiveTextureARB(GL_TEXTURE0_ARB);
gBindTexture(light->texid);
glReadPixels(0,0,window->width,window->height,GL_DEPTH_COMPONENT,GL_UNSIGNED_BYTE,light->image);/*attention:
width et heght doivent être des puissances de 2*/
TraiterImage(light->image); /*mise a 0 de la valeur des bords de la
texture, et convolution eventuelle */
glTexImage2D(GL_TEXTURE_2D,
0,
GL_ALPHA,
window->width,
window->height,
0,
GL_ALPHA,
GL_UNSIGNED_BYTE,
light->image);
Ensuite, la technique va consister à
projeter cette carte sur la scène. Pour chaque point de la
scène, on a le z de la flèche jaune du shéma dans
le reprère de la lampe. Il faut donc avoir de comparer cette
information avec le z de la flèche bleue. Comment obtenir ce
placage et le z du fragment dans le repère de la lampe? La
solution est d'utilliser deux textures. Une texture qui va mapper le
ZBuffer de la lampe, et une autre texture pour obtenir les
coordonnées du fragment dans le repere de la lampe. Pour cela,
il faut avoir compris comment marchait la génération de
coordonnées de texture automatique. En fait, OpenGL permet de
générer les coordonnées de texture (s,t,r,q) en
fonction des (x,y,z,w) du fragment. Nous avons uniquement
utilisé le mode plan. A quoi correspond le mode plan? En fait,
tout simplement appliquer U(M
-1)(x,y,z,w)T.
(Notation: M = ModelView Matrix, U = une certaine matrice) Nous définissons alors la
matrice U pour obtenir ce que nous recherchons. Le (x,y,z,w) correspond aux coordonnées passées à OpenGL sur lesquelles a été appliqué M.
Si on veut obtenir le Z dans le repére de la lampe, il faudrait appliquer sur (x,y,z,w) sa matrice ModelView, puis sa projection. Comme OpenGL applique la Modelview puis son inverse, il suffit d'utiliser la Modelview de la lampe puis la projection de la lampe. Nous avons deux possibilitées pour transformer le point dans le repere de la lampe:
- soit effectuer soi-même le produit des differentes matrices
(projection_lampe par modelview_lampe) et de la passer dans U (propre)
- soit chargeant les transformations dans la texture matrix, et en donnant comme matrice U l'identite (propre mais limite a certains hardwares specifiques (heureusement que la geforce est une carte bien pensee))
- soit en sauvegardant une des matrices (modelview par exemple),
charger l'identite puis les transformations (projection + modelview de
la lampe), recuperer la matrice calculee par OpenGL, la passer comme
matrice U, puis restaurer la matrice initialement sauvegardee. (Cette
méthode est sale car l'execution du processeur et celle de la carte graphique ne sont pas aussi rapides, donc l'envoi puis la recuperation de la matrice fait transiter des données sur le bus inutilement)
Enfin, pour avoir une bonne texture de z, il suffit d'utiliser une texture 1D representant l'identite (f(x)=x), puis d'appliquer comme transformation mappant z selon s.
N'oublions pas que nous sommes dans le cas de repere d'image 2D ou 1D et que ces donnees sont mappees a partir dans l'espace [-1,1] au lieu de [0,1]. Il faut donc effectuer une translation et une mise a l'echelle apres avoir effectue les diverses operations.
Recapitulons:
Pour la texture de Z dans le repere de la lumiere, la texture matrix est construite comme suit:
GLfloat SPlane={1.,0.,0.,0.};
GLfloat TPlane={0.,1.,0.,0.};
GLfloat RPlane={0.,0.,1.,0.};
GLfloat QPlane={0.,0.,0.,1.};
GLdouble RSmatrix[16] = {
0, 0, 0, 0,
0, 0, 0, 0,
0.5, 128, 0, 0,
0.5, 128, 0, 1.0
};
glActiveTextureARB(GL_TEXTURE1_ARB);//la texture de Z sera la texture 2
glEnable(GL_TEXTURE_1D);
glBindTexture(GL_TEXTURE_1D,texid[1]);
glTexGeni(GL_S,GL_TEXTURE_GEN_MODE,GL_EYE_LINEAR);//configuration du mode de generation de coordonnees de texture
glTexGeni(GL_T,GL_TEXTURE_GEN_MODE,GL_EYE_LINEAR);
glTexGeni(GL_R,GL_TEXTURE_GEN_MODE,GL_EYE_LINEAR);
glTexGeni(GL_Q,GL_TEXTURE_GEN_MODE,GL_EYE_LINEAR);
glTexGenfv(GL_S,GL_EYE_PLANE,SPlane);
glTexGenfv(GL_T,GL_EYE_PLANE,TPlane);
glTexGenfv(GL_R,GL_EYE_PLANE,RPlane);
glTexGenfv(GL_Q,GL_EYE_PLANE,QPlane);
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
glMultMatrixd(RSmatrix);//mappe z selon s
glMultMatrixf(mat);//matrice de projection de la lampe
glMultMatrixf(MVmatrix);//matrice de modelview de la lampe
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_GEN_R);
glEnable(GL_TEXTURE_GEN_Q);
glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_REPLACE);//important car le mode de melange par defaut est modulate
Pour projetter la depth map sur la scene, il faut:
- inverser la matrice de projection de la camera
- inverser la matrice de modelview de la camera
- appliquer la matrice de modelview de la lampe
- appliquer la matrice de projection de la lampe
D'apres ce que nous avons vu precedemment, pour le mode plane linear, la matrice de projection n'est pas encore applique sur les coordonnees transformees. Il suffit donc d'appliquer l'inverse de la matrice de modelview qui est automatiquement appliquee par le mode plane linear. En fait, il nous faut donc uniquement appliquer la matrice de modelview de la camera, puis la projection de la camera. Ceci est obtenu en faisant comme suit:
glActiveTextureARB(GL_TEXTURE0_ARB);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D,texid[0]);
glTexGeni(GL_S,GL_TEXTURE_GEN_MODE,GL_EYE_LINEAR);
glTexGeni(GL_T,GL_TEXTURE_GEN_MODE,GL_EYE_LINEAR);
glTexGeni(GL_R,GL_TEXTURE_GEN_MODE,GL_EYE_LINEAR);
glTexGeni(GL_Q,GL_TEXTURE_GEN_MODE,GL_EYE_LINEAR);
glTexGenfv(GL_S,GL_EYE_PLANE,SPlane);
glTexGenfv(GL_T,GL_EYE_PLANE,TPlane);
glTexGenfv(GL_R,GL_EYE_PLANE,RPlane);
glTexGenfv(GL_Q,GL_EYE_PLANE,QPlane);
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
//operations pour mapper l'image dans l'espace [-1,1]
glTranslatef(0.5,0.5,0.);
glScalef(0.5,0.5,1.);
glMultMatrixf(mat);//projection de la lampe
glMultMatrixf(MVmatrix);//modelview de la lampe
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_GEN_R);
glEnable(GL_TEXTURE_GEN_Q);
glEnable(GL_TEXTURE_2D);
glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_REPLACE);
Configuration du shader:
En fait, l'algorithme que nous souhaitons employer est:
pour chaque fragment
{
si (Zfragment=Zdepthmap)
{
Cfragment=Ceclairee
} sinon {
/* zfragment>Zdepthmap */
Cfragment=Combree
}
}
Dans cet exemple, pour que le shader soit simple a comprendre, j'ai
considere que la seule forme d'éclairage ombree, était
celle de la lumiere ambiante.
La macro-formule que nous allons employer est la suivante:
SPARE0.alpha=(1*Texture0(depthmap))+(1*(1-Texture1(z)))-0.5
ce qui revient à
SPARE0.alpha=depthmap-z +0.5
Etage 1 |
Sources
|
Registre |
Mapping |
Variable |
Signification |
A.alpha |
inversion non signee |
0 |
1 |
B.alpha |
identite non signee |
Texture1.alpha |
Z(fragment) |
C.alpha |
inversion non signee |
0 |
1 |
D.alpha |
inversion non signee (1-x) |
Texture0.alpha |
1-Z(depth map) |
|
Destination
|
Combinaison
|
Operation
|
Mapping
|
Destination
|
Signification
|
AB.alpha
|
composante a composante
|
|
discard
|
|
CD.alpha
|
composante a composante
|
|
discard
|
AB.alpha+CD.alpha
|
addition
|
bias de -0.5 |
SPARE0.alpha
|
Z(fragment)-Z(depthMap)+0.5 |
|
Etage 2 |
Sources
|
Registre |
Mapping |
Variable |
Signification |
A.RGB |
identite non signee |
Primary Color |
Couleur du
fragment eclaire |
B.alpha |
inverse non signee |
0 |
1 |
C.alpha |
identite non signee |
Constant Color 0 |
Couleur de la lampe |
D.alpha |
identite non signale |
Constant color 1 |
couleur ambiante du fragment |
|
Destination
|
Combinaison
|
Operation
|
Mapping
|
Destination
|
Signification
|
AB.alpha
|
composante a composante
|
|
discard
|
|
CD.alpha
|
composante a composante
|
|
discard
|
AB.alpha+CD.alpha
|
addition
|
pas de bias |
SPARE1.RGB
|
if zfragment < zdepth map
alors SP1=couleur ombree
sinon SP1=couleur eclairee
|
|
Enfin la couleur finale (final combiner):
Af=SPARE1.RGB
Bf=1
Cf=0
Df=0
Gf=SPARE0.alpha
Donc
il suffit donc d'utiliser deux etages de combiners et le code obtenu
(grâce â mon générateur de code de mon site pour les registres generaux et pour l'etage final)
Les Couleurs du materiau ambiant et de la lampe, sont passees comme les couleurs constantes ConstantColor0NV et ConstantColor1NV.
Le code correspondant
glEnable(GL_REGISTER_COMBINERS_NV);
//formule a calculer:
//texture 1 = s (z du fragment vu de la lampe)
//texture 0 = z de l'element de surface vu par la lampe
// texture (s-z)*primary
//A=z B=1 C=-s D=1
glCombinerParameteriNV(GL_NUM_GENERAL_COMBINERS_NV,2);
glCombinerInputNV(GL_COMBINER0_NV,GL_ALPHA,GL_VARIABLE_A_NV,GL_ZERO,GL_UNSIGNED_INVERT_NV,GL_ALPHA);
glCombinerInputNV(GL_COMBINER0_NV,GL_ALPHA,GL_VARIABLE_B_NV,GL_TEXTURE1_ARB,GL_UNSIGNED_IDENTITY_NV,GL_ALPHA);
glCombinerInputNV(GL_COMBINER0_NV,GL_ALPHA,GL_VARIABLE_C_NV,GL_ZERO,GL_UNSIGNED_INVERT_NV,GL_ALPHA);
glCombinerInputNV(GL_COMBINER0_NV,GL_ALPHA,GL_VARIABLE_D_NV,GL_TEXTURE0_ARB,GL_UNSIGNED_INVERT_NV,GL_ALPHA);
glCombinerOutputNV(GL_COMBINER0_NV,GL_ALPHA,GL_DISCARD_NV,GL_DISCARD_NV,GL_SPARE0_NV,GL_NONE,GL_BIAS_BY_NEGATIVE_ONE_HALF_NV,GL_FALSE,GL_FALSE,GL_FALSE);
glCombinerInputNV(GL_COMBINER1_NV,GL_RGB,GL_VARIABLE_A_NV,GL_PRIMARY_COLOR_NV,GL_UNSIGNED_IDENTITY_NV,GL_RGB);
glCombinerInputNV(GL_COMBINER1_NV,GL_RGB,GL_VARIABLE_B_NV,GL_ZERO,GL_UNSIGNED_INVERT_NV,GL_RGB);
glCombinerInputNV(GL_COMBINER1_NV,GL_RGB,GL_VARIABLE_C_NV,GL_CONSTANT_COLOR0_NV,GL_UNSIGNED_IDENTITY_NV,GL_RGB);
glCombinerInputNV(GL_COMBINER1_NV,GL_RGB,GL_VARIABLE_D_NV,GL_CONSTANT_COLOR1_NV,GL_UNSIGNED_IDENTITY_NV,GL_RGB);
glCombinerOutputNV(GL_COMBINER1_NV,GL_RGB,GL_DISCARD_NV,GL_DISCARD_NV,GL_SPARE0_NV,GL_NONE,GL_NONE,GL_FALSE,GL_FALSE,GL_TRUE);
glCombinerInputNV(GL_COMBINER1_NV,GL_ALPHA,GL_VARIABLE_A_NV,GL_ZERO,GL_UNSIGNED_INVERT_NV,GL_ALPHA);
glCombinerInputNV(GL_COMBINER1_NV,GL_ALPHA,GL_VARIABLE_B_NV,GL_SPARE0_NV,GL_UNSIGNED_IDENTITY_NV,GL_ALPHA);
glCombinerInputNV(GL_COMBINER1_NV,GL_ALPHA,GL_VARIABLE_C_NV,GL_ZERO,GL_UNSIGNED_INVERT_NV,GL_ALPHA);
glCombinerInputNV(GL_COMBINER1_NV,GL_ALPHA,GL_VARIABLE_D_NV,GL_TEXTURE1_ARB,GL_UNSIGNED_INVERT_NV,GL_ALPHA);
glCombinerOutputNV(GL_COMBINER1_NV,GL_ALPHA,GL_SPARE0_NV,GL_DISCARD_NV,GL_DISCARD_NV,GL_NONE,GL_NONE,GL_FALSE,GL_FALSE,GL_FALSE);
glFinalCombinerInputNV(GL_VARIABLE_A_NV,GL_SPARE0_NV,GL_UNSIGNED_IDENTITY_NV,GL_RGB);
glFinalCombinerInputNV(GL_VARIABLE_B_NV,GL_ZERO,GL_UNSIGNED_INVERT_NV,GL_RGB);
glFinalCombinerInputNV(GL_VARIABLE_C_NV,GL_ZERO,GL_UNSIGNED_IDENTITY_NV,GL_RGB);
glFinalCombinerInputNV(GL_VARIABLE_D_NV,GL_ZERO,GL_UNSIGNED_IDENTITY_NV,GL_RGB);
glFinalCombinerInputNV(GL_VARIABLE_G_NV,GL_SPARE0_NV,GL_UNSIGNED_INVERT_NV,GL_ALPHA);
Bilan positif:
Version sans eclairage (sans utilisation du shader)
Version avec eclairage (avec le shader)
Affichage du cone d'eclairage
Bilan negatif
Imprecision du test, les donnees sont echantillonnees, interpolees, donc le long de la sphere, on a des zigzags
Conclusion: le shadow mapping en une seule passe presente des avantages, notamment:
- le calcul est fait en une seule passe (pas de stencil ou de calcul de polygones comme dans le cas de l'algorithme de shadow volume)
- les resultats sont relativement convaincants
Le shadow mapping presente des desavantages lies intrinsequement a son implementation:
- le resultat du z buffer ne peut etre utilise qu'avec une artihmetique trop limitee (valeurs echantillonnees sur 8 bits ou 16 bits)
- la tolerance du test (egalite) ne peut etre variable
- necessite un hardware specifique (multitexture, et glTexEnvi etendus (limites aux geforce et ati) au moins, Register combiners au mieux)
Pour toute suggestion, contactez moi.
Le code de cet exemple sera bientot accessible le temps que j'enleve mes commentaires et mes traces.... Voila pour l'instant le binaire qui tourne sous linux. Le portage sous win32 et tout le reste viendra plus tard....
Auteur:Franck Senegas