CMU-15-462-计算机图形学笔记-全-
CMU 15-462 计算机图形学笔记(全)
1:欢迎视频 🎬

在本节课中,我们将学习卡内基梅隆大学《计算机图形学》课程(15-462/662)的入门信息,包括课程结构、学习工具和基本要求。
欢迎来到卡内基梅隆大学的计算机图形学课程(15-462/662)。我是基南·克兰,计算机科学与机器人学教授,我的研究方向是计算机图形学,特别是几何算法领域。本视频旨在提供本学期成功学习所需的所有信息。我们会定期上传短视频,以说明管理事务、讨论本周进展并解答出现的重要问题。
所有信息均可在课程网站 15462.courses.cs.cmu.edu 找到。请访问该链接,并仔细阅读课程信息页面,因为其中包含许多本视频未提及但对课程学习至关重要的内容。本学期我们拥有一支优秀的助教团队,如有任何问题,请随时通过电子邮件、Piazza论坛等方式联系我们,无需犹豫。
课程准备步骤 📝
以下是开始课程前需要完成的两个关键步骤。
- 注册Piazza:访问
piazza.com/cmu,找到我们的课程(462)并注册。 - 注册课程网站账户:点击课程网站右上角的“登录”按钮,然后点击“注册”链接。注册需要一个特殊密码,该密码仅在Piazza上提供,因此请先完成Piazza的注册。
如果您尚未正式注册本课程,也无需担心。通常选课不会有问题,本学期我们也没有严格的班级人数限制。您可以先注册网站账户,开始阅读作业等内容。这样,如果您决定加入课程,就能跟上进度。
课程结构与学习方式 🏫
我们将完全远程进行本课程,主要包括三个部分:讲座、复习课和办公时间,均在线进行。
复习课和办公时间将通过Zoom进行,您可以在Piazza上找到相应的Zoom链接。讲座将以预录视频的形式在YouTube上发布,但我们会在讲座时间段在线,以便在您观看视频时解答问题。如果您在观看视频时遇到不理解的地方,可以暂停播放,进入Zoom房间与我们讨论、提问,然后再继续观看。
关于观看长视频,这里有几个小技巧。在家集中注意力观看长达80分钟的讲座视频可能很困难。我们的学生发现以下方法很有帮助:一是使用YouTube的加速播放功能(点击右下角的齿轮图标,选择“播放速度”);二是将视频分成几个部分观看,例如今天看40分钟,明天再看40分钟,这有助于保持专注。

课程作业与考核 📚
课程作业主要包括三项内容。
- 课后作业:每次讲座后会有两到三个问题,旨在确保您理解所学内容。
- 编程作业:本学期共有四个主要的编程作业。您将逐步构建一个名为 Scotty3D 的3D软件包。它像任何现代3D软件一样,包含建模、渲染和动画功能,但不同之处在于,其所有核心例程已被移除,需要由您来填充实现,最终创造出很酷的模型和动画。
- 考试:我们将进行一次期中考试和一次期末考试。请注意,这两项考试仅各占总成绩的10%,主要是为了确保您在概念上理解课程内容,而不仅仅是完成编程作业。
课程政策 ⚙️
关于作业提交,您有五天的自由支配延期时间,无需事先申请许可,可根据需要自行使用。
在协作方面,我们鼓励同学们相互交流、在Piazza上进行有趣的讨论、参加办公时间提问。但是,您最终提交的作品必须是独立完成的。本课程所有作业均为个人任务。关于协作和学术诚信的更多细节,请再次查阅课程网站。
本节课中,我们一起了解了CMU计算机图形学课程的基本框架、学习工具(Piazza、课程网站、Zoom)以及课程要求(作业、考试与政策)。如果您有任何问题,请通过Piazza或电子邮件联系我们。如果您已准备好开始,可以立即观看第一讲视频,只需访问课程网站查看课程安排,并点击视频链接即可。
2:课程概述 🎬


在本节课中,我们将对计算机图形学进行一个全面的概述。我们将探讨计算机图形学是什么,它在世界中的应用,并实际动手实现第一个从三维形状生成图像的算法。课程的所有后勤信息都可以在网页上找到,今天我们将直接深入内容。
什么是计算机图形学? 🤔
当你想到计算机图形学时,脑海中可能会浮现出这样的图像,比如来自动画电影或视觉特效的画面。然而,在本课程中,我们更想从根本上研究计算机图形学在计算机科学中的广泛体现,它在数字计算中的作用,以及我们为什么需要它。









回顾最早的计算机,它们看起来像这样,这些巨大的机器占据了整个房间。与这些机器交互的方式非常原始,你可能需要在卡片上打孔,然后将其输入计算机进行计算,最后计算机再输出一张同样打了孔的卡片。显然,这需要大量时间来设置程序,也需要大量时间来解读结果,以理解计算机输出了什么。因此,你会想,一定有更好的方法来获取数字信息,使其以一种我们可以理解和消化的形式呈现。


如果你开始审视计算的历史,你会发现随着时间的推移,人们很自然地倾向于使用某种视觉显示来传达机器存储或计算的信息。最终,我们达到了这样一个阶段:人们开始真正将显示和交互问题作为一个独立的研究课题来思考。这是最早有人真正开始思考计算机图形学和人机交互的例子之一。这是一个名为“画板”的演示,由伊万·萨瑟兰开发。我们播放一小段视频,看看如何以图形方式与计算机进行通信。我们在这里使用示波器,它很像电视机,但由计算机驱动。为了将信息输入计算机,我们必须以某种方式绘制,我们使用光笔。因此,人们确实开始思考如何将信息输入计算机,尤其是如何从计算机输出信息。

如果我们快进到2020年,显示技术已经发展到具有越来越高的分辨率和更丰富的色彩。我们不再只是从机器中获取一张带有少量信息的打孔卡片,而是现在可以在显示器上显示单个图像。一个8K显示器可能传达大约95兆字节的信息。因此,我们确实可以从机器中输出大量信息。如果我们开始关注即将出现的新设备,如虚拟现实头显、增强现实头显,这些设备需要我们提供海量信息来驱动它们。今天你可能购买的头显有两个显示器,每只眼睛一个,每个显示器的分辨率是2160 x 2160像素。它们有一定数量的比特来表示颜色,并且为了获得某种逼真的视觉再现,它们必须每秒显示90次图像。如果你做一个小小的计算,这意味着我们每秒从计算机输出大约2.3千兆字节的信息,这比计算技术诞生之初要多得多。
这里一个自然而然的问题是:为什么视觉信息如此重要?我们本可以尝试通过许多其他方式从机器获取信息,比如播放声音或产生某种振动。为什么我们在计算机科学和计算机图形学中如此专注于视觉信息?一个重要的原因是,这就是你的大脑的工作方式。大约30%的大脑专门用于视觉处理。因此,如果你从系统层面思考,你的眼睛就像是进入大脑的最高带宽端口。所以,如果你试图将信息从机器传递到你的大脑,进行视觉处理就非常有意义。
这或许解释了为什么计算机图形学得以发展,以及为什么它成为计算机科学中如此重要的一部分。我们至少可以尝试性地给出计算机图形学的定义:计算机图形学是什么?我们可能会说,计算机图形学是使用计算机合成视觉信息。我们拥有数字信息,即0和1,我们希望通过某种计算将其转化为人类可以消费的某种视觉信息。
如果我们这样定义,如果说其目的是为人类创造消费信息,那么很自然地会问:为什么只专注于视觉信息?当然,大脑拥有很强的视觉计算能力,但我们还可以做其他事情。事实上,自早期以来,图形学已经发展到关注比仅仅如何在高清显示器上开关像素更广泛的一系列问题和议题。因此,有人致力于模拟声音,从虚构或虚拟的三维模型生成声音;有人致力于合成或模拟触觉,以便你能感受到物体在物理现实中可能的感觉。因此,我们可以拓宽计算机图形学的定义,说我们真正感兴趣的是使用计算机来合成和操纵感官信息。你可以继续思考味觉、嗅觉和其他感官,这可能很有趣,但这里的关键词是“合成”。计算机图形学是计算机科学中一个独特的学科,它获取信息并合成感知刺激。还有其他学科,比如计算机视觉,研究的是逆问题:我有现实世界中的刺激,我想消费它们,解释它们,并将它们转换为数字信息。从这个意义上说,你可能认为计算机图形学和计算机视觉是彼此的逆过程。这两个学科的相互作用实际上非常重要,这是非常当代的。
超越在屏幕上显示像素的是将数字信息转化为我们可以体验的另一件事物,即物理实体。例如,在3D打印方面,计算机图形学有很多工作。我有一个数字模型,我想把它打印出来。在这里,我正一片一片地打印这只兔子,直到得到一个可以握在手中的实体物理模型。但这远不止3D打印。人们正在研究各种将数字模型转化为物理模型的不同方式,无论是将数字物体转化为食物、服装还是建筑。右下角的例子是一只出生时缺少一只脚的鸭子,有人扫描了它的另一只脚,在计算机上镜像,然后打印出来,制作了一个假肢。因此,利用计算机图形学的技术可以做很多有趣的事情,这些技术超越了图像生成,并开始对更广阔的世界产生重大影响。
因此,这将是我们本课程的工作定义:如何使用计算将数字信息转化为感官刺激。我想说,如果你和从事大量计算机图形学工作的人交谈,他们仍然会说,这个定义还是有点狭隘,它并没有真正涵盖计算机图形学中发生的所有有趣的事情。因此,如果你想很好地了解计算机图形学正在发生什么,一个很好的地方是ACM SIGGRAPH会议,每年都会在那里展示计算机图形学所有的新成果。有很多令人兴奋的新研究论文,他们制作了一个很好的技术论文预告片,重点介绍图形学中一些很酷的东西。让我们来看一下。
欢迎来到SIGGRAPH 2020技术论文预告片。我们将展示技术论文项目中一些令人兴奋的突破性成果的片段。一种基于学习的关键帧视频风格化方法使这位艺术家能够实时打造个人风格。通过使用四个鱼眼单色相机,我们可以在空间中追踪某人的手,使他们能够对这些积木进行分类并赢得游戏。利用巧妙的铰链设计,一个由薄柔性条组成的扁*晶格可以变形为复杂的三维结构,然后再次展*。为了创建逼真的针织兔子,该算法使用能量密度函数来驱动薄壳模拟器。它还可以通过插值稀疏的三维姿态序列来编织犰狳。该算法产生*滑复杂的运动,同时仍允许艺术控制。该算法支持浸入式气泡和自由表面流模拟器,产生饮水机熟悉的咕噜声。一种管理非线性可变形物体动力学的方法让我们可以将这个毛茸茸的弹性玩具弹到墙上。窄带网络可以通过去除外部阴影和模拟柔和的填充光来改善人像照片。高效耦合固体物体和湍流之间的相互作用让我们能够生成对抗网络和深度强化学习,帮助这个四足动物阿比盖尔找到惊喜。通过动画化各向同性和正交各向异性材料的动态断裂,我们可以撕下五花肉的表层。一种利用配置空间中的隧道的算法可以解决由纠缠刚性形状组成的复杂谜题。一种隐式建模合理人脸空间的方法让我们可以通过素描或合并现有部分来发现新面孔。通过规划其两个机器人手臂的运动,手持一根可变形热线,该设备可以切割复杂的三维形状,比如兔子。该系统测量我们在真实和模拟世界中感受到的力,然后通过指尖设备施加这些力,这样我们就能感受到我们看到的东西。这块土壤是由一种用于物理材料的大规模并行物质点方法模拟的。它也可以让犰狳发生碰撞。该算法让我们可以从新颖的视角观察我们的场景。这种技术模拟了大量带有摩擦的碰撞弹性体。因此,运行基于粒子的流体模拟方法让我们可以将神经纹理应用于流体,即使它们在移动时也可以。一种用于非线性固体和流体双向耦合的物质方法让我们可以绘制装满水和橙汁的墙壁。
希望从这些内容中,你能感受到计算机图形学确实是一个丰富多样的学科,它远远超出了仅仅在屏幕上开关像素,它触及了生活的各个不同领域。计算机图形学确实无处不在。我们很多人首先想到计算机图形学出现在娱乐领域,如电影和游戏中,这当然是计算机图形学的一个非常重要的用途。但即使在娱乐领域,它也出现在你可能甚至没有想到的地方。不仅仅是卡通动画,如今你去看的很多电影可能都有特效,这些特效如此之好,以至于你没有意识到它们的存在。它们可能是在去除皮肤皱纹,或者将某人插入一张旧照片中,而这个人实际上并不在那里,这有点可怕。
当然,计算机图形学如今在艺术和设计中也被大量使用。除了传统媒介,人们几乎总是在进行某种数字媒体绘画或建模等。同样,在工业设计中,你希望制造的产品不仅美观,而且具有特定的功能,因此你需要在美学方面与功能或工程方面进行*衡。这正是计算机图形学中人们研究的那类问题的核心:如何*衡物理和机械约束与美学考虑。同样,在进行工程尝试时,你可能只需要可视化数据,这是非常重要的事情。你进行汽车碰撞模拟,需要了解发生了什么。这些模拟中产生的数据量如此之大,以至于很难真正理解发生了什么,而可视化技术对此非常重要。建筑学正被计算机图形学的技术所革新,特别是被称为离散微分几何的领域,对于易于建造但仍能以非常自由形式设计的结构有很多见解。在科学和数学可视化中,你同样有大量可能来自某些高分辨率模拟的数据。计算机能够完成所有这些工作固然很好,我们能够构建解决如此大规模问题的系统固然很好,但一旦你解决了这些问题,人类如何解读答案呢?有太多的信息需要理解。同样,在医学或解剖学可视化中,我们有设备可以对身体进行非常详细的扫描。如何开发技术和算法,让你能够高效地查看所有这些数据并理解发生了什么?计算机图形学在导航、在世界中移动的方式中扮演着巨大角色,从简单的二维地图到更复杂的三维地图,再到自动驾驶汽车进行自动驾驶,或者像将不同游客拍摄的照片集合起来,以某种方式组装它们,让人们了解那个地方的样子等有趣的事情。你可以做很多有趣的事情。在通信方面,有些东西你不认为是计算机图形学,但需要一些相当惊人的技术。实际上,当你在阅读报纸或网站时,你在屏幕上看到的每一个字母,都需要一些相当复杂的算法来绘制所有这些字体,并且绘制得非常非常快。它就像魔法一样工作,你甚至没有意识到有一些复杂的算法在运行,因为人们在开发这项技术方面做得非常好,但它就在后台运行。同样,人们正在探索各种超越书面文字的新通信机制,比如虚拟化身,你可以坐在摄像头前,将自己变成三维模型。可能性是无穷无尽的。

有这么多事情要做,我们在这门课中要做什么呢?我们真的要研究所有这些不同应用背后的计算基础。因为所有这些应用,无论你对哪个领域感兴趣,都需要复杂的理论和计算机系统的复杂处理。在理论类别中,我们将讨论诸如基本表示之类的东西:如何对形状或运动进行数字编码?本课程的一个大主题将是采样和混叠:如何获取信号并再现信号,以便你再现的东西忠实地代表你获取的东西?我们将大量讨论数值方法:如何数值地操纵信号?如何数值地解方程?在许多计算机科学教育中,重点是离散组合问题、处理整数等。这将是真正深入探讨你第一次真正动手体验,也许是大量使用浮点数表示数字,这可能有点棘手。我们将讨论辐射学和光传输:如何以非常精确的方式讨论光的颜色、亮度以及它在场景中的分布方式?这些问题对于尝试创建逼真的图像都非常重要。我们还将至少稍微联系到感知。请再次记住,目标是将数字信息转化为人类可以消费的刺激。如果是这样的话,那么理解人类如何感知视觉信息和其他刺激,将真正以重要的方式影响我们的算法。例如,如果我们说我们想压缩一张图像,以便人们不会注意到任何变化,我们真的必须了解一些人类心理学。在系统方面,如果我们想创建算法,将千兆字节的数据输出到VR头显,那么我们真的需要考虑性能,以及信息在我们的流水线中由不同进程和算法交换的方式。因此,我们将讨论诸如并行或异构处理之类的事情,这些可能是运行快速图形算法所需要的。我们将稍微讨论一下图形特定的编程语言,比如着色器语言。更一般地说,我们将讨论如何根据流水线来表述计算机图形学中的问题:将我们的问题分解成哪些基本元素是合适的,以便我们能够有效地通过硬件流式传输计算?
以上就是高层次概述。让我们真正深入探讨,动手解决一些具体问题。在本节课剩余的大部分时间里,我们将尝试思考如何建模一个三维物体,将其编码为数字信息,然后将该物体的数字编码转化为一张我们可以看的二维图片。特别是,我们将考虑一个非常简单的几何体:一个立方体。我们想生成一个看起来有点逼真的立方体绘图,并且我们希望以算法方式完成,而不仅仅是在纸上素描。因此,我们必须回答这两个关键问题:一个是建模问题,我们如何在计算机上描述或编码立方体?然后是一个渲染问题,“渲染”这个词在本课程中会经常出现。当我们说渲染时,我们指的是如何将数字描述转化为具体图片。那么,我们如何可视化我们的立方体模型?
为了让我们开始,这显然是一个简单的例子,但为了具体起见,假设我们有一个以三维空间原点(0, 0, 0)为中心的立方体。它是一个2x2x2的立方体,宽2,高2,深2。立方体的边将与X、Y和Z轴*行,是一种轴对齐的立方体。我们可能问的第一个问题是:我们已经编码了立方体吗?我们已经给出了立方体的描述,这相当精确。我们知道它在空间中的位置,知道它有多大,知道它的方向。但是,这个描述不能很好地推广。如果我们想开始描述其他物体,比如一张脸或一辆车,仅仅给出位置、大致大小和方向是不够的。为了最终制作它的图片并渲染它,我们还需要以某种方式编码关于形状的更详细信息。也许你可以思考一下,对于立方体,我能给出什么更明确的描述,真正详细说明它的几何形状是什么样子?
好吧,这是一个有点棘手的问题,有点抽象,但让我们试试这个。我们可以做的一件非常具体的事情是,首先,我们可以说出所有角点或立方体所有顶点的坐标。根据立方体的这个描述,我们应该能够找出它在空间中的八个角点位于哪里。那么,一个例子可能是什么?也许你想一想,其中一个角点的坐标是什么?你能想到任何一个吗?如果你思考一下,你会想,它是2x2x2,但以原点为中心,所以它的半径大概是1。那么,至少有一个角点会在(1, 1, 1),另一个可能在(-1, -1, -1),然后我们还有其他所有可能的符号组合。我们有三个坐标,每个坐标有两种可能的符号,2的3次方是8,所以我们得到了立方体的八个顶点。这里的重点是,对于立方体来说这很容易,而且对于立方体来说似乎几乎是不必要的。但是,如果我想传达一张脸或一辆车的形状,我也可以列出该形状上的一堆点。所以,这里的重点是,我们有一个可以推广到更有趣几何形状的描述。
但我们还没有完全完成。到目前为止,我们还没有一个真正捕捉立方体本质的描述。我们只有这八个漂浮在空间中的点。如果我只是向你展示这八个点,如果我现在将这些点渲染到一个二维画布上,可能很难看出这是一个立方体。我只会得到这八个点,它们位于一些奇怪的位置。那么,为了真正捕捉立方体,我可能还需要提供什么额外信息呢?这里有几个自然的选择。一个相当直接的方法是,我可以列出所有的边。我可以说,不仅知道八个角点在空间中的位置,还要知道哪些点通过立方体的边相互连接。指定这一点的一个简单方法是说,一条边有两个端点。所以,如果我有这八个点A到H,我将一条边指定为一对字母。例如,AB是一条边,CD是一条边,EF是一条边,等等。如果你愿意,可以暂停视频检查一下。你想检查什么?你如何知道我是否正确?对于这些边中的一条,你应该检查的是,当我从一个端点移动到另一个端点时,只有一个坐标发生变化。边与X、Y和Z轴对齐,这意味着当我从一个边的端点移动到另一个端点时,只有X坐标变化,或者只有Y坐标变化,或者只有Z坐标变化。但我相当确定这些是我立方体的所有正确边。

现在,我们有了立方体的数字描述。它不再是一个概念,不再只是一个词或字符串“立方体”,而是立方体非常非常明确的数字编码。如果我们愿意,我们可以一直将其编码为二进制数据,而不是幻灯片上看到的这些文本字符串。但这就是我们所说的几何或场景的数字编码。因此,我们基本上完成了建模任务。这有点麻烦,我们可能通常不希望以这种方式描述我们的模型。在本学期晚些时候,我们将看到如何构建工具,使我们能够更轻松地设计和建模三维物体。但现在,我们对立方体有了一个合理的数字描述。



下一个问题是:我们如何将这个立方体绘制成*面的二维图像?我们如何渲染它?基本问题在于,每个顶点都有三个坐标,但如果我们想在二维纸上绘制它,我们需要二维坐标。那么,我们如何将这三个坐标转化为两个坐标?这就是我们基本的渲染问题。这里有很多可能性。首先,我们可以直接丢弃其中一个坐标。我们可以说,如果我想在二维*面上绘制一个三维点,我就直接丢弃Z坐标,只绘制X和Y。这样做可以,但如果我们这样做,我们并不能真正获得这个立方体的三维感。在那种情况下你会看到什么?假设我丢弃了Z坐标,将这些点投射到*面上,然后连接属于一条边的任何一对点,你只会看到一个正方形。这就像从侧面看立方体,但没有任何透视,这将是一个非常无聊的图像。所以我们想做点更有趣的事情。


我们的基本策略是以某种有趣的方式将三维点映射到二维点。然后,我们将用直线连接这些二维点。这是一个算法吗?这真的是我们可以去实现的东西吗?还不完全是,因为它还没有真正分解成计算机知道如何执行的原子操作。但它是我们想要设计的算法的一个很好的草图。那么,让我们更详细地讨论一下这些步骤中的每一个将如何实现。第一个是透视投影。我们说过,仅仅丢弃一个坐标这个非常简单的想法不会给我们一个非常有趣的图像。我们想做的是捕捉我们从日常观察世界的经验中知道是真实的东西。我们从在世界上行走中知道的一件事是,物体离得越远,看起来就越小。这就是透视现象。这是如此自然的事情,以至于大多数人从未真正想过为什么会发生这种情况。为什么如果我看着市中心的一栋建筑,而我站在一个高山上,那栋建筑看起来非常小?难道那栋建筑真的缩小了吗?当我走到山顶时?不,我的意思是这很疯狂,那栋建筑没有改变。那么,为什么它看起来那么小?你有没有想过这个问题?让我们真正思考一下这个问题。理解正在发生什么的一种方法是考虑所谓的针孔相机模型。这实际上是一种你可以建造的真实相机。它的工作原理是,你可以想象你有一个内部完全黑暗的纸板箱,然后你戳一个非常小的洞让光线进入,在戳洞的对面放一张胶片。传统的胶片是一种感光材料,里面有卤化银晶体之类的。它的工作原理是,每次光线进入,每次光子击中胶片,都会引起化学反应,那些小晶体粘在相纸上。任何光线没有照射到的地方,晶体就不会粘住。然后你进入你的照片实验室,用某种特定的化学物质冲洗胶片,所有没有结晶的东西都被洗掉,所有粘住的东西留在相纸上。这真的是你的底片,它创造了通过那个孔照射进来的亮度或暗度的印象。这就是数字相机出现之前传统摄影的工作原理。所有这些化学过程和胶片如何工作并不是非常重要。我们想再次
3:线性代数复习 📐



在本节课中,我们将要学习计算机图形学中至关重要的数学基础——线性代数。我们将从最直观的几何概念出发,理解向量、向量空间、线性映射等核心思想,并探讨它们在图形学中的广泛应用。

什么是线性代数? 🤔
线性代数是研究向量空间以及向量空间之间线性映射的数学分支。在计算机图形学中,线性代数是将几何、物理等问题与具体计算连接起来的有效桥梁。一旦你能用线性代数来表达问题的解,就可以将其交给计算机或现有的数值线性代数包来处理,从而高效地完成大量工作。因此,线性代数是表述图形学中各种问题的一个非常强大的抽象工具,而快速数值线性代数的实现是现代计算机图形学得以发展的基石。


向量的直观理解 🧭

为了深入理解,我们不应只接受一系列抽象规则,而应思考这些规则从何而来。一个很好的起点是将向量想象成“小箭头”。这个简单的图像能帮助我们推导出向量空间的许多性质。

向量编码了什么信息?
一个向量本质上编码了方向和大小(模长)。例如,在二维*面中,一个向量可以用极坐标 (r, θ) 表示,其中 r 是长度,θ 是与参考方向(如水*轴)的夹角。

重要提示:在线性代数中,向量通常被视为从原点出发,不带有特定的“基点”。向量的测量值(如坐标)是相对于特定坐标系而言的。例如,长度 r 不依赖于坐标系的选择,但方向角 θ 则依赖于参考方向。因此,在编写代码时,必须时刻注意当前使用的坐标系,不能混用不同坐标系下的坐标值。
向量的坐标表示
除了极坐标,更常见的是笛卡尔坐标表示法。例如,在二维*面中,一个向量可以用 (x, y) 来表示其在两个坐标轴方向上的分量。同样,我们必须注意,笛卡尔坐标和极坐标不能直接比较,需要进行转换。


向量的基本操作 ➕✖️

对向量有两种基本操作:加法和数乘。

向量加法
将两个向量 u 和 v 首尾相接,得到的和向量 u + v 就是从 u 的起点到 v 的终点的向量。从几何上可以直观看出,向量加法满足交换律:u + v = v + u。


标量乘法
将一个向量 u 乘以一个标量 a,意味着将其长度缩放 a 倍,方向在 a 为负时反转。数乘满足结合律等性质,例如 a(bu) = (ab)u。


运算的分配律
通过几何作图可以验证,向量运算满足分配律:a(u + v) = au + av。这些看似简单的规则都源于对“小箭头”行为的自然观察。
通过总结这些几何行为,我们得到了向量空间的形式化定义:任何满足一系列性质(如加法交换律、结合律、存在零向量等)的对象集合,都可以被称为一个向量空间。

向量空间的例子 🌌


“小箭头”模型对应的最常见向量空间是n维欧几里得空间 R^n,即由 n 个实数构成的元组。这在图形学中非常实用,因为我们可以用三个浮点数 (x, y, z) 轻松表示三维空间中的一个点或向量。

然而,向量空间的概念远不止于此。许多其他类型的对象,如函数,也可以构成向量空间。

函数作为向量
考虑定义在区间 [0, 1] 上的实值函数。我们可以定义函数的加法 (f+g)(x) = f(x) + g(x) 和数乘 (af)(x) = a * f(x)。可以验证,这样的函数集合同样满足向量空间的所有公理。零向量就是恒为零的函数 f(x)=0。因此,函数在抽象意义上也是“向量”,即使它们看起来不像箭头。这种观点允许我们将信号、图像、几何形状等许多图形学中的对象视为向量来处理。
坐标下的运算 🧮

为了进行具体计算或编程,我们需要使用向量的坐标表示。

坐标下的加法与数乘
设有向量 u = (4, 1), v = (1, 3)。那么它们的和是分量相加:u + v = (4+1, 1+3) = (5, 4)。数乘运算则是每个分量乘以标量:对于 u = (4, 2), (3/2)u = (4*3/2, 2*3/2) = (6, 3)。这些坐标运算与几何定义是一致的。

点的线性插值
在图形学中,经常需要计算两点间的中点或进行插值。给定点 A = (3, 4) 和 B = (7, 2),其中点 M 可以通过公式 M = (A+B)/2 计算。利用分配律:
M = ( (3,4) + (7,2) ) / 2 = (10, 6) / 2 = (5, 3)
或者先缩放再相加:M = (3,4)/2 + (7,2)/2 = (1.5, 2) + (3.5, 1) = (5, 3)。

向量的范数(长度)📏

范数(或称长度、模长)用于度量向量的大小。


范数的直观性质
基于几何直觉,范数应满足以下性质:
- 非负性:
||u|| >= 0。 - 零点:
||u|| = 0当且仅当u是零向量。 - 齐次性:
||a u|| = |a| * ||u||。 - 三角不等式:
||u|| + ||v|| >= ||u + v||(最短路径是直线)。
常见的范数定义
- 欧几里得范数(L2范数):对于向量
u = (u1, u2, ..., un),其定义为:
||u|| = sqrt(u1^2 + u2^2 + ... + un^2)
例如,u = (4, 2)的范数为sqrt(4^2 + 2^2) = sqrt(20) = 2√5。 - 函数的L2范数:对于定义在区间
[0,1]上的函数f(x),其L2范数定义为:
||f|| = sqrt( ∫_0^1 f(x)^2 dx )
这个定义与向量的欧几里得范数在思想上一致(求和变为积分)。例如,对于函数f(x) = √3 * x,其范数计算如下:
||f||^2 = ∫_0^1 (√3 x)^2 dx = ∫_0^1 3x^2 dx = [x^3]_0^1 = 1,所以||f|| = 1。

注意:在图形学中,为不同应用选择合适的范数至关重要。例如,对于图像,直接使用像素亮度*方和的L2范数可能会认为明亮的天空比内容丰富的暗处图像“更大”。但如果我们关心的是图像的细节(如边缘),则可以考虑使用图像梯度的范数,这样内容丰富的图像会具有更大的范数值。
内积与角度 📐

