US3S10L8——Bloom效果
US3S10L8——Bloom效果
本章代码关键字
1 | UNITY_UV_STARTS_AT_TOP // 判断纹理是否会进行y轴翻转 |
Bloom 效果
Bloom 效果(中文也可以叫做高光溢出效果),是一种使画面中亮度较高的区域产生一种光晕或发光效果的图像处理技术
Bloom 效果 的主要目的是 模拟现实世界中强光源在相机镜头或人眼中造成的散射和反射现象
使得画面中较亮的区域“扩散”到周围的区域,造成一种朦胧的效果
Bloom 效果的基本原理
三步骤概括 Unity Shader 中实现 Bloom 效果的基本原理:
- 提取:提取原图像中的亮度区域存储到一张新纹理中
- 模糊:将提取出来的纹理进行模糊处理(一般采用高斯模糊)
- 合成:将模糊处理后的亮度纹理和源纹理进行颜色叠加
关键知识点:多个 Pass
的运用,如何提取,如何模糊,如何合成
多个Pass的运用
通过高斯模糊效果的学习,我们知道可以在屏幕后处理时,单独调用某个 Pass
对渲染纹理进行处理,只需要利用 Unity 中的三个函数即可
- 获取中间缓冲纹理:RenderTexture.GetTemporary()
- 对纹理应用着色器处理,复制到另一个纹理内:Graphics.Blit()
- 释放缓冲纹理:
RenderTexture.ReleaseTemporary()
处理高斯模糊效果时,我们使用了 2 个 Pass
,分别用来计算水平卷积核竖直卷积
而我们在处理 Bloom 效果时将使用 4 个 Pass
,他们分别是:
- 用于 提取亮度区域 存储到新纹理中的 1 个
Pass
- 用于 模糊处理提取出来的纹理的高斯模糊 的 2 个Pass(可以复用高斯模糊 Shader 中写好的
Pass
) - 用于 与原图像进行合成 的 1 个
Pass
通过使用这 4 个 Pass
,我们就能够完成:提取、模糊、合成,这三个步骤
提取亮度区域
我们需要在 Shader 中声明一个亮度阈值变量,亮度低于该值的区域不会被提取,主要用于 “提取” Pass
的片元着色器函数当中,
用当前像素的灰度值 与 亮度阈值变量(_LuminanceThreshold
)进行计算
1 | fixed luminance(fixed4 color) |
- 灰度值 – 亮度阈值变量: 是为了仅保留超过阈值的部分,可以提取出图像中亮度较高的地方。
- Clamp 函数: 如果结果小于 0 则为 0,大于 1 则为 1。得到的
val
表示像素的亮度贡献 - 颜色 * 亮度贡献: 基于亮度阈值调节颜色的亮度,若
val
为 0 则为黑色,越接近 1 越接近原始颜色
这样做的目的是保留高亮区域的颜色信息,同时衰减低亮区域的颜色。
如果 “提取” Pass
是 Shader 中的第一个 Pass
,
那么我们完全可以利用 RenderTexture.GetTemporary
和 Graphics.Blit
函数将源纹理提取亮度信息后存储到缓存区中
1 | RenderTexture buffer = RenderTexture.GetTemporary(renderTextureW, renderTextureH, 0); |
模糊亮度区域纹理
由于我们目前已经在高斯模糊的课程中写好了两个用于进行水平和竖直卷积运算的 Pass
因此我们可以使用以前学习 SubShader
语句时学习的 UsePass 指令
相关内容详见:US2S2L5-3——Pass-渲染通道
只需要在之前的高斯模糊 Shader 中为两个 Pass
取名,然后在 Bloom 的 Shader 中复用即可
复用了两个 Pass
后,我们只需要在Bloom效果对应的 C# 代码中对 “提取” 出来的纹理,进行高斯模糊 Pass
的调用即可(逻辑和高斯模糊一致)
接着,我们需要在 Bloom 效果的 Shader 中声明一个纹理属性 _Bloom
,用于存储模糊处理完毕后的纹理
在 C# 代码中完成高斯模糊处理后,只需要将缓存区内容,写入材质球中的纹理属性即可
完成这一步后,在 Shader 中就能够得到模糊处理后的亮度纹理信息了,我们便可以进行第三步 “合成”
合成模糊过的亮度区域纹理
在“合成”的 Pass
中我们只需要用主纹理 _MainTex
(其中使用的纹理是屏幕原图像)和纹理属性 _Bloom
(其中使用的纹理是模糊处理后的亮度纹理信息)
进行颜色叠加即可,我们对两张纹理进行采样,将获取到的颜色信息进行加法运算,
因为颜色相加带来的效果就是增加亮度,使得原本高亮的部分变得更加显眼,从而达到 Bloom 效果(高光溢出效果)
Bloom 效果具体实现
新建 Shader,取名Bloom ,删除无用代码,接下来在这个 Shader 内依次实现 Bloom 效果需要的 Pass
提取颜色区域纹理
主要目的:提取原图像中的亮度区域存储到一张新纹理中
Shader 代码:
-
声明属性
- 主纹理
_MainTex
- 亮度区域纹理
_Bloom
- 亮度阈值
_LuminanceThreshold
1
2
3
4
5
6Properties
{
_MainTex("Texture", 2D) = "white"{}
_Bloom("Bloom", 2D) = ""{}
_LuminanceThreshold("_LuminanceThreshold", Float) = 0.5;
} - 主纹理
-
在
CGINCLUDE...ENDCG
中实现共享 CG 代码- 属性映射
- 结构体(顶点,
uv
) - 灰度值(亮度值)计算函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20CGINCLUDE
sampler2D _MainTex;
sampler2D _Bloom;
float _LuminanceThreshold;
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
// 计算颜色的亮度值(灰度值)
fixed luminance(fixed4 color)
{
return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
}
ENDCG -
屏幕后处理标配
-
ZTest Always
-
Cull Off
-
ZWrite Off
1
2
3
4
5Tags { "RenderType"="Opaque" }
ZTest Always
Cull Off
ZWrite Off -
-
提取
Pass
实现- 顶点着色器
- 顶点转换、UV赋值
- 片元着色器
- 颜色采样、亮度贡献值计算、颜色*亮度贡献值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26Pass
{
CGPROGRAM
v2f vert(appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 采样原纹理颜色,得到亮度贡献值,返回颜色*纹理贡献值即得到亮度区域纹理
fixed4 color = tex2D(_MainTex, i.uv);
fixed value = clamp(luminance(color) - _LuminanceThreshold, 0, 1);
return color * value;
}
ENDCG
}
C# 代码:
-
创建 C# 脚本,名为
Bloom
-
继承屏幕后处理基类
PostEffect
代码详见:US3S10L4——屏幕后处理基类
-
声明亮度阈值成员变量
-
重写
OnRenderImage
函数 -
设置材质球的亮度阈值
-
在其中利用
Graphics.Blit
、RenderTexture.GetTemporary
、RenderTexture.ReleaseTemporary
函数对纹理进行Pass
处理
1 | using UnityEngine; |
显示效果(亮度阈值是0.5):
模糊颜色区域纹理
主要目的:将提取出来的纹理进行模糊处理(一般采用高斯模糊)
Shader 代码:
-
添加模糊半径属性
_BlurSize
,进行属性映射(注意:需要用到纹素)1
2
3
4
5
6
7Properties
{
_MainTex("Texture", 2D) = "white"{}
_Bloom("Bloom", 2D) = ""{} // 提取出来的亮度区域纹理
_LuminanceThreshold("_LuminanceThreshold", Float) = 0.5 // 亮度阈值
_BlurSpread("BlurSpread", Float) = 1 // 模糊半径
}1
2
3
4
5
6
7
sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _Bloom;
float _LuminanceThreshold;
float _BlurSize; -
修改之前高斯模糊 Shader,为其中的两个
Pass
命名具体代码详见:US3S10L7——高斯模糊
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// Shader: "PostEffect/GaussianBlur"
// 水平方向上的Pass
Pass
{
Name "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
ENDCG
}
// 垂直方向上的Pass
Pass
{
Name "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
ENDCG
} -
在 Bloom Shader 中,利用
UsePass
复用 高斯模糊 Shader 中两个Pass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18SubShader
{
CGINCLUDE
//...
ENDCG
Tags { "RenderType"="Opaque" }
ZTest Always
Cull Off
ZWrite Off
Pass {/*...提取...*/}
// 复用高斯模糊的两个Pass
UsePass "PostEffect/GaussianBlur/GAUSSIAN_BLUR_HORIZONTAL"
UsePass "PostEffect/GaussianBlur/GAUSSIAN_BLUR_VERTICAL"
}
C# 代码:
- 复制高斯模糊中的 3 个属性
- 复制高斯模糊中 C# 代码中处理高斯模糊的逻辑(注意修改使用的
Pass
索引!) - 将模糊处理后的纹理,存储
_Bloom
纹理属性
1 | using UnityEngine; |
显示效果(亮度阈值0.5,降低采样程度2,迭代次数3,模糊半径3):
将模糊区域纹理合并到源图像上
主要目的:将模糊处理后的亮度纹理和源纹理进行颜色叠加
Shader代码:
-
合并
Pass
实现-
结构体
顶点坐标,4 维的
uv
(xy
存主纹理,wz
存亮度提取纹理)1
2
3
4
5struct v2fBloom
{
float4 pos : SV_POSITION;
half4 uv : TEXCOORD0; // xy主要用于对主纹理进行采样,zw主要用于亮度模糊后的纹理采样
}; -
顶点着色器(判断外部传入的
RenderTexture
是否被翻转)顶点坐标转换,纹理坐标赋值
注意:亮度纹理的
uv
坐标需要判断是否进行 y 轴翻转,因为使用RenderTexture
写入到 Shader 的纹理变量时
Unity 可能会对其进行 y 轴翻转(根据平台不同而不同),我们可以利用 Unity 提供的预处理宏进行判断1
2如果这个宏被定义,说明当前平台的纹理坐标系的 y 轴原点在顶部,还可以在该宏中用纹素进行判断
如果纹素的 y 小于 0,为负数,表示需要对 y 轴进行调整,配合起来使用就是1
2
3
4
5// 用宏判断uv坐标是否被翻转
// 如果纹素的y小于0,为负数,表示需要对Y轴进行调整
o.uv.w = _MainTex_TexelSize.y < 0 ? 1 - o.uv.w : o.uv.w;主纹理不需要我们进行额外处理,一般 Unity 会自动处理,一般只需要在使用
RenderTexture
时才考虑该问题1
2
3
4
5
6
7
8
9
10
11
12
13
14
15v2fBloom vertBloom(appdata_base v)
{
v2fBloom o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.texcoord; // 亮度纹理和主纹理要采样相同的地方进行颜色叠加
o.uv.zw = v.texcoord;
// 用宏判断uv坐标是否被翻转
// 如果纹素的y小于0,为负数,表示需要对Y轴进行调整
o.uv.w = _MainTex_TexelSize.y < 0 ? 1 - o.uv.w : o.uv.w;
return o;
} -
片元着色器
两个纹理颜色采样后相加
1
2
3
4
5fixed4 fragBloom(v2fBloom i) : SV_Target
{
// 将两个纹理颜色叠加起来
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
} -
FallBack Off
这里不需要后备 Shader,因为如果 Shader 不支持就直接不执行了
-
C# 代码:
对源纹理进行合并处理
显示效果(亮度阈值0.5,降低采样程度2,迭代次数3,模糊半径3):
可见,Bloom 效果让使得高光部分更亮,并且呈现扩散效果。
其中,哪些亮度区域能够变得更亮是由亮度阈值决定的,而变亮的部分的亮度和扩散程度主要是由模糊程度,也就是那三个高斯模糊相关参数决定
Bloom 效果完整代码
完整 Shader 代码:
1 | Shader "PostEffect/Bloom" |
完整 C# 代码:
1 | using UnityEngine; |