高效计算:从摄像机空间到屏幕空间的方向变换¶
原文出处:(Forward Scattering - The Weblog of Nicholas Chapman) 核心思想: 通过微积分求导,将一个成本高昂的几何问题(计算屏幕空间方向)转变为一个仅需8次浮点运算的超高效算法。
摘要¶
在计算机图形学中,我们经常需要知道一个3D空间中的方向(如运动矢量)在2D屏幕上是如何表现的。本文推导并实现了一种高性能算法,用于计算一个3D摄像机空间中的方向向量在2D屏幕空间中的瞬时方向。
其核心思想是:放弃“暴力”的“点减法”方法,转而通过微积分(对变换方程求导)来直接计算方向,从而将着色器中的操作数量从约60次(32次乘法、24次加法、4次除法)锐减到仅 8 次(6次乘法、2次减法)。
1. “天真”的(低效)方法¶
一个直观的、但性能低下的方法是:
-
在3D摄像机空间中,在射线上取两个点:起点 \(p\) 和一个沿方向 \(d\) 延伸一点的点 \(p + d\)。
-
将这两个点分别通过完整的投影矩阵变换和透视除法,投影到2D屏幕空间,得到 \(screen\_p1\) 和 \(screen\_p2\)。
-
通过 \(screen\_p2 - screen\_p1\) 来获得2D屏幕空间的方向。
High-level shader language
// 一个完整的摄像机空间到屏幕空间的点变换
// (包含了昂贵的矩阵乘法和透视除法)
vec2 camToScreenSpace(vec4 p_cs) {
vec4 v = projection_matrix * p_cs;
return v.xy / v.w; // 透视除法
}
// “天真”的方向变换方法
// 返回一个未标准化的向量
vec2 camSpaceDirToScreenSpaceDir_Naive(vec4 p_cs, vec4 dir_cs) {
vec4 p2_cs = p_cs + dir_cs;
vec2 screen_p1 = camToScreenSpace(p_cs);
vec2 screen_p2 = camToScreenSpace(p2_cs);
return screen_p2 - screen_p1;
}
这个方法虽然可行,但极其昂贵。它需要执行 2次 完整的矩阵向量乘法和 2次 透视除法。这在着色器中是一个巨大的性能浪费。
2. 高效的(数学推导)方法¶
优化的核心原理是:将“几何问题”转变为“微积分问题”。
我们要求的“屏幕空间方向”本质上是一个“速度”概念,即:当我的3D点 \(p\) 沿着 \(d\) 方向移动时,它在2D屏幕上的瞬时变化率(速度)是多少?
在数学上,“速度”就是“位置”对“时间”的导数(Derivative)。
2.1 简化的投影公式¶
首先,我们放弃完整的 \(projection\_matrix \times p\) 矩阵乘法,转而使用更直接的薄透镜(thin-lens)透视投影方程:
(其中 \(p\) 是摄像机空间的点, \(l\_over\_w\) 和 \(l\_over\_h\) 是由摄像机焦距和传感器宽高决定的常量。)
2.2 定义问题并求导¶
我们将3D射线定义为一个关于参数 \(t\) 的函数: \(r(t) = p + t \times d\)
-
\(p_x(t) = p_x + t \times d_x\)
-
\(p_z(t) = p_z + t \times d_z\)
将它代入 \(pss_x(t)\) 的方程(为简化,暂时忽略常量 \(l\_over\_w\) 和 \(0.5\)):
我们要求的就是 \(pss_x(t)\) 在 \(t=0\) 时刻的导数 \(\frac{d(pss_x)}{dt}\)。
这是一个 \(f(t) / g(t)\) 的形式,我们可以应用微积分中的除法定则 (Quotient Rule):
-
\(f(t) = p_x + t \times d_x\)
-
\(g(t) = -(p_z + t \times d_z)\)
-
\(f'(t) = d_x\)
-
\(g'(t) = -d_z\)
应用除法定则:
现在,我们在 \(t=0\) 时(即在射线起点 \(p\) 处)求值:
2.3 最终的优化公式¶
把我们之前忽略的常量 \(l\_over\_w\) 加回来,屏幕空间X方向的速度为:
\(ScreenDir_y\) 同理可得:
此时我们发现一个关键点: \(1 / (p_z^2)\) 这一项对于 \(X\) 和 \(Y\) 两个分量是完全相同的。它是一个标量(Scalar),只会缩放这个2D方向向量的长度,但不会改变它的方向。
既然我们的目标只是一个未标准化的(unnormalised)方向,那么这个既昂贵(需要一次乘法和一次除法)又对方向没影响的公共标量 \(1 / p_z^2\) 就可以被安全地省略掉!
这就得到了我们最终的、超高效的算法:
High-level shader language
/**
* 高效计算摄像机空间到屏幕空间的方向变换
* @param p 摄像机空间中的点 (p.z 应为负值)
* @param d 摄像机空间中的方向向量
* @param l_over_w 投影常量 (例如 ProjectionMatrix[0][0])
* @param l_over_h 投影常量 (例如 ProjectionMatrix[1][1])
* @return vec2 屏幕空间中的未标准化方向
*/
vec2 fastCamSpaceDirToScreenSpaceDir(vec4 p, vec4 d, float l_over_w, float l_over_h)
{
// l_over_w 和 l_over_h 通常可以直接从投影矩阵中获取
// 假设是标准透视投影矩阵:
// l_over_w = Proj[0][0]
// l_over_h = Proj[1][1]
return vec2(
(p.x * d.z - p.z * d.x) * l_over_w,
(p.y * d.z - p.z * d.y) * l_over_h
);
}
3. 结论¶
通过深入理解变换背后的数学原理(从几何问题转向微积分求导问题),我们将一个需要约60次浮点运算的复杂操作,优化为了一个仅包含6次乘法和2次减法的超高效公式。
这对于需要大量计算屏幕空间导数(例如用于运动模糊、LOD选择或某些后期处理效果)的场景,具有极高的性能价值。