在第12 章中,我们学习的屏幕后处理效果都只是在屏幕颜色图像上进行各种操作来实现的。然而,很多时候我们不仅需要当前屏幕的颜色信息,还希望得到深度和法线信息。例如,在进行边缘检测时,直接利用颜色信息会使检测到的边缘信息受物体纹理和光照等外部因素的影响,得到很多我们不需要的边缘点。一种更好的方法是,我们可以在深度纹理和法线纹理上进行边缘检测,这些图像不会受纹理和光照的影响,而仅仅保存了当前渲染物体的模型信息,通过这样的方式检测出来的边缘更加可靠。
在本章中,我们将学习如何在Unity 中获取深度纹理和法线纹理来实现特定的屏幕后处理效果。
在13.1 节中,我们首先会学习如何在Unity 中获取这两种纹理。
在13.2 节中,我们会利用深度纹理来计算摄像机的移动速度,实现摄像机的运动模糊效果。
在13.3 节中,我们会学习如何利用深度纹理来重建屏幕像素在世界空间中的位置,从而模拟屏幕雾效。
13.4 节会再次学习边缘检测的另一种实现,即利用深度和法线纹理进行边缘检测。
camera.depthTextureMode = DepthTextureMode.Depth;
一旦设置好了上面的摄像机模式后,我们就可以在Shader 中通过声明 _CameraDepthTexture变量来访问它。这个过程非常简单,但我们需要知道这两行代码的背后, Unity 为我们做了许多工作(见13.1.1 节〉。
camera.depthTextureMode = DepthTextureMode.DepthNormals;
然后在Shader 中通过声明 _CameraDepthNormalsTexture 变量来访问它。
在Unity 5 中,我们还可以在摄像机的Camera 组件上看到当前摄像机是否需要渲染深度或深度+法线纹理。当在Shader 中访问到深度纹理 _CameraDepthTexture 后,我们就可以使用当前像素的纹理坐标对它进行采样。绝大多数情况下,我们直接使用tex2D 函数采样即可,但在某些平台(例如PS3 和PSP2 )上,我们需要一些特殊处理。Unity 为我们提供了一个统一的宏
camera.depthTextureMode |= DepthTextureMode.Depth; camera.depthTextureMode |= DepthTextureMode.DepthNormals;
float d = SAMPLE_DEPTH_TEXTURE(_CarneraDepthTexture, i.uv);
其中, i.uv 是一个float2 类型的变量,对应了当前像素的纹理坐标。类似的宏还有SAMPLE_DEPTH_TEXTURE_PROJ 和
float d = SAMPLE_DEPTH_TEXTURE_PROJ(_CarneraDepthTexture, UNITY_PROJ_COORD(i.scrPos));
其中, i.scrPos 是在顶点着色器中通过调用ComputeScreenPos(o.pos)得到的屏幕坐标。上述这些宏的定义,读者可以在Unity 内置的HLSLSupport.cginc 文件中找到。
-
inline void DecodeDepthNormal( float4 enc, out float depth, out float3 normal)
-
{
-
depth = DecodeFloatRG (enc.zw);
-
normal= DecodeViewNormalStereo(enc);
-
}
DecodeDepthNormal 的第一个参数是对深度+法线纹理的采样结果,这个采样结果是Unity 对深度和法线信息编码后的结果, 它的xy 分量存储的是视角空间下的法线信息, 而深度信息被编码进了zw 分量。通过调用DecodeDepthNormal 函数对采样结果解码后,我们就可以得到解码后的深度值和法线。这个深度值是范围在[0, 1]的线性深度值(这与单独的深度纹理中存储的深度值不同〉,而得到的法线则是视角空间下的法线方向。同样, 我们也可以通过调用DecodeFloatRG 和 DecodeViewNormaLStereo 来解码深度+法线纹理中的深度和法线信息。
-
float depth= SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
-
float linearDepth = LinearOlDepth(depth);
-
return fixed4(linearDepth, linearDepth, linearDepth,
1.0);
或是输出法线方向:
-
fixed3 normal = DecodeViewNormalStereo(tex2D( _CameraDepthNormalsTexture, i.uv).xy);
-
return fixed4 (normal *
0.5 +
0.5,
1.0);
在查看深度纹理时,读者得到的画面有可能几乎是全黑或全白的。这时候读者可以把摄像机的远裁剪平面的距离( Unity 默认为1000 )调小, 使视锥体的范围刚好覆盖场景的所在区域。这是因为,由于投影变换时需要覆盖从近裁剪平面到远裁剪平面的所有深度区域, 当远裁剪平面的 距离过大时, 会导致离摄像机较近的距离被映射到非常小的深度值,如果场景是一个封闭的区域 (如图13.4 所示〉, 那么这就会导致画面看起来几乎是全黑的。相反, 如果场景是一个开放区域, 且物体离摄像机的距离较远, 就会导致画面儿乎是全白的。
public class MotionBlurWithDepthTexture : PostEffectsBase {
( 2 )声明该效果需要的Shader,并据此创建相应的材质:
-
public Shader motionBlurShader;
-
private Material motionBlurMaterial = null;
-
-
public Material material {
-
get {
-
motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
-
return motionBlurMaterial;
-
}
-
}
(3)定义运动模糊时模糊图像使用的大小:
-
[Range(
0.0f,
1.0f)]
-
public
float blurSize =
0.5f;
( 4)由于本节需要得到摄像机的视角和投影矩阵,我们需要定义一个Camera 类型的变量,以获取该脚本所在的摄像机组件:
-
private Camera myCamera;
-
public Camera camera {
-
get {
-
if (myCamera == null) {
-
myCamera = GetComponent
();
-
}
-
return myCamera;
-
}
-
}
( 5 )我们还需要定义一个变量来保存上一帧摄像机的视角*投影矩阵:
private Matrix4x4 previousViewProjectionMatrix;
( 6 )由于本例需要获取摄像机的深度纹理, 我们在脚本的OnEnable 函数中设置摄像机的状态:
-
void OnEnable() {
-
camera.depthTextureMode |= DepthTextureMode.Depth;
(7)最后, 我们实现了OnRenderlmage 函数:
-
void OnRenderImage (RenderTexture src, RenderTexture dest) {
-
if (material != null) {
-
material.SetFloat(
"_BlurSize", blurSize);
-
-
material.SetMatrix(
"_PreviousViewProjectionMatrix", previousViewProjectionMatrix);
-
Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
-
Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;
-
material.SetMatrix(
"_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);
-
previousViewProjectionMatrix = currentViewProjectionMatrix;
-
-
Graphics.Blit (src, dest, material);
-
}
else {
-
Graphics.Blit(src, dest);
-
}
-
}
上面的OnRenderlmage 函数很简单, 我们首先需要计算和传递运动模糊使用的各个属性。本例需要使用两个变换矩阵一一前一帧的视角*投影矩阵以及当前帧的视角*投影矩阵的逆矩阵。因此,我们通过调用camera.worldToCameraMtrix 和camera.projectionMatrix 来分别得到当前摄像机的视角矩阵和投影矩阵。对它们相乘后取逆, 得到当前帧的视角*投影矩阵的逆矩阵,并传递给材质。然后, 我们把取逆前的结果存储在previousViewProjectionMatrix 变量中,以便在下一帧时传递给材质的
-
Properties {
-
_MainTex (
"Base (RGB)",
2D) =
"white" {}
-
_BlurSize (
"Blur Size", Float) =
1.0
-
}
_MainTex 对应了输入的渲染纹理, _BlurSize 是模糊图像时使用的参数。我们注意到,虽然在脚本里设置了材质的
(3)声明代码中需要使用的各个变量:
SubShader { CGINCLUDE ... ENDCG ...
在上面的代码中, 除了定义在Propertity声明的 _MainTex 和 _BlurSize 属性, 我们还声明了其他三个变量。_CameraDepthTexture 是Unity 传递给我们的深度纹理,而 _CurrentViewProjectionlnverseMatrix 和 _PreviousViewProjectionMatrix 是由脚本传递而来的矩阵。除此之外,我们还声明了 _MainTex_TexelSize 变量,它对应了主纹理的纹素大小,我们需要使用该变量来对深度纹理的采
sampler2D _MainTex; half4 _MainTex_TexelSize; sampler2D _CameraDepthTexture; float4x4 _CurrentViewProjectionInverseMatrix; float4x4 _PreviousViewProjectionMatrix; half _BlurSize;
-
struct v2f {
-
float4 pos : SV_POSITION;
-
half2 uv : TEXCOORD0;
-
half2 uv_depth : TEXCOORD1;
-
};
-
-
v2f vert(appdata_img v) {
-
v2f o;
-
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
-
o.uv = v.texcoord;
-
o.uv_depth = v.texcoord;
-
-
#if UNITY_UV_STARTS_AT_TOP
-
if (_MainTex_TexelSize.y <
0)
-
o.uv_depth.y =
1 - o.uv_depth.y;
-
#endif
-
-
return o;
-
}
由于在本例中,我们需要同时处理多张渲染纹理,因此在DirectX 这样的平台上,我们需要处理平台差异导致的图像翻转问题。在上面的代码中,我们对深度纹理的采样坐标进行了平台差异化处理,以便在类似DirectX 的平台上,在开启了抗锯齿的情况下仍然可以得到正确的结果。
-
fixed4 frag(v2f i) : SV_Target {
-
// Get the depth buffer value at this pixel.
-
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
-
// H is the viewport position at this pixel in the range -1 to 1.
-
float4 H = float4(i.uv.x *
2 -
1, i.uv.y *
2 -
1, d *
2 -
1,
1);
-
// Transform by the view-projection inverse.
-
float4 D = mul(_CurrentViewProjectionInverseMatrix, H);
-
// Divide by w to get the world position.
-
float4 worldPos = D / D.w;
-
-
// Current viewport position
-
float4 currentPos = H;
-
// Use the world position, and transform by the previous view-projection matrix.
-
float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
-
// Convert to nonhomogeneous points [-1,1] by dividing by w.
-
previousPos /= previousPos.w;
-
-
// Use this frame's position and last frame's to compute the pixel velocity.
-
float2 velocity = (currentPos.xy - previousPos.xy)/
2.0f;
-
-
float2 uv = i.uv;
-
float4 c = tex2D(_MainTex, uv);
-
uv += velocity * _BlurSize;
-
for (
int it =
1; it <
3; it++, uv += velocity * _BlurSize) {
-
float4 currentColor = tex2D(_MainTex, uv);
-
c += currentColor;
-
}
-
c /=
3;
-
-
return fixed4(c.rgb,
1.0);
-
}
我们首先需要利用深度纹理和当前帧的视角*投影矩阵的逆矩阵来求得该像素在世界空间下的坐标。过程开始于对深度纹理的采样,我们使用内置的SAMPLE_DEPTH_TEXTURE 宏和纹理坐标对深度纹理进行采样, 得到了深度值d。由13.1.2 节可知, d 是由NDC 下的坐标映射而来的。我们想要构建像素的NDC 坐标H, 就需要把这个深度值重新映射回NDC。这个映射很简单,只需要使用原映射的反函数即可,即d * 2 - 1 。同样, NDC 的 xy 分量可以由像素的纹理坐标映射而来( NDC 下的xyz 分量范围均为
-
Pass {
-
ZTest Always Cull Off ZWrite Off
-
-
CGPROGRAM
-
-
#pragma vertex vert
-
#pragma fragment frag
-
-
ENDCG
-
}
(7)最后,我们关闭了shader 的Fallback:
FallBack Off
完成后返回编辑器,并把Chapter13-MotionBlurWithDepthTexture 拖曳到摄像机的MotionBlurWithDepthTexture.cs 脚本中的 motionBlurShader 参数中。当然,我们可以在 MotionBlurWithDepthTexture.cs 的脚本面版中将motionBlurShader 参数的默认值设置为Chapter13-MotionBlurWithDepthTexture , 这样就不需要以后使用时每次都手动拖曳了。
public class FogWithDepthTexture : PostEffectsBase {
(2 )声明该效果需要的Shader,并据此创建相应的材质:
-
public Shader fogShader;
-
private Material fogMaterial = null;
-
-
public Material material {
-
get {
-
fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
-
return fogMaterial;
-
}
-
}
(3 )在本节中,我们需要获取摄像机的相关参数,如近裁剪平面的距离、FOV 等,同时还需要获取摄像机在世界空间下的前方、上方和右方等方向,因此我们用两个变量存储摄像机的Camera 组件和Transform 组件:
-
private Camera myCamera;
-
public Camera camera {
-
get {
-
if (myCamera == null) {
-
myCamera = GetComponent
();
-
}
-
return myCamera;
-
}
-
}
-
-
private Transform myCameraTransform;
-
public Transform cameraTransform {
-
get {
-
if (myCameraTransform == null) {
-
myCameraTransform = camera.transform;
-
}
-
-
return myCameraTransform;
-
}
-
}
(4 )定义模拟雾效时使用的各个参数:
-
[Range(
0.0f,
3.0f)]
-
public
float fogDensity =
1.0f;
-
-
public Color fogColor = Color.white;
-
-
public
float fogStart =
0.0f;
-
public
float fogEnd =
2.0f;
fogDensity 用于控制雾的浓度, fogColor 用于控制雾的颜色。我们使用的雾效模拟函数是基于高度的,因此参数fogStart 用于控制雾效的起始高度, fogEnd 用于控制雾效的终止高度。
-
void OnEnable() {
-
camera.depthTextureMode |= DepthTextureMode.Depth;
-
}
( 6 )最后, 我们实现了OnRenderlmage 函数:
-
void OnRenderImage (RenderTexture src, RenderTexture dest) {
-
if (material != null) {
-
Matrix4x4 frustumCorners = Matrix4x4.identity;
-
-
float fov = camera.fieldOfView;
-
float near = camera.nearClipPlane;
-
float aspect = camera.aspect;
-
-
float halfHeight = near * Mathf.Tan(fov *
0.5f * Mathf.Deg2Rad);
-
Vector3 toRight = cameraTransform.right * halfHeight * aspect;
-
Vector3 toTop = cameraTransform.up * halfHeight;
-
-
Vector3 topLeft = cameraTransform.forward * near + toTop - toRight;
-
float scale = topLeft.magnitude / near;
-
-
topLeft.Normalize();
-
topLeft *= scale;
-
-
Vector3 topRight = cameraTransform.forward * near + toRight + toTop;
-
topRight.Normalize();
-
topRight *= scale;
-
-
Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight;
-
bottomLeft.Normalize();
-
bottomLeft *= scale;
-
-
Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop;
-
bottomRight.Normalize();
-
bottomRight *= scale;
-
-
frustumCorners.SetRow(
0, bottomLeft);
-
frustumCorners.SetRow(
1, bottomRight);
-
frustumCorners.SetRow(
2, topRight);
-
frustumCorners.SetRow(
3, topLeft);
-
-
material.SetMatrix(
"_FrustumCornersRay", frustumCorners);
-
-
material.SetFloat(
"_FogDensity", fogDensity);
-
material.SetColor(
"_FogColor", fogColor);
-
material.SetFloat(
"_FogStart", fogStart);
-
material.SetFloat(
"_FogEnd", fogEnd);
-
-
Graphics.Blit (src, dest, material);
-
}
else {
-
Graphics.Blit(src, dest);
-
}
-
}
OnRenderlmage 首先计算了近裁剪平面的四个角对应的向量, 并把它们存储在一个矩阵类型的变量(frustumCorners)中。计算过程我们已经在13.3.1 节中详细解释过了,代码只是套用了之前讲过的公式而己。我们按一定顺序把这四个方向存储到了frustumCorners 不同的行中,这个顺序是非常重要的,因为这决定了我们在顶点着色器中使用哪一行作为该点的待插值向量。随后,我们把结果和其他参数传递给材质,并调用Graphics.Blit (src, dest, material)把渲染结果显示在屏幕上。
-
Properties {
-
_MainTex (
"Base (RGB)",
2D) =
"white" {}
-
_FogDensity (
"Fog Density", Float) =
1.0
-
_FogColor (
"Fog Color", Color) = (
1,
1,
1,
1)
-
_FogStart (
"Fog Start", Float) =
0.0
-
_FogEnd (
"Fog End", Float) =
1.0
-
}
( 2 )在本节中, 我们使用CGINCLUDE 来组织代码。我们在SubShader 块中利用CGINCLUDE 和 ENDCG 语义来定义一系列代码:
(3)声明代码中需要使用的各个变量:
SubShader { CGINCLUDE ... ENDCG ...
-
float4x4 _FrustumCornersRay;
-
-
sampler2D _MainTex;
-
half4 _MainTex_TexelSize;
-
sampler2D _CameraDepthTexture;
-
half _FogDensity;
-
fixed4 _FogColor;
-
float _FogStart;
-
float _FogEnd;
_FrustumCornersRay 虽然没有在Properties 中声明, 但仍可由脚本传递给Shader。除了 Properties 中声明的各个属性,我们还声明了深度纹理 _CameraDepthTexture, Unity 会在背后把得到的深度纹理传递给该值。
-
struct v2f {
-
float4 pos : SV_POSITION;
-
half2 uv : TEXCOORD0;
-
half2 uv_depth : TEXCOORD1;
-
float4 interpolatedRay : TEXCOORD2;
-
};
-
-
v2f vert(appdata_img v) {
-
v2f o;
-
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
-
o.uv = v.texcoord;
-
o.uv_depth = v.texcoord;
-
-
#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.texcoord.x <
0.5 && v.texcoord.y <
0.5) {
-
index =
0;
-
}
else
if (v.texcoord.x >
0.5 && v.texcoord.y <
0.5) {
-
index =
1;
-
}
else
if (v.texcoord.x >
0.5 && v.texcoord.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;
-
}
在v2f 结构体中,我们除了定义顶点位置、屏幕图像和深度纹理的纹理坐标外,还定义了 interpolatedRay 变量存储插值后的像素向量。在顶点着色器中,我们对深度纹理的采样坐标进行了平台差异化处理。更重要的是,我们要决定该点对应了4 个角中的哪个角。我们采用的方法是判断它的纹理坐标。我们知道,在Unity 中,纹理坐标的(0, 0)点对应了左下角,而(1, 1)点对应了右上角。我们据此来判断该顶点对应的索引,这个对应关系和我们在脚本中对 frustumCorners 的赋值顺序是一致的。实际上,不同平台的纹理坐标不一定是满足上面的条件的,例如DirectX 和 Metal 这样的平台,左上角对应了(0, 0)点,但大多数情况下Unity 会把这些平台下的屏幕图像进行翻转,因此我们仍然可以利用这个条件。但如果在类似DirectX 的平台上开启了抗锯齿, Unity就不会进行这个翻转。为了此时仍然可以得到相应顶点位置的索引值,我们对索引值也进行了平台差异化处理(详见5.6.1 节〉,以便在必要时也对索引值进行翻转。最后,我们使用索引值来获取 _FrustumCornersRay 中对应的行作为该顶点的interpolatedRay 值。
-
fixed4 frag(v2f i) : SV_Target {
-
float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
-
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.rgb, fogDensity);
-
-
return finalColor;
-
}
首先,我们需要重建该像素在世界空间中的位置。为此,我们首先使用SAMPLE_DEPTH_TEXTURE对深度纹理进行采样,再使用LinearEyeDepth 得到视角空间下的线性深度值。之后,与interpolatedRay 相乘后再和世界空间下的摄像机位置相加,即可得到世界空间下的位置。
-
Pass {
-
ZTest Always Cull Off ZWrite Off
-
-
CGPROGRAM
-
-
#pragma vertex vert
-
#pragma fragment frag
-
-
ENDCG
-
}
(7 )最后,我们关闭了Shader 的Fallback:
FallBack Off
完成后返回编辑器,并把Chapter13-FogWithDepthTexture 拖曳到摄像机的FogWithDepthTexture.cs脚本中的fogShader 参数中。当然,我们可以在FogWithDepthTexture.cs 的脚本面板中将fogShader 参数的默认值设置为Chapter13-FogWithDepthTexture,这样就不需要以后使用时每次都手动拖曳了。
public class EdgeDetectNormalsAndDepth : PostEffectsBase {
( 2 ) 声明该效果需要的Shader , 并据此创建相应的材质:
-
public Shader edgeDetectShader;
-
private Material edgeDetectMaterial = null;
-
public Material material {
-
get {
-
edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
-
return edgeDetectMaterial;
-
}
-
}
(3 )在脚本中提供了调整边缘线强度描边颜色以及背景颜色的参数。同时添加了控制采样距离以及对深度和法线进行边缘检测时的灵敏度参数:
-
[Range(
0.0f,
1.0f)]
-
public
float edgesOnly =
0.0f;
-
-
public Color edgeColor = Color.black;
-
-
public Color backgroundColor = Color.white;
-
-
public
float sampleDistance =
1.0f;
-
-
public
float sensitivityDepth =
1.0f;
-
-
public
float sensitivityNormals =
1.0f;
sampleDistance 用于控制对深度+法线纹理采样时,使用的采样距离。从视觉上来看,sampleDistance 值越大,描边越宽。sensitivityDepth 和sensitivityNormals 将会影响当邻域的深度值或法线值相差多少时,会被认为存在一条边界。如果把灵敏度调得很大,那么可能即使是深度或法线上很小的变化也会形成一条边。
-
void OnEnable() {
-
GetComponent
().depthTextureMode |= DepthTextureMode.DepthNormals;
-
}
( 5 ) 实现OnRenderlmage 函数,把各个参数传递给材质:
-
[ImageEffectOpaque]
-
void OnRenderImage (RenderTexture src, RenderTexture dest) {
-
if (material != null) {
-
material.SetFloat(
"_EdgeOnly", edgesOnly);
-
material.SetColor(
"_EdgeColor", edgeColor);
-
material.SetColor(
"_BackgroundColor", backgroundColor);
-
material.SetFloat(
"_SampleDistance", sampleDistance);
-
material.SetVector(
"_Sensitivity",
new Vector4(sensitivityNormals, sensitivityDepth,
0.0f,
0.0f));
-
-
Graphics.Blit(src, dest, material);
-
}
else {
-
Graphics.Blit(src, dest);
-
}
-
}
需要注意的是,这里我们为OnRenderlmage 函数添加了[ImageEffectOpaque]属性。我们曾在12.1节中提到过该属性的含义。在默认情况下,OnRenderlmage 函数会在所有的不透明和透明的Pass 执行完毕后被调用,以便对场最中所有游戏对象都产生影响。但有时,我们希望在不透明的Pass (即渲染队列小于等于2 500 的Pass,内置的Background、Geometry 和AlphaTest 渲染队列均在此范围内)执行完毕后立即调用该函数,而不对透明物体(渲染队列为Transparent 的Pass )产生影响,此时,我们可以在OnRenderlmage 函数前添加ImageEffectOpaque 属性来实现这样的目的。在本例中,我们只希望对不透明物体迸行描边,而不希望透明物体也被描边, 因此需要添加该属性。
-
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)
-
_SampleDistance (
"Sample Distance", Float) =
1.0
-
_Sensitivity (
"Sensitivity", Vector) = (
1,
1,
1,
1)
-
}
其中,_Sensitivity 的xy 分量分别对应了法线和深度的检测灵敏度, zw 分量则没有实际用途。
(3)为了在代码中访问各个属性,我们需要在CG 代码块中声明对应的变量:
SubShader { CG INCLUDE ... ENDCG ...
-
sampler2D _MainTex;
-
half4 _MainTex_TexelSize;
-
fixed _EdgeOnly;
-
fixed4 _EdgeColor;
-
fixed4 _BackgroundColor;
-
float _SampleDistance;
-
half4 _Sensitivity;
-
-
sampler2D _CameraDepthNormalsTexture;
在上面的代码中,我们声明了需要获取的深度+法线纹理 _CameraDepthNormalsTexture。由于我们需要对邻域像素进行纹理采样,所以还声明了存储纹素大小的变量 _MainTex_TexelSize 。
-
struct v2f {
-
float4 pos : SV_POSITION;
-
half2 uv[
5]: TEXCOORD0;
-
};
-
-
v2f vert(appdata_img v) {
-
v2f o;
-
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
-
-
half2 uv = v.texcoord;
-
o.uv[
0] = 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;
-
}
我们在v2f 结构体中定义了一个维数为5 的纹理坐标数组。这个数组的第一个坐标存储了屏幕颜色图像的采样纹理。我们对深度纹理的采样坐标进行了平台差异化处理, 在必要情况下对它的竖直方向进行了翻转。数组中剩余的 4 个坐标则存储了使用Roberts 算子时需要采样的纹理坐标, 我们还使用了 _SampleDistance 来控制采样距离。通过把计算采样纹理坐标的代码从片元着色器中转移到顶点着色器中, 可以减少运算, 提高性能。由于从顶点着色器到片元着色器的插值是线性的, 因此这样的转移并不会影响纹理坐标的计算结果。
-
fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target {
-
half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[
1]);
-
half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[
2]);
-
half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[
3]);
-
half4 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, _EdgeOnly);
-
}
我们首先使用4 个纹理坐标对深度+法线纹理进行采样,再调用CheckSame 函数来分别计算对角线上两个纹理值的差值。CheckSame 函数的返回值要么是0,要么是1,返回0 时表明这两点之间存在一条边界,反之则返回1 。它的定义如下:
-
half CheckSame(half4 center, half4 sample) {
-
half2 centerNormal = center.xy;
-
float centerDepth = DecodeFloatRG(center.zw);
-
half2 sampleNormal = sample.xy;
-
float sampleDepth = DecodeFloatRG(sample.zw);
-
-
// difference in normals
-
// do not bother decoding normals - there's no need here
-
half2 diffNormal =
abs(centerNormal - sampleNormal) * _Sensitivity.x;
-
int isSameNormal = (diffNormal.x + diffNormal.y) <
0.1;
-
// difference in depth
-
float diffDepth =
abs(centerDepth - sampleDepth) * _Sensitivity.y;
-
// scale the required threshold by the distance
-
int isSameDepth = diffDepth <
0.1 * centerDepth;
-
-
// return:
-
// 1 - if normals and depth are similar enough
-
// 0 - otherwise
-
return isSameNormal * isSameDepth ?
1.0 :
0.0;
-
}
CheckSame 首先对输入参数进行处理,得到两个采样点的法线和深度值。值得注意的是,这里我们并没有解码得到真正的法线值,而是直接使用了 xy 分量。这是因为我们只需要比较两个采样值之间的差异度,而并不需要知道它们真正的法线值。然后,我们把两个采样点的对应值相减并取绝对值,再乘以灵敏度参数,把差异值的每个分量相加再和一个阀值比较,如果它们的和小于阀值,则返回1,说明差异不明显,不存在一条边界;否则返回0。最后,我们把法线和深度的检查结果相乘,作为组合后的返回值。
-
Pass {
-
ZTest Always Cull Off ZWrite Off
-
-
CGPROGRAM
-
-
#pragma vertex vert
-
#pragma fragment fragRobertsCrossDepthAndNormal
-
-
ENDCG
-
}
(7)最后,我们关闭了该Shader 的Fallback:
FallBack Off
完成后返回编辑器,并把Chapter13-EdgeDetectNormalAndDepth 拖曳到摄像机的EdgeDetectNormalsAndDepth.cs 脚本中的edgeDetectShader 参数中。当然,我们可以在EdgeDetectlNormaIsAndDepth.cs 的脚本面板中将edgeDetectShader 参数的默认值设置为Chapter13-EdgeDetectNormaIAndDepth,这样就不需要以后使用时每次都手动拖曳了。