内积(或点积、标量积)用于度量两个向量的方向对齐程度。

内积的直观性质与定义
- 对称性:
<u, v> = <v, u>。(对齐程度与顺序无关) - 正定性:
<u, u> >= 0,且等于0当且仅当u=0。 - 线性性:
<au, v> = a<u, v>,<u+v, w> = <u,w> + <v,w>。
几何上,对于单位向量,内积等于一个向量在另一个向量上投影的长度。

常见的内积定义
- 欧几里得内积:对于向量
u, v ∈ R^n:
<u, v> = u1*v1 + u2*v2 + ... + un*vn
例如,u=(4,1),v=(1,3)的内积为4*1 + 1*3 = 7。 - 函数的L2内积:对于函数
f, g:
<f, g> = ∫_0^1 f(x)g(x) dx
例如,f(x)=x^2,g(x)=1-x^2,它们的内积∫_0^1 x^2(1-x^2) dx值较小,说明这两个函数“方向”差异较大。

线性映射 🗺️

线性代数的另一个核心是研究向量空间之间的线性映射。

什么是线性映射?
直观上,线性映射是将直线映射为直线,并且保持原点不变的变换。形式化定义是:一个映射 f 是线性的,如果它满足:
f(u + v) = f(u) + f(v)f(a u) = a f(u)
即,映射保持向量的加法和数乘运算。

重要区分:函数 f(x) = ax + b 的图像是一条直线,但它不是线性映射(除非 b=0),因为它不满足上述定义(例如,f(x1+x2) ≠ f(x1)+f(x2))。这种函数被称为仿射函数。在图形学中,我们经常通过引入齐次坐标的技巧将仿射变换转化为线性变换来处理。

线性映射的矩阵表示
虽然理解几何本质更重要,但为了数值计算,我们常用矩阵来表示线性映射。设有一个从 R^2 到 R^3 的线性映射 f,它由两个固定的三维向量 a1, a2 定义:f(u) = u1*a1 + u2*a2。
我们可以构造一个 3x2 的矩阵 A,其列就是向量 a1 和 a2。那么,通过矩阵向量乘法 A * [u1, u2]^T,我们就能得到 f(u) 的结果。矩阵向量乘法的几何意义正是对矩阵列向量的线性组合。

基、正交基与傅里叶分析 🔢

基与张成空间
一组向量 {v1, v2, ..., vk} 的张成空间是所有能表示为它们线性组合的向量的集合。如果一组向量能张成整个空间 R^n,且个数正好为 n 并线性无关,则这组向量构成 R^n 的一个基。任何向量都可以用基向量的坐标来唯一表示。

正交基
如果一个基中的所有向量两两正交(内积为0)且都是单位向量(范数为1),则称之为标准正交基。在标准正交基下,向量的欧几里得范数*方等于其各坐标分量的*方和,计算非常方便。
格拉姆-施密特正交化是一种将一组线性无关向量转化为标准正交基的算法。对于大规模或数值敏感的问题,可以使用更稳定的QR分解算法。

函数空间的正交基与傅里叶分析
函数空间也可以有标准正交基。一个经典的例子是对于周期为 2π 的函数,正弦和余弦函数序列 {1, cos(nx), sin(mx)}(经过适当归一化)构成了一组正交基。将任意周期函数投影到这组基上,就得到了它的傅里叶级数表示,其系数反映了函数在不同频率分量上的强度。
这种将信号(如音频、图像、几何)分解为不同频率成分的思想(傅里叶分析),是图形学中许多算法(如滤波、压缩、模拟)的基础。


线性方程组 ⚖️


线性方程组由多个线性方程构成,形如 A x = b,其中 A 是矩阵,x 是未知向量,b 是常数向量。
几何解释
求解线性方程组 A x = b 可以理解为:
- 寻找多个超*面(或直线、*面)的交点。
- 寻找原像:给定线性映射
A下的像b,找哪个x能映射到b。
解的存在性与唯一性
线性方程组的解可能有三种情况:
- 无解:例如,方程组描述的两条直线*行但不重合,或者
b不在映射A的值域内。 - 唯一解:方程组描述的超*面相交于一点。
- 无穷多解:方程组描述的超*面相交于一条线或一个面等。例如,当从高维空间映射到低维空间时(如三维投影到二维),一个二维点对应无穷多个三维原像。
在编写图形学算法时,必须意识到并处理这些情况,因为数值求解器可能不会自动给出明确警告。

关于矩阵的思考 💭

虽然矩阵是数值计算中不可或缺的工具,但过度依赖矩阵表示可能会阻碍对线性代数几何本质的理解。矩阵的条目依赖于坐标系的选择,同一个线性映射在不同基下对应的矩阵完全不同。此外,矩阵还可以用来表示二次型等其他对象,容易混淆。
因此,在学习线性代数时,应始终将矩阵操作与其背后的几何意义(向量、线性映射、基变换等)联系起来。



本节课中我们一起学习了线性代数的核心概念:从向量的几何直观出发,理解了向量空间、范数、内积、线性映射的实质。我们看到了这些概念不仅适用于箭头,也适用于函数和图像等对象。我们还探讨了基、正交基、傅里叶分析的思想,以及线性方程组的几何含义。掌握这些概念的几何本质,而不仅仅是矩阵运算规则,对于在计算机图形学中进行创造性思考和算法设计至关重要。
4:向量微积分复习 📚




在本节课中,我们将复习向量微积分(或称多元微积分)的核心概念。这些数学工具对于理解计算机图形学中的空间关系、变化率、变换以及物理模拟至关重要。我们将从基本的向量运算开始,逐步深入到梯度、散度、旋度、拉普拉斯算子和海森矩阵等微分算子。

向量运算回顾 🔄

上一节我们介绍了线性代数的基础知识。本节中,我们来看看在几何背景下,向量运算的具体含义。
欧几里得范数

在几何计算中,我们最常关心的是欧几里得范数。它衡量的是向量的几何长度,并且这个长度在空间的刚性运动(旋转、*移、反射)下保持不变。在正交归一化基下,向量 u 的欧几里得范数可以写为:
[
| \mathbf{u} | = \sqrt{u_1^2 + u_2^2 + \dots + u_n^2}
]
注意:这个坐标表达式仅在向量表示在正交归一化基下时,才代表其几何长度。

欧几里得内积(点积)

对于几何计算,我们使用欧几里得内积来衡量两个向量的对齐程度。它的几何定义不依赖于坐标:
[
\langle \mathbf{u}, \mathbf{v} \rangle = | \mathbf{u} | | \mathbf{v} | \cos \theta
]
其中 (\theta) 是两向量间的夹角。在正交归一化基下,这等价于点积:
[
\mathbf{u} \cdot \mathbf{v} = \sum_{i=1}^{n} u_i v_i
]
同样,此表达式仅在正交归一化基下才有几何意义。
叉积
叉积是三维空间特有的运算,它输入两个向量,输出一个向量:(\mathbf{u} \times \mathbf{v})。
- 几何意义:结果向量的大小等于由 u 和 v 张成的*行四边形的面积。
- 方向:结果向量正交于 u 和 v 所在的*面。
方向由右手定则或行列式定义确定。在正交归一化基下,其坐标表达式为:
[
\mathbf{u} \times \mathbf{v} = \begin{pmatrix}
u_2 v_3 - u_3 v_2 \
u_3 v_1 - u_1 v_3 \
u_1 v_2 - u_2 v_1
\end{pmatrix}
]
一个有用的技巧是:与单位法向量 n 的叉积,等价于在垂直于 n 的*面内旋转90度。

行列式与体积 📦

现在,让我们将视角转向行列式,并理解其深刻的几何意义。

行列式在三维空间中有清晰的几何解释:三个向量 u, v, w 的行列式的绝对值,等于由它们张成的*行六面体的体积。其符号表示向量的取向(手性)。

这通过三重积公式联系起来:
[
\text{det}(\mathbf{u}, \mathbf{v}, \mathbf{w}) = \mathbf{u} \cdot (\mathbf{v} \times \mathbf{w}) = \mathbf{v} \cdot (\mathbf{w} \times \mathbf{u}) = \mathbf{w} \cdot (\mathbf{u} \times \mathbf{v})
]
对于一个表示线性映射的矩阵 A,其行列式的绝对值表示该映射对单位体积的缩放因子。

以下是几个有用的向量恒等式:
- 标量三重积:(\mathbf{u} \cdot (\mathbf{v} \times \mathbf{w}) = \mathbf{v} \cdot (\mathbf{w} \times \mathbf{u}) = \mathbf{w} \cdot (\mathbf{u} \times \mathbf{v}))
- 向量三重积:(\mathbf{u} \times (\mathbf{v} \times \mathbf{w}) = \mathbf{v}(\mathbf{u} \cdot \mathbf{w}) - \mathbf{w}(\mathbf{u} \cdot \mathbf{v}))
- 雅可比恒等式:(\mathbf{u} \times (\mathbf{v} \times \mathbf{w}) + \mathbf{v} \times (\mathbf{w} \times \mathbf{u}) + \mathbf{w} \times (\mathbf{u} \times \mathbf{v}) = 0)

微分算子 🔍

理解了静态的向量关系后,本节我们来看看如何描述向量场的变化率,这些是物理模拟和几何处理的核心。
微分算子为我们提供了描述变化率的语言,广泛应用于求解偏微分方程和数值优化中。

方向导数与梯度

对于多元函数 (f: \mathbb{R}^n \to \mathbb{R}),在点 (\mathbf{x}0) 沿方向 (\mathbf{u}) 的方向导数定义为:
[
D{\mathbf{u}} f(\mathbf{x}0) = \lim \frac{f(\mathbf{x}_0 + \epsilon \mathbf{u}) - f(\mathbf{x}_0)}{\epsilon}
]
梯度 (\nabla f) 是一个向量场,它包含了函数在所有方向上的变化信息。梯度方向是函数值上升最快的方向。
- 坐标定义:(\nabla f = \left( \frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, \dots, \frac{\partial f}{\partial x_n} \right)^\top)
- 与方向导数的关系:(D_{\mathbf{u}} f = \nabla f \cdot \mathbf{u})
- 线性*似的观点:(f(\mathbf{x}) \approx f(\mathbf{x}_0) + \nabla f(\mathbf{x}_0) \cdot (\mathbf{x} - \mathbf{x}_0))

矩阵与函数的梯度


梯度计算可以推广到更抽象的对象。例如:
- 对于 (f(\mathbf{x}) = \mathbf{x}^\top \mathbf{y}),有 (\nabla_{\mathbf{x}} f = \mathbf{y})。
- 对于 (f(\mathbf{x}) = \mathbf{x}^\top \mathbf{x}),有 (\nabla_{\mathbf{x}} f = 2\mathbf{x})。

甚至对于“函数的函数”(泛函),如 (F(f) = \langle f, g \rangle_{L^2} = \int f g),其“梯度”为 (\nabla F = g)。这表明处理更复杂对象的微分时,其核心模式与普通微积分相似。
向量场的导数:散度与旋度 🌪️

对于向量场 (\mathbf{X}: \mathbb{R}^n \to \mathbb{R}^n),我们有两种基本的导数。

散度
散度 (\nabla \cdot \mathbf{X}) 衡量向量场在某点是“源”(发散)还是“汇”(汇聚)。
- 直观理解:想象流体的流动,散度为正表示流体从该点流出,为负表示流入。
- 坐标定义(在 (\mathbb{R}^n)):(\nabla \cdot \mathbf{X} = \sum_{i=1}^n \frac{\partial X_i}{\partial x_i})
- 输出:标量场。

旋度

旋度 (\nabla \times \mathbf{X}) 衡量向量场在某点的旋转强度和方向。
- 直观理解:想象流体的漩涡,旋度的大小表示旋转的强弱,方向由右手定则确定(垂直于旋转*面)。
- 坐标定义(在 (\mathbb{R}^3)):
[
\nabla \times \mathbf{X} = \left( \frac{\partial X_3}{\partial x_2} - \frac{\partial X_2}{\partial x_3},\ \frac{\partial X_1}{\partial x_3} - \frac{\partial X_3}{\partial x_1},\ \frac{\partial X_2}{\partial x_1} - \frac{\partial X_1}{\partial x_2} \right)^\top
] - 输出:向量场(在3D中)。
- 二维旋度:常指上述结果的第三个分量,是一个标量场,表示垂直于*面的旋转。

在二维中,一个有趣的联系是:一个向量场的散度,等于将该场旋转90度后所得向量场的旋度。

拉普拉斯算子 Δ 📐

拉普拉斯算子是图形学中无处不在的核心算子,用于描述函数的“*均曲率”或“*滑度”,是模拟热传导、波动等现象的基础。



拉普拉斯算子 (\Delta f) 作用在标量函数上,输出另一个标量函数。它是一个线性算子。
- 多种等价定义:
- 梯度的散度:(\Delta f = \nabla \cdot (\nabla f))
- 二阶偏导和:(\Delta f = \sum_{i=1}^n \frac{\partial^2 f}{\partial x_i^2})
- 狄利克雷能量的梯度:(\Delta f = -\nabla \left( \frac{1}{2} | \nabla f |^2 \right)) (在 (L^2) 内积下)
- 与邻域*均的偏差:在离散网格上,(\Delta f) *似于“邻居值的*均”减去“中心值”。
- 直观意义:在一点处,拉普拉斯算子值为正,说明该点函数值低于其周围*均值(如山谷底部);为负则说明高于周围*均值(如山峰顶部)。


海森矩阵 ∇² 📈
最后,我们介绍海森矩阵,它包含了函数的所有二阶导数信息,对于构建局部二次*似和设计高级优化算法至关重要。
海森矩阵 (\nabla^2 f) 是函数 (f: \mathbb{R}^n \to \mathbb{R}) 的二阶导数矩阵。
- 定义:它是一个 (n \times n) 的对称矩阵,其第 ((i, j)) 项为 (\frac{\partial^2 f}{\partial x_i \partial x_j})。
- 作用:对向量 (\mathbf{u}) 应用海森矩阵,得到的是梯度在方向 (\mathbf{u}) 上的方向导数:((\nabla^2 f) \mathbf{u} = D_{\mathbf{u}} (\nabla f))。
- 在泰勒展开中的应用:函数在点 (\mathbf{x}_0) 附*的二阶泰勒展开为:
[
f(\mathbf{x}) \approx f(\mathbf{x}_0) + \nabla f(\mathbf{x}_0)^\top (\mathbf{x} - \mathbf{x}_0) + \frac{1}{2} (\mathbf{x} - \mathbf{x}_0)^\top [\nabla^2 f(\mathbf{x}_0)] (\mathbf{x} - \mathbf{x}_0)
]
这给出了函数在局部的最佳二次*似。

总结 🎯


本节课中我们一起学习了向量微积分的关键内容:
- 几何向量运算:欧几里得范数、内积(点积)和叉积的几何意义与坐标表示。
- 行列式:其几何解释是体积(或面积),并通过三重积与点积、叉积关联。
- 梯度:描述了多元函数增长最快的方向,是方向导数的汇集,也是线性*似的核心。
- 散度与旋度:分别描述了向量场的“源汇”特性和“旋转”特性。
- 拉普拉斯算子:衡量函数的曲率或*滑度,是许多物理过程(如扩散、波动)的数学基础。
- 海森矩阵:包含了函数的所有二阶信息,用于构建二次*似和进行二阶优化。


掌握这些概念和它们的几何直观,将为后续学习计算机图形学中的光照、着色、几何处理、物理模拟等高级主题打下坚实的数学基础。下一讲,我们将正式进入光栅化,探讨如何将三角形快速绘制到屏幕上。
5:绘制三角形和采样介绍 🖥️
在本节课中,我们将要学习计算机图形学中一个看似基础但至关重要的任务:如何在屏幕上绘制三角形。这个问题将揭示图形学中的许多深层挑战。我们将介绍光栅化技术,这是将几何图形转换为屏幕像素的两种主要方法之一。
光栅化与光线追踪概述
上一节我们介绍了计算机图形学的两种主要技术。本节中我们来看看第一种:光栅化。
光栅化的基本思想是:对于每一个图元(如三角形、线段或点),我们需要确定图像的哪些像素应该被点亮。这项技术非常流行且速度极快,使用现代图形硬件每秒可以绘制数十亿个三角形。
然而,光栅化有一个显著的缺点:它很难生成具有逼真阴影、反射和各种光照效果的照片级真实感图像。这使得光栅化非常适合2D矢量艺术、卡通图像、字体渲染以及场景的快速3D预览。
在本课程后期,我们将讨论第二种技术:光线追踪。其核心操作是:对于每个像素,判断哪些图元(如三角形)通过该像素可见。光线追踪可以生成逼真的图像,但速度要慢得多,生成一帧图像可能需要几分钟甚至几小时。
图形渲染管线 🚀

为了理解图像生成,我们可以将其视为一个管线。这是一个在计算机科学中广泛存在的概念,其基本思想是将计算结构化为一系列阶段。每个阶段都以高度结构化的方式请求输入数据,并产生结构化的输出数据。这种高度结构化和可预测性使得每个阶段内的计算可以被极大地简化和优化。
我们已经在本课程的第一讲中见过一个简单的图形管线例子:绘制一个立方体。我们首先将三维顶点投影到二维图像*面上,然后将这些点连接成线段。现代光栅化管线则更为复杂。
现代光栅化管线输入与输出
现代光栅化管线的基本输入是点和三角形。实际上,我们想要绘制的任何物体最终都可以用三角形(可能带有附加属性)来表示。除了顶点的空间位置,我们可能还知道每个顶点的颜色或纹理坐标。
这些数据被送入光栅化管线,最终输出是一个位图,即一个像素值数组。每个像素存储一个颜色值,可能还包含深度(距离屏幕的远*)和透明度等附加属性。
我们的目标是构建管线中的各个阶段,理解如何从基本的输入数据生成最终的图像。值得注意的是,实际的光栅化通常由图形处理单元执行,这是一块专门为高效执行光栅化管线而设计的硬件芯片。
为什么是三角形?🔺

人们为图形管线的设计思考了多年,最终决定将三角形作为基本的原子图元。这有几个非常重要的原因:
以下是使用三角形作为基本图元的主要优点:
- 强大的*似能力:足够多的三角形可以*似表示任何复杂形状。
- *面性:三个顶点总是位于同一个*面上。这对于着色计算(如计算表面法线方向)至关重要。
- 易于插值:在三角形三个角点上的数据(如颜色)可以非常高效、*滑地插值到整个三角形内部,这通过重心坐标实现。
- 简化优化:一旦所有图形都转化为三角形,我们就可以专注于构建一个为绘制三角形而高度优化的管线。
因此,你可以将图形管线视为一个“三角形管线”,它是一个超级优化的机器,只专注于把三角形画得又快又好。
光栅化管线阶段总览


接下来,我们将详细探讨光栅化管线的各个阶段。首先给出一个高层概览:
- 变换与定位:将三角形列表中的物体变换并定位到三维场景中的指定位置。
- 投影:将这些三维物体投影到二维屏幕上,就像我们对立方体所做的那样。
- 采样三角形覆盖范围:对于每个三角形,判断屏幕上的哪些像素被该三角形覆盖。这是本节课的重点。
- 插值顶点属性:在三角形内部*滑地插值顶点的属性(如颜色、纹理坐标)。
- 合成:将所有三角形的颜色(可能还包括深度和透明度信息)组合成最终的图像。
这是一个对真实世界图形管线(如OpenGL、Direct3D、Vulkan)的很好概括。这些API本质上是与图形管线各个阶段通信的接口。
绘制三角形:覆盖与遮挡问题 🎯
现在,让我们尝试在屏幕上绘制三角形。为此,我们需要回答两个核心问题:
- 覆盖问题:对于一个给定的三角形(例如红色三角形),它覆盖了哪些像素?这是光栅化的基本问题。
- 遮挡问题:如果红色三角形和蓝色三角形都覆盖了同一个像素,哪个三角形离相机更*?我们最终应该看到哪个颜色?这就是可见性问题。
我们可以用小孔相机模型来思考遮挡问题:从针孔通过每个像素向外发射一条光线,这条光线可能会击中场景中的多个三角形,第一个被击中的三角形就是离相机最*的,决定了该像素的颜色。
本节课我们将主要关注三角形覆盖问题。
定义像素覆盖
要实际实现管线的这一阶段,我们需要理解一个基本问题:一个像素被三角形覆盖意味着什么?

如果我们放大观察一个像素和几个三角形:
- 三角形1完全未与像素相交,显然不覆盖。
- 三角形4完全覆盖了整个像素,显然覆盖。
- 三角形2和3部分覆盖了像素,情况就比较模糊。

这表明,也许我们需要的输出不是一个简单的“是/否”二进制值,而是像素被三角形覆盖的面积比例。这样,如果三角形覆盖了像素的10%,该像素就显示10%的红色;覆盖了60%,就显示60%的红色,以此类推。
然而,精确计算这个覆盖比例非常困难,尤其是在考虑多个三角形相互重叠、穿透等复杂情况时。
将光栅化视为采样问题 📊
我们的基本策略是:承认精确计算覆盖比例不切实际,而是将确定覆盖范围的问题视为一个采样问题。

这意味着我们不去计算精确的解析解,而是在每个像素内测试一系列采样点。对于每个采样点,我们判断它是否位于三角形内部。如果我们有足够多的采样点,并且合理地布置它们,就能得到对覆盖比例的很好估计。

在深入讨论三角形采样之前,让我们先一般性地了解一下采样。
采样与重构基础
采样通常从一维信号开始。一个信号可以表示为一个函数 f(x)。采样意味着我们选取一些值 x1, x2, x3...,并记录函数在这些点上的值 f(x1), f(x2), f(x3)...。一个很好的例子是音频文件,它存储了一维声音信号(扬声器振幅随时间变化)在多个时间点上的样本。
一旦我们有了这些离散的样本数据,就面临重构问题:如何将这些数字变回我们可以聆听的连续信号?最简单的方法是分段常数*似,即对于任意 x,使用最*采样点的函数值。更好一点的方法是分段线性*似,用直线连接采样点。
如果重构信号丢失了原始信号的某些特征(如快速振荡),最根本的解决方法是提高采样率,即更密集地采样。这就是为什么消费级音频的采样率是每秒44,100次,以确保不丢失声音中的重要细节。
对于图像,情况类似。采样值测量每个像素中心(或其它位置)的图像强度或颜色。然后,我们通过某种插值或重构滤波器,将这些离散的样本值重建为整个图像*面上的连续图像。我们可以使用最*邻插值(分段常数),或双线性插值等方法。
采样是将连续信号转化为离散样本的过程。重构是其逆过程,将离散样本重建为连续信号。

光栅化中的采样
现在,让我们将采样概念与光栅化问题联系起来。在光栅化中,我们采样的函数是什么?
我们可以想象,我们试图采样的是覆盖函数。这个函数定义在整个图像*面的每一个点上(不仅仅是像素中心),其值为:
1:如果该点位于三角形内部。0:如果该点位于三角形外部。


光栅化的简单方法就是:在像素网格上的采样点(例如像素中心)处,评估每个三角形的覆盖函数。

但即使是这个基本的采样过程也可能遇到棘手情况。例如,如果两个三角形共享一条边,而这条边恰好穿过我们的采样点,那么这个采样点算作被哪个三角形覆盖?处理不当会导致图像中出现难看的瑕疵。在实际算法中,需要有清晰的定义来处理这类边界情况(例如,规定属于三角形左边缘或上边缘的采样点算作在三角形内部)。
重构与走样现象
一旦我们完成了三角形覆盖的采样,每个像素中心就得到了一个0或1的值。如何将这些离散样本变回图像?这实际上就是你的显示器所做的工作:每个图像样本被转换成一小块发光的方块,即一个像素。
如果我们简单地将每个样本值显示为一个纯色方块(即使用分段常数重构),得到的图像会呈现明显的锯齿状边缘。我们的重构信号与原始信号(理想的三角形覆盖函数)之间存在*似误差。
这引出了计算机图形学中一个核心现象:走样。走样在图形学的各个领域(几何、渲染、动画等)都会出现。
理解走样:频率视角
为了更好地理解走样,我们需要从频率的视角来思考信号。任何一维信号(如音频)都可以分解为不同频率的正弦波的叠加。低频对应缓慢变化(如低音),高频对应快速变化(如高音)。

图像也可以分解为频率成分。在图像的频率谱中,低频位于中心,对应图像中大面积的*滑变化和模糊区域;越向外,频率越高,对应图像中的边缘和细节。
走样发生在以下情况:当原始信号中包含很高的频率,但我们的采样率不够高(即采样点太少)时,这些高频成分在采样和重构后,会错误地表现为低频成分。这就是为什么车轮在视频中看起来会反向旋转,或者为什么高频正弦波采样后听起来像低频波。

奈奎斯特-香农定理与理想重构
奈奎斯特-香农定理是信号处理中非常重要的定理。它指出:如果一个信号是带限的,即不包含高于某个阈值的频率,那么只要以两倍于最高频率的速率进行采样,就可以通过使用sinc滤波器进行理想重构,完美恢复原始信号。
然而,在计算机图形学中,我们面临两个问题:
- 信号通常不是带限的。例如,三角形的覆盖函数在边界处是突变的,这需要无限高的频率成分才能精确表示。
- 理想sinc滤波器不实用。因为它具有无限支撑域,重构每个像素都需要考虑图像中所有其他像素的贡献,计算复杂度是
O(n^2),过于昂贵。





因此,不完美的采样和重构是现实,走样现象也就不可避免。



图形学中的走样瑕疵






在图形学中,走样表现为多种瑕疵:
- 锯齿:静态图像中线段或物体边缘呈现阶梯状。
- 闪烁/蠕动:动画中,精细图案(如远处棋盘格)会产生虚假的运动模式。
- 摩尔纹:规则图案采样后产生的新低频干涉图案。

抗锯齿:超采样策略
我们能做些什么来减少走样?虽然无法完全消除,但我们可以尽力匹配采样和重构过程,使输出信号尽可能接*原始信号。
对于像素,我们希望像素发出的总光量与原始连续信号在该像素区域内的总光量相同。换句话说,我们需要对输入信号在像素区域内进行积分。我们已经知道精确积分很难,所以我们可以通过采样来*似积分。

回到覆盖问题,最简单的方法是每个像素只在中心采样一次。为了减少误差,最基本的方法是增加采样频率。在最终显示分辨率固定的情况下,我们采用超采样技术。
超采样不是在每个像素只取一个样本,而是在每个像素内取多个样本(例如2x2的网格)。然后,我们将这些密集采样值*均(或滤波)下来,得到该像素的最终颜色值。例如,如果一个像素内4个采样点有2个被三角形覆盖,那么我们就认为该像素有50%的面积被覆盖,并用50%的红色来显示。
通过增加每个像素内的采样数(如4x4,16x16),我们可以得到越来越*滑的结果。但根据奈奎斯特-香农定理,只要信号包含无限频率,无论采样多密,总会有一些误差。对于像棋盘格这样的特殊情况,可能存在解析积分的方法来获得完美*滑的图像,但对于任意复杂的几何图形和纹理,采样和重构是唯一的通用解决方案。

三角形光栅化算法

现在回到图形管线的具体阶段:如何算法化地采样三角形的覆盖函数?

最基本的需求是:给定一个三角形和一个像素网格,判断哪些像素被覆盖。这可以分解为一个原子查询:如何检查一个给定点 q 是否在三角形内部?

方法本质上是检查点 q 是否同时位于三角形三条边所定义的三个半*面内。对于一条从 p_i 到 p_j 的边,可以通过向量叉积等线性代数运算来判断点 q 位于线的左侧还是右侧。需要小心处理边的顺序和恰好落在边上的情况。
遍历策略
要光栅化整个三角形,我们需要以某种顺序遍历图像中的像素(或采样点),并对每个进行内部测试。
- 增量式扫描线算法:一种传统方法,按行遍历三角形覆盖的区域,并利用相邻像素测试的相似性增量更新计算,提高效率并改善内存访问连续性。
- 并行边界盒测试:更现代的方法。首先计算三角形的轴向包围盒,然后并行测试AABB内所有像素的采样点。现代GPU硬件拥有宽并行执行单元,适合这种策略。但当三角形又长又瘦时,这种方法会测试大量无效像素,效率低下。
- 分块与层次化策略:一种混合方法。首先将屏幕分成中等大小的块,检查每个块是否与三角形相交。如果完全不相交,则跳过整个块;如果完全被包含,则整个块内的像素都点亮;否则,再进一步细分块或测试块内单个像素。现代图形硬件常采用某种形式的分块策略来早期剔除大量无效工作。理论上,还可以递归地进行层次化细分,将计算精力精确集中在需要的地方。

