Unity-Shader读书笔记(12)

        在游戏开发过程中,我们会需要获取到深度纹理和法线纹理来实现特定的屏幕后处理效果。

深度和法线纹理的原理

        深度纹理就是一张渲染纹理,但是它存储的是一个高精度的深度值。因为深度值被存储在纹理中,所以深度值的大小范围为[0,1],且为非线性分布。在我们进行投影变换后得到的NDC(就是变成正方体空间那个)。那么它的z轴分量就是在[-1,1]之间(像DirectX这些则是在[0,1]之间)。所以我们要通过类似之前法线纹理一样将z分量控制[0,1]之间。

        在Unity中获取深度纹理有两种方式:一种是直接取深度缓存,另一种则是由一个单独的Pass进行渲染得到。这个取决于使用的渲染路径和硬件。通常来说,如果我们使用延迟渲染路径,它会将深度信息存储在G-Buffer中,所以我们能直接获取。如果我们要使用Pass进行获取,那么我们务必要给这个Pass设定正确的渲染路径。Unity会使用着色器替换技术选择那些渲染类型(即SubShader的RenderType标签)为Opaque的物体,判断它们使用的渲染队列是否小于等于2500(内置的Background、Geometry和AlphaTest渲染队列均在此范围内),如果满足条件,就把它渲染到深度和法线纹理中。

        我们仍然要选择一个摄像机去生成深度和法线纹理。如果我们只是要深度纹理,那么Unity要么直接获取要么用Pass渲染得到获取。如果我们要深度+法线纹理(即深度信息和法线信息被存储在一个纹理中),Unity会创建一张和屏幕分辨率相同的纹理。法线信息会被安排在R通道和G通道,深度信息则在B通道和A通道中。法线信息在延迟渲染中可以很容易的得到,Unity只需要融合深度和法线缓存。而在前向渲染中,Unity底层会使用一个单独的Pass来渲染得到这些信息。

        我还找到了一篇文章详细的说明了深度纹理,可以看看:Unity3D Shader系列之深度纹理_unity 深度纹理_WangShade的博客-CSDN博客

如何在Unity中获取到深度纹理和法线纹理

        在Unity中获取深度纹理是十分简单的。我们可以用以下的脚本代码来获取到:

1
Camera.main.depthTextureMode = DepthTextureMode.Depth;

(这里我偷懒就用Camera.main,具体是哪个摄像机看项目而定)然后再shader中声明_CameraDepthTexture来访问。

        如果要获取深度+法线纹理,代码如下:

1
Camera.main.depthTextureMode = DepthTextureMode.DepthNormals;

然后再shader中声明_CameraDepthNormalTexture来访问它。

        我们还可以使用下面的代码来让Unity同时生成一张深度和深度+法线的纹理(这里我就不明白为什么要这么做了,明明深度信息都得到了但是还要多弄一份)

1
2
Camera.main.depthTextureMode |= DepthTextureMode.Depth;
Camera.main.depthTextureMode |= DepthTextureMode.DepthNormals;

        对于_CameraDepthTexture,Unity针对平台差异提供了SAMPLE_DEPTH_TEXTURE来进行采样。而通过纹理采样得到的深度值往往是非线性的,这种非线性来自透视投影用的裁剪矩阵。然而在我们计算过程中通常是需要线性的深度值,也就是说我们要将其变换到线性空间下。Unity提供了两个辅助函数来方便我们LinearEyeDepth和Linear01Depth(具体原理大家可以去搜一下)。LinearEyeDepth负责把深度纹理的采样结果转换到视角空间下的深度值。而Linear01Depth是将深度值范围在[0.1]的线性深度值。

        而对于_CameraDepthNormalTexture这深度+法线的纹理,Unity也提供了辅助函数DecodeDepthNormal来进行采样。我们通过DecodeDepthNormal进行采样后的深度值是在[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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

[Range(0,1f)]
public float blurSize = 0.5f;

// 把你自己要的摄像机拖入其中
public Camera myCamera;

private Matrix4x4 previousViewProjectionMatrix;

private void OnEnable()
{
// 这里做一个预防
if (myCamera == null)
{
myCamera = Camera.main;
}

myCamera.depthTextureMode |= DepthTextureMode.Depth;
}

private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (M_Material != null)
{
M_Material.SetFloat("_BlurSize", blurSize);
M_Material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix);
Matrix4x4 currentViewProjectionMatrix = myCamera.projectionMatrix * myCamera.worldToCameraMatrix;
Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;
M_Material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);
previousViewProjectionMatrix = currentViewProjectionMatrix;

Graphics.Blit(source, destination, M_Material);
}
else
Graphics.Blit(source, destination);
}
}
}

        这段代码中我们使用blurSize来控制运动模糊时模糊图像的大小。而我们还需要传递当前的视角变换逆矩阵和之前的视角变换矩阵。你会在camera中看到类似变量Camera-previousViewProjectionMatrix - Unity 脚本 API,但是这个并不是和我们内容一样。只是名称一样。所以当前的视角变换矩阵就需要我们自行计算,并在使用后存储起来用作下次当前一帧的矩阵。接下来就是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
Shader "Learn/MotionBlurWithDepth"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BlurSize ("BlurSize",Float) = 1.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

CGINCLUDE
#include "UnityCG.cginc"

sampler2D _CameraDepthTexture;
sampler2D _MainTex;
float4 _MainTex_TexelSize;
float _BlurSize;
float4x4 _PreviousViewProjectionMatrix;
float4x4 _CurrentViewProjectionInverseMatrix;

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

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

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.uv_depth = v.uv;
#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0)
o.uv_depth.y = 1 - o.uv_depth.y;
#endif
return o;
}

fixed4 frag(v2f i) : SV_Target
{
// 获取每个像素对应的深度缓冲值
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv_depth);
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1,d * 2 - 1, 1);
float4 D = mul(_CurrentViewProjectionInverseMatrix,H);
float4 worldPos = D / D.w;

float4 currentPos = H;
float4 prePos = mul(_PreviousViewProjectionMatrix,worldPos);
prePos = prePos / prePos.w;
float2 velocity = (currentPos.xy - prePos.xy) / 2;

float2 uv = i.uv;
float4 c = tex2D(_MainTex,uv);
uv += velocity * _BlurSize;
for(int j = 1;j < 3; j++,uv += velocity * _BlurSize)
{
float4 curColor = tex2D(_MainTex,uv);
c += curColor;
}
c /= 3;
return fixed4(c.rgb, 1);
}

ENDCG

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

        在顶点函数中,我们进行了一个纹理的特殊处理(这个纹理是由Unity传给我们的,不同的平台下存在差异。这个操作和之前的Bloom操作传递纹理一样。)

        片元函数中,我们先使用SAMPLE_DEPTH_TEXTURE获取到视角空间下的深度值。然后用这个和uv坐标算出这个像素在视角空间下的坐标。首先我们要知道的uv坐标的范围是在[0,1]之间,但是在NDC中xyz的分量范围在[-1,1]之间。所以我们要将这个uv坐标映射到这个范围内。同理深度值也是一样。所以才有这段代码取得NDC下的坐标:

1
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1,d * 2 - 1, 1);

然后我们通过矩阵变换将其变换到世界坐标下,因为对于点来说w分量必须为1。所以我们要将得到的结果整体除于其w分量。我们得到这个世界坐标再使用之前的变换矩阵再将其变换到视角坐标中。同理我们仍要处理其w值。之后我们使用之前和现在在视角坐标中的xy的差值除2作为速度(这个速度计算的公式,书中并没有明确说为什么是这样。那么这个我认为应该是按照项目而定)。接下来我们先取得现在的颜色值,然后依照速度的方向并乘以我们设定的模糊大小来选取像素值。选取的数量为什么是3(包括其当前的纹素值),书中也没有说明。所以这个你也可以按照你项目自行决定。但是最后取平均值(或者你按照你的方法对你选取的像素进行权值处理)。

