以文本方式查看主题

-  计算机科学论坛  (http://bbs.xml.org.cn/index.asp)
--  『 C/C++编程思想 』  (http://bbs.xml.org.cn/list.asp?boardid=61)
----  [推荐]NeHe OpenGL教程(中英文版附带VC++源码)Lesson 27-lesson  28  (http://bbs.xml.org.cn/dispbbs.asp?boardid=61&rootid=&id=54180)


--  作者:一分之千
--  发布时间:10/22/2007 8:04:00 PM

--  [推荐]NeHe OpenGL教程(中英文版附带VC++源码)Lesson 27-lesson  28

第二十七二十八课源码

第二十七课


按此在新窗口浏览图片影子:

这是一个高级的主题,请确信你已经熟练的掌握了基本的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 );

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

  
图 1: 步骤1 图 2: 步骤2


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

 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;       //成功返回
}


   
其他的函数我们不做过多解释了,这会分散你的注意力,好好享受阴影带给你的快感吧.
下面还有一些说明:
球体不会产生阴影,因为我们没有设置其投影.
如果你发现程序很慢,买块好的显卡吧.

最后我希望你喜欢它,如果有什么好的建议,请告诉我.


--  作者:一分之千
--  发布时间:10/22/2007 8:05:00 PM

--  
Lesson 27
   
Welcome to a fairly complex tutorial on shadow casting. The effect this demo creates is literally incredible. Shadows that stretch, bend and wrap around other objects and across walls. Everything in the scene can be moved around in 3D space using keys on the keyboard.

This tutorial takes a fairly different approach - It assumes you have a lot of OpenGL knowledge. You should already understand the stencil buffer, and basic OpenGL setup. If you need to brush up, go back and read the earlier tutorials. Functions such as CreateGLWindow and WinMain will NOT be explained in this tutorial. Additionally, some fundamental 3D math is assumed, so keep a good textbook handy! (I used my 1st year maths lecture notes from University - I knew they'd come in handy later on! :)

First we have the definition of INFINITY, which represents how far to extend the shadow volume polygons (this will be explained more later on). If you are using a larger or smaller coordinate system, adjust this value accordingly.   
   

// Definition Of "INFINITY" For Calculating The Extension Vector For The Shadow Volume
#define INFINITY 100

   
Next is the definition of the object structures.

The Point3f structure holds a coordinate in 3D space. This can be used for vertices or vectors.   
   

// Structure Describing A Vertex In An Object
struct Point3f
{
 GLfloat x, y, z;
};

   
The Plane structure holds the 4 values that form the equation of a plane. These planes will represent the faces of the object.   
   

// Structure Describing A Plane, In The Format: ax + by + cz + d = 0
struct Plane
{
 GLfloat a, b, c, d;
};

   
The Face structure contains all the information necessary about a triangle to cast a shadow.
The indices specified are from the object's array of vertices.
The vertex normals are used to calculate the orientation of the face in 3D space, so you can determine which are facing the light source when casting the shadows.
The plane equation describes the plane that this triangle lies in, in 3D space.
The neighbour indices are indices into the array of faces in the object. This allows you to specify which face joins this face at each edge of the triangle.
The visible parameter is used to specify whether the face is "visible" to the light source which is casting the shadows.
  
   

// Structure Describing An Object's Face
struct Face
{
 int vertexIndices[3];   // Index Of Each Vertex Within An Object That Makes Up The Triangle Of This Face
 Point3f normals[3];   // Normals To Each Vertex
 Plane planeEquation;   // Equation Of A Plane That Contains This Triangle
 int neighbourIndices[3];  // Index Of Each Face That Neighbours This One Within The Object
 bool visible;    // Is The Face Visible By The Light?
};

   
Finally, the ShadowedObject structure contains all the vertices and faces in the object. The memory for each of the arrays is dynamically created when it is loaded.   
   

struct ShadowedObject
{
 int nVertices;
 Point3f *pVertices;   // Will Be Dynamically Allocated

 int nFaces;
 Face *pFaces;    // Will Be Dynamically Allocated
};

   
The readObject function is fairly self explanatory. It will fill in the given object structure with the values read from the file, allocating memory for the vertices and faces. It also initializes the neighbours to -1, which means there isn't one (yet). They will be calculated later.   
   

bool readObject( const char *filename, ShadowedObject& object )
{
 FILE *pInputFile;
 int i;

 pInputFile = fopen( filename, "r" );
 if ( pInputFile == NULL )
 {
  cerr << "Unable to open the object file: " << filename << endl;
  return false;
 }

 // Read Vertices
 fscanf( pInputFile, "%d", &object.nVertices );
 object.pVertices = new Point3f[object.nVertices];
 for ( i = 0; i < object.nVertices; i++ )
 {
  fscanf( pInputFile, "%f", &object.pVertices[i].x );
  fscanf( pInputFile, "%f", &object.pVertices[i].y );
  fscanf( pInputFile, "%f", &object.pVertices[i].z );
 }

 // Read Faces
 fscanf( pInputFile, "%d", &object.nFaces );
 object.pFaces = new Face[object.nFaces];
 for ( i = 0; i < object.nFaces; i++ )
 {
  int j;
  Face *pFace = &object.pFaces[i];

  for ( j = 0; j < 3; j++ )
   pFace->neighbourIndices[j] = -1; // No Neigbours Set Up Yet

  for ( j = 0; j < 3; j++ )
  {
   fscanf( pInputFile, "%d", &pFace->vertexIndices[j] );
   pFace->vertexIndices[j]--;  // Files Specify Them With A 1 Array Base, But We Use A 0 Array Base
  }

  for ( j = 0; j < 3; j++ )
  {
   fscanf( pInputFile, "%f", &pFace->normals[j].x );
   fscanf( pInputFile, "%f", &pFace->normals[j].y );
   fscanf( pInputFile, "%f", &pFace->normals[j].z );
  }
 }
 return true;
}

   
Likewise, killObject is self-explanatory - just delete all those dynamically allocated arrays inside the object when you are done with them. Note that a line was added to KillGLWindow to call this function for the object in question.   
   

void killObject( ShadowedObject& object )
{
 delete[] object.pFaces;
 object.pFaces = NULL;
 object.nFaces = 0;

 delete[] object.pVertices;
 object.pVertices = NULL;
 object.nVertices = 0;
}

   
Now, with setConnectivity it starts to get interesting. This function is used to find out what neighbours there are to each face of the object given. Here's some pseudo code:   
   

for each face (A) in the object
 for each edge in A
  if we don't know this edges neighbour yet
   for each face (B) in the object (except A)
    for each edge in B
     if A's edge is the same as B's edge, then they are neighbouring each other on that edge
      set the neighbour property for each face A and B, then move onto next edge in A

   
The last two lines are accomplished with the following code. By finding the two vertices that mark the ends of an edge and comparing them, you can discover if it is the same edge. The part (edgeA+1)%3 gets a vertex next to the one you are considering. Then you check if the vertices match (the order may be different, hence the second case of the if statement).   
   

 int vertA1 = pFaceA->vertexIndices[edgeA];
 int vertA2 = pFaceA->vertexIndices[( edgeA+1 )%3];

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

 // Check If They Are Neighbours - IE, The Edges Are The Same
 if (( vertA1 == vertB1 && vertA2 == vertB2 ) || ( vertA1 == vertB2 && vertA2 == vertB1 ))
 {
  pFaceA->neighbourIndices[edgeA] = faceB;
  pFaceB->neighbourIndices[edgeB] = faceA;
  edgeFound = true;
  break;
 }

   
Luckily, another easy function while you take a breath. drawObject renders each face one by one.   
   

// Draw An Object - Simply Draw Each Triangular Face.
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();
}

   
Calculating the equation of a plane looks ugly, but it is just a simple mathematical formula that you grab from a textbook when you need it.   
   

void calculatePlane( const ShadowedObject& object, Face& face )
{
 // Get Shortened Names For The Vertices Of The 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) );
}

   
Have you caught your breath yet? Good, because you are about to learn how to cast a shadow! The castShadow function does all of the GL specifics, and passes it on to doShadowPass to render the shadow in two passes.

First up, we determine which surfaces are facing the light. We do this by seeing which side of the plane the light is on. This is done by substituting the light's position into the equation for the plane. If this is larger than 0, then it is in the same direction as the normal to the plane and visible by the light. If not, then it is not visible by the light. (Again, refer to a good Math textbook for a better explanation of geometry in 3D).   
   

void castShadow( ShadowedObject& object, GLfloat *lightPosition )
{
 // Determine Which Faces Are Visible By The Light.
 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;
 }

   
The next section sets up the necessary OpenGL states for rendering the shadows.

First, we push all the attributes onto the stack that will be modified. This makes changing them back a lot easier.

Lighting is disabled because we will not be rendering to the color (output) buffer, just the stencil buffer. For the same reason, the color mask turns off all color components (so drawing a polygon won't get through to the output buffer).

Although depth testing is still used, we don't want the shadows to appear as solid objects in the depth buffer, so the depth mask prevents this from happening.

The stencil buffer is turned on as that is what is going to be used to draw the shadows into.   
   

 glPushAttrib( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ENABLE_BIT | GL_POLYGON_BIT | GL_STENCIL_BUFFER_BIT );
 glDisable( GL_LIGHTING );     // Turn Off Lighting
 glDepthMask( GL_FALSE );     // Turn Off Writing To The Depth-Buffer
 glDepthFunc( GL_LEQUAL );
 glEnable( GL_STENCIL_TEST );     // Turn On Stencil Buffer Testing
 glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE );  // Don't Draw Into The Colour Buffer
 glStencilFunc( GL_ALWAYS, 1, 0xFFFFFFFFL );

   
Ok, now the shadows are actually rendered. We'll come back to that in a moment when we look at the doShadowPass function. They are rendered in two passes as you can see, one incrementing the stencil buffer with the front faces (casting the shadow), the second decrementing the stencil buffer with the backfaces ("turning off" the shadow between the object and any other surfaces).   
   

 // First Pass. Increase Stencil Value In The Shadow
 glFrontFace( GL_CCW );
 glStencilOp( GL_KEEP, GL_KEEP, GL_INCR );
 doShadowPass( object, lightPosition );
 // Second Pass. Decrease Stencil Value In The Shadow
 glFrontFace( GL_CW );
 glStencilOp( GL_KEEP, GL_KEEP, GL_DECR );
 doShadowPass( object, lightPosition );

   
To understand how the second pass works, my best advise is to comment it out and run the tutorial again. To save you the trouble, I have done it here:

  
Figure 1: First Pass Figure 2: Second Pass


The final section of this function draws one blended rectangle over the whole screen, to cast a shadow. The darker you make this rectangle, the darker the shadows will be. So to change the properties of the shadow, change the glColor4f statement. Higher alpha will make it more black. Or you can make it red, green, purple, ...!   
   

 glFrontFace( GL_CCW );
 glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE ); // Enable Rendering To Colour Buffer For All Components

 // Draw A Shadowing Rectangle Covering The Entire Screen
 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();
 glPopAttrib();
}

   
Ok, the next part draws the shadowed quads. How does that work? What happens is that you go through every face, and if it is visible, then you check all of its edges. If at the edge, there is no neighbouring face, or the neighbouring face is not visible, the edge casts a shadow. If you think about the two cases clearly, then you'll see this is true. By drawing a quadrilateral (as two triangles) comprising of the points of the edge, and the edge projected backwards through the scene you get the shadow cast by it.

