OpenGL中的WBOIT:无需排序即可透明

这篇文章是关于加权混合顺序无关透明度(WBOIT)的-这是JCGT在2013年涵盖的技巧。

当屏幕上出现多个透明对象时,像素颜色取决于哪一个离观看者更近。 这是在这种情况下使用的著名混合运算符:

\开始{matrix} C = C_ {near} \ alpha + C_ {far}(1- \ alpha)&&(1)\ end {matrix}



片段排序很重要。 运算符包含接近片段的颜色(C near )和不透明度( α )以及其后面所有片段的整体颜色(C far )。 不透明度范围为0到1; 0表示对象是完全透明的(不可见),而1表示对象是完全不透明的。

为了使用此运算符,您需要按深度对片段进行排序。 想象一下这是什么诅咒。 通常,您需要对每帧进行一次排序。 如果对对象进行分类,则可能必须处理必须切成段的不规则形状的表面,然后必须对这些表面的切除部分进行分类(对于相交的表面,您肯定需要这样做)。 如果对片段进行排序,则将实际的排序放入着色器中。 此方法称为“顺序无关的透明度”(OIT),它基于存储在视频内存中的链接列表。 几乎不可能预测必须为该列表分配多少内存。 而且,如果内存不足,屏幕上会出现伪像。

如果您可以调节场景中透明对象的数量并调整它们的相对位置,请认为自己很幸运。 但是,如果您开发CAD,则取决于用户放置对象的位置,因此将有他们想要的对象数量,并且其放置将是任意的。

现在您知道为什么找到不需要初步排序的混合运算符如此诱人。 并且有这样一个运算符-在我一开始提到的一篇论文中。 实际上,有几个公式,但是其中的一个作者(和我自己)认为最好:

\ begin {matrix} C = {{\ sum_ {i = 1} ^ {n} C_i \ alpha_i} \ over {\ sum_ {i = 1} ^ {n} \ alpha_i}}(1- \ prod_ {i = 1} ^ {n}(1- \ alpha_i))+ C_0 \ prod_ {i = 1} ^ {n}(1- \ alpha_i)&&(2)\ end {matrix}





在屏幕截图上,可以看到在四个深度层上排列的透明三角形组。 左侧是用WBOIT渲染的,右侧是经典的依赖顺序的混合-具有公式(1)-(从现在开始,我将其称为CODB)。

在开始渲染透明对象之前,我们需要渲染所有非透明对象。 之后,将通过深度测试渲染透明对象,但无需将任何内容写入深度缓冲区(可以通过以下方式完成: glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); )。

现在,让我们看一下屏幕空间坐标(x,y)在某些情况下会发生什么。 透明片段-碰巧比非透明片段更近-通过深度测试,无论它们相对于已渲染的透明片段如何放置。 那些不透明碎片后面的透明碎片-好吧,它们没有通过深度测试,因此自然地被丢弃了。

公式(2)中的C 0是在该点(x,y)上渲染的不透明片段的颜色。 我们总共有n个通过深度测试的透明片段,它们的索引为i∈[1,n]。 C i是第i个透明片段的颜色,而αi是其不透明性。

公式(2)与公式(1)有些相似,尽管不是很明显。 更换 C 接近 ,C 0 ,C α和公式(1)将会完全一样。 的确 是所有透明片段的颜色的加权算术平均值 (力学上有类似的“质心”公式),它将用于接近片段C near的颜色。 C 0是我们计算加权算术平均值的所有那些透明片段后面的非透明片段的颜色。 换句话说,我们将所有透明片段替换为一个“加权均值”片段,并使用标准的混合运算符-公式(1)。 现在, α有了一些复杂的公式,我们还没有弄清楚它的含义。

 alpha=1 prodi=1n1 alphai


它是n维空间中的标量函数。 所有αi都包含在[0,1]中,因此它相对于任何αi的偏导数都是一些非负常数。 这意味着,当您增加任何透明片段的不透明度时,“加权均值”片段的不透明度会增加,这正是我们想要的。 此外,它线性增加。

如果某个片段的不透明度为0,则它​​是完全不可见的。 它根本不影响最终的颜色。

如果至少一个片段的不透明度为1,则α也为1。 即,非透明片段变得不可见,这是很好的。 问题是,其他透明片段(在此不透明度= 1的片段之后)仍然可以透过它看到,并有助于产生最终的颜色:



此图像上的橙色三角形位于顶部,绿色三角形位于其下方,绿色三角形下方是灰色和青色三角形。 背景是黑色的。 橙色三角形的不透明度为1; 所有其他的不透明度= 0.5。 在这里您可以看到WBOIT看起来很差。 真正的橙色出现的唯一位置是用不透明的白线勾勒出的绿色三角形的边缘。 正如我刚才提到的,如果非透明片段在其顶部具有不透明度= 1的透明片段,则它是不可见的。

在下一张图片中可以更好地看到它:



橙色三角形的不透明度为1,关闭透明性的绿色三角形仅使用非透明对象进行渲染。 看起来像绿色的三角形后面的三角形一样,通过橙色过滤通过顶部的三角形。

