Shader笔记十五 获取深度和法线纹理

很多时候不仅需要当前屏幕的信息,同时希望得到深度和法线信息。例如在做边缘检测时,是直接根据渲染完后的屏幕图像的RGB值进行计算的,如果某些物体受到阴影的影响,那么检测结果就不一定准确。如果可以直接在深度纹理和法线纹理上进行边缘检测,那么就不会受到场景中的阴影的影响。

获取深度和法线纹理
首先深度纹理是一张纹理图,既然是一张纹理图,那么其值的范围是在[0,1]之间的,那么这个值的范围是如何转化得到的呢?
这些深度值是经过顶点变化后经过透视除法得到归一化的设备坐标(NDC)后转化得来,一个顶点在经过MVP的矩阵变换后从模型空间变换到投影空间,再经过裁剪,透视除法和屏幕映射最终在屏幕上成像。这里有一点需要注意的是,在顶点着色器中,顶点只经历了MVP的矩阵变换,得到的只是投影空间的坐标,其裁剪,透视除法和屏幕映射都是在顶点着色器之后进行的。 这里提供一个顶点转换过程及着色器处理的时间轴流程图:

在得到NDC后,深度值对应顶点坐标的z分量,由于NDC的z分量范围在[-1,1]之间,因此需要通过映射公式使其映射到[0,1]之间:

d=Zndc/2+1/2      

d对应深度纹理中的像素值,Zndc对应NDC坐标中的z分量的值。
Unity中的深度纹理可以来自真正的深度缓存,也可以由一个单独的Pass渲染而得。当使用延迟渲染路径时,深度纹理可以通过G-buffer得到。而当无法直接获取深度缓存时,深度纹理和法线纹理是通过单独渲染的Pass得到。这时需要在Shader中设置正确的RenderType标签,使物体出现在深度和法线纹理中。

在Unity中,可以选择让一个摄像机生成一张深度纹理和法线纹理(Unity会通过访问深度缓存或特定的Pass得到),获取深度纹理和法线纹理只需设置对应的摄像机模式,然后再在Shader中访问特定的纹理属性。
例如获取深度纹理:

camera.depthTextureMode=DepthTextureMode.Depth;
//然后在Shader中声明_CameraDepthTexture变量来访问

获取深度+法线纹理:

camera.depthTextureMode=DepthTextureMode.DepthNormals;
//然后在Shader中声明_CameraDepthNormalsTexture变量来访问   

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

camera.depthTextureMode |= DepthTextureMode.Depth;
camera.depthTextureMode |= DepthTextureMode.DepthNormals;      
//在Shader中声明对应的变量即可使用          

当在Shader中通过_CameraDepthTexture变量得到深度纹理后,可以使用当前像素的纹理坐标对深度纹理进行采样,大多数情况下直接使用Tex2D()函数采样即可。对于需要特殊处理的平台,Unity提供统一的宏SAMPLE _DEPTH _TEXTURE,用来处理平台差异的问题。在Shader中,使用该宏对深度纹理进行采样,

float d=SAMPLE_DEPTH_TEXTURE(_CameraDepthTxeture,i.uv);     
//i.uv是对应当前的像素纹理坐标

类似宏还有SAMPLE _DEPTH _TEXTURE _PROJ和SAMPLE _DEPTH _TEXTURE _LOD,SAMPLE _DEPTH _TEXTURE _PROJ宏接受两个参数,深度纹理和一个float3或float4类型的纹理坐标,内部使用tex2Dproj进行投影纹理采样。SAMPLE _DEPTH _TEXTURE _PROJ的第二个参数通常是由顶点着色器输出的插值而得到的屏幕坐标,比如:

float d=SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexTure,UNITY_PROJ_COORD(i.srcPos));  
//i.srcPos是在顶点着色器中通过调用ComputeScreenPos(o.pos)得到的屏幕坐标     

当通过纹理采样得到的深度值后,得到的深度值是非线性的,这是由于观察空间到投影空间的矩阵变换是非线性的。而在计算过程中,需要在线性空间下,因此需要将得到深度值变换到线性空间下,比如观察空间。将深度纹理采样的结果反映射到投影空间,再由投影空间变换到观察空间可以通过顶点变换过程的逆向过程计算出来,Unity提供了辅助函数简便计算过程:

LinearEyeDepth();    //负责将深度纹理的采样结果转换到观察空间下     
Linear01Depth();   //返回一个范围在[0,1]线性深度值   

