Unity Shader屏幕后处理4.0:基于高斯模糊的Bloom
Posted 九九345
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity Shader屏幕后处理4.0:基于高斯模糊的Bloom相关的知识,希望对你有一定的参考价值。
原本打算写高斯模糊和双重模糊两个实现Bloom方法的对比,但两个加在一起篇幅过长,于是拆成两篇文章来进行。
学习前建议应先搞清楚的几个概念
- HDR
- LDR
- ToneMapping
- 几种模糊算法
1 高斯模糊实现Bloom
最近一直在学习Unity Shader实现各种后处理效果,Bloom效果就是其中之一,它也是游戏中最常见的效果之一,也是必不可少的效果之一吧!百人计划就有专门介绍实现Bloom的过程,跟《Unity Shader 入门精要》12.5章介绍的Bloom效果实现方法是一样的:
- 根据一个阈值提取屏幕图像中较亮的区域,储存在一张RT中
- 利用高斯模糊进行模糊处理
- 将模糊后的结果与最初的屏幕图像混合
经过以上三个大步骤就得到了最终的Bloom效果,步骤的Shader一共包含了4个Pass(中间2个Pass是高斯模糊分成的水平和竖直方向)。先不谈这个方法跟其他方法比较有什么优劣,我们先过一遍实现过程。
1.1 C#脚本
跟之前写过的边缘检测/高斯模糊后处理一样,实现Bloom也需要C#脚本和Unity Shader一起完成。
完整脚本在这:
//jiujiu345
//2022.11.14
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using UnityEngine;
[ExecuteInEditMode]
public class Bloom : MonoBehaviour
public Shader bloomShader;
public Material bloomMaterial;
[Header("模糊迭代次数")]
[Range(0, 4)]
public int interations = 2;
[Header("模糊范围")]
[Range(0.3f, 3.0f)]
public float blurSpread = 0.3f;
[Header("降采样次数")]
[Range(1, 8)]
public int downSample = 1;
//控制提取较亮区域时使用的阈值大小
//开启HDR后,亮度值会超过1,所以范围在1~4
[Header("阈值")]
[Range(0.0f, 4.0f)]
public float luminanceThreshold = 0.6f;
public void OnRenderImage (RenderTexture source, RenderTexture destination)
if(bloomMaterial != null)
bloomMaterial.SetFloat("_LuminanceThreshold", luminanceThreshold);
int rtW = source.width / downSample;
int rtH = source.height / downSample;
//定义rt
RenderTexture rt0 = RenderTexture.GetTemporary(rtW, rtH, 0);
rt0.filterMode = FilterMode.Bilinear;
//第一个Pass提取图片较亮的部分
Graphics.Blit(source, rt0, bloomMaterial, 0);
for(int i = 0; i < interations; i++)
bloomMaterial.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture rt1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//第二个Pass
Graphics.Blit(rt0, rt1, bloomMaterial, 1);
RenderTexture.ReleaseTemporary(rt0);
rt0 = rt1;
rt1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//第三个Pass
Graphics.Blit(rt0, rt1, bloomMaterial, 2);
RenderTexture.ReleaseTemporary(rt0);
rt0 = rt1;
//rt0储存模糊后的图片
bloomMaterial.SetTexture("_Bloom", rt0);
//第四个Pass-混合
Graphics.Blit(source, destination, bloomMaterial, 3);
RenderTexture.ReleaseTemporary(rt0);
else
Debug.Log("Please input your Material");
Graphics.Blit(source, destination);
可控参数
脚本中提供了四个用户可控参数:
- 模糊迭代次数——模糊效果不断叠加
- 模糊范围——每次高斯模糊的模糊范围
- 降采样次数
- 阈值——亮度超过该阈值的区域才能被提取
传递给Material的参数
通过.SetFloat()进行了基本参数传递,分别传递了:
- luminanceThreshold——阈值
- blurspread——模糊范围,_BlurSize=1+i*blurSpread,迭代次数越多范围越大
通过.SetTexture()传递了储存模糊结果的渲染纹理_Bloom
四个Pass
四个Pass都是通过调用Graphics.Blit()实现的,高斯模糊的第二和第三个Pass由于要实现模糊迭代,引入了一个for循环进行模糊迭代,两个渲染纹理rt0和rt1交替储存结果:
for(int i = 0; i < interations; i++)
bloomMaterial.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture rt1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//第二个Pass
Graphics.Blit(rt0, rt1, bloomMaterial, 1);
RenderTexture.ReleaseTemporary(rt0);
rt0 = rt1;
rt1 = RenderTexture.GetTemporary(rtW, rtH, 0);
//第三个Pass
Graphics.Blit(rt0, rt1, bloomMaterial, 2);
RenderTexture.ReleaseTemporary(rt0);
rt0 = rt1;
1.2 Shader代码
即四个Pass的具体顶点和片元着色器,完整代码在这:
//jiujiu345
//2022.11.14
Shader "Unity Shaders Book/Chapter 12/Bloom_GaussianBlur"
Properties
_MainTex ("Base(RGB)", 2D) = "white" //src
_Bloom("Bloom(RGB)", 2D) = "black" //高斯模糊后的较亮区域
//无须定义在Shader面板,采取C#脚本控制
//_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;
//Pass0-提取较亮区域
struct v2f_Extract
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
;
v2f_Extract vertExtractBright(appdata_img v)
v2f_Extract 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;
//用luminanceThreshold控制亮度强度
fixed4 fragExtractBright(v2f_Extract i) : SV_Target
fixed4 c = tex2D(_MainTex, i.uv);
fixed val = saturate(Luminance(c) - _LuminanceThreshold);
return val * c;
//Pass2&3-高斯模糊
struct v2f_Gaussian
float4 pos : SV_POSITION;
half2 uv[5] : TEXCOORD0;
;
//水平
v2f_Gaussian vertBlurVertical(appdata_img v)
v2f_Gaussian o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(0.0f, _MainTex_TexelSize.y * 1.0 * _BlurSize);
o.uv[2] = uv - float2(0.0f, _MainTex_TexelSize.y * 1.0 * _BlurSize);
o.uv[3] = uv + float2(0.0f, _MainTex_TexelSize.y * 2.0 * _BlurSize);
o.uv[4] = uv - float2(0.0f, _MainTex_TexelSize.y * 2.0 * _BlurSize);
return o;
//竖直
v2f_Gaussian vertBlurHorizontal(appdata_img v)
v2f_Gaussian o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0 * _BlurSize, 0.0f);
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0 * _BlurSize, 0.0f);
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0 * _BlurSize, 0.0f);
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0 * _BlurSize, 0.0f);
return o;
fixed4 GaussianBlur(v2f_Gaussian i) : SV_Target
float weight[3] = 0.4026, 0.2442, 0.0545;
fixed3 color = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
for(int j=1;j<3;j++)
color += tex2D(_MainTex, i.uv[2*j-1]).rgb * weight[j];
color += tex2D(_MainTex, i.uv[2*j]).rgb * weight[j];
return fixed4(color, 1.0);
//Pass3-混合亮度和原图
struct v2f_Bloom
float4 pos : SV_POSITION;
half4 uv : TEXCOORD0;
;
v2f_Bloom vertBloom(appdata_img v)
v2f_Bloom o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord;
o.uv.zw = v.texcoord;
//用以判断是否在Direct3D平台
#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(v2f_Bloom 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
Pass
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment GaussianBlur
ENDCG
Pass
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment GaussianBlur
ENDCG
Pass
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
FallBack Off
关于Properties语义块声明的属性
发现在Properties语义只声明了两个用到的Texture属性:
- 一个是source纹理会被传递给Shader中的_MainTex
- 一个是C#脚本中定义的储存模糊后图像的渲染纹理_Bloom
其实这里_Bloom也是可以省略的,但是加上的话就可以在材质面板上实时看到模糊后的图像了。接下来可以直接在CGINCLUDE--ENDCG定义的代码段中定义Shader需要的变量,很大一部分直接来自C#脚本传入的变量。
还有一点废话:如果一个变量在Properties中被声明,又在C#脚本中被定义,可能会出现脚本界面改动变量无效的情况,为了避免这个情况我们只其一就行(我一般选择脚本界面改动)。
如何提取亮度区域?
根据RGB三通道的值转亮度值,具体为什么这么做可以参考这篇文章的亮度部分:
//计算像素的亮度
fixed Luminance(fixed4 color)
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
接着加入C#脚本传递过来的阈值参数,用一个saturate(Luminance(c) - _LuminanceThreshold)提取阈值以上的亮度。
关于高斯模糊那块儿,上一篇博客已经涉及到了,这里直接搬过来代码就行!
1.3 Bloom效果展示
参数设置如下:
后处理前后对比如下:
1.4 FrameDebug看看过程
又要打开我们的老朋友Frame Debugger了:
可以看到整个后处理经历了6个步骤,如下动图:
经历的Pass顺序是:Pass0 -> Pass1 -> Pass2 -> Pass1 -> Pass2 -> Pass3,经历两边1&2是因为迭代次数选择了2.
2 进一步实现自发光Bloom
Unity其实是自带Bloom效果的,我们直接上对比就能感受到区别了。
2.1 准备场景
场景中拖入随便一个模型,关闭场景中的光源(为了更好的观察效果),然后给模型拖入Unity的Standard材质,打开Emission,设置如下:
未进行任何后处理的初始效果如下:
接下来进行对比操作。
2.2 方法1:高斯模糊
基于当前的代码实现自发光Bloom,效果其实是比较差的。。.下图是我尝试调整到了最能体现自发光的效果:
2.3 方法2:Unity自带的Bloom
顺便提一嘴:Unity自带的Bloom应该是采用降采样+升采样——双重模糊的模糊算法(我不是特别确定),关于到底如何降/升采样我们后面就会讲到。
我们再用Unity自带的Post Processing里的Bloom效果 (添加方式可参考(50) EPIC GLOW IN UNITY 2020.2 - YouTube),看看出来的效果是什么样的:
由于参数设置等一些原因没办法从性能上进行一个对比,但单从两种方法“能调整到的最好的实现效果”上,后者完胜。
3 谈谈高斯模糊实现Bloom的优缺点
推荐结合这一篇文章来看第3小节。
3.1 优点
不知道算不算“优点”:这个方法对于实现简单的大片的泛光,加强本来亮点就很明显的场景泛光效果还是蛮不错的,除了上述天空的例子,再举两个例子:
(原图来自ArtStation - Jinji's Grotto, Connor Sheehan)
(原图来自ArtStation - Vermillion Forest, Anton Fadeev)
3.2 缺点1:性能上
因为模糊算法用的是高斯模糊,高斯模糊本质还是卷积核,如果我们想要大范围的Bloom效果,就只能靠增大滤波范围or增加滤波次数来实现,基于上述代码的话采取操作分别是:
- 增加滤波次数——Interation↑
- 增大滤波范围——BlurSpread↑
首先增大滤波次数相当于多来几次Pass1&2,一下子性能消耗就上去了,其次如果真的实践你会发现,无论是增加滤波次数还是增大滤波范围,达到的扩大效果也不是很能让人满意。
3.3 缺点2:实现效果上
除了上述的自发光实现效果,我在调整的过程中还感受到:高斯模糊实现的自发光Bloom总是有一种明显的边界感。这是为什么?
我猜应该是因为高斯模糊(感觉只要是基于卷积核的模糊都是一样的)总是依据卷积核每一格的权重进行加权平均计算出中间项的值,所以即使降采样了,每一level之间还是会存在明显的亮度突变,于是源图和模糊后的图的亮度无论调整哪个参数都不会过渡均匀。
下一篇将会介绍双重模糊实现Bloom的方法。(断更了很久了,最近简直是从石头缝里挤时间来额外学习TA的内容,太难了太难了)
Unity shader学习之屏幕后期处理效果之高斯模糊
高斯模糊,见 百度百科。
也使用卷积来实现,每个卷积元素的公式为:
其中б是标准方差,一般取值为1。
x和y分别对应当前位置到卷积中心的整数距离。
由于需要对高斯核中的权重进行归一化,即使所有权重相加为1,因此e前面的系数实际不会对结果产生任何影响。
转载请注明出处:http://www.cnblogs.com/jietian331/p/7238032.html
综上,公式简化为:
G(x,y) = e-(x*x+y*y)/2
因此,高斯核计算代码如下:
1 using System; 2 3 namespace TestShell 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 Console.WriteLine("输入需要得到的高斯卷积核的维数(如3,5,7...):"); 10 11 string input = Console.ReadLine(); 12 int size; 13 14 if (!int.TryParse(input, out size)) 15 { 16 Console.WriteLine("不是数字..."); 17 return; 18 } 19 20 // 计算 21 double[] r2 = null; 22 double[,] r = null; 23 try 24 { 25 r = CalcGaussianBlur(size, out r2); 26 } 27 catch (Exception ex) 28 { 29 Console.WriteLine("错误: " + ex.Message); 30 } 31 32 if (r != null && r2 != null) 33 { 34 // 卷积如下: 35 Console.WriteLine(); 36 Console.WriteLine("{0}x{0}的高斯卷积核如下:", size); 37 for (int i = 0; i < r.GetLongLength(0); i++) 38 { 39 for (int j = 0; j < r.GetLongLength(1); j++) 40 { 41 Console.Write("{0:f4}\\t", r[i, j]); 42 } 43 Console.WriteLine(); 44 } 45 Console.WriteLine(); 46 47 Console.WriteLine("可拆成2个一维的数组:"); 48 for (int i = 0; i < r2.Length; i++) 49 { 50 Console.Write("{0:f4}\\t", r2[i]); 51 } 52 Console.WriteLine(); 53 Console.WriteLine(); 54 55 Console.WriteLine("验证,使用这2个一维的数组也可以得到同样的结果:"); 56 for (int i = 0; i < size; i++) 57 { 58 for (int j = 0; j < size; j++) 59 { 60 Console.Write("{0:f4}\\t", r2[i] * r2[j]); 61 62 } 63 Console.WriteLine(); 64 } 65 } 66 67 Console.WriteLine(); 68 Console.WriteLine("按任意键结束..."); 69 Console.ReadKey(); 70 } 71 72 static double[,] CalcGaussianBlur(int size, out double[] r2) 73 { 74 if (size < 3) 75 throw new ArgumentException("size < 3"); 76 if (size % 2 != 1) 77 throw new ArgumentException("size % 2 != 1"); 78 79 double[,] r = new double[size, size]; 80 r2 = new double[size]; 81 int center = (int)Math.Floor(size / 2f); 82 double sum = 0; 83 84 for (int i = 0; i < size; i++) 85 { 86 for (int j = 0; j < size; j++) 87 { 88 int x = Math.Abs(i - center); 89 int y = Math.Abs(j - center); 90 double d = CalcItem(x, y); 91 r[i, j] = d; 92 sum += d; 93 } 94 } 95 96 for (int i = 0; i < size; i++) 97 { 98 for (int j = 0; j < size; j++) 99 { 100 r[i, j] /= sum; 101 if (i == j) 102 r2[i] = Math.Sqrt(r[i, i]); 103 } 104 } 105 106 return r; 107 } 108 109 static double CalcItem(int x, int y) 110 { 111 return Math.Pow(Math.E, -(x * x + y * y) / 2d); 112 } 113 } 114 }
工具在: http://files.cnblogs.com/files/jietian331/CalcGaussianBlur.zip
一个5 x 5的高斯核如下:
使用2个一维数组可简化计算量,提高性能,通过观察可知,只需要计3个数:
使用unity shader屏幕后期处理来实现高斯模糊,代码如下。
子类:
1 using UnityEngine; 2 3 public class GaussianBlurRenderer : PostEffectRenderer 4 { 5 [Range(1, 8)] 6 [SerializeField] 7 public int m_downSample = 2; // 降采样率 8 [Range(0, 4)] 9 [SerializeField] 10 public int m_iterations = 3; // 迭代次数 11 [Range(0.2f, 3f)] 12 [SerializeField] 13 public float m_blurSpread = 0.6f; // 模糊扩散量 14 15 protected override void OnRenderImage(RenderTexture src, RenderTexture dest) 16 { 17 int w = (int)(src.width / m_downSample); 18 int h = (int)(src.height / m_downSample); 19 RenderTexture buffer0 = RenderTexture.GetTemporary(w, h); 20 RenderTexture buffer1 = RenderTexture.GetTemporary(w, h); 21 buffer0.filterMode = FilterMode.Bilinear; 22 buffer1.filterMode = FilterMode.Bilinear; 23 Graphics.Blit(src, buffer0); 24 25 for (int i = 0; i < m_iterations; i++) 26 { 27 Mat.SetFloat("_BlurSpread", 1 + i * m_blurSpread); 28 29 Graphics.Blit(buffer0, buffer1, Mat, 0); 30 Graphics.Blit(buffer1, buffer0, Mat, 1); 31 } 32 33 Graphics.Blit(buffer0, dest); 34 RenderTexture.ReleaseTemporary(buffer0); 35 RenderTexture.ReleaseTemporary(buffer1); 36 } 37 38 protected override string ShaderName 39 { 40 get { return "Custom/Gaussian Blur"; } 41 } 42 }
shader:
1 // Upgrade NOTE: replaced \'mul(UNITY_MATRIX_MVP,*)\' with \'UnityObjectToClipPos(*)\' 2 3 Shader "Custom/Gaussian Blur" 4 { 5 Properties 6 { 7 _MainTex("Main Texture", 2D) = "white" {} 8 _BlurSpread("Blur Spread", float) = 1 9 } 10 11 SubShader 12 { 13 CGINCLUDE 14 15 sampler2D _MainTex; 16 float4 _MainTex_TexelSize; 17 uniform float _BlurSpread; 18 19 struct appdata 20 { 21 float4 vertex : POSITION; 22 float2 uv : TEXCOORD0; 23 }; 24 25 struct v2f 26 { 27 float4 pos : SV_POSITION; 28 float2 uv[5] : TEXCOORD0; 29 }; 30 31 v2f vertHorizontal(appdata v) 32 { 33 v2f o; 34 o.pos = UnityObjectToClipPos(v.vertex); 35 float tsx = _MainTex_TexelSize.x * _BlurSpread; 36 o.uv[0] = v.uv + float2(tsx * -2, 0); 37 o.uv[1] = v.uv + float2(tsx * -1, 0); 38 o.uv[2] = v.uv; 39 o.uv[3] = v.uv + float2(tsx * 1, 0); 40 o.uv[4] = v.uv + float2(tsx * 2, 0); 41 return o; 42 } 43 44 v2f vertVertical(appdata v) 45 { 46 v2f o; 47 o.pos = UnityObjectToClipPos(v.vertex); 48 float tsy = _MainTex_TexelSize.y * _BlurSpread; 49 o.uv[0] = v.uv + float2(0, tsy * -2); 50 o.uv[1] = v.uv + float2(0, tsy * -1); 51 o.uv[2] = v.uv; 52 o.uv[3] = v.uv + float2(0, tsy * 1); 53 o.uv[4] = v.uv + float2(0, tsy * 2); 54 return o; 55 } 56 57 fixed4 frag(v2f i) : SV_TARGET 58 { 59 float g[3] = {0.0545, 0.2442, 0.4026}; 60 fixed4 col = tex2D(_MainTex, i.uv[2]) * g[2]; 61 for(int k = 0; k < 2; k++) 62 { 63 col += tex2D(_MainTex, i.uv[k]) * g[k]; 64 col += tex2D(_MainTex, i.uv[4 - k]) * g[k]; 65 } 66 return col; 67 } 68 69 ENDCG 70 71 Pass 72 { 73 Name "HORIZONTAL" 74 ZTest Always 75 ZWrite Off 76 Cull Off 77 78 CGPROGRAM 79 #pragma vertex vertHorizontal 80 #pragma fragment frag 81 ENDCG 82 } 83 84 Pass 85 { 86 Name "VERTICAL" 87 ZTest Always 88 ZWrite Off 89 Cull Off 90 91 CGPROGRAM 92 #pragma vertex vertVertical 93 #pragma fragment frag 94 ENDCG 95 } 96 } 97 98 Fallback Off 99 }
调整参数:
DownSample,即降采样率,越大性能越好,图像越模糊,但过大可能会使图像像素化。
Iteraitions, 即迭代次数,越大图像模糊效果越好,但性能也会下降。
BlurSpread,即模糊扩散量,越大图像越模糊,但过大会造成虚影。
效果如下:
以上是关于Unity Shader屏幕后处理4.0:基于高斯模糊的Bloom的主要内容,如果未能解决你的问题,请参考以下文章
Unity Shader 屏幕后处理5.0:讨论双重模糊的Bloom