Un exemple d'utilisation des register combiners: shadow mapping

Ce tutorial sera compose de 4 parties:

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: 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: 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 shadow mapping presente des desavantages lies intrinsequement a son implementation: