想象你面前放着一块巨大的半透明果冻,浅蓝透亮,里面参差嵌着许多果肉碎。太阳从侧面照来,果冻内部泛着朦胧的光晕,深的地方颜色浓,浅的地方透亮。现在请你回答:这块果冻最终呈现什么颜色?你不可能只用一句“它是蓝的”来描述,因为每个角度、每个深度、每一处密度,颜色都在变化。 为了“画”出这块果冻,你需要一根吸管。把吸管从果冻表面插进去,每推进一小截,你就吮一口——这一口告诉你当前这一格果冻的“甜度”(密度)和“果味”(颜色)。把吸管从这头吸到那头,把所有采样得到的味道按顺序混合起来,你就得到了整条光路上的“味道剖面”,也就是这条视线最终应该呈现的颜色。体积渲染技术,本质就是重复做这件事:把云、雾、烟看成一块巨大的果冻,从相机发射无数根虚拟吸管(光线),每根吸管一点点插入“果冻”内部采样,最后把结果画到屏幕上。 本篇文章会带你认识存储果冻的格子(体素数据)、吸管采样的手法(光线步进)、光在果肉间乱撞的物理(散射计算)、以及光一路被“吃掉”的规律(吸收与透射),最后聊聊游戏里是如何一边品果冻一边保住帧率的(实时优化),电影里又是如何花几个月时间慢慢雕琢的(离线渲染),以及AI时代的新玩法(神经辐射场)。

岗位关联

体积渲染是游戏引擎与影视特效岗位的高频技能。理解它能让你在工作里真正造出“有体积感”的云海、晨雾与爆炸烟火,而不是用贴图硬撑。这也是通往高级渲染工程师与TA(技术美术)岗位的必修课。

目录
  1. 可视化演示
  2. 体素数据:把果冻切成储物格
  3. 光线步进:把吸管一寸寸插入果冻
  4. 散射计算:光在果肉间弹来弹去
  5. 吸收透射:光一路被果冻“吃掉”
  6. 实时优化:先粗尝一口再细品
  7. 离线应用:米其林大厨的慢工细活
  8. 前沿技术:神经网络当品鉴师
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示云雾烟怎么画的核心机制,通过架构/数据/状态/性能四种模式展示关键概念与交互流程。

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

云雾烟怎么画:体积渲染 体积渲染与参与介质概念 体素数据与体积纹理表示 核心机制可视化 云雾烟怎么画整体架构与关键组件

体素数据:把果冻切成储物格

还记得小时候超市里卖的果冻块吗?工厂先把一整桶果冻倒进方形的模具盘里,切成整齐划一的小方块再包装出售。我们要在计算机里“装下”一整块云雾,也需要这样的模具盘——只不过它有三个维度:长、宽、高。这个三位一体的“模具盘”叫做体素网格,其中每一个小方块就是一个体素,英文叫 Voxel,是“体积”和“像素”两个词拼出的孩子。

每个小格子里装什么?两样东西:这个位置的密度(这口果冻有多“实”)和颜色(是云的白、雾的灰,还是火焰的橙)。把密度填进每一个格子,就得到了密度场——它描述“云雾在空间里哪块厚、哪块薄”。想想看,真正的云朵边缘总是薄薄的、透光的,中心却是厚重的一团白,用密度场来描述再合适不过:边缘密度低,中心密度高。

在具体实现时,体素数据通常用三维数组或者 3D 纹理存储,数组下标 $(i, j, k)$ 对应世界坐标的一个小方块。为了模拟更自然的云,常用 Perlin 噪声等程序化方法给密度场叠加多层细节,让云拥有蓬松的絮状边缘,而不是一个规整的立方体。

回到果冻的比喻:体素数据就是把云雾这坨“果冻”事先切好并标注好每一格的甜度和口味。接下来的问题只有一个——我们该如何“品尝”它?

ρ(x)=V[i][j][k]\rho(\mathbf{x}) = V[i][j][k]
这个式子的意思是:空间里任意一点 $\mathbf{x}$ 的密度值,可以直接从三维数组 $V$ 的对应下标 $(i, j, k)$ 里取出来。密度越大,说明这一小格果冻越“实”,光越不容易穿过。
  1. 位置映射先把空间坐标 $\mathbf{x}$ 换算成网格下标 $(i, j, k)$,类似查字典时先找到页码。
    x→(i,j,k)\mathbf{x} \to (i, j, k)
  2. 查表取值从三维数组 $V$ 中取出下标处的存储值,这个值就是该点的密度。
    ρ=V[i][j][k]\rho = V[i][j][k]
  3. 空间插值若采样点落在格子之间,用三线性插值得到平滑密度,避免“一格一色”的方块感。
    ρ=lerp(ρi,j,k,ρi+1,j,k,t)\rho = \text{lerp}(\rho_{i,j,k}, \rho_{i+1,j,k}, t)

光线步进:把吸管一寸寸插入果冻

你拿到一块标注完的果冻,要怎么知道从某个角度看它是什么颜色?答案是:从眼睛出发,向果冻射出一根“视线吸管”,然后一段一段地插进去。每插入一小段 $\Delta t$,就停下来吸一口——读取当前格子的密度和颜色,估算这一小段对最终颜色有多少贡献,然后继续前进。这个“走一步、采样一次”的过程,就是光线步进。

光线步进在数学上其实就是把“光路上的积分”近似成“多段求和”。光在每一小段 $\Delta t$ 中,会先乘上当前点的透射率 $T_i$(说明光走到这里还剩多少能量),再乘上密度 $\sigma_i$ 和散射光颜色 $c_i$,最后累加到结果里。步长 $\Delta t$ 可以固定,也可以自适应——密度大的区域步长缩小,密度稀薄的区域大步快走,就像吃果冻时遇到大块果肉要小口抿,遇到清汤可以一口吞。

吸管一寸寸插入果冻,每一口都让你对整块果冻的了解加深一分。当吸管从另一头穿出时,你已经完整掌握了这条视线上的全部“味道信息”——这就是一个像素的最终颜色。对屏幕上每一个像素都做一遍,你就“画”出了整朵云。

L≈∑i=0n−1Ti⋅σi⋅ci⋅ΔtiL \approx \sum_{i=0}^{n-1} T_i \cdot \sigma_i \cdot c_i \cdot \Delta t_i
这条公式是光线步进的核心:把一整条光路的颜色贡献拆成 $n$ 小段,逐段累加。$T_i$ 表示光到达第 $i$ 段时还剩多少能量,$\sigma_i$ 是这一段的密度,$c_i$ 是这一段的散射颜色,$\Delta t_i$ 是这一段走过的长度。
  1. 分段时间把光线从起点到终点均匀(或按需)切分成 $n$ 个长度为 $\Delta t_i$ 的小段。
    si=s0+i⋅Δtis_i = s_0 + i \cdot \Delta t_i
  2. 逐段取值在第 $i$ 段位置取出密度 $\sigma_i$ 与颜色 $c_i$,模拟吸管“吸”到的那口味道。
    (σi,ci)=sample(si)(\sigma_i, c_i) = \text{sample}(s_i)
  3. 能量衰减用前面累计的透射率 $T_i$ 为这段贡献加权,光走得越远能量越弱。
    Ti=exp⁡(−∑k<iσkΔtk)T_i = \exp\left(-\sum_{k<i} \sigma_k \Delta t_k\right)
  4. 累加合成把每段贡献加起来,得到最终像素颜色 $L$。
    L=∑iTi σi ci ΔtiL = \sum_{i} T_i\, \sigma_i\, c_i\, \Delta t_i

散射计算:光在果肉间弹来弹去

为什么云是白的,天是蓝的,晚霞是红的?这就要说到光粒子和云雾粒子之间的“碰撞游戏”了。阳光射入云雾后,并不会乖乖直行,而是会被无数小水滴或尘埃粒子撞得偏离方向——这个现象叫散射。散射的方向分布不是均匀的,它由粒子的尺寸和光的波长共同决定。

