清晨六点半,你走进一片松树林。雾气还没散,东边的太阳从树梢的缝隙间钻出来,投下一道道金白色的光柱,像一扇扇半透明的门斜插在空气中。你以为光本来就是这样一条一条的,直到你走到树下才发现:光其实是无形的,是空气中漂浮的水雾和尘埃,把笔直前进的阳光一点点“折射”到你眼睛里,才让你看见了它的路径。这就是著名的丁达尔效应,也是游戏世界里“体积光”(God Rays)这个名字的由来——游戏工程师把这道清晨的魔法搬进了显卡的渲染管线,让《荒野大镖客》的晨雾、《塞尔达》的古树缝隙、甚至你手机里那张逆光照片都拥有了电影级的立体感。 本篇文章要讲的,就是这道光柱从“看到”到“算出来”的全过程:空气为什么能现形、光线怎么一步一步走、树影怎么剪出光柱的形状、性能又如何在半张草稿纸上省出大半帧开销。

岗位关联

对 AI 视觉与实时渲染从业者而言,体积光是大场景氛围与画质上限的关键拼图,掌握它等于掌握了电影级光照的“最后一公里”,无论是做游戏 TA、渲染引擎开发还是虚拟制片,都是加分极强的硬技能。

目录
  1. 可视化演示
  2. 1. 光柱现象:丁达尔视野
  3. 2. 散射介质:让光现形的微粒
  4. 3. 射线步进:沿着视线“舀汤”
  5. 4. 遮挡贴图:光柱的剪影底稿
  6. 5. 径向模糊:把光束晕开的水彩
  7. 6. 性能优化:用小草稿画出大场面
  8. 7. 参数调校:光影的性格
  9. 8. 工程应用:把整条流水线装进引擎
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态演示体积光God Rays的核心机制,通过全局/局部/动态/静态四种模式展示关键概念与交互流程。

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

体积光God Rays:体积光物 体积光物理原理与大气散射模型 屏幕空间后处理God Rays 核心机制可视化 体积光God Rays整体架构与关键组件

1. 光柱现象:丁达尔视野

你清晨走进松树林,雾气把阳光切成一道道金白色的“门”。如果你把雾气抽走,光柱会瞬间消失——阳光还是那束阳光,只是没有了空气中的小水滴和尘埃做“显影剂”。摄影师把这种现象叫丁达尔效应:当一束光穿过含有微粒的胶体时,微粒会把光散射到四面八方,其中一部分恰好进入你的眼睛,于是原本看不见的光路被“画”了出来。

在游戏渲染里,我们不可能真的模拟上亿颗雾粒,但可以抓住这个现象的本质:光在空气里走,能量会随着走过的距离被介质逐渐消耗。这个消耗规律用比尔-朗伯定律描述:

有了这个“能量衰减”的直觉,你就能理解为什么远处的光柱总是比近处的淡——不是光源变暗了,而是光在赶路时被空气不断“打劫”。游戏画面里的体积光,本质上就是把这场“打劫”按像素一笔一笔算出来。

I(d)=I0⋅e−σd\displaystyle I(d) = I_0 \cdot e^{-\sigma d}
光穿过雾气的路程越长,剩下来的能量越少,而且是按指数减少,不是匀速减少。
  1. 初始光强 $I_0$阳光刚射入雾气时携带的能量,是光柱的“起跑线”,光线越强,后续光柱越亮。
    I0=光源辐射强度\displaystyle I_0 = \text{光源辐射强度}
  2. 介质吸收系数 $\sigma$空气里雾粒和尘埃的浓度,$\sigma$ 越大,光每走一小段就被“吃掉”越多。
    σ=单位距离内被散射或吸收的比例\displaystyle \sigma = \text{单位距离内被散射或吸收的比例}
  3. 穿透距离 $d$光在空气中走过的距离,$e^{-\sigma d}$ 就是光“劫后余生”剩下的比例,距离越远剩余越少。
    T=e−σd=透射率\displaystyle T = e^{-\sigma d} = \text{透射率}