全局雾效

        雾效(fog)是游戏里常用效果,Unity内置的雾效可以产生基于距离的线性或指数雾效。如果要实现这个效果,我们需要在Shader中添加#pragma multi_compile_fog指令,同时还需要Shader中使用相关的内置宏UNITY_FOG_COORDS、UNITY_TRANSFER_FOG、UNITY_APPLY_FOG等。在我现在的2019.4.40f1版本中,直接创建Unlit Shader就会存在这个几个变量。这种方法的缺点在于,我们需要为场景中的每一个物体添加相关的代码。而且实现的效果也有限。

        书中雾效是依靠深度纹理来重建每个像素在世界空间下的位置。虽然运动模糊那边已经使用过这样的方法了,但是这里是另一种实现。首先我们对图像空间下的视椎体射线(从摄像机出发,指向图像上的某点的射线)进行插值,这条射线存储了该像素在世界空间下到摄像机的方向信息。然后我们把该射线和线性化后的视角空间下的深度值相乘,再加上摄像机的世界位置,就可以得到该像素在世界空间下的位置。当我们得到世界坐标后,我们就可以使用各个公式来模拟全局雾效。

如何从深度纹理中重建世界坐标

        坐标系中的一个顶点坐标可以通过它相对于另一个顶点坐标的偏移量得到。重建像素的世界坐标也是这样的思想。我们只需要知道摄像机在世界坐标下的位置,以及世界空间该像素相对于摄像机的偏移量,把它们相加就可以得到该像素的世界坐标。具体原理书中东西太多,且公式写起来复杂(就是我懒而已)。所以我找了一篇文章大家可以看看Unity3D Shader系列之深度纹理重建世界坐标_深度 重建世界坐标_WangShade的博客-CSDN博客

雾效计算

        在简单的雾效实现中,我们需要计算一个雾效系数f,作为混合原始颜色和雾的颜色的混合系数。这个雾效系数f有很多计算方法。在Unity内置的雾效实现中,支持三种雾的计算方式——线性(Linear)、指数(Exponential)以及指数的平方(Exponential Squared)。当给定距离z后,f的计算公式如下:

线性:

1
f = (Dmax - |z|) / (Dmax - Dmin) 

Dmax和Dmin分别表示受雾影响的最大和最小距离。

指数:

1
f = e^(-d|z|)

d是控制雾的浓度的参数。

        而书中实现的方法是基于高度雾效。具体方法是当给定一点在世界空间下的高度y后,f的计算公式为:

1
f = (Hend - y) / (Hend - Hstart)

Hend和Hstart分别表示受雾影响的起始高度和终止高度。

实现

        脚本代码:

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

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

[Range(0, 3f)]
public float fogDensity = 0.5f;

public Color fogColor = Color.white;

public float fogStart = 0.0f;
public float fogEnd = 2.0f;

// 把你自己要的摄像机拖入其中
public Camera myCamera;

private void OnEnable()
{
// 这里做一个预防
if (myCamera == null)
{
myCamera = Camera.main;
}

myCamera.depthTextureMode |= DepthTextureMode.Depth;
}

private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (M_Material != null)
{
Matrix4x4 frustumCorners = Matrix4x4.identity;

float fov = myCamera.fieldOfView;
float near = myCamera.nearClipPlane;
float aspect = myCamera.aspect;

float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
Vector3 toRight = myCamera.transform.right * halfHeight * aspect;
Vector3 toTop = myCamera.transform.up * halfHeight;

Vector3 topLeft = myCamera.transform.forward * near + toTop - toRight;
float scale = topLeft.magnitude / near;

topLeft.Normalize();
topLeft *= scale;

Vector3 topRight = myCamera.transform.forward * near + toTop + toRight;
topRight.Normalize();
topRight *= scale;

Vector3 bottomLeft = myCamera.transform.forward * near - toTop - toRight;
bottomLeft.Normalize();
bottomLeft *= scale;

Vector3 bottomRight = myCamera.transform.forward * near - toTop + toRight;
bottomRight.Normalize();
bottomRight *= scale;

frustumCorners.SetRow(0, bottomLeft);
frustumCorners.SetRow(1, bottomRight);
frustumCorners.SetRow(2, topRight);
frustumCorners.SetRow(3, topLeft);

m_Material.SetMatrix("_FrustumCornersRay", frustumCorners);

m_Material.SetFloat("_FogDensity", fogDensity);
m_Material.SetColor("_FogColor", fogColor);
m_Material.SetFloat("_FogStart", fogStart);
m_Material.SetFloat("_FogEnd", fogEnd);

Graphics.Blit(source, destination, m_Material);
}
else
Graphics.Blit(source, destination);
}
}
}

        fogDensity用于控制雾的浓度,fogColor控制雾的颜色。我们使用的模拟函数是基于高度的,所以我们使用后fogStart控制雾的起始高度,fogEnd控制雾的终止高度。我们仍需要深度纹理,所以在OnEnable中我们要设置摄像头深度纹理模式。在OnRenderImage中,我们先计算出四个角对应的方向值并将其存储到frustumCorners不同行。这个顺序是非常重要的,因为这决定了我们在顶点着色器中使用哪一行作为改点的特定插值向量(其实顺序可以自己来写,只要后面和Shader通信的时候,你能知道那个下标对应的是哪个区域就可以了)。

        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
