《shader入门精要》笔记-第12章-屏幕后处理效果

屏幕后处理效果(screen post-processing effects)是游戏中实现屏幕特效的常见方法.

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

屏幕后处理,指的是在渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作,实现各种屏幕特效.
这种技术可以为游戏添加很多的艺术效果.,例如景深(Depth of Field), 运动模糊(Motion Blur)等.
因此,想要实现屏幕后处理的基础在于得到渲染后的屏幕图像,即抓取屏幕,而Unity为我们提供了这样一个方便的接口:OnRenderImage函数:

MonoBehaviour.OnRenderImage (RenderTexture src, RenderTexture dest)

当我们在脚本中声明此函数后,Unity会把当前渲染得到的图像存储在第一个参数对应的源渲染纹理中,通过函数中的一系列操作后,再把目标渲染纹理,即第二个参数对应的渲染纹理显示到屏幕上.
在OnRenderImage函数中,我们通常是利用Graphics.Blit函数来完成对渲染纹理的处理.

public static void Blit(Texture src, RenderTexture dest)
public static void Blit(Texture src, RenderTexture dest, Material mat, int pass = -1)
public static void Blit(Texture src, Material mat, int pass = -1)

其中,src对应了源纹理, 在屏幕后处理技术中,这个参数就是当前屏幕的渲染纹理或是上一步处理后得到的渲染纹理.
参数dest是目标渲染纹理,如果它的值为null就会直接将结果显示在屏幕上.
参数mat是我们使用的材质,这个材质使用的Unity Shader将会进行各种屏幕后操作,而src纹理将会被传递给Shader中名为_MainTex的纹理属性.
参数pass的默认值为-1, 表示将会依次调用Shader内的所有Pass.否则,只会调用指定索引的Pass.

在默认情况下,OnRenderImage函数会在所有的不透明和透明的Pass执行完毕后被调用,一遍对场景中所有游戏对象产生影响.但有时,我们希望在不透明Pass执行完毕后就立即调用OnRenderImage函数,从而不对透明物体产生任何影响.此时,我们可以在OnRenderImage函数前添加ImageEffectOpaque属性来实现这样的目的.(将在13.4节遇到)

因此,要在Unity中实现屏幕后处理效果,过程通常如下:

  • 首先 我们需要在摄像机中添加一个用于屏幕后处理的脚本.
    在这个脚本中,我们会实现OnRenderImage函数来获取当前屏幕的渲染纹理.
  • 然后 再调用Graphics.Blit函数使用特定的UnityShader来对当前图像进行处理,再把返回的渲染纹理显示到屏幕上.
    对于一些复杂的屏幕特效,我们可能需要多次调用Graphics.Blit函数来对上一步的输出结果进行下一步处理…

但是,在进行屏幕后处理之前,我们需要检查一系列条件是否满足,例如当前平台是否支持渲染纹理和屏幕特效,是否支持当前使用的Unity 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
using UnityEngine;
using System.Collections;

// 使在编辑器状态下也可以执行该脚本来查看效果
[ExecuteInEditMode]
[RequireComponent (typeof(Camera))]
public class PostEffectsBase : MonoBehaviour {

// 为了提前检查各种资源和条件是否满足,我们在Start函数中调用CheckResource函数
protected void CheckResources() {
bool isSupported = CheckSupport();

if (isSupported == false) {
NotSupported();
}
}

// Called in CheckResources to check support on this platform
protected bool CheckSupport() {
if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false) {
Debug.LogWarning("This platform does not support image effects or render textures.");
return false;
}

return true;
}

// Called when the platform doesn't support this effect
protected void NotSupported() {
enabled = false;
}

protected void Start() {
CheckResources();
}
// 一些屏幕特效可能需要更多的设置,例如设置一些默认值等,可以重载Start,CheckResources或CheckSupport函数.


// 由于每个品目后处理效果通常需要指定一个Shader来创建一个用于处理渲染纹理的材质,因此基类中也提供了这种方法:
// Called when need to create the material used by this effect
// CheckShaderAndCreateMaterial函数接受两个参数,第一个参数制定了该特效需要使用的Shader,第二个参数则是用于后期处理的材质.
// 该函数首先检查Shader的可用性,检查通过之后就返回一个使用了该Shader的材质,否则返回null
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;
}
}
}

在下面一节我们就会看到如何继承PostEffect.cs来创建一个简单的用于调整屏幕的亮度,饱和度和对比度的特效脚本.

