一、说明

本文将解释如何在给定窗口空间顶点位置的情况下重新计算眼空间顶点位置。以及相反的计算。其中包括一些数学概念,需要仔细推敲。

二、定义

在开始之前,我们需要定义一些符号:

上表中,世界坐标就是绝对坐标,是一切度量的根本原点坐标。

三、来自gl_FragCoord

gl_FragCoord.xyz 是窗口空间位置 W,一个 3D 向量。gl_FragCoord.w 包含剪辑空间 W 的倒数:

g l _ F r a g C o o r d w = 1 C w . gl\_FragCoord_{w}={\tfrac {1}{C_{w}}}. gl_FragCoordw=Cw1.

给定这些值,我们有一个相当简单的方程组:
【OpenGL的数学01】从窗口空间计算视空间-LMLPHP

在 GLSL 片段着色器中,代码如下所示:

vec4 ndcPos;
ndcPos.xy = ((2.0 * gl_FragCoord.xy) - (2.0 * viewport.xy)) / (viewport.zw) - 1;
ndcPos.z = (2.0 * gl_FragCoord.z - gl_DepthRange.near - gl_DepthRange.far) /
    (gl_DepthRange.far - gl_DepthRange.near);
ndcPos.w = 1.0;

vec4 clipPos = ndcPos / gl_FragCoord.w;
vec4 eyePos = invPersMatrix * clipPos;

这假定存在一个名为 viewport 的统一,该 viewport 是一个 vec4,按照传递给该函数的顺序与 glViewport 的参数匹配。此外,这假设 invPersMatrix 是透视投影矩阵的逆矩阵(不提倡在片段着色器中计算)。请注意,gl_DepthRange 是可用于片段着色器的内置变量。

四、来自gl_FragCoord的XYZ

这种情况对于延迟渲染技术非常有用,但最后一种方法也非常有用。在延迟渲染中,我们将对象的材质参数渲染为图像。然后,我们对这些图像进行多次传递,加载这些材料参数并对它们执行照明计算。

在光通道中,我们需要重建眼空间顶点位置,以便进行照明。但是,我们实际上没有gl_FragCoord;不适用于产生材料参数的片段。取而代之的是,我们有 gl_FragCoord.xy 中的窗口空间 X 和 Y 位置,并且我们有窗口空间深度,通过访问深度缓冲区进行采样,该缓冲区也是从延迟传递中保存的。

我们缺少的是原始的窗口空间 W 坐标。

因此,我们必须找到一种方法,从窗口空间 XYZ 坐标和透视投影矩阵来计算它。本讨论将假定您的透视投影矩阵采用以下形式:

[ xx xx xx xx ]
[ xx xx xx xx ]
[ 0 0 T1 T2 ]
[ 0 0 E1 0 ]
xx 表示“任何”,它们可以是您在投影中使用的任何值。0 在投影矩阵中必须为零。T1、T2 和 E1 可以是任意项,具体取决于投影矩阵的工作方式。

如果您的投影矩阵不适合此形式,则以下代码将变得更加复杂。

4.1 从窗口到ndc

我们有窗口空间的 XYZ:

W ⃗ = [ g l _ F r a g C o o r d . x g l _ F r a g C o o r d . y f r o m D e p t h T e x t u r e ] {\vec W}={\begin{bmatrix}gl\_FragCoord.x\\gl\_FragCoord.y\\fromDepthTexture\end{bmatrix}} W = gl_FragCoord.xgl_FragCoord.yfromDepthTexture

从窗口空间计算 NDC 空间与上述相同:

N ⃗ = [ ( 2 ∗ W x ) − ( 2 ∗ V x ) V w − 1 ( 2 ∗ W y ) − ( 2 ∗ V y ) V h − 1 ( 2 ∗ W z ) − D f − D n D f − D n ] {\displaystyle {\vec {N}}={\begin{bmatrix}{\tfrac {(2*W_{x})-(2*V_{x})}{V_{w}}}-1\\{\tfrac {(2*W_{y})-(2*V_{y})}{V_{h}}}-1\\{\tfrac {(2*W_{z})-D_{f}-D_{n}}{D_{f}-D_{n}}}\end{bmatrix}}} N = Vw2Wx2Vx1Vh2Wy2Vy1DfDn2WzDfDn

请记住:在本例中,视口和景深范围参数是用于渲染原始场景的参数。当然,视口不应该改变,但深度范围肯定可以改变(假设你甚至在延迟渲染器的光照通道中有一个深度范围)。

4.2 从NDC到剪辑

