[TinyRenderer] Lesson 4透视投影

本文介绍了二维和三维图形变换的基础,包括线性变换、仿射变换和齐次坐标。通过矩阵表示,阐述了缩放、错切、旋转和平移的概念,特别是如何使用齐次坐标实现平移。还探讨了透视投影,解释了如何通过3x3矩阵实现中心投影,从而创建出具有深度感的图像。最后,文章强调了组合变换的重要性及其在图形编程中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

翻译

原文地址
英语好的直接看原项目

1 目标

在之前的课程中,我们通过简单地移除 z 坐标来渲染我们的正交投影模型。 今天的目标是学习如何透视投影完成绘制:

在这里插入图片描述

2 二维几何

2.1 线性变换

平面上的线性变换可以用对应的矩阵来表示。 如果我们取一个点 (x,y),那么它的变换可以写成如下:

最简单(不是退化)的变换是恒等式,它不移动任何一点:
在这里插入图片描述

矩阵的对角系数沿坐标轴进行缩放。 让我们来说明一下,如果我们进行以下转换:
在这里插入图片描述

然后白色物体(切掉一个角的白色正方形)将变成黄色物体。 红色和绿色线段分别给出与 x 和 y 对齐的单位长度向量:

在这里插入图片描述

All the images for this article were generated using this code.

本文的所有图像都是使用 此代码 生成的。

我们为什么要用矩阵? 因为它很方便。 首先,用矩阵的形式,我们可以像这样表达整个对象的变换:

在这里插入图片描述

在这个表达式中,变换矩阵与前一个相同,但 2x5 矩阵只是我们方形对象的顶点。 我们简单地将数组中的所有顶点乘以变换矩阵,得到变换后的对象。 很酷吧?

好吧,真正的原因隐藏在这里:很多时候,我们希望通过连续多次变换来变换我们的对象。 想象一下,在您的源代码中,您编写了转换函数,例如

vec2 foo(vec2 p) return vec2(ax+by, cx+dy);
vec2 bar(vec2 p) return vec2(ex+fy, gx+hy);
[..]
for (each p in object) {
    p = foo(bar(p));
}

这段代码对我们对象的每个顶点执行两次线性变换,我们通常以百万为单位计算这些顶点。 而且连续几十次变换也不是什么稀罕事,导致上千万次的操作,真的很贵。 在矩阵形式中,我们可以预乘所有的变换矩阵并一次变换我们的对象。 对于只有乘法的表达式,我们可以将括号放在我们想要的位置,可以吗?

好的,让我们继续。 我们知道矩阵的对角线系数沿坐标轴缩放我们的对象。 还有其他系数的作用是什么? 让我们考虑以下转换:

在这里插入图片描述

这是它对我们对象的作用:
在这里插入图片描述

它是沿 x 轴的简单剪切。 另一个反对角元素沿 y 轴剪切我们的空间。 因此,平面上有两种基本线性变换:缩放和错切。 很多读者反应:等等,旋转呢?!

tips: 错切是在某方向上,按照一定的比例对图形的每个点到某条平行于该方向的直线的有向距离做放缩得到的平面图形。

From https://blog.csdn.net/LaoYuanPython/article/details/113856503

shear是保持图形上各点的某一坐标值不变,而另一坐标值关于该保持不变坐标值进行线性变换。坐标不变的轴称为依赖轴,其余坐标轴称为方向轴。

类似于在图像外接平行四边形固定一边的情况下,在该固定边的对边某个角施加了一个推力,该推力的作用线与x或y轴方向平行,在该推力的作用下图像的外接平行四边形发送的形变就是shear,可以想象一下,一个正方形在推力作用下变成一个平行四边形的场景,下图是正方形ABCD经过shear变成平行四边形A’B’CD:

在这里插入图片描述

因此从该变换的功能说,shear翻译成剪切不是很合适,翻译为推移更直白、而翻译成错切则既兼顾了英文单词含义、又表达形象贴切。

事实证明,任何旋转(围绕原点)都可以表示为三个错切的复合动作,这里白色物体被转换为红色的,然后是绿色的,最后是蓝色的:

