最近需要用OpenGL做一个手部模型,从而得到一个手部深度图的数据库,所以把从头到尾的学习笔记放在这里。
安装VS2017
直接看这两篇安装教程、无法登陆的问题
opengl安装
配置
解决的两个错误:无法解析的外部符号 _WinMain@16、丢失opengl.dll
opengl教程
教程
安装java
1662 sudo add-apt-repository ppa:webupd8team/java
1663 sudo apt-get update
1664 sudo apt-get install oracle-java8-installer
1665 sudo update-java-alternatives -s java-8-oracle
1666 java -version
1667 javac -version
安装netbean
1641 chmod +x netbeans-8.2-cpp-linux-x64.sh
1643 sh -c "/home/chen/Downloads/netbeans-8.2-cpp-linux-x64.sh"
安装glfw,glut,glew等库,安装在user/include/GL下user/local/include/GLFW下
1648 unzip glfw-3.2.1.zip -d glfw-3.2.1
1649 sudo apt-get build-dep glfw
1650 sudo apt-get install cmake xorg-dev libglu1-mesa-dev
1652 sudo apt-get install g++ freeglut3-dev glew1.5-dev libmagick++-dev libassimp-dev libglfw-dev
1653 sudo ln /usr/lib/pkgconfig/libglfw.pc /usr/lib/pkgconfig/glfw3.pc
1654 apt-get install libglew1.6 libglew1.6-dev
1655 sudo apt-get install libglew1.6 libglew1.6-dev
1656 sudo cmake ../
1657 make && make install
1658 sudo make && make install
1659 sudo make
1660 sudo install
1661 sudo make install
然后在netbean下就可以引入这些包了
OPENGL的教程——Ubuntu下搭建环境
上面这个链接中第17章是一个分水岭,从这一章起的代码更规范,结构更完整与复杂,这里描述一下我对程序运行流程的理解。
3d模型解析
核心内容摘要
目前常用的3D游戏模型制作软件有Autodesk、3D Max、Maya等,发布的模型格式主要有OBJ、FBX、3DMAX、3DS、DAE等等,这些模型在Unity等游戏引擎中可直接导入使用,但在OpenGL和DirectX等底层图形库游戏开发中无法直接导入游戏模型,需要自行解析模型数据并在工程中渲染,然后进行游戏开发。
3d模型实际上是一组数据的集合,模型的解析即读取模型对应的数据并存储,然后使用模型的数据在OpenGL或DirectX等环境下进行渲染实现模型的导入。3d模型的数据主要有顶点、法线、纹理坐标和材质使用信息等。
OBJ是一种相对比较简单的3d模型格式,模型的数据信息存储在纯文本中,文本中每一行的前缀来表示不同的模型信息,如:v表示一个顶点信息,vn表示一个法线向量,vt表示一个纹理坐标,f表示一个表面(Face)等等。
(1) #表示注释,可忽略;
(2) mtllib用来指明模型的材质;
(3) v表示顶点,对应x,y,z三个坐标轴上的坐标值;
(4) vt表示纹理贴图的坐标;
(5) vn表示顶点法线,用于确定光照方向;
(6) f表示平面图元,一般每一行对应三组数据,表示基本图元为三角形,如果四组数据表示基本图元为四边形,以此类推,也存在两种以上图元混合的情况。每一组数据对应三个数据,分别为:顶点索引、顶点法线索引、纹理贴图索引,用’/’分隔开,其中法线索引和纹理贴图索引可缺省。通过索引可以搜索到上面对应的顶点坐标、顶点法线和纹理贴图坐标,如此表示可以减少顶点等数据的重复表示,大大节省空间。
OBJ模型的解析即调用文件流读取OBJ文件中的文本信息,将顶点、顶点法线、面、顶点索引、顶点法线索引等数据信息分离开并封装保存到程序中。这里定义相应的数据结构,保存顶点、法线等数据信息,使用函数统计模型中顶点、法线、面等元素的数量,之后使用C++的指针定义动态数组作为不同数据的存储容器,存储从模型解析的数据,用于模型的绘制和渲染。
根据解析后的数据,可得到每个多边形图元对应的顶点坐标以及对应顶点法线向量,使用OpenGL绘制出所有的多边形图元即可将模型绘制出来,通过基本的图形变换调整绘制出的模型的位置使其显示在窗口中。
(1)glGenBuffers;glBindBuffer;glBufferData;
这些函数都是用来创建、设置buffer,然后往里面填充数据的
创建Vertices,Indices,都是一样的三个步骤
(2)glUniformMatrix4fv
计算并把变换矩阵加载到shader程序中
这里的变换矩阵包含了接下来讲的a、b、c、d的内容
(3)glEnableVertexAttribArray
渲染管线将使用索引获取数据
(4)glBindBuffer
每一帧都需要不断更新管线的状态。
(5)glVertexAttribPointer
管线解析buffer
(6)glDrawArrays;glDrawElements
整合这个指令收到的绘制参数和之前为这一个点的图形建立的状态数据来将结果渲染在屏幕上。
前一个是顺序绘制,后一个是索引绘制
先在回调函数中计算变换矩阵,然后在glUniformMatrix4fv中将计算的变换矩阵传递到shader中的一致变量。
光栅器对从顶点着色器传来的变量进行插值,并执行片断着色器遍历三角形的每一个像素,设置顶点的颜色值为以顶点位置为参数的一个函数。
复合变换的时候注意:通过先旋转后移动可以避免这两个操作的相互依赖性,这也是尽量围绕原点对称建模的原因,那样当你缩放或者旋转物体不会产生副作用,缩放和旋转后物体依然保持和之前一样对称。
代码中用Pipeline管线类抽象出了一个物体的所有变换的数据信息。现在有3个私有vector成员变量分别来存储物体在世界空间中的缩放比例,位置和每个像素的旋转角度。另外有接口函数来设置他们的值,以及可以获取表示所有变换最终的复合变换矩阵。复合变换矩阵用glUniformMatrix4fv传递到shader中的一致变量。
完整版本的讲解
下面是上面讲解的摘要
在计算机三维图像中,投影可以看作是一种将三维坐标变换为二维坐标的方法,常用到的有正交投影和透视投影。正交投影多用于三维健模,透视投影则由于和人的视觉系统相似,多用于在二维平面中对三维世界的呈现。
可以想象视平面为透明的玻璃窗,视点为玻璃窗前的观察者,观察者透过玻璃窗看到的外部世界,便等同于外部世界在玻璃窗上的透视投影
实际应用中,往往取位于两个横截面中间的棱台为可视区域(如图4所示),完全位于棱台之外的物体将被剔除,位于棱台边界的物体将被裁减。该棱台也被称为视椎体,它是计算机图形学中经常用到的一个投影模型。
x轴指向屏幕的右方,y轴指向屏幕的上方,z轴指向屏幕外(右手坐标系)
透视投影的一般模型研究视点E在任意位置,任意姿态下透视图的生成算法。思路很简单,先将一般模型变换为标准模型,然后使用标准模型的透视投影公式便能计算透视结果。下面研究一般模型变换为标准模型的数学公式。
投影变换矩阵——用矩阵表示xp、yp
OpenGL中的解决办法是将这个变换分解成两步:先乘以一个投影变换矩阵,然后再单独除以Z分量的值。我们的应用中会提供那个投影变换矩阵,shader中要进行顶点和投影变换矩阵相乘的这个步骤,除以Z分量的单独步骤在GPU中是固定的,而且是在光栅器中进行(在顶点着色器和片段着色器之间的某个地方)。
我们要做的是找到上面只关于X和Y两个分量的投影变换矩阵。乘以这个投影变换矩阵之后GPU之后会自动帮我们进行Z值得相除变换使我们可以得到我们想要的最终结果。
接下来还需要处理z轴上的剪切问题zNear、zFar
将顶点位置向量和投影变换矩阵相乘之后,坐标系将会变换到裁剪空间中,并且在透视分离之后坐标系会变换到NDC 空间(Normalized Device Coordinates:单位化设备坐标系)
接下来还需要从视平面转换为屏幕坐标
(1). 移动相机很简单,如果相机从原点移动到(x,y,z),那么相应物体的变换矩阵就应该是(-x,-y,-z)。原因很简单:相机从原点按照向量(x,y,z)平移,所以然后再将相机移回原点同时又保证和物体相对位置不变的话,那么物体就要按照相反的方向向量(-x-y-z)进行平移。对应的变换矩阵如下:
(2).然后是旋转相机朝向世界坐标系中定义的一些物体。实现的方法称作‘UVN相机’
相机的键盘控制在Camera::OnKeyboard函数中定义,在代码中把键盘事件传递给相机,在渲染回调函数中相机的那几个向量属性值都直接从相机类中获取。
鼠标控制也是类似的,补充以下,通过鼠标坐标变化计算相机角度变化的背景知识
为了实现纹理贴图我们需要做三件事:
(1)将一张贴图加载到OpenGL中
(2)提供纹理坐标和顶点(将纹理对应匹配到顶点上)GPU要做的就是让纹理紧跟三角形图元顶点的移动使其看上去真实。在GPU光栅化三角形阶段,会对纹理坐标进行插值计算并覆盖到整个三角形面上,并且在片段着色器中开发者要将这些坐标跟纹理进行匹配。这个操作叫做‘取样’,取样的结果叫做‘纹素’(纹理中的一个像素)。为了增加通用性,在不改变纹理坐标的情况下随意更换纹理贴图。因此,纹理坐标是定义在‘纹理空间’的,也就是定义在单位化的[0,1]范围内
(3)并使用纹理坐标从纹理中进行取样操作取得像素颜色。
纹理对象绑定到‘纹理单元’上,‘纹理单元’的索引会被传到shader中,因此shader是通过纹理单元得到纹理对象,每个纹理单元可以同时绑定几个纹理对象。采样操作通常发生在片段着色器,对此有一组特殊的一致变量(取样器一致变量)
顶点缓冲器的顶点结构既包含位置数据同时又包含Vector2f类型的纹理坐标。
shader.vs将纹理坐标从顶点缓冲器传递到片段着色器。栅格器会对纹理坐标在整个三角形面上进行插值,并且每一个片段着色器都会和其特有的纹理坐标一起起作用。
顶点着色器中的layout变量声明与shader主渲染循环中的代码中的glEnableVertexAttribArray一一对应
光照模型模拟光照射到物体上的主要效果并使物体可见,基本的光照模型主要包括‘环境光/漫反射/镜面反射’。
(1)环境光也就被建模为一个没有光源、没有方向并且对场景中的所有物体产生相同的点亮效果的一种光。
(2)漫反射光强调的是光照射到物体表面的角度对物体亮度效果的影响,漫反射光最重要的特性就是光的方向。
(3)镜面反射光与其说是光本身的特性不如说是物体的一种属性,计算镜面反射光既要考虑光的入射角度又要考虑观察者的视角位置。
3D应用中使用的光源类型通常是有那三种光照模型和其他一些特殊属性的一些不同组合性质的光源。例如,一个手电筒会发出锥形的光源,离手电筒太远的物体根本不会被照亮。
当白光照射到物体表面上是时反射的颜色就是物体表面的颜色,但亮度会随着光源强度变化,但还是那个颜色。光只能暴露显示物体的实际颜色,但不能往上添加颜色。光源的颜色我们会定义为一个包含三个浮点数的三元组,浮点数介于[0,1]之间(之后会和物体表面的颜色相乘,相当于各通道的颜色饱和率),环境光的强度参数就可以定义为一个[0,1]之间的一个单一的浮点数
顶点着色器保持不变,还是负责传递位置(和WVP矩阵相乘之后)和纹理坐标。新的逻辑操作都放在了片断着色器中,这里唯一增添的部分是使用struct关键字定义了平行光的数据结构。结构体要保持和应用中定义的结构体一样这样应用程序才能和shader进行数据交流。然后这里有一个DirectionalLight类型的新的一致变量,变量由应用程序来进行更新。
漫射光使物体朝向它的那一面比其他背向光的面要更亮。漫射光还增加了一个光强度的变化现象,光的强度大小还取决于光线和物体表面的角度
有一个问题是对于尖锐的边缘,会看到边界的颜色变化不平滑,因此这个明显是需要进行优化的。
优化的办法中使用到一个概念叫做‘顶点法线’。顶点法线是共用一个顶点的所有三角形法线的平均值。事实上我们并没有在顶点着色器中计算漫射光颜色,而只是将顶点法线作为一个成员属性传给片段着色器。光栅器会得到三个不同的法向量并对其之间进行插值运算。片段着色器将会对每个像素计算其特定的插值法向量对应的颜色值。
在世界坐标系中来定义光线的方向才是最合理的,毕竟光线的方向决定于世界空间中某个地方的光源将光线投射到某个方向(甚至太阳都是在世界空间中,只是距离极远)。所以,在计算之前,我们首先要将法线向量变换到世界坐标系空间。所以平行光的数据结构,有两个新的成员变量:方向是定义在世界空间的一个3维向量,漫射光光照强度是一个浮点数(和环境光的用法一样)。
所以顶点着色器,有一个新的顶点属性:法向量,这个法向量要由应用程序提供。世界变换有其自己的一致变量我们要和WVP变换矩阵一并提供。
片段着色器接受到插值后并在顶点着色器中转换到世界空间的顶点法向量,执行光照计算。我们加入了环境光和漫射光的部分,并将结果和从纹理中取样得到的颜色相乘。
LightingTechnique类中有了三个新属性:眼睛(观察者)的位置、镜面反射强度和材料的镜面参数。
顶点着色器多了最后一行代码,世界变换矩阵(之前用来变换法线的那个世界变换矩阵)这里用来将顶点的世界坐标传给片段着色器
片段着色器中的变化是多了三个新的一致性变量,用来存储计算镜面光线的一些属性(像眼睛的位置、镜面光强度和镜面反射参数)。
环境光颜色的计算和前面两篇教程中的计算一样。
然后创建漫射光和镜面光颜色向量并初始化为0,之后只有当光线和物体表面的角度小于90度时颜色值才不为零。
计算世界空间中从顶点位置到观察者位置的向量,这个通过观察者世界坐标和顶点的世界坐标相减计算得到
镜面颜色值是通过将光源颜色和材料的镜面反射强度以及材料镜面反射参数相乘计算得到
从纹理中取样的颜色相乘得到最终的像素颜色。
镜面反射光颜色的使用很简单。在渲染循环我们得到了camera的位置(在世界空间中已经维护好了)并将它传给了LightingTechnique类。这里还设置了镜面反射强度和镜面参数。剩下的就由着色器来处理了。
之前已经学习了三个基本的光照模型(环境光,漫射光和镜面反射光),这三种模型都是基于平行光的。平行光只是通过一个向量来表示,没有光源起点,因此它不会随着距离的增大而衰减。光源类型,它有光源起点而且有衰减效果,距离光源越远光线越弱。点光源的经典例子是灯泡,真实光线的衰减是按照平方反比定律,我们添加了几个新的因素到公式中使对其的控制更加灵活:
现在总结计算点光源需要的步骤:
计算和平行光一样的环境光;
计算一个从像素点(世界空间中的)到点光源的向量作为光线的方向。利用这个光线方向就可以计算和平行光一样的漫射光以及镜面反射光了;
计算像素点到点光源的距离用来计算最终的光线强度衰减值;
将三种光叠加在一起,计算得到最终的点光源颜色,通过点光源的衰减性三种光看上去也可以被分离开了。
点光源则添加了世界坐标系中的位置变量和那三个衰减参数因子。
聚光灯光源也会随着距离衰减,但它不是像点光源照向四面八方的而是像平行光那样有一个聚光方向(相当于取点光源的一个锥形的一小部分),聚光灯光源呈锥形,因此有一个新的属性,就是离光源越远,照亮的圆形区域会越大(光源位于锥形体的尖端)。聚光灯光源,顾名思义,对应于现实中的聚光灯,例如:手电筒。
一个真实的聚光灯光源会从照亮区域的中心向圆形边缘慢慢衰减。
在片段着色器上:
聚光灯光源的结构体继承自点光源的结构体,并添加了两个属性和点光源区别开:一个是光源的方向向量,另一个是截断光源照亮范围的一个阈值。阈值代表的是光源方向向量和光源到可照亮像素之间的最大夹角。然后计算聚光灯光源效果和点光源颜色相乘计算得到最终的聚光灯颜色值。
在回调函数中:更新着色器程序