Unity-Shader读书笔记(11)

        屏幕后处理特效(screen post-processing effects)是游戏中实现屏幕后处理特效的常见方法。屏幕后处理通常指的是在渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作,实现各种屏幕特效。我们使用这种技术可以为游戏画面添加更多的艺术效果,比如景深(Depth of Field)、运动模糊(Motion Blur)等。

建立一个基本的屏幕后处理脚本系统

        想要实现屏幕后处理,我们先要抓取屏幕图像。Unity提供了OnRenderImage函数(MonoBehaviour-OnRenderImage(RenderTexture,RenderTexture) - Unity 脚本 API)就可以帮我们做到。在这个函数中,我们通常是使用Graphics.Blit(Graphics-Blit - Unity 脚本 API)函数来完成对渲染纹理的处理。一般而言OnRenderImage函数会在所有的不透明和透明Pass执行完毕后被调用,以便对场景中所有游戏对象都产生影响。但是有时候我们希望在不透明物体渲染完成后立即调用OnRenderImage。那么我们就可以在函数前面加ImageEffectOpaque(ImageEffectOpaque - Unity 脚本 API)属性来实现。

        在Unity中实现一个屏幕后处理的过程通常如下:我们首先在摄像机中添加一个用于屏幕后处理的脚本,我们用这个脚本来获取当前屏幕的渲染纹理。然后我们再调用Graphics.Blit函数使用特定的Unity Shader来对当前图像进行处理,再把返回的渲染纹理显示到屏幕。对于复杂的屏幕特效。我们可能需要多次调用Graphics.Blit函数来对上一步输出结果进行下一步处理。但是并非所有的平台都支持屏幕后处理,对此我们要进行条件判断(这里判断不会给出依照具体需求,而书中给出的判断在Unity2019.4.40f1版本中已经弃用了)。书中作者是写了一个屏幕后处理的基类,然后其他后处理效果继承这个基类。具体代码如下:

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
26
27
28
29
30
31
32
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace ShaderScript
{
[ExecuteInEditMode] // 确保在编辑器模式下也可以执行
[RequireComponent(typeof(Camera))] // 我们要求这个组件必须要有摄像机
public class ScreenPostEffectsBase : MonoBehaviour
{
// 指定Shader来处理纹理
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material)
{
if(shader == null)
return null;
if(shader.isSupported && material && material.shader == shader)
return material;

if(!shader.isSupported)
return null;
else
{
material = new Material(shader);
material.hideFlags = HideFlags.DontSave;
if (material)
return material;
else
return null;
}
}
}
}

调整屏幕的亮度、饱和度和对比度

        我们随意将一个图片(这个图片要设置为spire类型)拖入场景。我们要创建一个新的类来实现这个功能,类的具体实现如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace ShaderScript
{
public class BrightnessSaturationAndContrast : ScreenPostEffectsBase
{
public Shader shader;
private Material m_Material;
public Material M_Material {
get {
m_Material = CheckShaderAndCreateMaterial(shader, m_Material);
return m_Material;
}
}

[Range(0.0f,3.0f)]
public float brightness = 1.0f;

[Range(0.0f, 3.0f)]
public float saturation = 1.0f;

[Range(0.0f, 3.0f)]
public float contrast = 1.0f;

private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
// 如果有材质可用,那么把参数传给材质然后用Graphics.Blit进行处理
if (M_Material != null)
{
M_Material.SetFloat("_Brightness", brightness);
M_Material.SetFloat("_Saturation", saturation);
M_Material.SetFloat("_Contrast", contrast);
Graphics.Blit(source, destination, M_Material);
}
else // 否则直接输出原图
Graphics.Blit(source, destination);
}
}
}

同时我们也要写一个Shader,类中的shader变量就是由这个Shader来填充。我们需要注意的是我们传递的值名称必须和Shader中的名称一一对应。不然类似SetFloat这样的函数是不会有作用的。Shader中我们还要多声明一个纹理其名必须是_MainTex,这样Graphics.Blit才会将取到的图像给Shader。brightness控制亮度,saturation控制饱和度,contrast控制对比度。