优化三角形光栅化算法,思考如何使计算结构与问题结构相匹配,正是计算机图形学研究的核心活动之一。
总结 📝
本节课中我们一起学习了:
- 将计算机图形学中的许多问题(特别是光栅化)置于采样与重构的框架下理解。
- 采样是将连续信号转化为数字信息的过程。
- 重构是其逆过程,将数字样本重建为连续信号。
- 走样发生在采样率不足时,重构信号错误地呈现了原始信号的特征。
- 我们可以将光栅化视为一个采样问题:采样对象是三角形覆盖函数,采样位置是像素网格,重构方式是为每个像素显示一个纯色方块(分段常数重构)。
- 在此背景下,走样表现为锯齿边缘、运动时的闪烁瑕疵等。
- 减少光栅化走样的基本策略是超采样,即提高每个像素内的采样频率以获得更好的覆盖估计。
- 三角形光栅化是图形管线的基本构建模块。所有复杂的图像绘制最终都归结为高速绘制三角形。
- 绘制单个三角形的基础是进行三次半*面测试。我们讨论了多种加速策略:增量扫描、并行测试、分块和层次化遍历等。


下一节课,我们将讨论管线中的另一个重要阶段。
5:空间变换 🎨


在本节课中,我们将要学习计算机图形学中一个非常基础的部分:对空间中的物体应用变换。我们将探讨不同类型的空间变换,了解它们的数学表示,并学习如何组合这些变换来创建复杂的图形效果。
概述

空间变换本质上是一个函数,它为空间中的每个点分配一个新的位置。我们可以将其视为一个函数 f,它将 R^n 中的点映射到 R^n 中的点。本节课我们将特别关注那些可以用线性映射(如旋转、缩放等)编码的空间变换。这些变换在计算机图形学中无处不在,用于定位或变形空间中的物体、移动摄像机、制作动画等。
什么是线性变换? ➕

上一节我们介绍了空间变换的基本概念,本节中我们来看看线性变换的具体定义。

线性变换在几何上意味着它将直线映射为直线,并且保持原点不变。从代数角度看,线性变换是保持向量加法和标量乘法运算的映射。
以下是线性变换的两个等价定义:
- 几何定义:一个映射 f: R^n → R^n 是线性的,如果它将每条直线映射为一条直线,并且保持原点固定(即 f(0) = 0)。
- 代数定义:一个映射 f: R^n → R^n 是线性的,如果对于任意向量 u, v ∈ R^n 和任意标量 a ∈ R,满足:
- f(u + v) = f(u) + f(v)
- f(a * u) = a * f(u)
我们关心线性变换的原因有很多。首先,应用线性变换的计算成本很低。其次,线性变换的组合仍然是线性的,这允许我们将多个变换矩阵相乘,合并成一个单一的变换矩阵。这种统一的表示简化了图形算法、系统(如GPU和API)以及我们的思考方式。
常见的线性变换示例 🔄
现在,让我们通过一些常见的例子来具体了解线性变换。以下是几种基本的线性变换类型及其核心不变性:
- 旋转:保持原点固定、任意两点间距离不变以及方向(如文本从左到右的阅读顺序)不变。
- 反射:保持原点固定和任意两点间距离不变,但会反转方向。
- 缩放:保持所有向量的方向不变,但改变其大小(模长)。
- 剪切:沿一个给定方向 u 位移每个点 x,位移量与该点在另一个固定方向 v 上的投影距离成正比。
这些变换的核心并非由公式或矩阵属性定义,而是由它们在变换过程中所保持的不变量所决定。例如,旋转保持距离和方向,缩放保持方向但改变大小。
旋转的表示 🌀

我们已经了解了旋转的特性,本节中我们来看看如何用数学公式和矩阵来表示旋转。

一个2D旋转可以通过一个角度 θ 来描述。它将点 x 映射到半径为 ||x|| 的圆上的一个新点。对于标准基向量:
- 点 (1, 0) 旋转 θ 后变为 (cos θ, sin θ)。
- 点 (0, 1) 旋转 θ 后变为 (-sin θ, cos θ)。
利用线性性质,我们可以将任意向量 x = (x1, x2) 表示为基向量的线性组合,然后分别旋转。这导出了2D旋转矩阵:

R(θ) = [[cos θ, -sin θ], [sin θ, cos θ]]
在3D中,围绕单个坐标轴的旋转可以类似地构造。例如,围绕 z 轴旋转 θ 角的矩阵为:
R_z(θ) = [[cos θ, -sin θ, 0], [sin θ, cos θ, 0], [0, 0, 1]]

围绕 x 轴和 y 轴的旋转矩阵结构类似,只是余弦和正弦项出现在对应的行和列中。
一个关于旋转的重要性质是:旋转矩阵的逆等于其转置,即 R^T = R^{-1}。满足 Q^T Q = I 的矩阵称为正交矩阵,它代表旋转或反射。其中,行列式为正的正交矩阵代表旋转,行列式为负的则代表反射。

缩放的表示 📏
上一节我们讨论了旋转,本节中我们来看看另一种基本变换:缩放。
缩放将每个向量 u 映射为一个标量倍数 a u,其中 a 是一个实数。它的基本不变量是保持所有向量的方向。缩放是一个线性变换。
均匀缩放(所有轴按相同比例)可以用一个对角矩阵表示:

D = a * I,其中 I 是单位矩阵。

非均匀缩放(各轴按不同比例)的矩阵为:
D = diag(a, b, c),作用于向量 (u1, u2, u3) 得到 (a u1, b u2, c u3)。

如果我们想沿着任意一组正交轴进行非均匀缩放,可以先旋转到新坐标系,应用对角缩放矩阵,再旋转回来。这可以表示为 A = R^T D R,其中 R 是旋转矩阵。有趣的是,这样得到的矩阵 A 是一个对称矩阵(A = A^T)。
事实上,根据谱定理,任何对称矩阵都对应着沿某一组正交轴的非均匀缩放。如果对称矩阵是正定的(所有特征值为正),则代表的是纯粹的拉伸,没有翻转。

剪切的表示 ✂️
除了旋转和缩放,剪切也是一种基本的线性变换。
剪切沿一个给定方向 u 位移每个点 x,位移量等于 x 在另一个固定方向 v 上的投影(点积)。其函数形式为:
f_{u,v}(x) = x + (v · x) * u

这可以表示为一个矩阵:A = I + u v^T,其中 I 是单位矩阵。例如,一个在2D中随时间 t 变化的剪切矩阵可能如下所示(假设位移方向与 x 轴相关,且依赖于 y 坐标):

A(t) = [[1, cos t, 0], [0, 1, 0], [0, 0, 1]]

变换的组合与分解 🧩
我们已经学习了多种基本变换,本节中我们来看看如何将它们组合起来,以及如何分解复杂的变换。


通过矩阵乘法,我们可以将基本的旋转、缩放、剪切等变换组合成复杂的复合变换。例如:A(t) = R_x(t) * R_y(t) * S(t)。需要注意的是,矩阵乘法的顺序很重要,最后一个(最右边)矩阵最先被应用。

反过来,我们常常需要将一个给定的线性变换矩阵分解成更基本的组成部分。这种分解不是唯一的,但有多种标准方法,每种都有其用途:
- 奇异值分解 (SVD):在信号处理等领域很常见。
- LU分解:用于求解线性方程组。
- 极分解:对处理空间变换特别有用。

极分解将任意矩阵 A 分解为一个正交矩阵 Q(代表旋转/反射)和一个对称半正定矩阵 P(代表非负缩放)的乘积:A = Q P。这让我们可以直观地理解 A 中的旋转/反射成分和缩放/拉伸成分。

进一步,对 P 应用谱分解(P = V D V^T),我们可以得到奇异值分解 (SVD):A = U D V^T,其中 U 和 V 是正交矩阵,D 是对角矩阵(奇异值)。这可以解释为先旋转 (V^T),再沿坐标轴缩放 (D),最后再旋转 (U)。
这些分解在图形学中非常有用,例如用于在动画中插值两个变换。简单地线性插值两个变换矩阵通常会产生不自然的效果。更好的方法是分别对极分解中的旋转部分和缩放部分进行插值,然后再组合起来。
齐次坐标:统一表示的关键 🔑
到目前为止,我们忽略了一个非常基本的变换:*移。*移只是简单地给一个点加上一个偏移向量 u:f(x) = x + u。

然而,*移不是线性变换,因为它不保持原点,也不满足线性加法和数乘的性质。这带来了一个难题:我们如何将*移与我们之前讨论的线性变换(旋转、缩放等)统一地组合起来?
解决方案是一个巧妙的想法:使用齐次坐标。齐次坐标通过增加一个维度,将 n 维空间中的仿射变换(包括*移)表示为 n+1 维空间中的线性变换。

对于2D点 (x, y),其齐次坐标可以是 (x, y, 1) 或任何非零标量倍数 (cx, cy, c)。要恢复原始2D坐标,只需将前两个分量除以第三个分量。

在齐次坐标下,2D*移可以表示为一个3D剪切矩阵:
T(u1, u2) = [[1, 0, u1], [0, 1, u2], [0, 0, 1]]
当这个矩阵作用于齐次坐标 (x, y, 1) 时,得到 (x+u1, y+u2, 1),投影回2D后正是*移后的点。

类似地,2D旋转和缩放在齐次坐标下表示为3x3矩阵,只是在右下角增加了一个1,其他地方与2x2形式相同。这样,所有变换(线性变换和*移)都可以统一为矩阵乘法,极大地简化了组合操作。
在3D中,我们使用4x4齐次坐标矩阵。*移矩阵形式如下:
T(u, v, w) = [[1, 0, 0, u], [0, 1, 0, v], [0, 0, 1, w], [0, 0, 0, 1]]

齐次坐标的另一个巨大优势是它能清晰地区分点和向量:
- 点 的齐次坐标最后一个分量为非零值(通常为1),例如 (x, y, z, 1)。*移会影响点的位置。
- 向量 的齐次坐标最后一个分量为0,例如 (dx, dy, dz, 0)。*移不会影响向量,因为其方向性不受位置影响。这确保了在变换物体时,其法向量能正确保持正交性。
此外,齐次坐标天然地支持透视投影。透视投影矩阵可以将3D点变换为带透视除法的齐次坐标,从而实现*大远小的效果。

场景图:组织复杂变换 🌳

当场景中有许多物体以层级关系组织时(如机器人、角色),使用场景图来管理变换非常有效。
场景图是一种有向图结构,每个节点存储一个相对于其父节点的变换矩阵(通常是4x4齐次坐标矩阵)。要计算某个节点在世界空间中的最终变换,需要将从根节点到该节点路径上的所有变换矩阵依次相乘。



这种层级结构的好处是:
- 局部控制:移动父节点(如身体)会自动影响所有子节点(如手臂、腿)。
- 消除冗余:可以通过实例化来重复使用相同的几何模型,只需在不同节点应用不同的变换矩阵即可,无需复制几何数据。
- 组织清晰:可以方便地管理模型、灯光、摄像机等场景元素。



在遍历场景图应用变换时,必须牢记变换顺序至关重要。不同的乘法顺序会产生完全不同的结果。例如,“先缩放再*移”与“先*移再缩放”得到的效果是不同的。

一个常见的技巧是:要绕一个物体的自身中心旋转,需要先将物体*移到原点,旋转,再*移回原位置。

总结
本节课中我们一起学习了计算机图形学中空间变换的核心知识。
我们了解到,变换从根本上是由其保持的不变量所定义的。我们探讨了几种基本的线性变换:缩放、旋转、反射、剪切,以及非线性的*移和透视投影。我们看到了通过引入齐次坐标,可以将*移和透视投影也表示为高维空间中的线性变换,从而实现所有变换的统一矩阵表示。

我们还学习了如何通过矩阵乘法组合基本变换来创建复杂效果,以及如何通过极分解、SVD等方法分解复杂变换。这些分解有助于理解变换的几何意义并进行*滑插值。
最后,我们介绍了场景图这一强大工具,它利用变换的层级关系来高效、清晰地组织复杂场景中的众多物体及其变换。


所有这些知识为我们构建完整的图形渲染管线奠定了基础:从定义模型局部坐标,通过场景图进行层级变换,应用视图和投影变换,最终到屏幕像素坐标,每一步都离不开对空间变换的深入理解和熟练运用。
7:3D旋转与复数表示 🌀









在本节课中,我们将深入学习三维旋转这一特别有趣且有时颇具挑战性的变换类型。我们还将探讨复数和其它复数表示如何让处理变换变得更加容易。

回顾:什么是旋转?

上一节我们介绍了空间变换,本节中我们来看看旋转。旋转不是由某个公式或矩阵属性定义的,而是由变换所保持的不变量定义的。当我们看到一个物体在旋转时,比如这本旋转的书,我们知道它是旋转,原因有二:
- 物体上任意两点间的长度和距离保持不变,没有拉伸或剪切。
- 方向得以保持,书上的文字始终可读。
这与镜像反射不同。此外,当我们说“旋转”时,通常指固定原点的变换,否则就是旋转加*移的组合。
旋转的自由度与顺序

现在,我们来深入探讨旋转的行为和特性。一个非常基本的问题是:描述一个旋转需要多少个参数?

一个很自然的想法是三个:分别指定绕X轴、Y轴和Z轴旋转的角度。但问题是,三个数足够吗?一个旋转矩阵是3x3矩阵,有9个元素,三个角度真的能描述所有旋转吗?
让我们从几何角度思考。假设我们想旋转地球,让匹兹堡移动到圣保罗的位置。这至少需要两个数:目标城市的纬度和经度。但这是唯一能把匹兹堡转到圣保罗的旋转吗?一旦我们将匹兹堡转到圣保罗,我们还可以让地球绕着通过圣保罗的轴再旋转,而圣保罗保持不动。因此,我们实际上有三个自由度:两个用于将球面上的一个点转到另一个点,第三个用于指定绕新点的旋转。
接下来,我们看看旋转的操作顺序。在二维中,旋转顺序无关紧要,旋转角度可以简单相加,我们说二维旋转是可交换的。但在三维中,情况截然不同。

你可以通过一个简单的实验来验证:拿一个水瓶,建立坐标系(例如,Y轴向上,Z轴指向你,X轴向右)。按顺序执行以下旋转:
- 绕Y轴旋转90度。
- 绕Z轴旋转90度。
- 绕X轴旋转90度。


记录最终方向。然后,将水瓶放回初始状态,按相反顺序执行相同的旋转。你会发现,最终方向完全不同。这个实验生动地证明了三维旋转不可交换。在编写涉及旋转的代码(如飞机控制系统)时,顺序错误可能导致严重后果。

从二维旋转矩阵到欧拉角

上一节我们思考了旋转的本质,本节中我们来看看如何具体表示旋转进行计算。首先,回顾一下我们是如何得到二维旋转矩阵的。

想象一个函数 s(θ),它输入一个角度θ,输出单位圆上从X轴正方向逆时针旋转θ角后得到的点 (x, y)。那么:
- 基向量
e1旋转θ角后就是s(θ)。 - 基向量
e2旋转θ角后是s(θ + π/2)。
对于任意向量 u = a*e1 + b*e2,由于旋转是线性变换,旋转后的向量为 a*s(θ) + b*s(θ + π/2)。因此,旋转矩阵的列就是变换后的基向量,即 [s(θ), s(θ + π/2)]。代入 s(θ) = (cosθ, sinθ) 和 s(θ+π/2) = (-sinθ, cosθ),就得到了熟悉的二维旋转矩阵:
R(θ) = [ cosθ -sinθ ]
[ sinθ cosθ ]

那么,如何表示三维旋转呢?一个直接的想法是利用我们已经掌握的二维旋转,分别绕X、Y、Z轴进行旋转。这种方案称为欧拉角。
欧拉角的优点是概念上非常容易理解。但它的缺点很多,其中一个著名的问题是万向节死锁。
当你使用欧拉角(θx, θy, θz)旋转物体时,可能会到达一个特殊位置(例如 θy = π/2),此时改变θx或θz参数,物体可能不再绕预期的轴旋转,而是被“锁”在了一个单一的旋转*面上,失去了一个自由度。这是因为欧拉角参数化方式本身存在奇点。在飞机控制或3D软件操作中,这可能表现为控制失灵或视角卡住。

另一种表示是轴-角表示:给定一个旋转轴 u 和一个角度 θ,有一个通用的矩阵公式来描述绕该轴的旋转。虽然可以直接使用这个矩阵,但公式较为复杂。接下来,我们将看到更好的方法。

复数的几何视角
在进入三维之前,我们需要先理解复数,它将为我们提供一种优雅的方式来处理二维变换。请完全忘记“i是-1的*方根”这种说法。我们从几何角度重新认识它。

我们将虚数单位 i 定义为逆时针方向的90度旋转。
- 从向量 1(指向X轴正方向)开始。
- i * 1 = i,得到一个指向Y轴正方向的向量。
- i * i = i²,即两次90度旋转,得到指向X轴负方向的向量,所以 i² = -1。
- 继续旋转,i³ = -i,i⁴ = 1。

复数就是二维向量,只是我们换了一套基的名称:用 1 代替 e1,用 i 代替 e2。一个复数 z = a + bi 就等价于向量 (a, b)。我们拥有向量的所有常规操作(加法和标量乘法),但额外增加了一个关键操作:复数乘法。

复数乘法在几何上非常简洁:
- 幅值相乘:如果
z1长度为r1,z2长度为r2,则乘积长度为r1 * r2。 - 角度相加:如果
z1角度为θ1,z2角度为θ2,则乘积角度为θ1 + θ2。
在笛卡尔坐标下,复数乘法的公式为:
(a+bi) * (c+di) = (ac - bd) + (ad + bc)i
其中 (ac - bd) 称为实部,(ad + bc) 称为虚部,这只是历史名称,没有特别含义。

更优雅的写法是利用欧拉公式:e^(iθ) = cosθ + i sinθ。这里 e^(iθ) 可以简单地理解为单位圆上角度为θ的点。这样,任何复数都可以写为 z = r * e^(iα),其中 r 是幅值,α 是角度。那么复数乘法就是:
z1 * z2 = (r1 * e^(iα)) * (r2 * e^(iβ)) = (r1 * r2) * e^(i(α+β))
复数与矩阵表示对比

现在,我们来比较使用复数和矩阵进行二维旋转的差异。假设我们要将向量 u 先旋转θ角,再旋转φ角。


- 矩阵形式:
u' = R(φ) * R(θ) * u。展开后表达式较长,涉及多个三角函数项的乘积与求和,进行进一步操作(如求导)比较繁琐。 - 复数形式:将向量
u表示为r * e^(iα)。旋转θ角对应于乘以e^(iθ),旋转φ角对应于乘以e^(iφ)。复合旋转结果为:u' = e^(iφ) * e^(iθ) * r * e^(iα) = r * e^(i(α+θ+φ))。表达式极其简洁,求导也非常直接。


虽然复数和矩阵在功能上是等价的,但复数表示带来了表达式的简化、概念的清晰以及代码的易读性。在计算机图形学中,选择最合适的表示形式来简化问题和求解至关重要。
四元数:通向三维旋转
理解了复数的妙处后,我们自然希望将其推广到三维旋转。然而,汉密尔顿发现,用三个分量的“三元数”无法实现类似复数的优雅旋转表示。他最终意识到需要四个分量,从而发明了四元数。

四元数 q 可以写为 q = a + bi + cj + dk,其中 a 是实部,(b, c, d) 构成虚部。我们可以更直观地将其视为一个标量和一个三维向量的组合:q = (a, **v**),其中 v = (b, c, d)。
四元数的乘法规则由以下基本公式定义:
i² = j² = k² = ijk = -1
由此可以推导出乘法表(如 ij = k, ji = -k)。一个关键特性是:四元数乘法不可交换(qp ≠ pq),这与三维旋转不可交换的特性是一致的。
四元数乘法用标量-向量形式表示更为简洁。对于 q1 = (a, **u**) 和 q2 = (b, **v**):
q1 * q2 = (a*b - **u·v**, a**v** + b**u** + **u×v**)
这个公式巧妙地将点积和叉积融合在了一起。特别地,对于纯虚四元数(实部为0,代表三维空间点 p = (0, x)),其乘积为:
p1 * p2 = (-**x1·x2**, **x1×x2**)
用四元数表示旋转

那么,如何用四元数表示三维旋转呢?对于一个三维向量 x(表示为纯虚四元数 p = (0, **x**))和一个单位四元数 q(满足 a² + b² + c² + d² = 1),变换 p' = q * p * q^(-1) 的结果 p' 也是一个纯虚四元数,其虚部就是向量 x 旋转后的结果。这里 q^(-1) 是 q 的逆,对于单位四元数,q^(-1) = q*(共轭,即实部不变,虚部取反)。

给定旋转轴(单位向量)u 和旋转角 θ,对应的单位四元数为:
q = cos(θ/2) + sin(θ/2) * **u**
这个公式比轴-角旋转矩阵要容易记忆和操作得多。

四元数的一个巨大优势是能方便地进行旋转插值。在关键帧动画中,我们需要在两个朝向(旋转)之间*滑过渡。线性插值欧拉角会产生奇怪的路径。而使用四元数,我们可以采用球面线性插值(Slerp)公式,在单位四元数表示的球面上沿最短弧进行插值,从而得到最自然、均匀的旋转过渡。
复数与四元数的其他应用

除了旋转,复数和四元数在图形学中还有其他重要应用:
- 纹理映射:将纹理贴合到曲面时,常希望保持局部角度不变(共形映射)。复数理论是描述和分析这类映射的自然语言。
- 分形生成:曼德博集等经典分形通过迭代复数函数生成。使用四元数迭代则可以生成绚丽的三维分形结构。
总结与拓展
本节课我们一起学习了三维旋转的复杂性及其多种表示方法。
- 从欧拉角(直观但存在万向节死锁)到轴-角表示。
- 重点探讨了复数在二维旋转中的优雅性,以及其向高维的推广——四元数,它为三维旋转提供了简洁、无奇点的表示,并支持优质的球面插值。

需要强调的是,不存在“唯一正确”的旋转表示。除了今天讨论的,还有李群与李代数(非常适合处理大旋转、旋转*均和微积分)、几何代数等众多视角。每种表示都有其适用的场景和优势。掌握越多的视角,解决问题的能力就越强,也能与更广泛的领域进行交流。旋转的数学世界丰富而美妙,值得深入探索。

下一讲,我们将讨论透视和纹理映射。
8:透视投影和纹理映射 👁️📐

在本节课中,我们将要学习透视投影和纹理映射。我们将看到如何将三维图元通过透视变换转换为二维屏幕上的像素,以及如何将二维纹理图像映射到三维表面上,为图像增添丰富的细节。同时,我们也会探讨透视变换给纹理映射带来的挑战。

上一节我们介绍了光栅化管线的基本流程,本节中我们来看看透视投影和纹理映射如何融入这个流程。
透视投影 👓
透视是我们都熟悉的效果:世界中的物体离我们越远,看起来就越小。此外,我们注意到,尤其是在图像中,*行线会在远处的地*线上汇聚。从数学角度看,人们已经理解了为什么会发生这种现象。
在计算机图形学中,我们首先需要精确掌握透视的数学原理,以生成逼真的图像。然而,有时我们可能不希望有透视效果,例如在绘制城市地图或工程示意图时,正交投影可能更有帮助,因为它能更好地保持物体尺寸。
坐标变换序列 🔄
一个物体从输入描述到最终在屏幕上显示的位置,需要经历一系列变换。以下是完整的变换序列:
- 世界坐标:物体在全局坐标系中的初始位置。
- 视图坐标:根据相机的位置和方向,对世界坐标应用逆变换,使相机位于原点并看向 -Z 轴方向。
- 裁剪坐标:将视图坐标映射到一个标准立方体(例如从 -1 到 1 的立方体)中,便于后续剔除操作。
- 归一化坐标:通过投影(透视或正交)将三维坐标映射到二维*面,得到一个在归一化坐标系(如 -1 到 1 的正方形)中的坐标。
- 屏幕坐标:将归一化坐标拉伸并映射到实际的图像尺寸上,并翻转 Y 轴(因为图像坐标系通常 Y 轴向下)。
至此,我们得到了可以在二维*面上进行光栅化的图元顶点坐标。
相机变换 🎥





为了将物体置于以相机为中心的坐标系中,我们需要进行相机变换。这通常包括一个*移(将相机位置移至原点)和一个旋转(使相机看向 -Z 轴方向)。






对于一个看向方向 w 的相机,我们可以构造一个由三个正交向量 u, v, w 组成的旋转矩阵 R,其列向量分别为 u, v, w。这个矩阵将标准坐标轴映射到新的相机坐标系。由于我们需要对物体应用相机的逆变换,因此实际使用的变换矩阵是 R 的转置。
视锥体与裁剪 📦
视锥体是相机可以看到的空间区域。为了简化,我们总是假设相机位于原点并看向 -Z 轴。视锥体由**面、远*面以及左、右、上、下*面界定。
裁剪是剔除位于视锥体之外的三角形的过程。这样做可以避免光栅化那些不可见的图元,从而节省大量计算资源。对于部分在视锥体内的三角形,通常的做法是将其分割成多个完全在内部的子三角形,因为图形硬件专门为绘制三角形进行了优化。
设置**面和远*面的一个重要原因与深度缓冲有关。深度缓冲用于处理物体间的遮挡关系。如果深度值的范围设置得过大(例如**面非常*,远*面非常远),会导致深度值的精度不足,从而产生“Z-fighting”的视觉伪影,即两个表面在深度上“打架”,产生闪烁的锯齿状边缘。因此,通常将裁剪*面设置为刚好包含场景中的所有几何体。
投影矩阵 📏
在光栅化管线中,一个重要步骤是将视锥体映射到标准立方体(-1 到 1)。这可以通过一个矩阵变换来完成。
对于正交投影,变换矩阵 M_ortho 可以表示为:
M_ortho = [
[2/(r-l), 0, 0, -(r+l)/(r-l)],
[0, 2/(t-b), 0, -(t+b)/(t-b)],
[0, 0, 2/(n-f), -(n+f)/(n-f)],
[0, 0, 0, 1]
]
其中,l, r, b, t, n, f 分别代表左、右、下、上、*、远*面的坐标。
对于透视投影,矩阵更为复杂,其核心思想是将 x 和 y 坐标除以 z 坐标。在齐次坐标中,这可以通过构造一个矩阵来实现,该矩阵将 z 值复制到齐次坐标分量 w 中。经过齐次除法后,就得到了透视投影的效果。
完成投影和齐次除法后,我们得到了归一化设备坐标(NDC),最后再通过一个屏幕变换将其映射到最终的像素坐标。
属性插值 🎨
在光栅化三角形时,我们不仅需要确定像素是否在三角形内,还需要在三角形内部*滑地插值顶点属性,例如颜色或纹理坐标。
重心坐标 🧮
在二维三角形中,任何内部点 x 都可以用三个顶点 A, B, C 的重心坐标 (α, β, γ) 来表示,其中 α + β + γ = 1。点 x 的属性值 f(x) 可以通过顶点属性值 f_A, f_B, f_C 的线性插值得到:

f(x) = α * f_A + β * f_B + γ * f_C


重心坐标可以通过面积比或点到对边的距离比来计算。有趣的是,在光栅化过程中进行三角形包含性测试(半*面测试)时,我们实际上已经计算出了与重心坐标成比例的量。


透视校正插值 🔧
然而,在三维空间中进行透视投影后,直接在屏幕空间的二维三角形上进行线性插值是不正确的。因为投影会扭曲空间关系。我们需要进行“透视校正插值”。
透视校正插值的步骤如下:
- 对于每个顶点属性 φ 和其深度值 z,预先计算
Z = 1/z和P = φ / z。 - 在屏幕空间上,使用标准的二维重心坐标插值
Z和P。 - 对于每个像素,最终的属性值为
φ = P / Z。

这种方法等价于在三维空间中线性插值属性,然后再进行投影。
纹理映射 🖼️
纹理映射的核心思想是将二维图像“包裹”到三维物体表面,以增加视觉细节,而无需增加几何复杂度。

纹理坐标 📍

纹理映射需要为表面上的每个点指定一个二维纹理坐标 (u, v),通常范围在 [0, 1] 之间。对于三角形网格,我们在每个顶点上定义纹理坐标,然后在三角形内部通过重心坐标进行插值,得到每个像素对应的 (u, v)。
纹理采样与滤波 🔍
得到像素的纹理坐标 (u, v) 后,我们需要从纹理图像中获取颜色。这个过程称为纹理采样。由于 (u, v) 是连续值,而纹理图像是离散的像素(纹素)阵列,因此需要解决如何从离散数据中获取连续值的问题,并避免走样(Aliasing)。

根据屏幕像素覆盖的纹理区域大小,我们面临两种情况:
-
放大(Magnification):屏幕像素小于纹素。此时,简单的最*邻采样会导致块状伪影。更好的方法是双线性插值,它使用目标点周围四个最*纹素的加权*均值。
-
缩小(Minification):屏幕像素覆盖了一大片纹理区域。此时,如果只采样一个点,会丢失大量信息并导致严重的走样(如摩尔纹)。我们需要计算该覆盖区域内的*均颜色。
直接计算区域*均值非常耗时。因此,通常采用预滤波技术,即预先计算并存储纹理在不同尺度下的模糊版本,形成一个图像金字塔,称为 Mipmap。
Mipmap 🗺️
Mipmap 是一系列逐渐降采样的纹理图像。第 0 级是原始分辨率图像,第 1 级是长宽各缩小一半的图像(通过对上一级 2x2 区域求*均得到),依此类推,直到图像缩小为 1x1。