Shader "Learn/FogWithDepthTexture"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_FogDensity ("FogDensity",Float) = 1.0
_FogColor ("FogColor",Color) = (1,1,1,1)
_FogStart ("FogStart",Float) = 0.0
_FogEnd ("FogEnd",Float) = 1.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
CGINCLUDE


#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_TexelSize;
sampler2D _CameraDepthTexture;
float4 _FogColor;
float4x4 _FrustumCornersRay;
float _FogDensity;
float _FogStart;
float _FogEnd;

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

struct v2f
{
float2 uv : TEXCOORD0;
float2 uv_depth : TEXCOORD1;
float4 vertex : SV_POSITION;
float4 interpolatedRay : TEXCOORD2;
};

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

#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0)
o.uv_depth.y = 1 - o.uv_depth.y;
#endif

int index = 0;
if(v.uv.x < 0.5 && v.uv.y < 0.5)
index = 0;
else if(v.uv.x > 0.5 && v.uv.y < 0.5)
index = 1;
else if(v.uv.x > 0.5 && v.uv.y > 0.5)
index = 2;
else
index = 3;

#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0)
index = 3 - index;
#endif

o.interpolatedRay = _FrustumCornersRay[index];

return o;
}

fixed4 frag(v2f i) : SV_Target
{
// 获取每个像素对应的深度缓冲值
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv_depth);
float linearDepth = LinearEyeDepth(d);
float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;
float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
fogDensity = saturate(fogDensity * _FogDensity);

fixed4 finalColor = tex2D(_MainTex,i.uv);
finalColor.rgb = lerp(finalColor.rgb,_FogColor,fogDensity);
return fixed4(finalColor.rgb, 1);
}
ENDCG

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

        在顶点函数中,我们使用uv坐标来判断出这个纹素处于摄像机四个角的哪个区域并记录在index。随后我们按照之前划分得到对应的视角射线。在片元函数中,我们使用Unity的函数得到线性空间下的深度值。根据之前的公式,我们就可以得出该像素对应的世界坐标。接下来我们通过我们之前的雾模拟函数得出fogDensity并将其限制在[0,1]之间。然后我们使用插值进行计算来模拟雾效果。

边缘检测

        之前我们使用算子来进行边缘检测来实现描边效果。但是,这种直接利用颜色信息进行边缘检测的方法会产生很多我们不希望得到的边缘线。而这次我们将使用深度+法线纹理并用Roberts算子来进行边缘检测(关于Roberts算子,可以看之前的文章里提到的说明)。

        首先是脚本实现:

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

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

[Range(0, 1f)]
public float edgesOnly = 0.0f;
public float sampleDistance = 1.0f;
public float sensitivityDepth = 1.0f;
public float sensitivityNormals = 1.0f;

public Color edgeColor = Color.black;
public Color backgroundColor = Color.white;

private void OnEnable()
{
Camera.main.depthTextureMode |= DepthTextureMode.DepthNormals;
}

[ImageEffectOpaque]
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (M_Material != null)
{
m_Material.SetFloat("_EdgesOnly", edgesOnly);
m_Material.SetFloat("_SampleDistance", sampleDistance);

m_Material.SetColor("_EdgeColor", edgeColor);
m_Material.SetColor("_BackgroundColor", backgroundColor);

m_Material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0, 0));

