UnityShader学习笔记 使用深度和法线纹理

获取深度和法线纹理

背后的原理

深度纹理实际上就是一张渲染纹理,只不过它里面存储的像素值不是颜色值,而是一个高精度的深度值。这些深度值来源于顶点变换后的归一化的设备坐标(NDC)。

 

如何获取

设置摄像机的depthTextureMode来完成,

camera.depthTextureMode = DepthTextureMode.Depth;

设置完相机模式后,我们就可以在Shader中通过声明_CameraDepthTexture来访问它

 

同理,如果想要获取深度+法线纹理,只需要

camera.depthTextureMode = DepthTextureMode.DepthNormals;

然后在Shader中通过声明_CameraDepthNormalTexture来访问它

 

我们还可以组合这些模式,让摄像机同时产生深度和深度+法线纹理:

camera.depthTextureMode  |=  DepthTextureMode.Depth;

camera.depthTextureMode  |=  DepthTextureMode.DepthNormals;

 

当在Shader中访问到深度纹理_CameraDepthTexture后,我们就可以使用当前像素的纹理坐标对它进行采样,只需要在Shader中使用:

 

 

查看深度和法线纹理

通过FrameDebugger或者直接在Shader中进行颜色的输出显示

 

再谈运动模糊

使用速度映射图来实现,在速度映射图中存储了每个像素的速度,然后使用这个速度来决定模糊的方向和大小。

速度缓冲的生成方法:

把场景中的所有物体的速度都渲染到一张纹理中,缺点在于需要修改场景中所有物体的Shader代码,使其添加计算速度的代码并输出到一个渲染纹理中。

屏幕后处理代码部分:

定义运动模糊时模糊图像使用的大小blurSize;

由于需要得到摄像机的视角和投影矩阵,需要定义一个Camera类型的变量获取摄像机组件;

定义一个变量来保存上一帧摄像机的视角*投影矩阵;

在OnEnable函数中设置摄像机的状态;

using UnityEngine;
using System.Collections;

public class MotionBlurWithDepthTexture : PostEffectsBase {

	public Shader motionBlurShader;
	private Material motionBlurMaterial = null;

	public Material material {  
		get {
			motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
			return motionBlurMaterial;
		}  
	}

	private Camera myCamera;
	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;
	
	void OnEnable() {
		camera.depthTextureMode |= DepthTextureMode.Depth;

		previousViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
	}
	
	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);
		}
	}
}

 

Shader部分:

声明使用的各个属性:_MainTex输入的渲染纹理,_BlurSize模糊图像时使用的参数;

声明代码中使用到的各个变量,_MainTex和BlurSize,_CameraDepthTexture时Unity传递给我们的深度纹理,而_CurrentViewProjectionInverseMatrix和_PreviousViewProjectionMatrix是由脚本传递而来的矩阵。除此之外还声明了_MainTex_TexelSize变量,对应了主纹理的纹素大小;

顶点着色器中,增加了专门对于深度纹理采样的纹理坐标变量,并处理了平台差异导致的图像翻转问题;

在片元着色器中,首先利用深度纹理和当前帧的视角*投影矩阵的逆矩阵求出该像素在世界空间中的坐标。过程开始于对深度纹理的采样,使用内置的SAMPLE_DEPTH_TEXTURE宏和纹理坐标对深度纹理进行采样,得到深度值d。d是NDC下的坐标映射而来的,再将深度值重新映射回NDC,使用原映射的反函数d*2-1即可,得到像素的NDC坐标H。同样,NDC的xy分量可以由像素的纹理坐标映射而来,当得到NDC下的坐标后,就可以使用当前帧的视角*投影矩阵的逆矩阵对其就行变换,并把结果值除以它的w分量来得到世界空间下的坐标worldPos。

在得到世界空间下的坐标后,就可以使用前一帧的视角*投影矩阵对它进行变换,得到前一帧在NDC下的坐标previousPos,然后计算前一帧和当前帧在屏幕空间下的位置差得到该像素的速度velocity。

