想象你是一位剧组导演,面前摆着一台神奇的摄像机。这台摄像机不是”拍“画面,而是朝场景里发射光线:光线从镜头出发,撞到演员脸上,弹向墙面,再弹回灯光,最后把沿途看到的颜色全部记下来——这就是光线追踪。过去三十年,游戏画面靠的是另一套“快拍”手法,叫光栅化,它像群演站好位、摄影师快速按快门,速度快但拍不出真实的反射和折射。如今,NVIDIA、AMD 的显卡里装上了专门“追光”的硬件,而 DXR(DirectX Raytracing)和 Vulkan Ray Tracing 就是指挥这套硬件的两本“导演手册”。这篇文章,我们就用一场“电影剧组”的比喻,把 DXR 和 Vulkan 光线追踪的核心原理、加速结构、混合渲染和性能优化讲明白。

岗位关联

对 AI 从业者而言,理解光线追踪不仅是进入图形学与自动驾驶仿真世界的钥匙,更能帮你在神经渲染、AI 降噪、合成数据生成等前沿方向找到 GPU 编程的共通思维。

目录
  1. 可视化演示
  2. 光线追踪的本质
  3. DXR 与 Vulkan:两本导演手册
  4. 加速结构:BVH 是后台的“快递分拣系统”
  5. 混合渲染:让替身演员先上场
  6. 性能优化:降噪、复用与自适应
  7. 工程实践:道具、场记与预算
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

光线追踪的本质

想象你既是导演又是唯一一位摄影师。传统剧组的做法是:让所有演员站好,灯光师打开灯,摄影师站在机位前“咔嚓”一拍,整个画面的颜色瞬间记录在底片上——这就是光栅化,快,但它拍不到镜面反射,因为光线不会停下来追问“你看到了什么”。而光线追踪是另一种拍法:摄影师只背对演员,把镜头对准画面,朝每个像素发射一根“侦察光线”,让它去问场景里的每一个物体:“你这里是什么颜色?你反射了什么?你把谁挡住了?”因为光线是从相机出发、逆着真实光路走的,所以我们叫它逆向追踪。

这背后的“光量守恒”规则,就是图形学里著名的渲染方程。它告诉我们:一个像素最终的颜色,等于物体自己发光的颜色,加上所有从四面八方射来的光经过物体表面反射后的总和。

所以,光线追踪本质上是一台“递归追问机”。DXR 和 Vulkan 要做的,就是把这台追问机搬上 GPU,让它跑得足够快,快到能塞进一帧 16 毫秒的游戏画面里。

Lo(x,ωo)=Le(x,ωo)+∫Ωfr(x,ωi,ωo) Li(x,ωi) (ωi⋅n) dωi\displaystyle L_o(\mathbf{x}, \omega_o) = L_e(\mathbf{x}, \omega_o) + \int_{\Omega} f_r(\mathbf{x}, \omega_i, \omega_o) \, L_i(\mathbf{x}, \omega_i) \, (\omega_i \cdot \mathbf{n}) \, \mathrm{d}\omega_i
这个公式像剧组的“灯光结算单”:出射的亮度 \(L_o\),由自发光 \(L_e\) 和所有入射光贡献的积分共同构成。
  1. 出射亮度某点朝某方向射出的光线亮度,即像素最终看到的颜色。
    Lo(x,ωo)\displaystyle L_o(\mathbf{x}, \omega_o)
  2. 自发光项物体自身发出的光,如灯泡、太阳。
    Le(x,ωo)\displaystyle L_e(\mathbf{x}, \omega_o)
  3. BRDF 材质函数描述表面如何反射入射光,决定它是镜面、哑光还是金属。
    fr(x,ωi,ωo)\displaystyle f_r(\mathbf{x}, \omega_i, \omega_o)
  4. 余弦衰减入射光方向与表面法线的夹角越斜,贡献越小。
    (ωi⋅n)\displaystyle (\omega_i \cdot \mathbf{n})
  5. 积分与递归把所有入射方向 \(\omega_i\) 的光累加起来,而每一束入射光本身又是其他表面的出射光,形成递归。
    ∫Ω⋯ dωi\displaystyle \int_{\Omega} \cdots \, \mathrm{d}\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 加速和递归光线思维,学会一门,另一门便能触类旁通。

