news 2026/10/12 6:20:10

GAMES101图形学中篇:从几何表示到辐射度量学

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GAMES101图形学中篇:从几何表示到辐射度量学

你整理到中段了,说明前几篇笔记已经把变换、光栅化、着色这些关卡啃下来了。GAMES101这套现代计算机图形学入门课程,越往中间走,越能感觉到知识点开始从“画出来”转向“为什么这么画”。这篇汇总(中)我按自己的复习逻辑,把几何表示、曲线曲面、网格处理、光线追踪、加速结构和辐射度量学串成了一条线。和上篇不一样的是,这里每个概念都环环相扣,适合已经掌握基础变换和光栅化流程、想深入真实感渲染方向的读者。

1. 先划清中篇笔记的边界:从几何表示到辐射度量学

整理这半个阶段的笔记时,我做的最重要的一件事是给知识点划边界。上篇的核心是“把三角形画到屏幕上”:顶点怎么变换、光栅化怎么插值、Blinn-Phong怎么算高光、纹理怎么采样。中篇则完全不同,它要回答两个更基本的问题:场景里的几何形体到底该用什么方式描述?屏幕上一个像素的颜色,如何依赖光在场景中真实的传播?这两个问题不解决,后面所有关于路径追踪、材质建模和动画模拟的内容,连符号都看不懂。

课程在中间这段的排布非常讲究。它先讲几何表示,让你明白网格、隐式函数、参数曲面各自擅长什么;接着讲贝塞尔曲线曲面和网格处理,把“表达一个形状”这件事从连续域走到离散域;然后突然转向光线追踪,从相机出发逆推光线的命运;最后用辐射度量学补上“光到底携带多少能量”的物理基础。我在这篇笔记里严格按这条链子走,每一节都会标出它依赖前面的哪个概念。

不少同学在这会突然觉得课程变难,我也有同感。原因是前面知识相对独立,缺一块还能靠直觉硬补;从几何表示开始,每个章节的结论会反复被后面引用。贝塞尔曲面要用参数化的思路,网格细分要用邻接关系和局部坐标,BVH的构建依赖包围盒求交的数值稳定性,辐射度量学的radiance定义直接决定反射方程的写法。我在笔记里把这种依赖关系用显式的方式标出来,复习时才不会迷路。

1.1 为什么中段是整门课的分水岭

从学习体验上说,上篇几乎每一讲都能单独做个小demo:转一个三角形、画一个纹理、调一个高光参数,效果立竿见影。中篇开始,输出不再是一个直观的像素点,而是“如何描述表面”“如何追踪一根光线的命运”“如何度量光能”。这个转变不是难度爬坡,而是思维方式的转折,你不再只关心怎么画出来,而是开始关心画出来凭什么是对的。

另一个让我觉得中段是分水岭的原因,是它直接通向渲染方程。渲染方程把出射radiance写成入射radiance在一个半球上的积分,而积分里有几何项cosθ、有交点处的位置和法线、有求交能力才能知道光线打到哪。换句话说,中篇的几何和求交部分,是在为最后那几条看着简单的方程做前置准备。我甚至发现,以前写光栅化作业时的不少bug,根源根本不是着色模型,而是交点位置算错了,这是学完中篇再回头看才能意识到的。

1.2 笔记里的记号约定

为了不让自己在中篇推导时反复被符号绕晕,我统一了一套记号,这里也分享出来。向量用小写粗体,光线方向记作d,表面法线记作n;矩阵用大写字母,比如视图矩阵V、投影矩阵P。光线统一写成p(t) = o + t d,其中o是起点,d是单位方向向量,t大于等于0表示沿方向的参数距离。三角形网格里的顶点记作Vi,三角形的三个顶点按逆时针顺序排列,这样法线方向能和叉积结果保持一致。

这个约定里最关键的是光线的方向符号。我习惯把入射方向定义为“指向光源或指向交点朝向光线的来向”的方向,而不是“离开交点”的方向,这样可以避免在后续反射方程里出现符号错误。整理笔记的头几天,我经常因为方向正负号不对而卡壳,统一这套约定后推导顺畅多了。如果你有自己的习惯,不一定照搬,但一定要保证全文前后一致,不然后面路径追踪会让你欲哭无泪。

2. 几何表示的取舍:隐式、显式与网格为什么这么分

