Z深度相关知识

渲染中深度信息很重要,但是也很让人迷惑,透视投影是什么,为什么要做透视除法,view空间,clip空间,ndc空间对应的z值又代表什么,这里简单总结下。

一.顶点变换的完整过程

二.View空间下的顶点和Z值

        输入顶点在经过MV矩阵变换后,变化到View空间,也就是相机视锥空间(上图中的Eye Space),在这个空间内的z值代表着顶点到摄像机Z方向的距离(也就是相机到幕布的垂直距离,下图中的Depth而不是Distance),在View空间内Z_{view}是线性的 

在该空间内,假设有一点(X_{view}Y_{view}Z_{view}) ,转成齐次坐标后为(X_{view}Y_{view}Z_{view},1)

 三.Clip空间下的顶点和Z值

        在这一步,我们暂且先放下,只假设该空间内的顶点为(X_{clip},Y_{clip},Z_{clip},W_{clip})下看看接下来我们目标NDC空间所需的顶点和Z值长什么样。

 四.NDC空间下的顶点和Z值

        NDC标准空间下的顶点值我们记为(X_{ndc}Y_{ndc}Z_{ndc}, 1).在渲染管线NDC空间中,X_{ndc}

Y_{ndc}范围为-1到1, Z_{ndc}在在openGL环境下是范围是-1到1,DirectX中是0到1。

X_{ndc}X_{view}Y_{ndc}Y_{view}之间是存在联系的:

 具体推导过程详见这篇文章:透视投影矩阵的推导 - bluebean - 博客园

这里单说结论:其中x^{''}X_{ndc},x即X_{view},y方向同理,下图中的z为Z_{view}

 已知了他们的关系,假设存在一个矩阵变换,使得View空间中的顶点 (X_{view}Y_{view}Z_{view},1)可以直接转换到(X_{ndc}Y_{ndc}Z_{ndc}, 1),那么该矩阵续满足(变量脑补替换下,xyz对应X_{view}Y_{view}Z_{view}):

我们发现求解

很难找出合适的m00、m02,因为左边x和z是以加法的形式相邻,右边z确成为了x的分母。

解决方法:将右边的以四维列向量表示的坐标每一项乘以z,所以有:

 所以可以求得矩阵为

再根据其他两个特殊条件,Z_{view}在近平面时Z_{ndc}为-1. Z_{view}在远平面时Z_{ndc}为1.

最后求得投影矩阵为

 将这样的矩阵乘以视锥体内的一个顶点坐标(X_{view}Y_{view}Z_{view},1),得到一个新的向量(X_{clip},Y_{clip},Z_{clip},W_{clip}),再将这个向量的每个分量除以第四个分量(Z_{view})(即透视除法),这样就可以得到顶点映射到NDC的坐标(X_{ndc}Y_{ndc}Z_{ndc}, 1),这时Z_{ndc}Z_{view}不再是线性关系,Z_{ndc} 与Z_{view}的倒数是线性关系

五.再梳理一遍

那么以为的个人理解,开始回推一下整个流程。

1.为什么要有透视除法?

因为为了求出消除Z分量影响的投影矩阵

2.为什么要用消除Z分量影响的投影矩阵?

因为这样整个渲染中,一个相机只需要一个投影矩阵,否则,每个顶点都要传入其带有Z分量影响的投影矩阵。

3.流程再梳理:

 (X_{view}Y_{view}Z_{view},1),Z_{view}线性的经过投影矩阵变换 

变为(X_{clip},Y_{clip},Z_{clip},W_{clip}

注意矩阵中的第四行三列,值为1,也即W_{clip} = Z_{view}。大家在很多shader中经常会看到用Clip中w分量去计算一些东西,原理其实就是这样,其实它存的就是View空间中的Z_{view}值大小。

X_{clip},Y_{clip},Z_{clip},W_{clip})= (X_{clip},Y_{clip},Z_{clip},​​​​​ Z_{view}​​),在Clip空间中Z_{clip}Z_{view}还是线性关系