为简单起见,以下是从 NDC 空间到剪辑空间的公式:

C w = T 2 N z − T 1 E 1 C ⃗ x y z = N ⃗ ∗ C w {\begin{aligned}C_{w}&={\tfrac {T2}{N_{z}-{\tfrac {T1}{E1}}}}\\{\vec C}_{{xyz}}&={\vec N}*C_{w}\end{aligned}} CwC xyz=NzE1T1T2=N Cw

推导
推导这两个等价位是非常不平凡的;这是一个相当大的绊脚石。让我们从我们所知道的开始。

我们可以从剪辑空间转换为 NDC 空间,这样我们就可以返回:

N ⃗ = C ⃗ C w C ⃗ = N ⃗ ∗ C w {\begin{aligned}{\vec N}&={\tfrac {{\vec C}}{C_{w}}}\\{\vec C}&={\vec N}*C_{w}\end{aligned}} N C =CwC =N Cw

问题是我们没有 Cw。我们之前能够使用 gl_FragCoord.w 来计算它,但是当我们在延迟照明通过中事后执行此操作时,这不可用。

那么我们如何计算它呢?好吧,我们知道剪辑空间位置最初是这样计算的:

C ⃗ = M ∗ P ⃗ {\vec C}=M*{\vec P} C =MP

因此,我们知道 Cw 是由 P 的点积与 M 的第四行计算得出的。鉴于我们上面对 M 第四行的定义,我们可以得出结论:
C w = E 1 ∗ P z N ⃗ = C ⃗ E 1 ∗ P z {\begin{aligned}C_{w}&=E1*P_{z}\\{\vec N}&={\tfrac {{\vec C}}{E1*P_{z}}}\end{aligned}} CwN =E1Pz=E1PzC

当然,这只是用一个未知来换取另一个未知。但是我们可以使用它。事实证明,Nz 与此有一些共同点:
N z = C z E 1 ∗ P z N_{z}={\tfrac {C_{z}}{E1*P_{z}}} Nz=E1PzCz

看看 Cz 的来源很有趣。和以前一样,我们知道它是由 P 的点积与第三行 M 计算得出的。再一次,鉴于我们上面对 M 的定义,我们可以得出结论:

C z = T 1 ∗ P z + T 2 ∗ P w N z = T 1 ∗ P z + T 2 ∗ P w E 1 ∗ P z {\begin{aligned}C_{z}&=T1*P_{z}+T2*P_{w}\\N_{z}&={\tfrac {T1*P_{z}+T2*P_{w}}{E1*P_{z}}}\end{aligned}} CzNz=T1Pz+T2Pw=E1PzT1Pz+T2Pw

我们这里仍然有两个未知值,Pz 和 Pw。但是,我们可以假设 Pw 为 1.0,因为眼空间位置通常就是这种情况。给定这个假设,我们只有一个未知数 Pz,我们可以求解它:
【OpenGL的数学01】从窗口空间计算视空间-LMLPHP