当得到该像素的速度后,就可以使用这个速度值对它的邻域像素进行采样,相加后取平均值得到一个模糊的效果,采样时使用_BlurSize来控制采样距离。

Shader "Custom/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;
		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 = 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
					 
			return o;
		}
		
		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);
		}
		
		ENDCG
		
		Pass {      
			ZTest Always Cull Off ZWrite Off
			    	
			CGPROGRAM  
			
			#pragma vertex vert  
			#pragma fragment frag  
			  
			ENDCG  
		}
	} 
	FallBack Off
}

 

这种运动模糊适用于场景静止,摄像机快速运动的情况。

如果想要对快速移动的物体产生运动模糊的效果,就需要生成更加精确的速度映射图,可以在ImageEffect包中找到更多的运动模糊的实现方法。

 

全局雾效

 

重建世界坐标

 

雾的计算

 

实现

屏幕后处理脚本部分:

用两个变量存储摄像机的Camera组件和Transform组件:myCamera,myCameraTransform;

定义雾效模拟使用的各个参数:

fogDensity控制雾的浓度;

fogColor控制雾的颜色;

fogStart控制雾效的起始高度;

fogEnd控制雾效的终止高度;

在OnRendererImage函数中,首先计算了近裁剪平面的四个角对应的向量,并把他们存储在一个矩阵类型的变量(frustumCorners)中,把结果和和其他参数传递给材质,并调用Graphic.Blit(src,dest,material)把渲染结果显示在屏幕上。

using UnityEngine;
using System.Collections;

public class FogWithDepthTexture : PostEffectsBase {

	public Shader fogShader;
	private Material fogMaterial = null;

	public Material material {  
		get {
			fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
			return fogMaterial;
		}  
	}

	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;
	public float fogEnd = 2.0f;

	void OnEnable() {
		camera.depthTextureMode |= DepthTextureMode.Depth;
	}
	
	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);
		}
	}
}

 

 

Shader部分:

声明代码中需要使用的各个变量:

_FrustumCornersRay虽然没有在Properties中声明,但仍可由脚本传递给Shader,还声明了深度纹理_CameraDepthTexture,Unity会在背后把得到的深度纹理传递给该值;

定义顶点着色器:在v2结构体中,我们除了定义顶点位置、屏幕图像和深度纹理坐标外,还定义了interpolatedRay变量存储插值后的像素元素。在顶点着色器中,我们对深度纹理的采样坐标进行了平台差异化处理;

在片元着色器中产生雾效:首先重建该像素在世界空间中的位置。为此,首先使用SAMPLE_DEPTH_TEXTURE对深度纹理进行采样,再使用LinerEyeDepth得到视角空间下的线性深度值。之后,与interpolatedRay相乘后再和世界坐标下的摄像机位置相加,即可得到世界空间下的位置;

得到世界坐标后,模拟雾效就变得非常容易,根据材质属性_FogEnd和_FogStart计算当前的像素高度worldPos.y对应的雾效系数fogDensity,在和参数_FogDensity相乘后,利用saturate函数截取到[0 , 1]范围内,作为最后的雾效系数。然后使用该系数将雾的颜色和原始颜色进行混合后返回。

Shader "Custom/Fog With Depth Texture" {
	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
	}
	SubShader {
		CGINCLUDE
		
		#include "UnityCG.cginc"
		
		float4x4 _FrustumCornersRay;
		
		sampler2D _MainTex;
		half4 _MainTex_TexelSize;
		sampler2D _CameraDepthTexture;
		half _FogDensity;
		fixed4 _FogColor;
		float _FogStart;
		float _FogEnd;
		
		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 = 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
			
			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;
		}
		
		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;
		}
		
		ENDCG
		
		Pass {
			ZTest Always Cull Off ZWrite Off
			     	
			CGPROGRAM  
			
			#pragma vertex vert  
			#pragma fragment frag  
			  
			ENDCG  
		}
	} 
	FallBack Off
}

 

再谈边缘检测

屏幕后处理脚本:

