OpenGL–渲染技巧

1.渲染过程产生的问题

截屏2021-08-10 14.29.34.png

上一篇文章介绍了使用平面着色器绘制金字塔,花环,花盘的实现,为了让渲染更接近真实世界,需要增加光照效果。

上图是使用默认光源着色器渲染的甜甜圈,甜甜圈是由OpenGL提供实现的,外界只需调用即可得到。

当静止不动时,甜甜圈效果很完美。可一旦转动,就穿帮了。

QQ20210810-141709-HD.gif

出现隐藏面的原因在于:

使用默认光源着色器时,正面正常渲染,背面会被渲染成黑色。旋转发生时,背面的隐藏面无法被正面遮挡。

而使用平面着色器绘制的金字塔,花环,花盘也进行了旋转等操作却没有出现此现象。这是因为平面着色器使用同一颜色渲染所有图元,正面和背面都是同一种颜色,旋转之后正背面无视觉差。

传送门

2.油画算法

对于这个问题,一个可行的解决方法是:对这些三角形进行排序,然后首先渲染较远的三角形,在渲染较近的三角形,这种方式叫做油画算法

截屏2021-08-10 15.31.42.png

如图,先绘制远处的山。在绘制草地,最后绘制树,这样即可解决隐藏面的问题。

但这样的图形处理方式在计算机中是非常低效的。因为任何发生重叠的像素都要进行多次写操作。

更重要的是,当遇到某些场景时,油画算法无法很好的解决问题。

截屏2021-08-10 15.49.49.png

当显示这样的图形时,油画算法退出了群聊。

3.正背面剔除

三角形被区分为正面和背面的用处之一就是可以进行正背面剔除

试想,任何一个3d物体,从任意方向看过去不可能看到它的全貌,也就一定会存在一些不可见的面。对于观察者而言,这些不可见的面不渲染也不影响观察物体;对于计算机而言,既然不可见为什么还去渲染,直接丢弃不就好了。

于是问题就变成了如何区分可见和不可见,这个问题等价于如何区分正面和背面

至于如何区分正面和背面是通过三角形顶点的连接顺序,而连接顺序决定法向量的朝向

  • 正面:顶点按照逆时针顺序连接的三角形面,法向量朝向观察者

  • 背面:顶点按照顺时针顺序连接的三角形面,法向量背对观察者

截屏2021-08-10 17.00.09.png

但是立体图形的正面和背面还和观察者的视角有关。

  • 当观察者在右侧时,右边的三角形为逆时针则是正面,左侧的三角形为顺时针则是反面

  • 当观察者在左侧时,左边的三角形为逆时针则是正面,右侧的三角形为顺时针则是反面

不难理解,观察者看到的为正面是合理的。

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

这里的背面是指被遮住的面,而不是一个面的前后两面。同一个面的前后两面在3D中同一位置的点是一模一样颜色的,面是没有厚度概念的。

由于状态机的存在,正背面剔除使用的方式及其简单:

  • 开启正背面剔除(默认背面剔除)
glEnable(GL_CULL_FACE);
复制代码
  • 关闭正背面剔除(默认背面剔除)
glDisable(GL_CULL_FACE);
复制代码

状态机决定了一个功能使用完之后必须关闭,否则会影响到后续帧的渲染。

QQ20210811-110800-HD.gif

如图,使用正背面剔除渲染的甜甜圈隐藏面问题已经解决,但当旋转到一定角度后,出现甜甜圈被啃一口的现象。

这是因为当前后的像素重叠时,OpenGL不确定是渲染前面还是后面

要想解决这个问题,需要用到深度测试

4.深度测试

什么是深度?

深度其实就是该像素点在3D世界中距离摄像机的距离,即Z值

什么是深度缓冲区?

深度缓存区就是⼀块内存区域,专⻔存储着每个像素点深度值.深度值(Z值)越⼤,则离摄像机就越远

为什么需要深度缓冲区?

当场景中包含很多模型时,对于不透明物体,不考虑它们的渲染顺序也能得到正确的排序效果,这正是由于深度缓冲区的存在

