一个入门的绘图程序通常长这样:
int main(int argc, char** argv)
{
glutInit(&argc, argv);//对GLUT进行初始化,这个函数必须在其它的GLUT使用之前调用一次
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE); //设置显示方式
glutInitWindowPosition(100, 100); //设置窗口位置
glutInitWindowSize(400, 400);//窗口大小
glutCreateWindow("第一个OpenGL程序"); //根据前面设置的信息创建窗口。参数将被作为窗口的标题。
glutDisplayFunc(&myDisplay); //当需要画图时,请调用myDisplay函数
glutMainLoop();
return 0;
}
一个进阶的绘图程序如下:
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);//使用双缓存模式和深度缓存
glutInitWindowSize(800, 800);
glutInitWindowPosition(0, 0);
glutCreateWindow("2D Bezier曲线");
myInit();
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutKeyboardFunc(keyboard);
glutIdleFunc(animate);//设置空闲时调用的函数
glutMainLoop();
return 0;
}
glutInitWindowSize(800, 800);
glutInitWindowPosition(0, 0);
glutCreateWindow("2D Bezier曲线");
这三个函数很直观了很容易理解了。
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);//使用双缓存模式和深度缓存
这个函数是告诉系统使用双缓存模式(GLUT_DOUBLE ),RGB颜色模型(GLUT_RGB ),还有深度测试(GLUT_DEPTH)。有些属性在真正使用之前需要激活,比如深度测试需要在myInit()函数中用glEnable(GL DEPTH_TEST)来激活。
myInit();
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutKeyboardFunc(keyboard);
glutIdleFunc(animate);//设置空闲时调用的函数
glutMainLoop();
其中,display,reshape,keyboard,animate都是我们需要定义的函数。
下面解释一下GLUT如何工作的:
GLUT完全通过事件来操作。对程序需要处理的每个事件,都需要在main()函数中定义相应的回调函数。回调函数是当相关事件发生时,系统事件处理程序调用的函数。主事件循环启动后,改变窗口(Reshape)事件生成窗口,显示(display)事件调用自身的回调函数在窗口中绘出初始图像。
如果其他事件也定义了相应的回调函数,则日当事件发生时,启动相应回调函数。回调函数允许用户拖动窗口或改变窗口大小,当用户对窗口进行操作时被调用。
空闲(idle)回调函数在系统空闲时间(当系统不生成图像或响应其他事件时)重新计算图像,并在终端上显示改变的图像。
这里reshape回调函数设置系统的窗口参数,包括大小、形状、窗口位置,并定义视图的投影方法。进人主事件循环及窗口事件(比如调整大小或拖拽)发生时,调用该函数。当reshape结束时,调用redisplay函数,在redisplay中调用display回调函数,用于设置视图并定义场景几何模型。这些操作完成后,OpenGL操作结束,图形系统转到计算机,查看是否有其他图形相关事件。如果有其他图形相关事件,则程序需要定义回调函数来进行管理。如果没有,则产生idle事件,调用idle回调函数。这时可能改变一些几何参数,然后又一次调用redisplay函数。
myinit()的任务是建立程序运行环境。这里计算定义几何模型数组的值,定义特定颜色,或其他一次性操作。最后,该函数还设置初始投影操作类型。
animate()的任务是处理空闲(idle)事件—无任何事件发生的事件。这个函数定义了无用户交互时程序所作的工作,也是程序实现动画的一种方法。
若无其他事件发生,则程序的执行顺序如图0-8所示。main()函数调用事件处理函数glutMainLoop()。glutMainLoop()永远不会终止,等待事件进人系统事件队列,然后调用响应的事件处理回调函数。由于此时窗口状态尚未设置,因此,由系统设置display事件。当glutMainLoop开始启动时display函数就立即被调回。若无其他事件行为,则程序将不断调用idle()函数,使图像随时间不断改变,也就是产生动画效果。
在模型空间中的连续直线用屏幕空间中的离散像素集合来表达。由于像素相对是比较粗粒度的,表示一条直线时直线被分割成无数个像素点,也就是直线被表示成很多的像素的集合,斜的直线就会留下锯齿状的边界,出现所谓的走样。例子。通常,通过采样获得数值时会产生走样。
(选择像素的方法是有或者无:通过计算出一个像素在几何体内部,这时用几何体定义的颜色表示;或者在几何体外部,这时保留它原来的颜色不变。由于屏幕空间相对是比较粗粒度的,这种有或者无的方法会在几何体和背景中间的空间留下锯齿状的边界。)
有很多技术可减少走样带来的负面效果,所有这样的技术称为反走样。它们的工作原理都是通过识别几何体的边界它们的工作原理都是通过识别几何体的边界,对于独立的像素点采用一种合适的方法,仅部分覆盖一个像素。每一种反走样的技术都需要计算覆盖率,然后根据几何体中像素的覆盖率来点亮该像素。由干背景会发生改变,这种变化的亮度可以通过控制像素颜色的混合值来管理,使用颜色(R, G, B, A)表示几何体的颜色,A是几何对象中像素的覆盖比例。
在glBegin(mode)和g1End()的方式使用不同的mode值来说明顶点列表创建图像。
指定顶点的命令必须包含在glBegin函数之后,glEnd函数之前(否则指定的顶点将被忽略)。
在调用glBegin()函数时,我们需要传入一个参数,以告诉OpenGL我们将绘制什么类型的图元。
glBegin(GL_POINTS);
glVertex2f(0.0f, 0.0f);
glVertex2f(0.5f, 0.0f);
glEnd();
2个点定义1条线段,4个点定义2条线段:
glBegin(GL_LINES);
glVertex2f(0.0f, 0.0f); glVertex2f(0.5f, 0.0f);
glEnd();
glBegin(GL_LINES);
glVertex2f(0.0f, 0.0f); glVertex2f(0.5f, 0.0f);//第一条线段
glVertex2f(0.0f, 0.0f); glVertex2f(1.5f, 2.0f);//第二条线段
glEnd();
相连接的线在OpenGL中称作线段序列,使用模式GL_ LINE_ STRIP来指定。顶点列表定义了线段序列。
如果定义了N个顶点,将得到N-1条直线段。
glBegin(GL_ LINE_ STRIP);
glVertex2f(0.0f, 0.0f);
glVertex2f(0.5f, 0.0f);
glVertex2f(1.5f, 2.0f);
glEnd();
封闭线段和线段序列类似,不同的是将连接第一个顶点和最后一个顶点生成一个封闭的环。封闭线段是通过GL_ LINE_ LOOP来指定的。
glBegin(GL_ LINE_ LOOP);
glVertex2f(0.0f, 0.0f);
glVertex2f(0.5f, 0.0f);
glVertex2f(1.5f, 2.0f);
glEnd();
画不相连的三角形。三个点定义一个三角形。
glBegin(GL_TRIANGLES);
glVertex(1,0,1);
glVertex(0,1,0);
glVertex(1,1,0);
glEnd;
OpenGL具有两种标准的针对三角形序列的几何体压缩技术:三角形条带和三角扇形,它们分另用GL_ TRIANGLE_ STRIP和GL_ TRIANGLE_FAN指定glBegin/glEnd模式。
glBegin(GL_QUADS);
glVertex3f(-1.0f, 1.0f, 0.0f);
glVertex3f( 1.0f, 1.0f, 0.0f);
glVertex3f( 1.0f,-1.0f, 0.0f);
glVertex3f(-1.0f,-1.0f, 0.0f);
glEnd();
为了创建四边形条带,glBegin/glEnd的绘制模式应该设为GL_QUAD_STRIP。顶点的顺序与GL_QUADS模式不同,因为四边形条带的执行与三角形条带相同。
在glBegin/g1End中设置GL_ POLYGON绘制模式可以让用户显示一个凸多边形。顶点列表中的顶点按照顺序被认为是多边形的顶点(从上往下看为逆时针,并且必须是凸多边形)。使用该操作不能显示多个多边形,因为这个函数假定它接受的所有顶点都属于同一个多边形。
#include
#include
const int n = 6;
const GLfloat R = 0.5f;
const GLfloat Pi = 3.1415926536f;
void myDisplay(void)//绘制六边形
{
int i;
glClear(GL_COLOR_BUFFER_BIT); //清除颜色
// glBegin(GL_POINTS);
glBegin(GL_LINE_LOOP);
for(i=0; i<n; ++i)
glVertex2f(R*cos(2*Pi/n*i), R*sin(2*Pi/n*i));
glEnd();
glFlush();//保证前面的OpenGL命令立即执行(而不是让它们在缓冲区中等待)
}
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE); //设置显示方式
glutInitWindowPosition(100, 100);
glutInitWindowSize(400, 400);
glutCreateWindow("第一个OpenGL程序"); //根据前面设置的信息创建窗口。参数将被作为窗口的标题。
glutDisplayFunc(&myDisplay); //当需要画图时,请调用myDisplay函数
glutMainLoop(); //进行一个消息循环
return 0;
}
绘制不同类型的图元只需要修改myDisplay(void)函数来实现就行了,这样就掌握了一些基本的绘制图形的方法了。
立方体有6个面,8个顶点,(12条边) 24条半边(因为每个面对应4条边),如下:
typedef float Point3[3];//每个顶点3个参数
typedef int edge[2];//每条边有两个顶点
typedef int face[4];//立方体的每个面都有四条边
Point3 vertices[8] = {{-1.0, -1.0, -1.0},
{-1.0,-1.0,1.0},
{-1.0,1.0,-1.0},
{-1.0,1.0,1.0},
{1.0,-1.0,-1.0},
{1.0,-1.0,1.0},
{1.0,1.0,-1.0},
{1.0,1.0,1.0}
};
edge edges[24] = {//6个face共24条边,对应24对顶点
{0, 1},{1, 3},{3, 2},{2, 0},
{0, 4},{1, 5},{3, 7},{2, 6},
{4, 5},{5, 7},{7, 6},{6, 4},
{1, 0},{3, 1},{2, 3},{0, 2},
{4, 0},{5, 1},{7, 3},{6, 2},
{5, 4},{7, 5},{6, 7},{4, 6}
};
face cube[6] = {//立方体的6个face,每个面都有4条edge
{0, 1, 2, 3},
{5, 9, 18, 13},
{14, 6, 10, 19},
{7, 11, 16, 15},
{4, 8, 17, 12},
{22,21, 20, 23}
};
Point3 normals[6] = {{0.0, 0.0, 1.0},
{0.0, -1.0, 0.0},
{0.0, 0.0, -1.0},
{1.0, 0.0, 0.0},
{0.0, -1.0, 0.0},
{0.0, 1.0, 0.0}
};
void CreateCube()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glColor3f(0.0, 1.0, 0.0);
glBegin(GL_QUADS);
for (int i = 0; i < 6; i++)//6个face,face序号cube[face]
{
glNormal3fv(normals[i]);
for (int j = 0; j < 4; j++)//每个face4条edge,由4个顶点构成。
{
glVertex3fv(vertices[edges[cube[i][j]][0]]);
}
}
glEnd();
glutSwapBuffers();
}
每一个面通过glBegin/glEnd函数对中的一次循环来定义,我们为每一个面都指定了法向量。由于GL_QUADS画图模式将每四个顶点作为四边形的顶点,所以不需要通过两次指定第一个点来让四边形封闭。
GLU提供很多二次曲面对象,或者用二次曲面方程(含有三个变量并且不超过2次的多项式方程)定义的对象。这包括球体(gluSphere )、圆柱体(gluCylinder)和圆盘(gluDisk)。每一个GLU体素声明成一个GLU二次曲面,并通过下面的函数进行分配:
GLUquadric* gluNewQuadric(void)
每一个二次曲面对象都是一个绕Z轴旋转而成的表面,它是由绕Z轴的分段(称为切片)和沿Z轴的分段(称为栈)构成。切片和堆栈确定了粒度,或者说是模型的平滑度。
(GLU二次曲面也支持很多OpenGL渲染功能。比如可以用gluQuadricDrawStyle()函数设置绘图风格,这样对象可以是填充的、线框的、侧影的或由点构成的。也可以使用gluQuadricNormals()函数来为光照模型和平滑着色处理设置法向量,这样用户就可以选择是否使用法线,用Flat着色处理还是平滑着色处理。)
GLU圆盘同其他GLU体素不同,因为它只有两维,完全在X-Y平面里。这样就不需要定义栈了,第二个参数换成了loops,它定义的是圆盘同心圆的数量。
void gluDisk(GLUquadric* quad, GLdouble inner, GLdouble outer, Glint slices, Glint loops)
quad就是先前使用gluNewQuadric创建的二次曲面对象,inner是圆盘的内半径(可以为0),outer是圆盘的外半径。
void gluSphere(GLUquadric* quad, GLdouble radius, GLint slices. Glint stacks)
quad就是先前使用gluNewQuadric创建的二次曲面对象,radius是球体半径。
//全局变量
GLUquadric* Q;
void display(void)
{
int i;
glClear(GL_COLOR_BUFFER_BIT } GL_DEPTH_BUFFER_BIT);
for <i=0; i<6; i++){
glPushMatrix();
glTranslatef(positions[i][0],positions[i][1],positions[i][2]);
switch(i)
{
case 0: {
Q=gluNewQuadric();
gluCylinder(Q, 2.,1., 1.,20, 1);break;
};
case 1: {
Q=gluNewQuadric();
gluDisk(Q, 0.5,1.,20, 10);break;
};
case 2: {
glutSolidCone(1., 1.,20, 10);
break;
};
case 3: {
glPushMatrix p;
glScalef(2.。2.,2.);
glutSolidIcosahedron();
g1PopMatrix();
break;
};
case 4: {
glutSolidTorus(0.5,1.,20,20);
break;
};
case S: {
glPushMatrix();
glRotatef(90.,1.,0.,0.);
glutSolidTeapot(1.);
g1PopMatrix Q;
break;
};
}
g1PopMatrix();
}
glutSwapBuffers();
}
注:此段代码我没有实际测试过,不确定是否能跑起来,放在这个地方主要是是为了便于理解如何使用上面的几种几何对象。
注:本文内容主要来源于《计算机图形学》。
(glut是比较老的库了,建议大家还是学习使用GLFW,理解了glut,转到GLFW应该也比较快。大家可以看看我写的这篇OpenGL入门——简单的GLFW学习,比较简单快捷的上手,因为很多内容是与glut相似的。同时也给大家推荐一个比较好的资源,https://learnopengl-cn.github.io/intro/,里面一系列文章,讲的比较清楚详细,也很好理解,为之良心推荐。)