天空呈现蔚蓝色,要归功于瑞利散射:空气分子这种“极小的果粒”对短波长的蓝光格外“敏感”,把蓝光弹向四面八方,于是整个天穹看起来都是蓝的。相比起来,米氏散射发生在粒子尺寸接近光波波长时(比如大水滴、尘埃),它倾向于让光继续朝前方传播,所以云层内部的散射光大多保持着向前的方向,让云看起来是明亮的白色。用什么数学工具来描述“光朝不同方向散射的概率”呢?答案是相位函数,它像一个打分器,给定入射方向和出射方向的夹角 $\theta$,返回一个权重值。

回到果冻的比喻——果肉颗粒就是云雾里的水滴。囗感细腻的小果粒(蓝光)更容易被弹向侧面,于是天空蔚蓝;粗大的果粒(云滴)则推着光继续向前,于是云朵白亮。理解散射,你就能解释并控制体积渲染中最重要的色彩来源。

p(θ)=316π(1+cos⁡2θ)p(\theta) = \frac{3}{16\pi}(1 + \cos^2\theta)
这是瑞利散射的简化相位函数。$\theta$ 是光原来的方向与散射后方向的夹角,函数值越大说明光越有可能往这个角度偏转。这个式子的形状像一个“哑铃”:前后方向的散射概率最高,侧面最低。
  1. 角度定义设入射方向 $\mathbf{L}$ 与观察方向 $\mathbf{V}$ 的夹角为 $\theta$,用点积计算余弦。
    cos⁡θ=L⋅V\cos\theta = \mathbf{L} \cdot \mathbf{V}
  2. 概率映射代入相位函数,得到偏转到该角度的相对概率 $p(\theta)$。
    p(θ)=316π(1+cos⁡2θ)p(\theta) = \frac{3}{16\pi}(1 + \cos^2\theta)
  3. 归一化系数 $\frac{3}{16\pi}$ 保证在所有方向上积分结果为 1,使它成为合法概率分布。
    ∫4πp(θ) dω=1\int_{4\pi} p(\theta)\, d\omega = 1

吸收透射:光一路被果冻“吃掉”

当光线穿过果冻时,除了被弹偏方向,还有一部分能量会被果冻“扣留”——被吸收转化成热量,或者散射到别的方向、再也到不了观察者眼里。这种“吃掉光”的能力,用吸收系数和散射系数来描述,二者之和叫做消光系数 $\sigma_t$。消光系数越大,光衰减得越快。

描述光强随距离指数衰减的规律,叫比尔-朗伯定律。它告诉我们:光每前进一小段距离,剩余能量就乘上一个小于 1 的因子;走得越远,衰减越快,是指数级别的崩塌。在体积渲染中,从起点 $a$ 到终点 $b$ 的透射率 $T(a,b)$ 就是把这段路程中每一小步的衰减乘起来。透射率等于 1 时说明光完全没被挡住;趋近 0 时说明光已经全被“吃光”了——这就是阴影最暗的地方。

就像吸管吸果冻时,吸得越长、果肉越稠密,你能吸上来的“光”越少——吸收与透射决定了体积渲染的明暗骨架。理解这个指数衰减,你就能解释为什么浓雾里车灯只能照亮几米远,而稀薄的晨雾却能透出长长的光柱。

T(a,b)=exp⁡(−∫abσt(s) ds)T(a,b) = \exp\left(-\int_{a}^{b} \sigma_t(s)\, ds\right)
这个公式说:光从 $a$ 走到 $b$ 后还剩下的比例,等于消光系数沿路径积分的负指数。积分值越大(介质越厚、越浓),透射率越小,光越暗。
  1. 消光系数采样沿路径每个位置 $s$ 获取消光系数 $\sigma_t(s)$,它由吸收和散射两部分相加而来。
    σt(s)=σa(s)+σs(s)\sigma_t(s) = \sigma_a(s) + \sigma_s(s)
  2. 光学厚度积分把路径上所有消光系数累加起来,得到光学厚度 $\tau$,它衡量整段路径的“遮挡总量”。
    τ=∫abσt(s) ds\tau = \int_a^b \sigma_t(s)\, ds
  3. 指数映射透射率等于 $e^{-\tau}$,光学厚度越大,透射率越小。
    T(a,b)=e−τT(a,b) = e^{-\tau}

实时优化:先粗尝一口再细品

