64.屏幕后期处理效果-效果实现-高斯模糊-基础实现
64.1 知识点
知识回顾
在利用两个一维高斯滤波核计算高斯模糊时,对于一个5x5的滤波核,其元素,无论是水平还是竖直方向,都是 (0.0545, 0.2442, 0.4026, 0.2442, 0.0545)
。其中主要的数值为 0.4026
, 0.2442
, 0.0545
,这些数值将用于卷积计算。
知识补充:封装共享代码预处理指令 CGINCLUDE
在实现高斯模糊效果时,我们将在Shader中利用两个Pass来分别计算水平卷积和竖直卷积,而两个Pass会存在相同的代码。我们将使用一个新的预处理指令 CGINCLUDE
,将共享的代码包裹在其中。它写在SubShader语句块中,Pass外。
CGINCLUDE
//....
//中间包裹CG代码
//....
ENDCG
CGINCLUDE
的作用是用于封装共享代码,可以在其中定义常量、函数、结构体、宏等内容。这些封装起来的代码可以在同一个Shader文件中的多个Pass中使用,也可以在其他Shader文件中引用。使用它可以避免重复编写相同的代码,从而提高代码复用性和可维护性。
实现高斯模糊基础效果的制作思路
在Shader中编写两个Pass,一个用于计算水平方向的卷积,另一个用于计算竖直方向的卷积。
两个Pass的区别:
- 顶点着色器中计算的UV偏移位置不同,一个是水平偏移,一个是竖直偏移。
两个Pass的共同点:
- 使用相同的内置文件。
- 使用相同的属性。
- 片元着色器的计算规则可以相同。我们可以用UV数组存储5个像素的UV坐标偏移。数组中存储的像素UV偏移分别为:
- 元素index: 0, 1, 2, 3, 4
- x或y相对于原点的偏移: 0, 1, -1, 2, -2
其中:
- 第0个元素对应的高斯核元素为0.4026
- 第1, 2个元素对应的高斯核元素为0.2442
- 第3, 4个元素对应的高斯核元素为0.0545
因此,无论是竖直还是水平卷积,都可以使用统一的计算规则进行处理。
实现高斯模糊基础屏幕后期处理效果对应 Shader
主要步骤
- 新建Shader,命名为
高斯模糊 GaussianBlur
,并删除无用代码 - 声明变量:
- 主纹理
_MainTex
- 主纹理
- 利用
CGINCLUDE
和ENDCG
实现两个Pass共享的代码:- 引用内置文件
- 映射属性,特别是纹素,用于UV偏移计算
- 定义结构体,包含顶点和UV数组(用于存储5个像素的UV偏移)
- 实现两个顶点着色器函数:一个用于水平偏移,另一个用于竖直偏移
- 片元着色器实现共同的卷积计算方式
- 设置屏幕后处理效果的标配:
ZTest Always
Cull Off
ZWrite Off
- 实现两个Pass:
- 通过编译指令指定顶点和片元着色器调用的函数
新建Shader,命名为高斯模糊 GaussianBlur,删除无用代码,保留骨架
Shader "Unlit/Lesson64_GaussianBlur"
{
Properties
{
//...
}
SubShader
{
//...
}
}
在SubShader最前面,利用补充知识通过 CGINCLUDE 和 ENDCG 实现了两个 Pass 共享的代码。引用了内置文件,声明主纹理 _MainTex和属性映射,包括纹素的映射
Shader "Unlit/Lesson64_GaussianBlur"
{
Properties
{
// 主纹理
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
//用于包裹共用代码 在之后的多个Pass当中都可以使用的代码
CGINCLUDE
//内置文件
#include "UnityCG.cginc"
// 主纹理
sampler2D _MainTex;
//纹素 x=1/宽 y=1/高
half4 _MainTex_TexelSize;
ENDCG
Tags
{
"RenderType"="Opaque"
}
}
Fallback Off
}
CGINCLUDE
内定义结构体,包含顶点和 UV 数组,用于存储 5 个像素的 UV 偏移
struct v2f
{
//5个像素的uv坐标偏移
half2 uv[5] : TEXCOORD0;
//顶点在裁剪空间下坐标
float4 vertex : SV_POSITION;
};
实现水平和竖直两个顶点函数,先放在CGINCLUDE 内,等一下可以移动到各种的Pass(其实不移动也行)。主要逻辑是把5个像素的偏移放到结构体的uv内
//水平方向的 顶点着色器函数
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);
v2f.uv[2] = uv - half2(_MainTex_TexelSize.x * 1, 0);
v2f.uv[3] = uv + half2(_MainTex_TexelSize.x * 2, 0);
v2f.uv[4] = uv - half2(_MainTex_TexelSize.x * 2, 0);
return v2f;
}
//竖直方向的 顶点着色器函数
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);
v2f.uv[2] = uv - half2(0, _MainTex_TexelSize.x * 1);
v2f.uv[3] = uv + half2(0, _MainTex_TexelSize.x * 2);
v2f.uv[4] = uv - half2(0, _MainTex_TexelSize.x * 2);
return v2f;
}
片元函数写共同的卷积计算方式的逻辑,对位相乘后相加,中间的建立的逻辑其实就是遍历拿到index为1 2 3 4的uv采样并和对应的卷积核相乘
//片元着色器函数
//两个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);
}
添加屏幕后处理效果标配指令,通过深度测试,关闭背面剔除和深度写入
// 设置深度测试模式为始终通过
// 这意味着无论新绘制的片元的深度值与当前深度缓冲区中的值关系如何,该片元都将通过深度测试并有可能被绘制
ZTest Always
// 关闭背面剔除功能
// 通常在渲染场景时,为了提高性能会剔除那些背对摄像机的面,这里关闭此功能后,所有面都会被考虑渲染
Cull Off
// 关闭深度写入功能
// 即不将绘制的片元的深度值写入深度缓冲区,这样可能会导致一些渲染效果上的特殊情况,比如多个物体在深度上可能会出现重叠绘制的现象
ZWrite Off
把水平和竖直的两个顶点函数分开到两个Pass中,其实不移动也可以的
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);
v2f.uv[2] = uv - half2(_MainTex_TexelSize.x * 1, 0);
v2f.uv[3] = uv + half2(_MainTex_TexelSize.x * 2, 0);
v2f.uv[4] = uv - half2(_MainTex_TexelSize.x * 2, 0);
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);
v2f.uv[2] = uv - half2(0, _MainTex_TexelSize.x * 1);
v2f.uv[3] = uv + half2(0, _MainTex_TexelSize.x * 2);
v2f.uv[4] = uv - half2(0, _MainTex_TexelSize.x * 2);
return v2f;
}
ENDCG
}
高斯模糊基础屏幕后期处理效果对应 C#
补充知识:两个Pass处理时的中间缓存区
由于我们需要使用两个Pass对图像进行处理,首先进行水平卷积计算得到一个中间结果,然后使用这个中间结果进行竖直卷积计算,得到最终结果。因此,我们需要通过Graphics.Blit
执行两次Pass代码。
为此,我们需要一个中间纹理缓存区,用于存储中间处理结果。
我们将使用RenderTexture.GetTemporary
方法来获取一个临时的RenderTexture
对象,用于存储中间结果。具体来说,我们使用RenderTexture.GetTemporary
的三参数重载:
RenderTexture.GetTemporary(纹理宽度, 纹理高度, 深度缓冲(一般填0即可));
需要注意的是,使用该方法返回的RenderTexture
对象,必须在使用完成后,通过RenderTexture.ReleaseTemporary
方法来释放该对象缓存。
主要步骤
- 创建C#脚本,命名为
GaussianBlur
。 - 继承屏幕后处理基类
PostEffectBase
。 - 重写
OnRenderImage
函数。 - 在函数中利用
Graphics.Blit
、RenderTexture.GetTemporary
和RenderTexture.ReleaseTemporary
对纹理进行两次Pass处理。
创建高斯模糊GaussianBlur的 C# 脚本,继承屏幕后处理基类 PostEffectBase,重写 OnRenderImage 函数,在重写的该函数中利用 Graphics.Blit、RenderTexture.GetTemporary 以及 RenderTexture.ReleaseTemporary 函数来完成对纹理的两次 Pass 处理
public class Lesson64_GaussianBlur : PostEffectBase
{
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//base.OnRenderImage(source, destination);不需要用Base的 自己重写
//判断这个材质球是否为空 如果不为空 就证明这个shader能用来处理屏幕后处理效果
if (material != null)
{
//因为我们需要用两个Pass 处理图像两次 所以需要准备一个缓存区
RenderTexture buffer = RenderTexture.GetTemporary(source.width, source.height, 0);
//进行第一次 水平卷积计算 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#脚本挂载到摄像机上,激活失活可以看出高斯模糊的效果
64.2 知识点代码
Lesson64_屏幕后期处理效果_效果实现_高斯模糊_基础实现.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson64_屏幕后期处理效果_效果实现_高斯模糊_基础实现 : MonoBehaviour
{
void Start()
{
#region 知识回顾
//利用两个一维高斯滤波核来计算高斯模糊时
//对于5x5的滤波核,它其中的元素,不管是水平还是竖直方向,都是
//(0.0545, 0.2442, 0.4026, 0.2442, 0.0545)
//而其中主要的数值就三个
//0.4026, 0.2442, 0.0545
//我们将利用该高斯核来进行卷积计算
#endregion
#region 知识补充 封装共享代码预处理指令CGINCLUDE
//在实现高斯模糊效果时,
//我们将在Shader中利用两个Pass来分别计算水平卷积和竖直卷积
//而两个Pass会存在相同的代码
//我们将使用一个新的预处理指令
//CGINCLUDE
//....
//中间包裹CG代码
//....
//ENDCG
//它写在SubShader语句块中,Pass外
//它的作用是用于封装共享代码
//可以在其中定义常量、函数、结构体、宏等等内容
//这些封装起来的代码可以在同一个Shader文件中的多个Pass中使用
//也可以在其他Shader文件中引用
//使用它可以避免我们重复编写一些相同的代码
//从而提高代码复用性和可维护性
#endregion
#region 知识点一 实现高斯模糊基础效果的制作思路
//在Shader中写两个Pass
//一个Pass用来计算 水平方向卷积
//一个Pass用来计算 竖直方向卷积
//两个Pass的区别:
// 顶点着色器中计算的uv偏移位置不同,一个水平偏移,一个竖直偏移
//两个Pass的共同点:
// 1.使用的内置文件相同
// 2.使用的属性相同
// 3.片元着色器的计算规则可以相同
// 我们可以用uv数组存储5个像素的UV坐标偏移
// 数组中存储的像素UV偏移分别为
// 元素index 0 1 2 3 4
// x或y相对于原点的偏移 0 1 -1 2 -2
// 其中
// 第0个元素 对应的高斯核元素为 0.4026
// 第1,2个元素 对应的高斯核元素为 0.2442
// 第3,4个元素 对应的高斯核元素为 0.0545
// 那么不管竖直还是水平可以统一一套计算规则进行计算
#endregion
#region 知识点二 实现 高斯模糊基础屏幕后期处理效果 对应 Shader
//1.新建Shader,取名为高斯模糊GaussianBlur,删除无用代码
//2.声明变量
// 主纹理 _MainTex
//3.利用补充知识 CGINCLUDE ... ENDCG
// 实现两个Pass共享的代码
// 3-1.内置文件引用
// 3-2.属性映射,注意映射纹素,需要用于uv偏移计算
// 3-3.结构体
// 顶点和uv数组(用于存储5个像素的uv偏移)
// 3-4.两个顶点着色器函数
// 一个水平偏移采样
// 一个竖直偏移采样
// 3-5.片元着色器
// 共同的卷积计算方式,对位相乘后相加
//4.屏幕后处理效果标配
// ZTest Always
// Cull Off
// ZWrite Off
//5.实现两个Pass
// 主要用编译指令指明顶点和片元着色器调用的函数即可
#endregion
#region 知识点三 实现 高斯模糊基础屏幕后期处理效果 对应 C#
//补充知识
//由于我们需要用两个Pass对图像进行处理
//相当于先让捕获的图像进行水平卷积计算得到一个结果
//再用这个结果进行竖直卷积计算得到最终结果
//因此我们需要利用Graphics.Blit进行两次Pass代码的执行
//所以我们需要一个中间纹理缓存区,用于记录中间的处理结果
//我们需要用到
//RenderTexture.GetTemporary方法
//它的作用是获取一个临时的RenderTexture对象,我们可以利用它来存储中间结果
//我们使用它传入3个参数的重载
//RenderTexture.GetTemporary(纹理宽、纹理高、深度缓冲-一般填0即可)
//需要注意的是
//使用该方法返回的 RenderTexture对象
//需要配合使用 RenderTexture.ReleaseTemporary(对象) 方法来释放该对象缓存
//1.创建C#脚本,名为高斯模糊GaussianBlur
//2.继承屏幕后处理基类PostEffectBase
//3.重写OnRenderImage函数
//4.在其中利用Graphics.Blit、RenderTexture.GetTemporary、 RenderTexture.ReleaseTemporary
// 函数对纹理进行两次Pass处理
#endregion
}
}
Lesson64_GaussianBlur.shader
Shader "Unlit/Lesson64_GaussianBlur"
{
Properties
{
// 主纹理
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
//用于包裹共用代码 在之后的多个Pass当中都可以使用的代码
CGINCLUDE
//内置文件
#include "UnityCG.cginc"
// 主纹理
sampler2D _MainTex;
//纹素 x=1/宽 y=1/高
half4 _MainTex_TexelSize;
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);
v2f.uv[2] = uv - half2(_MainTex_TexelSize.x * 1, 0);
v2f.uv[3] = uv + half2(_MainTex_TexelSize.x * 2, 0);
v2f.uv[4] = uv - half2(_MainTex_TexelSize.x * 2, 0);
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);
v2f.uv[2] = uv - half2(0, _MainTex_TexelSize.x * 1);
v2f.uv[3] = uv + half2(0, _MainTex_TexelSize.x * 2);
v2f.uv[4] = uv - half2(0, _MainTex_TexelSize.x * 2);
return v2f;
}
ENDCG
}
}
Fallback Off
}
Lesson64_GaussianBlur.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson64_GaussianBlur : PostEffectBase
{
protected override void OnRenderImage(RenderTexture source, RenderTexture destination)
{
//base.OnRenderImage(source, destination);不需要用Base的 自己重写
//判断这个材质球是否为空 如果不为空 就证明这个shader能用来处理屏幕后处理效果
if (material != null)
{
//因为我们需要用两个Pass 处理图像两次 所以需要准备一个缓存区
RenderTexture buffer = RenderTexture.GetTemporary(source.width, source.height, 0);
//进行第一次 水平卷积计算 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);
}
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com