欢迎光临散文网 会员登陆 & 注册

学习笔记——Bloom

2023-03-16 20:59 作者:残败花纷飞  | 我要投稿

1.应用场景

显示器能产生的光量是有限的。它可以从黑色到全亮态,这在着色器(shader)中对应RGB数值的0和1.这就是光的低动态范围(LDR)。全白像素的亮度应显示器而异,可以根据使用情况做调整,当时他不会过曝到闪瞎我们的狗眼。

真实世界是不会局限于LDR光照的。它没有最大的亮度。同一时间汇集的光子越多,物体就会越亮,直到让我们眼睛有痛觉或闪瞎我们狗眼。

为了表达出更明亮的颜色,我们可以超越LDR进入高动态范围(HDR)。这仅仅意味着我们不局限于最大值为1。shader使用HDR颜色没有问题,只要输入和输出的格式可以存储大于1的值。但是显示器不能超过他们的最大亮度,所以最终的颜色仍然被固定在LDR上。

为了使HDR颜色可见,它们必须映射到LDR上,这被称为色调映射(tonemapping)。这可以使图像非线性变暗,因此可以区分最初的HDR颜色。这有点类似于我们的眼睛如何应对并处理高亮的场景,尽管tonemapping是恒定的。还有动态曝光技术。可以动态调整图像亮度。两者都是可以一起使用的。但是我们的眼睛并不是总能完美适应。有些场景就是单纯的太亮了,让我们无法看清。我们如何能够在LDR显示器上展示这种效果呢?

这个时候Bloom就出现了,bloom是一种通过使像素的颜色渗透到相邻像素来扰乱图像的效果。这就像模糊图像一样,但是基于亮度。这样我们就可以通过模糊来传达过于鲜艳的颜色。这有点类似于光在我们眼睛里的扩散方式,在高亮度的情况下会变得明显,但这主要是一种真实的效果。

许多人不喜欢bloom效果,因为它破坏了原本清晰的图像,是物体看起来不像真实的发光。这并不是bloom的固有缺陷,只是它经常被使用而已。如果你是写实主义,在合理的情况下适度使用bloom即可。bloom也可以用于艺术上的非写实效果,例如梦境序列帧,表示眩晕场景,或者场景过渡等。

1.1.Bloom场景

我将通过相机后期效果组件创建我们自己的bloom效果,创建一个默认照明的新场景。在里面放一堆明亮的物体,在黑暗的背景上。我用了一个黑色的平面,上面有一堆白色、黄色、绿色和红色的立方体和不同大小的球体。确保相机启用了HDR。还要将项目设置为使用线性色彩空间,这样我们才能最好地看到效果。

黑色背景上不同颜色的明亮物体

通常,你会将色调映射应用到线性和HDR渲染的场景中。你可以先进行自动曝光,然后应用bloom,然后执行最终的色调映射。但在本案例中,我将专注于bloom,不会应用任何其他效果。这意味着所有超出LDR的颜色都将在最终图像中被约束。

1.2Bloom影响

创建一个新的BloomEffect组件,让它在编辑模式下执行,并给它一个OnRenderImage方法。最初它不做任何额外的事情,所以只是从源渲染纹理到目标渲染纹理。

