1.甜甜圈绘制
关于绘制图像的项目架构和主要方法在之前的文章中都介绍过了。这里只讲一下差异化的地方,重点介绍下面三个方法。甜甜圈的投影方式,选择使用透视投影。在这个例子中,我们不在改变物体,而是操作视角做相应的形变处理。
1.1 SetupRC
viewFrame视角帧,就是透视投影中人的视角位置。将视角后移,来显示物体,后移的位置越远,看到的物体也就越小(显示的图像越小)。gltMakeTorus是OpenGL提供给我们的一个方法,可以构建一个圆环面。
void SetupRC()
{
//1.设置背景颜色
glClearColor(0.3f, 0.3f, 0.3f, 1.0f );
//2.初始化着色器管理器
shaderManager.InitializeStockShaders();
//3.将相机向后移动7个单元:肉眼到物体之间的距离
viewFrame.MoveForward(7.0);
//4.创建一个甜甜圈
//void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
//参数1:GLTriangleBatch 容器帮助类
//参数2:外边缘半径
//参数3:内边缘半径
//参数4、5:主半径和从半径的细分单元数量
gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26);
//5.点的大小(方便点填充时,肉眼观察)
glPointSize(4.0f);
}
1.2 ChangeSize
void ChangeSize(int w, int h)
{
//1.防止h变为0
if(h == 0)
h = 1;
//2.设置视口窗口尺寸
glViewport(0, 0, w, h);
//3.setPerspective函数的参数是一个从顶点方向看去的视场角度(用角度值表示)
// 设置透视模式,初始化其透视矩阵
viewFrustum.SetPerspective(35.0f, float(w)/float(h), 1.0f, 100.0f);
//4.把透视矩阵加载到透视矩阵对阵中
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
//5.初始化渲染管线
transformPipeline.SetMatrixStacks(modelViewMatix, projectionMatrix);
}
1.3 SpecialKeys
与之前的例子不同,SpecialKeys中不再改变物体的坐标,而是改变视角viewFrame坐标。
void SpecialKeys(int key, int x, int y)
{
//1.判断方向
if(key == GLUT_KEY_UP)
//2.根据方向调整观察者位置
viewFrame.RotateWorld(m3dDegToRad(-5.0), 1.0f, 0.0f, 0.0f);
if(key == GLUT_KEY_DOWN)
viewFrame.RotateWorld(m3dDegToRad(5.0), 1.0f, 0.0f, 0.0f);
if(key == GLUT_KEY_LEFT)
viewFrame.RotateWorld(m3dDegToRad(-5.0), 0.0f, 1.0f, 0.0f);
if(key == GLUT_KEY_RIGHT)
viewFrame.RotateWorld(m3dDegToRad(5.0), 0.0f, 1.0f, 0.0f);
//3.重新刷新
glutPostRedisplay();
}
1.4 RenderScene
void RenderScene()
{
//1.清除窗口和深度缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//2.把摄像机矩阵压入模型矩阵中
modelViewMatix.PushMatrix(viewFrame);
//3.设置绘图颜色
GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
//4.使用默认光源着色器
//通过光源、阴影效果跟提现立体效果
//参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器
//参数2:模型视图矩阵
//参数3:投影矩阵
//参数4:基本颜色值
shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
//5.绘制
torusBatch.Draw();
//6.出栈 绘制完成恢复
modelViewMatix.PopMatrix();
//7.交换缓存区
glutSwapBuffers();
}
经过上面几个方法流程,最终运行项目会得到如前面所示的一样的效果图。但是当我们通过特殊键位,操作甜甜圈发生旋转时,就会出现下图所示的问题,我们发现甜甜圈上出现了黑色的阴影,这是什么原因呢?
2.隐藏面消除
在绘制3D场景的时候,我们需要决定哪些部分是对观察者可见的,或者哪些部分对观察者是不可见的。对于不可见的部分,应该及早丢弃。例如在一个不透明的墙壁后,就不该渲染。这种情况叫做“隐藏面消除”(Hidden surface elimination)。
通过上面的定义,我们就能理解为什么会出现上面的情况了。在使用默认光源着色器时,正对向我们的部分,显示的红色,背对我们的是黑色阴影,通过旋转,我们看到了本不该被看到的部分,所以会出现这种现象。那么应该如何避免这种情况呢?
2.1 油画算法
先绘制场景中的离观察者较远的物体,再绘制较近的物体。这样一层层的绘制,就可以解决隐藏面消除的问题。
2.2 正背⾯剔除(Face Culling)
2.2.1 分析
当我们从任意方向和角度上看一个立方体时,最多只能看到它的三个面。那么我们是不是可以只绘制这三个面呢?如果我们能以某种方式,丢弃不需要绘制的部分数据,OpenGL在渲染的性能上即可提高超过50%。
2.2.2 正/背面
任何平面都有两个面,正面和背面,在同一个时刻我们只能看到一个面。OpenGL可以做到检查所有正面朝向观察者的面,并渲染它们,从而丢弃背面朝向的面,这样可以节约偏远着色器的性能。
但是我们又如何让OpenGL知道那个是正面,哪个是背面呢?答案就是分析顶点数据的顺序。
在前面那分析三角形环绕方式的时候,我们知道了按照逆时针顺序连接顶点的面试正面,按照顺时针顺序连接顶点的面试背面。
请看下图立方体中的正背面:
- 左侧三角形的顶点顺序为1->2->3,右侧三角形的顶点顺序为1->2->3
- 当观察者在右侧时,右侧三角形的顶点顺序为逆时针,也就是正面。左侧三角形的顶点顺序为顺时针,为反面。
- 当观察者在做测试,则左侧三角形被判定为正面,右侧三角形呗判定为背面。
总结:
- 正面和背面是由三角形的顶点顺序和观察者方向功能决定的。随着观察者方向的改变,正背面也会跟着改变。
2.2.3 OpenGL 代码设置
- 开启表面剔除(默认背面剔除)
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
2.2.4 剔除背面
通过上面的分析,我们是不是可以解决上面的隐藏面消除问题了呢。我们对RenderScene中的代码做如下修改,然后运行查看一下效果。
void RenderScene()
{
//1.清除窗口和深度缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//开启/关闭正背面剔除功能
if (iCull) {
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glCullFace(GL_BACK);
}
else
{
glDisable(GL_CULL_FACE);
}
//2.把摄像机矩阵压入模型矩阵中
modelViewMatix.PushMatrix(viewFrame);
.......
}
在我们刚开始旋转甜甜圈的时候,发现之前的黑色阴影已经没有了,那是不是问题已经得到解决了呢?
3 深度测试
3.1 了解深度
- 什么是深度?
深度其实就是在OpenGL坐标系中,像素点的Z坐标距离观察者的距离。由于观察者可以放在坐标系中的任意位置,所以不能简单的说Z值越小,物体越靠近观察者。 - 什么是深度缓冲区?
深度缓冲区就是一块显存区域,专门存储着每个像素点(绘制在屏幕上的)深度值。 - 为什么需要深度缓冲区?
在不使用深度测试的时候,如果我们先绘制一个距离比较近的物体,再绘制距离比较远的物体,则距离远的物体因为后绘制,会把距离近的物体覆盖掉。这样显示的时候就会出现问题,所以要先绘制距离远的物体,再绘制距离近的物体。但是有了深度缓冲区,绘制的先后顺序就变得不那么重要了。
深度缓存区的默认值为1.0, 表示最⼤的深度值,深度值的范围是[0,1]之间。
3.2 深度测试
**深度缓冲区(DepthBuffer)和颜⾊缓存区(ColorBuffer)**是对应的。颜⾊缓存区存储像素的颜⾊信息,⽽深度
缓冲区存储像素的深度信息。 在决定是否绘制⼀个物体表⾯时, ⾸先要将表⾯对应的像素的深度值与当前深
度缓冲区中的值进⾏⽐较。 如果⼤于深度缓冲区中的值,则丢弃这部分。否则利⽤这个像素对应的深度值和颜
⾊值,分别更新深度缓冲区和颜⾊缓存区。这个过程称为”深度测试”。
开启深度测试:
glEnable(GL_DEPTH_TEST);
关闭深度测试:
glDisable(GL_DEPTH_TEST);
在开始绘制前,需要先清除深度缓冲区和颜色缓冲区。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
指定深度测试判断模式
void glDepthFunc(GLEnum mode);
打开/阻断 深度缓存区写⼊入
void glDepthMask(GLBool value);
value : GL_TURE 开启深度缓冲区写入; GL_FALSE 关闭深度缓冲区写入
3.3 修改调试
修改RenderScene中的代码,加入深度测试的代码,再次运行,旋转甜甜圈,发现不在出现缺一块的情况了。
// 召唤场景
void RenderScene(void)
{
//清除窗口和深度缓冲区
//可以给学员演示一下不清空颜色/深度缓冲区时.渲染会造成什么问题. 残留数据
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//开启/关闭正背面剔除功能
if(iCull)
{
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glCullFace(GL_BACK);
}
else
{
glDisable(GL_CULL_FACE);
}
//根据设置iDepth标记来判断是否开启深度测试
if(iDepth)
glEnable(GL_DEPTH_TEST);
else
{
glDisable(GL_DEPTH_TEST);
}
}
3.4 ZFighting闪烁
3.4.1 问题描述
由于深度缓冲区的精度限制,对于深度相差非常小的情况下,OpenGL就可能会出现不能正确判断两者的深度值,会导致深度测试的结果不可预知。显示图像时,图像交错闪烁。如下图所示:
3.4.2 解决
启用多边形偏移(Polygon Offset)
glEnable(GL_POLYGON_OFFSET_FILL)
参数列列表:
GL_POLYGON_OFFSET_POINT 对应光栅化模式: GL_POINT
GL_POLYGON_OFFSET_LINE 对应光栅化模式: GL_LINE
GL_POLYGON_OFFSET_FILL 对应光栅化模式: GL_FILL
Polygon Offset就是让深度值中间增加一些间隔,让OpenGL对重叠的两个图形能够加以区分。增加的偏移量的值是可以通过glPolygonOffset函数来指定的。一般⽽言,只需要将-1.0 和 -1 这样简单赋值给glPolygonOffset 的两个参数factor和units基本可以满⾜需求了。
void glPolygonOffset(Glfloat factor,Glfloat units);
应⽤用到⽚片段上总偏移计算⽅方程式:
Depth Offset = (DZ * factor) + (r * units);
DZ:深度值(Z值)
r:使得深度缓冲区产⽣生变化的最⼩小值
一个⼤于0的Offset会把模型推到离你(摄像机)更远的位置,相应的⼀个⼩于0的Offset 会把模型拉近
最后还要记得关闭Polygon Offset,因为相当于有一个全局状态机来控制,我们不能把所有的绘图过程都开启Polygon Offset。
glDisable(GL_POLYGON_OFFSET_FILL)