2. 散射介质:让光现形的微粒

还是那片清晨的松林。你伸手挥一下,光柱里有细小的灰尘在跳舞。它们就是光的“舞台舞美”——没有它们,阳光再好,你也只能看见树梢上的亮斑,看不见空气本身。雾气、烟尘、雨丝、甚至是极细的沙尘,在渲染术语里统称为“参与介质”(participating media):它们参与了光的传播,把本来沿直线前进的光撬向四面八方。

但散射并不是均匀的:绝大多数雾粒会把光“顺手”扔回原方向附近。你站在太阳背后看雾,雾是亮的;站在太阳对面看雾,雾就显得没那么亮。这种“偏心”用 Henyey-Greenstein 相位函数描述:

记住这个“偏心散射”就好:你在看清晨光柱时,大部分光其实是朝太阳方向继续走,只是极少数恰好折进你眼睛里。渲染引擎模拟的,正是这“极少数”的旅程。

p(θ)=1−g24π(1+g2−2gcos⁡θ)3/2\displaystyle p(\theta) = \frac{1 - g^2}{4\pi\left(1 + g^2 - 2g\cos\theta\right)^{3/2}}
这个公式回答的问题是:打进雾气的光,最终有多大比例朝你所在的观察方向跑过来。
  1. 散射角 $\theta$入射光方向和你的视线方向之间的夹角,$\theta = 0$ 表示光笔直朝你飞来。
    θ=入射方向与观察方向夹角\displaystyle \theta = \text{入射方向与观察方向夹角}
  2. 前向因子 $g$$g$ 越接近 $1$,光越倾向于继续朝前方散射,雾气看起来“顺光发亮”。
    g=前向散射强度,常见雾天取 0.7∼0.9\displaystyle g = \text{前向散射强度,常见雾天取 } 0.7 \sim 0.9
  3. 归一化分母分母中的 $4\pi$ 保证把整个球面上所有方向的散射概率加起来正好是 $1$,不会凭空多出能量。
    ∫4πp(θ) dω=1\displaystyle \int_{4\pi} p(\theta)\, d\omega = 1

3. 射线步进:沿着视线“舀汤”

光柱怎么算?想象你端着一只勺子,从眼睛出发,沿着视线方向一步步走向远处的树。每走一小段,你就舀一勺“空气汤”尝一口:这一段里有多少雾?雾被阳光照亮了几分?这一段挡掉了多少后面来的光?把沿途所有“勺子”的结果叠在一起,就是你看那道光柱的颜色和亮度。

显卡里管这个叫射线步进(Ray Marching)。视线变成一条射线,从近到远切成一格一格的采样段,每段的长度是 $\Delta t$。每一段都有三笔账要算:这段空气自己的透光率、这段空气从光源接到的散射能量、以及之前所有段累积下来还剩下多少光。把它们合成起来:

步进步长 $\Delta t$ 越小,结果越精细,但计算次数也越多,本质上是用时间换质量。游戏里为了流畅,通常把步长调得稀疏一点,再靠后处理补妆。

Cray=∑i=1N(1−Ti) Ai∏j=1i−1Tj\displaystyle C_{\text{ray}} = \sum_{i=1}^{N} (1 - T_i)\, A_i \prod_{j=1}^{i-1} T_j
前面的空气挡掉的光越多,后面的空气即使再亮,你也看不真切——这就是为什么雾深处的光柱总是朦朦胧胧。
  1. 每段透射率 $T_i$第 $i$ 段空气的透光率,由比尔-朗伯定律给出 $T_i = e^{-\sigma_i \Delta t}$。
    Ti=e−σiΔt\displaystyle T_i = e^{-\sigma_i \Delta t}
  2. 散射能量 $A_i$这一段空气从光源那里接受到并散射进你眼睛的那份光,受相位函数和光源强度共同影响。
    Ai=Lin⋅p(θi)\displaystyle A_i = L_{\text{in}} \cdot p(\theta_i)
  3. 前缀累积 $\prod_{j=1}^{i-1} T_j$从近处一路乘到第 $i$ 段之前,表示前面每一段透射率的乘积,越深的光柱越容易被近处雾气“罩住”。
    前方累计透射=T1⋅T2⋯Ti−1\displaystyle \text{前方累计透射} = T_1 \cdot T_2 \cdots T_{i-1}