The brute force approach used here just draws to "infinity", and the shadow polygon is clipped against all the polygons it encounters. This causes piercing, which will stress the video hardware. For a high-performance modification to this algorithm, you should clip the polygon to the objects behind it. This is much trickier and has problems of its own, but if that's what you want to do, you should refer to this Gamasutra article.

The code to do all of that is not as tricky as it sounds. To start with, here is a snippet that loops through the objects. By the end of it, we have an edge, j, and its neighbouring face, specified by neighbourIndex.   
   

void doShadowPass( ShadowedObject& object, GLfloat *lightPosition )
{
 for ( int i = 0; i < object.nFaces; i++ )
 {
  const Face& face = object.pFaces[i];

  if ( face.visible )
  {
   // Go Through Each Edge
   for ( int j = 0; j < 3; j++ )
   {
    int neighbourIndex = face.neighbourIndices[j];

   
Next, check if there is a visible neighbouring face to this object. If not, then this edge casts a shadow.   
   

    // If There Is No Neighbour, Or Its Neighbouring Face Is Not Visible, Then This Edge Casts A Shadow
    if ( neighbourIndex == -1 || object.pFaces[neighbourIndex].visible == false )
    {

   
The next segment of code will retrieve the two vertices from the current edge, v1 and v2. Then, it calculates v3 and v4, which are projected along the vector between the light source and the first edge. They are scaled to INFINITY, which was set to a very large value.   
   

     // Get The Points On The Edge
     const Point3f& v1 = object.pVertices[face.vertexIndices[j]];
     const Point3f& v2 = object.pVertices[face.vertexIndices[( j+1 )%3]];

     // Calculate The Two Vertices In Distance
     Point3f v3, v4;

     v3.x = ( v1.x-lightPosition[0] )*INFINITY;
     v3.y = ( v1.y-lightPosition[1] )*INFINITY;
     v3.z = ( v1.z-lightPosition[2] )*INFINITY;

     v4.x = ( v2.x-lightPosition[0] )*INFINITY;
     v4.y = ( v2.y-lightPosition[1] )*INFINITY;
     v4.z = ( v2.z-lightPosition[2] )*INFINITY;

   
I think you'll understand the next section, it justs draws the quadrilateral defined by those four points:   
   

     // Draw The Quadrilateral (As A Triangle Strip)
     glBegin( GL_TRIANGLE_STRIP );
      glVertex3f( v1.x, v1.y, v1.z );
      glVertex3f( v1.x+v3.x, v1.y+v3.y, v1.z+v3.z );
      glVertex3f( v2.x, v2.y, v2.z );
      glVertex3f( v2.x+v4.x, v2.y+v4.y, v2.z+v4.z );
     glEnd();
    }
   }
  }
 }
}

   
With that, the shadow casting section is completed. But we are not finished yet! What about drawGLScene? Lets start with the simple bits: clearing the buffers, positioning the light source, and drawing a sphere:   
   

bool drawGLScene()
{
 GLmatrix16f Minv;
 GLvector4f wlp, lp;

 // Clear Color Buffer, Depth Buffer, Stencil Buffer
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

 glLoadIdentity();      // Reset Modelview Matrix
 glTranslatef(0.0f, 0.0f, -20.0f);    // Zoom Into Screen 20 Units
 glLightfv(GL_LIGHT1, GL_POSITION, LightPos);   // Position Light1
 glTranslatef(SpherePos[0], SpherePos[1], SpherePos[2]);  // Position The Sphere
 gluSphere(q, 1.5f, 32, 16);     // Draw A Sphere

   
Next, we have to calculate the light's position relative to the local coordinate system of the object. The comments explain each step in detail. Minv stores the object's transformation matrix, however it is done in reverse, and with negative arguments, so it is actually the inverse of the transformation matrix. Then lp is created as a copy of the light's position, and multiplied by the matrix. Thus, lp is the light's position in the object's coordinate system.   
   

 glLoadIdentity();      // Reset Matrix
 glRotatef(-yrot, 0.0f, 1.0f, 0.0f);    // Rotate By -yrot On Y Axis
 glRotatef(-xrot, 1.0f, 0.0f, 0.0f);    // Rotate By -xrot On X Axis
 glTranslatef(-ObjPos[0], -ObjPos[1], -ObjPos[2]);  // Move Negative On All Axis Based On ObjPos[] Values (X, Y, Z)
 glGetFloatv(GL_MODELVIEW_MATRIX,Minv);    // Retrieve ModelView Matrix (Stores In Minv)
 lp[0] = LightPos[0];      // Store Light Position X In lp[0]
 lp[1] = LightPos[1];      // Store Light Position Y In lp[1]
 lp[2] = LightPos[2];      // Store Light Position Z In lp[2]
 lp[3] = LightPos[3];      // Store Light Direction In lp[3]
 VMatMult(Minv, lp);      // We Store Rotated Light Vector In 'lp' Array

   
Now, palm off some of the work to draw the room, and the object. Calling castShadow draws the shadow of the object.   
   

 glLoadIdentity();      // Reset Modelview Matrix
 glTranslatef(0.0f, 0.0f, -20.0f);    // Zoom Into The Screen 20 Units
 DrawGLRoom();       // Draw The Room
 glTranslatef(ObjPos[0], ObjPos[1], ObjPos[2]);   // Position The Object
 glRotatef(xrot, 1.0f, 0.0f, 0.0f);    // Spin It On The X Axis By xrot
 glRotatef(yrot, 0.0f, 1.0f, 0.0f);    // Spin It On The Y Axis By yrot
 drawObject(obj);      // Procedure For Drawing The Loaded Object
 castShadow(obj, lp);      // Procedure For Casting The Shadow Based On The Silhouette

   
The following few lines draw a little orange circle where the light is:   
   

 glColor4f(0.7f, 0.4f, 0.0f, 1.0f);    // Set Color To An Orange
 glDisable(GL_LIGHTING);      // Disable Lighting
 glDepthMask(GL_FALSE);      // Disable Depth Mask
 glTranslatef(lp[0], lp[1], lp[2]);    // Translate To Light's Position
         // Notice We're Still In Local Coordinate System
 gluSphere(q, 0.2f, 16, 8);     // Draw A Little Yellow Sphere (Represents Light)
 glEnable(GL_LIGHTING);      // Enable Lighting
 glDepthMask(GL_TRUE);      // Enable Depth Mask

   
The last part updates the object's position and returns.   
   

 xrot += xspeed;       // Increase xrot By xspeed
 yrot += yspeed;       // Increase yrot By yspeed

 glFlush();       // Flush The OpenGL Pipeline
 return TRUE;       // Everything Went OK
}

   
We did specify a DrawGLRoom function, and here it is - a bunch of rectangles to cast shadows against:   
   

