Ombres, reflets et stencil buffer
Apres des belles surfaces en relief qui se dessinent toutes seules, il
est temps de revenir a des effets moins miraculeux. Non, il n'y a pas de fonction
glReflect ou glShadow. Mais ce n'est pas pour autant bien complique, sinon je n'aurais
meme pas aborde le sujet.
Le but est de dessiner ca (pour une fois je mets le resultat final au
debut) :
Commencons par les reflets, parce que c'est le plus facile. Il suffit
de voir les choses d'une certaine facon : le reflet d'un objet sur un plan, ce n'est rien
d'autre que le meme objet dessine a l'envers, avec un peu de transparence (il me semble
que l'on etudie ca en 1ere). Les inconvenients de cette astuce sont que l'on dessine deux
fois le meme objet (difficile d'y echapper), et que cela ne marche qu'avec un plan
(difficile, mais possible, d'y echapper).
Les etapes d'un reflet sont :
- glPushMatrix
- utilisation de glScale pour inverser le
dessin (par exemple glScalef(1.0, -1.0, 1.0) permet d'obtenir simplement l'inverse par
rapport au plan Y = 0 !), attention au culling
- rappel du positionnement des sources
lumineuses (pour qu'elles aussi soient inversees)
- dessin de l'objet
- glPopMatrix
- positions originales des sources lumineuses
- dessin (rectangle transparent par exemple) du
l'objet miroir
glPushMatrix();
gluLookAt(CUBE_R*NB_CUBE_X/2, 22, 30,
CUBE_R*NB_CUBE_X/2, 21.9, 29,
0, 1, 0);
float zz=float(2*CUBE_R * (3 +
2*sin(tt/3000)));
glPushMatrix();
glScalef(1.0, -1.0, 1.0);
for (int x = 0 ; x < NB_CUBE_X ; x++)
for (int y = 0 ; y <
NB_CUBE_Y ; y++)
for (int z = 0 ; z < NB_CUBE_Z ; z++)
{
Cube[x][y][z]->Update(t);
Cube[x][y][z]->Draw(0,-20,zz+20);
}
glPopMatrix();
glDisable(GL_LIGHTING);
glDisable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA,
GL_ONE_MINUS_SRC_ALPHA);
drawFloor();
glDisable(GL_BLEND);
glEnable(GL_TEXTURE_2D);
glEnable(GL_LIGHTING);;
for (x = 0 ; x < NB_CUBE_X ; x++)
for (int y = 0 ; y <
NB_CUBE_Y ; y++)
for (int z = 0 ; z < NB_CUBE_Z ; z++)
Cube[x][y][z]->Draw(0,-20,zz+20);
glPopMatrix();
avec :
void drawFloor()
{
glColor4f(0.5f, 0.5f, 0.5f, 0.75f);
glBegin(GL_QUADS);
glVertex3f(0.0, 0.0,
-20.0);
glVertex3f(12.0, 0.0,
-20.0);
glVertex3f(12.0, 0.0,
-70.0);
glVertex3f(0.0, 0.0,
-70.0);
glEnd();
}
Reste un probleme : le "reflet" peut deborder de la
surface reflichissante. C'est la qu'intervient le stencil-buffer. Tout comme le
depth-buffer permet de dessiner ou de ne pas dessiner suivant les conditions voulues, le
stencil permet de limiter l'affichage. Le principe est le suivant : on efface le stencil
buffer (avec glClear), puis on met des "1" la ou la surface reflechissante va
etre dessinee (en dessinant la surface avec tout desactive sauf le stencil-buffer), et on
dessine l'objet reflechi que si le stencil est a "1". La suite ne change pas. La
plupart des cartes 3D (toutes sauf 3Dfx il me semble) gere le stencil-buffer en hardware.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glPushMatrix();
gluLookAt(CUBE_R*NB_CUBE_X/2, 37, 30,
CUBE_R*NB_CUBE_X/2, 36.7, 29,
0, 1, 0);
float zz=float(2*CUBE_R * (3 +
2*sin(tt/3000)));
glDisable(GL_DEPTH_TEST);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE,
GL_FALSE);
glEnable(GL_STENCIL_TEST);
glStencilOp(GL_REPLACE, GL_REPLACE,
GL_REPLACE);
glStencilFunc(GL_ALWAYS, 1, 0xffffffff);
drawFloor();
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE,
GL_TRUE);
glEnable(GL_DEPTH_TEST);
glStencilFunc(GL_EQUAL, 1, 0xffffffff); /* draw
if ==1 */
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
glPushMatrix();
glScalef(1.0, -1.0, 1.0);
for (int x = 0 ; x < NB_CUBE_X ; x++)
for (int y = 0 ; y <
NB_CUBE_Y ; y++)
for (int z = 0 ; z < NB_CUBE_Z ; z++)
{
Cube[x][y][z]->Update(t);
Cube[x][y][z]->Draw(0,-20,zz+20);
}
glPopMatrix();
glDisable(GL_STENCIL_TEST);
glDisable(GL_LIGHTING);
glDisable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA,
GL_ONE_MINUS_SRC_ALPHA);
drawFloor();
glDisable(GL_BLEND);
glEnable(GL_TEXTURE_2D);
glEnable(GL_LIGHTING);;
for (x = 0 ; x < NB_CUBE_X ; x++)
for (int y = 0 ; y <
NB_CUBE_Y ; y++)
for (int z = 0 ; z < NB_CUBE_Z ; z++)
Cube[x][y][z]->Draw(0,-20,zz+20);
glPopMatrix();
La seule chose qui reste a faire, c'est de trouver le bon glScale
suivant le plan qui reflete.
Passons aux ombres. Cette fois il n'y a pas d'astuce. La plus grosse
difficulte, c'est qu'il n'y a pas de moyen simple d'obtenir une matrice de projection
pratique dans OpenGL. Alors vive le copier-coller (d'un programme de "Mark J.
Kilgard") :
/* Create a matrix that will project the desired shadow. */
void shadowMatrix(GLfloat shadowMat[4][4], GLfloat groundplane[4], GLfloat lightpos[4])
{
GLfloat dot;
dot = groundplane[X] * lightpos[X] +
groundplane[Y] * lightpos[Y] +
groundplane[Z] * lightpos[Z] +
groundplane[W] * lightpos[W];
shadowMat[0][0] = dot - lightpos[X] * groundplane[X];
shadowMat[1][0] = 0.f - lightpos[X] * groundplane[Y];
shadowMat[2][0] = 0.f - lightpos[X] * groundplane[Z];
shadowMat[3][0] = 0.f - lightpos[X] * groundplane[W];
shadowMat[X][1] = 0.f - lightpos[Y] * groundplane[X];
shadowMat[1][1] = dot - lightpos[Y] * groundplane[Y];
shadowMat[2][1] = 0.f - lightpos[Y] * groundplane[Z];
shadowMat[3][1] = 0.f - lightpos[Y] * groundplane[W];
shadowMat[X][2] = 0.f - lightpos[Z] * groundplane[X];
shadowMat[1][2] = 0.f - lightpos[Z] * groundplane[Y];
shadowMat[2][2] = dot - lightpos[Z] * groundplane[Z];
shadowMat[3][2] = 0.f - lightpos[Z] * groundplane[W];
shadowMat[X][3] = 0.f - lightpos[W] * groundplane[X];
shadowMat[1][3] = 0.f - lightpos[W] * groundplane[Y];
shadowMat[2][3] = 0.f - lightpos[W] * groundplane[Z];
shadowMat[3][3] = dot - lightpos[W] * groundplane[W];
}
Les parametres sont ceux de l'equation d'un plan : ax + by + cz + d =
0. Ou plus simplement utiliser :
/* Find the plane equation given 3 points. */
enum { X, Y, Z, W };
enum { A, B, C, D };
void findPlane(GLfloat plane[4], GLfloat v0[3], GLfloat v1[3], GLfloat v2[3])
{
GLfloat vec0[3], vec1[3];
/* Need 2 vectors to find cross product. */
vec0[X] = v1[X] - v0[X];
vec0[Y] = v1[Y] - v0[Y];
vec0[Z] = v1[Z] - v0[Z];
vec1[X] = v2[X] - v0[X];
vec1[Y] = v2[Y] - v0[Y];
vec1[Z] = v2[Z] - v0[Z];
/* find cross product to get A, B, and C of plane equation */
plane[A] = vec0[Y] * vec1[Z] - vec0[Z] * vec1[Y];
plane[B] = -(vec0[X] * vec1[Z] - vec0[Z] * vec1[X]);
plane[C] = vec0[X] * vec1[Y] - vec0[Y] * vec1[X];
plane[D] = -(plane[A] * v0[X] + plane[B] * v0[Y] + plane[C] * v0[Z]);
}
Il y maintenant tout ce qu'il faut : il reste a dessiner en noir
(transparent a 50%) l'objet pour obtenir son ombre sur un plan.
Un sol un peu eclaire :
void drawFloor()
{
GLfloat white[] = {1.0f, 1.0f, 1.0f, 1.0f};
GLfloat floorc[] = {0.5f, 0.5f, 0.5f, 0.75f};
glMaterialfv(GL_FRONT, GL_DIFFUSE, floorc);
glMaterialfv(GL_FRONT, GL_SPECULAR, floorc);
glBegin(GL_QUADS);
glVertex3fv(vfloor[0]);
glVertex3fv(vfloor[1]);
glVertex3fv(vfloor[2]);
glVertex3fv(vfloor[3]);
glEnd();
glMaterialfv(GL_FRONT, GL_DIFFUSE, white);
glMaterialfv(GL_FRONT, GL_SPECULAR, white);
}
La suite de OnPaint :
glDisable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA,
GL_ONE_MINUS_SRC_ALPHA);
drawFloor();
glDisable(GL_BLEND);
glEnable(GL_TEXTURE_2D);
glStencilFunc(GL_ALWAYS,
2, 0xffffffff);
glStencilOp(GL_KEEP, GL_KEEP,
GL_REPLACE);
for (x = 0 ; x < NB_CUBE_X ; x++)
for (int y = 0 ; y <
NB_CUBE_Y ; y++)
for (int z = 0 ; z < NB_CUBE_Z ; z++)
Cube[x][y][z]->Draw(0,-20,zz+20);
glStencilFunc(GL_EQUAL,
1, 0xffffffff); /* draw if ==1 */
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA,
GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_LIGHTING);
glDisable(GL_TEXTURE_2D);
glDisable(GL_DEPTH_TEST);
glColor4f(0.0, 0.0, 0.0, 0.5);
static GLfloat floorPlane[4];
static GLfloat floorShadow[4][4];
findPlane(floorPlane, vfloor[1], vfloor[2],
vfloor[3]);
shadowMatrix(floorShadow, floorPlane,
lightZeroPosition);
glPushMatrix();
glMultMatrixf((GLfloat
*) floorShadow);
for (x = 0 ; x <
NB_CUBE_X ; x++)
for (int y = 0 ; y < NB_CUBE_Y ; y++)
for (int z = 0 ; z < NB_CUBE_Z ; z++)
Cube[x][y][z]->Draw(0,-20,zz+20);
glPopMatrix();
glEnable(GL_DEPTH_TEST);
glEnable(GL_TEXTURE_2D);
glDisable(GL_BLEND);
glEnable(GL_LIGHTING);
glDisable(GL_STENCIL_TEST);
Premiere chose, un ombre peut etre cachee par l'objet lui-meme, donc la
ou le vrai objet est dessine, le stencil est mis a 2. Ensuite l'ombre n'est dessinee que
la ou le stencil est a 1, et chaque point n'est dessine qu'une fois (le stencil est
incrementee la ou l'ombre a deja ete portee) car plusieurs surfaces peuvent avoir des
ombres qui se supperposent (ca n'assombri pas pour autant l'ombre). Le depth-test est
desactive car le calcul de l'ombre est imprecis, elle peut etre legerment au-dessus (elle
est visible) ou au-dessous (elle n'est pas visible) du sol.
Ces exemples sont la base, cela ne marche qu'avec une source lumineuse.
Pour des ombres veritables (projetees sur n'importe quoi avec plusieurs sources
lumineuses) en temps reel : http://trant.sgi.com/opengl/toolkits/glut-3.5/progs/advanced/advanced.html
Code source : Tutorial10.zip