但是这些都是复杂的细节,为了简单起见,可以直接写一个旋转矩阵(你还记得预乘技巧吗?):
在这里插入图片描述

我们可以以任意顺序相乘,但是让我们记住,矩阵的乘法是不可交换的:

在这里插入图片描述

这是有道理的:错切一个物体然后旋转它与旋转它然后错切它是不一样的!

在这里插入图片描述

3 二维仿射变换

因此,平面上的任何线性变换都是比例变换和错切变换的组合。 这意味着我们可以做任何我们想要的线性变换,原点永远不会移动! 那些可能性很大,但是如果我们不能进行简单的平移,我们的编程就会很痛苦。 我们可以吗? 好的,平移不是线性的,没问题,让我们在执行线性部分之后尝试附加平移:
在这里插入图片描述

这个表达方式真的很酷。 我们可以旋转、缩放、错切和平移。 但是,让我们回想一下,我们对组合多个转换感兴趣。 这是两个转换的组合的样子(请记住,我们需要组合几十个):
在这里插入图片描述

只是一个顶点(x,y)变换就得嵌套这么多层,这显然是不行的。

4 齐次坐标

好的,现在是黑魔法的时候了。 想象一下,我将一列和一行添加到我们的变换矩阵(从而使其成为 3x3),并将一个始终等于 1 的坐标附加到要变换的向量:
在这里插入图片描述

如果我们将这个矩阵和增加 1 的向量相乘,我们会得到另一个向量,最后一个分量为 1,但其他两个分量的形状正是我们想要的! 很棒。

事实上,这个想法很简单。 平行平移在 2D 空间中不是线性的。 所以我们将 2D 嵌入到 3D 空间中(通过简单地为第 3 个组件添加 1)。 这意味着我们的 2D 空间是 3D 空间中的平面 z=1。 然后我们执行线性 3D 转换并将结果投影到我们的 2D 物理平面上。 平行变换并没有变成线性的,但是流水线很简单。

我们如何将 3D 投影回 2D 平面? 只需除以 3d 分量:

在这里插入图片描述

4.1 等一下,禁止除以零!

谁说的? 让我们回顾一下管道:

  • 我们将 2D 嵌入到 3D 中,将其置于平面 z=1 内
  • 我们在 3d 中做任何我们想做的事
  • 对于我们想要从 3D 投影到 2D 的每个点,我们在原点和要投影的点之间画一条直线,然后我们找到它与平面 z=1 的交点。

在此图像中,我们的 2D 平面为洋红色,点 (x,y,z) 投影到 (x/z, y/z) 上:

在这里插入图片描述

让我们想象一条通过点 (x,y,1) 的垂直轨道。 点 (x,y,1) 将投影到哪里? 答案是,在 (x,y) 平面上:

在这里插入图片描述

现在让我们下降到轨道上,例如点 (x,y,1/2) 投影到 (2x, 2y) 上:

在这里插入图片描述

让我们继续,点(x,y,1/4) 成为(4x, 4y):

在这里插入图片描述

如果我们继续这个过程,接近 z=0,那么投影在 (x,y) 方向上离原点更远。 换句话说,点 (x,y,0) 投影到方向 (x,y) 上的无限远点。 它是什么? 对,它只是一个向量!

齐次坐标允许区分向量和点。 如果程序员写vec2(x,y),它是向量还是点? 很难说。 在齐次坐标中,z=0 的所有事物都是向量,其余的都是点。 看:向量+向量=向量。 矢量-矢量=矢量。 点+矢量=点。 这样比较好,不是吗?

4.2 复合变换

正如我之前所说,我们应该能够积累几十个转换。 为什么? 假设我们需要围绕一个点 (x0,y0) 旋转一个对象 (2D)。 怎么做? 好吧,我们可以在某个地方查找公式,或者我们可以手动完成,我们拥有所需的所有工具!

我们知道绕原点旋转,我们知道如何平移。 这就是我们所需要的:将 (x0,y0) 平移成原点,旋转,取消平移,完成:

