MIT-6-837-计算机图形学笔记-全-

MIT 6.837 计算机图形学笔记(全)

001:课程介绍与概述

在本节课中,我们将学习麻省理工学院6.837计算机图形学课程的基本框架、课程结构以及计算机图形学领域的核心概念和应用。

课程基本信息

我是你们的讲师Justin Solomon,是EECS和CSAIL的教授。本课程是麻省理工学院的计算机图形学导论课程。

我们拥有四名助教。课程的所有信息、作业和成绩将通过Canvas平台发布。我们还将使用Piazza论坛进行课程相关的交流和答疑,建议所有问题都在Piazza上提出,以便获得更快的回复。

课程日历将通过一个链接到Canvas的Google表格管理,其中包含了所有讲座日期、主题、作业截止日期、测验日期等信息。

考虑到当前的特殊情况,课程将尝试进行混合式教学。讲座将通过YouTube进行直播,但直播可能出现技术故障。如果直播失败,学生仍需对课程内容负责。此外,我们还将使用Slack频道发布每堂课的直播链接并收集问题。

课程评分与作业

课程评分结构如下:

  • 作业:共5次计分作业,大约每两周一次。这些作业将引导你实现视频游戏流水线和光线追踪的核心组件。
  • **期末项目**:一个开放式的项目,允许你探索课程未涵盖的、与图形学相关的进阶主题。
    
  • 期中测验:安排在退课截止日期之前。
  • 微型测验:旨在确保你掌握课程内容,你将有约24小时的时间窗口完成。

以下是关于作业和协作的具体说明:

  • 所有主要作业都需要提交代码,仅提交可执行文件无效。
  • 鼓励与同学和课程工作人员讨论,但最终必须独立编写并提交自己的代码。
  • 期末项目可以两人一组完成。
  • 课程提供3个“迟交日”,无需解释即可延迟提交作业24小时。超过后,作业每天扣减25%的分数。

所需背景与预备知识

课程作业将使用C++和GLOW库实现。GLOW是基于OpenGL的面向对象图形库。

我们不强求严格的先修课程,但你需要自行评估风险。课程将安排以下复习环节:

  • C++复习课:9月10日,可选参加。
  • GLOW库介绍课:强烈建议参加,以熟悉作业使用的图形库。
  • 微积分与线性代数复习课:9月20日。

课程涉及大量数学,但主要集中在三维和四维矩阵的操作上。我们还将介绍齐次坐标、常微分方程模拟等概念,并简要讨论傅里叶变换和抗锯齿。

什么是计算机图形学?

计算机图形学是一个由多种主题融合而成的领域。这意味着课程将涵盖大量不同的主题,有些你可能非常感兴趣,有些则可能不然。但好处是,大部分讲座内容相对独立。

计算机图形学技术已无处不在。当你想到计算机图形学时,可能会联想到以下应用:

  • 电影中的计算机生成图像和特效
  • 电子游戏
  • 计算机辅助设计
  • 3D打印
  • 模拟
  • 增强现实与虚拟现实
  • 科学可视化
  • 医学成像

本质上,图形学技术是关于如何将场景的数字描述最终转化为你在屏幕上看到的图像。这个过程涉及许多环节。

课程内容概览

本课程将按照图形学流水线的顺序组织内容,从起点到终点。

首先,我们需要对场景进行建模。这涉及到如何在计算机上表示和存储物体形状。不同的形状表示法适用于艺术家或计算机等不同需求。

接着,在有了物体的物理形状后,我们需要定义它们由何种材料制成。我们将讨论光线与物体交互的物理原理,以描述材质并最终在屏幕上绘制它们。

然后,我们需要让物体动起来。动画有多种类型:

  • 关键帧动画:艺术家绘制物体运动的路径。
  • 蒙皮:通过控制数字角色的骨骼,并让皮肤随之变形的技术。
  • 基于物理的动画:通过模拟物理定律来产生运动。

最后,是渲染环节,即将所有数字描述转化为视觉内容。我们将重点介绍两种代表性的渲染算法:

  • 光线追踪:准确但较慢的算法。
  • 光栅化:用于显卡的快速实时渲染算法。

现代图形流水线常常结合使用这两种技术。

详细课程大纲

以下是本学期将涵盖主题的详细预览:

建模与变换
我们将讨论多种表示场景的方法,包括三角网格曲面和样条。例如,皮克斯短片《杰瑞的游戏》中的角色是细分曲面技术的早期测试案例。我们还将涵盖变换,包括物体刚体运动所需的变换堆栈,以及用于实现透视效果的齐次坐标。

动画
我们将从关键帧动画开始讨论,它本质上是刚体运动空间中的一条曲线。此外,还将介绍蒙皮技术,以及基于物理的动画,例如你将实现的粒子系统,它可以模拟布料、爆炸等效果。

渲染
我们将深入探讨渲染。光线投射/追踪的基本算法伪代码如下:

for 每个像素 in 图像:
    for 每个物体 in 场景:
        找到离该像素最近的物体
    绘制该物体

此算法简单但可能非常慢,尤其是处理反射、光照等需要递归追踪光线的情况。因此,我们将讨论空间分割树等数据结构来加速光线追踪。

纹理与采样
我们将讨论如何将2D图像包裹到3D模型上以增加细节。同时,也会探讨采样和抗锯齿问题,即如何将连续的真实世界场景转换为离散的像素网格。

高级光照与实时图形
我们将回到光线追踪,讨论全局光照技术,以模拟光线在场景中多次反弹的效果。对于实时图形,我们将介绍光栅化算法。它与光线追踪的主要区别在于循环顺序:光线追踪遍历每个像素寻找物体,而光栅化遍历每个物体并绘制它。实时图形流水线包含许多巧妙的技巧,如视锥体裁剪等。

其他主题
课程最后将涵盖一些其他重要主题,如色彩感知与表示、显示技术以及GPU的基本原理。

作业预览

课程作业将紧密跟随上述内容展开:

  • 作业0:不计分,但强烈建议完成,用于设置C++和GLOW开发环境。
  • 作业1:曲线与曲面建模,实现多项式曲线和曲面的基本规则。
  • 作业2:层次模型,实现基本的蒙皮技术。
  • 作业3:物理模拟,模拟一块布料的运动。
  • 作业4:实现一个光线追踪器。
  • 作业5:实时图形,使用OpenGL编写着色器,实现一个包含动态光影的3D场景。

总结

本节课我们一起学习了6.837计算机图形学课程的基本信息、评分结构、所需背景,并对计算机图形学领域及其广泛应用进行了概述。我们预览了本学期的课程大纲,它将按照图形学流水线组织,涵盖建模、动画、渲染等核心主题。最后,我们了解了与这些主题对应的系列编程作业,这些作业将帮助你从零开始构建图形系统的关键组件。

请留意关于复习课地点的通知。如有任何问题,可以通过Piazza、Slack等渠道提出。希望能在下节课见到大家。

003:曲线与曲面

在本节课中,我们将要学习如何表示和连接曲线,并进一步扩展到曲面。我们将从回顾贝塞尔曲线开始,探讨如何将多段曲线平滑地连接成样条,然后介绍B样条基函数。最后,我们将学习如何将曲线表示扩展到曲面,包括张量积曲面和细分曲面。

回顾:贝塞尔曲线与德卡斯特里奥算法

上一节我们介绍了贝塞尔曲线和德卡斯特里奥细分算法。本节中,我们来看看如何将这些曲线连接起来。

贝塞尔曲线由一组控制点定义。对于一个三次贝塞尔曲线,我们有四个控制点:P0, P1, P2, P3。曲线由伯恩斯坦多项式混合这些点得到:

P(t) = (1-t)³ P0 + 3(1-t)²t P1 + 3(1-t)t² P2 + t³ P3,其中 t ∈ [0, 1]

德卡斯特里奥算法提供了一种递归细分曲线的方法:将每条控制多边形线段对半分,连接中点,重复此过程,最终得到曲线上的点。

从曲线到样条

到目前为止,我们绘制的单条三次曲线形状有限。为了绘制更复杂的形状(例如一个复杂的数学图形),我们需要将多条曲线段连接起来。这种分段多项式曲线称为样条

样条这个术语源自造船和木工。工匠们使用细长的柔性木条(样条),通过在其上固定钉子(节点)来绘制平滑曲线。有趣的是,物理样条在钉子之间形成的形状在数学上确实是分段三次的。

我们的目标是理解如何将两条曲线段在连接点处平滑地“粘合”在一起。这需要我们理解曲线的微分性质。

曲线的连续性:Cⁿ 与 Gⁿ

为了描述两条曲线连接的光滑程度,我们需要定义连续性。这里有两个主要概念:参数连续性(Cⁿ)和几何连续性(Gⁿ)。

  • C0 / G0 连续性:曲线在连接点处位置连续,没有跳跃。
  • C1 连续性:曲线在连接点处的一阶导数(速度向量)连续。这意味着参数变化的速度和方向都匹配。
  • G1 连续性(切线连续性):曲线在连接点处的切线方向连续,但速度的大小可以不同。这通过匹配单位切向量 T = P‘ / ||P‘|| 来实现。
  • C2 连续性:曲线在连接点处的二阶导数连续。
  • G2 连续性(曲率连续性):曲线在连接点处的曲率连续。曲率向量 κ 是单位切向量 T 对弧长的导数,它总是垂直于 T

关键点在于:参数连续性(Cⁿ)关注参数 t 的函数本身是否光滑;几何连续性(Gⁿ)关注曲线在空间中的形状是否光滑。一条参数光滑的曲线可能在空间中有一个尖点(例如 γ(t) = (t², t³)),而一条看起来光滑的曲线(如抛物线)可能由参数不连续的两段构成。

以下是不同连续性级别的直观对比:

  • C0:曲线相连,但可能有尖角。
  • G1:曲线平滑相连,切线方向相同。
  • C1:曲线平滑相连,且切线方向与大小都相同,过渡更均匀。

连接贝塞尔曲线

现在,我们可以将这些连续性条件转化为对贝塞尔曲线控制点的约束。假设我们有两段三次贝塞尔曲线,第一段的最后一点是第二段的起点。

  • C0 连续性:只需共享一个控制点。
  • G1 连续性:共享控制点,并且连接点前后的两个控制线段(即第一段的 P2->P3 和第二段的 Q1->Q2)必须方向平行
  • C1 连续性:共享控制点,并且连接点前后的两个控制向量(即 P3 - P2 和 Q1 - Q0)必须完全相等(包括方向和长度)。

更高阶的连续性(C2/G2)条件涉及更多控制点,且更为复杂。

虽然这种方法可行,但从编辑角度看并不理想。移动一个控制点可能会破坏连接点的连续性,迫使我们必须调整相邻曲线的控制点来维持约束,这很繁琐。

另一种基:B样条

为了解决连接问题,我们引入另一种表示三次曲线的基函数:三次B样条基。它也是由四个控制点定义一段曲线,但其设计使得连接多段曲线变得非常容易。

B样条的关键特性是:要获得下一段曲线,你只需“滑动窗口”——去掉第一个控制点,加入下一个新控制点。通过这种构造,只要相邻曲线段共享三个控制点(按顺序),它们就会自动以 C2 连续性 连接。

然而,B样条曲线有一个显著特点:它不插值(不经过)其控制点,而是被控制在控制点的凸包内部。这使得直接几何控制不如贝塞尔曲线直观。

贝塞尔曲线和B样条曲线只是同一组三次多项式函数的不同基表示。因此,我们可以通过一个矩阵变换在它们之间进行转换。这在图形学中很常见,我们可以根据任务需求选择最方便的表示形式。

从曲线到曲面

掌握了曲线之后,我们自然要扩展到曲面。在图形学中,有多种表示曲面的方法:

  • 三角网格
  • 张量积样条
  • 细分曲面
  • 隐式曲面
  • 程序化生成

张量积贝塞尔曲面

一种直接的方法是将曲线构造推广到曲面。我们可以创建一个双三次贝塞尔曲面片

设想我们有一条以 u 为参数的三次贝塞尔曲线,但其四个控制点本身又是另一条以 v 为参数的贝塞尔曲线。当我们让 uv 都在 [0, 1] 范围内变化时,就扫掠出了一个曲面片。

数学上,曲面由16个控制点构成的网格定义:
P(u, v) = Σᵢ ΣⱰ Bᵢ(u) BⱰ(v) PᵢⱰ,其中 B 是三次伯恩斯坦多项式。

这个曲面片在 uv 为常数的每个截面上都是一条贝塞尔曲线。这种构造称为张量积

著名的犹他茶壶就是由许多这样的贝塞尔曲面片拼接而成的。然而,确保曲面片之间高阶(如C1)的连续性比曲线要复杂得多,因为需要整条边界上的导数都匹配。

细分曲面

另一种强大的曲面表示方法是细分曲面。我们从粗糙的“控制网格”(可以是三角形或四边形网格)开始,然后反复应用一个细分规则。这个规则定义了如何生成新的、更密的顶点和面,并计算它们的位置(通常是邻域顶点的加权平均),最终收敛到一个光滑曲面。

两个著名的细分规则是:

  1. Loop细分:用于三角形网格。将每条边一分为二,每个三角形被细分为四个小三角形。新顶点的位置由邻近旧顶点的加权平均决定。
  2. Catmull-Clark细分:可用于任意多边形网格。在每条边的中点(边点)和每个面的中心(面点)插入新顶点,然后连接它们以形成新的四边形为主的网格。

细分曲面的优点包括:能处理任意拓扑结构、生成光滑曲面、支持动态细节层次(LOD)。缺点是在奇异点(如价不为6的顶点)处的连续性可能降低,且难以获得显式的参数方程。

其他曲面表示

我们简要提及其他表示:

  • 隐式曲面:由方程 F(x, y, z) = 0 定义的点集构成。非常适合处理拓扑变化(如流体融合)和内外测试,但采样和可视化可能更复杂。

总结

本节课中我们一起学习了计算机图形学中曲线与曲面的核心表示方法。

我们首先回顾了贝塞尔曲线,并深入探讨了如何通过定义参数连续性(Cⁿ)和几何连续性(Gⁿ)来将多条曲线平滑连接成样条。为了更便捷地实现高阶连续连接,我们引入了B样条基函数。

接着,我们将曲线概念扩展到曲面。我们学习了通过张量积构造从贝塞尔曲线生成贝塞尔曲面片的方法,并了解了其优缺点。然后,我们介绍了强大的细分曲面技术,它通过从粗糙控制网格递归应用规则来生成光滑曲面,是生产中的常用工具。最后,我们简要概述了隐式曲面等其他表示方法。

掌握了这些几何表示的基础后,下一步我们将学习如何对这些形状进行变换(如移动、旋转、缩放),并将它们组合到场景中,这是图形管线后续阶段的关键。

004:变换

在本节课中,我们将学习计算机图形学中一个核心概念:变换。我们将系统地探讨如何表示和处理点、向量以及坐标系统,并引入齐次坐标这一强大工具来处理平移和相机投影等非线性操作。通过建立清晰的数学符号和概念,我们将为后续的层次化建模打下坚实基础。

向量空间回顾

上一节我们介绍了图形学中的数学基础。本节中,我们来看看如何用线性代数的语言精确描述几何对象。

首先,我们回顾向量空间的概念。一个向量空间包含一组向量,并定义了向量加法和标量乘法两种运算。零向量是向量空间中的一个特殊元素。

在有限维向量空间中,我们可以选择一组基向量。任何向量都可以表示为这些基向量的线性组合。

公式v = Σ (c_i * b_i),其中 c_i 是标量系数,b_i 是基向量。

我们可以将基向量排列成矩阵 B,将系数排列成列向量 c,从而将上述关系写成矩阵形式。

公式v = B * c

这里,B 的列是基向量,c 是坐标向量。我们引入符号约定:带箭头的字母(如 v)表示几何向量,粗体字母(如 c)表示坐标(数字列向量),大写字母(如 B)表示由向量组成的矩阵。

线性变换

理解了向量的表示后,我们来看看如何对它们进行变换。线性变换是满足以下两个条件的映射 L

  1. L(v1 + v2) = L(v1) + L(v2)
  2. L(α * v) = α * L(v)

一个重要的性质是,线性变换将零向量映射为零向量:L(0) = 0

对于有限维空间,线性变换 L 完全由它作用于一组基向量的结果决定。如果我们知道 L(b_i) 在基 B 下的坐标,就可以将这些坐标排列成一个矩阵 M

公式L(B) = B * M,其中 M 的列是 L(b_i) 在基 B 下的坐标。

因此,对一个坐标为 c 的向量 v 应用线性变换 L,等价于用矩阵 M 乘以其坐标。

公式L(v) = L(B * c) = B * (M * c)

这里有一个关键的双重解释:

  • (B * M) * c:可以理解为坐标 c 保持不变,但基从 B 变成了新的基 B' = B * M(改变坐标系)。
  • B * (M * c):可以理解为基 B 保持不变,但向量的坐标从 c 变成了 M * c(变换物体本身)。

这两种视角是等价的,取决于我们如何加括号。这种理解对于图形学中管理多个坐标系至关重要。

从向量到点:仿射空间

然而,线性变换有一个局限:它无法表示平移,因为平移不会将零向量映射到自身。平移作用于点,而非向量。

因此,我们需要引入仿射空间的概念。一个仿射空间由一个向量空间加上一个原点构成。在仿射空间中,我们可以明确区分(位置)和向量(位移)。

我们扩展之前的符号:用带波浪线的字母(如 )表示点。一个仿射坐标系由一个原点 和一组基向量 b_i 定义。

一个点 可以表示为原点加上一个位移向量。

公式p̃ = õ + Σ (c_i * b_i)

为了使用矩阵运算的便利性,我们引入齐次坐标的雏形。我们将点和向量都用四个坐标表示,但最后一位不同:

  • 点:(c1, c2, c3, 1)
  • 向量:(c1, c2, c3, 0)

这样,点与向量的运算就有了清晰的几何意义:

  • 点 - 点 = 向量
  • 点 + 向量 = 点
  • 向量 + 向量 = 向量

仿射变换

现在,我们可以用矩阵来表示包含平移在内的所有仿射变换(旋转、缩放、错切、平移)。一个4x4的仿射变换矩阵 M_affine 具有以下分块结构:

公式

M_affine = [ A    t ]
           [ 0^T  1 ]

其中 A 是一个3x3的线性变换矩阵,t 是一个3x1的平移向量,0^T 是行向量 [0, 0, 0]

将这个矩阵乘以齐次坐标:

  • 作用于点 (x, y, z, 1):实现线性变换 A 和平移 t
  • 作用于向量 (x, y, z, 0):仅实现线性变换 A(平移部分被忽略)。

这完美地编码了我们的几何直觉。

变换的组合与顺序

在构建复杂场景时,我们需要组合多个变换。组合变换对应于矩阵乘法。

公式M_combined = M_n * ... * M_2 * M_1

