3D图形学理论入门指南
发表于2017-10-28
介绍
当我还小的时候,我曾以为计算机图形学是最酷的玩意儿。但是随即我认识到,学习图形学——创建那些超级闪亮的计算机程序——比我想象的要难上许多。我四处出击,阅读OpenGL渲染管线详解之类的文章,浏览关于图形工作原理的博客、网站等,对照着教程学习,试图搞懂一切。结果呢,一无所获。当我按照NeHe的教程设置好一切,却因为错误的调用了某个glXXX()函数,导致各种错误。我不具备正确调试程序的基础理论知识,所以我放弃了——就像我那个年纪的少年在遇到挫折时通常会做的那样。
然而,在若干年之后,我有机会能够在大学里参加一些计算机图形学的课程。这次我终于知道它们是如何正确工作了。如果我早知道这些,我那时应该能获得更多成功。所以,为了帮助和我有类似困境的人们,我打算分享下我学到的东西。
图形学背后的理念
概览
先想想真实世界的样子。在3D真实世界里,光线从许多个不同的光源发出,在多个物体间跳转,然后一部分光子通过眼球刺激到你的视网膜。在真实的场景里,3D的世界投影到2D的表面。虽然你的大脑从环境中获取各种视觉元素然后组成一个立体的影像来反映整个3D空间,但这些都源于2D信息。当场景中的物体移动,或者你相对于你的场景发生移动,或光照改变时,视网膜上的2D图像也立刻发生改变。我们的视觉系统快速处理图像,然后大脑据此构造出3D模型。
如果我们能够获取一些图片,然后以类似或更高的速率来交替显示它们,就能生成一个看起来像真实空间的场景。电影大致基于同一原理工作。在电影里,3D场景的图片高速闪过,看起来就像连续的一样。请参照上面马的例子。如果我们能够在计算机上持续的绘制一个运动的场景,那么它看起来就像一个3D的世界一样。图形学就是这样工作的:将虚拟的3D世界快速转换成2D表现形式,让大脑感觉像是一个3D的场景一样。
约束条件
人类视觉将一系列图片看作连续的阈值大约时16Hz。对计算机来说,我们有最多62.5毫秒来完成下列事情:
判断虚拟场景中眼睛看向哪里。
计算场景在这个角度下如何呈现。
计算需要被绘制在屏幕上的像素的颜色。
用这些颜色填充帧缓存。
将缓存发送至显示设备。
显示图片。
这是一个复杂的问题。时间上的限制意味着我们不能直接硬来——比如往3D场景里扔一堆光子,计算它们的轨迹和强度,算出哪些能够照进眼睛并将之映射到2D的图片上,最后绘制。(这并不完全正确,因为这有点像光线追踪时做的事。但光线追踪技术相当复杂,而且完全是另一回事,因此也可以这么说。)幸运的是,我们可以利用一些很酷的技巧来大大缩减计算量。
基础图形理论
整个世界是一个舞台
打个比方。假设你在一个山谷里面,四面环山,河流前方是一片草地,类似上面这张Bob Ross的绘画作品中的场景。你想用一张图展现这个3D场景。该怎么做呢?嗯,我们可以尝试绘制一张包含所有景色的图。那意味着我们必须得挑选一个观察角度,只绘制能看到的部分,忽略其余的。然后还得决定哪些部分在前,哪些部分在后。我们能看到草地,但它遮挡了部分河流。群山在远处,但它们遮挡了后方的一切景色。由于场景的真实大小远大于我们画布的大小,我们还得计算如何缩放。然后我们绘制场景,将光照和阴影考虑进去,以及远处山间的雾气,等等。这和计算机中的图形处理过程十分相像。主要步骤如下:
决定世界中的物体长什么样子。
决定物体在世界中的位置。
决定相机的位置和哪些场景需要被绘制。
决定物体相对于相机的位置。
绘制场景中的物体。
将场景缩放到视窗大小一致的图像上。
上述步骤大致是为了把3D世界中的某个点映射到屏幕上的2D图像上。乍一看工作量很大,其实可以利用一些数学技巧来快速完成工作。还记得学代数的时候曾想,“学这个有什么用?”其中一个用处就是图形学!
我们可以用矩阵来将世界中的坐标映射到图像上。为什么选择矩阵?其中一个原因是很多操作都可以用矩阵来表示。最重要的是我们可以通过矩阵乘法将多个操作连接起来,最终只用一个矩阵来代表一系列操作!即使要做50个变换,只需将所有矩阵相乘就得到一个同时执行所有变换的矩阵。
我们可以定义一些矩阵来完成我们上述绘画示例中的操作(定义场景,定义视野等)。这些矩阵能够将场景从一个坐标系转换到另一个坐标系。这些坐标系之间的转换称为变换。我们将要讨论每一个坐标系以及不同坐标系之间的转换过程。
模型坐标系——分解模型
如何在屏幕上快速绘制物体呢?计算机相当擅长连续快速完成大量相对简单的指令。为了利用这一点,如果我们能够用简单的形状来表示整个世界,就能够通过快速处理许多简单的形状来优化图形算法。因此,计算机不需要知道一座山或一片草地到底是什么就能够绘制它们。
我们需要创建一些算法把图形分解成简单的多边形。这个过程称作曲面细分(Tessellation)。虽然我们也可以用正方形,但三角形更好一些。使用三角形有很多优势,例如所有的三角形的点都是共平面(coplanar)的,而且几乎所有形状都能近似分解成多个三角形。唯一的问题是圆形物体会看起来像是有棱角的。不过,要是三角面足够小的话,比如1个像素大小,我们就不会注意到了。关于如何曲面细分的“最佳方法”有很多,具体取决于你要分解的物体的形状。
假设我们要分解一个球形。我们可以将球体的中心位置定为本地的原点。这样我们就可以用一个公式来获得球面上的一些点然后将这些点连接成多边形以供绘制。一个常用的公式是S(u,v)=[r sin u cos v,r sin u sin v,r cos v],u和v的取值范围分别是u∈[0,π],v∈[0,2π],r是球体的半径。就像你在图中看到的那样,球体表面的点被绘制成矩形。我们能够很方便的把它们连成三角形。
球面上的点位于所谓的模型坐标系。坐标相对于本地的原点定义,比如示例中球体的中心位置。如果我们想要将物体放置于场景中,我们可以定义一个从场景原点到场景中球体的原点的向量,然后把这个向量和球面上每个点的坐标相加。这样我们就将模型放置到了世界坐标系中。
世界坐标系——将物体置于世界中
到这儿我们的图形学之旅才真正开始。我们在某处定义一个原点,场景中的每个点都基于从原点到该点的一个向量来定义。虽然场景是3D的,我们还是得用一个4维的坐标来定义每个点[x,y,z,w],代表该点的坐标为[x/w,y/w,z/w]。这种映射称为齐次坐标。使用齐次坐标有一些好处,但是这里不做讨论。只需知道我们使用齐次坐标就够了。
假设我们要在场景中移动,那么问题来了。如果我们要移动视线,或者移动相机到另一个位置,或者让整个世界围着相机移动。在计算机的世界里,移动整个世界更容易一点,所以我们就这样做,让相机固定不动。模型-视图矩阵(modelview matrix)是一个4x4矩阵,可以用来移动世界中的每一个点,然后让相机固定不动。这个矩阵基本上就是一系列旋转、位移、缩放的集合。我们在世界坐标系中将点和模型-视图矩阵相乘,这将使我们进入观察坐标系(viewing coordinates)。
观察坐标系—选择能看到的
在我们旋转、位移、缩放了世界之后,我们可以只选择世界中的一部分加以呈现。我们通过定义一个视锥体来实现。视锥由观察坐标系中的六个裁剪面组成。在最终绘制阶段,所有位于视锥范围以外的物体都将被裁掉,或丢弃掉。视锥通过一个4x4矩阵来定义。OpenGL的glFrustrum()方法是这样定义这个矩阵的:
我们可以改变这个矩阵以应对不同情况如正交或透视。透视图里有一个消失的点,正交视图没有。通常在绘画里见到的是透视图,正交视图在技术图中中较为多见。因为这个矩阵决定了物体是如何投影到屏幕上的,所以也叫做投影矩阵。t,b,l,r,n,f代表顶部、底部、左侧、右侧、近处、原处的裁剪面的坐标。乘以投影矩阵将使点从观察坐标系前往所谓的裁剪坐标系(clip coordinates)。
裁剪坐标系——只绘制能看到的
这个坐标系有点不同,因为它是左手坐标系(在此之前我们一直使用的右手坐标系),而且是从我们之前定义的视锥体映射到一个x,y,z范围都在(-1,1)之间的正方体。
到现在为止,我们一直追踪场景中的所有点。然而,一旦进入裁剪空间,我们就可以开始裁掉一部分了。还记得坐标从4D到3D的转换吗?我们曾说过,[x,y,z,w]4D=[x/w,y/w,z/w]3D。因为我们只需要位于视锥范围以内的点,我们接下来只需处理符合−1≤x/w≤1或−w≤x≤w的点即可。y和z坐标也一样。这是一个简单的办法分辨一个点是否位于我们视野之内。
如果某些点位于视锥体内,我们对它们执行透视出发(perspective divide),对每个坐标除以w来将其从4D坐标转换成3D坐标。这些点还是位于左手裁剪坐标系中,但是到了这个阶段,我们称其为规格化设备坐标(normalized device coordinates)。
规格化设备坐标系——计算遮挡关系
你可以把这个想成映射到图像的中间步骤。想象一下所有可能的图像大小,我们并不想渲染成一张图片然后进行各种缩放或拉伸或当图像大小发生改变时重新渲染。规格化设备坐标(NDC)很有用,因为无论图片最终大小是多少,你可以在NDC里面针对性的进行合适的缩放。在NDC里你将看到图片如何被构造。被渲染的图像是视锥体里的物体在近裁剪面上的投影。因此,一个点在Z轴上的值越小,这个点就越近。
这个阶段,通常我们不再进行矩阵计算,而是应用一个视窗变换。这通常只是拉伸坐标来适应视窗,或最终图像大小。最后一步是通过转换坐标到窗口坐标来绘制图像。
窗口坐标系——将物体缩放到画布
窗口是图像最终被绘制的地方。在这里,我们的3D世界呈现为近裁剪面上的一张2D图像。我们可以使用一系列的线条和多边形算法来绘制最终图像。此时,一些2D效果,如抗锯齿和多边形裁剪,在图片被被绘制之前执行。
然而,窗口可能有不同的坐标系统。比如,有时图片基于向右为X ,向下为Y 绘制。为了正确绘制图片,有时候可能需要做一些转换。
又回来了——图形渲染管线
上述步骤你不用都亲历亲为。某种程度上,你会使用图形渲染库来定义诸如模型视图矩阵、投影矩阵以及世界坐标系中的多边形之类的东西,渲染库会搞定一切。如果你在设计一个游戏,你不需要在意多边形是如何被绘制的,只需确保它们执行的正确又快速,对吗?
OpenGL和DirectX之类的库效率很高,而且能够有效利用精密的图形硬件来简单又快速的执行这些计算。它们广泛使用,所以最好适应它们。它们还给你留下很大空间来自定义一些事情,你会为你能做到的某些事感到惊讶的!
结论
这是一个关于图形学理论的简单概览。渲染过程中后续还有很多步骤发生,但是这应该能给你一个大致的方向,让你在阅读其它文章或论坛里的相关技术时能够理解的更好。