zoukankan      html  css  js  c++  java
  • NeHe OpenGL教程 第二十七课:影子

    转自【翻译】NeHe OpenGL 教程

    前言

    声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改。对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢。

    NeHe OpenGL第二十七课:影子

    影子:

    这是一个高级的主题,请确信你已经熟练的掌握了基本的OpenGL,并熟悉蒙板缓存。当然它会给你留下深刻的印象的。
     
    欢迎来到另一个有些复杂的课程,阴影。这一课的效果好的有些让人不可思议,阴影可以变形,混合在其他的物体上。
    这一课要求你必须对OpenGL比较了解,它假设你知道许多OpenGL的知识,你必须知道蒙板缓存,基本的OpenGL步骤。如果你对这些不太熟悉,我建议你可以看看前面的教程。当然,在这一课里,我们用到了很多数学知识,请准备好一本数学手册在你的身边。
    首先我们定义阴影体可以延伸的距离。 
      
    // 定义阴影体可以延伸的距离
    #define INFINITY 100

    下面定义一个3D顶点结构 
      
    // 3D顶点结构
    struct sPoint
    {
     GLfloat x, y, z;
    };

    定义一个平面结构 
      
    // 平面方程为: ax + by + cz + d = 0
    struct sPlaneEq
    {
     GLfloat a, b, c, d;
    };

    下面定义一个用来投影的三角形的结构
    3个整形索引指定了模型中三角形的三个顶点
    第二个变量指定了三角形面的法线
    平面方程描述了三角所在的平面
    临近的3个顶点索引,指定了与这个三角形相邻的三个顶点
    最后一个变量指定这个三角形是否投出阴影
     
    // 描述一个模型表面的结构
    struct sPlane
    {
     unsigned int p[3];   // 3个整形索引指定了模型中三角形的三个顶点
     sPoint normals[3];   // 第二个变量指定了三角形面的法线
     unsigned int neigh[3];   // 与本三角形三个边相邻的面的索引
     sPlaneEq PlaneEq;   // 平面方程描述了三角所在的平面
     bool visible;   // 最后一个变量指定这个三角形是否投出阴影?
    };

    最后我们用下面的结构描述一个产生阴影的物体。 
      
    struct glObject{ GLuint nPlanes, nPoints; sPoint points[100]; sPlane planes[200];};
      
    下面的代码用来读取模型,它的代码本身就解释了它的功能。它从文件中读取数据,并把顶点和索引存储在上面定义的结构中,并把所有的临近顶点初始化为-1,它代表这没有任何顶点与它相邻,我们将在以后计算它。 
    bool readObject( const char *filename, glObject*o)
    {
      FILE *file;
      unsigned int i;

      file = fopen(st, "r");
      if (!file) return FALSE;
      //读取顶点
      fscanf(file, "%d", &(o->nPoints));
      for (i=1;i<=o->nPoints;i++){
        fscanf(file, "%f", &(o->points[i].x));
        fscanf(file, "%f", &(o->points[i].y));
        fscanf(file, "%f", &(o->points[i].z));
      }
      //读取三角形面
      fscanf(file, "%d", &(o->nPlanes));
      for (i=0;inPlanes;i++){
        fscanf(file, "%d", &(o->planes[i].p[0]));
        fscanf(file, "%d", &(o->planes[i].p[1]));
        fscanf(file, "%d", &(o->planes[i].p[2]));
     //读取每个顶点的法线
        fscanf(file, "%f", &(o->planes[i].normals[0].x));
        fscanf(file, "%f", &(o->planes[i].normals[0].y));
        fscanf(file, "%f", &(o->planes[i].normals[0].z));
        fscanf(file, "%f", &(o->planes[i].normals[1].x));
        fscanf(file, "%f", &(o->planes[i].normals[1].y));
        fscanf(file, "%f", &(o->planes[i].normals[1].z));
        fscanf(file, "%f", &(o->planes[i].normals[2].x));
        fscanf(file, "%f", &(o->planes[i].normals[2].y));
        fscanf(file, "%f", &(o->planes[i].normals[2].z));
      }
     return true;
    }

    现在从setConnectivity函数开始,事情变得越来越复杂了,这个函数用来查找每个面的相邻的顶点,下面是它的伪代码:
     
    对于模型中的每一个面A 对于面A中的每一条边  如果我们不只到这条边相邻的顶点   那么对于模型中除了面A外的每一个面B    对于面B中的每一条边     如果面A的边和面B的边是同一条边,那么这两个面相邻      设置面A和面B的相邻属性
      
    下面的代码完成上面伪代码中最后两行的内容,你先获得每个面中边的两个顶点,然后检测他们是否相邻,如果是则设置各自的相邻顶点信息 
      
     int vertA1 = pFaceA->vertexIndices[edgeA];
     int vertA2 = pFaceA->vertexIndices[( edgeA+1 )%3];

     int vertB1 = pFaceB->vertexIndices[edgeB];
     int vertB2 = pFaceB->vertexIndices[( edgeB+1 )%3];

     // 测试他们是否为同一边,如果是则设置相应的相邻顶点信息
     if (( vertA1 == vertB1 && vertA2 == vertB2 ) || ( vertA1 == vertB2 && vertA2 == vertB1 ))
     {
      pFaceA->neighbourIndices[edgeA] = faceB;
      pFaceB->neighbourIndices[edgeB] = faceA;
      edgeFound = true;
      break;
     }

    完整的SetConnectivity函数的代码如下 
      
    // 设置相邻顶点信息
    inline void SetConnectivity(glObject *o){
     unsigned int p1i, p2i, p1j, p2j;
     unsigned int P1i, P2i, P1j, P2j;
     unsigned int i,j,ki,kj;

     //对于模型中的每一个面A
     for(i=0;inPlanes-1;i++)
     {
      //对于除了此面的其它的面B
      for(j=i+1;jnPlanes;j++)
      {
       //对于面A中的每一个相邻的顶点
       for(ki=0;ki<3;ki++)
       {
        //如果这个相邻的顶点没有被设置
        if(!o->planes[i].neigh[ki])
        {
         for(kj=0;kj<3;kj++)
         {
          p1i=ki;
          p1j=kj;
          p2i=(ki+1)%3;
          p2j=(kj+1)%3;

          p1i=o->planes[i].p[p1i];
          p2i=o->planes[i].p[p2i];
          p1j=o->planes[j].p[p1j];
          p2j=o->planes[j].p[p2j];
        
          //如果面A的边P1i->P1j和面B的边P2i->P2j为同一条边,则又下面的公式的P1i=P1j,并且P2i=P2j
          P1i=((p1i+p2i)-abs(p1i-p2i))/2;
          P2i=((p1i+p2i)+abs(p1i-p2i))/2;
          P1j=((p1j+p2j)-abs(p1j-p2j))/2;
          P2j=((p1j+p2j)+abs(p1j-p2j))/2;

          //记录与这个边相邻的面的索引
          if((P1i==P1j) && (P2i==P2j))
          {
           o->planes[i].neigh[ki] = j+1;  
           o->planes[j].neigh[kj] = i+1;  
          }
         }
        }
       }
      }
     }
    }

    下面的函数用来绘制模型 
      
    // 绘制模型,像以前一样它绘制组成模型的三角形
    void drawObject( const ShadowedObject& object )
    {
     glBegin( GL_TRIANGLES );
     for ( int i = 0; i < object.nFaces; i++ )
     {
      const Face& face = object.pFaces[i];

      for ( int j = 0; j < 3; j++ )
      {
       const Point3f& vertex = object.pVertices[face.vertexIndices[j]];

       glNormal3f( face.normals[j].x, face.normals[j].y, face.normals[j].z );
       glVertex3f( vertex.x, vertex.y, vertex.z );
      }
     }
     glEnd();
    }

    下面的函数用来计算平面的方程参数 
      
    void calculatePlane( const ShadowedObject& object, Face& face )
    {
     // 获得平面的三个顶点
     const Point3f& v1 = object.pVertices[face.vertexIndices[0]];
     const Point3f& v2 = object.pVertices[face.vertexIndices[1]];
     const Point3f& v3 = object.pVertices[face.vertexIndices[2]];

     face.planeEquation.a = v1.y*(v2.z-v3.z) + v2.y*(v3.z-v1.z) + v3.y*(v1.z-v2.z);
     face.planeEquation.b = v1.z*(v2.x-v3.x) + v2.z*(v3.x-v1.x) + v3.z*(v1.x-v2.x);
     face.planeEquation.c = v1.x*(v2.y-v3.y) + v2.x*(v3.y-v1.y) + v3.x*(v1.y-v2.y);
     face.planeEquation.d = -( v1.x*( v2.y*v3.z - v3.y*v2.z ) +
        v2.x*(v3.y*v1.z - v1.y*v3.z) +
        v3.x*(v1.y*v2.z - v2.y*v1.z) );
    }

    你还可以呼吸么?好的,我们继续:) 接下来你将学习如何去投影,castShadow函数几乎用到了所有OpenGL的功能,完成这个函数后,把它传递到doShadowPass函数来通过两个渲染通道绘制出阴影.
    首先,我们看看哪些面面对着灯光,我们可以通过灯光位置和平面方程计算出.如果灯光到平面的位置大于0,则位于灯光的上方,否则位于灯光的下方(如果有什么问题,翻一下你高中的解析几何). 
      
    void castShadow( ShadowedObject& object, GLfloat *lightPosition )
    {
     // 设置哪些面在灯光的前面
     for ( int i = 0; i < object.nFaces; i++ )
     {
      const Plane& plane = object.pFaces[i].planeEquation;

      GLfloat side = plane.a*lightPosition[0]+
       plane.b*lightPosition[1]+
       plane.c*lightPosition[2]+
       plane.d;

      if ( side > 0 )
       object.pFaces[i].visible = true;
      else
       object.pFaces[i].visible = false;
     }

    下面设置必要的状态来渲染阴影.
    首先,禁用灯光和绘制颜色,因为我们不计算光照,这样可以节约计算量.
    接着,设置深度缓存,深度测试还是需要的,但我们不希望我们的阴影体向实体一样具有深度,所以关闭深度缓存.
    最后我们启用蒙板缓存,让阴影体的位置在蒙板中被设置为1. 
      
     glDisable( GL_LIGHTING );     // 关闭灯光
     glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE );  // 关闭颜色缓存的写入 
     glDepthFunc( GL_LEQUAL );     // 设置深度比较函数
     glDepthMask( GL_FALSE );     // 关闭深度缓存的写入 
     glEnable( GL_STENCIL_TEST );    // 使用蒙板缓存
     glStencilFunc( GL_ALWAYS, 1, 0xFFFFFFFFL );   // 设置蒙板函数

    现在到了阴影被实际渲染得地方了,我们使用了下面提到的doShadowPass函数,它用来绘制阴影体的边界面.我们通过两个步骤来绘制阴影体,首先使用前向面增加阴影体在蒙板缓存中的值,接着使用后向面减少阴影体在蒙板缓存中的值. 
      
     // 如果是逆时针(即面向视点)的多边形,通过了蒙板和深度测试,则把蒙板的值增加1
     glFrontFace( GL_CCW );
     glStencilOp( GL_KEEP, GL_KEEP, GL_INCR );
     doShadowPass( object, lightPosition );
     // 如果是顺时针(即背向视点)的多边形,通过了蒙板和深度测试,则把蒙板的值减少1
     glFrontFace( GL_CW );
     glStencilOp( GL_KEEP, GL_KEEP, GL_DECR );
     doShadowPass( object, lightPosition );

    为了更好的理解这两个步骤,我建议你把第二步注释掉看看效果,如下所示:


    最后一步就是把阴影体所在的位置绘制上阴影的颜色 

     glFrontFace( GL_CCW );
     glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE ); 

     // 把阴影绘制上颜色
     glColor4f( 0.0f, 0.0f, 0.0f, 0.4f );
     glEnable( GL_BLEND );
     glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
     glStencilFunc( GL_NOTEQUAL, 0, 0xFFFFFFFFL );
     glStencilOp( GL_KEEP, GL_KEEP, GL_KEEP );
     glPushMatrix();
     glLoadIdentity();
     glBegin( GL_TRIANGLE_STRIP );
      glVertex3f(-0.1f, 0.1f,-0.10f);
      glVertex3f(-0.1f,-0.1f,-0.10f);
      glVertex3f( 0.1f, 0.1f,-0.10f);
      glVertex3f( 0.1f,-0.1f,-0.10f);
     glEnd();
     glPopMatrix();
    }

    下面的部分我们绘制构成阴影体边界的四边形,当我们循环所有的三角形面的时候,我们检测它是否是边界边,如果是我们绘制从灯光到这个边界边的射线,并衍生它用来构成四边形.
    这里要用一个蛮力,我们检测物体模型中每一个三角形面,找出其边界并连接灯光到边界的直线,把直线延长出一定的距离,构成阴影体.

    下面的代码完成这些功能,它看起来并没有想象的复杂.
     
    void doShadowPass(glObject *o, float *lp)
    {
     unsigned int i, j, k, jj;
     unsigned int p1, p2;
     sPoint   v1, v2;

     //对模型中的每一个面
     for (i=0; inPlanes;i++)
     { 
      //如果面在灯光的前面
      if (o->planes[i].visible)
      {
       //对于被灯光照射的面的每一个相邻的面
       for (j=0;j<3;j++)
       {
        k = o->planes[i].neigh[j];
        //如果面不存在,或不被灯光照射,那么这个边是边界
        if ((!k) || (!o->planes[k-1].visible))
        {
         // 获得面的两个顶点
         p1 = o->planes[i].p[j];
         jj = (j+1)%3;
         p2 = o->planes[i].p[jj];

         //计算边的顶点到灯光的方向,并放大100倍
         v1.x = (o->points[p1].x - lp[0])*100;
         v1.y = (o->points[p1].y - lp[1])*100;
         v1.z = (o->points[p1].z - lp[2])*100;

         v2.x = (o->points[p2].x - lp[0])*100;
         v2.y = (o->points[p2].y - lp[1])*100;
         v2.z = (o->points[p2].z - lp[2])*100;
         
         //绘制构成阴影体边界的面
         glBegin(GL_TRIANGLE_STRIP);
          glVertex3f(o->points[p1].x,
             o->points[p1].y,
             o->points[p1].z);
          glVertex3f(o->points[p1].x + v1.x,
             o->points[p1].y + v1.y,
             o->points[p1].z + v1.z);

          glVertex3f(o->points[p2].x,
             o->points[p2].y,
             o->points[p2].z);
          glVertex3f(o->points[p2].x + v2.x,
             o->points[p2].y + v2.y,
             o->points[p2].z + v2.z);
         glEnd();
        }
       }
      }
     }

    }

    既然我们已经能绘制阴影了,那么我们开始绘制我们的场景吧 
      
    bool drawGLScene()
    {
     GLmatrix16f Minv;
     GLvector4f wlp, lp;

     // 清空缓存
     glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

     glLoadIdentity();       // 设置灯光,并绘制球
     glTranslatef(0.0f, 0.0f, -20.0f);    
     glLightfv(GL_LIGHT1, GL_POSITION, LightPos);   
     glTranslatef(SpherePos[0], SpherePos[1], SpherePos[2]);  
     gluSphere(q, 1.5f, 32, 16);     
      
    下面我们计算灯光在物体坐标系中的位置 
      
     glLoadIdentity();      
     glRotatef(-yrot, 0.0f, 1.0f, 0.0f);    
     glRotatef(-xrot, 1.0f, 0.0f, 0.0f);    
     glTranslatef(-ObjPos[0], -ObjPos[1], -ObjPos[2]);  
     glGetFloatv(GL_MODELVIEW_MATRIX,Minv);    // 计算从世界坐标系变化到物体坐标系中的坐标
     lp[0] = LightPos[0];      // 保存灯光的位置
     lp[1] = LightPos[1];      
     lp[2] = LightPos[2];      
     lp[3] = LightPos[3];      
     VMatMult(Minv, lp);      // 计算最后灯光的位置

    下面绘制房间,物体和它的阴影 
      
     glLoadIdentity();  
     glTranslatef(0.0f, 0.0f, -20.0f);    
     DrawGLRoom();       // 绘制房间
     glTranslatef(ObjPos[0], ObjPos[1], ObjPos[2]);   
     glRotatef(xrot, 1.0f, 0.0f, 0.0f);    
     glRotatef(yrot, 0.0f, 1.0f, 0.0f);    
     DrawGLObject(obj);       // 绘制物体
     CastShadow(&obj, lp);      // 绘制物体的阴影

    下面的代码绘制一个黄色的球代表了灯光的位置 
      
     glColor4f(0.7f, 0.4f, 0.0f, 1.0f);    
     glDisable(GL_LIGHTING);      
     glDepthMask(GL_FALSE);      
     glTranslatef(lp[0], lp[1], lp[2]);    
     gluSphere(q, 0.2f, 16, 8);     
     glEnable(GL_LIGHTING);      
     glDepthMask(GL_TRUE);      
      
    最后设置物体的控制 
      
     xrot += xspeed;       // 增加X轴选择速度
     yrot += yspeed;       // 增加Y轴选择速度

     glFlush();       // 强制OpenGL完成所有的命令
     return TRUE;       // 成功返回
    }

    绘制房间墙面 
      
    void DrawGLRoom()        // 绘制房间(盒装)
    {
     glBegin(GL_QUADS);       // 绘制四边形
      // 地面
      glNormal3f(0.0f, 1.0f, 0.0f);    // 法线向上
      glVertex3f(-10.0f,-10.0f,-20.0f);   
      glVertex3f(-10.0f,-10.0f, 20.0f);   
      glVertex3f( 10.0f,-10.0f, 20.0f);   
      glVertex3f( 10.0f,-10.0f,-20.0f);   
      // 天花板
      glNormal3f(0.0f,-1.0f, 0.0f);    // 法线向下
      glVertex3f(-10.0f, 10.0f, 20.0f);   
      glVertex3f(-10.0f, 10.0f,-20.0f);   
      glVertex3f( 10.0f, 10.0f,-20.0f);   
      glVertex3f( 10.0f, 10.0f, 20.0f);   
      // 前面
      glNormal3f(0.0f, 0.0f, 1.0f);    // 法线向后
      glVertex3f(-10.0f, 10.0f,-20.0f);   
      glVertex3f(-10.0f,-10.0f,-20.0f);   
      glVertex3f( 10.0f,-10.0f,-20.0f);   
      glVertex3f( 10.0f, 10.0f,-20.0f);   
      // 后面
      glNormal3f(0.0f, 0.0f,-1.0f);    // 法线向前
      glVertex3f( 10.0f, 10.0f, 20.0f);   
      glVertex3f( 10.0f,-10.0f, 20.0f);   
      glVertex3f(-10.0f,-10.0f, 20.0f);   
      glVertex3f(-10.0f, 10.0f, 20.0f);   
      // 左面
      glNormal3f(1.0f, 0.0f, 0.0f);    // 法线向右
      glVertex3f(-10.0f, 10.0f, 20.0f);   
      glVertex3f(-10.0f,-10.0f, 20.0f);   
      glVertex3f(-10.0f,-10.0f,-20.0f);   
      glVertex3f(-10.0f, 10.0f,-20.0f);   
      // 右面
      glNormal3f(-1.0f, 0.0f, 0.0f);    // 法线向左
      glVertex3f( 10.0f, 10.0f,-20.0f);   
      glVertex3f( 10.0f,-10.0f,-20.0f);  
      glVertex3f( 10.0f,-10.0f, 20.0f);   
      glVertex3f( 10.0f, 10.0f, 20.0f);   
     glEnd();        // 结束绘制
    }

    下面的函数完成矩阵M与向量V的乘法M=M*V
     
    void VMatMult(GLmatrix16f M, GLvector4f v)
    {
     GLfloat res[4];       // 保存中间计算结果
     res[0]=M[ 0]*v[0]+M[ 4]*v[1]+M[ 8]*v[2]+M[12]*v[3];
     res[1]=M[ 1]*v[0]+M[ 5]*v[1]+M[ 9]*v[2]+M[13]*v[3];
     res[2]=M[ 2]*v[0]+M[ 6]*v[1]+M[10]*v[2]+M[14]*v[3];
     res[3]=M[ 3]*v[0]+M[ 7]*v[1]+M[11]*v[2]+M[15]*v[3];
     v[0]=res[0];       // 把结果保存在V中
     v[1]=res[1];
     v[2]=res[2];
     v[3]=res[3];       
    }

    下面的函数用来初始化模型对象 
      
    int InitGLObjects()       // 初始化模型对象
    {
     if (!ReadObject("Data/Object2.txt", &obj))    // 读取模型数据
     {
      return FALSE;      // 返回失败
     }

     SetConnectivity(&obj);      // 设置相邻顶点的信息

     for ( int i=0;i < obj.nPlanes;i++)     // 计算每个面的平面参数
      CalcPlane(obj, &obj.planes[i]);   

     return TRUE;       //成功返回
    }
    原文及其个版本源代码下载:

    http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=27

     
     
  • 相关阅读:
    Spring Boot 入门之持久层篇(三)
    Spring Boot 入门之 Web 篇(二)
    Spring Boot 入门之基础篇(一)
    Java 设计模式之建造者模式(四)
    Java 设计模式之抽象工厂模式(三)
    Java 设计模式之工厂模式(二)
    c++ type_info and typeid
    opengl& 颜色
    OpenGl And 视图
    如何写一个c++插件化系统
  • 原文地址:https://www.cnblogs.com/arxive/p/6239509.html
Copyright © 2011-2022 走看看