UNITY_Z_0_FAR_FROM_CLIPSPACE的作用

fogerror
在一个开了深度雾,平面和天空盒由头摄像机渲染,而材质球由正交相机渲染的场景下,调节正交相机的近裁剪面为负时,会出现材质球突变成雾的颜色的bug。

需要把URP源码中的 #define _FOG_FRAGMENT 1 注释掉

一般来说,连续调节某个数值,变化也应当是连续的,而雾出现这种情况必然有哪个地方不对劲。

解析UNITY_Z_0_FAR_FROM_CLIPSPACE的实现#

通过查看雾的源码,找到了UNITY_Z_0_FAR_FROM_CLIPSPACE,它是Unity内置线性雾中计算Factor的关键部分。

real ComputeFogFactor(float zPositionCS)
{
	float clipZ_0Far = UNITY_Z_0_FAR_FROM_CLIPSPACE(zPositionCS);
	return ComputeFogFactorZ0ToFar(clipZ_0Far);
}

ComputeFogFactorZ0ToFar(clipZ_0Far)的意思是将clipZ_0Far根据雾的start和end位置,计算出雾的混合因子(Factor)。clipZ_0Far是相机空间下Z的位置。我们看一下它是如何得出来的。

if UNITY_REVERSED_Z
// TODO: workaround. There's a bug where SHADER_API_GL_CORE gets erroneously defined on switch.
#if (defined(SHADER_API_GLCORE) && !defined(SHADER_API_SWITCH)) || defined(SHADER_API_GLES) || defined(SHADER_API_GLES3)
//GL with reversed z => z clip range is [near, -far] -> remapping to [0, far]
#define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) max((coord - _ProjectionParams.y)/(-_ProjectionParams.z-_ProjectionParams.y)*_ProjectionParams.z, 0)
#else
//D3d with reversed Z => z clip range is [near, 0] -> remapping to [0, far]
        //max is required to protect ourselves from near plane not being correct/meaningful in case of oblique matrices.
#define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) max(((1.0-(coord)/_ProjectionParams.y)*_ProjectionParams.z),0)
#endif
#elif UNITY_UV_STARTS_AT_TOP
    //D3d without reversed z => z clip range is [0, far] -> nothing to do
#define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord)
#else
//Opengl => z clip range is [-near, far] -> remapping to [0, far]
#define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) max(((coord + _ProjectionParams.y)/(_ProjectionParams.z+_ProjectionParams.y))*_ProjectionParams.z, 0)
#endif

这个宏分为是否Reversed-Z、是否是D3D共4种情况。它的作用是将ClipSpace的Z值映射到[0, Far]的范围。

假设一点经过mvp矩阵变换后,我们得到齐次空间下的点,再除以w可以得到NDC空间下的点。雾效的计算需要知道shading point在雾的start和end范围内的比例,而start和end是相机空间下的,我们需要求出shading point在相机空间下的z值。

下面会用到投影矩阵,所有的矩阵都是列向量。假设屏幕宽高比为α,Fov为θ,近裁剪面为n,远裁剪面为f。

D3D#

D3D的投影矩阵:

[1αtan(θ/2)00001tan(θ/2)0000ffnfnfn0010]

设shading point在相机空间下的坐标为(x,y,z,w),相机空间下z的范围是[n, f],与投影矩阵相乘得到clip space下的坐标。
我们不需要考虑xy,clip space下的zw为(zffnfnfn,z),我们将其记为zclipwclip

将其除以w,转换到NDC空间下,得到zndc=ffn·znz,范围是[0, 1],那么将其映射到[near, far]即可。也就是

lerp(n,f,zndc)=n·(1zndc)+f·zndc=n+f·1zclipwclip

而Unity并不是用的这个公式,而是直接用的zclip进行映射。我是这样理解的,zclip的范围可以很容易的得出来是[0, f],而我们想要将z映射的范围是[n, f],用zclip近似也是可以的。以下的推导也以[0, f]为准

OpenGL#

OpenGL的投影矩阵

[1αtan(θ/2)00001tan(θ/2)0000(f+n)fn2nffn0010]

跟上面的推导类似,zclip=(f+n)·z2nffn,在OpenGL的投影矩阵中,z的取值范围是[-f, -n],所以zclip的范围是[-n, f](near plane时为-n,far plane时为f)。将其映射到[0, f]可以得出映射关系是f·(zclip+n)f+n,与unity的代码是一致的。
20230417234416

D3D & Reversed-Z#

D3D的reverse-Z是在计算投影矩阵时,near Plane映射到1,而far Plane映射到0。简单来说,可以将原来的D3D投影矩阵的n与f交换,就可以得到Reversed-Z下的投影矩阵。

[1αtan(θ/2)00001tan(θ/2)0000nnffnnf0010]

zclip=n(zf)nf,取值范围是[n, 0](near plane时为n,far plane时为0),将其映射到[0, f],可以得到映射关系为
f·(1zclipn)

OpenGL & Reverse-Z#

同上,OpenGL的Reversed-Z投影矩阵为

[1αtan(θ/2)00001tan(θ/2)0000(f+n)nf2nfnf0010]

zclip=(f+n)·z+2nffn,取值范围为[n, -f](near plane时为n,far plane时为-f),将其映射到[0, f],可以退出映射关系为
f(nz)n+f

结论#

以上是在透视相机下的雾的UNITY_Z_0_FAR_FROM_CLIPSPACE的推导,而这个宏里没有考虑正交相机(正交相机的w部分为1),zclip在D3D下的范围是[0, 1],在OpenGL下的范围是[-1, 1],要映射到[0, f],与上面透视投影矩阵的推导方式肯定是不同的,所以就出现了显示错误。

而在开启_FOG_FRAGMENT宏的时候,效果没有问题,是因为直接使用了摄像机空间下的深度进行了FogFactor的计算,正确且直接。

float viewZ = -(mul(UNITY_MATRIX_V, positionWS).z);
// View Z is 0 at camera pos, remap 0 to near plane.
float nearToFarZ = max(viewZ - _ProjectionParams.y, 0);
fogFactor = ComputeFogFactorZ0ToFar(nearToFarZ);

参考#

posted @   silence394  阅读(259)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示