跳到主要内容
腾讯题库 · Q16

原题 Q16 · 当前收录 15 / 49

学习状态 未开始

动态天气体积光

Shader weather · volume 对应课程:实时特效与质量分级

面试官问

PDF 第 36 页

如何在 Shader 中实现“动态天气系统”的云层体积光效果?需使用哪些纹理与计算方法?(冷门题,理清逻辑)

学习进度

0/9 已勾

只保存在这台浏览器;勾满 9 项即视为本题完成。

先把题目说成人话

你在大雾里打开手电筒,灯口附近很亮,远处却越来越暗;从侧面看,还能看见一条发亮的雾柱。云也是同一类问题:阳光进入云以后,一部分继续穿过,一部分离开原方向,还有一部分被送进我们的视线。把这种“光在有厚度的雾、烟、云里一边损失、一边被看见”的现象叫作体积光(Volumetric Lighting),把穿过一段介质后仍然活着的光所占比例叫作透射率(Transmittance)。

这道题不是先背天气图和采样代码,而是先把一束光怎样穿过一小段雾看明白。等这笔能量账成立以后,再回答游戏如何制造一整片会生长、会移动的云,以及怎样让相机沿途重复这笔账。

这是虚构教学故事,不是个人项目经历。

场景:先让问题变得看得见

先只看一束手电筒穿过均匀的雾。把光路切成许多很短的小段:每进入一段,先问这里的雾有多浓,再问这段路有多长。浓雾或长路都会让更多光离开原来的方向。相机不是一次看穿整团雾,而是从近到远重复这件事。

(右脑)画面:想象光背着一桶能量走进雾里。它每走一小步,雾都会舀走一点;桶里剩下的光决定后面还能走多亮。与此同时,太阳照亮这一小团雾,其中有一点光恰好被拨向相机,于是眼前多出一点亮度。相机继续向前,反复执行“检查雾有多浓、扣掉离开的光、收下朝我来的光”。走完整条路以后,桶里还剩多少决定背景能透出多少,沿途收集了多少决定云本身有多亮。

(左脑)对表:先写不用符号的能量账:这一步之后还剩的光,来自之前还剩下的光,再乘上这一小段路的存活比例。 接着才给画面中的三个对象命名:这一小步在世界里的长度是 Δs\Delta s,单位是米;采样点的云有多浓是 ρ\rho,这里约定为零到一的无量纲比例;每米介质让光离开原方向的强弱是 σt\sigma_t,单位是 1/m1/\text{m}。三者相乘得到无量纲的光学厚度。因此这一段之后的存活比例是 e−σtρΔse^{-\sigma_t\rho\Delta s},损失比例才是 1−e−σtρΔs1-e^{-\sigma_t\rho\Delta s},不能把光学厚度本身直接叫作“损失百分比”。若项目让 ρ\rho 带真实密度单位,σt\sigma_t 的单位也必须随之改变,最终指数仍须无量纲。

到这里才问“每个位置的 ρ\rho 从哪里来”。从天空上方看去,用一张低频分布图决定大片区域哪里有云、属于哪类天气,这张图叫天气总图(Weather Map);再用高度曲线决定云底和云顶,用三维纹理里的大块起伏与小块破碎刻出主体和边缘,它们叫基础噪声与细节噪声(Base Noise / Detail Noise)。相机沿视线逐段执行前面的能量账,叫视线步进(Ray March);在有云的点向太阳再检查一小段遮挡,叫光照步进(Light March)。这些名字现在只是已经看见的动作标签,不再是四套同时降临的新概念。

小明:先形成一个完整猜测

小明先用一张滚动的黑白云图去乘天空颜色。远看确实像云在移动,而且很便宜;但镜头向上穿进云层时,图仍然贴在天顶。太阳转到云后面,内部也没有“前面先吃光、后面更暗”的关系。

这次尝试留下了清楚边界:二维图可以决定远景外形,却没有沿深度变化的密度,也没有逐段更新的透射率。它可以成为低质量远景,不能仅凭会滚动就叫作体积云。

小红:把猜测放回现实检查

小红不先拼完整天空,而是做三个能单独看懂的中间画面。第一张只显示密度:黑色是空,白色是浓云,确认世界中确实存在有内外的云层。第二张只显示视线透射率:相机越深入浓云,数值只能保持或下降,不能无缘无故回升。第三张只显示太阳方向的透射率:被厚云挡住的样本更暗,云边和薄处更亮。

她随后缩短步长重算同一条射线。如果结果逐渐靠近稳定值,说明离散近似正在收敛;如果画面随步长大幅变色,就还不能把当前采样数当答案。当前样本密度为零只代表这一小步没有云,后面仍可能遇到下一团,不能因此终止整条射线。

最后才让晴天向暴雨变化。她改变的是同一片世界空间密度场的覆盖、云型、厚度和降水,而不是把两张最终天空图互相淡化;风只推动采样位置,不能在切换天气时突然重置整片噪声。

