UnityShader学习——深度和法线纹理(应用)

深度和法线纹理应用

1.运动模糊

之前的运动模糊是通过混合多张屏幕图像来模拟运动模糊的效果。但是,另一种应用更加广泛的技术则是使用速度映射图。

使用屏幕图像混合的运动模糊
在这里插入图片描述

使用速度映射图的运动模糊

在这里插入图片描述

速度映射图中存储了每个像素的速度,然后使用这个速度来决定模糊的方向和大小。
速度缓冲的生成有多种方法:

  • 把场景中所有物体的速度渲染到一张纹理中。但这种方法的缺点在于需要修改场景中所有物体的Shader代码,使其添加计算速度的代码并输出到一个渲染纹理中。
  • 利用深度纹理在片元着色器中为每个像素计算其在世界空间下的位置。这是通过使用当前的视角投影矩阵的逆矩阵对NDC下的顶点坐标进行变换得到的。当得到世界空间中的顶点坐标后,我们使用前一帧的视角投影矩阵对其进行变换,得到该位置在前一帧中的NDC坐标。然后,我们计算前一帧和当前帧的位置差,生成该像素的速度。这种方法的优点是可以在一个屏幕后处理步骤中完成整个效果的模拟,但缺点是需要在片元着色器中进行两次矩阵乘法的操作,对性能有所影响。

摄像机脚本关键代码:

......
public class MotionBlurWithDepthTexture : PostEffectsBase {
	......
	private Camera myCamera;//本节需要得到摄像机的视角和投影矩阵,我们需要定义一个Camera类型的变量
	public Camera camera {
		get {if (myCamera == null) {myCamera = GetComponent<Camera>();}	return myCamera;}
	}
	[Range(0.0f, 1.0f)]	public float blurSize = 0.5f;//运动模糊时模糊图像使用的大小
	private Matrix4x4 previousViewProjectionMatrix;//定义一个变量来保存上一帧摄像机的视角*投影矩阵
	
	//要获取摄像机的深度纹理,我们在脚本的OnEnable函数中设置摄像机的状态
	void OnEnable() {
		camera.depthTextureMode |= DepthTextureMode.Depth;
		previousViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
	}	
	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			//计算和传递运动模糊使用的各个属性
			material.SetFloat("_BlurSize", blurSize);			
			//把取逆前的结果存储在previousViewProjectionMatrix变量,以便在下一帧时传递给材质
			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);
		}
	}
}

Shader关键代码:

Shader "ShaderBook/Chapter13/Motion Blur With Depth Texture" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_BlurSize ("Blur Size", Float) = 1.0
	}
	SubShader {
		CGINCLUDE		
		#include "UnityCG.cginc"		
		sampler2D _MainTex;
		half4 _MainTex_TexelSize;
		half _BlurSize;
		sampler2D _CameraDepthTexture;//Unity传递给我们的深度纹理
		//由脚本传递而来的矩阵
		float4x4 _CurrentViewProjectionInverseMatrix;
		float4x4 _PreviousViewProjectionMatrix;		
		
		struct v2f {
			float4 pos : SV_POSITION;
			half2 uv : TEXCOORD0;
			half2 uv_depth : TEXCOORD1;//增加了专门用于对深度纹理采样的纹理坐标变量
		};
		
		v2f vert(appdata_img v) {
			v2f o;
			o.pos = UnityObjectToClipPos(v.vertex);			
			o.uv = v.texcoord;
			o.uv_depth = v.texcoord;
			//对深度纹理的采样坐标进行了平台差异化处理
			//以便在类似DirectX的平台上(图像翻转问题),在开启了抗锯齿的情况下仍然可以得到正确的结果
			#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 {
			//使用内置的SAMPLE_DEPTH_TEXTURE宏和纹理坐标对深度纹理进行采样
			float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
			//d是由NDC下的坐标映射而来的,把这个深度值重新映射回NDC,得到像素的NDC坐标H[-1,1]
			float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
			//使用当前帧的视角*投影矩阵的逆矩阵对其进行变换
			float4 D = mul(_CurrentViewProjectionInverseMatrix, H);
			//把结果值除以它的w分量来得到世界空间下的坐标表示worldPos
			float4 worldPos = D / D.w;
			
			// Current viewport position 
			float4 currentPos = H;
			//使用前一帧的视角*投影矩阵对它进行变换,得到前一帧在NDC下的坐标previousPos  
			float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
			//Convert to nonhomogeneous points [-1,1] by dividing by w.
			previousPos /= previousPos.w;
			
			//计算前一帧和当前帧在屏幕空间下的位置差,得到该像素的速度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) {
				//使用_BlurSize来控制采样距离
				float4 currentColor = tex2D(_MainTex, uv);
				c += currentColor;
			}
			c /= 3;			
			return fixed4(c.rgb, 1.0);
		}		
		ENDCG		
		Pass {      
			ZTest Always Cull Off ZWrite Off			    	
			CGPROGRAM  			
			#pragma vertex vert  
			#pragma fragment frag  			  
			ENDCG  
		}
	} 
	FallBack Off
}