进入中篇第一个专题前,我先回答自己当时特别困惑的问题:图形学为什么需要单独花一整块内容讨论“几何表示”?光栅化渲染的不就是三角形吗?答案在于,三角形网格只是几何表示的一种形态,不同来源的模型可能是点云、参数曲面、隐式函数或者混合体。我们需要一套统一视角,才能判断后续的渲染和几何处理算法到底该在什么数据结构上跑。

课程粗暴又好用地把几何表示分成两大类。隐式表示不会直接给出面上的点,而是给出“满足某个条件的点集”,代表作是隐式函数F(x,y,z)=0、CSG布尔运算和有符号距离场SDF。显式表示则直接给出点或参数映射,代表作是点云、多边形网格、参数曲面。判断一个表示属于哪一类,最简单的办法是问一句:给定一个坐标,你能不能立刻知道它在不在表面上?能立刻判断的是隐式,需要遍历或搜索才知道的是显式。

这个分类看起来像理论玩具,实际上非常影响工程选型。判断“点在形状内部还是外部”这件事,在碰撞检测、布尔运算、体积渲染里到处遇到,隐式几何天然擅长;而“把表面画出来”需要遍历所有面片,显式几何天然适合。课程花这么多时间讲分类,不是让你考试时归类,而是让你在选算法时知道该拿哪种表示去喂它。

2.1 隐式几何:判定简单,但采样很难

隐式几何我最早接触到的是球面:F(p) = x²+y²+z²−r²。判断某个点是否在球内,只要算F(p)的符号,小于等于零就在球内,这个操作快得惊人,不需要查表,不需要遍历。课程里还提了SDF,也就是每个点保存到最近表面的带符号距离,绝对值表示距离,符号表示在表面哪一侧。SDF除了判定内外,还能通过距离值直接获得“安全移动距离”,在步进式光线追踪里能大幅缩短求交时间。

隐式几何的弱点在采样和可视化。要从F(x,y,z)=0里提取三角网格,主流做法是Marching Cubes:把空间切成格子,检查每个格子的8个顶点符号变化,再在格子里插值生成三角形。这个方法能用,但分辨率一高计算量就爆炸,拓扑复杂时格子尺寸没选对还会出现裂缝和自交。另一个麻烦是隐式函数没有直观的参数域,想沿表面贴一张纹理,不如显式网格方便。我自己总结的实践原则是:界面判定、体积计算、流体表面重建优先考虑隐式,常规渲染场景交给显式网格。

2.2 显式几何:点云、多边形网格与参数曲面

显式表示的理念是“直接给点给面”。点云最朴素,每个表面点一个坐标,但点之间没有连接关系,重建连续表面要自己补;多边形网格在此基础上加了顶点、边、面的拓扑,是当前图形学硬件最友好的形式;参数曲面用二维参数(u,v)映射到三维坐标,本质上是一族连续的点。

显式表示的优点是采样自然。渲染时直接遍历三角形做光栅化或求交就行;细分时直接对边和顶点做局部更新;修改形状时直接拖顶点或控制点。缺点是数据必须保证拓扑合法,比如相邻三角形不能共享一条边却朝向相反,顶点不能悬空。点云插值时容易出洞,参数曲面如果有控制点排布混乱,映射出来的曲面会折叠。显式表示看起来简单,真正做几何处理时坑比隐式多得多。

2.3 三角形网格成为默认选择不是巧合

多边形网格里有三角形、四边形和五花八门的n边形,为什么几乎所有渲染管线都默认三角形?课程给了一个非常本质的回答:三个点一定能定义一个平面,四个点不一定。三角形天然位于同一平面,光栅化时插值深度、着色、纹理坐标都非常稳定;四边形一旦四个顶点不共面,插值出来的点会轻微漂移,产生极难排查的走样和接缝。

三角形还有利于保证“流形”结构。在正规三角形网格里,每条内部边恰好被两个三角形共享,每个顶点的邻域拓扑像一个圆盘,法线方向可以全局一致地传播。一旦出现一条边被三个三角形共享,或者两个顶点退化成同一个点,法线会出问题,邻接遍历也可能死循环。我写网格处理练习时,会先写一个顶点-边邻接检查函数,把非法网格提前筛掉,这比在算法崩溃后回头调试省心太多。

3. 贝塞尔曲线与曲面:控制点不是边界,是趋势

