原本打算写高斯模糊和双重模糊两个实现Bloom方法的对比,但两个加在一起篇幅过长,于是拆成两篇文章来进行。
学习前建议应先搞清楚的几个概念
最近一直在学习Unity Shader实现各种后处理效果,Bloom效果就是其中之一,它也是游戏中最常见的效果之一,也是必不可少的效果之一吧!百人计划就有专门介绍实现Bloom的过程,跟《Unity Shader 入门精要》12.5章介绍的Bloom效果实现方法是一样的:
经过以上三个大步骤就得到了最终的Bloom效果,步骤的Shader一共包含了4个Pass(中间2个Pass是高斯模糊分成的水平和竖直方向)。先不谈这个方法跟其他方法比较有什么优劣,我们先过一遍实现过程。
跟之前写过的边缘检测/高斯模糊后处理一样,实现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);
- }
- }
- }
脚本中提供了四个用户可控参数:
通过.SetFloat()进行了基本参数传递,分别传递了:
通过.SetTexture()传递了储存模糊结果的渲染纹理_Bloom
四个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;
- }
即四个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语义只声明了两个用到的Texture属性:
其实这里_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)提取阈值以上的亮度。
关于高斯模糊那块儿,上一篇博客已经涉及到了,这里直接搬过来代码就行!
参数设置如下:
后处理前后对比如下:
又要打开我们的老朋友Frame Debugger了:
可以看到整个后处理经历了6个步骤,如下动图:
经历的Pass顺序是:Pass0 -> Pass1 -> Pass2 -> Pass1 -> Pass2 -> Pass3,经历两边1&2是因为迭代次数选择了2.
Unity其实是自带Bloom效果的,我们直接上对比就能感受到区别了。
场景中拖入随便一个模型,关闭场景中的光源(为了更好的观察效果),然后给模型拖入Unity的Standard材质,打开Emission,设置如下:
未进行任何后处理的初始效果如下:
接下来进行对比操作。
基于当前的代码实现自发光Bloom,效果其实是比较差的。。.下图是我尝试调整到了最能体现自发光的效果:
顺便提一嘴:Unity自带的Bloom应该是采用降采样+升采样——双重模糊的模糊算法(我不是特别确定),关于到底如何降/升采样我们后面就会讲到。
我们再用Unity自带的Post Processing里的Bloom效果 (添加方式可参考(50) EPIC GLOW IN UNITY 2020.2 - YouTube),看看出来的效果是什么样的:
由于参数设置等一些原因没办法从性能上进行一个对比,但单从两种方法“能调整到的最好的实现效果”上,后者完胜。
推荐结合这一篇文章来看第3小节。
不知道算不算“优点”:这个方法对于实现简单的大片的泛光,加强本来亮点就很明显的场景泛光效果还是蛮不错的,除了上述天空的例子,再举两个例子:
(原图来自ArtStation - Jinji's Grotto, Connor Sheehan)
(原图来自ArtStation - Vermillion Forest, Anton Fadeev)
因为模糊算法用的是高斯模糊,高斯模糊本质还是卷积核,如果我们想要大范围的Bloom效果,就只能靠增大滤波范围or增加滤波次数来实现,基于上述代码的话采取操作分别是:
首先增大滤波次数相当于多来几次Pass1&2,一下子性能消耗就上去了,其次如果真的实践你会发现,无论是增加滤波次数还是增大滤波范围,达到的扩大效果也不是很能让人满意。
除了上述的自发光实现效果,我在调整的过程中还感受到:高斯模糊实现的自发光Bloom总是有一种明显的边界感。这是为什么?
我猜应该是因为高斯模糊(感觉只要是基于卷积核的模糊都是一样的)总是依据卷积核每一格的权重进行加权平均计算出中间项的值,所以即使降采样了,每一level之间还是会存在明显的亮度突变,于是源图和模糊后的图的亮度无论调整哪个参数都不会过渡均匀。
下一篇将会介绍双重模糊实现Bloom的方法。(断更了很久了,最近简直是从石头缝里挤时间来额外学习TA的内容,太难了太难了)