Projection矩阵 Reverse-Z推导

2020-11-12  本文已影响0人  crossous

一般DirectX透视矩阵推导

符号表:
\begin{align} &\alpha\quad\quad垂直FOV角 \\ &\beta\quad\quad水平FOV角 \\ &w、h\quad\quad宽、高 \\ &r\quad\quad宽高比\frac{w}{h} \\ &d\quad\quad当半高为1时,平面与相机的距离 \end{align}

为什么要半高为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}}
  给一点p=(x,y,z),求它的透视在半高为1平面上的点p'=(x', y', d),可以得到以下关系:
\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}
  希望转换到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,针对平台差异,和我们推导出的矩阵有一定差异。

//当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相比,差异主要体现在三方面:

//当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))
上一篇 下一篇

猜你喜欢

热点阅读