几何表示讲完,课程顺理成章进入曲线曲面。为什么单独讲曲线?因为很多物体表面不是直接雕刻网格,而是先画曲线再扫掠、旋转或放样成面。贝塞尔曲线是所有参数曲线的起点,也是理解B样条和NURBS的基座。我第一次看de Casteljau算法时,第一反应是“这不就是纯递归插值吗”,真正写代码才发现里面的数值稳定性很有讲究。

贝塞尔曲线由一组控制点定义:三个控制点得到二次曲线,四个控制点得到三次曲线。它的核心性质是曲线不直接穿过所有控制点,而是被控制点“吸引”着走,这和用折线连接控制点完全不同。对美术人员来说,这种操作手感很自然:拖一个控制点,附近曲线跟着变。对工程师来说,贝塞尔曲线的价值在于参数化:给定任意t∈[0,1],都能得到唯一的曲线点,这让后续对曲面的细分、曲率计算和动画轨迹插值都变得极其方便。

3.1 de Casteljau算法到底在算什么

de Casteljau算法的流程一句话就能概括:对相邻控制点做线性插值,得到一层新点,再对新点重复插值,直到只剩一个点,这个点就是曲线在参数t处的坐标。以三个控制点P0、P1、P2为例,先取P0和P1之间的插值点P01,以及P1和P2之间的插值点P12,再对P01和P12做插值,得到的点就是二次贝塞尔曲线在t处的值。

展开化简后,这个递归过程对应Bernstein多项式:B(t) = (1−t)²P0 + 2t(1−t)P1 + t²P2。但我实际写代码时几乎不会直接展开多项式,而是保留递归的de Casteljau形式,因为它在数值上更稳定。高次贝塞尔曲线如果直接算多项式系数,系数会非常大,t接近端点时出现灾难性消去;递归插值每一步都是对0到1之间的数做加权平均,浮点误差可控得多。

把de Casteljau每一层的临时点可视化出来,能看到曲线如何从控制折线一步步“收敛”到目标点。我调试时习惯用它检查:如果曲线偏离控制多边形太多,先看中间层插值点是否合理,若出现交叉,多半是控制点顺序错了,而不是算法本身有问题。

3.2 贝塞尔曲线的性质,每一条都能当排查工具

整理笔记时,我把贝塞尔曲线的性质当调试检查单来用。第一,曲线经过第一个和最后一个控制点,但不经过中间控制点,端点和起点永远锁定;第二,曲线完全落在控制点构成的凸包内,所以可以用凸包做粗略碰撞剔除;第三,曲线对控制点做平移、旋转、缩放等仿射变换,等价于先变换控制点再重新生成曲线,这对动画系统极其友好,物体做刚体变换时曲线不需要重新采样;第四,曲线的多项式阶数等于控制点数减一,控制点一多阶数暴涨,曲线会变得难操控。

最影响工程选型的性质是“全局性”:移动任何一个控制点,整条曲线都会改变。在简单形状上这无所谓,一旦要做精细建模,设计师往往只微调局部,却被逼重新评估全局,于是就有了B样条。理解了这条性质,再去看B样条的局部支撑,思路一下子就顺了。

3.3 贝塞尔曲面:两次一维插值得到的二维控制

贝塞尔曲面是贝塞尔曲线的二维推广,控制点从一串变成一张网格。以4×4控制点网格为例,求曲面在参数(u,v)处的坐标时,先固定v,沿u方向对四组控制点分别做de Casteljau,得到四个临时点;再拿这四个临时点沿v方向做一次de Casteljau,最终得到曲面点。这种先u后v的操作叫张量积曲面,好处是曲面的数学结构完全复用一维曲线的结论,代码实现也简单。

我写贝塞尔曲面求值时踩过一个很实在的坑:控制点数组的行列顺序和参数方向对不上。比如先在u方向用了行数据,v方向却也用了行数据,曲面会出现扭曲。后来我在代码里强制约定“第一个参数对应网格第一维,第二个参数对应第二维”,并且用3×3的退化网格做单元测试,确保u=0、v=0时能取到角点,再跑复杂模型。

贝塞尔曲面在工业建模里常见的做法是拼合多块曲面,块与块之间需要保证参数连续。课程里没深入连续阶问题,但至少要知道:只让边界点重合,曲面拼接处可能只达到C0连续;要让切线也连续,需要额外约束相邻控制点的共线关系。这个细节在后续做曲面重建时迟早会遇到。

