想象你是一位舞台剧的灯光师。大幕拉开前,你爬上舞台正上方的灯架,站在聚光灯的位置,举起相机对着舞台拍了一张照片——但这张照片不记录演员的脸,只记录一个信息:每个位置离你有多远。前排道具近,后排布景远,这张“远近照片”就是传说中的深度图。演出开始后,你回到观众席,每当看到一位演员站在道具后面,你就在心里默念:“他离灯架的距离,比照片里那个位置记录的还远,说明光线半路被挡住了,该打阴影。”这个“先拍远近照片、再比对距离”的招数,就是游戏引擎里大名鼎鼎的阴影映射。 不过事情远没这么简单。硬邦邦的影子会有锯齿,像被狗啃过;影子在物体底部还会冒出一片“粉刺”;远处的影子闪个不停。于是灯光师发明了 PCF 滤波让影子边缘变得柔和,发明了 CSM 级联阴影让近处清晰、远处不闪烁。而真正让舞台活起来的是全局光照——你会发现,关掉主灯,房间也不会全黑,因为墙壁、地板、道具都在互相“反射”光线。提前把灯光效果烘焙成“照片”的 Lightmass,和现场实时追踪光线弹射的 Lumen,是两种风格迥异的做法。这篇文章会借这位灯光师的眼睛,带你从零看懂阴影映射与全局光照的每一块拼图。

岗位关联

掌握阴影映射与全局光照,你就能读懂游戏引擎里“一束光如何讲故事”——无论调试一个棘手的阴影瑕疵,还是在 AI 生成虚拟环境中让光照以假乱真,这都是图形渲染技术栈中最核心、最实用的地基。

目录
  1. 可视化演示
  2. 阴影映射原理
  3. 阴影瑕疵防治
  4. PCF软阴影
  5. CSM级联阴影
  6. 全局光照基础
  7. Lightmass预烘焙
  8. Lumen实时全局光照
  9. 质量与性能平衡
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示阴影映射深度比较原理、CSM级联阴影多分辨率策略与全局光照光线弹射模拟,切换模式理解游戏引擎核心渲染技术。

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

阴影映射深度比较原理 从光源视角渲染深度图,记录最近表面距离形成阴影纹理 片元深度与深度图比较,被遮挡处标记为阴影区域 Shadow = (depth_f > depth_map) ? 1 : 0 阴影映射是实时阴影的基础,通过双pass深度比较生成硬阴影

阴影映射原理

灯光师站在灯架上拍下的“远近照片”,在图形学里有一个正式名字:阴影贴图(Shadow Map),也叫深度图。它不是给人看的照片,而是一张记录“从光源视角看,每个方向最近的物体离我有多远”的深度纹理。GPU 在做这件事时,只需要把光源当作一台临时相机,把场景的深度渲染到一张纹理中,不需要任何颜色和光照计算,因此速度很快。

当正式渲染画面时,每个像素都可以知道自己在世界空间中的位置。图形管线会把这个点变换到光源坐标系下,得到它到光源的实际距离;同时用它在阴影贴图中的坐标,采样出“光源视角下最近的阻挡距离”。接下来只需一次比较:如果实际距离大于贴图记录的距离,说明这个点被某个更靠近光源的物体挡住了,它就应该落在阴影里。

这套“先拍远近、再比对”的思路,让灯光师一眼就看出舞台上谁躲在谁的身后。相比传统的手工遮挡判断,阴影映射把问题变成了“一次渲染加一次比较”,硬件友好且容易并行,因此成为实时渲染中最主流的阴影方案。