Shader的实现如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
Shader "Learn/BrightnessSaturationAndContrast"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Brightness ("Brightness", Range(0,3)) = 1
_Saturation ("Saturation", Range(0,3)) = 1
_Contrast ("Contrast", Range(0,3)) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
ZTest Always
Cull Off
ZWrite Off

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
float4 _MainTex_ST;
float _Brightness;
float _Saturation;
float _Contrast;

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
col.rgb *= _Brightness;
fixed luminance = 0.2125 * col.r + 0.7154 * col.g + 0.0721 * col.b;
fixed3 luminanceColor = fixed3(luminance,luminance,luminance);
col.rgb = lerp(luminanceColor,col.rgb,_Saturation);
col.rgb = lerp(fixed3(0.5,0.5,0.5),col.rgb,_Contrast);
return col;
}
ENDCG
}
}
FallBack Off
}

        屏幕后处理实际上是在场景中绘制了一个与屏幕同宽同高的四边形面片,为了防止它对其他物体产生影响,我们需要设置相关的渲染状态,如上便是关闭深度写入和剔除。之前我们说过,OnRenderImage是可以在不透明物体渲染完后立刻进行然后再是透明物体。所以我们不这么设置可能会影响到透明物体的渲染。

        顶点函数其实就是简单的采集纹理罢了,剩下的操作都在片元函数中。只不过我们并不需要对纹理uv坐标进行处理,我认为是因为这里uv本来就是填充整个屏幕了。

        我们提高亮度是直接在得到的颜色值中乘上我们设定的亮度值。其实你可以发现如果你设定的亮度值很大,那么最终得到的rgb值就是(1,1,1)。这个值其实就是白色,因此我理解就是所谓的提高亮度值就是将其颜色更趋近于白色。

        在设定饱和度前,我们先要计算出这个纹素对应的亮度值。亮度值公式是根据我们人眼对颜色r、g、b通道不同的感知进行加权计算。我在搜亮度公式的时候,我就搜到过很多不同的亮度公司。所以你不用太在意上面的计算亮度公式,用到的时候网上搜索一下就好了。至于后面的饱和度调节为什么是这样的去做的,我只能搜到以下的解释。我们先得到同等亮度下,饱和度最低的值(书中说这个饱和度值为0)。而这个值便是fixed3(luminance,luminance,luminance)。然后对其和原来的颜色进行插值处理。(更多关于饱和度的文章:RGB和饱和度的关系unity渲染篇:画面亮度、饱和度、对比度调整_unity 色相 饱和度_unity大话东游的博客-CSDN博客)。

        关于对比度,那就是我们设定一个值然后用原来的颜色进行插值(可能这就叫做对比吧)。

边缘检测

        边缘检测的原理是利用一些边缘检测算子对图像进行卷积(卷积_百度百科)操作。关于边缘检测算子,大家可以看这个文章边缘检测系列1:传统边缘检测算子 - 飞桨AI Studio。虽然它用的语言是Python,但是我们只要了解一下原理。

        类代码如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace ShaderScript
{
public class EdgeDetection : ScreenPostEffectsBase
{
// 我是觉得这段可以合到父类,但是我这里就按照书上来了
public Shader shader;
private Material m_Material;
public Material M_Material
{
get
{
m_Material = CheckShaderAndCreateMaterial(shader, m_Material);
return m_Material;
}
}

[Range(0.0f, 1.0f)]
public float edgesOnly = 1.0f;

public Color edgeColor = Color.black;

public Color backgroundColor = Color.white;

private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
// 如果有材质可用,那么把参数传给材质然后用Graphics.Blit进行处理
if (M_Material != null)
{
M_Material.SetFloat("_EdgesOnly", edgesOnly);
M_Material.SetColor("_EdgeColor", edgeColor);
M_Material.SetColor("_BackgroundColor", backgroundColor);
Graphics.Blit(source, destination, M_Material);
}
else // 否则直接输出原图
Graphics.Blit(source, destination);
}
}
}

        edgesOnly决定多少是使用边缘线背景加边缘线。edgeColor设定边缘线颜色。backgroundColor设置边缘线背景颜色。

        Shader代码如下:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