3.4 B样条与NURBS:解决全局影响的方案

B样条外观上跟贝塞尔很像,都由控制点和基函数构成,但核心改进是基函数只在局部区间不为零。这意味着移动一个控制点,只会影响曲线的一部分,而不是全局。B样条用节点向量控制基函数的分段位置,节点可以重叠,重叠会改变曲线的连续阶,重复多次甚至能让曲线经过某个控制点。这个自由度让工业建模软件如鱼得水。

NURBS在B样条基础上又加了权重,权重能让曲线更靠近或更远离控制点,甚至可以精确表示圆弧和圆锥曲线。课程到这里点到为止,没有让我深挖节点插入和升阶算法,但理解NURBS对理解建模软件内部结构很重要。我把这部分的学习心得整理成一句话:网格负责离散雕刻和高模制作,曲线曲面负责基础形状和布尔运算前的高精度表达,两者是平行工具,不是替身。

4. 网格处理三件套:细分、简化与正则化的思路

有了几何表示和曲线曲面,中篇实践味道最重的部分是网格处理。课程把它拆成三个方向:细分增加分辨率,简化降低分辨率,正则化改善网格质量。我一开始以为这是三个独立算法,复习时才发现它们共享很多底层设施,比如邻接表、顶点度数、边的长度统计。可以说,把网格当成一个数据结构来理解,这一段才算真正入门。

细分通过对每个三角形或面片局部再分,让网格从粗糙变光滑。细分不是简单插值,而是把连续性嵌入到插值规则里,所以细分次数越多,表面越趋近光滑极限面。课程重点讲了Loop细分和Catmull-Clark细分:前者面向三角形网格,后者处理任意多边形网格。理解这两个算法后,再看雕刻软件里的多重精度操作,本质就是在多级细分结果之间切换。

4.1 Loop细分:三角形网格的经典方案

Loop细分第一步是拓扑细分:把每个三角形通过三条边中点一分为四,得到四个小三角形。第二步是几何调整:更新新旧顶点的位置。新顶点(边中点)的位置由共享这条边的两个端点以及另外两个对角顶点加权得到,课程里的权重直觉是:离共享边端点更近的信息占更大权重,对角顶点的影响相对小。旧顶点则根据邻接顶点的数量和位置做加权平均,度数越高,顶点越被“拉平”,网格整体更光滑。

实现时最容易漏的是边界处理。内部顶点的更新规则依赖完整邻环,但边界处邻接点不完整,课程专门给了边界规则,通常简化为曲线细分规则。忽略边界会让边界过度光滑,视觉上出现塌陷。另一个细节是Loop细分的权重参数不是拍脑袋的数,它影响极限曲面的光滑度。标准取值里,对内部旧顶点通常使用ω = 3/(8n),其中n是顶点度数,当n=3时取3/16,这是为了让极限曲面在规则点处达到一定连续阶。写代码前先把参数搞清楚,后面调效果会省很多心。

4.2 Catmull-Clark细分:任意拓扑怎么切

Loop细分只处理三角形,但很多模型初始是四边形甚至混合多边形。Catmull-Clark细分放宽了这个限制:对面片、边、顶点分别计算新位置,再按拓扑连接起来。第一次细分后,所有面片都会变成四边形,但有个反直觉的现象:原来的奇异点(度数不为4的顶点)会在每次细分后保留下来,只是周围四边形越来越多。你无法通过反复细分消除奇异点,但可以让它周围的区域越来越接近规则网格。

我做过一个立方体细分实验:立方体6个四边形面、8个角点,经过一次Catmull-Clark细分,面变成24个四边形,角点还在,但形状开始变圆。继续细分几轮,角点周围出现大量小四边形,整体越来越接近光滑球体。这个过程里最值得注意的是面点和边点的权重规则:面点是所属面所有顶点平均,边点是共享该边的两个面点平均后再与边的两个端点取平均,顶点按邻接面数和边数的加权规则更新。权重公式不复杂,但顺序错了结果完全不对,我建议实现时把三个规则写成独立函数再统一组装。

4.3 边坍缩与二次误差度量:减面不是随机删点

