给角色做燃烧、冰冻、电光、符文觉醒这类实时特效时,最容易出现的问题不是缺素材,而是特效浮在角色表面、不贴合模型轮廓、也不跟随骨骼动画。很多初期方案是在角色身上叠加一团粒子,再配一张 Tiling 贴图,静止看有点效果,可角色一动起来,粒子就明显跟模型脱节。Mesh-Based VFX 这类技术路线,把角色 Mesh 本身当作特效数据的来源:从网格顶点取位置、法线、UV、顶点色,再用这些数据驱动粒子系统、VFX Graph 或自定义 Shader,让特效在真实贴近模型表面的位置出现和变化。以 Realistic Mesh VFX 为代表的这类方案,核心思路就是把几何数据变成表现力,而不是单纯叠贴图。
下面按一条完整链路展开:先理解 Mesh-Based VFX 是如何把几何数据变成视觉效果的,再准备 Unity 环境和角色 Mesh,从最小示例开始让粒子按网格顶点发射,接着解释顶点色、法线、UV 这些参数如何影响最终表现,最后补齐运行验证、性能观测和常见问题排查。整个流程不绑定具体商业插件,核心思路和 API 在 Unity 内置渲染管线、URP 和 HDRP 下都适用。
1. 理解 Mesh-Based VFX:为什么特效要跟 Mesh 数据发生关系
1.1 传统角色特效的两个瓶颈
传统特效方案在静态模型上表现尚可,一旦进入角色动画场景,通常会遇到两个问题。
第一个问题是贴合度差。角色表面不是平面,有衣领、袖口、脸型、铠甲接缝。粒子发射器如果挂在某个骨骼下,发射出来的粒子只会围绕一个固定点位扩散,无法精确覆盖模型表面。贴图方案更明显,平面贴图投影到复杂网格上会出现拉伸、错位和穿模。
第二个问题是动画跟随差。角色骨骼在运动,手臂摆动、身体旋转、表情变化,皮肤顶点每帧都在移动。如果特效只是挂在 Transform 上,或者粒子发射器绑定在骨骼节点上,特效会出现明显的延迟和漂移。真正常用的做法是先取出当前动画姿势下的 Mesh 顶点数据,再基于这些数据生成特效,这就是 Mesh-Based VFX 的核心场景。
1.2 Mesh 能提供哪些几何数据
Mesh 在图形学里表示三维几何体,不只是外观模型,更是一份可以反复采样的数据表。一个 Mesh 通常包含以下数据:
| 数据 | 含义 | 在特效中的典型用途 |
|---|---|---|
| Position | 顶点位置 | 粒子出生坐标、碎片爆裂点、贴花投影位置 |
| Normal | 顶点法线 | 粒子喷射方向、表面朝向、远离模型表面的速度 |
| Tangent | 顶点切线 | 粒子旋转方向、沿表面流动的方向 |
| UV1 / UV2 | 贴图坐标 | 采样遮罩纹理、控制特效出现的区域 |
| Color | 顶点色 | 控制强度分区、颜色权重、粒子大小分布 |
| Triangle Index | 三角形索引 | 按面片随机、控制部分区域优先产生特效 |
这些数据在 Mesh Asset 里天然存在,可以在模型导入时配置,也可以在运行帧里刷新。Mesh-Based VFX 的本质就是把这些数据结构读出来,传给粒子系统、VFX Graph 或者 Shader,让几何体本身成为特效的“发射器”和“控制器”。
1.3 Mesh-Based VFX 的完整工作链路
一个基于 Mesh 的角色实时特效,从数据到画面通常经过这五步:
- 模型阶段:在 3D 软件里规划好顶点色、UV、材质 ID。
- 导入阶段:确认 Unity 导入设置保留了顶点色、法线和切线。
- 采样阶段:运行时从 Mesh 或 SkinnedMeshRenderer 读取顶点数据。
- 计算阶段:粒子系统、VFX Graph 或 C# 脚本把这些数据转成粒子位置、速度和朝向。
- 渲染阶段:自定义 Shader 读取顶点色、UV 等信息,输出最终颜色和透明度。
只要这条链路是通的,角色表面出现火焰、冰霜、电流、符文时,特效就能紧贴模型,并且跟随骨骼变化实时更新。这里要注意,Mesh 在无线组网领域也指网状网络,但本文讨论的是 Unity 图形学里的网格数据。
1.4 适合与不适合的场景
Mesh-Based VFX 适合的场景有这些:
- 角色身上持续燃烧、结冰、通电的状态特效。
- 符文在装备表面浮现、流动、消失。
- 受伤时伤口位置发光或产生碎片。
- 数字孪生场景中,设备 Mesh 表面显示温度、压力、运行状态。
- 怪物、BOSS 在某个部位蓄力、产生辐射效果。
不太适合的场景包括:远处的大范围天气特效、全屏后期效果、高粒子量的弹幕。这些场景更适合使用公告牌粒子、体积雾或全屏 Shader,因为它们的表现重点不是贴合某个具体模型,而是覆盖整个空间。
2. 环境准备与技术选型:先定管线,再动特效
2.1 确认 Unity 版本和渲染管线
不同渲染管线对 VFX Graph 和 Shader Graph 的支持程度不一样,这里先给出一份保守的选型建议,落地时再结合项目版本确认。
| 项目 | 推荐选择 | 说明 |
|---|---|---|
| Unity 版本 | 优先 Unity 2021.3 LTS 或更新版本 | 2022.3 LTS 对 VFX Graph 更稳定 |
| 渲染管线 | URP / HDRP / 内置管线 | VFX Graph 在内置管线支持有限,建议 URP 或 HDRP 项目使用 |
| 特效工具 | Visual Effect Graph 或 Particle System | VFX Graph 适合 GPU 粒子,Particle System 适合兼容性优先的场景 |
| Shader 工具 | Shader Graph 或手写 ShaderLab | URP 项目建议 Shader Graph,内置管线可手写 |
| 角色模型 | FBX / OBJ,带蒙皮和骨骼 | 以 SkinnedMeshRenderer 为主 |
这里有一点要强调:如果输入材料没有给出项目具体的 Unity 版本,不要直接假设某个版本能跑通全部功能。工程里常见的做法是先打开 Unity Hub 确认编辑器版本,再在 Package Manager 里查一下 VFX Graph 包和 URP 包的版本兼容表。
2.2 安装依赖包和激活检查
在 Unity 中打开Window -> Package Manager,在搜索框输入以下包名并安装:
Visual Effect GraphShader Graph
如果项目使用 URP,还需要确认Universal RP包已经安装。安装完成后,在 Project Settings 中把项目的渲染管线资产切到 URP Asset。
打开工程时,控制台如果出现No valid Unity Editor license found. Please activate your license.,说明当前机器没有有效许可证。处理方式不是去下载第三方工具,而是回到 Unity Hub 登录账号,检查许可证绑定状态,再重新激活。公司内网环境还要确认网络能正常访问 Unity 的许可证服务。这里先把激活检查放在环境准备阶段,后面排查问题时会少一个变量。
2.3 角色 Mesh 的准备要求
角色模型不是随便拿一个高模就能直接做特效。为了让顶点色、UV、法线在后期可控,模型制作阶段最好按下面的要求整理。
- 顶点数:角色特效采样用的 Mesh 不需要太高精度,建议控制在 2000 到 10000 顶点以内,具体取决于目标平台。
- 顶点色:在 3D 软件里预先烘焙顶点色,例如 Red 通道控制燃烧强度,Green 通道控制扩散速度,Blue 通道控制贴花透明度。
- 法线:确保法线方向正确,否则粒子喷射方向会反。
- 切线:如果需要沿表面流动的效果,需要切线数据,FBX 导入设置里不要勾选错误的切线计算选项。
- UV:特效遮罩图和流动方向通常依赖第二套 UV,建模阶段就要分配好。
- 网格布局:三角形尽量均匀,不要出现大量极小面和退化面,否则粒子密度会不均匀。
模型导入到 Unity 后,右键模型资源,在导入设置的 Model 标签页确认Read/Write Enabled按需开启。如果运行时需要读取顶点数据,开启这个选项;如果只给 VFX Graph 采样静态网格,可以保持关闭以节省内存。
2.4 性能预算要按平台分层
Mesh-Based VFX 不是无代价的方案,性能开销主要在顶点数据读取、粒子数量、Shader 复杂度三块。建议在学习环境里先把功能跑通,到开发环境再调性能,进入生产环境前必须做真机评估。
| 目标平台 | 粒子预算 | Mesh 顶点预算 | 采样频率 |
|---|---|---|---|
| PC 高端 | 20000 粒子以内 | 10000 顶点以内 | 每帧可采样 |
| PC 低配 | 5000 粒子以内 | 5000 顶点以内 | 每 2 到 3 帧采样一次 |
| 移动端中端 | 1000 粒子以内 | 2000 顶点以内 | 每 3 到 5 帧采样一次 |
| 移动端低端 | 300 粒子以内 | 1000 顶点以内 | 尽量不每帧采样 |
这些数值不是绝对标准,只是参考。实际项目里最终以 Profiler 的 CPU 和 GPU 耗时为准。移动端的 URP 项目尤其要注意 VFX Graph 的兼容性,部分节点在移动端 GPU 上不受支持,需要提前做兼容方案。
3. 最小可运行案例:让粒子从角色 Mesh 表面发射
3.1 场景搭建
先创建一个最简单的测试场景。用 Unity 自带的基本几何体也可以,但为了贴合文章主题,建议准备一个带 SkinnedMeshRenderer 的低模角色。
场景里有三个关键对象:
- 角色模型对象,挂载 SkinnedMeshRenderer。
- 一个特效对象,可以是 Particle System,也可以是 Visual Effect Graph 对象。
- 一个脚本对象,负责读取角色 Mesh 数据并同步给特效系统。
静止测试时,也可以先使用 MeshRenderer 代替 SkinnedMeshRenderer。区别在于 MeshRenderer 的 Mesh 是静态的,SkinnedMeshRenderer 需要 BakeMesh 才能拿到当前动画姿势下的网格。
3.2 方式一:Particle System 按 Mesh 发射
在场景中创建空物体,挂上 Particle System 组件。打开 Particle System 的 Shape Module,把 Shape 类型选择为Mesh,再把角色模型的 Mesh 拖到Mesh字段。
此时的发射逻辑是:粒子从 Mesh 表面上随机分布的位置出生。调整 Emission 模块的Rate over Time控制粒子数量,调整 Shape 模块的Random Direction Amount控制粒子飞出方向的变化程度。
这个方式实现简单,但有一个明显限制:Particle System 的 Mesh Shape 只在发射时采样一次网格数据。如果角色做骨骼动画,Mesh 表面在运动,粒子出生点不会随着骨骼变化实时刷新,因此必须换一种方式。
3.3 方式二:VFX Graph 采样网格数据
VFX Graph 的处理方式更灵活。新建一个 VFX Graph,打开 Graph 编辑器,会看到默认的四个上下文:Spawn、Initialize Particle、Update Particle、Output Particle Quad。
要做基于 Mesh 的发射,关键改动在 Initialize Particle:
- Spawn 模块设置一个初始生成数量,例如每帧 100 个。
- Initialize Particle 中添加节点
Sample Mesh Position和Sample Mesh Normal。 - 把 Sample Mesh Position 的输出连到 Initialize Particle 的 Position 属性。
- 把 Sample Mesh Normal 的输出连到 Initialize Particle 的 Velocity 方向。
- 添加
Sample Mesh Color,把顶点色输出连到 Color 属性。
不同 Unity 版本里节点名称可能有差异。新版 VFX Graph 提供了 Skinned Mesh Sampling 相关节点,可以直接绑定 SkinnedMeshRenderer 采样动画蒙皮结果;如果版本较老,就要借助 C# 脚本,先把 SkinnedMeshRenderer 烘焙成普通 Mesh,再传给 VFX Graph。
Particle System 方式适合快速验证,VFX Graph 方式适合正式效果。下面这段 C# 脚本是关键的桥梁。
3.4 用 C# 脚本同步蒙皮网格数据
在角色动画播放过程中,SkinnedMeshRenderer 的 Mesh 数据在每帧都会变化。为了让 VFX Graph 采样到正确姿势,需要调用 BakingMesh API,把蒙皮结果烘焙到一个静态 Mesh 对象上,再传给 VFX Graph。
using UnityEngine; using UnityEngine.VFX; public class SkinnedMeshToVfx : MonoBehaviour { [Header("组件引用")] public SkinnedMeshRenderer skinnedMesh; public VisualEffect vfx; [Header("采样参数")] public int bakeEveryFrames = 3; private Mesh bakedMesh; private int frameCount; void Awake() { if (skinnedMesh == null) { skinnedMesh = GetComponent<SkinnedMeshRenderer>(); } bakedMesh = new Mesh(); frameCount = 0; } void LateUpdate() { frameCount++; if (frameCount < bakeEveryFrames) { return; } frameCount = 0; if (skinnedMesh == null || vfx == null) { return; } skinnedMesh.BakeMesh(bakedMesh); vfx.SetMesh("SourceMesh", bakedMesh); } }这段脚本做了三件事:
- 在 Awake 中创建一个空的 Mesh 对象,作为烘焙结果的缓存容器。
- 在 LateUpdate 中按固定帧间隔调用
BakeMesh。 - 把烘焙结果通过
vfx.SetMesh传给 VFX Graph 中名为SourceMesh的属性。
使用LateUpdate而不是Update的原因是:骨骼动画和 Animator 的状态在 Update 阶段更新,LateUpdate 阶段再读取数据时,拿到的是当前帧完整更新后的动画姿势。按帧间隔烘焙是为了控制性能,bakeEveryFrames = 3表示每 3 帧才刷新一次网格数据。如果角色动作很快,这个值要调小;如果角色大量发生且特效不明显,可以调大。
脚本里使用了UnityEngine.VFX命名空间,编译前必须先在 Package Manager 安装 Visual Effect Graph 包,否则会报命名空间不存在。
3.5 检查点
跑通最小示例后,用以下三个现象确认流程正常:
- 场景中能看到粒子从网格表面出生,而不是从原点或某个固定平面出生。
- 角色播放动画时,粒子出生位置跟随网格表面变化,不出现明显滞后。
- 角色旋转、移动时,特效仍然贴合模型表面,不漂移。
如果粒子没有出现,优先检查 VFX Graph 属性名是否与脚本里的字符串一致。vfx.SetMesh("SourceMesh", bakedMesh)中的SourceMesh必须在 VFX Graph 中创建对应 Mesh 属性,脚本只是往属性里写数据。
4. 关键参数与 Shader 细节:顶点色、法线、UV 如何变成表现力
4.1 Mesh 数据到特效表现的映射关系
Mesh 数据的价值在于它能把模型表面特征直接转化为粒子表现。下面是常用映射关系:
| Mesh 数据 | 特效表现 | 实现方式 |
|---|---|---|
| Position | 粒子出生位置 | Sample Mesh Position |
| Normal | 喷射方向 | 把法线乘一个速度系数 |
| Tangent | 表面流动方向 | 用切线作为粒子 Velocity 基准 |
| UV | 遮罩和贴图坐标 | 输出到 Shader 的 UV |
| Color 的 R | 燃烧强度 | 控制粒子大小或发光强度 |
| Color 的 G | 扩散速度 | 控制粒子初始速度 |
| Color 的 B | 贴花透明度 | 控制材质 Alpha |
| Color 的 A | 裁剪阈值 | 控制是否生成粒子 |
实际项目中,这种多通道用途可以显著减少贴图数量,也能让美术在模型阶段就确定特效表现区域。
4.2 粒子系统关键参数速查
在 Particle System 和 VFX Graph 中,参数都会直接影响表现,这里给出一个速查表,方便调试时快速定位问题。
| 参数 | 作用 | 调大的影响 | 调小的结果 | 典型值 |
|---|---|---|---|---|
| Max Particles | 粒子总数上限 | 画面更密集,CPU/GPU 压力上升 | 画面稀疏,可能出现粒子突然消失 | PC 5000,移动端 500 |
| Start Lifetime | 每个粒子的存活时间 | 拖尾变长,堆积感强 | 粒子快速消失,适合火花 | 0.5 到 2 秒 |
| Start Speed | 粒子初始速度 | 喷射范围大,飞离模型表面快 | 粒子贴附表面,流动感弱 | 0.5 到 5 |
| Start Size | 粒子尺寸 | 覆盖面积大,但容易遮挡模型细节 | 精细,但远处不可见 | 0.05 到 0.5 |
| Simulation Space | 模拟空间是 World 还是 Local | Local 跟随特效节点,World 不会自动跟随 | 取决于是否想让粒子留在原地 | 角色特效一般选 Local |
| Rate over Time | 每秒发射数量 | 粒子更密,开销增加 | 表现断续 | 50 到 500 |
这里的典型值适用于中等体型的人形角色,不是通用标准。调试时先把 Start Speed 调成 0,看粒子是否贴合表面;再把 Start Speed 逐步加大,观察粒子离开表面的方向。
4.3 自定义 Shader 读取顶点色
粒子系统的内置 Shader 不一定能充分使用顶点色。要精确控制效果,需要自定义 Shader。下面这段 ShaderLab 示例演示了如何读取顶点色并用 Red 通道控制发光强度。
Shader "VFX/VertexColorGlow" { Properties { _BaseColor ("Base Color", Color) = (0.2, 0.2, 0.2, 1) _GlowColor ("Glow Color", Color) = (1, 0.6, 0, 1) _GlowPower ("Glow Power", Range(0, 5)) = 1 } SubShader { Tags { "RenderType"="Transparent" "Queue"="Transparent" } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float4 color : COLOR0; }; struct v2f { float4 pos : SV_POSITION; float4 color : COLOR0; }; float4 _BaseColor; float4 _GlowColor; float _GlowPower; v2f vert(appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.color = v.color; return o; } fixed4 frag(v2f i) : SV_Target { float glow = saturate(i.color.r) * _GlowPower; fixed3 col = lerp(_BaseColor.rgb, _GlowColor.rgb, glow); return fixed4(col, i.color.a); } ENDCG } } }代码里最关键的一行是float4 color : COLOR0,它声明了顶点色输入。如果 Shader 里根本没有这个结构体字段,模型表面就算有顶点色也不会对渲染结果产生任何影响。用这个 Shader 可以验证:模型顶点色 Red 通道越高的区域,越接近_GlowColor指定的发光色。
这段示例面向内置渲染管线。URP 项目不建议直接搬运这段 Cg 代码,应当使用 URP Shader 模板和 HLSL 库。在 URP 项目里更快的做法是打开 Shader Graph,添加一个 Vertex Color 节点,把输出接到 Base Color 或 Emission 上。
4.4 Shader 变体与材质管理
Mesh-Based VFX 的 Shader 变体需要提前规划。同一个 Shader 如果既要支持顶点色,又要支持遮罩纹理,还要支持不同的混合模式,会产生较多变体。项目里出现内存陡增、构建时间变长时,先排查特效相关 Shader 的变体数量。
建议做法是:
- 特效 Shader 单独成一类,不跟普通角色材质共用。
- 用到顶点色时,把
#pragma multi_compile替换成固定关键词,尽量减少组合。 - 移动端不使用的特性尽量用
#pragma shader_feature_local隔离。
4.5 淡出控制用脚本完成,不要直接销毁
角色技能结束、物体消失时,特效应该有一个从浓到淡的消失过程。不要用Destroy直接删除特效对象,因为粒子系统即使在销毁瞬间也会产生视觉跳变。
正确做法是通过脚本插值控制两个参数:
using UnityEngine; public class VfxFadeOut : MonoBehaviour { public ParticleSystem targetParticle; public float fadeDuration = 0.5f; private float startEmissionRate; private float timer; void Start() { ParticleSystem.EmissionModule emission = targetParticle.emission; startEmissionRate = emission.rateOverTime.constant; } public void PlayFadeOut() { StartCoroutine(FadeOutRoutine()); } private System.Collections.IEnumerator FadeOutRoutine() { timer = 0f; while (timer < fadeDuration) { timer += Time.deltaTime; float t = Mathf.Clamp01(timer / fadeDuration); ParticleSystem.EmissionModule emission = targetParticle.emission; emission.rateOverTime = Mathf.Lerp(startEmissionRate, 0f, t); yield return null; } targetParticle.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); } }这段脚本的思路是把发射率从初始值平滑降到 0,等现有粒子自然消亡后再停止和清空。实际项目里还可以把材质 Alpha、粒子 Size 一起插值,效果更细腻。
5. 运行验证与性能观测:不能只看屏幕上有没有特效
5.1 功能验证清单
效果做出来后,至少要检查以下五个维度,不能只看粒子是否出现。
- 贴合度:旋转摄像机,从各个角度看粒子是否都在模型表面附近,而不是贴在某个固定平面。
- 动画跟随:播放奔跑、攻击、表情动画,确认粒子出生位置随骨骼姿势变化。
- 顶点色权重:模型阶段如果规划了 Red 控制强度,就要验证高 Red 区域是否确实更亮、低 Red 区域是否更暗。
- 区域裁剪:用第二套 UV 或顶点色作为遮罩时,验证特效没有出现在模型以外的区域。
- 生命周期:技能结束、角色死亡、物体隐藏时,特效是否能平滑消失,不残留也不跳变。
5.2 性能观测点
Unity 自带 Profiler 是主要观测工具,打开Window -> Analysis -> Profiler,重点观察两个窗口:
- CPU:脚本里的
LateUpdate、BakeMesh、粒子系统更新耗时。 - GPU:粒子渲染、Shader 复杂度、Overdraw。
除了这两个窗口,还需要检查以下指标:
| 指标 | 检查方式 | 关注点 |
|---|---|---|
| Draw Call | 打开 Frame Debugger | 特效是否产生了不必要的新批次 |
| 粒子数量 | Particle System 面板或 VFX Graph 输出 | 是否超过性能预算 |
| BakeMesh 调用频率 | 在脚本里加 Log 或 Profiler 标记 | 是否每帧调用,是否无意义刷新 |
| Shader 变体数 | 构建报告或 Shader Explorer | 特效相关变体是否过多 |
| 内存 | Memory Profiler | Mesh 缓存对象是否被反复创建 |
一个常见的性能坑是在 Update 里每帧都new Mesh(),这会产生大量 GC 压力。正确做法是在 Awake 里创建一次 Mesh 对象,后续每帧只复用这同一个缓存对象。
5.3 学习环境与生产环境的验证差异
学习环境的目标是快速看效果,可以牺牲性能。生产环境必须同时验证功能和资源。
学习环境下建议先使用低模 Cube 或标准资源验证技术链路,不要在美术模型未到位前就把效果完整做出来,否则后面换模型时会重复调参。
开发环境下要验证不同骨骼绑定、不同动画文件、不同 LOD 层级的兼容性。生产环境还需要额外验证移动端真机表现、网络资源加载、热更新资源的管理,以及特效 Shader 在不同 GPU 上的回退情况。
5.4 不同实现方案的效果对比
| 实现方案 | 贴合度 | 动画跟随 | 移动端压力 | 灵活度 | 实现成本 |
|---|---|---|---|---|---|
| 传统粒子叠加 | 低 | 差 | 中 | 中 | 低 |
| 纯 Shader 特效 | 高 | 差 | 低 | 低 | 中 |
| Particle System + Mesh Shape | 中 | 差 | 中 | 中 | 低 |
| VFX Graph + Mesh 采样 | 高 | 好 | 高 | 高 | 高 |
| Particle System + 自定义 Shader 读取顶点色 | 中 | 中 | 中 | 中 | 中 |
这里不是要说明哪一种方案绝对更好,而是要根据项目目标和平台选型。需要细腻的逐顶点控制,就选择 VFX Graph 方向;平台兼容性优先,就选择 Particle System 配合自定义 Shader。
6. 常见问题排查:漂移、朝向错、不显示、性能抖动
6.1 特效整体不显示
现象:场景一切正常,但特效完全没有粒子。
排查顺序:
- 检查 Mesh 是否为空。如果 VFX Graph 的
SourceMesh属性没有传入网格,粒子就没有出生位置。 - 检查材质 Shader 是否支持需要的数据。如果 Shader 要求顶点色,但模型没有顶点色,输出的颜色可能被裁剪为完全透明。
- 检查特效对象的 Layer 和相机剔除掩码。粒子渲染在默认层,而相机只渲染其他层时,粒子不会可见。
- 检查 Particle System 的
Max Particles。如果粒子数超出限制,新粒子会被丢弃,表现上就是没有粒子。 - 检查 VFX Graph 是否处于 Enable 状态,以及可视化面板里是否出现编译错误。
6.2 粒子位置漂移,不跟随骨骼
现象:粒子刚开始贴合角色,角色跑动后粒子留在原地或偏离。
可能原因:
- 模拟空间设置错误。角色特效应该使用 Local 模拟空间,如果粒子系统选了 World,粒子会留在世界坐标中,不跟随角色移动。
- 脚本没有在 LateUpdate 中执行 BakeMesh,导致网格数据停留在上一帧姿势。
bakeEveryFrames设置过大,动画变化没能及时反映到 Mesh 采样结果中。- VFX Graph 中采样的 Mesh 属性没有被脚本更新,一直使用初始静态网格。
处理方式:
- 把 Simulation Space 改为 Local。
- 确认脚本更新频率与动画变化匹配。
- 在 VFX Graph 属性面板中确认
SourceMesh每帧都在变化。
6.3 喷射方向反了或朝向错误
现象:粒子朝模型内部喷射,或者粒子朝向与期望方向相反。
排查路径:
- 先确认 Mesh 的法线是否朝外。在模型资源或 3D 软件里检查法线方向。
- 再确认采样法线时使用的空间。如果采样到的是本地空间法线,而粒子系统使用的是世界空间,方向就会错。
- 检查 VFX Graph 中法线乘的速度系数是否为正数。乘了一个负数系数,方向自然反转。
- 检查粒子渲染方式。Quad 渲染的粒子如果朝向设置不正确,看起来会变成线或消失。
6.4 顶点色没有生效
现象:模型在 3D 软件里刷了顶点色,但 Unity 里特效完全没有按权重出现。
原因通常有三个:
- 模型导入设置没有保留顶点色。FBX 导入后,在 Model 选项卡下确认相关选项。
- Shader 没有声明
COLOR0输入,就像上面示例 Shader 里缺失float4 color : COLOR0一样。 - 项目里美术制作流程本身没有把顶点色烘焙进模型资源,只是把颜色存在贴图里。
检查方式:在模型预览窗口打开顶点颜色通道,确认顶点色数据确实存在。再用一个简单的 Shader 把顶点色直接输出到 Base Color,如果画面呈现彩色,说明数据通道已打通。
6.5 移动端性能抖动
现象:PC 上表现正常,安卓真机上粒子数量不稳定,帧率波动大。
通常原因:
- 粒子数量超预算,CPU 更新压力大。
- VFX Graph 某些节点在移动端 GPU 不兼容,回退执行到 CPU 路径。
- Shader 变体数量过多,渲染时反复编译。
- 特效贴图没有合图,采样次数过多。
处理建议:
- 先做真机 Profile,拿到 CPU 和 GPU 的具体耗时,不要凭感觉猜。
- 把粒子数量降到移动端预算内。
- 把不支持的 VFX Graph 节点替换成 Particle System 或 Shader 方案。
- 特效贴图合并到图集中,减少采样切换。
- 移动端控制 Overdraw,缩小粒子尺寸并减少透明叠加层数。
6.6 问题速查表
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 特效完全不显示 | Mesh 为空或 Shader 裁剪 | 检查 VFX Graph 属性、材质 | 补材质和 Mesh 数据 |
| 粒子漂移 | 模拟空间 World | 检查 Simulation Space | 改为 Local |
| 不跟随骨骼 | BakeMesh 未调用 | 检查脚本 Update 顺序 | 在 LateUpdate 中烘焙 |
| 喷射方向反 | 法线空间或系数错误 | 检查法线采样节点 | 校正空间和系数 |
| 顶点色无效 | Shader 未读 COLOR0 | 检查 Shader 输入 | 添加顶点色声明 |
| 性能抖动 | 粒子数超预算 | Profiler 看 CPU/GPU | 降粒子/换方案 |
7. 生产落地建议与扩展方向
7.1 从 Demo 到生产需要补齐的工程能力
Demo 和生产的差距通常不在这里技术路线本身,而在资源管理和异常处理。
生产环境要做的事包括:
- 资源版本管理:特效材质、Shader、Mesh 缓存对象要有明确的命名和目录规范。
- 按距离和可见性淘汰:角色远离摄像机时降低粒子量,完全不可见时停掉特效。
- 对象池管理:角色经常被创建和销毁时,特效对象要复用,避免反复实例化造成 GC。
- LOD 策略:高精度角色使用完整顶点采样,低精度角色切换到普通贴图特效。
- 异常处理:
BakeMesh前后检查 SkinnedMeshRenderer 是否被销毁,避免空引用。 - 日志和监控:特效异常时输出 Error 日志,记录粒子数和脚本执行耗时。
7.2 发布前检查清单
制作完成后,在提交版本前建议按这个清单过一遍,可以显著减少发布后的意外问题。
| 检查项 | 检查方式 | 失败时的处理 | |