本节实现的运动模糊适用于场景静止、摄像机快速运动的情况,这是因为我们在计算时只考虑了摄像机的运动。因此,如果读者把本节中的代码应用到一个物体快速运动而摄像机静止的场景,会发现不会产生任何运动模糊效果。如果我们想要对快速移动的物体产生运动模糊的效果,就需要生成更加精确的速度映射图。本节选择在片元着色器中使用逆矩阵来重建每个像素在世界空间下的位置。但是,这种做法往往会影响性能。

2.屏幕雾效

雾效(Fog)是游戏里经常使用的一种效果。实现雾效的方法有:

  • Unity内置的雾效:可以产生基于距离的线性或指数雾效。无法对雾效进行一些个性化操作,例如使用基于高度的雾效等。
  • 在顶点/片元着色器中实现这些雾效:需要在Shader中添加#pragma multi_compile_fog指令,同时还需要使用相关的内置宏,例如UNITY_FOG_COORDSUNITY_TRANSFER_FOGUNITY_APPLY_FOG等。这种方法的缺点在于,需要为场景中所有物体添加相关的渲染代码,能够实现的效果也非常有限。
  • 基于屏幕后处理的全局雾效的实现:不需要更改场景内渲染的物体所使用的Shader代码,依靠一次屏幕后处理的步骤即可。这种方法的自由性很高,我们可以方便地模拟各种雾效,例如均匀的雾效、基于距离的线性/指数雾效、基于高度的雾效等。

基于屏幕后处理的全局雾效的关键是,根据深度纹理来重建每个像素在世界空间下的位置。然而,在片元着色器中进行矩阵乘法的操作通常会影响游戏性能。还有一个快速从深度纹理中重建世界坐标的方法:

  • 首先对图像空间下的视锥体射线(从摄像机出发,指向图像上的某点的射线)进行插值,这条射线存储了该像素在世界空间下到摄像机的方向信息。
  • 然后,我们把该射线和线性化后的视角空间下的深度值相乘,再加上摄像机的世界位置,就可以得到该像素在世界空间下的位置。
  • 当我们得到世界坐标后,就可以轻松地使用各个公式来模拟全局雾效了。

在这里插入图片描述

重建世界坐标

我们知道,坐标系中的一个顶点坐标可以通过它相对于另一个顶点坐标的偏移量来求得。重建像素的世界坐标也是基于这样的思想。我们只需要知道摄像机在世界空间下的位置,以及世界空间下该像素相对于摄像机的偏移量,把它们相加就可以得到该像素的世界坐标。整个过程可以使用下面的代码来表示:

float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;
  • _WorldSpaceCameraPos:摄像机在世界空间下的位置,这可以由Unity的内置变量直接访问得到。
  • linearDepth * interpolatedRay:则可以计算得到该像素相对于摄像机的偏移量。
    • linearDepth:由深度纹理得到的线性深度值。
    • interpolatedRay:由顶点着色器输出并插值后得到的射线,它不仅包含了该像素到摄像机的方向,也包含了距离信息。

interpolatedRay的计算