游戏里的云海每一帧都要渲染,可光线步进是一个极其“贪吃”的过程——每个像素都要沿光线走几十步,每步都要做一次三维纹理采样。想想看:一根吸管尝完整块果冻已经够费劲,如果整个屏幕几百万个像素都要各插一根吸管,算力瞬间就会爆炸。实时渲染的第一条救生索是降采样:先在四分之一甚至八分之一分辨率下“粗尝”一遍体积效果,得到低分辨率的颜色缓冲,再在最终输出时做上采样和边缘修复。肉眼对云雾这类低频细节的分辨并不敏感,这一招能省下超过一半的开销。

第二条救生索是时间重投影:既然云在相邻两帧之间变化不大,我们就不必每帧都从头步进,而是把上一帧已经算好的结果“搬”到这一帧来。做法是把上一帧的像素按云的运动方向和相机运动做一次坐标重投影,如果新旧位置的重叠度高,就直接复用历史结果;同时配合少量抖动采样(Temporal AA 的思路),让误差在时间上均匀散布而不是集中在某处,从而显著降低闪烁感。第三招是级联分辨率:离相机近的云雾用高分辨率体素精细渲染,远处的用粗糙的大网格粗略带过,就像果冻近处用小口细品、远处大口吞掉——人眼对远处的细节本来就不敏感。

实时渲染高手,是一位懂得“偷懒”的品鉴师:先用粗吸管快速吸一遍判断整体风味,再借助上一口的余味弥补细节,最后只对嘴边的那一小块果冻细细品味。正是这些聪明的偷懒,让你的显卡能在 16 毫秒内画完整片云海。

Cost∝Nsamples×W×H\text{Cost} \propto N_{\text{samples}} \times W \times H
这一式揭示了实时体积渲染的性能本质:总开销正比于“每个像素的采样次数”乘以“屏幕像素总数”。所有优化策略,本质都是在想方设法减少 $N_{\text{samples}}$ 或 $W, H$。
  1. 分辨率项屏幕宽高 $W, H$ 相乘是像素总数,降采样直接把这一项砍到原来的 $1/4$。
    Wlow×Hlow=W×H16W_{\text{low}} \times H_{\text{low}} = \frac{W \times H}{16}
  2. 采样项$N_{\text{samples}}$ 是每条光线的步进采样数,通过自适应步长减小它。
    Nsamples=PathLengthΔtN_{\text{samples}} = \frac{\text{PathLength}}{\Delta t}
  3. 复用项时间重投影让一部分像素无需重新计算,相当于把有效 $N_{\text{samples}}$ 进一步打折。
    Costeffective=Cost×(1−reuse_ratio)\text{Cost}_{\text{effective}} = \text{Cost} \times (1 - \text{reuse\_ratio})

离线应用:米其林大厨的慢工细活

如果说实时渲染是快餐店里狼吞虎咽,那离线渲染就是米其林三星后厨的耐心摆盘。影视特效中的爆炸火焰、翻涌浓烟、仙气缭绕的山谷,每一帧都允许渲染几分钟甚至几十分钟,因此可以不计成本地增加采样数、使用更高精度的体素网格,甚至模拟多重散射——光线在云层内部被弹了又弹,最后才抵达摄像机。多重散射是让云层拥有“自发光蓬松感”的关键,电影里那些云内部透出的柔和光晕,就来源于此。

离线渲染所用的体积光传输方程把吸收、散射、自发光统一在一起:观测到的出射光 $L_o$ 等于背景光乘透射率,加上路径上每一处散射进视线方向的增量。这个方程比我们前面列出的累加公式多了“多重散射”的积分项,通常需要用蒙特卡洛方法迭代求解,计算量比实时那套高几个数量级,但换来的是照片级的真实感。

电影工作者就像一位耐心的米其林大厨,愿意花几小时只为让一块果冻在灯光下呈现出最完美的通透和光泽。也正因如此,虚拟制片现场需要实时预览云海背景,而最终成片却要交给离线渲染慢慢炖——两套流程相辅相成。