void DrawGLRoom()       // Draw The Room (Box)
{
 glBegin(GL_QUADS);      // Begin Drawing Quads
  // Floor
  glNormal3f(0.0f, 1.0f, 0.0f);    // Normal Pointing Up
  glVertex3f(-10.0f,-10.0f,-20.0f);   // Back Left
  glVertex3f(-10.0f,-10.0f, 20.0f);   // Front Left
  glVertex3f( 10.0f,-10.0f, 20.0f);   // Front Right
  glVertex3f( 10.0f,-10.0f,-20.0f);   // Back Right
  // Ceiling
  glNormal3f(0.0f,-1.0f, 0.0f);    // Normal Point Down
  glVertex3f(-10.0f, 10.0f, 20.0f);   // Front Left
  glVertex3f(-10.0f, 10.0f,-20.0f);   // Back Left
  glVertex3f( 10.0f, 10.0f,-20.0f);   // Back Right
  glVertex3f( 10.0f, 10.0f, 20.0f);   // Front Right
  // Front Wall
  glNormal3f(0.0f, 0.0f, 1.0f);    // Normal Pointing Away From Viewer
  glVertex3f(-10.0f, 10.0f,-20.0f);   // Top Left
  glVertex3f(-10.0f,-10.0f,-20.0f);   // Bottom Left
  glVertex3f( 10.0f,-10.0f,-20.0f);   // Bottom Right
  glVertex3f( 10.0f, 10.0f,-20.0f);   // Top Right
  // Back Wall
  glNormal3f(0.0f, 0.0f,-1.0f);    // Normal Pointing Towards Viewer
  glVertex3f( 10.0f, 10.0f, 20.0f);   // Top Right
  glVertex3f( 10.0f,-10.0f, 20.0f);   // Bottom Right
  glVertex3f(-10.0f,-10.0f, 20.0f);   // Bottom Left
  glVertex3f(-10.0f, 10.0f, 20.0f);   // Top Left
  // Left Wall
  glNormal3f(1.0f, 0.0f, 0.0f);    // Normal Pointing Right
  glVertex3f(-10.0f, 10.0f, 20.0f);   // Top Front
  glVertex3f(-10.0f,-10.0f, 20.0f);   // Bottom Front
  glVertex3f(-10.0f,-10.0f,-20.0f);   // Bottom Back
  glVertex3f(-10.0f, 10.0f,-20.0f);   // Top Back
  // Right Wall
  glNormal3f(-1.0f, 0.0f, 0.0f);    // Normal Pointing Left
  glVertex3f( 10.0f, 10.0f,-20.0f);   // Top Back
  glVertex3f( 10.0f,-10.0f,-20.0f);   // Bottom Back
  glVertex3f( 10.0f,-10.0f, 20.0f);   // Bottom Front
  glVertex3f( 10.0f, 10.0f, 20.0f);   // Top Front
 glEnd();       // Done Drawing Quads
}

   
And before I forget, here is the VMatMult function which multiplies a vector by a matrix (get that Math textbook out again!):   
   

void VMatMult(GLmatrix16f M, GLvector4f v)
{
 GLfloat res[4];       // Hold Calculated Results
 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];       // Results Are Stored Back In v[]
 v[1]=res[1];
 v[2]=res[2];
 v[3]=res[3];       // Homogenous Coordinate
}

   
The function to load the object is simple, just calling readObject, and then setting up the connectivity and the plane equations for each face.   
   

int InitGLObjects()       // Initialize Objects
{
 if (!readObject("Data/Object2.txt", obj))   // Read Object2 Into obj
 {
  return FALSE;      // If Failed Return False
 }

 setConnectivity(obj);      // Set Face To Face Connectivity

 for ( int i=0;i < obj.nFaces;i++)    // Loop Through All Object Faces
  calculatePlane(obj, obj.pFaces[i]);   // Compute Plane Equations For All Faces

 return TRUE;       // Return True
}

   
Finally, KillGLObjects is a convenience function so that if you add more objects, you can add them in a central place.   
   

void KillGLObjects()
{
 killObject( obj );
}

   
All of the other functions don't require any further explanantion. I have left out the standard NeHe tutorial code, as well as all of the variable definitions and the keyboard processing function. The commenting alone explains these sufficiently.

Some things to note about the tutorial:
The sphere doesn't stop shadows being projected on the wall. In reality, the sphere should also be casting a shadow, so seeing the one on the wall won't matter, it's hidden. It's just there to see what happens on curved surfaces :)
If you are noticing extremely slow frame rates, try switching to fullscreen mode, or setting your desktop colour depth to 32bpp.
Arseny L. writes: If you are having problems with a TNT2 in Windowed mode, make sure your desktop color depth is not set to 16bit. In 16bit color mode, the stencil buffer is emulated, resulting in sluggish performance. There are no problems in 32bit mode (I have a TNT2 Ultra and I checked it).
I've got to admit this was a lengthy task to write out this tutorial. It gives you full appreciation for the work that Jeff puts in! I hope you enjoy it, and give a huge thanks to Banu who wrote the original code! IF there is anything that needs further explaining in here, you are welcome to contact me (Brett), at brettporter@yahoo.com.

Banu Cosmin (Choko) & Brett Porter

Jeff Molofee (NeHe)



--  作者:一分之千
--  发布时间:10/22/2007 8:23:00 PM

--  

第二十八课

按此在新窗口浏览图片贝塞尔曲面:

这是一课关于数学运算的,没有别的内容了。来,有信心就看看它吧。

  
   
   
贝塞尔曲面

作者: David Nikdel ( ogapo@ithink.net )

这篇教程旨在介绍贝塞尔曲面,希望有比我更懂艺术的人能用她作出一些很COOL的东东并且展示给大家。教程不能用做一个完整的贝塞尔曲面库,而是一个展示概念的程序让你熟悉曲面怎样实现的。而且这不是一篇正规的文章,为了方便理解,我也许在有些地方术语不当;我希望大家能适应这个。最后,对那些已经熟悉贝塞尔曲面想看我写的如何的,真是丢脸;-)但你要是找到任何纰漏让我或者NeHe知道,毕竟人无完人嘛?还有,所有代码没有象我一般写程序那样做优化,这是故意的。我想每个人都能明白写的是什么。好,我想介绍到此为止,继续看下文!

数学::恶魔之音::(警告:内容有点长~)