4. 遮挡贴图:光柱的剪影底稿

清晨的树影为什么是光柱的“形状”?因为树枝把阳光挡住了,没挡住的地方光才能一路射下来。换成渲染的语言:我们需要知道“从太阳的角度看,这个像素到底有没有被树挡住”。这个问题跟阴影贴图(Shadow Map)几乎一模一样。

做法是把相机搬到太阳的位置,朝地面方向渲染一张“深度图”,记下太阳视角下每个点离太阳有多远,这张图就是遮挡贴图。之后在正常渲染时,把每个像素重新投影到太阳视角下,比较它的距离与遮挡贴图里记录的深度,就能判定它露没露头:

这张只含 $0$ 和 $1$ 的“剪影底稿”送到下一步,就变成了光柱的骨架。它不需要很高分辨率,因为后处理模糊会帮它磨皮。

V(p)={1,dsm(p)≤dscene(p)0,otherwise\displaystyle V(p) = \begin{cases} 1, & d_{\text{sm}}(p) \le d_{\text{scene}}(p) \\[4pt] 0, & \text{otherwise} \end{cases}
判定规则说白了就是一句话:太阳“看得见”我,我就有资格发亮;太阳被树挡住了,我这条光柱就该乖乖闭嘴。
  1. 光源视角深度 $d_{\text{sm}}(p)$把像素 $p$ 投影到太阳视角的深度图上,读出的那个深度值,代表太阳眼中 $p$ 的距离。
    dsm(p)=shadow map 采样深度\displaystyle d_{\text{sm}}(p) = \text{shadow map 采样深度}
  2. 场景实际深度 $d_{\text{scene}}(p)$从主相机视角看过去,$p$ 离相机的实际距离。
    dscene(p)=相机深度缓冲值\displaystyle d_{\text{scene}}(p) = \text{相机深度缓冲值}
  3. 深度比较若 $d_{\text{sm}} \le d_{\text{scene}}$,说明从太阳看,$p$ 前面没有更近的遮挡物,可见性为 $1$;否则被挡住,可见性为 $0$。
    V(p)∈{0,1}\displaystyle V(p) \in \{0, 1\}

5. 径向模糊:把光束晕开的水彩

有了剪影底稿还不够:真实的光柱边缘是柔的,不是一把硬邦邦的刀切出来的。就像水彩笔在纸上点了一个点,你要用干净的水笔沿着某个方向轻轻扫开,颜色才会顺着水流洇出去,形成一头浓一头淡的笔触。

God Rays 后处理里这个“扫水”的动作叫径向模糊:以画面中太阳所在的像素为“圆心”,对每个像素沿“像素到太阳”的连线方向,前后各取若干采样点,按权重相加。离圆心越近权重越高,越远越淡:

采样半径 $R$ 越大,光柱拖得越长,也越柔和。这也是“光柱为什么从太阳向四周放射”这一观感的数学来源——不是光真的在画面里跑,而是径向模糊把剪影底稿里的能量沿着太阳方向“抹”了出去。

Iout(x)=1K∑i=−RRwi Iin ⁣(x+i d⃗ ),d⃗=x−xsun∥x−xsun∥\displaystyle I_{\text{out}}(x) = \frac{1}{K}\sum_{i=-R}^{R} w_i\, I_{\text{in}}\!\left(x + i\,\vec{d}\,\right), \qquad \vec{d} = \frac{x - x_{\text{sun}}}{\left\lVert x - x_{\text{sun}} \right\rVert}
每个像素都朝太阳方向“拉”出一条丝,无数根丝叠在一起,就是水彩洇开般的光柱。
  1. 径向方向 $\vec d$从像素 $x$ 指向太阳像素 $x_{\text{sun}}$ 的单位向量,它决定模糊沿着哪个方向扫。
    d⃗=x−xsun∥x−xsun∥\displaystyle \vec d = \frac{x - x_{\text{sun}}}{\lVert x - x_{\text{sun}} \rVert}
  2. 偏移量 $i\,\vec d$沿着径向方向前后移动 $i$ 步,采样点就像水彩笔刷的毛,从中心向两端散开。
    xi=x+i d⃗,i∈[−R,R]\displaystyle x_i = x + i\,\vec d,\quad i \in [-R, R]
  3. 归一化权重 $w_i / K$中间权重高、边缘权重低,$K = \sum w_i$ 保证整体亮度不因模糊而膨胀或缩水。
    K=∑i=−RRwi\displaystyle K = \sum_{i=-R}^{R} w_i

6. 性能优化:用小草稿画出大场面

画水彩的时候,没人会在四平方米的墙面上练笔——都是先在一张手掌大的草稿纸上试颜色、试浓度,满意了再放大。God Rays 后处理也一样:与其在全分辨率下做几十次径向采样,不如先把画面缩到一半甚至四分之一分辨率,算完光柱再放大贴回去,肉眼几乎看不出区别,速度却快出好几倍。

为什么半分辨率这么赚?因为计算量跟像素数成正比,而像素数跟分辨率的平方成正比。把宽高各缩一半,总像素直接变成四分之一:

除了降分辨率,常见的还有两招:减少采样次数,靠抖动和时序累积(temporal accumulation)复用上一帧的采样结果来补噪声;以及提前用较便宜的深度测试剔除大量无关像素。三者组合,God Rays 从“奢侈品”变成“日用品”。

Ccost≈W⋅Hs2⋅N⋅S\displaystyle C_{\text{cost}} \approx \frac{W \cdot H}{s^2} \cdot N \cdot S
降采样系数 $s$ 是加速器的大功臣:$s=2$ 时计算量直接砍到四分之一,$s=4$ 时只剩十六分之一。
  1. 全屏像素 $W \cdot H$宽乘高就是这一帧画面里要处理的像素总数,是成本的地基。
    像素数=W×H\displaystyle \text{像素数} = W \times H
  2. 降采样系数 $s^2$宽高都除以 $s$ 后,像素数除以 $s^2$,这是性能优化的最大杠杆。
    s=分辨率缩放因子,常用 2 或 4\displaystyle s = \text{分辨率缩放因子,常用 } 2 \text{ 或 } 4
  3. 步数与着色开销 $N \cdot S$$N$ 是径向模糊或射线步进的采样次数,$S$ 是单次采样的着色开销,两者相乘后同样乘在前面。
    N=采样次数,S=单次采样开销\displaystyle N = \text{采样次数},\quad S = \text{单次采样开销}

7. 参数调校:光影的性格

同样的阳光,在纱雾里是温柔的长纱,在浓雾里是浑厚的云柱,在暴雨后的林间则是又短又利的金箭头。这些“性格”全由灯光师手上的几个旋钮决定。调参不是玄学,每个旋钮都对应一个明确的光学量。

最核心的四个旋钮是:强度 $A$、距离衰减 $D$、径向半径 $r(x)$ 和染色 $\text{Tint}$。强度负责整体音量,衰减负责光柱能拖多长,染色则给整根光柱镀上一层暖黄或冷白。把它们组合起来,一根光柱的最终颜色可以写成:

实际项目里,这组参数通常暴露在引擎的 PostProcess Volume 里,美术同学在场景里实时拨动旋钮,一边看画面一边找“那一口气”。

G(x)=Iray(x)⋅A⋅e−D r(x)⋅Tint\displaystyle G(x) = I_{\text{ray}}(x) \cdot A \cdot e^{-D\, r(x)} \cdot \text{Tint}
这几个参数就像音响上的旋钮:A 是总音量,D 是混响时长,Tint 是 EQ 音色,每个都各管一摊,互不抢戏。
  1. 整体强度 $A$一个乘在所有光柱像素上的标量,调大它,光柱整体变亮或变暗,相当于总音量旋钮。
    A=光柱强度系数\displaystyle A = \text{光柱强度系数}
  2. 距离衰减指数 $e^{-D r(x)}$$r(x)$ 是像素到光源的距离,$D$ 是衰减快慢,$D$ 越大,光柱在远处迅速断掉,显得短促有力。
    r(x)=∥x−xsun∥\displaystyle r(x) = \lVert x - x_{\text{sun}} \rVert
  3. 染色 $\text{Tint}$一个 RGB 颜色向量,乘上去之后给光柱的亮部染上倾向色,晨光偏金黄、月光偏冷蓝。
    Tint=(rt,gt,bt)\displaystyle \text{Tint} = (r_t, g_t, b_t)

8. 工程应用:把整条流水线装进引擎

最后一步,把前面所有工序组装成一条真正的游戏渲染流水线。想象清晨松林被装进一台摄像机:先用普通光照管线画出没有光柱的场景底图,再从太阳视角出一张低分辨率遮挡图,然后对遮挡图做几步射线估算和径向模糊,得到一张只含光柱的“特效图层”,最后把这张图层叠加回底图上。

叠加很讲究:体积光本质是空气中的散射光,它只会往画面里加能量,不会把原有的颜色盖掉,所以用加法混合而不是普通覆盖。最终公式可以写成:

在 Unity 的 Sun Shafts、虚幻引擎的体积雾、以及主流引擎的 Light Shaft 组件里,这套流程已经被封装成一站式节点:你拖一个“上帝光”组件进去,指定太阳方向、树影贴图源和几个旋钮,剩下的交给底层 Shader。知道原理的你,看到这些节点时,就能一眼认出它们其实是“遮挡 → 步进 → 径向模糊 → 加色混合”的老朋友。

Ffinal=Cscene+Ggodray⊙Ceye\displaystyle F_{\text{final}} = C_{\text{scene}} + G_{\text{godray}} \odot C_{\text{eye}}
主场景负责“写实”,光柱图层负责“加滤镜”,两者一拍即合,画面立刻有了体积感。
  1. 场景底图 $C_{\text{scene}}$还没加体积光的原始帧,是光柱的“画布”。
    Cscene=原渲染输出\displaystyle C_{\text{scene}} = \text{原渲染输出}
  2. 光柱图层 $G_{\text{godray}}$前面所有工序产出的、只含光束能量的特效图。
    Ggodray=体积光后处理结果\displaystyle G_{\text{godray}} = \text{体积光后处理结果}
  3. 逐元素乘并叠加$\odot$ 表示逐像素乘以一个常数光色向量 $C_{\text{eye}}$,然后与底图做加法混合,亮度只增不减。
    Ceye=观察者方向光色校准\displaystyle C_{\text{eye}} = \text{观察者方向光色校准}

AI 应用场景

游戏引擎实时体积光(技术美术调参)

阶段:后处理渲染管线集成(推理出图阶段)

解决的问题:过去模拟丁达尔光柱要手动摆放几十个大光晕贴图,视角一换就穿帮;用 God Rays 后处理 + 遮挡贴图后,光柱形状随相机视角实时变化,美术只需要拖动强度、衰减、染色三个参数就能预览。

具体案例:在 Unity 体积光插件中,技术美术把场景中的树冠层设为遮挡源,太阳方向对准晨雾,输出 1/2 分辨率的光柱图层并与主画面加法混合,角色走进光柱时皮肤和武器上的光斑随之流动,帧耗时仅增加 0.8ms。

AI辅助光照参数推荐

阶段:自动调参(生成式设计辅助)

解决的问题:God Rays 参数多且互相影响,新手美术容易把光柱调成“刺眼的白条”;通过采集大量场景 HDR 截图与美术标注,训练一个轻量回归模型,输入目标氛围(柔和、神圣、冷峻),自动推荐强度与染色参数。

具体案例:某 AIGC 场景生成工具中,用户选择“清晨森林”风格,模型先根据深度图自动放置太阳方向,再生成一组 God Rays 参数,配合湿度参数一起输出,画面直达电影级氛围。

虚拟制片逆光预演

阶段:虚拟预演可视化(推理渲染阶段)

解决的问题:影视虚拟制片拍摄时,导演需要实时看到“逆光 + 灰尘”的氛围,但现场不可能真的布一层雾;用体积光渲染在 LED 背景墙上模拟丁达尔效果,演员和摄影机运动时,光柱随视角实时变化,演员走位信心大增。

具体案例:某科幻短片拍摄中,灯光师在引擎里放置太阳光源和雾浓度体,God Rays 从废土建筑的窗户缝隙射入,特效团队现场调整衰减参数配合演员调度,省去了昂贵的光束物理装置。

Python 演示

python43 行
import numpy as np def build_scene(h, w):    """生成天空渐变、太阳和树冠遮挡的简化场景"""    yy, xx = np.mgrid[0:h, 0:w]    sky = 0.25 + 0.5 * (xx / max(w - 1, 1))       # 从暗到明的天空    sun_x, sun_y = int(w * 0.28), int(h * 0.22)    sun = np.sqrt((xx - sun_x) ** 2 + (yy - sun_y) ** 2) < w * 0.06    occluder = np.ones((h, w))                     # 1 = 无遮挡    for cx, cy, rx, ry in [(90, 120, 30, 45), (170, 90, 40, 55), (255, 135, 28, 42)]:        occluder[((xx - cx) / rx) ** 2 + ((yy - cy) / ry) ** 2 < 1] = 0    return xx, yy, sun, occluder def radial_blur(src, sun_x, sun_y, steps=12, spread=0.4):    """沿太阳方向做多次偏移采样的径向模糊"""    h, w = src.shape    out = np.zeros_like(src)    for i in range(steps):        k = (i - steps / 2) / steps        dx = int(round((w / 2 - sun_x) * k * spread))        dy = int(round((h / 2 - sun_y) * k * spread))        out += np.roll(np.roll(src, dy, axis=0), dx, axis=1)    return out / steps def main():    h, w = 80, 140    xx, yy, sun, occ = build_scene(h, w)    sun_x, sun_y = int(w * 0.28), int(h * 0.22)    dist = np.sqrt((xx - sun_x) ** 2 + (yy - sun_y) ** 2)    density = np.clip(1 - dist / (w * 0.85), 0, 1)   # 距离越远光越弱    rays = occ * density ** 2                          # 遮挡决定光柱形状    rays = radial_blur(rays, sun_x, sun_y, steps=14)    final = np.clip(occ * 0.3 + rays * 1.5 + sun, 0, 1)   # 加法合成    chars = " .:-=+*#%@"    for y in range(0, h, 2):        row = ""        for x in range(w):            v = final[y, x]            row += chars[min(int(v * len(chars)), len(chars) - 1)]        print(row) if __name__ == "__main__":    main()
目的:用 80×140 的低分辨率棋盘模拟太阳、树冠遮挡、距离衰减和径向模糊,打印出 God Rays 光柱的 ASCII 预览,验证“遮挡+模糊+加色”三步核心原理。
  1. occluder[((xx - cx) / rx) ** 2 + ...] = 0用椭圆方程标记树冠,把被遮挡像素置为 0,模拟遮挡贴图的“剪影”。
  2. density = np.clip(1 - dist / (w * 0.85), 0, 1)距离越远光照越弱,对应比尔-朗伯式的衰减直觉。
  3. rays = occ * density ** 2遮挡为 0 的地方光柱直接熄灭,形成光束与阴影交错。
  4. final = np.clip(occ * 0.3 + rays * 1.5 + sun, 0, 1)加法混合:底图叠光柱图层再点亮太阳,亮度只增不减。

常见误区

误区“体积光嘛,我直接在屏幕上画几道白色渐变光斑不就完了?为什么还要做遮挡图和模糊?”用过贴图冒充光柱的人,往往发现视角一换光柱就跟场景对不上,还特别假。

正解真正的体积光要同时依赖三样东西:遮挡贴图决定光柱的“形状骨架”、空气散射决定能量的空间分布、径向模糊决定边缘的柔腻质感。画白线只是贴一张静态海报,而 God Rays 是一套能响应相机与光源变化的实时计算流程。就像清晨森林里的光柱会随着你挪动脚步改变长短浓淡,静态贴图永远做不到这一点。

为何常见:体积光在成品画面上看起来就是“几道亮光”,直觉上容易认为可以用简单贴图近似;但没意识到它其实是连续的光学过程在二维投影上的综合结果。

误区“光柱不够细腻?把射线步进和径向模糊的采样次数都拉到 64,画面一定更美。”结果帧率从 60 掉到 20,老板的脸色比屏幕还黑。

正解采样次数的收益递减很明显:从 8 次加到 16 次画质提升明显,从 32 次加到 64 次肉眼几乎看不出区别,代价却是线性翻倍的计算量。更聪明的做法是保持低采样数,用时间抖动 + 时序累积把噪声在几帧内消化掉,或者用半分辨率隐藏锯齿。

为何常见:初学者容易把画质和计算量当成线性关系,以为“越多越好”,忽略了人眼对高频噪声的容忍度和时序滤波这个免费的外挂。

误区“光柱边缘那么柔和,放低分辨率再放大不就糊了吗?必须全分辨率算!”于是每一帧都在 4K 屏幕上跑几十次径向采样。

正解God Rays 本来就是低频效果——它没有树叶边缘那种高频细节,光柱的能量分布极其平滑。半分辨率的遮挡图和模糊结果放大后,几乎没有人能分辨出区别,运算量却少了四分之三。这是游戏后处理最经典的“草稿纸”策略。

为何常见:人眼对“清晰”有天然执念,但没意识到不同效果对分辨率的敏感度天差地别:体积光的“清晰”来自形状而非像素密度。

误区“反正光柱是加法混合,把光柱图层 paint 到屏幕上就完事了,不需要管遮挡和深度。”结果太阳被山挡住时,光柱却穿过了山体,穿帮穿到火星。

正解加法混合只解决“加能量”这一步,但光柱能不能穿过几何体,完全取决于遮挡贴图和深度测试。被遮挡的像素在可见性比较中被置为 0,光柱自然断掉;否则光会穿过所有不透明物体,变成幽灵光。加法混合是最后一环,前面的遮挡判断才是灵魂。

为何常见:加法混合的 shader 写起来非常简单,容易让人误以为体积光的全部秘密就在这行混合指令里,而忘了它只是流水线末端的一道工序。

面试题

Q1. God Rays 在摄影中对应的自然现象叫什么?

单选题 · easy

  • A.丁达尔效应← 正确答案
  • B.漫反射现象
  • C.镜面反射现象
  • D.偏振滤光现象

答案:A。阳光穿过雾气或尘埃时,微粒散射使光路可见,这正是丁达尔效应。漫反射是光线在粗糙表面上的均匀反射,镜面反射是光滑表面的定向反射,偏振是光波按特定方向振动,都与“空气现形”无关。清晨森林的光柱就是丁达尔效应的天然名片。

Q2. 光在参与介质中强度随距离衰减的规律由哪个定律描述?

单选题 · easy

  • A.比尔-朗伯定律← 正确答案
  • B.安培定律
  • C.胡克定律
  • D.库仑定律

答案:A。比尔-朗伯定律指出透射率随介质厚度呈指数衰减 $T=e^{-\sigma d}$,是体积光计算光强的基石。安培定律描述电流与磁场关系,胡克定律描述弹簧形变,库仑定律描述静电力,都与光的能量衰减无关。

Q3. 场景中能看见光柱,主要因为光线发生了什么?

单选题 · easy

  • A.被微粒散射← 正确答案
  • B.被物体吸收
  • C.产生镜面反射
  • D.形成完全偏振

答案:A。光柱可见的本质是空气中的微粒把一部分光散射进观察者眼睛,相当于给无形光路涂上了显影剂。吸收只会让光变暗,镜面反射需要光滑表面,完全偏振是理想化的特殊状态,都无法解释空气中四处可见的光路。

Q4. Ray Marching 中透射率 T 的逐段更新方式是什么?

单选题 · mid

  • A.乘上指数衰减因子← 正确答案
  • B.减去一个固定步长
  • C.重置为固定常数
  • D.与像素距离相加

答案:A。每走一步,透射率都要乘以该段介质的透射率 $T \mathrel{*}= e^{-\sigma\Delta t}$,表示“剩余的光又被打折了一次”。减法是离散错误的直觉,常数重置会丢失累计遮挡信息,相加则完全不符合光的指数衰减物理。

Q5. God Rays 径向模糊的采样方向由什么决定?

单选题 · mid

  • A.从像素指向光源← 正确答案
  • B.屏幕水平方向
  • C.屏幕垂直方向
  • D.深度法线方向

答案:A。径向模糊的意义在于让能量沿着“像素—光源”连线扩散,所以采样方向 $\vec d=(x-x_{\text{sun}})/\lVert x-x_{\text{sun}}\rVert$ 从像素指向光源。水平或垂直方向会得到普通高斯模糊,法线方向与光柱的形状生成无关。

Q6. 遮挡贴图在体积光中的作用有哪些?

多选题 · mid

  • A.记录光源能否照到像素← 正确答案
  • B.充当光柱的剪影底稿← 正确答案
  • C.完全替代其他光照计算
  • D.提供介质密度数据

答案:AB。遮挡贴图从光源视角渲染深度,通过深度比较得到可见性 $V(p)$,既记录了光源能否照到该像素,也作为后续径向模糊的剪影底稿。它不替代光照模型,也不提供密度场——密度来自参与介质数据,而非遮挡图。

Q7. 后处理分辨率降到 1/2 后,单帧体积光计算量约为原来的多少?

单选题 · mid

  • A.四分之一← 正确答案
  • B.二分之一
  • C.八分之一
  • D.完全不变

答案:A。计算量正比于像素数,分辨率宽高各缩一半后总像素数为原来的 $1/2 \times 1/2 = 1/4$。二分之一是只缩单方向的直觉误区,八分之一没有对应的缩放关系,体积光的模糊性质决定了半分辨率后效果几乎无损。

Q8. 合成体积光时通常采用加法混合的根本原因是什么?

单选题 · hard

  • A.散射向原图叠加能量← 正确答案
  • B.直接替换原图所有颜色
  • C.避免任何浮点运算
  • D.模拟相减式的光照

答案:A。散射光是把空气中的辐射能“塞”进已有画面里,能量只增不减,因此 $F_{\text{final}}=C_{\text{scene}}+G_{\text{godray}}$。替换颜色会丢失底图细节,加法混合并不省浮点运算,散射也跟相减式光照完全相反。

Q9. 下列哪些参数会直接影响 God Rays 的性能开销?

多选题 · hard

  • A.光线步进次数← 正确答案
  • B.模糊采样数量← 正确答案
  • C.半分辨率因子← 正确答案
  • D.光束的染色颜色

答案:ABC。光线步进次数 N 与模糊采样数直接决定循环体内执行次数;半分辨率因子 s 使像素数按 $s^2$ 变化,是最大杠杆。染色只是乘法系数,改变 RGB 值不改变循环次数,因此不占用额外耗时。

Q10. 关于体积光的时序累积(temporal accumulation)优化,下列说法正确的有哪些?

多选题 · extreme

  • A.复用上一帧结果抗闪烁← 正确答案
  • B.用历史帧补当前帧采样← 正确答案
  • C.能完全替代降采样策略
  • D.需要运动权重校正← 正确答案

答案:ABD。时序累积把每帧的少量采样分布到时间轴上,用历史帧信息补足当前帧的采样密度,从而抑制闪烁,这是它的核心思想。但相机运动时历史像素会错位,必须用运动向量做重投影校正。它不能完全替代降采样——半分辨率降低的是整体计算负担,二者是互补而非替代关系。