Lo=Lin⋅T+∫sT(s)⋅σs(s)⋅∫4πp(θ) Li(s,ω) dω dsL_o = L_{\text{in}} \cdot T + \int_{s} T(s) \cdot \sigma_s(s) \cdot \int_{4\pi} p(\theta)\, L_i(s, \omega)\, d\omega \, ds
这个式子的含义是:最终看到的颜色 = 背景光衰减后剩余的部分 + 路径上每一处粒子把四周光照散射到视线方向的总和。内层积分表示“从四面八方射来的光”,外层积分表示“沿着视线累积”。
  1. 背景透射背景光 $L_{\text{in}}$ 经过整段路径的衰减后,剩下 $L_{\text{in}} \cdot T$。
    Lbackground=Lin⋅T(a,b)L_{\text{background}} = L_{\text{in}} \cdot T(a,b)
  2. 单点散射在路径上某一点 $s$,粒子把来自各个方向 $\omega$ 的光按相位函数 $p(\theta)$ 散射到视线方向。
    Lscatter(s)=σs(s)∫4πp(θ) Li(s,ω) dωL_{\text{scatter}}(s) = \sigma_s(s) \int_{4\pi} p(\theta)\, L_i(s, \omega)\, d\omega
  3. 沿路径积分把每个点的散射贡献乘上到达观察者的透射率再累加,就是最终的体积光。
    Lo=LinT+∫sT(s) Lscatter(s) dsL_o = L_{\text{in}} T + \int_s T(s)\, L_{\text{scatter}}(s)\, ds

前沿技术:神经网络当品鉴师

如果有一种方法,不需要把果冻切成一格一格的储物格,而是直接“尝一口就知道整块果冻任意位置的味道”,那该有多好?神经辐射场(NeRF) 正是这样的尝试:它用一个神经网络 $F_\theta$ 直接编码三维空间中任意一点的密度和颜色,而不是存储在离散的体素网格里。渲染时,光线步进的采样点不再从数组里查表,而是喂给神经网络,让它“即兴回答”这一点的密度和颜色。由于神经网络的表达能力极强,NeRF 能够用连续函数表示出无比细腻的云雾细节,这也是近年 AI 三维重建火爆的核心引擎。

另一个前沿方向是流体仿真联动:用物理仿真(如欧拉网格或 SPH)实时计算烟雾的速度场和密度场,再把仿真结果直接作为体素数据送入渲染管线。这样一来,烟火不仅有好看的“皮”,还有符合流体力学规律的“骨”。再加上硬件光线追踪逐步普及,实时全局体积光正在成为游戏引擎的新标配——每一束光穿过雾气留下的光柱,都可以被逐像素实时追踪。

如果说传统体积渲染是“把果冻切好再逐格品尝”,那神经渲染就是“请一位品鉴大师,你问他任何位置的味道,他都能立即告诉你”。从切格子的模具盘,到吸管采样的手法,再到神经网络这位AI品鉴师——体积渲染的旅程,正从“画得更像”走向“懂什么是像”。

(σ,c)=Fθ(x,d)(\sigma, \mathbf{c}) = F_\theta(\mathbf{x}, \mathbf{d})
这个式子表示:神经网络 $F_\theta$ 接受空间位置 $\mathbf{x}$ 和观察方向 $\mathbf{d}$ 作为输入,输出该点的密度 $\sigma$ 与颜色 $\mathbf{c}$。它用一组可学习的参数 $\theta$ 替代了体素数组,实现了“无限分辨率”的体积表示。
  1. 输入坐标把采样点的空间坐标 $\mathbf{x}$ 和视线方向 $\mathbf{d}$ 作为网络输入。
    input=(x,d)\text{input} = (\mathbf{x}, \mathbf{d})
  2. 网络推理用多层感知机 $F_\theta$ 前向传播,得到密度和颜色两个输出。
    (σ,c)=Fθ(x,d)(\sigma, \mathbf{c}) = F_\theta(\mathbf{x}, \mathbf{d})
  3. 体渲染合成沿着光线对网络输出的密度颜色做我们熟悉的积分采样,得到最终像素。
    L=∫T(s) σ(s) c(s) dsL = \int T(s)\, \sigma(s)\, \mathbf{c}(s)\, ds

AI 应用场景

游戏引擎中的实时体积云

阶段:实时渲染优化

