【Graphic】渲染顺序相关问题


在OpenGL及RTR3的学习中,渲染顺序主要牵扯到透明物体的渲染,深度测试以及提前深度测试相关的知识,在书中这些知识点比较分散,所以对此做一个总结。

1. 深度缓冲算法

1.1 深度测试

Z-Buffer:OpenGL存储它的所有深度信息于一个Z缓冲(Z-buffer)**中,每个像素记录了离相机最近的片元深度,即片元的z坐标

深度测试:当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。这个过程称为深度测试(Depth Testing)

深度写入:如果设置了允许深度写入,则OpenGL会将通过测试的片元的深度值更新到Z缓冲中。即重新更新Z缓冲中的深度阈值

深度缓冲是在fragment shader运行之后在屏幕空间中运行的。因此读取的是gl_FragCoord内建变量,此变量的x和y分了代表了片段的屏幕空间坐标,而z分量包含了片段真正的深度值。z值就是需要与深度缓冲内容所对比的那个值。

1.2 深度写入

开启深度测试后,openGL会默认开启深度写入。但有些情况下我们需要对所有片段都执行深度测试并丢弃相应的片段,但不希望更新深度缓冲。此时我们需要关闭深度写入,即使用一个只读的深度缓冲。我们只需要将深度掩码(Depth Mask)设置为false:
glDepthMask(GL_FALSE);

1.3 深度测试函数

OpenGL允许我们修改深度测试中使用的比较运算符。即我们可以自定义什么时候该通过或丢弃一个片段,什么时候更新深度缓冲,我们需要调用:
glDepthFunc(GL_LESS);
这个函数接受的比较运算符:

运算符 作用
GL_ALWAYS 永远通过深度测试
GL_NEVER 永远不通过深度测试
GL_LESS 在片段深度值小于缓冲的深度值时通过测试
GL_LEQUAL 在片段深度值小于等于缓冲区的深度值时通过测试
GL_GREATER 在片段深度值大于缓冲区的深度值时通过测试
GL_NOTEQUAL 在片段深度值不等于缓冲区的深度值时通过测试

下面是LearnOpenGL中将深度测试函数改为always后的效果:

glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_ALWAYS);

此时深度测试将会永远通过,所以最后绘制的片段将会总是会渲染在之前绘制片段的上面,即使之前绘制的片段本就应该渲染在最前面。因为我们是最后渲染地板的,它会覆盖所有的箱子片段:

2. 混合

OpenGL中,混合(Blending)通常是实现物体透明度(Transparency)的一种技术。

2.1 透明度测试技术AlphaTest

有些图片并不需要半透明,只需要根据纹理颜色值,显示一部分,或者不显示一部分,没有中间情况。OpenGL提供了discard命令,一旦被调用,可以保证片段被丢弃,不会进入颜色缓冲。我们可以在片段着色器中检测一个片段的alptha是否低于某个阈值并直接丢弃。

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D texture1;

void main()
{             
    vec4 texColor = texture(texture1, TexCoords);
    if(texColor.a < 0.1)
        discard;
    FragColor = texColor;
}

2.2 透明度混合技术Alpha Blending

要想渲染有多个透明度级别的图像,我们需要启用混合(Blending)。和OpenGL大多数的功能一样,我们需要启用GL_BLEND来启用混合:
glEnable(GL_BLEND);
OpenGL中的混合是通过下面这个方程来实现的:

片段着色器运行完成后,并且所有的测试都通过之后,这个混合方程(Blend Equation)才会应用到片段颜色输出与当前颜色缓冲中的值(当前片段之前储存的之前片段的颜色)上。
源因子值和目标因子值我们可以使用glBlendFunc来进行设置,glBlendFunc(GLenum sfactor, GLenum dfactor)函数接受两个参数,来设置源和目标因子。

3. 渲染半透明物体

3.1 半透明与不透明物体的渲染

由于通过透明物体是可以看见在其后面的物体,因此对于颜色缓存就不能直接替换更新,而是要做透明度混合(Blend),用颜色缓存中已有的颜色和透明片元的透明色混合出一个新的颜色。

如果深度测试和混合一起使用时,就会产生错误的渲染,如图所示,最前面窗户的透明部分遮蔽了背后的窗户:

当写入深度缓冲时,深度缓冲不会检查片段是否是透明的,所以透明的部分会和其它值一样写入到深度缓冲中。因此即使透明部分应该显示后面的窗户,但在深度测试中仍然丢弃了后面的片元。

所以要渲染半透明物体,我们需要最先绘制最远的物体,最后绘制最近的物体,大体原则如下:

  1. 先绘制所有不透明的物体(后关闭深度写入)
  2. 对所有半透明的物体排序
  3. 按从远到近顺序绘制所有透明的物体

