Unity ShaderGraph实战:角色灰飞烟灭特效制作全流程解析
2026/7/22 6:05:04 网站建设 项目流程

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:检查模型是否有第二套UV(UV1)。溶解用的噪波贴图需要采样坐标,如果模型只有一套UV且用于了主纹理,直接使用可能会拉伸。如果有UV1,用它来采样噪波图通常效果更好,能避免纹理拉伸。如果没有,你可能需要确保主UV布局合理,或者考虑在Shader中简单使用模型的世界坐标或物体坐标来驱动噪波,但这可能产生不同的视觉效果。
  • 模型比例与位置:将模型摆放在场景原点(0,0,0)附近,并调整到一个合适的大小。这有助于我们在世界空间中进行计算时,参数更容易调整。

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。

  1. 创建属性(Properties):我们需要一些可在材质面板上调节的参数。点击Blackboard区域,创建以下属性:
    • _NoiseTex(Texture 2D): 用于溶解的噪波贴图。
    • _DissolveThreshold(Vector1): 溶解阈值,范围0到1。这将是我们控制溶解进度的主参数。
    • _EdgeWidth(Vector1): 溶解边缘的宽度。
    • _EdgeColor(Color): 溶解边缘的颜色(例如橙色或亮蓝色,模拟能量消散)。
    • _EdgeIntensity(Vector1): 边缘颜色的强度。
  2. 采样噪波图:添加一个Sample Texture 2D节点,将_NoiseTex属性连接到它的Texture输入口。它的UV输入口需要连接坐标。这里我选择使用模型的第二套UV(UV1),通过UV节点并选择通道1来获取。如果模型没有UV1,可以连接Object Position节点并经过一些处理(比如除以一个缩放系数)来模拟,但效果不同。
  3. 阈值比较与裁剪:添加一个Step节点。将_DissolveThreshold属性连接到它的Edge输入,将Sample Texture 2D节点的R通道(灰度图所以用R)连接到In输入。Step节点会输出一个非0即1的值:当In>=Edge时输出1(保留),否则输出0(溶解)。我们将这个结果连接到一个Clip节点的Threshold输入。Clip节点会丢弃所有输入值小于0.5的片元(像素),从而实现硬裁剪。这样,当噪波值小于阈值时,该片元就被直接剪掉,形成透明窟窿。

4.2 添加溶解边缘效果

硬裁剪的边缘很生硬,我们需要一个过渡带。

  1. 计算边缘区域:添加一个Smoothstep节点。它的三个输入分别是Edge1,Edge2,In。我们将_DissolveThreshold连接到Edge1,将_DissolveThreshold加上一个很小的值(比如0.01,可通过_EdgeWidth控制)连接到Edge2(这里需要一个Add节点)。In依然是噪波采样的R通道。Smoothstep会输出一个在Edge1Edge2之间平滑过渡的0-1值,这个过渡区就是我们的溶解边缘。
  2. 混合边缘颜色:添加一个Lerp(线性插值) 节点。A端口连接模型原本的纹理颜色(通过另一个Sample Texture 2D采样主贴图),B端口连接_EdgeColor属性,T端口连接上一步Smoothstep的输出。这样,在边缘过渡区内,颜色会从原本纹理色渐变到边缘色。再将这个混合结果乘以_EdgeIntensity来控制发光强弱。
  3. 输出到主节点:将处理后的颜色连接到Master StackFragment下的Base Color。将_DissolveThreshold通过一个Remap节点(例如从(0,1)映射到(1,0))或直接用One Minus节点处理一下,连接到Alpha输入,可以实现整体透明度随溶解而变化(可选,因为裁剪已经产生透明)。Alpha Clipping Threshold可以保持默认或微调。

至此,一个基础的溶解Shader就完成了。在材质面板上滑动_DissolveThreshold,你应该能看到模型随着阈值变化而被噪波图“啃食”的效果。

4.3 关键技巧:顶点位置信息输出