网格简化最朴素的想法是隔几个顶点删一个,但这样会让表面严重形变甚至出现空洞。课程给出的经典方案是边坍缩:选择一条边,把两个端点合并成一个新点,同时删除相关三角形。这里真正的问题不是“坍缩哪条边”,而是“新点放哪里”。放任意端点网格会扭曲,放理想位置需要量化坍缩带来的误差。课程使用二次误差度量QEM,用点到相关平面距离平方的和作为误差。

具体实现时,对每个顶点维护它邻接三角形所在平面,坍缩时计算候选点到这些平面的距离平方之和,求让总误差最小的位置。因为距离平方是二次函数,误差可以写成矩阵形式,求最小值只需要解一个小线性系统。我把每条候选边的QEM值塞进最小堆,每次弹出误差最小的边执行坍缩,再更新受影响边的误差值重新入堆。这个贪心策略工程上效果非常好,是很多减面软件的底层核心。

实际使用中,我会额外加规则:不允许坍缩边界边,否则模型轮廓会不断内缩,出现明显“漏水”。还有一个认知是误差最小不等于视觉最好。如果模型最终要烘焙法线贴图,单纯按QEM减面会把细节抹平,合理的做法是在QEM基础上加法线显著性权重。不过这是课程之外的进阶话题,先把基础的QEM跑通最重要。

5. 光线追踪入门:光栅化答不清的问题换一条路

几何和网格处理结束后,中篇最大的转折点是光线追踪。为什么需要它?光栅化本质是“从几何到像素”的正向路径上做离散化,对每个三角形执行投影和填充,跑得飞快,但很多物理现象处理起来非常别扭。完美的镜面反射需要递归查找反射方向对应的场景内容,光栅化没有自然的递归机制;折射需要知道光线在介质间弯曲,光栅化只能后处理近似;软阴影需要面积光源的可见性积分,光栅化只能硬阴影加模糊。

光线追踪换了一个方向:不从光源出发,而从相机出发,为每个像素逆推一条光线,看它最远撞到什么。真实世界光子从光源到相机,反向追踪计算量小得多。每个像素攒够足够多的光线样本,就能逼近真实画面。课程先讲最基础的Whitted风格光线追踪,它没有完整求解渲染方程,但对建立整体框架极有帮助。

5.1 光栅化的天花板到底有多高

光栅化的阴影通常用阴影贴图:从光源方向渲染一张深度图,用它判断场景点是否可见。硬阴影下效果不错,但贴图分辨率有限,容易锯齿和自遮挡。镜面反射可以做环境映射,用预计算立方体贴图模拟反射环境,但动态物体间的反射失真。间接光照更是光栅化老大难,传统方案靠预先烘焙光照贴图,动态场景只能作弊。不是说光栅化不能用,而是当目标是“接近真实”时,它处处碰壁。

光线追踪则把每个像素的光照拆成一个个实际传播的光线事件,物体间的反射、折射、阴影都是顺序求交问题,天然支持动态场景。代价是计算量大:每像素多根光线、每根光线多次求交。课程的路径很清晰:先用最朴素的Whitted风格建立直觉,再用加速结构和辐射度量学把效率与质量提上去。

5.2 Whitted风格光线追踪的完整流程

Whitted风格的基本流程可以归纳为四步。第一步,从相机出发,为每个像素生成主光线,找场景中最近的交点。第二步,在交点做局部着色,用Blinn-Phong得到直接光照颜色。第三步,如果表面是镜面,沿反射方向生成反射光线,递归寻找它撞到的点,并把那个点的局部颜色叠加到当前像素;如果是透明材质,按折射方向生成折射光线,同样递归计算。第四步,每条递归光线乘上衰减系数(反射率或透过率),累计到主交点颜色上,写回像素。

写起来不难,但排查bug时要特别注意两点。第一是递归深度控制:两面镜子对望会让递归无限增长,必须设最大深度,比如5层,超过就停。第二是自相交:光线刚从交点出发,由于浮点误差,二次求交可能又命中同一物体,导致像素反复叠加自己的颜色。解决办法是给光线的t轴一个微小偏移,比如从t=1e-4开始求交,绝不让它从t=0开始。

5.3 光线求交:从球到三角形的数值细节

