后处理

Blit

Unity 为我们提供的函数是:

1
MonoBehaviour.OnRenderImage (RenderTexture src, RenderTexture dest)

在 OnRenderImage 函数中,我们通常是利用 Graphics.Blit 函数来完成对渲染纹理的处理。

1
2
3
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 是我们使用的材质,而 src 纹理会被传递
材质上 Shader 中名为 _MainText 的纹理属性。
参数 pass 的默认值为 -1,表示将依次调用 Shader 内的所有 Pass。否则只会调用给定索引的 Pass。

调整光的 明度、饱和度、对比度

注意区分明度和亮度的区别,物理学上的亮度和色彩学里面的亮度不是同一种含义,这里用明度来区分更好一点。
核心 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
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unlit/BrightnessSaturation"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Brightness ("BrightNess", Float) = 1
_Saturation ("Saturation", Float) = 1
_Contrast ("Contrast", Float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog

#include "UnityCG.cginc"

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

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

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

fixed3 finalColor = renderTex.rgb * _Brightness;

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

fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);
return fixed4(finalColor, renderTex.a);
}
ENDCG
}
}
}

明度调整的逻辑,用原来的颜色直接乘以一个明度系数。

这里正好可以再说一下光的亮度单位,流明、尼特等。
当描述点状光源的时候,用流明单位。
当描述单位面积的光源亮度时,用尼特单位。

饱和度调整的逻辑:

  • 先计算对应像素的明度值,通过对每个颜色分量乘以一个特定的系数,然后用该明度值创建一个饱和度为 0 的颜色值。
  • 用 _Saturation 属性在该像素上一步得到的颜色进行插值,从而得到希望的饱和度颜色。

对比度调整的逻辑:

  • 首先创建一个对比度为 0 的颜色(各分量均为 0.5)
  • 再使用 _Contrast 属性在其和上一步得到的颜色插值,从而得到最终的结果

边缘检测

首先理解卷积的概念。
不过在图像处理中,卷积操作具体就是指用一个卷积核对一张图像中的每个像素进行一系列操作。
当对图像中的某个像素进行卷积时,我们会把卷积核的中心放置于该像素上。翻转核之后再一次计算每个元素和其覆盖的图像像素值的乘积并求和。

常见的三种边缘检测算子。

Sobel 算子

1
2
3
4
5
6
7
8
9
10
11
12
13
int[] Sobel_X = 
{
-1, -2, -1,
0, 0, 0,
1, 2, 1,
};

int[] Sobel_Y =
{
-1, 0, 1,
-2, 0, 2,
-1, 0, 1,
};

Roberts 算子

1
2
3
4
5
6
7
8
9
10
11
12
int[] Roberts_X = 
{
-1, 0,
0, 1,
};

int[] Roberts_Y =
{
-1, 0,
0, 1,
};

Prewitt 算子

1
2
3
4
5
6
7
8
9
10
11
12
13
int[] Prewitt_X = 
{
-1, -1, -1,
0, 0, 0,
1, 1, 1,
};

int[] Prewitt_Y =
{
-1, 0, 1,
-1, 0, 1,
-1, 0, 1,
};

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
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unlit/EdgeDetection"
{
Properties
{
_MainTex ("Texture", 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
#pragma vertex vert
#pragma fragment fragSobel
#include "UnityCG.cginc"

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

sampler2D _MainTex;
uniform half4 _MainTex_TexelSize;
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;

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

half2 uv = v.texcoord;

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 luminace(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 it = 0; it < 9; it++)
{
texColor = luminace(tex2D(_MainTex, i.uv[it]));
edgeX += texColor * Gx[it];
edgeY += texColor * Gy[it];
}

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

fixed4 fragSobel(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
}
}
}

在上面的代码中有一个内置的 Unity 提供的变量, _MainTex_TexelSize。_xxx_TexelSize 表示当前访问纹理 _xxx 对应纹素的大小。

它的值为 Vector4(1 / width, 1 / height, width, height),比如最终屏幕输出是1k x 2k 那么:

_MainTex_TexelSize.x = 1/1024

_MainTex_TexelSize.y = 1/2048

下面的这段代码表示计算该点正上方的一个像素的坐标

  • uv 的取值范围是 [0,1]
  • _MainTex_TexelSize.xy 的取值范围也是 [0,1]
1
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);

在上面的 Shader 中,计算 Sobel 算子采样时需要的 9 个邻域纹理坐标的代码在顶点着色器中,而没有放到片元着色器中,这样做的好处可
以提高性能,并且由于从顶点到片元的插值是线性的,这样做也不会影响最终结果。

高斯模糊

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
Shader "GaussianBlur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
half2 uv[5] : TEXCOORD0;
};

sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;

v2f vertBlurVertical (appdata_img v)
{
v2f o;
o.pos = 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 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
{
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]).rgb * weight[it];
sum += tex2D(_MainTex, i.uv[2 * it]).rgb * weight[it];
}

return fixed4(sum,1.0);
}

ENDCG

ZTest Always Cull Off ZWrite Off

Pass
{
Name "GAUSSIAN_BLUR_VERTIVAL"

CGPROGRAM

#pragma vertex vertBlurVertical
#pragma fragment fragBlur

ENDCG
}

Pass
{
Name "GAUSSIAN_BLUR_HORIZONTAL"

CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
Fallback Off
}

逻辑和边缘检测很类似,不过不同的是,这里分别用了两个 Pass 去处理,这样可以节省很多性能。
但是在 Urp 中只能用一个 Pass 的情况下就需要在 CPU 端准备好数据,然后传给 GPU 性能就比较差。
但是可以用 Compute Shader 优化这种方式,因为 Compute Shader 是分线程并行运算,并且有 GroupSharedMemory ,会更节省性能。

Bloom 效果

Bloom 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
Shader "Unlit/Bloom"
{
Properties
{
_MainTex ("Base (RGB)", 2D) = "white" {}
_Bloom ("Bloom (RGB)", 2D) = "black" {}
_LuminanceThreshold ("Luminance Threshhold", Float) = 0.5
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader
{
CGINCLUDE

#include "UnityCG.cginc"

sampler2D _MainTex;
half4 _MainTex_TextSize;
sampler2D _Bloom;
float _LuminanceThreshold;
float _BlurSize;


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

v2f vertExtractBright(appdata_img v){
v2f o;
o.pos = 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 fragExtractBright(v2f i) : SV_Target{
fixed4 c = tex2D(_MainTex, i.uv);
fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0);
return c * val;
}

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;
o.uv.zw = v.texcoord;

#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TextSize.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
{
CGPROGRAM
#pragma vertex vertExtractBright
#pragma fragment fragExtractBright
ENDCG
}

UsePass "GaussianBlur/GAUSSIAN_BLUR_VERTIVAL"
UsePass "GaussianBlur/GAUSSIAN_BLUR_HORIZONTAL"

Pass{

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