关键警告:矩阵乘法不满足交换律。变换的顺序极其重要!

例如,“先缩放再平移”与“先平移再缩放”会产生完全不同的结果。在代码中,必须仔细确认矩阵相乘的顺序,这通常通过一个矩阵堆栈来管理。

齐次坐标与透视投影

我们之前用第四坐标的0或1来区分点和向量。现在,我们赋予它更强大的功能,引入完整的齐次坐标概念。

在齐次坐标中,我们使用n+1维坐标表示n维空间中的点。例如,3D点用4个坐标 (x, y, z, w) 表示。关键规则是:所有非零标量 k 乘以齐次坐标 (x, y, z, w) 得到的 (kx, ky, kz, kw) 表示同一个点。

w ≠ 0 时,可以通过除以 w 得到对应的普通(非齐次)3D坐标:(x/w, y/w, z/w)。当 w = 0 时,它表示一个“方向”或“无穷远点”,对应于向量。

齐次坐标最大的威力在于它能将透视投影——一种非线性变换——表示为线性矩阵乘法。

考虑一个简单的透视:将3D点 (x, y, z) 投影到距离相机1个单位远的图像平面上,投影后的2D位置是 (x/z, y/z)。这个除以 z 的操作是非线性的。

然而,在齐次坐标下,我们可以这样操作:

  1. 将3D点表示为 (x, y, z, 1)
  2. 用一个简单的矩阵乘法得到 (x, y, z, z)
  3. 当我们将这个齐次坐标转换回非齐次坐标时,自动执行了除法:(x/z, y/z, z/z) = (x/z, y/z, 1)。前两个坐标就是投影后的图像平面坐标。

透视投影矩阵(简化版)

[1 0 0 0]
[0 1 0 0]
[0 0 1 0]
[0 0 1 0] // 这个矩阵将 (x, y, z, 1) 变换为 (x, y, z, z)

通过这种方式,复杂的、非线性的相机透视变换被优雅地整合进了我们的线性(齐次坐标下)变换框架中。

总结

本节课中我们一起学习了计算机图形学中变换的数学基础。

我们首先回顾了向量空间和线性变换,并强调了明确记录坐标系(基矩阵 B)的重要性。接着,我们引入了仿射空间来区分点和向量,并使用齐次坐标(第四坐标为0或1)来统一表示它们。这允许我们用4x4矩阵表示包括平移在内的所有仿射变换。

我们讨论了变换组合通过矩阵乘法实现,并强调了顺序的重要性。最后,我们探索了齐次坐标更一般的定义(允许第四坐标为任意值),并展示了其核心应用:将非线性透视投影变换表示为线性矩阵运算,这是实现3D到2D渲染的关键。

这些概念和工具为我们下一讲构建层次化场景模型和数据结构奠定了坚实的数学与概念基础。

005:层次建模 🏗️

在本节课中,我们将学习如何将多个变换组合在一起,构建复杂的3D场景。我们将从回顾变换的基础知识开始,然后深入探讨如何通过“运动链”和“场景图”来组织和管理这些变换,从而实现层次化建模。


回顾:变换与坐标系统

上一节我们介绍了单个变换(如旋转、平移)及其矩阵表示。本节中,我们来看看如何将这些变换组合起来。

在计算机图形学中,我们经常使用矩阵来表示从一个坐标系到另一个坐标系的变换。我们使用记号 M_{ij} 来表示一个矩阵,它将坐标系 j 中的点或向量变换到坐标系 i 中。

例如,一个平移矩阵在齐次坐标下是3x3的矩阵(2D情况),其形式通常为:

[ 1, 0, p ]
[ 0, 1, q ]
[ 0, 0, 1 ]

其中 (p, q) 是平移量。旋转矩阵则使用余弦和正弦函数来填充左上角的2x2子矩阵。

一个实用的技巧是:矩阵的每一列代表了该矩阵对标准基向量的作用。例如,要推导一个旋转45度的矩阵,我们可以计算它对基向量 (1, 0)(0, 1) 的作用,然后将结果作为矩阵的列。


运动链与正向运动学 🤖

当我们构建一个复杂模型(如机器人手臂或动画角色)时,各个部件通过关节连接。这种连接关系形成了一个“运动链”。

正向运动学 是指:当我们知道所有关节的角度(或位移)参数时,可以通过连续应用这些关节的变换矩阵,计算出末端执行器(例如机器人的手)在世界坐标系中的位置。

用公式表示,如果我们有一个包含所有关节参数的向量 θ,末端执行器的位置 E 可以表示为:
E = F(θ)
其中 F 是一个复杂的非线性函数,它是一系列矩阵乘法的结果。

以下是正向运动学的一个简单示例步骤:

  1. 从末端(如手部)的局部坐标系开始。
  2. 依次乘以将手部坐标系变换到前臂、上臂、躯干坐标系的矩阵。
  3. 最终得到手部在身体(或世界)坐标系中的位置。

这个过程在概念上很简单,但在编码时需要特别注意矩阵乘法的顺序和各个变换的符号。


逆向运动学及其挑战 🔄

然而,对于动画师来说,正向运动学并不直观。动画师更关心的是“我想让角色的手放在这里”,而不是“我需要将肘关节旋转多少度”。这就是 逆向运动学 要解决的问题。

逆向运动学试图解决相反的问题:给定末端执行器期望的位置 E,求解出所需的关节参数 θ。即,寻找函数 G,使得 θ = G(E)

逆向运动学面临几个主要挑战:

  • 解可能不存在:例如,目标点超出了机械臂的可达范围。
  • 解不唯一:可能存在多种关节配置都能让手到达同一点。
  • 求解困难:函数 F 高度非线性,直接求逆非常复杂。

一种常见的求解方法是牛顿法,它通过迭代线性化来逼近解。其核心思想是,在当前位置 θ_k 对函数 F 进行线性近似:
E ≈ J(θ_k) * (θ - θ_k) + F(θ_k)
其中 JF 的雅可比矩阵(一阶导数矩阵)。然后求解这个线性方程组得到一个新的估计值 θ_{k+1},并不断迭代。然而,牛顿法严重依赖于初始猜测,且不一定总能收敛。


场景图:组织复杂场景 🌳

为了管理整个3D场景中所有对象的变换关系,我们使用一种称为 场景图 的数据结构。场景图通常是一棵树(或是有向无环图),其中每个节点代表一个变换或一个可渲染的对象。

场景图的工作原理如下:

  • 根节点 通常代表世界坐标系。
  • 内部节点 代表变换(如平移、旋转),它们定义了其子节点所在的坐标系。
  • 叶节点 代表实际的几何物体(如三角形网格)。

渲染时,我们采用深度优先遍历算法。在遍历过程中,我们维护一个 矩阵栈

  1. 当向下遍历到一个变换节点时,我们将当前变换矩阵乘以该节点的变换矩阵,并将结果压入栈顶。这个栈顶矩阵代表了从当前节点坐标系到世界坐标系的累积变换。
  2. 渲染该节点的所有子几何体时,使用栈顶矩阵进行变换。
  3. 当向上回溯时,我们将栈顶矩阵弹出,恢复到父节点的变换状态。

这种方法避免了直接计算和存储逆矩阵,更加高效和稳定。许多图形库(如旧版OpenGL)都内置了这种矩阵栈机制。

场景图还可以推广为有向无环图,以支持 实例化。例如,同一个“灌木”模型可以被多个不同的变换节点引用,从而在场景中多次出现,这节省了内存并简化了场景描述。


法向量的变换 🧭

最后,我们讨论一个重要的细节:如何正确变换曲面的法向量。当我们用一个矩阵 M 变换一个物体(及其顶点)时,不能直接用 M 去变换法向量,否则会导致光照错误。

原因:法向量需要始终保持与变换后的曲面垂直。而 M 可能包含非均匀缩放或剪切,这会破坏垂直关系。

正确方法:假设矩阵 M 将点从物体空间变换到世界空间。那么,对应于物体空间法向量 n_obj 的世界空间法向量 n_world 应为:
n_world = (M{-1})T * n_obj
然后通常需要将其重新归一化为单位长度。

推导简述:法向量 n 与曲面上的任意切向量 v 垂直,即 n^T · v = 0。通过将恒等式 I = M^{-1} M 插入这个点积公式,并利用矩阵乘法的转置性质,可以推导出上述变换规则。

一个特例是:如果 M 只是一个旋转矩阵(正交矩阵),那么 M^{-1} = M^T,因此 (M{-1})T = M。这意味着对于纯旋转,你可以直接用 M 来变换法向量。


总结 📚

本节课中我们一起学习了层次建模的核心概念:

  1. 正向运动学:通过已知的关节参数计算末端位置,是构建运动链的基础。
  2. 逆向运动学:根据期望的末端位置求解关节参数,这是一个更具挑战性但更符合动画师直觉的问题。
  3. 场景图:一种树状或图状的数据结构,用于高效地组织和管理复杂3D场景中所有对象的层次变换关系。
  4. 法向量变换:变换几何体时,必须使用变换矩阵的逆转置来变换法向量,以保证光照计算的正确性。

掌握这些概念,你将能够构建并操纵复杂的、具有层次结构的3D模型和场景,这是计算机图形学中角色动画和场景构建的基石。

006:动画与蒙皮

在本节课中,我们将学习计算机动画的基础知识,特别是如何让静态的3D角色动起来。我们将从传统手绘动画的历史讲起,了解关键帧等概念如何被计算机图形学所继承和发展。最后,我们将深入探讨一种核心的动画技术——蒙皮,学习如何通过骨骼的运动来驱动角色网格的形变。

从手绘动画到计算机动画

上一节我们介绍了场景变换和线性代数在图形学中的应用。本节中,我们来看看如何让场景“动”起来,即计算机动画。

计算机动画的许多技术都源于传统手绘动画。在20世纪早期,动画师需要逐帧绘制每一幅画面,这是一个极其繁琐的过程。为了提高效率,动画工作室发展出了“关键帧”和“中间画”的工作流程。

以下是传统动画中的几个核心概念及其在计算机图形学中的对应:

  • 关键帧:由资深动画师绘制角色动作中的关键姿势。
  • 中间画:由其他动画师绘制关键帧之间的过渡帧,使动作流畅。
  • 赛璐珞动画:将背景和移动的角色分别绘制在不同的透明胶片上,避免重复绘制整个场景。

在计算机动画中,关键帧变成了样条曲线上的控制点,而中间画的生成则由计算机通过插值算法自动完成,这与我们之前讨论的样条曲线技术一脉相承。

此外,为了模拟摄像机的移动和景深效果,早期动画师还发明了如“多重透视全景图”和“多平面摄像机”等巧妙的机械装置。这些创新都体现了在计算机技术普及之前,人们对于创造动态、立体视觉效果的追求。

随着计算机技术的发展,动画制作从完全手绘转向数字化。像皮克斯这样的公司最初就是技术公司,为迪士尼开发早期的计算机图形软件和硬件。计算机不仅提高了制作效率,更重要的是,它彻底改变了艺术创作的形式,使得实现复杂的摄像机运动、物理模拟和逼真的特效成为可能。

计算机动画的类型

传统动画的原则为计算机动画奠定了基础。现在,我们来看看计算机动画中几种主要的技术类型。

关键帧动画是最基础的类型,它直接对应手绘动画的关键帧理念。动画师设定物体在关键时间点的姿态(如位置、旋转角度),计算机使用样条曲线在这些关键帧之间进行插值,生成平滑的动画。这里的样条曲线,其横轴是时间,纵轴可能是某个关节的角度或物体的坐标。

程序化动画是通过编写代码来生成动画。一个简单的例子是模拟时钟指针的转动:代码根据当前时间计算指针的角度。更复杂的应用包括生成大规模群组动画(如军队冲锋)或无限延伸的游戏场景,这些都不适合手动逐帧制作。

基于物理的动画利用物理定律来模拟物体的运动,常用于生成那些繁琐或难以手动控制的次级运动,例如旗帜飘扬、头发晃动或流体模拟。在电子游戏中,物理引擎需要在实时性和模拟精度之间做出权衡;而在电影中,则可以花费数小时来计算一帧画面,以达到极高的视觉保真度。

近年来,数据驱动的方法,如运动捕捉和机器学习,也被广泛应用于动画制作。运动捕捉通过记录真人演员的动作数据,将其映射到虚拟角色上,可以快速生成逼真的动画。而强化学习等技术则能训练虚拟角色自主完成复杂的动作,如后空翻。

角色动画流程:建模、绑定与蒙皮

在了解了动画的多种类型后,我们聚焦于角色动画。要让一个3D角色动起来,通常需要三个主要步骤:建模、绑定和动画。

上一节我们介绍了正向运动学和逆向运动学,它们是控制角色骨架运动的两种基本工具。本节中我们来看看如何将骨架的运动传递给角色的“皮肤”——即我们看到的3D模型表面。这个过程的核心技术称为蒙皮

蒙皮技术解决了如何让角色网格模型随着内部骨骼关节的运动而产生自然形变的问题。最常用的一种方法是线性混合蒙皮,也称为骨骼子空间变形。

其核心思想是:模型表面的每个顶点,其最终位置是由影响它的所有骨骼的运动加权平均后决定的。

以下是实现线性混合蒙皮所需的两个核心要素:

  • 蒙皮权重:这是一个矩阵 W_ij,表示第 i 个顶点受第 j 根骨骼运动影响的程度。权重通常是正数,并且对于一个顶点,所有影响它的骨骼权重之和为1(即构成一个“单位划分”)。这些权重通常由美术师手动绘制在模型上。
  • 骨骼变换矩阵:对于每根骨骼 j,都有一个变换矩阵 T_j,描述了该骨骼从绑定姿势(骨架初始静止的姿势)到当前姿势的刚体变换(包括旋转和平移)。

顶点的最终变换位置 P_i‘ 由以下公式计算:
P_i‘ = Σ_j ( W_ij * T_j * P_i )
其中 P_i 是顶点在绑定姿势下的原始位置。

实际上,T_j 矩阵需要考虑到骨骼的层级关系。完整的公式通常写作:
P_i‘ = Σ_j ( W_ij * (T_j * B_j^{-1}) * P_i )
这里 B_j 是将顶点从模型全局坐标变换到骨骼 j 局部坐标的矩阵(在绑定姿势下)。B_j^{-1} 则是其逆矩阵。(T_j * B_j^{-1}) 这个组合实际上计算的是从绑定姿势到当前姿势的相对变换。

注意:当使用矩阵 M 变换顶点位置时,用于光照计算的法线向量需要使用 M 的逆转置矩阵 (M^{-1})^T 来变换,以保持正确方向。

线性混合蒙皮的局限性

虽然线性混合蒙皮被广泛使用且实现简单,但它有一个著名的缺陷:糖果纸效应

问题根源在于,我们对多个旋转矩阵进行了线性加权平均。然而,旋转矩阵的空间不是线性的,两个旋转矩阵的线性平均值通常不再是一个旋转矩阵。这可能导致在关节弯曲处(如肘部)体积不自然地收缩甚至塌陷,就像拧糖果纸一样。

例如,将旋转+90度和-90度的两个矩阵平均,得到的矩阵会使所有向量长度缩为零,这解释了为什么关节在弯曲时看起来会变扁。

为了解决这个问题,研究者们提出了更高级的蒙皮方法,例如对偶四元数蒙皮。它不是在矩阵空间中进行平均,而是在能更好表示旋转的四元数空间中进行插值,从而得到更自然、体积保持更好的形变效果。

自动绑定与权重生成

最后,我们简要讨论如何自动化蒙皮流程。传统上,放置骨骼和绘制权重都需要美术师手动完成。

然而,也存在自动化的方法:

  • 自动权重生成:可以根据顶点与骨骼的几何距离(如最近邻或热扩散方程)来近似计算权重。也有研究尝试用机器学习方法,通过观察角色在不同姿势下的形态来反推权重。
  • 自动骨骼绑定:给定一个静态的3D模型,算法可以自动估算并放置一个合理的骨架结构进去,这个过程称为自动绑定。例如,2007年的“Pinocchio”系统就是这方面的早期工作。

本节课中我们一起学习了计算机动画的概览,从历史渊源到现代技术类型。我们重点剖析了角色动画中的蒙皮技术,理解了线性混合蒙皮的原理、数学公式及其固有的局限性。掌握这些基础知识,是进一步学习更复杂动画和物理模拟的关键。下一讲,我们将开始深入探讨基于物理的动画,接触一些微分方程,让物体的运动更加真实可信。

007:粒子系统与常微分方程 🎯

在本节课中,我们将学习粒子系统的基本概念及其在计算机图形学中的应用。粒子系统是一种模拟大量小粒子运动的技术,常用于模拟烟雾、火焰、水流等效果。我们将探讨如何通过常微分方程(ODE)来描述粒子的运动,并介绍一种简单的数值积分方法——前向欧拉法。


概述 📋

上一节我们介绍了关键帧动画和过程动画,本节中我们将转向基于物理的动画,特别是粒子系统。粒子系统通过模拟大量粒子的运动来生成复杂的视觉效果,如火花、烟雾和鸟群。我们将学习如何用常微分方程描述粒子运动,并介绍数值积分的基本方法。


粒子系统的基本概念 🧩

粒子系统是计算机图形学中用于模拟大量小粒子运动的工具箱。每个粒子具有位置、速度等属性,并通过力场相互作用。以下是粒子系统的基本组成部分:

  • 粒子:系统中的基本单元,具有位置、速度、质量等属性。
  • 发射器:生成粒子的源头,通常位于特定位置。
  • :影响粒子运动的因素,如重力、空气阻力等。
  • 生命周期:粒子从生成到消失的时间,避免内存无限增长。

常微分方程与物理模拟 📐

在物理模拟中,我们通常使用牛顿第二定律来描述粒子的运动:

公式
[
F = m \cdot a
]

其中,( F ) 是力,( m ) 是质量,( a ) 是加速度。加速度是位置对时间的二阶导数,因此我们需要将其转化为一阶常微分方程以便数值求解。

降阶为一阶常微分方程

通过引入速度变量 ( v ),我们可以将二阶方程转化为一阶方程组:

公式
[
\frac{d}{dt} \begin{bmatrix} x \ v \end{bmatrix} = \begin{bmatrix} v \ \frac{F}{m} \end{bmatrix}
]

这样,我们只需处理一阶导数,简化了数值求解过程。


数值积分方法:前向欧拉法 🔢

前向欧拉法是最简单的数值积分方法,通过离散时间步长来近似求解常微分方程。其基本思想如下:

公式
[
x_{n+1} = x_n + h \cdot F(x_n, t_n)
]

其中,( h ) 是时间步长,( F(x_n, t_n) ) 是当前状态下的导数。

前向欧拉法的局限性

尽管前向欧拉法简单易实现,但它存在稳定性问题。例如,在模拟圆周运动时,前向欧拉法会导致能量逐渐增加,粒子轨迹向外螺旋扩散。这是因为该方法无法精确保持系统的能量守恒。


