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

我们将讨论“加权混合的不依赖顺序的透明性”(以下简称WBOIT)-JCGT在2013年描述的技术( 链接 )。

当屏幕上出现几个透明物体时,像素的颜色取决于哪一个更靠近观察者。 这是这种情况下的众所周知的混色公式:

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


片段排列的顺序对此很重要:Near片段的颜色及其不透明度用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是不透明片段的颜色,在该不透明片段的顶部绘制了透明片段,其中有n个片段,由索引1至n表示。 C i是第i个透明片段的颜色, αi是其不透明度。

如果仔细观察,则公式(2)有点像公式(1)。 如果你想像到 C 在附近 ,C 0在C 在附近 ,并且 -这是α ,那么这将是第一个公式,一对一。 真的 -这是透明片段颜色的加权平均值(质心在力学上由相同公式确定),它将是最近的片段C near的颜色。 C 0是位于所有片段后面的不透明片段的颜色,为此我们计算了该加权平均值,并且对于C far ,它将通过。 也就是说,我们用一个“平均”片段替换了所有透明片段,并应用了用于混合颜色的标准公式-公式(1)。 原始文章的作者为我们提供的α这个狡猾公式是什么?

 alpha=1 prodi=1n1 alphai


这是n维空间中的标量函数,因此让我们回想一下对多个变量的函数进行的差分分析。 假定所有αi都在0到1的范围内,则相对于任何变量的偏导数将始终是非负常数。 这意味着“平均”片段的不透明度随任何透明片段的不透明度的增加而增加,这正是我们所需要的。 另外,它线性增加。

如果片段的不透明度为0,则根本不可见,也不会影响最终的颜色。

如果至少一个片段的不透明度为1,则α为1。即,不透明的片段变得不可见,通常是好的。 仅位于透明度为1的片段后面的透明片段仍会通过它发光并影响最终的颜色:



在这里,橙色三角形位于其上方,绿色位于其下,灰色和青色位于绿色下,而所有这些都是黑色背景。 蓝色不透明度= 1,其他所有颜色-0.5。 右边的图片是应该的。 如您所见,WBOIT看起来令人作呕。 正常橙色显示的唯一位置是绿色三角形的边缘,周围是不透明的白线。 就像我刚才说的,如果透明片段的不透明度为1,则不透明片段是不可见的。

这在这里更好看:



橙色三角形的不透明度为1,简单地用不透明的对象绘制了透明性已关闭的绿色三角形。 看起来绿色三角形通过橙色三角形穿过橙色。

为了使图片看起来像样,最简单的方法是不为对象分配高不透明度。 在我的工作项目中,我不允许将不透明度设置为大于0.5。 这是3D CAD,其中示意性地绘制了对象,并且不需要特殊的现实感,因此在此允许这样的限制。

使用低不透明度值时,左右图片几乎相同:



而且较高时它们明显不同:



这是透明多面体的样子:




多面体具有橙色的侧面和绿色的水平面。 不幸的是,您乍看之下不会理解这一点,即 图片看起来并不令人信服。 前面有橙色墙的地方,您需要的不仅仅是橙色,而绿色比绿色多。 用一种颜色绘制面会更好:



基于深度的WBOIT


为了以某种方式弥补按深度排序的不足,本文的作者提出了几种在公式(2)中增加深度的选项。 这使实施更加困难,结果难以预测,并且取决于特定三维场景的特征。 我没有深入研究这个主题,所以谁在乎-我建议阅读这篇文章。

有人认为,WBOIT有时具有传统分拣透明性无法提供的功能。 例如,您仅使用两个粒子-深色和浅色烟雾,将烟雾绘制为一个粒子系统。 当一个粒子通过另一个粒子时,经典的颜色混合和分类会产生难看的结果-来自灯光的烟雾的颜色急剧变暗。 文章说,深度敏感的WBOIT允许平滑过渡,并且看起来更可信。 可以用细管的形式为皮毛和头发建模的方式也是如此。

代号


现在介绍如何在OpenGL上实现公式(2)。 示例代码位于Github( 链接 )上,文章中的大多数图片都来自此。 您可以收集和玩我的三角形。 使用Qt框架。

对于那些刚开始研究透明对象的渲染的人,我建议这两篇文章:

学习OpenGL。 第4.3课-混合颜色
使用Direct3D 11和OpenGL 4上的链表的顺序无关的透明度算法

但是,第二篇对于理解该材料并不那么重要,但是第一篇是必读的。

要计算公式(2),我们需要2个额外的帧缓冲区,3个多样本纹理和一个渲染缓冲区,在其中写入深度。 在第一个纹理-colorTextureNT(NT表示非透明)中,我们将渲染不透明的对象。 它的类型为GL_RGB10_A2。 第二个纹理(colorTexture)的类型为GL_RGBA16F; 在此纹理的前三个组成部分中,我们将编写以下公式(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框架,所有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); // ...   ... 

我们只是在colorTextureNT纹理上绘制了所有不透明的对象,并将深度写入渲染缓冲区。 在下一个绘图阶段使用相同的渲染缓冲区之前,您需要确保所有不透明对象的深度均已写入那里。 为此,使用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(); { // ...   ... } 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是附件数组中元素的索引,我们将其传递给第3个列表中的glDrawBuffers(具体来说,location = 0的输出参数将发送到绑定到GL_COLOR_ATTACHMENT0的纹理,以及位置= 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-CN457284/


All Articles