好,如果想理解贝塞尔曲面没有对其数学基本的认识是很难的,如果你不愿意读这一部分或者你已经知道了关于她的数学知识你可以跳过。首先我会描述贝塞尔曲线再介绍生成贝塞尔曲面。
奇怪的是,如果你用过一个图形程序,你就已经熟悉了贝塞尔曲线,也许你接触的是另外的名称。它们是画曲线的最基本的方法,而且通常被表示成一系列点,其中有两个点与两端点表示左右两端的切线。下图展示了一个例子。

这是最基础的贝塞尔曲线(长点的由很多点在一起(多到你都没发现))。这个曲线由4个点定义,有2个端点和2个中间控制点。对计算机而言这些点都是一样的,但是特意的我们通常把前后两对点分别连接,因为他们的连线与短点相切。曲线是一个参数化曲线,画的时候从曲线上平均找几点连接。这样你可以控制曲线曲面的精度(和计算量)。最通常的方法是远距离少细分近距离多细分,对视点,看上去总是很完好的曲面而对速度的影响总是最小。
贝塞尔曲面基于一个基本方程,其他复杂的都是基于此。方程为:

t + (1 - t) = 1

看起来很简单不是?的确是的,这是最基本的贝塞尔曲线,一个一维的曲线。你也许从术语中猜到,贝塞尔曲线是多项式形式的。从线性代数知,一个一维的多项式是一条直线,没多大意思。好,因为基本方程对所有t都成立,我们可以平方,立方两边,怎么都行,等式都是成立的,对吧?好,我们试试立方。

(t + (1-t))^3 = 1^3

t^3 + 3*t^2*(1-t) + 3*t*(1-t)^2 + (1-t)^3 = 1

这是我们最常用的计算贝塞尔曲面的方程,a)她是最低维的不需要在一个平面内的多项式(有4个控制点),而且b)两边的切线互相没有联系(对于2维的只有3个控制点)。那么你看到了贝塞尔曲线了吗?呵呵,我们都没有,因为我还要加一个东西。
好,因为方程左边等于1,可以肯定如果你把所有项加起来还是等于1。这是否意味着在计算曲线上一点时可以以此决定该用每个控制点的多少呢?(答案是肯定的)你对了!当我们要计算曲线上一点的值我们只需要用控制点(表示为向量)乘以每部分再加起来。基本上我们要用0<=t<=1,但不是必要的。不明白了把?这里有函数:

P1*t^3 + P2*3*t^2*(1-t) + P3*3*t*(1-t)^2 + P4*(1-t)^3 = Pnew

因为多项式是连续的,有一个很好的办法在4个点间插值。曲线仅经过P1,P4,分别当t=1,0。
好,一切都很好,但现在我怎么把这个用在3D里呢?其实很简单,为了做一个贝塞尔曲面,你需要16个控制点,(4*4),和2个变量t,v。你要做的是计算在分量v的沿4条平行曲线的点,再用这4个点计算在分量t的点。计算了足够的这些点,我们可以用三角带连接他们,画出贝塞尔曲面。

    

恩,我认为现在已经有足够的数学背景了,看代码把!  
   

#include <math.h>       // 数学库
#include <stdio.h>       // 标准输入输出库
#include <stdlib.h>      // 标准库

typedef struct point_3d {      // 3D点的结构
 double x, y, z;
} POINT_3D;

typedef struct bpatch {      // 贝塞尔面片结构
 POINT_3D anchors[4][4];     // 由4x4网格组成
 GLuint  dlBPatch;     // 绘制面片的显示列表名称
 GLuint  texture;     // 面片的纹理
} BEZIER_PATCH;

BEZIER_PATCH  mybezier;     // 创建一个贝塞尔曲面结构
BOOL   showCPoints=TRUE;    // 是否显示控制点
int   divs = 7;     // 细分精度,控制曲面的显示精度

   
以下是一些简单的向量数学的函数。如果你是C++爱好者你可以用一个顶点类(保证其为3D的)。  
   

// 两个向量相加,p=p+q
POINT_3D pointAdd(POINT_3D p, POINT_3D q) {
 p.x += q.x;  p.y += q.y;  p.z += q.z;
 return p;
}

// 向量和标量相乘p=c*p
POINT_3D pointTimes(double c, POINT_3D p) {
 p.x *= c; p.y *= c; p.z *= c;
 return p;
}

