Loading

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值。

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

D3D

D3D的投影矩阵:

\[\begin{bmatrix} \frac{1}{\alpha\tan{(\theta/2)}} & 0 & 0 & 0 \\ 0 & \frac{1}{tan{(\theta/2)}} & 0 & 0 \\ 0 && 0 && \frac{f}{f - n} && \frac{-fn}{f-n} \\ 0 && 0 && 1 && 0 \end{bmatrix} \]

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

将其除以w,转换到NDC空间下,得到\(z_{ndc} = \frac{f}{f-n} · \frac{z- n}{z}\),范围是[0, 1],那么将其映射到[near, far]即可。也就是

\[lerp(n, f, z_{ndc}) \\ = n· (1 - z_{ndc}) + f· z_{ndc} \\ = n+ f· (1 - \frac{z_{clip}}{{w_{clip}}}) \]

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

OpenGL

OpenGL的投影矩阵

\[\begin{bmatrix} \frac{1}{\alpha\tan{(\theta/2)}} & 0 & 0 & 0\\ 0 & \frac{1}{tan{(\theta/2)}} & 0 & 0\\ 0 && 0 && \frac{-(f + n)}{f - n} && \frac{-2nf}{f-n}\\ 0 && 0 && -1 && 0 \end{bmatrix} \]

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

D3D & Reversed-Z

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

\[\begin{bmatrix} \frac{1}{\alpha\tan{(\theta/2)}} & 0 & 0 & 0 \\ 0 & \frac{1}{tan{(\theta/2)}} & 0 & 0 \\ 0 && 0 && \frac{n}{n - f} && \frac{-fn}{n-f} \\ 0 && 0 && 1 && 0 \end{bmatrix} \]

\(z_{clip} = \frac{n(z-f)}{n-f}\),取值范围是[n, 0](near plane时为n,far plane时为0),将其映射到[0, f],可以得到映射关系为
\(f·(1 - \frac{z_{clip}}{n})\)

OpenGL & Reverse-Z

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

\[\begin{bmatrix} \frac{1}{\alpha\tan{(\theta/2)}} & 0 & 0 & 0\\ 0 & \frac{1}{tan{(\theta/2)}} & 0 & 0\\ 0 && 0 && \frac{-(f + n)}{n - f} && \frac{-2nf}{n-f}\\ 0 && 0 && -1 && 0 \end{bmatrix} \]

\(z_{clip} = \frac{(f+n)·z + 2nf}{f- n}\),取值范围为[n, -f](near plane时为n,far plane时为-f),将其映射到[0, f],可以退出映射关系为
\(\frac{f(n-z)}{n+f}\)

结论

以上是在透视相机下的雾的UNITY_Z_0_FAR_FROM_CLIPSPACE的推导,而这个宏里没有考虑正交相机(正交相机的w部分为1),\(z_{clip}\)在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 @ 2023-04-18 00:41  silence394  阅读(172)  评论(0编辑  收藏  举报