OpenGL学习笔记——求值器和NURBS
在最底层,图形硬件所绘制的是点、直线和多边形(通常是三角形和四边形)。平滑的曲线或表面是通过使用大量的微小线段或多边形模拟的。但是,从数学角度而言,许多非常实用的曲线和表面可以用少许几个参数(例如控制点)来描述。保存一个表面的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)方向均匀排列的网格。
-
一维求值器
GLfloat ctrlpoints[4][3] = { { -4.0, -4.0, 0.0}, { -2.0, 4.0, 0.0}, {2.0, -4.0, 0.0}, {4.0, 4.0, 0.0}}; void init(void) { glClearColor(0.0, 0.0, 0.0, 0.0); glShadeModel(GL_FLAT); glMap1f(GL_MAP1_VERTEX_3, 0.0, 1.0, 3, 4, &ctrlpoints[0][0]); glEnable(GL_MAP1_VERTEX_3); } void display(void) { int i; glClear(GL_COLOR_BUFFER_BIT); glColor3f(1.0, 1.0, 1.0); glBegin(GL_LINE_STRIP); for (i = 0; i <= 30; i++) { glEvalCoord1d((GLfloat)i/30.0); } glEnd(); glPointSize(5.0); glColor3f(1.0, 1.0, 0.0); glBegin(GL_POINTS); for (i = 0; i < 4; i++) { glVertex3fv(&ctrlpoints[i][0]); } glEnd(); glFlush(); }
样例的曲线是由位于glBegin()和glEnd()之间的display()函数绘制的。由于启用了求值器,调用glEvalCoord1f()函数就像调用glVertex()函数一样,曲线上的一个顶点的坐标对应于输入参数u。
可以一次使用多个求值器进行计算。例如,如果已经定义并启用了一个GL_MAP1_VERTEX_3和一个GL MAP1 COLOR_4求值器,就可以调用glEvalCoord1()函数同时生成一个位置和一种颜色。对于两个顶点求值器,一次只能启用一个,尽管可能同时对它们进行了定义。类似地,只有一个纹理求值器可以处于活动状态。
可以在glEvalCoord1()函数中使用任何u值,但目前最常用的是均匀分布的值。
-
二维求值器
GLfloat ctrlpoints[4][4][3] = { {{-1.5, -1.5, 4.0}, {-0.5, -1.5, 2.0}, {0.5, -1.5, -1.0}, {1.5, -1.5, 2.0}}, {{-1.5, -0.5, 1.0}, {-0.5, -0.5, 3.0}, {0.5, -0.5, 0.0}, {1.5, -0.5, -1.0}}, {{-1.5, 0.5, 4.0}, {-0.5, 0.5, 0.0}, {0.5, 0.5, 3.0}, {1.5, 0.5, 4.0}}, {{-1.5, 1.5, -2.0}, {-0.5, 1.5, -2.0}, {0.5, 1.5, 0.0}, {1.5, 1.5, -1.0}} }; void display(void) { int i, j; glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glColor3f(1.0, 1.0, 1.0); glPushMatrix (); glRotatef(85.0, 1.0, 1.0, 1.0); for (j = 0; j <= 8; j++) { glBegin(GL_LINE_STRIP); for (i = 0; i <= 30; i++) glEvalCoord2f((GLfloat)i/30.0, (GLfloat)j/8.0); glEnd(); glBegin(GL_LINE_STRIP); for (i = 0; i <= 30; i++) glEvalCoord2f((GLfloat)j/8.0, (GLfloat)i/30.0); glEnd(); } glPopMatrix(); glFlush(); } void init(void) { glClearColor (0.0, 0.0, 0.0, 0.0); glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &ctrlpoints[0][0][0]); glEnable(GL_MAP2_VERTEX_3); glMapGrid2f(20, 0.0, 1.0, 20, 0.0, 1.0); glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); } GLfloat ctrlpoints[4][4][3] = { { {-1.5, -1.5, 4.0}, {-0.5, -1.5, 2.0}, {0.5, -1.5, -1.0}, {1.5, -1.5, 2.0}}, { {-1.5, -0.5, 1.0}, {-0.5, -0.5, 3.0}, {0.5, -0.5, 0.0}, {1.5, -0.5, -1.0}}, { {-1.5, 0.5, 4.0}, {-0.5, 0.5, 0.0}, {0.5, 0.5, 3.0}, {1.5, 0.5, 4.0}}, { {-1.5, 1.5, -2.0}, {-0.5, 1.5, -2.0}, {0.5, 1.5, 0.0}, {1.5, 1.5, -1.0}} }; void initlights(void) { GLfloat ambient[] = {0.2, 0.2, 0.2, 1.0}; GLfloat position[] = {0.0, 0.0, 2.0, 1.0}; GLfloat mat_diffuse[] = {0.6, 0.6, 0.6, 1.0}; GLfloat mat_specular[] = {1.0, 1.0, 1.0, 1.0}; GLfloat mat_shininess[] = {50.0}; glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glLightfv(GL_LIGHT0, GL_AMBIENT, ambient); glLightfv(GL_LIGHT0, GL_POSITION, position); glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse); glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular); glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess); } void display(void) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glPushMatrix(); glRotatef(85.0, 1.0, 1.0, 1.0); glEvalMesh2(GL_FILL, 0, 20, 0, 20); glPopMatrix(); glFlush(); } void init(void) { glClearColor(0.0, 0.0, 0.0, 0.0); glEnable(GL_DEPTH_TEST); glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &ctrlpoints[0][0][0]); glEnable(GL_MAP2_VERTEX_3); glEnable(GL_AUTO_NORMAL); glMapGrid2f(20, 0.0, 1.0, 20, 0.0, 1.0); initlights(); }
-
使用求值器进行纹理处理
GLfloat ctrlpoints[4][4][3] = { {{ -1.5, -1.5, 4.0}, { -0.5, -1.5, 2.0}, {0.5, -1.5, -1.0}, {1.5, -1.5, 2.0}}, {{ -1.5, -0.5, 1.0}, { -0.5, -0.5, 3.0}, {0.5, -0.5, 0.0}, {1.5, -0.5, -1.0}}, {{ -1.5, 0.5, 4.0}, { -0.5, 0.5, 0.0}, {0.5, 0.5, 3.0}, {1.5, 0.5, 4.0}}, {{ -1.5, 1.5, -2.0}, { -0.5, 1.5, -2.0}, {0.5, 1.5, 0.0}, {1.5, 1.5, -1.0}} }; GLfloat texpts[2][2][2] = {{{0.0, 0.0}, {0.0, 1.0}},{{1.0, 0.0}, {1.0, 1.0}}}; void display(void) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glColor3f(1.0, 1.0, 1.0); glEvalMesh2(GL_FILL, 0, 20, 0, 20); glFlush(); } #define imageWidth 64 #define imageHeight 64 GLubyte image[3*imageWidth*imageHeight]; void makeImage(void) { int i, j; float ti, tj; for (i = 0; i < imageWidth; i++) { ti = 2.0*3.14159265*i/imageWidth; for (j = 0; j < imageHeight; j++) { tj = 2.0*3.14159265*j/imageHeight;
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曲线。
修剪曲线必须闭合并且互不相交。可以任意组合修剪曲线,只要修剪曲线能够形成环路。