解决的问题:开放世界游戏需要大面积云海,但逐像素光线步进开销极高,传统做法只能用平面贴图冒充云层,视角一偏就穿帮。AI 与算法结合后的体积云方案,用降采样 + 时间重投影把成本压到可实时,同时保持云层在不同高度、不同光照下的真实体积感。

具体案例:在虚幻引擎 5 中,开发者通过自定义体积云材质,将 Perlin 噪声密度场输入光线步进着色器,配合级联分辨率让近处云层细节丰富、远处云海模糊过渡。玩家飞行穿越云层时,云会真实地包裹摄像机,并投下体积阴影。

影视特效的烟火与爆炸

阶段:离线渲染与合成

解决的问题:电影中爆炸烟尘、魔法迷雾需要高度物理真实感,传统的粒子精灵(Billboard)在慢镜头下会明显露出“纸片感”。离线体积渲染通过多重散射模拟光线在浓烟内部的反复弹射,让烟尘拥有厚重的立体光感。

具体案例:在一部科幻电影的特效镜头中,特效师使用 Houdini 进行流体仿真,生成高精度密度场,再导入离线渲染器(如 Arnold)执行数万根光线的蒙特卡洛体积散射。每一帧渲染耗时约 20 分钟,最终得到连烟尘内部暗部层次都清晰可辨的爆炸镜头。

自动驾驶仿真中的雾天传感

阶段:仿真数据生成

解决的问题:自动驾驶算法需要海量雾天、烟尘场景的传感器数据,但实车采集成本高且场景不可控。体积渲染让仿真环境能够物理正确地模拟激光雷达和摄像头在雾中的信号衰减,从而生成高保真训练数据。

具体案例:在自动驾驶仿真平台中,工程师用光线步进渲染浓雾体积,并据此计算激光雷达光线在雾中的后向散射回波强度,生成带有真实噪点的点云。算法团队用这批合成数据训练模型后,在真实雾天的目标检测率提升了约 18%。

Python 演示

python47 行
import math GRID_W, GRID_H = 72, 24  def density_at(x, y):    """返回 (x, y) 位置的密度值:两团高斯云叠加。"""    val = 0.0    dx = (x - 18.0) / 5.0    dy = (y - 8.0) / 3.5    val += 1.4 * math.exp(-(dx * dx + dy * dy))    dx = (x - 46.0) / 7.0    dy = (y - 16.0) / 4.5    val += 0.8 * math.exp(-(dx * dx + dy * dy))    return val  def raymarch_vertical(x, steps):    """从底部向顶部步进,累计光学厚度。"""    optical_depth = 0.0    for s in range(steps):        y = s + 0.5        optical_depth += density_at(x, y)    return optical_depth  CHARS = " .:-=+*#%@"  def render():    """逐像素渲染并返回字符串画面。"""    rows = []    for y in range(GRID_H):        row = ""        for x in range(GRID_W):            tau = raymarch_vertical(x, GRID_H)            brightness = math.exp(-tau * 0.45)  # 比尔-朗伯: 亮度随光学厚度指数衰减            idx = int(brightness * (len(CHARS) - 1))            if idx >= len(CHARS):                idx = len(CHARS) - 1            row += CHARS[idx]        rows.append(row)    return "\n".join(rows)  if __name__ == "__main__":    print(render())
目的:在终端里渲染出一幅由字符构成的两团“云”画面。控制台里越亮的字符对应光学厚度越小(光穿透多)的位置,越暗的字符对应云层越厚的位置。
  1. density_at(x, y)用两个高斯函数叠加模拟两团云,返回任意坐标的密度值,相当于“果冻储物格”里查询甜度。
  2. raymarch_vertical(x, steps)对每一列像素,从底部到顶部逐格采样并累加光学厚度,这就是最简版“吸管步进”。
  3. math.exp(-tau * 0.45)直接用比尔-朗伯定律计算剩余光强,$e^{-\tau}$ 是透射率的核心。
  4. idx = int(brightness * (len(CHARS) - 1))把 0~1 的亮度映射到字符索引,暗处用 @、亮处用空格表示。
  5. if __name__ == "__main__":保证脚本被直接运行时才输出画面,被 import 时不会意外打印。

常见误区