调整屏幕的亮度,饱和度和对比度.

新建一个脚本,名BrightnessSaturationAndContrast.cs

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
using UnityEngine;
using System.Collections;

// 首先,继承PostEffectsBase类
public class BrightnessSaturationAndContrast : PostEffectsBase {

// 声明使用的Shaser,并据此创建相应的材质.
public Shader briSatConShader; // 指定的Shader
private Material briSatConMaterial; // 创建的材质
public Material material {
// material的get函数调用了基类的CheckShaderAndCreateMaterial函数来得到对应的材质
get {
briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, briSatConMaterial);
return briSatConMaterial;
}
}

// 调整亮度,饱和度和对比度的参数.
[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;

// 定义OnRenderImage函数来进行真正的特效处理
void OnRenderImage(RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_Brightness", brightness);
material.SetFloat("_Saturation", saturation);
material.SetFloat("_Contrast", contrast);

Graphics.Blit(src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}

每当OnrenderI函数被调用时,它会检查材质是否可用.如果可用,就把参数传递给材质,再调用Graphics.Blit进行处理;
否则,直接把原图像显示到屏幕上,不做任何处理.

之后是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

Shader "Unity Shaders Book/Chapter 12/Brightness Saturation And Contrast" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}

// 声明用于调整亮度,饱和度和对比度的属性.这些值将会由脚本传递而得
// 事实上,我们可以省略这些属性声明,因为对于屏幕特效来说,它们使用的材质都是临时创建的,我们不需要在材质面板上调整参数,而是直接从脚本你传递给UnityShader
_Brightness ("Brightness", Float) = 1
_Saturation("Saturation", Float) = 1
_Contrast("Contrast", Float) = 1
}
SubShader {
Pass {

// 屏幕后处理实际上是在场景中绘制了一个与屏幕同宽同高的四边形面片,为了防止它对其他物体产生影响,我们需要设置相关的渲染状态:
// 关闭深度写入,为了防止其挡住在其后面渲染的物体
// 这些状态设置是屏幕后处理的Shader的标配
ZTest Always Cull Off ZWrite Off

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

sampler2D _MainTex;
half _Brightness;
half _Saturation;
half _Contrast;

struct v2f {
float4 pos : SV_POSITION;
half2 uv: TEXCOORD0;
};

// 顶点着色器比较简单,只需进行必要的顶点变换并把正确的纹理传递给片元着色器
// TODO: 疑问:屏幕后渲染不是把模型都渲染到屏幕空间后再进行的么?为什么要把顶点坐标变换到剪裁空间?
// 这里的appdata_img是unity内置的结构体,他只包含了图像处理时必须的顶点坐标和纹理坐标等变量. 可以在UnityCG.cginc中找到它的声明
// 抄下来:
//struct appdata_img
//{
// float4 vertex : POSITION;
// half2 texcoord : TEXCOORD0;
// UNITY_VERTEX_INPUT_INSTANCE_ID
//};
v2f vert(appdata_img v) {
v2f o;

o.pos = UnityObjectToClipPos(v.vertex);

o.uv = v.texcoord;

return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed4 renderTex = tex2D(_MainTex, i.uv);

// Apply brightness
fixed3 finalColor = renderTex.rgb * _Brightness;

// Apply saturation
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
finalColor = lerp(luminanceColor, finalColor, _Saturation);

// Apply contrast
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);

return fixed4(finalColor, renderTex.a);
}

ENDCG
}
}

Fallback Off
}

边缘检测

边缘检测的原理是利用一些边缘检测算子对图像进行卷积(convolution)操作.

什么是卷积

在图像处理中,卷积指的是使用一个卷积核(kernel)对一张图片中的每个像素进行一些列操作.
卷积核通常是一个四方形网格结构,该区域的每个方格都有一个权重值.当对图像中的某个像素进行卷积时,我们会把卷积核的中心置于该像素上.
如图所示,翻转核之后再以此计算何种每个元素和其覆盖的图像的像素值的乘积并求和,得到的结果就是该位置的新像素值.
20190327154039.png
这样的操作虽然简单,但可以实现很多常见的图像处理效果,如图像模糊,边缘检测等.
例如,如果我们想多图像进行均值模糊,可以使用一个3x3的卷积核,核内每个元素的值均为1/9.

常见的边缘检测算子

