大部分解释说明都在代码注释里面,部分代码很长,本人进行了折叠,帮助更好理清思路,推荐复制放入VS CODE观看
VS Code 展开/折叠快捷键:ctrl+k 再 ctrl+j/0
高斯模糊
-
高斯核的构建
G ( x , y ) = 1 2 π σ 2 e − x 2 + y 2 2 σ 2 G(x,y)=\frac{1}{2\pi\sigma^2}e^{-\frac{x^2+y^2}{2\sigma^2}} G(x,y)=2πσ21e−2σ2x2+y2
很简单,从二维正态分布中均匀的采样(按照矩阵的形状),得到采样点的值,之后归一化即可
-
模糊的程度与参数的关系:
核的范围越大,模糊程度越高
核的分布越重尾,模糊程度越高(存疑,还没有实验)
模糊运算的次数越多越模糊
-
高斯核的分解:
由于卷积核是二维的,其运算量是随边长平方数增长的,负担太大;
若能分解为两个一维的,其运算量只会线性增长
将上述的两条,当作两个模糊核,分别做一次模糊运算,其结果和一个二维高斯模糊是一致的,这大大减少了运算
另外由于两条的权值是一样的,单独的也是对称的,所以对于一个高斯核的存储,可以大大压缩: 5 × 5 5\times5 5×5的高斯核实际上只用存3个数字
在实际计算中,会用到两个Pass,分别对应两条一维核(猜测是由于两个核不同,所以不能共用一个pass?)
实际上为了节省运算,一般还需要缩放图像来节省运算(先把图像分辨率降低,再模糊,反正都是模糊)
代码
脚本
using UnityEngine;
public class G_Blur : PostEffectsBase
{
public Shader G_Blur_Shader;
private Material G_Blur_Mat = null;
public Material material{
get{
G_Blur_Mat = CheckShader_CreateMat(G_Blur_Shader,G_Blur_Mat);
return G_Blur_Mat;
}
}
//迭代次数
[Range(0,4)]
public int iterations = 3;
//控制相邻两个采样点的距离
[Range(0.2f, 10.0f)]
public float blurSpread = 0.6f;
//下采样次数 这个参数是控制图像分辨率的,越多次图像越像素
[Range(1,8)]
public int downSample = 2;
//1.0这里参数中就直接创建了两个纹理变量
// void OnRenderImage(RenderTexture src, RenderTexture dest)
// {
// if (material != null)
// {
// //这里我们建立了一块缓冲区,大小与屏幕一致
// //原因因为两次模糊需要两个Pass,所以在第一次模糊后不能直接输出到目标纹理,而是先输出到缓冲中暂存
// int rtW = src.width; //src因为是渲染纹理,所以其长宽和屏幕一致
// int rtH = src.height;
// RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH,0); //这个函数能分配一块缓存用来作buffer
//列,调用第1个pass进行第一次模糊,并存储到刚才创建的缓冲中
// Graphics.Blit(src, buffer, material, 0);
//行,调用第2个pass,输出到目标纹理
// Graphics.Blit(buffer, dest, material ,1);
// RenderTexture.ReleaseTemporary(buffer);//释放缓存
// }
// else //没材质直接原样输出(实际上上面有创建材质的函数,没材质说明出错了)
// {
// Graphics.Blit(src, dest);
// }
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
int rtW = src.width/downSample;
int rtH = src.height/downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0); //因为没有材质参数,会直接传递
//请注意这里的渲染纹理,循环内部创建的渲染纹理在循环外部是访问不到的
//所以这里用buffer0和buffer1来来回回换数据
for (int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the vertical pass
Graphics.Blit(buffer0, buffer1, material, 0);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the horizontal pass
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0, dest);
RenderTexture.ReleaseTemporary(buffer0);
} else {
Graphics.Blit(src, dest);
}
}
}
Shader
Shader "Unlit/G_Blur"
{
Properties
{
_MainTex ("Base", 2D) = "white" {
}
_BlurSize ("Blur Size", float) = 1.0
//控制的是卷积核的每个元素之间的距离,1则为一个像素,1.5则为一个半像素
//你要清楚这个卷积采样不是以像素为单位的,而是浮点距离,像素大小只不过是参考而已
//过大的blursize会产生虚影,所以不能单纯的扩大blursize
}
SubShader
{
//CGINCLUDE中的代码相当于在所有的Pass中写一遍
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize; //内置变量,可以获取纹素大小
float _BlurSize;
//后处理不需要顶点数据结构
struct v2f
{
float2 uv[5] : TEXCOORD0; //5维数组,用来存储卷积核
float4 vertex : SV_POSITION;
};
v2f vertBlurCol(appdata_img v)//注意这里的appdata_img
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
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;
}
v2f vertBlurRow(appdata_img v)//注意这里的appdata_img
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
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(v2f i) : SV_Target
{
float weight[3] = {
0.4026,0.2442,0.0545};
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
sum += tex2D(_MainTex, i.uv[1]) * weight[1];
sum += tex2D(_MainTex, i.uv[2]) * weight[1];
sum += tex2D(_MainTex, i.uv[3]) * weight[2];
sum += tex2D(_MainTex, i.uv[4]) * weight[2];
return fixed4(sum,1.0);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vertBlurCol
#pragma fragment fragBlur
ENDCG
}
Pass
{
CGPROGRAM
#pragma vertex vertBlurRow
#pragma fragment fragBlur
ENDCG
}
}
Fallback Off
}
Bloom
非常简单:
选取渲染纹理中亮度足够高的部分,进行高斯模糊,之后再和原图进行混合
- 这里有一个很容易误解的点:Bloom是屏幕特效,那它是怎么单独模糊一个物体的呢?场景中其他的部分势必会收到影响啊。
- 其实很简单,把需要bloom的物体单独提出到一张RenderTex,bloom算完之后和原图混合就好了。
代码
Script
using UnityEngine;
public class bloom : PostEffectsBase
{
public Shader bloomShader;
private Material bloom_Mat = null;
public Material material{
get{
bloom_Mat = CheckShader_CreateMat(bloomShader, bloom_Mat);
return bloom_Mat;
}
}
//模糊所需参数
[Range(0,4)]
public int iterations = 3;
[Range(0.2f, 10.0f)]
public float blurSpread = 0.6f;
[Range(1,8)]
public int downSample = 2;
//bloom截取强度
[Range(0.0f, 4f)]
public float lumi = 0.6f; //不止到1,是为了HDR
void OnRenderImage(RenderTexture src, RenderTexture dest) {
if (material != null){
material.SetFloat("_Lumi", lumi);
int rtW = src.width/downSample;
int rtH = src.height/downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH,0);
buffer0.filterMode = FilterMode.Bilinear;
//先走第一个pass提取需要bloom的部分
Graphics.Blit(src, buffer0,material,0);
//注意循环内部创建的RenderTex在循环外是访问不到的,因此需要左手倒右手
for (int i = 0; i < iterations; i++) {
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
Graphics.Blit(buffer0, buffer1, material, 2);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
//这一步很重要啊,把bloom好的图像传递给Shader
material.SetTexture("_Bloom", buffer0);
Graphics.Blit(src, dest, material, 3);
RenderTexture.ReleaseTemporary(buffer0);
}else{
Graphics.Blit(src, dest);
}
}
}
Shader
Shader "Unlit/bloom"
{
Properties
{
_MainTex ("Base", 2D) = "white" {
}
_Bloom ("Bloom", 2D) = "black"{
} //用于存储亮度图
_BlurSize ("Blur Size", float) = 1.0
_Lumi ("lumi", float) = 0.6 //这个值是用来控制bloom的阈值
}
SubShader
{
//region CGINCLUDE
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
sampler2D _Bloom;
half4 _MainTex_TexelSize; //内置方法,可以获取纹素大小
float _BlurSize;
float _Lumi;
//region PASS_1
struct v2f{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vertBloom(appdata_img v){
v2f o;
o.vertex = 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;
}
fixed4 fragBloom(v2f i): SV_Target {
fixed3 c = tex2D(_MainTex, i.uv);
fixed l = clamp(Luminance(c) - _Lumi, 0, 1); //clamp函数是一个截取函数,后面两个参数是范围
return fixed4 (c*l,1);//为什么这里要乘?
//这里要乘而不是单纯的取0/1,是因为:
//(1)GPU不擅长分支运算
//(2)数值越大的fragment在模糊时,影响的范围更大,符合真实逻辑,乘法保留了这种特性
}
//endregion
//region PASS_2
struct v2fMerg{
float4 vertex : SV_POSITION;
float4 uv : TEXCOORD0;
};
v2fMerg vertMerge(appdata_img v)
{
v2fMerg o;
o.vertex = UnityObjectToClipPos(v.vertex);
//将两张纹理的坐标分开是为了处理平台差异化
o.uv.xy = v.texcoord;
o.uv.zw = v.texcoord;
//平台差异化处理,详见“Unity的一些机制”和《精要》5.6.1
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0.0)
o.uv.w = 1.0 - o.uv.w;
#endif
return o;
}
fixed4 fragMerge(v2fMerg i): SV_Target{
//直接加?
//当然直接相加,不然不就是单纯的模糊了,bloom的亮度就是要超额,超额也更适合HDR
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.xy);
}
//endregion
ENDCG
//endregion
ZTest Always Cull Off ZWrite Off
Pass//提取亮度Pass
{
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
//模糊Pass
//注意这里是直接引用了同工程下的另外一个shader中的Pass
UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_VERTICAL"
UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_HORIZONTAL"
Pass//混合Pass
{
CGPROGRAM
#pragma vertex vertMerge
#pragma fragment fragMerge
ENDCG
}
}
Fallback Off
}