65.屏幕后期处理效果-效果实现-高斯模糊-完整实现
65.1 知识点
高斯模糊效果的计算方式优化
实现高斯模糊效果时,通常需要扩大高斯滤波核的大小以增加模糊程度。然而,这会消耗更多性能。因此,在 Shader 中通常不会提供控制高斯滤波核大小的参数,而是固定滤波核大小为 5x5。为了控制模糊程度,可以采用以下三种方法:
- 控制缩放纹理大小。
- 控制模糊代码执行次数。
- 控制纹理采样间隔距离。
添加控制纹理大小参数
制作思路
在高斯模糊效果中添加控制缩放参数 downSample
,主要用于降低采样质量。
参数应用
在 OnRenderImage
函数中,利用 RenderTexture.GetTemporary
获取渲染纹理缓存时,将源纹理尺寸除以 downSample
。
这样,通过调用 Graphics.Blit
进行图像复制处理时,相当于对源纹理进行了缩小处理,同时在缩小过程中材质球会进行效果处理。
设置缩放过滤模式
在复制处理前,可以设置渲染纹理缓存对象的缩放过滤模式:
buffer.filterMode = FilterMode.Bilinear;
- 过滤模式
FilterMode.Point
点过滤,不进行插值。每个像素直接从最近的纹理像素获取颜色。FilterMode.Bilinear
双线性过滤。采样时使用相邻四个纹理像素的加权平均值进行插值,生成更平滑的图像。FilterMode.Trilinear
三线性过滤。基于双线性过滤的基础,增加了在不同 MIP 贴图级别之间的插值。
创建Lesson65_GaussianBlurPerfect 脚本复制Lesson64_GaussianBlur,添加控制纹理大小参数可以控制模糊程度。这种方式会让计算像素的点数也会减少
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson65_GaussianBlurPerfect : PostEffectBase
{
[Range(1, 8)] public int downSample = 1;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//base.OnRenderImage(source, destination);不需要用Base的 自己重写
//判断这个材质球是否为空 如果不为空 就证明这个shader能用来处理屏幕后处理效果
if (material != null)
{
int rtW = source.width / downSample;
int rtH = source.height / downSample;
//准备一个缓存区
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//采用双线性过滤模式来缩放 可以让缩放效果更平滑
buffer.filterMode = FilterMode.Bilinear;
//进行第一次 水平卷积计算 pass要填0 才会用第一个Pass
Graphics.Blit(source, buffer, material, 0); //Color1
//进行第二次 垂直卷积计算 pass要填1 才会用第二个Pass
Graphics.Blit(buffer, destination, material, 1); //在Color1的基础上乘上Color2 得到最终的高斯模糊计算结果
//释放缓存区
RenderTexture.ReleaseTemporary(buffer);
}
//如果为空 就不用处理后处理效果了 直接显示原画面就可以了
else
{
Graphics.Blit(source, destination);
}
}
}
修改纹理大小参数可以看到模糊的效果
添加控制模糊代码执行次数参数
制作思路
在高斯模糊效果的 C# 代码中,加入一个控制模糊代码执行次数的参数。
这个参数主要用于多次执行材质球中的两个 Pass,因此命名为 iteration(迭代)。
使用方法
- 在
OnRenderImage
函数中,使用一个for
循环,对原图像进行多次高斯模糊效果处理。
注意事项
- 确保每次使用完
RenderTexture.GetTemporary
分配的缓存区后,
必须 调用RenderTexture.ReleaseTemporary
函数将其释放。
添加多次执行模糊代码的脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson65_GaussianBlurPerfect : PostEffectBase
{
[Range(1, 8)] public int downSample = 1;
[Range(1, 128)] public int iterations = 1;
[Range(0, 3)] public float blurSpread = 0.6f;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//base.OnRenderImage(source, destination);不需要用Base的 自己重写
//判断这个材质球是否为空 如果不为空 就证明这个shader能用来处理屏幕后处理效果
if (material != null)
{
int rtW = source.width / downSample;
int rtH = source.height / downSample;
//准备一个缓存区
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//采用双线性过滤模式来缩放 可以让缩放效果更平滑
buffer.filterMode = FilterMode.Bilinear;
//直接缩放写入到缓存纹理中
Graphics.Blit(source, buffer);
//多次去执行 高斯模糊逻辑
for (int i = 0; i < iterations; i++)
{
//又声明一个新的缓冲区
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//因为我们需要用两个Pass 处理图像两次 所以用两个buffer做
//进行第一次 水平卷积计算
Graphics.Blit(buffer, buffer1, material, 0); //Color1
//这时 关键内容都在buffer1中 buffer没用了 释放掉
RenderTexture.ReleaseTemporary(buffer);
//让两个buffer指向的内存空间相等
buffer = buffer1;
//又声明一个新的缓冲区
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//进行第二次 垂直卷积计算
Graphics.Blit(buffer, buffer1, material, 1); //在Color1的基础上乘上Color2 得到最终的高斯模糊计算结果
//释放缓存区
RenderTexture.ReleaseTemporary(buffer);
//buffer和buffer1指向的都是这一次高斯模糊处理的结果
buffer = buffer1;
}
//在for循环中得到最终的模糊结果 然后写入到目标纹理中
Graphics.Blit(buffer, destination);
//释放掉缓存区
RenderTexture.ReleaseTemporary(buffer);
}
//如果为空 就不用处理后处理效果了 直接显示原画面就可以了
else
{
Graphics.Blit(source, destination);
}
}
}
修控制模糊代码执行次数参数可以看到模糊的效果,但是不建议值改很大,不然Pass要执行很多次
添加控制纹理采样间隔距离
制作思路
在高斯模糊效果的 Shader 代码中,加入一个控制纹理采样间隔距离的属性 _BlurSpread
(模糊半径)。该属性的主要作用是控制 UV 坐标偏移的间隔单位,用于调整模糊效果。
参数使用方式
在顶点着色器中进行 UV 坐标偏移时,乘以 _BlurSpread
属性值,可以通过该参数控制偏移量的大小,从而调整模糊效果。
注意事项
像素偏移的理论基础
理论上,像素是以 1 个单位为间隔进行偏移的,因此_BlurSpread
的变化通常为整数。精细化控制
为了更加精确地调整模糊程度,可以将_BlurSpread
设置为小数值。通过小数变化,可以更细微地控制模糊效果,实现更平滑的图像处理。
复制Lesson64_GaussianBlur.shader,基于他修改。添加纹理偏移间隔单位参数和属性映射,在水平和竖直顶点函数计算uv时乘上。
Shader "Unlit/Lesson65_GaussianBlurPerfect"
{
Properties
{
// 主纹理
_MainTex ("Texture", 2D) = "white" {}
//纹理偏移间隔单位
_BlurSpread("BlurSpread", Float) = 1
}
SubShader
{
CGINCLUDE
//...
//纹理偏移间隔单位
float _BlurSpread;
//...
ENDCG
//...
Pass
{
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
//水平方向的 顶点着色器函数
v2f vertBlurHorizontal(appdata_base appdata_base)
{
v2f v2f;
v2f.vertex = UnityObjectToClipPos(appdata_base.vertex);
//5个像素的uv偏移
half2 uv = appdata_base.texcoord;
//去进行5个像素 水平位置的偏移获取
v2f.uv[0] = uv;
v2f.uv[1] = uv + half2(_MainTex_TexelSize.x * 1, 0)*_BlurSpread;
v2f.uv[2] = uv - half2(_MainTex_TexelSize.x * 1, 0)*_BlurSpread;
v2f.uv[3] = uv + half2(_MainTex_TexelSize.x * 2, 0)*_BlurSpread;
v2f.uv[4] = uv - half2(_MainTex_TexelSize.x * 2, 0)*_BlurSpread;
return v2f;
}
ENDCG
}
Pass
{
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
//竖直方向的 顶点着色器函数
v2f vertBlurVertical(appdata_base appdata_base)
{
v2f v2f;
v2f.vertex = UnityObjectToClipPos(appdata_base.vertex);
//5个像素的uv偏移
half2 uv = appdata_base.texcoord;
//去进行5个像素 水平位置的偏移获取
v2f.uv[0] = uv;
v2f.uv[1] = uv + half2(0, _MainTex_TexelSize.x * 1)*_BlurSpread;
v2f.uv[2] = uv - half2(0, _MainTex_TexelSize.x * 1)*_BlurSpread;
v2f.uv[3] = uv + half2(0, _MainTex_TexelSize.x * 2)*_BlurSpread;
v2f.uv[4] = uv - half2(0, _MainTex_TexelSize.x * 2)*_BlurSpread;
return v2f;
}
ENDCG
}
}
Fallback Off
}
在C#脚本添加控制纹理采样间隔距离参数,在OnRenderImage设置shader的_BlurSpread属性。可以在循环外设置,也可以再循环内设置,循环内设置效果跟家明显。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson65_GaussianBlurPerfect : PostEffectBase
{
[Range(1, 8)] public int downSample = 1;
[Range(1, 128)] public int iterations = 1;
[Range(0, 3)] public float blurSpread = 0.6f;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//base.OnRenderImage(source, destination);不需要用Base的 自己重写
//判断这个材质球是否为空 如果不为空 就证明这个shader能用来处理屏幕后处理效果
if (material != null)
{
int rtW = source.width / downSample;
int rtH = source.height / downSample;
//准备一个缓存区
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//采用双线性过滤模式来缩放 可以让缩放效果更平滑
buffer.filterMode = FilterMode.Bilinear;
//直接缩放写入到缓存纹理中
Graphics.Blit(source, buffer);
//在使用材质球之前设置 模糊半径属性
//material.SetFloat("_BlurSpread", blurSpread);
//多次去执行 高斯模糊逻辑
for (int i = 0; i < iterations; i++)
{
//如果想要模糊半径影响模糊想过更强烈 更平滑
//一般可以在我们的迭代中进行设置 相当于每次迭代处理高斯模糊时 都在增加我们的间隔距离
material.SetFloat("_BlurSpread", 1 + i * blurSpread);
//又声明一个新的缓冲区
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//因为我们需要用两个Pass 处理图像两次 所以用两个buffer做
//进行第一次 水平卷积计算
Graphics.Blit(buffer, buffer1, material, 0); //Color1
//这时 关键内容都在buffer1中 buffer没用了 释放掉
RenderTexture.ReleaseTemporary(buffer);
//让两个buffer指向的内存空间相等
buffer = buffer1;
//又声明一个新的缓冲区
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//进行第二次 垂直卷积计算
Graphics.Blit(buffer, buffer1, material, 1); //在Color1的基础上乘上Color2 得到最终的高斯模糊计算结果
//释放缓存区
RenderTexture.ReleaseTemporary(buffer);
//buffer和buffer1指向的都是这一次高斯模糊处理的结果
buffer = buffer1;
}
//在for循环中得到最终的模糊结果 然后写入到目标纹理中
Graphics.Blit(buffer, destination);
//释放掉缓存区
RenderTexture.ReleaseTemporary(buffer);
}
//如果为空 就不用处理后处理效果了 直接显示原画面就可以了
else
{
Graphics.Blit(source, destination);
}
}
}
改控制纹理采样间隔距离查看效果
总结
在实现高斯模糊的过程中,我们并未通过改变高斯滤波核的大小来控制最终的模糊程度,而是采用了以下三种方式。尽管这种做法并不完全符合高斯模糊的理论,但它具有以下优点:
高效简单
不依赖复杂的滤波核计算,减少计算开销。灵活性强
可以通过多种参数调整模糊效果,适应不同需求。效果可接受
最终的视觉效果与理论实现差异不大,满足应用需求。
这进一步印证了图像学中的一个重要原则: 只要最终效果是好的,就不必严格遵循数学和物理规则。
在实际开发中,我们应更多地从以下几个方向来解决问题:
效果优先
以用户感知为核心,确保最终呈现效果优质。性能优先
优化算法和资源消耗,提升运行效率。开发效率优先
简化实现逻辑,提升开发迭代速度。
65.2 知识点代码
Lesson65_屏幕后期处理效果_效果实现_高斯模糊_完整实现.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson65_屏幕后期处理效果_效果实现_高斯模糊_完整实现 : MonoBehaviour
{
void Start()
{
#region 知识回顾 高斯模糊效果的计算方式优化
//想要图片变模糊,那么需要扩大高斯滤波核的大小,越大越模糊
//如果通过扩大高斯滤波核的大小来达到更模糊的目的,付出的代价就是会更加消耗性能
//因此在Shader中我们不会提供控制高斯滤波核大小的参数,我们的滤波核始终会使用5x5大小的
//因此,我们就只能使用其他方式来控制模糊程度了,我们一般会使用以下三种方式:
//1.控制缩放纹理大小
//2.控制模糊代码执行次数
//3.控制纹理采样间隔距离
#endregion
#region 知识点一 添加控制纹理大小参数
//在高斯模糊效果的C#代码中加入一个控制缩放的参数
//它主要是用来降低采样质量的,因此取名叫 downSample(降低采样)
//如何使用该参数:
//在OnRenderImage函数中
//我们使用RenderT.GetTemporary获取渲染纹理缓存区时
//用源纹理尺寸除以 downSample
//这样在调用Graphics.Blit进行图像复制处理时
//相当于就将源纹理缩小了,同时在缩小的过程过程中还会用材质球进行效果处理
//注意:
//在进行复制处理之前,我们可以设置渲染纹理缓存对象的 缩放过滤模式
//buffer.filterMode = FilterMode.Bilinear;
//FilterMode.Point:点过滤。不进行插值。每个像素都直接从最近的纹理像素获取颜色
//FilterMode.Bilinear:双线性过滤。它在纹理采样时使用相邻四个纹理像素的加权平均值进行插值,以生成更平滑的图像
//FilterMode.Trilinear:三线性过滤。它在双线性过滤的基础上增加了在不同 MIP 贴图级别之间的插值。
#endregion
#region 知识点二 添加控制模糊代码执行次数参数
//在高斯模糊效果的C#代码中加入一个控制模糊代码执行次数的参数
//它主要是用来多次执行材质球中的两个Pass,因此取名叫 iteration(迭代)
//如何使用该参数:
//在OnRenderImage函数中
//我们使用一个for循环,来对原图像进行多次高斯模糊效果处理
//注意:
//要保证每次使用完 RenderT.GetTemporary 分配的缓存区
//都要使用 RenderTexture.ReleaseTemporary 函数将其释放
#endregion
#region 知识点三 添加控制纹理采样间隔距离
//在高斯模糊效果的Shader代码中加入一个控制纹理采样间隔距离的属性
//它主要是用来控制间隔多少单位偏移uv坐标,因此取名叫 _BlurSpread(模糊半径)
//如何使用该参数:
//在顶点着色器进行uv坐标偏移时,乘以该属性,可以通过它控制偏移的多少
//注意:
//理论上来说像素是1个单位1个单位偏移的
//_BlurSpread应该为整数变化
//但是为了更精细的控制模糊程度,我们可以让其为小数
//小数变化可以更细微的调整模糊程序
#endregion
#region 总结
//我们并没有通过改变高斯滤波核的大小来控制最终的模糊程度
//而是通过以上三种方式
//虽然这样并不符合高斯模糊的理论
//但是这样更加的高效简单,灵活性也更强,效果也是可以接受的
//这样更加印证了
//图像学中,只要最终的效果是好的,那么不必严格遵循数学和物理规则
//我们应该更多的从效果优先、性能优先、开发效率优先的方向去解决问题
#endregion
}
}
Lesson65_GaussianBlurPerfect.shader
Shader "Unlit/Lesson65_GaussianBlurPerfect"
{
Properties
{
// 主纹理
_MainTex ("Texture", 2D) = "white" {}
//纹理偏移间隔单位
_BlurSpread("BlurSpread", Float) = 1
}
SubShader
{
//用于包裹共用代码 在之后的多个Pass当中都可以使用的代码
CGINCLUDE
//内置文件
#include "UnityCG.cginc"
// 主纹理
sampler2D _MainTex;
//纹素 x=1/宽 y=1/高
half4 _MainTex_TexelSize;
//纹理偏移间隔单位
float _BlurSpread;
struct v2f
{
//5个像素的uv坐标偏移
half2 uv[5] : TEXCOORD0;
//顶点在裁剪空间下坐标
float4 vertex : SV_POSITION;
};
//片元着色器函数
//两个Pass可以使用同一个 我们把里面的逻辑写的通用即可
fixed4 fragBlur(v2f v2f):SV_Target
{
//卷积运算
//卷积核 其中的三个数 因为只有这三个数 没有必要声明为5个单位的卷积核
float weight[3] = {0.4026, 0.2442, 0.0545};
//先计算当前像素点
fixed3 sum = tex2D(_MainTex, v2f.uv[0]).rgb * weight[0];
//去计算左右偏移1个单位的 和 左右偏移两个单位的 对位相乘 累加
// 其实就是遍历拿到index为1 2 3 4的uv采样并和对应的卷积核相乘
// 1 2 对应0.2442
// 3 4 对应0.0545
for (int it = 1; it < 3; it++)
{
//要和右元素相乘
sum += tex2D(_MainTex, v2f.uv[it * 2 - 1]).rgb * weight[it];
//和左元素相乘
sum += tex2D(_MainTex, v2f.uv[it * 2]).rgb * weight[it];
}
return fixed4(sum, 1);
}
ENDCG
Tags
{
"RenderType"="Opaque"
}
// 设置深度测试模式为始终通过
// 这意味着无论新绘制的片元的深度值与当前深度缓冲区中的值关系如何,该片元都将通过深度测试并有可能被绘制
ZTest Always
// 关闭背面剔除功能
// 通常在渲染场景时,为了提高性能会剔除那些背对摄像机的面,这里关闭此功能后,所有面都会被考虑渲染
Cull Off
// 关闭深度写入功能
// 即不将绘制的片元的深度值写入深度缓冲区,这样可能会导致一些渲染效果上的特殊情况,比如多个物体在深度上可能会出现重叠绘制的现象
ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
//水平方向的 顶点着色器函数
v2f vertBlurHorizontal(appdata_base appdata_base)
{
v2f v2f;
v2f.vertex = UnityObjectToClipPos(appdata_base.vertex);
//5个像素的uv偏移
half2 uv = appdata_base.texcoord;
//去进行5个像素 水平位置的偏移获取
v2f.uv[0] = uv;
v2f.uv[1] = uv + half2(_MainTex_TexelSize.x * 1, 0)*_BlurSpread;
v2f.uv[2] = uv - half2(_MainTex_TexelSize.x * 1, 0)*_BlurSpread;
v2f.uv[3] = uv + half2(_MainTex_TexelSize.x * 2, 0)*_BlurSpread;
v2f.uv[4] = uv - half2(_MainTex_TexelSize.x * 2, 0)*_BlurSpread;
return v2f;
}
ENDCG
}
Pass
{
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
//竖直方向的 顶点着色器函数
v2f vertBlurVertical(appdata_base appdata_base)
{
v2f v2f;
v2f.vertex = UnityObjectToClipPos(appdata_base.vertex);
//5个像素的uv偏移
half2 uv = appdata_base.texcoord;
//去进行5个像素 水平位置的偏移获取
v2f.uv[0] = uv;
v2f.uv[1] = uv + half2(0, _MainTex_TexelSize.x * 1)*_BlurSpread;
v2f.uv[2] = uv - half2(0, _MainTex_TexelSize.x * 1)*_BlurSpread;
v2f.uv[3] = uv + half2(0, _MainTex_TexelSize.x * 2)*_BlurSpread;
v2f.uv[4] = uv - half2(0, _MainTex_TexelSize.x * 2)*_BlurSpread;
return v2f;
}
ENDCG
}
}
Fallback Off
}
Lesson65_GaussianBlurPerfect.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson65_GaussianBlurPerfect : PostEffectBase
{
[Range(1, 8)] public int downSample = 1;
[Range(1, 128)] public int iterations = 1;
[Range(0, 3)] public float blurSpread = 0.6f;
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//base.OnRenderImage(source, destination);不需要用Base的 自己重写
//判断这个材质球是否为空 如果不为空 就证明这个shader能用来处理屏幕后处理效果
if (material != null)
{
int rtW = source.width / downSample;
int rtH = source.height / downSample;
//准备一个缓存区
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
//采用双线性过滤模式来缩放 可以让缩放效果更平滑
buffer.filterMode = FilterMode.Bilinear;
//直接缩放写入到缓存纹理中
Graphics.Blit(source, buffer);
//在使用材质球之前设置 模糊半径属性
//material.SetFloat("_BlurSpread", blurSpread);
//多次去执行 高斯模糊逻辑
for (int i = 0; i < iterations; i++)
{
//如果想要模糊半径影响模糊想过更强烈 更平滑
//一般可以在我们的迭代中进行设置 相当于每次迭代处理高斯模糊时 都在增加我们的间隔距离
material.SetFloat("_BlurSpread", 1 + i * blurSpread);
//又声明一个新的缓冲区
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//因为我们需要用两个Pass 处理图像两次 所以用两个buffer做
//进行第一次 水平卷积计算
Graphics.Blit(buffer, buffer1, material, 0); //Color1
//这时 关键内容都在buffer1中 buffer没用了 释放掉
RenderTexture.ReleaseTemporary(buffer);
//让两个buffer指向的内存空间相等
buffer = buffer1;
//又声明一个新的缓冲区
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//进行第二次 垂直卷积计算
Graphics.Blit(buffer, buffer1, material, 1); //在Color1的基础上乘上Color2 得到最终的高斯模糊计算结果
//释放缓存区
RenderTexture.ReleaseTemporary(buffer);
//buffer和buffer1指向的都是这一次高斯模糊处理的结果
buffer = buffer1;
}
//在for循环中得到最终的模糊结果 然后写入到目标纹理中
Graphics.Blit(buffer, destination);
//释放掉缓存区
RenderTexture.ReleaseTemporary(buffer);
}
//如果为空 就不用处理后处理效果了 直接显示原画面就可以了
else
{
Graphics.Blit(source, destination);
}
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com