提供了调整边缘线强度edgeOnly,描边颜色edgeColor以及背景颜色backgriundColor的参数,同时添加了控制采样距离sampleDistance,sampleDistance值越大描边越宽,以及对深度和法线进行边缘检测时的灵敏度参数sensitivityDepth,sensitivityNormals,sensitivityDepth和sensitivityNormals将会影响当邻域的深度值或法线值相差多少时,会被认为存在一条边界;

由于需要获取到摄像机的深度和法线纹理,在OnEnable函数中设置摄像机的相应状态;

实现OnRenderImage函数,把各个参数传递给材质,ImageEffectOpaque属性添加上后,那么OnRenderImage函数会在所有的不透明的Pass(即渲染队列小于2500的Pass,内置的Background、Geometry和AlphaTest渲染队列均在此范围内)执行完毕后立即调用此函数,而不对透明物体(渲染队列为Transparent的Pass)产生影响。

using UnityEngine;
using System.Collections;

public class EdgeDetectNormalsAndDepth : PostEffectsBase {

	public Shader edgeDetectShader;
	private Material edgeDetectMaterial = null;
	public Material material {  
		get {
			edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
			return edgeDetectMaterial;
		}  
	}

	[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]
	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部分:

首先声明各个属性,其中Sensitivity的xy分量分别对应了法线和深度的检测灵敏度,zw分量则没有实际用途;

为了在代码中方位各个属性,需要在CG代码块中声明对应的变量;

在上面的代码中,我们声明了需要获取的深度+法线纹理_CameraDepthNormalsTexture。由于需要对邻域像素进行纹理采样,所以还声明了存储纹素大小的变量_MainTex_TexelSize;

定义顶点着色器:

在v2f结构体中定义了一个维数为5的纹理坐标数组,这个数组的第一个坐标存储了屏幕颜色图像的采样纹理。对深度纹理的采样坐标进行了平台差异化处理,在必要情况下对它的竖直方向进行了翻转。数组中剩余的4个坐标则存储了使用Roberts算子时需要采样的纹理坐标,还使用了_SampleDistance来控制采样的距离;

定义片元着色器:

首先对4个纹理坐标进行深度+法线纹理进行采样,再调用CheckSame函数来分别计算对角线上两个纹理值的插值。CheckSame函数的返回值要么是0,要么是1,返回0时表明这两点之间存在一条边界,反之则返回1;

CheckSame函数中:

首先对输入参数进行处理,得到两个采样点的法线和深度值,值得注意的是这里得到的并不是真正的法线值,而是直接使用了xy分量。这是因为我们两个采样值之间的差异度,而不需要知道它们真正的法线值。然后再把两个采样点的对应值相减并取绝对值,而并不需要知道它们真正的法线值。然后,把两个采样点的对应值相减并取绝对值,再乘以灵敏度参数,把差异值的每个分量相加再和一个阈值比较,如果它们的和小于阈值,则返回1,说明差异不明显,不存在一条边界;否则返回0.最后把法线和深度的检查结果相乘,作为组合后的返回值。

当通过CheckSame函数得到边缘信息后,片元着色器就利用该值进行颜色混合。

Shader "Custom/Edge Detection Normals And Depth" {
	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)
	}
	SubShader {
		CGINCLUDE
		
		#include "UnityCG.cginc"
		
		sampler2D _MainTex;
		half4 _MainTex_TexelSize;
		fixed _EdgeOnly;
		fixed4 _EdgeColor;
		fixed4 _BackgroundColor;
		float _SampleDistance;
		half4 _Sensitivity;
		
		sampler2D _CameraDepthNormalsTexture;
		
		struct v2f {
			float4 pos : SV_POSITION;
			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) {
			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;
		}
		
		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);
		}
		
		ENDCG
		
		Pass { 
			ZTest Always Cull Off ZWrite Off
			
			CGPROGRAM      
			
			#pragma vertex vert  
			#pragma fragment fragRobertsCrossDepthAndNormal
			
			ENDCG  
		}
	} 
	FallBack Off
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值