资讯详情

Unity Shader程序化火焰:从噪声到动态光照的独立游戏实战

📅 2026/10/3 4:48:50 | 华诺云谱 👁 阅读
Unity Shader程序化火焰:从噪声到动态光照的独立游戏实战
1. 从一团静态贴图到会呼吸的火焰项目背景与核心需求做独立游戏的人大概都有过这种执念明明玩法框架已经跑通了数值也调得七七八八可画面里总有几个地方看着“不对劲”。我在推进《五星地牢》开发日志04这个阶段时卡住的正是火焰效果。地牢题材里火把、篝火、熔岩、燃烧的敌人几乎是标配元素但早期版本里我用的就是几张循环播放的序列帧贴图结果玩家测试时反馈最多的一句话是——“这火看着像贴纸”。这句话点醒了我。静态序列帧的问题不在于画得不好而在于它缺少三个关键特征随机性、动态形变、与环境的交互。真实的火焰每一帧形状都不一样火苗会向上抽动、会左右摇摆、会因为气流产生撕裂感还会把周围的地面和墙壁映出跳动的暖光。这些特征叠加起来才让玩家的大脑认定“这是火”。所以这一阶段的核心需求很明确用尽可能低的性能开销把地牢里的火焰从“贴纸”升级成“会呼吸的光源”。这个需求拆解下来其实是三件事。第一是火焰本体的动态表现需要解决形状随机变化和向上流动的问题第二是光照的联动火焰必须作为一个动态点光源去影响周围场景而且亮度、色温要跟着火焰的节奏抖动第三是性能控制地牢里可能同时存在十几个火源如果每个都用高精度粒子系统帧率会直接崩掉。这三件事的优先级和实现难度并不一样我在动手之前先做了一轮方案选型这也是后面要展开讲的重点。适合参考这篇日志的人大概是正在做2D或2.5D游戏、需要处理动态光照和粒子效果的独立开发者也包括对Shader和渲染管线感兴趣、想找一个具体案例上手的技术美术方向的朋友。我不会假设你精通图形学但会假设你至少能看懂基础的Shader代码结构知道顶点着色器和片元着色器大概在干什么。如果你完全是零基础建议先把Unity或Godot的官方渲染入门过一遍再回来不然中间某些参数调整的“为什么”会看得比较吃力。2. 火焰效果的技术选型为什么放弃序列帧又为什么没上全物理模拟2.1 三种主流方案的横向对比在动手之前我把能想到的方案列了个表从实现难度、性能开销、视觉上限、可控性四个维度做了对比。这个对比不是拍脑袋而是基于我之前做其他项目踩过的坑以及在地牢这种“多火源同屏”场景下的实际约束。方案实现难度性能开销视觉上限可控性适用场景序列帧贴图极低极低低高远景、装饰性火源粒子系统中中高中高中主角技能、少量重点火源Shader程序化生成中高低高高大量同屏火源、需要动态光照流体/物理模拟极高极高极高低影视级、单场景演示序列帧的问题前面说了缺乏随机性。粒子系统的问题在于Unity的Particle System或者Godot的GPUParticles虽然能做得很漂亮但每个粒子都要参与CPU或GPU的调度十几个火源同时跑Draw Call和Overdraw会迅速吃掉帧率。流体模拟就更不用想了独立游戏的性能预算根本撑不住。最终我选的是Shader程序化生成这条路。核心思路是用噪声函数在片元着色器里实时计算出火焰的形状和颜色配合一张简单的遮罩贴图控制火焰的整体轮廓。这样做的好处是每个火源本质上只是一个带材质的QuadDraw Call极低噪声函数的参数可以随机化让每个火源长得不一样而且因为是在Shader里算的可以很方便地把火焰的强度、颜色、抖动频率暴露成材质参数方便美术和策划调整。2.2 噪声函数的选择Perlin还是Simplex程序化火焰的核心是噪声。我试过Perlin噪声和Simplex噪声两种最后选了Simplex。原因很实际Perlin噪声在高维度下计算量偏大而且它的梯度方向有轴向偏好生成的火焰容易出现“方格感”火苗会不自然地沿着X轴或Y轴拉伸。Simplex噪声没有这个问题它的梯度分布更均匀计算量也更低在移动端GPU上跑起来更稳。具体到火焰的形变我用的是两层噪声叠加。第一层是低频噪声控制火焰整体的左右摇摆和大尺度形变第二层是高频噪声负责火苗边缘的撕裂和细节抖动。两层噪声的UV滚动方向不同低频层主要沿Y轴向上滚动模拟火焰上升高频层则做小幅度的X轴扰动制造火苗舔舐的感觉。这个“双层滚动”的思路是我从一篇讲程序化云层的文章里借鉴过来的改吧改吧用在火焰上效果出奇地好。提示噪声的UV滚动速度不要设成一样的低频层慢一点比如0.3高频层快一点比如1.2这样火焰的层次感会更强。如果两层速度一样看起来会像整块布在飘很假。2.3 光照联动的实现路径火焰不能只是自己好看它得照亮周围。我一开始想用Unity的实时点光源每个火源挂一个Point Light但很快就发现不行——十几个动态点光源在URP里会触发额外的阴影计算和光照Pass性能直接掉了一个档次。后来改成在Shader里手动计算光照贡献具体做法是把火源的位置和强度传给场景的地面和墙壁材质在它们的片元着色器里叠加一个基于距离衰减的暖色光照项。这个方案的好处是完全绕开了引擎的光照系统性能开销几乎可以忽略。代价是光照不会产生阴影但对于地牢这种环境光很暗、火源主要起“照亮轮廓”作用的场景来说没有阴影反而更符合氛围。如果你需要火焰投射阴影那就只能老老实实用实时点光源或者用一张假的阴影贴图去模拟。3. 火焰Shader的核心实现从噪声到最终像素3.1 顶点着色器让火焰“立起来”虽然火焰本质上是一个Quad但我没有让它完全平躺在场景里而是做了一点顶点偏移。具体来说在顶点着色器里根据UV的Y坐标把顶部的两个顶点稍微向外和向上推一点形成一个上宽下窄的梯形。这样做的好处是火焰在视觉上有了“体积感”从侧面看不会像一张纸片。代码大概长这样v2f vert (appdata v) { v2f o; float topMask smoothstep(0.0, 1.0, v.uv.y); v.vertex.xy float2(0.0, topMask * _HeightStretch); v.vertex.x (v.uv.x - 0.5) * topMask * _WidthSpread; o.vertex UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; }_HeightStretch控制火焰向上拉伸的幅度_WidthSpread控制顶部展开的宽度。这两个参数我调了很久最后定在_HeightStretch 0.15、_WidthSpread 0.3左右火焰看起来既有向上的动势又不会散得太开。3.2 片元着色器噪声采样与颜色映射片元着色器是整个火焰效果的核心。流程分四步采样噪声、计算火焰形状、映射颜色、叠加光照。第一步是采样噪声。我用了一张预生成的Simplex噪声图而不是在Shader里实时计算噪声函数。原因很简单实时计算Simplex噪声虽然可行但在移动端GPU上每个像素都跑一遍噪声函数开销还是偏大。预生成一张512x512的噪声图用tex2D采样性能好很多而且噪声图可以复用多个火源共享同一张图只是UV偏移和缩放不同。float noise1 tex2D(_NoiseTex, uv * _NoiseScale1 float2(0, _Time.y * _ScrollSpeed1)).r; float noise2 tex2D(_NoiseTex, uv * _NoiseScale2 float2(_Time.y * _ScrollSpeed2 * 0.3, _Time.y * _ScrollSpeed2)).r; float noise noise1 * 0.7 noise2 * 0.3;这里_NoiseScale1和_NoiseScale2控制两层噪声的缩放_ScrollSpeed1和_ScrollSpeed2控制滚动速度。注意第二层的X轴滚动速度我乘了0.3让它有一点横向的扰动但不会太剧烈。第二步是计算火焰形状。我用了一个径向渐变遮罩让火焰从中心向边缘逐渐透明同时用噪声去扰动这个遮罩的边缘。float2 centerUV uv - float2(0.5, 0.0); float dist length(centerUV * float2(1.0, 0.6)); float mask 1.0 - smoothstep(0.0, _FlameWidth, dist noise * _NoiseStrength);_FlameWidth控制火焰的宽度_NoiseStrength控制噪声对边缘的扰动强度。这个dist的计算里我把Y轴乘了0.6是为了让火焰的遮罩呈椭圆形而不是正圆形更符合火焰向上收窄的形态。第三步是颜色映射。火焰的颜色不是单一的橙色而是从底部的亮黄色到顶部的深红色有一个渐变。我用UV的Y坐标做插值float3 bottomColor float3(1.0, 0.9, 0.4); float3 midColor float3(1.0, 0.5, 0.1); float3 topColor float3(0.8, 0.1, 0.0); float3 flameColor lerp(bottomColor, midColor, smoothstep(0.0, 0.5, uv.y)); flameColor lerp(flameColor, topColor, smoothstep(0.5, 1.0, uv.y));这个颜色梯度是我对着真实火焰的照片调出来的。关键点是底部要足够亮、接近白色中部是饱和的橙色顶部偏红且透明度要高。如果顶部颜色太实火焰会显得很“重”没有轻盈感。第四步是叠加光照贡献。前面提到我不走引擎光照而是在火焰Shader里输出一个额外的光照强度值通过全局变量或者Command Buffer传给场景材质。简单起见我先把火焰的亮度信息写进一个Render Texture然后地面和墙壁的Shader去采样这张RT根据距离做衰减。3.3 参数暴露与随机化为了让每个火源看起来不一样我把噪声缩放、滚动速度、火焰宽度、颜色偏移这几个参数暴露成了材质属性然后在生成火源的时候用脚本随机化这些值。随机范围不能太大否则火焰会看起来像两种不同的东西。我的经验是噪声缩放在0.8到1.2之间随机滚动速度在0.8到1.5之间随机火焰宽度在0.9到1.1之间随机颜色偏移在±0.05以内。这样既保证了多样性又维持了统一的视觉风格。注意随机化参数一定要在材质实例上做不要直接改共享材质。Unity里用Renderer.material而不是Renderer.sharedMaterial否则所有火源会一起变。这个坑我踩过调试了半天才发现是共享材质的问题。4. 动态光照的联动方案让火焰真正照亮地牢4.1 光照信息的传递方式火焰照亮环境这件事我试了三种方案。第一种是直接用Unity的Point Light前面说了性能不行。第二种是在每个需要被照亮的物体Shader里手动计算到所有火源的距离但火源数量一多每个像素要遍历所有火源计算量爆炸。第三种是我最终采用的把火源信息打包成一张全局纹理场景材质采样这张纹理来获取光照。具体做法是创建一个低分辨率的Render Texture比如256x256每个像素代表场景中的一个位置。每帧把所有火源的位置、颜色、强度渲染到这张RT上用加法混合。然后地面和墙壁的Shader在片元阶段采样这张RT根据世界坐标映射到UV获取光照值。这个方案的性能开销是固定的跟火源数量无关只跟RT分辨率有关。256x256对于地牢这种小场景来说足够了光照过渡稍微有点糊但配合火焰本身的闪烁反而显得柔和。4.2 光照的闪烁与色温变化火焰的光照不能是恒定的必须跟着火焰的节奏抖动。我在光照计算里加了一个基于时间的噪声扰动float flicker tex2D(_NoiseTex, float2(_Time.y * 2.0, 0.5)).r; float intensity _BaseIntensity * (0.8 flicker * 0.4);这个flicker值在0.8到1.2之间波动让光照强度有轻微的呼吸感。色温方面我在火焰颜色和光照颜色之间做了一个微妙的偏移火焰本身偏橙黄但光照稍微偏红一点这样照在墙壁上会有一种温暖的、略带昏黄的感觉更符合地牢火把的氛围。4.3 多火源叠加的处理当地牢里同时有多个火源时光照会叠加。加法混合天然支持这一点但要注意过曝的问题。如果两个火源靠得很近叠加后的光照强度可能超过1.0导致地面变成一片死白。我的处理是在最终输出前做一个软裁剪float3 finalLight 1.0 - exp(-totalLight);这个1.0 - exp(-x)的函数在x较小时接近线性x较大时逐渐饱和到1.0能有效防止过曝同时保留高光区域的层次。这个技巧我是从HDR渲染的色调映射里学来的用在2D光照上同样好使。5. 性能优化与实测数据5.1 Draw Call与Overdraw的控制火焰Shader虽然计算量不大但Overdraw是个隐患。火焰是半透明物体多个火焰重叠时每个像素会被多次绘制。我的优化手段有三个第一火焰的Quad尽量小只覆盖火焰实际占据的区域不要留太多透明边距第二火焰的渲染队列设为Transparent但不写深度避免深度测试的开销第三对于远处的、不重要的火源降低Shader的复杂度比如去掉第二层噪声只保留一层。实测下来在红米Note系列这种中低端手机上同屏12个火源每个火源Quad大小约128x128像素帧率能稳定在55到60帧。如果去掉光照RT的采样帧率还能再高5帧左右。这个性能表现对于地牢题材来说完全够用了。5.2 噪声图的压缩与复用噪声图我存成了单通道的R8格式512x512大小内存占用只有256KB。所有火源共享这一张图只是UV偏移和缩放不同。这样既省内存又省去了每个火源单独生成噪声的开销。如果你需要更丰富的噪声细节可以生成一张1024x1024的但512对于火焰这种本身就有模糊感的元素来说已经足够了。5.3 移动端与PC端的参数差异移动端GPU的浮点精度和纹理采样速度跟PC有差异我在Shader里做了两套参数。移动端把噪声采样的次数从两次降到一次光照RT的分辨率从256降到128火焰的顶点偏移也简化了。这些差异通过#if SHADER_API_MOBILE宏来控制打包时自动切换。实测下来移动端的火焰效果虽然细节少一点但整体观感没有明显下降玩家根本注意不到。6. 常见问题与排查技巧实录6.1 火焰看起来像“果冻”怎么办这是最常见的问题原因是噪声的滚动速度太慢或者两层噪声的速度太接近。火焰的动感来自于不同频率的噪声以不同速度滚动如果速度太统一就会像一块整体在飘的果冻。解决办法是拉开两层噪声的速度差低频层慢、高频层快同时给高频层加一点横向的扰动。6.2 火焰边缘出现硬边或锯齿这通常是遮罩计算的问题。smoothstep的过渡范围太窄或者噪声的强度太大导致遮罩边缘被噪声“切”出了硬边。把smoothstep的第二个参数调大一点让过渡更柔和同时降低_NoiseStrength让噪声对边缘的扰动更温和。6.3 光照RT出现接缝或错位如果地面和墙壁的Shader在采样光照RT时世界坐标到UV的映射不一致就会出现光照错位。检查一下映射公式确保所有材质用的是同一套世界坐标到RT UV的转换。另外RT的Wrap Mode要设成Clamp不然边缘会出现重复采样的问题。6.4 火焰在相机移动时抖动这通常是顶点偏移在相机空间和世界空间之间不一致导致的。顶点偏移应该在模型空间做然后统一变换到裁剪空间。如果偏移量跟相机距离有关相机一动火焰就会抖。检查一下顶点着色器里的偏移计算确保它不依赖于相机位置。问题现象可能原因排查方向解决方法火焰像果冻噪声速度太统一检查两层噪声的ScrollSpeed拉开速度差高频层加横向扰动边缘硬边smoothstep过渡太窄检查_NoiseStrength和smoothstep参数调大过渡范围降低噪声强度光照错位世界坐标映射不一致对比各材质的世界坐标到UV转换统一映射公式RT设Clamp相机移动时抖动顶点偏移依赖相机检查顶点着色器的偏移计算在模型空间做偏移统一变换多火源过曝加法混合后强度超1检查光照叠加逻辑用1-exp(-x)做软裁剪移动端帧率低噪声采样和RT分辨率过高检查移动端宏定义降采样次数降RT分辨率提示调试火焰Shader时把噪声图直接输出到屏幕上会很有帮助。你可以临时把片元着色器的返回值改成float4(noise, noise, noise, 1.0)看看噪声的滚动和缩放是否符合预期。这个“可视化调试”的习惯能帮你省下大量猜测的时间。6.5 火焰颜色在不同设备上偏差大这是色彩空间的问题。Unity的Gamma空间和Linear空间下颜色的计算结果会不一样。确保你的项目统一使用Linear空间并且在Shader里对颜色做正确的转换。如果项目必须用Gamma空间那就手动在Shader里做一次pow(color, 2.2)的转换保证颜色在不同设备上一致。7. 后续可以继续打磨的方向火焰效果做到这个程度基本满足了《五星地牢》当前阶段的需求。但如果你想让火焰更上一层楼还有几个方向可以继续挖。第一个是火焰与风场的交互在地牢里加入气流区域让火焰根据气流方向倾斜和加速这个在Shader里加一个风向向量就能实现。第二个是火焰对物体的灼烧效果比如火焰附近的木箱会逐渐变黑这需要把火焰的光照信息传递给物体的材质做一个基于时间的颜色插值。第三个是火焰的粒子飞溅在火焰上方偶尔弹出几个小火星用极简的粒子系统就能做能显著提升火焰的生动感。我自己在实际操作中的体会是程序化火焰最难的不是Shader代码本身而是参数的调试。同样的代码参数差一点效果就天差地别。我的建议是先把所有参数暴露成材质属性然后在场景里放一排火源一边调一边看找到那组“看起来对”的参数。这个过程没有捷径但一旦调好了后面所有火源都可以复用这套参数只是做微小的随机化。另外别忘了在真机上测试模拟器和PC上的效果跟手机上的差异可能比你想象的大。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