粒子系统中的力模型 🌪️

粒子系统中的力可以是物理的(如重力、空气阻力),也可以是人为设计的(如艺术导向的力场)。以下是一些常见的力模型:

  • 重力:恒定向下的力,常用于模拟下落物体。
  • 空气阻力:与速度相关的力,模拟物体在空气中运动的阻力。
  • 伦纳德-琼斯势:用于模拟粒子间的吸引和排斥,公式为:
    [
    F(r) = \frac{A}{r^m} - \frac{B}{r^n}
    ]
    其中 ( r ) 是粒子间距离,( A )、( B )、( m )、( n ) 是参数。

鸟群模拟:Boids算法 🐦

Boids算法是一种模拟鸟群或鱼群行为的粒子系统。每个粒子(代表一只鸟)遵循以下三条规则:

  1. 分离:避免与邻近粒子碰撞。
  2. 对齐:与邻近粒子的平均速度方向保持一致。
  3. 聚集:向邻近粒子的平均位置移动。

这些简单规则可以生成非常逼真的群体行为,广泛应用于电影和游戏中的群体动画。


粒子渲染技术 🎨

粒子系统的渲染不仅限于绘制点。我们可以使用多种技术来增强视觉效果:

  • 公告牌:始终面向摄像机的二维图像,用于模拟火花、烟雾等效果。
  • 纹理映射:为粒子添加纹理,使其看起来更真实。
  • 颜色与透明度:根据粒子的生命周期调整颜色和透明度,模拟火焰或烟雾的渐变效果。

总结 📝

本节课中,我们一起学习了粒子系统的基本概念及其在计算机图形学中的应用。通过常微分方程描述粒子运动,并使用前向欧拉法进行数值积分,我们可以模拟各种自然现象。尽管前向欧拉法简单,但其稳定性有限,下一节课我们将介绍更稳定的数值积分方法,并将其应用于布料模拟等复杂场景。


注意:本教程基于MIT 6.837课程第7讲内容整理,删除了语气词,并按照要求格式化为Markdown。内容力求简单直白,适合初学者学习。

008:布料模拟与数值积分

在本节课中,我们将要学习物理模拟的核心——数值积分方法,并探讨如何应用这些方法来模拟布料等复杂物体。我们将从回顾常微分方程(ODE)求解开始,深入分析不同积分器的精度与稳定性,最后学习如何用弹簧质点系统构建布料模型。

回顾:初始值问题与一阶化

上一节我们介绍了物理模拟的数学基础。本质上,我们处理的是一个初始值问题:已知系统在初始时刻 t₀ 的状态 x(t₀),需要根据物理定律(通常表示为 dx/dt = f(x, t))预测其随时间 t 的演化。

然而,牛顿第二定律 F = ma 包含了二阶导数(加速度)。为了让我们的ODE求解器(通常只处理一阶导数)能够工作,我们引入了一阶化技巧:引入速度变量 v

定义状态向量 X = [x; v],我们可以将二阶ODE转化为一阶ODE系统:

dx/dt = v
dv/dt = F/m

这种方法被称为降阶。虽然我们只需处理一阶导数,但代价是系统变量的数量翻倍了。

显式积分器:从欧拉方法开始

在上一讲的回顾之后,本节我们来看看最基础的数值积分方法。ODE求解的核心是近似计算 x(t + h),其中 h 是时间步长。

前向欧拉法是世界上最简单的近似:

x(t + h) ≈ x(t) + h * f(x(t), t)

从几何上看,这相当于在相空间中,沿着当前点 (t, x(t)) 的切线方向前进一步。

精度分析:泰勒展开的视角

为了理解积分器的精度,我们使用泰勒展开。x(t + h) 的真实值为:

x(t + h) = x(t) + h * f(x(t), t) + O(h²)

前向欧拉法的单步误差是 O(h²)。然而,如果我们想从时间 0 模拟到某个固定时间 T,需要走 T/h 步。因此,总误差变为 O(h²) * (T/h) = O(h)

所以,我们称前向欧拉法是一阶精度的。这意味着如果将步长减半(计算量翻倍),误差大约也会减半。

稳定性分析:一个模型方程

精度只是问题的一部分。我们还需要关心稳定性——当步长 h 较大时,解是否会失控“爆炸”。

一个经典的分析方法是考察模型方程:

dx/dt = -k * x,   (k > 0)

其解析解是指数衰减:x(t) = x₀ * e^{-kt}

应用前向欧拉法:

x(t + h) = x(t) + h * (-k * x(t)) = (1 - hk) * x(t)

经过 L 步后:x(t + Lh) = (1 - hk)^L * x(t)

以下是关键观察:

  • h 很小时,(1 - hk) 是一个略小于1的正数,解会近似指数衰减。
  • h 增大使得 1 - hk < -1(即 h > 2/k)时,因子变为绝对值大于1的负数。每步不仅放大解,还会正负振荡,导致数值解迅速发散。

这种现象被称为数值不稳定。系统参数 k 越大(物理上表示变化越快),允许的稳定步长 h 就必须越小。这类对步长有严格限制的问题被称为数值刚性问题。例如,蛋白质折叠、湍流流体、 stiff 布料或折纸模拟都是典型的刚性问题。

更高阶的显式方法

前向欧拉法精度和稳定性都欠佳。以下是两种常见的二阶精度显式方法,它们通过在不同位置计算导数来获得更好的近似:

梯形法则

f₀ = f(x(t), t)
x_temp = x(t) + h * f₀
f₁ = f(x_temp, t + h)
x(t + h) ≈ x(t) + (h/2) * (f₀ + f₁)

中点法

f_mid = f(x(t) + (h/2)*f(x(t), t), t + h/2)
x(t + h) ≈ x(t) + h * f_mid

这些方法单步误差为 O(h³),整体为二阶精度 O(h²)。在图形学和游戏工业中,最常用的是四阶龙格-库塔法(RK4),它在精度和稳定性之间取得了很好的平衡,属于显式积分器。

隐式积分器:用稳定性换取计算量

对于刚性系统,显式方法要求极小的步长。本节我们来看看另一类能提供更好稳定性的方法。

我们从导数的另一种近似出发(后向差分):

[ x(t + h) - x(t) ] / h ≈ f( x(t + h), t + h )

将其中的近似符号改为等号,并移项,就得到了后向欧拉法(隐式欧拉)的公式:

x(t + h) = x(t) + h * f( x(t + h), t + h )

注意,未知量 x(t + h) 同时出现在等号两边,因此这个方程是隐式的——要得到 x(t + h),需要求解一个(可能是非线性的)方程,例如使用牛顿法。

隐式欧拉的稳定性

再次使用模型方程 dx/dt = -kx 进行分析。应用隐式欧拉公式:

x(t + h) - x(t) = h * (-k * x(t + h))

求解 x(t + h)

x(t + h) = x(t) / (1 + hk)

经过 L 步后:x(t + Lh) = x(t) / (1 + hk)^L

无论步长 h 取多大,放大因子 1/(1+hk) 的绝对值始终小于1。因此,隐式欧拉法对于此模型是无条件稳定的

几何解释:隐式欧拉不是在当前点沿切线向前走,而是寻找一个点,使得从该点“向后看”的导数能指向当前点。

隐式方法的代价与特点

稳定性并非没有代价:

  1. 计算成本高:每一步都需要求解一个系统方程,可能涉及复杂的非线性求解。
  2. 数值粘性:当 h 很大时,放大因子 1/(1+hk) 变得非常小,导致解被过度阻尼,衰减得比真实物理更快。这使得运动看起来“沉重”或“粘滞”。早期布料模拟常使用隐式欧拉,结果布料显得异常垂坠。

弹簧质点系统:建模布料与毛发

掌握了积分器之后,本节我们来看看如何构建物理模型。在图形学中,一个强大而简单的建模范式是弹簧质点系统

核心思想:将复杂物体(如布料、毛发)离散化为一个质点网络,质点之间用各种弹簧连接。通过调节弹簧的刚度、阻尼和连接拓扑,可以模拟多种材料行为。

基础:胡克定律

连接质点 ij 的弹簧,其产生的力遵循胡克定律:

F_spring = -k_s * (|x_ij| - L₀) * (x_ij / |x_ij|)

其中:

  • k_s 是弹簧刚度。
  • L₀ 是弹簧原长。
  • x_ij = x_j - x_i 是质点间的向量。

布料建模:弹簧网络拓扑

如果只在布料网格的每条边上放置弹簧,模拟效果会很差,因为网格对角线方向容易发生非物理的剪切变形(“手风琴效应”)。

一个经典模型(Provot, 1995)为规则网格定义了三种弹簧:

  1. 结构弹簧:连接水平与垂直方向的相邻质点。抵抗拉伸。
  2. 剪切弹簧:连接网格对角线上的质点。抵抗剪切变形。
  3. 弯曲弹簧:连接水平或垂直方向上相隔一个质点的质点(如 (i, j)(i, j+2))。抵抗弯曲,保持布料挺括。

通过组合这些弹簧,可以构建出视觉上合理的布料模拟。对于非规则三角网格,需要定义相应的连接规则。

毛发建模与进阶

模拟单股毛发可以用一维弹簧链。但要表现发束的聚合行为、卷曲或扭转,则需要更复杂的模型:

  • 角度约束:在弹簧链中,添加惩罚相邻线段夹角偏离的力。
  • 跨接弹簧:在非相邻质点间添加弹簧,以引导毛发呈现特定曲率。
  • 离散弹性杆模型:这是一个更物理的模型,它跟踪中心线的弯曲和截面的扭转,能模拟毛发打结、缠绕等复杂现象。

模拟中的其他挑战

在实现弹簧质点系统时,还需考虑:

  • 约束:如固定点、关节连接。处理方法包括在每步后投影到约束流形,或使用广义坐标。
  • 碰撞检测与响应:布料自碰撞、与外部物体碰撞是巨大挑战,通常通过添加排斥力或修正位置来处理。
  • 数值刚性:布料弹簧通常很“硬”,容易导致显式积分器不稳定。隐式积分或自适应步长是常见解决方案。

总结与课程收尾

本节课中我们一起学习了物理模拟的核心数值工具。我们从ODE求解的回顾开始,深入比较了显式与隐式积分器在精度和稳定性上的权衡。前向欧拉简单但不稳定;高阶显式方法(如RK4)更精确;隐式欧拉非常稳定但计算成本高且有数值粘性。

接着,我们探讨了如何使用弹簧质点系统这一灵活框架来建模布料、毛发等物体。通过设计不同的弹簧网络拓扑,我们可以近似模拟多种材料的力学行为。

物理模拟是一个广阔而活跃的领域,本课仅触及冰山一角。从关键帧动画到过程化方法,再到今天的物理基础动画,这些技术共同驱动着数字世界中令人信服的运动。


下一讲,我们将离开动画的领域,开启计算机图形学另一个核心篇章:光线追踪。

009:光线投射

在本节课中,我们将要学习计算机图形学中一个核心概念——渲染。具体来说,我们将深入探讨一种基础的渲染算法:光线投射。我们将学习如何从虚拟相机生成光线,以及如何计算这些光线与场景中基本几何体(如平面和球体)的交点。


概述:什么是渲染?

渲染是将三维场景的描述转换为二维屏幕上像素颜色的过程。想象一下,你面前有一个虚拟的相机,你的任务就是根据场景中所有物体的信息,为相机“取景器”上的每一个小方格(即像素)决定一个颜色。

这听起来简单,但实际上充满挑战。例如,一个像素内可能包含多种颜色,但我们只能为它分配一个颜色。为了简化,我们目前假设从相机发射一条光线,穿过每个像素的中心,并将该光线首次击中的物体颜色赋予该像素。


光线投射算法基础

上一节我们介绍了渲染的基本任务。本节中,我们来看看实现这一任务的核心算法——光线投射。

光线投射算法的核心思想非常直观:从相机位置(眼睛)向场景中的每个像素发射一条光线,找出这条光线首先与哪个物体相交,然后对该交点进行着色。

以下是该算法的基本步骤:

  1. 遍历图像中的每个像素
  2. 对于每个像素,构造一条从眼睛穿过该像素中心的光线
  3. 遍历场景中的每个物体,计算光线与该物体的交点。
  4. 在所有的交点中,选择距离眼睛最近(且为正)的交点
  5. 根据该交点处的物体材质和光照信息,计算像素的颜色

这个算法的关键在于一个函数:给定一条光线,返回它首先击中的物体及交点信息。这个函数将在后续更复杂的效果(如阴影、反射)中被反复调用。


第一步:生成光线

在开始计算交点之前,我们需要知道如何用数学表示一条光线,以及如何为每个像素生成对应的光线。

光线的数学表示

一条光线可以用一个起点和一个方向向量来参数化表示。公式如下:

P(t) = R_origin + t * R_direction

其中:

  • R_origin 是光线的起点(例如相机位置)。
  • R_direction 是光线的方向向量。
  • t 是一个标量参数(t >= 0),表示沿光线方向移动的距离。

针孔相机模型

我们使用针孔相机模型来生成光线。在这个模型中,相机被简化为一个点(眼睛),前面放置一个虚拟的成像平面(即我们的屏幕)。

为了生成穿过像素 (i, j) 的光线,我们需要:

  1. 将像素坐标 (i, j) 转换为成像平面上的三维点坐标。
  2. 计算从眼睛 (E) 指向该三维点的方向向量。

这个过程涉及相机坐标系(眼坐标空间)的设定,通常包括眼睛位置 E、观察方向 W 以及成像平面的向上 V 和向右 U 向量。通过简单的线性组合,我们可以得到成像平面上任意点对应的光线方向。


第二步:计算光线-物体交点

现在我们有了光线,下一步是计算它与场景中物体的交点。我们的目标是找到最小的正参数 t,使得 P(t) 位于某个物体的表面上。

光线-平面相交

首先,我们学习如何计算光线与无限大平面的交点。

平面的表示:一个平面可以通过其法向量 N 和一个常数 D 来隐式定义,所有满足方程 N · P + D = 0 的点 P 都在该平面上。

相交计算:将光线的参数方程 P(t) = R_o + t * R_d 代入平面方程:
N · (R_o + t * R_d) + D = 0

解出 t
t = - (N · R_o + D) / (N · R_d)

如果分母 N · R_d 为 0,意味着光线方向与平面平行,没有交点(或有无穷多交点)。计算出的 t 必须为正才有效。得到 t 后,代回 P(t) 即可得到交点坐标。平面的法向量 N 可直接用于后续着色计算。

光线-球体相交

接下来,我们处理更常见的物体——球体。

球体的表示:一个球体可以用其中心 C 和半径 r 来隐式定义。所有满足 ||P - C||^2 - r^2 = 0 的点 P 都在球面上。

相交计算:同样,将光线方程 P(t) 代入球面方程:
|| (R_o + t * R_d) - C ||^2 - r^2 = 0

展开并整理后,我们得到一个关于 t 的二次方程:
A * t^2 + B * t + C = 0
其中:

  • A = R_d · R_d
  • B = 2 * R_d · (R_o - C)
  • C = (R_o - C) · (R_o - C) - r^2

使用二次方程求根公式解出 t
t = (-B ± sqrt(B^2 - 4*A*C)) / (2*A)

根号内的部分 B^2 - 4*A*C 称为判别式,它决定了交点的数量:

  • 判别式 < 0:无实根,光线与球体不相交。
  • 判别式 = 0:有一个重根,光线与球体相切。
  • 判别式 > 0:有两个不同的实根,光线穿过球体。

我们需要选择最小的、为正t 值作为有效交点。得到交点 Q 后,球体在该点的法向量为 (Q - C),通常需要归一化为单位向量。


总结与展望

本节课中,我们一起学习了渲染的基础——光线投射算法。我们掌握了以下核心内容:

  1. 渲染流程:理解了从三维场景到二维像素的转换过程。
  2. 光线生成:学会了在针孔相机模型下,为每个像素构造对应的光线。
  3. 交点计算:推导并实现了光线与平面球体这两种基本几何体的交点计算公式。

现在,你已经可以渲染由平面和球体组成的简单场景了!然而,现实中的复杂模型通常由三角形网格构成。在下节课中,我们将探讨如何计算光线与三角形的交点,这将引入重心坐标这一重要工具。同时,我们也会看到当前朴素算法的性能瓶颈(需要对每个像素遍历所有物体),并为后续讲解加速数据结构(如BVH)埋下伏笔。

010:光线投射 II

在本节课中,我们将继续学习光线投射技术。我们将首先探讨如何计算光线与三角形的交点,这是渲染复杂3D模型的关键。接着,我们将学习一种称为构造实体几何(CSG)的技术,它允许我们通过组合简单形状来创建复杂对象,而无需显式建模其几何形状。最后,我们将了解如何通过变换来高效地渲染对象的多个实例。

课程回顾与概述

上一节我们介绍了光线投射的基本原理,包括如何生成从眼睛出发的光线,以及如何计算光线与平面和球体的交点。本节中,我们来看看如何将这一技术扩展到更复杂的几何形状。

光线投射的核心是找到光线与场景中物体的第一个交点。我们用一个公式表示光线:P(t) = O + t * D,其中 O 是原点,D 是方向向量,t 是一个非负参数。我们的任务是找到最小的 t 值,使得 P(t) 位于某个物体上。

重心坐标:理解三角形内部

为了计算光线与三角形的交点,我们需要引入一个核心概念:重心坐标。这是一种描述三角形内部点位置的方法。

一个平面可以通过三个不共线的点 ABC 来定义。平面上的任意点 P 都可以表示为这三个点的加权平均:
P = α * A + β * B + γ * C
其中,权重 αβγ 满足 α + β + γ = 1。这三个数 (α, β, γ) 就称为点 P 相对于三角形 ABC重心坐标

以下是重心坐标的一些关键特性:

  • αβγ 均为正数时,点 P 位于三角形内部。
  • 当其中一个坐标为零时,点 P 位于三角形的对应边上。
  • 重心坐标可以用于在三角形内部平滑地插值顶点属性,例如颜色、法向量或材质参数。

计算光线与三角形的交点

现在,我们利用重心坐标来计算光线与三角形的交点。思路是:将光线的参数方程与三角形的重心坐标表示联立。

我们有:

  1. 光线方程:P(t) = O + t * D
  2. 三角形平面方程(用重心坐标表示):P = A + β * (B - A) + γ * (C - A)。这里我们消去了 α,因为 α = 1 - β - γ

令这两个表达式相等,我们得到一个关于未知数 tβγ 的方程组:
O + t * D = A + β * (B - A) + γ * (C - A)

我们可以将这个方程组重写为一个3x3的线性系统:

[ -D, (B-A), (C-A) ] * [ t, β, γ ]^T = (O - A)

通过求解这个矩阵方程(例如使用克莱姆法则或调用数值求解库),我们可以同时得到 tβγ