Shader "Learn/EdgeDetection"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_EdgesOnly ("Brightness", Range(0,1)) = 1
_EdgeColor ("EdgeColor", Color) = (1,1,1,1)
_BackgroundColor ("BackgroundColor", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
ZTest Always
ZWrite Off
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
half2 uv[9] : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _EdgesOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv[0] = v.uv + _MainTex_TexelSize.xy * half2(-1,-1);
o.uv[1] = v.uv + _MainTex_TexelSize.xy * half2(0,-1);
o.uv[2] = v.uv + _MainTex_TexelSize.xy * half2(1,-1);
o.uv[3] = v.uv + _MainTex_TexelSize.xy * half2(-1,0);
o.uv[4] = v.uv;
o.uv[5] = v.uv + _MainTex_TexelSize.xy * half2(1,0);
o.uv[6] = v.uv + _MainTex_TexelSize.xy * half2(-1,1);
o.uv[7] = v.uv + _MainTex_TexelSize.xy * half2(0,1);
o.uv[8] = v.uv + _MainTex_TexelSize.xy * half2(1,1);
return o;
}

fixed luminance(fixed4 color)
{
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}

half Sobel(v2f i)
{
const half Gx[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
const half Gy[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};
half texColor;
half edgeX = 0;
half edgeY = 0;

for (int j = 0; j < 9; j++) {
texColor = luminance(tex2D(_MainTex, i.uv[j]));
edgeX += texColor * Gx[j];
edgeY += texColor * Gy[j];
}

half edge = 1 - abs(edgeX) - abs(edgeY);

return saturate(edge);
}

fixed4 frag (v2f i) : SV_Target
{
half edge = Sobel(i);
fixed4 withEdgeColor = lerp(_EdgeColor,tex2D(_MainTex,i.uv[4]),edge);
fixed4 OnlyEdgeColor = lerp(_EdgeColor,_BackgroundColor,edge);
return lerp(withEdgeColor,OnlyEdgeColor,_EdgesOnly);
}
ENDCG
}
}
FallBack Off
}

        在顶点片元器中,我们根据每一个纹素的大小得到这个纹素点周围的纹素值,这样是为了给片元着色器做卷积。Shader中使用的卷积核是Sobel。片元着色器中使用纹素对应的亮度值进行卷积操作,然后我们用得到的值进行插值。

高斯模糊

        高斯模糊(高斯模糊_百度百科)也是卷积的另一种应用。

        类的实现:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace ShaderScript
{
public class GaussianBlur : ScreenPostEffectsBase
{
public Shader shader;
private Material m_Material;
public Material M_Material
{
get
{
m_Material = CheckShaderAndCreateMaterial(shader, m_Material);
return m_Material;
}
}

[Range(0,4)]
public int iterations = 1;

[Range(0.2f, 3)]
public float blurSpread = 0.6f;

[Range(1, 8)]
public int downSample = 2;

private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (M_Material != null)
{
int rtw = source.width;
int rth = source.height;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtw / downSample, rth / downSample);
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(source, buffer0);
for (int i = 0; i < iterations; i++)
{
M_Material.SetFloat("_BlurSize", 1 + i * blurSpread);

RenderTexture buffer1 = RenderTexture.GetTemporary(rtw, rth);
Graphics.Blit(buffer0, buffer1, M_Material, 0);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtw, rth);
Graphics.Blit(buffer0, buffer1, M_Material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0, destination);
RenderTexture.ReleaseTemporary(buffer0);
}
else
Graphics.Blit(source, destination);
}
}
}

        iterations是模糊迭代的次数,随着迭代次数的提升会越来越模糊。blurSpread和downSample都是出于性能的考虑,在高斯核不变的情况下,blurSpread越大模糊程度越高。但是过大的blurSpread会造成虚影,这可能不是我们希望看到的。而downSample越大,需要处理的像素数就越少,同时也能进一步提高模糊程度,但是过大的downSample会使得图像像素化。在代码中体现就是让原本应该是屏幕大小的纹理变成屏幕大小/downSample。

        高斯模糊为了节约性能将原本二维的高斯核,变成了两个一维的进行计算,而且这个两个一维还一模一样。这样就要使用两个pass分别对其进行操作。至于为什么高斯模糊可以将二维高斯核变成两个一维的,这里我贴一个网站的解释链接:高斯模糊降维的解释。所以我们使用Graphics.Blit函数的第四个参数来做UnityShader中Pass的选择。而我们不需要记录高斯函数所有的一维信息值,下面是一个5阶的高斯函数一维时的值:

1
0.0545,0.2442,0.4026,0.2442,0.0545

明显这里是由重复值的,而且是以0.4026为中心两边重复。既然这么有规律,那么我们就可以通过这个规律去减少我们存储的数值。

PS: 我这里很多数学上的说明都不严谨,大家知道意思就好了。如果你因为我的说明而感到困惑,那么我只能说声:“非常抱歉,我太菜了”。

Shader的实现:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
Shader "Learn/GaussianBlur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BlurSize ("BlurSize", Float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

CGINCLUDE
#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv[5] : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;

v2f vertBlurVertical (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv[0] = v.uv;
o.uv[1] = v.uv + float2(0,_MainTex_TexelSize.y * 1) * _BlurSize;
o.uv[2] = v.uv - float2(0,_MainTex_TexelSize.y * 1) * _BlurSize;
o.uv[3] = v.uv + float2(0,_MainTex_TexelSize.y * 2) * _BlurSize;
o.uv[4] = v.uv - float2(0,_MainTex_TexelSize.y * 2) * _BlurSize;
return o;
}

v2f vertBlurHorizontals (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv[0] = v.uv;
o.uv[1] = v.uv + float2(_MainTex_TexelSize.x * 1,0) * _BlurSize;
o.uv[2] = v.uv - float2(_MainTex_TexelSize.x * 1,0) * _BlurSize;
o.uv[3] = v.uv + float2(_MainTex_TexelSize.x * 2,0) * _BlurSize;
o.uv[4] = v.uv - float2(_MainTex_TexelSize.x * 2,0) * _BlurSize;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
float weight[3] = {0.4026,0.2442,0.0545};
fixed3 sum = tex2D(_MainTex,i.uv[0]).rgb * weight[0];
for(int j = 1; j < 3; j++)
{
sum += tex2D(_MainTex,i.uv[2 * j - 1]).rgb * weight[j];
sum += tex2D(_MainTex,i.uv[j * 2]).rgb * weight[j];
}
return fixed4(sum,1);
}

ENDCG
ZTest Always Cull Off ZWrite Off

Pass
{
NAME "GaussianBlur vertBlurVertical"

CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment frag
ENDCG
}

Pass
{
NAME "GaussianBlur vertBlurHorizontals"

CGPROGRAM
#pragma vertex vertBlurHorizontals
#pragma fragment frag
ENDCG
}
}
FallBack Off
}

        这里首次用了CGINCLUDE,它就类似于c++中的头文件。如果你还不理解,那么就你可以这么认为它这里定义好了片元函数和顶点函数,然后在Pass中你可以直接用。具体在这个脚本中,两个Pass的片元函数都是一样,那么我们没必要多写一个一模一样的。

        在顶点着色器中,我们让uv坐标以纹素大小为单位按照水平(垂直)方向进行uv坐标设置。期间我们还使用_BlurSize来控制采样距离。在高斯核不变的情况下_BlurSize越大,模糊程度越高,但采样数不会受到影响。

        在片元着色器中,我们用uv进行采样后乘以其对应的权值。

        这样经过两个Pass后,我们就将其实现了。在Pass中,我们还使用了NAME语义,这个是为了让其他的Shader可以通过它们的名字来直接调用高斯模糊的效果。

Bloom效果

        Bloom效果可以让患者较亮的地方“扩散”到周围的区域中,造成一种朦胧的效果。

        Bloom效果的原理就是根据一个阈值选出图像中亮的地方,把它们存储在一个渲染纹理中,再利用高斯模糊对得到的纹理进行模糊处理,模拟光线扩散的效果,最后再将其和原图像进行混合。

        类实现:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace ShaderScript
{
public class Bloom : ScreenPostEffectsBase
{
public Shader shader;
private Material m_Material;
public Material M_Material
{
get
{
m_Material = CheckShaderAndCreateMaterial(shader, m_Material);
return m_Material;
}
}

[Range(0, 4)]
public int iterations = 1;

[Range(0.2f, 3)]
public float blurSpread = 0.6f;

[Range(1, 8)]
public int downSample = 2;

[Range(0f, 4f)]
public float luminanceThreshold = 0.6f;

private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (M_Material != null)
{
M_Material.SetFloat("_LuminanceThreshold", luminanceThreshold);
int rtw = source.width / downSample;
int rth = source.height / downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtw, rth);
buffer0.filterMode = FilterMode.Bilinear;
// 先找到较亮的区域并存储起来
Graphics.Blit(source, buffer0,M_Material,0);
for (int i = 0; i < iterations; i++)
{
M_Material.SetFloat("_BlurSize", 1 + i * blurSpread);

RenderTexture buffer1 = RenderTexture.GetTemporary(rtw, rth);
Graphics.Blit(buffer0, buffer1, M_Material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtw, rth);
Graphics.Blit(buffer0, buffer1, M_Material, 2);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
M_Material.SetTexture("_BloomTex", buffer0);
Graphics.Blit(source, destination, M_Material,3);
RenderTexture.ReleaseTemporary(buffer0);
}
else
Graphics.Blit(source, destination);
}
}
}

        iterations、blurSpread和downSample都是用来进行模糊处理。luminanceThreshold则是我们设定的亮度阈值。我们先使用Shader中的第一个Pass得到较亮的区域并使用一个Texture进行,代码中的语句为Graphics.Blit(source, buffer0,M_Material,0)。直接下来就是走模糊的流程。模糊流程完后,我们将模糊后的纹理传递给Shader进行最后的处理。请记住这个时候我们已经完成了纹理的传递,接下来是进行混合这时候Graphics.Blit传递是原纹理,代码中就是rial.SetTexture("_BloomTex", buffer0)。

        具体的Shader代码:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
Shader "Learn/BloomShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BloomTex ("Save Texture", 2D) = "black" {}
_BlurSize ("Blur Size", Float) = 1
_LuminanceThreshold ("Luminance Threshold", Float) = 0.5
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
CGINCLUDE
#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
half4 _MainTex_TexelSize;
sampler2D _BloomTex;
float4 _BloomTex_TexelSize;
float _BlurSize;
float _LuminanceThreshold;

v2f vertExtractBright (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}

fixed luminance(fixed4 color)
{
return 0.2125 * color.r + 0.7154 * color.g + color.b * 0.0721;
}

fixed4 fragExtractBright (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex,i.uv);
fixed val = clamp(luminance(col) - _LuminanceThreshold,0,1);
return col * val;
}

struct v2fBloom
{
float4 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

v2fBloom vertBloom (appdata v)
{
v2fBloom o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv.xy = v.uv;
o.uv.zw = v.uv;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv.w = 1 - o.uv.w;
#endif
return o;
}

fixed4 fragBloom (v2fBloom i) : SV_Target
{
return tex2D(_MainTex,i.uv.xy) + tex2D(_BloomTex,i.uv.zw);
}

ENDCG

ZTest Always Cull Off ZWrite Off

Pass
{
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}

UsePass "Learn/GaussianBlur/GaussianBlur vertBlurVertical"
UsePass "Learn/GaussianBlur/GaussianBlur vertBlurHorizontals"

Pass
{
CGPROGRAM
#pragma vertex vertBloom
#pragma fragment fragBloom
ENDCG
}
}
    FallBack Off
}

        这里我们要注意一下,因为我们要使用之前写好的模糊效果,那么变量名称必须是按照之前写的模糊效果中的变量名进行命名。

        先说找较亮区域,我们仍然使用之前的亮度公式得到纹素的亮度值。然后我们减去其阈值并将其固定在0到1的范围内。然后我们将这个值和原像素相乘就提取出亮度的区域。这里我不明白为什么不是直接将大于阈值的按照原本颜色保留下来,我在网上搜索Bloom的时候还是有发现有些人就直接保留原本颜色的。这里我能想到的答案就是作者并不想得到的Bloom效果非常亮所以才进行这样的计算。不然即使你不设置阈值,除非他的亮度为1(即白色的状态下),否则所有的颜色值都会变小即变暗。

        混合期间,我们要判断一下纹理坐标的差异。我们不需要对原纹理进行判断,但是我们传递的纹理可能因为平台差异而出现问题。然后我们之间将这个两个颜色相加就好了。

