现在的
光栅化
图形渲染技术的核心是绘制大量三角形来组成
3D模型
,而Tessellation技术就是利用GPU
硬件加速
,将现有3D模型的三角形拆分得更细小、更细致,也就是大大增加三角形数量,使得渲染对象的表面和边缘更平滑、更精细。直到DX11时代,GPU自身的性能有了长足的进步,硬件上真正具备了
细分曲面
的实力,再加上微软重新改写API渲染流程,专为Tessellation开辟了新的
着色器
,这才让Tessellation技术得以重见天日。
这里讨论的是OGL 软件的Tessellation技术,包括:
1.把凹形的填充多边形分格化为多个凸多边形,以便用标准OGL进行渲染。
2.对曲面进行细分。
一、多边形分格化
1.gluNewTess()创建分格化对象。
创建分格化对象,具有一些相关数据,例如顶点,边和回调函数。只是一些指针,数据都在外部,多个任务可以共享该对象。多线程多进程任务中创建多个有更好的效率。应该是一个存在GPU服务器端的对象,因为分格化算法是由OGL内部来实现的。
2.注册回调函数
GLU_TESS_BEGIN 在glBegin时候会调用, GLU_TESS_VERTEX 在glVertex时候会调用,GLU_TESS_END 在glEnd时候会调用,GLU_TESS_COMBINE 在分格化算法检查到边缘相交时候创建新的顶点时调用, 当分格化算法决定把两个非常靠近的顶点合并为一个时也调用。
3.调用gluTessProperty函数制定网格化的属性,如环绕规则,生成多边形是以填充多边形还是以轮廓的形式绘制,以及合并顶点所需的距离阈值。
void gluTessProperty(GLUtesselator *tess, GLenum which, GLdouble value );
tess分格化对象,which为:
/* TessProperty */
#define GLU_TESS_WINDING_RULE 100140 // 环绕规则,指定那些在外部那些在内部
#define GLU_TESS_BOUNDARY_ONLY 100141 //是否用边框着色模式,默认是GL_FALSE填充
#define GLU_TESS_TOLERANCE 100142 //设置GLU_TESS_COMBINE时合并顶点的最小距离
对于凹多边形的GLU_TESS_WINDING_RULE ,分格化对象的环绕数是:
每个点的环绕数就是环绕这个点的所有轮廓线的代数和(外部包含的轮廓线或自己所在的轮廓线)。
轮廓线是逆时针环绕数+1,顺时针环绕数-1.
对于同一个区域的所有点,他们的环绕数是一样的。
环绕规则指定,可以对指定的环绕数的轮廓线进行绘制。
/* TessWinding */
GLdouble currentWinding = GLU_TESS_WINDING_ODD;
if (currentWinding == GLU_TESS_WINDING_ODD)
currentWinding = GLU_TESS_WINDING_NONZERO; // 100131内部填充非0
else if (currentWinding == GLU_TESS_WINDING_NONZERO)
currentWinding = GLU_TESS_WINDING_POSITIVE; // 100132内部填充正数
else if (currentWinding == GLU_TESS_WINDING_POSITIVE)
currentWinding = GLU_TESS_WINDING_NEGATIVE; // 100133内部填充负数
else if (currentWinding == GLU_TESS_WINDING_NEGATIVE)
currentWinding = GLU_TESS_WINDING_ABS_GEQ_TWO; // 100134内部填充绝对值大于或等于2的区域
else if (currentWinding == GLU_TESS_WINDING_ABS_GEQ_TWO)
currentWinding = GLU_TESS_WINDING_ODD; // 100130内部填充奇数区域,默认设置
gluTessProperty(tobj, GLU_TESS_WINDING_RULE,
currentWinding);
其它分格化函数:
(1)获取分格化属性的当前值:
void APIENTRY gluGetTessProperty(
GLUtesselator *tess,
GLenum which,
GLdouble *value );
(2)分格化对象被用于生成轮廓线而不是填充多边形,那么gluTessNormal可用来指定生成的轮廓线的环绕方向(生成的三角形的方向是根据法线方向的来逆时针顶点顺序生成)。
The
gluTessNormal
function specifies a normal for a polygon.
void APIENTRY gluTessNormal(
GLUtesselator *tess,
GLdouble x,
GLdouble y,
GLdouble z );
4.通过指定一个或多个闭合多边形的轮廊线来渲染经过分格化的多边形(可以放到显示列表中),此时应用分格化算法。如果是静态的可以将经过分格化的多边形放到显示列表中提高效率。
指定一个或多个封闭多边形轮廓,用于创建分割后的简单多边形集合。
// 开始或结束对分格化多边形的指定
void APIENTRY gluTessBeginPolygon(
GLUtesselator *tess,
void *polygon_data ); // polygon_data用于回调函数为GLU_TESS_*_DATA类型中
// 开始或结束对一条轮廓线的指定,轮廓线需要0次或多次调用gluTessVertex指定。
void APIENTRY gluTessBeginContour(
GLUtesselator *tess );
// 一条有意义的轮廓线,至少要3个顶点来指定,*data顶点指针信息会发送到GLU_TESS_VERTEX或GLU_TESS_VERTEX_DATA指定的回调函数中, 所以一般 coords和data的值一样,但data指针可以提供给回调函数更多的信息。
void APIENTRY gluTessVertex(
GLUtesselator *tess,
GLdouble coords[3],
void *data );
void APIENTRY gluTessEndContour(
GLUtesselator *tess );
// 该函数被调用时候,OGL就调用分格化算法,并生成和渲染经过分格化的多边形,使用分格化对象
// 上的回调函数和分格化属性
void APIENTRY gluTessEndPolygon(
GLUtesselator *tess );
实例:
glNewList(list, GL_COMPILE);
gluTessBeginPolygon(tobj, NULL);
gluTessBeginContour(tobj);
for (i = 0; i < 4; i++)
gluTessVertex(tobj, rects[i], rects[i]);
gluTessEndContour(tobj);
gluTessBeginContour(tobj);
for (i = 4; i < 8; i++)
gluTessVertex(tobj, rects[i], rects[i]);
gluTessEndContour(tobj);
gluTessBeginContour(tobj);
for (i = 8; i < 12; i++)
gluTessVertex(tobj, rects[i], rects[i]);
gluTessEndContour(tobj);
gluTessEndPolygon(tobj);
glEndList();
5.需要对其它物体进行分格化,可以复用原来的分格化对象,如果不需要了用gluDeleteTess()删除,并删除与它相关联的所有内存(存在GPU端)。(一般情况下使用一个网格化对象即可,多处理器,多线程的情况可使用多个网格化对象,以提高效率)。
实例:
#include <GL/glut.h>
#include <stdlib.h>
#include <stdio.h>
#ifndef CALLBACK
#define CALLBACK
#endif
GLuint startList;
void display (void) {
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1.0, 1.0, 1.0);
glCallList(startList);
glCallList(startList + 1);
glFlush();
}
// CALLBACK宏是为了linux和windows兼容, glBegin时候会调用
void CALLBACK beginCallback(GLenum which)
{
glBegin(which);
}
void CALLBACK errorCallback(GLenum errorCode)
{
const GLubyte *estring;
estring = gluErrorString(errorCode);
fprintf(stderr, "Tessellation Error: %s\n", estring);
exit(0);
}
// glEnd时候会调用
void CALLBACK endCallback(void)
{
glEnd();
}
// glVertex时候会调用
void CALLBACK vertexCallback(GLvoid *vertex)
{
const GLdouble *pointer;
pointer = (GLdouble *) vertex;
glVertex3dv(vertex);
glColor3dv(pointer+3);
}
/* combineCallback is used to create a new vertex when edges
* intersect. coordinate location is trivial to calculate,
* but weight[4] may be used to average color, normal, or texture
* coordinate data. In this program, color is weighted.
*/
// 分格化算法检查到边缘相交时候创建新的顶点时调用, 当分格化算法决定把两个非常靠近的顶点合并为一个时也调用
void CALLBACK combineCallback(GLdouble coords[3],
GLdouble *vertex_data[4],// vertex_data为附近的顶点,这里在star时候才使用
GLfloat weight[4], GLdouble **dataOut )
{
GLdouble *vertex;
int i;
vertex = (GLdouble *) malloc(6 * sizeof(GLdouble));
vertex[0] = coords[0];
vertex[1] = coords[1];
vertex[2] = coords[2];
for (i = 3; i < 6; i++)
vertex[i] = weight[0] * vertex_data[0][i]
+ weight[1] * vertex_data[1][i]
+ weight[2] * vertex_data[2][i]
+ weight[3] * vertex_data[3][i];
*dataOut = vertex;
}
void init (void)
{
GLUtesselator *tobj;
GLdouble rect[4][3] = {50.0, 50.0, 0.0,
200.0, 50.0, 0.0,
200.0, 200.0, 0.0,
50.0, 200.0, 0.0};
GLdouble tri[3][3] = {75.0, 75.0, 0.0,
125.0, 175.0, 0.0,
175.0, 75.0, 0.0};
GLdouble star[5][6] = {250.0, 50.0, 0.0, 1.0, 0.0, 1.0,
325.0, 200.0, 0.0, 1.0, 1.0, 0.0,
400.0, 50.0, 0.0, 0.0, 1.0, 1.0,
250.0, 150.0, 0.0, 1.0, 0.0, 0.0,
400.0, 150.0, 0.0, 0.0, 1.0, 0.0};
glClearColor(0.0, 0.0, 0.0, 0.0);
startList = glGenLists(2);
// 创建分格化对象,具有一些相关数据,例如顶点,边和回调函数。只是一些指针,数据都在外部,多个任务可以共享该对象。
// 设置分格化回调函数。
tobj = gluNewTess();
gluTessCallback(tobj, GLU_TESS_VERTEX,
glVertex3dv);
gluTessCallback(tobj, GLU_TESS_BEGIN,
beginCallback);
gluTessCallback(tobj, GLU_TESS_END,
endCallback);
gluTessCallback(tobj, GLU_TESS_ERROR,
errorCallback);
/* rectangle with triangular hole inside */
// 使用显示列表
glNewList(startList, GL_COMPILE);
glShadeModel(GL_FLAT);
// 用Tess形式的函数,设置调动分格化对象指针,Begin, Vertex,End
gluTessBeginPolygon(tobj, NULL);
gluTessBeginContour(tobj);
gluTessVertex(tobj, rect[0], rect[0]);// 每个Vertex的处理会调用GLU_TESS_VERTEX
gluTessVertex(tobj, rect[1], rect[1]);
gluTessVertex(tobj, rect[2], rect[2]);
gluTessVertex(tobj, rect[3], rect[3]);
gluTessEndContour(tobj);
gluTessBeginContour(tobj);
gluTessVertex(tobj, tri[0], tri[0]);
gluTessVertex(tobj, tri[1], tri[1]);
gluTessVertex(tobj, tri[2], tri[2]);
gluTessEndContour(tobj);
gluTessEndPolygon(tobj);
glEndList();
// 设置分格化回调函数
gluTessCallback(tobj, GLU_TESS_VERTEX,
vertexCallback);
gluTessCallback(tobj, GLU_TESS_BEGIN,
beginCallback);
gluTessCallback(tobj, GLU_TESS_END,
endCallback);
gluTessCallback(tobj, GLU_TESS_ERROR,
errorCallback);
gluTessCallback(tobj, GLU_TESS_COMBINE,
combineCallback);
/* smooth shaded, self-intersecting star */
glNewList(startList + 1, GL_COMPILE);
glShadeModel(GL_SMOOTH);
gluTessProperty(tobj, GLU_TESS_WINDING_RULE,
GLU_TESS_WINDING_POSITIVE);
gluTessBeginPolygon(tobj, NULL);
gluTessBeginContour(tobj);
// 每个Vertex会调用GLU_TESS_VERTEX处理,且设置了GLU_TESS_COMBINE会用分格化算法,决定是否拆分或合并
gluTessVertex(tobj, star[0], star[0]);
gluTessVertex(tobj, star[1], star[1]);
gluTessVertex(tobj, star[2], star[2]);
gluTessVertex(tobj, star[3], star[3]);
gluTessVertex(tobj, star[4], star[4]);
gluTessEndContour(tobj);
gluTessEndPolygon(tobj);
glEndList();
gluDeleteTess(tobj);
}
void reshape (int w, int h)
{
glViewport(0, 0, (GLsizei) w, (GLsizei) h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0.0, (GLdouble) w, 0.0, (GLdouble) h);
}
void keyboard(unsigned char key, int x, int y)
{
switch (key) {
case 27:
exit(0);
break;
}
}
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
glutInitWindowSize(500, 500);
glutCreateWindow(argv[0]);
init();
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutKeyboardFunc(keyboard);
glutMainLoop();
return 0;
}
二、二次方程表面分格化
二次方程表面是通过a1X^2 + a2Y^2 + a3Z^2 + a4XY + a5XZ + a6YZ + a7X + a8Y + a9Z + a10 = 0进行定义的。
对二次方程表面进行分格化,需要使用二次方程对象,该对象类似分格化对象,拥有一些相关数据,例如顶点,边和回调函数。创建和渲染二次方程表面,需要下面的步骤。
1.使用gluNewQuadirc函数,创建一个二次方程对象,多处理器或多线程中可以生成多个该二次方程对象。
2.指定这个二次方程对象的属性(除非对默认值感到满意).
1)使用gluQuadricOrientation函数控制环绕方向,其实是控制法线的方向,区分内部区域或外部区域。
2)使用gluQuadricDrawStyle确定着色模型。
3)对于使用了光照的二次方程表面,可以使用gluQuadricNormals函数为每个顶点或每个面指定一条法向量。默认情况下不会生成任何法线。
4)对于进行了纹理贴图的二次方程表面,可以使用glQuadricTexture函数生成纹理坐标。
3.使用gluQuadricCallback(qobj, GLU_ERROR, errorCallback)注册一个错误回调函数。
4.根据需要调用相应的二次方程表面渲染函数
gluSphere,gluCylinder,gluDisk,gluPartialDisk,可以把二次方程对象封装到显示列表中。
5.用glDeleteQuadric(),释放该二次方程对象
若需要可以复用该对象,若多处理器或多线程中可以生成多个该二次方程对象。
实例代码
#include <GL/glut.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef CALLBACK
#define CALLBACK
#endif
GLuint startList;
void CALLBACK errorCallback(GLenum errorCode)
{
const GLubyte *estring;
estring = gluErrorString(errorCode);
fprintf(stderr, "Quadric Error: %s\n", estring);
exit(0);
}
void init(void)
{
GLUquadricObj *qobj;
GLfloat mat_ambient[] = { 0.5, 0.5, 0.5, 1.0 };
GLfloat mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat mat_shininess[] = { 50.0 };
GLfloat light_position[] = { 1.0, 1.0, 1.0, 0.0 };
GLfloat model_ambient[] = { 0.5, 0.5, 0.5, 1.0 };
glClearColor(0.0, 0.0, 0.0, 0.0);
glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);
glLightfv(GL_LIGHT0, GL_POSITION, light_position);
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, model_ambient);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_DEPTH_TEST);
/* Create 4 display lists, each with a different quadric object.
* Different drawing styles and surface normal specifications
* are demonstrated.
*/
startList = glGenLists(4);
qobj = gluNewQuadric();
gluQuadricCallback(qobj, GLU_ERROR,
errorCallback);
gluQuadricDrawStyle(qobj, GLU_FILL); /* smooth shaded */
gluQuadricNormals(qobj, GLU_SMOOTH);
glNewList(startList, GL_COMPILE);
gluSphere(qobj, 0.75, 15, 10);
glEndList();
gluQuadricDrawStyle(qobj, GLU_FILL); /* flat shaded */
gluQuadricNormals(qobj, GLU_FLAT);
glNewList(startList+1, GL_COMPILE);
gluCylinder(qobj, 0.5, 0.3, 1.0, 15, 5);
glEndList();
gluQuadricDrawStyle(qobj, GLU_LINE); /* all polygons wireframe */
gluQuadricNormals(qobj, GLU_NONE);
glNewList(startList+2, GL_COMPILE);
gluDisk(qobj, 0.25, 1.0, 20, 4);
glEndList();
gluQuadricDrawStyle(qobj, GLU_SILHOUETTE); /* boundary only */
gluQuadricNormals(qobj, GLU_NONE);
glNewList(startList+3, GL_COMPILE);
gluPartialDisk(qobj, 0.0, 1.0, 20, 4, 0.0, 225.0);
glEndList();
}
void display(void)
{
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPushMatrix();
glEnable(GL_LIGHTING);
glShadeModel (GL_SMOOTH);
glTranslatef(-1.0, -1.0, 0.0);
glCallList(startList);
glShadeModel (GL_FLAT);
glTranslatef(0.0, 2.0, 0.0);
glPushMatrix();
glRotatef(300.0, 1.0, 0.0, 0.0);
glCallList(startList+1);
glPopMatrix();
glDisable(GL_LIGHTING);
glColor3f(0.0, 1.0, 1.0);
glTranslatef(2.0, -2.0, 0.0);
glCallList(startList+2);
glColor3f(1.0, 1.0, 0.0);
glTranslatef(0.0, 2.0, 0.0);
glCallList(startList+3);
glPopMatrix();
glFlush();
}
void reshape (int w, int h)
{
glViewport(0, 0, (GLsizei) w, (GLsizei) h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
if (w <= h)
glOrtho(-2.5, 2.5, -2.5*(GLfloat)h/(GLfloat)w,
2.5*(GLfloat)h/(GLfloat)w, -10.0, 10.0);
else
glOrtho(-2.5*(GLfloat)w/(GLfloat)h,
2.5*(GLfloat)w/(GLfloat)h, -2.5, 2.5, -10.0, 10.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void keyboard(unsigned char key, int x, int y)
{
switch (key) {
case 27:
exit(0);
break;
}
}
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB | GLUT_DEPTH);
glutInitWindowSize(500, 500);
glutInitWindowPosition(100, 100);
glutCreateWindow(argv[0]);
init();
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutKeyboardFunc(keyboard);
glutMainLoop();
return 0;
}