使图片看起来合理的最简单方法是不为对象设置高不透明度。 在使用此技术的项目中,我将不透明度设置为不超过0.5。 在3D CAD中,对象是示意性绘制的,不需要看起来很逼真,因此此限制是可以接受的。

在低不透明度的情况下,左右图片看起来非常相似:



而且它们的高度不透明性明显不同:



这是一个透明的多面体:




它有橙色的侧面和绿色的水平面,不幸的是,它并不明显,这意味着图片看起来不可信。 无论何时橙色表面恰好位于顶部,颜色都必须更橙色,而在绿色表面后面的位置,颜色必须更绿色。 最好用一种颜色绘制它们:



向融合运算符中注入深度


为了弥补深度排序的不足,上述JCGT论文的作者提出了几种在公式(2)中注入深度的方法。 它使实现变得复杂,并使结果难以预测。 为了使其工作,必须根据特定的3D场景调整混合参数。 我没有深入研究这个主题,因此,如果您想了解更多信息,请阅读本文。

作者声称,有时WBOIT能够完成CODB无法完成的工作。 例如,考虑将烟雾作为具有两个粒子的粒子系统进行绘制:深色烟雾和浅色烟雾。 当粒子移动而一个粒子通过另一个粒子时,它们的混合颜色会立即从暗变成亮,这不好。 具有深度的WBOIT运算符可通过平滑的颜色过渡产生更好的结果。 建模为一组细管的头发或毛皮具有相同的属性。

代码


现在为OpenGL实现公式(2)。 您可以在GitHub上查看实现 。 这是一个基于Qt的应用程序,您在此处看到的图片大部分来自该应用程序。

如果您不熟悉透明渲染,那么这里是入门级的好材料:
学习OpenGL。 调和

我建议在继续阅读本文之前先阅读它。

为了评估公式(2),我们需要2个额外的帧缓冲区,3个多采样纹理和一个深度渲染缓冲区。 非透明对象将被渲染为第一个纹理colorTextureNT。 它的类型是GL_RGB10_A2。 第二个纹理(colorTexture)的类型为GL_RGBA16F。 colorTexture的前三个成分将包含公式(2)的这一部分: 将被写入第四部分。 GL_R16类型的最后一个纹理alphaTexture将包含

首先,我们需要创建所有这些对象并从OpenGL获取其标识符:
  f->glGenFramebuffers (1, &framebufferNT ); f->glGenTextures (1, &colorTextureNT ); f->glGenRenderbuffers(1, &depthRenderbuffer); f->glGenFramebuffers(1, &framebuffer ); f->glGenTextures (1, &colorTexture); f->glGenTextures (1, &alphaTexture); 

正如您所记得的,我使用Qt framewok,并且所有对OpenGL的调用都是从QOpenGLFunctions_4_5_Core类型的对象进行的,对于该对象,我始终使用名称f。

接下来是内存分配:
  f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_RGB16F, w, h, GL_TRUE ); f->glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer); f->glRenderbufferStorageMultisample( GL_RENDERBUFFER, numOfSamples, GL_DEPTH_COMPONENT, w, h ); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_RGBA16F, w, h, GL_TRUE ); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture); f->glTexImage2DMultisample( GL_TEXTURE_2D_MULTISAMPLE, numOfSamples, GL_R16F, w, h, GL_TRUE ); 

帧缓冲设置:
  f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT, 0 ); f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer ); f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTexture, 0 ); f->glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D_MULTISAMPLE, alphaTexture, 0 ); GLenum attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}; f->glDrawBuffers(2, attachments); f->glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer ); 

在第二次渲染过程中,片段着色器的输出将进入两个纹理,必须使用glDrawBuffers明确指定它们。
程序启动时,大多数代码将执行一次。 每次更改窗口大小时,都会执行用于纹理和渲染缓冲区内存分配的代码。 现在,我们继续执行每次窗口内容更新时执行的代码。
  f->glBindFramebuffer(GL_FRAMEBUFFER, framebufferNT); // ... rendering non-transparent objects ... // ....... // done! (you didn't expect me to explain how do I render primitives in OpenGL, did you? // It's not relevant for this topic 