在采样时,我们根据屏幕像素在纹理空间中覆盖区域的大小,估算出一个合适的 Mipmap 层级 D(一个连续值):
D = log2(L), 其中 L 是像素覆盖区域在纹理空间中的最大边长估计。
为了获得更*滑的结果,我们不仅从最接*的整数层级采样,而是进行三线性过滤:
- 在 Mipmap 层级
floor(D)上进行一次双线性插值,得到颜色C1。 - 在 Mipmap 层级
ceil(D)上进行一次双线性插值,得到颜色C2。 - 最后,根据
D的小数部分frac(D),在C1和C2之间进行线性插值。
各向异性过滤 ⚖️
当表面以掠射角朝向相机时,屏幕像素在纹理空间中覆盖的区域可能被拉伸成一个狭长的形状,而不是*似的正方形。此时,标准的 Mipmap(各向同性)会过度模糊。各向异性过滤通过沿不同方向采样多个 Mipmap 区域并进行加权*均,可以更好地处理这种情况,当然计算开销也更大。
总结 📝
本节课中我们一起学习了透视投影和纹理映射的核心内容。
我们首先回顾了将三维图元变换到屏幕坐标的完整管线,重点讲解了透视投影矩阵的推导和视锥体裁剪的重要性。接着,我们探讨了如何在三角形内部插值属性,并强调了进行透视校正的必要性。
然后,我们深入研究了纹理映射。纹理坐标将表面点映射到二维图像。纹理采样面临走样问题,我们通过不同的滤波技术来解决:对于放大情况使用双线性插值;对于缩小情况,引入了 Mipmap 预滤波技术,并通过三线性过滤在层级间*滑过渡。对于极端情况,还可以使用各向异性过滤来提升质量。


构建高效的光栅化管线需要在图像质量与计算效率之间取得*衡。纹理映射,尤其是高质量的纹理滤波,是图形硬件(GPU)需要大量优化和专用电路来处理的核心任务之一。理解“屏幕像素覆盖了纹理的多大区域”这一核心问题,是掌握纹理滤波技术的关键。
9:深度与透明度 🎨

在本节课中,我们将为光栅化管线添加两个关键特性:深度处理与透明度混合。我们将学习如何将来自不同图元的采样点组合成最终图像,同时正确处理物体间的遮挡与半透明效果。


深度与遮挡
上一节我们介绍了如何通过光栅化对三角形进行采样和着色。本节中,我们来看看如何确定在每一个采样点上,哪个三角形是可见的。这个问题对于不透明物体称为遮挡处理。
对于一个三角形,我们在投影后除了得到其顶点的2D屏幕坐标 (x, y),还保留了每个顶点在观察空间中的深度值 z。深度值代表了该顶点到观察者的距离。
为了计算三角形内部任意采样点 (x, y) 的深度 d,我们可以利用重心坐标插值。因为深度在三角形表面是线性变化的,所以插值结果是准确的。
// 伪代码:使用重心坐标(alpha, beta, gamma)插值深度
d = alpha * d_i + beta * d_j + gamma * d_k;


现在面临的挑战是:光栅化管线一次只处理一个三角形,而我们最终需要知道在每个采样点上,深度最小(即离观察者最*)的三角形是哪一个。解决方案是使用一个称为深度缓冲区(或Z缓冲区)的辅助数据结构。


深度缓冲区为每一个采样点(或像素)存储一个深度值,代表当前已处理的所有图元中,离观察者最*的那个的深度。初始时,所有深度值被设置为一个很大的数(如无穷大)。
以下是深度测试的基本流程:

- 对当前三角形进行光栅化,得到一系列采样点及其插值后的深度
d_new。 - 对于每个采样点
(x, y),读取深度缓冲区中存储的当前深度d_buffer。 - 比较
d_new和d_buffer。如果d_new < d_buffer,说明当前三角形更*,则:- 用当前三角形的颜色更新颜色缓冲区。
- 用
d_new更新深度缓冲区。
- 如果
d_new >= d_buffer,则不做任何操作,当前三角形在该点被遮挡。

深度缓冲区算法逐采样点进行比较,因此能正确处理三角形相互穿插的情况,无需对三角形进行全局排序或分割,实现简单高效。
透明度与混合
现在我们来处理半透明表面。半透明表面的不透明度(或相反,透明度)通常用一个在0到1之间的Alpha值 α 来表示。α=1 表示完全不透明,α=0 表示完全透明。
为了将半透明图像 B 合成到背景图像 A 之上,我们使用 “Over”操作符。这个操作不是可交换的,B over A 与 A over B 结果不同,因此合成顺序至关重要。
如果直接使用颜色 C 和 Alpha α 进行合成,在图像缩放或反复合成时容易产生颜色失真和黑边/白边伪影。为了解决这个问题,图形学中普遍采用预乘Alpha的颜色表示法。

预乘Alpha将颜色通道预先乘以Alpha值:
// 原始颜色: (R, G, B, A)
// 预乘后颜色: (R*A, G*A, B*A, A)
Color_premultiplied = (R * A, G * A, B * A, A);
使用预乘Alpha颜色进行Over操作的公式如下:
// 合成 B over A,其中颜色均已预乘
Color_result = Color_B + (1 - Alpha_B) * Color_A;
Alpha_result = Alpha_B + (1 - Alpha_B) * Alpha_A;

合成完成后,如果需要得到标准颜色,可以进行“反预乘”:
Color_standard = (R_result / A_result, G_result / A_result, B_result / A_result);
预乘Alpha的优势包括:
- 所有通道(RGB和A)使用相同的合成公式,实现简单。
- 合成操作是封闭的,支持多层半透明表面的正确混合。
- 能避免图像缩放(如生成Mipmap)时的颜色伪影。
- 其数学形式与齐次坐标相似,能自然地融入现有的图形管线。
在管线中整合深度与透明度
在实际渲染包含不透明和半透明物体的场景时,通常采用以下策略:
- 渲染所有不透明物体:使用深度缓冲区算法,以任意顺序绘制。这会正确解决不透明物体间的遮挡问题,并填充好深度缓冲区。
- 渲染所有半透明物体:
- 禁用深度写入:不再更新深度缓冲区。
- 启用深度测试:仍然进行深度比较,确保半透明物体只有在其深度值小于缓冲区值(即在不透明物体之前)时才被绘制。
- 按从后到前的顺序绘制:对半透明物体进行排序(通常按到相机的*均距离),然后依次使用Over操作符进行混合。这是实现正确半透明效果的关键,但也是光栅化管线的难点,因为排序可能很耗时,且对于相互交叉的半透明物体可能无法正确排序。
处理大量无序半透明物体是光栅化管线的一个弱点。更高级的技术(如深度剥离)或像光线追踪这样的替代渲染算法能更好地处理此问题。
光栅化管线总结 🚀
本节课中我们一起学习了如何通过深度缓冲区和透明度混合来完成光栅化渲染管线的最后一步。现在,我们可以回顾整个从3D场景到2D图像的光栅化管线流程:
- 输入:三角形列表、纹理坐标、模型/视图/投影变换矩阵、纹理贴图、输出图像尺寸。
- 顶点处理:对每个三角形顶点应用视图和投影变换,将其变换到裁剪空间。
- 裁剪:剔除完全在视锥体外的三角形,对部分在内部的三角形进行裁剪。
- 透视除法与视口变换:将顶点变换到2D屏幕坐标。
- 光栅化:遍历三角形覆盖的采样点,利用重心坐标插值深度、纹理坐标等属性。
- 片段着色:使用插值得到的纹理坐标查询纹理,并进行滤波(如双线性过滤),确定片段颜色。
- 深度测试与混合:
- 对于不透明片段:进行深度测试,通过则写入颜色和深度。
- 对于半透明片段:通常需要在所有不透明物体渲染后,按从后到前顺序进行深度测试和颜色混合。
- 输出:最终颜色被写入帧缓冲区,形成图像。

现代GPU硬件将这一管线高度并行化和固定功能化,以实现实时的高性能渲染。尽管管线正变得更加可编程和灵活,但其核心原理与我们在此讨论的完全一致。


展望未来 🔮


至此,我们已经完成了对光栅化渲染管线的全面讨论。然而,要生成我们在电影和游戏中看到的复杂、逼真的图像,我们还有很长的路要走。在接下来的课程中,我们将探索:
- 几何:如何表示和建模复杂的形状,而不仅仅是三角形。
- 材质与光照:光如何与物体表面交互,从而实现照片级真实感渲染。
- 动画:如何数字化地描述物体的运动。






这些主题将带领我们超越基础的光栅化,进入计算机图形学更广阔、更精彩的领域。下次课,我们将从“几何”开始我们的新旅程。
10:几何导论 🎨
在本节课中,我们将要学习几何处理与建模的基础知识。我们将从光栅化的讨论转向几何处理,探索如何为模型增加几何复杂度,例如添加有趣的曲线、褶皱和细节。
概述 📋
上一节我们完成了对光栅化的讨论。本节中,我们将探讨几何处理与建模的核心概念。几何学不仅仅是关于角度和三角形的证明,它更广泛地研究形状、大小、模式和位置。在计算机图形学中,我们需要用数字数据来编码形状,这涉及到多种不同的表示方法。
什么是几何学? 🤔
从语言学的角度看,“几何”(Geometry)一词源于“geo”(地球)和“metry”(测量),最初指对地球的测量。广义上,几何学是研究形状、大小、模式和位置的学科。另一种定义是,几何学研究可以测量长度、角度等量的空间。
历史上,人们很早就开始用类似计算机图形学中多边形网格的离散模型来描述几何。例如,柏拉图曾将地球描述为一个十二面体。在计算机科学中,我们需要讨论如何用数字表示来描述形状。
描述形状的方法 📐
有多种方法可以描述一个给定的形状。以屏幕上的红色曲线(单位圆)为例:
- 语言描述:直接称之为“单位圆”。在某些编程语言(如SVG)中,可以直接使用“circle”这个词。
- 隐式描述:将圆描述为满足方程 x² + y² = 1 的所有点 (x, y) 的集合。这并不直接给出圆上的点,但可以测试一个给定点是否在圆上。
- 显式描述:使用参数方程,将圆描述为所有点 (cosθ, sinθ) 的集合,其中 θ ∈ [0, 2π]。这通过参数直接生成圆上的点。
- 其他方法:包括微分方程(描述粒子轨迹)、离散*似(用多边形逼*圆)、对称性描述(圆在旋转下保持不变),或通过曲率等属性定义。

关键在于,对于任何形状,都可能存在多种描述方式。因此,我们需要思考:在计算机上编码几何的最佳方式是什么?
几何表示的多样性 🌍
现实世界中的形状种类繁多,没有一种“最佳”的表示方法适用于所有情况。
- 厨房器皿:可能由一条曲线绕圆旋转而成,相对容易表示。
- 汽车引擎:由多个组件通过交集、并集等布尔运算组合而成。
- 人脸:几何结构复杂且微妙,细微变化都承载大量信息,对表示的准确性要求极高。
- 动态布料或流体:形状随时间剧烈变化,甚至可能发生分裂或融合,需要能处理拓扑变化的表示方法。
- 复杂结构(如神庙):具有跨越不同尺度的细节,可能需要实例化等解决方案。
- 毛发或微观结构(如蛋白质):可能具有体积特性,而不仅仅是表面;形状对其功能至关重要。
正如皮克斯高级研究科学家David Baraff所说:“我讨厌网格,我无法相信这有多难。” 几何处理确实充满挑战,但我们可以通过一些基本思路来分解并处理这些复杂性。
数字编码几何的两大类别 🗂️
数字编码几何的方法主要可分为两大类:
1. 显式表示
显式表示直接给出形状上的点。
- 点云:最简单的表示,即属于物体的点的列表。优点是简单,缺点是缺乏连接信息。
- 多边形网格:不仅存储点(顶点),还存储连接关系(多边形,通常是三角形)。这是我们描述立方体时使用的方法。
- 更复杂的显式几何:如细分曲面和NURBS(非均匀有理B样条)。
显式表示使某些任务(如采样表面点)变得非常容易。


2. 隐式表示
隐式表示不直接给出点,而是提供一个测试,判断给定点是否在形状内。



- 基本思想:形状是所有满足 F(x, y, z) = 0 的点 (x, y, z) 的集合,其中 F 是一个函数。
- 例子:单位球体是所有满足 x² + y² + z² = 1 的点。
隐式表示使其他任务(如判断点在内/外)变得非常容易。
隐式与显式表示的比较 ⚖️
通过两个思维游戏可以理解它们的优缺点:


游戏一(隐式):给定隐式函数 F(x, y, z) = x - 1.23,其零值点构成一个*面。任务是找出该*面上的任意一点。这很困难,因为你需要解方程。
结论:隐式表示难以直接采样表面上的点。
游戏二(隐式):给定隐式函数 F(x, y, z) = x² + y² + z² - 1(单位球体)。任务是判断点 (3/4, 1/2, 1/4) 是否在球体内。这很容易,只需计算函数值。
结论:隐式表示很容易进行内外测试。
游戏三(显式):给定显式参数曲面 f(u, v) = (1.23, u, v)(一个*面)。任务是采样该曲面上的点。这很容易,只需为 u 和 v 选择任意值。
结论:显式表示很容易采样表面点。
游戏四(显式):给定显式参数曲面描述一个圆环面。任务是判断一个给定点是否在圆环面内部。这非常困难,因为需要反解参数。
结论:显式表示很难进行内外测试。
因此,没有一种表示在所有任务上都最优。最佳选择取决于具体任务和几何类型。在实践中,经常需要在不同表示之间进行转换。
常见的隐式表示方法 🧊
以下是几种常见的隐式表示:
1. 代数曲面
将曲面表示为多项式 P(x, y, z) = 0 的零值集。
- 例子:球体 x² + y² + z² = 1;圆环面有更复杂的公式。
- 缺点:很难为复杂形状构造合适的多项式。
2. 构造实体几何
通过布尔运算(并集、交集、差集)组合基本形状(球体、圆柱体等)来构建复杂形状。
- 优点:适合建模机械零件等硬边物体。
- 例子:通过球体与立方体的交集,再减去三个圆柱体,可以得到一个带圆角且穿孔的立方体。
3. 水*集方法
不使用封闭公式,而是使用一个网格来存储函数 F 的*似值。曲面位于插值后函数值为零的地方。
- 优点:可以表示非常复杂的形状(如飞溅的流体),易于处理拓扑变化(合并、分裂)。
- 缺点:存储和计算成本高(O(n³)),存在走样问题。常用窄带存储等稀疏数据结构来优化。
- 应用:医学成像(CT/MRI)、流体模拟。
4. 分形
描述具有自相似性和大量细节的自然现象。
- 例子:曼德博集合。对于复*面上的每个点 c,通过迭代公式 z_{n+1} = z_n² + c(从 z₀ = 0 开始)判断其是否发散。不发散的点属于该集合。
- 特点:能用简单规则产生极其复杂的图案,但形状难以精确控制。
隐式表示的优缺点总结:
- 优点:描述可能很紧凑;内外测试简单;易于计算到表面的距离;对于简单形状有精确描述;易于处理拓扑变化。
- 缺点:难以采样所有表面点;用解析形式建模复杂形状困难。
常见的显式表示方法 🔷
以下是几种常见的显式表示:
1. 点云
最简单的表示,即属于表面的点的列表,可附带法线、颜色等属性。
- 优点:可以表示任何几何;易于绘制(点渲染)。
- 缺点:如果采样稀疏,需要填补空白;难以进行需要连接信息的处理或模拟。
2. 多边形网格
存储顶点列表和连接这些顶点的多边形(通常是三角形)列表。
- 表示:顶点列表:
[(x1, y1, z1), (x2, y2, z2), ...];三角形列表:[(v_idx1, v_idx2, v_idx3), ...],索引指向顶点列表。 - 表面解释:每个三角形通过重心插值填充:P = α * P_i + β * P_j + γ * P_k,其中 α + β + γ = 1 且均非负。
- 优点:易于处理和模拟;支持自适应采样(在曲率大的地方用更多三角形);是计算机图形学中最常见的表示之一。
- 缺点:数据结构更复杂;邻域关系不规则,编码比图像处理更复杂。
3. 曲线与曲面(贝塞尔、B样条、NURBS)
使用参数方程和控制点来定义*滑的曲线和曲面。

贝塞尔曲线:
- 使用伯恩斯坦基函数 B_i^n(t) 定义:C(t) = Σ_{i=0}^{n} B_i^n(t) * P_i。
- 性质:插值端点;端点切线与控制多边形边重合;曲线位于控制点的凸包内。
- 高次曲线问题:难以控制。实践中常使用分段低次(如三次)贝塞尔曲线,并确保连接处的位置和切线连续(C¹连续)。

从曲线到曲面(贝塞尔曲面片):
- 通过张量积将曲线推广到曲面:S(u, v) = Σ_{i=0}^{m} Σ_{j=0}^{n} B_i^m(u) B_j^n(v) * P_{ij}。
- 连接多个曲面片可以形成复杂曲面,但确保片与片之间的光滑连接比曲线情况更复杂。
NURBS(非均匀有理B样条):
- NURBS = Non-Uniform Rational B-Splines。
- 非均匀:节点向量可以非均匀。
- 有理:在齐次坐标下进行插值,然后投影回*面,这允许精确表示圆锥曲线(如圆、圆柱)。
- 优点:易于求值;可精确表示圆锥曲线;具有高阶连续性。
- 缺点:拼接曲面片以保证连续性较难;编辑可能复杂。
4. 细分曲面
从一个粗糙的控制网格(控制笼)开始,通过重复地细分(分割面)和*均(按规则更新顶点位置)来生成越来越光滑的曲面。
- 例子:Lane-Riesenfeld算法用于曲线,Catmull-Clark细分用于四边形网格,Loop细分用于三角形网格。
- 优点:建模更容易(只需移动控制笼顶点);非常流行(如皮克斯广泛使用)。
- 缺点:求值比参数曲面稍复杂;在奇异点(如非四边面汇合点)需要特殊处理连续性。
显式表示的优缺点总结:
- 优点:易于采样表面点;适合渲染;某些表示(如细分曲面)易于编辑。
- 缺点:内外测试困难;数据结构可能复杂;确保参数曲面片之间的连续性可能具有挑战性。
总结 🎓
本节课中,我们一起学习了计算机图形学中几何处理的基础。我们探讨了:
- 几何学的多种描述方式,以及数字表示的必要性。
- 隐式与显式表示的根本区别及其核心优缺点:隐式利于查询(内外测试),显式利于采样。
- 多种具体的几何表示方法,包括代数曲面、构造实体几何、水*集、分形等隐式方法,以及点云、多边形网格、贝塞尔曲线/曲面、NURBS和细分曲面等显式方法。

每种表示方法都有其适用的任务和几何类型。在计算机图形学实践中,根据需求(如建模难度、渲染效率、模拟要求)选择合适的表示,并经常在它们之间进行转换,是一项关键技能。下一讲,我们将更深入地探讨曲线、曲面以及处理网格数据结构。
11:网格和流形 📐


在本节课中,我们将深入学习几何表示的核心内容,特别是多边形网格。我们将探讨什么是“流形”表面,并详细介绍几种用于存储和处理网格的数据结构,从简单的列表到更复杂的半边数据结构。
概述:从几何到网格
上一节我们开始讨论几何表示。本节中,我们将深入探讨处理网格的具体细节。我们之前看到,几何表示主要分为两类:隐式(通过测试点是否在形状内来定义)和显式(直接列出或生成形状中的点)。多边形网格属于显式表示。

为了在计算机图形学中高效地处理几何,我们需要做出一些关键的假设,并使用合适的数据结构。这自然地将我们引向下一节关于几何处理的内容。

为什么是网格?一个图像的类比

在深入网格之前,让我们回顾一个更熟悉的例子:二维图像。在光栅化中,我们将图像表示为像素的正方形网格。为什么选择正方形?
- 简单高效:每个像素有四个邻居(上、下、左、右),易于索引、应用滤波器和存储(一个长数组)。
- 通用性:理论上可以表示任何图像。

当然,正方形网格并非完美(例如,它偏好水*和垂直方向),但在简单性、效率和通用性之间取得了良好*衡。我们在为几何选择数据结构时,也需要权衡类似的准则。


什么是表面与流形?🧩

首先,我们需要明确“表面”的含义。在数学上,表面特指物体的边界或外壳,而非其内部实体。一个关键属性是,表面通常是流形。

流形的直观理解是:在形状上的任意一点,如果你足够放大,该点的邻域应该看起来像一个二维坐标网格(即一个*面片)。例如,地球从太空看是球体,但放大到曼哈顿岛,你可以用街道和大道建立一个局部网格。
那么,是否所有形状都是流形?并非如此。考虑一个“沙漏”形状的中心点,无论放大多少,都无法在其上放置一个简单的二维网格,因为该点连接了多个方向。因此,并非所有形状都是流形。

流形网格的判定条件
对于多边形网格(特别是三角形网格),流形条件可以具体化为两条简单的规则,可以用一个口诀记忆:有扇区,无鳍片。

以下是判定条件:
- 边的条件:网格中的每条边必须恰好被两个多边形共享(对于内部边),或者只被一个多边形共享(对于边界边)。
- 顶点的条件:网格中的每个顶点必须被一个单一的多边形扇区(或环)所包围(对于内部顶点),或者被一个多边形条带所包围(对于边界顶点)。
违反这些条件的例子包括:一条边被三个或更多多边形共享(形成“鳍片”),或者一个顶点属于两个分离的多边形扇区(如沙漏尖端)。
为什么假设流形?⚖️
做出流形假设的原因与我们选择正方形像素网格类似:
- 简单性与高效性:流形结构规则,可以简化算法和数据结构的设计,我们无需处理各种奇怪的连接情况。
- 通用性:对于动画、模拟和建模中需要表示的大多数形状(如角色、物体),流形假设通常是成立的。
这个假设将帮助我们设计出强大而高效的数据结构。
如何存储网格?数据结构探索 🗃️
面对存储网格数据的挑战,让我们从一个简单问题开始:如何存储一个数字列表?两种基本选择是:
- 数组:查找快,内存访问连贯,但插入/删除元素成本高。
- 链表:易于插入/删除,但查找慢,内存访问可能不连贯。

多边形网格的数据结构也体现了类似的权衡。让我们逐一探讨。
1. 多边形汤(Polygon Soup)
这是最简单的方法:直接存储每个三角形的三个顶点坐标 (x, y, z)。
// 例如:两个三角形
[x1, y1, z1, x2, y2, z2, x3, y3, z3] // 三角形1
[x1, y1, z1, x3, y3, z3, x4, y4, z4] // 三角形2
- 优点:极其简单,易于光栅化。
- 缺点:数据冗余(顶点被重复存储);难以进行任何需要邻域信息的操作(如*滑)。
2. 邻接列表(Adjacency List)
这种方法将数据分为两部分:
- 顶点列表:存储所有唯一的顶点坐标
(x, y, z)。 - 面列表:存储构成每个三角形的顶点索引。
// 顶点列表
V = [v0(x,y,z), v1(x,y,z), v2(x,y,z), v3(x,y,z)]
// 面列表(索引)
F = [[0, 1, 2], [0, 2, 3], ...]
- 优点:消除了顶点冗余,分离了几何(位置)和拓扑(连接关系)。
- 缺点:查找一个顶点的所有邻接面效率低下(需要遍历所有面)。

3. 关联矩阵(Incidence Matrices)
这种方法显式地存储元素之间的关联关系,使用矩阵。例如:
- 顶点-边关联矩阵:行代表边,列代表顶点,如果边包含顶点,则对应位置为1。
- 边-面关联矩阵:行代表面,列代表边,如果面包含边,则对应位置为1。

由于矩阵非常稀疏(大多数元素为0),我们使用稀疏矩阵格式存储,如压缩列存储格式:
- 存储所有非零值及其行索引。
- 用一个额外数组记录每列非零值的累积数量。
- 优点:能快速回答邻域查询。
- 缺点:存储开销相对较大;修改网格连接性较复杂。
4. 半边数据结构(Half-Edge Data Structure)🔗

这是最强大、最常用的网格数据结构之一,它类似于链表的思想。核心概念是将每条边拆分为两个方向相反的半边。
每个半边存储以下指针:
twin:指向同一原始边上的另一个半边。next:指向同一多边形内按顺序的下一个半边。vertex:指向该半边起点的顶点。face:指向该半边所属的多边形。edge:指向该半边所属的边。
其他元素(顶点、边、面)只需存储一个指向其关联的半边的引用。

遍历示例:要遍历一个面的所有顶点,可以从 face.halfedge 开始,然后重复执行 halfedge = halfedge.next,直到回到起点,并沿途收集 halfedge.vertex。
半边数据结构天然强制流形条件。有效的半边连接(满足 twin.twin == self,twin != self,每个半边都是某个半边的 next)必然描述一个流形网格。
优点:
- 支持高效的局部网格操作(如翻转、分裂、收缩边)。
- 邻域查询速度快。

缺点:
- 存储开销大(许多指针)。
- 内存访问可能不连贯。
- 通常假定流形网格。

连接性与几何的分离 💡
一个关键且容易混淆的点是:完美的流形连接性并不保证合理的几何形状。
- 连接性(拓扑)指的是顶点、边、面如何连接。
- 几何指的是顶点的实际
(x, y, z)坐标。

你可以有一个连接性完全正确(流形)的网格,但将顶点位置设置得乱七八糟,导致面相交、重叠等视觉上的“错误”。在调试时,需要区分是连接性问题还是几何问题。
网格操作与应用 🛠️

拥有了像半边结构这样强大的数据结构,我们可以实现许多局部网格操作:
- 边翻转:改变共享边的两个三角形的对角线连接。
- 边分裂:在边中点插入新顶点,细分相邻面。
- 边收缩:将一条边的两个顶点合并为一个。
这些操作是细分曲面建模的基础。艺术家可以从一个粗糙的“控制笼”开始,通过局部操作塑造大体形态,然后通过细分规则自动生成光滑的细节表面。

此外,这些数据结构使得数字几何处理成为可能,即像处理音频、图像信号一样,对几何形状进行滤波、*滑、简化等操作。

总结



本节课我们一起深入探讨了多边形网格的核心概念:
- 流形表面:定义了我们可以有效处理的“规整”形状类型,其局部类似于*面。
- 数据结构权衡:我们比较了从简单的多边形汤、邻接列表、关联矩阵到复杂的半边数据结构。没有一种结构是万能的,选择取决于任务需求(如是否需要快速邻域查询、动态编辑)。
- 半边数据结构:作为重点,它通过丰富的指针网络高效编码流形网格,支持复杂的局部操作和遍历,是许多几何处理算法的基础。
- 连接性与几何:理解了拓扑连接和顶点位置是独立的概念,这对于调试和算法设计至关重要。



掌握这些基础知识,为我们下一节进入几何处理领域做好了准备。
12:数字几何处理 📐




在本节课中,我们将学习数字几何处理的基础知识。上一节我们深入探讨了网格数据结构,本节中我们来看看如何利用这些数据结构来实际操作和处理几何形状。

数字几何处理是数字信号处理在几何形状领域的延伸。与处理图像、音频和视频类似,我们希望对几何信号(即形状)进行上采样、下采样、重采样和滤波等操作。几何处理和网格算法是计算机图形学许多领域的基础构建模块,无论是渲染、动画还是模拟,都需要几何表示。




当前是几何处理的激动人心时期,我们看到了各种有趣且经济实惠的技术,既能将现实物体数字化,也能将数字几何转化为实体。例如,通过3D扫描获取几何数据,或通过3D打印将数字模型变为实体。

几何处理流程 🔄




几何处理流程不像光栅化管线那样精确,但大致遵循以下步骤:首先从现实世界获取物理对象,通过扫描将其采样并转化为数字表示;然后在计算机中通过算法进行处理、编辑、变换和分析;最后,当我们对修改结果满意时,可以将其打印出来,赋予其新的功能。
以下是处理流程中可能涉及的一些核心任务:


- 重建:从点云等样本数据中重建出表面。
- 上采样:提高几何形状的分辨率,例如通过细分技术。
- 下采样:降低几何形状的分辨率,简化模型。
- 重采样:改变网格元素(如多边形)的分布和质量,而不一定改变分辨率。
- 滤波:去除噪声或增强特定特征。
- 压缩:减少存储大小,消除冗余数据。
- 形状分析:对模型进行分割、识别或匹配等高级分析。


什么是好的网格? ✅

在开始处理之前,我们需要明确什么是“好”的网格。一个好的网格应该能很好地*似原始形状。但这不仅仅是顶点位置接*表面那么简单。

- 法线*似:网格的法线也应接*光滑表面的法线。如果法线*似良好,许多其他几何属性(如曲率)也会得到良好保持。
- 元素形状:对于许多算法(特别是数值模拟),三角形的形状至关重要。通常,接*等边的“胖”三角形比又长又瘦的“瘦长”三角形表现更好。一个有用的质量标准是 Delaunay 条件:在*面三角化中,每个三角形的外接圆内不包含其他顶点。Delaunay三角化能最大化最小角,并提供最*滑的插值。
- 顶点度数:在三角形网格中,规则的顶点度数(通常为6)也是理想特性。它有助于获得更规则的三角形形状,并且在细分等操作中能产生更*滑的结果。然而,在曲面上无法让所有顶点都达到规则度数,但我们可以尽量减少不规则顶点的数量。