误区“做云的时候,直接拿一张 Perlin 噪声纹理的 alpha 通道当透明度用不就行了?我看很多教程就是这么连节点的。”

正解噪声纹理提供的是“密度”而非“透明度”。密度要经过比尔-朗伯定律的指数变换,再沿光线逐个采样累加,才能得到正确的透射率。如果把密度直接当透明度,云会显得单薄、没有体积感,因为光线穿透不同厚度的积累效果完全没被模拟。你可以把密度理解为果冻的“甜度”,而透明度是吸管吸完整条路径后尝到的“总甜度”——两者差了整整一次积分。

为何常见:许多可视化教程为了简化,把灰度噪声直接映射成透明通道展示,让人误以为这就是体积渲染的全部。

误区“光线步进不是采样越密越精确吗?那把步长设成 0.001,云肯定又细腻又好看。”

正解步长过小会带来两个问题:性能成倍恶化,且由于体素纹理本身有分辨率上限,过密的采样只是在重复读取同一格插值结果,并不会增加有效细节。工程实践通常用自适应步长:密度高处加密、低处放宽,并且配合抖动(jitter)打破步进对齐产生的条带瑕疵。就像一个品酒师不会把一口酒含在嘴里一小时——恰到好处的品鉴次数才能高效又准确地判断风味。

为何常见:直觉上认为“更多采样 = 更高质量”,忽略了体素数据本身的分辨率瓶颈和渲染预算约束。

误区“体积渲染是 3A 大作和好莱坞电影才用得起的黑科技,普通设备根本跑不动。”

正解体积渲染是一套算法框架,画质与性能是连续谱系。移动端游戏早已用低分辨率 + 时间重投影实现体积雾,网页端甚至有实时体积云 Demo。关键在于合理选择采样数、分辨率、级联策略,甚至可以用 2.5D 的“伪体积”方案(多层切片 + 视差偏移)在低端设备上获得体积感。这不是“有没有资格吃果冻”的问题,而是“用粗吸管还是细吸管、品几口”的取舍问题。

为何常见:大众接触到的体积渲染案例往往是最顶级的影视和主机游戏效果,容易把“高质量效果”和“技术本身”画上等号。

误区“云用体积云着色器,雾用指数高度雾,烟用粒子系统——这三种东西的渲染原理应该完全不一样吧?”

正解它们本质是同一套体积渲染框架的三种参数配置。云是高空大尺度密度场(由噪声驱动),雾是覆盖大范围的低密度均匀介质,烟是局部高密度且随时间演化的密度场(由流体仿真驱动)。渲染时的光线步进、散射、吸收逻辑完全一致。就像果冻、布丁和奶冻——配方比例不同,但都是“凝胶 + 调味”的产物。

为何常见:游戏引擎中云、雾、烟通常以不同组件形式出现(如 Unity 的 Volumetric Clouds 和 Fog 是不同模块),让人误以为是两套独立技术。

面试题

Q1. 体积渲染中的“体素”指的是什么?

单选题 · easy

  • A.一种 GPU 缓存块
  • B.三维网格单元← 正确答案
  • C.云的物理粒子
  • D.渲染管线阶段

答案:B。体素是“体积像素”,即三维网格中的一个小立方体单元,用来存储密度和颜色等信息。GPU 缓存块、物理粒子、渲染阶段都不是体素的正确定义。可以把体素理解为“切好的果冻小方块”。

Q2. 光线步进过程中,每一步主要执行什么操作?

单选题 · easy

  • A.转换所有光线到统一坐标
  • B.对当前点采样并累加颜色贡献← 正确答案
  • C.将场景几何体切分成网格
  • D.计算相机传感器的曝光时间

答案:B。光线步进的核心是“逐步推进、逐点采样”:沿光线每前进一段,就读取当前点的密度和颜色,乘以透射率和步长后累加。坐标转换、几何细分、曝光时间都不是步进步骤本身。

Q3. 比尔-朗伯定律描述了什么规律?

单选题 · easy

  • A.光在介质中随距离指数衰减← 正确答案
  • B.光颜色随温度线性变化
  • C.反射强度与法线方向无关
  • D.散射强度与波长平方成反比

