[简化版 GAMES 101] 计算机图形学 21从光栅化到光线追踪Bilibili 同步视频 一、光栅化的「阿喀琉斯之踵」——全局光照的痛 1.1 阴影Shadow Mapping的「挣扎求生」 1.2 Glossy反射铜镜般的「朦胧美」 1.3 间接光照光线的「弹弹乐」⚡ 二、光线追踪物理正确的「慢工出细活」⚖️ 2.1 光栅化 vs 光线追踪速度与质量的终极博弈 2.2 光线追踪的三大光学假设 假设一光线沿直线传播 假设二光线之间互不碰撞 假设三光路可逆性Reciprocity 三、光线投射Ray Casting光追的「入门版」 3.1 主光线与阴影光线 3.2 光线投射流程图 四、Whitted-Style递归光线追踪让光线「弹」起来 4.1 反射与折射的递归魔法⚡ 4.2 能量守恒不能让光「无限增殖」 4.3 光线家族大合影 五、光线的数学定义一条射线的自我修养✏️ 5.1 光线的参数方程⚽ 六、光线与球体求交解个二次方程就行 6.1 数学推导 6.2 C代码实现 七、通用隐式表面求交f(otd)0的艺术 八、光线与三角形求交光追世界的「硬通货」 8.1 两步走思路⚡ 8.2 Möller-Trumbore 算法 8.3 额外福利点在多边形内判断 九、总结从理论到实践的漫漫长路 9.1 核心知识点回顾 9.2 前方还有什么 本文导读如果你曾困惑于「为什么光栅化做不出电影级真实感」「光线追踪到底是个啥黑科技」「Whitted光线追踪怎么实现」那么恭喜你挖到宝了 本文将用最通俗的语言 最硬核的数学推导带你从零入门光线追踪的奇妙世界Bilibili 同步视频[简化版 GAMES 101] 计算机图形学 21从光栅化到光线追踪 一、光栅化的「阿喀琉斯之踵」——全局光照的痛哈喽各位图形学爱好者们 今天咱们来聊一个图形学里「又爱又恨」的话题——光栅化 vs 光线追踪。说起光栅化啊那可是咱们实时渲染的「老大哥」了 从你手机上的小游戏到3A大作的每一帧画面背后都有光栅化的身影。它就像一个速度飞快的「速写画家」刷刷刷几下就能把3D场景「拍扁」到2D屏幕上每秒给你画个几十上百帧让你感觉画面在「动」。但是⚠️ 正所谓「金无足赤人无完人」光栅化这位老大哥也有它搞不定的事儿——那就是全局效果Global Effects。啥叫全局效果呢简单说就是光线在场景里弹来弹去最后才进入你的眼睛。这种「你中有我我中有你」的光影效果光栅化处理起来那叫一个费劲啊… 1.1 阴影Shadow Mapping的「挣扎求生」先说阴影吧。咱们上节课聊过Shadow Mapping——从光源视角渲染一张深度图然后对比像素深度来判断是不是在阴影里。听起来挺完美是不是但是问题来了——这玩意儿默认只能做硬阴影而且得是点光源才行。 啥叫硬阴影就是阴影边缘锐利得像刀割一样一看就很「假」。后来啊聪明的程序员们又搞出了各种软阴影算法什么PCF啊、PCSS啊…折腾来折腾去总算是能做出「离物体越远阴影越模糊」的效果了。** 灵魂拷问**为了一个阴影咱们得额外渲染深度图、做采样、做过滤…这成本是不是有点高而且效果还不一定能保证物理正确… 1.2 Glossy反射铜镜般的「朦胧美」再来说说Glossy反射。啥是Glossy想象一下咱们老祖宗用的铜镜——磨得光溜溜的能照见人影但又没有现代玻璃镜子那么清晰。朦朦胧胧的别有一番韵味这种「有点镜面反射但又带点粗糙」的材质就叫Glossy。这种反射的光路是啥样的呢光线打到物体A → 反射出去 → 打到物体B → 再反射 → 进入人眼。看到没**光线弹了两次** 而光栅化的着色模型比如Blinn-Phong默认只考虑「光源→着色点→相机」这一次弹射。多弹几次光栅化表示「臣妾做不到啊…」 1.3 间接光照光线的「弹弹乐」最后也是最头疼的——间接光照。想象一个阳光明媚的午后你坐在房间里。☀️ 阳光透过窗户照进来先打到地板上地板漫反射到墙上墙再反射到天花板…光线就在屋子里「弹弹乐」弹了好多次才最终进入你的眼睛。这就是间接光照——光线到达人眼前弹射不止一次。而光栅化呢它默认只算「直接光照」也就是光线只弹一次。想算间接光照那可就麻烦了…什么光照贴图啊、辐照度体积啊、屏幕空间环境光遮蔽啊…各种奇技淫巧层出不穷但都是「近似」没法保证物理上的正确性。⚠️ 光栅化小结✅ 优点速度飞快适合实时渲染游戏≥30FPS❌ 缺点全局光照效果实现复杂、近似度高、难以保证物理正确 典型场景《绝地求生》这类大地图游戏为了帧率不得不牺牲画质⚡ 二、光线追踪物理正确的「慢工出细活」既然光栅化有这么多局限那有没有「一招鲜吃遍天」的方案呢有那就是——**光线追踪Ray Tracing**✨⚖️ 2.1 光栅化 vs 光线追踪速度与质量的终极博弈咱们先来看一张对比表直观感受一下两者的差距 对比维度️ 光栅化 光线追踪渲染精度近似、物理不严谨物理准确还原真实光路性能表现极快实时游戏友好极慢传统为离线渲染适用场景3A游戏、实时交互电影动画、影视离线渲染计算开销低GPU并行友好极高早期一帧需上千CPU小时全局光照需各种奇技淫巧近似天然支持物理正确说到光线追踪有多慢给大家讲个「恐怖故事」早期电影渲染一帧画面要花10000个CPU小时什么概念就是你用一颗CPU不眠不休地算得算一年多才能算出一帧…当然啦现在硬件进步了没那么夸张了。但跟光栅化比起来还是慢了好几个数量级。 趣闻《疯狂动物城》里那些栩栩如生的画面每一帧都是光线追踪的杰作。想想看迪士尼的渲染农场得有多少台机器同时工作才能把一部电影给「算」出来啊…这就是图形学里最经典的Trade-off权衡——质量 vs 速度。你想要更好的画质就得付出更多的时间你想要更快的速度就得在画质上做妥协。就像MSAA抗锯齿一样开了8xMSAA画质上去了帧率就下来了…人生啊处处都是选择题♂️ 2.2 光线追踪的三大光学假设在正式开讲光线追踪之前咱们得先「约法三章」——光线追踪是基于这三个简化假设的 假设一光线沿直线传播这个应该没啥争议吧虽然从物理上来说光是一种电磁波有衍射、干涉这些波动现象。但在图形学里咱们就用几何光学的简化模型——光线就是沿直线走的毕竟咱们做的是「看起来真实」不是「物理上100%精确」。够用就行 假设二光线之间互不碰撞两束光线交叉而过会发生什么答案是**啥也不发生**♂️它们各走各的路互不干扰。虽然严格来说这也不对光子之间是有相互作用的但在宏观尺度上这个假设完全没问题。 假设三光路可逆性Reciprocity这个就有意思了也是光线追踪最核心的理论基础。什么是光路可逆性简单说就是如果光线能从A点走到B点那么把方向反过来从B点也一定能走回A点。用咱们「人话」来说就是**你能看见别人别人也能看见你。** 尼采说过「当你凝视深渊时深渊也在凝视你。」——这句话完美诠释了光路可逆性当然尼采本来不是这个意思啦那这个「光路可逆」有啥用呢用处可大了真实世界里光线是从光源出发在场景里弹来弹去最后进入人眼。但如果我们也这么模拟的话效率会极低——因为大部分光线根本就不会进入相机啊那不就白算了吗于是聪明的程序员们想到了既然光路可逆那我反过来算不就行了从相机/眼睛出发往场景里发射「感知光线」反向追踪到光源。这样每一根光线都是「有用的」效率大大提升这就是「光线追踪」里「追踪」二字的由来——我们是从相机出发追踪每一根光线的去向。** 历史小彩蛋**古希腊的柏拉图、欧几里德、托勒密这些大佬都曾认为「人眼会发出光线打到物体上我们才能看见东西」。现在看来这个理论当然是错的但它居然和光线追踪「从相机出发发射光线」的思路不谋而合只能说历史有时候真的很奇妙 三、光线投射Ray Casting光追的「入门版」好了铺垫了这么多咱们终于要进入正题了先从最简单的版本说起——光线投射Ray Casting。这玩意儿可以说是光线追踪的「青春版」只算直接光照不算反射折射。 3.1 主光线与阴影光线光线投射的流程其实超级简单就四步第一步发射主光线Primary Ray / Camera Ray想象一下你的眼睛就是一个点针孔相机模型面前有一个屏幕屏幕被分成了一个个像素格子。对于每一个像素你从眼睛出发连一条线穿过像素中心射入场景中。这条线就是「主光线」。第二步找最近交点主光线射出去之后会和场景里的各种物体相交。可能和A物体交也可能和B物体交。但我们只关心最近的那个交点——因为离你近的物体会挡住离你远的物体嘛。**✨ 划重点**就这么简单一步光线投射就天然解决了「深度测试」的问题想想光栅化里我们为了搞个Z-Buffer费了多大劲…而光线追踪呢沿着一根光线找最近交点就行了简直不要太优雅第三步发射阴影光线Shadow Ray找到了交点之后怎么知道这个点亮不亮呢很简单——从交点往光源连一条线这条线就是「阴影光线」。如果这条线中途没有被任何物体挡住说明光源能照到这个点那它就是亮的如果被挡住了说明它在阴影里那它就是暗的。第四步计算着色写入像素如果点能被照亮那就用Blinn-Phong或者其他着色模型算一下这个点的颜色然后写进像素里。搞定 3.2 光线投射流程图给大家画个流程图一目了然怎么样是不是挺简单的但是等等——这好像和光栅化也没啥本质区别啊不都是算直接光照吗别急这只是「入门版」。真正的大招还在后面呢… 四、Whitted-Style递归光线追踪让光线「弹」起来1979年一位叫Turner Whitted的「上古大神」提出了一个革命性的想法「光线打到物体上之后为啥不能继续追踪呢」就这么一个简单的想法开启了光线追踪的新时代✨ 4.1 反射与折射的递归魔法想象一下你有一个玻璃球和一个镜面球。当光线打到玻璃球上时会发生什么——一部分光线会被反射出去另一部分光线会折射进入玻璃球内部。折射进去的光线呢在玻璃球内部传播打到另一边的球面上又会发生一次反射和折射然后折射出来继续传播…看到没**光线可以一直弹下去弹无数次**♾️Whitted光线追踪的核心思想就是 在任意一个交点你都可以继续生成新的光线反射光、折射光然后递归地追踪这些光线直到满足终止条件比如弹射次数达到上限或者光线能量衰减到可以忽略。而且每一个弹射的交点都要发射一条阴影光线判断这个点能不能被光源照亮。能照亮的话就把这个点的着色贡献加到最终结果里。这样一来什么镜面反射啊、玻璃折射啊、透明物体的淡阴影啊…这些光栅化头疼的效果光线追踪天然就支持了而且是物理正确的给大家看个效果对比同样是球纯镜面的球投下的阴影很深很实而玻璃球的阴影呢因为光线可以折射穿过去所以阴影会淡很多甚至能看到光斑。这就是物理正确的魅力啊⚡ 4.2 能量守恒不能让光「无限增殖」有同学可能会问了「光线一直弹一直弹那能量岂不是越累加越多最后画面不就全白了」问得好这就是能量守恒的问题。实际上光线每次弹射的时候能量都会被「分流」。比如一束100%能量的光线打到玻璃上40%的能量被反射出去反射光60%的能量折射进入玻璃折射光反射光和折射光各自带着自己那部分能量继续传播下次弹射时再继续分流…这样能量就会越来越少不会无限累加。这就像你把一块蛋糕 分给两个人每人再分给两个人…越分越小最后就可以忽略不计了。 4.3 光线家族大合影到这里咱们已经认识了好几种光线了。来给它们拍个「全家福」吧 光线类型 英文名 说明主光线Primary Ray从相机/眼睛发射的初始光线每个像素一根次级光线Secondary Ray弹射后产生的光线包括反射光线、折射光线阴影光线Shadow Ray从交点向光源发射用于判断可见性不递归反射光线Reflection Ray镜面反射方向的次级光线折射光线Refraction Ray折射方向的次级光线斯涅尔定律 Whitted光线追踪小结✅ 支持完美镜面反射、透明折射✅ 物理正确的阴影包括透明物体的淡阴影✅ 递归算法优雅简洁❌ 只能处理镜面反射/折射不支持漫反射的间接光照那是路径追踪的活儿 五、光线的数学定义一条射线的自我修养好了原理讲了这么多咱们该来点「硬菜」了——数学要实现光线追踪第一步就是得把「光线」用数学语言给定义出来。不然怎么写代码嘛✏️ 5.1 光线的参数方程其实超级简单光线在数学上就是一条射线Ray由两个东西定义起点 O光线从哪里出发比如相机位置➡️方向向量 d光线往哪个方向走单位向量那么光线上任意一个点 P都可以用这个公式表示b o l d s y m b o l r ( t ) b o l d s y m b o l o t b o l d s y m b o l d , q u a d 0 l e t i n f t y boldsymbol{r}(t) boldsymbol{o} tboldsymbol{d},quad 0 le t inftyboldsymbolr(t)boldsymbolotboldsymbold,quad0letinfty其中 t 是一个参数表示「从起点出发沿着方向走了多远」。因为光线是射线不是直线所以 t 必须大于等于 0t0 的话就是往反方向走了那是相机后面没意义。** 翻译成人话**你站在 O 点面朝 d 方向往前走 t 步就到了 r(t) 这个点。t 越大你走得越远。就这么简单⚽ 六、光线与球体求交解个二次方程就行有了光线的定义接下来最核心的问题就是怎么求光线和物体的交点咱们从最简单的物体开始——球Sphere。 6.1 数学推导首先球的隐式方程大家都知道吧球心 C半径 R球面上任意点 P 满足( b o l d s y m b o l p − b o l d s y m b o l c ) c d o t ( b o l d s y m b o l p − b o l d s y m b o l c ) − R 2 0 (boldsymbol{p}-boldsymbol{c}) cdot (boldsymbol{p}-boldsymbol{c}) - R^2 0(boldsymbolp−boldsymbolc)cdot(boldsymbolp−boldsymbolc)−R20翻译一下就是P 到球心 C 的距离等于半径 R。没毛病那什么是「交点」呢——这个点既在光线上又在球面上。所以我们把光线方程代入球面方程就得到了一个关于 t 的方程( b o l d s y m b o l o t b o l d s y m b o l d − b o l d s y m b o l c ) c d o t ( b o l d s y m b o l o t b o l d s y m b o l d − b o l d s y m b o l c ) − R 2 0 (boldsymbol{o}tboldsymbol{d}-boldsymbol{c}) cdot (boldsymbol{o}tboldsymbol{d}-boldsymbol{c}) - R^2 0(boldsymbolotboldsymbold−boldsymbolc)cdot(boldsymbolotboldsymbold−boldsymbolc)−R20把这个式子展开整理一下就变成了一个标准的一元二次方程a t 2 b t c 0 at^2 bt c 0at2btc0其中a b o l d s y m b o l d c d o t b o l d s y m b o l d a boldsymbol{d} cdot boldsymbol{d}aboldsymboldcdotboldsymboldb 2 ( b o l d s y m b o l o − b o l d s y m b o l c ) c d o t b o l d s y m b o l d b 2(boldsymbol{o}-boldsymbol{c}) cdot boldsymbol{d}b2(boldsymbolo−boldsymbolc)cdotboldsymboldc ( b o l d s y m b o l o − b o l d s y m b o l c ) c d o t ( b o l d s y m b o l o − b o l d s y m b o l c ) − R 2 c (boldsymbol{o}-boldsymbol{c}) cdot (boldsymbol{o}-boldsymbol{c}) - R^2c(boldsymbolo−boldsymbolc)cdot(boldsymbolo−boldsymbolc)−R2然后用求根公式t f r a c − b p m s q r t b 2 − 4 a c 2 a t frac{-b pm sqrt{b^2-4ac}}{2a}tfrac−bpmsqrtb2−4ac2a根据判别式的值有三种情况 判别式 交点情况 几何意义Δ 0无实数解光线与球相离不相交Δ 0一个解重根光线与球相切Δ 0两个不同的解光线穿过球有进有出两个交点最后我们还要过滤一下解t 必须大于 0因为是射线不能往回走在所有有效解中取最小的 t——那就是离我们最近的交点怎么样是不是超级简单 说白了就是解个一元二次方程初中数学知识就够了 6.2 C代码实现光说不练假把式来看看代码怎么写structRay{Vec3 o;// 起点 originVec3 d;// 方向 direction单位向量};structSphere{Vec3 c;// 球心 centerfloatr;// 半径 radius};// 返回最近交点的 t 值无交点返回 -1floatintersectRaySphere(constRayray,constSpheresphere){Vec3 ocray.o-sphere.c;// o - cfloatadot(ray.d,ray.d);// d·d 如果d是单位向量a1floatb2.0f*dot(oc,ray.d);// 2*(o-c)·dfloatcdot(oc,oc)-sphere.r*sphere.r;// (o-c)·(o-c) - r²floatdiscriminantb*b-4*a*c;// 判别式 Δ b² - 4acif(discriminant0){return-1.0f;// 无实根不相交}floatsqrtDiscsqrt(discriminant);// 两个根t1 t2floatt1(-b-sqrtDisc)/(2.0f*a);// 近点floatt2(-bsqrtDisc)/(2.0f*a);// 远点// 找最小的正 tif(t10.001f){// 加个 epsilon 防止自相交returnt1;}elseif(t20.001f){returnt2;}return-1.0f;// 两个交点都在相机后面}** 小坑提醒**注意代码里的 0.001f 这个 epsilon 值。为啥要有这个因为光线是从物体表面发射的比如反射光如果 t 从 0 开始算很可能会和「自己」相交产生「自相交」的bug。所以我们从一个很小的正数开始跳过自己 七、通用隐式表面求交f(otd)0的艺术球是一种特殊的「隐式表面」。那如果是任意的隐式表面呢思路是完全一样的隐式表面的定义是f§ 0满足这个方程的点构成物体表面把光线方程代入f(o td) 0然后解这个关于 t 的方程就行了简单的隐式表面比如球、平面、圆柱可以求出解析解就是直接用公式算。复杂的隐式表面呢没有解析解怎么办——没关系咱们有数值方法啊什么牛顿迭代法啊、二分法啊…都可以用来求函数的根。现在数值计算软件这么发达解个根还不是分分钟的事⚡解出 t 之后交点坐标就是 p o td完美 八、光线与三角形求交光追世界的「硬通货」说了半天隐式表面但咱们实际做渲染的时候用得最多的还是三角形网格啊毕竟三维模型都是由一个个三角形面片组成的嘛所以光线与三角形求交可以说是光线追踪里最核心、最常用的几何运算了没有之一 8.1 两步走思路求光线和三角形的交点一般分两步求光线与三角形所在平面的交点判断这个交点是不是在三角形内部第一步很简单平面方程代入光线方程解个 t 就完了。第二步呢可以用重心坐标来判断——如果重心坐标的三个分量都大于等于 0而且加起来等于 1那点就在三角形内部。⚡ 8.2 Möller-Trumbore 算法当然业界有更高效的算法——Möller-Trumbore 算法可以一步到位直接求出 t 和重心坐标不用先求平面。速度快、精度高是光线追踪的标配这个算法的推导稍微有点复杂涉及到克莱姆法则什么的…咱们今天就先不展开了大家知道有这么个东西就行以后有机会单独开一篇讲 8.3 额外福利点在多边形内判断光线和三角形求交还能干嘛告诉你一个冷知识** 奇技淫巧**给你一个任意封闭的几何体再给你一个点怎么判断这个点在物体内部还是外部方法很简单从这个点随便发射一条射线数它和物体表面相交了多少次。• 奇数次 → 点在物体内部 • 偶数次 → 点在物体外部 是不是很神奇这就是「奇偶规则」在很多领域都有用 九、总结从理论到实践的漫漫长路好了今天的内容就到这里啦让我们来总结一下都学了啥 9.1 核心知识点回顾光栅化的局限全局光照效果阴影、Glossy反射、间接光照实现困难近似度高光线追踪的优势物理正确天然支持全局光照但速度慢传统为离线渲染光路可逆性从相机出发反向追踪光线是光线追踪的核心理论基础光线投射主光线 阴影光线只算直接光照是光追的入门版Whitted递归光线追踪支持反射、折射的递归追踪能做出镜面和玻璃效果光线的数学定义r(t) o td一条射线光线与球求交解一元二次方程简单优雅光线与三角形求交光追最核心的运算Möller-Trumbore算法是标配 9.2 前方还有什么当然Whitted光线追踪只是光线追踪的「入门款」。前方还有更多硬核内容等着我们路径追踪Path Tracing支持漫反射间接光照真正的全局光照加速结构BVH、KD-Tree让光线求交从 O(n) 变成 O(log n)快到飞起蒙特卡洛积分随机采样的艺术噪点与速度的博弈⚡实时光线追踪RTX显卡的黑科技DXR、Vulkan Ray Tracing…图形学的世界真是又大又深啊 写在最后从1979年Whitted的经典论文到如今RTX显卡走进千家万户光线追踪走过了四十多年的历程。硬件在进步算法在优化但那些最核心、最优雅的数学原理却始终散发着迷人的光芒。✨希望这篇文章能帮你推开光线追踪世界的大门。如果觉得有帮助别忘了点赞 收藏⭐ 关注➕三连哦咱们下期再见 参考资料计算机图形学经典课程、《Real-Time Rendering》、《Physically Based Rendering》