r=WrtWall\displaystyle r = \frac{W_{rt}}{W_{all}}
这是“硬件追光算力占比”公式:\(W_{rt}\) 是光追专用算力,\(W_{all}\) 是 GPU 总算力。
  1. 光追算力显卡中 RT Core 处理光线求交的吞吐量。
    Wrt\displaystyle W_{rt}
  2. 总算力GPU 所有计算单元(CUDA 核、光栅化单元等)的总吞吐量。
    Wall\displaystyle W_{all}
  3. 占比该比值越高,说明光线追踪的“专属设备”越多,追光越快。
    r=WrtWall\displaystyle r = \frac{W_{rt}}{W_{all}}

加速结构:BVH 是后台的“快递分拣系统”

光线追踪最大的麻烦是“大海捞针”:几百万根光线要逐一问几百万个三角形“你被撞到了吗”?这显然是灾难。于是 GPU 的“道具间”里多了一套快递分拣系统——BVH(Bounding Volume Hierarchy,层级包围盒树)。

BVH 的构建很像快递站分区:先把所有三角形装进一个大纸箱(根节点),然后从中间一刀切成两半,分别装进两个小纸箱;再对每个小纸箱切分……直到每个纸箱里只剩几个三角形。当一根光线来“寻件”时,它先看最大的纸箱在哪,若光线连大纸箱都碰不到,整棵子树直接跳过;若碰到了,就进小纸箱继续查。这个过程叫遍历剪枝。

构建 BVH 时还常使用 SAH(表面积启发) 来决定从哪一刀切开最划算:表面积大的纸箱,被光线碰到的概率高,应优先切分。DXR 和 Vulkan 都允许你“离线建好树再上传”,也支持每帧增量更新——这正是游戏里物体移动时仍能保持光追性能的秘诀。

T(n)=O(log⁡n)vsTnaive(n)=O(n)\displaystyle T(n) = O(\log n) \quad \text{vs} \quad T_{\text{naive}}(n) = O(n)
BVH 让每一次光线求交的代价从“问遍所有人”变成“二分查找”,复杂度从线性降到对数级。
  1. 暴力复杂度没有加速结构时,每根光线要检查全部 \(n\) 个三角形。
    Tnaive(n)=O(n)\displaystyle T_{\text{naive}}(n) = O(n)
  2. BVH 复杂度每层排除一半纸箱,查找次数只与树的深度有关。
    T(n)=O(log⁡n)\displaystyle T(n) = O(\log n)
  3. 对比当 \(n=1{,}000{,}000\) 时,暴力要查 \(10^6\) 次,BVH 只需约 20 步。
    O(log⁡n)≪O(n)\displaystyle O(\log n) \ll O(n)

混合渲染:让替身演员先上场

即使有了 BVH,全场景纯光线追踪依然昂贵。于是现代引擎普遍采用混合渲染:就像导演拍动作戏时,先让廉价替身把大场面走完,再让主角补几个特写镜头。具体分工是——

1. 光栅化打底:先用传统光栅化把不透明的几何体快速画出来,把每个像素的颜色、深度、法线存到 GBuffer(一张“信息底片”)里;

2. 光追补光:只对需要反射、阴影、全局光的像素发射光线,读取 GBuffer 就能知道“该往哪个方向追”;

3. 屏幕空间技术:反射优先用屏幕空间反射(SSR)在已有画面里“捞”颜色,捞不到的地方再用光追补齐。

Tframe=Traster+Trt+Tpost\displaystyle T_{frame} = T_{raster} + T_{rt} + T_{post}

1. 光栅化耗时:传统三角形填充耗时,负责“打底”。

Traster\displaystyle T_{raster}

2. 光追耗时:光线求交与着色耗时,负责“点睛”。

Trt\displaystyle T_{rt}

3. 后处理耗时:降噪、色调映射等收尾工序。

Tpost\displaystyle T_{post}

4. 总预算:一帧只有约 16.6ms,混合渲染就是三方争抢这个预算。