它来源于对近裁剪平面的4个角的某个特定向量的插值,这4个向量包含了它们到摄像机的方向和距离信息,我们可以利用摄像机的近裁剪平面距离、FOV、横纵比计算而得。
在这里插入图片描述
为了方便计算,我们可以先计算两个向量—— t o T o p toTop t o R i g h t toRight ,它们是起点位于近裁剪平面中心、分别指向摄像机正上方和正右方的向量。它们的计算公式如下:
在这里插入图片描述其中,Near是近裁剪平面的距离,FOV是竖直方向的视角范围,camera.up、camera.right分别对应了摄像机的正上方和正右方。当得到这两个辅助向量后,我们就可以计算4个角相对于摄像机的方向了。我们以左上角为例(见图中的TL点),它的计算公式如下: T L = c a m e r a . f o r w a r d N e a r + t o T o p t o R i g h t TL=camera.forward·Near+toTop-toRight 同理,其他3个角的计算也是类似的: T R = c a m e r a . f o r w a r d N e a r + t o T o p + t o R i g h t TR=camera.forward · Near+toTop+toRight B L = c a m e r a . f o r w a r d N e a r t o T o p t o R i g h t BL=camera.forward · Near-toTop-toRight B R = c a m e r a . f o r w a r d N e a r t o T o p + t o R i g h t BR=camera.forward · Near-toTop+toRight 在这里插入图片描述
上面求得的4个向量不仅包含了方向信息,它们的模对应了4个点到摄像机的空间距离。由于我们得到的线性深度值并非是点到摄像机的欧式距离,而是在z方向上的距离,因此,我们不能直接使用深度值和4个角的单位方向的乘积来计算它们到摄像机的偏移量,如图13.7所示。想要把深度值转换成到摄像机的欧式距离也很简单,我们以TL点为例,根据相似三角形原理,TL所在的射线上,像素的深度值和它到摄像机的实际距离的比等于近裁剪平面的距离和TL向量的模的比,即
在这里插入图片描述由此可得,我们需要的TL距离摄像机的欧氏距离dist:
在这里插入图片描述由于4个点相互对称,因此其他3个向量的模和TL相等,即我们可以使用同一个因子和单位向量相乘,得到它们对应的向量值:在这里插入图片描述
在这里插入图片描述
屏幕后处理的原理是使用特定的材质去渲染一个刚好填充整个屏幕的四边形面片。这个四边形面片的4个顶点就对应了近裁剪平面的4个角。因此,我们可以把上面的计算结果传递给顶点着色器,顶点着色器根据当前的位置选择它所对应的向量,然后再将其输出,经插值后传递给片元着色器得到interpolatedRay。

雾的计算

在简单的雾效实现中,我们需要计算一个雾效系数f,作为混合原始颜色和雾的颜色的混合系数:

float3 afterFog = f * fogColor + (1-f) * origColor;

这个雾效系数f有很多计算方法。在Unity内置的雾效实现中,支持三种雾的计算方式——线性(Linear)、指数(Exponential)以及指数的平方(Exponential Squared)。当给定距离 z z 后, f f 的计算公式分别如下:

  • Linear: d m i n d_{min} d m a x d_{max} 分别表示受雾影响的最小距离和最大距离。 f = d m a x z d m a x d m i n f={d_{max} - |z|} \over{d_{max} - d_{min}}
  • Exponential: d d 是控制雾的浓度的参数。 f = e d z f=e^{-d· |z|}
  • Exponential Squared: d d 是控制雾的浓度的参数。 f = e ( d z ) 2 f=e^{-(d· |z|)^2}

我们将使用类似线性雾的计算方式,计算基于高度的雾效。具体方法是,当给定一点在世界空间下的高度 y y 后, f f 的计算公式为: H s t a r t H_{start} H e n d H_{end} 分别表示受雾影响的起始高度和终止高度。 f = H e n d y H e n d H s t a r t f={H_{end}-y}\over{H_{end}-H_{start}}

摄像机脚本关键代码:

......
public class FogWithDepthTexture : PostEffectsBase {
	......
	//需要获取摄像机的相关参数,如近裁剪平面的距离、FOV等
	private Camera myCamera;
	public Camera camera {get {if (myCamera == null) {myCamera = GetComponent<Camera>();}return myCamera;}}
	//还需要获取摄像机在世界空间下的前方、上方和右方等方向
	private Transform myCameraTransform;
	public Transform cameraTransform {get {if (myCameraTransform == null) {myCameraTransform = camera.transform;}return myCameraTransform;}}