并且X_{clip}Y_{clip}Z_{clip}的范围均为-W_{clip}W_{clip}(Opengl),在Clip空间会进行裁剪,之后

X_{clip},Y_{clip},Z_{clip},W_{clip})经过透视除法,除以W_{clip}后,就得到了NDC空间中的坐标

 (X_{ndc}Y_{ndc}Z_{ndc}, 1)。这时Z_{ndc}Z_{view}不再是线性关系,Z_{ndc} 与Z_{view}的倒数是线性关系

六.Unity中相关的一些问题

1.VertexShader中的顶点输出是啥?

        Vertex Shader的输出在Clip Space,即 (X_{clip},Y_{clip},Z_{clip},​​​​​ Z_{view}​​)

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博客公式为

Z_{view}(视空间) = 1 / (param1 * Z_{ndc} + 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空间?

有知道的大佬可以指教下,知乎上的一个回答是:

 

        

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
点云转化为深度图的过程可以理解为将三维坐标映射到二维平面上,也就是将点云中每个点的深度信息转化为像素的灰度值。下面是一个简单的点云转深度图的 C 代码示例: ```c #include <stdio.h> #include <stdlib.h> #include <math.h> #define WIDTH 640 #define HEIGHT 480 typedef struct { float x, y, z; } Point3f; typedef struct { unsigned char r, g, b; } ColorRGB; typedef struct { Point3f pos; ColorRGB color; } Vertex; // 将点云投影到深度图上 void projectPointCloud(Vertex *pointCloud, int numPoints, unsigned short *depthMap) { // 将深度图初始化为最大值 for (int i = 0; i < WIDTH * HEIGHT; i++) { depthMap[i] = 0xFFFF; } float fx = 525.0f; // 相机内参 float fy = 525.0f; float cx = 319.5f; float cy = 239.5f; for (int i = 0; i < numPoints; i++) { Point3f pos = pointCloud[i].pos; int x = (int)(pos.x * fx / pos.z + cx); int y = (int)(pos.y * fy / pos.z + cy); // 将点的深度信息转化为灰度值 unsigned short depth = (unsigned short)(pos.z * 1000.0f); if (x >= 0 && x < WIDTH && y >= 0 && y < HEIGHT && depth < depthMap[y * WIDTH + x]) { depthMap[y * WIDTH + x] = depth; } } } int main(void) { int numPoints = 10000; Vertex *pointCloud = (Vertex *)malloc(sizeof(Vertex) * numPoints); unsigned short *depthMap = (unsigned short *)malloc(sizeof(unsigned short) * WIDTH * HEIGHT); // 生成随机点云 for (int i = 0; i < numPoints; i++) { pointCloud[i].pos.x = ((float)rand() / RAND_MAX - 0.5f) * 10.0f; pointCloud[i].pos.y = ((float)rand() / RAND_MAX - 0.5f) * 10.0f; pointCloud[i].pos.z = ((float)rand() / RAND_MAX) * 5.0f + 5.0f; pointCloud[i].color.r = (unsigned char)(255 * (pointCloud[i].pos.x / 10.0f + 0.5f)); pointCloud[i].color.g = (unsigned char)(255 * (pointCloud[i].pos.y / 10.0f + 0.5f)); pointCloud[i].color.b = (unsigned char)(255 * (pointCloud[i].pos.z / 10.0f)); } // 将点云投影到深度图上 projectPointCloud(pointCloud, numPoints, depthMap); // 保存深度图 FILE *fp = fopen("depthMap.bin", "wb"); fwrite(depthMap, sizeof(unsigned short), WIDTH * HEIGHT, fp); fclose(fp); free(pointCloud); free(depthMap); return 0; } ``` 这段代码中,我们首先定义了一个点云数据结构 `Vertex`,其中包含了每个点的坐标和颜色信息。然后我们实现了一个 `projectPointCloud` 函数,该函数将点云投影到深度图上,其中使用了相机内参和透视投影的知识。最后我们生成了随机点云数据,将其投影到深度图上,并将深度图保存到文件中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值