在实时渲染中,深度测试相当于油画算法的颠倒,先处理离观察者近的对象,在处理远的对象,它可以决定哪些物体的哪些部分会被渲染在前面,而哪些部分会被遮挡,所以它也能用来处理可见性的问题。

它的工作原理是:当渲染一个片元时,需要把它的深度值和已存于深度缓冲区该片元的深度值作对比,如果它的值距离摄像机更远,说明这个片元不应该被渲染,直接丢弃,否则应该覆盖掉颜色缓冲区对应的像素值,并且在深度缓冲区更新它的深度值(深度写入开启时)。

深度写入:将刚通过测试的片元的深度值更新到深度缓存。深度写入的目的在于更新了一个深度阈值。之后进行深度测试时,不符合新阈值条件的片元都会被舍弃。

截屏2021-08-11 17.48.32.png

截屏2021-08-11 14.21.22.png

由于状态机的存在,深度测试使用的方式也及其简单:

  • 开启深度测试
glEnable(GL_DEPTH_TEST);
复制代码
  • 关闭深度测试
glDisable(GL_DEPTH_TEST);
复制代码
  • 是否开启深度写入
glDepthMask(GLboolean flag);
复制代码

注意,如果没有申请深度缓冲区深度测试的命令将被忽略。

  • 申请颜色缓冲区和深度缓冲区
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
复制代码

QQ20210811-143851-HD.gif

如图,使用深度测试后的甜甜圈解决隐藏面缺口问题了。

深度值⼀般由16位,24位或者32位值表示,通常是24位。位数越⾼的话,深度的精确度越好。深度值的范围在[0,1]之间,值越⼩表示越靠近观察者,值越⼤表示远离观察者。由于深度缓冲区精度的限制,在深度值相差非常小的情况下,会出现斑驳现象,显示出来的效果交错闪烁。(想象下,渲染一架机翼上带有贴纸logo的飞机时,机翼表面和贴纸深度值非常接近)

截屏2021-08-11 16.59.02.png

导致这个现象的原因,叫做ZFighting。解决ZFighting的冲突,可以使用多边形偏移

5.多边形偏移

多边形偏移在上一篇金字塔案例中描边的步骤有涉及。

多边形偏移的思想是:

在执⾏深度测试前将深度值做⼀些细微的改变,让深度值之间产生间隔,就能将重叠的2个图形深度值区分开来。

同样,由于状态机的存在,多边形偏移使用的方式也及其简单:

  • 开启多边形偏移,参数对应多边形的填充方式
glEnable(GL_POLYGON_OFFSET_POINT);
glEnable(GL_POLYGON_OFFSET_LINE);
glEnable(GL_POLYGON_OFFSET_FILL);
复制代码
  • 关闭多边形偏移
glDisable(...);
复制代码
  • 开启多边形偏移后,还需要指定偏移量
glPolygonOffset(GLfloat factor, GLfloat units);
复制代码

Offset大于0会把模型推到离摄像机更远的位置,⼩于0Offset会把模型拉近。
⼀般⽽⾔,只需要给glPolygonOffset赋值为-1和1基本可以满⾜需求。对于金字塔案例中的描边是设置为-1

ZFighting问题的预防:

  • 不要将两个物体靠的太近,避免渲染时三⻆形叠在⼀起。这种⽅式要求对场景中物体插⼊⼀个少量的偏移,那么就可能避免ZFighting现象。
  • 尽可能将近裁剪⾯设置得离观察者远⼀些。在近裁剪平⾯附近,深度的精确度是很⾼的,因此尽可能让近裁剪⾯远⼀些的话,会使整个裁剪范围内的精确度变⾼⼀些。但是这种⽅式会使离观察者较近的物体被裁减掉,因此需要调试好裁剪⾯参数。
  • 使⽤更⾼位数的深度缓冲区,通常使⽤的深度缓冲区是24位的,现在有⼀些硬件使⽤使⽤32位的缓冲区,使精确度得到提⾼

6.混合

相关术语:

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

源颜⾊:作为当前渲染命令结果进⼊颜⾊缓存区的颜⾊值

