想象你是一位剧组导演,面前摆着一台神奇的摄像机。这台摄像机不是”拍“画面,而是朝场景里发射光线:光线从镜头出发,撞到演员脸上,弹向墙面,再弹回灯光,最后把沿途看到的颜色全部记下来——这就是光线追踪。过去三十年,游戏画面靠的是另一套“快拍”手法,叫光栅化,它像群演站好位、摄影师快速按快门,速度快但拍不出真实的反射和折射。如今,NVIDIA、AMD 的显卡里装上了专门“追光”的硬件,而 DXR(DirectX Raytracing)和 Vulkan Ray Tracing 就是指挥这套硬件的两本“导演手册”。这篇文章,我们就用一场“电影剧组”的比喻,把 DXR 和 Vulkan 光线追踪的核心原理、加速结构、混合渲染和性能优化讲明白。
对 AI 从业者而言,理解光线追踪不仅是进入图形学与自动驾驶仿真世界的钥匙,更能帮你在神经渲染、AI 降噪、合成数据生成等前沿方向找到 GPU 编程的共通思维。
目录
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
光线追踪的本质
想象你既是导演又是唯一一位摄影师。传统剧组的做法是:让所有演员站好,灯光师打开灯,摄影师站在机位前“咔嚓”一拍,整个画面的颜色瞬间记录在底片上——这就是光栅化,快,但它拍不到镜面反射,因为光线不会停下来追问“你看到了什么”。而光线追踪是另一种拍法:摄影师只背对演员,把镜头对准画面,朝每个像素发射一根“侦察光线”,让它去问场景里的每一个物体:“你这里是什么颜色?你反射了什么?你把谁挡住了?”因为光线是从相机出发、逆着真实光路走的,所以我们叫它逆向追踪。
这背后的“光量守恒”规则,就是图形学里著名的渲染方程。它告诉我们:一个像素最终的颜色,等于物体自己发光的颜色,加上所有从四面八方射来的光经过物体表面反射后的总和。
所以,光线追踪本质上是一台“递归追问机”。DXR 和 Vulkan 要做的,就是把这台追问机搬上 GPU,让它跑得足够快,快到能塞进一帧 16 毫秒的游戏画面里。
- 出射亮度某点朝某方向射出的光线亮度,即像素最终看到的颜色。
- 自发光项物体自身发出的光,如灯泡、太阳。
- BRDF 材质函数描述表面如何反射入射光,决定它是镜面、哑光还是金属。
- 余弦衰减入射光方向与表面法线的夹角越斜,贡献越小。
- 积分与递归把所有入射方向 \(\omega_i\) 的光累加起来,而每一束入射光本身又是其他表面的出射光,形成递归。
DXR 与 Vulkan:两本导演手册
既然要“追光”,总得有设备。DXR 是微软为 DirectX 12 定制的追光套件,就像好莱坞大制片厂的专用摄影棚,Windows 和 Xbox 上“开箱即用”,与 DirectX 的其余管线无缝衔接。Vulkan Ray Tracing 则是 Khronos 集团推出的通用巡演摄影棚,它不绑定任何操作系统,可以在 Windows、Linux、Android 上跨平台运行,但需要你亲手搭更多的脚手架。两者目标一致:告诉 GPU“这里有根光线,请帮我找它撞到的第一个物体”。
DXR 的核心流程像剧组的“三张场记单”:第一张是加速结构(Acceleration Structure),把场景里的几何体提前装进 BVH 树;第二张是 Shader Binding Table(SBT),记录每个物体命中后该调用哪个材质的“演技脚本”;第三张是 TraceRay 指令,导演喊“Action”,光线出发。Vulkan 的流程几乎相同,只是把“场记单”的名字改成 VkAccelerationStructureKHR、VkRayTracingPipelineKHR。如果显卡里有专门的光追硬件单元(RT Core),两本手册都能直接指挥它;如果没有,就只能用通用计算单元“软磨硬泡”,速度会慢一个数量级。
选 DXR 还是 Vulkan,本质是选“生态”还是选“自由”。对 AI 从业者来说,两者底层共享同一套 BVH 加速和递归光线思维,学会一门,另一门便能触类旁通。
- 光追算力显卡中 RT Core 处理光线求交的吞吐量。
- 总算力GPU 所有计算单元(CUDA 核、光栅化单元等)的总吞吐量。
- 占比该比值越高,说明光线追踪的“专属设备”越多,追光越快。
加速结构:BVH 是后台的“快递分拣系统”
光线追踪最大的麻烦是“大海捞针”:几百万根光线要逐一问几百万个三角形“你被撞到了吗”?这显然是灾难。于是 GPU 的“道具间”里多了一套快递分拣系统——BVH(Bounding Volume Hierarchy,层级包围盒树)。
BVH 的构建很像快递站分区:先把所有三角形装进一个大纸箱(根节点),然后从中间一刀切成两半,分别装进两个小纸箱;再对每个小纸箱切分……直到每个纸箱里只剩几个三角形。当一根光线来“寻件”时,它先看最大的纸箱在哪,若光线连大纸箱都碰不到,整棵子树直接跳过;若碰到了,就进小纸箱继续查。这个过程叫遍历剪枝。
构建 BVH 时还常使用 SAH(表面积启发) 来决定从哪一刀切开最划算:表面积大的纸箱,被光线碰到的概率高,应优先切分。DXR 和 Vulkan 都允许你“离线建好树再上传”,也支持每帧增量更新——这正是游戏里物体移动时仍能保持光追性能的秘诀。
- 暴力复杂度没有加速结构时,每根光线要检查全部 \(n\) 个三角形。
- BVH 复杂度每层排除一半纸箱,查找次数只与树的深度有关。
- 对比当 \(n=1{,}000{,}000\) 时,暴力要查 \(10^6\) 次,BVH 只需约 20 步。
混合渲染:让替身演员先上场
即使有了 BVH,全场景纯光线追踪依然昂贵。于是现代引擎普遍采用混合渲染:就像导演拍动作戏时,先让廉价替身把大场面走完,再让主角补几个特写镜头。具体分工是——
1. 光栅化打底:先用传统光栅化把不透明的几何体快速画出来,把每个像素的颜色、深度、法线存到 GBuffer(一张“信息底片”)里;
2. 光追补光:只对需要反射、阴影、全局光的像素发射光线,读取 GBuffer 就能知道“该往哪个方向追”;
3. 屏幕空间技术:反射优先用屏幕空间反射(SSR)在已有画面里“捞”颜色,捞不到的地方再用光追补齐。
1. 光栅化耗时:传统三角形填充耗时,负责“打底”。
2. 光追耗时:光线求交与着色耗时,负责“点睛”。
3. 后处理耗时:降噪、色调映射等收尾工序。
4. 总预算:一帧只有约 16.6ms,混合渲染就是三方争抢这个预算。
性能优化:降噪、复用与自适应
光线追踪生成的一帧画面,往往每个像素只采样 1~4 根光线,噪声大得像老式电视的雪花。怎么办?剧组有三大法宝:
第一,降噪器。就像美颜滤镜,利用相邻像素颜色相近、时间上前后帧相近的特性,把噪声抹平。硬件 RTX 显卡甚至内置了专用降噪单元。第二,时间复用。上一帧已经算过的光照信息别丢,下一帧直接复用,只对新出现的像素重新追光。第三,自适应采样。画面平坦的地方少采几根光线,边缘和反射剧烈的地方多采几根,把预算花在刀刃上。
1. 基础成本:每个像素最低采样开销。
2. 复杂度权重:根据像素邻域方差计算,变化越剧烈权重越高。
3. 加权成本:平坦区域 \(w_n \approx 1\),复杂区域 \(w_n\) 增大,实现“好钢用在刀刃上”。
工程实践:道具、场记与预算
在实际项目里选择 DXR 还是 Vulkan,就像决定在哪个摄影棚开机:如果你的目标是 Windows 游戏和 Xbox,选 DXR——它和 DirectX 12 的生态无缝衔接,调试工具 PIX 也最成熟;如果团队要做跨平台引擎(Windows/Linux/移动端),Vulkan 是唯一解,但要做好“自己搭更多脚手架”的心理准备,调试工具则常用 RenderDoc 和 Nsight Graphics。
内存管理方面,加速结构是 GPU 显存里的“大件快递”,要避免每帧重建;Shader 调试要善用 Ray Generation Shader 里的断言和可视化输出。一个经典的实战案例是《赛博朋克 2077》的“光追模式”:先用光栅化打出整个夜之城的底,再对车窗、积水、霓虹灯反射发射光线,配合 DLSS(深度学习超采样)降低渲染分辨率,最终在 4K 下也能跑出流畅的 60 帧。这套“混合渲染 + 降噪 + 超分”的组合拳,正是当前所有 AAA 光追游戏的标配。
AI 应用场景
场景一:AI 降噪器
阶段:数据后处理
解决的问题:传统降噪依赖手工调参,在低采样率(每像素 1 根光线)下容易糊掉或产生鬼影;AI 降噪器用神经网络学习“噪声图 → 干净图”的映射,让光线追踪能以极低采样率运行再还原出高质量画面。
具体案例:NVIDIA 的 RTXDI 与 OptiX AI 降噪(OIDN),在游戏《地铁:离去》增强版中,AI 降噪让每像素仅采样 1 根光线即可输出干净画面,性能提升超过 3 倍。
场景二:自动驾驶仿真数据生成
阶段:训练数据合成
解决的问题:真实激光雷达数据采集成本高、长尾场景难覆盖;利用 DXR/光追渲染,可精确模拟激光雷达光束在雨、雾、黑夜中的反射与吸收,批量生成带标注的合成点云。
具体案例:NVIDIA DRIVE Sim 使用 RTX 光线追踪生成逼真的摄像头与激光雷达传感器数据,用于训练自动驾驶感知模型,雨天场景生成速度比实车采集快 100 倍。
Python 演示
import math class Vec3: def __init__(self, x=0, y=0, z=0): self.x, self.y, self.z = x, y, z def __add__(self, o): return Vec3(self.x+o.x, self.y+o.y, self.z+o.z) def __sub__(self, o): return Vec3(self.x-o.x, self.y-o.y, self.z-o.z) def __mul__(self, s): return Vec3(self.x*s, self.y*s, self.z*s) def dot(self, o): return self.x*o.x + self.y*o.y + self.z*o.z def norm(self): l = math.sqrt(self.dot(self)) return Vec3(self.x/l, self.y/l, self.z/l) if l > 0 else Vec3() class AABB: def __init__(self, c, r): self.c, self.r = c, r def hit(self, o, d): t0, t1 = -1e9, 1e9 for i in range(3): oc = o.__dict__[list("xyz")[i]] - self.c.__dict__[list("xyz")[i]] inv = 1.0 / d.__dict__[list("xyz")[i]] if abs(d.__dict__[list("xyz")[i]]) > 1e-8 else 1e8 tmin = (-self.r - oc) * inv tmax = (self.r - oc) * inv if tmin > tmax: tmin, tmax = tmax, tmin t0 = max(t0, tmin) t1 = min(t1, tmax) if t1 < t0: return False return True class Sphere: def __init__(self, c, r, color): self.c, self.r, self.color = c, r, color def intersect(self, o, d): oc = o - self.c b = oc.dot(d) c = oc.dot(oc) - self.r * self.r disc = b * b - c if disc < 0: return -1 sq = math.sqrt(disc) t = -b - sq if -b - sq > 0 else -b + sq return t if t > 0 else -1 def build_bvh(spheres, l, r): if r - l <= 2: box = spheres[l] return [box, None, None] mid = (l + r) // 2 nodes = [spheres[l]] left = build_bvh(spheres, l, mid) right = build_bvh(spheres, mid, r) return [box_from(spheres, l, r), left, right] def box_from(spheres, l, r): c = Vec3() for s in spheres[l:r]: c = c + s.c n = r - l c = c * (1.0 / n) rr = 0.0 for s in spheres[l:r]: rr = max(rr, (s.c - c).dot(s.c - c) + s.r * s.r) return AABB(c, math.sqrt(rr)) def find_hit(bvh, o, d, t_min): node, left, right = bvh if isinstance(node, Sphere): t = node.intersect(o, d) return (node, t) if 0 < t < t_min else (None, 1e9) if not node.hit(o, d): return (None, 1e9) h1, t1 = find_hit(left, o, d, t_min) h2, t2 = find_hit(right, o, d, t_min) return (h1, t1) if t1 < t2 else (h2, t2) def trace(spheres, bvh, o, d, depth): if depth <= 0: return Vec3(0.1, 0.1, 0.2) hit, t = find_hit(bvh, o, d, 1e9) if hit is None: return Vec3(0.1, 0.1, 0.2) p = o + d * t n = (p - hit.c).norm() lambert = max(0.0, n.dot(Vec3(0.5, 0.7, 1.0).norm())) reflected = trace(spheres, bvh, p, (d - n * (2 * d.dot(n))).norm(), depth - 1) return hit.color * (lambert * 0.6 + reflected * 0.4) def main(): spheres = [ Sphere(Vec3(0, 0.2, 2.0), 0.5, Vec3(0.9, 0.3, 0.3)), Sphere(Vec3(0.9, -0.2, 1.2), 0.35, Vec3(0.3, 0.8, 0.3)), Sphere(Vec3(-0.9, 0, 1.6), 0.4, Vec3(0.3, 0.4, 0.9)), ] bvh = build_bvh(spheres, 0, len(spheres)) w, h = 36, 12 for j in range(h - 1, -1, -1): line = "" for i in range(w): u = (i / (w - 1)) * 2.4 - 1.2 v = (j / (h - 1)) * 1.4 - 0.7 col = trace(spheres, bvh, Vec3(0, 0, 0), Vec3(u, v, 1).norm(), 3) ch = " .:+#" line += ch[min(4, int(col.x * 5.0))] print(line) if __name__ == "__main__": main()
class Vec3三维向量类,光线追踪里所有坐标、方向、颜色都靠它表达。class AABB轴对齐包围盒,BVH 中“快递纸箱”的最小单位,hit方法快速判断光线是否穿过纸箱。class Sphere球体几何体,intersect解二次方程求交点,对应 GPU 里的三角形求交。bvh自顶向下构建 BVH:递归二分球体列表,每层生成一个大纸箱。 — 这条规则让 BVH 最快、最根本地发挥作用。find_hit二分遍历:先测纸箱,命中才递归进子树,否则整棵跳过。trace递归追踪主函数:碰到物体后沿法线方向散射,并继续追踪反射光线,直到深度耗尽。main组装场景、构建 BVH、逐像素发射光线并输出字符画。
常见误区
误区既然光追这么真实,那游戏引擎应该彻底抛弃光栅化,全场景都用光线追踪渲染。
正解现实恰恰相反,纯光追的渲染开销大到连顶级显卡都无法实时承载。工程上普遍采用混合渲染:光栅化负责“打底”,光追只补充反射、阴影、全局光等难点效果。就像拍电影不会让主角替身演完全程,但替身的确承担了大量危险动作。
为何常见:媒体宣传常把光追描述成“革命性技术”,让人误以为它是光栅化的替代品,而忽略了工程上的折中。
误区DXR 是微软的,Vulkan 是开源的,两者底层原理完全不同,学了 DXR 对 Vulkan 没帮助。
正解它们在概念层高度同构:加速结构(BVH)、Shader Binding Table、TraceRay 调用,甚至连遍历流程都几乎一一对应。区别主要在 API 语法和平台生态,而非原理。会开一种摄影棚的灯,换一个摄影棚同样能开灯。
为何常见:初学者看到两套完全不同的 API 函数名,容易误以为底层也完全不同。
误区BVH 树分得越细、节点越多,剔除越精确,性能一定越好。
正解BVH 的性能是“构建/更新成本”与“遍历效率”的权衡。树过深会导致遍历时栈操作开销增大,且构建时间变长。工程上常用表面积启发式(SAH)寻找最优切分,而非一味细分。快递站纸箱分得太多,找件时翻箱子的时间反而变长。
为何常见:“优化 = 更精细”的直觉很强,但忽略了遍历本身的固定开销。
面试题
Q1. 光线追踪中,光线是从哪里出发的?
- A.从光源向场景发射
- B.从相机向场景发射← 正确答案
- C.从物体向光源发射
- D.从屏幕向相机发射
答案:B。光线追踪采用逆向追踪,从相机出发射向场景。若从光源出发,大部分光线根本不会进入相机视野,造成巨大浪费。
Q2. 渲染方程中,\(L_e\) 代表什么?
- A.物体表面反射的总亮度
- B.物体自身发出的亮度← 正确答案
- C.入射光的总能量
- D.相机接收的环境光
答案:B。\(L_e\) 是自发光项,代表光源或发热物体自身发出的光。其余选项分别对应渲染方程中的其他项。
Q3. BVH 加速结构最核心的优势是?
- A.减少三角形数量
- B.将求交复杂度从 \(O(n)\) 降为 \(O(\log n)\)← 正确答案
- C.让光栅化速度更快
- D.消除阴影锯齿
答案:B。BVH 通过层级包围盒剪枝,使光线求交时不必遍历所有三角形,复杂度降到对数级。它并不减少三角形数量,也不影响光栅化。
Q4. DXR 与 Vulkan Ray Tracing 的共同点是?
- A.只能运行在 Windows 平台
- B.使用完全相同的 API 函数名
- C.底层都依赖加速结构加速光线求交← 正确答案
- D.仅支持 NVIDIA 显卡
答案:C。两者 API 不同、生态各异,但核心都是基于 BVH 加速结构的光线求交流程,硬件也无厂商绑定。
Q5. 混合渲染中,光栅化主要负责什么?
- A.计算全局光照
- B.绘制基础几何体并填充 GBuffer← 正确答案
- C.发射阴影光线
- D.降噪后处理
答案:B。混合渲染先用光栅化快速绘制不透明几何体并生成 GBuffer,光线追踪只对反射、阴影等局部效果补充计算。
Q6. 自适应采样中,像素的采样权重 \(w_n\) 通常由什么决定?
- A.像素距离屏幕中心的欧氏距离
- B.像素邻域的方差或梯度变化程度← 正确答案
- C.像素的 RGB 亮度总和
- D.相机曝光时间
答案:B。权重由邻域方差决定,方差大说明光照变化剧烈,需要增加采样。亮度高并不等于变化剧烈,距离也与复杂度无关。
Q7. 遍历 BVH 时,若光线与某包围盒不相交,正确的做法是?
- A.递归检查该盒所有子节点
- B.渲染该盒为黑色
- C.直接跳过整棵子树← 正确答案
- D.将该盒标记为光源
答案:C。BVH 的剪枝核心:光线连外部大纸箱都碰不到,纸箱内的物体必然也碰不到,因此整棵子树可以直接跳过。
Q8. 下列哪些属于光线追踪可以实现的视觉效果?
- A.镜面反射← 正确答案
- B.透明折射← 正确答案
- C.柔和阴影← 正确答案
- D.颜色插值
答案:ABC。光追天然支持反射、折射、柔和阴影,因为它们都是光线与几何体物理交互的结果;颜色插值是光栅化中的常见技术。
Q9. DXR 管线中的关键组成部分包括?
- A.加速结构← 正确答案
- B.Shader Binding Table← 正确答案
- C.TraceRay 指令← 正确答案
- D.深度缓冲
答案:ABC。DXR 三部曲:加速结构组织几何、SBT 绑定材质、TraceRay 发射光线。深度缓冲属于光栅化阶段的产品。
Q10. 以下哪些是提升光线追踪性能的常用策略?
- A.时间复用上一帧光照结果← 正确答案
- B.降低 BVH 树的最大深度
- C.自适应采样← 正确答案
- D.AI 降噪← 正确答案
答案:ACD。时间复用、自适应采样、AI 降噪都是主流优化手段。降低 BVH 深度反而会增加每个节点的求交开销,通常得不偿失。