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) :

   Tutorial12.jpg (9631 bytes)

    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

    Etape precedente