如果一个片元通过了所有测试,并且开启了混合,那么颜色缓冲区已有的颜色(目标颜色)和当前片元的颜色(源颜⾊)会进行混合目标颜色源颜⾊的组合方式是由混合方程式控制的。

截屏2021-08-12 16.14.30.png

默认的混合方程式如下:

  • Cf:最终计算参数的颜⾊
  • Cs: 源颜⾊
  • Cd:⽬标颜⾊
  • S:源混合因⼦
  • D:⽬标混合因⼦
Cf = (Cs * S) + (Cd * D)
复制代码

其中,可以设置混合因⼦,需要⽤到glBlendFun命令:

  • S:源混合因⼦
  • D:⽬标混合因⼦
glBlendFunc(GLenum S,GLenum D);
复制代码

8113f339e9824646aa486b77da2d0c6d_tplv-k3u1fbpfcp-watermark.png

通过常用的混合命令组合说明:

glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA)
复制代码

如果源颜色的alpha值为0.6,即S=0.6,那么目标混合因子D=1-0.6

代入混合方程式就是:

Cf = (Cs * 0.6) + (Cd * 0.4);
复制代码

源颜⾊的alpha值越⾼,添加的源颜⾊成分越⾼,⽬标颜⾊所保留的成分就会越少。

除了可以设置混合因子混合函数本身也可以被设置:

glbBlendEquation(GLenum mode);
复制代码

截屏2021-08-12 14.31.03.png

使用以上组合可以任意更改混合函数,默认的混合函数是使用加号

同样,由于状态机的存在,混合使用的方式也及其简单:

  • 开启混合
glEnable(GL_BLEND);
复制代码
  • 关闭混合
glDisable(GL_BLEND);
复制代码

需要注意的是,混合针对图层时一般不自行处理混合方程式,只需要开关;而在片元着色器给定某个颜色而不是图层进行处理时,需要使用混合方程式自定义。

由于深度测试和深度写入的存在,渲染不透明物体可以不讲究顺序,可是当开启混合时,意味着场景中存在半透明物体,这时候渲染的顺序就变得很重要了。

试想,当半透明物在前时,如果开启了深度写入,意味着该片元的深度值被更新,该片元深度值之下的物体不会被渲染,可半透明物体遮挡后面场景是错误的。

  • 所以在渲染半透明物体时,是需要关闭深度写入的,但要开启深度测试

那为什么需要保留深度测试?

  • 因为深度测试的目的是通过比对深度值判断是否需要舍弃该片元。如果该片元在不透明物体之后,是要被舍弃的,自然需要深度测试来判断是舍弃还是留下。

基于这半透明和不透明物体的渲染特性,渲染引擎一般会对物体先进行排序,在渲染:

  1. 先渲染所有的不透明物体,渲染的时候开启深度测试深度写入

  2. 半透明物体按照从后前的顺序渲染,渲染的时候开始深度测试,关闭深度写入

关于混合的案例,感受下深度测试深度写入的打开关闭的区别
混合案例1 &
混合案例2

7.正背面剔除和深度测试的区别

关于正背面和深度,我曾经思考过一个问题,不知道看到这里的你是否也会思考同样的问题。

正背面剔除深度测试似乎做的是同一件事,本质都是去除被遮挡住的像素。那为什么需要两步操作,似乎只做深度测试最终结果也是一样的,它们有什么区别?

正背面剔除是根据三角形面片的法向量,一旦可以确定这个法向量是不可见的,那就可以成片成片的消除场景中的三角形,而深度测试是一个一个像素的判断,效率是低下的。这也是二者必须共存的本质区别。

下面思考两种特殊情况:

只做深度测试时

可以想象,像素会一个一个被处理,也能得到同样的渲染结果,只是效率比较低。

只做正背面剔除时

甜甜圈案例说明了动态情况下,只做正背面剔除渲染会发生错误,那静态情况下是否就能得到正确的结果呢,答案也是否定的。

一个面除了分为完全可见和完全不可见外,还有部分可见的情况。部分可见的面用法向量判断会被标记为完全可见,导致渲染错误,这时就需要做更多的测试来保证渲染结果。而更多的测试也就是深度测试了。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享