如果相邻像素之间存在差别明显的颜色,亮度,纹理等属性,我们会认为它们之间应该有一条边界.
这种相邻像素之间的插值可以用梯度(gradient)来表示,可以想象得到,边缘处的梯度绝对值会比较大.
基于这样的理解,有几种不同的边缘检测算子(即用于边缘检测的卷积核)被先后提出来.
卷积操作的神奇之处在于卷积核.
20190327154710.png
它们都包含两个方向的卷积核,分别用于检测水平方向和竖直方向上的边缘信息.
在进行边缘检测时,我们需要对每个像素分别进行一次卷积计算,得到两个方向上的梯度值G(x)和G(y),而整体的梯度可按下面的公式计算而得:

G = sqrt(G(x)**2 + G(y)**2)

由于开根号操作比较复杂,出于性能考虑,我们有时候会使用绝对值操作来代替开根号操作:

G = |G(x)| + |G(y)|

得到梯度G后,我们就可以据此来判断哪些像素对应了边缘:梯度越大,越有可能是边缘.

实践: 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
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
105
106
Shader "Unity Shaders Book/Chapter 12/Edge Detection" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_EdgeOnly ("Edge Only", Float) = 1.0
_EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
ZTest Always Cull Off ZWrite Off

CGPROGRAM

#include "UnityCG.cginc"

#pragma vertex vert
#pragma fragment frag

sampler2D _MainTex;

// XXX_TexelSize是Unity为我们提供的访问XXX纹理对应的每个纹素的大小.
// 由于卷积需要对相邻区域内的纹理进行采样,因此我们需要利用_MainTex_TexSize来计算各个相邻区域的纹理坐标.
uniform half4 _MainTex_TexelSize;

fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;

struct v2f {
float4 pos : SV_POSITION;

// 位数为9的纹理数组,对应的是使用Sobel算子采样是需要的9个邻域纹理坐标
half2 uv[9] : TEXCOORD0;
};

v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

half2 uv = v.texcoord;

// _MainTex_TexelSize.xy和移动量相乘之后,才得到真正的坐标(大概这个意思)
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = 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;
}

// 使用Sobel算子对原图进行边缘检测的函数
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 it = 0; it < 9; it++) {

// 采样之后计算亮度值
texColor = luminance(tex2D(_MainTex, i.uv[it]));

// 亮度值与卷积核对应的权重相乘,叠加至各自的梯度值上
edgeX += texColor * Gx[it];
edgeY += texColor * Gy[it];
}

// edge越小,梯度越大,该点越可能是边缘
half edge = 1 - abs(edgeX) - abs(edgeY);

return 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, _EdgeOnly);
}

ENDCG
}
}
FallBack Off
}

需要注意的是,本例中的边缘检测仅仅利用了屏幕的颜色信息,而实际应用中,物体的纹理,阴影等信息均会影响边缘检测的结果,使得结果包含许多非预期的描边.
为了得到更准确的边缘嘻嘻,我们往往会在屏幕的深度纹理和法线纹理上进行边缘检测.(将会在13章实现)

高斯模糊

模糊的实现有许多种方法,例如均值模糊和中值模糊.

均值模糊同样使用了卷积操作,它只用的卷积核的各个元素值都相等,且相加等于1.
也就是说,卷积后得到的像素值是其邻域中的各个像素的平均值.

而中值模糊是选择邻域内对应所有像素排序后的中值替换掉原有的颜色.

一个更高级的模糊方法是高斯模糊.

高斯滤波

高斯模糊同样使用了卷积计算,它使用的卷积核名为高斯核.
高斯核是一个正方形大小的滤波核,其中每个元素的计算都是基于下面的高斯方程:
20190327165209.png
其中,打不出来的那个是标准方差(一般取值为1),x,y分别对应了当前位置到卷积核中心的整数距离.
要构建一个高斯核,我们只需要计算高斯核中各个位置对应的高斯值.
为了保证滤波后的图像不会变暗,我们需要对高斯核中的权重进行归一化,即让每个权重除以所有权重的和,这样可以保证所有权重的和为1.
因此,高斯函数中e前面的系数实际不会对结果有任何影响.