Graphics.Blit(source, destination, m_Material);
}
else
Graphics.Blit(source, destination);
}
}
}

        edgesOnly是显示背景颜色的程度值,这个和之前的边缘检测一样。如果edgesOnly为1那么就只会显示边缘线和背景颜色。edgeColor控制描边的颜色,backgoundColor是控制描边外的背景的颜色值。sampleDistance用于控制深度+法线纹理采样时,使用的采样距离。从视觉上来看,sampleDistance值越大,描边越宽。sensitivityDepth和sensitivityNormal将会影响当领域的深度值或法线值相差多少时,会被认为存在一条边界。如果把灵敏度调得很大,那么可能即使是深度或法线上很小的变化也会形成一条边。

        这次我们要得到的是深度+法线纹理所以我们要将摄像机的depthTextureMode设置为DepthTextureMode.DepthNormals来获取深度+法线纹理。书中作者说只想对不透明物体描边所以才使用了ImageEffectOpaque。

        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/EdgeDetectNormalsAndDepth"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_EdgesOnly ("EdgesOnly", Float) = 1.0
_SampleDistance ("SampleDistance", Float) = 1.0
_EdgeColor ("EdgeColor", Color) = (1,1,1,1)
_BackgroundColor ("BackgroundColor", Color) = (1,1,1,1)
_Sensitivity ("Sensitivity",Vector) = (1,1,1,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;
float4 _MainTex_TexelSize;
sampler2D _CameraDepthNormalsTexture;

float _EdgesOnly;
float _SampleDistance;
float4 _EdgeColor;
float4 _BackgroundColor;
half4 _Sensitivity;

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv[0] = v.uv;
float2 uv = v.uv;
#if UNITY_UV_STARTS_AT_TOP
if(_MainTex_TexelSize.y < 0)
uv.y = 1 - uv.y;
#endif
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1,1) * _SampleDistance;
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1,-1) * _SampleDistance;
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1,1) * _SampleDistance;
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(1,-1) * _SampleDistance;

return o;
}

half CheckSame(float4 center, float4 sample)
{
float2 centerNormal = center.xy;
float centerDepth = DecodeFloatRG(center.zw);
float2 samplerNormal = sample.xy;
float samplerDepth = DecodeFloatRG(sample.zw);

float2 diffNormal = abs(centerNormal - samplerNormal) * _Sensitivity.x;
int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;

float diffDepth = abs(centerDepth - samplerDepth) * _Sensitivity.y;
int isSameDepth = diffDepth < 0.1 * centerDepth;

return isSameNormal * isSameDepth ? 1.0:0;
}

fixed4 frag (v2f i) : SV_Target
{
float4 sample1 = tex2D(_CameraDepthNormalsTexture,i.uv[1]);
float4 sample2 = tex2D(_CameraDepthNormalsTexture,i.uv[2]);
float4 sample3 = tex2D(_CameraDepthNormalsTexture,i.uv[3]);
float4 sample4 = tex2D(_CameraDepthNormalsTexture,i.uv[4]);

half edge = 1.0;

edge *= CheckSame(sample1, sample2);
edge *= CheckSame(sample3, sample4);

fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge);
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);

return lerp(withEdgeColor, onlyEdgeColor, _EdgesOnly);
}
ENDCG

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

        因为我们要传递的是深度+法线纹理。所以我们使用的纹理名称为_CameraDepthNormalsTexture。当然我们仍然要进行差异化处理。在顶点函数中,我们使用4个坐标存储Roberts算子时需要采样的纹理坐标,我们还使用了_SampleDistance来控制采样距离。

        在片元函数中,我们首先使用4个纹理坐标对深度+法线进行采样,再调用CheckSame函数来分别计算对角线上两个纹理值的差值。CheckSame返回值要么是1,要么是0。返回0时便表示这个两点之间存在一条边界,防止返回1。具体CheckSame内部的实现还是进行法线和深度值的比较。书中也是介绍了过程而没有介绍原因,大家可以根据自己项目自行调整计算方式。剩下的操作就和之前的边缘检测操作一样了。