Tframe\displaystyle T_{frame}

性能优化:降噪、复用与自适应

光线追踪生成的一帧画面,往往每个像素只采样 1~4 根光线,噪声大得像老式电视的雪花。怎么办?剧组有三大法宝:

第一,降噪器。就像美颜滤镜,利用相邻像素颜色相近、时间上前后帧相近的特性,把噪声抹平。硬件 RTX 显卡甚至内置了专用降噪单元。第二,时间复用。上一帧已经算过的光照信息别丢,下一帧直接复用,只对新出现的像素重新追光。第三,自适应采样。画面平坦的地方少采几根光线,边缘和反射剧烈的地方多采几根,把预算花在刀刃上。

C=wn×Cbase\displaystyle C = w_n \times C_{base}

1. 基础成本:每个像素最低采样开销。

Cbase\displaystyle C_{base}

2. 复杂度权重:根据像素邻域方差计算,变化越剧烈权重越高。

wn=1+λ⋅Var\displaystyle w_n = 1 + \lambda \cdot \mathrm{Var}

3. 加权成本:平坦区域 \(w_n \approx 1\),复杂区域 \(w_n\) 增大,实现“好钢用在刀刃上”。

C=wn×Cbase\displaystyle C = w_n \times C_{base}

工程实践:道具、场记与预算

在实际项目里选择 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 演示

python106 行
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()
目的:用 Python 实现了一个极简的光线追踪器,包含 BVH 加速结构和 3 级递归反射,输出 ASCII 像素画。它演示了 DXR 后台的核心机制——加速结构构建、包围盒剔除、递归光线求交。
  1. class Vec3三维向量类,光线追踪里所有坐标、方向、颜色都靠它表达。
  2. class AABB轴对齐包围盒,BVH 中“快递纸箱”的最小单位,hit 方法快速判断光线是否穿过纸箱。
  3. class Sphere球体几何体,intersect 解二次方程求交点,对应 GPU 里的三角形求交。
  4. bvh自顶向下构建 BVH:递归二分球体列表,每层生成一个大纸箱。 — 这条规则让 BVH 最快、最根本地发挥作用。
  5. find_hit二分遍历:先测纸箱,命中才递归进子树,否则整棵跳过。
  6. trace递归追踪主函数:碰到物体后沿法线方向散射,并继续追踪反射光线,直到深度耗尽。
  7. main组装场景、构建 BVH、逐像素发射光线并输出字符画。

常见误区

误区既然光追这么真实,那游戏引擎应该彻底抛弃光栅化,全场景都用光线追踪渲染。

正解现实恰恰相反,纯光追的渲染开销大到连顶级显卡都无法实时承载。工程上普遍采用混合渲染:光栅化负责“打底”,光追只补充反射、阴影、全局光等难点效果。就像拍电影不会让主角替身演完全程,但替身的确承担了大量危险动作。

为何常见:媒体宣传常把光追描述成“革命性技术”,让人误以为它是光栅化的替代品,而忽略了工程上的折中。

误区DXR 是微软的,Vulkan 是开源的,两者底层原理完全不同,学了 DXR 对 Vulkan 没帮助。

正解它们在概念层高度同构:加速结构(BVH)、Shader Binding Table、TraceRay 调用,甚至连遍历流程都几乎一一对应。区别主要在 API 语法和平台生态,而非原理。会开一种摄影棚的灯,换一个摄影棚同样能开灯。

为何常见:初学者看到两套完全不同的 API 函数名,容易误以为底层也完全不同。

误区BVH 树分得越细、节点越多,剔除越精确,性能一定越好。

正解BVH 的性能是“构建/更新成本”与“遍历效率”的权衡。树过深会导致遍历时栈操作开销增大,且构建时间变长。工程上常用表面积启发式(SAH)寻找最优切分,而非一味细分。快递站纸箱分得太多,找件时翻箱子的时间反而变长。

为何常见:“优化 = 更精细”的直觉很强,但忽略了遍历本身的固定开销。

面试题

Q1. 光线追踪中,光线是从哪里出发的?

