般DirectX透视矩阵推导
- α 垂直FOV角
- β 水平FOV角
- w、h 宽、高
- r 宽高比
- d 当半高为1时,平面与相机的距离
符号表:
为什么要半高为1?
因为我们希望最终计算结果在NDC空间中,范围在xy中都是[-1, 1],其中我们令y的半高为1,根据宽高比,x的半宽为r,后面我们回让x/r来达到xy都处于[-1, 1]范围内。
给一点,求它的透视在半高为1平面上的点,可以得到以下关系:
同理。
此平面上,y取值范围为[-1, 1],x取值范围为[-r, r],为保证1:1,将x除以r。
当前矩阵为:
当view空间乘此矩阵时得到:
当前矩阵为:
由此得到view->ndc z的转换函数:
希望转换到NDC时,近平面是0,远平面1,所以:
最终得到矩阵:
Reverse-Z
差异主要体现在A和B计算时的差异,我们希望NDC近平面是1,远平面为0,因此重写公式:
最终得到Reverse-Z矩阵:
Unity中的DirectX矩阵
Unity的PC平台API是DX,针对平台差异,和我们推导出的矩阵有一定差异。
- 渲染到RT时传递的矩阵,要对y取反
- 要将view矩阵取反的z重新取反,所以Unity渲染时传入的DX矩阵是这个公式:
我们可以得到剪裁空间关于z和w的函数:
并且经过齐次除法后
验证下,假如我们场景中,有个点在位置的点,因为Unity View矩阵取反特性,变为(view空间下),带入此点:,同理,,。
URP下有个函数:
//当Reverse-z(API为DX时)
// { (f-n)/n, 1, (f-n)/(n*f), 1/f }
float4 _ZBufferParams;
float LinearEyeDepth(float depth, float4 zBufferParam)
{
return 1.0 / (zBufferParam.z * depth + zBufferParam.w);
}
使用方法是在片元着色器传入SV_Position或深度图采样出来的值,注意片元着色器中的SV_Position是已经经历过透视除法,乃至视口变换的,z值相当于上边的NDC公式。
我们根据公式写一遍:
这个z是view空间的,因为view矩阵本身对z取反,这个-z操作正好让我们察觉不到view矩阵的取反操作。
同样还有Linear01Depth方法:
float Linear01Depth(float depth, float4 zBufferParam)
{
return 1.0 / (zBufferParam.x * depth + zBufferParam.y);
}
推导公式:
Unity移动平台、OpenGL透视矩阵推导
与PC端DirectX相比,差异主要体现在三方面:
- 二行二列无需对y轴取反
- w项乘项为-1
- ndc范围为[-1, 1],n映射到-1,f映射到1
注意Opengl中,相机空间是看向z轴负半轴方向,所以严格说是-n映射到-1,-f映射到1
同样写下基础矩阵:
得到的最终矩阵为:
不过因为深度要存储在0-1范围内,因此在片元着色器中得到的SV_Position值,实际上被重映射过:
//当API为OPENGL时
// { (n-f)/n, f/n, (n-f)/(n*f), 1/n }
float4 _ZBufferParams;
Unity中投影矩阵的获取
调用Camera的API即可获取矩阵:
Camera camera = Camera.main;
Matrix4x4 oglProj = camera.projectionMatrix
这个矩阵其实是“死”的,是上面我们推导出来的OpenGL(移动平台)矩阵,Unity有一个API能根据当前平台,获取当前API的矩阵:
Matrix4x4 proj = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true);
第一个参数是从Camera获取的矩阵,第二个参数是:是否渲染到RT。这个RT包括普通颜色缓冲,传入Shader的unity_MatrixVP就是这么算的:
Matrix4x4 unity_MatrixVP = proj * camera.worldToCameraMatrix;
用python构造Unity中的两个矩阵
测试用:
import glm
import numpy as np
near = n = 0.3
far = f = 1000
fov = glm.radians(60)
aspect = 2
dxproj_reverse_z = np.matrix([[1 / (aspect * np.tan(fov/2)), 0, 0, 0],
[0, -1 / np.tan(fov/2), 0, 0],
[0, 0, n / (f - n), -1],
[0, 0, n * f / (f - n), 0]])
print(glm.transpose(dxproj_reverse_z))
#print(glm.transpose(glm.perspective(fov, aspect, near, far)))
oglproj = np.matrix([
[1 / (aspect * np.tan(fov/2)), 0, 0, 0],
[0, 1 / np.tan(fov/2), 0, 0],
[0, 0, -(f + n) / (f - n), -1],
[0, 0, -2*n*f/(f-n), 0],])
print(glm.transpose(oglproj))