在最底层,图形硬件所绘制的是点、直线和多边形(通常是三角形和四边形)。平滑的曲线或表面是通过使用大量的微小线段或多边形模拟的。但是,从数学角度而言,许多非常实用的曲线和表面可以用少许几个参数(例如控制点)来描述。保存一个表面的16个控制点要比保存1000个三角形以及这些三角形每个顶点的法线向量信息所需要的空间要少的多。另外,这1000个三角形只能对真正的表面进行近似的模拟,而这些控制点却能够准确地描述真正的表面。
求值器提供了一种方式,只有少数的控制点来指定曲线或表面上的点。然后,就可以按照任意的精度来渲染曲线或表面。此外,它还可以自动计算表面的法线向量。可以按照多种方式使用求值器返回的点,例如绘制点状表面、绘制线框表面,也可以绘制进行了完全的光照、着色甚至纹理处理的表面。
可以使用求值器描述任何角度的多项式或有理多项式样条或表面,它们几乎包括了如今所有常见的样条或样条表面,包括B-样条、NURBS(非均匀有理B-样条)表面、Bezier曲线和表面,以及Hermite样条。由于求值器只提供了对曲线或表面的底层描述,因此它们一般存在于底层的工具函数库中。程序员使用的一般是更高层次的接口,GLU的NURBS工具就是这样一种高层接口。NURBS函数封装了大量的复杂代码。NURMBS所完成的最终渲染大部分是由求值器完成的。但是有些情况下(例如,修剪曲线),NURBS函数使用平面多边形进行渲染。
前提条件
如果想使用求值器来渲染Bezier曲线和表面的一部分,需要对它们的分割粒度做出决定,在做出决定时,需要在图像质量和渲染速度之间进行权衡。
求值器
Bezier曲线是单变量的向量值函数,Bezier曲面是双变量的向量值函数。对于每个u(如果是曲面则是u和v),C()(或S())公式计算曲线(或曲面)上的一个点。为了使用求值器,首先需要定义函数C()或S(),然后启用它,并使用glEvalCoord1()或glEvalCoord2()函数代替glVertex()函数。按照这种方式,我们可以像使用其他顶点一样使用曲线或曲面上的顶点。另外,有一些函数会自动生成一系列的顶点,组成一个沿u(或u和v)方向均匀排列的网格。
-
一维求值器
样例的曲线是由位于glBegin()和glEnd()之间的display()函数绘制的。由于启用了求值器,调用glEvalCoord1f()函数就像调用glVertex()函数一样,曲线上的一个顶点的坐标对应于输入参数u。
可以一次使用多个求值器进行计算。例如,如果已经定义并启用了一个GL_MAP1_VERTEX_3和一个GL MAP1 COLOR_4求值器,就可以调用glEvalCoord1()函数同时生成一个位置和一种颜色。对于两个顶点求值器,一次只能启用一个,尽管可能同时对它们进行了定义。类似地,只有一个纹理求值器可以处于活动状态。
可以在glEvalCoord1()函数中使用任何u值,但目前最常用的是均匀分布的值。
-
二维求值器
-
使用求值器进行纹理处理
image[3*(imageHeight*i+j)] = (GLubyte) 127*(1.0+sin(ti)); image[3*(imageHeight*i+j)+1] = (GLubyte) 127*(1.0+cos(2*tj)); image[3*(imageHeight*i+j)+2] = (GLubyte) 127*(1.0+cos(ti+tj)); } }
} void init(void) {
-
glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &ctrlpoints[0][0][0]);
-
glMap2f(GL_MAP2_TEXTURE_COORD_2, 0, 1, 2, 2, 0, 1, 4, 2, &texpts[0][0][0]);
-
glEnable(GL_MAP2_TEXTURE_COORD_2);
-
glEnable(GL_MAP2_VERTEX_3);
-
glMapGrid2f(20, 0.0, 1.0, 20, 0.0, 1.0);
-
makeImage();
-
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
-
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
-
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
-
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, imageWidth, imageHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, image); glEnable(GL_TEXTURE_2D);
-
glEnable(GL_DEPTH_TEST);
-
glShadeModel (GL_FLAT); }
GLU的NURBS接口
求值器是能够直接绘制曲线和表面的唯一OpenGL图元。
-
一个简单的NURBS例子
- 如果相对NURBS表面应用光照,可以用GL_AUTO_NORMAL为参数调用glEnable()函数,自动生成法线向量。
- 使用gluNewNurbsRenderer()函数创建一个指向NURBS对象的指针。在创建自己的NURBS曲线或表面时,将会用到这个指针。
- 如果需要,可以调用gluNurbsProperty()函数选择渲染值。例如,用于渲染NURBS对象的直线或多边形的最大数量。gluNurbsProperty()函数还可以启用一种模式,使用户可以在这种模式下通过回调接口提取分格化的几何数据。
- 如果想在遇到错误时得到通知,可以调用gluNurbsCallback()函数。gluNurbsCallback()函数还可以注册用于提取分格化后的几何图形数据的函数。
- 调用gluBeginCurve()或gluBeginSurface()函数,开始绘制曲线或表面。
- 生成和渲染曲线或表面。至少需要调用1次gluNurbsCurve()或gluNurbsSurface()函数,以NURBS对象的控制点、节点序列以及多项式基函数的阶数为参数。可以多次调用这些函数,指定法线向量和纹理坐标。
- 调用gluEndCurve()或gluEndSurface()函数完成曲线或表面的绘制。
-
管理NURBS对象
与NURBS对象相关联的一组属性可以影响物体的渲染方式。这些属性包括表面如何被光栅化、显示还是返回分格化顶点以及分格化的精度。
-
创建NURBS曲线或表面
为了渲染NURBS表面,可以在一对gluBeginSurface()和gluEndSurface()函数之间调用gluNurbsSurface()函数。绘制NURBS曲线,需要在一对gluBeginCurve()和gluEndCurve()函数之间调用绘制曲线的函数。
在默认情况下,NURBS分格化对象把NURBS对象分解为几何直线和多边形,然后再对它们进行渲染。GLU1.3增加了额外的回调函数,可以不再渲染后分格化的值,而是把它们返回给应用程序。
-
修剪NURBS表面
为了用OpenGL创建经过修剪的表面,一开始执行的步骤与创建未修剪的表面相同。在调用glugluBeginSurface()和gluNurbsSurface()之后,但在调用gluEgluEndSurface()之前,可以调用gluBeginTrim()函数对表面进行修剪。
可以创建两种类型的修剪曲线:用gluPwlCurve()函数创建一条分段的线性曲线或者用gluNurbsCurve()函数创建一条NURBS曲线。
修剪曲线必须闭合并且互不相交。可以任意组合修剪曲线,只要修剪曲线能够形成环路。
参数方程表现形式
在中学的时候,我们都学习过直线的参数方程:y = kx + b;其中k表示斜率,b表示截距(即与y轴的交点坐标)。类似地,我们也可以用一个参数方程来表示一条曲线。1962年,法国工程师贝塞尔发明了贝塞尔曲线方程。关于贝塞尔曲线的详细介绍可以参考(维基贝塞尔)。这里只介绍OpenGL实现贝塞尔的函数。
OpenGl定义一条曲线时,也把它定义为一个曲线方程。我们把这条曲线的参数成为u,它的值域就是曲线的定义域。曲面则需要u和v两个参数来描述。注意,u和v参数只表示了描述曲线的参数方程的范围,它们并没有反映实际的坐标值。其坐标可以表示为:
x = f(u); y = g(u); z = h(u);
如下图:
控制点
贝塞尔曲线的形状由控制点来控制。贝塞尔曲线的控制点个数为曲线的阶。根据控制点的个数,贝塞尔曲线又分为二次贝塞尔曲线,三次贝塞尔曲线,高阶贝塞尔曲线。
线性曲线
线性贝塞尔曲线演示动画,t in [0,1]
二次方曲线
为建构二次贝塞尔曲线,可以中介点Q0和Q1作为由0至1的t:
-
由P0至P1的连续点Q0,描述一条线性贝塞尔曲线。
-
由P1至P2的连续点Q1,描述一条线性贝塞尔曲线。
-
由Q0至Q1的连续点B(t),描述一条二次贝塞尔曲线。
二次贝塞尔曲线的结构
二次贝塞尔曲线演示动画,t in [0,1]
三次方曲线
为建构高阶曲线,便需要相应更多的中介点。对于三次曲线,可由线性贝塞尔曲线描述的中介点Q0、Q1、Q2,和由二次曲线描述的点R0、R1所建构:
三次贝塞尔曲线的结构
三次贝塞尔曲线演示动画,t in [0,1]
连续性
两段曲线是否相连接,代表这两段曲线是否连续的。曲线的连续性分为4种,无连续,点连续,正切连续,曲率连续。下图分别表示了这几种情况:
其中曲率连续的曲线过渡的更平滑。我们可以通过参数来设置曲线的连续性。
求值器
OpenGL提供了一些函数来绘制贝塞尔曲线和曲面。我们只需要提供控制点和u,v作为参数,然后调用求值函数来绘制曲线。
2D曲线的例子:
//控制点 GLint numOfPoints = 4; static GLfloat controlPoints[4][3] = {{-4.0f, 0.0f, 0.0f},
{-6.0f, 4.0f, 0.0f},
{6.0f, -4.0f, 0.0f},
{4.0f, 0.0f, 0.0f}}; void SetupRC()
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glColor3f(1.0f, 0.0f, 1.0f);
}
//画控制点
void DrawPoints()
{
glPointSize(2.5f);
glBegin(GL_POINTS); for (int i = 0; i < numOfPoints; ++i)
{
glVertex3fv(controlPoints[i]);
}
glEnd();
}
void ChangeSize(GLsizei w, GLsizei h)
{
if (h == 0)
{
h = 1;
}
glViewport(0, 0, w, h);
//使用正交投影
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(-10.0f, 10.0f, -10.0f, 10.0f);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT);
//设置贝塞尔曲线,这个函数其实只需要调用一次,可以放在SetupRC中设置
glMap1f(GL_MAP1_VERTEX_3, //生成的数据类型
0.0f, //u值的下界
100.0f, //u值的上界
3, //顶点在数据中的间隔,x,y,z所以间隔是3
numOfPoints, //u方向上的阶,即控制点的个数
&controlPoints[0][0] //指向控制点数据的指针 );
//必须在绘制顶点之前开启
glEnable(GL_MAP1_VERTEX_3);
//使用画线的方式来连接点
glBegin(GL_LINE_STRIP);
for (int i = 0; i <= 100; i++)
{
glEvalCoord1f((GLfloat)i);
}
glEnd();
DrawPoints();
glutSwapBuffers();
}
在RenderScene函数中调用glMap1f来为曲线创建映射。第一个参数为GL_MAP1_VERTEX3,设置求值器产生顶点为三元组(x,y,z).还可以设置为产生纹理坐标和颜色信息。参考glMap1.后面的两个参数设定了u的取值范围[0,100],第四个参数指定了顶点在数组中的间隔,由于顶点是由3个浮点数组成,所以间隔是3.第五个参数指定了控制点的个数,最后一个参数是控制点数组。然后我们需要启用求值器,调用如下:
glEnable(GL_MAP1_VERTEX3); //启用求值器
glEvalCoord1f函数,接受一个参数为曲线的参数值。调用这个函数会通过求值函数求出顶点坐标值,然后内部调用了glVertex。这里使用连线的方式来连接这些顶点:
glBegin(GL_LINE_STRIP);
for(i = 0; I <= 100; i++)
{
glEvalCoord1f((GLfloat)i); //启用之后,调用此生成一个位置和一种颜色
}
glEnd();
计算曲线
OpenGl还提供了更简单的方式来完成上面的任务。我们可以通过glMapGrid函数来设置一个网格,来告诉OpenGL在u的值域的范围内创建一个包含各个点的空间对称的网格。然后,我们调用glEvalMesh,使用指定的图元(GL_LINE或GL_POINTS)来链接各个点。
我们用下面的两个函数调用
glMapGrid1f(100, 0.0f, 100.0f);
glEvalMesh1(GL_LINE, 0, 100);
可以替换下面的代码
glBegin(GL_LINE_STRIP);
for (int i = 0; i <= 100; i++)
{
glEvalCoord1f((GLfloat)i);
}
glEnd();
使用这种方式更为紧凑。
3D表面
创建一个贝塞尔曲面与创建一个贝塞尔曲线类似。除了给出u的定义域之外,还要给出v的定义域。下面的例子是创建一个贝塞尔曲面。与之前不同的是,我们沿着v的定义域定义了3组控制点。为了保持曲面的简单,这几组控制点只是z值不同。用这种方式画的曲面,看起来像是曲线沿z轴的扩展。
//控制点 GLint nNumPoints = 3;
GLfloat ctrlPoints[3][3][3]= {{{ -4.0f, 0.0f, 4.0f},
{ -2.0f, 4.0f, 4.0f},
{ 4.0f, 0.0f, 4.0f }},
{{ -4.0f, 0.0f, 0.0f},
{ -2.0f, 4.0f, 0.0f},
{ 4.0f, 0.0f, 0.0f }},
{{ -4.0f, 0.0f, -4.0f},
{ -2.0f, 4.0f, -4.0f},
{ 4.0f, 0.0f, -4.0f }}}; //画控制点 void DrawPoints(void)
{ int i,j;
glColor3f(1.0f, 0.0f, 0.0f); //把点放大一点,看得更清楚 glPointSize(5.0f);
glBegin(GL_POINTS);
for(i = 0; i < nNumPoints; i++)
for(j = 0; j < 3; j++)
glVertex3fv(ctrlPoints[i][j]);
glEnd();
}
void RenderScene(void)
{
// Clear the window with current clearing color
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 保存模型视图矩阵
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
//旋转一定的角度方便观察
glRotatef(45.0f, 0.0f, 1.0f, 0.0f);
glRotatef(60.0f, 1.0f, 0.0f, 0.0f);
glColor3f(0.0f, 0.0f, 1.0f); //设置映射方式,只需要设置一次可以在SetupRC中调用。
glMap2f(GL_MAP2_VERTEX_3, //生成的数据类型
0.0f, // u的下界
10.0f, //u的上界
3, //数据中点的间隔
3, //u方向上的阶
0.0f, //v的下界
10.0f, //v的上界
9, // 控制点之间的间隔
3, // v方向上的阶
&ctrlPoints[0][0][0]); //控制点数组
//启用求值器
glEnable(GL_MAP2_VERTEX_3);
//从0到10映射一个包含10个点的网格
glMapGrid2f(10,0.0f,10.0f,10,0.0f,10.0f);
// 计算网格
glEvalMesh2(GL_LINE,0,10,0,10);
//画控制点
DrawPoints();
glPopMatrix();
glutSwapBuffers();
}
在这里我们用glMap2f替换了之前的glMap1f, 这个函数指定了u和v两个域上的点。除了指定u的上界和下界之外,还要指定v的上界和下界。v定义域内点的距离是9,因为这里使用了3维数组,包含了3个u值,每个u值又包含了3个点,3x3=9。然后指定v方向上的阶,即每个u分支上v方向有多少个点。最后一个参数是指向控制点的指针。
然后我们设置求值器.
//启用求值器
glEnable(GL_MAP2_VERTEX_3);
//从0到10映射一个包含10个点的网格
glMapGrid2f(10,0.0f,10.0f,10,0.0f,10.0f);
计算网格网格表面,用线的方式表示。
// 计算网格
glEvalMesh2(GL_LINE,0,10,0,10);
光照和法线
求值器还可以帮我们生成表面的法线,只需简单的修改一些代码:
把glEvalMesh2(GL_LINE, 0, 10, 0, 10);替换为glEvalMesh2(GL_FILL, 0, 10, 0, 10);然后在初始化时 SetupRC中调用glEnable(GL_AUTO_NORMAL);就可以得到一个收到光照的曲面了。