(给Python开发者加星标,提升Python技能)1 预备知识
OpenGL 是 Open Graphics Library 的简写,意为“开放式图形库”,是用于渲染 2D、3D 矢量图形的跨语言、跨平台的应用程序编程接口(API)。OpenGL 不是一个独立的平台,因此,它需要借助于一种编程语言才能被使用。C / C++ / python / java 都可以很好支持 OpengGL,我当然习惯性选择 python 语言。
如果读者是 python 程序员,并且了解 numpy,接下来的阅读应该不会有任何障碍;否则,我建议先花半小时学习一下 python 语言。关于 numpy,可以参考我的另一篇博文《数学建模三剑客MSN》。事实上,我觉得 python 语言近乎于自然语言,只要读者是程序员,即便不熟悉 python,读起来也不会有多大问题。
另外,读者也不必担心数学问题。使用 OpenGL 不需要具备多么高深的数学水平,只要能辅导初中学生的数学作业,就足够用了。
1.1 坐标系
在 OpenGL 的世界里,有各式各样的坐标系。随着对 OpenGL 概念的理解,我们至少会接触到六种坐标系,而初始只需要了解其中的三个就足够用了(第一次阅读这段话的时候,只需要了解世界坐标系就可以了)。
世界坐标系是右手坐标系,以屏幕中心为原点(0, 0, 0),且是始终不变的。
- 视点坐标系(Eye or Camera Coordinates)
视点坐标是以视点为原点,以视线的方向为Z+轴正方向的坐标系。OpenGL 管道会将世界坐标先变换到视点坐标,然后进行裁剪,只有在视线范围(视景体)之内的场景才会进入下一阶段的计算。
- 屏幕坐标系(Window or Screen Coordinates)
OpenGL 的重要功能之一就是将三维的世界坐标经过变换、投影等计算,最终算出它在显示设备上对应的位置,这个位置就称为设备坐标。在屏幕、打印机等设备上的坐标是二维坐标。值得一提的是,OpenGL 可以只使用设备的一部分进行绘制,这个部分称为视区或视口(viewport)。投影得到的是视区内的坐标(投影坐标),从投影坐标到设备坐标的计算过程就是设备变换了。
1.2 投影
三维场景中的物体最终都会显示在类似屏幕这样的二维观察平面上。将三维物体变为二维图形的变换成为投影变换。最常用的投影有两种:平行投影和透视投影。如下图所示,F 是投影面,p1p2 为三维空间中的一条直线,p’1 和 p’2 分别是 p1 和 p2 在 F 上的投影,虚线表示投影线,O 为投影中心。
这里所说的平行投影,特指正交平行投影——投影线垂直于投影面。将一个三维点 (x,y,z) 正交平行投影到 xoy 平面上,则投影点坐标为 (x,y,0)。由于平行投影丢弃了深度信息,所以无法产生真实感,但可以保持物体之间相对大小关系不变。
透视投影将投影面置于观察点和投影对象之间,距离观察者越远的物体,投影尺寸越小,投影效果具有真实感,常用于游戏和仿真领域。
1.3 视景体
无论是平行投影还是透视投影,投影成像都是在投影面上——我们可以把投影面理解成显示屏幕。世界坐标系描述的三维空间是无限的,投影平面是无限的,但(我们能够看到的)屏幕面积总是有限的,因此在投影变换时,通常只处理能够显示在屏幕上的那一部分三维空间。从无限三维空间中裁切出来的可以显示在屏幕上的部分三维空间,我们称之为视景体。视景体有六个面,分别是左右上下和前后面。
对于平行投影而言,视景体是一个矩形平行六面体;对于透视投影来说,视景体是一个棱台。理解这一点并不难:因为越远处的物体在投影窗口的透视投影越小,也就意味着填满投影窗口需要更大的体量,视景体自然就变成了棱台。
1.4 视口
对于平行投影而言,视口就是由视景体的左右上下四个面围成的矩形,对于透视投影来说,视口就是视景体的前截面在投影窗口上的透视投影。
视口是 OpenGL 中比较重要的概念,现阶段可以简单理解成屏幕(或其他输出设备)。事实上,视口和屏幕是相关但又不相同的,屏幕有固定的宽高比,而视口大小可以由用户自行定义。通常,为了适应不同宽高比的屏幕,在设置视口时,会根据屏幕宽高比调整视景体(增加宽度或高度)。
1.5 视点
现实生活中,人们看到的三维空间物体的样子取决于观察者站在什么角度去看。这里面包含着三个概念:
- 观察者的位置:眼睛在哪儿?
- 观察者的姿势:站立还是倒立?左侧卧还是右侧卧?
- 观察对象:眼睛盯着哪里?
对应在 OpenGL 中,也有同样的概念,即视点的位置、瞄准方向的参考点,以及(向上的)方向。
1.6 OpenGL 变换
下图是三维图形的显示流程。世界坐标系中的三维物体经过视点变换和一系列几何变换(平移、旋转、缩放)之后,坐标系变换为视点坐标系;经过投影和裁剪之后,坐标系变换为归一化设备坐标系;最后经过视口变换显示在屏幕上,相应地,坐标系变成了窗口坐标系。
- 视点变换:相当于设置视点的位置和方向
- 模型变换:包括平移、旋转、缩放等三种类型
- 裁剪变换:根据视景体定义的六个面(和附加裁剪面)对三维空间裁剪
- 视口变换:将视景体内投影的物体显示在二维的视口平面上
2 安装 pyopengl
如果想当然地使用 pip 如下所示安装,可能会有一些麻烦。
当我这样安装之后,运行 OpenGL 代码,得到了这样的错误信息:
- NullFunctionError: Attempt to call an undefined function glutInit, check for bool(glutInit) before calling
复制代码 原来,pip 默认安装的是32位版本的pyopengl,而我的操作系统是64位的。建议点击这里下载适合自己的版本,直接安装.whl文件。我是这样安装的:
- pip install PyOpenGL-3.1.3b2-cp37-cp37m-win_amd64.whl
复制代码 3 OpenGL 库及函数简介
我第一次接触 OpenGL 的 GL / GLU / GLUT 的时候,一下就被这些长得像孪生兄弟的库名字给整懵圈了,要不是内心强大,也许就跟 OpenGL 说再见了。时间久了才发现,OpenGL 的库及函数命名规则非常合理,便于查找、记忆。
OpenGL函数的命名格式如下:
- glColor4f(0.0,1.0,1.0,1.0) # 设置当前颜色为青色,不透明度
复制代码- glColor3ub(0, 0, 255) # 设置当前颜色为蓝色
复制代码 glColor 也支持将三个或四个参数以向量方式传递,例如:
- glColor3fv([0.0,1.0,0.0]) # 设置当前颜色为绿色
复制代码 特别提示:OpenGL 是使用状态机模式,颜色是一个状态变量,设置颜色就是改变这个状态变量并一直生效,直到再次调用设置颜色的函数。除了颜色,OpenGL 还有很多的状态变量或模式。在任何时间,都可以查询每个状态变量的当前值,还可以用 glPushAttrib() 或 glPushClientAttrib() 把状态变量的集合保存起来,必要的时候,再用 glPopAttrib() 或 glPopClientAttrib() 恢复状态变量。
4.1.2 设置顶点
顶点(vertex)是 OpengGL 中非常重要的概念,描述线段、多边形都离不开顶点。和设置颜色类似,设置顶点的函数也有几十个,都是以 glVertex 开头,后面跟着参数个数和参数类型,同样也支持将多个以向量方式传递。 两个参数的话,分别表示 xy 坐标,三个参数则分别表示 xyz 坐标。如有第四个参数,则表示该点的齐次坐标 w;否则,默认 w=1。至于什么是齐次坐标,显然超出了初中数学的范畴,在此不做探讨。
- glVertex2f(1.0,0.5) # xoy平面上的点,z=0
复制代码- glVertex3f(0.5,1.0,0.0) # 三维空间中的点
复制代码 4.1.3 绘制基本图形
仅仅设置颜色和顶点,并不能画出来什么。我们可以在任何时候改变颜色,但所有的顶点设置,都必须包含在 glBegin() 和 glEnd() 之间,而 glBegin() 的参数则指定了将这些顶点画成什么。以下是 glBegin() 可能的参数选项:
参数 说明
GL_POINTS 绘制一个或多个顶点
GL_LINES 绘制线段
GL_LINE_STRIP 绘制连续线段
GL_LINE_LOOP 绘制闭合的线段
GL_POLYGON 绘制多边形
GL_TRIANGLES 绘制一个或多个三角形
GL_TRIANGLE_STRIP 绘制连续三角形
GL_TRIANGLE_FAN 绘制多个三角形组成的扇形
GL_QUADS 绘制一个或多个四边形
GL_QUAD_STRIP 绘制连续四边形
4.2 第一个 OpenGL 程序
通常,我们使用工具库(GLUT)创建 OpenGL 应用程序。为啥不用 GL 或者 GLU 库呢?画画之前总得先有一块画布吧,不能直接拿起画笔就开画。前文说过,工具库主要提供窗口相关的函数,有了窗口,就相当于有了画布,而核心库和实用库,就好比各式各样的画笔、颜料。使用工具库(GLUT)创建 OpenGL 应用程序只需要四步(当然,前提是你需要先准备好绘图函数,并给它取一个合适的名字):
- 初始化glut库
- 创建glut窗口
- 注册绘图的回调函数
- 进入glut主循环
OK,铺垫了这么多之后,我们终于开始第一个 OpenGL 应用程序了:绘制三维空间的世界坐标系,在坐标原点的后方(z轴的负半区)画一个三角形。代码如下:
- # -------------------------------------------
复制代码- # quidam_01.py 三维空间的世界坐标系和三角形
复制代码- # -------------------------------------------
复制代码- from OpenGL.GLUT import *
复制代码- [/code][code] # ---------------------------------------------------------------
复制代码- [/code][code] glBegin(GL_LINES) # 开始绘制线段(世界坐标系)
复制代码- glColor4f(1.0, 0.0, 0.0, 1.0) # 设置当前颜色为红色不透明
复制代码- glVertex3f(-0.8, 0.0, 0.0) # 设置x轴顶点(x轴负方向)
复制代码- glVertex3f(0.8, 0.0, 0.0) # 设置x轴顶点(x轴正方向)
复制代码- [/code][code] glColor4f(0.0, 1.0, 0.0, 1.0) # 设置当前颜色为绿色不透明
复制代码- glVertex3f(0.0, -0.8, 0.0) # 设置y轴顶点(y轴负方向)
复制代码- glVertex3f(0.0, 0.8, 0.0) # 设置y轴顶点(y轴正方向)
复制代码- glColor4f(0.0, 0.0, 1.0, 1.0) # 设置当前颜色为蓝色不透明
复制代码- glVertex3f(0.0, 0.0, -0.8) # 设置z轴顶点(z轴负方向)
复制代码- glVertex3f(0.0, 0.0, 0.8) # 设置z轴顶点(z轴正方向)
复制代码- [/code][code] # ---------------------------------------------------------------
复制代码- glBegin(GL_TRIANGLES) # 开始绘制三角形(z轴负半区)
复制代码- glColor4f(1.0, 0.0, 0.0, 1.0) # 设置当前颜色为红色不透明
复制代码- glVertex3f(-0.5, -0.366, -0.5) # 设置三角形顶点
复制代码- glColor4f(0.0, 1.0, 0.0, 1.0) # 设置当前颜色为绿色不透明
复制代码- glVertex3f(0.5, -0.366, -0.5) # 设置三角形顶点
复制代码- glColor4f(0.0, 0.0, 1.0, 1.0) # 设置当前颜色为蓝色不透明
复制代码- glVertex3f(0.0, 0.5, -0.5) # 设置三角形顶点
复制代码- [/code][code] glEnd() # 结束绘制三角形
复制代码- # ---------------------------------------------------------------
复制代码- glFlush() # 清空缓冲区,将指令送往硬件立即执行
复制代码- if __name__ == "__main__":
复制代码- glutCreateWindow('Quidam Of OpenGL') # 2. 创建glut窗口
复制代码- glutDisplayFunc(draw) # 3. 注册回调函数draw()
复制代码- glutMainLoop() # 4. 进入glut主循环
复制代码 运行代码,我这里显示结果如下面左图所示。如果尝试运行这段代码出错的话,我猜应该是 pyopengl 安装出现了问题,建议返回到前面重读 pyopengl 的安装。
短暂的激动之后,你可能会尝试画一些其他的线段,变换颜色或者透明度,甚至绘制多边形。很快你会发现,我们的第一个程序有很多问题,比如:
- 窗口的标题不能使用中文,否则会显示乱码
- 窗口的初始大小和位置无法改变
- 改变窗口的宽高比,三角形宽高比也会改变(如上面右图所示)
- 三角形不应该遮挡坐标轴
- 改变颜色的透明度无效
- 不能缩放旋转
没关系,除了第1个问题我不知道怎么解决(貌似无解),其他问题都不是事儿。和我们的代码相比,一个真正实用的 OpenGL 程序,还有许多工作要做:
- 设置初始显示模式
- 初始化画布
- 绘图函数里面需要增加:
- 清除屏幕及深度缓存
- 投影设置
- 模型试图设置
- 绑定鼠标键盘的事件函数
4.3 设置初始显示模式
初始化 glut 库的时候,我们一般都要用 glutInitDisplayMode() 来设置初始的显示模式,它的参数可以是下表中参数的组合。
参数 说明
GLUT_RGB 指定RGB颜色模式的窗口
GLUT_RGBA 指定RGBA 颜色模式的窗口
GLUT_INDEX 指定颜色索引模式的窗口
GLUT_SINGLE 指定单缓存窗口
GLUT_DOUBLE 指定双缓存窗口
GLUT_ACCUM 窗口使用累加缓存
GLUT_ALPHA 窗口的颜色分量包含 alpha 值
GLUT_DEPTH 窗口使用深度缓存
GLUT_STENCIL 窗口使用模板缓存
GLUT_MULTISAMPLE 指定支持多样本功能的窗口
GLUT_STEREO 指定立体窗口
GLUT_LUMINANCE 窗口使用亮度颜色模型
使用双缓存窗口,可以避免重绘时产生抖动的感觉。我一般选择 GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH 作为参数来设置初始的显示模式。
4.4 初始化画布
开始绘图之前,需要对画布做一些初始化工作,这些工作只需要做一次。比如:
- glClearColor(0.0, 0.0, 0.0, 1.0) # 设置画布背景色。注意:这里必须是4个参数
复制代码- glEnable(GL_DEPTH_TEST) # 开启深度测试,实现遮挡关系
复制代码- glDepthFunc(GL_LEQUAL) # 设置深度测试函数(GL_LEQUAL只是选项之一)
复制代码 如有必要,还可以开启失真校正(反走样)、开启表面剔除等。
4.5 清除屏幕及深度缓存
每次重绘之前,需要先清除屏幕及深度缓存。这项操作一般放在绘图函数的开头。
- glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
复制代码 4.5 设置投影
投影设置也是每次重绘都需要的步骤之一。glOrtho() 用来设置平行投影,glFrustum() 用来设置透视投影。这两个函数的参数相同,都是视景体的 left / right / bottom / top / near / far 六个面。
视景体的 left / right / bottom / top 四个面围成的矩形,就是视口。near 就是投影面,其值是投影面距离视点的距离,far 是视景体的后截面,其值是后截面距离视点的距离。far 和 near 的差值,就是视景体的深度。视点和视景体的相对位置关系是固定的,视点移动时,视景体也随之移动。
我个人认为,视景体是 OpengGL 最重要、最核心的概念,它和视口、视点、投影面、缩放、漫游等概念密切关联。只有正确理解了视景体,才能正确设置它的六个参数,才能呈现出我们期望的效果。
为了在窗口宽高比改变时,绘制的对象仍然保持固定的宽高比,一般在做投影变换时,需要根据窗口的宽高比适当调整视景体的 left / right 或者 bottom / top 参数。
假设 view 是视景体,width 和 height 是窗口的宽度和高度,在投影变换之前,需要先声明是对投影矩阵的操作,并将投影矩阵单位化:
- glMatrixMode(GL_PROJECTION)
复制代码- glFrustum(view [0]*k, view [1]*k, view [2], view [3], view [4], view [5])
复制代码- glFrustum(view [0], view [1], view [2]*k, view [3]*k, view [4], view [5])
复制代码 4.6 设置视点
视点是和视景体关联的概念。设置视点需要考虑眼睛在哪儿、看哪儿、头顶朝哪儿,分别对应着eye, lookat 和 eye_up 三个向量。
- look_at[0], look_at[1], look_at[2],
复制代码- eye_up[0], eye_up[1], eye_up[2]
复制代码 4.7 设置视口
视口也是和视景体关联的概念,相对简单一点。
- glViewport(0, 0, width, height)
复制代码 4.8 设置模型视图
模型平移、旋转、缩放等几何变换,需要切换到模型矩阵:
- glMatrixMode(GL_MODELVIEW)
复制代码 4.9 捕捉鼠标事件、键盘事件和窗口事件
GLUT 库提供了几个函数帮我们捕捉鼠标事件、键盘事件和窗口事件:
glutMouseFunc()
该函数捕捉鼠标点击和滚轮操作,返回4个参数给被绑定的事件函数:键(左键/右键/中键/滚轮上/滚轮下)、状态(1/0)、x坐标、y坐标
1.glutMotionFunc()
该函数捕捉有一个鼠标键被按下时的鼠标移动给被绑定的事件函数,返回2个参数:x坐标、y坐标
2.glutPassiveMotionFunc()
该函数捕捉鼠标移动,返回2个参数给被绑定的事件函数:x坐标、y坐标
3.glutEntryFunc()
该函数捕捉鼠标离开或进入窗口区域,返回1个参数给被绑定的事件函数:GLUT_LEFT 或者 GLUT_ENTERED
4.glutKeyboardFunc(keydown)
该函数捕捉键盘按键被按下,返回3个参数给被绑定的事件函数:被按下的键,x坐标、y坐标
5.glutReshapeFunc()
该函数捕捉窗口被改变大小,返回2个参数给被绑定的事件函数:窗口宽度、窗口高度
如果我们需要捕捉这些事件,只需要定义事件函数,注册相应的函数就行:
- def reshape(width, height):
复制代码- [/code][code]def mouseclick(button, state, x, y):
复制代码- [/code][code]def keydown(key, x, y):
复制代码- glutReshapeFunc(reshape) # 注册响应窗口改变的函数reshape()
复制代码- glutMouseFunc(mouseclick) # 注册响应鼠标点击的函数mouseclick()
复制代码- glutMotionFunc(mousemotion) # 注册响应鼠标拖拽的函数mousemotion()
复制代码- glutKeyboardFunc(keydown) # 注册键盘输入的函数keydown()
复制代码 4.10 综合应用
是时候把我们上面讲的这些东西完整的演示一下了。下面的代码还是画了世界坐标系,并在原点前后各画了一个三角形。鼠标可以拖拽视点绕参考点旋转(二者距离保持不变),滚轮可以缩放模型。敲击退格键或回车键可以让视点远离或接近参考点。敲击 x/y/z 可以减小参考点对应的坐标值,敲击 X/Y/Z 可以增大参考点对应的坐标值。敲击空格键可以切换投影模式。
上图左是平行投影模式的显示效果,上图右是透视投影模式的显示效果。代码如下:
- # -------------------------------------------
复制代码- # quidam_02.py 旋转、缩放、改变视点和参考点
复制代码- # -------------------------------------------
复制代码- from OpenGL.GLUT import *
复制代码- [/code][code]IS_PERSPECTIVE = True # 透视投影
复制代码- VIEW = np.array([-0.8, 0.8, -0.8, 0.8, 1.0, 20.0]) # 视景体的left/right/bottom/top/near/far六个面
复制代码- SCALE_K = np.array([1.0, 1.0, 1.0]) # 模型缩放比例
复制代码- [/code][code]EYE = np.array([0.0, 0.0, 2.0]) # 眼睛的位置(默认z轴的正方向)
复制代码- [/code][code]LOOK_AT = np.array([0.0, 0.0, 0.0]) # 瞄准方向的参考点(默认在坐标原点)
复制代码- [/code][code]EYE_UP = np.array([0.0, 1.0, 0.0]) # 定义对观察者而言的上方(默认y轴的正方向)
复制代码- WIN_W, WIN_H = 640, 480 # 保存窗口宽度和高度的变量
复制代码- LEFT_IS_DOWNED = False # 鼠标左键被按下
复制代码- MOUSE_X, MOUSE_Y = 0, 0 # 考察鼠标位移量时保存的起始位置
复制代码- dist = np.sqrt(np.power((EYE-LOOK_AT), 2).sum())
复制代码- phi = np.arcsin((EYE[1]-LOOK_AT[1])/dist)
复制代码- theta = np.arcsin((EYE[0]-LOOK_AT[0])/(dist*np.cos(phi)))
复制代码- DIST, PHI, THETA = getposture() # 眼睛与观察目标之间的距离、仰角、方位角
复制代码- [/code][code] glClearColor(0.0, 0.0, 0.0, 1.0) # 设置画布背景色。注意:这里必须是4个参数
复制代码- [/code][code] glEnable(GL_DEPTH_TEST) # 开启深度测试,实现遮挡关系
复制代码- [/code][code] glDepthFunc(GL_LEQUAL) # 设置深度测试函数(GL_LEQUAL只是选项之一)
复制代码- [/code][code] global IS_PERSPECTIVE, VIEW
复制代码- [/code][code] global EYE, LOOK_AT, EYE_UP
复制代码- [/code][code] global SCALE_K
复制代码- [/code][code] global WIN_W, WIN_H
复制代码- [/code][code] # 清除屏幕及深度缓存
复制代码- [/code][code] glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
复制代码- [/code][code] # 设置投影(透视投影)
复制代码- [/code][code] glMatrixMode(GL_PROJECTION)
复制代码- [/code][code] glLoadIdentity()
复制代码- [/code][code] if WIN_W > WIN_H:
复制代码- [/code][code] if IS_PERSPECTIVE:
复制代码- [/code][code] glFrustum(VIEW[0]*WIN_W/WIN_H, VIEW[1]*WIN_W/WIN_H, VIEW[2], VIEW[3], VIEW[4], VIEW[5])
复制代码- [/code][code] glOrtho(VIEW[0]*WIN_W/WIN_H, VIEW[1]*WIN_W/WIN_H, VIEW[2], VIEW[3], VIEW[4], VIEW[5])
复制代码- [/code][code] if IS_PERSPECTIVE:
复制代码- [/code][code] glFrustum(VIEW[0], VIEW[1], VIEW[2]*WIN_H/WIN_W, VIEW[3]*WIN_H/WIN_W, VIEW[4], VIEW[5])
复制代码- [/code][code] glOrtho(VIEW[0], VIEW[1], VIEW[2]*WIN_H/WIN_W, VIEW[3]*WIN_H/WIN_W, VIEW[4], VIEW[5])
复制代码- [/code][code] glMatrixMode(GL_MODELVIEW)
复制代码- [/code][code] glLoadIdentity()
复制代码- [/code][code] glScale(SCALE_K[0], SCALE_K[1], SCALE_K[2])
复制代码- [/code][code] EYE[0], EYE[1], EYE[2],
复制代码- [/code][code] LOOK_AT[0], LOOK_AT[1], LOOK_AT[2],
复制代码- [/code][code] EYE_UP[0], EYE_UP[1], EYE_UP[2]
复制代码- [/code][code] glViewport(0, 0, WIN_W, WIN_H)
复制代码- [/code][code] # ---------------------------------------------------------------
复制代码- glBegin(GL_LINES) # 开始绘制线段(世界坐标系)
复制代码- [/code][code] glColor4f(1.0, 0.0, 0.0, 1.0) # 设置当前颜色为红色不透明
复制代码- [/code][code] glVertex3f(-0.8, 0.0, 0.0) # 设置x轴顶点(x轴负方向)
复制代码- [/code][code] glVertex3f(0.8, 0.0, 0.0) # 设置x轴顶点(x轴正方向)
复制代码- [/code][code] glColor4f(0.0, 1.0, 0.0, 1.0) # 设置当前颜色为绿色不透明
复制代码- [/code][code] glVertex3f(0.0, -0.8, 0.0) # 设置y轴顶点(y轴负方向)
复制代码- [/code][code] glVertex3f(0.0, 0.8, 0.0) # 设置y轴顶点(y轴正方向)
复制代码- [/code][code] glColor4f(0.0, 0.0, 1.0, 1.0) # 设置当前颜色为蓝色不透明
复制代码- [/code][code] glVertex3f(0.0, 0.0, -0.8) # 设置z轴顶点(z轴负方向)
复制代码- [/code][code] glVertex3f(0.0, 0.0, 0.8) # 设置z轴顶点(z轴正方向)
复制代码- [/code][code] glEnd() # 结束绘制线段
复制代码- [/code][code] # ---------------------------------------------------------------
复制代码- [/code][code] glBegin(GL_TRIANGLES) # 开始绘制三角形(z轴负半区)
复制代码- [/code][code] glColor4f(1.0, 0.0, 0.0, 1.0) # 设置当前颜色为红色不透明
复制代码- [/code][code] glVertex3f(-0.5, -0.366, -0.5) # 设置三角形顶点
复制代码- [/code][code] glColor4f(0.0, 1.0, 0.0, 1.0) # 设置当前颜色为绿色不透明
复制代码- [/code][code] glVertex3f(0.5, -0.366, -0.5) # 设置三角形顶点
复制代码- [/code][code] glColor4f(0.0, 0.0, 1.0, 1.0) # 设置当前颜色为蓝色不透明
复制代码- [/code][code] glVertex3f(0.0, 0.5, -0.5) # 设置三角形顶点
复制代码- # ---------------------------------------------------------------
复制代码- [/code][code] glBegin(GL_TRIANGLES) # 开始绘制三角形(z轴正半区)
复制代码- glColor4f(1.0, 0.0, 0.0, 1.0) # 设置当前颜色为红色不透明
复制代码- glVertex3f(-0.5, 0.5, 0.5) # 设置三角形顶点
复制代码- [/code][code] glColor4f(0.0, 1.0, 0.0, 1.0) # 设置当前颜色为绿色不透明
复制代码- [/code][code] glVertex3f(0.5, 0.5, 0.5) # 设置三角形顶点
复制代码- [/code][code] glColor4f(0.0, 0.0, 1.0, 1.0) # 设置当前颜色为蓝色不透明
复制代码- [/code][code] glVertex3f(0.0, -0.366, 0.5) # 设置三角形顶点
复制代码- [/code][code] glEnd() # 结束绘制三角形
复制代码- # ---------------------------------------------------------------
复制代码- [/code][code] glutSwapBuffers() # 切换缓冲区,以显示绘制内容
复制代码- def reshape(width, height):
复制代码- [/code][code] global WIN_W, WIN_H
复制代码- [/code][code] WIN_W, WIN_H = width, height
复制代码- [/code][code] glutPostRedisplay()
复制代码- [/code][code]def mouseclick(button, state, x, y):
复制代码- [/code][code] global SCALE_K
复制代码- [/code][code] global LEFT_IS_DOWNED
复制代码- [/code][code] global MOUSE_X, MOUSE_Y
复制代码- [/code][code] if button == GLUT_LEFT_BUTTON:
复制代码- LEFT_IS_DOWNED = state==GLUT_DOWN
复制代码- [/code][code] elif button == 3:
复制代码- THETA += 2*np.pi*dx/WIN_W
复制代码- EYE[1] = DIST*np.sin(PHI)
复制代码- if 0.5*np.pi < PHI < 1.5*np.pi:
复制代码- global EYE, LOOK_AT, EYE_UP
复制代码- global IS_PERSPECTIVE, VIEW
复制代码- [/code][code] if key in [b'x', b'X', b'y', b'Y', b'z', b'Z']:
复制代码- if key == b'x': # 瞄准参考点 x 减小
复制代码- elif key == b'X': # 瞄准参考 x 增大
复制代码- elif key == b'y': # 瞄准参考点 y 减小
复制代码- elif key == b'Y': # 瞄准参考点 y 增大
复制代码- [/code][code] elif key == b'z': # 瞄准参考点 z 减小
复制代码- [/code][code] LOOK_AT[2] -= 0.01
复制代码- elif key == b'Z': # 瞄准参考点 z 增大
复制代码- [/code][code] LOOK_AT[2] += 0.01
复制代码- [/code][code] DIST, PHI, THETA = getposture()
复制代码- elif key == b'\r': # 回车键,视点前进
复制代码- [/code][code] EYE = LOOK_AT + (EYE - LOOK_AT) * 0.9
复制代码- [/code][code] DIST, PHI, THETA = getposture()
复制代码- [/code][code] glutPostRedisplay()
复制代码- [/code][code] elif key == b'\x08': # 退格键,视点后退
复制代码- [/code][code] EYE = LOOK_AT + (EYE - LOOK_AT) * 1.1
复制代码- [/code][code] DIST, PHI, THETA = getposture()
复制代码- [/code][code] glutPostRedisplay()
复制代码- [/code][code] elif key == b' ': # 空格键,切换投影模式
复制代码- [/code][code] IS_PERSPECTIVE = not IS_PERSPECTIVE
复制代码- [/code][code] glutPostRedisplay()
复制代码- [/code][code]if __name__ == "__main__":
复制代码- displayMode = GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH
复制代码- glutInitDisplayMode(displayMode)
复制代码- [/code][code] glutInitWindowSize(WIN_W, WIN_H)
复制代码- glutInitWindowPosition(300, 200)
复制代码- glutCreateWindow('Quidam Of OpenGL')
复制代码- [/code][code] init() # 初始化画布
复制代码- [/code][code] glutDisplayFunc(draw) # 注册回调函数draw()
复制代码- [/code][code] glutReshapeFunc(reshape) # 注册响应窗口改变的函数reshape()
复制代码- [/code][code] glutMouseFunc(mouseclick) # 注册响应鼠标点击的函数mouseclick()
复制代码- glutMotionFunc(mousemotion) # 注册响应鼠标拖拽的函数mousemotion()
复制代码- glutKeyboardFunc(keydown) # 注册键盘输入的函数keydown()
复制代码- glutMainLoop() # 进入glut主循环
复制代码 4.11 小结
虽然还有很多领域需要我们继续探索,比如灯光、材质、雾化、拾取等,但那不是奇幻之旅的目标。奇幻之旅仅仅是帮助读者建立 OpenGL 的基本概念。至此,我们基本完成了任务。
5 加速渲染
实际应用 OpenGL 绘制三维图像时,往往需要处理数以万计的顶点,有时甚至是百万级、千万级。我们通常不会在绘制函数里面传送这些数据,而是在绘制之前,将这些数据提前传送到GPU。绘制函数每次绘制时,只需要从GPU的缓存中取出数据即可,极大地提高了效率。这个机制地实现,依赖于顶点缓冲区对象(Vertex Buffer Object),简称VBO。
尽管 VBO 是显卡的扩展,其实没有用到GPU运算,也就是说 VBO 不用写着色语言,直接用opengl函数就可以调用,主要目的是用于加快渲染的速。
VBO 将顶点信息放到 GPU 中,GPU 在渲染时去缓存中取数据,二者中间的桥梁是 GL-Context。GL-Context 整个程序一般只有一个,所以如果一个渲染流程里有两份不同的绘制代码,GL-context 就负责在他们之间进行切换。这也是为什么要在渲染过程中,在每份绘制代码之中会有 glBindbuffer、glEnableVertexAttribArray、glVertexAttribPointer。如果把这些都放到初始化时候完成,使用一种结构记录该次绘制所需要的所有 VBO 所需信息,把它保存到 VBO特定位置,绘制的时候直接在这个位置取信息绘制,会简化渲染流程、提升渲染速度。这就是 VAO 概念产生的初衷。
VAO 的全名是 Vertex Array Object,首先,它不是 Buffer-Object,所以不用作存储数据;其次,它针对“顶点”而言,也就是说它跟“顶点的绘制”息息相关。VAO 记录的是一次绘制中所需要的信息,这包括“数据在哪里 glBindBuffer”、“数据的格式是怎么样的 glVertexAttribPointer”、shader-attribute 的 location 的启用 glEnableVertexAttribArray。
根据我查到的资料,几乎所有的显卡都支持 VBO,但不是所有的显卡都支持 VAO,而 VAO 仅仅是优化了 VBO 的使用方法,对于加速并没有实质性的影响,因此本文只讨论 VBO 技术。
5.1 创建顶点缓冲区对象(VBO)
假定画一个六面体,顶点是这样的:
- [/code][code]# ------------------------------------------------------
复制代码- [/code][code]# v4----- v5
复制代码- [/code][code]# v0------v1|
复制代码- [/code][code]# | v7----|-v6
复制代码- [/code][code]# v3------v2
复制代码- [/code][code]vertices = np.array([
复制代码- [/code][code] -0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, # v0-v1-v2-v3
复制代码- [/code][code] -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5, -0.5, -0.5, -0.5, -0.5, -0.5 # v4-v5-v6-v7
复制代码- [/code][code]], dtype=np.float32)
复制代码- [/code][code]indices = np.array([
复制代码- [/code][code] 0, 1, 2, 3, # v0-v1-v2-v3 (front)
复制代码- [/code][code] 4, 5, 1, 0, # v4-v5-v1-v0 (top)
复制代码- [/code][code] 3, 2, 6, 7, # v3-v2-v6-v7 (bottom)
复制代码- [/code][code] 5, 4, 7, 6, # v5-v4-v7-v6 (back)
复制代码- [/code][code] 1, 5, 6, 2, # v1-v5-v6-v2 (right)
复制代码- [/code][code] 4, 0, 3, 7 # v4-v0-v3-v7 (left)
复制代码- [/code][code]], dtype=np.int)
复制代码 在GPU上创建VBO如下:
- from OpenGL.arrays import vbo
复制代码- [/code][code]vbo_vertices = vbo.VBO(vertices)
复制代码- vbo_indices = vbo.VBO(indices, target=GL_ELEMENT_ARRAY_BUFFER)
复制代码 创建 顶点 VBO 时,默认 target=GL_ARRAY_BUFFER, 而创建索引 VBO 时,target=GL_ELEMENT_ARRAY_BUFFER,因为顶点的数据类型是 np.float32,索引的数据类型是np.int。
在VBO保存的顶点数据集,除了顶点信息外,还可以包含颜色、法线、纹理等数据,这就是顶点混合数组的概念。假定我们在上面的顶点集中增加每个顶点的颜色,则可以写成这样:
- 0.3, 0.6, 0.9, -0.35, 0.35, 0.35, # c0-v0
复制代码- 0.6, 0.9, 0.3, 0.35, 0.35, 0.35, # c1-v1
复制代码- 0.9, 0.3, 0.6, 0.35, -0.35, 0.35, # c2-v2
复制代码- 0.3, 0.9, 0.6, -0.35, -0.35, 0.35, # c3-v3
复制代码- 0.6, 0.3, 0.9, -0.35, 0.35, -0.35, # c4-v4
复制代码- 0.9, 0.6, 0.3, 0.35, 0.35, -0.35, # c5-v5
复制代码- 0.3, 0.9, 0.9, 0.35, -0.35, -0.35, # c6-v6
复制代码- 0.9, 0.9, 0.3, -0.35, -0.35, -0.35 # c7-v7
复制代码 5.2 分离顶点混合数组
使用 glInterleavedArrays() 函数可以从顶点混合数组中分离顶点、颜色、法线和纹理。比如,对只包含顶点信息的顶点混合数组:
- [/code][code]glInterleavedArrays(GL_V3F, 0, None)
复制代码 如果顶点混合数组包含了颜色和顶点信息:
- glInterleavedArrays(GL_C3F_V3F, 0, None)
复制代码 glInterleavedArrays() 函数第一个参数总共有14个选项,分别是:
- GL_V2F
- GL_V3F
- GL_C4UB_V2F
- GL_C4UB_V3F
- GL_C3F_V3F
- GL_N3F_V3F
- GL_C4F_N3F_V3F
- GL_T2F_V3F
- GL_T4F_V4F
- GL_T2F_C4UB_V3F
- GL_T2F_C3F_V3F
- GL_T2F_N3F_V3F
- GL_T2F_C4F_N3F_V3F
- GL_T4F_C4F_N3F_V4F
5.3 使用顶点缓冲区对象(VBO)
使用glDrawElements() 等函数绘制前,需要先绑定顶点数据集和索引数据集,然后使用glInterleavedArrays() 分理出顶点、颜色、法线等数据。
- glInterleavedArrays(GL_V3F, 0, None)
复制代码- glDrawElements(GL_QUADS, int(vbo_indices .size/4), GL_UNSIGNED_INT, None)
复制代码 6 致谢
写作过程中,我参考了很多资料,包括纸质书籍和网页,列写于此,一并致谢!
- 《OpenGL编程精粹》杨柏林 陈根浪 徐静 编著
- Opengl开发库介绍
- OpenGL的API函数使用手册
- glut处理鼠标事件
- Learn OpenGL
【本文作者】
许向武:山东远思信息科技有限公司CEO,网名牧码人(天元浪子),齐国土著,太公之后。少小离家,独闯江湖,后归隐于华不注山。素以敲击键盘为业,偶尔游戏于各网络对局室,擅长送财送分,深为众棋友所喜闻乐见。
推荐阅读
(点击标题可跳转阅读)
写给工程师的 10 条精进原则
写给程序员的有效学习方法
觉得本文对你有帮助?请分享给更多人
关注「Python开发者」加星标,提升Python技能
好文章,我在看
|
|