Unity大气散射Shader实战:从零实现体绘制天空与雾效

发布时间:2026/10/17 16:38:31
Unity大气散射Shader实战:从零实现体绘制天空与雾效 1. 从零理解大气散射为什么你的天空看起来像塑料布很多做Unity项目的人都有过这种体验场景搭好了地形刷完了模型也摆得七七八八但抬头一看天空总觉得哪里不对。要么是一片死板的纯色要么是渐变生硬得像贴了张壁纸完全没有真实世界那种“空气感”。这个问题的根源往往不在美术资源上而在于缺少一套靠谱的大气散射渲染方案。大气散射说白了就是光线穿过空气时被空气中的分子和悬浮颗粒“撞偏”了方向一部分光改变了传播路径进入我们的眼睛。白天天空是蓝的傍晚是红的远处山峦发灰发白这些都是散射在起作用。在Unity里想还原这种效果常规做法有几种用天空盒贴图、用简单的梯度Shader、或者用基于物理的体绘制方案。前两种省事但效果有限视角一变就露馅体绘制方案计算量大一些但能做出真正随视角、光照、高度动态变化的天空和雾效。这篇内容要聊的就是怎么在Unity里用体绘制的方式实现一套大气散射Shader。核心思路是把大气层当成一个参与介质沿着视线方向做数值积分累积每个采样点上的散射贡献。适合有一定Shader基础、想让场景氛围上一个台阶的开发者也适合对图形学感兴趣、想搞明白“天空为什么是蓝的”这类问题的朋友。我会从整体设计思路讲到具体代码实现再到实际调试中踩过的坑尽量把每个参数背后的道理说清楚。2. 方案选型与整体设计为什么选体绘制而不是贴图2.1 三种常见大气方案的对比在动手写代码之前先把可选路线理清楚。Unity里做大气效果大致有三条路方案原理优点缺点适用场景天空盒贴图预烘焙或手绘全景图性能极低开销效果可控静态不随光照变化视角固定风格化、低配项目屏幕空间梯度根据屏幕坐标做颜色插值实现简单开销小无物理依据视角转动穿帮快速原型、休闲游戏体绘制散射沿视线积分散射方程物理正确动态响应光照和视角计算量较大参数调试复杂写实场景、开放世界我一开始也用过梯度方案图省事嘛几行代码就能出个渐变天空。但问题很快就来了摄像机一旋转地平线附近的颜色就跟着屏幕跑完全不对太阳位置一变整个天空的色调也没法自然过渡。后来换成体绘制虽然调试花了不少时间但一旦参数调对那种“空气真的存在”的感觉是贴图方案给不了的。2.2 体绘制的核心思路拆解体绘制的本质是把大气层看作一个充满粒子的体积光线从摄像机出发穿过这个体积到达远处沿途每个点都会发生散射。我们要算的就是这条视线上所有散射光累积到摄像机的结果。具体拆成两步第一步是光线在介质中的透射率也就是光走了一段距离后还剩多少没被散射掉第二步是内散射也就是沿途各点散射进视线的光有多少。这两步都依赖两个关键参数散射系数和相位函数。散射系数决定了介质对光的“阻挡”能力瑞利散射和米氏散射的系数不一样前者负责天空的蓝色后者负责太阳周围的光晕和雾的灰白。相位函数则描述光被散射后往哪个方向偏得多瑞利散射接近各向同性米氏散射强烈前向偏置这就是为什么太阳周围特别亮。在Unity里实现通常是在一个全屏的Shader里做射线-球体求交算出视线穿过大气层的入射点和出射点然后在这段区间里做步进采样每个采样点算一次散射贡献最后累加。步数越多越精确但性能也越吃紧所以实际项目里要在质量和开销之间找平衡。2.3 为什么选择在片元着色器里做积分有人可能会问能不能在顶点着色器里算答案是不太行。大气散射的积分是沿视线方向的每个像素的视线方向都不一样顶点着色器拿不到逐像素的射线方向。而且散射方程是非线性的没法用简单的顶点插值近似。所以必须放到片元着色器里逐像素做数值积分。这也意味着性能开销和屏幕分辨率直接挂钩。1080p下每个像素都要跑几十次采样累积起来不是小数目。后面我会讲怎么通过降分辨率渲染、提前退出、动态步数等技巧把开销压下来。3. 核心细节解析散射方程到底在算什么3.1 瑞利散射与米氏散射的分工大气散射里最重要的两个角色就是瑞利散射和米氏散射。它们的分工可以用一句话概括瑞利管颜色米氏管光晕。瑞利散射是光被空气分子主要是氮气和氧气散射散射强度与波长的四次方成反比。这就是为什么蓝光被散射得最厉害天空呈现蓝色。瑞利散射的相位函数接近对称前后散射差不多。米氏散射是光被较大的悬浮颗粒尘埃、水汽、气溶胶散射对波长不敏感所以雾看起来是灰白色的。它的相位函数强烈前向偏置这就是为什么太阳周围会有一圈明亮的光晕而背对太阳的方向就暗得多。在Shader里这两个散射通常分别计算再叠加。瑞利系数一般取一个较小的值米氏系数稍大再配合各自的高度衰减参数模拟大气密度随高度下降的规律。3.2 透射率计算Beer-Lambert定律的实战应用透射率描述的是光穿过一段介质后剩下的比例遵循Beer-Lambert定律透射率等于散射系数的负指数。写成公式就是T exp(-σ * d)其中σ是散射系数d是距离。在体绘制里透射率是逐段累积的。每走一小步就用这一步的散射系数乘以步长累加到光学深度里最后取负指数得到透射率。这里有个细节散射系数是随高度变化的越往高处空气越稀薄散射越弱。所以每一步都要根据当前采样点的高度重新算散射系数不能用一个常数糊弄过去。我一开始就是偷懒用了常数系数结果地平线附近和天顶的颜色过渡特别假后来改成按高度指数衰减立刻就自然多了。高度衰减的公式一般是exp(-height / scaleHeight)scaleHeight是标高瑞利和米氏各取不同的值。3.3 相位函数决定光晕形状的关键相位函数描述的是散射光在不同方向上的强度分布。瑞利散射的相位函数比较简单近似为(1 cos²θ)的形式θ是散射角。米氏散射用Henyey-Greenstein近似公式里有个各向异性参数gg越大前向散射越强。这个g值很关键。g取0就是各向同性取0.9以上就是强烈前向散射。实际大气里米氏散射的g大概在0.76到0.85之间我一般从0.8开始调。g值调小了太阳光晕会散得太开整个天空发白调大了光晕又太集中像个手电筒。这个参数需要结合场景的光照强度一起调没有万能值。3.4 射线与大气层求交别小看这一步体绘制的积分区间是视线穿过大气层的那一段所以要先算出射线和大气层外球的交点。这一步看着简单但有几个坑。第一个坑是摄像机在大气层内部的情况。如果摄像机在地面上射线起点就在球内那入射点就是摄像机位置本身只需要算出射点。如果摄像机在大气层外比如太空场景那就要算两个交点。代码里要分支处理不能一刀切。第二个坑是射线和球不相交的情况。比如摄像机朝下看地面射线可能根本不穿过大气层外球这时候要直接返回背景色不能硬算。判断方法是看判别式是否小于零。第三个坑是数值精度。当射线几乎和球面相切时判别式接近零开根号容易出问题。我一般会加一个极小值保护避免出现NaN。4. 实操过程一步步写出可运行的大气散射Shader4.1 场景准备与渲染管线选择先说环境。我用的是Unity的URP管线内置管线也能做但URP的Shader写法更清晰而且后续做后处理更方便。创建一个新的URP项目场景里放一个摄像机方向朝天空再放一个平行光当太阳。渲染方式上我选择用一个全屏的Blit Pass把大气散射的结果渲染到一张RT上再和场景颜色混合。这样做的好处是大气效果独立于场景几何不会因为场景里有什么东西而改变。具体做法是写一个ScriptableRendererFeature在渲染完不透明物体之后、透明物体之前插入这个Pass。如果你不想搞RendererFeature也可以用一个挂在摄像机上的脚本在OnRenderImage里做Blit。内置管线用后者更方便URP的话还是走RendererFeature正规一些。4.2 Shader骨架搭建与参数定义Shader的骨架大致是这样Properties里定义瑞利系数、米氏系数、标高、各向异性参数、太阳强度等顶点着色器只做最基本的坐标变换片元着色器里做射线求交和积分。参数定义这块我建议把瑞利和米氏分开成两组每组三个分量对应RGB三个通道。瑞利系数大概在(5.8, 13.5, 33.1)这个量级单位是每米的倒数乘以一个缩放因子。米氏系数各通道相同大概在21左右。这些数值来自大气物理的参考数据实际用的时候要乘以一个统一的强度系数来适配场景的亮度。标高方面瑞利用8000米米氏用1200米。这两个值决定了散射随高度衰减的快慢瑞利衰减慢所以高空还有蓝色米氏衰减快所以雾一般贴地。4.3 射线求交与积分区间确定射线求交的代码逻辑是这样的先算射线起点到球心的距离然后解二次方程。判别式小于零直接返回背景色。判别式大于等于零算出两个根取合适的那个作为出射点。如果起点在球内入射点就是起点如果在球外入射点是较小的那个根。这里要注意坐标系的统一。我一般把大气层球心放在世界原点半径设为大气层顶部高度加上地球半径。地球半径取6371千米大气层顶部取6421千米也就是100千米厚的大气层。这些数值在Shader里用宏定义好方便调整。积分区间确定后用步长把它分成若干段。步长可以固定也可以根据距离动态调整。我一般用固定步数比如16步或32步然后步长等于区间长度除以步数。步数越多越平滑但开销也越大。4.4 逐点采样与散射累积积分循环里每个采样点要做这几件事算当前高度、算该高度的散射系数、算该点到太阳的光学深度、算透射率、算相位函数、累加内散射。算太阳方向的光学深度时又要做一次射线求交和积分因为光从太阳到采样点也要穿过大气层。这一步是性能大头因为每个采样点都要算一次。优化方法是用一个预计算的查找表把光学深度按高度和太阳角度存起来运行时直接查表。我一开始没做查找表32步采样下帧率直接掉到30以下加了查找表之后回到60以上。内散射的累积公式是每步的散射贡献乘以透射率再乘以步长累加到总散射里。透射率是逐步累积的每步都要更新。这里有个细节透射率要用当前步之前累积的光学深度来算不能提前算好否则顺序就错了。4.5 与场景颜色的混合大气散射算完之后要和场景本身的颜色混合。混合方式取决于你的需求如果只是做天空那直接输出散射结果就行如果要做全屏雾效那就要把散射结果和场景颜色按透射率混合。混合公式是最终颜色 场景颜色 * 透射率 散射颜色。透射率是视线穿过整个大气层的透射率散射颜色是累积的内散射。这样远处的物体会被大气“罩”上一层颜色近处的物体受影响小符合真实物理。混合的时候要注意颜色空间。如果项目用的是线性空间散射计算也要在线性空间做最后再转到伽马空间输出。我一开始忘了这一步结果颜色偏得厉害调了半天参数才发现是颜色空间的问题。5. 性能优化与常见问题排查5.1 降分辨率渲染与上采样体绘制的开销和像素数成正比所以最直接的优化就是降分辨率。把大气散射渲染到半分辨率甚至四分之一分辨率的RT上再上采样回全分辨率。因为大气散射本身是低频信号降分辨率带来的画质损失很小但性能提升很明显。具体做法是在RendererFeature里创建一个低分辨率的RT把Blit的目标设成它然后在后续的Pass里用双线性采样上采样。注意上采样的时候要用全屏的UV不要用RT的UV否则会错位。5.2 提前退出与动态步数不是所有像素都需要跑满步数。比如射线和大气层相交区间很短的像素可以少跑几步射线方向朝下的像素可以直接跳过。我在Shader里加了几个提前退出的条件区间长度小于阈值直接返回透射率小于阈值提前结束循环太阳在地平线以下时降低步数。动态步数是另一个思路根据摄像机高度和视线角度调整步数。高空俯视时步数可以少一些低空平视时步数多一些。这样能在不同场景下保持相对稳定的帧率。5.3 常见问题速查表问题现象可能原因排查方向解决方法天空全黑射线求交失败或积分区间为零检查判别式和入射出射点加保护值确认摄像机在大气层内颜色偏紫或偏绿瑞利系数通道顺序错了检查RGB对应关系确认系数按波长从长到短排列太阳周围有硬边相位函数参数不当检查g值和归一化调整g到0.76-0.85确认相位函数归一化地平线附近有接缝步长太大或采样不足增加步数或减小步长步数提到32以上或用抖动采样帧率骤降光学深度重复计算检查是否有查找表加预计算查找表降分辨率渲染远处物体没有雾效混合公式或透射率错误检查混合顺序和透射率计算确认先算散射再混合透射率用全程累积值5.4 实操心得与避坑经验第一个心得是参数不要一次调太多。大气散射的参数之间耦合很强改一个另一个就跟着变。我的做法是先固定瑞利系数和标高把米氏关掉调出纯瑞利的蓝天然后再开米氏调光晕最后调太阳强度和曝光。一步步来比一锅乱炖高效得多。第二个心得是用参考图对照。找几张真实天空的照片把摄像机角度和太阳位置对齐然后对着调。纯靠感觉调很容易越调越偏。我一般会截屏和参考图并排看色相、饱和度、明度三个维度分别对比。第三个心得是注意场景的整体曝光。大气散射算出来的颜色是物理量但最终显示要经过色调映射。如果场景的曝光设置和散射强度不匹配要么天空过曝一片白要么欠曝一片黑。我一般把散射强度设成让天空亮度在0.7到0.9之间再让色调映射去处理高光。第四个心得是移动端要格外小心。移动端GPU的浮点精度和带宽都有限全分辨率体绘制基本跑不动。我的做法是移动端降到四分之一分辨率步数砍到8步关掉米氏散射只留瑞利这样还能勉强跑。如果还不行就退回梯度方案虽然假但至少能跑。6. 进阶扩展让大气效果更上一层楼6.1 加入高度雾与体积光大气散射做完之后可以进一步和高度雾结合。高度雾是近地面的一层雾密度随高度指数衰减和大气散射的米氏部分有重叠但侧重点不同。高度雾更贴近地面影响范围小但浓度高大气散射覆盖整个大气层影响范围大但浓度低。两者叠加能让近景和远景的过渡更自然。体积光则是另一个方向。当太阳被物体遮挡时光线从缝隙中透出来形成光柱这就是体积光。实现方式是在大气散射的积分循环里额外判断每个采样点到太阳的路径上有没有遮挡有遮挡就减少该点的散射贡献。这样光柱会自然地出现在物体边缘。6.2 动态时间与天气系统大气散射的参数可以和时间、天气绑定做出动态变化。比如把太阳角度和一天的时间关联早上和傍晚太阳角度低光线穿过的大气层更厚瑞利散射更强天空更红中午太阳角度高光线穿过的大气层薄天空更蓝。这个用太阳方向向量的Y分量就能驱动。天气方面米氏散射的系数可以关联雾霾程度系数越大雾越重。下雨天还可以额外加一层雨雾的散射让画面更有氛围。这些参数用脚本在运行时动态调整就能做出完整的昼夜和天气循环。6.3 与后处理栈的整合大气散射的结果可以和后处理栈里的其他效果整合比如 bloom、色调映射、颜色分级。顺序很重要先做大气散射再做 bloom最后做色调映射和颜色分级。这样太阳周围的光晕会被 bloom 增强整体色调也能统一调整。如果用的是URP的后处理栈可以把大气散射做成一个自定义的 RendererFeature插在 bloom 之前。注意 RT 的格式要选对HDR 的话用 RGBA16FLDR 用 RGBA8。格式选错了要么丢精度要么浪费带宽。6.4 从体绘制到预计算性能敏感场景的取舍如果项目对性能极其敏感比如 VR 或者移动端体绘制的开销可能还是太高。这时候可以考虑预计算方案把大气散射的结果按视角和太阳角度预计算成一张或多张查找表运行时直接采样。查找表的维度一般是视角方向θ, φ加太阳方向θ, φ四维查找表数据量不小但可以降维或者用球谐函数压缩。预计算的缺点是动态性差太阳角度变了要重新算或者插值。但对于太阳角度固定的场景比如某些关卡设计预计算是性价比很高的选择。我一般会先做体绘制版本确认效果满意后再根据目标平台决定要不要转预计算。7. 我个人在实际操作中的几点体会这套大气散射Shader我从头到尾写了两三遍第一遍是照着论文硬啃公式推了半天代码写出来一堆bug第二遍是简化了模型只保留瑞利和米氏把查找表加上性能好了很多第三遍是整合到项目里调参数、做适配、处理各种边界情况。整个过程下来最大的体会是物理公式是起点不是终点。论文里的公式是理想情况实际项目里要考虑性能、精度、美术需求该简化就简化该近似就近似。另一个体会是调试工具很重要。我专门写了一个调试模式可以把瑞利、米氏、透射率、相位函数分别可视化出来哪个环节出问题一眼就能看到。没有这个工具的时候只能靠猜效率极低。建议做类似效果的时候先把调试可视化做好后面调参会轻松很多。最后分享一个小技巧如果天空颜色怎么调都不对先把太阳强度设成0只看环境光下的散射。这时候天空应该是均匀的蓝色如果偏色或者有奇怪的条纹那就是散射系数或者积分步数的问题。等纯散射调对了再开太阳调光晕和相位函数。这个顺序能帮你快速定位问题出在哪个环节。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询