Unity特效进阶:基于FXLab思路与Shader的水烟雾粒子系统实战 📅 2026/8/10 1:30:30 1. 项目概述从“能看”到“好看”的视觉特效进阶在Unity3D项目开发中视觉特效VFX往往是决定游戏或应用“第一眼”品质的关键。一个流畅、写实的粒子效果或是一团通透、富有体积感的水烟雾能瞬间提升场景的氛围和沉浸感。然而很多开发者尤其是从基础功能转向高级表现的朋友常常会陷入一个瓶颈知道怎么用粒子系统Particle System和材质Material但做出来的效果要么“塑料感”十足要么性能开销巨大远达不到商业项目的要求。这个项目我们就来啃一块硬骨头使用FXLab的思路结合Unity原生粒子系统和自定义Shader实现一套高质量、可交互、且性能可控的水烟雾特效。为什么是水烟雾因为它几乎涵盖了特效开发的所有核心挑战形态的随机性与流动性、光影的半透与折射、与环境的物理交互以及实时的性能平衡。你可能会在瀑布、魔法技能、爆炸烟尘、环境雾气等场景中频繁用到它。我遇到过不少项目粒子发射得漫天飞舞材质球也调得五颜六色但最终效果就是一团“糊”没有层次没有细节更谈不上美感。这背后的核心往往不是工具不会用而是缺乏一套从底层逻辑到上层表现再到实战优化的完整方法链。本次实战我将抛开那些华而不实的理论直接带你进入一个资深TA技术美术或特效师的思考和工作流中。无论你是想提升个人作品集的质感还是为解决项目中棘手的特效性能问题寻找方案相信接下来的内容都能给你带来直接的、可复现的启发。2. 核心思路拆解为什么是FXLab思路与Shader的结合在深入代码和参数之前我们必须先统一思想做高级特效绝不能只停留在Unity编辑器里拖拽预制体、调调曲线。我们需要一个更系统、更物理的思维方式我称之为“FXLab思路”。这并不是指某个特定的插件而是一种工作哲学将特效视为一个由“发射器Emitter- 粒子Particle- 渲染器Renderer- 环境Environment”构成的动态系统其中每一个环节都受到精确的物理规则和艺术参数控制。2.1 FXLab思路的核心四要素发射器Emitter与初始状态水烟雾从哪里来是持续喷涌的泉眼还是瞬间爆开的冲击波发射器的形状球形、网格、边缘、发射率Emission Rate、初始速度Start Speed和随机性Randomize共同定义了特效的“出生”性格。一个常见的误区是只用一个发射器。对于复杂的水烟雾我们可能需要多个发射器层叠一个负责发射大颗粒、慢速的“水核”另一个负责发射高速、细密的“水雾”再一个负责发射缓慢扩散的“蒸汽”。粒子Particle的生命周期与运动每个粒子从诞生到消亡其大小Size、颜色Color、速度Velocity和旋转Rotation如何随时间变化这里的关键是理解力场Forces。除了重力Gravity水烟雾通常受到湍流力Turbulence、涡流力Vortex和阻力Drag的影响。通过脚本或Visual Effect Graph虽然强大但本次我们聚焦更通用的粒子系统模拟这些力可以让粒子的运动轨迹摆脱机械感更加自然混沌。渲染器Renderer与材质Material这是赋予粒子“灵魂”的一步。你看到的不是粒子而是材质赋予它的视觉表象。对于水烟雾我们几乎必然要使用半透明Transparent或加法Additive混合模式的Shader。核心挑战在于如何用Shader模拟水的折射Refraction、散射Scattering和菲涅尔效应Fresnel Effect以及烟雾的体积感Volume和密度衰减Density Falloff。一个纯色贴图加上Alpha通道是远远不够的。环境Environment交互特效不是孤立的。水烟雾如何接收场景光照是否受光照影响Receive Shadows如何与场景中的物体发生碰撞使用粒子碰撞模块Particle Collision碰撞后是消散、反弹还是附着这些交互细节是让特效“活”起来融入场景而非“浮”在场景之上的关键。2.2 技术选型为什么不用Visual Effect Graph或第三方插件你可能会问Unity有更现代的Visual Effect Graph也有很多强大的第三方特效插件如Asset Store上的热门产品为什么我们还要基于传统的粒子系统和手写Shader来折腾原因有三深度可控性与理解传统粒子系统Particle System组件是Unity的基石其API稳定所有参数和生命周期都暴露无遗。通过它来实践FXLab思路你能透彻理解每一帧粒子状态变化的根源。VFX Graph虽然强大且易于做出复杂效果但它更像一个黑盒或者说蓝盒对于初学者理解底层原理反而不利。项目兼容性与性能基线传统粒子系统在所有Unity渲染管线Built-in, URP, HDRP中都有良好支持是性能优化的公认基线。掌握它的极致优化方法能让你在任何项目中都心中有数。许多第三方插件虽然效果炫酷但可能带来未知的依赖和性能开销在团队协作或项目后期容易成为“技术债”。Shader能力的终极自由任何顶点/片元着色器Shader都可以应用于粒子渲染器。这意味着你可以实现任何你能想象到的材质表现。本次实战的重点——水烟雾材质正是通过自定义Shader来实现的这是提升特效品质不可绕过的核心技术。注意这并不是说VFX Graph或插件不好。恰恰相反在充分理解底层原理后使用这些高效工具能极大提升生产力。本实战的目的是为你打下不可撼动的根基。3. 实战准备构建你的水烟雾特效实验室理论说得再多不如动手调一调。我们首先搭建一个基础的测试场景和资源结构。3.1 场景与资源准备创建新项目或场景建议使用URP通用渲染管线项目开始。URP对移动端和高端PC都有较好的支持且其Shader编写规则相对清晰。在Package Manager中确保已安装Universal RP。组织项目文件夹Assets/ ├── _VFX_WaterSmoke/ // 项目根文件夹 │ ├── Materials/ // 存放材质球 │ ├── Shaders/ // 存放自定义Shader文件 │ ├── Textures/ // 存放噪声图、法线图等贴图 │ ├── Prefabs/ // 存放制作好的特效预制体 │ └── Scripts/ // 存放控制粒子行为的脚本准备核心贴图水烟雾特效极度依赖高质量的噪声纹理Noise Texture。你需要准备至少一张3D噪声图或2D模拟3D和一张流动图Flow Map。可以在网上寻找Perlin Noise或Simplex Noise的生成工具来制作或使用Substance Designer等工具生成。将其导入Unity记得将Wrap Mode设置为RepeatFilter Mode设置为Bilinear或Trilinear以获得平滑的过渡。3.2 创建基础粒子系统在Hierarchy中右键 - Effects - Particle System创建一个新的粒子系统命名为VFX_WaterSmoke_Core。我们先进行一些基础设置为后续的复杂效果搭建一个干净的舞台Particle System (主模块):Duration: 5.0 (粒子系统持续时间)Looping: ✔ (勾选循环播放)Prewarm: ✔ (勾选预热一开始就是满效果)Emission (发射模块):Rate over Time: 20.0 (每秒发射20个粒子初始值后续会调整)Shape (形状模块):Shape: Sphere (球形发射器)Radius: 0.5 (半径0.5米)Randomize Direction: 1.0 (方向完全随机模拟自然扩散)Renderer (渲染器模块):Render Mode: Mesh (我们后续将使用一个简单的面片Quad Mesh)Material: 暂时留空稍后我们会创建自定义材质并赋予它。现在你会在场景中看到一个持续从一个小球中向各个方向发射白色小点的系统。这将是我们的“画布”。4. 核心环节一粒子动力学模拟——让烟雾“活”起来静态的粒子云只是一团雾。我们需要为粒子注入符合物理规律的“生命”。4.1 配置粒子基础属性打开VFX_WaterSmoke_Core的粒子系统我们开始细化粒子本身的属性Start Lifetime (初始生命周期): 设置为3.0到5.0之间的随机值点击右侧小箭头选择Random Between Two Constants。生命周期不同能自然形成密度梯度。Start Speed (初始速度): 设置为0.5到2.0之间的随机值。给予一个基础的扩散动力。Start Size (初始大小): 设置为0.05到0.15之间的随机值。大小不一能增强细节。Start Rotation (初始旋转): 设置为0到360之间的随机值。这对于使用带方向性纹理的面片粒子很重要。Start Color (初始颜色): 设置为淡蓝色RGBA约180, 220, 255, 200。这是水体的基础色。4.2 添加关键物理力场模块这是模拟自然运动的核心。点击粒子系统下方的“”号添加以下模块Velocity over Lifetime (生命周期内速度):这个模块可以让我们控制粒子在生命过程中的速度变化。对于上升的烟雾或蒸汽我们可以在Y轴Local或World空间赋予一个正速度比如(0, 0.5, 0)。同时可以加入少量随机性(rand(-0.1, 0.1), 0, rand(-0.1, 0.1))让运动轨迹不那么整齐。Limit Velocity over Lifetime (生命周期内限速):粒子在受力后可能速度过快显得不自然。这个模块就像一个“空气阻力”调节器。将Speed设置为2.0Dampen阻尼设置为0.5。这意味着当粒子速度超过2时会受到一个比例为0.5的阻尼力使其慢下来。Force over Lifetime (生命周期内力):这是模拟湍流、涡流等复杂力的关键。我们可以用一个脚本动态控制这个力但为了直观先设置一个简单的世界空间力例如(rand(-0.2,0.2), 0.2, rand(-0.2,0.2))。这会给粒子一个向上的升力和随机的水平扰动。Noise (噪声模块):这是实现烟雾缭绕、流体般不规则运动的神器强烈建议开启。Strength:0.3(噪声强度不宜过大)Frequency:0.5(噪声频率值越低扰动越平滑、缓慢)Scroll Speed:0.2(噪声图滚动速度模拟动态变化)Damping: ✔ (勾选使噪声影响随速度增加而减弱更物理)Octaves:2(噪声叠加次数增加细节层次)实操心得Noise模块非常消耗性能尤其是在粒子数量多的时候。在移动端项目需要谨慎使用或完全关闭用Force over Lifetime配合简单的随机函数来近似模拟。在PC端则可以大胆调高Octaves和Quality质量来获得更细腻的运动。Collision (碰撞模块):为了让水烟雾能与场景交互比如撞击地面后溅开添加此模块。Type: World (与世界碰撞)Mode: 3D (如果你的场景是3D的)Collides With: Everything (或指定层)Dampen:0.5(碰撞后速度保留比例)Bounce:0.2(弹性对于水雾可以设低)Lifetime Loss:0.3(碰撞后损失的生命周期比例模拟能量消散)调整完这些参数后播放场景你应该能看到粒子不再直线运动而是以一种更自然、混沌的方式上升、扩散、相互交织初步具备了“活”的动感。5. 核心环节二Shader魔法——编写水烟雾材质粒子有了灵动的运动现在需要为它们披上“水”和“烟”的外衣。这一步完全依赖于Shader。我们将创建一个适用于URP的Unlit Shader Graph对于Built-in管线思路类似代码写法不同。5.1 创建Shader Graph与基础设置在Shaders/文件夹右键 - Create - Shader Graph - URP - Unlit Shader Graph命名为WaterSmoke_URP。双击打开Shader Graph编辑器。设置主节点Master Stack:Surface Type设置为Transparent。Blending Mode设置为Alpha传统半透明或Additive发光、火焰、轻烟常用。对于水烟雾Alpha混合更能表现其实体感和深度但需要处理渲染排序问题Additive混合则不会出现深度遮挡错误且能自发光适合表现光雾。我们先选择Alpha。Depth Write设置为Off半透明物体通常不写入深度避免后续渲染错误。Culling设置为Off双面渲染确保从任何角度都能看到。5.2 构建材质表现网络我们的目标是模拟两个核心视觉特征内部的密度/颜色变化和边缘的菲涅尔效应。基础纹理与颜色添加一个Sample Texture 2D节点将之前准备的噪声图连接至Texture输入。UV端口连接一个Time节点乘以一个较小的系数如0.1控制流动速度和一个Panner节点模拟纹理的缓慢流动。将采样出的噪声R通道连接到一个Color节点的AAlpha通道作为基础透明度。噪声值高的地方更不透明密度大低的地方更透明。添加一个Color节点设置为水的基色如淡蓝色将其RGB输出与另一个控制烟雾颜色的Color节点如灰白色进行Lerp线性插值。用噪声图的另一个通道如G通道作为Lerp的T插值因子这样就能实现水色与烟雾色的自然混合。模拟体积与菲涅尔效应菲涅尔效应是指物体在视线与表面法线夹角越大即边缘时反射越强看起来更亮/更不透明。对于水烟雾边缘往往因为光线散射更多而显得更厚实。添加一个Fresnel Effect节点。将Normal输入连接Normal Vector节点世界空间或视图空间Power参数控制边缘宽度值越大边缘越细。例如设置为3.0。将Fresnel Effect的输出一个0到1的值边缘接近1与之前的基础透明度来自噪声进行Multiply相乘或Add相加。相乘会使边缘和内部同时受菲涅尔影响整体更协调相加会强化边缘使其更亮更实。这里我们选择Multiply得到一个受视角影响的整体透明度。扰动与折射模拟进阶为了模拟光线穿过不均匀介质如热水上方的热空气产生的扭曲感我们可以扰动背景。添加一个Scene Color节点在URP中可能需要Screen节点和Scene Depth/Scene Color节点的配合具体取决于URP版本和设置。较新版本在Master Stack的Surface选项中有Receive SSR等开关并提供了Screen Position节点。用一个简单的噪声或流动图对屏幕UV进行扰动然后用扰动后的UV去采样Scene Color。将扰动后的场景颜色与粒子自身颜色进行混合如Lerp混合因子可以用粒子的透明度来控制。这样在粒子半透明的地方就能看到背后场景的扭曲效果模拟简单的折射。最终输出将混合后的颜色连接到Master Stack的Color。将经过菲涅尔调制后的最终透明度连接到Master Stack的Alpha。5.3 创建并应用材质在Materials/文件夹右键创建Material命名为Mat_WaterSmoke并将其Shader选择为我们刚创建的WaterSmoke_URP。将必要的贴图拖拽到材质的对应属性槽中。回到粒子系统VFX_WaterSmoke_Core在Renderer模块中将Material指定为Mat_WaterSmoke并将Render Mode改为MeshMesh选择一个简单的Quad在Primitive类型中。播放场景你应该能看到原本单调的粒子现在拥有了基于噪声的颜色变化、透明度变化并且从侧面看和从正面看边缘的透明度会有所不同菲涅尔效应。注意事项半透明渲染的顺序Render Order是个经典难题。Unity默认按物体到相机的距离排序但粒子系统内部可能有大量重叠的面片。为了减少排序错误一个本应在后面的粒子被渲染在了前面可以尝试在粒子系统的Renderer模块中调整Sorting Fudge值。将材质球的Render Queue设置为Transparent通常是3000。对于复杂的特效考虑将粒子系统拆分为多个层分别控制它们的渲染顺序。6. 核心环节三性能优化实战——让特效“飞”得更稳一个再华丽的效果如果导致游戏帧率骤降也是失败的。粒子特效是性能消耗大户我们必须从设计阶段就考虑优化。6.1 粒子系统层面的优化控制粒子数量#1原则这是最有效的优化手段。不断问自己“我真的需要这么多粒子吗”使用Emission over Distance基于距离发射替代持续的Emission over Time。当相机远离时停止或减少发射。精细调整Start Lifetime。更短的生命周期意味着屏幕上同时存在的粒子更少。使用Max Particles最大粒子数硬性限制上限防止意外情况导致粒子爆炸。简化模拟复杂度如之前所述谨慎使用Noise模块在移动端考虑禁用。减少Collision模块的检测精度降低Voxel Size体素大小或碰撞频率。如果不需要旋转关闭Rotation over Lifetime模块。渲染优化使用最简单的MeshQuad两个三角形是性能最好的选择。避免使用复杂模型作为粒子。合并Draw Call确保所有粒子使用同一个材质球Material。Unity会自动对使用相同材质的粒子进行合批Batching。如果材质相同但参数不同通过脚本修改则会打断合批。使用GPU Instancing在材质的Inspector面板中勾选Enable GPU Instancing。这能将大量相同网格、相同材质的渲染合并到一个Draw Call中极大提升渲染效率。确保你的Shader是支持Instancing的在Shader Graph中主节点设置里可以开启。6.2 Shader与材质层面的优化简化Shader计算在Shader Graph中检查节点数量。复杂的数学运算如多个Custom Function节点、多次纹理采样Sample Texture 2D都会增加GPU负担。对于移动端考虑移除或简化菲涅尔效应、折射扰动等“锦上添花”的效果。保留最核心的颜色和透明度变化。使用LOD (Level of Detail)为你的Shader创建多个简化版本根据相机距离或平台性能动态切换材质。纹理优化使用尽可能小的纹理尺寸如512x512甚至256x256。对于噪声图有时64x64也足够因为其本身就需要模糊感。确保纹理压缩格式正确如ASTC for Mobile, DXT5 for PC。将多个灰度图如噪声的R、G通道打包到一张RGBA纹理的不同通道中减少纹理采样次数。6.3 使用脚本进行动态优化创建一个名为ParticleSystemOptimizer的C#脚本挂载到粒子系统上实现动态控制using UnityEngine; [RequireComponent(typeof(ParticleSystem))] public class ParticleSystemOptimizer : MonoBehaviour { private ParticleSystem ps; private Transform camTransform; public float maxRenderDistance 50.0f; // 最大渲染距离 public float emissionRateAtMaxDistance 0.1f; // 最远距离时的发射比例 private float initialEmissionRate; void Start() { ps GetComponentParticleSystem(); camTransform Camera.main.transform; var emission ps.emission; initialEmissionRate emission.rateOverTime.constant; // 获取初始发射率 } void Update() { float distanceToCam Vector3.Distance(transform.position, camTransform.position); var emission ps.emission; if (distanceToCam maxRenderDistance) { // 距离过远完全停止发射并清空现有粒子可选 emission.rateOverTime 0; // ps.Clear(); // 谨慎使用可能造成视觉突变 } else { // 根据距离动态调整发射率 float distanceFactor 1.0f - Mathf.Clamp01(distanceToCam / maxRenderDistance); float dynamicRate Mathf.Lerp(initialEmissionRate * emissionRateAtMaxDistance, initialEmissionRate, distanceFactor); emission.rateOverTime dynamicRate; } // 可选根据帧时间动态调整模拟精度LOD // if (Time.deltaTime 0.033f) { // 帧率低于30 // ps.main.simulationSpeed 0.8f; // } else { // ps.main.simulationSpeed 1.0f; // } } }这个脚本实现了基于距离的动态发射率控制确保特效在远处不会浪费性能。7. 常见问题与排查技巧实录在实际开发中你一定会遇到各种奇怪的问题。这里记录了几个最典型的“坑”及其解决方案。7.1 问题粒子材质显示为紫色Missing Shader现象粒子在场景中显示为亮紫色。原因这是Unity的“粉红/紫色错误”表示材质球丢失了Shader或者Shader本身编译错误、不兼容当前渲染管线。排查检查材质球Mat_WaterSmoke的Shader属性是否确实选择了WaterSmoke_URP。如果Shader名称正确双击Shader文件查看控制台Console是否有编译错误。URP Shader Graph必须放在Assets目录下或明确的Shader Graph搜索路径且项目必须是URP项目。确保你创建的是URP的Unlit Shader Graph而不是Built-in管线的。7.2 问题半透明粒子渲染顺序错乱前后闪烁现象粒子层相互穿透本该在后面的粒子有时渲染到了前面。原因Unity对半透明物体的排序Sorting问题。粒子系统内部每个粒子的渲染顺序是动态的与相机距离和粒子生成顺序都有关。解决方案调整粒子自身排序在粒子系统的Renderer模块中Sort Mode: 尝试改为Youngest First或Oldest First看哪种更符合预期。Sorting Fudge: 调整这个值可正可负影响整个粒子系统在渲染队列中的排序偏移。调整材质渲染队列在材质Mat_WaterSmoke的Inspector中尝试修改Render Queue值。Transparent通常是3000你可以尝试设置为2999或3001来微调。拆分图层如果特效由明显的“前景层”和“背景层”如核心水柱和外围水雾将它们拆分为两个独立的粒子系统GameObject通过调整Sorting Layer和Order in Layer在Renderer模块来严格定义前后关系。接受不完美对于大量、快速运动且高度重叠的粒子如浓烟完美的排序几乎不可能且开销巨大。有时需要在视觉瑕疵和性能之间取得平衡或者通过艺术设计让粒子更稀疏、更小来减轻问题。7.3 问题移动设备上特效非常卡顿现象在PC上运行流畅发布到手机或模拟器上帧率大幅下降。原因粒子数量过多或Shader计算过于复杂超出了移动GPU的处理能力。性能排查与优化使用Unity Profiler在编辑器里运行游戏打开Window - Analysis - Profiler。切换到Rendering区域查看SetPass Calls和Batches。一个粒子系统如果使用同一个材质应该只产生少量Batches。如果Batches很高说明合批失败。查看GPU耗时在Profiler的GPU区域找到渲染你的粒子特效所消耗的时间。这是最直接的指标。实施“降级”方案脚本动态降级在ParticleSystemOptimizer脚本中增加平台判断在移动端自动减少Max Particles、关闭Noise模块、使用更简单的材质变体通过MaterialPropertyBlock或直接替换材质。制作LOD系统创建VFX_WaterSmoke_Low预制体使用更少的粒子、更简单的Shader。在运行时根据设备性能或相机距离进行切换。7.4 问题粒子与场景碰撞没有反应现象开启了Collision模块但粒子直接穿过了地板或其他碰撞体。排查确保场景中的地板等物体有Collider组件如Mesh Collider或Box Collider。在粒子系统的Collision模块中检查Collides With层掩码Layer Mask是否包含了目标物体所在的层。检查碰撞体的Is Trigger属性是否被错误地勾选了如果是触发器则不会发生物理碰撞。尝试将Collision的Mode从3D切换到2D或反之确保与碰撞体维度匹配。7.5 问题自定义Shader中的纹理采样效果不对全黑/全白现象在Shader Graph中连接了纹理但效果显示异常。排查检查Sample Texture 2D节点的Texture输入是否确实连接了纹理资产并且该纹理的Texture Type是否正确通常是Default或Normal map对于噪声图用Default即可。检查UV输入。如果UV没有正确连接默认是(0,0)就会只采样纹理的一个角。通常需要连接UV节点或Time驱动的Panner节点。在Shader Graph的Blackboard中将纹理定义为Texture2D类型的Property然后在材质球面板中检查这个属性是否成功关联了纹理文件。经过以上从理论到实践从创作到优化的完整流程你应该已经能够独立制作出一个性能可控、视觉效果不俗的水烟雾特效了。特效开发是一个不断迭代和打磨的过程核心在于对物理规律的观察、对工具链的深刻理解以及在艺术表现与性能开销之间找到最佳平衡点的权衡能力。记住最好的特效往往是那些让玩家感觉不到其存在却深深沉浸在它所营造的氛围中的特效。