单选题 · easy

  • A.从光源向场景发射
  • B.从相机向场景发射← 正确答案
  • C.从物体向光源发射
  • D.从屏幕向相机发射

答案:B。光线追踪采用逆向追踪,从相机出发射向场景。若从光源出发,大部分光线根本不会进入相机视野,造成巨大浪费。

Q2. 渲染方程中,\(L_e\) 代表什么?

单选题 · easy

  • A.物体表面反射的总亮度
  • B.物体自身发出的亮度← 正确答案
  • C.入射光的总能量
  • D.相机接收的环境光

答案:B。\(L_e\) 是自发光项,代表光源或发热物体自身发出的光。其余选项分别对应渲染方程中的其他项。

Q3. BVH 加速结构最核心的优势是?

单选题 · mid

  • A.减少三角形数量
  • B.将求交复杂度从 \(O(n)\) 降为 \(O(\log n)\)← 正确答案
  • C.让光栅化速度更快
  • D.消除阴影锯齿

答案:B。BVH 通过层级包围盒剪枝,使光线求交时不必遍历所有三角形,复杂度降到对数级。它并不减少三角形数量,也不影响光栅化。

Q4. DXR 与 Vulkan Ray Tracing 的共同点是?

单选题 · mid

  • A.只能运行在 Windows 平台
  • B.使用完全相同的 API 函数名
  • C.底层都依赖加速结构加速光线求交← 正确答案
  • D.仅支持 NVIDIA 显卡

答案:C。两者 API 不同、生态各异,但核心都是基于 BVH 加速结构的光线求交流程,硬件也无厂商绑定。

Q5. 混合渲染中,光栅化主要负责什么?

单选题 · mid

  • A.计算全局光照
  • B.绘制基础几何体并填充 GBuffer← 正确答案
  • C.发射阴影光线
  • D.降噪后处理

答案:B。混合渲染先用光栅化快速绘制不透明几何体并生成 GBuffer,光线追踪只对反射、阴影等局部效果补充计算。

Q6. 自适应采样中,像素的采样权重 \(w_n\) 通常由什么决定?

单选题 · hard

  • A.像素距离屏幕中心的欧氏距离
  • B.像素邻域的方差或梯度变化程度← 正确答案
  • C.像素的 RGB 亮度总和
  • D.相机曝光时间

答案:B。权重由邻域方差决定,方差大说明光照变化剧烈,需要增加采样。亮度高并不等于变化剧烈,距离也与复杂度无关。

Q7. 遍历 BVH 时,若光线与某包围盒不相交,正确的做法是?

单选题 · hard

  • A.递归检查该盒所有子节点
  • B.渲染该盒为黑色
  • C.直接跳过整棵子树← 正确答案
  • D.将该盒标记为光源

答案:C。BVH 的剪枝核心:光线连外部大纸箱都碰不到,纸箱内的物体必然也碰不到,因此整棵子树可以直接跳过。

Q8. 下列哪些属于光线追踪可以实现的视觉效果?

多选题 · easy

  • A.镜面反射← 正确答案
  • B.透明折射← 正确答案
  • C.柔和阴影← 正确答案
  • D.颜色插值

答案:ABC。光追天然支持反射、折射、柔和阴影,因为它们都是光线与几何体物理交互的结果;颜色插值是光栅化中的常见技术。

Q9. DXR 管线中的关键组成部分包括?

多选题 · mid

  • A.加速结构← 正确答案
  • B.Shader Binding Table← 正确答案
  • C.TraceRay 指令← 正确答案
  • D.深度缓冲

答案:ABC。DXR 三部曲:加速结构组织几何、SBT 绑定材质、TraceRay 发射光线。深度缓冲属于光栅化阶段的产品。

Q10. 以下哪些是提升光线追踪性能的常用策略?

多选题 · extreme

  • A.时间复用上一帧光照结果← 正确答案
  • B.降低 BVH 树的最大深度
  • C.自适应采样← 正确答案
  • D.AI 降噪← 正确答案

答案:ACD。时间复用、自适应采样、AI 降噪都是主流优化手段。降低 BVH 深度反而会增加每个节点的求交开销,通常得不偿失。