得到解后,我们进行判断:

  1. 检查 t 是否为正(表示交点在光线前方)。
  2. 检查 βγ 是否为正,并且 β + γ ≤ 1(表示交点在三角形内部)。

如果以上条件都满足,那么我们就找到了光线与三角形的有效交点。交点位置可以通过 P = O + t * D 得到。

构造实体几何(CSG):组合形状的艺术

构造实体几何是一种通过布尔运算(并集、交集、差集)组合简单几何体(称为图元)来创建复杂形状的技术。在光线追踪中,实现CSG有一个非常巧妙的方法。

核心思想是:我们不需要显式地计算出组合后复杂形状的精确几何模型(例如三角网格)。我们只需要知道光线何时进入和离开这个组合形状。

为此,我们扩展物体的接口。除了返回第一个交点,物体还需要能够返回一个区间列表,表示光线位于该物体内部的 t 值范围(例如,从 t_entert_exit)。

基于这些区间信息,CSG操作可以这样实现:

  • 并集 (A ∪ B):合并A和B的区间列表。
  • 交集 (A ∩ B):取A和B区间列表的重叠部分。
  • 差集 (A \ B):取在A中但不在B中的区间部分。

通过创建一个新的“CSG对象”,它内部存储两个子对象和所需的布尔操作,并在求交时进行上述区间运算,我们就可以渲染出复杂的组合形状,而无需为其创建复杂的网格。

实例化与变换:高效复用几何体

在复杂场景中,经常需要渲染同一个物体的多个副本(例如一片森林中的树木)。为每个副本都存储一份几何数据是低效的。实例化技术允许我们只存储一份几何体,然后通过应用不同的变换(平移、旋转、缩放)来放置多个实例。

在光线追踪中实现实例化有一个优雅的技巧:与其变换物体本身,不如变换光线。

假设我们有一个物体 Object 和一个变换矩阵 MM 将物体从局部坐标系变换到世界坐标系。为了求光线 Ray 与这个已变换物体的交点,我们可以:

  1. 计算变换矩阵的逆 M_inv
  2. 将光线 Ray 变换到物体的局部坐标系:Ray_local.origin = M_inv * Ray.originRay_local.direction = M_inv * Ray.direction(注意:对于方向向量,通常使用 M_inv 的左上3x3部分,忽略平移)。
  3. 计算 Ray_local 与原始物体 Object 的交点,得到局部坐标系下的交点信息(包括交点参数 t_local 和法向量 N_local)。
  4. 世界坐标系下的交点位置为:P_world = Ray.origin + t_local * Ray.direction(注意:t 值在变换下保持不变)。
  5. 世界坐标系下的法向量需要特殊处理:N_world = (M_inv)^T * N_local,然后进行归一化。这是因为法向量与切向量的变换方式不同。

通过这种方式,我们可以轻松地渲染经过任意变换的物体实例,而无需复制或修改其底层几何数据。

实现细节与注意事项

在实现光线追踪器时,需要注意一些细节以确保正确性和稳定性:

  • 浮点精度问题:光线与物体相切或击中共享边界的三角形时,可能因浮点误差导致错误。通常引入一个小的容差值 epsilon 来处理边界情况。
  • 自相交:当从交点处发射新的光线(如阴影线、反射光线)时,需要将原点沿法线方向稍微偏移,以防止新光线立即与当前表面再次相交。
  • 面向对象设计:将场景中的物体、材质、相机等都设计为具有统一接口的类,可以使代码结构清晰,易于扩展。例如,所有几何体都可以继承自一个基类,并提供 Intersect(Ray) 方法。

总结

本节课中我们一起学习了光线投射技术的几个高级主题。

  1. 我们深入探讨了重心坐标的概念和计算,并推导了光线与三角形求交的公式,这是渲染三角网格模型的基础。
  2. 我们介绍了构造实体几何(CSG),学习了如何通过布尔运算和区间算法来组合简单形状,从而创建复杂对象,而无需显式建模。
  3. 我们探讨了实例化与变换,掌握了通过变换光线而非几何体来高效渲染同一物体多个副本的技巧。

这些技术展示了光线追踪的强大和灵活之处:通过组合简单的抽象和算法,我们可以渲染出极其复杂的场景。下一讲,我们将开始关注如何优化光线追踪器的性能,使其运行得更快。

011:光线追踪

在本节课中,我们将学习光线追踪算法。我们将看到,通过反复使用“光线与场景物体求交”这一核心代码段,可以实现多种强大的视觉效果。我们将重点讨论阴影、反射和折射的实现,并了解光线追踪算法的计算开销和优化思路。


概述

上一讲我们介绍了光线投射(Ray Casting)的基本原理,包括相机模型、光线与几何体的求交计算,以及构造实体几何(CSG)和变换。本节中,我们将在此基础上,引入光线追踪(Ray Tracing)算法。核心区别在于,光线追踪允许在一条光线与物体相交后,向场景中发射次级光线,以计算更复杂的光照效果。

我们将学习如何实现阴影、镜面反射和折射。这些效果都依赖于向场景中发射新的光线。我们还会讨论光线追踪带来的计算挑战,例如递归深度控制和性能优化。


阴影

在光线追踪器中实现阴影,逻辑非常直接。当我们从眼睛发射一条光线并击中一个物体表面上的点P后,我们需要判断点P是否被某个光源照亮。

基本思路是:从点P向光源方向发射一条新的“阴影光线”。如果这条阴影光线在到达光源之前击中了场景中的任何其他物体,那么点P就处于该光源的阴影中。

以下是计算阴影的伪代码框架:

Color shadePoint(HitRecord hit) {
    // 环境光项(非物理的简化处理)
    Color finalColor = hit.object.ambientColor;

    // 遍历所有光源
    for (Light light : scene.lights) {
        // 创建从命中点指向光源的阴影光线
        Ray shadowRay(hit.point, directionTo(light.position));
        bool inShadow = false;

        // 检查阴影光线是否与场景中任何物体相交
        for (Object obj : scene.objects) {
            float t = obj.intersect(shadowRay);
            // 如果存在交点,且交点在光源之前,则点处于阴影中
            if (t > 0 && t < distanceToLight) {
                inShadow = true;
                break;
            }
        }

        if (!inShadow) {
            // 计算该光源对点P的着色贡献
            finalColor += computeShading(hit, light);
        }
    }
    return finalColor;
}

注意点:

  1. 自阴影(Self-Shadowing)问题:由于浮点数精度误差,计算出的交点P可能略微位于物体内部。这会导致从P点发出的阴影光线立刻与物体自身相交,错误地判定为处于阴影中,产生黑色斑点。解决方法通常是为t值设置一个小的正阈值(如epsilon),或者将阴影光线的起点沿法线方向略微偏移。
  2. 优化:阴影计算只需要知道光线是否被遮挡,而不需要知道最近的交点。因此,可以实现一个更快的、仅返回布尔值的相交测试函数。
  3. 硬阴影:目前我们的光源是理想点光源,这会产生非常锐利的阴影边缘。现实世界中,光源有大小,会产生柔和的半影(Penumbra)区域。我们将在后面讨论如何通过随机采样来模拟面光源,从而产生软阴影。

反射

接下来,我们看看如何实现像镜子那样完美的镜面反射。其核心是计算反射光线的方向。

反射方向计算

假设:

  • V 是从表面交点指向眼睛(或相机)的入射光线方向向量(单位向量)。
  • N 是交点处的表面法线向量(单位向量)。
  • R 是我们需要计算的反射光线方向向量(单位向量)。

反射定律指出,反射角等于入射角,并且入射光线、法线和反射光线共面。反射方向 R 可以通过以下公式计算:

R = V - 2 * (V · N) * N

公式推导理解

  1. (V · N)V 在法线 N 方向上的投影长度(带符号)。
  2. (V · N) * NV 的法线分量向量。
  3. V - (V · N) * N 得到的是 V 在切平面上的分量。
  4. 为了得到反射方向 R,我们需要将法线分量反转。因此,从 V 中减去两倍的法线分量:R = V - 2 * (V · N) * N

在着色函数中,一旦计算出反射方向 R,我们就从交点处沿 R 方向发射一条新的光线,并递归地调用着色函数来计算该反射光线的颜色。这个颜色就是当前交点反射所看到的颜色。

非理想反射(菲涅尔效应)

现实中几乎没有完美的镜子。反射的强度通常与观察角度有关。当视线与表面几乎平行(掠射角)时,反射更强烈;当垂直看向表面时,反射较弱。这种现象可以用菲涅尔方程描述。

在图形学中,常使用Schlick近似公式来模拟这一效果:
F = F0 + (1 - F0) * (1 - cosθ)^5

其中:

  • F 是反射系数(用于调制反射光颜色)。
  • F0 是材质在垂直入射时的基础反射率。
  • θ 是视线方向与法线方向的夹角,cosθ = V · N

这样,我们可以根据角度动态地调整反射光的权重,使材质看起来更真实。


折射

对于透明或半透明物体(如玻璃、水),光线会穿透物体并发生弯折,这就是折射。折射方向由斯涅尔定律(Snell‘s Law)决定。

斯涅尔定律与折射方向

斯涅尔定律描述了光线在两种不同介质界面处的行为:
η₁ * sinθ₁ = η₂ * sinθ₂

其中:

  • η₁η₂ 是两种介质的折射率。
  • θ₁θ₂ 分别是入射角和折射角。

在光线追踪中,我们需要一个能用向量 I(入射方向)和 N(法线)直接计算出折射方向 T 的公式。经过推导(过程涉及向量分解和三角恒等式),最终公式如下:

T = η_r * I - (η_r * c₁ + sqrt(1 - η_r² * (1 - c₁²))) * N

其中:

  • I 是指向物体内部的入射方向(单位向量)。通常,在求交后我们知道光线是从外部射入还是内部射出,需要据此调整。
  • N 是表面法线(单位向量),通常指向外部。
  • c₁ = cosθ₁ = -(I · N) (注意负号,因为 IN 方向相反)。
  • η_r = η₁ / η₂ 是相对折射率。当光线从介质1进入介质2时,η₁ 是当前介质折射率,η₂ 是目标介质折射率。需要根据光线是进入还是离开物体来正确设置 η_r

重要情况:全内反射
当公式中的平方根项 1 - η_r² * (1 - c₁²) 为负数时,意味着没有实数解,此时发生全内反射(光线全部反射,不折射)。在代码中需要检测这种情况,并改为计算反射光线。光纤通信就利用了全内反射原理。

实现与组合

在着色时,一个材质可能同时具有反射和折射属性(例如水面)。最终的颜色可以是反射光颜色和折射光颜色的加权和,权重由材质的属性(如反射系数、透射系数)以及菲涅尔效应决定。


光线追踪的挑战与优化

虽然上述每种效果实现起来代码量不大,但它们会极大地增加计算成本。

计算开销的来源

  1. 次级光线:每个像素的主光线可能产生多条次级光线(阴影、反射、折射)。
  2. 递归:反射和折射光线可能再次击中反射/折射表面,形成递归。必须设置一个最大递归深度以避免无限递归和栈溢出。
  3. 抗锯齿:为了消除锯齿(Jaggies),不能只对每个像素中心采样一次,而需要在像素内进行多次随机采样并平均(超采样)。这直接乘上了像素采样数倍的计算量。
  4. 软阴影:为了模拟面光源的软阴影,需要对光源区域进行多次随机采样。
  5. 模糊效果:通过随机扰动光线,可以模拟景深、运动模糊、粗糙表面(如 brushed metal)的模糊反射等效果,但这都需要大量采样来平均掉噪声。

所有这些“多次采样”都意味着计算量的乘法级增长

性能优化思路

  1. 递归深度限制:这是必须的。通常设置一个较小的数字(如5-10),因为每次反射/折射后光能会衰减,深度贡献很小。
  2. 加速数据结构:最昂贵的操作是“光线-物体求交”。当场景中有数百万个三角形时,为每条光线检查所有三角形是不可行的。下一讲我们将学习层次包围盒(BVH)等空间数据结构,来大幅减少需要检查的物体数量。
  3. 并行化:像素之间的着色计算是独立的,可以并行处理。但需要注意负载均衡问题,因为不同像素的着色复杂度可能差异很大。
  4. 自适应采样:对颜色变化平缓的区域使用较少采样,对边缘或复杂区域使用更多采样。

光线树

理解光线追踪计算复杂性的一个概念模型是光线树。树的根节点是从眼睛出发的主光线。每个节点(代表一条光线)在着色时可能产生多个子节点(阴影光线、反射光线、折射光线)。这棵树直观地展示了计算如何指数级扩展。


总结

本节课我们一起学习了光线追踪的核心扩展功能。

  • 我们首先学习了如何通过发射阴影光线来判断点是否被光源照亮,并了解了自阴影问题和硬阴影的局限性。
  • 接着,我们推导了镜面反射的向量公式,并介绍了菲涅尔效应来模拟更真实的非理想反射。
  • 然后,我们深入探讨了折射,从斯涅尔定律出发,推导出可用于代码实现的折射方向计算公式,并解释了全内反射现象。
  • 最后,我们认识到这些强大效果的代价是巨大的计算开销。我们讨论了递归控制、超采样、软阴影等概念带来的性能挑战,并简要提及了通过加速数据结构和并行化进行优化的必要性。

光线追踪的魅力在于,用相对简单的代码(发射新光线并递归调用着色器)就能模拟复杂物理现象。然而,要使其高效实用,我们必须借助精心设计的算法和数据结构,这正是后续课程的重要内容。

012:光线追踪加速技术

在本节课中,我们将学习如何解决光线追踪算法速度慢的问题。我们将回顾光线追踪为何耗时,并深入探讨几种主要的加速策略,特别是使用包围盒和空间数据结构来减少光线与物体求交的计算量。

光线追踪算法回顾与性能瓶颈

上一节我们介绍了如何通过增加光线数量来实现各种高级渲染效果。本节中,我们来看看这带来的性能问题。

光线追踪的核心操作是 trace_ray 函数。该函数接收一条光线(可能来自眼睛、反射或阴影),遍历场景中的所有光源和物体,计算颜色并合成。这是一个典型的递归算法。例如,遇到镜面时,会反射光线并递归调用以获取反射光线的颜色。

这导致了光线树的概念。从眼睛发出的一条光线,在计算其颜色时,可能会生成许多其他光线(如反射光、折射光、阴影光)。最终,这一切计算只是为了返回一个像素的RGB三个数值。这正是光线追踪开销巨大的根源。

在上一讲中,我们介绍了通过扩展光线树来实现的多种效果,如软阴影、抗锯齿、光泽反射、运动模糊和景深。这些效果统称为分布式光线追踪,其核心思想是向场景中发射大量光线并对结果取平均。

例如,要实现软阴影,光源不再是一个点,而是一个区域。我们可以随机在光源表面采样多个点,向每个点发射阴影光线进行计算,然后对结果取平均。

虽然这些效果在概念上清晰,实现也相对直接,但问题在于光线数量过多。每条光线都需要进行昂贵的计算,导致渲染时间急剧增加。

从算法复杂度分析,渲染一帧的成本至少与像素数量(宽 × 高)成正比。此外,场景中的物体数量、光线树的深度、阴影光线的数量等因素都会成为乘数因子,进一步增加计算时间。

总结:光线追踪能生成高质量的图像,但速度很慢,因为需要处理大量光线,且每条光线的求交计算都很昂贵。

加速策略总览

我们的目标是减少光线追踪的计算开销。主要有两种思路:

  1. 减少光线数量
  2. 降低每条光线的计算成本(特别是光线-物体求交)。

本节课我们将重点讨论第二种思路,即如何高效地判断一条光线是否与场景中的物体相交。这是加速光线追踪的主要途径之一。

一个直观的想法是:如果我们能用一个简单的形状(包围盒)包裹住复杂的物体,那么当光线连这个包围盒都碰不到时,就肯定碰不到盒内的物体,从而可以跳过盒内所有物体的求交计算。

包围盒

包围盒是一个包裹住物体的简单体积。如果光线不与包围盒相交,则无需与盒内任何物体求交。这是一种保守的加速方法:如果光线与包围盒相交,它仍可能不与盒内物体相交;但如果光线不与包围盒相交,则一定不与盒内物体相交。

设计包围盒时需要考虑以下权衡:

  • 求交计算复杂度:包围盒形状应简单,以便快速进行光线求交。
  • 紧密性:包围盒应尽可能紧密地包裹物体,以减少误报(光线击中包围盒但未击中物体)。

常见的包围盒类型有:

  • 轴对齐包围盒:与坐标轴对齐的立方体。
  • 非轴对齐包围盒
  • 球体
  • 椭球体
  • 半平面交集(凸区域)。

其中,轴对齐包围盒因其求交计算简单而非常流行。

光线与轴对齐包围盒求交

一个轴对齐包围盒可以由其最小角点 (x1, y1, z1) 和最大角点 (x2, y2, z2) 定义。光线定义为 origin + t * direction

求交思路借鉴了构造实体几何中的方法:将包围盒视为三对平行平面的交集。分别计算光线进入和离开每一对平面的 t 值区间(t_xmin, t_xmax; t_ymin, t_ymax; t_zmin, t_zmax)。光线与包围盒相交的 t 区间是这三个区间的交集,即:

t_enter = max(t_xmin, t_ymin, t_zmin)
t_exit = min(t_xmax, t_ymax, t_zmax)

如果 t_enter < t_exitt_exit > 0,则光线与包围盒相交。

以下是求交的伪代码核心逻辑:

// 计算光线与一对平行平面(如x方向)的t值区间
tx1 = (x1 - ray.origin.x) / ray.direction.x;
tx2 = (x2 - ray.origin.x) / ray.direction.x;
tmin = min(tx1, tx2);
tmax = max(tx1, tx2);
// 对y, z方向重复上述计算...
// 求交集
t_enter = max(tx_min, ty_min, tz_min);
t_exit = min(tx_max, ty_max, tz_max);
if (t_enter < t_exit && t_exit > 0) {
    // 相交
}

优化提示:除法操作较慢。可以预计算射线方向的倒数并存储,用乘法代替除法。

为图元计算包围盒

为基本图元(如三角形、球体)计算轴对齐包围盒是直接的:

  • 三角形:取三个顶点坐标在各轴上的最小值和最大值。
  • 球体:中心坐标加减半径。
  • 平面:平面是无限的,通常不为其计算包围盒。
  • 变换后的物体:对物体应用变换(如旋转)后,需要重新计算其轴对齐包围盒,而不是简单变换原来的包围盒角点。

总结:包围盒是一个简单有效的初步加速手段。但单个包围盒对于复杂场景或物体占据大部分视野的情况帮助有限。我们需要更精细的结构。

包围盒层次结构

为了获得更大加速,我们可以构建包围盒层次结构(Bounding Volume Hierarchy, BVH)。其核心思想是递归地将场景中的物体分组,并为每个组创建包围盒,形成一棵树。