上采样:细分 🆙


上采样网格的基本工具是细分。其基本思想是:反复将每个元素分割成更小的部分(改变连接关系),然后根据相邻顶点位置的加权*均来更新顶点位置(改变几何形状)。

细分方案有多种,主要考虑因素包括:
- 插值 vs *似:极限曲面是精确穿过原始顶点,还是仅仅接*它们?
- 连续性:极限曲面具有多少阶可微性(光滑程度)?


以下是两种常见的细分方案:

- Catmull-Clark细分(适用于四边形网格):通过插入面点和边点来细分,所有面最终都会变成四边形。新顶点位置的更新规则涉及面点、边点和原始顶点的加权组合。公式为:
新顶点坐标 = (Q + 2R + (n-3)S) / n,其中n是顶点度数,Q是周围面点的*均,R是周围边点的*均,S是原始顶点位置。 - Loop细分(适用于三角形网格):将每个三角形分割为四个。新边点位置是相邻四个顶点的加权*均(端点权重为3/8,对点权重为1/8)。旧顶点的新位置是其自身与其所有邻接顶点的加权*均。对于度数为
n的顶点,权重u = 3/(8n)(当n=3时u=3/16),新位置 =(1 - n*u) * 原始顶点 + u * 所有邻接顶点之和。


细分可以通过基本的边操作(如分裂和翻转)来实现。例如,Loop细分可以先分裂所有边,然后翻转连接新顶点和旧顶点的边,最后更新顶点位置。

下采样:简化 🔽

下采样网格的一种流行方法是基于边折叠的迭代简化。这是一种贪心算法:为每条边分配一个折叠“代价”,然后反复折叠代价最小的边,直到达到目标网格元素数量。
一个非常有效的代价度量是 二次误差度量。其核心思想是:为网格的每个区域(如顶点)维护一个二次函数(用一个4x4矩阵 K 表示),该函数衡量任意点到该区域所有三角形所在*面的*方距离之和。

距离计算:点 x 到法向量为 n、经过点 p 的*面的*方距离,在齐次坐标下可以表示为二次型 u^T K u,其中 u = [x, y, z, 1]^T,K = v v^T,v = [a, b, c, d]^T([a,b,c] 是单位法向量 n,d = -n·p)。


二次误差度量:对于一组*面,总的*方距离就是它们对应矩阵 K_i 的和。因此,我们可以通过累加矩阵来高效地表示到复杂区域的误差。
边折叠算法步骤:
- 初始化:为每个三角形计算其*面的
K矩阵。为每个顶点i计算其二次误差Q_i,即其所属所有三角形K矩阵之和。为每条边(i,j)计算其二次误差Q_ij = Q_i + Q_j。 - 计算代价:对于每条边,找到使
Q_ij(x)最小的新顶点位置x*(通过求解∇Q_ij = 0得到)。该最小值Q_ij(x*)即为折叠该边的代价。 - 迭代折叠:
- 找到当前代价最小的边
(i,j)。 - 将其折叠到最优位置
x*。 - 新顶点的二次误差设为
Q_new = Q_i + Q_j。 - 更新所有与新顶点相连的边的代价。
- 找到当前代价最小的边
- 注意事项:折叠后需检查是否导致三角形法向翻转,若是则放弃此次折叠。

重采样:改善网格质量 ✨


有时我们不想改变分辨率,而是想改善网格元素的质量(如形状、顶点度数)。以下是一些简单有效的方法:

- Delaunay化(*面):对于*面三角化,可以通过贪婪地翻转边来使其成为Delaunay三角化。判断条件是:对于每条边,如果其对两个三角形的对角之和大于180度(π),则翻转该边。
- 改善顶点度数:同样可以使用边翻转操作。计算翻转前后相关顶点度数与理想度数(如6)的偏差总和,如果翻转后总偏差减小,则执行翻转。
- 改善三角形形状:可以通过将每个顶点移动到其所有邻接顶点的*均位置(拉普拉斯*滑)来使三角形更接*等边。在曲面上操作时,为了保持几何形状,应只沿切向移动顶点,即从移动向量中减去法向分量。

一个结合了多种操作的重采样算法可以产生形状均匀、质量高的网格:
- 分裂 过长的边(如长度 > 4/3 * 目标长度)。
- 折叠 过短的边(如长度 < 4/5 * 目标长度)。
- 翻转 边以优化顶点度数。
- 切向*滑 顶点位置。
重复以上步骤直到收敛。

重采样的风险与几何查询 ⚠️

反复对信号(包括几何)进行重采样会导致质量下降,因为信息在每次采样和重建中都会丢失。一个有趣的想法是:在处理后,将新网格的顶点投影回原始网格表面。但这引出了一个核心问题:如何高效地找到空间中一点到一个网格表面的最*点?



这涉及到一系列几何查询问题:点是否在表面内/外?两个三角形是否相交?我们学过的隐式/显式表示或半边数据结构是否能简化这些任务?如何加速对大网格的查询?这些将是下一节课要探讨的有趣话题。

总结 📝



本节课我们一起学习了数字几何处理的基础。我们了解了什么是好的网格质量标准,掌握了通过细分进行上采样、通过二次误差度量简化进行下采样的核心算法,并探索了通过边操作和顶点*滑来改善网格质量的重采样技术。最后,我们认识到重采样过程存在信息丢失的风险,并引出了高效几何查询这一重要课题。这些工具和概念是操作、分析和优化三维几何模型的基石。
13:几何查询 🧮
在本节课中,我们将学习如何对几何体进行各种查询,例如计算点到几何体的最*距离、射线与几何体的交点,以及判断几何体之间是否相交。这些查询是计算机图形学中许多核心任务的基础,包括动画、渲染和几何处理。
上一节我们讨论了网格数据结构,本节我们将重点转向如何利用这些结构回答具体的几何问题。
几何查询概述
几何查询的核心是询问两个或多个几何对象之间的空间关系。常见的查询包括:
- 最*点查询:给定一个点,找到几何体上离它最*的点。
- 射线相交查询:判断一条射线是否与几何体相交,并找出交点。
- 相交测试:判断两个几何体是否相交。
这些查询在图形学中无处不在。例如,在动画中用于碰撞检测,在渲染中用于计算阴影和可见性。
最*点查询
最*点查询是许多几何处理任务的基础。例如,在处理网格时,我们可能希望将变形后的顶点“拉回”到原始表面上,以保持形状的保真度。这需要为每个顶点找到其在原始表面上的最*点。
点到点的最*距离
我们从最简单的情况开始:计算一个查询点 P 到另一个点 A 的距离。这很简单,距离就是两点之间的欧几里得距离。
公式:
distance(P, A) = sqrt((P.x - A.x)^2 + (P.y - A.y)^2 + (P.z - A.z)^2)
点到直线的最*点
现在考虑查询点 P 到一条无限直线 L 的最*点。直线可以用隐式方程 n^T * X = c 表示,其中 n 是单位法向量。
算法思路:
从点 P 出发,沿着(或反方向)法向量 n 移动一段距离 t,直到到达直线上的点 X。即求解 X = P + t * n,并满足直线方程 n^T * X = c。
推导与公式:
- 将
X = P + t * n代入直线方程:n^T * (P + t * n) = c - 展开:
n^T * P + t * (n^T * n) = c - 由于
n是单位向量,n^T * n = 1。 - 解得:
t = c - n^T * P - 因此,最*点为:
closest_point = P + (c - n^T * P) * n
点到线段的最*点
线段由两个端点 A 和 B 定义。点到线段的最*点可能是线段内部的点,也可能是某个端点。
算法步骤:
- 首先,将线段视为其所在的无限直线,利用上述方法找到查询点
P在该直线上的投影点X_proj。这可以通过参数t表示:X_proj = A + t * (B - A),其中t的计算方式与点到直线类似,但需针对线段进行调整。 - 然后,检查参数
t:- 如果
0 <= t <= 1,则投影点在线段内部,它就是最*点。 - 如果
t < 0,则最*点是端点A。 - 如果
t > 1,则最*点是端点B。
- 如果
点到三角形的最*点(2D & 3D)
对于二维*面内的三角形,查询点 P 的最*点可能是三角形的某个顶点、某条边上的点,或者就是 P 本身(如果 P 在三角形内部)。
算法思路(3D情况):
- 投影到*面:首先,将查询点
P垂直投影到三角形所在的*面上,得到点P_proj。计算点到*面的最*点方法与点到直线类似,只是将直线法向量替换为*面法向量。 - 判断位置:判断投影点
P_proj是否在三角形内部(使用重心坐标或符号面积等测试)。- 如果在内部,则
P_proj就是最*点。 - 如果在外部,则最*点必定在三角形的某条边上。此时,问题转化为分别计算
P到三条边的最*点,然后取其中距离最小的一个。
- 如果在内部,则
点到三角形网格的最*点(朴素算法)
对于一个由许多三角形组成的网格,最直接的算法是遍历网格中的每一个三角形。
朴素算法步骤:
- 对于网格中的每个三角形,计算查询点
P到该三角形的最*点及其距离。 - 在遍历过程中,始终记录当前找到的最小距离以及对应的最*点。
- 遍历完成后,记录的点即为全局最*点。
复杂度分析:该算法需要检查每一个三角形,因此时间复杂度为 O(n),其中 n 是三角形的数量。对于包含数百万甚至数十亿三角形的复杂模型,这个开销非常大。我们将在下一讲讨论如何利用空间加速数据结构来优化此类查询。
隐式表面的最*点查询
当几何体以隐式表面(例如,符号距离函数 SDF)表示时,最*点查询可以采用不同的策略。
算法思路(梯度下降):
- 从查询点
P开始。 - 计算距离场在该点的梯度
∇f(P)。梯度的方向指向距离增加最快的方向,因此反方向-∇f(P)指向表面。 - 沿着
-∇f(P)方向移动一小步,得到新的点P_new。 - 重复步骤2和3,直到
f(P)的值接*零(即到达表面),或达到最大迭代次数。
优缺点:
- 优点:算法是局部的,不需要遍历整个几何表示。
- 缺点:可能需要多次迭代;可能收敛到局部最*点而非全局最*点;需要计算梯度。
- 内存与计算权衡:另一种策略是预计算并存储每个网格单元对应的最*点坐标。这会占用大量内存,但查询时只需一次查找,速度极快,适用于需要执行海量查询的场景。
射线相交查询
射线相交查询在光线追踪、碰撞检测和内外测试中至关重要。一条射线由起点 O(原点)和单位方向向量 D 定义:R(t) = O + t * D,其中 t >= 0。
射线-球体相交
球体(以原点为中心,半径为1)的隐式方程为:f(X) = ||X||^2 - 1 = 0。
算法步骤:
- 将射线方程代入球体方程:
||O + tD||^2 - 1 = 0。 - 展开并整理成关于
t的二次方程:at^2 + bt + c = 0,其中:a = D·D(通常为1,因为D是单位向量)b = 2*(O·D)c = O·O - 1
- 求解二次方程。判别式
Δ = b^2 - 4ac决定了相交情况:Δ > 0:两个实根,射线穿过球体(进和出)。Δ = 0:一个实根(重根),射线与球体相切。Δ < 0:无实根,射线与球体不相交。
- 在得到的正实根中,取最小的
t值作为首次交点。
射线-*面相交
*面由隐式方程 n^T * X = c 定义,n 为单位法向量。
算法步骤:
- 将射线方程代入*面方程:
n^T * (O + tD) = c。 - 求解
t:t = (c - n^T * O) / (n^T * D)。 - 特殊情况处理:
- 如果
n^T * D = 0,射线方向与*面*行。若n^T * O = c,则射线在*面内;否则不相交。 - 如果求得的
t < 0,交点在射线起点后方,不算有效相交。
- 如果
射线-三角形相交
这是图形学中最核心的几何查询之一。
算法步骤(常用Möller-Trumbore算法):
- 首先利用射线-*面相交公式,求出射线与三角形所在*面的交点
P及其参数t。 - 然后判断交点
P是否在三角形内部。最有效的方法之一是使用重心坐标(u, v):- 如果
u >= 0,v >= 0, 且u+v <= 1,则交点在三角形内。 - 否则,交点不在三角形内。
- 如果
- 重心坐标可以通过向量运算直接求解,避免显式计算*面方程和投影,效率更高。
代码示意(Möller-Trumbore核心思想):
vec3 edge1 = v1 - v0;
vec3 edge2 = v2 - v0;
vec3 h = cross(rayDir, edge2);
float a = dot(edge1, h);
if (abs(a) < epsilon) return false; // 射线与三角形*行
// ... 计算重心坐标 u, v 和射线参数 t ...
if (u < 0.0 || u > 1.0 || v < 0.0 || (u + v) > 1.0) return false; // 交点不在三角形内
由于其极端重要性(每帧可能需要进行数亿次射线-三角形相交测试),该算法有大量高度优化的实现,并已被集成到现代GPU的硬件光追单元中。

相交测试
相交测试用于判断两个几何体是否在空间上重叠,常见于碰撞检测。

基础相交测试
以下是构建更复杂测试的基础:
- 点-点相交:直接比较坐标是否相等。
- 点-线相交:将点坐标代入线的隐式方程,检查是否成立。
- 线-线相交(2D):求解两条线
a^T X = b和c^T X = d构成的线性方程组。需要特别注意数值稳定性,尤其是当两线接**行时。
三角形-三角形相交测试
这是检测两个网格是否碰撞的基础。
算法思路(基于边-三角形测试):
一个可靠的策略是将问题分解为一系列更简单的测试:
- 检查三角形
T1的每条边是否与三角形T2相交。 - 同样,检查三角形
T2的每条边是否与三角形T1相交。 - 如果任何一条边与对面三角形相交,则两个三角形相交。
- 还需要考虑两个三角形共面但边不相交的特殊情况(如一个三角形完全包含另一个),这需要通过顶点内外测试来补充。
边-三角形测试步骤:
- 将边视为一条有限长的射线。
- 测试该射线与对面三角形所在*面的交点。
- 检查该交点是否同时位于边的线段范围内和对面三角形的内部。
连续碰撞检测(CCD):在动画中,我们不仅关心某一时刻是否相交,更关心在上一帧到当前帧的时间间隔内是否发生过碰撞。这可以通过将时间作为额外维度,将运动的三角形体视为四维空间中的“棱柱”来进行相交测试,或者使用保守推进(Conservative Advancement)等算法。
总结与前瞻
本节课我们一起学习了计算机图形学中几种基本的几何查询:
- 最*点查询:我们从点到点、线、线段、三角形,最后到完整网格,逐步构建了解决方案。对于显式表示(网格),朴素算法是
O(n)的;对于隐式表示(如SDF),则可以使用基于梯度的迭代方法。 - 射线相交查询:我们探讨了射线与球体、*面以及最重要的三角形之间的相交测试。射线-三角形相交是光线追踪的基石,拥有大量高度优化的算法和硬件支持。
- 相交测试:我们了解了如何通过分解为更简单的子问题(如边-三角形测试)来判断两个三角形是否相交,并简要提到了连续碰撞检测的概念。

所有这些基础查询在朴素实现下,对于复杂几何体都可能非常缓慢。下一节课,我们将学习空间加速数据结构(如包围盒层次结构BVH、四叉树、八叉树等)。这些数据结构的核心思想类似于在有序列表中进行二分查找,但它们将这种思想推广到了二维和三维空间,能够让我们大幅减少需要检查的图元数量,从而高效地处理大型场景的几何查询。
14:空间数据结构 📊



在本节课中,我们将学习空间数据结构。上一节我们介绍了基本的几何查询,本节中我们来看看如何利用空间数据结构,极大地提升我们处理复杂几何问题的能力。

概述
处理真实世界中的几何体时,其复杂度可能极高,包含不同尺度的细节,且规模庞大。核心问题是如何从计算角度高效地对如此复杂的场景执行几何查询。一个重要的应用场景是光线追踪和照片级真实感渲染中的光线与超大场景求交。

光线与三角形求交回顾
首先,我们回顾如何判断一条光线是否与一个三角形相交。

问题定义:给定一个由三个顶点 p0, p1, p2 定义的三角形,以及一条从原点 o 出发、沿方向 d 传播的光线 r(t) = o + t*d。我们需要判断光线是否击中三角形,如果击中,则找出交点位置(即参数 t)。



方法一:*面求交法
- 三角形所在*面的隐式方程为:
n^T * x = c,其中n是法向量,c是常数。 - 将光线方程代入*面方程:
n^T * (o + t*d) = c。 - 求解
t:t = (c - n^T * o) / (n^T * d)。 - 将
t代回光线方程得到交点坐标。 - 最后,还需检查交点是否在三角形内部(例如,通过计算重心坐标并检查其非负性)。
方法二:重心坐标法
我们也可以用重心坐标 (u, v, 1-u-v) 参数化三角形上的点:
P(u, v) = (1-u-v)*p0 + u*p1 + v*p2
有效的 (u, v) 需满足 u >= 0, v >= 0, u+v <= 1。


将光线方程代入:
o + t*d = p0 + u*(p1-p0) + v*(p2-p0)
这等价于求解一个关于 [u, v, t]^T 的 3x3 线性系统:
M * [u, v, t]^T = o - p0
其中矩阵 M 的列向量为 (p1-p0), (p2-p0), -d。求解后,同样需要检查 u, v 的有效性以及 t 是否为正。

扩展到复杂场景:第一命中问题
现在考虑更实际的问题:在一个由 n 个图元(如三角形)定义的大型场景中,给定一条光线 R,找到光线击中的第一个(最*的)图元及其交点。
朴素算法:
遍历场景中的所有三角形,对每个三角形执行光线求交测试,并记录最小的 t 值。
# 伪代码示意
closest_t = INFINITY
closest_primitive = None
for each triangle in scene:
t = intersect(ray, triangle)
if t is valid and t < closest_t:
closest_t = t
closest_primitive = triangle
该算法的时间复杂度为 O(n),对于大型场景效率很低。


初步加速:包围盒
一个简单的加速思路是使用包围盒(Bounding Box)。

方法:
- 为整个场景计算一个轴对齐包围盒(AABB),即包含所有图元顶点的最小立方体。
- 在测试所有三角形之前,先测试光线是否与这个包围盒相交。
- 如果光线连包围盒都没击中,则可立即断定它不会击中场景中的任何图元,提前结束计算。
- 如果击中了包围盒,则仍需遍历所有三角形进行测试。
光线与轴对齐包围盒求交:
对于每个坐标轴(X, Y, Z),计算光线进入和离开该轴对齐*面(即包围盒的六个面)的时间 t_min 和 t_max。光线击中包围盒的条件是:在所有轴向上,光线处于盒内的时间区间有重叠。如果最终的交集为空,则光线未击中包围盒。


然而,在最坏情况下(光线击中了包围盒),我们仍需进行 O(n) 次三角形测试,并未从渐进复杂度上改善问题。包围盒的构建成本是 O(n),但可以分摊到多次查询中。


关键思想:空间数据结构
为了真正实现加速,我们需要像处理有序数组的二分查找一样,对空间中的几何体进行“组织”或“排序”。这引出了层次化空间数据结构的概念。

以下是构建高效空间数据结构的核心思路:

包围体层次结构
包围体层次结构(Bounding Volume Hierarchy, BVH)是一种图元划分策略。它将图元集合递归地组织成一棵树。

BVH 节点结构:
- 内部节点:不存储实际图元,而是存储一个包围其所有子节点图元的包围盒,以及指向子节点(通常是两个)的指针。
- 叶子节点:存储一个较小的图元列表(如三角形)以及这些图元的包围盒。

BVH 光线追踪算法(递归版):
# 伪代码
def intersect_bvh(ray, bvh_node, hit_info):
# 1. 检查光线是否击中当前节点的包围盒
if not intersect_box(ray, bvh_node.bbox):
return
# 2. 如果是叶子节点,则与其中所有图元求交
if bvh_node.is_leaf:
for primitive in bvh_node.primitives:
t = intersect(ray, primitive)
if t is valid and t < hit_info.closest_t:
update hit_info with (primitive, t)
return
# 3. 如果是内部节点,则递归处理子节点
intersect_bvh(ray, bvh_node.left_child, hit_info)
intersect_bvh(ray, bvh_node.right_child, hit_info)



遍历优化:
为了尽早终止搜索,可以采用从前到后的遍历顺序。即先处理与光线交点更*的子节点,如果在该子节点中找到了足够*的命中,就可以跳过对另一个子节点的测试。

表面积启发式
如何构建一个高质量的 BVH?目标是使树的遍历成本最小化。常用方法是表面积启发式。


成本模型:对于一个内部节点,将其子节点 A 和 B 的遍历期望成本建模为:
Cost = C_trav + (SA(A)/SA(N)) * |A| * C_intersect + (SA(B)/SA(N)) * |B| * C_intersect
其中:
C_trav是遍历一个节点的成本。SA(X)是节点X包围盒的表面积。|X|是节点X包含的图元数量。C_intersect是单个图元求交的成本。(SA(Child)/SA(Parent))*似表示在击中父节点包围盒的条件下,击中子节点包围盒的概率(基于“随机光线”假设)。


构建 BVH 时,会尝试不同的划分方式(如沿不同坐标轴,在不同位置分割图元),并选择使上述成本估计值最小的划分。

空间划分结构
与 BVH 的“划分图元”思想不同,空间划分策略直接递归地细分空间本身。

KD-Tree:
- 使用轴对齐的*面递归地将空间分割成两部分。
- 叶子节点对应空间中的一个区域,存储与该区域相交的图元列表。
- 优势:可以严格按照光线前进顺序遍历节点,便于早期终止。
- 挑战:一个图元可能跨越多个节点,导致重复测试。可通过“邮件盒”技术缓存结果来优化。
均匀网格:
- 将空间均匀划分为许多大小相同的体素(Voxel)。
- 每个体素存储与其相交的图元列表。
- 光线追踪时,按照光线穿过体素的顺序依次测试。
- 优点:构建简单快速。
- 缺点:难以适应非均匀的几何分布(如“体育场中的茶壶”问题),且最坏情况下的性能可能较差。通常建议体素数量与图元数量大致相当。



四叉树/八叉树:
- 在 2D/3D 空间中,递归地将每个单元格划分为 4/8 个相等的子单元格。
- 是均匀网格和 KD-Tree 之间的折中,比均匀网格更自适应,但通常不如 KD-Tree 高效。


光线追踪 vs. 光栅化
最后,我们对比一下利用空间数据结构进行光线追踪与传统光栅化流程的区别。

光栅化(三角形顺序):
for each triangle in scene:
project triangle to screen
for each pixel covered by the projected triangle:
if triangle is closer than current zbuffer[pixel]:
update zbuffer[pixel] and colorbuffer[pixel]
- 优点:易于实现硬件加速,内存访问模式规律,天然支持无限大的场景(流式处理)。
- 缺点:处理全局光照效果(如阴影、反射、折射)复杂,需要额外技巧(如阴影映射、屏幕空间反射)。

光线追踪(像素/光线顺序):
for each pixel in image:
ray = generate_ray_through_pixel()
closest_t = INFINITY
closest_primitive = None
# 使用BVH等加速结构进行求交
for each primitive in scene (via acceleration structure):
t = intersect(ray, primitive)
if t < closest_t:
closest_t = t
closest_primitive = primitive
color = shade(closest_primitive, ray, closest_t)
set_pixel_color(pixel, color)
- 优点:非常自然地处理全局光照效果(阴影、反射、折射等),视线顺序处理便于实现透明度。
- 缺点:需要将整个场景和加速结构保存在内存中,性能受场景空间结构影响大,计算量通常更大。


总结
本节课中我们一起学习了空间数据结构。我们从基础的光线与三角形求交出发,探讨了处理复杂场景时朴素算法的局限性。为了加速查询,我们引入了层次化结构的思想,重点介绍了包围体层次结构这种图元划分方法,以及KD-Tree、均匀网格等空间划分方法。我们了解了BVH的构建和遍历,以及用于指导构建的表面积启发式。最后,我们对比了基于光线追踪和基于光栅化两种不同的渲染流水线,理解了它们各自的核心循环顺序和适用场景。掌握这些空间数据结构是进行高效光线追踪和许多其他图形学计算(如碰撞检测、最*点查询)的关键基础。
15:颜色 🎨


在本节课中,我们将要学习颜色的物理基础、人类视觉系统如何感知颜色,以及如何在计算机图形学中表示和处理颜色。理解颜色对于实现逼真的渲染、跨设备色彩管理以及处理各种视觉现象至关重要。

什么是颜色?🤔

上一节我们介绍了课程概述,本节中我们来看看颜色的本质。从物理角度来看,颜色是光波振荡频率的表现。光是一种振荡的电磁场,其振荡频率决定了光的颜色。频率(f)和波长(λ)是描述同一现象的两个相关量,它们的关系是:f = c / λ,其中 c 是光速。
一个常见的现象是,当炉子加热时会变红。这是因为热量会使物体内的带电粒子运动,从而产生电磁辐射,即光。物体的温度决定了粒子运动的频率,进而决定了发出光的颜色。
可见光谱与光谱 🌈
然而,并非所有频率的光人类都能看见。人类可见的光波长范围非常狭窄,这被称为可见光谱,大致从红色(长波)到紫色(短波)。超出此范围的光,如红外线和紫外线,人类无法直接感知。
描述光源颜色最根本的方式是使用光谱。光谱描述了光强度随波长(或频率)的分布。有两种主要的光谱:
- 发射光谱:描述光源本身在各个波长上发出的光强度。
- 吸收/反射光谱:描述物体材料对不同波长光的吸收或反射比例。
以下是不同类型光源的发射光谱示例:
- 日光:在可见光范围内分布相对均匀。
- 白炽灯:红光部分较多,蓝光较少,呈现“暖”色调。
- 荧光灯:光谱呈尖峰状,分布不均匀,可能让人感觉不自然。
颜色感知:人类视觉系统 👁️
上一节我们了解了颜色的物理描述,本节中我们来看看人类如何感知颜色。颜色感知是一个复杂的生理和心理过程,始于眼睛。
光线通过瞳孔进入眼睛,在视网膜上成像。视网膜上有两种感光细胞:
- 视杆细胞:对光强敏感,负责暗视觉,但不区分颜色。
- 视锥细胞:负责明视觉和色觉。人类通常有三种视锥细胞,分别对短(S)、中(M)、长(L)波长的光最敏感,大致对应蓝、绿、红光。
每种视锥细胞的敏感度由光谱响应函数描述。眼睛对入射光谱的测量结果,实际上是光谱强度与三种视锥细胞响应函数乘积的积分。最终,大脑接收到的是三个数值信号(S, M, L),而非完整的光谱。
这个机制引出了一个关键概念:同色异谱。即两种不同的光谱分布,可能在人类眼中产生完全相同的颜色感知。这是因为眼睛的积分测量是粗略的,丢失了光谱细节。同色异谱现象是颜色复制(如显示器和打印)能够实现的基础,但也可能导致在不同光源下颜色看起来不同。
颜色模型与颜色空间 🎯
由于人眼感知的局限性和工程实践的需要,我们使用各种颜色模型在特定的颜色空间中表示颜色。颜色空间定义了可用的颜色范围(调色板),而颜色模型则提供了在该空间中指定具体颜色的方法(如坐标或名称)。
以下是几种常见的颜色模型:
- RGB(加色模型):用于发光设备(如显示器)。通过混合红(R)、绿(G)、蓝(B) 三种色光来产生各种颜色。例如,
#FF6600这个十六进制编码表示一个橙色的RGB值。 - CMYK(减色模型):用于吸光材料(如印刷)。使用青(C)、品红(M)、黄(Y)、黑(K) 四种油墨,通过吸收白光中的某些颜色成分来呈现色彩。
- HSV/HSL:更符合人类直觉的模型,用色调(H)、饱和度(S)、明度(V)或亮度(L) 来描述颜色,便于艺术家选择和调整颜色。
- CIE XYZ / Lab:基于人类颜色感知实验建立的与设备无关的颜色空间。Lab 空间在设计上追求感知均匀性,即数值上的均匀变化对应感知上的均匀变化。
选择不同的颜色模型出于多种考虑:用户指定颜色的便利性、颜色处理的方便性(如混合、插值)、存储与编码效率等。
颜色管理中的挑战 ⚙️
在计算机图形学中处理颜色时,我们面临几个核心挑战:
- 色域:指一个设备或颜色模型能够表示的颜色范围。不同设备(如高端显示器与普通打印机)的色域差异很大。将广色域图像转换到窄色域设备时,可能需要进行色域映射,处理无法显示的颜色。
- 伽马校正:显示设备(尤其是传统的CRT显示器)的输入电压与输出亮度之间通常是非线性关系,*似为 亮度 ∝ 电压^γ。为了确保存储的像素值能线性地对应感知亮度,需要在图像处理管线中进行伽马编码和解码(校正)。
- 感知压缩:人眼对亮度的变化比对颜色的变化更敏感。利用这一点,许多图像和视频压缩格式(如JPEG, MPEG)会使用类似 Y‘CbCr 的颜色模型,对亮度分量(Y’)保留高分辨率,而对色度分量(Cb, Cr)进行大幅降采样,从而高效压缩数据。

总结 📚




