Shader中深度相关坐标的转换
简要记录一下shader中几种Depth的定义与转换规则,同时再记录下容易混淆的裁切空间,NDC,屏幕空间。
视角空间
那就一个一个看,先看UNITY_MATRIX_V,从世界坐标到视角坐标。
这个矩阵和之前类似,视角坐标系是右手,所以Z轴要翻转(UNITY_MATRIX_V这个矩阵就已经包含翻转了)。也就是Z轴朝向屏幕外。
这里的posVS.z
表示的是物体距离摄像机在摄像机forward反方向的距离。所以如果要用记得取负。记得这里和裁切平面没有什么关系。
裁切空间
这里指在顶点着色器中输出的有SV_POSITION
语义的裁切坐标。
一般直接由
float4 TransformObjectToHClip(float3 positionOS)
{
// More efficient than computing M*VP matrix product
return mul(GetWorldToHClipMatrix(), mul(GetObjectToWorldMatrix(), float4(positionOS, 1.0)));
}
这一函数直接得出。
后面的ObjectToWorldMatrix不用多说,看看前面的矩阵就是UNITY_MATRIX_VP。
来看看UNITY_MATRIX_P。即从视角空间至裁切空间的投影矩阵。
具体的推导就不写了,就记录一下结果的各个含义。
首先posCS.w = -posVS.z。
posCS.xyz 在未进行透视除法之前意义不大。
但是这里在DX和GL上就有区别了。
DX的Y轴是指向下方的,而GL的Y轴是指向上方的。
造成这一差异的原因是Untiy在DX环境中修改了投影矩阵,翻转了Y轴。
而且DX的Z轴,近平面为0,但Gl的Z轴近平面为-1
Y轴翻转
这是因为Untiy默认遵循OpenGL的约定,UV的原点(0,0)在左下角。
但是DX默认为左上角。所以unity在DX环境中时,将贴图传至GPU时,会翻转贴图(可以用RenderDOC来验证)。
这里摘自Unity坐标系变换那些事
任何引擎或者API的世界坐标都是Y轴朝上,但屏幕空间都是Y轴朝下的。所以一定历奇数次的翻转。
DX选择在视口变换时翻转。
GL则是在最后让操作系统去翻转。
首先 untiy的世界空间,视角空间,屏幕空间以及NDC全都是Y轴朝上的。
所以untiy为了统一,当环境为DX时,在投影矩阵上偷偷翻转y轴(通过RenderDoc看他的投影矩阵和用C#代码看的不一样)。
这样DX视口变换又翻转了一次
,相当于没有翻转,所以让DX和GL保持了一致。而没有被翻转即意味着在RenderDoc中看到的就是倒立的图像。
最终Untiy会在写到Buffer中时翻转一次。
而如果是GL,因为GL会在最终让操作系统翻转一次,所以unity就无需再次翻转了。
NDC坐标
可以手动做透视除法来将裁切空间坐标变成NDC。
做完透视除法后,w分量为1就没有存储的意义了。
所以NDC.xy = posCS.xy/posCS.w范围即(-1,1),相当于屏幕UV了。
在GL中,左下角为(-1,-1)右上角为(1,1)
如果单纯的让DX也这样做
那在DX中,左上角为(-1,-1)右下角(1,1),为了兼容,这里也要Y轴翻转。
所以NDC.y = - posCS.y/posCS.w
但这仅仅在我们手动计算时才需要。
而NDC.z =
posCS.z/posCS.w即深度了,近平面1,远平面0。这里untiy自动做了深度翻转(通过修改UNITY_MATRIX_P矩阵)。为了提升远处的精度。
如果是Opengl,这里就是近平面的-1到远平面的1。
屏幕空间
当参数传递至片元着色器时,这个语义的值是经历了透视除法和视口变换的。这里是由GPU完成的。
这时的posHCS.xy
表示屏幕空间,范围是从左下角的(0,0),到右上角的_ScreenParams.xy。
如果要用作UV,uv = input.positionHCS.xy /
_ScreenParams.xy;这样就映射回了(0-1)
posHCS.z则是从posCS.z映射到(0,1)。
如果是DX,则完全等同,如果是gl则要从(-1,1)映射到(0,1)
这也是最终写入深度图的值。如果直接采样深度图,得到的也是这个值。
因为NDC.w = 1没有存储的意义,所以posHCS.w与posCS.w一样,都是等于-posVS.z,表示物体距离摄像机在摄像机forward方向的距离
RawDepth
上面得到的值即原始深度,仅能用来比较大小,应该没什么物理含义。
Linear01Depth
0表示相机位置,1表示远平面。这里的深度就是线性的了,有实际物理意义了。
一般用
float Linear01Depth(float depth, float4 zBufferParam)
{
return 1.0 / (zBufferParam.x * depth + zBufferParam.y);
}
来取,简单乘以远平面,或者直接
// zBufferParam = { (f-n)/n, 1, (f-n)/n*f, 1/f }
float LinearEyeDepth(float depth, float4 zBufferParam)
{
return 1.0 / (zBufferParam.z * depth + zBufferParam.w);
}
就能得到实际物体的在Z轴上的深度了。即posVS.z。
重建坐标
在很多全屏后处理中,依据深度图来重建坐标也是经常要用的。
首先是得到UV,即左下角(0,0)右上角(1,1)的坐标。
即 float2 UV = IN.positionHCS.xy / _ScaledScreenParams.xy;
重建裁切坐标
这里的裁切坐标是进行透视除法之后的坐标
float4 positionCS = float4(positionNDC * 2.0 - 1.0, deviceDepth, 1.0);
#if UNITY_UV_STARTS_AT_TOP
positionCS.y = -positionCS.y;
#endif
取消透视除法
取消的第一步就是要计算posCS.w,即-posVS.z。
float ZVS = LinearEyeDepth(rawDepth, _ZBufferParams);
float4 posCS = ZVS * posHCS;
重建视角坐标
用逆矩阵相乘即可。
float4 positionVS = mul(invProjMatrix, posCS);
这里Untiy的ComputeViewSpacePosition函数会主动再把Z轴取反,不知道出于什么考虑。
再往下的由视角坐标再重建世界坐标就不赘述了。
但这种一步一步慢慢倒推是没必要的,untiy的Common库中有效率更高的做法,这里有一个齐次世界坐标我是没搞明白,但最终的结果是一样的。
float3 ComputeWorldSpacePosition(float2 positionNDC, float deviceDepth, float4x4 invViewProjMatrix)
{
float4 positionCS = ComputeClipSpacePosition(positionNDC, deviceDepth);
float4 hpositionWS = mul(invViewProjMatrix, positionCS);
return hpositionWS.xyz / hpositionWS.w;
}
深度偏移
对于那些需要深度偏移的shader。最后输出的深度要转换成原始深度。
最简单的方式就是
float deviceDepth = ComputeNormalizedDeviceCoordinatesWithZ(currentPos, GetWorldToHClipMatrix()).z;
#ifndef UNITY_UV_STARTS_AT_TOP
deviceDepth = (deviceDepth +1 )/2;
#endif
outDepth = deviceDepth;
总结
untiy为了统一两种API,整了一堆绕来绕去的骚操作,还全部隐藏起来不开源。
自带的FrameDebug还为不同的API自动翻转了图像,不用RenderDoc根本不知道到底图像是怎么翻转的。
总之就踩到坑再回来补充吧。