BVH的构建过程如下:

  1. 为场景中所有图元创建一个根节点包围盒。
  2. 将图元列表分割成两个子集。分割策略有多种启发式方法:
    • 沿包围盒中心点排序,取中位数分割。
    • 沿某一轴将空间平分成两半。
    • 利用艺术家建模时已有的层次结构(如自行车的车轮、车架)。
  3. 为每个子集递归创建子包围盒,直到子集中图元数量足够少(成为叶节点)。

BVH的遍历(光线求交)过程是递归的:

  1. 如果光线与当前节点的包围盒不相交,则跳过该节点及其所有子节点。
  2. 如果相交,且当前节点是叶节点,则与叶节点内所有图元求交。
  3. 如果当前节点是内部节点,则递归地对其两个子节点执行步骤1和2。

关键点:即使光线先与一个子包围盒相交并命中盒内物体,也必须检查另一个子包围盒,因为另一个子盒中可能存在更近的相交点(当包围盒在空间上有重叠时)。只有在确定光线与某个子包围盒不相交时,才能安全跳过它。

BVH是一种在实现难度和加速效果之间取得良好折衷的流行方法。

KD树

KD树是另一种加速光线追踪的经典空间数据结构。它与BVH的主要区别在于:

  • BVH:物体仅属于一个包围盒,但包围盒之间可以重叠。
  • KD树:空间被轴对齐的平面递归分割,分割产生的单元格(包围盒)互不重叠,但一个物体可能跨越分割平面,从而属于多个单元格。

KD树的节点存储以下信息:

  • 分割的维度(x, y, z)。
  • 分割平面的位置。
  • 指向左右子节点的指针。
  • (叶节点中)包含的图元列表。

遍历KD树

遍历KD树比BVH更复杂,但可以利用空间不重叠的特性进行更多优化。伪代码概述如下:

function intersect_ray_kdtree(ray, node, t_min, t_max):
    if node is leaf:
        intersect ray with all primitives in node.list
        return closest hit
    else:
        // 计算光线与分割平面的交点参数 t_split
        t_split = (node.split_pos - ray.origin[node.axis]) / ray.direction[node.axis]

        // 确定遍历顺序:先近后远
        if ray.direction[node.axis] >= 0:
            first_child = node.left
            second_child = node.right
        else:
            first_child = node.right
            second_child = node.left

        if t_split <= t_min:
            // 光线只与第二个子空间相交
            return intersect_ray_kdtree(ray, second_child, t_min, t_max)
        else if t_split >= t_max:
            // 光线只与第一个子空间相交
            return intersect_ray_kdtree(ray, first_child, t_min, t_max)
        else:
            // 光线与两个子空间都相交
            hit = intersect_ray_kdtree(ray, first_child, t_min, t_split)
            if hit found:
                return hit // 提前终止,无需检查第二个子空间
            else:
                return intersect_ray_kdtree(ray, second_child, t_split, t_max)

这种遍历方式实现了提前终止:如果在先检查的子空间中找到了交点,由于空间不重叠,该交点一定比后检查子空间中的任何交点都近,因此可以立即返回,无需检查后一个子空间。

构建KD树

构建KD树的关键是选择分割平面。常用启发式方法包括:

  • 中位数分割:沿某轴对图元中心排序,在中位数处分割,以平衡树。
  • 表面积启发式:一种更复杂的、旨在最小化预期求交成本的启发式方法,在图形学中很常见。

对于静态场景,KD树可以预计算并存储,分摊构建开销。

光线步进

最后,我们简要介绍光线步进算法。这是一种介于光线追踪和光栅化之间的技术。

算法步骤如下:

  1. 构建均匀网格:将整个场景空间划分为均匀的3D网格单元。
  2. 填充网格:对于每个图元,将其放入所有与之相交的网格单元中。
  3. 遍历光线
    • 找出光线进入的第一个网格单元。
    • 使用类似Bresenham画线算法的三维数字微分分析器,沿着光线路径依次遍历网格单元。
    • 在每个被访问的单元中,与单元内所有图元进行求交测试。
    • 一旦找到交点,即可停止。

光线步进的优点是实现简单,并且遍历顺序固定,易于并行化。其性能很大程度上取决于网格分辨率的选择。

总结

本节课中,我们一起学习了如何加速光线追踪算法。我们首先分析了算法慢的原因:光线数量多,且每条光线的求交计算成本高。

接着,我们探讨了三种主要的加速数据结构:

  1. 包围盒:用简单体积包裹物体,快速排除不相交的光线。
  2. 包围盒层次结构:递归分组物体,形成树结构,在多个层次上提供加速机会。
  3. KD树:递归分割空间,确保子空间不重叠,允许更积极的遍历提前终止。
  4. 光线步进:使用均匀网格和光栅化式遍历来加速求交。

这些技术通过精心组织场景数据,显著减少了每条光线所需进行的图元求交测试次数,是生产级光线追踪器的基石。下一讲,我们将开始探讨着色与材质,为场景添加更丰富的外观。

013:着色 🎨

在本节课中,我们将要学习如何为计算机生成的场景添加更真实、更丰富的视觉效果。我们将超越简单的漫反射和镜面反射,探索更复杂的着色模型,理解光线如何与不同材质相互作用,并学习如何用数学公式来描述这些交互。

上一节我们介绍了光线追踪的基础,能够判断哪个物体在最前面,并为其应用简单的漫反射、镜面反射或折射材质。本节中我们来看看如何模拟更广泛的真实世界材质。

光线衰减与入射角

在深入讨论材质之前,我们需要先理解光线从光源到达物体表面的过程。我们之前忽略了光线强度随距离衰减的物理现象。

  • 距离衰减:光线从一个点光源发出,会均匀地向四周扩散。随着距离增加,相同的光能量会分布到更大的球体表面上。因此,到达物体表面的光强度与距离的平方成反比。公式表示为:
    I_received = I_source / (r^2)
    其中 I_source 是光源的原始强度,r 是光源到物体表面的距离。

  • 入射角影响:光线照射到表面的角度也至关重要。如果光线几乎平行于表面,则表面接收到的光能很少。表面接收到的光能与表面法线和光线方向夹角的余弦值成正比。这可以简单地用点积表示:
    cosθ = max(0, N · L)
    其中 N 是表面单位法向量,L 是指向光源的单位方向向量。我们使用 max 函数来确保当光线从表面背后照射时(点积为负),贡献为零。

综合以上两点,到达表面某点的入射辐照度计算公式为:
E = (I_source / r^2) * max(0, N · L)

以下是关于光源的一些补充说明:

  • 聚光灯:聚光灯的光强还依赖于照射方向。通常使用一个关于角度θ的函数(如样条曲线)来模拟从中心热点到边缘的衰减。
  • 方向光:对于像太阳这样的无限远光源,通常忽略距离衰减项(1/r^2),只保留角度余弦项。

双向反射分布函数

光线到达表面后,如何反射出去则完全由材质属性决定。描述这一行为的核心工具是双向反射分布函数

BRDF 是一个函数,它描述了从某个特定方向入射的光线,有多少比例被反射到另一个特定的出射方向。它本质上定义了材质的光学特性。

  • 数学表达:BRDF 是入射光方向 ω_i 和出射(观察)方向 ω_o 的函数,通常写作 f_r(ω_i, ω_o)。由于方向可以用单位向量表示,它也可以看作是 f_r(L, V),其中 L 指向光源,V 指向相机。
  • 核心公式:根据定义,出射的辐射亮度 L_o 等于入射的辐射照度 E_i 乘以 BRDF:
    L_o = f_r(L, V) * E_i
  • 可视化:通常固定入射光方向 L,然后将 BRDF 值作为 V 方向的函数在球面上可视化。结果图形被称为“波瓣”,其形状直观反映了材质的反射特性(如漫反射是均匀的球体,镜面反射是尖锐的波瓣)。

BRDF 模型基于一些简化假设,例如:

  • 它通常假设光线在同一个表面点入射和出射,因此无法模拟次表面散射(如皮肤、玉石)或毛发等复杂效果。
  • 它通常相对于表面法线方向定义,与物体的绝对朝向无关。

常见着色模型

现在,我们来看几个历史上重要且至今仍在广泛使用的参数化 BRDF 模型。它们都是基于观察和启发式设计出来的函数。

1. 朗伯模型

这是我们早已熟悉的漫反射模型。它假设表面是理想粗糙的,将入射光均匀地反射到所有方向。

  • BRDF:朗伯材质的 BRDF 是一个常数。结合之前的光线衰减和入射角计算,完整的着色公式为:
    L_o = k_d * (I / r^2) * max(0, N · L)
    其中 k_d 是材质的漫反射颜色(一个 RGB 三元组)。

2. 冯氏着色模型

冯氏模型是一个经验模型,它将最终颜色表示为环境光、漫反射和镜面反射三项之和。它并非严格基于物理,但效果直观且计算高效。

以下是冯氏模型的三个组成部分:

  • 环境光项k_a * I_a。这是一个常数项,用于模拟场景中的间接光照,防止阴影区域完全变黑。它不符合物理规律,但实用。
  • 漫反射项k_d * (I / r^2) * max(0, N · L)。即标准的朗伯反射。
  • 镜面反射项k_s * (I / r^2) * max(0, R · V)^p。此项模拟高光。R 是光线 L 关于法线 N 的完美反射方向。(R · V) 衡量观察方向 V 与理想反射方向 R 的接近程度。指数 p(光泽度系数)控制高光的集中程度:p 越大,高光越锐利、越小;p 越小,高光越扩散、越大。

完整的冯氏着色公式为:
L_o = k_a*I_a + (k_d*max(0,N·L) + k_s*max(0,R·V)^p) * (I / r^2)

3. 微表面理论与 Cook-Torrance 模型

更先进的模型试图从物理原理推导 BRDF。微表面理论将宏观表面视为由无数微观尺度上的微小镜面(微表面)构成。宏观的反射行为是这些微表面统计分布的结果。

Cook-Torrance 模型是基于微表面理论的经典模型。它比冯氏模型更复杂,但能更好地模拟菲涅尔效应(掠射角反射增强)和阴影遮蔽效应。

其 BRDF 通常包含以下部分的乘积:

  • 菲涅尔项 F:描述反射率随入射角的变化。
  • 法线分布函数 D:描述微表面法线朝向的统计分布,决定高光的形状和大小。
  • 几何衰减项 G:描述微表面之间相互遮挡(阴影和遮蔽)导致的光线衰减。

虽然理解其物理推导需要更多背景知识,但作为程序员,我们可以直接使用其数学公式来获得更真实的材质效果。

空间变化的材质

到目前为止,我们都假设物体的材质是均匀的。然而,真实世界的物体通常由多种材料组成,或者材质属性在表面变化(如木材纹理、织物图案)。

为了模拟这一点,我们需要让 BRDF 的参数(如 k_d, k_s, p)成为表面位置的函数。

以下是两种主要实现方法:

  • 顶点属性插值:将材质参数存储为网格顶点的属性,然后在三角形内部通过重心坐标进行插值。这种方法适用于平滑变化的材质。
  • 纹理映射:这是处理复杂、高频空间变化的主要技术。我们将材质参数(通常是漫反射颜色 k_d)存储在一张图像(纹理)中,并建立从物体表面点到纹理图像的映射关系。在着色时,根据交点位置查找纹理图像,获取该点的材质参数。这将是我们下一讲的重点。

总结

本节课中我们一起学习了如何为光线追踪场景实现更丰富的着色效果。

我们首先完善了光照计算,引入了基于距离平方反比的光线衰减和基于入射角余弦的光照贡献。接着,我们认识了描述材质反射行为的核心概念——双向反射分布函数。我们探讨了从简单的常数朗伯 BRDF,到经验性的冯氏模型,再到基于物理的 Cook-Torrance 模型。最后,我们指出真实材质属性通常在空间变化,并引出了通过纹理映射来实现这一点的解决方案。

现在,你的渲染器已经具备了模拟各种非理想反射材质的基础。记住,这些着色模型本质上是将物理现象和艺术控制相结合的数学函数,你可以灵活地调整甚至创造自己的模型来达到想要的视觉效果。

014:纹理映射 🎨

在本节课中,我们将学习如何为三维场景中的物体表面添加丰富、变化的视觉细节。我们将重点介绍两种核心方法:纹理映射和程序化渲染,它们能让简单的几何模型呈现出复杂的材质效果。


概述

上一节我们介绍了材质和双向反射分布函数(BRDF),它们描述了光线与物体表面某一点的交互方式。然而,现实世界中的物体表面材质(如颜色、粗糙度)通常是随空间位置变化的。本节中,我们来看看如何将这种空间变化引入到我们的渲染系统中。


纹理映射的基本思想 🗺️

我们手中的三维网格模型通常比较粗糙,可以看到明显的三角形面片。我们希望附加到表面的纹理细节,其密度应高于网格三角形本身的密度。

纹理映射的核心思想是:将三维表面“展开”或“映射”到一个二维平面上。这个二维图像被称为纹理贴图。然后,在渲染时,将这个纹理像墙纸一样“包裹”回三维表面。

以下是实现纹理映射的关键步骤:

  1. 建立映射关系:对于三维网格中的每个三角形,我们不仅存储其三个顶点在三维空间中的坐标,还存储它们在二维纹理图像(常称为UV平面)中对应的坐标。
  2. 渲染时查询:当一条光线与三维三角形相交时,我们计算出交点在三角形内的重心坐标 (α, β, γ)
  3. 纹理查找:使用完全相同的重心坐标 (α, β, γ),在纹理图像中对应的三角形内进行插值,得到该点的纹理颜色。

代码描述:假设三维三角形的顶点为 P0, P1, P2,对应的纹理坐标为 (u0, v0), (u1, v1), (u2, v2)。光线交点的重心坐标为 (α, β, γ),其中 γ = 1 - α - β。则该点的纹理坐标 (u, v) 计算如下:

u = α * u0 + β * u1 + γ * u2
v = α * v0 + β * v1 + γ * v2

然后从纹理图像中读取 (u, v) 坐标处的颜色。


纹理映射的挑战与细节

上一节我们介绍了纹理映射的基本流程,本节中我们来看看实际操作中会遇到的一些关键挑战和解决方案。

映射的切割与接缝

一个重要的几何事实是:大多数三维曲面(如球体)无法无失真、无切割地连续展开到平面上。因此,创建纹理映射通常需要在三维模型上做一些“切割”。

这意味着什么? 在三维空间中共享一条边的两个三角形,在纹理平面中可能被映射到完全不同的位置,它们的边不再相连。因此,在数据结构上,我们不能简单地为每个三维顶点存储一个纹理坐标。必须为每个三角形的每个顶点独立存储纹理坐标,即使这些顶点在三维空间中是同一个点。

纹理过滤

当纹理像素(texel)与屏幕像素(pixel)的比例不匹配时,会出现走样问题。

  • 放大(Magnification):当纹理被拉伸,一个屏幕像素覆盖纹理的一小部分时,直接取最近像素颜色会导致块状瑕疵。通常的解决方案是双线性插值
  • 缩小(Minification):当纹理被压缩,一个屏幕像素覆盖纹理的一大片区域时,简单采样会导致严重的闪烁和噪声(摩尔纹)。这需要更复杂的处理,我们将在下一讲深入讨论的 Mipmapping 技术就是为了解决这个问题。

Mipmapping 预览:其核心思想是预先计算并存储原始纹理的一系列缩小版本(如1/2, 1/4, 1/8大小)。在渲染时,根据屏幕像素覆盖的纹理区域大小,自动选择合适的层级进行采样,相当于进行了预积分,能有效减少缩小带来的走样。


如何获得纹理坐标(参数化)

既然纹理映射如此重要,一个自然的问题是:我们如何得到将三维网格映射到二维平面的这些纹理坐标(即参数化)呢?

以下是几种常见的方法:

  • 手工制作:在许多影视制作中,艺术家会使用三维软件(如3D Studio Max)手动“裁剪”模型并展开。他们会将接缝藏在不易察觉的地方(如耳后)。
  • 公式化投影:对于简单形状(如圆柱、球体),可以使用圆柱投影、球面投影等数学公式直接计算纹理坐标。
  • 自动参数化算法:这是计算机图形学中的一个研究领域。目标是通过算法自动计算失真最小的映射。一个经典算法是 Tutte参数化,它要求每个内部顶点的UV坐标是其所有邻接顶点坐标的平均值,并固定边界顶点在一个凸多边形上,通过求解线性系统得到结果。

自动参数化是一个难题,因为它需要在保持映射单射性(无三角形重叠)的同时,最小化纹理的拉伸和扭曲。现代研究致力于在切割长度和内部失真之间寻找最佳平衡。


超越颜色:其他信息的纹理映射

纹理贴图不仅可以存储颜色信息,还可以存储任何与表面着色相关的参数。

  • 高光贴图:存储不同区域的高光强度系数,使物体表面有些部分闪亮,有些部分暗淡。
  • 法线贴图:这是一种极其重要的技术。它不在纹理中存储颜色,而是存储法向量的扰动。这使得我们可以在不增加几何顶点的情况下,通过改变着色计算用的法线,模拟出表面的微小凹凸细节,极大地丰富了视觉细节。

程序化渲染与着色器 ⚙️

纹理映射需要预先存储图像,另一种强大的方法是程序化渲染:通过编写一小段代码,在渲染时动态计算每个点的颜色或材质属性。

这段在渲染时被频繁调用的代码被称为着色器。着色器是实时图形编程(如OpenGL)的核心概念。

程序化渲染的优缺点

优点

  • 无限分辨率:由于是数学计算,放大时不会出现像素块。
  • 紧凑:不需要存储大型纹理图像。
  • 易于生成重复或变化模式:如棋盘格、砖墙。

缺点

  • 需要编程:创作特定纹理可能比绘画更复杂。
  • 难以匹配真实照片

柏林噪声:程序化纹理的基石

柏林噪声是一种经典的程序化伪随机函数,它能产生看起来“随机”但又有一定空间连贯性的值。它是许多自然纹理(云、大理石、木材纹理)的构建基础。

其核心思想是:在整数格点上生成随机梯度向量,然后在格点之间使用三次插值(如埃尔米特插值)进行平滑。通过将不同“八度”(不同频率)的柏林噪声叠加,可以创造出非常复杂的自然图案。


总结

本节课中我们一起学习了如何为三维模型表面添加空间变化的细节。

  • 我们掌握了纹理映射的核心流程:通过UV坐标将二维图像包裹到三维表面,并在渲染时进行纹理查询和过滤。
  • 我们了解了获取纹理坐标(参数化)的挑战与方法,包括手工制作和自动算法。
  • 我们认识到纹理不仅可以存储颜色,还可以存储法线、高光等任何着色参数。
  • 我们引入了程序化渲染的概念和着色器,学习了如何通过代码(如使用柏林噪声)动态生成纹理,这提供了极大的灵活性和无限分辨率。

