一、什么是Bloom算法
1、首先看一下Bloom效果长什么样
2、什么是Bloom
● Bloom,也称辉光,是一种常见的屏幕效果
● 模拟摄像机的一种图像效果,让画面中较亮的区域“扩散”到周围的区域中,造成一种朦胧的效果
● 可以让物体具有真实的明亮效果
● 可以实现光晕效果
3、Bloom的实现原理
①Bloom实现原理
● 实现思路:
○ 1.提取原图较亮区域(利用阈值)
○ 2.模糊该图像
○ 3.与原图混合/叠加
● /在HDR和LDR的那节课中也提到过bloom,我直接把当时的作的一张流程图摘过来以供参考:
②前置知识1:HDR和LDR
● HDR和LDR分别是是高动态范围和低动态范围的缩写
● LDR
○ jpg、png格式图片
○ RGB在[0,1]
● HDR
○ HDR、EXR格式图片
○ 可以超过1
● 因为自然界中的亮度差异是很大的(比如蜡烛的光强度约为15,而太阳的强度约为10w),只用LDR的话很多效果完全表现不出来
③前置知识2:高斯模糊
● 实现图像模糊的一种方式
● 高斯模糊:
○ 利用高斯核进行卷积运算,得到模糊的图像
高斯核
○ 高斯核:
■ 通过高斯函数定义的卷积核
○ 核中心:(0,0)
○ 核大小:3x3
○ 标准方差σ:1.5
○ 计算步骤:
■ 将(x,y)带入公式中,计算出权重值,(权重值代表当前处理像素的影响程度,离中心越近权重越大)
■ 为了保证卷积后图像不变暗,需要对高斯核进行归一化处理(每个权重除以所有权重的和)
● 补充:
○ 参考:GAMES101-L6内容
○ 滤波(Filtering)
■ 滤波就是抹掉特殊频率的东西
■ 不同滤波的效果:
● 高通滤波 = 边界
● 低通滤波 = 模糊
○ 滤波(Filtering)=卷积(Convolution)=平均(Averaging)
○ 卷积操作的定义
■ ①原始信号的任意一个位置,取其周围的平均
■ ②作用在一个信号上,用一种滤波操作,得到一个结果Result
三、 Bloom算法的应用
四、Reference
● 图片参考:· https://unsplash.com/·
● 自行拍摄项目参考:· https://github.com/keijiro/KinoBloom· https://github.com/MarcusXie3D/FastBloomForMobiles
● 资料参考:
○ learnopengl:https://learnopengl.com/Advanced-Lighting/Bloom·
○ Unity Shader入门精要:12.5 Bloom效果·
○ https://en.wikipedia.org/wiki/Bloom_(shader_effect)
○ https://en.wikipedia.org/wiki/High_dynamic_range
○ https://zhuanlan.zhihu.com/p/76505536
Shader "Unlit/DS_Bloom"
{
Properties
{
// _MainTex为渲染纹理,变量名固定不能改变
//模糊结果、阈值、模糊半径的变量名与C#脚本中的对应
_MainTex ("Texture", 2D) = "white" {}
_Bloom ("Bloom (RGB)", 2D) = "black" {} //高斯模糊后的结果
_LuminanceThreshold ("Luminance Threshold", Float) = 0.5 //阈值
_BlurSize ("Blur Size", Float) = 1.0 //模糊半径
}
SubShader
{
//用CGINCLUDE和ENDCG
//Unity会把它们之间的代码插入到每一个pass中,已达到声明一遍,多次使用的目的。
CGINCLUDE
#include "UnityCG.cginc"
//声明属性和C#脚本中用到的变量
sampler2D _MainTex;
half4 _MainTex_TexelSize;//纹素大小
sampler2D _Bloom;
float _LuminanceThreshold;
float _BlurSize;
//########第1个pass使用########
//输出结构
struct v2fExtractBright {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
//顶点着色器
v2fExtractBright vertExtractBright(appdata_img v) {
//appdata_img是官方提供的输入结构,只包含图像处理时必须的顶点坐标和uv等变量
v2fExtractBright o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
// 明亮度公式
// 在RGB模式下,像素亮度的计算公式为:L=R*0.30+G*0.59+B*0.11,简称305911公式
fixed luminance(fixed4 color) {
//计算得到像素的亮度值
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
//片元着色器->提取高亮区域
fixed4 fragExtractBright(v2fExtractBright i) : SV_Target {
fixed4 c = tex2D(_MainTex, i.uv);// 贴图采样
// 调用luminance得到采样后像素的亮度值,再减去阈值
// 使用clamp函数将结果截取在[0,1]范围内
//clamp() 函数的作用是把一个值限制在一个上限和下限之间,当这个值超过最小值和最大值的范围时,在最小值和最大值之间选择一个值使用
fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0);
// 将val与原贴图采样得到的像素值相乘,得到提取后的亮部区域
return c * val;
}
//########第2、3个pass使用########
//输出结构
struct v2fBlur {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;
// 此处定义5维数组用来计算5个纹理坐标
// 由于卷积核大小为5x5的二维高斯核可以拆分两个大小为5的一维高斯核
// uv[0]存储了当前的采样纹理
// uv[1][2][3][4]为高斯模糊中对邻域采样时使用的纹理坐标
};
//顶点着色器->计算竖直方向进行高斯模糊的uv
v2fBlur vertBlurVertical(appdata_img v) {
v2fBlur o;
o.pos = UnityObjectToClipPos(v.vertex);//将顶点从模型空间变换到裁剪空间下
half2 uv = v.texcoord;
o.uv[0] = uv;
//uv[0]就是(0,0)
//对竖直方向进行模糊
//对应到邻域就是下边的情况
//uv[1],向上挪动1个单位(0, 1)
//uv[2],向下挪动1个单位(0, -1)
//uv[3],向上挪动2个单位(0, 2)
//uv[3],向下挪动2个单位(0, -2)
//最后乘上模糊半径作为参数控制
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
return o;
}
//顶点着色器->计算水平方向进行高斯模糊的uv
v2fBlur vertBlurHorizontal(appdata_img v) {
v2fBlur o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
//uv[0]就是(0,0)
//对水平方向进行模糊
//同理,uv[1]到[4]分别对应(1, 0)、(-1, 0)、(2, 0)、(-2, 0)
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
return o;
}
//片元着色器->进行高斯模糊
fixed4 fragBlur(v2fBlur i) : SV_Target {
float weight[3] = {0.4026, 0.2442, 0.0545};
// 因为二维高斯核具有可分离性,而分离得到的一维高斯核具有对称性
// 所以只需要在数组存放三个高斯权重即可
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
// 结果值sum初始化为当前的像素值乘以它对应的权重值
// 进行卷积运算,根据对称性完成两次循环
// 第一次循环计算第二个和第三个格子内的结果
// 第二次循环计算第四个和第五个格子内的结果
for (int it = 1; it < 3; it++) {
sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
}
return fixed4(sum, 1.0);// 返回滤波后的结果
}
//########第4个pass使用########
//输出结构
struct v2fBloom {
float4 pos : SV_POSITION;
half4 uv : TEXCOORD0;
};
//顶点着色器
v2fBloom vertBloom(appdata_img v) {
v2fBloom o;
o.pos = UnityObjectToClipPos (v.vertex);
o.uv.xy = v.texcoord; //xy分量为_MainTex的纹理坐标
o.uv.zw = v.texcoord; //zw分量为_Bloom的纹理坐标
// 平台差异化处理
//判断y是否小于0,如果是就进行翻转处理
#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(v2fBloom i) : SV_Target {
// 把这两张纹理的采样结果相加即可得到最终效果
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
}
ENDCG
// 开启深度测试,关闭剔除和深度写入
ZTest Always
Cull Off
ZWrite Off
//第一个pass,提取较亮区域
Pass{
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}
//第二个pass,进行竖直方向高斯模糊
Pass{
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
//第三个pass,进行水平方向高斯模糊
Pass{
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
//第四个pass,混合高亮区域和原图
Pass{
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
}
FallBack Off
}