清晨六点半,你走进一片松树林。雾气还没散,东边的太阳从树梢的缝隙间钻出来,投下一道道金白色的光柱,像一扇扇半透明的门斜插在空气中。你以为光本来就是这样一条一条的,直到你走到树下才发现:光其实是无形的,是空气中漂浮的水雾和尘埃,把笔直前进的阳光一点点“折射”到你眼睛里,才让你看见了它的路径。这就是著名的丁达尔效应,也是游戏世界里“体积光”(God Rays)这个名字的由来——游戏工程师把这道清晨的魔法搬进了显卡的渲染管线,让《荒野大镖客》的晨雾、《塞尔达》的古树缝隙、甚至你手机里那张逆光照片都拥有了电影级的立体感。 本篇文章要讲的,就是这道光柱从“看到”到“算出来”的全过程:空气为什么能现形、光线怎么一步一步走、树影怎么剪出光柱的形状、性能又如何在半张草稿纸上省出大半帧开销。
对 AI 视觉与实时渲染从业者而言,体积光是大场景氛围与画质上限的关键拼图,掌握它等于掌握了电影级光照的“最后一公里”,无论是做游戏 TA、渲染引擎开发还是虚拟制片,都是加分极强的硬技能。
目录
可视化演示
动态演示体积光God Rays的核心机制,通过全局/局部/动态/静态四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
体积光God Rays:体积光物 体积光物理原理与大气散射模型 屏幕空间后处理God Rays 核心机制可视化 体积光God Rays整体架构与关键组件
1. 光柱现象:丁达尔视野
你清晨走进松树林,雾气把阳光切成一道道金白色的“门”。如果你把雾气抽走,光柱会瞬间消失——阳光还是那束阳光,只是没有了空气中的小水滴和尘埃做“显影剂”。摄影师把这种现象叫丁达尔效应:当一束光穿过含有微粒的胶体时,微粒会把光散射到四面八方,其中一部分恰好进入你的眼睛,于是原本看不见的光路被“画”了出来。
在游戏渲染里,我们不可能真的模拟上亿颗雾粒,但可以抓住这个现象的本质:光在空气里走,能量会随着走过的距离被介质逐渐消耗。这个消耗规律用比尔-朗伯定律描述:
有了这个“能量衰减”的直觉,你就能理解为什么远处的光柱总是比近处的淡——不是光源变暗了,而是光在赶路时被空气不断“打劫”。游戏画面里的体积光,本质上就是把这场“打劫”按像素一笔一笔算出来。
- 初始光强 $I_0$阳光刚射入雾气时携带的能量,是光柱的“起跑线”,光线越强,后续光柱越亮。
- 介质吸收系数 $\sigma$空气里雾粒和尘埃的浓度,$\sigma$ 越大,光每走一小段就被“吃掉”越多。
- 穿透距离 $d$光在空气中走过的距离,$e^{-\sigma d}$ 就是光“劫后余生”剩下的比例,距离越远剩余越少。
2. 散射介质:让光现形的微粒
还是那片清晨的松林。你伸手挥一下,光柱里有细小的灰尘在跳舞。它们就是光的“舞台舞美”——没有它们,阳光再好,你也只能看见树梢上的亮斑,看不见空气本身。雾气、烟尘、雨丝、甚至是极细的沙尘,在渲染术语里统称为“参与介质”(participating media):它们参与了光的传播,把本来沿直线前进的光撬向四面八方。
但散射并不是均匀的:绝大多数雾粒会把光“顺手”扔回原方向附近。你站在太阳背后看雾,雾是亮的;站在太阳对面看雾,雾就显得没那么亮。这种“偏心”用 Henyey-Greenstein 相位函数描述:
记住这个“偏心散射”就好:你在看清晨光柱时,大部分光其实是朝太阳方向继续走,只是极少数恰好折进你眼睛里。渲染引擎模拟的,正是这“极少数”的旅程。
- 散射角 $\theta$入射光方向和你的视线方向之间的夹角,$\theta = 0$ 表示光笔直朝你飞来。
- 前向因子 $g$$g$ 越接近 $1$,光越倾向于继续朝前方散射,雾气看起来“顺光发亮”。
- 归一化分母分母中的 $4\pi$ 保证把整个球面上所有方向的散射概率加起来正好是 $1$,不会凭空多出能量。
3. 射线步进:沿着视线“舀汤”
光柱怎么算?想象你端着一只勺子,从眼睛出发,沿着视线方向一步步走向远处的树。每走一小段,你就舀一勺“空气汤”尝一口:这一段里有多少雾?雾被阳光照亮了几分?这一段挡掉了多少后面来的光?把沿途所有“勺子”的结果叠在一起,就是你看那道光柱的颜色和亮度。
显卡里管这个叫射线步进(Ray Marching)。视线变成一条射线,从近到远切成一格一格的采样段,每段的长度是 $\Delta t$。每一段都有三笔账要算:这段空气自己的透光率、这段空气从光源接到的散射能量、以及之前所有段累积下来还剩下多少光。把它们合成起来:
步进步长 $\Delta t$ 越小,结果越精细,但计算次数也越多,本质上是用时间换质量。游戏里为了流畅,通常把步长调得稀疏一点,再靠后处理补妆。
- 每段透射率 $T_i$第 $i$ 段空气的透光率,由比尔-朗伯定律给出 $T_i = e^{-\sigma_i \Delta t}$。
- 散射能量 $A_i$这一段空气从光源那里接受到并散射进你眼睛的那份光,受相位函数和光源强度共同影响。
- 前缀累积 $\prod_{j=1}^{i-1} T_j$从近处一路乘到第 $i$ 段之前,表示前面每一段透射率的乘积,越深的光柱越容易被近处雾气“罩住”。
4. 遮挡贴图:光柱的剪影底稿
清晨的树影为什么是光柱的“形状”?因为树枝把阳光挡住了,没挡住的地方光才能一路射下来。换成渲染的语言:我们需要知道“从太阳的角度看,这个像素到底有没有被树挡住”。这个问题跟阴影贴图(Shadow Map)几乎一模一样。
做法是把相机搬到太阳的位置,朝地面方向渲染一张“深度图”,记下太阳视角下每个点离太阳有多远,这张图就是遮挡贴图。之后在正常渲染时,把每个像素重新投影到太阳视角下,比较它的距离与遮挡贴图里记录的深度,就能判定它露没露头:
这张只含 $0$ 和 $1$ 的“剪影底稿”送到下一步,就变成了光柱的骨架。它不需要很高分辨率,因为后处理模糊会帮它磨皮。
- 光源视角深度 $d_{\text{sm}}(p)$把像素 $p$ 投影到太阳视角的深度图上,读出的那个深度值,代表太阳眼中 $p$ 的距离。
- 场景实际深度 $d_{\text{scene}}(p)$从主相机视角看过去,$p$ 离相机的实际距离。
- 深度比较若 $d_{\text{sm}} \le d_{\text{scene}}$,说明从太阳看,$p$ 前面没有更近的遮挡物,可见性为 $1$;否则被挡住,可见性为 $0$。
5. 径向模糊:把光束晕开的水彩
有了剪影底稿还不够:真实的光柱边缘是柔的,不是一把硬邦邦的刀切出来的。就像水彩笔在纸上点了一个点,你要用干净的水笔沿着某个方向轻轻扫开,颜色才会顺着水流洇出去,形成一头浓一头淡的笔触。
God Rays 后处理里这个“扫水”的动作叫径向模糊:以画面中太阳所在的像素为“圆心”,对每个像素沿“像素到太阳”的连线方向,前后各取若干采样点,按权重相加。离圆心越近权重越高,越远越淡:
采样半径 $R$ 越大,光柱拖得越长,也越柔和。这也是“光柱为什么从太阳向四周放射”这一观感的数学来源——不是光真的在画面里跑,而是径向模糊把剪影底稿里的能量沿着太阳方向“抹”了出去。
- 径向方向 $\vec d$从像素 $x$ 指向太阳像素 $x_{\text{sun}}$ 的单位向量,它决定模糊沿着哪个方向扫。
- 偏移量 $i\,\vec d$沿着径向方向前后移动 $i$ 步,采样点就像水彩笔刷的毛,从中心向两端散开。
- 归一化权重 $w_i / K$中间权重高、边缘权重低,$K = \sum w_i$ 保证整体亮度不因模糊而膨胀或缩水。
6. 性能优化:用小草稿画出大场面
画水彩的时候,没人会在四平方米的墙面上练笔——都是先在一张手掌大的草稿纸上试颜色、试浓度,满意了再放大。God Rays 后处理也一样:与其在全分辨率下做几十次径向采样,不如先把画面缩到一半甚至四分之一分辨率,算完光柱再放大贴回去,肉眼几乎看不出区别,速度却快出好几倍。
为什么半分辨率这么赚?因为计算量跟像素数成正比,而像素数跟分辨率的平方成正比。把宽高各缩一半,总像素直接变成四分之一:
除了降分辨率,常见的还有两招:减少采样次数,靠抖动和时序累积(temporal accumulation)复用上一帧的采样结果来补噪声;以及提前用较便宜的深度测试剔除大量无关像素。三者组合,God Rays 从“奢侈品”变成“日用品”。
- 全屏像素 $W \cdot H$宽乘高就是这一帧画面里要处理的像素总数,是成本的地基。
- 降采样系数 $s^2$宽高都除以 $s$ 后,像素数除以 $s^2$,这是性能优化的最大杠杆。
- 步数与着色开销 $N \cdot S$$N$ 是径向模糊或射线步进的采样次数,$S$ 是单次采样的着色开销,两者相乘后同样乘在前面。
7. 参数调校:光影的性格
同样的阳光,在纱雾里是温柔的长纱,在浓雾里是浑厚的云柱,在暴雨后的林间则是又短又利的金箭头。这些“性格”全由灯光师手上的几个旋钮决定。调参不是玄学,每个旋钮都对应一个明确的光学量。
最核心的四个旋钮是:强度 $A$、距离衰减 $D$、径向半径 $r(x)$ 和染色 $\text{Tint}$。强度负责整体音量,衰减负责光柱能拖多长,染色则给整根光柱镀上一层暖黄或冷白。把它们组合起来,一根光柱的最终颜色可以写成:
实际项目里,这组参数通常暴露在引擎的 PostProcess Volume 里,美术同学在场景里实时拨动旋钮,一边看画面一边找“那一口气”。
- 整体强度 $A$一个乘在所有光柱像素上的标量,调大它,光柱整体变亮或变暗,相当于总音量旋钮。
- 距离衰减指数 $e^{-D r(x)}$$r(x)$ 是像素到光源的距离,$D$ 是衰减快慢,$D$ 越大,光柱在远处迅速断掉,显得短促有力。
- 染色 $\text{Tint}$一个 RGB 颜色向量,乘上去之后给光柱的亮部染上倾向色,晨光偏金黄、月光偏冷蓝。
8. 工程应用:把整条流水线装进引擎
最后一步,把前面所有工序组装成一条真正的游戏渲染流水线。想象清晨松林被装进一台摄像机:先用普通光照管线画出没有光柱的场景底图,再从太阳视角出一张低分辨率遮挡图,然后对遮挡图做几步射线估算和径向模糊,得到一张只含光柱的“特效图层”,最后把这张图层叠加回底图上。
叠加很讲究:体积光本质是空气中的散射光,它只会往画面里加能量,不会把原有的颜色盖掉,所以用加法混合而不是普通覆盖。最终公式可以写成:
在 Unity 的 Sun Shafts、虚幻引擎的体积雾、以及主流引擎的 Light Shaft 组件里,这套流程已经被封装成一站式节点:你拖一个“上帝光”组件进去,指定太阳方向、树影贴图源和几个旋钮,剩下的交给底层 Shader。知道原理的你,看到这些节点时,就能一眼认出它们其实是“遮挡 → 步进 → 径向模糊 → 加色混合”的老朋友。
- 场景底图 $C_{\text{scene}}$还没加体积光的原始帧,是光柱的“画布”。
- 光柱图层 $G_{\text{godray}}$前面所有工序产出的、只含光束能量的特效图。
- 逐元素乘并叠加$\odot$ 表示逐像素乘以一个常数光色向量 $C_{\text{eye}}$,然后与底图做加法混合,亮度只增不减。
AI 应用场景
游戏引擎实时体积光(技术美术调参)
阶段:后处理渲染管线集成(推理出图阶段)
解决的问题:过去模拟丁达尔光柱要手动摆放几十个大光晕贴图,视角一换就穿帮;用 God Rays 后处理 + 遮挡贴图后,光柱形状随相机视角实时变化,美术只需要拖动强度、衰减、染色三个参数就能预览。
具体案例:在 Unity 体积光插件中,技术美术把场景中的树冠层设为遮挡源,太阳方向对准晨雾,输出 1/2 分辨率的光柱图层并与主画面加法混合,角色走进光柱时皮肤和武器上的光斑随之流动,帧耗时仅增加 0.8ms。
AI辅助光照参数推荐
阶段:自动调参(生成式设计辅助)
解决的问题:God Rays 参数多且互相影响,新手美术容易把光柱调成“刺眼的白条”;通过采集大量场景 HDR 截图与美术标注,训练一个轻量回归模型,输入目标氛围(柔和、神圣、冷峻),自动推荐强度与染色参数。
具体案例:某 AIGC 场景生成工具中,用户选择“清晨森林”风格,模型先根据深度图自动放置太阳方向,再生成一组 God Rays 参数,配合湿度参数一起输出,画面直达电影级氛围。
虚拟制片逆光预演
阶段:虚拟预演可视化(推理渲染阶段)
解决的问题:影视虚拟制片拍摄时,导演需要实时看到“逆光 + 灰尘”的氛围,但现场不可能真的布一层雾;用体积光渲染在 LED 背景墙上模拟丁达尔效果,演员和摄影机运动时,光柱随视角实时变化,演员走位信心大增。
具体案例:某科幻短片拍摄中,灯光师在引擎里放置太阳光源和雾浓度体,God Rays 从废土建筑的窗户缝隙射入,特效团队现场调整衰减参数配合演员调度,省去了昂贵的光束物理装置。
Python 演示
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()
occluder[((xx - cx) / rx) ** 2 + ...] = 0用椭圆方程标记树冠,把被遮挡像素置为 0,模拟遮挡贴图的“剪影”。density = np.clip(1 - dist / (w * 0.85), 0, 1)距离越远光照越弱,对应比尔-朗伯式的衰减直觉。rays = occ * density ** 2遮挡为 0 的地方光柱直接熄灭,形成光束与阴影交错。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 在摄影中对应的自然现象叫什么?
- A.丁达尔效应← 正确答案
- B.漫反射现象
- C.镜面反射现象
- D.偏振滤光现象
答案:A。阳光穿过雾气或尘埃时,微粒散射使光路可见,这正是丁达尔效应。漫反射是光线在粗糙表面上的均匀反射,镜面反射是光滑表面的定向反射,偏振是光波按特定方向振动,都与“空气现形”无关。清晨森林的光柱就是丁达尔效应的天然名片。
Q2. 光在参与介质中强度随距离衰减的规律由哪个定律描述?
- A.比尔-朗伯定律← 正确答案
- B.安培定律
- C.胡克定律
- D.库仑定律
答案:A。比尔-朗伯定律指出透射率随介质厚度呈指数衰减 $T=e^{-\sigma d}$,是体积光计算光强的基石。安培定律描述电流与磁场关系,胡克定律描述弹簧形变,库仑定律描述静电力,都与光的能量衰减无关。
Q3. 场景中能看见光柱,主要因为光线发生了什么?
- A.被微粒散射← 正确答案
- B.被物体吸收
- C.产生镜面反射
- D.形成完全偏振
答案:A。光柱可见的本质是空气中的微粒把一部分光散射进观察者眼睛,相当于给无形光路涂上了显影剂。吸收只会让光变暗,镜面反射需要光滑表面,完全偏振是理想化的特殊状态,都无法解释空气中四处可见的光路。
Q4. Ray Marching 中透射率 T 的逐段更新方式是什么?
- A.乘上指数衰减因子← 正确答案
- B.减去一个固定步长
- C.重置为固定常数
- D.与像素距离相加
答案:A。每走一步,透射率都要乘以该段介质的透射率 $T \mathrel{*}= e^{-\sigma\Delta t}$,表示“剩余的光又被打折了一次”。减法是离散错误的直觉,常数重置会丢失累计遮挡信息,相加则完全不符合光的指数衰减物理。
Q5. God Rays 径向模糊的采样方向由什么决定?
- A.从像素指向光源← 正确答案
- B.屏幕水平方向
- C.屏幕垂直方向
- D.深度法线方向
答案:A。径向模糊的意义在于让能量沿着“像素—光源”连线扩散,所以采样方向 $\vec d=(x-x_{\text{sun}})/\lVert x-x_{\text{sun}}\rVert$ 从像素指向光源。水平或垂直方向会得到普通高斯模糊,法线方向与光柱的形状生成无关。
Q6. 遮挡贴图在体积光中的作用有哪些?
- A.记录光源能否照到像素← 正确答案
- B.充当光柱的剪影底稿← 正确答案
- C.完全替代其他光照计算
- D.提供介质密度数据
答案:AB。遮挡贴图从光源视角渲染深度,通过深度比较得到可见性 $V(p)$,既记录了光源能否照到该像素,也作为后续径向模糊的剪影底稿。它不替代光照模型,也不提供密度场——密度来自参与介质数据,而非遮挡图。
Q7. 后处理分辨率降到 1/2 后,单帧体积光计算量约为原来的多少?
- A.四分之一← 正确答案
- B.二分之一
- C.八分之一
- D.完全不变
答案:A。计算量正比于像素数,分辨率宽高各缩一半后总像素数为原来的 $1/2 \times 1/2 = 1/4$。二分之一是只缩单方向的直觉误区,八分之一没有对应的缩放关系,体积光的模糊性质决定了半分辨率后效果几乎无损。
Q8. 合成体积光时通常采用加法混合的根本原因是什么?
- A.散射向原图叠加能量← 正确答案
- B.直接替换原图所有颜色
- C.避免任何浮点运算
- D.模拟相减式的光照
答案:A。散射光是把空气中的辐射能“塞”进已有画面里,能量只增不减,因此 $F_{\text{final}}=C_{\text{scene}}+G_{\text{godray}}$。替换颜色会丢失底图细节,加法混合并不省浮点运算,散射也跟相减式光照完全相反。
Q9. 下列哪些参数会直接影响 God Rays 的性能开销?
- A.光线步进次数← 正确答案
- B.模糊采样数量← 正确答案
- C.半分辨率因子← 正确答案
- D.光束的染色颜色
答案:ABC。光线步进次数 N 与模糊采样数直接决定循环体内执行次数;半分辨率因子 s 使像素数按 $s^2$ 变化,是最大杠杆。染色只是乘法系数,改变 RGB 值不改变循环次数,因此不占用额外耗时。
Q10. 关于体积光的时序累积(temporal accumulation)优化,下列说法正确的有哪些?
- A.复用上一帧结果抗闪烁← 正确答案
- B.用历史帧补当前帧采样← 正确答案
- C.能完全替代降采样策略
- D.需要运动权重校正← 正确答案
答案:ABD。时序累积把每帧的少量采样分布到时间轴上,用历史帧信息补足当前帧的采样密度,从而抑制闪烁,这是它的核心思想。但相机运动时历史像素会错位,必须用运动向量做重投影校正。它不能完全替代降采样——半分辨率降低的是整体计算负担,二者是互补而非替代关系。