Projection矩阵 Reverse-Z推导

原文:https://www.jianshu.com/p/cda012fb96df

般DirectX透视矩阵推导

  • α 垂直FOV角
  • β 水平FOV角
  • w、h 宽、高
  • r 宽高比\frac{w}{h}
  • d 当半高为1时,平面与相机的距离

符号表:

为什么要半高为1?
因为我们希望最终计算结果在NDC空间中,范围在xy中都是[-1, 1],其中我们令y的半高为1,根据宽高比,x的半宽为r,后面我们回让x/r来达到xy都处于[-1, 1]范围内。

\tan{\frac{\alpha}{2}} = \frac{1}{d}\quad\quad\stackrel{}\Rightarrow\quad\quad d = \frac{1}{\tan{\frac{\alpha}{2}}}
\frac{w_1}{1}=\frac{w}{h}=r\quad\quad\stackrel{}\Rightarrow\quad\quad w_1=r tan{\frac{\beta}{2}}=\frac{w_1}{d}=\frac{r}{d}=r\tan{\frac{ \alpha }{2}}

  给一点,求它的透视在半高为1平面上的点,可以得到以下关系:

\frac{y}{z}=\frac{y'}{d} \quad\quad\stackrel{}\Rightarrow\quad\quad y'=\frac{y}{z\tan{\frac{\alpha}{2}}}

  同理x'=\frac{x}{z\tan{\frac{\alpha}{2}}}
  此平面上,y取值范围为[-1, 1],x取值范围为[-r, r],为保证1:1,将x除以r
  当前矩阵为:

\left[ \begin{matrix} \frac{1}{r\tan{\frac{\alpha}{2}}} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan{\frac{\alpha}{2}}} & 0 & 0 \\ 0 & 0 & A & 1 \\ 0 & 0 & B & 0 \\ \end{matrix}\right]

  当view空间乘此矩阵时得到:
  当前矩阵为:

\left[x, y, z, 1\right] \left[ \begin{matrix} \frac{1}{r\tan{\frac{\alpha}{2}}} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan{\frac{\alpha}{2}}} & 0 & 0 \\ 0 & 0 & A & (1) \\ 0 & 0 & B & 0 \\ \end{matrix}\right] =[\frac{x}{r\tan{\frac{\alpha}{2}}}, \frac{y}{\tan{\frac{\alpha}{2}}}, Az+B, z]
\quad\quad\stackrel{divide\quad w}\Rightarrow\quad\quad [\frac{x}{rz\tan{\frac{\alpha}{2}}}, \frac{y}{z\tan{\frac{\alpha}{2}}}, A+\frac{B}{z}, 1]

  由此得到view->ndc z的转换函数:g(z)=A+\frac{B}{z}

扫描二维码关注公众号,回复: 15080880 查看本文章

  希望转换到NDC时,近平面是0,远平面1,所以:

g(f)=A+\frac{B}{f}=1\quad and\quad g(n)=A+\frac{B}{n}= 0
B=\frac{nf}{n-f}\quad A=\frac{-f}{n-f}

  最终得到矩阵:

\left[ \begin{matrix} \frac{1}{r\tan{\frac{\alpha}{2}}} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan{\frac{\alpha}{2}}} & 0 & 0 \\ 0 & 0 & \frac{-f}{n-f} & 1 \\ 0 & 0 & \frac{nf}{n-f} & 0 \\ \end{matrix}\right]

Reverse-Z

  差异主要体现在A和B计算时的差异,我们希望NDC近平面是1,远平面为0,因此重写公式:

g(f)=A+\frac{B}{f}=0\quad and\quad g(n)=A+\frac{B}{n}= 1
B=\frac{-nf}{n-f}\quad A=\frac{n}{n-f}

  最终得到Reverse-Z矩阵:

\left[ \begin{matrix} \frac{1}{r\tan{\frac{\alpha}{2}}} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan{\frac{\alpha}{2}}} & 0 & 0 \\ 0 & 0 & \frac{n}{n-f} & 1 \\ 0 & 0 & \frac{-nf}{n-f} & 0 \\ \end{matrix}\right]