如图显示的是一个标准方差为1的5x5大小的高斯核
20190327165805.png
高斯方程很好的模拟了邻域每个像素对当前处理像素的影响程度–距离越近,影响越大.
高斯核维度越高,模糊程度越大.采用一个NxN的高斯核对图像进行卷积滤波,就需要NxNxWxH(W和H分别是图像的宽和高)次纹理采样.当N的大小不断增加时,采样的次数会变得非常巨大.
幸运的是,我们可以吧这个二维高斯函数拆分成两个一维函数.也就是说,我们可以使用两个一维的高斯核进行滤波,它们得到的结果跟直接使用二维高斯核进行滤波是一样的,但采样次数只需要2xNxWxH.
我们可以观察到,两个一维高斯核中包含很多重复的权重,实际上我们只需要计算三个权重即可.

实践: 高斯模糊

我们使用一个5x5的高斯核对原图进行高斯模糊.
我们将先后调用两个Pass,第一个Pass将会使用竖直方向的一维高斯核对图像进行滤波,第二个Pass再使用水平方向的一维高斯核对图像进行滤波,得到最终图像.
我们还将利用图像缩放来进一步提高性能,并通过调整高斯滤波的次数来控制模糊程度(次数越多,图像越模糊)
绑定在摄像头的C#代码:

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
using UnityEngine;
using System.Collections;

public class GaussianBlur : PostEffectsBase {

public Shader gaussianBlurShader;
private Material gaussianBlurMaterial = null;

public Material material {
get {
gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader, gaussianBlurMaterial);
return gaussianBlurMaterial;
}
}

// 迭代次数,次数越多,模糊程度越高
[Range(0, 4)]
public int iterations = 3;

// 模糊范围和缩放系数
// 这两项是为性能考虑的
// 模糊范围越大,模糊程度越高,但采样数不会受到影响.但过大的模糊范围可能会造成虚影.
// 缩放系数越大,需要处理的像素数越少,同时也进一步提高模糊程度,但过大可能会是图像像素化.
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f;

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

void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {

//先利用缩放对图像进行降采样
int rtW = src.width/downSample;
int rtH = src.height/downSample;

// 与之前不同,这里使用了RenderTexture.GetTemporary函数分配了一块缓冲区.
// 这是因为,高斯模糊需要调用两个Pass,我们需要使用一块中间缓存来存储第一个Pass执行完毕后得到的模糊结果.
// 缓冲区的宽高为缩放后的宽高
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);

buffer0.filterMode = FilterMode.Bilinear;

Graphics.Blit(src, buffer0);

// 迭代多次
for (int i = 0; i < iterations; i++) {

material.SetFloat("_BlurSize", 1.0f + i * blurSpread);

RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

// 第一个Pass
Graphics.Blit(buffer0, buffer1, material, 0);

RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

// 第二个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代码:

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
Shader "Unity Shaders Book/Chapter 12/Gaussian Blur" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {

// CGINCLUDE和ENDCG定义的代码不需要包含在任何Pass中,在使用时,只需在Pass中指定需要的顶点和片元着色器即可.
// 由于高斯模糊需要定义两个Pass,但他们使用的片元着色器的代码是完全相同的,使用CGINCLUDE可以避免我们编写两个完全一样的frag函数
CGINCLUDE

#include "UnityCG.cginc"

sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;

struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;
};

// 竖直方向上的顶点着色器代码
v2f vertBlurVertical(appdata_img v) {

v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

half2 uv = v.texcoord;

o.uv[0] = uv;

// 和_BlurSize相乘控制控制采样距离.
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 vertBlurHorizontal(appdata_img v) {

v2f o;
o.pos = 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 {

// 我们只需记录3个高斯权重
float weight[3] = {0.4026, 0.2442, 0.0545};

fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];

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);
}

ENDCG

ZTest Always Cull Off ZWrite Off

Pass {

// 为Pass定义名字,可以在其他Shader中直接通过它们的名字来使用该Shader,而不用编写重复的代码
NAME "GAUSSIAN_BLUR_VERTICAL"

CGPROGRAM

#pragma vertex vertBlurVertical
#pragma fragment fragBlur

ENDCG
}

Pass {
NAME "GAUSSIAN_BLUR_HORIZONTAL"

CGPROGRAM

#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur

ENDCG
}
}
FallBack "Diffuse"
}

Bloom效果

Bloom是游戏中常见的一种屏幕效果,这种特效可以模拟真实摄像机的一种图像效果,它让画面中较亮的区域"扩散"到周围的区域,造成一种朦胧的感觉.

20190328091452.png 20190328091356.png

实现原理: 我们首先根据一个阈值提取出图像中较亮的区域,把他们存储在一张纹理中,再利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果,最后再将其和原图像进行混合,得到最终效果.