Keywords: 3D, foreshortening, stereoscopic vision, origin, coordinates, coordinate system, 3D scene, topology, model, mesh, polygon, vertices, edges, perspective projection, viewing frustum, perspective divide, similar triangles, screen space, normalize.
Understand How It Works!
如果您在这里,可能是因为您想学习计算机图形学。每个读者可能有不同的理由来到这里,但我们都被同一个愿望所驱使:了解它是如何运作的! Scratchapixel 的创建就是为了回答这个特定的问题。在这里,您将了解它的工作原理并学习用于创建 CGI 的技术,从最简单和最重要的方法到更复杂和不太常见的方法。也许你喜欢电子游戏,你想知道它是如何运作的,它们是如何制作的。也许你看过一部皮克斯电影,想知道它背后的魔力是什么。无论您是在学校、大学、已经在该行业工作(或退休),对这些主题感兴趣、学习或提高您的知识永远都不是坏事,我们总是需要像 Scratchapixel 这样的资源来找到这些问题的答案问题。这就是我们在这里的原因。
Scratchapixel 可供所有人使用。它们是所有级别的课程。当然,它需要最少的编程知识。虽然我们计划在不久的将来编写一个关于编程的快速入门课程,但 Scratchapixel 的任务不是教你编程。然而,在您将学习如何实现用于生成 CGI 的不同技术的同时,您也可能会在此过程中提高您的编程技能并学习一些编程技巧。无论您认为自己是初学者还是编程专家,您都可以在这里找到适合您水平的各种课程。从简单开始,从基本程序和进展开始。虽然我们讨论的是复杂性,但如果您查看每个类别的课程列表,您会看到一个数学标签,后跟一系列加号(符号“+”)。加分越多,数学课就越难(三个加分是最大的)。
A Gentle Introduction to Computer Graphics Programming
你想学习计算机图形学。首先你知道它是什么吗?在本节的第二课中,您可以找到计算机图形学的定义,并了解它的一般工作原理。也许您听说过建模、几何、动画、3D、2D、数字图像、3D 视口、实时渲染、合成等术语,但您不确定它们的含义,更重要的是,不确定它们之间的关系。本节的第二课将回答这些问题。从那里,您应该对 CG 编程知之甚少,但对 CG 以及制作 CGI 所涉及的不同工具和流程有一个大致的了解。
下一步是什么?我们的世界基本上是三维的。至少就我们可以用我们的感官体验而言。有些人喜欢在其中添加时间的维度。时间在 CGI 中扮演着重要的角色,但我们稍后会回到这一点。来自现实世界的物体是三维的。这是一个事实,我们认为我们都可以达成一致,而无需进行证明。然而,有趣的是,视觉,这是可以体验这个三维世界的一种感官,主要是一个二维过程。我们也许可以说在我们脑海中创造的图像是无量纲的(我们还不太清楚图像如何“出现”在我们的大脑中),但是当我们谈到图像时,它通常对我们来说意味着一个平面,在对象的维度已经从三个维度减少到两个维度(画布表面或屏幕表面)。画布上的这个图像实际上在我们的大脑中看起来准确的唯一原因,是因为物体离你站立的位置越远,它们就越小,这种效果称为透视。如果您还不相信,可以将图像想象成镜面反射。镜子的表面是完全平坦的,然而,我们无法区分从镜子反射的场景图像和直接看场景:你感知不到反射,只是感知物体。正是因为我们有两只眼睛,我们才能真正感受到 3D 中的事物,我们称之为立体视觉。每只眼睛从稍微不同的角度看同一个场景,大脑可以使用同一场景的这两个图像来近似估计 3D 空间中物体相对于彼此的距离和位置。然而,立体视觉在某种程度上是非常有限的,因为我们无法非常准确地测量到物体的距离或它们的大小(计算机可以做到)。人类的视觉是相当复杂的,是进化的一个令人印象深刻的结果,但它仍然是一个技巧,很容易被愚弄(许多魔术师的技巧都基于此)。在某种程度上,计算机图形是一种手段,通过它我们可以创建人工世界的图像并将它们呈现给大脑(通过视觉),作为对现实的体验(我们称之为照片写实),就像一面镜子反射。这个主题在科幻小说中很常见,但技术离使这成为可能并不遥远。
在这里要说的是,虽然我们似乎更关注生成这些图像的过程,我们称之为渲染的过程,但计算机图形学不仅涉及制作图像,还涉及开发模拟流体运动等事物的技术 ,软体和刚体的运动,找到动画对象和化身的方法,以便准确模拟它们的运动以及由该运动产生的所有效果(例如,当您走路时,肌肉的形状会发生变化以及身体的整体外部形状 是这些肌肉变形的结果吗?要创建逼真的头像,您需要找到模拟这些效果的方法。我们还将在 Scratchapixel 上了解这些技术。
到目前为止我们学到了什么? 世界是三维的,我们看待它的方式是二维的,如果你能复制物体的形状和外观,大脑就无法区分直接看这个物体和看 在这些物体的图像上。 计算机图形不仅限于创建逼真的图像,虽然创建非逼真的图像比创建完美的逼真的图像更容易,但计算机图形的目标显然是真实的(就像事物移动的方式而不是它们的外观一样)。
我们现在需要做的就是了解制作这种真实照片的规则是什么,这也是您将在 Scratchapixel 上学到的内容。
Describing Objects Making Up the Virtual World
实际绘制真实场景的画家(除非绘画的主题来自他/她的想象)与我们尝试用计算机创建图像之间的区别在于,我们实际上必须先以某种方式描述形状 构成我们想要渲染到计算机的图像的场景的对象的(和外观)
我们在学校学到的最简单和最重要的概念之一是可以定义点的空间概念。点的位置通常是相对于原点定义的。在标尺上,这通常是标有数字零的刻度。如果我们使用两个标尺,一个垂直于另一个,我们可以在二维中定义点的位置。添加第三个标尺,垂直于前两个标尺,您可以在三个维度上定义点的位置。表示点相对于其中一根树标尺位置的实际数字称为点坐标。我们都熟悉要标记的坐标概念,如果我们是相对于某个参考点或线(例如格林威治子午线)。我们现在可以在三个维度上定义点。假设您刚买了一台电脑。这台电脑可能装在一个盒子里,这个盒子有八个角(抱歉我说得太明显了)。描述此框的一种方法是测量这 8 个角相对于其中一个角的距离。这个角作为我们坐标系的原点,显然这个参考角相对于自身的距离在所有维度上都是 0。然而,从参考角到其他七个角的距离将不同于 0。让我们想象一下我们的盒子有以下尺寸:
corner 1: ( 0, 0, 0)
corner 2: (12, 0, 0)
corner 3: (12, 8, 0)
corner 4: ( 0, 8, 0)
corner 5: ( 0, 0, 10)
corner 6: (12, 0, 10)
corner 7: (12, 8, 10)
corner 8: ( 0, 8, 10)
第一个数字代表宽度,第二个数字代表高度,第三个数字代表角的深度。 如您所见,拐角 1 是测量所有过角的原点。 从这里开始,您需要做的就是编写一个程序,在其中定义三维点的概念,并使用它来存储刚测量的八个点的坐标。 在 C/C++ 中,这样的程序可能如下所示:
typedef float Point[3];
int main()
{
Point corners[8] = {
{ 0, 0, 0},
{12, 0, 0},
{12, 8, 0},
{ 0, 8, 0},
{ 0, 0, 10},
{12, 0, 10},
{12, 8, 10},
{ 0, 8, 10},
};
return 0;
}
就像在任何语言中一样,做同样的事情总是有不同的方式。此程序显示了在 C/C++ 中定义点(第 1 行)概念并将盒角存储在内存中的一种可能方法(在本例中为一个包含八个点的数组)
您以某种方式创建了您的第一个 3D 程序。它尚未生成图像,但您已经可以将 3D 对象的描述存储在内存中。在 CG 中,这些对象的集合称为场景(场景还包括相机和灯光的概念,但我们将在另一个时间讨论)。正如之前所建议的,我们缺少两个非常重要的东西来使这个过程真正完整和有趣。首先要在计算机的内存中实际表示盒子,理想情况下,我们还需要一个系统来定义这八个点如何相互连接以组成盒子的面。在 CG 中,这称为对象的拓扑结构(对象也称为模型)。我们将在几何部分和 3D 基本渲染部分(在渲染三角形和多边形网格的课程中)讨论这一点。拓扑是指我们通常称为顶点的点如何相互连接以形成面(或平面)。这些面也称为多边形。盒子将由六个面或六个多边形组成,多边形组形成我们所说的多边形网格或简单的网格。我们缺少的第二件事是创建该框图像的系统。这需要将盒子的角实际投影到想象的画布上,我们称之为透视投影的过程。
Creating an Image of this Virtual World
在画布表面投影3D点的过程,其实涉及到一个特殊的矩阵叫做透视矩阵(不知道矩阵是什么也别担心)。使用这个矩阵来投影点并不是绝对必要的,但会使事情变得更容易。但是,您实际上并不需要数学和矩阵来弄清楚它是如何工作的。您可以将图像或画布视为某种平面放置在离眼睛一定距离的地方。追踪四条线,从眼睛开始到画布的四个角中的每一个角,并将这些线延伸到世界更远的地方(尽可能远)。你会得到一个金字塔,我们称之为视锥体(而不是视锥体)。视锥体定义了 3D 空间中的某种体积,而画布本身只是该体积垂直于视线的平面切割。把你的盒子放在画布前面。接下来,从盒子的每个角到眼睛画一条线,并在该线与画布相交的地方标记一个点。在画布上找出对应于盒子十二边中每条边的点,并在这些点之间画一条线。你看到了什么?盒子的图像。
用来测量箱角坐标的三把尺子形成了我们所说的坐标系。这是一个可以测量点的系统。所有点的坐标都与该坐标系相关。请注意,坐标可以是正数或负数(或零),具体取决于它位于标尺原点(值 0)的右侧还是左侧。在CG中,这个坐标系常被称为世界坐标系,点(0,0,0)就是原点。
让我们在原点移动视锥体的顶点,并沿着负 z 轴(图 3)定向视线(视图方向)。许多图形应用程序使用此配置作为其默认的“查看系统”。请记住,金字塔的顶部实际上是我们观察场景的点。让我们也将画布从原点移动一个单位。最后,让我们将盒子从原点移开一段距离,使其完全包含在截锥体的体积内。因为盒子在一个新的位置(我们移动了它),它的八个角的坐标发生了变化,我们需要再次测量它们。请注意,由于该框位于我们测量对象深度的标尺原点的左侧,因此所有深度坐标(也称为 z 坐标)将为负。其中四个角也位于用于测量对象高度的参考点下方,并且具有负高度或 y 坐标。最后,四个角将位于测量对象宽度的标尺原点的左侧:它们的宽度或 x 坐标也将为负。盒子角的新坐标是:
corner 1: ( 1, -1, -5)
corner 2: ( 1, -1, -3)
corner 3: ( 1, 1, -5)
corner 4: ( 1, 1, -3)
corner 5: (-1, -1, -5)
corner 6: (-1, -1, -3)
corner 7: (-1, 1, -5)
corner 8: (-1, 1, -3)
让我们从侧面看一下我们的设置,并从一个角到原点(视点)绘制一条线。 我们可以定义两个三角形:ABC 和 AB'C'。 如您所见,这两个三角形具有相同的原点 (A)。 它们也以某种方式相互复制,从某种意义上说,边 AB 和 AC 定义的角度与边 AB'、AC' 定义的角度相同。 这样的三角形被认为是相似的。 相似三角形有一个有趣的特性:它们的相邻边和对边之比相同。 换句话说:
因为画布距离原点 1 个单位,所以我们知道 AB' 等于 1。我们也知道 B 和 C 的位置,它们分别是角的 z(深度)和 y 坐标(高度)。 如果我们将这些数字代入上述等式中,我们将得到:
其中 y' 实际上是从角落到视点的线与画布相交的点的 y 坐标,也就是我们之前所说的,我们可以从该点在画布上绘制框的图像。 因此:
如您所见,画布上角的 y 坐标的投影只不过是角的 y 坐标除以其深度(z 坐标)。这可能是计算机图形学中最简单和最基本的关系之一,称为 z 或透视除法。完全相同的原则适用于 x 坐标。投影点 x 坐标 (x') 是角的 x 坐标除以其 z 坐标。
但请注意,因为在我们的示例中 P 的 z 坐标为负(我们将在专门介绍透视投影矩阵的 3D 渲染基础部分的课程中解释为什么总是这种情况),当 x 坐标为正时,投影点的 x 坐标将变为负值(同样,如果 Px 为负,P'.x 将变为正值。y 坐标也会出现同样的问题)。结果 3D 对象的图像在垂直和水平方向上都被镜像,这不是我们想要的效果。因此,为了避免这个问题,我们将用 -P.z 来划分 P.x 和 P.y 坐标;这将保留 x 和 y 坐标的符号。我们终于得到:
我们现在有一种方法来计算角落出现在画布表面时的实际位置。 这些是投影在画布上的点的二维坐标。 让我们更新我们的基本程序来计算这些坐标:
typedef float Point[3];
int main()
{
Point corners[8] = {
{ 1, -1, -5},
{ 1, -1, -3},
{ 1, 1, -5},
{ 1, 1, -3},
{-1, -1, -5},
{-1, -1, -3},
{-1, 1, -5},
{-1, 1, -3}
};
for (int i = 0; i < 8; ++i) {
// divide the x and y coordinates by the z coordinate to
// project the point on the canvas
float x_proj = corners[i][0] / -corners[i][2];
float y_proj = corners[i][1] / -corners[i][2];
printf("projected corner: %d x:%f y:%f\n", i, x_proj, y_proj);
}
return 0;
}
画布本身的大小也是任意的。它也可以是正方形或矩形。在我们的示例中,我们在两个维度上都设置了两个单位的宽度,这意味着画布上任何点的 x 和 y 坐标都包含在 -1 到 1 的范围内(图 9)。
问题:如果任何投影点坐标不在此范围内,例如 x' 等于 -1.1,会发生什么?该点根本不可见,它位于画布边界之外。
此时我们说投影点坐标在屏幕空间(屏幕的空间,在这个上下文中屏幕和画布是我们的同义词)。但是它们不容易操纵,因为它们可以是负数也可以是正数,而且我们真的不知道它们指的是什么,例如您计算机屏幕的尺寸(如果我们想在屏幕上显示这些点) )。出于这个原因,我们将首先对它们进行归一化,这意味着我们将它们从它们最初所处的任何范围转换为范围 [0,1]。在我们的例子中,因为我们需要将坐标从 -1,1 映射到 0,1 我们可以简单地写:
float x_proj_remap = (1 + x_proj) / 2;
float y_proj_remap = (1 + y_proj) / 2;
投影点的坐标不在 0,1 范围内。 据说这样的坐标是在 NDC 空间中定义的,它代表标准化设备坐标。 这很方便,因为无论画布(或屏幕)的原始大小如何,这取决于您使用的设置,我们现在在公共空间中定义了所有点坐标。 术语归一化非常常见。 这意味着您以某种方式将值从它们最初所在的任何范围重新映射到范围 [0,1]。 最后,我们通常更喜欢根据最终图像的尺寸来定义点坐标,您可能知道或不知道,它是根据像素定义的。 数字图像只不过是二维像素阵列(就像您的计算机屏幕一样)。
你知道你的屏幕的分辨率或尺寸是多少像素吗?
512x512 图像是具有 512 行 512 像素的数字图像,您更喜欢以相反的方式查看它,512 列 512 个垂直对齐的像素。 由于我们的坐标已经标准化,我们需要做的就是用像素来表达它们,就是将这些 NDC 坐标乘以图像尺寸(512)。 在这里,我们的画布是方形的,我们还将使用方形图像:
#include <cstdlib>
#include <cstdio>
typedef float Point[3];
int main()
{
Point corners[8] = {
{ 1, -1, -5},
{ 1, -1, -3},
{ 1, 1, -5},
{ 1, 1, -3},
{-1, -1, -5},
{-1, -1, -3},
{-1, 1, -5},
{-1, 1, -3}
};
const unsigned int image_width = 512, image_height = 512;
for (int i = 0; i < 8; ++i) {
// divide the x and y coordinates by the z coordinate to
// project the point on the canvas
float x_proj = corners[i][0] / -corners[i][2];
float y_proj = corners[i][1] / -corners[i][2];
float x_proj_remap = (1 + x_proj) / 2;
float y_proj_remap = (1 + y_proj) / 2;
float x_proj_pix = x_proj_remap * image_width;
float y_proj_pix = y_proj_remap * image_height;
printf("corner: %d x:%f y:%f\n", i, x_proj_pix, y_proj_pix);
}
return 0;
}
得到的坐标据说是在光栅空间中(XX raster 是什么意思,请解释一下)。 我们的程序仍然有限,因为它实际上并没有创建盒子的图像,但是如果您编译它并使用以下命令运行它(将代码复制/粘贴到文件中并将其保存为 box.cpp):
c++ box.cpp
./a.out
corner: 0 x:307.200012 y:204.800003
corner: 1 x:341.333344 y:170.666656
corner: 2 x:307.200012 y:307.200012
corner: 3 x:341.333344 y:341.333344
corner: 4 x:204.800003 y:204.800003
corner: 5 x:170.666656 y:170.666656
corner: 6 x:204.800003 y:307.200012
corner: 7 x:170.666656 y:341.333344
您可以使用绘图程序创建图像(将其大小设置为 512x512),并在您使用该程序计算的像素坐标处添加点。 然后将点连接起来形成盒子的边缘,您将获得盒子的实际图像(如下面的视频所示)。 像素坐标是整数,因此您需要对程序给出的数字进行四舍五入。
-
What have we learned?
- 我们首先需要使用顶点和拓扑结构(有关这些顶点如何相互连接以形成多边形或面的信息)来描述 3D 对象,然后才能生成 3D 场景的图像(一个场景是一个集合)对象)。
- 该渲染是创建 3D 场景图像的过程。无论您使用哪种技术来创建 3D 模型(有很多),渲染都是“看到”任何 3D 虚拟世界的必要步骤。
- 从这个简单的练习中,很明显数学(不仅仅是编程)在用计算机制作图像的过程中是必不可少的。其实计算机只是一个用来加速计算的工具,但是用来创建这个图像的规则是纯数学的。几何在这个过程中扮演着特别重要的角色,特别是处理物体的变换(缩放、旋转、平移),但也为诸如计算线之间的角度、或找出线与其他简单形状(平面)的交点等问题提供了解决方案。 、球体等)。
- 总之,计算机图形学主要是应用于计算机程序的数学,其目的是以尽可能快的速度(以及计算机能够达到的准确性)生成图像(真实与否)。
- 建模包括用于创建 3D 模型的所有技术。建模技术将在几何/建模部分讨论。
- 虽然静态模型很好,但也可以随着时间的推移对其进行动画处理。这意味着需要在每个时间步渲染模型的图像(例如,您可以在每个连续图像之间稍微平移、旋转或缩放框,无论是通过动画角坐标还是将变换矩阵应用于模型) .可以使用更高级的动画技术来模拟骨骼和肌肉对皮肤的变形。但是所有这些技术的共同点是几何体(构成模型的面)会随着时间的推移而变形。因此,正如引言中所建议的那样,时间在 CGI 中也很重要。查看动画部分以了解有关此主题的信息。
- 一个特定的领域与动画和建模重叠。它包括用于以逼真的方式模拟对象运动的所有技术。一个非常大的计算机图形领域致力于模拟流体(水、火、烟)、织物、头发等的运动。物理定律应用于 3D 模型,使它们像在 3D 模型中一样移动、变形或断裂现实世界。物理模拟通常在计算上非常昂贵,但它们也可以实时运行(这完全取决于您模拟的场景的复杂性)。
- 渲染也是计算上昂贵的任务。有多昂贵取决于您的场景由多少几何体组成以及您希望最终图像的照片真实程度。在渲染中,我们区分了两种模式,离线渲染模式和实时渲染模式。实时用于视频游戏(实际上是一种要求),其中 3D 场景的内容需要至少以每秒 30 帧的速度渲染(通常每秒 60 帧被认为是标准)。大多数实时渲染是在 GPU 上执行的,GPU 是专门设计用于以尽可能快的速度渲染 3D 场景的处理器。实时渲染技术将在实时部分讨论。离线渲染通常用于不要求实时性的电影的 CGI 制作(图像在以 24 或 30 或 60 fps 显示之前预先计算和存储)。完成单个图像可能需要几秒钟到几个小时的时间,但与实时渲染相比,它通常可以处理更多的几何图形并生成更高质量的图像。然而,实时或离线渲染现在确实越来越重叠,视频游戏推动了它们可以处理的几何数量和质量,而离线渲染引擎试图利用该领域的最新进展CPU技术大大提高了它们的性能。离线渲染是有关 Scratchapixel 的几个部分的主要主题:3D 渲染的基础、特定于光线跟踪的技术、光传输算法、着色和程序纹理。我们建议您按时间顺序阅读第一部分的课程。
Where Should I start?
我们希望这个简单的盒子示例能让您着迷,但本介绍的主要目标是强调几何在计算机图形学中的作用。当然,这不仅仅是关于几何,很多问题都可以用几何来解决。大多数计算机图形学书籍都以几何一章开始,这总是有点令人沮丧,因为看起来你需要学习很多才能真正开始制作有趣的东西。但是,我们真的建议您先阅读几何课程。我们将讨论和学习点以及向量和法线的概念。我们将学习坐标系,更重要的是学习矩阵的概念。矩阵被广泛用于处理旋转、缩放或平移等变换。这些概念在所有计算机图形学文献中随处可见,这就是您需要首先研究它们的原因。
许多 CG 书籍没有很好地介绍几何,这可能是因为作者假设读者已经了解它,或者最好阅读专门针对此特定主题的书籍。我们的几何课非常不同。它非常彻底,用非常简单的语言解释了一切(包括只有在生产中工作的人才会告诉你的事情)。请先阅读本课开始学习
What Should I Read Next?
开始学习带有渲染的计算机图形编程通常更容易也更有趣。 您浏览本网站内容的一种可能方式是按时间顺序开始阅读 3D 渲染基础部分的课程。 要了解课程内容,您可能需要先了解一些其他技巧。 在每节课的开头,您都会找到其他课程的列表,其中包含您需要了解的所有内容,以便了解您即将开始的课程内容(我们称之为先决条件)。 使用此列表来指导您浏览网站的内容,更重要的是获得您在学习中取得进步所需的基础。