目录
GaussianBlurFeature:负责参数配置和创建渲染 Pass
GaussianBlurPass:负责纹理管理、模糊绘制与资源释放
一、什么是高斯模糊?
定义:通过卷积操作对图像进行平滑处理,减少噪声或细节。
这个效果的使用高频且广泛。它既可以直接柔化画面,做毛玻璃、景深虚化、Bloom 泛光、发光描边,也可以作为复合效果的中间步骤,通过与深度、遮罩及颜色混合配合,得到不错的视觉效果。
对于这个效果的原理,我们需要了解:
高斯模糊的核心是高斯核,里面的每个元素是利用一个二维高斯函数做计算,再与图片中每个像素做卷积运算。
对于这个二维高斯函数:在模糊计算中,通常把当前待处理像素作为原点,即一个二维像素位置 (x,y)。此时 x,y 表示当前像素到卷积中心的整数距离,公式简化为:
此方程表示:邻域像素离当前像素越近,对当前像素影响越大;高斯核维度越大,模糊程度越大
使用NxN的高斯核对图像进行卷积滤波,则要进行NxNxWxH次纹理采样。故在此将卷积核拆分为水平、垂直的两个一维高斯核,则仅需2xNxWxH次纹理采样。
此处采用5 x 5的卷积核。左右权重对称,只需记录3个权重值。
在unity中,采用全屏后处理的方式,通过renderer feature和render pass将其叠加到屏幕上。
加前:

加后:

1. 增长量、降采样不变,增大迭代次数