通过结合精确的几何、真实的材质BRDF以及本节学习的丰富纹理细节,我们构建逼真计算机图形图像的能力得到了极大的增强。下一讲,我们将深入探讨纹理采样中的走样问题及其解决方案。

015:抗锯齿 🎨

在本节课中,我们将要学习计算机图形学中一个至关重要且无处不在的主题:抗锯齿。我们将探讨为什么在渲染图像时会出现锯齿状的边缘和奇怪的视觉伪影,并学习如何利用采样和重建的理论来减轻这些影响。虽然完整的理论需要深入的傅里叶分析知识,但我们将通过直观的图示和核心概念来理解其基本原理。

概述:采样与重建的问题

到目前为止,在我们的渲染过程中,我们通常通过向每个像素中心发射一条光线来确定其颜色。然而,现实世界是连续的,而我们的显示设备(像素网格)是离散的。当一个尖锐的边缘或高频率的纹理落在像素之间时,这种简单的“点采样”方法就会失败,导致图像出现锯齿状的边缘(称为“走样”或“锯齿”),或者产生其他奇怪的视觉伪影,如摩尔纹。

本节课的核心在于理解:抗锯齿本质上是一个采样和重建的问题。我们需要决定如何从连续的场景中获取有限数量的样本(采样),以及如何根据这些样本重建出最终的图像(重建)。这两个步骤都可能引入伪影。

锯齿现象无处不在

在深入理论之前,让我们看看锯齿现象在图形学和其他领域中的多种表现形式:

  • 渲染中的锯齿边缘:在低分辨率下渲染的球体边缘会出现明显的阶梯状。
  • 纹理映射中的走样:当纹理细节过于丰富,而屏幕像素无法充分采样时,会产生随机噪点般的伪影。
  • 光栅化直线的“楼梯”效应:绘制非水平或垂直的直线时,会呈现锯齿状。
  • 数码摄影中的摩尔纹:拍摄具有精细规则图案(如砖墙)的物体时,图像中会出现低频的波纹图案。
  • “车轮效应”:在视频中,高速旋转的车轮看起来可能转得很慢甚至向后转。这是因为相机的采样频率(帧率)与车轮的旋转频率不匹配,导致了频率混叠。

所有这些现象的共同点在于:我们试图用一个离散的系统(像素网格、相机传感器)去表示一个连续的世界,当系统无法捕捉到足够的高频信息时,就会产生视觉上的错误

核心概念:采样、重建与傅里叶变换

为了系统地解决这个问题,我们需要一套数学语言来描述信号的“频率内容”。这就是傅里叶变换

傅里叶变换简介

傅里叶变换的核心思想是:任何函数(比如我们的图像亮度分布)都可以分解成一系列不同频率的正弦波和余弦波的叠加。这与泰勒级数用多项式来逼近函数的思想类似,只不过基函数换成了正弦和余弦。

公式表示:对于一个空间域函数 f(x),其傅里叶变换 F(ξ)(其中 ξ 代表频率)大致可以理解为 f(x) 与频率为 ξ 的复指数函数 e^(i2πξx) 的“内积”,它衡量了该频率成分在 f(x) 中的含量。
F(ξ) = ∫ f(x) * e^(-i2πξx) dx

直观理解

  • 空间域:我们看到的图像,xy 是空间坐标。
  • 频率域:图像经过傅里叶变换后的表示,它告诉我们图像中包含了哪些频率的成分。平滑变化的区域对应低频(集中在频率域中心),尖锐的边缘和精细的纹理对应高频(分布在频率域外围)。

采样在频率域的影响

采样在数学上可以看作是用一个脉冲序列(在采样点处有值,其他地方为0)乘以原始连续函数。傅里叶变换的一个关键性质是:空间域中的相乘,对应于频率域中的卷积

核心结论:当我们以一定的间隔对连续信号进行采样时,其频率域表示 F(ξ) 会被周期性地重复和叠加。采样间隔越宽(采样率越低),这些重复的副本在频率域中就靠得越近。

理想重建与奈奎斯特-香农采样定理

如果原始信号是带限的,即其频率成分有一个上限(比如最高频率为 B),那么只要我们以高于 2B 的频率进行采样(即奈奎斯特率),在频率域中,这些重复的副本就不会发生重叠。

在这种情况下,我们可以通过一个完美的重建滤波器来恢复原始信号:在频率域中,我们只需用一个矩形函数(在 -BB 之间为1,之外为0)乘以采样后的频谱,滤掉所有多余的副本,然后进行逆傅里叶变换,就能得到原始信号。

在空间域中,这个理想的矩形滤波器对应的是 sinc 函数sinc(x) = sin(πx) / (πx)
因此,理论上完美的重建是通过用 sinc 函数对采样点进行卷积来实现的。

现实挑战与实用抗锯齿技术

然而,现实世界的图像(尤其是带有锐利边缘的图像)并非带限信号,它们包含无限的高频成分。此外,sinc 函数是无限延伸且包含负值的,这在计算和显示上都不现实(像素颜色不能为负)。

因此,在实践中,我们采用各种近似和启发式方法来达到可接受的效果。以下是常见的抗锯齿技术:

1. 超采样 (Supersampling)

这是最直观的方法。我们不再只向每个像素中心发射一条光线,而是在像素内规则地(如网格)或随机地(抖动采样)发射多条光线,然后将这些光线的颜色结果进行平均,作为该像素的最终颜色。

  • 均匀网格超采样:在像素内进行 NxN 的规则采样。这相当于使用了一个盒式滤波器进行重建。虽然简单,但可能无法完全消除某些高频伪影。
  • 抖动采样 (Jittering):随机扰动采样点的位置。这并不能提高信号的频率上限,但可以将规则的结构性伪影(如摩尔纹)转化为不那么显眼的随机噪声,人眼对后者更不敏感。

2. 改进的重建滤波器

我们不使用理想的 sinc 函数,而是使用具有有限支撑(只在有限范围内非零)且值全为正的滤波器来对超采样的样本进行加权平均。

  • 高斯滤波器:权重从中心向四周衰减。
  • Mitchell-Netravali 滤波器:一种基于三次样条的滤波器,在质量和计算成本之间取得了很好的平衡,是实践中常用的选择。

选择不同的滤波器,会在图像锐度和平滑度之间进行不同的权衡。

3. 纹理抗锯齿与 Mipmapping

纹理映射是抗锯齿的一个特例和难点。当纹理被缩小显示(Minification)时,一个像素可能覆盖纹理上的一大片区域,如果只用纹理上的一个点来代表,就会导致严重的走样。

Mipmapping 是解决此问题的经典技术:

  • 原理:预先计算并存储原始纹理的一系列下采样版本(形成 Mipmap 链)。在渲染时,根据像素在纹理平面上所覆盖区域的大小,自动选择合适的 Mipmap 层级进行查找。
  • 优点:极大地提高了纹理缩小时的渲染质量和速度。
  • 局限性:对于各向异性拉伸的纹理(如极度倾斜的表面),标准的 Mipmapping 可能仍会产生模糊。更高级的技术如各向异性过滤会考虑像素覆盖区域的形状,使用椭圆形的滤波器进行查找。

4. 特殊应用:字体渲染 (ClearType)

字体由许多极细的线条和曲线构成,对锯齿非常敏感。ClearType 等技术利用了大多数液晶显示屏的物理特性:每个像素由红、绿、蓝三个子像素并列组成。

  • 原理:不是以整个像素为单位进行抗锯齿,而是以子像素为单位独立控制亮度。这相当于将水平方向的分辨率提高了三倍,从而能够渲染出更平滑、更清晰的字体边缘。
  • 注意:这种技术高度依赖于显示设备的子像素排列方式。

总结

本节课我们一起学习了计算机图形学中抗锯齿的核心思想。我们了解到:

  1. 锯齿的根源在于用离散的采样系统去表示连续的信号,当采样率不足以捕捉信号中的高频成分时,就会产生走样。
  2. 傅里叶变换为我们提供了在频率域分析此问题的强大工具。理想情况下,对带限信号以奈奎斯特率采样,并用 sinc 函数重建,可以完美复原信号。
  3. 现实妥协:由于图像非带限且 sinc 函数不实用,我们发展了一系列实用技术,包括超采样、使用正值的重建滤波器、针对纹理的 Mipmapping 以及针对字体的子像素渲染等。

抗锯齿是渲染中平衡质量与性能的关键环节。理解其背后的原理,将帮助你在未来使用或开发图形系统时,做出更明智的选择。

016:全局光照

在本节课中,我们将学习全局光照的概念。我们将探讨为什么传统的反向光线追踪无法捕捉某些光照效果,并介绍几种用于模拟全局光照的先进算法,例如路径追踪和光子映射。最后,我们将讨论如何通过重要性采样等蒙特卡洛积分技术来提高渲染效率。


全局光照的动机

上一节我们介绍了光线追踪的基本原理。本节中,我们来看看为什么传统方法在某些情况下会失效。

光线追踪算法并非模拟真实的物理过程。它从眼睛反向追踪光线到光源,利用光路可逆的对称性。然而,某些光照效果,例如透明物体产生的焦散或复杂反射,其光路非常复杂,难以通过反向追踪有效计算。

例如,考虑一个透明立方体将光线聚焦到平面上形成亮斑(焦散)。对于平面上的这个亮点,反向光线追踪需要找到一条穿过立方体内部并最终到达光源的复杂路径,这在计算上是不可行的。

因此,我们需要引入“全局光照”技术,它不仅考虑从光源直接到达表面的光线,还考虑光线在场景中所有表面之间多次反弹后间接到达的光线。


渲染方程

为了从理论上理解全局光照,我们引入一个核心概念:渲染方程。它描述了场景中任意一点向任意方向出射的光亮度。

对于表面上的一点 x,沿观察方向 v 的出射光亮度 L_out(x, v) 由两部分组成:

  1. 该点自身发射的光亮度 L_e(x, v)(如果是光源)。
  2. 从所有可能入射方向 l 到达该点的光亮度 L_in(x, l),经表面材质(由双向反射分布函数 f_r 描述)反射后,贡献到方向 v 的部分。

这可以用一个积分方程表示:

L_out(x, v) = L_e(x, v) + ∫_Ω f_r(x, l, v) * L_in(x, l) * cos(θ) dω

其中:

  • Ω 是以点 x 法线为中心的半球空间。
  • θ 是入射方向 l 与法线的夹角。
  • cos(θ) 项考虑了光线入射角度的影响。

这个方程的关键在于 L_in(x, l) 本身通常是场景中另一点 yx 点方向出射的光亮度 L_out(y, -l)。因此,渲染方程实际上是一个庞大的、相互关联的积分方程组,描述了场景中所有点的光照相互依赖关系。直接求解这个系统非常困难。


蒙特卡洛光线追踪与路径追踪

由于无法解析求解渲染方程,我们转向近似方法。核心思想是使用蒙特卡洛积分来估算方程中的积分项。

蒙特卡洛光线追踪 的基本步骤如下:

  1. 从相机发射一条光线,与场景相交于点 x
  2. 为了计算 x 点出射到相机的光亮度,我们需要估算半球积分。我们随机在半球上采样多个方向,并沿这些方向发射新的光线(递归过程)。
  3. 当递归达到一定深度(例如,光线反弹了3次)后,我们停止递归,并直接计算该点接收到的直接光照(即向光源发射阴影光线)。
  4. 将所有采样方向返回的光亮度值平均,作为该积分项的估计。

这种方法的问题是计算量巨大,因为每个递归步骤都需要大量采样,导致计算量指数级增长。

路径追踪 是蒙特卡洛光线追踪的一种高效变体。其核心区别在于:

  • 它不再试图在每个交点都精确计算半球积分。
  • 相反,从相机出发的每条光线(路径),在每次与表面相交时,只随机选择一个方向继续传播。
  • 这样,每条路径只对像素颜色贡献一个“猜测”值。
  • 然后,我们为每个像素发射大量(例如,数百条)这样的独立路径,并将它们的颜色结果平均起来。

路径追踪通过将计算冗余全部放在最顶层的像素采样上,避免了递归层级的计算爆炸,在实践中更为常用。然而,由于采样随机性,图像中会产生噪声(“散斑噪声”),需要大量采样才能平滑。


加速技术:辐照度缓存与光子映射

为了减少路径追踪所需的采样数量,人们开发了多种加速技术。它们的基本观察是:间接光照(即非直接来自光源的光照)通常在空间上变化缓慢。

辐照度缓存 利用了屏幕空间的连贯性:

  • 在渲染时,我们只在稀疏的一组像素点上进行完整的间接光照计算(路径追踪),并将结果(辐照度值)缓存起来。
  • 对于其他像素,我们检查其邻近的、已计算过的缓存点。
  • 如果找到几何位置和法线方向相似的缓存点,我们就插值使用它们的辐照度值,而不是重新计算。
  • 这大大减少了需要完整路径追踪的像素数量。

光子映射 则采用了不同的策略,在场景空间(而非屏幕空间)进行缓存:

  • 第一步(光子追踪):从光源向场景发射大量“光子”,并让它们在场景中反弹。每当光子在某个表面点被反射或折射时,我们就在该位置记录一个光子信息(包括位置、入射方向和能量)。
  • 第二步(渲染):进行常规的光线追踪(从相机出发)。当需要计算某点的间接光照时,我们就在其周围空间内搜索邻近的光子记录。
  • 这些光子的密度和能量被用来估算该点接收到的间接光照。

光子映射特别擅长捕捉焦散等复杂的光路效果。为了高效地进行邻近光子搜索,通常需要使用空间加速数据结构,如KD树。


提高采样效率:重要性采样

无论是路径追踪还是其他蒙特卡洛积分,采样策略都直接影响效率和图像质量。目前我们采用的是均匀随机采样,但这可能不是最优的。

核心问题在于,被积函数(例如BRDF)在某些区域的值可能远大于其他区域。均匀采样会浪费大量样本在贡献很小的区域。

重要性采样 的思想是:让样本的分布概率 p(x) 尽可能与被积函数 f(x) 的形状相似。这样,更多的样本会落在函数值大的重要区域。

以下是实施重要性采样的步骤:

  1. 根据一个与被积函数形状相似的概率密度函数 p(x) 来生成样本。
  2. 在计算蒙特卡洛估计时,每个样本的贡献需要除以 p(x) 来进行修正,以保证估计的无偏性。

修正后的蒙特卡洛估计公式为:
估计值 ≈ (1/N) * Σ [ f(x_i) / p(x_i) ]

在渲染中,一个常见的做法是根据BRDF函数本身来采样入射光方向,这样更多的光线会射向BRDF值高的方向(即材质最可能反射的方向),从而更有效地估算积分。

另一种辅助技术是分层采样,它试图结合均匀采样和随机采样的优点,例如将积分域划分为均匀的网格,然后在每个网格内进行随机采样,以确保样本在空间上分布得更均匀,避免出现某些区域完全没有样本的极端情况。


总结

本节课中我们一起学习了全局光照的概念和主要算法。

  • 我们首先了解了传统反向光线追踪的局限性,并引入了渲染方程作为描述全局光照的理论框架。
  • 为了求解这个方程,我们介绍了蒙特卡洛积分的基本思想,并由此衍生出路径追踪算法,它通过随机采样光路来近似求解。
  • 为了加速计算,我们探讨了两种缓存技术:辐照度缓存(利用屏幕空间连贯性)和光子映射(利用场景空间的光子分布)。
  • 最后,我们讨论了如何通过重要性采样分层采样等策略来更有效地利用采样点,从而在相同采样数下获得更清晰、噪声更少的图像。

掌握这些知识后,你便具备了实现一个高质量(但可能较慢)光线追踪器的理论基础。现代渲染器正是将这些技术以各种复杂的方式结合,以在质量和速度之间取得最佳平衡。

017:光栅化 🖥️

在本节课中,我们将要学习实时图形学的基础——光栅化。我们将探讨光栅化与光线追踪的核心区别,并详细介绍光栅化管线的各个步骤,包括投影、扫描转换、片段生成、着色以及深度缓冲。


概述

到目前为止,我们的课程主要关注如何生成高质量的图像,即使牺牲计算效率。现在,我们将进入一个截然不同的领域:实时图形学。在这里,我们必须在严格的时间预算内(例如每秒30帧)生成图像,否则用户体验将受到严重影响。为了实现这一目标,我们将引入一种新的算法——光栅化。

光栅化与光线追踪的区别

上一节我们介绍了实时图形学的挑战,本节中我们来看看光栅化与光线追踪的核心区别。

从宏观且略带调侃的角度看,两者的区别非常简单。如果你有一个非常基础的光线投射器,想把它变成一个非常基础的光栅化器,只需改变一行代码:交换两个循环的顺序

  • 光线投射(Ray Casting):外层循环遍历图像中的每个像素,内层循环遍历场景中的每个三角形,寻找与光线相交且最近的三角形。
    for each pixel in image:
        for each triangle in scene:
            if ray intersects triangle and is closest so far:
                shade pixel
    
  • 光栅化(Rasterization):外层循环遍历场景中的每个三角形,内层循环(或类似过程)确定该三角形覆盖了哪些像素,然后进行着色。
    for each triangle in scene:
        for each pixel covered by triangle:
            shade pixel (if visible)
    

本质上,光线投射是为每个像素寻找最近的物体,而光栅化则是逐个三角形地进行“绘制”。然而,当我们开始为这两种方法添加效率优化时,它们的优化策略将变得非常不同。

光栅化管线

了解了基本区别后,我们来详细看看典型的光栅化管线包含哪些步骤。这些步骤大致遵循从几何处理到最终像素着色的逻辑顺序。

以下是光栅化管线的主要步骤:

  1. 投影(Projection):将三角形的3D顶点坐标投影到2D图像平面上。
  2. 扫描转换(Scan Conversion):确定投影后的三角形覆盖了图像平面上的哪些像素(生成片段)。
  3. 片段着色(Fragment Shading):为每个生成的片段计算颜色(例如,基于材质、光照、纹理)。
  4. 可见性测试与帧缓冲更新(Visibility Test & Frame Buffer Update):使用深度缓冲(Z-Buffer)判断片段是否可见,并更新最终图像。

需要注意的是,整个管线在一个巨大的循环中执行,该循环遍历场景中的所有三角形。

1. 投影

我们首先需要知道三角形在屏幕上的位置。投影步骤将三角形的3D顶点映射到2D图像坐标。

一个有用的数学性质是:一个3D三角形经过(标准的透视或正交)投影后,在图像平面上仍然是一个2D三角形。这使得后续的扫描转换可以在2D平面上进行,简化了计算。

我们之前课程中介绍的投影矩阵(透视投影)形式如下,它将点 \((X, Y, Z, 1)\) 变换到齐次坐标 \((x, y, z, w)\)

\[\begin{bmatrix} x \\ y \\ z \\ w \end{bmatrix} = \mathbf{P} \begin{bmatrix} X \\ Y \\ Z \\ 1 \end{bmatrix} \]

