渲染中深度信息很重要,但是也很让人迷惑,透视投影是什么,为什么要做透视除法,view空间,clip空间,ndc空间对应的z值又代表什么,这里简单总结下。
一.顶点变换的完整过程
二.View空间下的顶点和Z值
输入顶点在经过MV矩阵变换后,变化到View空间,也就是相机视锥空间(上图中的Eye Space),在这个空间内的z值代表着顶点到摄像机Z方向的距离(也就是相机到幕布的垂直距离,下图中的Depth而不是Distance),在View空间内是线性的
在该空间内,假设有一点(, ,) ,转成齐次坐标后为(, ,,1)
三.Clip空间下的顶点和Z值
在这一步,我们暂且先放下,只假设该空间内的顶点为(,,,)下看看接下来我们目标NDC空间所需的顶点和Z值长什么样。
四.NDC空间下的顶点和Z值
NDC标准空间下的顶点值我们记为(,,, 1).在渲染管线NDC空间中,
范围为-1到1, 在在openGL环境下是范围是-1到1,DirectX中是0到1。
和,和之间是存在联系的:
具体推导过程详见这篇文章:透视投影矩阵的推导 - bluebean - 博客园
这里单说结论:其中即,x即,y方向同理,下图中的z为
已知了他们的关系,假设存在一个矩阵变换,使得View空间中的顶点 (, ,,1)可以直接转换到(,,, 1),那么该矩阵续满足(变量脑补替换下,xyz对应):
我们发现求解
很难找出合适的m00、m02,因为左边x和z是以加法的形式相邻,右边z确成为了x的分母。
解决方法:将右边的以四维列向量表示的坐标每一项乘以z,所以有:
所以可以求得矩阵为
再根据其他两个特殊条件,在近平面时为-1. 在远平面时为1.
最后求得投影矩阵为
将这样的矩阵乘以视锥体内的一个顶点坐标(, ,,1),得到一个新的向量(,,,),再将这个向量的每个分量除以第四个分量()(即透视除法),这样就可以得到顶点映射到NDC的坐标(,,, 1),这时与不再是线性关系, 与的倒数是线性关系
五.再梳理一遍
那么以为的个人理解,开始回推一下整个流程。
1.为什么要有透视除法?
因为为了求出消除Z分量影响的投影矩阵
2.为什么要用消除Z分量影响的投影矩阵?
因为这样整个渲染中,一个相机只需要一个投影矩阵,否则,每个顶点都要传入其带有Z分量影响的投影矩阵。
3.流程再梳理:
(, ,,1),是线性的经过投影矩阵变换
变为(,,,)
注意矩阵中的第四行三列,值为1,也即 = 。大家在很多shader中经常会看到用Clip中w分量去计算一些东西,原理其实就是这样,其实它存的就是View空间中的值大小。
(,,,)= (,,, ),在Clip空间中与还是线性关系。
并且的范围均为-到(Opengl),在Clip空间会进行裁剪,之后
(,,,)经过透视除法,除以后,就得到了NDC空间中的坐标
(,,, 1)。这时与不再是线性关系, 与的倒数是线性关系
六.Unity中相关的一些问题
1.VertexShader中的顶点输出是啥?
Vertex Shader的输出在Clip Space,即 (,,, )
2.Fragment Shader的输入是在什么空间?
不是NDC,而是屏幕空间Screen Space。
我们前面说到Vertex Shader的输出在Clip Space,接着GPU会自己做透视除法变到NDC。这之后GPU还有一步,应用视口变换,转换到Screen Space,输入给Fragment Shader:
(Vertex Shader) => Clip Space => (透视除法) => NDC => (视口变换) => Screen Space => (Fragment Shader)
3.LinearEyeDepth&Linear01Depth
NDC空间中的深度值(深度贴图中存储的值(范围为0到1,Opengl需要从-1到1映射到0到1))如何能反推得到View空间中的深度值呢,具体推倒见该文章Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果)_puppet_master的专栏-CSDN博客公式为
(视空间) = 1 / (param1 * + param2)
其中param1 = (N - F)/ NF,param2 = 1 / N。
Unity自带Shader中关于深度值LinearEyeDepth的处理:
// Z buffer to linear depth
inline float LinearEyeDepth( float z )
{return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}// Values used to linearize the Z buffer (http://www.humus.name/temp/Linearize%20depth.txt)
// x = 1-far/near
// y = far/near
// z = x/far
// w = y/far
float4 _ZBufferParams;
_ZBufferParams.z = _ZBufferParams.x / far = (1 - far / near)/ far = (near - far) / near * far
_ZBufferParams.w = _ZBufferParams.y / far = (far / near) / far = 1 / near
我们推导的param1 = _ZBufferParams.z,param2 = _ZBufferParams.w,实际上Unity中LinearEyeDepth就是将透视投影变换的公式反过来,用zbuffer图中的屏幕空间depth反推回当前像素点的相机空间深度值。
下面再来看一下Linear01Depth函数,所谓01,其实也比较好理解,我们上面得到的深度值实际上是真正的视空间Z值,但是这个值没有一个统一的比较标准,所以这个时候依然秉承着映射大法好的理念,把这个值转化到01区间即可。由于相机实际上可以看到的最远区间就是F(远裁剪面),所以这个Z值直接除以F即可得到映射到(0,1)区间的Z值了:
Z(视空间01) = Z(视空间) / F = 1 / (((N - F)/ N) * depth + F / N)
Z(视空间01) = 1 / (param1 * depth + param2),param1 = (N - F)/ N = 1 - F/N,param2 = F / N。
再来看一下Unity中关于Linear01Depth的处理:
// Z buffer to linear 0..1 depth
inline float Linear01Depth( float z )
{return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}// Values used to linearize the Z buffer (http://www.humus.name/temp/Linearize%20depth.txt)
// x = 1-far/near
// y = far/near
// z = x/far
// w = y/far
float4 _ZBufferParams;
可以看出我们推导的param1 = _ZBufferParams.x,param2 = _ZBufferParams.y。也就是说,Unity中Linear01Depth的操作值将屏幕空间的深度值还原为视空间的深度值后再除以远裁剪面的大小,将视空间深度映射到(0,1)区间。
Unity应该是OpenGL风格(矩阵,NDC等),上面的推导上是基于DX风格的DNC进行的,不过,如果是深度图的话,不管怎么样都会映射到(0,1)区间的,相当于OpenGL风格的深度再进行一步映射,就与DX风格的一致了。
4.unity_CameraProjection和UNITY_MATRIX_P (float4x4)
Unity shader 里面,要获取投影矩阵,有两个变量:unity_CameraProjection (float4x4) 和 UNITY_MATRIX_P (float4x4)。需要注意的是,这两个矩阵的内容实际上不一样。unity_CameraProjection:
UNITY_MATRIX_P:
他们两个的区别是:unity_CameraProjection一直是MainCamera的投影矩阵,并且是OpenGL规范的。
UNITY_MATRIX_P是当前渲染投影矩阵,不一定是OpenGL并且也不一定是MainCamera的
What's difference between UNITY_MATRIX_P and unity_CameraProjection? - Unity Forum
5.向量从 camera space 映射到 clip space / screen space
如果想要把一个camera space的向量,从 camera space 映射到 clip space / screen space,需要采取的操作是:用投影矩阵(unity_CameraProjection) 去乘那个向量(向量的齐次坐标 w 分量为0),例如:
6.ComputeScreenPos
inline float4 ComputeScreenPos (float4 pos)
{
float4 o = pos * 0.5f;
o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
o.zw = pos.zw;
return o;
}
首先,该函数传入的参数pos为顶点变换到齐次坐标系下的坐标(ClipSpace),也就是说,在shader中,你需要这么使用:
o.pos = UnityObjectToClipPos(v.vertex);
o.screenPos = ComputeScreenPos(o.pos);
ComputeScreenPos返回的值是齐次坐标系下的屏幕坐标值,其范围为[0, w]。那么为什么Unity要这么做呢?Unity的本意是希望你把该坐标值用作tex2Dproj指令的参数值,tex2Dproj会在对纹理采样前除以w分量。当然你也可以像下面代码那样自己除以w分量后进行采样,但是效率不如内置指令tex2Dproj:
pos = UnityObjectToClipPos(v.vertex);
screenPos = ComputeScreenPos(o.pos);
tex2D(_ScreenTex, float2(screenPos.xy / screenPos.w))
七.其它相关知识
1.ZBuffer的精度问题
2.Reverse-Z
3.Z&1/Z
这些相关知识就查看这篇博客吧Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果)_puppet_master的专栏-CSDN博客,大佬已经写的非常详细了,在此感谢大佬们的无私分享
参考资料:
透视投影矩阵的推导 - bluebean - 博客园
Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果)_puppet_master的专栏-CSDN博客
写给大家看的“透视除法” —— 齐次坐标和投影 - 简书
实时渲染中的坐标系变换(4):投影变换-2 - 知乎
掘金
Unity Shader中的ComputeScreenPos函数_linuxheik的专栏-CSDN博客
What's difference between UNITY_MATRIX_P and unity_CameraProjection? - Unity Forum
八.留个疑问待研究
为什么剪裁是在Clip空间而不是NDC空间?
有知道的大佬可以指教下,知乎上的一个回答是: