图形学基础|泛光(Bloom)
文章目录
- 图形学基础|泛光(Bloom)
- 一、前言
- 二、Bloom概述
- 2.1 亮度提取
- 2.2 模糊
- 2.2.1 高斯模糊
- 2.2.2 可分离滤波
- 2.2.3 双重模糊
- 2.3 叠加
- 2.4 小结
- 三、虚幻Bloom
- 参考博文
一、前言
泛光(Bloom) 效果会产生从图像明亮区域边界向外延伸的光线条纹,给人的感觉是极其明亮的光线压制住了摄像机。
这是一种现实世界中的光现象,通过它能够以较为适度的渲染性能成本极大地增强渲染图像的真实感。通常在后处理阶段实现。
泛光效果图:
二、Bloom概述
Bloom的实现非常简单,共分为三个步骤:
- 通过一个阈值,对需要处理的图像经过亮度提取;
- 对经过亮度提取的图像进行模糊处理;
- 叠加原图和模糊处理后的图像;
整个过程的示意图如下所示:
2.1 亮度提取
亮度提取很简单,首先给出标准的亮度公式:
为便于计算,快速算法不采用标准公式,而使用以下只取小数点后两个有效位的近似公式:
用代码表示即:
float Luminance(float3 Linear)
{
return dot(Linear, float3(0.3, 0.59, 0.11));
}
亮度提取成功之后我们可以通过一个阙值来控制,并且把值限制在0-1范围内。
具体的Shader实现如下:
RWTexture2D<float3> BloomResult : register(u0);
Texture2D SceneColorTexture : register(t0);
SamplerState LinearSampler : register(s0);
void GetSampleUV(uint2 ScreenCoord, inout float2 UV, inout float2 HalfPixelSize)
{
float2 ScreenSize;
BloomResult.GetDimensions(ScreenSize.x, ScreenSize.y);
float2 InvScreenSize = rcp(ScreenSize);
HalfPixelSize = 0.5 * InvScreenSize;
UV = ScreenCoord * InvScreenSize + HalfPixelSize;
}
[numthreads(8, 8, 1)]
void CS_ExtractBloom(uint3 DispatchThreadID : SV_DispatchThreadID)
{
float2 HalfPixelSize, UV;
GetSampleUV(DispatchThreadID.xy, UV, HalfPixelSize);
float3 Color = SceneColorTexture.SampleLevel(LinearSampler, UV, 0).xyz;
// clamp to avoid artifacts from exceeding fp16 through framebuffer blending of multiple very bright lights
Color.rgb = min(float3(256 * 256, 256 * 256, 256 * 256), Color.rgb);
half TotalLuminance = Luminance(Color);
half BloomLuminance = TotalLuminance - BloomThreshold;
// clamp to [0,1]
half BloomAmount = saturate(BloomLuminance * 0.5f);
BloomResult[DispatchThreadID.xy] = BloomAmount * Color;
}
2.2 模糊
在介绍本文泛光使用的具体模糊算法之前,会先介绍一下高斯模糊,以及可分离滤波。
这个两个概念会在以后涉及到的图形算法中有广泛的应用。
在这之后,将介绍本文具体使用的算法——双重模糊(Dual Blur)。
2.2.1 高斯模糊
图像与滤波中提到:
- 从数字信号处理的角度看,图像其实是一种波,可以用波的算法处理图像。
- 色彩剧烈变化的地方,就是图像的高频区域;色彩稳定平滑的地方,就是低频区域。
而图像模糊的本质一个过滤高频信号,保留低频信号的过程。
过滤高频的信号的一个常见可选方法是卷积滤波,以理解成每一个像素都取周边像素的平均值。
如下图所示,中间点和周围的点都取权重为1,进行求平均值,这是最简单的均值模糊。
在数值上,这是一种"平滑化"。在图形上,就相当于产生"模糊"效果,"中间点"失去细节。
如果使用简单平均,显然不是很合理,因为图像都是连续的,越靠近的点关系越密切,越远离的点关系越疏远。
因此,加权平均更合理,距离越近的点权重越大,距离越远的点权重越小。
正态分布显然是一种可取的权重分配模式。
在图形上,正态分布是一种钟形曲线,越接近中心,取值越大,越远离中心,取值越小。
上面的正态分布是一维的,图像都是二维的,所以我们需要二维的正态分布。
假定中心点的坐标是(0,0),那么距离它最近的8个点的坐标如下:
更远的点以此类推。
为了计算权重矩阵,需要设定σ的值。假定σ=1.5,则模糊半径为1的权重矩阵如下:
这9个点的权重总和等于0.4787147,如果只计算这9个点的加权平均,还必须让它们的权重之和等于1。
因此上面9个值还要分别除以0.4787147,得到最终的权重矩阵。
以上就是高斯模糊的权重矩阵生成过程。
那么计算高斯模糊,只需要像素值乘以对应的权重系数,再将各个结果进行叠加即可。
2.2.2 可分离滤波
图像滤波,在原图像上移动滤波器的过程中每一次的计算结果都不会影响到后面过程的计算结果。
因此,这是一个并行的算法,可以在GPU中进行实现,极大地加快图像滤波的处理速度。
当权重矩阵满足线性可分(Linearly separable) 性质,可以将滤波拆解为两个方向的滤波。
使用左侧的权重矩阵进行滤波和右侧先进行X方向滤波再进行Y方向滤波(顺序可以互换),二者结果都是相同的。
高斯模糊就是一种满足线性可分的,下图展示了一个Gaussian Kernel的线性分解过程:
通过这样的方式,可以降低纹理采样的数量,降低复杂度。
原本所需要的的采样数量为:
经过优化只需要:
其中,M和N是需要进行滤波的图像的维数,m和n是滤波器的维数。
不过,若采样分离滤波,则需要多使用一张临时纹理。
2.2.3 双重模糊
图像模糊算法在后处理渲染领域中占据着重要的地位。
高品质后处理:十种图像模糊算法的总结与实现对十种模糊算法进行总结、对比和盘点。
模糊算法的优劣,决定了后处理管线最终的渲染品质和消耗性能的多少。
文中提到了要评判一种模糊算法的好坏,主要有三个标准:
- 模糊品质(Quality) 。模糊品质的好坏是模糊算法是否优秀的主要指标。
- 模糊稳定性(Stability) 。模糊的稳定性决定了在画面变化过程中,模糊是否稳定,不会出现跳变或者闪烁。
- 性能(Performance) 。性能的好坏是模糊算法是否能被广泛使用的关键所在。
并给出十种图像模糊算法横向对比:
其中,双重模糊(Dual Blur)在三项指标均位于最佳,因而本文泛光中所需要的模糊采用该种模糊算法进行实现。
Dual Blur衍生于生于Kawase Filter,但是Kawase filter需要在相同分辨率下作乒乓模糊。
而Dual-Filter通过下采样和上采样实现。Dual filter对下采样和上采样通道使用不同的滤波内核,但采样距离是恒定的。
下采样滤波器通过对覆盖目标像素的四个像素进行采样来工作。
对角上的四个样本进行采样,以便从所有相邻像素中涂抹一些信息。这使得它总共有5个样本。
上采样滤波器通过从下采样过程中重建信息来工作。
选择这种图案是为了获得漂亮的光滑圆形。如果您想要更具艺术感的外观,也可以使用其他形状。还可以调整从目标像素到采样点的距离,以获得更大或更小的模糊。
具体的Shader实现如下:
[numthreads(8, 8, 1)]
void CS_DownSample(uint3 DispatchThreadID : SV_DispatchThreadID)
{
float2 HalfPixelSize, UV;
GetSampleUV(DispatchThreadID.xy, UV, HalfPixelSize);
float3 Result = SceneColorTexture.SampleLevel(LinearSampler, UV, 0).xyz * 4.0;
Result += SceneColorTexture.SampleLevel(LinearSampler, UV - HalfPixelSize, 0).xyz;
Result += SceneColorTexture.SampleLevel(LinearSampler, UV + HalfPixelSize, 0).xyz;
Result += SceneColorTexture.SampleLevel(LinearSampler, UV + float2(HalfPixelSize.x, -HalfPixelSize.y), 0).xyz;
Result += SceneColorTexture.SampleLevel(LinearSampler, UV + float2(-HalfPixelSize.x, HalfPixelSize.y), 0).xyz;
BloomResult[DispatchThreadID.xy] = Result / 8.0;
}
[numthreads(8, 8, 1)]
void CS_UpSample(uint3 DispatchThreadID : SV_DispatchThreadID)
{
float2 HalfPixelSize, UV;
GetSampleUV(DispatchThreadID.xy, UV, HalfPixelSize);
float3 Result = 0;
Result += SceneColorTexture.SampleLevel(LinearSampler, UV + float2(-HalfPixelSize.x * 2.0, 0.0), 0).xyz; //left
Result += SceneColorTexture.SampleLevel(LinearSampler, UV + float2(+HalfPixelSize.x * 2.0, 0.0), 0).xyz; //right
Result += SceneColorTexture.SampleLevel(LinearSampler, UV + float2(0.0, -HalfPixelSize.y * 2.0), 0).xyz; //up
Result += SceneColorTexture.SampleLevel(LinearSampler, UV + float2(0.0, +HalfPixelSize.y * 2.0), 0).xyz; //bottom
Result += SceneColorTexture.SampleLevel(LinearSampler, UV + float2(-HalfPixelSize.x, -HalfPixelSize.y), 0).xyz * 2.0; //top-left
Result += SceneColorTexture.SampleLevel(LinearSampler, UV + float2(+HalfPixelSize.x, -HalfPixelSize.y), 0).xyz * 2.0; //top-right
Result += SceneColorTexture.SampleLevel(LinearSampler, UV + float2(-HalfPixelSize.x, +HalfPixelSize.y), 0).xyz * 2.0; //bottom-left
Result += SceneColorTexture.SampleLevel(LinearSampler, UV + float2(+HalfPixelSize.x, +HalfPixelSize.y), 0).xyz * 2.0; //bottom-right
BloomResult[DispatchThreadID.xy] = Result / 12.0;
}
2.3 叠加
叠加这个过程就更简单了,就是将模糊后的图像和原图进行叠加。
混合的公式可以为:输出 = 原始图像 + 模糊图像 * bloom颜色 * bloom权值。
具体的Shader代码如下:
float4 PS_Merge(in VertexOutput Input) : SV_Target0
{
float3 Color = SceneColorTexture.Sample(LinearSampler, Input.Tex).xyz;
float3 Bloom = BloomTexture.Sample(LinearSampler, Input.Tex).xyz;
return float4(Color + Bloom * BloomIntensity, 1.0);
}
2.4 小结
Bloom是后处理比较简单的一种效果。
下面给出笔者实现的效果示意图。
- 上边为没有开启Bloom的效果;
- 下边为开启了Bloom的效果;
三、虚幻Bloom