运动模糊

        我理解的运动模糊就是出现残影。比如你手挥舞的快一点就可以发现这样的现象了。

        运动模糊的实现方法有很多种。书中只介绍了两种。第一种是使用一块累积缓存(accumulation buffer)来混合多张连续图像。当物体快速移动产生多张图像后,我们确它们之间的平均值作为最后的运动模糊图像。这种方法对性能的要求打,因为想要获取多张帧图像往往意味着我们需要再同一帧里渲染多次场景。另一种是速度缓存(velocity buffer),这个缓冲存储各各像素当前的运动速度,然后利用该值来决定模糊的方向和大小。书中这个章节里只说明了第一种的实现,而第二种的实现则需要其他的知识点。所以这里我们也只做第一种。而且书中实现的方法也并没有像累积缓存说明的那样渲染多次,而是保存之前的渲染结果,然后不断把当前的渲染图像叠加到之前的渲染图像中,从而产生运动模糊。(我运行作者的代码,我个人感觉这个效果看起来不够好)

        类实现:

1

        blurAmount是模糊参数。其越大拖尾的效果就越明显。我们使用buffer来存储之前图像叠加的结果。在OnRenderImage中,我们要先判断buffer是否符合条件。这些条件包括是否已经创建而且屏幕比是现在的屏幕比,如果不符合条件就创建新的(不为空则删除再新建)。且这个buffer要设置成既不保存也不能再Inspector界面显示。我们使用MarkRestoreExpected函数来进行一个恢复纹理的操作,但是这个函数在最新的版本已经被Unity弃用了。具体看官网的描述RenderTexture-MarkRestoreExpected - Unity 脚本 API。然后我们使用Graphics.Blit(source, buffer, M_Material)来存储这一帧的图像并让这次图像和上次的做混合,之后这一帧就是下一帧的值。

        具体Shader实现:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
Shader "Learn/MotionBlur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BlurAmount ("Blur Amount", Float) = 1.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

CGINCLUDE
#include "UnityCG.cginc"

sampler2D _MainTex;
float4 _MainTex_TexelSize;
float _BlurAmount;

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}

fixed4 fragRGB (v2f i) : SV_Target
{
return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
}

fixed4 fragA (v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}

ENDCG

ZTest Always Cull Off ZWrite Off

Pass
{
Blend SrcAlpha OneMinusSrcAlpha
ColorMask RGB

CGPROGRAM
#pragma vertex vert
#pragma fragment fragRGB
ENDCG
}

Pass
{
Blend One Zero
ColorMask A

CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
ENDCG
}
}
FallBack Off
}

        在Shader中我们使用两个Pass进行渲染。第一个是用来融合RGB通道值,而将A通道的值设定为blurAmount。但是由于我们使用了ColorMask RGB所以A通道的值不会写入渲染纹理中。可混合会以这个A通道作为依据进行融合。第二个Pass只将A通道输出,这个是为了维护渲染纹理的透明度通道值,不让其受到混合时的透明度值影响。(我将第二个Pass关闭后,我个人感觉没有太多的区别。可能是因为这个项目的问题吧。)具体关于混合操作描述,大家可以再看一下官网上的描述:ShaderLab 命令:Blend - Unity 手册