Shadow(p)={1,zeye(p)>zmap(pproj)0,zeye(p)≤zmap(pproj)\displaystyle \text{Shadow}(\mathbf{p}) = \begin{cases} 1, & z_{\text{eye}}(\mathbf{p}) > z_{\text{map}}(\mathbf{p}_{\text{proj}}) \\ 0, & z_{\text{eye}}(\mathbf{p}) \le z_{\text{map}}(\mathbf{p}_{\text{proj}}) \end{cases}
通俗地说:把场景点放到“灯架照片”里比对,谁比照片里的阻挡物更远,谁就被打阴影。
  1. 光源变换把世界空间中的点 p 用光源的视图投影矩阵变换到光源裁剪空间,得到它在“灯架相机”底片上的位置与深度。
    pproj=Mlight⋅p\mathbf{p}_{\text{proj}} = M_{\text{light}} \cdot \mathbf{p}
  2. 实际深度变换后的深度分量就是该点到光源的真实距离,记为 z_eye(p)。
    zeye(p)=pproj.zz_{\text{eye}}(\mathbf{p}) = \mathbf{p}_{\text{proj}}.z
  3. 贴图深度用裁剪坐标的 x、y 去阴影贴图采样,得到光源视角下最近挡光物的距离 z_map。
    zmap(pproj)=样本(ShadowMap,pproj.xy)z_{\text{map}}(\mathbf{p}_{\text{proj}}) = \text{样本}(\text{ShadowMap}, \mathbf{p}_{\text{proj}}.xy)
  4. 深度判决当实际距离大于贴图记录时,说明光线被中途拦截,标记为阴影,否则照亮。
    Shadow(p)=[zeye(p)>zmap(pproj)]\text{Shadow}(\mathbf{p}) = [z_{\text{eye}}(\mathbf{p}) > z_{\text{map}}(\mathbf{p}_{\text{proj}})]

阴影瑕疵防治

照片拍得再好,也会有瑕疵。灯光师很快发现,影子边缘出现了锯齿——因为阴影贴图的分辨率有限,好比一张低像素照片放大了全是马赛克。更恼人的是“阴影粉刺”:物体表面莫名其妙出现一片细碎的明暗条纹,像皮肤上长了痘痘。粉刺的根源在于深度值是用有限精度存储的,物体表面在比较时,自己的实际深度往往比贴图里记录的深度略微大一点点,于是表面被自己误判成阴影。

解决粉刺的常用办法是给比较加上一个深度偏移(Depth Bias):允许实际深度在贴图深度的基础上“让开”一小段距离,只有超出这段容差才算阴影。偏移量太小,粉刺压不住;偏移量太大,又会出现“彼得潘效应”——影子与物体底部脱离开,仿佛小飞侠彼得潘的影子被偷走了。优秀的实现在计算偏移时还会考虑表面与光线的夹角:光线越倾斜,深度差值越大,偏移也要相应增大。

灯光师拿着这支“偏移修正笔”,把影子边缘的痘痘一个个点掉。不过他也清楚,这只是治标——要让影子真正美观,还需要让边缘柔和起来,这就是我们接下来要聊的 PCF。

Shadow(p)={1,zeye(p)>zmap(pproj)+bias(θ)0,zeye(p)≤zmap(pproj)+bias(θ)\displaystyle \text{Shadow}(\mathbf{p}) = \begin{cases} 1, & z_{\text{eye}}(\mathbf{p}) > z_{\text{map}}(\mathbf{p}_{\text{proj}}) + \text{bias}(\theta) \\ 0, & z_{\text{eye}}(\mathbf{p}) \le z_{\text{map}}(\mathbf{p}_{\text{proj}}) + \text{bias}(\theta) \end{cases}
好比量身高时允许卷尺有一点缓冲,不让边框上的灰尘影响读数;但缓冲太多,脚又离地了。
  1. 比较容差在实际深度与贴图深度之间加入一个正偏移 bias,作为误差容限,消除自遮挡。
    zeye(p)>zmap(pproj)+bias(θ)z_{\text{eye}}(\mathbf{p}) > z_{\text{map}}(\mathbf{p}_{\text{proj}}) + \text{bias}(\theta)
  2. 角度缩放θ 是表面法线与光源方向的夹角,夹角越大深度误差越大,因此偏移量随角度增大而增大。
    bias(θ)=biasbase+biasslope⋅tan⁡θ\text{bias}(\theta) = \text{bias}_{\text{base}} + \text{bias}_{\text{slope}} \cdot \tan\theta
  3. 粉刺消除加入偏移后,表面自身的 z_eye 不再大于 z_map + bias,粉刺随之消失,而真正的遮挡仍会被正确判断。
    若  zeye≤zmap+bias  ⇒  照亮\text{若} \; z_{\text{eye}} \le z_{\text{map}} + \text{bias} \;\Rightarrow\; \text{照亮}

PCF软阴影

大晴天正午的影子边缘像刀切一样硬,阴天则柔和得多。现实中的光源并不是一个数学意义上的点,而是有面积的光源,因此物体边缘会形成从“完全被遮”到“完全照亮”的渐变区域,称为半影。在阴影映射里,这种“硬得像刀切”的效果正是点光源假设造成的。我们当然可以改作对大面积光源做积分,但那样开销太大。于是图形学用一个巧妙的笑话式方案:PCF(Percentage Closer Filtering,百分比更近滤波)。

PCF 的思路非常朴素:在对某个像素做深度比较时,不要只看深度图上的一个点,而是看它周围一圈邻居,对每个邻居都做一次深度比较,最后把结果取平均。这样,阴影边缘处有些邻居判定“被挡”、有些判定“照亮”,平均值就落在 0 到 1 之间,形成柔和的过渡。注意一个关键细节:PCF 模糊的是“比较后的结果”,而不是深度图本身。直接对深度图做高斯模糊会让深度值互相污染,反而产生漏光与错误遮挡。

滤波核的大小直接决定半影宽度:核越大,过渡越宽,影子越柔和,但采样开销也越高。灯光师把手指在照片边缘一抹,影子立刻温柔起来——这就是 PCF 带给画面的温度。

S(p)=1N∑k=1N1[zeye(p)>zmap(p+Δpk)+b]\displaystyle S(\mathbf{p}) = \frac{1}{N} \sum_{k=1}^{N} \mathbb{1}\left[ z_{\text{eye}}(\mathbf{p}) > z_{\text{map}}(\mathbf{p} + \Delta \mathbf{p}_k) + b \right]
灯光师不再盯着照片上的一个像素,而是把眼睛眯起来看周围一小片区域,然后凭“这一片区域里被挡住的比例”来判断影子深浅。
  1. 邻域偏移对深度图坐标 p 加上一系列小偏移 Δp_k,表示采样它周围 N 个邻居。
    p+Δpk,k=1,2,…,N\mathbf{p} + \Delta \mathbf{p}_k, \quad k = 1, 2, \dots, N
  2. 逐个比较每个邻居独立做深度比较:实际深度大于该处贴图深度加偏移,则记 1(阴影),否则记 0(亮)。
    1[zeye(p)>zmap(p+Δpk)+b]\mathbb{1}\left[ z_{\text{eye}}(\mathbf{p}) > z_{\text{map}}(\mathbf{p} + \Delta \mathbf{p}_k) + b \right]
  3. 求平均把所有比较结果加起来除以 N,得到 0 到 1 之间的阴影程度,边缘因此出现渐变。
    S(p)=1N∑k=1N1[⋅]S(\mathbf{p}) = \frac{1}{N} \sum_{k=1}^{N} \mathbb{1}[\cdot]

CSM级联阴影

舞台很大,从脚边到天边可能有几百米。如果只用一张阴影贴图覆盖整个舞台,近处的道具只占几个像素,影子糊成一团;远处的布景又浪费了大量精度。聪明的灯光师把视野分成近、中、远好几层,每层配一台“相机”单独拍一张深浅照片:近处的照片覆盖小范围、用高分辨率,远处的照片覆盖大范围、用低分辨率。这就是CSM(Cascaded Shadow Maps,级联阴影贴图)。

每一级对应视锥体的一段距离区间。划分位置不能是简单的均匀等分——近处物体在屏幕上占的面积大,需要更精细的层级;远处占的面积小,可以粗糙一些。实践中常用“对数划分”与“均匀划分”的加权混合:对数划分让近处获得更多级联,均匀划分保证远处也有覆盖。划分完毕后,渲染每个物体时根据它所在的层级选择对应的阴影贴图,并在级联边界处做融合,避免出现明显的“换层”断裂感。

有了 CSM,灯光师终于可以在一个巨大舞台上既看清演员脚边的细腻影子,又不让远处的群山变成一片闪烁的噪点。这是“把预算花在刀刃上”的经典智慧。

di=λ n(fn)i/m+(1−λ)(n+im(f−n)),i=1,2,…,m\displaystyle d_i = \lambda \, n \left( \frac{f}{n} \right)^{i/m} + (1-\lambda) \left( n + \frac{i}{m}(f-n) \right), \quad i = 1, 2, \dots, m
把舞台看作一排路灯:最近的路灯照得又亮又清,远处的路灯稀稀拉拉,但只要能大致看清轮廓就够了。
  1. 层级索引m 是级联总数,i 是当前级联的编号,d_i 是第 i 级联的远平面距离。
    i∈{1,2,…,m}i \in \{1, 2, \dots, m\}
  2. 对数项第一项为对数划分,级联越往前密度越高,负责近处的高质量阴影。
    λ n(fn)i/m\lambda \, n \left( \frac{f}{n} \right)^{i/m}
  3. 均匀项第二项为均匀划分,保证每一层都有基本覆盖,防止远处被遗漏。
    (1−λ)(n+im(f−n))(1-\lambda) \left( n + \frac{i}{m}(f-n) \right)
  4. 加权混合λ 在 0 到 1 之间调节两种策略的比重,通常 λ 取 0.5 到 0.8。
    di=对数项+均匀项d_i = \text{对数项} + \text{均匀项}

全局光照基础

聚光灯直直打到演员脸上,可如果你在夜晚的房间里只开一盏台灯,会发现墙角、床底甚至书本的暗面都不是纯黑的——因为光打在墙壁、地板和家具上后,会像乒乓球一样在房间里弹来弹去,照亮那些“没有直接被灯照到”的地方。这种光的多次弹射就是全局光照(Global Illumination, GI)的核心。与之相对,只考虑从光源直接射到表面的光称为直接光照,它决定了影子的“形状”;间接光照则决定了影子内部和暗处的“颜色”。

图形学理论中描述这个物理过程的“圣杯”是渲染方程(Rendering Equation):一个表面点向外发出的光,等于它自身发射的光,加上从四面八方射入的光经过材质反射后的总和。这个方程里所有方向都要积分,求解它极其昂贵,因此所有实时引擎都在做各种近似。但无论怎么近似,有一条物理规律必须遵守:能量守恒——反射出的能量永远不可能大于入射能量,否则画面会越来越亮,像一台永动机一样荒谬。

渲染方程告诉我们:真正的光照是一场无限次的弹射接力。当然,工程上不能真的无限追下去,于是游戏引擎分成了两大流派:一派像 Lightmass 那样提前把接力赛结果算好存起来,另一派像 Lumen 那样在比赛进行中实时追踪。

Lo(x,ωo)=Le(x,ωo)+∫Ωfr(x,ωi,ωo) Li(x,ωi) cos⁡θi 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) \, \cos\theta_i \, d\omega_i
灯光师意识到:想让舞台真实,不能只盯着那盏主灯,还要在意墙壁反射回来的“回音”——只不过这回音是光。
  1. 出射辐射L_o 是点 x 沿方向 ω_o 最终射向眼睛的光,就是屏幕上看到的颜色。
    Lo(x,ωo)L_o(\mathbf{x}, \omega_o)
  2. 自发光项L_e 表示点 x 自己发出的光,比如灯泡、屏幕这类光源表面。
    Le(x,ωo)L_e(\mathbf{x}, \omega_o)
  3. 入射积分积分号表示把所有从半球 Ω 方向射来的光 L_i 都累加起来,每一项乘以材质反射率 f_r 和余弦夹角。
    ∫Ωfr(x,ωi,ωo) Li(x,ωi) cos⁡θi dωi\int_{\Omega} f_r(\mathbf{x}, \omega_i, \omega_o) \, L_i(\mathbf{x}, \omega_i) \, \cos\theta_i \, d\omega_i
  4. 能量守恒反射率 f_r 永远小于 1,因此弹射次数越多,光能量越弱,最终趋于稳定。
    ∫Ωfr cos⁡θ dω≤1\int_{\Omega} f_r \, \cos\theta \, d\omega \le 1

Lightmass预烘焙

有些舞台剧的灯光要求极高,每个角落的亮度、每一缕色彩反弹都要精确控制。灯光师的解决办法是提前进场,花几个小时甚至几天反复调光、排练,把最终的光照效果“烘焙”成一张张光照贴图(Lightmap),演出时直接铺到模型表面。引擎里的 Lightmass 就是这样的离线烘焙系统:它用业界成熟的光子映射、路径追踪算法,在编辑器里预先计算好所有静态物体的间接光照,把结果存进纹理。

烘焙方案的最大优势是质量。因为计算不受实时帧率限制,光线可以弹射几十上百次,得到极其细腻的全局光照效果,而且运行时消耗几乎为零——显卡只需要把光照贴图采样出来叠加到材质上。代价则是静态:任何一盏灯的位置、颜色,或者一面墙的移动,都会让烘焙结果失效,必须重新烘焙。所以 Lightmass 最适合室内建筑、地形、固定道具这类永远不动的物体,搭配动态物体使用实时阴影来补充。

Lightmass 让静态场景拥有了电影级的细腻光影,但实时动态世界呼唤着更灵活的方案。灯光师不再满足于“提前拍好剧照”,他想要一套能随时即兴发挥的灯光系统——这就是 Lumen。

B(x)=∫ΩLi(x,ωi) ρ(x)π cos⁡θi dωi\displaystyle B(\mathbf{x}) = \int_{\Omega} L_i(\mathbf{x}, \omega_i) \, \frac{\rho(\mathbf{x})}{\pi} \, \cos\theta_i \, d\omega_i
就像演出前灯光师把所有灯位拍成一张“标准剧照”,演出时按照片放光——又快又美,但演员临时改走位就得重拍。
  1. 入射光照L_i(ω_i) 表示从方向 ω_i 射到表面点 x 的光,烘焙时由离线算法统计多次弹射后的结果。
    Li(x,ωi)L_i(\mathbf{x}, \omega_i)
  2. 漫反射率ρ(x) 是表面的漫反射颜色,除以 π 是朗伯漫反射的归一化因子,表示光被均匀散开。
    ρ(x)π\frac{\rho(\mathbf{x})}{\pi}
  3. 余弦权重入射方向与法线的夹角 θ_i 越小,光照贡献越大,符合“正面受光最亮”的直觉。
    cos⁡θi\cos\theta_i
  4. 辐照度累加对半球所有方向积分,得到该点最终烘焙的辐照度 B(x),写进光照贴图。
    B(x)=∫ΩLi ρπ cos⁡θi dωiB(\mathbf{x}) = \int_{\Omega} L_i \, \frac{\rho}{\pi} \, \cos\theta_i \, d\omega_i

Lumen实时全局光照

想象一支真正的“智能追光”团队:演员走到哪里,灯光师立刻调整光位,墙壁反射的颜色变化几乎同步呈现,不需要任何预先排练。Unreal Engine 5 的 Lumen 就是这样的实时全局光照系统。它没有使用传统硬件光线追踪那种昂贵的暴力算法,而是采用一套混合方案:对近距离和可见区域,用屏幕空间追踪复用上一帧的画面信息来估算间接光;对屏幕外或需要稳定信息的地方,用有符号距离场(SDF)做软件光线追踪。

屏幕空间追踪的好处是便宜且贴合当前画面,缺点是信息可能不在屏幕内;SDF 则像一个简化的“乐高模型”,把场景几何编码成距离场,光线在其中步进时非常快。Lumen 把两者结合,加上时域降噪与空间滤波,在每帧几毫秒的预算内逼近渲染方程的效果。最终像素的光可以拆成直接光、屏幕空间间接光、网格体间接光三部分之和——这既尊重物理,也照顾了性能。

Lumen 让“动态全局光照”从实验室走进了游戏和虚拟制片。这不是没有代价的——它需要不少 GPU 算力,于是灯光师手里的最后一项绝活,就是在预算内做取舍。

Lpixel≈Ldirect+Lscreen GI+Lmesh GI\displaystyle L_{\text{pixel}} \approx L_{\text{direct}} + L_{\text{screen GI}} + L_{\text{mesh GI}}
灯光师终于能在演出进行中“即兴调光”了:灯一动、人一走,墙壁和地面的颜色反弹马上跟上,让舞台仿佛活了过来。
  1. 直接光部分光源直接照射到表面的光,仍然用传统实时阴影计算。
    LdirectL_{\text{direct}}
  2. 屏幕空间间接光从当前屏幕画面里采样颜色和深度,模拟可见区域内的光线反弹。
    Lscreen GIL_{\text{screen GI}}
  3. 网格体间接光借助 SDF 距离场追踪屏幕外物体的反弹光,补足屏幕空间看不见的部分。
    Lmesh GIL_{\text{mesh GI}}
  4. 精细度权衡两部分间接光都经过降噪和近似,用可控误差换取实时性能。
    Lpixel≈Ldirect+Lscreen GI+Lmesh GIL_{\text{pixel}} \approx L_{\text{direct}} + L_{\text{screen GI}} + L_{\text{mesh GI}}

质量与性能平衡

舞台总监给灯光师下了死命令:整场演出帧耗时不能超过 16.6 毫秒,否则画面会卡顿。灯光师翻开账单:级联阴影要钱,PCF 采样要钱,Lumen 的屏幕空间追踪和 SDF 步进也要钱——所有效果都在争夺同一条性能预算。这就是渲染优化的本质:在有限的帧预算内,把质量分配到观众最在意的地方。

常见的分级策略包括:按距离分级,近处开满 4 级 CSM + PCF 软阴影,远处只留 2 级甚至 1 级;按场景动静分级,静态物体吃 Lightmass 烘焙的免费 GI,动态角色才用 Lumen 的实时反弹;按画质档位分级,主机高端档开全效果,低端档关掉 SDF 只留屏幕空间间接光。这些决策最后都归结为一个简单的公式:所有效果的时间加起来,必须小于一帧的预算。

好的灯光师不是把所有灯开到最亮,而是在这一刻让聚光灯聚焦主角,下一刻让环境光铺满全台。阴影映射与全局光照的终极智慧,就是用每一毫秒换观众眼睛里最真实的感动。

Tframe=Tshadow+TGI+Trender≤1000FPS ms\displaystyle T_{\text{frame}} = T_{\text{shadow}} + T_{\text{GI}} + T_{\text{render}} \le \frac{1000}{\text{FPS}} \ \text{ms}
灯光师学会了看“时间账本”:级联多加一层,SDF 步数就少一点;软阴影更柔和,远处 GI 就可以关小一档——一切都在 16.6 毫秒这根钢丝上跳舞。
  1. 阴影开销T_shadow 包括所有阴影贴图渲染和采样,级联数与分辨率直接决定它的规模。
    Tshadow=∑级联(贴图渲染+采样)T_{\text{shadow}} = \sum_{\text{级联}} (\text{贴图渲染} + \text{采样})
  2. GI 开销T_GI 包括 Lumen 的屏幕空间追踪、SDF 步进与降噪,Lightmass 烘焙的 GI 在运行时几乎为零。
    TGI=Tscreen+TSDF+T降噪T_{\text{GI}} = T_{\text{screen}} + T_{\text{SDF}} + T_{\text{降噪}}
  3. 渲染总开销T_render 是几何、着色、后处理等其他阶段,三者之和不能超过帧预算。
    Tframe=Tshadow+TGI+TrenderT_{\text{frame}} = T_{\text{shadow}} + T_{\text{GI}} + T_{\text{render}}
  4. 帧预算上限60 FPS 对应 16.6 毫秒,30 FPS 对应 33.3 毫秒,目标帧率越低预算越宽松。
    1000FPS ms\frac{1000}{\text{FPS}} \ \text{ms}

AI 应用场景

游戏引擎实时渲染管线

阶段:游戏引擎图形渲染管线打磨

解决的问题:传统单张阴影贴图在大场景中近处锯齿、远处闪烁,且缺少间接光照会让画面显得“假”。CSM 级联阴影与 Lumen 实时 GI 的配合,让动态天气、昼夜循环得以实现。

具体案例:某开放世界项目里,太阳作为方向光使用 4 级 CSM,近 50 米用 2048 分辨率贴图,远 500 米降为 512;室内场景启用 Lumen,让墙面颜色反弹到角色皮肤上形成自然的环境色。

AI合成数据生成

阶段:训练数据生成与仿真环境构建

解决的问题:自动驾驶与机器人需要海量带标注图像,真实采集成本高、极端天气难以覆盖。若合成场景的光照不真实,模型学到的特征无法泛化到真实世界。

具体案例:使用 Unreal Engine 配合 Lumen 实时 GI 生成虚拟街道数据集,程序化控制太阳高度角,生成黄昏长阴影、阴天软阴影、雨天路面反射等多样光照样本,用于训练语义分割模型。

虚拟制片与数字人

阶段:数字人渲染与虚实融合合成

解决的问题:虚拟角色必须与实拍场景的光照方向、阴影虚实完全一致,否则观众立刻会感到“违和”。实时 GI 让导演在现场就能看到最终光影效果。

具体案例:虚拟摄影棚中,演员站在 LED 幕墙前,Lumen 实时计算虚拟场景在地板和角色身上的颜色反弹,同时用阴影映射保证角色与虚拟道具之间接触阴影真实,导演监视器上即刻呈现合成画面。

Python 演示

python57 行
import mathimport numpy as np  def make_depth_map(obs_cx, obs_cy, obs_r, lx, ly, num_angles=720):    """从光源视角生成深度图: 记录每个方向上最近阻挡物的距离"""    depth = np.full(num_angles, np.inf)    for i in range(num_angles):        theta = 2 * math.pi * i / num_angles        dx, dy = math.cos(theta), math.sin(theta)        ox, oy = obs_cx - lx, obs_cy - ly        b = dx * ox + dy * oy        c = ox * ox + oy * oy - obs_r * obs_r        disc = b * b - c        if disc >= 0:            t = -b - math.sqrt(disc)            if t > 0:                depth[i] = t    return depth  def hard_shadow(points, light_pos, depth, num_angles=720):    """硬阴影: 深度比较, 距离大于深度图记录则计入阴影"""    lx, ly = light_pos    px = np.array([p[0] for p in points])    py = np.array([p[1] for p in points])    dist = np.hypot(px - lx, py - ly)    idx = (np.arctan2(py - ly, px - lx) / (2 * math.pi) * num_angles).astype(int)    idx = np.mod(idx, num_angles)    return dist > depth[idx] + 1e-6  def pcf_shadow(points, light_pos, depth, kernel=9, num_angles=720):    """PCF软阴影: 对深度图邻域取多次比较再平均"""    lx, ly = light_pos    px = np.array([p[0] for p in points])    py = np.array([p[1] for p in points])    dist = np.hypot(px - lx, py - ly)    idx = (np.arctan2(py - ly, px - lx) / (2 * math.pi) * num_angles).astype(int)    idx = np.mod(idx, num_angles)    half = kernel // 2    shadow = np.zeros(len(points))    for k in range(-half, half + 1):        d = np.take(depth, np.mod(idx + k, num_angles))        shadow += (dist > d + 1e-6).astype(float)    return shadow / kernel  if __name__ == "__main__":    light = (0.0, 8.0)    depth = make_depth_map(0.0, 0.0, 3.0, *light)    xs = np.linspace(-10, 10, 21)    pts = [(x, 0.0) for x in xs]    hard = hard_shadow(pts, light, depth)    soft = pcf_shadow(pts, light, depth, kernel=11)    for x, h, s in zip(xs, hard, soft):        print(f"x={x:6.2f}  hard={int(h)}  soft={s:.2f}")
目的:抽象出阴影映射最核心的“深度图 + 深度比较”逻辑,并展示 PCF 如何把 0/1 的硬阴影变成 0 到 1 之间的渐变软阴影。
  1. make_depth_map(obs_cx, obs_cy, obs_r, lx, ly, num_angles=720)从光源向 720 个方向发射射线,记录遇到的最近圆障碍物距离,构成深度图。
  2. disc = b * b - c判断光线与圆是否相交的二次方程判别式,大于零则说明有交点。
  3. t = -b - math.sqrt(disc)取最近的交点距离,对应“光源视角下最近的阻挡距离”。
  4. idx = (np.arctan2(py - ly, px - lx) / (2 * math.pi) * num_angles).astype(int)把地面点相对光源的角度映射到深度图的采样索引。
  5. hard_shadow对每个地面点做深度比较:距离大于深度图记录则标记为阴影,得到 0/1 硬阴影。
  6. pcf_shadow在深度图的相邻索引上多次比较并取平均,让阴影边缘出现渐变,得到软阴影。
  7. np.take(depth, np.mod(idx + k, num_angles))安全地读取邻域深度采样,越界时循环回绕。

常见误区

误区我看到物体被挡住,是因为相机方向上有遮挡,所以阴影贴图应该从相机视角渲染深度图。

正解阴影映射必须站在光源视角渲染深度图。相机视角只知道“谁在我前面”,不知道“谁挡住了光”。灯光师得先站到灯的位置拍一张远近照片,再回到观众席比对:把光源当相机,记录的是“光的视线”而非“人的视线”。

为何常见:人眼习惯以自己为中心判断遮挡,忽略了光有自己的传播方向。

误区软阴影就是把阴影贴图做高斯模糊再采样,边缘就柔和了。

正解PCF 模糊的是“深度比较后的结果”。它对深度图邻域多次采样、每次独立做深度比较,最后对比较结果取平均。直接模糊深度图会把深度值搅成中间值,既得不到正确遮挡,还会产生漏光和怪异阴影。

为何常见:看到“滤波”两个字就联想到图像模糊,忽略了 PCF 发生在深度比较环节,而不是贴图预处理环节。

误区CSM 级联越多,每层覆盖越精细,画质一定更好。

正解级联数增加会成倍提高阴影贴图渲染和采样开销,而且每级都要重新渲染场景。质量好坏更取决于级联距离划分是否合理、每级分辨率是否匹配可见需求。3 级优化得当的效果往往好于 5 级粗糙配置。

为何常见:把“数量”直接等同于“质量”,忽视了性能预算和覆盖范围的权衡。

误区要做出全局光照效果,就必须用光线追踪技术。

正解全局光照是效果目标——模拟光的多次弹射带来的间接照明;光线追踪只是实现手段之一。Lightmass 用离线预计算加光照贴图,Lumen 用屏幕空间与 SDF 软件光追,它们都不是传统意义上的暴力路径追踪。

为何常见:厂商营销常把“光追”与“全局光照”绑定宣传,让概念层级被混淆。

误区有了 Lumen 实时 GI,光照烘焙技术就该被淘汰了。

正解Lumen 在动态场景中表现出色,但它的实时近似在多次弹射细节、间接阴影锐度上仍不如离线烘焙;且运行时 GPU 开销更高。静态场景、移动端或低端硬件上,Lightmass 烘焙依然是更优选择。

为何常见:只看到新技术的光环,忽略了“质量-性能-动态性”三者的不可能三角。

面试题

Q1. 阴影映射的第一步是什么?

单选题 · easy

  • A.从光源视角渲染深度图← 正确答案
  • B.从相机视角渲染深度图
  • C.对屏幕画面做颜色模糊
  • D.计算场景包围盒大小

答案:A。阴影映射的核心是“站在光源处拍远近照片”,所以第一步必须把光源当作相机,渲染一张深度图。B 是相机视角,不知道光线是否被挡;C 和 D 与生成阴影没有直接关系。

Q2. 全局光照的核心是模拟什么?

单选题 · easy

  • A.直射阳光的亮度
  • B.光线的多次弹射← 正确答案
  • C.阴影边缘的柔化
  • D.屏幕空间泛光效果

答案:B。全局光照的物理本质是光在场景表面之间反复弹射,形成间接照明。A 是直接光照,C 是软阴影技术,D 是后处理效果,它们都不是全局光照的核心。

Q3. 阴影粉刺(自遮挡)的主要成因是什么?

单选题 · mid

  • A.阴影贴图分辨率过低
  • B.深度比较存在数值误差← 正确答案
  • C.光源颜色设置不正确
  • D.相机远平面距离过近

答案:B。深度图用有限精度存储深度,表面在比较时自己的实际深度会略微大于贴图深度,导致表面被误判为阴影。A 会造成锯齿而非粉刺;C 和 D 与深度比较的误差无关。

Q4. CSM 级联阴影主要解决什么问题?

单选题 · mid

  • A.让阴影整体变得柔和
  • B.给阴影添加环境颜色
  • C.保持远近场景画质平衡← 正确答案
  • D.降低场景总体光源数

答案:C。CSM 把视锥按距离分成多级,近处用高分辨率阴影贴图、远处用低分辨率,从而在巨大场景中同时保证近景清晰和远景稳定。A 属于 PCF 的职责,B、D 与级联无关。

Q5. 深度偏移(Depth Bias)主要用于解决什么问题?

单选题 · mid

  • A.阴影粉刺← 正确答案
  • B.阴影锯齿
  • C.阴影过软
  • D.阴影闪烁

答案:A。深度偏移在比较时给实际深度加一个容差,让表面自身不会因为数值误差被判成阴影,从而消除粉刺。偏移过大会导致彼得潘效应,即影子和物体脱离开。

Q6. Lightmass 预计算光照的特点有哪些?

多选题 · mid

  • A.适合静态场景← 正确答案
  • B.烘焙光照质量高← 正确答案
  • C.支持完全动态的光源
  • D.修改灯位后需重新烘焙← 正确答案

答案:ABD。Lightmass 是离线预计算方案,适合静态场景,光线可以弹射很多次因此质量高;代价是场景或光源变化后必须重新烘焙。它不是为动态光源设计的实时方案。

Q7. PCF 滤波真正模糊的是什么?

单选题 · easy

  • A.深度图中的各个像素
  • B.深度比较后的结果← 正确答案
  • C.场景的颜色缓冲
  • D.光源的强度贴图

答案:B。PCF 对深度图邻域多次采样,每次做深度比较,最后对 0/1 比较结果取平均产生渐变。直接模糊深度图会破坏深度值本身,导致错误的遮挡判断。

Q8. Unreal 的 Lumen 主要靠什么技术实现实时全局光照?

单选题 · hard

  • A.纯离线光线追踪
  • B.屏幕空间与SDF混合← 正确答案
  • C.环境光遮蔽近似
  • D.静态贴图预烘焙

答案:B。Lumen 的核心是混合方案:用屏幕空间追踪获取近距离的间接光,用有符号距离场(SDF)做软件光线追踪补足屏幕外信息,最后经过降噪输出。它既不是纯离线,也不是硬件暴力光追。

Q9. 关于软阴影的形成,下列说法正确的是?

多选题 · mid

  • A.光源本身具有面积← 正确答案
  • B.存在半影过渡区域← 正确答案
  • C.阴影贴图发生了模糊
  • D.物体表面材质半透明

答案:AB。真实光源有面积,物体边缘会形成从本影到半影的连续过渡,这是软阴影的物理成因。C 是 PCF 的实现手段而非成因,D 涉及半透明材质,与软阴影无关。

Q10. 帧预算紧张时,下列哪些做法是合理的?

多选题 · hard

  • A.减少级联阴影的层数← 正确答案
  • B.降低阴影贴图分辨率← 正确答案
  • C.关闭远距离的全局光照← 正确答案
  • D.无限提高光追采样数量

答案:ABC。减少级联、降低分辨率、关闭远处 GI 都是常见的质量分级(LOD)策略,能在几乎不影响观感的前提下释放性能。D 会反向拉高 GPU 开销,是错误做法。