// 创建一个3D向量
POINT_3D makePoint(double a, double b, double c) {
 POINT_3D p;
 p.x = a; p.y = b; p.z = c;
 return p;
}

   
这基本上是用C写的3维的基本函数,她用变量u和4个顶点的数组计算曲线上点。每次给u加上一定值,从0到1,我们可得一个很好的近似曲线。
求值器基于Bernstein多项式定义曲线,定义p(u ')为:
p(u')=∑Bni(u')Ri

这里Ri为控制点
Bni(u')=[ni]u'i(1-u')n-i

且00=1,[n0]=1

u'=(u-u1)/(u2-u1)

当为贝塞尔曲线时,控制点为4,相应的4个Bernstein多项式为:
1、B30 =(1-u)3
2、B31 =3u(1-u)2
3、B32 =3u2(1-u)
4、B33 =u3

  
   

// 计算贝塞尔方程的值
// 变量u的范围在0-1之间
POINT_3D Bernstein(float u, POINT_3D *p) {
 POINT_3D a, b, c, d, r;

 a = pointTimes(pow(u,3), p[0]);
 b = pointTimes(3*pow(u,2)*(1-u), p[1]);
 c = pointTimes(3*u*pow((1-u),2), p[2]);
 d = pointTimes(pow((1-u),3), p[3]);

 r = pointAdd(pointAdd(a, b), pointAdd(c, d));

 return r;
}

   
这个函数完成共享工作,生成所有三角带,保存在display list。我们这样就不需要每贞都重新计算曲面,除了当其改变时。另外,你可能想用一个很酷的效果,用MORPHING教程改变控制点位置。这可以做一个很光滑,有机的,morphing效果,只要一点点开销(你只要改变16个点,但要从新计算)。“最后”的数组元素用来保存前一行点,(因为三角带需要两行)。而且,纹理坐标由表示百分比的u,v来计算(平面映射)。
还有一个我们没做的是计算法向量做光照。到了这一步,你基本上有2种选择。第一是找每个三角形的中心计算X,Y轴的切线,再做叉积得到垂直与两向量的向量,再归一化,得到法向量。或者(恩,这是更好的方法)你可以直接用三角形的法矢(用你最喜欢的方法计算)得到一个近似值。我喜欢后者;我认为不值得为了一点点真实感影响速度。  
   

// 生成贝塞尔曲面的显示列表
GLuint genBezier(BEZIER_PATCH patch, int divs) {
 int  u = 0, v;
 float  py, px, pyold;
 GLuint  drawlist = glGenLists(1);   // 创建显示列表
 POINT_3D temp[4];
 POINT_3D *last = (POINT_3D*)malloc(sizeof(POINT_3D)*(divs+1)); // 更具每一条曲线的细分数,分配相应的内存

 if (patch.dlBPatch != NULL)     // 如果显示列表存在则删除
  glDeleteLists(patch.dlBPatch, 1);

 temp[0] = patch.anchors[0][3];    // 获得u方向的四个控制点
 temp[1] = patch.anchors[1][3];
 temp[2] = patch.anchors[2][3];
 temp[3] = patch.anchors[3][3];

 for (v=0;v<=divs;v++) {     // 根据细分数,创建各个分割点额参数
  px = ((float)v)/((float)divs);    
 // 使用Bernstein函数求的分割点的坐标
  last[v] = Bernstein(px, temp);
 }

 glNewList(drawlist, GL_COMPILE);    // 创建一个新的显示列表
 glBindTexture(GL_TEXTURE_2D, patch.texture);   // 邦定纹理

 for (u=1;u<=divs;u++) {
  py    = ((float)u)/((float)divs);   // 计算v方向上的细分点的参数
  pyold = ((float)u-1.0f)/((float)divs);  // 上一个v方向上的细分点的参数

  temp[0] = Bernstein(py, patch.anchors[0]);  // 计算每个细分点v方向上贝塞尔曲面的控制点
  temp[1] = Bernstein(py, patch.anchors[1]);
  temp[2] = Bernstein(py, patch.anchors[2]);
  temp[3] = Bernstein(py, patch.anchors[3]);

  glBegin(GL_TRIANGLE_STRIP);    // 开始绘制三角形带

  for (v=0;v<=divs;v++) {
   px = ((float)v)/((float)divs);  // 沿着u轴方向顺序绘制

   glTexCoord2f(pyold, px);   // 设置纹理坐标
   glVertex3d(last[v].x, last[v].y, last[v].z); // 绘制一个顶点

   last[v] = Bernstein(px, temp);  // 创建下一个顶点
   glTexCoord2f(py, px);   // 设置纹理
   glVertex3d(last[v].x, last[v].y, last[v].z); // 绘制新的顶点
  }

  glEnd();      // 结束三角形带的绘制
 }

 glEndList();      // 显示列表绘制结束

 free(last);      // 释放分配的内存
 return drawlist;      // 返回创建的显示列表
}

   
这里我们调用一个我认为有一些很酷的值的矩阵。  
   

void initBezier(void) {
 mybezier.anchors[0][0] = makePoint(-0.75, -0.75, -0.50); // 设置贝塞尔曲面的控制点
 mybezier.anchors[0][1] = makePoint(-0.25, -0.75,  0.00);
 mybezier.anchors[0][2] = makePoint( 0.25, -0.75,  0.00);
 mybezier.anchors[0][3] = makePoint( 0.75, -0.75, -0.50);
 mybezier.anchors[1][0] = makePoint(-0.75, -0.25, -0.75);
 mybezier.anchors[1][1] = makePoint(-0.25, -0.25,  0.50);
 mybezier.anchors[1][2] = makePoint( 0.25, -0.25,  0.50);
 mybezier.anchors[1][3] = makePoint( 0.75, -0.25, -0.75);
 mybezier.anchors[2][0] = makePoint(-0.75,  0.25,  0.00);
 mybezier.anchors[2][1] = makePoint(-0.25,  0.25, -0.50);
 mybezier.anchors[2][2] = makePoint( 0.25,  0.25, -0.50);
 mybezier.anchors[2][3] = makePoint( 0.75,  0.25,  0.00);
 mybezier.anchors[3][0] = makePoint(-0.75,  0.75, -0.50);
 mybezier.anchors[3][1] = makePoint(-0.25,  0.75, -1.00);
 mybezier.anchors[3][2] = makePoint( 0.25,  0.75, -1.00);
 mybezier.anchors[3][3] = makePoint( 0.75,  0.75, -0.50);
 mybezier.dlBPatch = NULL;     // 默认的显示列表为0
}

   
这是一个优化的调位图的函数。可以很简单的把他们放进一个简单循环里调一组。   
   

// 加载一个*.bmp文件,并转化为纹理

BOOL LoadGLTexture(GLuint *texPntr, char* name)
{
 BOOL success = FALSE;
 AUX_RGBImageRec *TextureImage = NULL;

 glGenTextures(1, texPntr);     // 生成纹理1

 FILE* test=NULL;
 TextureImage = NULL;

 test = fopen(name, "r");     
 if (test != NULL) {      
  fclose(test);      
  TextureImage = auxDIBImageLoad(name);   
 }

 if (TextureImage != NULL) {     
  success = TRUE;

  // 邦定纹理
  glBindTexture(GL_TEXTURE_2D, *texPntr);
  glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage->sizeX, TextureImage->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage->data);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
 }

 if (TextureImage->data)
  free(TextureImage->data);

 return success;
}

   
仅仅加了曲面初始化在这。你每次建一个曲面时都会用这个。再一次,这里是一个用C++的好地方(贝塞尔曲面类?)。
  
   

int InitGL(GLvoid)       // 初始化OpenGL
{
 glEnable(GL_TEXTURE_2D);     // 使用2D纹理
 glShadeModel(GL_SMOOTH);     // 使用平滑着色
 glClearColor(0.05f, 0.05f, 0.05f, 0.5f);   // 设置黑色背景
 glClearDepth(1.0f);     // 设置深度缓存
 glEnable(GL_DEPTH_TEST);     // 使用深度缓存
 glDepthFunc(GL_LEQUAL);     // 设置深度方程
 glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  

 initBezier();      // 初始化贝塞尔曲面
 LoadGLTexture(&(mybezier.texture), "./Data/NeHe.bmp");  // 载入纹理
 mybezier.dlBPatch = genBezier(mybezier, divs);  // 创建显示列表

 return TRUE;      // 初始化成功
}

   
首先调贝塞尔display list。再(如果边线要画)画连接控制点的线。你可以用SPACE键开关这个。
  
   

int DrawGLScene(GLvoid) {      // 绘制场景
 int i, j;
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
 glLoadIdentity();      
 glTranslatef(0.0f,0.0f,-4.0f);     // 移入屏幕4个单位
 glRotatef(-75.0f,1.0f,0.0f,0.0f);
 glRotatef(rotz,0.0f,0.0f,1.0f);     // 旋转一定的角度

 glCallList(mybezier.dlBPatch);     // 调用显示列表,绘制贝塞尔曲面

 if (showCPoints) {       // 是否绘制控制点
  glDisable(GL_TEXTURE_2D);
  glColor3f(1.0f,0.0f,0.0f);
  for(i=0;i<4;i++) {      // 绘制水平线
   glBegin(GL_LINE_STRIP);
   for(j=0;j<4;j++)
    glVertex3d(mybezier.anchors[i][j].x, mybezier.anchors[i][j].y, mybezier.anchors[i][j].z);
   glEnd();
  }
  for(i=0;i<4;i++) {      // 绘制垂直线
   glBegin(GL_LINE_STRIP);
   for(j=0;j<4;j++)
    glVertex3d(mybezier.anchors[j][i].x, mybezier.anchors[j][i].y, mybezier.anchors[j][i].z);
   glEnd();
  }
  glColor3f(1.0f,1.0f,1.0f);
  glEnable(GL_TEXTURE_2D);
 }

 return TRUE;       // 成功返回
}

   
KillGLWindow()函数没有改动
  
   
   
CreateGLWindow()函数没有改动  
   
   
我在这里加了旋转曲面的代码,增加/降低分辨率,显示与否控制点连线。   
   

   if (keys[VK_LEFT]) rotz -= 0.8f;  // 按左键,向左旋转
   if (keys[VK_RIGHT]) rotz += 0.8f; // 按右键,向右旋转
   if (keys[VK_UP]) {    // 按上键,加大曲面的细分数目
    divs++;
    mybezier.dlBPatch = genBezier(mybezier, divs); // 更新贝塞尔曲面的显示列表
    keys[VK_UP] = FALSE;
   }
   if (keys[VK_DOWN] && divs > 1) {    // 按下键,减少曲面的细分数目
    divs--;
    mybezier.dlBPatch = genBezier(mybezier, divs); // 更新贝塞尔曲面的显示列表
    keys[VK_DOWN] = FALSE;
   }
   if (keys[VK_SPACE]) {     // 按空格切换控制点的可见性
    showCPoints = !showCPoints;
    keys[VK_SPACE] = FALSE;
   }

   
恩,我希望这个教程让你了然于心而且你现在象我一样喜欢上了贝塞尔曲面。;-)如果你喜欢这个教程我会继续写一篇关于NURBS的如果有人喜欢。请EMAIL我让我知道你怎么想这篇教程。


--  作者:一分之千
--  发布时间:10/22/2007 8:27:00 PM

--  
Lesson 28
   
Bezier Patches

Written by: David Nikdel ( ogapo@ithink.net )

This tutorial is intended to introduce you to Bezier Surfaces in the hopes that someone more artistic than myself will do something really cool with them and show all of us. This is not intended as a complete Bezier patch library, but more as proof of concept code to get you familiar with how these curved surfaces actually work. Also, as this is a very informal piece, I may have occasional lapses in correct terminology in favor of comprehensability; I hope this sits well with everyone. Finally, to those of you already familiar with Beziers who are just reading this to see if I screw up, shame on you ;-), but if you find anything wrong by all means let me or NeHe know, after all no one's perfect, eh? Oh, and one more thing, none of this code is optimised beyond my normal programming technique, this is by design. I want everyone to be able to see exactly what is going on. Well, I guess that's enough of an intro. On with the show!

The Math - ::evil music:: (warning, kinda long section)

Ok, it will be very hard to understand Beziers without at least a basic understanding of the math behind it, however, if you just don't feel like reading this section or already know the math, you can skip it. First I will start out by describing the Bezier curve itself then move on to how to create a Bezier Patch.

