Unity ShaderGraph实战:角色灰飞烟灭特效制作全流程解析

Unity ShaderGraph实战:角色灰飞烟灭特效制作全流程解析 1. 项目概述从“灰飞烟灭”到ShaderGraph实战最近在鼓捣一些角色特效总想着怎么能让角色消失得更有“仪式感”而不是简单地淡出或者播放一个预设动画。看到《原神》里一些角色被击败后化作光点消散的效果就觉得特别带感那种从实体逐渐分解、化为尘埃粒子随风飘散的感觉既有视觉冲击力又能很好地烘托战斗氛围。于是我决定自己动手在Unity里用ShaderGraph复现一个类似的“灰飞烟灭”特效。这个项目的核心就是利用ShaderGraph的可视化着色器编辑能力结合粒子系统驱动一个从外部导入的《原神》风格角色模型实现从完整模型到完全粒子化的动态消散过程。整个过程不涉及复杂的脚本编写主要依靠节点连线来创造魔法非常适合想深入理解Shader和粒子特效交互的开发者。为什么选择ShaderGraph因为它把原本需要写在代码里的着色器逻辑变成了可视化的节点图大大降低了图形编程的门槛。你可以像搭积木一样连接各种功能节点实时预览效果迭代速度飞快。而“灰飞烟灭”这个效果本质上是一个顶点动画和透明度控制的游戏非常适合用ShaderGraph来定义模型如何“溶解”以及溶解后生成什么样的粒子。我们将从处理模型、构建着色器、驱动粒子三个核心环节一步步拆解这个炫酷特效的实现。2. 核心思路与方案设计拆解要实现“灰飞烟灭”不能简单地把模型隐藏然后播放一个独立的粒子特效那样会缺乏模型与粒子之间的视觉关联显得很假。我们的目标是让粒子看起来真的是从模型身上“剥落”并飘散出去的。因此整个方案可以拆解为三个环环相扣的阶段第一阶段模型的溶解与消隐。这是特效的起点。我们需要在Shader中定义一个“溶解”过程。通常我们会使用一张噪波贴图Noise Texture作为溶解的“地图”。通过一个从0到1逐渐变化的阈值去采样这张噪波图。对于模型上的每个像素更准确说是片元如果噪波图在该位置的值小于当前阈值那么这个像素就会被“溶解”掉——表现为变得完全透明或被裁剪掉。随着阈值从0增加到1模型就会从某些区域开始像被风沙侵蚀一样逐渐消失。这个溶解的边缘往往还需要一点颜色效果比如发光或者烧焦感来增强视觉细节。第二阶段溶解信息驱动粒子生成。这是连接模型与粒子的关键。模型不会凭空消失它的“物质”需要转化为粒子。我们可以在Shader的顶点着色器阶段做文章。当某个顶点因为溶解阈值达到而被标记为“即将消失”时我们可以将这个顶点的位置、法线方向等信息输出。在Unity中我们可以通过脚本或者更优雅地通过一些扩展方法将这些在Shader中计算好的数据例如每个顶点是否被溶解、它的世界坐标传递出来作为粒子系统的发射源。这样粒子就会精准地从模型正在消失的部位生成实现了“从哪里消失就从哪里产生粒子”的因果关系。第三阶段粒子的动态表现与艺术化调整。粒子生成后需要赋予它们生命。我们需要设计粒子的运动通常是先有一个向上的初速度模拟升腾然后受到模拟风力的影响进行随机、缓慢的飘散。同时粒子的颜色应该继承或近似于模型溶解部位的颜色大小可以随机并且在其生命周期内透明度会逐渐衰减直至消失。通过调整粒子系统的发射速率、生命周期、大小、速度等参数我们可以控制消散的密度、速度和整体氛围是急促的爆散还是缓慢的升华全在于此。这个方案的优势在于逻辑清晰且完全在Unity的可视化或半可视化框架内完成ShaderGraph Particle System。它避免了手写复杂Shader代码同时通过数据传递将模型着色器与粒子系统紧密耦合实现了高质量、可定制的角色消散特效。3. 前期准备模型与工具检查清单在开始连线节点之前充分的准备能避免后续很多坑。这个特效对模型和Unity工程设置有一些基本要求。3.1 模型资产处理要点首先你需要一个角色模型。为了复现《原神》的风格我找了一个类似的二次元风格角色模型注意确保它拥有正确的骨骼和动画虽然本项目不直接播放动画但规范的模型利于处理。导入Unity后检查以下几点材质与Shader模型通常自带材质。为了应用我们自定义的溶解Shader你需要创建新的材质球并指定为我们即将用ShaderGraph制作的着色器。记得备份原始材质以便切换。顶点色与UV检查模型是否有第二套UVUV1。溶解用的噪波贴图需要采样坐标如果模型只有一套UV且用于了主纹理直接使用可能会拉伸。如果有UV1用它来采样噪波图通常效果更好能避免纹理拉伸。如果没有你可能需要确保主UV布局合理或者考虑在Shader中简单使用模型的世界坐标或物体坐标来驱动噪波但这可能产生不同的视觉效果。模型比例与位置将模型摆放在场景原点000附近并调整到一个合适的大小。这有助于我们在世界空间中进行计算时参数更容易调整。3.2 Unity工程与插件配置渲染管线确认ShaderGraph与Unity的渲染管线绑定。你需要明确使用的是URP通用渲染管线还是HDRP高清渲染管线。两者都支持ShaderGraph但节点库和部分功能有差异。我使用的是URP因为它更轻量且兼容性广。确保你的项目是一个URP项目可在Package Manager中安装Universal RP并且场景中的灯光、后期处理等配置都是URP兼容的。创建ShaderGraph在Project窗口中右键选择Create - Shader - Universal Render Pipeline - Shader Graph 创建一个空白的ShaderGraph文件命名为“CharacterDissolve”。准备噪波贴图溶解效果的视觉丰富度很大程度上依赖于噪波贴图。你可以使用Photoshop、Substance Designer等工具制作也可以直接在Unity Asset Store搜索“Noise Texture”找到许多免费或付费的、各种类型的噪波图如Perlin Noise, Voronoi等。准备一张灰度噪波图白色区域代表“保留”黑色区域代表“溶解”灰色过渡区域会产生溶解边缘。将贴图导入Unity记得将其Texture Type设置为Default并勾选sRGB (Color Texture)选项以确保在Shader中颜色采样正确。注意如果你的模型面数极高在溶解时可能会因为顶点过多导致向粒子系统传递数据时产生性能开销。对于移动平台可以考虑使用LOD多层次细节模型在特效触发时切换到面数较低的模型版本进行计算。4. ShaderGraph核心节点网络构建这是整个特效的“大脑”。我们将在ShaderGraph编辑器中通过连接节点来构建溶解逻辑。4.1 建立溶解基础框架打开创建好的“CharacterDissolve” ShaderGraph。创建属性Properties我们需要一些可在材质面板上调节的参数。点击Blackboard区域创建以下属性_NoiseTex(Texture 2D): 用于溶解的噪波贴图。_DissolveThreshold(Vector1): 溶解阈值范围0到1。这将是我们控制溶解进度的主参数。_EdgeWidth(Vector1): 溶解边缘的宽度。_EdgeColor(Color): 溶解边缘的颜色例如橙色或亮蓝色模拟能量消散。_EdgeIntensity(Vector1): 边缘颜色的强度。采样噪波图添加一个Sample Texture 2D节点将_NoiseTex属性连接到它的Texture输入口。它的UV输入口需要连接坐标。这里我选择使用模型的第二套UVUV1通过UV节点并选择通道1来获取。如果模型没有UV1可以连接Object Position节点并经过一些处理比如除以一个缩放系数来模拟但效果不同。阈值比较与裁剪添加一个Step节点。将_DissolveThreshold属性连接到它的Edge输入将Sample Texture 2D节点的R通道灰度图所以用R连接到In输入。Step节点会输出一个非0即1的值当InEdge时输出1保留否则输出0溶解。我们将这个结果连接到一个Clip节点的Threshold输入。Clip节点会丢弃所有输入值小于0.5的片元像素从而实现硬裁剪。这样当噪波值小于阈值时该片元就被直接剪掉形成透明窟窿。4.2 添加溶解边缘效果硬裁剪的边缘很生硬我们需要一个过渡带。计算边缘区域添加一个Smoothstep节点。它的三个输入分别是Edge1,Edge2,In。我们将_DissolveThreshold连接到Edge1将_DissolveThreshold加上一个很小的值比如0.01可通过_EdgeWidth控制连接到Edge2这里需要一个Add节点。In依然是噪波采样的R通道。Smoothstep会输出一个在Edge1和Edge2之间平滑过渡的0-1值这个过渡区就是我们的溶解边缘。混合边缘颜色添加一个Lerp(线性插值) 节点。A端口连接模型原本的纹理颜色通过另一个Sample Texture 2D采样主贴图B端口连接_EdgeColor属性T端口连接上一步Smoothstep的输出。这样在边缘过渡区内颜色会从原本纹理色渐变到边缘色。再将这个混合结果乘以_EdgeIntensity来控制发光强弱。输出到主节点将处理后的颜色连接到Master Stack中Fragment下的Base Color。将_DissolveThreshold通过一个Remap节点例如从(0,1)映射到(1,0)或直接用One Minus节点处理一下连接到Alpha输入可以实现整体透明度随溶解而变化可选因为裁剪已经产生透明。Alpha Clipping Threshold可以保持默认或微调。至此一个基础的溶解Shader就完成了。在材质面板上滑动_DissolveThreshold你应该能看到模型随着阈值变化而被噪波图“啃食”的效果。4.3 关键技巧顶点位置信息输出为了驱动粒子我们需要知道哪些顶点被“溶解”了。一个常见思路是在顶点即将被裁剪即噪波值非常接近阈值时将该顶点的世界坐标输出。在ShaderGraph中添加一个Position节点设置空间为World。我们需要一个判断条件。可以计算噪波值与阈值的绝对差值abs(noise.r - _DissolveThreshold)。当这个差值小于一个很小的范围比如0.01时我们认为这个顶点处于“溶解边缘”。使用Compare节点小于比较。将上一步的差值连接到A一个固定小值如0.01连接到B。如果A B则输出True即1。将这个比较结果一个0或1的值作为一个自定义输出。在Blackboard的Graph Inspector中创建一个新的Graph Output命名为IsDissolving类型为Float。在节点图中将比较结果连接到这个输出节点。同时我们也可以将World Position直接作为另一个Graph Output输出命名为DissolvePointWS。注意直接在Shader中每帧输出所有顶点的位置数据到脚本是低效的。更常见的做法是我们只在Shader中标记通过IsDissolving然后在脚本中通过射线检测或根据阈值在模型表面随机生成点来模拟粒子发射源。但为了演示最直接的思路这里先介绍Shader输出信息的概念。在实际优化中我们可能会在脚本中根据_DissolveThreshold和模型网格数据动态计算发射点。5. 粒子系统设计与驱动逻辑实现模型溶解的视觉部分完成后我们需要让粒子“冒出来”。5.1 创建与配置粒子系统在角色模型对象下创建一个子对象添加Particle System组件。发射Emission将发射速率Rate over Time设为0因为我们不持续发射。通过脚本控制Bursts来在特定时刻爆发式发射或者使用Rate over Distance为0改为通过脚本调用Emit()方法。形状Shape选择Mesh并指定为你的角色模型对应的网格Mesh。这样粒子会从整个模型表面发射但我们需要更精确的控制。更高级的做法是使用Skinned Mesh Renderer作为形状可以跟随动画变形但本项目为简化使用静态Mesh。生命周期Lifetime设置一个范围如2到4秒让粒子有足够的飘散时间。速度Velocity初始速度可以设置一个向上的主方向如(0, 2, 0)并添加一些随机性随机在XYZ方向±0.5。为了模拟飘散可以启用Limit Velocity over Lifetime模块并设置速度阻尼让粒子慢慢减速。颜色随生命周期Color over Lifetime设置一个从白色或模型主色渐变到完全透明的颜色条实现粒子淡出。大小随生命周期Size over Lifetime可以设置为先略微增大再减小模拟膨胀后消散。渲染Renderer选择合适的渲染模式如Billboard始终面向相机材质使用一个简单的粒子着色器如Particles/Standard Unlit并赋予一个柔和的粒子贴图一个圆形或星形的渐变软边贴图。5.2 编写驱动脚本C#创建一个C#脚本命名为DissolveController挂载到角色或一个控制对象上。using UnityEngine; public class DissolveController : MonoBehaviour { public Material dissolveMaterial; // 关联我们的溶解Shader材质 public ParticleSystem dissolveParticles; // 关联粒子系统 public float dissolveDuration 3.0f; // 总溶解时间 private float currentDissolveThreshold 0f; private bool isDissolving false; void Start() { // 初始化材质阈值 if (dissolveMaterial ! null) { dissolveMaterial.SetFloat(_DissolveThreshold, 0f); } // 停止粒子发射等待触发 if (dissolveParticles ! null) { dissolveParticles.Stop(); } } void Update() { if (isDissolving) { // 更新溶解进度 currentDissolveThreshold Time.deltaTime / dissolveDuration; currentDissolveThreshold Mathf.Clamp01(currentDissolveThreshold); if (dissolveMaterial ! null) { dissolveMaterial.SetFloat(_DissolveThreshold, currentDissolveThreshold); } // 核心根据阈值发射粒子简化版 // 这里是一个简化的模拟。理想情况下应该从Shader获取“溶解点”信息。 // 我们改为在模型表面随机生成点其概率与溶解进度相关。 EmitParticlesBasedOnThreshold(currentDissolveThreshold); if (currentDissolveThreshold 1.0f) { isDissolving false; // 溶解完成可以销毁或隐藏模型 gameObject.SetActive(false); } } } // 触发开始溶解 public void StartDissolve() { if (!isDissolving) { isDissolving true; currentDissolveThreshold 0f; if (dissolveParticles ! null) { dissolveParticles.Play(); } } } // 基于阈值的粒子发射模拟 private void EmitParticlesBasedOnThreshold(float threshold) { if (dissolveParticles null) return; // 这是一个性能开销较大的简化示例仅用于说明逻辑。 // 实际项目应使用对象池并在每一帧控制发射数量。 MeshFilter meshFilter GetComponentInChildrenMeshFilter(); if (meshFilter null) return; Mesh mesh meshFilter.sharedMesh; Vector3[] vertices mesh.vertices; int emitCount Mathf.RoundToInt(vertices.Length * 0.01f * threshold); // 每帧发射顶点数的1% * 阈值 for (int i 0; i emitCount; i) { int randomIndex Random.Range(0, vertices.Length); // 将顶点从本地坐标转换到世界坐标 Vector3 worldPos transform.TransformPoint(vertices[randomIndex]); // 创建一个发射参数 ParticleSystem.EmitParams emitParams new ParticleSystem.EmitParams(); emitParams.position worldPos; emitParams.velocity new Vector3( Random.Range(-0.5f, 0.5f), Random.Range(1.0f, 3.0f), Random.Range(-0.5f, 0.5f) ); dissolveParticles.Emit(emitParams, 1); } } }这个脚本提供了一个基础框架。它驱动材质的_DissolveThreshold属性从0变化到1同时在每一帧根据当前阈值从模型网格的顶点中随机选取一些点作为粒子发射源。StartDissolve()方法可以绑定到角色血量归零等游戏事件上。5.3 更优的数据传递方案探讨上述脚本中的EmitParticlesBasedOnThreshold方法是一个性能较低的简化模拟。在实际追求高质量的项目中可以考虑以下优化方案使用Compute Shader将模型顶点数据、溶解阈值传入Compute Shader在GPU端并行计算哪些顶点需要发射粒子并将这些顶点的位置信息写入一个ComputeBuffer。然后在C#脚本中读取这个Buffer批量传递给粒子系统。这是性能最好的方式但实现复杂度较高。预生成发射点在特效开始前根据模型的网格预计算一系列分布在模型表面的点可以是顶点也可以是通过射线在表面采样的点。溶解过程中根据当前阈值和噪波图需要在C#端也采样同一张噪波图判断这些预生成点是否处于“已溶解”区域如果是则在该点发射粒子。这样避免了每帧遍历所有顶点。简化发射逻辑如果视觉效果要求不那么精确可以不用严格关联每个溶解点。改为在模型位置或骨骼关节位置直接发射粒子同时通过调整粒子系统的Shape模块如设置为Sphere并限制在模型Bounds内来大致模拟从身体散发的感觉。配合溶解Shader视觉上也能接受。6. 效果整合、参数调优与性能考量将Shader材质赋给角色模型将脚本挂好并关联材质和粒子系统运行游戏并调用StartDissolve()方法你应该能看到角色逐渐溶解并伴随粒子飘散的效果。但这只是初版距离“炫酷”还有距离需要精心调优。6.1 视觉艺术化调参溶解噪波图尝试不同的噪波图。Voronoi噪波会产生细胞状的溶解效果Perlin噪波更柔和、云状。可以混合多张噪波图或者对UV进行旋转、缩放、平移动画让溶解边缘动态变化更具活性。边缘效果调整_EdgeWidth和_EdgeColor。尝试让边缘颜色随着溶解进度变化例如从橙红色渐变为蓝白色。可以在ShaderGraph中加入Time节点让边缘颜色或强度微微脉动。粒子系统大小与速度让粒子初始大小更随机生命周期内先稍微变大再迅速变小。速度可以加上一些噪声力场Noise模块让飘散路径更自然、随机避免过于规律。颜色粒子颜色不要用纯白。可以尝试从模型溶解部位的纹理中采样颜色这需要更复杂的脚本将颜色信息传递给粒子或者简单地设置一个与溶解边缘色相近的渐变。旋转启用粒子的随机旋转并让其在整个生命周期中缓慢自旋。子发射器可以为主粒子添加一个子发射器Sub Emitter当主粒子消亡时发射更细小、更快的次级粒子模拟最终爆散成更细微尘埃的感觉。6.2 性能优化要点粒子数量控制这是性能大头。通过脚本严格控制每帧发射的粒子数量。使用粒子系统的Max Particles属性进行硬限制。对于移动平台数量可能需要控制在几百以内。模型LOD如前所述对高模角色使用低模版本进行溶解计算和粒子发射。Shader复杂度检查你的ShaderGraph。避免在片元着色器中使用过多复杂的数学运算或多次纹理采样。确保不必要的节点已被优化。对于溶解效果主要开销在纹理采样和Clip操作。数据传递优化务必避免在Update每帧中执行GetComponent、遍历所有顶点等重操作。所有引用在Start或Awake中缓存发射逻辑使用对象池和批处理。烘焙静态数据如果模型是静态的预计算的发射点数据可以序列化保存避免运行时计算。6.3 常见问题与排查技巧模型溶解时边缘闪烁或出现硬块原因可能是噪波贴图过滤模式设置不当或者Clip阈值附近精度问题。解决将噪波贴图的Filter Mode设置为Bilinear或Trilinear确保平滑采样。在ShaderGraph中将_DissolveThreshold与噪波值的比较从Step改为Smoothstep并设置一个很小的平滑范围可以柔化裁剪边缘避免闪烁。粒子发射位置偏离模型原因脚本中获取的顶点坐标是模型本地空间坐标没有考虑模型的旋转、缩放。解决确保使用transform.TransformPoint(vertexLocalPos)将本地坐标正确转换到世界坐标。检查粒子系统是否作为模型的子物体其本地变换是否归零。溶解完成后模型残留透明部分或阴影原因材质透明度或渲染队列设置问题。解决在ShaderGraph的Master Stack中确保Surface设置为Transparent如果使用透明度混合或Alpha Clipping已启用。调整Alpha Clipping Threshold。对于阴影如果不需要可以在材质的Inspector面板中禁用Cast Shadows和Receive Shadows。特效在移动设备上帧率下降严重原因粒子数量过多或Shader过于复杂。解决严格遵循性能优化要点。使用Unity的Profiler工具特别是Rendering和CPU Usage模块定位性能瓶颈。大幅减少粒子最大数量简化粒子物理计算如关闭碰撞、减少力场简化溶解Shader例如减少纹理采样次数。希望粒子从溶解边缘更精确地发射原因随机顶点发射法不够精确。解决转向“预生成发射点GPU信息查询”方案。这需要更高级的编程核心思路是在C#端也访问模型的网格和材质通过MaterialPropertyBlock获取或计算每个预生成点对应的噪波图值只有当该值接近当前阈值时才允许在该点发射粒子。这能实现粒子紧紧“贴”在溶解前沿生成的效果。通过以上步骤一个从《原神》风格模型出发利用ShaderGraph实现溶解并驱动粒子飘散的“灰飞烟灭”特效就基本完成了。整个过程融合了着色器编程、粒子系统艺术和性能优化思维。最关键的不是照搬参数而是理解每个环节背后的原理——为什么用噪波图、为什么用Step/Smoothstep、如何将GPU的数据状态反馈给CPU的粒子逻辑。掌握了这些你就能举一反三创造出各种风格的角色消失、建筑崩塌、物品分解等震撼的视觉特效了。