进行透视除法(除以 \(w\) )后,我们得到标准化设备坐标 (NDC)。在光栅化中,我们不仅需要 \((x/w, y/w)\) 来确定屏幕位置,还需要保留深度信息(通常与 \(z/w\)\(1/w\) 相关)用于后续的深度测试。因此,投影矩阵 \(\mathbf{P}\) 会被设计成在变换后保留必要的深度信息。

视景体(View Frustum) 是指摄像机可见的3D空间区域,通常是一个平头锥体。投影矩阵的一个作用就是将视景体映射到一个规范化的立方体(如 \([-1,1]^3\)),方便后续裁剪和计算。我们需要设定近裁剪平面(near)和远裁剪平面(far)来限定深度范围,以避免数值精度问题(如Z-fighting)。

2. 扫描转换与片段生成

投影之后,我们得到了一个2D屏幕空间中的三角形。扫描转换的任务是找出所有位于这个三角形内部的像素。

一个简单直接的方法是使用包围盒(Bounding Box)策略:

  1. 计算三角形在屏幕上的轴向包围盒。
  2. 遍历包围盒内的每一个像素。
  3. 对每个像素,检查它是否在三角形三条边所定义的半平面内(即同时满足三个边的半平面不等式)。如果满足,则为该三角形在此像素位置生成一个片段(Fragment)

片段与像素不同。一个像素是屏幕上的一个固定位置。而一个片段是在光栅化某个特定图元(如三角形)时,在该像素位置生成的潜在颜色和深度数据。同一个像素位置可能因为多个重叠的三角形而生成多个片段。

为了提高效率,还有更高级的扫描转换算法,例如:

  • 扫描线算法:逐行处理,利用边的连贯性。
  • 层次化光栅化:先以低分辨率测试大块区域,只对可能包含三角形部分的区域进行细化处理。

裁剪(Clipping) 是一个相关的重要步骤。在投影后,我们需要处理那些部分或完全位于视景体之外的三角形(例如,在摄像机后面的三角形)。裁剪算法将这些三角形切割,只保留位于视景体内的部分,并可能将其细分为多个更小的三角形。这避免了处理退化情况(如除以零)并提升了效率。

另一种避免裁剪某些退化情况的方法是齐次坐标光栅化(Homogeneous Rasterization)。它不在2D屏幕空间进行内外测试,而是在3D齐次空间中,利用通过摄像机点和三角形各边的平面来定义测试条件。这样可以自然地处理摄像机后方的图元。

3. 片段着色

一旦我们有了一个片段(即知道某个三角形可能覆盖某个像素),就需要计算它的颜色。这包括计算光照、应用纹理等。这个计算过程与我们在光线追踪中进行的局部着色计算非常相似,可以高度并行化,通常在片段着色器(Fragment Shader) 中执行。

4. 可见性测试:深度缓冲(Z-Buffer)

由于我们是逐个三角形绘制的,后绘制的三角形可能会覆盖先绘制的。为了正确处理遮挡关系,我们需要知道每个像素处哪个片段离摄像机最近。

深度缓冲算法是解决这个问题的经典且高效的方法:

  • 我们维护两个与屏幕分辨率相同的缓冲区:
    • 帧缓冲(Frame Buffer):存储每个像素的最终RGB颜色。
    • 深度缓冲(Depth Buffer / Z-Buffer):存储每个像素当前最浅(离摄像机最近)的深度值。
  • 当为一个片段计算好颜色和深度值后:
    1. 检查该片段所在像素位置的当前深度缓冲值。
    2. 如果该片段的深度值小于当前深度缓冲值(即更靠近摄像机),则:
      • 用该片段的颜色更新帧缓冲。
      • 用该片段的深度值更新深度缓冲。
    3. 否则,丢弃该片段。

深度缓冲算法简单、高效,并且能正确处理任何复杂的几何交错情况。在硬件图形管线中,深度测试通常是一个固定功能阶段或在片段着色器之后可配置的阶段。

与之对比的是画家算法(Painter‘s Algorithm),即按物体从远到近的顺序绘制。这种方法无法处理物体循环遮挡的情况,而深度缓冲则没有这个限制。

总结与展望

本节课中我们一起学习了实时图形学的核心——光栅化。我们首先通过交换循环顺序这个思想实验理解了光栅化与光线追踪的根本区别。然后,我们系统地介绍了光栅化管线:从3D顶点投影到2D屏幕,通过扫描转换确定三角形覆盖的像素(生成片段),为片段计算颜色,最后利用深度缓冲解决可见性问题以确保最终图像的遮挡关系正确。

光栅化的优势在于其易于并行化、内存访问模式规律,适合硬件实现。但其挑战在于实现全局光照效果(如反射、折射)更为复杂,通常需要特殊的技巧(如阴影映射、屏幕空间反射等)。

下一讲,我们将探讨一个关键的技术细节:透视校正插值(Perspective-Correct Interpolation)。我们将看到,在屏幕空间中对顶点属性(如纹理坐标、法线)进行简单的线性插值是不正确的,必须进行透视校正才能得到准确的结果,这对于纹理映射等效果至关重要。

018:光栅化 II

在本节课中,我们将继续学习光栅化技术。我们将深入探讨如何正确地在三角形内部进行插值计算,特别是处理透视投影带来的影响。我们还将介绍抗锯齿和纹理映射中的一些高级技术,以提高渲染图像的质量和效率。


透视校正插值

上一节我们介绍了光栅化的基本流程。本节中我们来看看一个关键细节:如何为屏幕上生成的每个片段(Fragment)计算其正确的三维属性,例如深度(Z值)和纹理坐标。

核心问题在于,一个三角形在三维空间中的点与其在二维屏幕上的投影点之间,重心坐标(Barycentric Coordinates)并不相同。如果我们简单地使用屏幕空间的重心坐标去插值三维属性(如纹理坐标),就会得到错误的结果,这被称为“仿射插值”(Affine Interpolation)或“古罗插值”(Gouraud Interpolation),在透视场景下是不正确的。

我们需要的是透视校正插值(Perspective-Correct Interpolation)。其核心思想是:在齐次坐标(Homogeneous Coordinates)下,插值是线性的;但在进行透视除法(除以W分量)后,关系就变成了非线性的。因此,我们需要逆向推导出屏幕空间片段对应的三维空间重心坐标。

以下是推导过程的核心步骤:

  1. 设三维空间中三角形的顶点为 A, B, C。空间内任意点 P 可表示为:
    P = αA + βB + γC,其中 α + β + γ = 1
  2. P 乘以相机投影矩阵 C,得到齐次裁剪空间坐标:
    P' = C * P = α(C*A) + β(C*B) + γ(C*C) = αA' + βB' + γC'
  3. 进行透视除法,得到屏幕坐标 (x, y)
    (x, y) = (P'x / P'w, P'y / P'w)
  4. 我们的目标是:已知屏幕坐标 (x, y),求对应的 (α, β, γ)。通过建立方程并利用 α + β + γ = 1 的条件,可以推导出:
    [α, β, γ]^T ∝ M^-1 * [x, y, 1]^T
    其中矩阵 MA', B', C' 的齐次坐标构成。
  5. 最后,将得到的向量归一化(使其分量和为1),即可得到正确的三维重心坐标 (α, β, γ)

得到正确的重心坐标后,我们就可以用它来线性插值任何顶点属性,包括深度(用于Z-Buffer)、颜色、法线、纹理坐标等。


抗锯齿技术

在光线追踪中,我们讨论了抗锯齿。在光栅化管线中,抗锯齿策略有所不同,因为我们需要高效地处理大量片段。

超采样

最简单的策略是超采样(Supersampling)。其原理是:

  1. 以高于最终显示分辨率(例如2倍或4倍)的尺寸渲染整个场景。
  2. 再将这幅大图下采样(例如取像素块的平均值)到目标分辨率。

这种方法计算开销很大,因为着色计算量随采样数线性增长。

多重采样

一种更高效的策略是多重采样抗锯齿(Multisample Anti-Aliasing, MSAA)。其核心观察是:锯齿主要发生在几何图形的边缘。而三角形内部的着色(如漫反射)通常是平滑变化的。

MSAA 的工作流程如下:

  1. 片段生成在子像素级别进行。例如,将一个像素划分为 4x4 的子采样点。
  2. 着色计算仅在像素级别进行一次(通常使用像素中心)。
  3. 对于该像素覆盖的所有子采样点,都赋予同一个着色结果。
  4. 最终像素颜色是其所覆盖的所有子采样点颜色的混合(根据覆盖率)。

这样,我们以一次着色的代价,实现了对几何边缘的抗锯齿,大幅提升了效率。但这种方法对于高频纹理内部的锯齿效果有限。


纹理映射与Mipmapping

纹理映射是另一个锯齿的主要来源,尤其是当纹理被缩小(Minification)时,一个像素可能覆盖纹理上的大片区域,导致严重的走样。

Mipmap 链

解决方案是使用 Mipmap。其核心思想是预计算并存储纹理的一系列缩小版本(通常每次尺寸减半),形成一个图像金字塔。

以下是使用 Mipmap 的基本步骤:

  1. 在渲染时,根据当前片段在纹理空间中覆盖区域的大小,估算所需的细节层级(LOD, Level of Detail)。
  2. 一个简单的 LOD 估算公式是:D = log2( max( |dU/dx|, |dV/dx|, |dU/dy|, |dV/dy| ) ),其中导数表示了屏幕像素变化对应的纹理坐标变化率。
  3. 根据估算出的连续 LOD 值,在两个最接近的 Mipmap 层级之间进行三线性插值(Trilinear Filtering):
    • 在每一层 Mipmap 上进行双线性插值(Bilinear Filtering)。
    • 再在两个层级的结果之间进行线性插值。

各向异性过滤

标准的 Mipmap 假设像素在纹理空间中的投影区域是近似圆形的。但当表面与视线方向夹角很大时,投影区域会变成狭长的椭圆形,此时使用圆形滤波会导致过度模糊。

各向异性过滤(Anisotropic Filtering)尝试解决这个问题。一种常见方法是在椭圆形的投影区域内,沿其长轴方向采集多个圆形的 Mipmap 采样,然后进行加权平均,从而更好地逼近椭圆滤波器的效果。

在硬件实现中,纹理坐标的偏导数(dU/dx, dV/dx 等)通常通过相邻片段之间的差分来近似计算,而非解析求解。


光栅化管线优化策略

为了提高实时渲染的效率,人们发展出了多种多通道渲染策略:

  • 延迟着色:第一遍通道(Geometry Pass)只渲染场景的几何信息(位置、法线、材质参数等)到多个缓冲区(G-Buffer),不进行光照计算。第二遍通道(Lighting Pass)利用 G-Buffer 中的信息,在屏幕空间进行光照计算。这避免了被遮挡物体的无效着色。
  • 提前深度测试:第一遍通道只写入深度缓冲(Z-Prepass)。第二遍通道进行完整的渲染,此时大部分被遮挡的片段会在早期被深度测试剔除,减少了着色器的调用。
  • 分块渲染:将屏幕分割成多个小块(Tile),依次渲染每个块。这有助于更好地利用缓存,在移动设备上尤其重要。

总结

本节课中我们一起学习了光栅化中的几个高级主题。
我们首先推导了透视校正插值的数学原理,这是正确计算片段深度和纹理坐标的基础。
接着,我们探讨了抗锯齿技术,比较了超采样和更高效的多重采样
然后,我们回顾了纹理映射中的走样问题,并深入介绍了Mipmap各向异性过滤这两种预滤波技术。
最后,我们简要了解了几种优化光栅化管线的多通道渲染策略。

这些技术共同构成了现代实时图形渲染的基石,在效率和质量之间取得了精妙的平衡。下一讲,我们将探讨实时阴影的渲染技术。

019:阴影映射

在本节课中,我们将要学习如何在光栅化渲染管线中实现阴影效果。我们将重点介绍一种称为“阴影映射”的主流技术,这是一种多通道渲染技术,需要从光源的视角渲染场景。我们还将简要了解其他阴影技术,如阴影体积和深度阴影映射。


课程概述与公告

上一节我们完成了期中考试。关于考试,请注意:成绩已公布,但请勿在公开论坛讨论试题内容。如有疑问,可通过 Piazza 私下联系助教。课程成绩的划分通常以90%为A档的基准线,并可能酌情调整以利于学生。

从本节开始,我们将进入课程的后半部分,涵盖一系列图形学中的其他主题。接下来的主要任务是完成关于阴影的作业五,以及后续的项目和测验。


为什么需要阴影?

在深入技术细节之前,让我们先思考为什么阴影在计算机图形学中如此重要。

阴影不仅仅是增加真实感的艺术效果。在缺乏双目立体视觉的二维屏幕上,阴影为我们理解三维场景的深度、物体间的相对顺序和接触关系提供了至关重要的视觉线索。

例如,一个简单的阴影变化可以完全改变我们对场景中球体是漂浮还是放置于平面上的感知。历史上,艺术家们也早已利用阴影来辅助绘画和增强画面的立体感。

因此,尽管计算阴影会增加开销,但它通常是渲染管线中优先实现的效果之一。


阴影映射的基本思想

在光线追踪中,实现阴影相对简单:从着色点向光源发射一条辅助光线,检查路径上是否有遮挡物。然而,这种方法与光栅化管线不兼容,因为光栅化通常以流式方式逐个处理三角形,无法在着色时查询整个场景的几何信息。

因此,我们需要一种全新的方法:阴影映射

阴影映射的核心思想基于一个关键的观察:一个点被光源照亮,当且仅当该点从光源的位置是可见的

这启发了我们一个巧妙的方法:复用我们已有的、用于从相机视角确定可见性的深度缓冲(Z-Buffer)技术。


阴影映射算法详解

阴影映射算法分为两个主要通道:

通道一:从光源视角渲染

首先,我们将相机“放置”在点光源的位置,并朝着我们关心的方向观察场景。然后,像普通渲染一样进行一次光栅化。但这次我们并不输出颜色,而是将计算得到的深度值存储在一张特殊的纹理中,这张纹理就称为 阴影贴图

公式:对于光源坐标系下的每个像素,阴影贴图存储的值是:
depth_light = distance(closest_surface_point, light_position)

这个深度图记录了从光源到其可见范围内最近物体的距离。

通道二:从相机视角渲染并应用阴影

接下来,我们切换回正常的相机视角,开始渲染最终图像。对于光栅化生成的每一个片段(Fragment),我们需要判断它是否处于阴影中。步骤如下:

  1. 计算片段在光源空间中的位置:利用片段的3D世界坐标和光源的视图投影矩阵,计算出该点在光源视角下的齐次坐标,并转换为归一化设备坐标(NDC)。
  2. 坐标变换:将NDC坐标(范围通常为[-1, 1])转换为纹理坐标(范围[0, 1]),以便从阴影贴图中采样。
  3. 深度比较
    • 从阴影贴图中采样,得到 depth_stored,即光源到该方向最近遮挡物的距离。
    • 计算当前片段到光源的实际距离 depth_current
    • 进行比较:如果 depth_current > depth_stored + bias,则说明当前片段位于某个遮挡物之后,应处于阴影中。否则,它被光源直接照亮。

代码 伪代码描述上述比较逻辑:

float shadow = 0.0;
vec4 lightSpacePos = lightProjectionMatrix * lightViewMatrix * vec4(worldPos, 1.0);
vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w; // 透视除法
projCoords = projCoords * 0.5 + 0.5; // 转换到 [0,1] 纹理空间
float closestDepth = texture(shadowMap, projCoords.xy).r;
float currentDepth = projCoords.z;
if(currentDepth > closestDepth + bias) {
    shadow = 1.0;
}

实现细节与挑战

在实现阴影映射时,需要注意以下几个关键问题和解决方案:

1. 自阴影(Surface Acne)与深度偏移(Bias)

由于数值精度和阴影贴图分辨率限制,直接比较深度可能导致表面出现条纹状的自阴影假象。解决方法是为 closestDepth 添加一个小的 偏移量(Bias)。但偏移量过大会导致阴影脱离接触点(Peter Panning现象)。需要仔细调整。

2. 阴影贴图分辨率与走样(Aliasing)

阴影贴图像素投影到场景中时,可能会被拉伸,导致阴影边缘出现明显的锯齿状块。这是因为阴影贴图的分辨率是固定的,但它在场景不同区域的投影密度不同。

以下是几种缓解方法:

  • 提高阴影贴图分辨率:最直接但最耗资源的方法。
  • 百分比渐近滤波(Percentage Closer Filtering, PCF):一种常用的软阴影技术。其核心不是对深度值进行滤波,而是对阴影测试的结果(0或1)进行滤波。通过在当前像素周围进行多次深度比较并平均结果,可以得到边缘柔化的阴影。
  • 级联阴影映射(Cascaded Shadow Maps):用于解决大场景中远近阴影质量不一的问题。它为光源创建多个不同覆盖范围、不同分辨率的阴影贴图(级联)。渲染时,根据片段到相机的距离,选择合适的级联贴图进行采样。

3. 光源类型

  • 聚光灯(Spotlight):非常适合阴影映射,其视锥体与相机视锥体类似。
  • 点光源(Point Light):需要向所有方向投射阴影。常用方法是立方体贴图阴影映射,即创建一个立方体贴图(6个面),分别从光源位置向6个方向渲染6张阴影贴图。

4. 坐标系统一

图形学中不同环节(如NDC、纹理采样)可能使用不同的坐标范围(如[-1,1] vs [0,1])。在算法中需要进行正确的转换,这是常见的错误来源。


其他阴影技术简介

除了阴影映射,还有其他值得了解的阴影技术。

阴影体积(Shadow Volumes)

这种方法基于几何体:为每个遮挡物构建一个延伸向光源方向的封闭体积(阴影体积)。判断一个点是否在阴影中,等价于判断该点是否位于任何阴影体积内部。

一个高效的实现利用了模板缓冲(Stencil Buffer) 和以下算法:

  1. 正常渲染场景,只写入深度缓冲。
  2. 渲染所有阴影体积的正面和背面。利用深度测试,当渲染的阴影体积面片比已有片段更近时,根据面片法向(朝向或背离相机)对模板值进行加或减操作。
  3. 再次渲染场景。此时,模板缓冲中非零值对应的像素即处于阴影中。

阴影体积的优点是可以产生像素级精度的硬阴影,不受分辨率限制。但计算开销大,复杂度与场景三角形数量相关,且需要处理相机位于阴影体积内的特殊情况。

深度阴影映射(Deep Shadow Maps)

主要用于体积介质(如烟雾、毛发)的阴影。传统的阴影贴图只存储一个深度值,而深度阴影贴图在每个像素存储一个可见性函数,记录了在不同深度上光能的衰减比例。渲染时,根据片段的深度进行查找和插值,得到精确的透光率。


总结