	[Range(0.0f, 3.0f)]	public float fogDensity = 1.0f;//控制雾的浓度
	public Color fogColor = Color.white;//控制雾的颜色
	public float fogStart = 0.0f,fogEnd = 2.0f;//控制雾效的起始高度、终止高度
	
	//设置摄像机的相应状态,获取摄像机的深度纹理
	void OnEnable() {
		camera.depthTextureMode |= DepthTextureMode.Depth;
	}
	
	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			//计算近裁剪平面的四个角对应的向量,存储在一个矩阵类型的变量(frustumCorners)中
			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不同的行中
			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);
		}
	}
}

Shader关键代码:

  • 这里的实现是基于摄像机的投影类型是透视投影的前提下。
  • 尽管我们这里使用了很多判断语句,但由于屏幕后处理所用的模型是一个四边形网格,只包含4个顶点,因此这些操作不会对性能造成很大影响。
Shader "ShaderBook/Chapter13/Fog With Depth Texture" {
	Properties {
		......
	}
	SubShader {
		CGINCLUDE		
		#include "UnityCG.cginc"		
		float4x4 _FrustumCornersRay;		
		......		
		struct v2f {
			float4 pos : SV_POSITION;
			half2 uv : TEXCOORD0;
			half2 uv_depth : TEXCOORD1;
			float4 interpolatedRay : TEXCOORD2;//interpolatedRay变量存储插值后的像素向量
		};
		
		v2f vert(appdata_img v) {
			v2f o;
			o.pos = UnityObjectToClipPos(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
			//决定该点对应4个角中的哪个角
			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;
			}
			//DirectX和Metal左上角对应了(0, 0)点,但大多数情况下Unity会把这些平台下的屏幕图像进行翻转
			//但如果在类似DirectX的平台上开启了抗锯齿,Unity就不会进行这个翻转。
			//保险起见,对索引值也进行平台差异化处理,以便在必要时也对索引值进行翻转
			#if UNITY_UV_STARTS_AT_TOP
			if (_MainTex_TexelSize.y < 0)
				index = 3 - index;
			#endif
			//使用索引值来获取_FrustumCornersRay中对应的行作为该顶点的interpolatedRay值
			o.interpolatedRay = _FrustumCornersRay[index];				 	 
			return o;
		}
		
		fixed4 frag(v2f i) : SV_Target {
			//使用SAMPLE_DEPTH_TEXTURE对深度纹理进行采样
			//再使用LinearEyeDepth得到视角空间下的线性深度值
			float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
			//与interpolatedRay相乘后再和世界空间下的摄像机位置相加,得到世界空间下的位置。
			float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;
			//根据材质属性_FogEnd和_FogStart计算当前的像素高度worldPos.y对应的雾效系数fogDensity			
			float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart); 
			//和参数_FogDensity相乘后,利用saturate函数截取到[0,1]范围内,作为最后的雾效系数
			fogDensity = saturate(fogDensity * _FogDensity);
			//使用该系数将雾的颜色和原始颜色进行混合后返回
			fixed4 finalColor = tex2D(_MainTex, i.uv);
			finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);			
			return finalColor;
		}		
		ENDCG		
		Pass {
			ZTest Always Cull Off ZWrite Off			     	
			CGPROGRAM  			
			#pragma vertex vert  
			#pragma fragment frag  			  
			ENDCG  
		}
	} 
	FallBack Off
}

3.边缘检测

之前介绍了如何使用Sobel算子对屏幕图像进行边缘检测,实现描边的效果。但是,这种直接利用颜色信息进行边缘检测的方法会产生很多我们不希望得到的边缘线(物体的纹理、阴影)。
在这里插入图片描述

我们可以在深度和法线纹理上进行边缘检测,这些图像不会受纹理和光照的影响,而仅仅保存了当前渲染物体的模型信息,通过这样的方式检测出来的边缘更加可靠。在这里插入图片描述

