在一个开了深度雾,平面和天空盒由头摄像机渲染,而材质球由正交相机渲染的场景下,调节正交相机的近裁剪面为负时,会出现材质球突变成雾的颜色的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的投影矩阵:
设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]即可。也就是
而Unity并不是用的这个公式,而是直接用的(z_{clip})进行映射。我是这样理解的,(z_{clip})的范围可以很容易的得出来是[0, f],而我们想要将z映射的范围是[n, f],用(z_{clip})近似也是可以的。以下的推导也以[0, f]为准
OpenGL
OpenGL的投影矩阵
跟上面的推导类似,(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的代码是一致的。
D3D & Reversed-Z
D3D的reverse-Z是在计算投影矩阵时,near Plane映射到1,而far Plane映射到0。简单来说,可以将原来的D3D投影矩阵的n与f交换,就可以得到Reversed-Z下的投影矩阵。
(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投影矩阵为
(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);
参考
- OpenGL Projection Matrix, http://www.songho.ca/opengl/gl_projectionmatrix.html
- Depth Precision Visualized, https://developer.nvidia.com/content/depth-precision-visualized
- Emil Persson, Depth Buffer Precision, http://www.humus.name/Articles/Persson_CreatingVastGameWorlds.pdf
文章来源: 博客园
- 还没有人评论,欢迎说说您的想法!