最近在看“红宝书”时发现了其中第165页中关于透视投影矩阵的错误。书中原式如下图所示:
其中
接下来我们自己重新推导这个矩阵。
-
首先需要明确的是,在观察空间中摄像机的取景范围将构成一个视锥体,而透视投影矩阵会将视锥体变换成一个立方体(NDC):可以想象是让视锥体的近平面保持不变,而远平面则缩放到与近平面同等大小,然后映射到 [-1,1] (或者 [0,1] )中。在远平面缩小的这个过程中,在视锥体中的物体也将跟着缩小,从而实现“近大远小”的效果。
-
其次,坐标系的偏手性非常重要,它关乎到
的符号和大小判定。本文采用的是:观察空间为右手系(换变前),NDC为左手系(换变后)。 -
最后,采用行向量还是列向量,以及映射范围的不同也将导致不一样的结果。本文采用列向量的形式,映射范围为 [-1,1].
投影点的坐标表示我们先观察下图:
由相似三角形可以得出,对于一个点
由于相机望向前方的位置是
视锥体的转换
由于观察空间是右手系,因此看向前方的
它们需要映射到
在斜截式方程表达式
因此对于点
同理,对于
如下图所示:
以上两个式子就是将
将以上两个变量代入上面
到这一步,视锥体已由锥体变为长方体。接着需要将
从这里就可以看出,这个映射颠倒了变换前(观察空间)
接着,考虑一下
- 首先,在应用透视投影矩阵后,每个顶点坐标都会被其齐次坐标
分量除(齐次除法),对于 分量来说,这意味着原始的 值会变成 的形式( 作为分母)。也就是说, 与 成比例。 - 其次,人类视觉系统对深度的感知是非线性的,我们希望远处的物体变化较小(远离摄像机的地方有较低的精度),而近处的物体变化较大(靠近摄像机的地方有更高的精度)。而我们希望在处理
分量时也能使用线性插值以获得正确的深度值。
正因为如此,在光栅化阶段顶点的
其中
解方程组,可得
将其代入以上的式子(2.4)可得关于
这里把
由式子(2.2),式子(2.3)和式子(2.5),可以得到关于齐次点的表达式如下:
提取矩阵因子
现在,可以提取式子(2.6)中关于
则透视投影矩阵已求。在“红宝书”中,变换前(观察空间)为左手系,
GAMES101中关于透视投影矩阵的推导
GAMES101中闫令琪老师引用“虎书”的推导方式:https://www.bilibili.com/video/BV1X7411F744/?spm_id_from=333.788.videopod.episodes&vd_source=57c4e007542d6a99df1a601686225ef6&p=4
其给出的推导方式更直观:首先假设已经存在一个透视投影矩阵
代码实现
代码内容比较简单,因此不注释了。
点击查看代码
public class Vector4
{
public float X { get; private set; }
public float Y { get; private set; }
public float Z { get; private set; }
public float W { get; private set; }
public Vector4(float x, float y, float z, float w)
{
X = x;
Y = y;
Z = z;
W = w;
}
// 矩阵与向量相乘的方法
public static Vector4 Multiply(float[,] matrix, Vector4 vector)
{
float x = matrix[0, 0] * vector.X + matrix[0, 1] * vector.Y + matrix[0, 2] * vector.Z + matrix[0, 3] * vector.W;
float y = matrix[1, 0] * vector.X + matrix[1, 1] * vector.Y + matrix[1, 2] * vector.Z + matrix[1, 3] * vector.W;
float z = matrix[2, 0] * vector.X + matrix[2, 1] * vector.Y + matrix[2, 2] * vector.Z + matrix[2, 3] * vector.W;
float w = matrix[3, 0] * vector.X + matrix[3, 1] * vector.Y + matrix[3, 2] * vector.Z + matrix[3, 3] * vector.W;
return new Vector4(x, y, z, w);
}
public override string ToString()
{
return $"({X:F2}, {Y:F2}, {Z:F2}, {W:F2})";
}
}
public class PerspMatrix
{
public float L { get; private set; }
public float R { get; private set; }
public float T { get; private set; }
public float B { get; private set; }
public float N { get; private set; }
public float F { get; private set; }
public PerspMatrix(float l, float r, float t, float b, float n, float f)
{
if (r == l || t == b || f == n)
throw new ArgumentException("Invalid frustum parameters: division by zero.");
L = l;
R = r;
T = t;
B = b;
N = n;
F = f;
}
public float[,] Frustum()
{
float invWidth = 1.0f / (R - L);
float invHeight = 1.0f / (T - B);
float invDepth = 1.0f / (F - N);
float[,] matrix4x4 = new float[4, 4];
matrix4x4[0, 0] = (float)(2.0 * N * invWidth);
matrix4x4[0, 1] = 0;
matrix4x4[0, 2] = (float)((R + L) * invWidth);
matrix4x4[0, 3] = 0;
matrix4x4[1, 0] = 0;
matrix4x4[1, 1] = (float)(2.0 * N * invHeight);
matrix4x4[1, 2] = (float)((T + B) * invHeight);
matrix4x4[1, 3] = 0;
matrix4x4[2, 0] = 0;
matrix4x4[2, 1] = 0;
matrix4x4[2, 2] = (float)(-(F + N) * invDepth);
matrix4x4[2, 3] = (float)(-2.0 * F * N * invDepth);
matrix4x4[3, 0] = 0;
matrix4x4[3, 1] = 0;
matrix4x4[3, 2] = -1;
matrix4x4[3, 3] = 0;
return matrix4x4;
}
public void PrintMatrix(float[,] matrix)
{
Console.WriteLine("Projection Matrix:");
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
Console.Write($"{matrix[i, j]:F2}\t");
}
Console.WriteLine();
}
}
public Vector4 Apply(Vector4 vector)
{
float[,] matrix = Frustum();
return Vector4.Multiply(matrix, vector);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了