OpenGL(五): 渲染技巧

字数 2268阅读 111

渲染过程中可能出现的问题

日常生活中,观察者在观察某个物体的时候,在观察者和物体的相对位置不变的情况下,观察者是没办法一次性观察到该物体的全貌的,物体总是有部分表面是对观察者不可见的。所以要想观察到物体的全貌,总是要使观察者和被观察的物体有一个相对的位置变换。

在OpenGL的渲染过程中,就如同观察者在观察某个物体,总是有部分部分表面不可见。在绘制3D场景时,如果将不可见的面也渲染的话,首先是没有必要的,其次,对性能也是极大的损耗。所以应该在渲染时,将不可见的部分及早丢弃,不再渲染,这种方法就叫做"隐藏面消除(Hidden surface elimination)"。

隐藏面消除的解决方案

油画算法

先绘制场景中离观察者较远的物体,再绘制较近的物体。

如下图的示例:先绘制红色部分,再绘制黄色部分,最后再绘制灰色部分,即可解决隐藏面消除的问题。


image-20190519215325773.png

油画算法的弊端:

使用油画算法,只要将场景按照物理距离观察者的距离远近排序,由远及近的绘制即可。但是是否会出现一种情况,无法对物体的距离进行排序呢?答案是肯定的。

如下图所示:因为三个三角形是交叉叠放在一起的,并不能区分出哪个三角形离观察者更近。

image-20190519215735472.png

正背面剔除(Face culling)

​ 对于一个3D立方体(有六个面),你从任何一个方向去观察,最多可以看到3个面。那么我们在渲染的过程中为什么要去绘制另外三个看不到的面呢?如果我们能以某种方式去丢弃这部分数据,OpenGL在渲染的性能即可提高50%。

​ OpenGL可以做到检查所有正面朝向观察者的面,并渲染它们。从而丢弃背面朝向的面,这样就可以节约片元着色器的性能。

​ 但是,如何告诉OpenGL哪个面是正面,哪个面试背面呢?

​ 答案就是:通过分析顶点数据的顺序。按照逆时针顶点连接顺序的三角形面为正面。按照顺时针顶点连接顺序的三角形面为背面。

​ 通过一个案例来详细讲解:

image-20190519220959804.png

分析:左侧三角形顶点顺序:1—> 2 —> 3;右侧三角形的顶点顺序:1—> 2 —> 3。

​ 当观察者在右侧时,右边的三角形方向为逆时针方向,则右侧三角形为正面,而左侧的三角形为顺时针,为背面。

​ 当观察者在左侧时,左边的三角形为逆时针方向,判定为正面,而右侧的三角形为顺时针,判定为背面。

​ 因此:正面和背面是由三角形的顶点定义顺序和观察者方向共同决定的。随着观察者的角度方向的改变,正面背面也会跟着改变。

​ 代码示例:

// 开启表面剔除(默认背面剔除)
glEnable(GL_CULL_FACE);
// 关闭表面剔除(默认背面剔除)
glDisable(GL_CULL_FACE);
// 用户选择剔除哪个面(正面/背面):GL_FRONT,GL_BACK,GL_FRONT_AND_BACK,默认GL_BACK
glCullFace(GL_BACK);
// 用户指定绕序哪个为正面:GL_CW(顺时针),GL_CCW(逆时针),默认GL_CCW
glFrontFace(GL_CCW); // 不推荐改变默认值

了解深度

​ 什么是深度?深度就是该像素点在3D世界中距离摄像机的距离,Z值。

​ 深度缓冲区:就是一块内存区域,专门存储着每个像素点(绘制在屏幕上的)深度值。深度值(Z值)越大,则离摄像机就越远。

​ 为什么需要深度缓冲区?在不使用深度测试的时候,如果我们先绘制一个距离较近的物体,再绘制距离较远的物体,则距离较远的位图因为后绘制,会把距离近的物体覆盖掉。有了深度缓冲区后,绘制物体的顺序就不那么重要了。实际上,只要存在深度缓冲区,OpenGL都会把像素的深度值写入到缓冲区中。除非调用glDepthMask(GL_FALSE);来禁止写入。

​ 深度缓冲区(DepthBuffer)和颜色缓存区(ColorBuffer)是一一对应的。颜色缓冲区存储像素的颜色信息,而深度缓冲区存储像素的深度信息。在决定是否绘制一个物体的表面时,首先要将表面对应的像素的深度值与当前深度缓冲区中的值进行比较。如果大于深度缓冲区中的值,则丢弃这部分。否则利用这个像素对应的深度值和颜色值,分别更新深度缓冲区和颜色缓存区。这个过程称为"深度测试"。

