接上篇:UnityShader学习——屏幕后处理效果(亮度等、边缘检测)
3.高斯模糊
模糊的实现有很多方法,例如均值模糊和中值模糊。均值模糊同样使用了卷积操作,它使用的卷积核中的各个元素值都相等,且相加等于1,也就是说,卷积后得到的像素值是其邻域内各个像素值的平均值。而中值模糊则是选择邻域内对所有像素排序后的中值替换掉原颜色。一个更高级的模糊方法是高斯模糊。
高斯模糊同样利用了卷积计算,它使用的卷积核名为高斯核。高斯核是一个正方形大小的滤波核,其中每个元素的计算都是基于下面的高斯方程:
其中,σ
是标准方差(一般取值为1), x
和y
分别对应了当前位置到卷积核中心的整数距离。要构建一个高斯核,我们只需要计算高斯核中各个位置对应的高斯值
。为了保证滤波后的图像不会变暗,我们需要对高斯核中的权重进行归一化
,即让每个权重除以所有权重的和,这样可以保证所有权重的和为1。因此,高斯函数中e前面的系数实际不会对结果有任何影响。
高斯方程很好地模拟了邻域每个像素对当前处理像素的影响程度——距离越近,影响越大
。高斯核的维数越高,模糊程度越大。使用一个NxN的高斯核对图像进行卷积滤波,就需要N×N×W×H(W和H分别是图像的宽和高)次纹理采样。当N的大小不断增加时,采样次数会变得非常巨大。幸运的是,我们可以把这个二维高斯函数拆分成两个一维函数。也就是说,我们可以使用两个一维的高斯核先后对图像进行滤波
,它们得到的结果和直接使用二维高斯核是一样的,但采样次数只需要2×N×W×H。两个一维高斯核中包含了很多重复的权重。对于一个大小为5的一维高斯核,我们实际只需要记录3个权重值即可。
我们将先后调用两个Pass,第一个Pass将会使用竖直方向的一维高斯核对图像进行滤波,第二个Pass再使用水平方向的一维高斯核对图像进行滤波,得到最终的目标图像。在实现中,我们还将利用图像缩放来进一步提高性能,并通过调整高斯滤波的应用次数来控制模糊程度(次数越多,图像越模糊)。
摄像机脚本关键代码:
在高斯核维数不变的情况下,_BlurSize越大,模糊程度越高,但采样数却不会受到影响。但过大的_BlurSize值会造成虚影,这可能并不是我们希望的。downSample越大,需要处理的像素数越少,同时也能进一步提高模糊程度,但过大的downSample可能会使图像像素化。
......
public class GaussianBlur : PostEffectsBase {
......
[Range(0, 4)] public int iterations = 3; //高斯模糊迭代次数
[Range(0.2f, 3.0f)] public float blurSpread = 0.6f;//模糊范围
[Range(1, 8)] public int downSample = 2;//缩放系数
//【版本1】:just apply blur
void OnRenderImage(RenderTexture src, RenderTexture dest) {
if (material != null) {
//利用 RenderTexture.GetTemporary函数分配了一块与屏幕图像大小相同的缓冲区
//高斯模糊需要调用两个Pass,我们需要使用一块中间缓存来存储第一个Pass执行完毕后得到的模糊结果。
int rtW = src.width;
int rtH = src.height;
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//使用第一个Pass(即使用竖直方向的一维高斯核进行滤波)对src进行处理,并将结果存储在了buffer中
Graphics.Blit(src, buffer, material, 0);
//使用第二个Pass(即使用水平方向的一维高斯核进行滤波)对buffer进行处理,返回最终的屏幕图像
Graphics.Blit(buffer, dest, material, 1);
//调用RenderTexture.ReleaseTemporary来释放之前分配的缓存
RenderTexture.ReleaseTemporary(buffer);
} else {
Graphics.Blit(src, dest);
}
}
//【版本2】:scale the render texture
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
//声明缓冲区的大小时,使用了小于原屏幕分辨率的尺寸
//调用第一个Pass时,我们需要处理的像素个数就是原来的几分之一
int rtW = src.width/downSample;
int rtH = src.height/downSample;
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//将该临时渲染纹理的滤波模式设置为双线性
buffer.filterMode = FilterMode.Bilinear;
//其余代码不变
Graphics.Blit(src, buffer, material, 0);
Graphics.Blit(buffer, dest, material, 1);
RenderTexture.ReleaseTemporary(buffer);
} else {
Graphics.Blit(src, dest);
}
}
//【版本3】:use iterations for larger blur
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
int rtW = src.width/downSample;
int rtH = src.height/downSample;
定义了第一个缓存buffer0,并把src中的图像缩放后存储到buffer0中
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0);
//【利用两个临时缓存在迭代之间进行交替】
for (int i = 0; i < iterations; i++) {