Odds are, if you've ever used a graphics program you are already familiar with Bezier curves, perhaps not by that name though. They are the primary method of drawing curved lines and are commonly represented as a series of points each with 2 points representing the tangent at that point from the left and right. Here's what one looks like:

This is the most basic Bezier curve possible (longer ones are made by attaching many of these together (many times without the user realizing it)). This curve is actually defined by only 4 points, those would be the 2 ending control points and the 2 middle control points. To the computer, all the points are the same, but to aid in design we often connect the first and the last two, respectively, because those lines will always be tangent to the endpoint. The curve is a parametric curve and is drawn by finding any number of points evenly spaced along the curve and connecting them with straight lines. In this way you can control the resolution of the patch (and the amount of computation). The most common way to use this is to tesselate it less at a farther distance and more at a closer distance so that, to the viewer, it always appears to be a perfectly curved surface with the lowest possible speed hit.

Bezier curves are based on a basis function from which more complicated versions are derived. Here's the function:

t + (1 - t) = 1

Sounds simple enough huh? Well it really is, this is the Bezier most basic Bezier curve, a 1st degree curve. As you may have guessed from the terminology, the Bezier curves are polynomials, and as we remember from algebra, a 1st degree polynomial is just a straight line; not very interesting. Well, since the basis function is true for all numbers t, we can square, cube, whatever, each side and it will still be true right? Well, lets try cubing it.

(t + (1-t))^3 = 1^3

t^3 + 3*t^2*(1-t) + 3*t*(1-t)^2 + (1-t)^3 = 1

This is the equation we use to calculate the most common Bezier, the 3rd degree Bezier curve. This is most common for two reasons, a) it's the lowest degree polynomial that need not necesarily lie in a plane (there are 4 control points) and b) the tangent lines on the sides are not dependant on one another (with a 2nd degree there would be only 3 control points). So do you see the Bezier curve yet? Hehe, me neither, that's because I still need to add one thing.

Ok, since the entire left side is equal to 1, it's safe to assume that if you add all the components they should still equal one. Does this sound like it could be used to descide how much of each control point to use in calculating a point on the curve? (hint: just say yes ;-) ) Well you're right! When we want to calculate the value of a point some percent along the curve we simply multiply each part by a control point (as a vector) and find the sum. Generally, we'll work with 0 <= t <= 1, but it's not technically necesary. Confused yet? Here's the function:

P1*t^3 + P2*3*t^2*(1-t) + P3*3*t*(1-t)^2 + P4*(1-t)^3 = Pnew

Because polynomials are always continuous, this makes for a good way to morp between the 4 points. The only points it actually reaches though are P1 and P4, when t = 1 and 0 respectively.

Now, that's all well and good, but how can I use these in 3D you ask? Well it's actually quite simple, in order to form a Bezier patch, you need 16 control points (4*4), and 2 variables t and v. What you do from there is calculate a point at v along 4 of the parallel curves then use those 4 points to make a new curve and calculate t along that curve. By calculating enough of these points, we can draw triangle strips to connect them, thus drawing the Bezier patch.

    

Well, I suppose that's enough math for now, on to the code!   
   

#include <windows.h>       // Header File For Windows
#include <math.h>       // Header File For Math Library Routines
#include <stdio.h>       // Header File For Standard I/O Routines
#include <stdlib.h>       // Header File For Standard Library
#include <gl\gl.h>       // Header File For The OpenGL32 Library
#include <gl\glu.h>       // Header File For The GLu32 Library
#include <gl\glaux.h>       // Header File For The Glaux Library

typedef struct point_3d {      // Structure For A 3-Dimensional Point ( NEW )
 double x, y, z;
} POINT_3D;

typedef struct bpatch {       // Structure For A 3rd Degree Bezier Patch ( NEW )
 POINT_3D anchors[4][4];     // 4x4 Grid Of Anchor Points
 GLuint  dlBPatch;     // Display List For Bezier Patch
 GLuint  texture;     // Texture For The Patch
} BEZIER_PATCH;

HDC   hDC=NULL;     // Private GDI Device Context
HGLRC   hRC=NULL;     // Permanent Rendering Context
HWND   hWnd=NULL;     // Holds Our Window Handle
HINSTANCE  hInstance;     // Holds The Instance Of The Application

DEVMODE   DMsaved;     // Saves The Previous Screen Settings ( NEW )

bool   keys[256];     // Array Used For The Keyboard Routine
bool   active=TRUE;     // Window Active Flag Set To TRUE By Default
bool   fullscreen=TRUE;    // Fullscreen Flag Set To Fullscreen Mode By Default

GLfloat   rotz = 0.0f;     // Rotation About The Z Axis
BEZIER_PATCH  mybezier;     // The Bezier Patch We're Going To Use ( NEW )
BOOL   showCPoints=TRUE;    // Toggles Displaying The Control Point Grid ( NEW )
int   divs = 7;     // Number Of Intrapolations (Controls Poly Resolution) ( NEW )

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);   // Declaration For WndProc

   
The following are just a few quick functions for some simple vector math. If you're a fan of C++ you might consider using a point class (just make sure it's 3d).   
   

// Adds 2 Points. Don't Just Use '+' ;)
POINT_3D pointAdd(POINT_3D p, POINT_3D q) {
 p.x += q.x;  p.y += q.y;  p.z += q.z;
 return p;
}

// Multiplies A Point And A Constant. Don't Just Use '*'
POINT_3D pointTimes(double c, POINT_3D p) {
 p.x *= c; p.y *= c; p.z *= c;
 return p;
}

// Function For Quick Point Creation
POINT_3D makePoint(double a, double b, double c) {
 POINT_3D p;
 p.x = a; p.y = b; p.z = c;
 return p;
}

   
This is basically just the 3rd degree basis function written in C, it takes a variable u and an array of 4 points and computes a point on the curve. By stepping u in equal increments between 0 and 1, we'll get a nice approximation of the curve.   
   

// Calculates 3rd Degree Polynomial Based On Array Of 4 Points
// And A Single Variable (u) Which Is Generally Between 0 And 1
POINT_3D Bernstein(float u, POINT_3D *p) {
 POINT_3D a, b, c, d, r;

 a = pointTimes(pow(u,3), p[0]);
 b = pointTimes(3*pow(u,2)*(1-u), p[1]);
 c = pointTimes(3*u*pow((1-u),2), p[2]);
 d = pointTimes(pow((1-u),3), p[3]);

 r = pointAdd(pointAdd(a, b), pointAdd(c, d));

 return r;
}

   
This function does the lion's share of the work by generating all the triangle strips and storing them in a display list. We do this so that we don't have to recalculate the patch each frame, only when it changes. By the way, a cool effect you might want to try might be to use the morphing tutorial to morph the patch's control points. This would yeild a very cool smooth, organic, morphing effect for relatively little overhead (you only morph 16 points, but you have to recalculate). The "last" array is used to keep the previous line of points (since a triangle strip needs both rows). Also, texture coordinates are calculated by using the u and v values as the percentages (planar mapping).

One thing we don't do is calculate the normals for lighting. When it comes to this, you basically have two options. The first is to find the center of each triangle, then use a bit of calculus and calculate the tangent on both the x and y axes, then do the cross product to get a vector perpendicular to both, THEN normalize the vector and use that as the normal. OR (yes, there is a faster way) you can cheat and just use the normal of the triangle (calculated your favorite way) to get a pretty good approximation. I prefer the latter; the speed hit, in my opinion, isn't worth the extra little bit of realism.   
   

