在本篇文章开始之前呢。我们需要先了解一个非常重要的概念:环绕。环绕是OpenGL中任何三角形都需要遵循的重要特性。
什么是环绕
如上图所示。在绘制右侧三角形的时,线条按照从v0到v1,再从v1到v2,最后回到v0的顺序来绘制一个闭合的三角形。这个路径是按照顶点被指定的顺序的。在观察者角度看是沿着顺时针方向。这种方向特性也体现在了第二个三角形中。这种顺序与方向结合来指定顶点的方式称为环绕。
默认情况下OpenGL认为逆时针方向环绕的多边形是正面的。顺时针方向环绕的多边形是反面的
渲染过程产生的问题
问题一:在绘制3D场景的时候,我们需要决定哪些部分是对观察者可见的,或者那些部分是对观察者不可见的,对于不可及那的部分,应该及早丢弃。例如在一个不透明的墙壁后的所有物体即使实际存在 也不应该被渲染 因为对观察者来说 它是不可见的。这种情况叫做“隐藏面消除”。
解决方案:油画算法(有弊端)
-
油画算法
油画算法会先绘制场景中的离观察者较远的物体,再绘制较近的物体。
例如下图所示:先绘制红色部分,再绘制黄色部分,最后再绘制灰色部分,即可解决隐藏面问题
- 油画算法弊端
使用油画算法,只要将场景按照物体距离观察者的距离远近排序,由远及近的绘制即可。但是有一些特殊情况是油画算法无法解决的。比如三个三角形叠加的情况,如图所示
那么 上图所示的这种状态下如何处理呢?
解决方案:正背面剔除(Face Culling)
- 设想 :尝试相信一个3D图形,你从任何一个方向去观察,最多可以看到几个面?答案是,最多三面。从一个立方体的任意位置和方向上看,你不可能看到多于三个面。那么思考?我们为何要去绘制渲染看不到的3个面?如果我们能以某种方式丢弃这部分数据。OpenGL在渲染的性能即可提高超过50%。
- 解决思路:如何知道某个面在观察者的视野中不会出现?任何平面都有两个面,正面/背面 意味着你一个时刻只能看到一个面。OpenGL 可以做到检查所有正面朝向观察者的面,并渲染它们,从而丢弃背面朝向的面,这样可以节约片元着色器的性能,如果告诉OpenGL 你绘制的图形哪个面是正面,哪个面试背面?答案在文章开始的那张图里。你需要通过分析顶点数据的顺序
正面: 按照逆时针顶点连接顺序的三角形面(能被观察者看到的面)
背面:按照顺时针顶点连接顺序的三角形面(不能被观察者看到的面)
- 总结:正面和背面是由三角形的顶点定义顺序和观察者方向共同决定的。随着观察者的角度方向的改变,正面背面也会跟着改变。
了解深度
- 什么是深度 ?
深度其实就是该像素点在3D世界中距离摄像机的距离。Z值
- 什么是深度缓冲区?
深度缓冲区就是一块内存区域,专门存储每个像素点(绘制在屏幕上的)深度值。深度值(Z值)越大,则离摄像机就越远。
- 为什么需要深度缓冲区?
在不使用深度测试的时候,如果我们先绘制一个距离比较近的物体,再绘制距离较远的物体。则距离较远的位图因为后绘制,会把距离近的物体覆盖掉,有了深度缓冲区后,绘制物体的顺序就不那么重要了。实际上,只要存在深度缓冲区,OpenGL 都会把像素的深度值写入到缓冲区中。除非调用glDepthMask(GL_FALSE) 来禁止写入。
- 深度测试
深度缓冲区(DepathBuffer)和颜色缓存区(ColorBuffer)是对应的。颜色缓存区存储像素的颜色信息,而深度缓冲区存储像素的深度信息。在决定是否绘制一个物体表面时,首先要将表面对应的像素的深度值与当前深度缓冲区中的值进行比较。如果大于深度缓冲区中的值,则丢弃这部分。否则利用这个像素对应的深度值和颜色值。分别更新深度缓冲区和颜色缓存区。这个过程为"深度测试"
深度值计算
- 深度值一般由16位,24位或者32位值表示,通常是24位。位数越高的话,深度的精确度越好。深度值的范围在[0,1]之间,值越小表示越靠近观察者。
- 深度缓冲主要是通过计算深度值来比较大小,在深度缓冲区中包含深度值介于0.0和1.0之间,从观察者看到其内容与场景中的所有对象的Z值进行了比较。这些视图空间中的Z值可以是投影平头截体的近平面和元平面之间的任何值。我们因此需要一些方法来转换这些视图空间Z值到[0,1]的范围内。方程如下:
far和near是提供到投影矩阵设置可见视图截锥的远近值
非线性深度缓存
- 方程带内锥截体的深度值Z ,并将其转换到[0,1]范围。在下面的图给出Z值和其相应的深度值的关系
- 在实践中是可以减少使用这样的线性深度缓冲区。正确的投影的非线性深度方程是和1/z 成正比的。由于非线性函数是和1/z成正比,例如1.0和2.0之间的Z值,将变为1.0到0.5之间,这样在Z非常小的时候给了我们很高的精度。方程如下:
要记住的要⼀点是在深度缓冲区的值不是线性的屏幕空间 (它们在视图空间投影矩阵应用之前是 线性)。值为 0.5 在深度缓冲区并不意味着该对象的 z 值是投影平头截体的中间;顶点的 z 值是实际 上相当接近平⾯!你可以看到 z 值和产生深度缓冲区的值在下列图中的非线性关系
非线性还原线性
//片元着色器
out vec4 color;
float LinearizeDepth(float depth)
{
float near = 0.1;
float far = 100.0;
float z = depth * 2.0 - 1.0; // Back to NDC
return (2.0 * near) / (far + near - z * (far - near));
}
void main() {
float depth = LinearizeDepth(gl_FragCoord.z);
color = vec4(vec3(depth), 1.0f);
}
使用深度测试
//开启深度测试
glEnable(GL_DEPTH_TEST);
//在绘制场景前,清除颜⾊色缓存区,深度缓冲 glClearColor(0.0f,0.0f,0.0f,1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//清除深度缓冲区默认值为1.0,表示最大的深度值,深度值的范围为(0,1)之间. 值越小表示越靠近观察者,值越⼤表示 越远离观察者
指定深度测试判断式
//指定深度测试判断模式
void glDepthFunc(GLEnum mode);
打开/阻断 深度缓存区写入
void glDepthMask(GLBool value);
value :
GL_TURE 开启深度缓冲区写入;
GL_FALSE 关闭深度缓冲区写⼊
ZFighting 闪烁问题
- 为什么会产生ZFighting闪烁问题?
因为开启深度测试后。OpenGL 就不会再去绘制模型被遮挡的部分。这样实现的显示更加真实,但是由于深度缓冲区精度的限制对于深度相差非常小的情况下(例如在同一平面上进行2次绘制),OpenGL 就可能出现不正确判断两者的深度值。会导致深度测试的结果不可预测 (原谅我看到不可预测就想到了野指针 哈哈)显示出来的现象 就是交错闪烁。下图两个画面 交错出现
- 如何解决 ZFighting 闪烁问题?
第⼀步: 启用 Polygon Offset 方式解决
解决方法: 让深度值之间产生间隔.如果2个图形之间有间隔,是不是意味着就不会产生干涉.可以理 解为在执行深度测试前将⽴方体的深度值做一些细微的增加.于是就能将叠的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
第二步: 指定偏移量
- 通过glPolygonOffset 来指定.glPolygonOffset 需要2个参数: factor , units
- 每个Fragment 的深度值都会增加如下所示的偏移量:
Offset = ( m * factor ) + ( r * units);
m : 多边形的深度的斜率的最⼤值,理解⼀个多边形越是与近裁剪⾯平行,m 就越接近于0.
r : 能产⽣于窗口坐标系的深度值中可分辨的差异最小值.r 是由具体是由具体OpenGL 平台指定的 ⼀个常量.
- ⼀个大于0的Offset 会把模型推到离你(摄像机)更远的位置,相应的⼀个小于0的Offset 会把模型拉 近
⼀般而言,只需要将-1.0 和 -1 这样简单赋值给glPolygonOffset 基本可以满足需求.
void glPolygonOffset(Glfloat factor,Glfloat units);
//应⽤到片段上总偏移计算方程式:
Depth Offset = (DZ * factor) + (r * units);
//DZ:深度值(Z值)
//r:使得深度缓冲区产⽣生变化的最⼩小值
负值,将使得z值距离我们更近,
⽽正值,将使得z值距离我们更远
第三步:关闭Polygon Offset
glDisable(GL_POLYGON_OFFSET_FILL)
ZFighting 闪烁问题预防
- 不要将两个物体靠的太近。避免渲染时三角形叠在一起
- 尽可能将近裁剪面设置的离观察者远一点
- 使用更高位数的深度缓冲区
裁剪
在OpenGL中提高渲染的一种方式。只刷新屏幕上发生变化的部分。OpenGL 允许将要进行渲染的窗口去指定一个裁剪框
基本原理:用于渲染时限制绘制区域。通过此技术可以在屏幕(帧缓冲)指定一个矩形区域。启用裁剪测试之后,不在此矩形区域的片元被丢弃,只有在此矩形区域的片元才有可能进入帧缓冲,因此实际达到的效果就是在屏幕上开辟了一个小窗口,可以在其中进行指定内容的绘制。
/1 开启裁剪测试
glEnable(GL_SCISSOR_TEST);
//2.关闭裁剪测试
glDisable(GL_SCISSOR_TEST);
//3.指定裁剪窗⼝
void glScissor(Glint x,Glint y,GLSize width,GLSize height);
x,y:指定裁剪框左下角位置;
width , height:指定裁剪尺⼨
混合
我们把OpenGL 渲染时会把颜色值存在颜色缓存区中,每个片段的深度值也是放在深度缓冲区。当深度缓冲区被关闭时,新的颜色将简单的覆盖原来颜色缓存区存在的颜色值,当深度缓冲区再次打开时,新的颜色片段只是当它们比原来的值更接近邻近的裁剪平面才会替换原来的颜色片段。
glEnable(GL_BlEND);
组合颜色
目标颜色:已经存储在颜色缓存区的颜色值
源颜色:作为当前渲染命令结果进入颜色缓存区的颜色值
当混合功能被启动时,源颜色和目标颜色的组合方式是混合方程式控制的。在默认情况下混合方程式如下所示:
Cf = (Cs * S) + (Cd * D);
Cf :最终计算参数的颜⾊
Cs : 源颜⾊
Cd :目标颜⾊
S:源混合因⼦
D:⽬标混合因子
设置混合因子
设置混合因子,需要用到glBlendFun函数
glBlendFunc(GLenum S,GLenum D);
S:源混合因子
D:目标混合因子
表中R、G、B、A 分别代表红、绿、蓝、透明度
表中下标S、D,分别代表源、目标
表中C 代表常量颜色(默认黑色)
下⾯通过一个常⻅的混合函数组合来说明问题:
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
如果颜⾊色缓存区已经有一种颜色红色(1.0f,0.0f,0.0f,0.0f),这个⽬目标颜色Cd,如果在这上⾯用一
种alpha为0.6的蓝色(0.0f,0.0f,1.0f,0.6f)
Cd (⽬标颜色) = (1.0f,0.0f,0.0f,0.0f);
Cs (源颜色) = (0.0f,0.0f,1.0f,0.6f);
S = 源alpha值 = 0.6f
D = 1 - 源alpha值= 1-0.6f = 0.4f
方程式 Cf = (Cs * S) + (Cd * D)
等价于 = (Blue * 0.6f) + (Red * 0.4f)
总结
最终颜⾊是以原先的红⾊(⽬标颜色)与 后来的蓝色(源颜色)进行组合。源颜⾊的alpha值 越高,添加的蓝⾊颜色成分越高,⽬标颜色所保留的成分就会越少。 混合函数经常用于实现在其他一些不透明的物体前⾯绘制⼀个透明物体的效果。
改变组合方程式
默认混合方程式:
Cf=(Cs * S)+(Cd * D);
实际上远不⽌这一种混合方程式,我们可以从5个不不同的方程式中进行选择
选择混合⽅方程式的函数:
glbBlendEquation(GLenum mode);
glBlendFuncSeparate函数
除了能使用glBlendFunc 来设置混合因子,还可以有更灵活的选择。
void glBlendFuncSeparate(GLenum strRGB,GLenum dstRGB ,GLenum strAlpha,GLenum dstAlpha);
strRGB: 源颜色的混合因子
dstRGB: 目标颜色的混合因子
strAlpha: 源颜色的Alpha因子
dstAlpha: ⽬标颜色的Alpha因子
glBlendFuncSeparate 注意
- glBlendFunc 指定 源和目标 RGBA值的混合函数;但是glBlendFuncSeparate函数则允许为RGB 和 Alpha 成分单独指定混合函数。
- 在混合因子表中,GL_CONSTANT_COLOR、GL_ONE_MINUS_CONSTANT_COLOR、GL_CONSTANT_ALPHA、GL_ONE_MINUS_CONSTANT值允许混合方程式中引入一个常量混合颜色。
常量混合颜色
常量混合颜色,默认初始化为黑色(0.0f,0.0f,0.0f,0.0f),但是还可以修改这个常量混合颜色
void glBlendColor(GLclampf red ,GLclampf green ,GLclampf blue ,GLclampf alpha );