本节课中我们一起学习了颜色的核心知识。我们从颜色的物理定义(光的频率/波长)出发,探讨了光谱如何描述光。接着,我们深入了解了人类视觉系统如何通过三种视锥细胞将丰富的光谱信息压缩为三个信号,从而产生颜色感知,并解释了同色异谱现象。然后,我们介绍了多种实用的颜色模型和颜色空间(如RGB, CMYK, HSV, CIE XYZ),以及它们各自的用途。最后,我们探讨了在实际颜色管理和图像处理中遇到的挑战,包括色域限制、伽马校正和基于感知的压缩技术。理解这些原理对于在计算机图形学、视觉设计和跨媒体内容创作中准确、高效地处理颜色至关重要。
15:辐射测量学 🌟

在本节课中,我们将学习辐射测量学。辐射测量学是计算机图形学中用于物理精确地量化和描述光与照明的核心知识。我们将从基本概念入手,逐步理解如何测量光能,并最终将这些概念应用于生成逼真的图像。

概述 📖
为了生成与照片难以区分的逼真图像,我们必须以物理上准确的方式量化光和照明。上一讲我们深入讨论了颜色,了解到光的颜色与其波长有关。然而,一幅完整的图像不仅需要颜色信息,还需要知道场景中每个点接收或反射了多少光。辐射测量学正是为此建立的一套测量电磁辐射的单位和体系。



光的几何光学模型 🔦