排序透明物体的一种方法是,从相机的视角获取物体的距离。这可以通过计算摄像机位置向量和物体的位置向量之间的距离所获得。然后再把距离和它对应的位置向量存储到一个STL库的map数据结构中,会自动排序:

std::map<float, glm::vec3> sorted;
for (unsigned int i = 0; i < windows.size(); i++)
{
    float distance = glm::length(camera.Position - windows[i]);
    sorted[distance] = windows[i];
}

//逆序(从远到近)从map中获取值
for(std::map<float,glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it) 
{
    model = glm::mat4();
    model = glm::translate(model, it->second);              
    shader.setMat4("model", model);
    glDrawArrays(GL_TRIANGLES, 0, 6);
}

这样就可以正确渲染半透明物体和不透明物体了

注意此时在渲染透明物体时的深度写入是关闭
如果深度写入开启,则透明物体如果通过深度测试,会将深度写入,那么就会丢弃同一屏幕坐标下深度值大片元,导致渲染结果出错。

对于半透明物体间相互交叠的问题,关闭深度写入会的影响较小一点。详细参考,更具体地做法会在下面给出。

3.2 半透明物体间相互交叠的问题

由于渲染半透明物体时是关闭深度写入的,因此无法确定半透明物体间的前后深度关系(排序只能以物体为单位,而不能以片元为单位),这就会导致半透明物体自身所有的片元都不会被其他半透明片元遮挡(只会被不透明片元遮挡)。

我们通过两个pass来渲染模型:

  • Pass1开启深度写入,但不写入颜色,这样被其他半透明遮挡的片元就不能通过深度测试了,同时不透明物体的背景颜色信息也没有被覆盖。
  • Pass2做正常的渲染着色以及透明度混合,这样被其他半透明遮挡住的自己的片元,就不会显现。

4. 提前深度测试技术

正常的渲染管线中深度测试一般在fragment shader之后,但我们知道fragment shader中会进行复杂的光照计算。如果一个片元经过fragment shader之后却在深度测试中被丢弃,那么之前的颜色计算也就白费了,引起了Overdraw(同一个像素绘制多次)问题。而提前深度测试就是为了解决这个问题而产生的,现在主要的技术有:early-z、z-cull、hi-z、z-perpass

4.1 Early-Z

Early-Z就是直接修改传统的渲染管线,由于每个片元的深度信息在光栅化之后就知道了,所以Early-Z就是提前在fragment shader前进行深度测试,操作与正常的深度测试完全一样。这样就可以提前舍弃掉不会进行颜色计算的不透明片元。但是最终的深度测试仍然需要进行,以保证最终的遮挡关系结果正确。前面的一次主要是Z-Cull为了裁剪以达到优化的目的,后一次主要是Z-Check。

要使Early-Z的效果最好,最理想条件下所有绘制顺序都是由近及远。此时我们会在每帧绘制时对场景的物体按照到摄像机的距离由远及近进行排序。这个操作会在cpu端进行,当场景复杂到一定程度,频繁的排序将会占用cpu的大量计算资源。

Early-Z失效:

  • 开启Alpha Test 或 clip/discard 等手动丢弃片元操作
  • 手动修改GPU插值得到的深度
  • 开启Alpha Blend(由于Zwrite off)
  • 关闭深度测试Depth Test

失效原因

  • 只开启Early-Z,Early-Z从前往后(先A再B)写入深度值,先写入A的深度值,然后B发现自己的深度更大,则这些深度更大的片元被抛弃。、
  • 只开启AlphaTest,执行片段着色器的时候,假设A不会通过AlphaTest,自然不会往深度缓冲中写入深度值,所以再渲染B的时候,B发现深度缓冲还没有被写入,就更新深度缓冲,并把自己的颜色写入颜色缓冲。
  • 同时开启,首先执行Early-Z,会把A的深度写入深度缓冲。在片段着色器后,如果A的AlphaTest没通过,会把片段A丢弃,理应不会写入深度值(但在Early-Z的时候已经写入了)。对于B来说,因为Early-z的时候B深度测试没通过,把粉红色部分的片段剔除了,这部分不会执行片段着色器,现在A和B都不在缓冲中。

4.2 Z-prepass

Z-prepass主要配合early-Z使用,主要做法是将场景做两个pass的绘制。

  1. 第一个pass仅写入深度,不做任何颜色的计算及写入。
  2. 第二个pass关闭深度写入,将深度比较函数设为“相等”
    这个方法和我们渲染半透明物体之间使用的方法一致。这样就不再需要使用early-z时对物体进行排序。

参考

LearnOpenGL
用一篇文章理解半透明渲染、透明度测试和混合、提前深度测试并彻底理清渲染顺序
一口气解决RenderQueue、Ztest、Zwrite、AlphaTest、AlphaBlend和Stencil
【技术美术百人计划】图形 3.5 Early-z和Z-prepass


文章作者: YukinoKyoU
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 YukinoKyoU !
评论
  目录