为了驱动粒子,我们需要知道哪些顶点被“溶解”了。一个常见思路是在顶点即将被裁剪(即噪波值非常接近阈值)时,将该顶点的世界坐标输出。

  1. 在ShaderGraph中,添加一个Position节点,设置空间为World
  2. 我们需要一个判断条件。可以计算噪波值与阈值的绝对差值:abs(noise.r - _DissolveThreshold)。当这个差值小于一个很小的范围(比如0.01)时,我们认为这个顶点处于“溶解边缘”。
  3. 使用Compare节点(小于比较)。将上一步的差值连接到A,一个固定小值(如0.01)连接到B。如果A < B,则输出True(即1)。
  4. 将这个比较结果(一个0或1的值)作为一个自定义输出。在BlackboardGraph Inspector中,创建一个新的Graph Output,命名为IsDissolving,类型为Float。在节点图中,将比较结果连接到这个输出节点。
  5. 同时,我们也可以将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 = GetComponentInChildren<MeshFilter>(); 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方法是一个性能较低的简化模拟。在实际追求高质量的项目中,可以考虑以下优化方案:

  1. 使用Compute Shader:将模型顶点数据、溶解阈值传入Compute Shader,在GPU端并行计算哪些顶点需要发射粒子,并将这些顶点的位置信息写入一个ComputeBuffer。然后在C#脚本中读取这个Buffer,批量传递给粒子系统。这是性能最好的方式,但实现复杂度较高。
  2. 预生成发射点:在特效开始前,根据模型的网格,预计算一系列分布在模型表面的点(可以是顶点,也可以是通过射线在表面采样的点)。溶解过程中,根据当前阈值和噪波图(需要在C#端也采样同一张噪波图),判断这些预生成点是否处于“已溶解”区域,如果是,则在该点发射粒子。这样避免了每帧遍历所有顶点。
  3. 简化发射逻辑:如果视觉效果要求不那么精确,可以不用严格关联每个溶解点。改为在模型位置(或骨骼关节位置)直接发射粒子,同时通过调整粒子系统的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、遍历所有顶点等重操作。所有引用在StartAwake中缓存,发射逻辑使用对象池和批处理。
  • 烘焙静态数据:如果模型是静态的,预计算的发射点数据可以序列化保存,避免运行时计算。

6.3 常见问题与排查技巧

  1. 模型溶解时,边缘闪烁或出现硬块:

    • 原因:可能是噪波贴图过滤模式设置不当,或者Clip阈值附近精度问题。
    • 解决:将噪波贴图的Filter Mode设置为BilinearTrilinear,确保平滑采样。在ShaderGraph中,将_DissolveThreshold与噪波值的比较,从Step改为Smoothstep并设置一个很小的平滑范围,可以柔化裁剪边缘,避免闪烁。
  2. 粒子发射位置偏离模型:

    • 原因:脚本中获取的顶点坐标是模型本地空间坐标,没有考虑模型的旋转、缩放。
    • 解决:确保使用transform.TransformPoint(vertexLocalPos)将本地坐标正确转换到世界坐标。检查粒子系统是否作为模型的子物体,其本地变换是否归零。
  3. 溶解完成后,模型残留透明部分或阴影:

    • 原因:材质透明度或渲染队列设置问题。
    • 解决:在ShaderGraph的Master Stack中,确保Surface设置为Transparent(如果使用透明度混合)或Alpha Clipping已启用。调整Alpha Clipping Threshold。对于阴影,如果不需要,可以在材质的Inspector面板中禁用Cast ShadowsReceive Shadows
  4. 特效在移动设备上帧率下降严重:

    • 原因:粒子数量过多或Shader过于复杂。
    • 解决:严格遵循性能优化要点。使用Unity的Profiler工具(特别是RenderingCPU Usage模块)定位性能瓶颈。大幅减少粒子最大数量,简化粒子物理计算(如关闭碰撞、减少力场),简化溶解Shader(例如减少纹理采样次数)。
  5. 希望粒子从溶解边缘更精确地发射:

    • 原因:随机顶点发射法不够精确。
    • 解决:转向“预生成发射点+GPU信息查询”方案。这需要更高级的编程,核心思路是:在C#端,也访问模型的网格和材质,通过MaterialPropertyBlock获取或计算每个预生成点对应的噪波图值,只有当该值接近当前阈值时,才允许在该点发射粒子。这能实现粒子紧紧“贴”在溶解前沿生成的效果。

通过以上步骤,一个从《原神》风格模型出发,利用ShaderGraph实现溶解,并驱动粒子飘散的“灰飞烟灭”特效就基本完成了。整个过程融合了着色器编程、粒子系统艺术和性能优化思维。最关键的不是照搬参数,而是理解每个环节背后的原理——为什么用噪波图、为什么用Step/Smoothstep、如何将GPU的数据状态反馈给CPU的粒子逻辑。掌握了这些,你就能举一反三,创造出各种风格的角色消失、建筑崩塌、物品分解等震撼的视觉特效了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询