我们绘制甜甜圈时,会发现有一些问题?
在渲染过程中可能产生的问题?
在绘制3D场景时,我们需要决定哪些部分对观察者是可见的,或者说哪些部分对观察者不可见,对于不可见的部分,我们应该及早的丢弃,例如在一个不透明的墙壁后的物体就不应该渲染。这种问题称之为隐藏面消除(Hidden surface elimination)。
解决方案:油画算法
油画算法:先绘制场景中离观察者较远的,再绘制较近的物体。
如下图:先绘制红色部分,再绘制黄色部分,最后再绘制灰色部分,即可解决隐藏面问题
问题好像很容易就解决了,我们看下另一个图例
这个图例好像并不适合用油画算法,那么问题又来了
油画算法弊端
如果三个三角形是叠加的情况,油画算法将无法处理,解决方案:正背面剔除
正背面剔除(Face Culling)
一个3D图形,不管你从哪个方向去观察,最多只能看到3个面。如果采用背面剔除,那么观察者看不到的面就可以不用绘制,渲染性能能提高超过50%。
我们是如何知道哪些面显示,哪些不显示?
其实在OpenGL中默认规定了逆时针方向绘制的三角形是正面,
正⾯: 按照逆时针顶点连接顺序的三⻆形⾯
背⾯: 按照顺时针顶点连接顺序的三⻆形⾯
GLfloat vertices[] = {
// Clockwise
vertices[0], // vertex 1
vertices[1], // vertex 2
vertices[2], // vertex 3
// Counter-clockwise
vertices[0], // vertex 1
vertices[2], // vertex 3
vertices[1] // vertex 2
};
正方体的正背面
分析
• 左侧三⻆形顶点顺序为: 1—> 2—> 3 ; 右侧三⻆形的顶点顺序为: 1—> 2—> 3 .
• 当观察者在右侧时,则右边的三⻆形⽅向为逆时针⽅向则为正⾯,⽽左侧的三⻆形为顺时针则为.⾯ • 当观察者在左侧时,则左边的三⻆形为逆时针⽅向判定为正⾯,⽽右侧的三⻆形为顺时针判定为背⾯.
总结:
正⾯和背⾯是有三⻆形的顶点定义顺序和观察者⽅向共同决定的.随着观察者的⻆度⽅向的改变,正⾯背⾯也 会跟着改变。
我们可以改顺时针为正面,但是一般不建议这么操作,主要是由于这个设置不仅仅是作用于你的项目,而是作用于OpenGL全局的。我们习惯OpenGL中默认的即可。
//用于修改正面的函数
void glFrontFace(GLenum mode);
//model有两种:GL_CW(顺时针),GL_CCW(逆时针),
//OpenGL中的默认值:GL_CCW
正背面剔除常用的三个方法
1、开启正背面剔除
//开启表面剔除 (默认背面剔除)
void glEnable(GL_CULL_FACE);
2、关闭正背面剔除
//关闭表面剔除(默认背面剔除)
void glDisable(GL_CULL_FACE);
3、设置需要剔除的面
void glCullFace(GLenum mode);
mode参数为: GL_FRONT(正面),GL_BACK(背面),GL_FRONT_AND_BACK(正背面) ,默认GL_BACK
代码
1、头文件
#include "GLTools.h"
#include "GLMatrixStack.h"
#include "GLFrame.h"
#include "GLFrustum.h"
#include "GLGeometryTransform.h"
#include <math.h>
#ifdef __APPLE__
#include <glut/glut.h>
#else
#define FREEGLUT_STATIC
#include <GL/glut.h>
#endif
头文件 | 说明 |
---|---|
GLTools | GLTool.h头文件包含了大部分GLTool中类似C语言的独立函数 |
GLMatrixStack | 矩阵的工具类。可以利于GLMatrixStack 加载单元矩阵/矩阵/矩阵相乘/压栈/出栈/缩放/平移/旋转 |
GLFrame | 矩阵工具类,表示位置。通过设置vOrigin, vForward ,vUp |
GLFrustum | 矩阵工具类,用来快速设置正/透视投影矩阵,完成坐标从3D->2D 映射过程。 |
GLGeometryTransform | 变换管道类,用来快速在代码中传输视图矩阵/投影矩阵/视图投影变换矩阵等 |
<math.h> | 数学库 |
<glut/glut.h> | 在Mac 系统下,#include<glut/glut.h> 在Windows 和 Linux上,我们使⽤freeglut的静态库版本并且需要添加⼀个宏 |
2、全局变量
////设置角色帧,作为相机
GLFrame viewFrame;
//使用GLFrustum类来设置透视投影
GLFrustum viewFrustum;
GLTriangleBatch torusBatch;
GLMatrixStack modelViewMatix;
GLMatrixStack projectionMatrix;
GLGeometryTransform transformPipeline;
GLShaderManager shaderManager;
//标记:背面剔除、深度测试
int iCull = 0;
变量 | 说明 |
---|---|
viewFrame | 设置观察者视图坐标 |
viewFrustum | 设置图元绘制时的投影⽅式. |
torusBatch | 容器 |
modelViewMatix | 模型视图矩阵 |
projectionMatrix | 投影矩阵 |
transformPipeline | 变换管道。存储模型视图/投影/模型视图投影矩阵 |
shaderManager | 存储着色器管理工具类 |
3、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.设置绘图颜色
GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
//4.
//使用平面着色器
//参数1:平面着色器
//参数2:模型视图投影矩阵
//参数3:颜色
// shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed);
//使用默认光源着色器
//通过光源、阴影效果跟提现立体效果
//参数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();
}
4、SetupRC函数
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);
}
5、SpecialKeys函数
//键位设置,通过不同的键位对其进行设置
//控制Camera的移动,从而改变视口
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();
}
6、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);
}
7、ProcessMenu函数
void ProcessMenu(int value)
{
switch(value)
{
case 1:
iCull = !iCull;
break;
}
glutPostRedisplay();
}
8、main函数
int main(int argc, char* argv[])
{
gltSetWorkingDirectory(argv[0]);
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
glutInitWindowSize(800, 600);
glutCreateWindow("Geometry Test Program");
glutReshapeFunc(ChangeSize);
glutSpecialFunc(SpecialKeys);
glutDisplayFunc(RenderScene);
//添加右击菜单栏
// Create the Menu
glutCreateMenu(ProcessMenu);
glutAddMenuEntry("Toggle cull backface",1);
GLenum err = glewInit();
if (GLEW_OK != err) {
fprintf(stderr, "GLEW Error: %s\n", glewGetErrorString(err));
return 1;
}
SetupRC();
glutMainLoop();
return 0;
}
运行发现正背面的问题已经解决了,但又出现新的问题,甜甜圈上有一个缺口
从上图中可以看出,在甜甜圈旋转过程中,当前后两部分重叠时,对于我们而言,需要显示的是前面部分,后面部分是隐藏面,但是OpenGL中并不能清楚的区分,两个图层谁显示在前,谁显示在后,由此导致甜甜圈产生了缺口。
怎么解决这个问题呢?开启深度测试
深度测试
深度缓冲区(DepthBuffer)和颜⾊缓存区(ColorBuffer)是对应的.颜⾊缓存区存储像素的颜⾊信息,⽽深度缓冲区存储像素的深度信息. 在决定是否绘制⼀个物体表⾯时, ⾸先要将表⾯对应的像素的深度值与当前深度缓冲区中的值进⾏⽐较. 如果⼤于深度缓冲区中的值,则丢弃这部分.否则利⽤这个像素对应的深度值和颜⾊值.分别更新深度缓冲区和颜⾊缓存区. 这个过程称为”深度测试” 。
- 什么是深度?
深度其实就是该像素点在3D世界中距离摄像机的距离,Z值 - 什么是深度缓冲区?
深度缓存区是指一块专门内存区域,存储在显存中,用于存储屏幕上所绘制图形的每个像素点的深度值。 - 为什么需要深度缓冲区?
在不使⽤深度测试的时候,如果我们先绘制⼀个距离⽐较近的物理,再绘制距离较远的物理,则距离远的位图因为后绘制,会把距离近的物体覆盖掉. 有了深度缓冲区后,绘制物体的顺序就不那么重要的. 实际上,只要存在深度缓冲区,OpenGL 都会把像素的深度值写⼊到缓冲区中. 除⾮调⽤glDepthMask(GL_FALSE).来禁⽌写⼊.
深度缓存区原理
将深度值与屏幕上的每个像素点进行一一对应,然后将深度值存储到深度缓冲区。
在深度缓存区中,每个像素点只会记录一个深度值
深度缓冲区的范围是[0, 1]之间,默认值是1.0,表示深度值的最大值
深度测试常用的两个方法
1、开启深度测试
glEnable(GL_DEPTH_TEST);
2、关闭深度测试
glDisable(GL_DEPTH_TEST);
3、指定深度测试判断模式
void glDepthFunc(GLEnum mode);
函数 | 说明 |
---|---|
GL_ALWAYS | 总是通过测试 |
GL_NEVER | 总是不通过测试 |
GL_LESS | 在当前深度值 < 存储的深度值时通过 |
GL_EQUAL | 在当前深度值 = 存储的深度值时通过 |
GL_LEQUAL | 在当前深度值 <= 存储的深度值时通过 |
GL_GREATER | 在当前深度值 > 存储的深度值时通过 |
GL_NOTEQUAL | 在当前深度值 不等于 存储的深度值时通过 |
GL_GEQUAL | 在当前深度值 >= 存储的深度值时通过 |
清理缓冲区记得清理深度缓冲区glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
在之前的基础上将代码进行优化
1、全局变量
添加一个变量作为深度测试标记
int iDepth = 0;
2、RenderScene函数
记得清理缓冲区 添加深度测试代码
if(iDepth)
glEnable(GL_DEPTH_TEST);
else
glDisable(GL_DEPTH_TEST);
3、ProcessMenu函数
添加代码
case 2:
iDepth = !iDepth;
break;
4、main函数
添加一个菜单按钮
glutAddMenuEntry("Toggle depth test",2);
看下效果
甜甜圈案例运行好像很正常,那么是否就没问题了呢?这里又有个新的可能出现的问题
多边形偏移
Z-Fighting(Z冲突,闪烁)问题
原因:
因为开启深度测试后,由于深度缓冲区精度的限制对于深度相差⾮常⼩的情况下,OpenGL 就可能出现不能正确判断两者的深度值,会导致深度测试的结果不可预测.画面出现交错闪烁。
解决方法:
多边形偏移(Polygon Offset)
多边形偏移 3个步骤:
1、开启多边形偏移
glEnable(GL_POLYGON_OFFSET_FILL);
2、设置偏移的factor和units,一般都使用 -1 即可
glPolygonOffset(GLfloat factor, GLfloat units);
3、关闭多边形偏移
glDisable(GL_POLYGON_OFFSET_FILL)
预防ZFighting闪烁
- 避免两个物体靠的太近:在绘制时,插入一个小偏移
- 将近裁剪面(设置透视投影时设置)设置的离观察者远一些:提高裁剪范围内的精确度
- 使用更高位数的深度缓冲区:提高深度缓冲区的精确度