2011. május 31., kedd

Tao Start 15 - Síkra vetített árnyékok

A háromdimenziós grafikus színterekben egy pixel színét az árnyalás és az árnyékolás együttes hatása alakítja ki. Eddigi OpenGL példáinkban csak az árnyalással foglalkoztunk. Az árnyalás a tárgyak képét alkotó pixelek színének kiszámítása a fényforrások és az objektum felületi színjellemzőinek figyelembevételével. Az árnyékolás alatt a tárgyak által a színtér más objektumaira vetett árnyékainak modellezését értjük. Ezt az OpenGL alapvetően nem támogatja. Ezért magunknak kell kidolgozni a szükséges eljárásokat.
Az árnyékszámítás egy nagyon érdekes terület a számítógépes grafikán belül. Sokféle megközelítés létezik:
  1. Vetített sík-árnyékok: síkfelületek esetén működik megfelelően
  2. Fénytérképek: csak statikus színtér esetén alkalmazható
  3. Árnyéktestek: az árnyéktestek kiszámítása költséges feladat
  4. Árnyéktérképek: rögzített pontosság miatt pontatlanságok
A sokféle árnyékvető algoritmus közül a legegyszerűbb módszer a síkra vetített árnyék. Ezt az eljárást kezdetben számos játék alkalmazta. Ma már elavult technikának számít.

A síkra vetített árnyékok geometriája
Lényege a következő, ismert a fényforrás l pozíciója és adott az S sík normálvektoros egyenlete, amelyen az árnyékot ki szeretnénk számolni:

Az l fényforrást a p ponttal összekötő egyenes egyenlete:
Az összefüggést a sík egyenletébe helyettesítve kifejezhetjük a metszéspontot, azaz a p vetített p' képének megfelelő ? értéket:
Az alfát visszahelyettesítve megkapjuk a p' síkra vetített pontot:
amelyet a következő alakban is felírhatunk
ahol gamma a fényforrás-sík távolság
Az egyenlet mindkét oldalát h-val szorozva, majd átrendezve kapjuk:
Vegyük észre, hogy p'h és h a p lineáris függvénye, így a leképezés egy projektív transzformációval is leírható. Ezt a projektív transzformációt a Tshadow 4×4-es mátrixszal szorozva végezzük el:
ahol az objektum az S síkra vetítő árnyék mátrix:
A vetítés után kapott felületi pont Descartes-koordinátáit homogén osztással számíthatjuk ki. Ha a síkra vetített, azaz kilapult objektumot sötét színnel jelenítjük meg, akkor olyan hatást érünk el, mintha az objektum árnyékát is kiszámoltuk volna. Ennél a módszernél tehát az árnyékokkal együttes megjelenítéshez minden objektumot kétszer kell megjeleníteni. Egyszer vetítés nélkül egyszer pedig vetítve, árnyékként. Nagy modellek esetén ez nem túl jó megoldás.
A megvalósításhoz szükséges két metódus pedig a következő:
  private float[] buildShadowMatrix(float[] lgtPosition, float[] plane)
  {
      float dotp;
      float[] matrix = new float[16];

      // Calculate the dot-product between the plane and the light's position
      dotp = plane[0] * lgtPosition[0] +
             plane[1] * lgtPosition[1] +
             plane[1] * lgtPosition[2] +
             plane[3] * lgtPosition[3];

      // First column
      matrix[00] = dotp - lgtPosition[0] * plane[0];
      matrix[04] = 0.0f - lgtPosition[0] * plane[1];
      matrix[08] = 0.0f - lgtPosition[0] * plane[2];
      matrix[12] = 0.0f - lgtPosition[0] * plane[3];

      // Second column
      matrix[01] = 0.0f - lgtPosition[1] * plane[0];
      matrix[05] = dotp - lgtPosition[1] * plane[1];
      matrix[09] = 0.0f - lgtPosition[1] * plane[2];
      matrix[13] = 0.0f - lgtPosition[1] * plane[3];

      // Third column
      matrix[02] = 0.0f - lgtPosition[2] * plane[0];
      matrix[06] = 0.0f - lgtPosition[2] * plane[1];
      matrix[10] = dotp - lgtPosition[2] * plane[2];
      matrix[14] = 0.0f - lgtPosition[2] * plane[3];

      // Fourth column
      matrix[03] = 0.0f - lgtPosition[3] * plane[0];
      matrix[07] = 0.0f - lgtPosition[3] * plane[1];
      matrix[11] = 0.0f - lgtPosition[3] * plane[2];
      matrix[15] = dotp - lgtPosition[3] * plane[3];

      return (matrix);
  }
A fenti eljárás feltölti a Shadow Mátrix-ot az elméletnek megfelelően. A következő eljárás pedig a sík együtthatóit adja vissza (A, B, C, D):
  // Desc: find the plane equation given 3 points
  private void findPlane(float[] v0, float[] v1, float[] v2, ref float[] plane)
  {
      float[] vec0 = new float[3];
      float[] vec1 = new float[3];

      // Need 2 vectors to find cross product
      vec0[0] = v1[0] - v0[0];
      vec0[1] = v1[1] - v0[1];
      vec0[2] = v1[2] - v0[2];

      vec1[0] = v2[0] - v0[0];
      vec1[1] = v2[1] - v0[1];
      vec1[2] = v2[2] - v0[2];

      // Find cross product to get A, B, and C of plane equation
      plane[0] = +(vec0[1] * vec1[2] - vec0[2] * vec1[1]);
      plane[1] = -(vec0[0] * vec1[2] - vec0[2] * vec1[0]);
      plane[2] = +(vec0[0] * vec1[1] - vec0[1] * vec1[0]);
      plane[3] = -(plane[0] * v0[0] + plane[1] * v0[1] + plane[2] * v0[2]);
  }