在宏观层面,为了人类视觉感知和图像生成,我们采用几何光学模型。该模型基于两个关键假设:
- 光子沿直线传播:我们可以用几何上的“光线”来表示光子的路径。
- 光的波长远小于场景物体尺寸:因此我们可以忽略衍射、干涉等小尺度效应。
这个模型简化了问题,使我们能够专注于光在场景中的宏观分布。
核心测量概念与术语 📐
理解概念比记住术语更重要。让我们从最直观的概念开始,逐步引入辐射测量学的标准术语和单位。
辐射能量 (Radiant Energy)
想象光子像小球一样击中场景中的表面。辐射能量就是所有光子击中表面的总次数。它代表了场景中所有光子的总能量。
- 概念:总击中次数。
- 单位:焦耳 (Joules)。
- 公式:
Q = Σ (每个光子的能量)。单个光子的能量公式为Q_photon = (h * c) / λ,其中h是普朗克常数,c是光速,λ是波长。
辐射通量 (Radiant Flux)
我们通常更关心单位时间内到达的能量,而不是总能量。辐射通量(或称辐射功率)就是单位时间内的辐射能量,即每秒的击中次数。
- 概念:每秒击中次数。
- 单位:瓦特 (Watts, W),即焦耳/秒 (J/s)。
- 公式:
Φ = dQ / dt。总辐射能量是辐射通量对时间的积分:Q = ∫ Φ dt。
辐照度 (Irradiance)
为了生成图像,我们需要知道能量在空间上的分布。辐照度是单位面积上接收到的辐射通量,即每秒每单位面积的击中次数。这类似于相机传感器上每个像素点接收到的光能。
- 概念:每秒每单位面积击中次数。
- 单位:瓦特/*方米 (W/m²)。
- 公式:
E = dΦ / dA。对于面积为A的表面,*均辐照度为E = Φ / A。
重要现象:余弦定律
辐照度与表面朝向有关。如果一束光垂直照射面积为 A 的表面,其辐照度为 Φ/A。如果表面倾斜角度 θ,相同的光束会散布在更大的面积 A' = A / cosθ 上,因此辐照度变为 E = (Φ * cosθ) / A。这就是余弦定律:E ∝ cosθ。
在计算机图形学中,这引出了最基本的着色模型:表面亮度与表面法向量 n 和光线方向 l 的点积成正比,即 max(0, n·l)。
辐射强度 (Radiant Intensity) 与立体角
为了更精细地描述光的方向性,我们引入立体角的概念。立体角是二维角度在三维球面上的推广,用于度量一个锥形方向区域的大小,单位是球面度 (steradian, sr)。整个球面的立体角是 4π sr。

辐射强度描述点光源在特定方向上的发光能力,定义为单位立体角内发出的辐射通量。

- 概念:每立体角方向的辐射通量。
- 单位:瓦特/球面度 (W/sr)。
- 公式:
I = dΦ / dω。
对于一个向所有方向均匀发光的各向同性点光源,其总通量 Φ = 4π * I。
距离衰减
点光源的辐照度会随着距离增加而减弱。这是因为从光源发出的恒定通量 Φ,会散布在半径不断增大的球面上。球面面积为 4πr²,因此某点的辐照度 E = Φ / (4πr²) = I / r²。这意味着辐照度与距离的*方成反比,即*方反比定律。
辐射亮度 (Radiance)
辐射亮度是辐射测量学中最核心、信息最完整的量。它描述了在空间某一点、沿某一方向、通过单位投影面积和单位立体角的辐射通量。简单说,它衡量的是“沿着一根特定光线”的亮度。
- 概念:每秒、每单位投影面积、每单位立体角的能量。
- 单位:瓦特/(*方米·球面度) (W/(m²·sr))。
- 公式:
L(p, ω) = d²Φ / (dA * cosθ * dω),其中θ是表面法线与方向ω的夹角。


辐射亮度的关键特性
- 与光线关联:辐射亮度是直接与一条几何光线相关联的物理量。
- 在真空中沿直线传播不变:在无介质(如真空)中,沿一条光线传播时,辐射亮度保持不变。
- 相机直接测量:一个理想的小孔相机模型,其传感器上每个点测量的正是来自对应方向光线的辐射亮度。

光谱辐射亮度 (Spectral Radiance)
为了包含颜色信息,我们将辐射亮度按波长进一步分解,得到光谱辐射亮度。它给出了每个波长上的辐射亮度,完整地描述了光在位置、方向、波长上的分布。
- 概念:每单位波长的辐射亮度。
- 单位:W/(m²·sr·m)。
拥有场景中所有点和所有方向的光谱辐射亮度信息,就等同于掌握了整个光场。光场摄影等技术正是基于捕获和操作这部分高维信息来实现重对焦、视角变换等效果。
入射与出射辐射亮度 ↔️
在讨论光与表面的交互时,区分入射辐射亮度 L_i 和出射辐射亮度 L_o 至关重要:
L_i(p, ω):从方向ω到达点p的光线亮度。L_o(p, ω):从点p向方向ω发出的光线亮度。
对于大多数表面(除光源本身),这两者通常不相等,其关系由材料的双向反射分布函数决定(这将是下一讲的内容)。

从辐射亮度计算辐照度 🔄
在渲染中,一个常见任务是计算表面上某点 p 接收到的总辐照度 E(p)。这需要积分来自半球所有可能入射方向 ω 的辐射亮度贡献,并考虑余弦定律:
E(p) = ∫_H L_i(p, ω) cosθ dω
其中 H 是以点 p 法线为中心的半球,θ 是入射方向与法线的夹角。
实例:环境光遮蔽
假设有一个均匀的半球面光源(如阴天天空),其入射辐射亮度 L_i 在所有方向上是常数 L。那么,点 p 的辐照度理论上为 E = L * π。然而,由于场景几何体的遮挡,某些方向的光线无法到达点 p。计算被遮挡后的辐照度,就得到了环境光遮蔽值。这个值可以预计算并存储为纹理(环境光遮蔽贴图),用于实时渲染中以*似全局光照的阴影效果,增加场景的真实感。

光度学:与人眼感知关联 👁️

以上所有量都属于辐射度量学,基于物理能量。光度学则将其与人眼视觉响应相结合。例如,与辐射亮度 L 对应的是光亮度 Y:
Y(p, ω) = ∫ L(p, ω, λ) * V(λ) dλ
其中 V(λ) 是人眼的视见函数(光度效率曲线)。光度学单位包括流明、勒克斯、尼特等。对于追求视觉真实感而非物理绝对精确的应用,光度学量更为直接相关。
总结 🎯

本节课我们一起学习了辐射测量学的基础知识:
- 我们首先明确了使用几何光学模型来简化光的行为描述。
- 从最基本的辐射能量概念出发,逐步引入了辐射通量、辐照度、辐射强度和辐射亮度等核心概念,理解了它们如何从时间、空间、方向维度分解和量化光能。
- 我们学习了余弦定律和*方反比定律这两个影响表面亮度的基本规律。
- 我们认识到辐射亮度是描述光场信息的最基本单位,它在真空中沿光线传播保持不变。
- 我们了解了如何通过积分入射辐射亮度来计算表面的辐照度,并以环境光遮蔽为例说明了其应用。
- 最后,我们简要对比了物理基础的辐射度量学和与人眼感知相关的光度学。
掌握这些概念是理解光与材质交互、实现全局光照等高级渲染技术的基础。下一讲,我们将探讨光如何与不同材质表面发生作用,即反射模型。
17:渲染方程 🎨
在本节课中,我们将把之前几讲中讨论的关于颜色、光的测量等概念整合起来,形成一个核心方程——渲染方程。这个方程是生成照片级真实感图像的基础。我们将从辐射度量学的基本概念出发,逐步推导出渲染方程,并解释其核心组成部分,特别是光与表面交互的散射函数。最后,我们将概述如何通过路径追踪等算法来求解这个复杂的方程。
辐射度量学回顾 📐
上一节我们介绍了辐射度量学中的关键术语。其中最重要的概念是辐射亮度。它完整地描述了场景中光的信息(至少是光谱辐射亮度)。如果我们知道空间中每个点、每个方向上的光的颜色,那么我们就掌握了场景中光传输的全部信息,并可以用它来生成图像。
一个重要的区分是入射辐射亮度和出射辐射亮度。它们并非不同的物理量,而是描述场景中辐射亮度的不同约定。
- 入射辐射亮度:指从环境中到达某一点的光。例如,你站在街角仰望天空和建筑物,每个方向都有不同颜色的光入射。
- 出射辐射亮度:指从某一点(如光源)发射出去的光。从光源的不同方向看出去,它会向不同方向发射不同强度和颜色的光。
两者的核心思想是:光照强度不仅取决于空间位置或时间,还高度依赖于方向。这意味着,在图像生成中,要确定一个点的总照度,我们必须考虑来自(或去往)所有方向的光照。
我们还区分了辐射亮度和辐照度。它们的区别在于是否考虑了方向积分。具体来说,辐照度 E 是辐射亮度在半球上的积分。例如,要计算从天空半球入射的总光量,需要对入射辐射亮度 L_i(ω) 在半球 H 上进行积分:

E = ∫_H L_i(ω) cosθ dω
其中 ω 是入射方向,θ 是入射方向与表面法线的夹角,cosθ dω 是积分所需的投影立体角微元。

更精确地说,在法线为 n 的表面点 p 处、方向 ω 上的辐射亮度 L,定义为:单位时间内、单位立体角内、垂直于 n 的单位面积上通过的辐射能量。其公式为:
L = dΦ / (dω * dA * cosθ)

这里需要澄清一个容易混淆的点:公式中的 cosθ 可能因两种完全不同的原因出现。
- 物理原因(朗伯余弦定律):当一束相同总能量的光斜射到表面时,覆盖的面积会变大,导致单位面积接收的能量变少,表面显得更暗。
- 数学原因(球面积分参数化):当我们对球面上的函数 f 进行积分时,如果用经纬度 (θ, φ) 参数化,积分微元会包含 cosθ dθ dφ。这里的 cosθ 项纯粹是由于参数化方式导致的,与物理定律无关。

渲染方程:核心框架 ⚙️
回到今天的主要问题:如何利用这些知识生成照片级真实感的图像?答案由所谓的渲染方程给出。

一个照片级真实感渲染器的核心功能,是估计给定点 p 在给定方向 ω_o 上的(入射或出射)辐射亮度 L_o(p, ω_o)。渲染方程总结了这一计算:
L_o(p, ω_o) = L_e(p, ω_o) + ∫_H f_r(p, ω_i → ω_o) L_i(p, ω_i) cosθ_i dω_i

让我们分解这个方程:
- L_o(p, ω_o):在点 p、沿方向 ω_o 出射的辐射亮度(即我们最终想要求得的值)。
- L_e(p, ω_o):点 p 自身在方向 ω_o 上发射的辐射亮度(例如,光源)。
- ∫_H ... dω_i:对点 p 上方的整个半球 H 所有入射方向 ω_i 进行积分。
- f_r(p, ω_i → ω_o):双向反射分布函数。它描述了从入射方向 ω_i 来的光,有多少被散射到出射方向 ω_o。这是决定表面外观的关键。
- L_i(p, ω_i):从方向 ω_i 入射到点 p 的辐射亮度。
- cosθ_i:入射方向 ω_i 与表面法线 n 夹角的余弦值。
这个方程求解困难的关键在于它的递归性。方程左侧要求解 L_o,而右侧需要知道 L_i。那么 L_i 又由什么决定呢?它由另一个点的渲染方程决定!具体来说,L_i(p, ω_i) 等于从点 p 沿 -ω_i 方向反向追踪光线,所到达的下一个表面点 q 在方向 ω_i(相对于 q 的出射方向)上的出射辐射亮度 L_o(q, ω_i)。
因此,计算辐射亮度就归结为递归地求解这个渲染方程。这个过程最终会在遇到发射体(如灯泡)时终止,因为发射项 L_e 提供了递归的基准情况。
这种递归求值的复杂性,正是我们需要光线追踪而非光栅化的原因。光栅化难以灵活控制需要评估哪些光线路径。
光的散射与BRDF 🌈
上一节我们介绍了渲染方程的整体框架,本节中我们来看看其中最关键也最复杂的部分:散射函数 f_r,即双向反射分布函数。
在几何光学模型中,散射是光入射到表面后,在不改变频率的情况下从同一侧离开表面的过程,也就是我们常说的“反射”。BRDF 描述了这一过程:给定入射方向 ω_i,有多少光被散射到出射方向 ω_o。它决定了表面的外观,例如吸收哪些颜色的光、在哪些方向上反射强烈。
以下是几种基本的反射类型:
- 镜面反射:光在特定方向反射,如完美镜子。反射方向由法线决定:ω_o = -ω_i + 2(ω_i·n)n。其BRDF可用狄拉克δ函数描述,表示光只沿精确的反射方向出射。
- 漫反射:光被均匀地散射到所有方向,如粗糙的墙面。理想的漫反射(朗伯反射)的BRDF是一个常数:f_r = ρ/π,其中 ρ 是反照率(介于0和1之间),表示表面的整体亮度。
- 光泽反射:介于镜面和漫反射之间,光在镜面反射方向周围的一个锥形区域内散射,如塑料材质。
- 逆向反射:光被反射回入射方向,如自行车尾部的反光片。月亮的视觉特性也*似于此。


BRDF 具有两个重要性质:
- 能量守恒:对于所有方向,BRDF的积分应 ≤ 1。小于1的部分代表光被吸收并转化为热能。
- 亥姆霍兹互易性:f_r(ω_i → ω_o) = f_r(ω_o → ω_i)。这意味着光路是可逆的。
除了反射,光还可能透射(如穿过玻璃),此时方向由斯涅尔定律决定:η_i sinθ_i = η_t sinθ_t,其中 η 是折射率。当光从光密介质射向光疏介质且入射角大于临界角时,会发生全内反射。
更复杂的现象还包括:
- 菲涅尔效应:反射率随观察角(入射角)变化,在掠射角时接*100%。
- 各向异性反射:反射特性随方位角变化,如拉丝金属。
- 次表面散射:光进入材质内部,经过多次散射后从另一点射出,如皮肤、玉石、树叶。这需要用更复杂的双向表面散射反射分布函数(BSSRDF)来建模,它考虑了入射点和出射点不同的情况。

求解渲染方程:蒙特卡洛积分与路径追踪 🚀
前面我们了解了渲染方程的构成和散射的复杂性,现在来看看如何实际计算它。渲染方程中的反射项是一个复杂的积分,我们通常使用蒙特卡洛积分方法来*似求解。
蒙特卡洛积分的基本思想是:通过随机采样来估计积分值。对于反射方程:

L_o = ∫_H f_r L_i cosθ dω

我们可以通过以下步骤进行估计:
- 随机选择 N 个入射方向 ω_j,采样概率为 p(ω_j)。
- 对每个样本,计算被积函数值 f_r(ω_j) * L_i(ω_j) * cosθ_j。
- 将所有样本的函数值除以对应的概率 p(ω_j),然后取*均。

估计公式为:
L_o ≈ (1/N) * Σ_{j=1}^{N} [ f_r(ω_j) L_i(ω_j) cosθ_j / p(ω_j) ]
其中,递归地获取 L_i(ω_j) 是核心挑战,这需要沿着 ω_j 方向发射新的光线并递归调用相同的着色程序。采样概率 p(ω) 的选择会影响效率,理想情况下应使其形状接*被积函数(重要性采样)。
基于这种蒙特卡洛积分思想,路径追踪算法被提出以求解完整的渲染方程。其关键策略是将光照分为两部分:
- 直接光照:光从光源直接照射到表面点,然后进入相机。
- 间接光照:光在场景中经过多次反射/散射后到达表面点,再进入相机。
路径追踪算法递归地追踪从相机出发的光线,在每次与表面相交时,同时采样直接光照(连接光源)和间接光照(随机采样新的反射方向),并将结果累加。为了避免无限递归,会使用俄罗斯轮盘赌等技术来以概率方式终止路径。
直接光照与间接光照的结合对于真实感至关重要。仅包含直接光照的图像看起来生硬、不自然;而加入间接光照(如颜色渗透、柔和阴影)后,图像会立刻呈现出照片级的真实感与丰富的细节。
总结 📝

本节课中我们一起学习了计算机图形学中最为核心的渲染方程。我们从辐射度量学的基础概念(辐射亮度、辐照度)出发,推导出了描述光能传输的渲染方程,并深入分析了其递归本质。我们重点探讨了决定材质外观的双向反射分布函数,涵盖了从理想的镜面反射、漫反射到复杂的次表面散射等多种现象。最后,我们介绍了通过蒙特卡洛积分和路径追踪算法来求解这一复杂方程的基本思路,理解了直接光照与间接光照在合成真实感图像中的关键作用。渲染方程及其求解方法构成了现代照片级真实感渲染的基石。
18:数值积分 📊


在本节课中,我们将学习数值积分。这是一个在计算机图形学乃至整个计算机科学和科学计算领域都极其重要的主题。我们的学习动机源于计算机图形学中的一个核心问题——照片级真实感渲染。
上一节我们讨论了渲染方程,它是我们模拟光线在场景中反弹以生成图像的基本工具。我们看到,渲染方程是一个递归的积分方程。为了确定从一个点P沿方向ω₀出射的辐亮度,我们需要对所有可能的入射方向ωᵢ进行积分。

在图形学中,我们关心的许多量(如总亮度、像素接收的光量、总面积、总曲率等)都自然地表达为积分。对于像微积分课上学到的简单多项式,我们可以解析地求解积分。然而,在实际的图形学、计算机视觉、科学计算和模拟问题中,大多数积分无法以这种方式求解。它们要么没有闭式解,要么求解过程过于复杂,要么积分项来自数据本身。因此,我们需要计算某种数值*似。
如何数值*似积分? 🔍

要理解数值积分,我们可以回到积分的定义。从概念上讲,积分是函数曲线下的面积。在微积分中,我们通过将区间分割成小柱体,然后随着柱体越来越细,将这些小柱体的面积相加来逼*积分。这正是我们数值计算要做的事情。
我们将对要积分的函数在一系列点上进行采样,然后将积分*似为这些点上函数值的加权和。公式如下:
I ≈ Σ w_i * f(x_i)
其中,w_i 是权重,f(x_i) 是在采样点 x_i 处的函数值。
在渲染中,我们积分的“面积”概念可能不太直观。例如,在场景中的一个点,我们需要累加来自所有方向的入射光。这就是为什么我们需要进行积分。
从简单情况开始:一维积分 📈
让我们从实轴上的函数开始。积分 ∫_a^b f(x) dx 表示从 a 到 b 的曲线下面积(蓝色区域)。另一种有用的思考方式是:积分等于函数在区间 [a, b] 上的*均值乘以区间长度 (b - a)。
一个重要的基础是微积分基本定理。它指出,一个函数导数的积分等于该函数在区间端点处的值之差:∫_a^b f'(x) dx = f(b) - f(a)。这表示从 a 到 b 的总变化量。
简单函数的积分
- 常数函数:积分
∫_a^b C dx = C * (b - a)。这只是一个矩形的面积。 - 仿射函数:函数形式为
f(x) = Cx + D。对于仿射函数,积分可以通过在区间中点采样精确求得:∫_a^b f(x) dx = (b - a) * f((a+b)/2)。这是因为仿射函数在区间上的*均值恰好等于其中点的函数值。
高斯求积法
对于更一般的多项式,是否存在一种通用的精确积分方法?答案是肯定的,即高斯求积法。它指出,对于任何 n 次多项式,总可以通过在一组特殊的点(高斯点)上采样函数值,并进行加权求和,来得到积分的精确值。
- 对于一次多项式(仿射函数),这个特殊点就是中点。
- 对于更高次的多项式,这些点的位置和权重计算更复杂,但原理相同。
高斯求积法是我们遇到的第一个求积法则的例子:通过在一组有限点采样,取这些样本值的加权组合来*似积分。对于多项式,高斯求积是精确的。
处理更复杂的函数:分段仿射函数 🔧
分段仿射函数在小子区间上是仿射函数。对于这样的函数,只在中点采样一个点会导致欠采样(或混叠),从而产生误差。

更明智的做法是:对分段定义的函数,我们可以将每个子区间的积分相加。对于每个仿射区间,我们已经知道精确的积分法则(梯形法则的雏形)。将每个子区间的积分结果 (f(x_i) + f(x_{i+1}))/2 * (x_{i+1} - x_i) 相加,就得到了整个函数的积分*似。

这个求和式可以展开并重新整理,最终表示为一组采样点 f(x_i) 乘上特定权重 w_i 的和。这再次印证了求积法则的核心思想:我们需要两组信息:求积点(在哪里采样)和权重(每个样本对最终积分的贡献)。
积分任意函数:梯形法则 📐
对于任意函数 f(x)(我们可能不知道其具体形式,只能将其视为黑盒),我们如何积分?一个策略是:用分段仿射函数来*似它。
具体方法是:选择一些区间,在区间端点采样,用直线连接相邻样本点,从而构造一个分段仿射*似函数。然后,应用分段仿射函数的积分法则。这种方法称为梯形法则。
如果区间是等宽的(宽度 h = (b-a)/(n-1)),梯形法则的公式为:
I ≈ (h/2) * [f(x_0) + 2f(x_1) + 2f(x_2) + ... + 2f(x_{n-1}) + f(x_n)]


计算成本与误差
我们关心计算成本如何随着精度(区间数 n)增加而增长。假设每次函数求值 f(x) 是固定成本 O(1)。
- 工作量:
O(n)。我们需要对n+1个点进行求值,并进行线性量级的算术运算。 - 误差:如果函数
f足够光滑,梯形法则的误差为O(h²)或O(1/n²)。这意味着我们以线性增长的工作量,换来了误差的*方级减少。
高维积分与维度灾难 🚨

现在考虑二维函数 f(x, y) 的积分,即计算曲面下的体积。我们可以将梯形法则推广到二维:先在 x 方向固定 y,用一维梯形法则*似关于 x 的积分,结果是一个关于 y 的函数;然后再对这个函数关于 y 应用梯形法则。
最终形式是双重求和:I ≈ Σ_i Σ_j w_{i,j} * f(x_i, y_j)。

- 误差:仍然是
O(h²)。 - 工作量:在一维是
O(n),在二维是O(n²)(需要在n×n的网格上采样)。
推广到 K 维,工作量将变为 O(n^K),而精度仍仅为 O(h²)。这种随着维度增加,计算量指数级增长而精度提升缓慢的现象,被称为维度灾难。

在图形学渲染中,由于渲染方程的递归性,我们需要积分的维度 K 可能非常高(数十、数百甚至数千)。因此,梯形法则这类方法无法扩展,我们需要一种根本不同的方法。

蒙特卡洛积分:应对高维的利器 🎲

蒙特卡洛积分是20世纪最重要的算法之一,它通过引入随机性来打破维度灾难。
其核心思想是:不再在确定性的规则点采样,而是随机地在积分域中选取样本点,用这些随机样本的函数值的*均来估计积分。这意味着每次运行算法,得到的估计值都可能不同。
蒙特卡洛积分的优点

- 只需函数求值:只需要在随机点计算函数值,不要求函数光滑,甚至可以处理有间断的函数(这在渲染中很常见)。
- 误差与维度无关:这是最关键的优势。蒙特卡洛估计的误差仅取决于使用的总样本数
N,与积分维度K无关。误差以O(1/√N)的速度下降。 - 适用于复杂积分:对于无法解析求积或维度很高的积分,蒙特卡洛通常是唯一可行的方法。
在一维时,梯形法则 (O(1/N²)) 可能比蒙特卡洛 (O(1/√N)) 更高效。但随着维度升高,蒙特卡洛的优势变得无可替代。
概率论基础回顾 🎯

要深入理解蒙特卡洛积分,我们需要回顾一些概率论基本概念。
随机变量与概率密度函数 (PDF)
随机变量 X 代表一系列可能取值的分布。我们用概率密度函数 p(x) 来描述这个分布,它表示随机过程选择特定值 x 的相对可能性。
- 离散分布:随机变量取有限个值
{x_i},每个值有概率p_i。概率必须非负且总和为1:p_i ≥ 0,Σ p_i = 1。 - 连续分布:随机变量在连续域上取值。PDF
p(x)必须非负,且在定义域上积分为1:p(x) ≥ 0,∫ p(x) dx = 1。某个区间[a, b]的概率是 PDF 在该区间上的积分。
累积分布函数 (CDF)

CDF P(x) 表示随机变量取值小于等于 x 的概率。
- 离散:
P_j = Σ_{i=1}^{j} p_i。 - 连续:
P(x) = ∫_{-∞}^{x} p(t) dt。
根据微积分基本定理,P(b) - P(a) 给出了 X 落在 [a, b] 内的概率。
如何生成随机样本?
给定一个 PDF,我们如何让计算机生成服从该分布的随机样本?
-
逆变换法(适用于可逆CDF):
- 计算 CDF
P(x)。 - 生成一个在
[0, 1]上均匀分布的随机数ξ。 - 计算
x = P^{-1}(ξ),则x即为服从 PDFp(x)的样本。 - 原理:因为
ξ是均匀的,x落在某个区间的概率正比于该区间对应的 CDF 变化量,即原 PDF 在该区间的积分(概率)。 - 挑战:需要能够解析地计算并求逆 CDF。
- 计算 CDF
-
拒绝采样法(适用于有界域且可找到包围盒):
- 用一个简单区域(如矩形、立方体)包围目标概率密度区域。
- 在简单区域内均匀采样一个点。
- 如果该点落在目标区域内,则接受它作为一个样本;否则,拒绝并重新采样。
- 优点:实现简单,无需计算 CDF。
- 缺点:如果目标区域占包围盒的比例很小(接受概率低),则效率低下,会浪费大量采样。
示例:均匀采样单位圆盘
- 错误方法:均匀采样半径
r ∈ [0,1]和角度θ ∈ [0, 2π),然后转换(r cosθ, r sinθ)。这会导致样本点向圆心聚集,分布不均匀。 - 正确方法之一(逆变换法):通过计算可知,应让
r的分布密度与r成正比(而非均匀),即采样r = √ξ₁,θ = 2πξ₂,其中ξ₁, ξ₂是独立的[0,1]均匀随机数。 - 正确方法之二(拒绝采样法):在包围圆的正方形
[-1,1]×[-1,1]内均匀采样点(x,y)。如果x² + y² ≤ 1,则接受该点;否则拒绝并重试。这种方法简单,且接受率约为π/4 ≈ 78.5%,效率尚可。
在选择采样方法时,需要在实现的复杂度和采样效率之间进行权衡。对于渲染中的常用分布,通常已有优化好的采样方法。
总结 📝
本节课我们一起学习了数值积分。
- 我们从积分的定义和简单函数的积分开始,介绍了求积法则的核心思想:通过采样点的加权和来*似积分。
- 我们探讨了梯形法则及其在一维和高维下的性能,并指出了传统方法在应对高维积分时的维度灾难问题。
- 为了克服维度灾难,我们引入了蒙特卡洛积分,它利用随机采样,使误差估计与维度无关。
- 为了实施蒙特卡洛积分,我们回顾了必要的概率论知识,包括概率密度函数、累积分布函数,以及生成特定分布随机样本的两种主要方法:逆变换法和拒绝采样法。

下一节,我们将把这些知识结合起来,把蒙特卡洛积分应用到递归的渲染方程中,从而学习如何生成真实感图像。
19:蒙特卡罗渲染 🎲


在本节课中,我们将学习如何通过蒙特卡罗渲染算法生成逼真的图像。我们将结合之前学过的光线追踪、辐射度量学、材质模型和数值积分等知识,来求解渲染方程,最终合成出照片级的图像。

光线追踪与光栅化的对比
上一节我们介绍了渲染的基本目标,本节中我们来看看两种主要的图像生成算法:光线追踪和光栅化。它们有相同的输入(场景几何、材质、光源、相机)和输出(图像),但计算顺序和适用场景不同。
以下是两种算法流程的核心区别:

- 光栅化:算法外层循环遍历所有图元(如三角形)。对于每个图元,确定它覆盖了屏幕上的哪些像素(采样点),然后计算覆盖率和颜色。通过深度缓冲区(Z-buffer)处理遮挡。
- 光线追踪:算法外层循环遍历所有输出图像的像素(采样点)。对于每个像素,遍历所有图元(或使用空间数据结构加速)来确定哪些图元在该像素可见,并计算其覆盖率和颜色。
这种顺序的翻转带来了能力上的巨大差异。由于光线追踪在处理每条光线时能“看到”整个场景,因此它能更自然、更统一地处理全局光照效果,如阴影、反射、折射和间接照明。而光栅化通常需要为每种效果单独设计*似技巧,且难以将它们完美地整合在一起。

蒙特卡罗积分基础
为了求解渲染方程,我们需要对复杂的积分进行数值估计。蒙特卡罗积分是一种强大的工具,它通过随机采样来估计积分值。
蒙特卡罗积分的基本公式如下:
I = ∫_Ω f(x) dx ≈ (|Ω| / N) * Σ_{i=1}^{N} f(X_i)

其中,X_i 是在定义域 Ω 内均匀随机抽取的样本点,N 是样本总数,|Ω| 是定义域的体积(或面积)。

蒙特卡罗方法的核心优势在于,其收敛速度与积分维度无关,这使其非常适合处理渲染方程中的高维积分问题。


概率论核心概念:期望与方差

为了深入理解蒙特卡罗估计的质量,我们需要两个关键的概率概念:期望值和方差。

- 期望值 (Expected Value):随机变量
Y的期望值E[Y]是其长期*均值。对于离散变量,计算公式为E[Y] = Σ_k P(Y=k) * k。期望是线性的,即E[aX + bY] = aE[X] + bE[Y]。 - 方差 (Variance):方差
V[Y]衡量随机变量Y与其期望值E[Y]的偏离程度,计算公式为V[Y] = E[(Y - E[Y])^2]。方差不是线性的,V[aY] = a^2 V[Y]。

在蒙特卡罗估计中,我们通过计算多个独立随机样本的*均值来逼*积分。根据大数定律,这个样本*均值会收敛到期望值。同时,样本*均值的方差会以 1/N 的速度减小,这意味着更多的样本会带来更精确(噪声更少)的估计。

重要性采样
均匀采样虽然简单,但效率可能很低。重要性采样通过改变采样分布,将更多的样本集中在被积函数值较大的区域,从而用更少的样本获得更低的方差(更清晰的图像)。

当我们从一个非均匀的概率密度函数 p(x) 中采样时,蒙特卡罗估计公式需要调整为:

I = ∫_Ω f(x) dx ≈ (1 / N) * Σ_{i=1}^{N} (f(X_i) / p(X_i))
这里,每个样本的贡献被其被选中的概率 p(X_i) 所修正。直观理解是:在采样密集的区域,每个样本的权重应该降低;在采样稀疏的区域,每个样本的权重应该增加,以确保估计的无偏性。

在渲染中,重要性采样可以应用于多个方面:
- 对光源采样:直接对光源表面(而非整个半球)进行采样,避免向黑暗空间发射大量无用光线。
- 对BRDF采样:根据材质的双向反射分布函数(BRDF)进行采样,优先考虑那些最可能反射光线的方向。
- 余弦加权采样:在计算辐照度时,对半球方向进行
cosθ加权的采样,因为接*法线方向(cosθ大)的贡献通常更大。

路径追踪:完整的蒙特卡罗渲染算法
现在,我们将蒙特卡罗积分应用于完整的、递归的渲染方程,这就得到了路径追踪算法。这是现代照片级渲染的核心算法。

路径追踪的基本思路是:为了计算从某个点 p 向相机方向 ω_o 的出射光 L_o(p, ω_o),我们需要估计渲染方程中的反射项积分。我们通过以下步骤进行:
- 从某个概率分布(如BRDF或余弦加权分布)中随机选取一个入射方向
ω_i。 - 从点
p向ω_i方向发射一条光线,找到最*的交点p'。 - 递归地计算在点
p'处朝向p的出射光L_o(p', -ω_i)。这本身又需要估计另一个渲染方程。 - 将采样得到的
L_o(p', -ω_i)乘以BRDF和余弦项,并除以采样概率,作为当前路径对该积分的一个样本贡献。 - 重复此过程,生成多条路径,并将所有路径的贡献*均起来,作为像素颜色的最终估计。

路径追踪自然地模拟了光在场景中的多次弹射(间接光照),从而能够生成包含柔和阴影、焦散、颜色渗透等复杂全局光照效果的图像。



俄罗斯轮盘赌:路径终止策略
路径可能无限反弹,我们需要一个策略来终止它。简单地设定一个最大反弹次数可能会丢失重要的长路径贡献(例如通过玻璃的多次折射)。俄罗斯轮盘赌是一种无偏的路径终止技术。
其工作原理如下:
- 在每次可能的光线反弹点,我们以概率
P_RR继续追踪路径,以概率(1 - P_RR)终止路径。 - 如果继续追踪,则将路径的贡献值除以
P_RR,以保持估计的无偏性。如果终止,则贡献为0。
通过调整 P_RR,我们可以在计算效率和图像噪声之间取得*衡。较低的继续概率能更快地终止贡献小的路径,从而加速渲染,但可能会增加图像的噪声。

总结


本节课中我们一起学习了蒙特卡罗渲染的核心内容。我们了解到,像素的最终颜色由递归的渲染方程描述。通过应用蒙特卡罗积分来估计这个方程,我们得到了路径追踪算法。该算法通过随机采样光传输路径来模拟复杂的光照现象。
我们认识到,采样策略至关重要。重要性采样通过将计算资源集中在贡献大的区域(如明亮的光源或主要的反射方向),可以显著降低估计的方差,从而用更少的样本获得更清晰的图像。最后,俄罗斯轮盘赌提供了一种智能的、无偏的路径终止机制,以*衡渲染时间和图像质量。

下一讲,我们将更深入地探讨方差缩减技术,学习如何进一步优化采样策略,以获得更高效的渲染。
20:方差减少 🎯


在本节课中,我们将要学习物理渲染的收尾部分,重点讨论用于减少方差的技术。上一节我们介绍了蒙特卡洛光线追踪,本节中我们来看看如何通过更智能的采样策略,用更少的计算量获得更*滑、更高质量的图像。
蒙特卡洛积分回顾 📊
蒙特卡洛积分是一种通用的数值积分方法。其基本思想是:通过在定义域 Ω 内随机采样,用样本点处函数值的*均值来*似积分值。
公式:
[
\int_{\Omega} f(x) , dx \approx \frac{V(\Omega)}{N} \sum_{i=1}^{N} f(X_i)
]
其中,( X_i ) 是在 Ω 上均匀分布的随机样本,( V(\Omega) ) 是定义域的体积,( N ) 是样本数量。

概率论基础概念 🔢
为了深入理解方差减少,我们需要回顾几个关键的概率论概念。
期望值
对于离散随机变量 ( X ),其期望值 ( E[X] ) 是各事件值按其概率的加权*均。

公式:
[
E[X] = \sum_{i=1}^{n} p_i \cdot x_i
]

对于连续随机变量,期望值通过概率密度函数 ( p(x) ) 计算。
公式:
[
E[X] = \int_{\Omega} p(x) \cdot x , dx
]

方差与标准差


方差衡量随机变量与其期望值的偏离程度。标准差是方差的*方根,更直观地描述了数据的离散程度。
公式:
[
\text{Var}(X) = E[(X - E[X])^2]
]
[
\sigma = \sqrt{\text{Var}(X)}
]

在渲染中,方差直接表现为图像中的噪点。高方差意味着图像噪声多,低方差则图像更*滑。

方差减少的核心:优化估计器 🎲
一个常见的误解是试图减少被积函数 ( f ) 本身的方差。实际上,我们无法改变被积函数。我们真正能优化的是估计器——即我们用来*似积分的那个公式。蒙特卡洛估计器就是其中之一,其方差取决于我们如何选取样本点 ( X_i )。
估计器的性质:一致性 vs. 无偏性

评估一个估计器时,两个关键性质是一致性和无偏性。
- 一致性:随着样本数 ( N ) 趋*于无穷,估计值以概率1收敛于真实积分值。
- 无偏性:对于任何有限的样本数 ( N ),估计值的期望等于真实积分值。
重要区别:一致性不意味着无偏性,反之亦然。一个估计器可能最终能给出正确答案(一致),但在有限样本时系统性地高估或低估(有偏)。



以下是不同渲染算法的性质对比:

| 算法 | 一致性 | 无偏性 |
|---|---|---|
| 光栅化 | 否 | 否 |
| 路径追踪 | *似是 | *似是 |
| 双向路径追踪 | 是 | 是 |
| 光子映射 | 是 | 否 |
| 辐射度算法 | 否 | 否 |
无偏估计器通常行为更可预测,误差以可预测的速率下降,并且需要调整的参数更少。
重要采样:将计算用在“刀刃”上 🎯
重要采样是减少方差最核心的技术之一。其思想是:与其均匀地采样整个定义域,不如根据函数值可能的大小来调整采样密度。在函数值大的区域多采样,在函数值小的区域少采样。

重要采样蒙特卡洛估计公式:
[
\int_{\Omega} f(x) , dx \approx \frac{1}{N} \sum_{i=1}^{N} \frac{f(X_i)}{p(X_i)}
]
其中,样本 ( X_i ) 根据概率密度函数 ( p(x) ) 抽取,而非均匀分布。我们通过除以 ( p(X_i) ) 来修正采样偏差。
理想情况:如果 ( p(x) ) 恰好与 ( f(x) ) 成正比,即 ( p(x) = f(x) / I ),其中 ( I ) 是待求积分,那么仅需一个样本就能得到精确结果。这揭示了重要采样的目标:让采样分布 ( p(x) ) 尽可能接*被积函数 ( f(x) ) 的形状。
在渲染中,有两种主要的局部重要采样策略:

- 基于材质的采样:对于高光材质,在镜面反射方向附*集中采样;对于漫反射材质,使用余弦加权采样。
- 基于光源的采样:直接对光源区域(如面光源)进行采样,而不是在整个半球随机采样。对于点光源,则直接向光源位置发射光线。

路径积分与全局策略 🌐

将渲染视为在路径空间的积分,为我们打开了全局重要采样策略的大门。路径空间包含了所有从光源到眼睛的可能光路。
路径积分公式:
[
I = \int_{\mathcal{P}} f(\bar{p}) , d\mu(\bar{p})
]
其中,( \mathcal{P} ) 是所有路径的集合,( f(\bar{p}) ) 是路径 ( \bar{p} ) 携带的光能。
这种视角让我们可以思考如何直接寻找那些对最终图像贡献大的重要路径,而不是一步步局部采样。
双向路径追踪

传统路径追踪只从眼睛出发。双向路径追踪则同时从眼睛和光源出发生成子路径,然后尝试连接它们。这能有效捕获那些仅从一端出发难以找到的路径(如焦散)。
大都市光传输
对于一些场景,重要路径(如穿过门缝的光)极其难以通过随机采样找到。MLT算法采用了一种“随机游走”的策略:当找到一个好路径(高贡献)后,不是抛弃它,而是对其稍加扰动,在其附*探索,从而更高效地聚集在重要路径区域。

核心步骤:
- 生成一个初始路径(例如通过双向路径追踪)。
- 对当前路径进行“突变”(如轻微改变一个反射方向)。
- 计算新路径的贡献值 ( f_{new} ) 与旧路径贡献值 ( f_{old} ) 的比值 ( \alpha )。
- 以概率 ( \min(1, \alpha) ) 接受新路径作为当前路径,否则保持旧路径。
- 重复步骤2-4,在游走过程中收集的路径贡献的*均值即为积分估计。


这种方法能自动将计算时间集中在贡献大的路径上。

多重重要采样:强强联合 🤝
当有多个重要采样策略(如基于光源和基于材质的策略)可用时,如何组合它们?多重重要采样提供了一种自动化的加权*均方法,以保留各个策略的优点。

*衡启发式公式:
[
I \approx \frac{1}{N} \sum_{i=1}^{n} \sum_{j=1}^{n_i} \frac{f(X_{ij})}{\sum_{k=1}^{n} n_k p_k(X_{ij})}
]
其中,( n ) 是策略数量,( n_i ) 是分配给第 ( i ) 个策略的样本数,( p_k ) 是第 ( k ) 个策略的概率密度。

这种方法能防止某个糟糕的采样策略主导结果,并能在不同场景区域自动选择最有效的策略。
采样模式的质量:超越随机 🎨
样本点 ( X_i ) 在定义域中的分布模式本身也极大影响方差。纯粹的随机采样可能导致样本聚集或留下过大空隙。
分层采样
将定义域均匀划分为若干子区域(层),然后在每个子区域内独立随机采样。这能保证样本分布更均匀,分层估计器的方差永远不会大于原始估计器。
低差异序列

低差异序列(如Halton序列、Hammersley序列)生成的样本点比随机样本更均匀地覆盖空间。它们通过确定性方法构造,具有极低的“差异”——即样本点在任意区域的数量与该区域面积的偏差很小。
差异定义(简化):
对于样本点集 ( P ) 和区域 ( S ),差异 ( D(P, S) = |\frac{|P \cap S|}{N} - \text{Area}(S)| )。低差异序列最大化最小化所有合理区域 ( S ) 上的差异。

使用低差异序列的蒙特卡洛积分称为拟蒙特卡洛方法。Koksma-Hlawka定理表明其误差受限于样本点集的差异和被积函数的变化程度。

蓝噪声采样

蓝噪声采样模式在视觉上和频谱上都呈现出各向同性和均匀的特性。它没有明显的规则网格结构,避免了与场景规则特征产生共振导致的走样。

特性:
- 各向同性:没有主导方向。
- 频谱均匀:在傅里叶频谱中,能量集中在高频(蓝色),中低频成分弱,避免了规则模式带来的强频谱峰值。
生成蓝噪声模式的方法包括泊松圆盘采样、劳埃德松弛算法(迭代移动点到其邻域中心)以及基于维诺图的优化方法。
高效采样分布:别名方法 ⚡


当需要从一个具有大量可能事件(如高分辨率环境贴图)的离散分布中重复采样时,使用累积分布函数进行二分查找的成本是 ( O(\log N) )。别名方法可以在 ( O(1) ) 的摊销时间内完成采样。

别名表构建思想:“劫富济贫”。将概率分布重新组织成一个表格,使得每一列对应至多两个事件,并且采样时只需一次均匀随机选择和一
次硬币抛掷即可决定事件。
采样步骤:
- 均匀随机选择一个列索引 ( i ) (1 到 N)。
- 生成一个在 [0, 1) 内的随机数 ( \xi )。
- 如果 ( \xi ) 小于该列存储的阈值,则返回该列的第一个事件标识,否则返回第二个。
其他渲染算法掠影 ✨


- 光子映射:从光源发射“光子”,并在其与场景交互的位置存储起来。渲染时,从眼睛出发,收集附*的光子来估算光照。特别擅长处理焦散和参与介质。
- 辐射度算法:将场景表面离散为面片,并计算面片间的光能传递,求解一个线性方程组。非常适合漫射主导的场景,但难以处理高光效果。

总结 📝

本节课中我们一起学习了多种用于减少蒙特卡洛渲染方差的技术:
- 重要采样通过使采样分布匹配被积函数来显著提高效率。
- 双向路径追踪和大都市光传输通过全局路径构建和探索策略,捕获难以通过标准路径追踪找到的光路。
- 多重重要采样智能地组合多个采样策略。
- 高质量采样模式(如分层、低差异序列、蓝噪声)通过改善样本分布本身来降低方差。
- 别名方法提供了从复杂离散分布中高效采样的工具。

理解这些技术的原理、权衡(如偏差、一致性)以及适用场景,对于实现高效、高质量的物理渲染至关重要。尽管蒙特卡洛方法在理论上可以无限逼*正确结果,但通过巧妙的方差减少技术,我们能够在有限的计算资源下获得视觉上令人满意的图像。
21:动画概论 🎬


在本节课中,我们将要学习计算机动画的基本概念。我们将从动画的历史发展讲起,探讨人类如何描绘和捕捉运动,然后深入讲解计算机动画的核心技术——关键帧动画与样条插值。我们将学习如何用数学方法在关键帧之间生成*滑的运动,并了解动画在计算机图形学中的多种应用方式。



从静态到动态:动画的历史与感知 👁️



上一节我们介绍了渲染技术,本节中我们来看看如何为静态模型添加动态效果——即动画。在课程中,我们模型的复杂度在逐渐增加:从描述几何形状,到应用几何变换,再到讨论材质与光照。然而,我们尚未涉及如何表现运动。






纵观历史,人类一直热衷于描绘和捕捉现实世界中的运动。以下是一些早期尝试描绘运动的例子:
- 在古希腊的陶瓶上,发现了描绘山羊跳跃吃浆果的连续图案,这可以被视为最古老的“动画”概念之一。
- 列奥纳多·达·芬奇的画作研究了人体运动时肌肉的膨胀和收缩等次级运动。
- 克劳德·莫奈等印象派画家则试图在静态画作中捕捉运动的“印象”,例如风吹草动和云彩流动。






但这些仍然是静态图像。真正推动动画发展的早期技术设备是“费纳奇镜”等装置。其原理是利用视觉暂留现象,通过旋转带有连续图案的圆盘并通过狭缝观看,欺骗人眼感知到连续运动。


电影技术的诞生进一步推动了动画发展。埃德沃德·迈布里奇为了研究马匹奔跑时是否四蹄同时离地,利用连续摄影技术捕捉了运动序列,这无意中加速了动画技术的发展。
在电影动画领域:
- 第一部手绘长篇动画是迪士尼的《白雪公主和七个小矮人》。
- 然而,第一部计算机生成的动画并非皮克斯的《玩具总动员》。更早的例子包括伊万·萨瑟兰开发的“画板”系统,以及波音公司用于研究飞行员行为的“波音人”动画。
- 随着计算能力提升,皮克斯最终成功制作了第一部计算机生成的长篇动画电影。

现代技术如3D打印,也被用于创造类似“走马灯”的实体动画装置,通过旋转和视觉暂留产生迷人的运动错觉。
理解运动感知的心理学原理对于设计数字动画至关重要。早期“视觉暂留”理论认为运动感知源于视网膜上图像的持续存在,但这已被推翻。更现代的解释包括:
- β运动现象:大脑将快速连续出现的静态图像感知为连续运动。这是电影和视频的基础。
- φ现象:大脑会根据视觉线索预判运动。例如,两个旋转速度相同但闪烁频率不同的物体,会被感知为以不同速度运动。
艺术家们早已利用这些现象。β现象让我们能将序列图像视为电影;而在静态艺术中,则利用视觉线索来暗示或引发对运动的预期。

关键帧动画与插值技术 ✏️
在传统手绘动画中,生成运动主要基于β现象:展示连续的帧并利用视觉记忆。高级动画师绘制关键帧,即突出重要动作或事件的帧;然后由助手绘制中间帧,填充关键帧之间的过渡动作。对于每秒24帧的动画,这是一个非常繁琐的过程。
计算机可以自动化这些重复性任务。核心问题是如何用计算机描述运动,让计算机替我们处理繁琐的中间帧生成。计算机动画有多种基本技术,其中一种非常类似传统手绘动画,即关键帧动画。
在数字环境中,关键帧动画的基本思想是:我们指定重要的状态(关键帧),然后由计算机通过插值或*似来填充中间状态。关键帧不仅可以定义位置,还可以定义颜色、光照强度、摄像机位置、几何变换等多种属性。通过为大量不同属性设置关键帧并插值,可以快速构建出丰富有趣的动画。
那么,如何在关键帧之间进行插值呢?我们可以回到一个古老的概念——样条插值。过去,设计师使用有弹性的木条或金属条(物理样条)来绘制光滑曲线,它会在控制点之间形成*滑的形状。
抽象到数学和计算机图形学中,样条是指任何分段多项式函数。插值的基本思想是:我们有一些数据点,希望找到一个连续函数穿过这些点。
最简单的策略是分段线性插值,即用直线段连接数据点。其公式简单,但对于动画而言可能产生不自然的运动,因为在节点处速度会突变,加速度理论上无限大,这看起来很不自然。
因此,我们倾向于使用更高阶的多项式,例如三次样条,以获得更好的连续性(如一阶、二阶导数连续)。在计算机图形学和数值分析中,三次样条被广泛使用,原因包括:
- 在小位移假设下,三次样条能精确模拟物理样条(弹性木条)的行为,即它在所有插值给定数据点的曲线中,最小化曲线二阶导数的范数(*似于最小化曲率)。
- 三次多项式具有简单的闭式解、较好的连续性,且自由度数量适中,易于操纵和预测。
- 使用非常高次的多项式插值可能导致龙格现象,即在数据点之间产生剧烈的振荡,因此通常避免使用。
样条曲线详解:从定义到类型 🔢
更正式地,在一维情况下,样条在实数线上插值数据。我们有一系列在时间 t_i(也称为节点)处取值 f_i。插值意味着函数 f(t) 在节点处精确通过数据点,即 f(t_i) = f_i。此外,函数在每两个节点之间的区间上是一个多项式。
考虑单个三次多项式 P(t) = a*t^3 + b*t^2 + c*t + d。如果我们只要求它匹配两个端点,即 P(0)=p0, P(1)=p1,那么我们有2个方程,4个未知数(a, b, c, d),解不唯一。
为了唯一确定曲线,我们可以增加约束,例如同时指定端点的值和一阶导数值。即 P(0)=p0, P(1)=p1, P'(0)=u0, P'(1)=u1。这样就得到了4个关于系数 a, b, c, d 的线性方程,可以求解出唯一的三次曲线。这种指定端点位置和切线(导数)的形式称为埃尔米特形式。
现在考虑由多个三次多项式片段组成的分段样条。我们希望整个样条曲线:
- 插值性:每个片段在其端点处插值给定的数据点。
- 连续性:在节点处,相邻片段的函数值、一阶导数甚至二阶导数相等。
对于有 n 个区间的样条,如果我们要求所有内部节点处达到 C2 连续性(即函数值、一阶导、二阶导均连续),再加上插值条件,总共会有 4n-2 个方程。而我们有 4n 个自由度(每个三次片段4个系数)。为了获得唯一解,我们需要额外指定两个条件,例如指定起点和终点的二阶导数为零(自然边界条件),这样得到的样条称为自然样条。
自然样条具有良好的插值性和 C2 连续性,但缺乏局部性:移动一个数据点需要重新求解整个线性系统,会影响整个曲线的形状。
另一种常见的样条是贝塞尔样条(或埃尔米特样条)。它通过指定每个片段端点的位置和切线来定义。对于每个片段,这4个条件正好唯一确定一个三次贝塞尔曲线。贝塞尔样条具有插值性和局部性(修改一个片段的数据只影响该片段),但通常不能保证节点处的 C2 连续性(二阶导数可能不连续)。
Catmull-Rom 样条是埃尔米特样条的一种特例,它不需要用户提供切线数据。相反,它根据相邻数据点的位置自动计算切线:节点 i 处的切线方向由 (f_{i+1} - f_{i-1}) / (t_{i+1} - t_{i-1}) 决定。然后像贝塞尔样条一样使用这些位置和计算的切线进行插值。Catmull-Rom 样条在计算机动画中很常用,因为它只需提供路径点就能生成看起来合理的运动。
最后,还有 B 样条。B 样条曲线不是通过控制点,而是通过控制顶点和B 样条基函数的线性组合来定义。B 样条具有很好的局部性和 C2 连续性,但它通常不插值控制顶点,而是*似地跟随它们。这意味着曲线不一定穿过你给出的点。B 样条基函数可以通过递归方式定义。
以下是不同类型样条的性质总结:
| 样条类型 | 插值性 | C2 连续性 | 局部性 |
|---|---|---|---|
| 自然样条 | 是 | 是 | 否 |
| (埃尔米特)贝塞尔样条 | 是 | 否 | 是 |
| B 样条 | 否 | 是 | 是 |

这个表格说明了一个重要原则:在样条设计中,没有免费午餐。插值性、高阶连续性和局部控制这三者通常不可兼得,必须根据具体任务的需求进行权衡选择。
动画中的应用:超越位置的插值 🎮


那么,在动画中我们具体插值什么呢?数据 f 可以是任何属性,不仅仅是位置。
- 摄像机动画:除了插值摄像机在空间中的
(x, y, z)位置,还需要插值其朝向(如前、上、右向量)。 - 角色动画:角色通常由一个骨骼层次结构(变换树)控制。我们可以对骨骼中的变换参数(如旋转角、位移量)进行插值,从而驱动整个角色的运动。这比直接插值每个顶点的位置高效得多。
- 逆向运动学:对于复杂的骨骼系统,直接设置每个关节的角度非常困难。逆向运动学允许动画师指定末端效应器(如手部)的目标位置,然后通过数值优化方法自动计算出所有中间关节应有的角度。
- 蒙皮动画:为了让基于骨骼的动画产生更有机的变形(如肌肉拉伸),需要使用蒙皮技术。每个骨骼影响网格顶点变形的权重不同,最终的顶点位置是所有相关骨骼变换的加权混合。
- 混合形状:常用于面部动画。艺术家预先制作一系列基础表情模型(如喜、怒、哀、乐)。动画时,通过插值这些基础形状的混合系数,并将系数与形状线性组合,来得到*滑过渡的面部表情。而这些系数随时间的变化,本身就可以用样条曲线来定义和控制。






本节课中我们一起学习了计算机动画的概论。我们从动画的历史和人类视觉感知原理出发,重点讲解了关键帧动画的核心——样条插值技术,包括自然样条、贝塞尔样条、Catmull-Rom样条和B样条等,并分析了它们在插值性、连续性和局部性之间的权衡。最后,我们探讨了这些技术在摄像机动画、角色骨骼动画、蒙皮和混合形状等具体场景中的应用。通过关键帧和样条,计算机能够自动生成*滑的中间帧,大大简化了动画制作流程。在接下来的课程中,我们将继续探索其他动画技术,如运动捕捉和基于物理的仿真。
22:动力学和时间积分 🎬
在本节课中,我们将学习如何使用数值模拟来生成计算机动画。我们将探讨动力学描述、时间积分方法,以及如何通过物理模拟来为虚拟世界添加复杂的运动。
概述
上一讲我们开始讨论动画,其基本思想是为世界模型添加运动。我们探讨了如何建模几何、材料以及光与这些几何和材料的交互。现在,我们希望通过添加运动让事物“活”起来。
上节课我们研究的基本技术是关键帧插值。这可以追溯到动画的起源,艺术家手工绘制关键姿势,然后由另一位艺术家填充中间帧。在数字时代,我们可以在三维几何模型上设置关键帧,让计算机通过样条插值来填充中间帧。
然而,即使是这种半自动化的过程,工作量也很大,因为可能需要为每个关键帧设置大量参数。今天,我们将探讨一种不同类型的计算机动画——基于物理的模拟动画。
动力学描述与物理模拟
动力学 vs. 运动学
动力学关注力的研究及其对运动的影响。与之相对的是运动学,它只研究物体的运动,而不考虑其产生的原因。关键帧插值或样条插值是一种运动学描述,因为我们只是指定了物体在不同时间点的期望位置,而没有理解导致其运动的力。
动画方程:牛顿第二定律
在渲染中我们有渲染方程,在计算机动画中,我们则有动画方程,即牛顿第二定律:
F = ma
其中,F 是力,m 是质量,a 是加速度。这是我们生成动画时需要求解的核心方程。我们描述系统中的质量和作用其上的力,然后求解这个二阶微分方程,从而得到速度,进而得到物体的运动轨迹。
广义坐标与系统状态
为了统一描述复杂系统,我们引入广义坐标的概念。任何系统在任何时刻都有一个构型,它是时间的函数。这个构型是一个很长的列表,包含了描述系统当前状态的所有变量。
例如,对于台球系统,这个向量会存储每个球的*面位置 (x, y)。如果我们有6个球,就会有12个坐标。我们可以将整个系统的演化想象为一个在更高维空间(本例中是12维)中移动的点的轨迹。
这种观点的优势在于,它自然地映射到我们实际用计算机求解方程的方式。我们将所有描述系统的变量堆叠成一个大向量,交给求解器,然后得到描述下一时刻所有位置的新向量。
拉格朗日力学:一种优雅的建模方法
对于复杂系统,直接分析受力可能很困难。拉格朗日力学提供了一种更通用、更优雅的方法来推导运动方程。
以下是使用拉格朗日力学的步骤:
- 写出系统的动能 (T)。例如,对于质点,动能为
(1/2) * m * v²。 - 写出系统的势能 (U)。例如,重力势能为
m * g * h。 - 定义拉格朗日量 (L):
L = T - U。 - 应用欧拉-拉格朗日方程:
d/dt (∂L/∂q̇) = ∂L/∂q
其中,q是广义坐标,q̇是广义速度。
这个方程的左端项类似于“质量 × 加速度”,右端项类似于“力”,本质上仍然是牛顿第二定律。这种方法的优点在于:
- 更容易处理:能量是标量,无需考虑方向,不易出错。
- 适用于任何广义坐标。
- 自然地引向一类优秀的数值模拟技术。
示例:单摆运动

让我们用拉格朗日方法推导单摆的运动方程。
- 广义坐标:摆角
θ。 - 动能:
T = (1/2) * m * l² * θ̇²。 - 势能:
U = -m * g * l * cosθ(以悬挂点为零势能点)。 - 拉格朗日量:
L = T - U = (1/2) * m * l² * θ̇² + m * g * l * cosθ。 - 应用欧拉-拉格朗日方程:
∂L/∂θ̇ = m * l² * θ̇d/dt (∂L/∂θ̇) = m * l² * θ̈∂L/∂θ = -m * g * l * sinθ- 得到方程:
m * l² * θ̈ = -m * g * l * sinθ,即θ̈ = -(g/l) * sinθ。
这就是单摆的运动方程。对于小角度摆动(sinθ ≈ θ),方程简化为 θ̈ = -(g/l) * θ,其解是简谐运动:θ(t) = A * cos(√(g/l) * t + B)。
然而,对于大角度摆动或更复杂的系统(如双摆),这个方程通常没有解析解。双摆系统是一个经典的混沌系统,初始条件的微小变化会导致轨迹的巨大差异。这时,我们必须依靠数值模拟。

粒子系统与质点-弹簧系统

在计算机图形学中,粒子系统和质点-弹簧系统是模拟复杂现象的强大工具。
粒子系统


粒子系统将现象建模为大量相互作用的粒子。每个粒子有自己的位置、速度,并受到各种力的影响。通过为粒子定义简单的行为规则,可以涌现出复杂的群体行为。
一个著名的例子是鸟群模拟(Boids模型),其中每个“鸟”(粒子)遵循三条基本规则:
- 分离:避免与邻居太*。
- 对齐:与邻居的*均飞行方向保持一致。
- 聚集:向邻居的*均位置靠拢。
通过数值积分这些粒子所受的力,就能模拟出逼真的鸟群运动。粒子系统也广泛用于模拟火焰、烟雾、流体和沙粒等。
质点-弹簧系统


质点-弹簧系统通过弹簧连接粒子,常用于模拟可变形体,如布料、头发和软组织。
- 弹簧势能:对于连接两点
x₁和x₂的弹簧,其势能为U = (1/2) * k * (||x₁ - x₂|| - l₀)²,其中k是刚度,l₀是原长。 - 布料模拟:可以将布料网格的顶点视为质点,网格的边视为弹簧。通过调整弹簧参数并处理碰撞,就能模拟出布料的各种动态效果。
时间积分:数值求解运动方程
既然我们得到了运动方程(通常是常微分方程 ODE),如何用计算机求解呢?核心思想是用差分来*似微分。
我们不再求解连续函数 q(t),而是在离散时间点 t₀, t₁, t₂, ... 上采样,时间步长为 Δt。我们的目标是,已知当前时刻 t_k 的状态 q_k,计算下一时刻 t_{k+1} 的状态 q_{k+1}。
前向欧拉法
最直接的方法是前向(显式)欧拉法:
q_{k+1} = q_k + Δt * f(q_k)
其中 f(q) 是速度函数(对于二阶方程,需先化为一阶方程组)。
- 优点:简单,计算快(显式)。
- 缺点:稳定性差。对于“刚性”系统(如很硬的弹簧),需要非常小的时间步长
Δt才能避免模拟爆炸(能量激增)。这会导致计算量巨大。
后向欧拉法
另一种方法是后向(隐式)欧拉法:
q_{k+1} = q_k + Δt * f(q_{k+1})
注意,等式右边依赖于未知的 q_{k+1},因此这是一个需要求解的隐式方程。
- 优点:无条件稳定。即使使用较大的时间步长,模拟也不会爆炸。
- 缺点:数值阻尼。模拟会逐渐损失能量,导致运动看起来“粘滞”,不够生动。并且每步都需要求解(可能非线性的)方程,计算更复杂。
辛欧拉法
在计算机图形学中,经常使用一种折中的方法——辛欧拉法(或蛙跳法)。其更新步骤是交错的:
- 用当前构型更新速度:
v_{k+1} = v_k + Δt * f(q_k) - 用新速度更新构型:
q_{k+1} = q_k + Δt * v_{k+1}
- 优点:能很好地保持能量(对于保守系统),长期模拟效果稳定,且实现简单。
- 缺点:精度和稳定性介于前向和后向欧拉法之间。
没有一种积分器是万能的。选择时需要权衡稳定性、精度、能量守恒性和计算效率。
微分计算:获取力与梯度
在模拟中,我们经常需要计算势能的梯度(即力)或其他导数。有几种方法:
- 手工推导:准确、高效,但工作量大且容易出错。
- 数值微分:通过有限差分*似,
f'(x) ≈ (f(x+h) - f(x)) / h。通用但不精确,且需要为每个变量多次计算函数值,速度慢。 - 自动微分:通过重载算术运算,让计算机在计算函数值的同时,利用链式法则计算导数值。精度高,速度快,是现代深度学习框架的核心,但需要改变代码使用特定的库。
- 符号微分:使用计算机代数系统(如 Mathematica)进行符号求导。适用于推导公式,但得到的表达式可能非常复杂,且不易集成到实时应用中。
- 几何微分:利用几何直觉直接推导梯度表达式。通常能得到最简洁、物理意义最明确的结果,但需要洞察力,且只适用于特定问题。
例如,求三角形面积关于其一个顶点 p 的梯度。符号微分可能给出一长串复杂的表达式。但几何上我们知道,面积梯度方向垂直于该顶点的对边,大小与对边长度成正比。因此,梯度向量就是 (1/2) * (对边向量) × (三角形单位法向) 的某种形式,非常简洁。
总结
本节课我们一起学习了计算机动画中的动力学模拟与时间积分。
- 我们首先区分了运动学(描述运动)和动力学(从力推导运动),并引入了基于牛顿第二定律
F=ma的物理模拟思想。 - 为了描述复杂系统,我们学习了使用广义坐标将整个系统的状态编码为一个高维向量,并将其运动视为在高维空间中的轨迹。
- 我们介绍了拉格朗日力学这一强大工具,它通过动能和势能标量来优雅地推导运动方程,并以单摆和双摆为例进行了说明。
- 我们探讨了粒子系统和质点-弹簧系统这两种在图形学中模拟群体行为、流体、布料等现象的常用模型。
- 核心挑战在于求解运动方程。我们学习了数值时间积分的基本方法:前向欧拉法(简单但不稳定)、后向欧拉法(稳定但有阻尼)和辛欧拉法(在能量守恒和效率间取得良好*衡)。
- 最后,我们讨论了计算模拟中所需要的微分(如力)的几种技术:手工推导、数值微分、自动微分、符号微分和几何微分,并比较了它们的优缺点。

通过将物理定律与数值计算相结合,我们可以让计算机自动生成丰富、复杂且逼真的动画,极大地扩展了动画创作的可能性。下一讲,我们将进入另一个核心主题:优化。
23:优化


在本节课中,我们将学习一个在计算机图形学、动画乃至整个计算领域都非常重要的主题:优化。我们将探讨如何通过定义目标函数和约束条件,并利用数值方法寻找最优解,从而驱动复杂的动画和图形效果。



上一节我们介绍了基于物理的动画,通过求解动力学方程(如 F=ma)来生成丰富的运动。本节中,我们来看看另一种强大的通用工具:数值优化。其核心思想是,我们定义一个量化解决方案好坏的目标函数,然后通过“沿山坡下滑”(例如,跟随梯度)的方式,不断改进我们的解,以找到越来越好的结果。






什么是优化问题?
优化是人们思考问题的自然方式:你手头有一个任务或问题,你希望在所有可能性中找到最佳解决方案。通常,你还会受到一些约束的限制,例如预算或时间。优化问题的一般形式是:在满足给定约束的条件下,最大化或最小化某个目标。
一个著名的早期例子是等周问题。故事中,公主狄多被允诺可以获得一张牛皮所能围起来的土地。她将牛皮切成细长的条带,并思考:用固定长度的条带,能围出的最大土地面积是什么形状? 答案是圆形。这就是一个优化问题:最大化面积(目标),约束条件是曲线长度固定。

优化在图形学中的应用
优化在图形学中无处不在。以下是一些例子:

- 动画与角色控制:例如,让角色在受到推搡后自动调整姿势以保持*衡,或伸手去抓取环境中的物体。通过优化角色关节角度,使其末端效应器(如手)尽可能接*目标位置,可以自动生成复杂的运动,而无需手动设置每一帧。
- 几何处理:例如,给定一个网格模型,通过优化使其对称性更强。这可用于模型简化、压缩或对称编辑。
- 3D打印与制造:优化物体的动力学属性,例如调整其惯性矩,使其在被推动时能按特定方式旋转。
- 图像处理与渲染:许多图像滤波、图像修复、光照计算等问题都可以表述为优化问题。
- 机器学习:机器学习本质上就是通过优化损失函数来训练模型参数。
优化问题的分类

以下是优化问题的主要分类方式:

- 离散优化:变量的定义域是一个离散集合(例如,有限的或可与整数对应的集合)。一个简单的例子是:炖菜中放哪种蔬菜最好?最朴素的策略是尝试所有可能性(穷举法),但对于复杂问题(如旅行商问题),这通常计算量巨大(NP难问题)。
- 连续优化:变量可以在一个连续区间(如实数)内取值。例如,煮鸡蛋的最佳温度是多少?由于有无限多种可能,我们需要更聪明的策略(如梯度下降)。在连续优化中,有一类重要且通常易于求解的问题是凸优化问题。
优化问题的标准形式

大多数连续优化问题都可以写成以下标准形式,这有助于我们使用通用工具进行求解:
最小化目标函数 f₀(x),满足约束条件 fᵢ(x) ≤ bᵢ (对于 i = 1, ..., m)。
其中:
x = (x₁, ..., xₙ)是包含n个参数的向量。f₀: ℝⁿ → ℝ是目标函数(或成本函数),我们希望通过选择x使其值尽可能小。fᵢ(x) ≤ bᵢ定义了可行解集,即所有允许的x必须满足的条件。
说明:
- 最大化问题:可以通过将目标函数取负号(最小化
-f₀(x))转化为最小化问题。 - 等式约束:可以通过一对不等式约束(
g(x) ≤ c和g(x) ≥ c)来表示。

解的存在性与最优性条件

在求解之前,我们需要知道解是否存在,以及如何判断一个点是否为最优解。

-
全局最小值 vs. 局部最小值:
- 全局最小值:在所有可行解中目标函数值最小的解。
- 局部最小值:在其某个邻域内目标函数值最小的解。我们通常更容易找到局部最小值。
- 对于许多问题(如机器学习、蛋白质折叠),找到一个好的局部最小值就足够了。
-
解可能不存在的情况:
- 目标函数无下界(例如,最小化
f(x) = x,x 为实数)。 - 约束条件不可行,即没有满足所有约束的
x(例如,要求x = 1且x = -1)。 - 目标函数值可以无限接*但永远达不到下界(例如,最小化
f(x) = e⁻ˣ,当x → ∞时f(x) → 0,但永远不为 0)。
- 目标函数无下界(例如,最小化
-
解存在的充分条件:
- 极值定理:如果目标函数连续,且定义域(可行解集)是
ℝⁿ中的紧集(闭且有界),则全局最小值存在。 - 强制性:当
||x|| → ∞时,目标函数f₀(x) → ∞。这保证了我们不会在无穷远处找到更小的值。
- 极值定理:如果目标函数连续,且定义域(可行解集)是

-
无约束问题的最优性条件(用于检验局部最小值):
对于一个二阶可微函数f₀(x),如果x*是一个局部最小值点,则必须满足:- 一阶条件:梯度为零,
∇f₀(x*) = 0。 - 二阶条件:Hessian 矩阵半正定,
∇²f₀(x*) ≽ 0(即对于任意向量u,有uᵀ ∇²f₀(x*) u ≥ 0)。
- 一阶条件:梯度为零,
-
有约束问题的最优性条件:
对于有约束的优化问题,最优解需要满足更复杂的 KKT条件(Karush-Kuhn-Tucker conditions),它引入了拉格朗日乘子来处理约束。

凸优化:一类“友好”的问题
凸优化问题因其良好的性质而备受青睐:
- 凸集:集合中任意两点的连线仍包含在该集合中。
- 凸函数:函数图像上任意两点的连线位于图像上方。
- 如果一个优化问题的定义域是凸集,且目标函数是凸函数,那么它就是凸优化问题。
凸优化的优点:
- 任何局部最小值都是全局最小值。
- 求解算法(如梯度下降、牛顿法)通常能保证收敛到全局最优解。
- 求解效率通常较高。

一个重要的子类:凸二次优化
形式为:最小化 f₀(x) = ½ xᵀ A x - xᵀ b,其中 A 是一个对称半正定矩阵。
- 其一阶最优性条件简化为求解线性系统:
A x = b。 - 许多图形学问题(如网格简化中的二次误差度量、最小二乘拟合)都可以归结为此类问题。
求解算法:梯度下降与牛顿法

对于一般(非凸)优化问题,没有放之四海而皆准的最佳求解器。但有两种基础且重要的迭代算法:

-
梯度下降法:
- 思想:沿着当前点梯度
∇f₀(x)的反方向(即最速下降方向)移动一小步,以减小目标函数值。 - 更新公式:
x_{k+1} = x_k - τ ∇f₀(x_k),其中τ是步长(学习率)。 - 优点:简单,每次迭代计算量小。
- 缺点:在“狭长山谷”形函数中收敛缓慢,容易产生锯齿形路径。步长
τ的选择至关重要,太小则收敛慢,太大可能发散。
- 思想:沿着当前点梯度
-
牛顿法:
- 思想:不仅使用梯度信息,还使用 Hessian 矩阵
∇²f₀(x)提供的曲率信息。它通过乘以 Hessian 的逆来“拉伸”空间,使局部地形更接*圆形,从而更直接地指向最小值。 - 更新公式:
x_{k+1} = x_k - τ [∇²f₀(x_k)]⁻¹ ∇f₀(x_k)。 - 优点:在最小值附*收敛速度非常快(二次收敛)。
- 缺点:每次迭代需要计算并求逆 Hessian 矩阵,计算量大;对于非凸问题,Hessian 可能不是正定的,导致算法行为不稳定。
- 思想:不仅使用梯度信息,还使用 Hessian 矩阵
实践建议:在实际图形学问题中,你需要根据具体问题的结构(规模、光滑性、凸性)来尝试和选择合适的求解器(如 L-BFGS、共轭梯度法、信赖域方法等)。这是一个需要经验和实验的领域。
应用实例:动画中的逆运动学

逆运动学是优化在动画中的一个经典应用。其目标是:给定角色末端效应器(如手)需要到达的目标位置,自动计算出所有关节的角度。
- 正向运动学:已知关节角度,计算末端位置。对于一条简单的二维运动链,末端位置
p₂可以通过根节点位置p₀和关节角度θ₀, θ₁计算得出(例如,使用复数表示旋转):p₂ = p₀ + e^(iθ₀)u₀ + e^(i(θ₀+θ₁))u₁。 - 逆运动学问题:我们希望找到一组关节角度
θ,使得末端位置p_n(θ)尽可能接*目标位置p̃_n。 - 优化建模:将其定义为一个无约束(或带关节限制约束)的优化问题:
- 目标函数:最小化末端与目标之间的距离*方
f₀(θ) = ½ ||p̃_n - p_n(θ)||²。 - 求解:使用梯度下降等算法,计算目标函数关于关节角度
θ的梯度,并迭代更新θ,使末端逐渐靠*目标。
- 目标函数:最小化末端与目标之间的距离*方

通过这种方式,动画师只需指定角色手部要抓取的目标,算法就能自动生成整个身体协调运动的动画,大大减少了手动设置关键帧的工作量。
总结

本节课中,我们一起学习了优化这一在计算机图形学中至关重要的数学工具。我们从优化问题的基本定义和实例出发,探讨了离散与连续优化的区别,并介绍了优化问题的标准形式。我们分析了解的存在性条件以及判断最优解的一阶和二阶条件。特别地,我们重点介绍了凸优化这类易于求解的问题,以及凸二次优化与线性系统的紧密联系。在求解算法方面,我们讲解了基础的梯度下降法和更高效的牛顿法,并讨论了它们各自的优缺点。最后,我们通过逆运动学这一生动实例,展示了如何将角色动画问题转化为优化问题并求解,从而实现自动化的运动生成。掌握优化思想,将为你解决图形学中各种复杂的建模、动画和仿真问题提供强大的武器。
24:基于物理的动画和 PDE 🎬
在本节课中,我们将要学习如何通过求解偏微分方程来实现基于物理的动画。我们将从回顾优化和常微分方程开始,逐步引入包含空间导数的偏微分方程,并探讨其在模拟水、烟雾、布料等自然现象中的应用。课程将涵盖PDE的基本概念、分类、离散化方法以及数值求解策略,并通过简单的代码示例展示其强大的图形学应用。
回顾:从优化到常微分方程
上一节我们简要介绍了优化方法,其核心思想是通过数值下降法来最小化目标函数。我们选择一个初始猜测,然后沿着梯度下降方向迭代,期望最终得到一个良好的解。
如果我们回顾更早的课程,会发现梯度下降等策略本质上是常微分方程的例子。ODE只涉及时间导数,其基本形式可以描述一个函数随时间的变化率。
例如,要模拟一块石头在空中的弹道轨迹,我们可以说石头的位置 x 在任何时间 t 的二阶时间导数等于重力方向。这给出了一个隐式关系,要得到精确轨迹需要求解这个方程。
公式:x''(t) = g
引入偏微分方程
本节中,我们将在ODE的基础上增加空间导数,从而引入偏微分方程。PDE同时包含时间和空间导数,能够描述更丰富的自然现象。

例如,石头落入池塘后产生的涟漪,其高度 u 的变化可以用一个PDE来描述:高度的二阶时间导数等于其拉普拉斯算子(空间二阶导数的和)。


公式:∂²u/∂t² = ∇²u


这同样是一个隐式描述,我们需要通过求解这个方程才能看到水的实际运动。这正是我们今天要探讨的核心。
PDE求解的基本思路
求解ODE的基本策略是在每个时间步长上增加一点速度。如果我们知道当前状态 q_k 和速度函数 f(q),那么新状态可以通过以下方式获得:
公式:q_{k+1} = q_k + τ * f(q_k)
其中 τ 是一个小的时间步长。
求解PDE在思想上与此类似,我们仍然在时间上向前推进。区别在于,现在的速度函数将涉及对邻域值的加权组合。例如,在波动方程中,更新规则可能类似于将直接邻居的值相加,然后减去4倍自身的值。
代码示例(概念):
# 伪代码示例:基于网格的简单更新
for each grid point (i, j):
velocity[i, j] += τ * (neighbor_sum - 4 * self_value)
height[i, j] += τ * velocity[i, j]
尽管PDE求解有更多细节和技术,但这个简单的思路为我们提供了入门的基础。令人惊讶的是,非常简单的代码就能产生相当逼真的行为,这正是PDE在图形学中备受青睐的原因。
一个简单的波动模拟示例

以下是模拟池塘波纹的一个简化代码框架,它展示了PDE求解的核心循环:


代码示例:
# 初始化
grid_size = 128
u = np.zeros((grid_size, grid_size)) # 高度场
v = np.zeros((grid_size, grid_size)) # 速度场
tau = 0.01 # 时间步长
alpha = 0.99 # 阻尼因子
# 主循环
while True:
# 随机投掷“石头”
if frame_count % 100 == 0:
i, j = random_position()
u[i, j] = -1.0
# 更新速度(基于拉普拉斯*似)
for i in range(grid_size):
for j in range(grid_size):
left = u[(i-1)%N, j]
right = u[(i+1)%N, j]
bottom = u[i, (j-1)%N]
top = u[i, (j+1)%N]
laplacian = (left + right + bottom + top - 4*u[i, j]) / (h*h)
v[i, j] += tau * laplacian
v[i, j] *= alpha # 应用阻尼
# 更新高度
for i in range(grid_size):
for j in range(grid_size):
u[i, j] += tau * v[i, j]
# 显示结果(例如,渲染为高度图或图像)
display(u)
这段代码虽然简单,却能产生美丽的波动行为。当然,真实的图形学应用(如流体、烟雾模拟)会复杂得多,但核心原理是相通的。

PDE在图形学中的应用实例
通过求解更复杂的PDE,我们可以创造出各种令人惊叹的视觉效果。以下是几个例子:
- 流体模拟:模拟不同密度液体的相互作用,产生复杂的流动和混合效果。
- 烟雾模拟:使用涡度方法模拟出飘逸、真实的烟雾。
- 布料模拟:模拟布料在跌落、碰撞时的褶皱和弯曲。
- 弹性体模拟:模拟可变形物体(如橡胶)的拉伸、压缩和断裂。
- 头发模拟:将头发视为一维弹性体,模拟其摆动和扭曲。
- 粘弹性材料:模拟像面团一样被挤压后发生永久形变的材料。
- 物质点法:用于电影《冰雪奇缘》中的雪模拟,混合了粒子和网格方法。

这些应用表明,PDE已成为电影特效、游戏和交互式工具中不可或缺的技术。

PDE的基本概念与分类

要开发这些技术,我们需要回到基础,理解PDE究竟是什么。PDE求解的是一个同时依赖于时间和空间的函数 u(t, x),该函数通过其导数的关系被隐式定义。
我们可以用一些术语来描述PDE的特性,这些特性决定了问题的求解难度和计算方法:
- 线性 vs 非线性:如果方程中未知函数及其导数仅以线性方式组合(如相乘、相加),则为线性PDE,通常更容易求解。如果包含像 u * ∂u/∂x 这样的项,则为非线性PDE。
- 阶数:方程中出现的最高阶导数的阶数。例如,波动方程在时间和空间上都是二阶的。
尽管PDE种类繁多,但有几类基本的模型方程,理解它们有助于把握不同类型PDE的特性和求解方法:
- 拉普拉斯方程:
∇²u = 0。这是椭圆型方程的典型例子。直观上,它寻求在给定边界数据下的“最*滑”插值函数。 - 热方程:
∂u/∂t = ∇²u。这是抛物型方程的典型例子。它描述了热量(或类似物质)随时间扩散的过程。 - 波动方程:
∂²u/∂t² = ∇²u。这是双曲型方程的典型例子。它描述了波(如水波、声波)的传播。
一般来说,椭圆型问题最容易求解,抛物型次之,双曲型则更具挑战性。非线性高阶方程最为复杂,但在图形学中又非常常见。
空间离散化:拉格朗日 vs 欧拉视角
要数值求解PDE,我们需要将连续的时空导数转化为计算机可以计算的离散形式。这涉及时间离散化和空间离散化。
在空间离散化中,一个基本的区分是拉格朗日描述和欧拉描述:
- 拉格朗日视角:跟踪单个“质点”(如粒子)随时间的运动。这类似于点云,粒子可以自由移动,分辨率不固定。但计算空间导数时需要查找邻居,可能较慢。
- 欧拉视角:在空间固定位置(如网格点)上观察物理量如何随时间变化。计算规则、快速(易于缓存和矢量化),但模拟被限制在网格范围内,且可能因分辨率不足而产生数值耗散。
现代方法常常混合使用这两种视角,例如FLIP流体模拟方法和物质点法,以结合两者的优点。
核心算子:拉普拉斯算子
我们看到的模型方程都包含了拉普拉斯算子 ∇²。它在物理、几何和图形学中极其重要。
拉普拉斯算子是一个微分算子,输入一个函数,输出其某种二阶导数。对于函数 u: ℝⁿ → ℝ,其拉普拉斯算子定义为:
公式:∇²u = ∂²u/∂x₁² + ∂²u/∂x₂² + ... + ∂²u/∂xₙ²
更直观地,拉普拉斯算子衡量了一个点的函数值与其邻域*均值的偏差。在波动方程中,如果一个点的水面高度远高于周围,拉普拉斯值就大,意味着该点将快速运动;如果水面*坦,拉普拉斯值为零,没有运动。
数值离散化:有限差分法
我们需要将拉普拉斯算子这样的连续算子离散化。有限差分法是用于规则网格的一种自然方法。
- 一阶导数*似:
∂u/∂x ≈ (u_{i+1} - u_i) / h - 二阶导数*似:
∂²u/∂x² ≈ (u_{i+1} - 2u_i + u_{i-1}) / h²
将其推广到二维网格,拉普拉斯算子的一个常见离散*似为:
公式:∇²u_{i,j} ≈ (u_{i-1,j} + u_{i+1,j} + u_{i,j-1} + u_{i,j+1} - 4u_{i,j}) / h²
对于三角形网格,则有著名的余切公式来计算拉普拉斯算子,它适用于非规则网格。
边界条件
在定义PDE问题时,我们必须指定边界条件,即解在求解区域边界上的行为。两种最基本的类型是:
- 狄利克雷边界条件:直接指定边界上的函数值 u = a。
- 诺伊曼边界条件:指定边界上函数法向导数的值 ∂u/∂n = b。
边界条件必须与PDE本身相容,否则问题可能无解。例如,对于拉普拉斯方程,诺伊曼边界条件必须满足净通量为零(即所有边界法向导数的积分和为零),否则内部无处安放这些“源”或“汇”。
重要提示:数值求解器可能不会自动检查边界条件的相容性。因此,在调用求解器后,计算残差(b - A x)进行验证是一个好习惯。
求解模型方程
现在我们可以尝试求解三个模型方程。
-
拉普拉斯方程
∇²u = 0- 离散化后,方程要求每个网格点的值等于其邻居的*均值。
- 这可以转化为一个大型的稀疏线性系统
A x = 0(或A x = b,如果包含非零边界条件)。 - 应使用高效的稀疏线性求解器(如Eigen、SuiteSparse)来求解,而不是使用收敛很慢的雅可比迭代法。
-
热方程
∂u/∂t = ∇²u- 结合时间离散化(如向前欧拉法)和空间离散化(有限差分拉普拉斯)。
- 更新公式为:
u_{k+1} = u_k + τ * ∇² u_k - 这允许我们观察热量从边界扩散到内部的过程。
-
波动方程
∂²u/∂t² = ∇²u- 有两种常见处理方式:
- 降阶法:引入速度变量
v = ∂u/∂t,将二阶方程转化为两个一阶方程组:∂u/∂t = v和∂v/∂t = ∇²u。 - 中心差分法:直接对时间二阶导数使用有限差分:
(u_{k+1} - 2u_k + u_{k-1})/τ² = ∇² u_k,然后求解u_{k+1}。
- 降阶法:引入速度变量
- 需要初始条件(初始位移和初始速度,或前两个时间步的状态)。
- 有两种常见处理方式:

这些原理可以很容易地移植到不同的几何表示(如网格、点云)上,只需替换相应的拉普拉斯算子离散化公式即可。这种基于PDE的算法设计思想,使得图形学算法能够更通用地应用于不同场景。
总结与展望


本节课中,我们一起学习了基于物理的动画与偏微分方程的基础知识。我们从ODE出发,引入了包含空间导数的PDE,并了解了其在模拟水、烟、布料等现象中的强大能力。我们探讨了PDE的分类(椭圆、抛物、双曲)、空间离散化的不同视角(拉格朗日 vs 欧拉)、核心的拉普拉斯算子、有限差分离散化方法、边界条件的重要性,以及如何数值求解几个经典的模型方程。

通过简单的代码示例,我们看到即使基础的PDE求解也能产生迷人的视觉效果。而更复杂的模拟,则建立在深入理解计算物理、应用数学和几何学的基础之上。PDE已成为现代计算机图形学中创造逼真动态世界的核心工具,从电影特效到实时交互应用,其影响力日益增长。鼓励有兴趣深入该领域的同学,去探索相关学科更丰富的文献和知识。
25:学生创作展示 🎨









在本节课中,我们将一起观看并学习CMU 15-462课程的学生创作展示。本节不涉及新的技术概念,而是通过欣赏学生们的作品,来回顾和感受计算机图形学技术的实际应用与创意表达。





上一节我们介绍了课程的技术核心,本节中我们来看看学生们如何运用这些知识进行创作。






学生们的项目展示了计算机图形学多个领域的应用,包括但不限于光线追踪、几何处理、动画和模拟。





以下是部分学生作品的简要介绍:





- 项目A:一个基于物理的光线追踪渲染器,实现了全局光照和软阴影。
- 核心算法涉及渲染方程:
L_o(p, ω_o) = L_e(p, ω_o) + ∫_Ω f_r(p, ω_i, ω_o) L_i(p, ω_i) (n·ω_i) dω_i
- 核心算法涉及渲染方程:
- 项目B:使用细分曲面技术生成的复杂有机体模型动画。
- 项目C:一个实时流体模拟系统,模拟了水的流动与交互。
- 项目D:利用运动捕捉数据驱动的三维角色动画。
- 项目E:实现了一种新的非真实感渲染风格,将3D场景转化为手绘风格图像。






这些作品表明,掌握了图形学的基础原理和编程技能后,可以创造出极具视觉冲击力和技术深度的内容。





本节课中我们一起观看了CMU学生的图形学创作展示。这些作品生动地体现了将理论知识转化为实践成果的过程,为我们的学习提供了宝贵的灵感和参考。

浙公网安备 33010602011771号