Blit(帧缓冲区位块传将一个平面的一部分或全部图象整块从这个平面复制到另一个平面

ExecuteInEditMode:在编辑模式即可预览awake和update等,无需在play模式下

ImageEffectAllowedInSceneView:可在scene窗口下看图像特效等。

让我们也把这个效果应用到场景视图中,这样就更容易从不同的角度看到效果。这是通过将ImageEffectAllowedInSceneView属性添加到类中来实现的。

2.模糊处理

bloom效果是通过获取原始图像,以某种方式模糊它,然后将结果与原始图像结合来创建的。因此,要创造bloom,我们必须首先能够模糊图像。

2.1渲染到另一个纹理

应用效果是通过从一个渲染纹理到另一个渲染纹理来完成的。如果我们在要给pass中完成所有工作,那么我们可以使用适当的着色器简单地从源到目的地进行blit。但是模糊化需要大量的工作。所以让我介绍一个中间步骤。我们首先从源纹理到临时纹理,然后从该纹理到最终目标。

获得一个临时渲染纹理最好是通过调用RenderTexture.GetTemporary来完成。这个方法负责为我们管理临时纹理,创建,缓存和销毁它们,因为Unity更适合这些。至少,我们必须指定纹理的尺寸。我们将从与源纹理相同的大小开始。

调用GetTemporary

当我们要模糊图像时,我们不会对深度缓冲区做任何事情。为此,使用0作为第三个参数。

深度缓冲数值为0

因为使用的是HDR,所以必须使用适当的纹理格式。由于相机应该启用HDR,源纹理的格式将是正确的,所以可以使用它。它很可能是ARGBHalf,但也可能使用另一种格式。

源纹理格式

不是直接从初始帧到目标帧,而是先从初始帧到临时纹理,然后再从临时纹理到目标帧。


插入目标纹理

之后,我们不再需要临时纹理。为了使它可以重用,可以调用RenderTexture.ReleaseTemporary来释放它。

释放临时纹理

虽然结果看起来仍然相同,但我们现在正在通过一个临时纹理传递它。

2.2微减像素采样(Downsampling)

模糊图像是通过平均像素来完成的。对于每个像素,我们必须决定一堆附近的像素来组合。包含哪些像素定义用于效果的过滤器内核。可以通过只平均几个像素来实现一点模糊,这意味着一个小内核。大量的模糊将需要一个大的内核,并结合许多像素。

内核中像素越多,我们对输入纹理采样的次数就越多。由于这是每个像素,一个大的内核可能需要大量的采样工作。所以让我们尽可能的简单。

求平均像素的最简单和最快的方法是利用GPU内置的双线性过滤。如果我们将临时纹理的分辨率减半,那么每组4个源像素就有一个像素。分辨率较低的像素将在原始四个像素之间进行采样,因此我们最终得到它们的平均值。我们甚至不需要使用自定义着色器。


双线性采样

使用一半大小的中间纹理意味着我们将源纹理采样到一半分辨率。在这一步之后,我们从临时纹理到目标纹理,从而再次放大采样到原始分辨率。

双线性上采样,显示插值一个像素。

这是一个两步模糊处理过程,每个像素都与它周围的4×4像素块混合在一起,有四种可能的配置。

所示像素的相对权重,总计64。

结果得到的图像比原始图像更厚实,也更模糊。


使用一半大小的中间纹理(1920*1080)


我们可以通过进一步减小中间步骤的大小来增加效果。


细分等级除以4 8 16 32。


2.3渐进降采样

不幸的是,直接下采样到低分辨率会导致较差的结果。我们通常会丢弃像素,只保留四个像素的孤立组的平均值。

直接切换到四分之一大小会减少16个像素中的12个。

更好的方法是多次降低采样,每一步将分辨率减半,直到达到所需的水平。这样一来,所有像素最终都会对最终结果做出贡献。


Downsampling到一半分辨率两次保存所有像素的信息

为了控制这样做的次数,可以添加一个公共迭代字段。将其设置为范围为1-16的滑块。这将允许我们将65536^2(2的16次幂)的纹理一直压缩到一个像素,这应该足够了。

代码定义迭代模糊的区间


迭代的滑块

要做到这一点,首先将r重命名为currentDestination。在第一个blit之后,添加一个显式的currentSource变量并将currentDestination分配给它,然后将其用于最后一个blit并释放它。

currentDestination

现在我们可以在当前源的声明和最后的blit之间放入一个循环。由于它位于第一个downsample之后,它的迭代器应该从1开始。每一步,从纹理大小再次减半开始。然后抓取一个新的临时纹理和blit当前源到它。然后释放当前源,并将当前目标作为新源。

for循环


这是有效的,除非我们最终有太多的迭代,将大小减小到零。为了防止这种情况发生,在这种情况发生之前跳出循环。典型显示器的高度通常小于它的宽度,因此可以仅以高度为基础。因为单像素线实际上添加的内容并不多,所以当纹理高度下降到2以下时,我已经中止了。

防止过小跳出循环

逐步向下采样2到5个迭代。

2.4渐进升采样

虽然渐进下采样是一种改进,但结果仍然会变得太块状、太快。让我们看看如果我们也逐步向上抽样是否会有帮助。

在两个方向上迭代意味着我们最终要对每个大小渲染两次,除了最小的大小。在每次渲染中,我们不再释放和声明两次相同的纹理,而是在一个数组中跟踪它们。我们可以简单地使用一个固定大小为16的数组字段,这应该足够了。

每次我们抓取一个临时纹理,也将它添加到数组中。

然后在第一个循环之后添加第二个循环。这是从最底层开始的。我们可以从第一个循环中取出迭代器,减去2,并将其作为另一个循环的起点。第二个循环返回,将迭代器一直减小到0。这是我们应该释放旧的源纹理的地方,而不是在第一个循环中。同样,我们也来清理一下这个数组。


6次迭代,有和没有渐进progressive upsampling

结果好了很多,但仍然不够好。

2.5自定义着色器

为了改善模糊,必须切换到比简单的双线性滤波更高级的滤波核。这需要使用一个自定义着色器,所以创建一个新的Bloom着色器。就像DeferredFog着色器一样,从一个简单的着色器开始,它有_MainTex属性,没有剔除,也不使用深度缓冲区。给它一个带有顶点和片段程序的单pass。

顶点程序甚至比雾效果的程序更简单。它只需要转换顶点位置来剪辑空间,并通过全屏四方的纹理坐标。因为我们将以多次传递结束,所以除了片段程序之外的所有内容都可以在CGINCLUDE块中共享和定义。

我们将在传递本身中定义FragmentProgram函数。最初,简单地采样源纹理并将其作为结果使用,使其变为红色以验证我们正在使用自定义着色器。通常HDR颜色以半精度格式存储,所以让我们显式地使用half而不是float,即使这对非移动平台没有区别。


为我们的效果添加一个公共字段来保存对这个着色器的引用,并在检查器中连接它。

添加一个字段来保存将使用这个着色器的材料,它不需要序列化。在渲染之前,检查我们是否有这个材料,如果没有创建它。我们不需要在层次结构中看到它,也不需要保存它,因此相应地设置它的hideFlags。

每次我们blit,它应该用这个材料而不是默认。

自定义shader效果

2.6阀箱取样(Box Sampling)

我们将调整我们的着色器,使它使用不同的采样方法,双线性滤波。因为采样取决于像素大小,所以将神奇的float4 _MainTex_TexelSize变量添加到CGINCLUDE块中。请记住,这对应于源纹理的texel大小,而不是目标纹理。

由于我们总是对主纹理进行采样,只关心RGB通道,让我们创建一个方便的最小Sample函数。

我们不再仅仅依赖于双线性滤波器,而是使用一个简单的盒形滤波器内核。它需要四个样本而不是一个,对角线定位,所以我们得到四个相邻的2×2像素块的平均值。将这些样本相加并除以4,所以我们最终得到4×4像素块的平均值,使我们的内核大小翻倍。


使用4×4框进行下采样,显示样本点。

在我们的片段程序中使用这个采样函数。


6次迭代,使用4×4Box Sampling

2.7不同的pass

结果更加流畅,质量也更高,但也更加模糊。这主要是由于新的4×4框过滤器的上采样。当我们使用源的texel大小来定位样本点时,我们最终覆盖了一个很大的区域,具有不聚焦的规则权重分布。

采样使用4x4的盒子

我们可以通过调整我们用来选择样本点的UV来调整我们的盒子过滤器。为了实现这一点,将delta转换为参数,而不是总是使用1。

复制我们的着色器通道,所以我们最终有两个。第一个-传递0 -将用于下采样,因此它应该使用原始的delta(1)。第二步是上采样,我们将使用0.5的增量。

当UV 降为为0.5时,我们最终覆盖了一个3×3盒子,其中有重叠的样本。因此,一些像素对结果的贡献不止一次,增加了它们的权重。中间像素在所有样本中都涉及,对角线像素只使用一次,而其他像素出现两次。结果是一个更集中的上采样内核。

强制向上采样

接下来,我们必须指出在分位时应该使用哪个通道。为了简化这个操作,在BloomEffect中添加常量,这样我们就可以使用名称而不是索引了。

前两次是向下传递,其他两次是向上传递。


在这一点上,我们有一个相当简单但体面的模糊过程。我们可以使用许多不同的内核来代替这些简单的过滤器内核,每种内核都有自己的优点——比如更好的时间稳定性——和成本。然而,在本教程中,我们将继续使用这些方法。

3.创建Bloom

模糊原始图像是创建bloom的第一步。第二步是将模糊图像与原始图像结合,使其变亮。然而,我们不会只使用最终的模糊结果,因为这会产生相当均匀的模糊。相反,较低的模糊量应该比较高的模糊量对结果的贡献更大。我们可以通过积累中间结果来做到这一点,并在上抽样时添加到旧数据中。


渐进式模糊

3.1叠加式混合

添加到我们已经在某些中间级别可以通过使用添加混合来完成,而不是替换纹理的内容。我们所要做的就是将上采样通道的混合模式设置为one one。

这种简单的方法适用于所有中间通道,但是当我们渲染到最终目的地时就会出错,因为我们还没有渲染到最终目的地。我们最终可能会在每一帧中积累光线,放大图像或其他东西,这取决于Unity如何重用纹理。为了解决这个问题,我们必须为最后一个上样例创建一个单独的通道,在那里我们将原始的源纹理与最后一个中间纹理结合起来。所以我们需要一个着色器变量的来源。

添加第三个通道,它是第二个通道的副本,除了它使用默认的混合模式,并将盒子样本添加到源纹理的样本中。

为这一过程定义一个常数,它将Bloon应用于原始图像。

最后一个blit必须使用这个通道,使用正确的源纹理。



3.2Bloom阈值

现在我们还在模糊整个图像。对于明亮的像素来说,这是最明显的。但是bloom的一个用途是只应用在非常亮的像素上。为了实现这一点,我们必须引入亮度阈值。为此添加一个公共字段,滑动条的范围从0到一些非常亮的值,比如10。让我们使用默认阈值1,不包括LDR像素。


阈值决定了哪些像素有助于Bloom效果。如果它们不够亮,就不应该包括在下采样和上采样过程中。简单地将它们转换为黑色就可以做到这一点,这必须由着色器完成。所以在我们blit之前设置材质的_Threshold变量。


我们将使用阈值过滤掉我们不希望包含的像素。当我们在模糊过程开始时这样做时,它被称为预滤镜步骤。为此创建一个函数,该函数接受颜色并输出过滤后的颜色。


为了避免在着色器中除以零,确保除数的最小值很小,比如0.00001。然后使用结果来调节颜色。

所以复制第一次传递,把它放在顶部作为传递0。将过滤器应用于盒样的结果。



为这个新的传递添加一个常数,并将所有后续传递的索引增加一个。


对第一个 blit 使用新的 pass。

此时,将阈值设置为 1,假设使用的光线和材质没有 HDR 值,您可能看不到或几乎看不到高光溢出。要使光晕出现,您可以增加某些材质的光贡献。例如,我将黄色材质设为自发光,它与反射光一起将黄色像素推入 HDR。

发射黄色

3.3Isolating Bloom

为了更好地了解图像的哪些部分导致了泛光,如果我们能够单独查看模糊效果,那将会很方便。因此,让我们为我们的效果添加一个调试选项,通过一个公共布尔字段进行控制。

我们将为调试目的创建一个单独的通道,因此在底部为其添加一个常量。


在调试模式下,使用调试通道将最后的中间结果直接 blit 到最终目的地,而不是将其添加到源。


新的调试通道简单地执行最后的上采样并将其与任何东西结合起来。



在调试模式下,我们可以清楚地看到黄色像素最终产生了光晕。除此之外,还包括一些白色像素,但前提是它们最终反射了大量的定向光。


3.4Soft Threshold

我们用来调制颜色的拐点曲线以一个角度切入零,导致一个突然的截止点。这就是为什么它也被称为硬膝。这意味着我们最终可以在产生开花的区域和不产生开花的区域之间形成急剧的过渡。这可以在上面屏幕截图中的大白色球体中看到。该领域有一个明确定义的部分被包括在内。这在某种程度上被模糊所混淆,但它仍然是一个严酷的过渡。

可以使这种过渡更平滑,从零贡献到完全贡献。我们将使用滑块来控制它。在 0 处,我们得到了当前的严酷过渡。在 1 处,我们得到一个软阈值,该阈值从亮度 0 一直平滑地淡化光晕,直到它与硬拐点匹配。我们将使用 0.5 作为默认值。

这种淡化也是由着色器完成的,因此将软阈值因子传递给材质。

并将它的变量添加到着色器中。



请注意,软拐点功能的某些部分可以被隔离,因此它们仅取决于配置值,每次通过都是恒定的。我们可以预先计算这些部分并将它们以向量的形式传递给着色器,从而减少它必须做的工作量。我们可以将这些与阈值组合在一个过滤器向量中。


3.5Bloom Intensity

最后,让我们可以调节光晕效果的强度。这使得淡入淡出成为可能,并且还可以创建非常强烈的效果。为此添加一个滑块,范围为 0-10。默认值应为 1。

将此强度值作为材质属性传递给着色器。由于通常使用伽马空间中的一个因子来设置光晕的强度,因此将其从伽马空间转换为线性空间。

将适当的变量添加到着色器。

在最后两次传递中将强度计入最终的盒子样本。

你现在有了一个基本的绽放效果。它与 Unity 的后期处理堆栈版本 2 的绽放效果非常相似。可以通过添加色调、使采样增量可配置、使用不同的过滤器等进一步扩展它。或者您可以转到景深。

本文引用:https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/




学习笔记——Bloom的评论 (共 条)

分享到微博请遵守国家法律