所有光线追踪的根基是求交。球求交最简单:把光线方程代入球方程,得到关于t的二次方程,判别式小于0不相交,等于0相切,大于0有两个交点,取最小非负t。实现时每次检查二次项系数是否接近零,防止退化。三角形求交更常用,Möller-Trumbore算法用重心坐标一步解出t、u、v,u和v就是重心坐标的两个分量,判断是否落在[0,1]区间即可。

bool rayTriangleIntersect(const Ray& ray, const Vec3& v0, const Vec3& v1, const Vec3& v2, float& t, float& u, float& v) { Vec3 e1 = v1 - v0; Vec3 e2 = v2 - v0; Vec3 pvec = cross(ray.dir, e2); float det = dot(e1, pvec); if (fabs(det) < 1e-8) return false; float invDet = 1.0f / det; Vec3 tvec = ray.orig - v0; u = dot(tvec, pvec) * invDet; if (u < 0.0f || u > 1.0f) return false; Vec3 qvec = cross(tvec, e1); v = dot(ray.dir, qvec) * invDet; if (v < 0.0f || u + v > 1.0f) return false; t = dot(e2, qvec) * invDet; return t > 0.0f; }

为什么非要用重心坐标解平面方程?因为重心坐标能同时告诉你三件事:交点是否在三角形内、交点离三个顶点的相对比例、如何平滑插值顶点属性。拿到重心坐标后,把三个顶点的法线和纹理坐标做插值,就能得到连续的法线变化,这也是网格渲染看起来平滑而不是呆板三角面的重要原因。

6. 加速结构BVH:从“每光线×每物体”到“每光线×log物体”

写出能正确求交的光线追踪器后,第一个性能瓶颈立刻出现:场景一旦有几千个三角形,每根光线都要遍历全部三角形做求交,渲染一张800×600的图光线数量轻松上百万,复杂度爆炸。课程给出两个加速方向:空间划分和物体层次划分。BVH属于后者,也是工程上的默认选择。

BVH的核心思想极简单:用包围盒把三角形分组,每个组有大包围盒,组内再细分,形成一棵树。求交时先测试光线是否碰到大包围盒,没碰到直接跳过整个组;碰到了再进内部检查子树。大部分光线能“裁掉”大量无关三角形,求交次数从O(N)降到O(log N)。BVH与三角形分布耦合强,但因为它是物体层次而不是绝对空间格子,能自动适应密集和稀疏区域。

6.1 均匀网格为什么打不过BVH

均匀网格把场景空间均匀切成立方体格子,把三角形放进覆盖它们的格子里,光线沿方向步进检查经过的格子。实现简单,但有著名的“格子里程碑”问题:格子太大,大三角形被放进一堆格子,光线浪费在格子上;格子太密,每格三角形太少,额外开销又上来。更麻烦的是均匀网格和场景几何不绑定,大房间角落放张椅子,大部分格子是空的,光线还得一步步穿过空区域。

BVH通过递归切分数据区域,把空区域排除在外,特别适应非均匀场景。课程也提到KD-Tree这类空间划分方法,但KD-Tree可能切到三角形本身,需要额外的物体列表管理,复杂度高;BVH直接围绕三角形分组,维护和遍历都简单。如今三角形数量大的场景,BVH几乎是标准配置。

6.2 BVH构建的实操要点

课程给出的BVH构建策略很直观:每次选一个轴,把所有三角形的包围盒中心按该轴排序,取中间位置分割成左右两组,递归构建子树。选轴的经验法则是选当前节点跨度最大的轴,也就是三个方向上包围盒尺寸最大的那个。按中心排序后取中点划分,能让左右子树规模接近,树深度接近logN,避免退化。

停止递归的条件通常是两个:节点内三角形数量足够少,比如小于4个;或树深度达到上限。此时节点变成叶子,存三角形列表和一个紧密包围盒。更高级的构建会用表面积启发式(SAH)枚举分割点,让划分代价最小化,课程点到为止。理解SAH对后续优化很有帮助:它把光线命中左右子树的概率近似为包围盒表面积之比,然后寻找使期望求交代价最小的切分位置。我写简易BVH时先选最长轴加中点分割,已经能获得不错性能,只有遇到严重不平衡场景才换SAH。

6.3 BVH遍历:优先近侧、维护t区间