答案:A。比尔-朗伯定律指出光强 $I = I_0 e^{-\sigma t}$,即光穿过介质时按指数规律衰减,衰减速度由消光系数决定。B、C 与定律无关,D 描述的是瑞利散射的波长依赖,不是比尔定律。

Q4. 下列哪些因素会显著增加实时体积渲染的计算开销?

多选题 · mid

  • A.增加光线步进的采样步数← 正确答案
  • B.提高体积渲染的计算分辨率← 正确答案
  • C.减少动态光源的数量
  • D.扩大体素网格的覆盖范围← 正确答案

答案:ABD。开销近似正比于“采样数 × 分辨率”,因此加大采样步数和分辨率都会显著增负。扩大体素网格会让每条光线穿过更多格子,同样增加采样量。减少光源数量反而是降低开销的措施。

Q5. 晴朗天空呈现蓝色,主要归因于哪种散射?

单选题 · mid

  • A.米氏散射
  • B.瑞利散射← 正确答案
  • C.镜面反射
  • D.漫反射

答案:B。瑞利散射对短波长(蓝光)的散射效率远高于长波长,因此蓝天是瑞利散射的杰作。米氏散射主要发生在粒子尺寸接近波长时,表现为前向散射,是白云偏白的原因。反射类说法与天空颜色无关。

Q6. 实时渲染中采用降采样技术的主要目的是什么?

单选题 · mid

  • A.减少采样点总数以控制开销← 正确答案
  • B.降低显示器刷新率来省电
  • C.提升颜色精度和动态范围
  • D.扩大体积网格的分辨率

答案:A。开销公式中屏幕像素数是乘数因子,把渲染分辨率降到 1/4 就能让开销大致降到 1/4,之后再通过上采样恢复显示分辨率。降低刷新率、提升颜色精度、扩大网格都不是降采样的目的。

Q7. 时间重投影技术在体积云渲染中的好处有哪些?

多选题 · hard

  • A.复用上一帧的有效渲染结果← 正确答案
  • B.减少云雾画面的闪烁与噪点← 正确答案
  • C.完全消除对历史数据的依赖
  • D.允许以较低分辨率计算当前帧← 正确答案

答案:ABD。时间重投影将上一帧结果按运动矢量搬到当前帧,从而减少重复计算,并配合抖动采样压低时域噪点;正因如此当前帧可以用较低分辨率计算。C 恰好说反,该技术强依赖历史帧数据。

Q8. 浓雾中某条光路的光学厚度很大,说明什么?

单选题 · mid

  • A.光线穿过时被大量吸收或散射掉← 正确答案
  • B.雾的粒子尺寸远小于光波长
  • C.光源位于场景正上方
  • D.该光路必须使用离线渲染

答案:A。光学厚度是消光系数沿路径的积分,数值大说明路径上介质又密又厚,透射率 $e^{-\tau}$ 接近 0,光几乎无法穿过。粒子尺寸决定散射类型而非光学厚度,光源位置和渲染方式与光学厚度无直接关系。

Q9. 下列哪些属于正确的体积渲染处理步骤?

多选题 · hard

  • A.沿光线逐步采样密度与颜色← 正确答案
  • B.累计透射率并加权散射贡献← 正确答案
  • C.把每个体素直接绘制为光源
  • D.与背景颜色按透射率合成← 正确答案

答案:ABD。体积渲染的标准流程是:光线步进采样→计算散射与吸收→累计透射率加权→与背景合成。C 错误,体素是参与介质本身,不是自发光源(除非特别指定自发光参数,那也需经过散射模型处理)。

Q10. 神经辐射场(NeRF)在体积渲染中的核心贡献是什么?

单选题 · extreme

  • A.用神经网络表示连续密度场与颜色场← 正确答案
  • B.用硬件光追替代全部体素采样
  • C.用视频压缩技术存储烟雾动画
  • D.用粒子系统模拟云朵运动

答案:A。NeRF 的核心是用多层感知机 $F_\theta(\mathbf{x}, \mathbf{d}) \to (\sigma, \mathbf{c})$ 隐式编码三维场景,渲染时采样点通过查询网络获得密度颜色,实现连续高分辨率表示。它并不替代硬件光追也不做视频压缩,更不是粒子系统。