Unity Shader系列文章:Unity Shader目录-初级篇
Unity Shader系列文章:Unity Shader目录-中级篇
效果:
卷积的另一个常见应用一高斯模糊。模糊的实现有很多方法,例如均值模糊和中值模糊。均值模糊同样使用了卷积操作,它使用的卷积核中的各个元素值都相等,且相加等于1, 也就是说,卷积后得到的像素值是其邻域内各个像素值的平均值。而中值模糊则是选择邻域内对所有像素排序后的中值替换掉原颜色。 一个更高级的模糊方法是高斯模糊。
高斯模糊同样利用了卷积计算 ,它使用的卷积核名为高斯核 。高斯核是一个正方形大小的滤波核(如下图),其中每个元素的计算都是基于下面的高斯方程:
其中, 是标准方差(一般取值为 1), 分别对应了当前位置到卷积核中心的整数距离。要构建一个高斯核 ,我们只需要计算高斯核中各个位置对应的高斯值 为了保证滤波后的图像不会变暗,我 需要对高斯核中的权重进行归 化,即让每个权重除以所有权重的和,这样可以保证所有权重的和为 。因此 高斯函数中 前面的系数实际不会对结果有任何影响。
高斯方程很好地模拟了邻域每个像素对当前处理像素的影响程度一距离越近,影响越大。高斯核的维数越高,模糊程度越大。使用一个 NxN 的高斯核对图像进行卷积滤波,就需要 NxNxWxH(W和H分别是图像的宽和高)次纹理采样。当N的大小不断增加时,采样次数会变得非常巨大幸运的是,我们可以把这个二维高斯函数拆分成两个一维函数。也就是说,我们可以使用两个一维的高斯核(如图)先后对图像进行滤波,它们得到的结果和直接使用二维高斯核是一样的,但采样次数只需要 2xNxWxH 。我们可以进一步观察到,两个一维高斯核中包含了很多重复的权重。对于一个大小为 的一维高斯核,我们实际只需要记录3个权重值即可。
具体实现思路:
将先后调用两个 Pass,第一个 Pass 将会使用竖直方向的一维高斯核对图像进行滤波,第二个 Pass 再使用水平方向的维高斯核对图像进行滤波,得到最终的目标图像。在实现中,还将利用图像缩放来进一步提高性能,并通过调整高斯滤波的应用次数来控制模糊程度(次数越多,图像越模糊)。
ScreenPostEffectsBase基类代码:
using UnityEngine;
///
/// 屏幕后处理效果基类
///
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class ScreenPostEffectsBase : MonoBehaviour
{
public Shader Shader;
public Material Material
{
get
{
return CheckAndCreateMaterial();
}
}
private Material _material;
protected void Start()
{
CheckResources();
}
///
/// 检查资源
///
protected void CheckResources()
{
if (!CheckSupport())
{
NotSupported();
}
}
///
/// 检查支持
///
///
protected bool CheckSupport()
{
bool isSupported = SystemInfo.supportsImageEffects;
return isSupported;
}
///
/// 不支持
///
protected void NotSupported()
{
enabled = false;
}
///
/// 检查和创建Material
///
///
protected Material CheckAndCreateMaterial()
{
if (!Shader || !Shader.isSupported)
{
return null;
}
if (_material && _material.shader == Shader)
{
return _material;
}
_material = new Material(Shader);
_material.hideFlags = HideFlags.DontSave;
return _material;
}
}
ScreenGaussianBlur派生类代码:
using UnityEngine;
///
/// 屏幕后处理效果:高斯模糊
///
public class ScreenGaussianBlur : ScreenPostEffectsBase
{
///
/// 迭代此时
///
[Range(0, 4)]
public int Iterations = 2;
///
/// 模糊扩散
///
[Range(0.2f, 3)]
public float BlurSpread = 1;
///
/// 缩放系数
///
[Range(1, 8)]
public int DownSample = 2;
// // 第一个版本,最简单的OnRenderlmage实现:
// private void OnRenderImage(RenderTexture src, RenderTexture dest)
// {
// if (Material != null)
// {
// int rtW = src.width;
// int rtH = src.height;
// // 利用 RenderTexture GetTemporary 函数分配了一块与屏幕图像大小相同的缓冲区。
// // 这是因为 高斯模糊需要调用两个Pass, 我们需要使用一块中间缓存来存储第一个Pass执行完毕后得到的模糊结果。
// RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
// // 使用 Shader 中的第1个Pass (即使用竖直方向的一维高斯核进行滤波)对src进行处理,并将结果存储在了buffer中。
// Graphics.Blit(src, buffer, Material, 0);
// // 然后,再使用Shader中的第2个Pass (即使用水平方向的一维高斯核进行滤波)对buffer进行处理,返回最终屏幕图像。
// Graphics.Blit(buffer, dest, Material, 1);
// // 最后 还需要释放之前分配的缓存。
// RenderTexture.ReleaseTemporary(buffer);
// }
// else
// {
// Graphics.Blit(src, dest);
// }
// }
// // 第二个版本实现:利用缩放对图像进行降采样,从而减少需要处理的像素 提高性能。
// private void OnRenderImage(RenderTexture src, RenderTexture dest)
// {
// if (Material)
// {
// int rtw = src.width / DownSample;
// int rtH = src.height / DownSample;
// // 使用了小于原屏幕分辨率的尺寸
// RenderTexture buffer = RenderTexture.GetTemporary(rtw, rtH, 0);
// // 并将该临时渲染纹理的滤波模式设置为双线性
// buffer.filterMode = FilterMode.Bilinear;
// // 这样,在调用第一个 Pass 时,我们需要处理的像素个数就是原来的几分之一。
// // 对图像进行降采样不仅可以减少需要处理的像素个数,提高性能,而且适当的降采样往往还可以得到更好的模糊效果。
// // 尽管 downSample 值越大,性能越好,但过大的 downSample 可能会造成图像像素化。
// Graphics.Blit(src, buffer, Material, 0);
// Graphics.Blit(buffer, dest, Material, 1);
// RenderTexture.ReleaseTemporary(buffer);
// }
// else
// {
// Graphics.Blit(src, dest);
// }
// }
// 第三个版本实现:考虑了高斯模糊的迭代次数,何利用两个临时缓存在迭代之间进行交替的过程。
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (Material)
{
int rtW = src.width / DownSample;
int rtH = src.height / DownSample;
// 定义了第一个缓存 bufferO, 并把 src 中的图像缩放后存储到 bufferO 中
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0);
for (var i = 0; i < Iterations; i++)
{
Material.SetFloat("_BlurSize", 1 + i * BlurSpread);
// 定义了第二个缓存 bufferl
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 执行第一个 Pass 时,输入 bufferO, 输出是 bufferl
Graphics.Blit(buffer0, buffer1, Material, 0);
// 完毕后首先把 bufferO 释放,再把结果值 buffer 存储到 bufferO 中,重新分配 bufferl
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 调用第二个Pass, 重复上述过程
Graphics.Blit(buffer0, buffer1, Material, 1);
// 完毕后再把 bufferO 释放,再把结果值 buffer 存储到 bufferO 中
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
// 迭代完成后 bufferO 将存储最终的图像,将结果显示到屏幕上
Graphics.Blit(buffer0, dest);
// 释放缓存
RenderTexture.ReleaseTemporary(buffer0);
}
else
{
Graphics.Blit(src, dest);
}
}
}
Shader代码:
// 高斯模糊
Shader "Custom/GaussianBlur"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" { }
_BlurSize ("Blur Size", Float) = 1 // 控制采样距离,在高斯核维数不变的情况下, BlurSize 越大,模糊程度越高
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
// _MainTex纹理的纹素大小,例如521x512大小的纹理,值约为0.001953,即1/512
half4 _MainTex_TexelSize;
float _BlurSize;
// 顶点着色器传递给片元着色器的数据
struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
half2 uv[5]: TEXCOORD0; // 数组的第一个坐标存储了当前的采样纹理,而剩余的四个坐标则是高斯模糊中对邻域采样时使用的纹理坐标
};
// 顶点着色器,竖直模糊处理
v2f vertBlurVertical(appdata_img v)
{
v2f o;
// 将顶点坐标从模型空间变换到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
// 竖直的5个相邻的像素
o.uv[0] = uv;
o.uv[1] = uv + float2(0, _MainTex_TexelSize.y * 1) * _BlurSize;
o.uv[2] = uv - float2(0, _MainTex_TexelSize.y * 1) * _BlurSize;
o.uv[3] = uv + float2(0, _MainTex_TexelSize.y * 2) * _BlurSize;
o.uv[4] = uv - float2(0, _MainTex_TexelSize.y * 2) * _BlurSize;
return o;
}
// 顶点着色器,水平模糊处理
v2f vertBlurHorizontal(appdata_img v)
{
v2f o;
// 将顶点坐标从模型空间变换到裁剪空间
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
// 水平的5个相邻的像素
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
{
// 一个 sxs 的二维高斯核可以拆分成两个大小为 5 的一维高斯核, 并且由千它的对称性,我们只需要记录3个高斯权重,
float weight[3] = {
0.4026, 0.2442, 0.0545
};
// 将结果值 sum 初始化为当前的像素值乘以它的权重值
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
// 根据对称性,进行了两次迭代,每次迭代包含了两次纹理采样,并把像素值和权重相乘后的结果叠加到 sum 中
for (int it = 1; it < 3; it++)
{
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
{
// Pass名字,这样别的Shader可以通过此名字调用此Pass
// 如: UsePass "Cusstom/GaussianBlur/GAUSSIAN_BLUR_VERTICAL"
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass
{
// Pass名字,这样别的Shader可以通过此名字调用此Pass
// 如: UsePass "Cusstom/GaussianBlur/GAUSSIAN_BLUR_HORIZONTAL"
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}