遍历BVH的递归版本很好写:光线与当前节点包围盒不相交就返回;是叶子就对内部所有三角形求交并更新最近交点;是内部节点就按顺序访问两个孩子。更高效的版本会先对比光线与两个子包围盒的交点距离,先进入近的那棵子树,更快找到粗糙近交点,提升剪枝效率。

实现里最大的坑在“最近交点”的维护。递归进入子树时,要带上当前最近t值和当前命中信息;返回后要用最新t值判断另一棵子树是否还有更近可能。如果节点包围盒求交得到的远侧t已经小于当前最近t,说明这棵子树里不可能有更近的交点,可以直接剪掉。这个剪枝逻辑用好了,性能差距能有数倍。另一个常见失误是阴影光线:shadow ray判断中间有没有遮挡物,不关心最近遮挡点,所以只要找到任意t小于光源距离的交点就能立即返回,不必遍历完整棵树。新手经常在这里画出完整的最近交点,白费一大半性能。

7. 辐射度量学:中篇最后的硬骨头,也是向路径追踪过渡的桥

几何和求交只能告诉你“光线打到哪里”,要回答“这个点多亮”,必须引入辐射度量学。中篇最后补了四组基本量:能量、通量、照度和辐射度。这些词听起来像物理课,但图形学里每个颜色计算最终都落在它们头上。我第一遍学时觉得抽象,后来才发现这只是把“亮度”拆成带方向的功率密度。

基本逻辑链是:光携带能量,能量的时间变化率是功率;单位面积上接收到的功率叫照度(irradiance);再除以单位立体角就是辐射度(radiance)。radiance是图形学最喜欢的量,因为它不随距离衰减:一条光线的radiance在真空中传播保持不变,只有碰到表面才改变。这意味着在光线追踪里,可以在交点处计算radiance然后直接沿光线传递,不必担心距离平方衰减重复计入。

7.1 立体角:用球面视角衡量方向占比

立体角是二维角度在三维的推广,数值上等于球面上对应区域面积除以半径平方,单位是球面度。点光源向周围辐射时,能量分布在球面上,说“朝某个方向看去占多少份额”需要立体角。课程里最常用的结果是:一个半球的立体角是2π,整球是4π。所有入射光积分几乎都是在一个半球面上做立体角积分。

记立体角最直观的方法是想象手电筒光斑:光斑打在远处墙上会变大变暗,因为单位立体角对应的实际面积随距离平方增长,而单位面积接收的功率按距离平方下降,两者抵消后,沿固定方向的radiance不变。这个直觉能帮你避免很多距离相关的错误推导,也是理解BRDF不随距离变化的基础。

7.2 Irradiance和Radiance:最容易混的一对概念

照度(irradiance)定义为单位面积接收的功率,单位W/m²,不区分方向,衡量一个点上来自四面八方入射光的总效果。辐射度(radiance)除以单位立体角,单位W/(m²·sr),描述“沿某一根细光线方向传来的功率密度”。可以这样区分:站在一个表面上,irradiance回答“这个点总共落了多少光”,radiance回答“这些光里有多少是朝特定方向来的”。

对反射方程,这个区分决定成败。渲染方程里,出射radiance等于入射radiance乘BRDF再乘cosθ对上半球积分。如果把radiance和irradiance混为一谈,积分里不是多出方向维度就是丢失方向维度,结果要么爆亮要么全黑。我做了一个自测题来检验自己:一个均匀亮的区域光源和一个亮度相同的点光源,分别照射同一表面点,得到的irradiance通常不同;但如果是同一方向、同一radiance的光线,传递到表面的radiance相同。能说清这句话,才算跨过这道坎。

7.3 Lambert漫反射与反射方程的前奏

漫反射表面看起来均匀,是因为它对每个方向的入射光都按同一个BRDF常数向外反射,课程里写作ρ/π,其中ρ是反照率,除以π是为保证入射radiance积分到出射radiance时能量不放大。对Lambert表面,反射方程积分可以提走BRDF常数,剩下的积分正好是irradiance,所以最终出射radiance等于反射率除以π再乘入射照度。

这也把前面Blinn-Phong模型重新解释了一遍:光照项里的max(0, n·l)就是cosθ,它来自反射方程里的投影面积余弦项。法线和光线方向越一致,接收的有效光能越多。很多人把Blinn-Phong当经验公式背,学完辐射度量学回头看,才发现它暗含余弦定律。这种前后呼应是我觉得中篇笔记最值得整理的地方。