KaTeX parse error: Expected & or \\ or \cr or \end at position 169: …{T2}{E1*P_{z}}}}̲ \\E1*P_{z}&={\…

现在有了 Pz,我们可以计算 Cw:

C w = E 1 ∗ P z C w = T 2 N z − T 1 E 1 {\begin{aligned}C_{w}&=E1*P_{z}\\C_{w}&={\tfrac {T2}{N_{z}-{\tfrac {T1}{E1}}}}\end{aligned}} CwCw=E1Pz=NzE1T1T2

因此,我们可以从中计算出 C 的其余部分:

C ⃗ x y z = N ⃗ ∗ C w C ⃗ x y z = N ⃗ ∗ ( T 2 N z − T 1 E 1 ) {\begin{aligned}{\vec C}_{{xyz}}&={\vec N}*C_{w}\\{\vec C}_{{xyz}}&={\vec N}*({\tfrac {T2}{N_{z}-{\tfrac {T1}{E1}}}})\end{aligned}} C xyzC xyz=N Cw=N NzE1T1T2

4.3 从剪辑到眼睛

计算完整的 4D 向量 C 后,我们可以像以前一样计算 P:
P ⃗ = M − 1 C ⃗ {\vec P}=M^{{-1}}{\vec C} P =M1C

4.4 GLSL示例

下面是一些 GLSL 示例代码,说明这将是什么样子的:

uniform mat4 persMatrix;
uniform mat4 invPersMatrix;
uniform vec4 viewport;
uniform vec2 depthrange;

vec4 CalcEyeFromWindow(in vec3 windowSpace)
{
	vec3 ndcPos;
	ndcPos.xy = ((2.0 * windowSpace.xy) - (2.0 * viewport.xy)) / (viewport.zw) - 1;
	ndcPos.z = (2.0 * windowSpace.z - depthrange.x - depthrange.y) /
    (depthrange.y - depthrange.x);

	vec4 clipPos;
	clipPos.w = persMatrix[3][2] / (ndcPos.z - (persMatrix[2][2] / persMatrix[2][3]));
	clipPos.xyz = ndcPos * clipPos.w;

	return invPersMatrix * clipPos;
}

视口是包含视口参数的矢量。depthrange 是包含 glDepthRange 参数的 2D 向量。windowSpace 向量是 gl_FragCoord 的前两个分量,第三个坐标是从深度缓冲区读取的深度。

五、从gl_FragCoord的XYZ优化方法

前面的方法当然有用,但速度有点慢。我们可以通过使用顶点着色器来提供帮助,从而显着帮助计算眼距位置。这使我们能够完全避免使用逆透视矩阵。

此方法分为两步。我们首先计算 Pz,即眼空间 Z 坐标。然后用它来计算完整的眼距位置。

第一部分其实很简单。我们上面使用的大多数计算都是必要的,因为我们需要 Cw,我们必须这样做,因为我们需要一个完整的剪辑空间位置。这种优化的方法只需要得到 Pz,我们可以直接从 Wz、深度范围和投影矩阵的三个分量中计算出来:

N z = ( 2 ∗ W z ) − D f − D n D f − D n P z = T 2 E 1 ∗ N z − T 1 {\displaystyle {\begin{aligned}N_{z}&={\tfrac {(2*W_{z})-D_{f}-D_{n}}{D_{f}-D_{n}}}\\P_{z}&={\tfrac {T2}{E1*N_{z}-T1}}\end{aligned}}} NzPz=DfDn2WzDfDn=E1NzT1T2

请注意,这也意味着我们不需要片段着色器中的视口设置。我们只需要深度范围和透视矩阵项。

这种方法的诀窍如下:我们如何从 Pz 到完整的眼空间位置 P。要了解其工作原理,下面是一些快速的几何图形:
【OpenGL的数学01】从窗口空间计算视空间-LMLPHP

图中的 E 表示眼睛位置,这是眼睛空间的原点。P是我们想要的位置,Pz是我们拥有的位置。那么,我们需要什么才能从 Pz 中得到 P?我们所需要的只是一个指向 P 的向量方向,但 z 分量为 1.0。这样,我们只需将该向量乘以 Pz;结果必然是 P。

那么我们如何得到这个向量呢?

这就是顶点着色器的用武之地。在延迟渲染中,顶点着色器通常是一个简单的直通着色器,不执行实际计算,也不传递用户定义的输出。因此,我们可以自由地将其用于某些事情。

在顶点着色器中,我们简单地构造一个从原点到眼空间中每个顶点坐标的向量,即近平面上的相应位置。我们将向量的 Z 距离设置为 -1.0。这构建了一个矢量,该矢量指向眼睛空间中相机前方的场景。近平面的范围可以很容易地从对焦和纵横比中计算出来。

此值的线性插值将确保为片段计算的每个向量的 Z 值为 -1.0。线性插值也将保证它直接指向生成的片段。

我们本可以在片段着色器中计算出来,但何必呢?这需要向片段着色器提供视口转换(以便我们可以将 Wxy 转换为眼睛空间)。而且 VS 并没有做任何事情…

一旦我们有了这个值,我们只需将其乘以 Pz 即可得到我们的眼距位置 P。

下面是一些着色器代码。

// Vertex shader
// Half the size of the near plane {tan(fovy/2.0) * aspect, tan(fovy/2.0) }
uniform vec2 halfSizeNearPlane; 

layout (location=0) in vec2 clipPos;
// UV for the depth buffer/screen access.
// (0,0) in bottom left corner (1, 1) in top right corner
layout (location=1) in vec2 texCoord;

out vec3 eyeDirection;
out vec2 uv;

void main()
{
  uv = texCoord;

  eye_direction = vec3((2.0 * halfSizeNearPlane * texCoord) - halfSizeNearPlane , -1.0);
  gl_Position = vec4(clipPos, 0, 1);
}

// Fragment shader
in vec3 eyeDirection;
in vec2 uv;

uniform mat4 persMatrix;
uniform vec2 depthrange;

uniform sampler2D depthTex;
05-12 22:45