// Generates A Display List Based On The Data In The Patch
// And The Number Of Divisions
GLuint genBezier(BEZIER_PATCH patch, int divs) {
 int  u = 0, v;
 float  py, px, pyold;
 GLuint  drawlist = glGenLists(1);   // Make The Display List
 POINT_3D temp[4];
 POINT_3D *last = (POINT_3D*)malloc(sizeof(POINT_3D)*(divs+1));
    // Array Of Points To Mark The First Line Of Polys

 if (patch.dlBPatch != NULL)     // Get Rid Of Any Old Display Lists
  glDeleteLists(patch.dlBPatch, 1);

 temp[0] = patch.anchors[0][3];     // The First Derived Curve (Along X-Axis)
 temp[1] = patch.anchors[1][3];
 temp[2] = patch.anchors[2][3];
 temp[3] = patch.anchors[3][3];

 for (v=0;v<=divs;v++) {      // Create The First Line Of Points
  px = ((float)v)/((float)divs);    // Percent Along Y-Axis
 // Use The 4 Points From The Derived Curve To Calculate The Points Along That Curve
  last[v] = Bernstein(px, temp);
 }

 glNewList(drawlist, GL_COMPILE);    // Start A New Display List
 glBindTexture(GL_TEXTURE_2D, patch.texture);   // Bind The Texture

 for (u=1;u<=divs;u++) {
  py    = ((float)u)/((float)divs);   // Percent Along Y-Axis
  pyold = ((float)u-1.0f)/((float)divs);   // Percent Along Old Y Axis

  temp[0] = Bernstein(py, patch.anchors[0]);  // Calculate New Bezier Points
  temp[1] = Bernstein(py, patch.anchors[1]);
  temp[2] = Bernstein(py, patch.anchors[2]);
  temp[3] = Bernstein(py, patch.anchors[3]);

  glBegin(GL_TRIANGLE_STRIP);    // Begin A New Triangle Strip

  for (v=0;v<=divs;v++) {
   px = ((float)v)/((float)divs);   // Percent Along The X-Axis

   glTexCoord2f(pyold, px);   // Apply The Old Texture Coords
   glVertex3d(last[v].x, last[v].y, last[v].z); // Old Point

   last[v] = Bernstein(px, temp);   // Generate New Point
   glTexCoord2f(py, px);    // Apply The New Texture Coords
   glVertex3d(last[v].x, last[v].y, last[v].z); // New Point
  }

  glEnd();      // END The Triangle Strip
 }

 glEndList();       // END The List

 free(last);       // Free The Old Vertices Array
 return drawlist;      // Return The Display List
}

   
Here we're just loading the matrix with some values I've picked that I think look cool. Feel free to screw around with these and see what it looks like. :-)   
   

void initBezier(void) {
 mybezier.anchors[0][0] = makePoint(-0.75, -0.75, -0.50); // Set The Bezier Vertices
 mybezier.anchors[0][1] = makePoint(-0.25, -0.75,  0.00);
 mybezier.anchors[0][2] = makePoint( 0.25, -0.75,  0.00);
 mybezier.anchors[0][3] = makePoint( 0.75, -0.75, -0.50);
 mybezier.anchors[1][0] = makePoint(-0.75, -0.25, -0.75);
 mybezier.anchors[1][1] = makePoint(-0.25, -0.25,  0.50);
 mybezier.anchors[1][2] = makePoint( 0.25, -0.25,  0.50);
 mybezier.anchors[1][3] = makePoint( 0.75, -0.25, -0.75);
 mybezier.anchors[2][0] = makePoint(-0.75,  0.25,  0.00);
 mybezier.anchors[2][1] = makePoint(-0.25,  0.25, -0.50);
 mybezier.anchors[2][2] = makePoint( 0.25,  0.25, -0.50);
 mybezier.anchors[2][3] = makePoint( 0.75,  0.25,  0.00);
 mybezier.anchors[3][0] = makePoint(-0.75,  0.75, -0.50);
 mybezier.anchors[3][1] = makePoint(-0.25,  0.75, -1.00);
 mybezier.anchors[3][2] = makePoint( 0.25,  0.75, -1.00);
 mybezier.anchors[3][3] = makePoint( 0.75,  0.75, -0.50);
 mybezier.dlBPatch = NULL;     // Go Ahead And Initialize This To NULL
}

   
This is basically just an optimised routine to load a single bitmap. It can easily be used to load an array of em just by putting it in a simple loop.   
   

// Load Bitmaps And Convert To Textures

BOOL LoadGLTexture(GLuint *texPntr, char* name)
{
 BOOL success = FALSE;
 AUX_RGBImageRec *TextureImage = NULL;

 glGenTextures(1, texPntr);     // Generate 1 Texture

 FILE* test=NULL;
 TextureImage = NULL;

 test = fopen(name, "r");     // Test To See If The File Exists
 if (test != NULL) {      // If It Does
  fclose(test);      // Close The File
  TextureImage = auxDIBImageLoad(name);   // And Load The Texture
 }

 if (TextureImage != NULL) {     // If It Loaded
  success = TRUE;

  // Typical Texture Generation Using Data From The Bitmap
  glBindTexture(GL_TEXTURE_2D, *texPntr);
  glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage->sizeX, TextureImage->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage->data);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
 }

 if (TextureImage->data)
  free(TextureImage->data);

 return success;
}

   
Just adding the patch initialization here. You would do this whenever you create a patch. Again, this might be a cool place to use C++ (bezier class?).   
   

int InitGL(GLvoid)       // All Setup For OpenGL Goes Here
{
 glEnable(GL_TEXTURE_2D);     // Enable Texture Mapping
 glShadeModel(GL_SMOOTH);     // Enable Smooth Shading
 glClearColor(0.05f, 0.05f, 0.05f, 0.5f);   // Black Background
 glClearDepth(1.0f);      // Depth Buffer Setup
 glEnable(GL_DEPTH_TEST);     // Enables Depth Testing
 glDepthFunc(GL_LEQUAL);      // The Type Of Depth Testing To Do
 glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  // Really Nice Perspective Calculations

 initBezier();       // Initialize the Bezier's Control Grid ( NEW )
 LoadGLTexture(&(mybezier.texture), "./Data/NeHe.bmp");  // Load The Texture ( NEW )
 mybezier.dlBPatch = genBezier(mybezier, divs);   // Generate The Patch ( NEW )

 return TRUE;       // Initialization Went OK
}

   
First call the bezier's display list. Then (if the outlines are on) draw the lines connecting the control points. You can toggle these by pressing SPACE.   
   