Unity中的DirectX矩阵

  Unity的PC平台API是DX,针对平台差异,和我们推导出的矩阵有一定差异。

  • 渲染到RT时传递的矩阵,要对y取反
  • 要将view矩阵取反的z重新取反,所以Unity渲染时传入的DX矩阵是这个公式:

\left[ \begin{matrix} \frac{1}{r\tan{\frac{\alpha}{2}}} & 0 & 0 & 0 \\ 0 & -\frac{1}{\tan{\frac{\alpha}{2}}} & 0 & 0 \\ 0 & 0 & \frac{-n}{n-f} & -1 \\ 0 & 0 & \frac{-nf}{n-f} & 0 \\ \end{matrix}\right]

  我们可以得到剪裁空间关于z和w的函数:C(z)=\frac{-n(z+f)}{n-f}\quad and \quad C(w)=-z

  并且经过齐次除法后NDC(z)=\frac{C(z)}{C(w)}=\frac{-n(z+f)}{(n-f)(-z)}
  验证下,假如我们场景中,有个点在[0, 0, near]位置的点,因为Unity View矩阵取反特性,变为[0, 0, -near](view空间下),带入此点:C(-n)=n,同理C(-f)=0NDC(-n)=1NDC(-f)=0
  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公式。
  我们根据公式写一遍:

LinearEyeDepth(NDC(z))=\frac{1}{\frac{-n(z+f)}{(n-f)(-z)}*\frac{f-n}{nf}+\frac{1}{f} }=-z

  这个z是view空间的,因为view矩阵本身对z取反,这个-z操作正好让我们察觉不到view矩阵的取反操作。
  同样还有Linear01Depth方法:

float Linear01Depth(float depth, float4 zBufferParam)
{
    return 1.0 / (zBufferParam.x * depth + zBufferParam.y);
}

推导公式:

Linear01Depth(NDC(z))=\frac{1}{\frac{-n(z+f)}{(n-f)(-z)}*\frac{f-n}{n}+1}=-\frac{z}{f}

Unity移动平台、OpenGL透视矩阵推导

  与PC端DirectX相比,差异主要体现在三方面:

  • 二行二列无需对y轴取反
  • w项乘项为-1
  • ndc范围为[-1, 1],n映射到-1,f映射到1
    注意Opengl中,相机空间是看向z轴负半轴方向,所以严格说是-n映射到-1,-f映射到1

  同样写下基础矩阵:

\left[ \begin{matrix} \frac{1}{r\tan{\frac{\alpha}{2}}} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan{\frac{\alpha}{2}}} & 0 & 0 \\ 0 & 0 & A & (-1) \\ 0 & 0 & B & 0 \\ \end{matrix}\right]

NDC(z)=-A- \frac{B}{z}
NDC(-n)=-A-\frac{B}{-n}=-1 and NDC(-f)=-A-\frac{B}{-f}=1
B=\frac{2nf}{n-f}\quad A=\frac{n+f}{n-f}

  得到的最终矩阵为:

\left[ \begin{matrix} \frac{1}{r\tan{\frac{\alpha}{2}}} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan{\frac{\alpha}{2}}} & 0 & 0 \\ 0 & 0 & \frac{n+f}{n-f} & (-1) \\ 0 & 0 & \frac{2nf}{n-f} & 0 \\ \end{matrix}\right]

NDC(z)=\frac{(n+f)z+2nf}{(f-n)z}

不过因为深度要存储在0-1范围内,因此在片元着色器中得到的SV_Position值,实际上被重映射过:

FragSVPos(z)=NDC(Z)*0.5+0.5

//当API为OPENGL时
// { (n-f)/n, f/n, (n-f)/(n*f), 1/n }
float4 _ZBufferParams;

LinearEyeDepth(NDC(Z)*0.5+0.5)=\frac{1}{\left(\frac{(n+f)z+2nf}{-(n-f)z}*\frac{1}{2}+\frac{1}{2}\right)*\frac{n-f}{nf}+\frac{1}{ n }}= -z
Linear01Depth(NDC(Z)*0.5+0.5)=\frac{1}{\left(\frac{(n+f)z+2nf}{-(n-f)z}*\frac{1}{2}+\frac{1}{2}\right)*\frac{n-f}{n}+\frac{f}{ n }}= -\frac{z}{f}

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))

猜你喜欢

转载自blog.csdn.net/tangyin025/article/details/126564270