A virtuális színtér kirajzolásának menete a következő:
    private void Render()
    {
        //
        // Define the plane of the planar surface that we want to cast a shadow on...
        //

        float[] shadowPlane = new float[4];
        float[] v0 = new float[3];
        float[] v1 = new float[3];
        float[] v2 = new float[3];

        // To define a plane that matches the floor, we need to 3 vertices from it
        v0[0] = floor[0].vX;
        v0[1] = floor[0].vY;
        v0[2] = floor[0].vZ;

        v1[0] = floor[1].vX;
        v1[1] = floor[1].vY;
        v1[2] = floor[1].vZ;

        v2[0] = floor[2].vX;
        v2[1] = floor[2].vY;
        v2[2] = floor[2].vZ;

        findPlane(v0, v1, v2, ref shadowPlane);

        //
        // Build a shadow matrix using the light's current position and the plane
        //

        shadowMatrix = buildShadowMatrix(lgtPosition, shadowPlane);

        // A képernyő és a mélység puffer ürítése
        Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);

        //
        // Place the view
        //

        Gl.glMatrixMode(Gl.GL_MODELVIEW);
        Gl.glLoadIdentity();
        Gl.glTranslatef(0.0f, -2.0f, -15.0f);
        Gl.glRotatef(-spinViewXY[1], 1.0f, 0.0f, 0.0f);
        Gl.glRotatef(-spinViewXY[0], 0.0f, 1.0f, 0.0f);

        //
        // Render the floor...
        //

        DrawFloor();

        //
        // Create a shadow by rendering the teapot using the shadow matrix.
        //

        Gl.glDisable(Gl.GL_DEPTH_TEST);
        Gl.glDisable(Gl.GL_LIGHTING);

        Gl.glColor3f(0.2f, 0.2f, 0.2f); // Shadow's color
        Gl.glPushMatrix();
        {
            Gl.glMultMatrixf(shadowMatrix);

            // Teapot's position & orientation (needs to use the same transformations used to render the actual teapot)
            Gl.glTranslatef(0.0f, 2.5f, 0.0f);
            Gl.glRotatef(-spinTeapot[1], 1.0f, 0.0f, 0.0f);
            Gl.glRotatef(-spinTeapot[0], 0.0f, 1.0f, 0.0f);
            Glut.glutSolidTeapot(1.0d);
        }
        Gl.glPopMatrix();


        Gl.glEnable(Gl.GL_DEPTH_TEST);
        Gl.glEnable(Gl.GL_LIGHTING);

        //
        // Render the light's position as a sphere...
        //

        Gl.glDisable(Gl.GL_LIGHTING);

        Gl.glPushMatrix();
        {
            // Place the light...
            Gl.glLightfv(Gl.GL_LIGHT0, Gl.GL_POSITION, lgtPosition);

            // Place a sphere to represent the light
            Gl.glTranslatef(lgtPosition[0], lgtPosition[1], lgtPosition[2]);

            Gl.glColor3f(1.0f, 1.0f, 0.5f);
            Glut.glutSolidSphere(0.1, 8, 8);
        }
        Gl.glPopMatrix();

        Gl.glEnable(Gl.GL_LIGHTING);

        //
        // Render normal teapot
        //

        Gl.glPushMatrix();
        {
            // Teapot's position & orientation
            Gl.glTranslatef(0.0f, 2.5f, 0.0f);
            Gl.glRotatef(-spinTeapot[1], 1.0f, 0.0f, 0.0f);
            Gl.glRotatef(-spinTeapot[0], 0.0f, 1.0f, 0.0f);
            Glut.glutSolidTeapot(1.0d);
        }
        Gl.glPopMatrix();

        //A grafikus csővezeték ürítése
        Gl.glFlush();
    }

    private void DrawFloor()
    {
        Gl.glColor3f(1.0f, 1.0f, 1.0f);
        Gl.glInterleavedArrays(Gl.GL_N3F_V3F, Vertex.Stride, floor);
        Gl.glDrawArrays(Gl.GL_QUADS, 0, 4);
    }
További részletek megtalálhatóak a forráskódban. A teljes forrás pedig letölthető innen:


a végeredmény pedig a lenti képen látható. Valósidőben még látványosabb a dolog, az egérrel dönthető a sík és forgatható a teáskanna, a kurzor billentyűkkel pedig a fényforrás pozíciója állítható.

Fényforrás, objektum, sík, árnyék

A képen jól látható, hogy a földet szimbolizáló sík véges kiterjedésű és így könnyen előfordulhat, hogy a vetített árnyék egy része kilóg a semmibe. Van még mit fejlesztenünk, folytatás következik.

Felhasznált Irodalom

2 megjegyzés :

  1. És mi a helyzet ha nem egy plane-re akarok shadowot rakni, hanem egy terrainre? vagy egy gömb árnyékát rávetíteni egy másik gömbre?

    VálaszTörlés
  2. Kedves blogger a síkra vetített árnyékok módszere rendkívül régi és csak speciális esetekben működik. A te kérdésedre válaszolva a Shadow Mappin és Shadow Volume módszereket kellene megismerned. Sajnos vagy szerencsére az árnyékvetés kérdése nem egyértelmű, így sokféle megközelítés létezik.

    Link:
    http://www.codesampler.com/oglsrc/oglsrc_8.htm#ogl_shadow_volume
    http://sakura7.blog.hu/2010/02/24/shadow_map_ismet

    VálaszTörlés