本节课我们一起学习了在光栅化渲染中实现阴影的核心技术。

  • 阴影映射 是当前实时渲染的主流方法,它通过从光源视角渲染深度图,并在主渲染通道中进行深度比较来决定阴影。我们讨论了其原理、实现步骤以及面临的自阴影走样等挑战和对应的解决方案(如BiasPCF级联阴影映射)。
  • 我们还简要了解了阴影体积深度阴影映射这两种技术,它们分别适用于需要极高精度阴影和体积介质渲染的特殊场景。

阴影计算是图形学中将简单概念转化为复杂工程实践的典型例子。掌握阴影映射,为你实现更逼真的实时渲染效果打下了坚实基础。请务必在作业五中实践这些概念。

020:颜色与感知 🎨

在本节课中,我们将学习计算机图形学中一个基础但至关重要的主题:颜色与人类视觉感知。我们将探讨光的物理特性、人眼如何感知颜色与亮度,以及这些知识如何影响显示技术和渲染算法的设计。


光的本质与颜色光谱 🌈

上一节我们介绍了图形学管线的基本流程,本节中我们来看看颜色这个基础概念。光是一种电磁波,我们肉眼可见的只是整个光谱中非常小的一部分,波长大约在400到700纳米之间。

在现实世界中,光是以连续光谱的形式存在的,这意味着有无限多种可能的“颜色”。然而,人眼感知颜色的方式并非直接接收整个光谱。


人眼的结构与感知机制 👁️

了解了光的基本特性后,我们来看看接收者——人眼。人眼的结构与相机类似,包含角膜、瞳孔、晶状体等部件,最终光线到达视网膜上的感光细胞。

视网膜上的感光细胞主要分为两类:

  • 视杆细胞:负责感知亮度,在弱光环境下更活跃。
  • 视锥细胞:负责感知颜色,在明亮环境下更活跃。

人眼拥有远比视锥细胞更多的视杆细胞,这意味着我们对亮度的变化比对颜色的变化更为敏感。这一事实直接影响了许多图形学算法的设计。

以下是利用此特性进行优化的一个例子:

# 伪代码:仅对亮度通道应用昂贵的图像滤镜以节省计算
def apply_filter_efficiently(image):
    # 1. 将RGB图像转换到YCbCr等分离亮度和颜色的色彩空间
    Y, Cb, Cr = convert_to_YCbCr(image)
    # 2. 仅对亮度通道Y应用复杂的滤镜(如双边滤波)
    Y_filtered = expensive_bilateral_filter(Y)
    # 3. 将处理后的亮度与原始颜色通道合并
    result = convert_to_RGB(Y_filtered, Cb, Cr)
    return result

三色视觉与同色异谱 👥

我们深入探讨负责颜色的视锥细胞。大多数人的视锥细胞分为三种,分别对短(蓝)、中(绿)、长(红)波长的光敏感,其敏感度曲线是重叠的。

当光线进入眼睛时,每种视锥细胞的兴奋程度可以近似看作光线能量分布 S(λ) 与该细胞敏感度曲线 C(λ) 的点积(积分):
Response = ∫ S(λ) * C(λ) dλ

最终,大脑接收到的关于颜色的信息仅仅是三个数字——三种视锥细胞的响应值,这被称为三刺激值。由于是从无限维的光谱空间投影到三维响应值,必然存在不同的光谱分布产生相同三刺激值的情况,这种现象称为同色异谱。这正是显示技术能够用有限种光源(如RGB)模拟自然界无数色彩的基础。


色彩合成与显示技术 🖥️

既然我们知道了同色异谱的原理,就可以理解显示技术如何工作。大多数显示屏采用加色合成,即通过混合红、绿、蓝三种基本色光来模拟目标颜色。

核心问题可以表述为一个线性代数运算:给定目标颜色的三刺激值 T_target,以及显示屏RGB三原色各自的光谱所对应的三刺激值矩阵 M_display,我们需要求解一个系数向量 c,使得:
M_display * c = T_target

国际照明委员会(CIE)通过颜色匹配实验建立了标准。显示设备能再现的所有颜色范围称为色域,通常用CIE色度图上的一个三角形区域来表示。为了覆盖更多颜色,一些高端显示器会加入第四种甚至更多原色。


色彩空间与亮度编码 🗺️

在实际应用中,我们需要不同的坐标系(色彩空间)来表示和操作颜色。

以下是几种常见的色彩空间:

  • CIE XYZ / LAB:基于人类视觉感知设计,力求感知均匀。
  • HSV/HSL:直观表示色调、饱和度和明度/亮度。
  • CMYK:用于印刷的减色模型,包含青、品红、黄和黑。

除了颜色,亮度的编码也至关重要。人眼对亮度比的敏感度高于对绝对亮度差的敏感度。因此,我们通常不会线性地存储亮度值,而是采用伽马编码。存储的像素值 V_stored 与实际亮度 L 的关系近似为:
V_stored = L^(1/γ)
其中γ通常约为2.2。这样可以在有限的数值范围(如0-255)内,为暗部区域分配更多比特,以匹配人眼的感知特性。


总结 📚

本节课中我们一起学习了颜色与感知的核心知识。我们从光的物理特性出发,了解了人眼通过视杆和视锥细胞感知亮度与颜色的生理机制,并揭示了三刺激值同色异谱现象是显示技术的理论基础。我们还探讨了不同的色彩空间伽马编码,它们都是为了更好地匹配人类视觉特性而对颜色信息进行的工程化处理。理解这些原理,能帮助我们在设计图形系统时,将计算资源更有效地投入到人眼真正能感知到的细节上。

021:图像处理 📸

在本节课中,我们将学习计算机图形学中的一个重要分支——图像处理。我们将探讨如何对图像进行各种操作,从简单的像素级调整到复杂的空间滤波,并了解这些技术在现实世界中的应用。

概述

图像处理是计算机图形学中一个广泛且重要的领域,它涉及对数字图像进行分析、增强和操作。从调整照片的亮度和对比度,到实现复杂的视觉效果(如模糊或边缘检测),图像处理技术无处不在。本节课我们将从基础概念开始,逐步介绍不同类型的图像处理操作。

图像合成与Alpha通道

上一节我们介绍了图像处理的基本概念,本节中我们来看看一个具体的应用:图像合成。这通常涉及将多个图像层组合成一个单一的图像。

为了实现合成,图像通常不仅包含红、绿、蓝(RGB)颜色通道,还包含一个第四通道——Alpha通道。Alpha通道表示像素的透明度。

核心公式:图像合成
对于前景图像颜色 C_F(带透明度 α)和背景图像颜色 C_B,合成后的颜色 C 计算公式为:
C = α * C_F + (1 - α) * C_B

α = 1 时,完全显示前景色;当 α = 0 时,完全显示背景色。为了效率,图像文件通常会存储预乘Alpha的值,即 α * C_F

图像合成的一个经典挑战是绿幕抠像。当主体(如人物)站在明亮的绿幕前时,绿幕反射的光线可能会照射到主体边缘(如肩膀),导致在合成时难以准确区分前景和背景,从而产生不自然的发光边缘或“咬边”现象。

像素级图像滤镜

了解了如何组合图像后,我们来看看如何修改单个图像。最简单的图像处理操作是像素级滤镜,即输出像素的颜色值仅取决于输入图像中同一位置的像素值。

这些操作非常适合在GPU上并行执行(SIMD操作),因为它们对每个像素应用相同的公式。

以下是几种常见的像素级滤镜:

  • 亮度调整:将每个颜色通道乘以一个常数。例如:新颜色 = 旧颜色 * 亮度系数。需要注意的是,系数过大会导致颜色值超过有效范围(如大于1),产生“过曝”区域。
  • 对比度增强:将图像的亮度范围映射到显示设备的整个动态范围。一种简单方法是进行线性拉伸:新颜色 = (旧颜色 - 最小值) / (最大值 - 最小值)
  • 灰度转换:将彩色图像转换为黑白图像。简单的方法是取RGB的平均值:灰度 = (R + G + B) / 3。更符合人眼感知的常用加权公式是:灰度 = 0.299*R + 0.587*G + 0.114*B

许多图像编辑软件允许用户通过绘制“转换曲线”来定义自定义的像素映射函数 F(输入颜色) -> 输出颜色,从而实现各种色调调整效果。

高动态范围成像与色调映射

现实世界中的亮度范围(动态范围)可能非常广,但相机传感器和显示设备的动态范围有限。这导致在拍摄高对比度场景(如室内有明亮窗户的教堂)时,难以在一张照片中同时保留亮部和暗部的细节。

解决这个问题的一种技术是曝光融合

  1. 用不同曝光时间拍摄同一场景的多张照片(一组欠曝、正常曝光和过曝的照片)。
  2. 从每张照片中选取细节最好的部分,融合成一张包含完整亮度信息的图像。

然而,这种方法对场景中的运动物体敏感,可能导致重影。

即使获得了高动态范围图像,我们仍需将其压缩到显示设备有限的动态范围内,这个过程称为色调映射。简单的线性缩放效果很差,会使图像看起来平淡、昏暗。因为人眼对亮度比(对数尺度)更敏感,而非绝对亮度。因此,更好的方法是在对数域进行压缩,然后再转换回来。

空间图像滤镜与卷积

像素级滤镜功能有限。更强大的图像处理涉及空间滤镜,即输出像素的颜色取决于输入图像中一个邻域内多个像素的值。最常见的操作是图像卷积

在卷积中,我们使用一个称为卷积核(或滤波器)的小矩阵(例如3x3)。计算输出图像中每个像素值时,将卷积核中心对准输入图像的相应像素,将核中的每个权重与覆盖的像素值相乘,然后将所有乘积相加。

核心概念:图像卷积
对于图像 I 和卷积核 K,在位置 (x, y) 的卷积结果 (I * K)(x, y) 为:
(I * K)(x, y) = Σ_i Σ_j I(x+i, y+j) * K(i, j)
其中求和范围覆盖卷积核 K 的所有位置。

常见的卷积核包括:

  • 模糊/均值滤波:核内所有权重为正且和为1,例如 [[1/9, 1/9, 1/9], [1/9, 1/9, 1/9], [1/9, 1/9, 1/9]]
  • 边缘检测(拉普拉斯算子):突出像素值与周围邻居的差异,例如 [[0, -1, 0], [-1, 4, -1], [0, -1, 0]]

直接实现卷积的复杂度是 O(n² * m²)(图像尺寸为 n x n,核尺寸为 m x m)。对于某些特殊核,有快速算法:

  1. 可分离性:如果二维卷积核可以表示为两个一维向量的外积(如高斯核),则可以先后进行水平和垂直方向的一维卷积,将复杂度降为 O(n² * m)
  2. 盒式滤波与中心极限定理:连续多次应用简单的盒式滤波(取邻域均值),其结果会趋近于高斯模糊。而盒式滤波可以通过滑动窗口累加的方式高效实现,每次更新输出像素值只需做一次减法和一次加法,复杂度为 O(n²)

高级与非线性的空间滤镜

标准的卷积是线性且空间不变的,这意味着它平等地处理图像中的所有区域,无法区分边缘和纹理。而许多视觉任务需要更智能的滤镜。

以下是两种重要的非线性空间滤镜:

  • 反锐化掩模:用于增强图像边缘。其步骤为:

    1. 将原图 I 进行高斯模糊,得到 I_blur
    2. 计算细节层:细节 = I - I_blur
    3. 将细节层乘以一个系数后加回原图:I_sharp = I + 系数 * 细节
      如果系数过大,可能在边缘处产生“光晕”伪影。
  • 双边滤波:一种能在平滑图像(去噪)的同时保持边缘的滤波器。它的权重不仅取决于像素之间的空间距离,还取决于像素值的相似度(颜色距离)。
    核心思想:一个像素只与它空间上邻近颜色相似的像素进行平均。这避免了跨越边缘的模糊,但计算成本比普通卷积高。

  • 中值滤波:用邻域内所有像素颜色的中值替换中心像素的颜色。它对椒盐噪声等异常值非常有效,因为中值不受极端值影响。

总结

本节课我们一起学习了计算机图形学中的图像处理基础。我们从图像合成和Alpha通道开始,理解了如何组合多层图像。然后,我们探讨了简单的像素级滤镜,如亮度、对比度调整和灰度转换。接着,我们介绍了处理高动态范围场景的挑战以及曝光融合和色调映射技术。

课程的重点转向了空间滤镜,详细解释了图像卷积的原理、常见核及其应用(如模糊和边缘检测)。我们还了解了实现快速卷积的算法技巧,如利用可分离性和中心极限定理。最后,我们简要介绍了更高级的非线性空间滤镜,如反锐化掩模、双边滤波和中值滤波,它们能够更好地理解图像内容,实现更智能的处理效果。

图像处理是一个深邃而实用的领域,本节课的内容仅为入门。希望这些知识能帮助你理解日常所用图像工具背后的原理,并激发你进一步探索的兴趣。

022:输出设备

在本节课中,我们将学习计算机图形学流程的最后一步:输出设备。我们将探讨各种2D和3D显示技术的基本原理,了解它们如何将数字信号转换为我们可以看到的图像,并讨论虚拟现实(VR)和增强现实(AR)设备面临的挑战。

2D显示技术

上一节我们介绍了从场景描述到像素渲染的完整流程。本节中,我们来看看这些像素颜色如何通过物理设备最终呈现在我们眼前。现代显示技术虽然种类繁多,但都旨在将数字信号转换为光信号。

以下是几种主流的2D显示技术及其基本原理:

  • 阴极射线管(CRT):这是一种较旧的技术。其核心是一个电子枪,它发射电子束高速扫描屏幕内侧的荧光粉涂层。当电子击中荧光粉时,荧光粉发光。通过控制电子束的强度和扫描路径,可以形成图像。其发光原理是磷光现象,即材料被激发后缓慢释放光能。
  • 液晶显示器(LCD):这是目前最常见的显示技术。它利用光偏振原理工作。显示器背光发出偏振光,穿过液晶层。液晶分子在电压控制下可以扭转,从而改变穿过它的光的偏振方向。最后,光线通过一个偏振滤光片,只有特定方向的光能通过,从而控制每个像素的明暗。颜色则通过红、绿、蓝子像素滤光片实现。
  • 发光二极管(LED)显示:原理最为直接,每个像素就是一个微小的LED灯,通过控制其开关和亮度来显示图像。这种技术常见于大屏幕,但难以小型化。
  • 等离子显示:每个子像素是一个充满惰性气体(如氖、氙)的微小单元。施加电压时,气体电离成等离子体,释放的紫外线激发荧光粉发光。这是一种自发光技术。
  • 数字光处理(DLP)投影:这项技术非常精妙。核心是一个由数百万个微镜组成的芯片(DMD)。每个微镜对应一个像素,可以通过静电作用快速翻转。光源发出的光被反射到微镜上,微镜根据信号将光线反射向镜头(亮像素)或吸光器(暗像素)。通过快速切换和色轮配合,形成彩色图像。
  • 电子墨水(E Ink)显示:用于电子书阅读器。它模拟真实墨水,利用带电的微胶囊。施加电压时,黑色或白色颗粒移动到胶囊顶部,形成图像。其最大优点是只在刷新页面时耗电,显示静态内容时不消耗能量。

所有这些技术中,颜色通常是通过将每个像素分解为紧密排列的红、绿、蓝(RGB) 子像素来实现的。了解这种排列方式对于实现像ClearType这样的子像素抗锯齿字体渲染技术至关重要。

3D显示技术

了解了2D显示后,我们自然会对3D显示产生好奇。然而,尽管技术不断涌现,3D显示并未像2D显示那样普及。本节中我们来看看原因以及几种实现方式。

实现3D显示的核心挑战在于协调多种深度线索,包括双目视差、聚焦、遮挡、相对大小等。当这些线索相互冲突时,就容易导致观看者不适或头痛。

以下是几种3D显示技术:

  • 立体显示(需佩戴眼镜)
    • 主动快门式:眼镜左右镜片交替快速开关,与屏幕交替显示左右眼图像同步。
    • 被动偏振式:屏幕同时投射两种不同偏振方向的光,眼镜左右镜片分别只允许对应偏振方向的光通过。
  • 自动立体显示(无需眼镜)
    • 光栅式:在LCD屏幕前加一层柱面透镜阵列。透镜将不同像素的光线折射向不同方向,使左右眼看到不同图像。
    • 视差屏障式:在屏幕前放置一个带有精确狭缝的屏障层,阻挡部分光线,从而让左右眼看到不同的像素列。
    • 多视角/光场显示:这是更高级的形式,旨在从多个方向再现光线,允许观看者移动头部看到不同视角。但这通常以牺牲空间分辨率或需要极高的渲染速度为代价。
  • 体三维显示:这类显示试图在真实三维空间中生成图像。
    • 扫描体显示:例如快速旋转的屏幕或反射镜,配合同步投影,在空中“绘制”出3D图像。缺点是包含高速运动部件。
    • 静态体显示:例如将多个透明屏幕堆叠,每层显示3D物体的一层切片。缺点是难以处理遮挡,且成本高昂。

3D显示未能普及的原因包括技术复杂、成本高、观看舒适度差,以及缺乏与之匹配的3D交互方式。

虚拟现实与增强现实

最后,我们简要探讨虚拟现实(VR)和增强现实(AR)设备。这些可视为特殊的头戴式3D显示系统。

VR/AR设备的历史比想象中更久远。早在1968年,Ivan Sutherland在MIT就发明了名为“达摩克利斯之剑”的头戴式显示设备原型,奠定了现代VR设备的基础。

现代VR头显(如Oculus)的核心组件与早期原型相似:为每只眼睛配备一个显示屏,并集成头部追踪系统。其面临的最大技术挑战之一是延迟——从用户移动头部到显示更新图像之间的时间必须极短,否则会引起晕动症。

AR设备则旨在将数字信息叠加到真实世界视野中。实现方式多样:

  • 光学透视式:如Google Glass,使用分光镜或“佩珀尔幻象”原理,将微型投影仪的光线反射到透明镜片上,与真实场景融合。
  • 视频透视式:如手机AR应用,通过摄像头捕捉真实世界,在屏幕上与虚拟信息合成后显示。

当前,VR/AR技术正在快速发展,在沉浸感、视野、分辨率和交互方式上不断取得突破,是一个充满活力的研究与应用领域。

总结

本节课中我们一起学习了计算机图形学输出环节的各种技术。我们从工作原理各异的2D显示技术(如LCD、DLP)出发,探讨了实现3D显示的多种途径及其面临的感知与交互挑战,最后了解了VR/AR设备的基本原理和发展现状。尽管底层技术复杂多样,但得益于标准化的接口和协议,我们作为计算机图形学开发者通常无需关心具体硬件细节。然而,理解这些基本原理对于推动显示技术,特别是在新兴的VR/AR领域进行创新,仍然至关重要。

posted @ 2026-03-29 09:21  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报