我:用检查结果修正模型

我把已经验证过的过程组织成三层:

  • 先回答哪里有云:天气总图给出大范围覆盖与类型,高度曲线夹住云底和云顶,三维噪声只负责把规则形状雕成自然边缘。
  • 再回答光怎样走:相机只在云层交段中前进,每一步先用更新前的透射率收集朝相机来的光,再更新剩余透射率;有密度时才向太阳做简化采样。
  • 最后回答怎样省:降低云缓冲分辨率和采样数以后,再用历史帧补稳定性;显露区、相机运动和风场改变时必须降低历史权重,避免拖影。

质量档优先减少视线样本、太阳样本、云缓冲分辨率和细节噪声。低档可以退回二维云层,但要明确它保留的是天气轮廓,不再计算体积内部传播。所有档位都要用目标构建比较画面、步长收敛和图形处理器时间,不能用“采样少了”代替性能证据。

(右脑)画面修正:原先的“会滚动的云图”现在退到远景布景;真正的云是一段相机可以走进去的雾路。天气总图只圈出哪里可能有云,三维密度决定每一步究竟有多浓,太阳光和视线各自带着一桶剩余能量穿行。晴转雨不是换背景,而是远处先长出覆盖、云底逐渐压低、透光一点点减少;整个变化始终发生在同一个空间里。

(左脑)严格对表:实现顺序不能交换:先用天气总图、高度曲线和世界空间三维噪声得到当前采样点的 ρ\rho;再求相机射线进入、离开云层的距离;随后用统一的 Δs\Delta s 从近到远推进。每一步先保存更新前的 TviewT_{view},向太阳采样得到 TsunT_{sun},用旧的视线透射累计入射散射 ΔL≈TviewσsρLsunTsunp Δs\Delta L\approx T_{view}\sigma_s\rho L_{sun}T_{sun}p\,\Delta s:其中 σs\sigma_s 是散射强度,LsunL_{sun} 是入射太阳辐亮度,pp 是把光分配到当前观察方向的相位函数。然后才更新 Tview←Tviewe−σtρΔsT_{view}\leftarrow T_{view}e^{-\sigma_t\rho\Delta s}。当前密度为零只跳过本步;只有透射率低到阈值,或加速结构证明后方为空,才能提前结束。这里是一阶单次散射近似,没有完整计算光在云内多次改变方向的传播。验收要缩小步长检查收敛,核对所有世界单位、风速的距离/时间单位、天气连续性、历史拖影和各质量档实测耗时。

老师解惑

前面的手电筒画面对应两本账。第一本只记“原方向还剩多少光”:

Tview,next=Tview e−σtρΔsT_{view,next}=T_{view}\,e^{-\sigma_t\rho\Delta s}

第二本记“这一小团云向相机送来了多少光”:

ΔL≈Tview  σsρ  Lsun  Tsun  p(cos⁡θ)  Δs,Lnext=L+ΔL\Delta L\approx T_{view}\;\sigma_s\rho\;L_{sun}\;T_{sun}\;p(\cos\theta)\;\Delta s, \qquad L_{next}=L+\Delta L

其中 σs\sigma_s 表示介质把光散向其他方向的强弱,LsunTsunL_{sun}T_{sun} 表示真正抵达该点的太阳光,pp 决定其中多少朝向相机。上面的亮度增量是有限步长的一阶近似,不是解析积分;必须通过缩小步长观察是否收敛。密度若只是程序化的零到一控制量,系数需要随项目世界尺度标定,不能把一组参数直接搬到另一套单位中。

  • **天气总图(Weather Map)**只管大势;它不直接保存最终云颜色。
  • **视线步进(Ray March)**重复相机这本账,**光照步进(Light March)**估计太阳到样本还剩多少光。
  • **时间重投影(Temporal Reprojection)**复用旧帧来弥补低分辨率和低采样,但旧信息在新显露区域并不可靠。

工具分工

  • Unity 的中间渲染纹理与自定义渲染入口:承载云缓冲和天空合成,具体接口随管线而变。
  • Frame Debugger:确认深度、云缓冲、历史帧和合成的先后关系;它不负责逐阶段精确计时。
  • Unity Profiler 的图形处理器视图:比较视线步数、太阳步数和分辨率改变后的实际耗时。
  • RenderDoc:逐项查看密度、透射率、历史权重和早退位置。

记忆钩子

先看一束光怎样走过一小段雾,再让相机重复这笔账;天气图管哪里有云,密度管光在里面还剩多少。

这是教学场景,不是目标项目的真实捕获或个人经历。

下一步:验证这条因果

在可计算的 1D / 2D 剖面中依次查 density、T_view、散射累计、步数误差与 HDR 范围。

教学边界:它只验证这一条画面因果,不替代完整答案、项目经历或目标设备数据。

进入教学 Lab:「云雾的质量,藏在每一小步里」