opengl教程
0. 动机
首先申明,我是拓幻科技图形处理工程师,自己接触OpenGL,图形图像等方面也有六年多了,很多人其实并不了解这方面,也不了解如何系统地去学,我觉得基于我硕士时期的课程和经验给大家。这些资料和经验也得感谢我的老师,来自普渡大学的终生教授迈克 贝利(Mike Bailey). 以此连载OpenGL学习教程,借助公司知乎给大家讲解,一起学习一下,不对之处,欢迎大家指出讨论。
所有渲染工作都离不开OpenGL, 着色器(Shader)这些,如果你和我一样对图形处理比较感兴趣的话,可能你和当初的我有着同样的困惑:如何开始?OpenGL有一些官方的手册和文档,但是一来内容比较分散,二来学习阶梯稍微陡峭了些而且晦涩难懂。这对于之前完全没有接触过有关内容的新人来说是相当不友好的。这些内容说是教程,倒不如说是一份我对于图形学课程或者项目的经验总结,希望能够帮到有需要的人。
所以,本“教程”的对象是
- 总的来说是新接触图形学OpenGL开发的人:也许你知道什么是OpenGL,什么是Shader,也会使用别人的Shader,但是仅限于知道一些基本的名词。想要系统性学习OpenGL,Shader开发的开发者,但是之前并没有图形学开发的经验。
1. 计算机图形学简史
1.1 1950s
在1950s,刚出现笔式绘图仪和计算机示波器。
1.2 1960s
在1960s,出现最初代的矢量显示器和一些最初的交互式设备。
在1960s,还有一个很著名的项目,计算机图形学之父 Ivan的SketchPad Project.感兴趣的同学可以搜一下伊凡·苏泽兰去了解一下,他的伟大就不多说了。
1.3 1970s
在1970s,出现直视存储管,彩色光栅显示器等,并且召开了SIGGRAPH大会。
1.4 1980s
在1980s,出现模拟飞行渲染技术。著名动画厂商皮克斯(Pixar)成立。1984年英伟达的创始人从我的母校毕业,?,感谢他为我的母校计算机学院捐的楼和实验室。
1.5 1990s
在1980s,在硬件上可以进行纹理映射,并且OpenGL也出现。侏罗纪公园,玩具总动员也是这个年代的产物。
1.6 2000s
在2000s,图形学发展尤为重要,出现了着色器shaders和OpenGL-ES。这是下面一切的基础。
2. 图形处理和图形渲染管线技术
2.1 图形处理
图形处理分为几何造型(3D Geometric Models),动画,光照,纹理,表面信息,图像存储和显示几大块。关系如下图所示。
其中3D几何模型经过一些映射方法,应用材质性质等处理后可以进行渲染。
3D动画是进行一些物体运动的设计,捕捉和计算。
纹理信息包括扫描图像纹理,程序计算所得纹理和手绘纹理等。
表面信息包括Alpha-Blended透明效果,反射,折射和散射信息的计算。下图是这四种状态。
光照包含光源种类(点光源point,方向光源directional和聚光灯光源spot),光源位置信息,光源颜色,强度信息。
2.2 图形渲染管线
图形渲染管线可以大致理解为定点数据经过定点变换后进入图元装配,并进行纹理映射、片元处理、光照等操作后,进行光栅化并输出倒帧缓冲里。
具体在顶点变换时其实过程还是比较复杂的,不能一笔带过。
比如最初送进来的是模型坐标系(Model Coodinates)坐标信息,经过模型变换(Model Transform)后变为世界坐标(World Coodinates)。模型变换通过对模型执行平移(translation)、缩放(scale)、旋转(rotation)、镜像(reflection)、错切(shear)等操作,来调整模型的过程。通过模型变换,我们可以按照合理方式指定场景中物体的位置等信息。
这里插一段对模型变换的解释。我们为什么需要模型变换?我们在OpenGL中通过定义一组顶点来定义一个模型,或者通过其他3D建模软件事先建好模型然后导入到OpenGL中。顶点属性定义了模型。那么如果我们要在一个场景中不同位置、不同的比例、不同角度显示同一个模型怎么办?如果继续以类似的顶点属性数据定义同一个模型,调整它满足上述需求的话,不仅浪费显卡内存,而且效率很低。所以,我们定义的模型根据需要可以进行放大、缩小等操作来不同比例显示,可以通过平移来放在不同位置,可以通过旋转来按不同角度显示。这种方式就是执行模型变换。
世界坐标(World Coodinates)经过视变换(View Transform)后得到眼坐标系(Eye Coodinates)。OpenGL成像采用的是虚拟相机模型,但这个相机并不存在。在现实生活中,我们通过移动相机来拍照,而在OpenGL中我们通过以相反方式调整物体,让物体以适当方式呈现出来。
在世界坐标系中,对于顶点进行顶点光照处理(Per-vertex Lighting)后,进行投影变换(Projection Transform),得到裁剪坐标系(Clip Coodinates)。这部分内容后期再着重介绍。
之后经过透视除法或者也可以叫做(Homegeneous Division)得到规范化设备坐标系(Noramlized Deviced Coodinates)坐标信息。这些坐标信息经过视口变换(Viewport Transform)后得到最终屏幕坐标系(Screen Coodinates)坐标信息。视口变换主要是将视景体内投影的物体显示到二维的视口平面上。在计算机图形学中,它的定义是将经过几何变换, 投影变换和裁剪变换后的物体显示于屏幕指定区域内。就好比照片拍好了,要确定照片的大小,放大照片还是缩小照片,也就是把图形画下来,是要占据整个屏幕还是屏幕的一部分。注意和视变换(View Transform)区别开。
这些过程便是顶点变换全过程。之后继续进行最初我们讲的光栅化,片元处理,纹理处理,片源光照等处理后进行光栅处理,最后输出到帧缓冲(Framebuffer)中去。
下图为全管线流程图:
OpenGL渲染管线流程图
到此为止就已经将前期知识都讲解完毕,请务必记号管线渲染技术流程图,很重要,很重要,很重要,说三遍!
3. OpenGL图形编程
我想现在你已经彻底对图形学懵了对吧,各种坐标,各种变换,各种不知道干什么用的名词。没关系,先了解一下,现在进入OpenGL 图形编程。
3.1 几何 vs. 拓扑
假设原始图形如下图所示:
如果改变几何,就是改变顶点位置,比如我们移动顶点3的位置到下图所示位置:
几何改变
这个时候图形的几何形状发生改变,但顶点连线顺序却仍然相同,为1-2-3-4-1.
拓扑变化则是不变化顶点位置,变换连线方式,比如由1-2-3-4-1变为1-2-4-3-1:
拓扑改变
总结来讲,几何就是描述事物在哪儿,比如坐标位置等,拓扑就是这些事物的连接方式是怎样。
了解了这两个概念,后边在OpenGL编程时会用到。
3.2 3D坐标系
3D坐标系分为左手坐标系和右手坐标系,具体情况如下图所示:
我们后期讲解都是基于右手坐标系的。至于为什么,我的老师当初是这样解释的:
就是这么任性,辛普森用的,我们也要用,哈哈哈?
上一节我们提到过模型变换时有旋转操作,那么在坐标系中,旋转的方向是怎样呢?比如旋转90度,到底向左是正还是右是正呢?我们可以用右手法则来进行正方向判断:
图中所示就是右手坐标系中旋转正方向。
3.3 3D绘制
下面到代码讲解。最基础的图形学,总要先画最基础的图形吧,那么现在来看以下代码:
看到glBegin括号里GL_LINE_STRIP了吗?顾名思义,画的是一条连线。glVertex3f是用来绘制顶点,坐标为括号里的值,这里我们画了v0,v1,v2,v3,v4 四个顶点,并将其按照顺序连接成以v0为首,v4为尾的一条线段。
我们改变括号里的参数,那么就可以画出不同的图形。对照表如下所示:
注意,在gl_Begin和gl_End中间是所绘制使用的顶点。比如现在画两个三角形,那么在画第一个三角形时,顶点使用v0, v1,v2,那么gl_Begin和gl_End代码中间需要添加的就是绘制的就是v0, v1,v2 的顶点代码,对应第二个三角形再加的就是第二个三角形顶点的绘制代码。
在这里我们再结合图形几何和拓扑的概念理解一下,图形的几何发生改变其实改变的是顶点坐标位置,也就是glVertex3f函数中参数的值。拓扑发生改变其实是gl_Begin和gl_End中间夹着的顶点代码的顺序,比如先画v0, 再画v1,最后画v2 ,还是先画v1, 再画v2,最后画v0 .
下面解释一下第一行:glColor函数。我们可以看到这里它的三个参数我是用r,g,b代替的。那么rgb是什么呢?r是red 红,g是green 绿,b是blue 蓝。小学学过,光的三原色,由他们可以组合出任意颜色。需要提到的是在OpenGL glColor中,颜色rgb的范围都是在0?1之间不会小于0,也不会大于1. 0代表程度无,1代表程度最深。颜色叠加图我这里就不多做介绍了,上过小学中学美术的都该知道。
OK。到目前为止,你已经能够用最简单的OpenGL代码绘制最简单的图形,甚至改变颜色啦。
又来提醒你,刚刚我们提到过模型变换还记得吗?我们通过平移,旋转,缩放来实现模型变换。那么我们如何使用OpenGL进行平移,旋转,缩放呢?
答案在这里:
下面我们来具体讲解,代码如下:
首先来看第一句,glMatrixModel()函数。这个函数其实就是对接下来要做什么进行一下声明,也就是在要做下一步之前告诉计算机我要对“什么”进行操作了,这个“什么”在glMatrixMode的“()”里的选项(参数)有3种模式: GL_PROJECTION 投影, GL_MODELVIEW 模型视图, GL_TEXTURE 纹理. 这里我们要做的是对模型视图的改变,所以参数是GL_MODELVIEW。待会儿我们再讲剩下的两个。
看下glTranslatef(), glRotatef()和glScalef()这三个函数。三个函数都末尾带有f,所以参数都是GLfloat型,也有末尾改成d的,参数就是GLdouble型。
首先glTranslatef()函数,三个参数的意思是沿着x,y,z轴平移的分量。
glRotatef()函数,第一个参数为旋转角度,后面三个参数表示沿着从(0,0,0)到(x,y,z)的向量方向旋转。再明确一点其实就是,x,y,z表达的意思并不是坐标点,而是要围绕哪个坐标轴旋转。还记得刚刚的右手法则吗?从坐标(0,0,0)即原点,引出一条线到(x,y,z),用右手握住这条线。假设xyz为(1,0,0),右手大拇指指向(0,0,0)至(1,0,0)的方向才握。另外四个手指的弯曲指向即是物体旋转方向。为什么是右手握住,而不是左手呢?因为OpenGL遵循的是右手法则。到这儿你就理解我为什么先给各位说坐标系和右手法则的事情了,如果刚刚你没认真看,现在看一下,再理解这一段。
最后一个glScalef(),三个参数就是x,y,z轴放大缩小的倍数。
重点是执行顺序问题,顺序是按照由下至上的顺序来的,也就是代码图中123的顺序来的。顺序看似是没有区别的,事实上区别很大。
看这个例子就知道为什么顺序会影响最终绘制结果了:
现在为止讲完第一个glMatrixModel(GL_MODELVIEW)。下一个就是glMatrixModel(GL_PROJECTION)。如果参数是GL_PROJECTION,这个是投影的意思,就是要对投影相关进行操作,进行投影变换,也就是把物体投影到一个平面上,就像我们照相一样,把3维物体投到2维的平面上。这样,接下来的语句可以是跟透视相关的函数。
我们这里介绍两个函数:
glOrtho( xl, xr, yb, yt, zn, zf );
gluPerspective( fovy, aspect, zn, zf );
第一个函数glOrtho是正交投影或平行投影,第二个gluPerspective是透视投影。
先说第一个glOrtho。创建一个平行视景体(就是一个长方体空间区域)。实际上这个函数的操作是创建一个正射投影矩阵,并且用这个矩阵乘以当前矩阵。只有在视景体里的物体才能显示出来,最后两个参数改成0,0后,视景体深度没有了,整个视景体都被压成个平面了,当然就显示不正确了。
第二个函数gluPerspective。创建一个表示对称透视视图平截头体的矩阵,并把它与当前矩阵相乘。fovy是YZ平面上视野的角度,范围0-180°。好的取值范围一般是50-100°。aspect是这个平截头体的纵横比,也就是宽度除于高度。near和far值分别是观察点与近侧裁剪平面以及远侧裁剪平面的距离(沿Z轴负方向)这两个值都是正的。
到此讲完第二个glMatrixModel(GL_PROJECTION)。第三个GL_TEXTURE暂时各位先记着,等讲到纹理时再详细阐述。
代码现在可以扩充成以下代码了:
现在也就是已经做过模型变换,投影变换了,还缺视变换和视口变换。下面我们就来做这两组变换。
介绍一个函数:
gluLookAt( ex, ey, ez, lx, ly, lz, ux, uy, uz );
这个函数第一组eyex, eyey,eyez 相机在世界坐标的位置,第二组centerx,centery,centerz 相机镜头对准的物体在世界坐标的位置,第三组upx,upy,upz 相机向上的方向在世界坐标中的方向。你把相机想象成为你自己的脑袋:第一组数据就是脑袋的位置,第二组数据就是眼睛看的物体的位置,第三组就是头顶朝向的方向(因为你可以歪着头看同一个物体)。这个就是进行视变换的。
再介绍一个函数:
glViewport( ixl, iyb, idx, idy );
调用glViewPort函数来决定视见区域,告诉OpenGL应把渲染之后的图形绘制在窗体的哪个部位。当视见区域是整个窗体时,OpenGL将把渲染结果绘制到整个窗口。这个函数进行视口变换。
这个函数前两个函数是指定视口的左下角位置的,一般为0,0。WindowsGDI中的窗口坐标(0,0)是左上角,而OpenGL所定义的(0,0)是左下角。所以这个函数在这个图中是定义左上角的。后两个参数就是宽度和高度。
这一节结束前再介绍一组有用的函数:
glPushMatrix()和glPopmatirx()
假设我们想要绘制太阳系,中间是太阳,静止不动,地球围绕太阳旋转,月亮围绕地球旋转。
我们首先将坐标系移动到太阳的位置,绘制太阳,再将坐标系移动到地球的位置,绘制地球,然后将坐标系移动到月亮的位置,绘制月亮;如果还有金星、木星、火星呢,他们也都是以太阳为中心旋转,这样子,我们可以绘制完月亮之后再将坐标系回退到绘制地球的时候的坐标系,移动相应的位置,绘制金星,然后再回退或者移动新位置绘制木星。这样的操作不但麻烦,而且容易出错。
那怎么办最好呢,其实就是绘制地球、金星、木星、火星等的时候以太阳为坐标原点,在绘制地球之前先把当前的模型视图矩阵压入堆栈中保存下来(glPushMatirx),这样你在进行变换就不会影响到堆栈中的矩阵,这个时候将坐标系移动到地球的位置绘制地球,绘制完成之后将模型视图矩阵堆栈中的栈顶矩阵(就是我们刚才保存的矩阵)弹出(glPopMatrix),恢复原来的坐标系,再压入堆栈,绘制金星,再弹出。
基本上这一讲已经结束了。各位对OpenGL该有个了大致的思路,并且也想尝试些一些代码了吧。那就动手吧,下期我会将我的github放出来,供大家看一些示例代码。
最后放出一些和本讲有关的,但不影响大家学习OpenGL的小知识,有兴趣的了解下,没兴趣的略过吧。
你只知道有红绿蓝三个颜色叠加出别的颜色,所以glcolor参数是rgb。不知道还可以由白光通过减法减出各种颜色吧。
在管线技术流程图里,提到个名词叫光栅化,可能很多人不明白。光栅化就是把顶点数据转换为片元的过程。片元中的每一个元素对应于帧缓冲区中的一个像素。那么顶点怎么转换为片元的呢?如下图,英语好的自己看吧:
对于fragment processing,在学习了shader之后,你会更加了解通过shader你做了很多你自己的fragment processing。