在这里插入图片描述

在 3D 中,动作序列会更长一些,但想法是一样的:我们只需要知道很少的基本变换,借助它们,我们就可以表示任何组合动作。

5 等一下,3×3矩阵的底部是做什么用的呢

当然! 让我们将以下变换应用于我们的标准方形对象:

在这里插入图片描述

回想一下,原始对象是白色的,单位轴矢量是红色和绿色的:

这是转换后的对象:

在这里插入图片描述

在这里,另一种魔法(白色!)发生了。 你还记得我们的 y-buffer 练习吗? 在这里我们将做同样的事情:我们将 2D 对象投影到垂直线 x=0 上。 让我们稍微强化一下规则:我们必须使用中心投影,我们的相机位于点 (5,0) 并指向原点。 为了找到投影,我们需要在相机和要投影的点(黄色)之间绘制直线,并找到与屏幕线的交点(白色垂直)。

现在我用转换后的对象替换原始对象,但我没有触摸我们之前绘制的黄线:

如果我们使用标准正交投影将红色物体投影到屏幕上,那么我们会找到完全相同的点! 让我们仔细看看转换是如何工作的:所有垂直段都被转换为垂直段,但是靠近相机的那些被拉伸,远离相机的那些被缩小。 如果我们正确选择系数(在我们的变换矩阵中是 -1/5 系数),我们会获得透视(中心)投影的图像!

6 是时候全 3D 工作了

让我们解释一下魔法。 对于 2D 仿射变换,对于 3D 仿射变换,我们将使用齐次坐标:点 (x,y,z) 增加 1 (x,y,z,1),然后我们将其转换为 4D 并投影回 3D . 例如,如果我们进行以下转换:
在这里插入图片描述

逆投影为我们提供了以下 3D 坐标:

在这里插入图片描述

让我们记住这个结果,但暂时搁置它。 让我们回到中心投影的标准定义,没有任何花哨的东西作为 4D 转换。 给定一个点 P=(x,y,z),我们想将它投影到平面 z=0 上,相机在 z 轴上的点 (0,0,c) 上:

在这里插入图片描述

三角形 ABC 和 ODC 是相似的。 这意味着我们可以写成:|AB|/|AC|=|OD|/|OC| => x/(c-z) = x’/c。 换一种说法:

在这里插入图片描述

通过对三角形 CPB 和 CP’D 进行相同的推理,很容易找到以下表达式:
在这里插入图片描述

它与我们刚才搁置的结果非常相似,但是我们通过单个矩阵乘法得到了结果。 我们得到系数的定律:r = -1/c。

7 让我们总结一下:今天的主要公式

如果你只是复制粘贴这个公式而不理解上面的内容,我不喜欢这样的你。

因此,如果我们想使用位于 z 轴上、距原点距离为 c 的相机**(重要!)相机计算中心投影**,那么我们通过将点增加 1 将其嵌入到 4D 中,然后我们 将其与以下矩阵相乘,并将其逆投影为 3D。

在这里插入图片描述

我们以某种方式变形了我们的对象,只要忘记它的 z 坐标,我们就会得到一个透视图。 如果我们想使用 z-buffer,那么自然不要忘记 z。 代码可在 此处 获得,其结果在文章的开头可见。


快速修复上述代码的编译错误(C++ 14/17):

In geometry.cpp, changing from:

template <> template <> Vec3<int>::Vec3<>(const Vec3<float> &v) : x(int(v.x+.5)), y(int(v.y+.5)), z(int(v.z+.5)) {}
template <> template <> Vec3<float>::Vec3<>(const Vec3<int> &v) : x(v.x), y(v.y), z(v.z) {}

To:

template <> template <> Vec3<int>::Vec3(const Vec3<float>& v) : x(int(v.x + .5)), y(int(v.y + .5)), z(int(v.z + .5)) {}
template <> template <> Vec3<float>::Vec3(const Vec3<int>& v) : x(v.x), y(v.y), z(v.z) {}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值