2. 增长量、迭代次数不变,增加降采样
以下为详细步骤:
二、Feature脚本
基类脚本封装好材质检查、Pass入队、RT管理和全屏绘制。基类脚本见:[Unity] URP Renderer Feature之基类封装-CSDN博客
GaussianBlurFeature:负责参数配置和创建渲染 Pass
1. 定义模糊参数
// 模糊迭代次数
[Range(0, 4)]
public int iterations = 3;
// 每轮采样间距的增长量
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f;
// 每轮采样间距的增长量
[Range(1, 8)]
public int downSample = 2;
2. 创建Pass,并传入材质、执行时机、模糊参数
protected override ScriptableRenderPass CreatePass()
{
return new GaussianBlurPass(material, renderPassEvent, iterations, blurSpread, downSample);
}
GaussianBlurPass:负责纹理管理、模糊绘制与资源释放
1. 声明RTHandle、模糊参数
2. 构造函数接收feature传入基类
3. ProfilerTag指定名字,方便在frame debugger中调试
protected override string ProfilerTag => "GaussianBlur";
4. OnCameraSetup:配置渲染目标(关闭深度采样、多重采样)、设置降采样尺寸、分配RTHandle、设置采样方式、名称
RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor;
descriptor.depthBufferBits = 0;
descriptor.msaaSamples = 1;
descriptor.width = Mathf.Max(1, descriptor.width / downSample);
descriptor.height = Mathf.Max(1, descriptor.height / downSample);
RenderingUtils.ReAllocateIfNeeded(ref buffer0, descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: $"_{ProfilerTag}_Buffer0");
RenderingUtils.ReAllocateIfNeeded(ref buffer1, descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: $"_{ProfilerTag}_Buffer1");
downSample:
- 含义:降采样。使用小于原屏幕分辨率尺寸的临时RT,滤波模式双线性。
- 优点:减少模糊处理的像素,提高性能
- 过高降采样会导致像素化。(也许这正是你期待的)
5. Execute:高斯模糊生产线。两个临时RT在迭代中交替使用,先纵向、后横向,最后将结果写回原始RT
CommandBuffer cmd = CommandBufferPool.Get(ProfilerTag);
try
{
RTHandle source = renderingData.cameraData.renderer.cameraColorTargetHandle;
// 对应 _MainTex_TexelSize:内置管线 blit 时自动填,URP 需要手动传(xy=1/w 1/h,zw=w h)
material.SetVector("_MainTex_TexelSize", new Vector4(1.0f / buffer0.rt.width, 1.0f / buffer0.rt.height, buffer0.rt.width, buffer0.rt.height));
// Downsample
Blitter.BlitCameraTexture(cmd, source, buffer0);
for (int i = 0; i < iterations; i++)
{
// 设置本轮采样间距
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
// 绘制垂直 pass:buffer0 -> buffer1
Blit(cmd, buffer0, buffer1, material, 0);
// 绘制水平 pass:buffer1 -> buffer0
Blit(cmd, buffer1, buffer0, material, 1);
}
Blitter.BlitCameraTexture(cmd, buffer0, source); // 最终结果写回原始RT
context.ExecuteCommandBuffer(cmd); // 提交给GPU执行
}
finally
{
CommandBufferPool.Release(cmd);
}
两个 RTHandle 始终存在,只是在它们之间来回写,这叫 Ping-Pong Buffer(乒乓缓冲)。
那为什么要来回传递,而不是直接写回原图?
因为读取当前的像素,处理后传回原图位置,不安全; GPU 像素并不是按照从上到下、从左到右的固定顺序执行的,它们通常是大批并行执行。无法得知他周围的像素是否被处理过,是原像素还是新像素;
6. Dispose:释放资源、临时RT
三、Shader
1. 定义属性(与脚本中传参的属性名保持一致)
2. 定义Varyings结构体:uv[5]保存5个采样坐标(与高斯核一致)
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv[5] : TEXCOORD0;
};
3. 实现横纵坐标采样
// 竖直方向大小为5的一维纹理坐标采样
Varyings vertBlurVertical(uint vertexID : SV_VertexID)
{
Varyings output;
output.positionCS = GetFullScreenTriangleVertexPosition(vertexID);
half2 uv = GetFullScreenTriangleTexCoord(vertexID) * _BlitScaleBias.xy + _BlitScaleBias.zw;
output.uv[0] = uv;
output.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
output.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
output.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
output.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
return output;
}
- _ BlurSize表示模糊范围,即1.0f + i * blurSpread;步进距离越大,模糊时采样的点越远,模糊效果越明显
4. 片元着色器:定义高斯核权重、采样像素颜色、卷积、输出加权颜色
5. 横、纵分两个pass执行
四、完整代码
1. 脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class GaussianBlurFeature : PostEffectFeatureBase
{
// Blur iterations - larger number means more blur.
[Range(0, 4)]
public int iterations = 3;
// Blur spread for each iteration - larger value means more blur
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f;
[Range(1, 8)]
public int downSample = 2;
protected override ScriptableRenderPass CreatePass()
{
return new GaussianBlurPass(material, renderPassEvent, iterations, blurSpread, downSample);
}
}
public class GaussianBlurPass : PostEffectPassBase
{
private RTHandle buffer0; // 当前渲染结果所在
private RTHandle buffer1; // 临时
private readonly int iterations;
private readonly float blurSpread;
private readonly int downSample;
// 接收feature传过来的参数
public GaussianBlurPass(Material material, RenderPassEvent renderPassEvent, int iterations, float blurSpread, int downSample) : base(material, renderPassEvent)
{
this.iterations = iterations;
this.blurSpread = blurSpread;
this.downSample = downSample;
}
protected override string ProfilerTag => "GaussianBlur";
// 对两块buffer降采样尺寸(减少处理像素个数、提高性能)、双线性过滤、无深度
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor;
descriptor.depthBufferBits = 0;
descriptor.msaaSamples = 1;
descriptor.width = Mathf.Max(1, descriptor.width / downSample);
descriptor.height = Mathf.Max(1, descriptor.height / downSample);
RenderingUtils.ReAllocateIfNeeded(ref buffer0, descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: $"_{ProfilerTag}_Buffer0");
RenderingUtils.ReAllocateIfNeeded(ref buffer1, descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: $"_{ProfilerTag}_Buffer1");
}
// 两个临时Buffer在迭代中交替使用,最终结果写回buffer0
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
if (material == null || buffer0 == null || buffer1 == null) return;
CommandBuffer cmd = CommandBufferPool.Get(ProfilerTag);
try
{
RTHandle source = renderingData.cameraData.renderer.cameraColorTargetHandle;
// 对应 _MainTex_TexelSize:内置管线 blit 时自动填,URP 需要手动传(xy=1/w 1/h,zw=w h)
material.SetVector("_MainTex_TexelSize", new Vector4(1.0f / buffer0.rt.width, 1.0f / buffer0.rt.height, buffer0.rt.width, buffer0.rt.height));
// Downsample
Blitter.BlitCameraTexture(cmd, source, buffer0);
for (int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
// 绘制垂直 pass:buffer0 -> buffer1
Blit(cmd, buffer0, buffer1, material, 0);
// 绘制水平 pass:buffer1 -> buffer0
Blit(cmd, buffer1, buffer0, material, 1);
}
Blitter.BlitCameraTexture(cmd, buffer0, source); // 最终结果写回原始RT
context.ExecuteCommandBuffer(cmd); // 提交给GPU执行
}
finally
{
CommandBufferPool.Release(cmd);
}
}
public override void Dispose()
{
base.Dispose();
buffer0?.Release(); // 非空时释放buffer0的RTHandle
buffer0 = null;
buffer1?.Release();
buffer1 = null;
}
}
2. shader
Shader "Custom/S_GaussianBlur"
{
Properties {
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader
{
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float _BlurSize;
CBUFFER_END
TEXTURE2D_X(_BlitTexture);
SAMPLER(sampler_LinearClamp);
float4 _BlitScaleBias; // Blitter 用于处理源/目标 RT 的 UV 翻转,顶点必须应用,否则画面可能上下颠倒
sampler2D _MainTex;
half4 _MainTex_TexelSize;
// 当前采样和4个邻域纹理坐标
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv[5] : TEXCOORD0;
};
// 竖直方向大小为5的一维纹理坐标采样
Varyings vertBlurVertical(uint vertexID : SV_VertexID)
{
Varyings output;
output.positionCS = GetFullScreenTriangleVertexPosition(vertexID);
half2 uv = GetFullScreenTriangleTexCoord(vertexID) * _BlitScaleBias.xy + _BlitScaleBias.zw;
output.uv[0] = uv;
output.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
output.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
output.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
output.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
return output;
}
// 水平方向大小为5的一维纹理坐标采样
Varyings vertBlurHorizontal(uint vertexID : SV_VertexID)
{
Varyings output;
output.positionCS = GetFullScreenTriangleVertexPosition(vertexID);
half2 uv = GetFullScreenTriangleTexCoord(vertexID) * _BlitScaleBias.xy + _BlitScaleBias.zw;
output.uv[0] = uv;
output.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
output.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
output.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
output.uv[4] = uv - float2( _MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
return output;
}
// 片元着色器
half4 fragBlur (Varyings input) : SV_Target
{
float weight[3] = {0.4026, 0.2442, 0.0545}; // 各个领域像素对应权重
half3 sum = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, input.uv[0]).rgb * weight[0];
for (int it = 1; it < 3; ++it)
{
sum += SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, input.uv[it * 2 - 1]).rgb * weight[it];
sum += SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, input.uv[it * 2]).rgb * weight[it];
}
return half4(sum, 1.0);
}
ENDHLSL
// 单Pass无法指定两个顶点着色器,拆成两个Pass
// Pass 0:垂直
Pass
{
Name "GAUSSIAN_BLUR_VERTICAL"
ZTest Always Cull Off ZWrite Off
HLSLPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDHLSL
}
// Pass 1:水平
Pass
{
Name "GAUSSIAN_BLUR_HORIZONTAL"
ZTest Always Cull Off ZWrite Off
HLSLPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDHLSL
}
}
FallBack "Hidden/Shader Graph/FallbackError"
}
参考资料
- 冯乐乐.《Unity Shader入门精要》.人民邮电出版社,2016.第12章,12.4 高斯模糊

809

被折叠的 条评论
为什么被折叠?