7.4 为什么课程在这里停一下

辐射度量学讲完后,课程没有立刻冲进完整路径追踪,而是先让反射方程和前面光线追踪的流程对上号。这样安排很受益:直接上蒙特卡洛积分和路径追踪,很多符号的意义会不清楚;但先有radiance和反射方程概念,再学路径追踪时每个采样的物理含义一目了然。中篇笔记停在这是个自然位置,下一部分才进入材质、BRDF、采样策略和动画模拟。

整理到这里时,我会做一遍贯穿练习:把上篇的Blinn-Phong着色器替换成基于反射方程的最小实现,用中篇的Whitted光线追踪框架跑一个简单球体场景,对比两次渲染差异。这个练习不复杂,但对打通几何、求交、辐射度量学的链路非常有帮助。做完后再看课程后续的路径追踪,障碍会少很多。

最后再分享一个小习惯。整理中篇笔记时,我把每个公式都用大白话在边上注了一句。比如de Casteljau的注解说“每层插值点就是一条更短折线的形状”,辐射度量学里radiance的注解说“一条光线携带的功率密度”。等到复习下篇时,这些注释比任何框架图都管用。如果你也在这里感到吃力,别急着往前赶,先把几何表示、求交和radiance三条线索串起来,后面会通畅得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 6:19:30

《原神》高刷改造:6倍帧生成+DLSS5整合包实战

最近我连续折腾了一周的老游戏高刷改造&#xff0c;这次的对象不是别的&#xff0c;正是《原神》。我手里的目标很简单&#xff1a;原生60帧的渲染逻辑不变&#xff0c;但让显示器上看到的画面流畅度尽量往上顶。标题里写着“6倍帧生成DLSS5”&#xff0c;我一开始也以为是标题…

作者头像 李华
网站建设 2026/10/12 6:18:19

JavaWeb原生一对一网页聊天系统实现与避坑指南

简介&#xff1a;这是一套基于JavaWeb技术栈实现的一对一网页聊天系统&#xff0c;面向Java初学者与Web开发入门者&#xff0c;帮助其掌握JSP、Servlet、AJAX异步通信及MySQL数据库集成等核心技能&#xff0c;适用于课程设计、毕业设计或Web实时交互功能实践。资源包共54个文件…

作者头像 李华
网站建设 2026/10/12 6:17:34

748GB统一内存实测:桌面AI工作站如何本地跑万亿参数模型

前阵子有台机器刚发布就引起了不少讨论——748GB统一内存、桌面级规格、本地跑万亿参数级模型&#xff0c;我第一反应是“这怕不是把一整套小型机房塞进了塔式机箱里”。仔细看完参数和实际表现之后&#xff0c;我的结论更直接&#xff1a;这台设备真正考验人的地方&#xff0c…

作者头像 李华
网站建设 2026/10/12 6:17:31

Flutter集成EmbeddingGemma 2:端侧AI语义搜索实战

1. 端侧AI的新拐点&#xff1a;为什么这个组合值得关注移动端跑大模型这件事&#xff0c;过去两年一直卡在三个死结上&#xff1a;模型体积太大塞不进App包、推理延迟高到用户等不起、隐私数据要上云导致合规风险。我身边不少做移动端的朋友&#xff0c;一提到"端侧AI&quo…

作者头像 李华
网站建设 2026/10/12 6:16:40

Python实现工程轮廓完美圆角:等距线、切点与分段函数详解

一聊到工程几何建模&#xff0c;很多人脑子里浮现的是NX、SolidWorks里那个Fillet命令&#xff1a;选中两条边&#xff0c;输入半径&#xff0c;回车&#xff0c;一个圆角就出来了。但如果你自己写过CAM前处理、解析过DXF轮廓、或者想用Python做参数化建模&#xff0c;就会发现…

作者头像 李华
网站建设 2026/10/12 6:16:16

Unity接入HMS SDK全流程指南:从demo到真机登录避坑

简介&#xff1a;在Unity项目中接入华为HMS SDK&#xff0c;是移动端开发者的常见需求。由linxinfa整理的这份Demo工程&#xff0c;完整演示了从SDK导入、IL2CPP配置到初始化、登录、推送等功能的调用流程&#xff0c;适合已掌握Unity基础、想快速接入华为生态的开发者参考。资…

作者头像 李华