如果需要获取深度+法线纹理,可以直接使用tex2D()对_CameraDepthNormalsTexture进行采样,得到里面存储的深度和法线信息。Unity提供了DecodeDepthNormal函数对采样结果进行解码,从而得到深度值和法线方向。DecodeDepthNormal函数定义:

inline void DecodeDepthNormal(float4 enc, out float depth, out float3 normal)
{
	depth=DecodeFloatRG(enc.zw);
	normal=DecodeViewNormalStereo(enc);
}

经过解码后得到的深度值是范围在[0,1]的线性深度值,得到的法线则是观察空间下的法线方向。

通过深度纹理实现扫描相交部分着色功能
扫描相交部分着色其实在游戏中很常见,比如非常火的一款游戏

玩过的朋友可能会注意到当开始缩圈的时候,毒圈经过的界面在与树木或建筑物相交处会有一圈物体轮廓的高亮显示,那么这个相交处的高亮显示就是要实现的扫描相交部分着色功能。
要想实现这个功能,需要判断扫描平面(也就是那个毒圈)与相交处物体的位置关系,只有足够近,才会有高亮着色效果。再来看这个位置关系,实际上是通过距离摄像机位置的远近来进行判断的。因此可以利用扫描平面当前的深度值和建筑物的深度值进行判断来得到远近关系,建筑物的深度值可以通过已经存在的深度纹理得到,而扫描平面当前的深度值可以通过变换得到。因此这里要确定正确的渲染顺序,即先渲染场景中的树木和建筑物,最后再渲染扫描平面

实例代码:

Shader "Custom/Scan" {
Properties{
	_MainColor("MainColor",Color)=(1,1,1,1)
	_HighLightingColor("HighLightingColor",Color)=(1,1,1,1)
	_Threshold("Threshold",Float)=2.0
	_MainTex("MainTex",2D)="white"{}
}
SubShader{
	Tags{"Queue"="Transparent" "RenderType"="Transparent"}
	//设置合理的渲染队列,使当前扫描平面在其他物体后渲染
	Pass{
		Tags{"LightMode"="ForwardBase"}

		Blend SrcAlpha OneMinusSrcAlpha
		//设置混合,使得扫描平面后面的部分仍然被看见
		CGPROGRAM
		#pragma vertex vert
		#pragma fragment frag
		#include "Lighting.cginc"
		#include "UnityCG.cginc"

		//提前定义
		uniform sampler2D_float _CameraDepthTexture;

		 fixed4 _MainColor;
		 fixed4 _HighLightingColor;
		 float   _Threshold;
		 sampler2D  _MainTex;
		 float4         _MainTex_ST;

		 struct a2v{
			float4 vertex:POSITION;
			float4 texcoord:TEXCOORD0;
		 };

		 struct v2f{
			float4 pos:SV_POSITION;
			float4 projPos:TEXCOORD0;
			float3 viewPos:TEXCOORD1;
			float2 uv:TEXCOORD2; 
		};

		v2f vert(a2v v){
			v2f o;
			o.pos=UnityObjectToClipPos(v.vertex);
			o.projPos=ComputeScreenPos(o.pos);
			o.viewPos=UnityObjectToViewPos(v.vertex);
			o.uv=TRANSFORM_TEX(v.texcoord,_MainTex);
			return o;
		}

		fixed4 frag(v2f i):SV_Target{
			fixed4 finalColor=_MainColor;
			float sceneZ=LinearEyeDepth(tex2Dproj(_CameraDepthTexture,UNITY_PROJ_COORD(i.projPos)));
			//使用LinearEyeDepth得到在观察空间下的深度值,这里需要注意的是Unity的观察空间中,摄像机正向对应着的z值
			//为负值,而为了得到深度值的正数表示,将原观察空间的深度值这里做了一个取反的操作
			float partZ=-i.viewPos.z;
			//因此这里得到当前平面的观察空间深度值后,取了反,与上面得到的结果对应
			float diff=min((abs(sceneZ-partZ))/_Threshold,1.0);
			//这里通过两者深度值的插值/阈值控制颜色插值运算的结果,深度值相差太大则是扫描平面自身颜色
			//而差值越小,则越接近高亮颜色
			finalColor=lerp(_HighLightingColor,_MainColor,diff)*tex2D(_MainTex,i.uv);

			return finalColor;
		}
		ENDCG
	}
}
FallBack "Transparent/VertexLit"
}  

这里可以注意下设置的渲染队列和混合操作,还有一点需要说明的是,为什么最后计算的时候都是使用的观察空间的深度值,之前提到过,投影矩阵的变换过程是非线性的,因此需要转化到线性空间进行计算,所以这里选择了观察空间。

实例效果:

参数设置:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值