在研究OpenGL渲染技巧之前,我们来绘制一个3D图形——甜甜圈。
相关代码
#include <stdio.h>
#include "GLMatrixStack.h"
#include "GLFrame.h"
#include "GLFrustum.h"
#include "GLGeometryTransform.h"
#include <GLUT/GLUT.h>
GLFrame viewFrame; // 物体
GLFrame cameraFrame; // 观察者
GLFrustum viewFrustum; // 管擦着投影矩阵
GLTriangleBatch torusBatch; // 甜甜圈批次类
GLMatrixStack modelViewMatrix; // 模型视图矩阵
GLMatrixStack projectionMatrix; // 投影矩阵
GLGeometryTransform transformPipeline; // 集合变换管道
GLShaderManager shaderManager; // 着色器管理类
GLfloat vRed[] = { 0.1f, 0.3f, 0.8f, 1.0f };
void changeSize(int w, int h) {
// 设置窗口
glViewport(0, 0, w, h);
// 创建透视投影
viewFrustum.SetPerspective(35.0, float(w) / float(h), 1.0, 100.0);
// 通过设置的投影方式获得投影矩阵,并将其存入投影矩阵中
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
}
void setupRC() {
// 设置背景颜色
glClearColor(0.3f, 0.3f, 0.3f, 1.0f );
// 初始化着色器管理器
shaderManager.InitializeStockShaders();
// 从初始化渲染管线
transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
// 将相机向后移动7个单元:肉眼到物体之间的距离
viewFrame.MoveForward(10);
// 创建一个甜甜圈
// 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);
// 点的大小(方便点填充时,肉眼观察)
glPointSize(4.0f);
}
void renderScene() {
// 清理缓存区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// 压栈
modelViewMatrix.PushMatrix(viewFrame);
// 使用默认光源着色器
// 参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器
// 参数2:模型视图矩阵
// 参数3:投影矩阵
// 参数4:基本颜色值
shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
// 绘制
torusBatch.Draw();
// 出栈
modelViewMatrix.PopMatrix();
// 交换缓冲区
glutSwapBuffers();
}
void specialKeys(int key, int x, int y) {
if(key == GLUT_KEY_UP) {
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);
}
glutPostRedisplay();
}
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("OpenGL渲染方式");
glutReshapeFunc(changeSize);
glutDisplayFunc(renderScene);
glutSpecialFunc(specialKeys);
GLenum status = glewInit();
if (status != GLEW_OK) {
printf("error: %s\n", glewGetString(status));
return 1;
}
setupRC();
glutMainLoop();
return 0;
}
运行代码之后,通过键盘的方向键控制甜甜圈的旋转,会出现下图的样子。
我们可以发现,甜甜圈有部分被黑色的填充了。这就是我们今天要讲的第一个内容:
正背面剔除
。
正背面剔除(Face Culling)
正背面剔除
是OpenGL的一种图形绘制技巧,主要用于处理立体图形绘制时,只绘制观察者能看到的部分,看不到的部分就丢弃不绘制。
如何知道哪个面显示,哪些不显示?
OpenGL中默认规定了逆时针方向绘制的三角形是正面,当然你可以改为顺时针为正面,但是一般不建议这么操作,主要是由于这个设置不仅仅是作用于你的项目,而是作用于OpenGL全局的(OpenGL是一个巨大的状态机)。我们习惯OpenGL中默认的即可。
// 用于修改正面的函数
// GL_CW(顺时针)
// GL_CCW(逆时针)
// 默认:GL_CCW
void glFrontFace(GLenum mode);
OpenGL内部会通过分析顶点数据的顺序来确认正反面,这个并需要开发者,我们只需将正背面剔除,开启/关闭即可。
- 开启表⾯面剔除(默认背⾯面剔除)
// 开启表面剔除 (默认背面剔除)
void glEnable(GL_CULL_FACE);
- 关闭表⾯面剔除(默认背⾯面剔除)
// 关闭表面剔除(默认背面剔除)
void glDisable(GL_CULL_FACE);
- 设置需要剔除的面
// 设置需要剔除的面
// GL_FRONT:剔除正面
// GL_BACK:剔除背面,是默认值
// GL_FRONT_AND_BACK:剔除正背面
void glCullFace(GLenum mode);
我们在上面代码的renderScene
函数里面添加背面剔除的代码
void renderScene() {
// 清理缓存区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// 开启背面剔除
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glCullFace(GL_BACK);
// 压栈
modelViewMatrix.PushMatrix(viewFrame);
// 使用默认光源着色器
// 参数1:GLT_SHADER_DEFAULT_LIGHT 默认光源着色器
// 参数2:模型视图矩阵
// 参数3:投影矩阵
// 参数4:基本颜色值
shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
// 绘制
torusBatch.Draw();
// 出栈
modelViewMatrix.PopMatrix();
// 交换缓冲区
glutSwapBuffers();
}
运行代码,通过键盘方向键控制甜甜圈的旋转,可以发现,没有黑色的填充了,但是当甜甜圈的侧面正对着我们的时候,出现了另外一个问题。
我们发现甜甜圈缺了一截,这就是今天要讲的第二个内容:
深度测试
深度测试
什么是深度?
深度其实就是像素点在3D世界中的z
坐标距离观察者的距离。
深度与图像中像素的z
坐标的关系
- 如果观察者在Z轴的正方向,Z值越大则越靠近观察者
- 如果观察者在Z轴的负方向,Z值越小则越靠近观察者
什么是深度缓冲区?
深度缓存区,就是⼀块内存区域,专门存储着每个像素点(绘制在屏幕上的)深度值.深度值(z值)越⼤大, 则离摄像机就越远。
深度缓冲区原理
将深度值与屏幕上的每个像素点进行一一对应,然后将深度值存储到深度缓冲区
- 在深度缓存区中,每个像素点只会记录一个深度值
- 深度缓冲区的范围是[0, 1]之间,默认值是1.0,表示深度值的最大值
为什么需要深度缓冲区?
在不使用深度测试的时候,如果我们先绘制一个距离比较近的物体,再绘制距离较远的物体,则距离 远的位图因为后绘制,会把距离近的物体覆盖掉. 有了深度缓冲区后,绘制物体的顺序就不那么重要了。 实际上,只要存在深度缓冲区,OpenGL 都会把像素的深度值写⼊到缓冲区中. 除非调⽤glDepthMask(GL_FALSE)
来禁⽌止写⼊。
深度测试
一个物体在绘制时,像素点新的深度值需要与深度缓存中已经存在的深度值作比较,如果新值 > 旧值
,则丢弃这部分不绘制,反之,将新的深度值更新至深度缓存区,由于深度缓存区与颜色缓存区是一一对应的,同时也需要更新该像素点的颜色值到颜色缓存区,这个过程就是深度测试
。
甜甜圈缺口的解决办法
要解决甜甜圈的缺口问题,我们只需要开启深度测试
glEnable(GL_DEPTH_TEST);
深度测试应用场景
- 类似于甜甜圈的缺口问题,当旋转时,OpenGL无法区分物体的两部分重叠情况,导致缺口出现。
- 利用深度测试解决隐藏面的消除。
Z-Fighting
开启深度测试后,由于深度缓冲区精度有限制,导致深度值在误差极小时,OpenGL出现无法判断的情况,导致出现画面交错闪现的现象。
其问题产生的主要原因是由于图形靠的太近,导致无法区分出图层先后次序。解决办法:
多边形偏移(Polygon Offset)
- 在绘制前,开启多边形偏移
如果2个图形之间有间隔,就意味着就不会产生干涉。可以理解为在执行深度测试前将⽴方体的深度值做⼀些细微的增加。于是就能将重叠的2个图形深度值加以区分。
glEnable(GL_POLYGON_OFFSET_FILL);
枚举值 | 对应的图像填充模式 |
---|---|
GL_POLYGON_OFFSET_POINT | GL_POINT |
GL_POLYGON_OFFSET_LINE | GL_LINE |
GL_POLYGON_OFFSET_FILL | GL_FILLd |
- 指定偏移量
glPolygonOffset (GLfloat factor, GLfloat units);
,参数一般填-1
和-1
。 - 在绘制完成后,关闭多边形偏移。
glDisable(GL_POLYGON_OFFSET_FILL);
如何预防ZFighting
闪烁
- 避免两个物体靠的太近:在绘制时,插入一个小偏移。
- 将近裁剪面(设置透视投影时设置)设置的离观察者远一些:提高裁剪范围内的精确度。
- 使用更高位数的深度缓冲区:提高深度缓冲区的精确度。通常使用的深度缓冲区是24位的,现在有⼀些硬件使用32位的缓冲区,使精确度得到提⾼。
混合
当开启深度测试后,两个重叠的图层中,如果有一个图层是半透明的,另一个是非半透明,此时就不能通过深度值比较,来进行颜色值的覆盖,而是需要将两个颜色进行混合,然后存入颜色缓冲区。
在一般情况下,OpenGL在渲染时把颜色值存放在颜色缓冲区中,把每个片段(像素)的深度值存放在深度缓冲区中。当深度检测被关闭时,新的颜色值简单地覆盖颜色缓冲区中已经存在的颜色值;当深度检测被打开时,新的颜色值只有当它比原来的颜色更接近临近的裁剪平面时才会替换原来的颜色。当然,这是在OpenGL的混合功能被关闭的情况下。当混合功能被启用时,新的颜色会与颜色缓冲区中原有的颜色进行组合。通过对这些颜色进行不同的组合,可以产生许多种不同的效果。
目标颜色:已经存储在颜色缓冲区中的颜色称为目标颜色;
源颜色:当前渲染命令的结果进入颜色缓冲区中的颜色称为源颜色;
我们正是通过对目标颜色和源颜色进行不同的组合操作,来实现颜色混合的功能
当混合功能被启用时,源颜色和目标颜色的组合方式是由混合方程式来控制的。在默认情况下,使用的混合方程式如下所示:
// Cf -- 最终组合的颜色值
// Cd:源颜色 -- 当前渲染命令传入的颜色值
// CS:目标颜色 -- 颜色缓冲区中已经存在的颜色值
// S:源混合因子
// D:目标混合因子
Cf = (Cs * S) + (Cd * D)
使用
- 通过
glEnable
开启组合 - 通过glBlendFunc,开启组合函数,计算混合因子,并在每次渲染,都会将屏幕上所有的像素点都更新一遍
- 绘制
- 绘制完成后,需要关闭混合功能,通过
glDisable
关闭。因为如果不关闭,会对其他项目造成影响,混合的开启是针对全局的,并不单单只是这个项目
// 1.开启混合
glEnable(GL_BLEND);
// 2.开启组合函数 计算混合颜色因子---每次渲染,会把屏幕上所有的像素点更新一遍
// 混合方程式:Cf = (Cs * S)+(Cd * D)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// 3.使用着色器管理器
//*使用 单位着色器
//参数1:简单的使用默认笛卡尔坐标系(-1,1),所有片段都应用一种颜色。GLT_SHADER_IDENTITY
// 参数2:着色器颜色
shaderManager.UseStockShader(GLT_SHADER_IDENTITY, vRed);
// 4.容器类开始绘制
squareBatch.Draw();
// 5.关闭混合功能
glDisable(GL_BLEND);