int DrawGLScene(GLvoid) {      // Here's Where We Do All The Drawing
 int i, j;
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  // Clear Screen And Depth Buffer
 glLoadIdentity();      // Reset The Current Modelview Matrix
 glTranslatef(0.0f,0.0f,-4.0f);     // Move Left 1.5 Units And Into The Screen 6.0
 glRotatef(-75.0f,1.0f,0.0f,0.0f);
 glRotatef(rotz,0.0f,0.0f,1.0f);     // Rotate The Triangle On The Z-Axis

 glCallList(mybezier.dlBPatch);     // Call The Bezier's Display List
         // This Need Only Be Updated When The Patch Changes

 if (showCPoints) {      // If Drawing The Grid Is Toggled On
  glDisable(GL_TEXTURE_2D);
  glColor3f(1.0f,0.0f,0.0f);
  for(i=0;i<4;i++) {     // Draw The Horizontal Lines
   glBegin(GL_LINE_STRIP);
   for(j=0;j<4;j++)
    glVertex3d(mybezier.anchors[i][j].x, mybezier.anchors[i][j].y, mybezier.anchors[i][j].z);
   glEnd();
  }
  for(i=0;i<4;i++) {     // Draw The Vertical Lines
   glBegin(GL_LINE_STRIP);
   for(j=0;j<4;j++)
    glVertex3d(mybezier.anchors[j][i].x, mybezier.anchors[j][i].y, mybezier.anchors[j][i].z);
   glEnd();
  }
  glColor3f(1.0f,1.0f,1.0f);
  glEnable(GL_TEXTURE_2D);
 }

 return TRUE;       // Keep Going
}

   
This function contains some modified code to make your projects more compatable. It doesn't have anything to do with Bezier curves, but it does fix a problem with switching back the resolution after fullscreen mode with some video cards (including mine, a crappy old ATI Rage PRO, and a few others). I hope, you'll use this from now on so me and others with similar cards can view your cool examples GL code properly. To make these modifications make the changes in KillGLWindow(), make sure and define DMsaved, and make the one line change in CreateGLWindow() (it's marked).   
   

GLvoid KillGLWindow(GLvoid)      // Properly Kill The Window
{
 if (fullscreen)       // Are We In Fullscreen Mode?
 {
  if (!ChangeDisplaySettings(NULL,CDS_TEST)) {   // If The Shortcut Doesn't Work ( NEW )
   ChangeDisplaySettings(NULL,CDS_RESET);  // Do It Anyway (To Get The Values Out Of The Registry) ( NEW )
   ChangeDisplaySettings(&DMsaved,CDS_RESET); // Change It To The Saved Settings ( NEW )
  } else {
   ChangeDisplaySettings(NULL,CDS_RESET);  // If It Works, Go Right Ahead ( NEW )
  }

  ShowCursor(TRUE);     // Show Mouse Pointer
 }

 if (hRC)       // Do We Have A Rendering Context?
 {
  if (!wglMakeCurrent(NULL,NULL))    // Are We Able To Release The DC And RC Contexts?
  {
   MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
  }

  if (!wglDeleteContext(hRC))    // Are We Able To Delete The RC?
  {
   MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
  }
  hRC=NULL;      // Set RC To NULL
 }

 if (hDC && !ReleaseDC(hWnd,hDC))    // Are We Able To Release The DC
 {
  MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
  hDC=NULL;      // Set DC To NULL
 }

 if (hWnd && !DestroyWindow(hWnd))    // Are We Able To Destroy The Window?
 {
  MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
  hWnd=NULL;      // Set hWnd To NULL
 }

 if (!UnregisterClass("OpenGL",hInstance))   // Are We Able To Unregister Class
 {
  MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
  hInstance=NULL;      // Set hInstance To NULL
 }
}

   
Just added the EnumDisplaySettings() command here to save the old display settings. (part of the old graphics card fix).   
   

// This Code Creates Our OpenGL Window.  Parameters Are:   *
// title  - Title To Appear At The Top Of The Window  *
// width  - Width Of The GL Window Or Fullscreen Mode  *
// height  - Height Of The GL Window Or Fullscreen Mode  *
// bits   - Number Of Bits To Use For Color (8/16/24/32)  *
// fullscreenflag - Use Fullscreen Mode (TRUE) Or Windowed Mode (FALSE) */

BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
{
 GLuint  PixelFormat;     // Holds The Results After Searching For A Match
 WNDCLASS wc;      // Windows Class Structure
 DWORD  dwExStyle;     // Window Extended Style
 DWORD  dwStyle;     // Window Style
 RECT  WindowRect;     // Grabs Rectangle Upper Left / Lower Right Values
 WindowRect.left=(long)0;     // Set Left Value To 0
 WindowRect.right=(long)width;     // Set Right Value To Requested Width
 WindowRect.top=(long)0;      // Set Top Value To 0
 WindowRect.bottom=(long)height;     // Set Bottom Value To Requested Height

 fullscreen=fullscreenflag;     // Set The Global Fullscreen Flag

 hInstance  = GetModuleHandle(NULL);  // Grab An Instance For Our Window
 wc.style  = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; // Redraw On Size, And Own DC For Window
 wc.lpfnWndProc  = (WNDPROC) WndProc;   // WndProc Handles Messages
 wc.cbClsExtra  = 0;     // No Extra Window Data
 wc.cbWndExtra  = 0;     // No Extra Window Data
 wc.hInstance  = hInstance;    // Set The Instance
 wc.hIcon  = LoadIcon(NULL, IDI_WINLOGO);  // Load The Default Icon
 wc.hCursor  = LoadCursor(NULL, IDC_ARROW);  // Load The Arrow Pointer
 wc.hbrBackground = NULL;     // No Background Required For GL
 wc.lpszMenuName  = NULL;     // We Don't Want A Menu
 wc.lpszClassName = "OpenGL";    // Set The Class Name

 EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &DMsaved); // Save The Current Display State ( NEW )

 if (fullscreen)       // Attempt Fullscreen Mode?
 {
  DEVMODE dmScreenSettings;    // Device Mode
  memset(&dmScreenSettings,0,sizeof(dmScreenSettings)); // Makes Sure Memory's Cleared
  dmScreenSettings.dmSize=sizeof(dmScreenSettings); // Size Of The Devmode Structure
  dmScreenSettings.dmPelsWidth = width;  // Selected Screen Width
  dmScreenSettings.dmPelsHeight = height;  // Selected Screen Height
  dmScreenSettings.dmBitsPerPel = bits;   // Selected Bits Per Pixel
  dmScreenSettings.dmFields=DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT;

 ... Code Cut To Save Space (No Further Changes To This Function) ...

 return TRUE;       // Success
}

   
All I did here was add commands to rotate the patch, raise/lower the resolution, and toggle the control lines.   
   

int WINAPI WinMain( HINSTANCE hInstance,   // Instance
     HINSTANCE hPrevInstance,  // Previous Instance
     LPSTR  lpCmdLine, // Command Line Parameters
     int  nCmdShow) // Window Show State
{
 MSG  msg;      // Windows Message Structure
 BOOL done=FALSE;      // Bool Variable To Exit Loop

 // Ask The User Which Screen Mode They Prefer
 if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
 {
  fullscreen=FALSE;     // Windowed Mode
 }

 // Create Our OpenGL Window
 if (!CreateGLWindow("NeHe's Solid Object Tutorial",640,480,16,fullscreen))
 {
  return 0;      // Quit If Window Was Not Created
 }

 while(!done)       // Loop That Runs While done=FALSE
 {
  if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))  // Is There A Message Waiting?
  {
   if (msg.message==WM_QUIT)   // Have We Received A Quit Message?
   {
    done=TRUE;    // If So done=TRUE
   }
   else      // If Not, Deal With Window Messages
   {
    TranslateMessage(&msg);   // Translate The Message
    DispatchMessage(&msg);   // Dispatch The Message
   }
  }
  else       // If There Are No Messages
  {
   // Draw The Scene.  Watch For ESC Key And Quit Messages From DrawGLScene()
   if ((active && !DrawGLScene()) || keys[VK_ESCAPE]) // Active?  Was There A Quit Received?
   {
    done=TRUE;    // ESC or DrawGLScene Signalled A Quit
   }
   else      // Not Time To Quit, Update Screen
   {
    SwapBuffers(hDC);   // Swap Buffers (Double Buffering)
   }


   if (keys[VK_LEFT]) rotz -= 0.8f;  // Rotate Left ( NEW )
   if (keys[VK_RIGHT]) rotz += 0.8f;  // Rotate Right ( NEW )
   if (keys[VK_UP]) {    // Resolution Up ( NEW )
    divs++;
    mybezier.dlBPatch = genBezier(mybezier, divs); // Update The Patch
    keys[VK_UP] = FALSE;
   }
   if (keys[VK_DOWN] && divs > 1) {  // Resolution Down ( NEW )
    divs--;
    mybezier.dlBPatch = genBezier(mybezier, divs); // Update The Patch
    keys[VK_DOWN] = FALSE;
   }
   if (keys[VK_SPACE]) {    // SPACE Toggles showCPoints ( NEW )
    showCPoints = !showCPoints;
    keys[VK_SPACE] = FALSE;
   }


   if (keys[VK_F1])    // Is F1 Being Pressed?
   {
    keys[VK_F1]=FALSE;   // If So Make Key FALSE
    KillGLWindow();    // Kill Our Current Window
    fullscreen=!fullscreen;   // Toggle Fullscreen / Windowed Mode
    // Recreate Our OpenGL Window
    if (!CreateGLWindow("NeHe's Solid Object Tutorial",640,480,16,fullscreen))
    {
     return 0;   // Quit If Window Was Not Created
    }
   }
  }
 }

 // Shutdown
 KillGLWindow();       // Kill The Window
 return (msg.wParam);      // Exit The Program
}

   
Well, I hope this tutorial has been enlightening and you all now love Bezier curves as much as I do ;-). If you like this tutorial I may write another one on NURBS curves if anyone's interested. Please e-mail me and let me know what you thought of this tutorial.

About The Author: David Nikdel is currently 18 and a senior at Bartow Senior High School. His current projects include a research paper on curved surfaces in 3D graphics, an OpenGL based game called Blazing Sands and being lazy. His hobbies include programming, football, and paintballing. He will (hopefully) be a freshman at Georgia Tech next year.

David Nikdel

Jeff Molofee (NeHe)


--  作者:liu_ying_qi
--  发布时间:4/4/2008 11:02:00 PM

--  
我顶!!!!!!!!!!!!!!!!!!!!!!!
W 3 C h i n a ( since 2003 ) 旗 下 站 点
苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
250.000ms