高斯模糊
滤波:在图像处理中,通过滤波强调图片的一些特征或去除图片中一些不重要的部分,高斯模糊就是一种滤波方式,它的作用就是让图片平滑的模糊,滤波是一个邻域操作算子,利用给定像素周围的像素值决定此像素的最终输出值
高斯模糊使用高斯核进行卷积运算,也就是对每个像素和其周围像素进行加权平均
图中左侧是一个5×5的高斯核,高斯核中所有的权重值相加为1,且中心权重值大,边缘权重值小,也就是距离越近的像素影响越大
使用一个NxN的高斯核对图像进行卷积滤波,就需要N×N×W×H(W和H是图像的宽和高)次纹理采样,我们可以把二维的高斯核拆成两个一维的高斯核先后对图像进行滤波,得到的结果和使用二维高斯核是一样的,但采样次数只需要2×N×W×H
这两个一维的高斯核是对称的,其中有很多重复的权重,实际只使用了 0.4026,0.2442, 0.0545 这三个权重
实现高斯模糊还需要设置三个参数
- iterations(迭代次数):进行几次高斯滤波,次数越多,图像越模糊
- blurSize(模糊范围):blurSize是用来控制高斯滤波器的偏移量的,也就是每个采样点之间的距离。blurSize越大,采样点越分散,模糊范围越大。但是,blurSize过大也会造成虚影的问题
- downSample(降采样):缩小图像来提高性能,比如1024 x 1024的的图像,降采样为2,图像就被缩小为512 x 512,降采样设置太大可能使图像像素化
代码实现,创建GaussianBlur脚本挂在相机上,用于调整参数
using UnityEngine;
public class GaussianBlur : PostEffectsBase
{
public Shader gaussianBlurShader;
private Material gaussianBlurMaterial = null;
public Material material
{
get
{
gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader, gaussianBlurMaterial);
return gaussianBlurMaterial;
}
}
// 降采样
[Range(1, 8)]
public int downSample = 2;
// 迭代次数
[Range(0, 4)]
public int iterations = 3;
// 模糊范围
[Range(0.1f, 3.0f)]
public float blurSize = 0.6f;
void OnRenderImage (RenderTexture src, RenderTexture dest)
{
if (material == null)
{
Graphics.Blit(src, dest);
return;
}
int rtW = src.width / downSample;
int rtH = src.height / downSample;
// 创建一个临时的渲染纹理并赋值给buffer0
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
// 把src中的图像缩放后存储到buffer0中
Graphics.Blit(src, buffer0);
for (int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1.0f + i * blurSize);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 调用第一个Pass时,输入是buffer0,输出是buffer1,buffer0纹理会被传递给Shader中名为_MainTex的纹理属性
Graphics.Blit(buffer0, buffer1, material, 0);
// 先把buffer0释放,再把buffer0指向buffer1,重新分配buffer1
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 调用第二个Pass
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0, dest);
RenderTexture.ReleaseTemporary(buffer0);
}
}
shader实现,两个Pass使用竖直方向和水平方向的一维高斯核对图像进行滤波,得到最终的目标图像
Shader "MyCustom/GaussianBlur"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader
{
// CGINCLUDE类似于C++中头文件的功能。由于高斯模糊需要定义两个Pass,但它们使用的片元着色器代码
// 是完全相同的,使用CGINCLUDE可以避免我们编写两个完全一样的frag函数。
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
struct v2f
{
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;
};
// appdata_img 只包含顶点坐标和uv
v2f vertBlurVertical(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
// 当前的uv
o.uv[0] = uv;
// 上下1个单位的uv,_BlurSize控制采样距离
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
// 上下2个单位的uv
o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
return o;
}
v2f vertBlurHorizontal(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
return o;
}
fixed4 fragBlur(v2f i) : SV_Target
{
float weight[3] = {0.4026, 0.2442, 0.0545};
// 当前的uv使用权重weight[0]
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
for (int it = 1; it < 3; it++)
{
//上下1个单位(或左右1个单位)的uv使用权重weight[1]
sum += tex2D(_MainTex, i.uv[it * 2 - 1]).rgb * weight[it];
sum += tex2D(_MainTex, i.uv[it * 2]).rgb * weight[it];
}
return fixed4(sum, 1.0);
}
ENDCG
ZTest Always
Cull Off
ZWrite Off
Pass
{
//定义名字,可以在其他shader中使用该pass,必须大写
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass
{
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}
脚本上引用shader,实际效果
用Frame Debug查看渲染过程,第一个和最后一个Pass是用来拷贝贴图的,其他的都是高斯模糊用到的两个Pass,迭代次数加1,就会多两个Pass,这是迭代次数为3的情况
Bloom(辉光效果)
Bloom效果是建立在高斯模糊的基础上的,它让画面中较亮的区域 “扩散” 到周围的区域中,造成一种朦胧的效果
实现过程:
- 根据一个阈值提取出图像中的较亮区域,把它们存储在一张RT中
- 利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果
- 将这张RT和原图像进行混合,得到最终的效果
在相机上添加脚本Bloom,内容和高斯模糊差不多,增加了一个参数用于提取较亮区域
using UnityEngine;
public class Bloom : PostEffectsBase
{
public Shader bloomShader;
private Material bloomMaterial = null;
public Material material
{
get
{
bloomMaterial = CheckShaderAndCreateMaterial(bloomShader, bloomMaterial);
return bloomMaterial;
}
}
// 降采样
[Range(1, 8)]
public int downSample = 2;
// 迭代次数
[Range(0, 4)]
public int iterations = 3;
// 模糊范围
[Range(0.1f, 3.0f)]
public float blurSize = 0.6f;
// 图像的亮度值一般不会超过1。但如果我们开启了HDR,硬件会允许我们把
// 颜色值存储在一个更高精度范围的缓冲中,此时像素的亮度值可能会超过1
[Range(0.0f, 4.0f)]
public float luminanceThreshold = 0.6f;
void OnRenderImage (RenderTexture src, RenderTexture dest)
{
if (material == null)
{
Graphics.Blit(src, dest);
return;
}
material.SetFloat("_LuminanceThreshold", luminanceThreshold);
int rtW = src.width / downSample;
int rtH = src.height / downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
// Pass0提取图像中的较亮区域,提取得到的较亮区域将存储在buffer0中
Graphics.Blit(src, buffer0, material, 0);
// Pass1,Pass2利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果
for (int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1.0f + i * blurSize);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 竖直方向对图像进行滤波
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 水平方向对图像进行滤波
Graphics.Blit(buffer0, buffer1, material, 2);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
material.SetTexture ("_Bloom", buffer0);
// Pass3将buffer0和原图src进行混合
Graphics.Blit(src, dest, material, 3);
RenderTexture.ReleaseTemporary(buffer0);
}
}
shader实现,高斯模糊部分直接引用上面的Pass
Shader "MyCustom/Bloom"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
_Bloom ("Bloom (RGB)", 2D) = "black" {} //高斯模糊后的较亮区域
_LuminanceThreshold ("Luminance Threshold", Float) = 0.5
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _Bloom;
float _LuminanceThreshold;
float _BlurSize;
struct v2f
{
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
v2f vertExtractBright(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed luminance(fixed4 color)
{
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
fixed4 fragExtractBright(v2f i) : SV_Target
{
//提取亮部区域
fixed4 c = tex2D(_MainTex, i.uv);
fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0);
return c * val;
}
struct v2fBloom
{
float4 pos : SV_POSITION;
half4 uv : TEXCOORD0;
};
v2fBloom vertBloom(appdata_img v)
{
v2fBloom o;
o.pos = UnityObjectToClipPos (v.vertex);
//xy分量对应了_MainTex,即原图像的纹理坐标。而它的zw分量是_Bloom
o.uv.xy = v.texcoord;
o.uv.zw = v.texcoord;
//对这个纹理坐标进行平台差异化处理
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0.0)
o.uv.w = 1.0 - o.uv.w;
#endif
return o;
}
fixed4 fragBloom(v2fBloom i) : SV_Target
{
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
}
ENDCG
ZTest Always
Cull Off
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}
UsePass "MyCustom/GaussianBlur/GAUSSIAN_BLUR_VERTICAL"
UsePass "MyCustom/GaussianBlur/GAUSSIAN_BLUR_HORIZONTAL"
Pass
{
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
}
FallBack Off
}
实际效果
用Frame Debug查看渲染过程
这个Pass0处理后的图像,保留了较亮区域,较暗区域为纯黑色
高斯模糊多次迭代后,最后和原图叠加就生成最终效果
参考
《Unity Shader入门精要》
【技术美术百人计划】图形 4.1 Bloom算法 游戏中的辉光效果实现