我们使用Roberts算子来进行边缘检测。它使用的卷积核如图所示:
在这里插入图片描述Roberts算子的本质就是计算左上角和右下角的差值,乘以右上角和左下角的差值,作为评估边缘的依据。在下面的实现中,我们也会按这样的方式,取对角方向的深度或法线值,比较它们之间的差值,如果超过某个阈值(可由参数控制),就认为它们之间存在一条边。

摄像机脚本关键代码:

......
public class EdgeDetectNormalsAndDepth : PostEffectsBase {
	......
	[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;
	
	void OnEnable() {
		GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
	}

	[ImageEffectOpaque]//在不透明的Pass执行完毕后立即调用该函数,而不对透明物体产生影响(透明物体不需要描边)
	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);
		}
	}
}

Shader关键代码:

Shader "ShaderBook/Chapter13/Edge Detection Normals And Depth" {
	Properties {
		......
	}
	SubShader {
		CGINCLUDE		
		......
		sampler2D _CameraDepthNormalsTexture;
		
		struct v2f {
			float4 pos : SV_POSITION;
			//第一个坐标存储了屏幕颜色图像的采样纹理
			//剩余的4个坐标则存储了使用Roberts算子时需要采样的纹理坐标
			half2 uv[5]: TEXCOORD0;
		};
		  
		v2f vert(appdata_img v) {
			v2f o;
			o.pos = UnityObjectToClipPos(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;
		}		
		//计算对角线上两个纹理值的差值
		half CheckSame(half4 center, half4 sample) {
			//得到两个采样点的法线和深度值
			//没有解码得到真正的法线值,而是直接使用了xy分量,因为只需要知道差值
			half2 centerNormal = center.xy;
			float centerDepth = DecodeFloatRG(center.zw);
			half2 sampleNormal = sample.xy;
			float sampleDepth = DecodeFloatRG(sample.zw);			
			//把两个采样点的对应值相减并取绝对值,再乘以灵敏度参数
			half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
			int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
			float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
			//把差异值的每个分量相加再和一个阈值比较
			//如果它们的和小于阈值,则返回1,说明差异不明显,不存在一条边界;否则返回0
			int isSameDepth = diffDepth < 0.1 * centerDepth;			
			return isSameNormal * isSameDepth ? 1.0 : 0.0;
		}
		
		fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target {
			//使用4个纹理坐标对深度+法线纹理进行采样
			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]);
			//调用CheckSame函数来分别计算对角线上两个纹理值的差值。
			//CheckSame函数的返回值要么是0,要么是1,返回0时表明这两点之间存在一条边界,反之则返回1
			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);
		}		
		ENDCG		
		Pass { 
			ZTest Always Cull Off ZWrite Off			
			CGPROGRAM      			
			#pragma vertex vert  
			#pragma fragment fragRobertsCrossDepthAndNormal			
			ENDCG  
		}
	} 
	FallBack Off
}

以上描边效果是基于整个屏幕空间进行的,场景内的所有物体都会被添加描边效果。但有时,我们希望只对特定的物体进行描边,例如当玩家选中场景中的某个物体后,我们想要在该物体周围添加一层描边效果。这时,我们可以这样做:

  • 使用Unity提供的Graphics.DrawMesh()Graphics.DrawMeshNow()函数把需要描边的物体再次渲染一遍(在所有不透明物体渲染完毕之后)。
  • 使用本节提到的边缘检测算法计算深度或法线纹理中每个像素的梯度值,判断它们是否小于某个阈值。
  • 如果是,就在Shader中使用clip()函数将该像素剔除掉,从而显示出原来的物体颜色。

总结

我们可以使用深度和法线纹理实现诸如全局雾效、边缘检测等效果。尽管我们只使用了深度和法线纹理,但实际上我们可以在Unity中创建任何需要的缓存纹理。这可以通过使用Unity的着色器替换(Shader Replacement)功能(即调用Camera.RenderWithShader(shader, replacementTag)函数)把整个场景再次渲染一遍来得到,而在很多时候,这实际也是Unity创建深度和法线纹理时使用的方法。深度和法线纹理在屏幕特效的实现中往往扮演了重要的角色。许多特殊的屏幕效果都需要依靠这两种纹理的帮助。

发布了195 篇原创文章 · 获赞 59 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_36622009/article/details/105634400
今日推荐