系列推荐文章:
图像渲染的实现
先看用一个平面着色器渲染出的一个甜甜圈
代码实现:
main
函数,程序入口。所以OpenGL
处理图形、图像都是链式形式,以及基于OpenGL
封装的图像处理框架也是链式编程
gltSetWorkingDirectory(argv[0]); glutInit(&argc, argv); // 初始化窗口 glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL); glutInitWindowSize(800, 600); glutCreateWindow("ZB"); // 注册函数 glutReshapeFunc(ChangeSize); glutSpecialFunc(SpecialKeys); glutDisplayFunc(RenderScene); GLenum err = glewInit(); if (GLEW_OK != err) { fprintf(stderr, "GLEW Error:%s\n", glewGetErrorString(err)); return 1; } // 主动触发,准备工作 SetupRC(); // 一个无限执行的循环,负责一直处理窗口和操作系统的用户输入等操作 glutMainLoop(); return 0;复制代码
changeSize
通过glutReshapeFunc
注册为重塑函数,当第一次创建窗口或屏幕大小发生改变时,会调用该函数调整窗口大小/视口大小
// 保证高度不能为0 if (h == 0) { h = 1; } // 将视口设置为窗口尺寸 glViewport(0, 0, w, h); // 创建投影矩阵,并将它载入投影矩阵堆栈中 viewFrustum.SetPerspective(35, float(w)/float(h), 1, 1000); projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix()); // 初始化渲染管线 transformPipeline.SetMatrixStacks(modelViweMatix, projectionMatrix);复制代码
SetupRC
设置需要渲染图形相关顶点数据、颜色值等,手动在main
函数调用
// 1. 设置背景色 glClearColor(0.3, 0.3, 0.3, 1); // 2. 初始化着色器管理器 shaderManager.InitializeStockShaders(); // 3. 将相机向后移动7个单元,肉眼到物体的距离 viewFrame.MoveForward(5.0); // 4. 创建一个甜甜圈 /** void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor); 参数1: GLTriangleBatch 容器帮助类 参数2: 外边缘半径 参数3: 内边缘半径 参数4、5: 主半径和从半径的细分单元数量 */ gltMakeTorus(torusBatch, 1, 0.3, 88, 33); // 5. 点的大小(方便点填充时,肉眼观察) glPointSize(4.0);复制代码
RenderScene
通过glutDisplayFunc
注册为渲染函数。当屏幕发生变化或者开发者主动渲染会调用此函数,用来实现数据->渲染过程
// 1. 清除窗口和深度缓冲区 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 2. 把摄像机矩阵压入模型矩阵中,压栈 -- 存储一个状态 modelViweMatix.PushMatrix(viewFrame); // 3. 设置绘图颜色 GLfloat vRed[] = {1, 0, 0, 1}; // 4. 使用平面着色器 shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed); // 5. 绘制 torusBatch.Draw(); // 6. 出栈,绘制完成恢复 出栈 -- 恢复一个状态 modelViweMatix.PopMatrix(); // 7. 强制执行缓存区 glutSwapBuffers();复制代码
到这里为止,编译运行就能过出现上图所示的效果图。利用的是平面着色器。 相当的low。
下面在此基础上进行酷炫的一波操作。
在main
函数中注册了一个函数SpecialKeys
,顾名思义,特殊键位,这里控制的是上下左右键位
// 1. 判断方向 if (key == GLUT_KEY_UP) { // 2. 根据方向调整观察者位置 // 参数1: 旋转的弧度 // 参数2、3、4:表示绕哪个轴进行旋转 viewFrame.RotateWorld(m3dDegToRad(-5), 1, 0, 0); } if (key == GLUT_KEY_DOWN) { viewFrame.RotateWorld(m3dDegToRad(5), 1, 0, 0); } if (key == GLUT_KEY_LEFT) { viewFrame.RotateWorld(m3dDegToRad(-5), 0, 1, 0); } if (key == GLUT_KEY_RIGHT) { viewFrame.RotateWorld(m3dDegToRad(5), 0, 1, 0); } // 3. 重新刷新 glutPostRedisplay();复制代码
看实现效果
在来一波更真实的操作,我们使用默认光源着色器来实现
// 1. 清除窗口和深度缓冲区 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 2. 把摄像机矩阵压入模型矩阵中 modelViweMatix.PushMatrix(viewFrame); // 3. 设置绘图颜色 GLfloat vRed[] = {1, 0, 0, 1}; // 4. 使用平面着色器// shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed); // 4.1 使用默认光源着色器 // 通过光源、阴影效果跟体现立体效果 // 参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器 // 参数2:模型视图矩阵 // 参数3:投影矩阵 // 参数4:基本颜色值 shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed); // 5. 绘制 torusBatch.Draw(); // 6. 出栈,绘制完成恢复 modelViweMatix.PopMatrix(); // 7. 强制执行缓存区 glutSwapBuffers();复制代码
效果图如下:
可以看出,我们的渲染出了问题。
问题分析
在使用默认光源着色器时,由于产生了光照,有光照的一面,按照原本的颜色显示,而背光面,则是黑暗的,我们看不见的。其实很好理解,太阳光照地球,迎光面是白天,背光面是黑夜。
在绘制3D场景的时候,我们需要决定哪些部分是对观察者可见的,或者哪些部分是对观察者不可见的,对于不可见的部分,应该及早丢弃。例如在一个不透明的墙壁后,就不应该有渲染,这种情况叫做隐藏面消除
下面讨论一下解决这个问题的方案。
解决问题的方案
油画算法
先绘制场景中离观察者较远的物体,在绘制较近的物体,如下图
绘制顺序依次是红、黄、灰,这样的话按序渲染能过解决隐藏面消除的问题。
但是随之而来的会有一些不好的问题出现
- 效率很低,重叠部分会进行多次绘制渲染,浪费资源
- 对于某些存在场景,无法区别远近顺序的,无法用该方法解决问题,如下图
正背面剔除
首先需要确定一个问题,任何平面都有2个面,正面/背面,意味着你一个时刻只能看到一面。
一个立方体图形,从任何一个方向去观察,最多可以看到3个面,意味着其他看不到的面,我们不需要去绘制它,如果能以某种方式去丢弃这部分数据,OpenGL
在渲染的性能即可提高50%。
没错,OpenGL
能够区别正面和背面,通过分析顶点数据的顺序
正面/背面区分
- 正面:按照逆时针顶点链接顺序的三角形面
- 背面:按照顺时针顶点连接顺序的三角形面
立方体中的正背面
分析:
- 左侧三角形顶点顺序为:1->2->3; 右侧三角形的顶点顺序为:1->2->3
- 当观察者在右侧时,则右边的三角形方向为逆时针方向为正面,而左侧的三角形为顺时针则为反面
- 当观察者在左侧时,则左边的三⻆形方向为逆时针⽅方向为正面,⽽右侧的三角形为顺时针则为背面
总结: 正面和背面是由三角形的顶点定义顺序和观察者方向共同决定的,随着观察者的角度方向的改变,正面背面也会跟着改变
相关代码
// 开启表面剔除(默认背面剔除)void glEnable(GL_CULL_FACE);// 关闭表面剔除(默认背面剔除)void glDisable(GL_CULL_FACE);// 用户选择剔除那个面(即可自定义剔除,默认为正面)void glCullFace(GLenum mode);mode参数为:GL_FRONT, GL_BACK, GL_FRONT_AND_BACK, 默认为GL_BACK// 用户也可以指定正面void glFrontFace(GLenum mode);mode参数为:GL_CW, GL_CCW, 默认为GL_CCW// 剔除正面实现glCullFace(GL_BACK);glFrontFace(GL_CW);或glCullface(GL_FRONT);复制代码
具体代码实现
// 1. 清除窗口和深度缓冲区 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 开启正背面剔除 glEnable(GL_CULL_FACE); glFrontFace(GL_CCW); glCullFace(GL_BACK); // 2. 把摄像机矩阵压入模型矩阵中 modelViweMatix.PushMatrix(viewFrame); // 3. 设置绘图颜色 GLfloat vRed[] = {1, 0, 0, 1}; // 后面代码和上面一样,不再重复复制代码
实现效果如下图:
可以看到,之前的问题已经解决了,可是又面临了一个尴尬的问题,这个甜甜圈貌似有个很大的缺口,了解过图形渲染的读者肯定知道,这是深度问题,下面来了解一下。
深度
深度就是该像素点在3D世界中距离摄像机的距离,也就是Z值。
深度缓冲区就是一块内存区域,专门存储着每个像素点(绘制在屏幕上的)深度值Z。Z越大,则距离屏幕越远。那么为什么需要深度缓冲区?
在不实用深度测试的时候,如果我们先绘制一个距离比较近的物体,在绘制距离远的物体,则距离远的位图因为后绘制,会把距离近的物体覆盖掉。有了深度缓冲区后,绘制物体的顺序就不那么重要了。上面出现的大缺口,也就是这个问题造成的。实际上,只要存在深度缓冲区,OpenGL
都会把像素的深度值写入到缓冲区中,除非调用glDepthMask(GL_FALSE)
来禁止写入。
深度测试
深度缓冲区和颜色缓冲区是对应的。颜色缓冲区存储像素的颜色信息,而深度缓冲区存储像素的深度信息。在决定是否绘制一个物体表面时,首先要将表面对应的像素的深度值与当前深度缓冲区中的值进行比较,如果大于深度缓冲区的值,则丢弃这部分,否则利用这个像素对应的深度值和颜色值,分别更新深度缓冲区和颜色缓冲区。这个过程称为深度测试。相关代码
// 开启深度测试glEnable(GL_DEPTH_TEST);// 在绘制场景前,清除颜色缓冲区和深度缓冲区glClearColor(0, 0, 0, 1);glClear(GL_COLOR_BUFFER_BIT | GL_GEPTH_BUFFER_BIT);复制代码
清除深度缓冲区默认值为1.0,表示最大的深度值,深度值的范围为(0,1)之间。值越小表示越靠近观察者,反正表示距离观察者越远。
下面有关深度测试的判断式
指定深度测试判断模式
void glDepthFunc(GLEnum mode);
打开/阻断 深度缓冲区写入void glDepthMask(GKBool value);
value :GL_TURE
开启写入GL_FALSE
关闭写入
最终的实现效果如下:
ZFighting闪烁问题
为什么会出现ZFighting闪烁问题
因为开启深度测试后,OpenGL
就不会去绘制模型被遮挡的部分,这样实现现实更加真实,但是由于深度缓冲区精度的限制,对于深度相差无几的情况下,OpenGL
就可能出现不能正确判断两者深度值,会导致深度测试的结果不可预测,现实出来的现象会交错闪烁。
解决方式
- 第一步:启用
Polygon Offset
方式解决 让深度值之间产生间隔,可以理解为在执行深度测试前,将立方体的深度值做一些细微的增加,于是就能将重叠的2个图形深度值之间有所区分。
// 启用Polygon Offset方式glEnable(GL_POLYGON_OFFSET_FILL);参数列表:GL_POLYGON_OFFSET_POINT 对应光栅化模式:GL_POINTGL_POLYGON_OFFSET_LINE 对应光栅化模式:GL_LINEGL_POLYGON_OFFSET_FILL 对应光栅化模式:GL_FILL复制代码
-
第二步:指定偏移量
- 通过
glPolygon Offset
来指定.glPolygon Offset
需要2个参数:factor
,units
. - 每个
Fragment
的深度值都会增加如下所示的偏移量:Offset = ( m * factor ) + ( r * units);m : 多边形的深度的斜率的最大值,理解一个多边形越是与近裁剪⾯平行,m就越接近于0.r : 能产生于窗口坐标系的深度值中可分辨的差异最小值.r是由具体是由具体OpenGL
平台指定的一个常量. - 一个⼤于0的
Offset
会把模型推到离你(摄像机)更远的位置,相应的⼀个小于0的Offset
会把模型拉近 - 一般⽽言,只需要将-1.0 和 -1 这样简单赋值给
glPolygon Offset
基本可以满⾜足需求.
- 通过
-
第三步:关闭
Polygon Offset
glDisable(GL_POLYGON_OFFSET_FILL);复制代码
OK,到此为止,我们完美的把这个甜甜圈给渲染出来了。上面遇到的一些问题也得已解决。