​ 深度缓冲区一般由窗口管理系统GLFW创建。深度值一般由16位、24位、32位值表示。通常是24位。位数越高,深度精确度更好。

//开启深度测试
glEnable(GL_DEPTH_TEST);
// 在绘制场景前,清除颜色缓存区,深度缓冲区
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

开启深度测试的弊端

​ 因为开启深度测试后,OpenGL就不会再去绘制模型被遮挡的部分。这样实现的显示更加真实。但是由于深度缓冲区精度的限制对于深度相差非常小的情况下(例如在同一平面上进行二次绘制),OpenGL就可能出现不能正确判断两者的深度值的情况,会导致深度测试的结果不可预测。显示出来的现象是交错闪烁的前面两个画面。这种现象叫做ZFighting闪烁问题。

​ 解决方式:让深度值之间产生间隔。如果两个图形之间有间隙,是不是意味着不会产生干涉。可以理解为在执行深度测试前将立方体的深度值做一些细微的增加。于是就能将重叠的两个图形深度值之间有所区分。

​ 1.第一步 启用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

​ 2.第二步 指定偏移量

​ 通过glPolygonOffset 来指定。glPolygonOffset需要两个参数:factor,units

​ 每个Fragment的深度值都会增加如下所示的偏移量:

Offset = (m * factor) + (r * units);
// m: 多边形的深度的斜率的最大值,理解一个多边形越是与裁剪面平行,m值就越接近于0。
// r: 能产生于窗口坐标系的深度值中可分辨的差异最小值。r具体是由OpenGL平台指定的一个常量。

​ 一个大于0的Offset会把模型推到离你(摄像机)更远的位置,相应的一个小于0的Offset会把模型拉近。一般而言,只需要将-1.0和-1这样简单赋值给glPolygonOffset基本可以满足需求。

// 指定便宜量
glPolygonOffset(GLfloat factor, GLfloat units);

​ 3.第三步 关闭Polygon Offset

// 绘制完成后需要关闭避免影响渲染其他图形
glDisable(GL_POLYGON_OFFSET_FILL);
ZFighting闪烁问题的预防
  • 不要将两个物体靠得太近,避免渲染时三角形叠在一起。这种方式要求对场景中物体插入一个少量的偏移,那么就可能避免ZFighting现象。
  • 尽可能将裁剪面设置得离观察者远一些。在近裁剪面附近,深度的精确度是很高的,因此尽可能让近裁剪面远一些的话,会使整个裁剪范围内的精确度高一些。但是这种方式会使离观察者较近的物体被裁减掉,因此需要调试好裁剪面参数。
  • 使用更高位数的深度缓冲区,通常使用的深度缓冲区是24位的,现在有一些硬件使用32位的缓冲区,使精确度得到提高。

混合

​ 我们把OpenGL渲染时会把颜色值存在颜色缓存区中,每个片段的深度值也是放在深度缓冲区。当深度缓冲区被关闭时,新的颜色将简单的覆盖原来颜色缓存区存在的颜色值,当深度缓冲区再次打开时,新的颜色片段只是当它们比原来的值更接近邻近的裁剪平面才会替换原来的颜色片段。

// 开启混合
glEnable(GL_BIEND); 

组合颜色

​ 目标颜色:已经存储在颜色缓存区的颜色值

​ 源颜色:作为当前渲染命令结果进入颜色缓存区的颜色值

​ 当混合功能被启动时,源颜色和目标颜色的组合方式是混合方程式控制的。在默认情况下,混合方程式如下所示:

Cf = (Cs * S) + (Cd * D)
Cf: 最终计算参数的颜色
Cs: 源颜色
Cd: 目标颜色
S : 源混合因子
D : 目标混合因子  

​ 设置混合因子:

​ 需要用到glBlendFunc函数

glBlendFunc(GLenum S, GLenum D);
S: 源混合因子
D: 目标混合因子
image-20190520230522014.png
表中R、G、B、A分别代表红、绿、蓝、alpha。
表中下标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值越高,添加的蓝色颜色成分越高,目标颜色所保留的成分就会越少。

​ 混合函数经常用于实现在其他一些不透明的物体前面绘制一个透明物体的效果。

推荐阅读更多精彩内容