我们只是将所有非透明对象渲染为colorTextureNT并在renderbuffer中写入了深度。 在下一个渲染通道使用相同的渲染缓冲区之前,我们需要确保深度渲染缓冲区中来自非透明对象的所有写入操作均已完成。 它可以通过GL_FRAMEBUFFER_BARRIER_BIT来实现。 渲染透明对象后,我们将调用ApplyTextures()函数,该函数将执行最终渲染过程,其中片段着色器将从纹理colorTextureNT,colorTexture和alphaTexture中采样以应用公式(2)。 那时必须已经准备好纹理,因此我们在调用ApplyTextures()之前使用GL_TEXTURE_FETCH_BARRIER_BIT。
  static constexpr GLfloat clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; static constexpr GLfloat clearAlpha = 1.0f; f->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); f->glClearBufferfv(GL_COLOR, 0, clearColor); f->glClearBufferfv(GL_COLOR, 1, &clearAlpha); f->glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT); PrepareToTransparentRendering(); { // ... rendering transparent objects ... } CleanupAfterTransparentRendering(); f->glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT); f->glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO); ApplyTextures(); 

defaultFBO是一个帧缓冲区,用于在屏幕上显示图片。 在大多数情况下,它是0,但在Qt中,它是QOpenGLWidget :: defaultFramebufferObject()。

在每次调用片段着色器时,我们都可以访问当前片段的颜色和不透明度。 但是在colorTexture中必须出现这些实体的总和(在alphaTexture中是乘积)。 为此,我们将使用混合。 此外,考虑到对于第一个纹理我们计算总和,而对于第二个纹理我们计算乘积,我们必须为每个附件提供不同的混合设置(glBlendFunc和glBlendEquation)。

这是PrepareToTransparentRendering()函数的内容:
  f->glEnable(GL_DEPTH_TEST); f->glDepthMask(GL_FALSE); f->glDepthFunc(GL_LEQUAL); f->glDisable(GL_CULL_FACE); f->glEnable(GL_MULTISAMPLE); f->glEnable(GL_BLEND); f->glBlendFunci(0, GL_ONE, GL_ONE); f->glBlendEquationi(0, GL_FUNC_ADD); f->glBlendFunci(1, GL_DST_COLOR, GL_ZERO); f->glBlendEquationi(1, GL_FUNC_ADD); 


以及CleanupAfterTransparentRendering()函数的内容:
  f->glDepthMask(GL_TRUE); f->glDisable(GL_BLEND); 

在我的片段着色器中,w代表不透明度。 颜色与w的乘积-和w本身-将进入第一个输出参数,而1- w将进入第二个输出参数。 必须以“ location = X”的形式为每个输出参数设置一个布局限定符,其中X是附件数组中一个元素的索引-我们赋予glDrawBuffers函数的元素。 确切地说,位置= 0的输出参数转到绑定到GL_COLOR_ATTACHMENT1的纹理,位置= 1的参数转到绑定到GL_COLOR_ATTACHMENT1的纹理。 在glBlendFunci和glBlendEquationi函数中使用了相同的数字来指示我们为其设置混合参数的颜色附件。

片段着色器:
 #version 450 core in vec3 color; layout (location = 0) out vec4 outData; layout (location = 1) out float alpha; layout (location = 2) uniform float w; void main() { outData = vec4(w * color, w); alpha = 1 - w; } 

在ApplyTextures()函数中,我们仅绘制一个覆盖整个视口的矩形。 片段着色器使用当前屏幕空间坐标作为纹理坐标,并使用当前样本索引(gl_SampleID)作为多样本纹理的样本索引,从所有三个纹理采样数据。 着色器代码中存在gl_SampleID变量,使系统为每个样本调用一次片段着色器(而通常每个像素调用一次,将其输出写入属于原始对象的所有样本)。

顶点着色器很简单:
 #version 450 core const vec2 p[4] = vec2[4]( vec2(-1, -1), vec2( 1, -1), vec2( 1, 1), vec2(-1, 1) ); void main() { gl_Position = vec4(p[gl_VertexID], 0, 1); } 


片段着色器:
 #version 450 core out vec4 outColor; layout (location = 0) uniform sampler2DMS colorTextureNT; layout (location = 1) uniform sampler2DMS colorTexture; layout (location = 2) uniform sampler2DMS alphaTexture; void main() { ivec2 upos = ivec2(gl_FragCoord.xy); vec4 cc = texelFetch(colorTexture, upos, gl_SampleID); vec3 sumOfColors = cc.rgb; float sumOfWeights = cc.a; vec3 colorNT = texelFetch(colorTextureNT, upos, gl_SampleID).rgb; if (sumOfWeights == 0) { outColor = vec4(colorNT, 1.0); return; } float alpha = 1 - texelFetch(alphaTexture, upos, gl_SampleID).r; colorNT = sumOfColors / sumOfWeights * alpha + colorNT * (1 - alpha); outColor = vec4(colorNT, 1.0); } 

最后-ApplyTextures()函数:
  f->glActiveTexture(GL_TEXTURE0); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTextureNT); f->glUniform1i(0, 0); f->glActiveTexture(GL_TEXTURE1); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, colorTexture); f->glUniform1i(1, 1); f->glActiveTexture(GL_TEXTURE2); f->glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, alphaTexture); f->glUniform1i(2, 2); f->glEnable(GL_MULTISAMPLE); f->glDisable(GL_DEPTH_TEST); f->glDrawArrays(GL_TRIANGLE_FAN, 0, 4); 


最后,必须释放OpenGL资源。 我在OpenGL小部件的析构函数中执行此操作:
  f->glDeleteFramebuffers (1, &framebufferNT); f->glDeleteTextures (1, &colorTextureNT); f->glDeleteRenderbuffers(1, &depthRenderbuffer); f->glDeleteFramebuffers (1, &framebuffer); f->glDeleteTextures (1, &colorTexture); f->glDeleteTextures (1, &alphaTexture); 

Source: https://habr.com/ru/post/zh-CN457292/


All Articles