需要从shader 中获取深度值,主要涉及到渲染流水线中的以下几个节点
1.观察空间:观察空间是以摄像机所在位置为原点的空间。我们尝试获取的深度信息就是在这个空间之下。
2.裁剪空间(投影空间、齐次裁剪空间):将顶点转换到此空间下来判断是否在视椎体内,从而裁剪掉不在摄像机视野内的顶点;将顶点变换到此空间下可以方便做后续的投影工作。
3.屏幕空间:将没有被裁剪的顶点从裁剪空间转换到此空间,从而最终呈现在屏幕上。
主要涉及到的术语
1.裁剪矩阵(投影矩阵):
用于将顶点从观察空间转换到裁剪空间的矩阵。
经过裁剪矩阵的操作后,顶点的x、y、z分量则被转换到裁剪空间中。
裁剪空间下的顶点满足以下条件的,即为在视野内的顶点:
-w<=x<=w;
-w<=y<=w;
-w<=z<=w;
以上裁剪原理与深度获取关系不大,只需要注意:
经过裁剪矩阵的操作后,顶点的w 分量保存了顶点在观察空间下的深度信息(观察空间的顶点的z 分量)。
2.齐次除法(透视除法):
经过裁剪矩阵的操作之后,将裁剪空间下的顶点的x、y、z 分量分别除以w 分量的过程。此过程完成后,裁剪空间下的顶点将会转换到NDC 中,即变换到一个各分量从-1到1长度为2的立方体内部。如果使用的是透视摄像机,因为z 分量保留了深度信息,经过透视除法以后,越远离摄像机的顶点其xy分量数值将越小(因为w 分量是观察空间中顶点的z 分量,对于两个顶点,如果他们x、y 分量相同,而w 分量不同即深度不同,越远的顶点经过透视除法后,其数值也就越小)。齐次除法完成后,顶点的x、y 分量再经过简单的缩放映射即可投射到屏幕空间二维坐标中,而z 分量通过d = 0.5*z + 0.5 转变为0-1 范围并作为最终会存贮于深度图中的数据。
3.屏幕空间:
渲染管线中最后一个流程将裁剪空间中的顶点通过透视除法和屏幕映射映射最终从3D 的裁剪空间转换到2D 的空间中,这个2D 空间就是屏幕空间。屏幕空间是左下角为(0,0),右上角为(screenwidth, screenheight)的二维空间。
Shader 中获取深度(unity 2019.4.19 urp)
Shader 中将顶点从裁剪空间转换到屏幕空间(即齐次除法和屏幕映射)由底层完成,而获取深度的原理就是再现这一操作中某些步骤的过程。
通过顶点数据可以直接获得顶点的深度;通过顶点数据和屏幕映射公式可以逆推获得屏幕纹理坐标,使用屏幕纹理坐标可以采样深度图,然后将深度图中的数据逆推回模型空间就可以获得需要的场景深度。
1.顶点着色器中需要完成将顶点从模型空间转换到裁剪空间下的任务。
2.来到片元着色器中,经过Unity 渲染流程的处理,顶点输出的裁剪空间顶点(SV_POSITION 语义)坐标的xy 分量已经做了透视除法和屏幕映射处理转换到了屏幕空间当中,z 分量也做了透视除法并转换到(0,1)范围内,w 分量仍然是观察空间下的深度值。
获取顶点的深度值:
裁剪空间顶点坐标w 分量就是视角空间下的深度值,将其除以远裁面就是0-1 的深度值。
远裁剪面的距离可以通过内置的_ProjectionParams.z 变量来获取。
或者比较傻的办法:
1.上面讲到片元着色器中,裁剪空间中的顶点(SV_POSITION 语义)坐标的z 分量已经做了透视除法并转换到了(0,1)范围中,此时只需要将其逆推回观察空间即可;
2.利用公式:(n表示远裁剪面;f表示近裁剪面;d表示ndc中重映射后的深度值,即第2步骤计算出的数值)
zv = 1/((n-f/n*f)*d + 1/n) 计算出观察空间下的深度值,将其除以远裁剪面就是0-1范围的观察空间下的深度值,公式:
z01=1/((n-f)/n*d + f/n)
以上公式实际上是将渲染管线处理后的顶点逆推回观察空间中,实际上是结合了投影矩阵z 分量上的处理公式、透视除法公式、屏幕映射公式三个步骤逆推出来的公式。
之所以说这个方法很傻,是因为绕了一个圈,因为顶点从裁剪空间转到屏幕空间下又逆推回了观察空间,之所以要介绍这种办法,是因为在遇到需要获取场景深度值的需求时,我们可以拿到的数据(即深度图)存储的正是步骤1 中顶点z 分量的值。
由于顶点在转换到裁剪空间时,其w 分量就是观察空间下的z 分量,如果只需要顶点的深度,还是建议直接使用裁剪空间顶点的w 分量作为深度值。
获取场景的深度值:
1.上面提到在片元着色器中,顶点坐标的xy 分量已经从裁剪空间转换到了屏幕空间。
2.直接将顶点坐标的xy 分量除以屏幕的长宽就可以得到0-1 范围的uv 坐标(屏幕纹理坐标),屏幕的长宽可以通过内置变量_ScreenParams 获取。
3.对深度图进行采样: SAMPLE_TEXTURE2D_X(_CamearaDepthTexture, sampler_CameraDepthTexture, uv)
采样深度图需要在shader 中作如下声明:
TEXTURE2D_X_FLOAT(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
4.利用公式:(n表示远裁剪面;f表示近裁剪面;d表示深度图中的数据,即第四步结果的x分量)
zv = 1/((n-f/n*f)*d + 1/n) 计算出模型空间下的深度值,将其除以远裁剪面就是0-1范围的深度值,公式:
z01=1/((n-f)/n*d + f/n)
也可以在顶点着色器中计算采样坐标,但是因为从顶点到片元着色器有一个插值过程,所以不能在顶点着色器中进行齐次除法,因此需要乘回w 分量,计算公式:(vc 为裁剪空间下的顶点坐标、vcw为该坐标的w分量)
vcw*(vc/vcw*0.5+0.5) => 0.5vc+0.5vcw
这也是内置函数ComputeScreenPos 的实现。
然后在片元着色器中进行齐次除法获得uv 坐标,然后从步骤3继续往下执行
如果在顶点着色器使用了ComputeScreenPos获得Vs;
那么对于步骤5可以使用unity 内部提供的两个方法Linear01Depth、LinearEyeDepth 传入屏幕纹理坐标和_ZBufferParams 来获取观察空间的场景深度和0-1线性深度,函数内部的原理即为上述步骤5。这也是shaderGraph 中SceneDepth 节点的做法。_ZBufferParams 是Unity 提供的内置变量,里面包含了远近裁剪平面相关的预计算。 同理,顶点的坐标也可以使用这种方式来获得,只不过顶点的深度我们可以直接通过齐次除法来取得。
源码
Shader "Custom/Depth"
{
Properties
{
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Geometry" "RenderPipeline" = "UniversalRenderPipeline"}
LOD 200
Pass
{
HLSLPROGRAM
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#pragma vertex vert
#pragma fragment frag
struct a2v
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
//结合ComputeScreenPos 使用
//float4 screenUV : TEXCOORD5;
};
CBUFFER_START(UnityPerMaterial)
CBUFFER_END
TEXTURE2D_X_FLOAT(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
v2f vert(a2v i)
{
v2f o;
o.vertex = TransformObjectToHClip(i.vertex.xyz);
//需要在片元中做透视除法
//o.screenUV = ComputeScreenPos(o.vertex,_ProjectionParams.x);
return o;
}
half4 frag(v2f i) : SV_TARGET
{
//顶点深度
half dv = i.vertex.w;
//顶点01 深度
half dv01 = i.vertex.w / _ProjectionParams.z;
//直接获取屏幕纹理坐标
float2 screenUV = i.vertex.xy / _ScreenParams.xy;
//结合ComputeScreenPos 计算屏幕纹理坐标
//float2 screenUV = i.screenUV.xy / i.screenUV.w;
//深度图中存储的深度,需要逆推回到观察空间
float dd = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV).r;
//观察空间中的0-1场景深度
float ds01 = Linear01Depth(dd, _ZBufferParams);
//观察空间中的场景深度
float ds = LinearEyeDepth(dd, _ZBufferParams);
return ds01;
}
ENDHLSL
}
}
}