第10章 粒子系统与VFX Graph
理论讲解
10.1 URP粒子系统Shader配置
Unity的粒子系统在URP中需要使用专门的Shader来确保正确的光照和渲染效果。URP提供了多种粒子系统Shader,以满足不同的视觉效果需求。
粒子系统Shader类型
- Particles/Lit:支持完整光照的粒子Shader
- Particles/Unlit:无光照的粒子Shader,性能更好
- Particles/SimpleLit:简化的光照粒子Shader
- VFX Graph专用Shader:为Visual Effect Graph优化的Shader
URP粒子系统特性
- 光照集成:与URP光照系统无缝集成
- 后处理兼容:支持URP后处理效果
- 性能优化:针对移动平台和VR优化
- GPU Instancing:支持GPU实例化以提高性能
10.2 Visual Effect Graph集成
Visual Effect Graph (VFX Graph) 是Unity的可视化粒子系统工具,它与URP深度集成,提供了强大的视觉效果创作能力。
VFX Graph核心概念
- System:整个视觉效果的容器
- Block:定义效果的各个阶段(初始化、更新、输出)
- Context:定义执行上下文(Spawn、Update、Output等)
- Attribute:粒子的属性(位置、速度、颜色等)
VFX Graph与URP的集成
- 渲染管线兼容:VFX Graph原生支持URP
- 光照交互:支持与URP光照系统交互
- 后处理效果:兼容URP后处理系统
- 材质系统:使用URP材质系统
10.3 粒子光照与阴影接收
在URP中,粒子系统可以接收场景光照并产生阴影,这为创建更真实的视觉效果提供了可能。
光照接收
- 主光源光照:粒子可以接收主光源的光照
- 附加光源光照:支持接收附加光源的光照
- 环境光照:支持环境光照(反射探针)
阴影接收
- 阴影贴图采样:粒子可以采样阴影贴图
- 软阴影:支持软阴影效果
- 阴影距离:可配置阴影接收距离
10.4 GPU Instancing优化
GPU Instancing是优化大量相似粒子渲染的关键技术,在URP中得到了很好的支持。
GPU Instancing优势
- 减少绘制调用:显著减少Draw Call数量
- 提高渲染性能:在大量粒子情况下性能提升明显
- 内存优化:减少内存使用
实现要求
- 相同的网格和材质
- 合适的Shader支持
- 正确的材质属性设置
10.5 半透明粒子渲染顺序
半透明粒子的渲染顺序对视觉效果至关重要,需要特别注意渲染队列和排序设置。
渲染队列管理
- Transparent队列:半透明粒子的标准队列
- 渲染顺序:从后往前渲染以确保正确混合
- 排序方法:基于距离的排序
10.6 性能优化建议
粒子系统优化
- 粒子数量控制:限制同时存在的粒子数量
- 纹理优化:使用适当大小的纹理
- Shader选择:根据需求选择合适的Shader复杂度
- 更新频率:优化粒子更新逻辑
VFX Graph优化
- Context优化:减少不必要的计算
- Attribute管理:只使用必要的属性
- Spawn优化:优化粒子生成逻辑
- LOD系统:实现细节层次系统
代码示例
10.7 粒子系统控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
| using UnityEngine; using UnityEngine.Rendering.Universal;
public class ParticleSystemController : MonoBehaviour { [Header("Particle System References")] public ParticleSystem particleSystem; public ParticleSystemRenderer particleRenderer; [Header("Light Interaction")] public bool receiveLights = true; public bool receiveShadows = true; public bool useGPUInstancing = true; [Header("Performance Settings")] public int maxParticles = 1000; public float emissionRate = 10f; [Header("Material Configuration")] public Material litMaterial; public Material unlitMaterial; private ParticleSystem.MainModule mainModule; private ParticleSystem.EmissionModule emissionModule; private ParticleSystemRenderer renderer; void Start() { InitializeParticleSystem(); ConfigureURPSettings(); } private void InitializeParticleSystem() { if (particleSystem == null) particleSystem = GetComponent<ParticleSystem>(); if (particleRenderer == null) particleRenderer = GetComponent<ParticleSystemRenderer>(); if (particleSystem != null) { mainModule = particleSystem.main; emissionModule = particleSystem.emission; renderer = particleSystem.GetComponent<ParticleSystemRenderer>(); } mainModule.maxParticles = maxParticles; emissionModule.rateOverTime = emissionRate; } private void ConfigureURPSettings() { if (renderer != null) { renderer.allowRoll = true; if (useGPUInstancing && SystemInfo.supportsInstancing) { renderer.enableGPUInstancing = true; } if (receiveLights) { renderer.supportsLighting = true; } if (receiveLights && litMaterial != null) { renderer.material = litMaterial; } else if (unlitMaterial != null) { renderer.material = unlitMaterial; } } } public void SetEmissionRate(float rate) { if (emissionModule.rateOverTime.constant != rate) { emissionModule.rateOverTime = rate; } } public void SetPaused(bool paused) { if (particleSystem != null) { particleSystem.Pause(paused); particleSystem.simulationSpeed = paused ? 0f : 1f; } } public void Play() { if (particleSystem != null) { particleSystem.Play(); } } public void Stop() { if (particleSystem != null) { particleSystem.Stop(); } } public int GetParticleCount() { if (particleSystem != null) { return particleSystem.particleCount; } return 0; } public void SetStartColor(Color color) { mainModule.startColor = color; } public void SetStartSize(float size) { mainModule.startSize = size; } public void SetStartLifetime(float lifetime) { mainModule.startLifetime = lifetime; } public void SwitchToLitMaterial() { if (litMaterial != null && renderer != null) { renderer.material = litMaterial; } } public void SwitchToUnlitMaterial() { if (unlitMaterial != null && renderer != null) { renderer.material = unlitMaterial; } } }
|
10.8 VFX Graph控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
| using UnityEngine; using UnityEngine.VFX;
public class VFXGraphController : MonoBehaviour { [Header("VFX Graph References")] public VisualEffect visualEffect; [Header("Performance Settings")] public int targetSpawnCount = 100; public float simulationSpeed = 1.0f; public bool useGPUInstancing = true; [Header("Parameter Controls")] public float intensity = 1.0f; public Color mainColor = Color.white; public float size = 1.0f; public float speed = 1.0f; [Header("LOD Settings")] public float lodDistance = 20f; public int lodLevel = 0; private bool isInitialized = false; void Start() { InitializeVFX(); UpdateParameters(); } private void InitializeVFX() { if (visualEffect == null) visualEffect = GetComponent<VisualEffect>(); if (visualEffect != null) { visualEffect.SetUInt("TargetSpawnCount", (uint)targetSpawnCount); visualEffect.SetFloat("SimulationSpeed", simulationSpeed); if (useGPUInstancing && SystemInfo.supportsInstancing) { } isInitialized = true; } } private void UpdateParameters() { if (visualEffect != null && isInitialized) { visualEffect.SetFloat("Intensity", intensity); visualEffect.SetVector3("MainColor", mainColor); visualEffect.SetFloat("Size", size); visualEffect.SetFloat("Speed", speed); } } void Update() { UpdateParameters(); HandleLOD(); } private void HandleLOD() { if (Camera.main != null) { float distance = Vector3.Distance(transform.position, Camera.main.transform.position); int newLODLevel = distance > lodDistance ? 1 : 0; if (newLODLevel != lodLevel) { lodLevel = newLODLevel; ApplyLODSettings(lodLevel); } } } private void ApplyLODSettings(int level) { if (visualEffect != null) { if (level == 0) { visualEffect.SetUInt("TargetSpawnCount", (uint)targetSpawnCount); } else { visualEffect.SetUInt("TargetSpawnCount", (uint)(targetSpawnCount / 2)); } } } public void SetParameter(string name, float value) { if (visualEffect != null) { visualEffect.SetFloat(name, value); } } public void SetParameter(string name, Vector3 value) { if (visualEffect != null) { visualEffect.SetVector3(name, value); } } public void SetParameter(string name, Color value) { if (visualEffect != null) { visualEffect.SetVector3(name, value); } } public void Play() { if (visualEffect != null) { visualEffect.Play(); } } public void Stop() { if (visualEffect != null) { visualEffect.Stop(); } } public void Pause() { if (visualEffect != null) { visualEffect.Pause(); } } public void Reset() { if (visualEffect != null) { visualEffect.Reinit(); } } public uint GetParticleCount() { if (visualEffect != null) { return visualEffect.aliveParticleCount; } return 0; } public void SetSpawnRate(float rate) { SetParameter("SpawnRate", rate); } public void SetLifetime(float lifetime) { SetParameter("Lifetime", lifetime); } }
|
10.9 粒子性能监控器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
| using System.Collections.Generic; using UnityEngine; using UnityEngine.Profiling;
public class ParticlePerformanceMonitor : MonoBehaviour { [Header("Monitoring Settings")] public float updateInterval = 1f; public bool enableLogging = true; [Header("Performance Thresholds")] public int warningParticleCount = 1000; public int criticalParticleCount = 2000; public float warningFrameTime = 16.6f; private float lastUpdateTime = 0f; private List<float> frameTimes = new List<float>(); private int maxFrameTimes = 60; [System.Serializable] public class PerformanceData { public float averageFrameTime; public int currentParticleCount; public int drawCallCount; public float memoryUsage; public float performanceScore; } public PerformanceData currentPerformance = new PerformanceData(); void Update() { UpdatePerformanceData(); if (Time.time - lastUpdateTime >= updateInterval) { AnalyzePerformance(); lastUpdateTime = Time.time; } } private void UpdatePerformanceData() { float frameTime = Time.deltaTime * 1000f; frameTimes.Add(frameTime); if (frameTimes.Count > maxFrameTimes) { frameTimes.RemoveAt(0); } float totalFrameTime = 0f; foreach (float time in frameTimes) { totalFrameTime += time; } currentPerformance.averageFrameTime = totalFrameTime / frameTimes.Count; currentPerformance.currentParticleCount = GetTotalParticleCount(); currentPerformance.drawCallCount = UnityEngine.Rendering.UnityRenderPipeline.activeRenderPassCount; currentPerformance.memoryUsage = Profiler.GetTotalAllocatedMemoryLong() / (1024f * 1024f); } private int GetTotalParticleCount() { int totalCount = 0; ParticleSystem[] particleSystems = FindObjectsOfType<ParticleSystem>(); foreach (var ps in particleSystems) { totalCount += ps.particleCount; } VisualEffect[] vfxSystems = FindObjectsOfType<VisualEffect>(); foreach (var vfx in vfxSystems) { totalCount += (int)vfx.aliveParticleCount; } return totalCount; } private void AnalyzePerformance() { float frameScore = Mathf.Clamp01((warningFrameTime - currentPerformance.averageFrameTime) / warningFrameTime) * 100f; float particleScore = Mathf.Clamp01((float)(warningParticleCount - currentPerformance.currentParticleCount) / warningParticleCount) * 100f; currentPerformance.performanceScore = (frameScore + particleScore) / 2f; if (enableLogging) { string performanceStatus = GetPerformanceStatus(); Debug.Log($"[Particle Performance] {performanceStatus} | " + $"Avg Frame: {currentPerformance.averageFrameTime:F1}ms | " + $"Particles: {currentPerformance.currentParticleCount} | " + $"Draw Calls: {currentPerformance.drawCallCount} | " + $"Score: {currentPerformance.performanceScore:F1}/100"); } if (currentPerformance.currentParticleCount > criticalParticleCount) { Debug.LogWarning($"[Particle Performance] Critical particle count: {currentPerformance.currentParticleCount}"); } else if (currentPerformance.currentParticleCount > warningParticleCount) { Debug.LogWarning($"[Particle Performance] High particle count: {currentPerformance.currentParticleCount}"); } if (currentPerformance.averageFrameTime > warningFrameTime) { Debug.LogWarning($"[Particle Performance] High frame time: {currentPerformance.averageFrameTime:F1}ms"); } } private string GetPerformanceStatus() { if (currentPerformance.performanceScore >= 80) return "Excellent"; else if (currentPerformance.performanceScore >= 60) return "Good"; else if (currentPerformance.performanceScore >= 40) return "Fair"; else return "Poor"; } public string GetPerformanceRecommendation() { List<string> recommendations = new List<string>(); if (currentPerformance.currentParticleCount > warningParticleCount) { recommendations.Add($"Reduce particle count from {currentPerformance.currentParticleCount} to below {warningParticleCount}"); } if (currentPerformance.averageFrameTime > warningFrameTime) { recommendations.Add($"Optimize rendering - current frame time {currentPerformance.averageFrameTime:F1}ms exceeds target {warningFrameTime:F1}ms"); } if (recommendations.Count == 0) return "Performance is optimal"; return string.Join(", ", recommendations); } public void ForcePerformanceAnalysis() { AnalyzePerformance(); } public void ResetMonitor() { frameTimes.Clear(); currentPerformance = new PerformanceData(); } }
|
10.10 粒子材质切换管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
| using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering.Universal;
public class ParticleMaterialManager : MonoBehaviour { [System.Serializable] public class MaterialPreset { public string name; public Material material; public bool supportsLighting = true; public bool useGPUInstancing = true; public BlendMode srcBlend = BlendMode.SrcAlpha; public BlendMode dstBlend = BlendMode.OneMinusSrcAlpha; } [Header("Material Presets")] public List<MaterialPreset> materialPresets = new List<MaterialPreset>(); [Header("Default Materials")] public Material defaultLitMaterial; public Material defaultUnlitMaterial; private Dictionary<string, MaterialPreset> presetMap = new Dictionary<string, MaterialPreset>(); private Dictionary<Renderer, Material> originalMaterials = new Dictionary<Renderer, Material>(); void Start() { InitializePresets(); } private void InitializePresets() { foreach (var preset in materialPresets) { if (preset.material != null) { presetMap[preset.name] = preset; } } } public void ApplyPresetToParticleSystem(ParticleSystem ps, string presetName) { if (presetMap.ContainsKey(presetName)) { var preset = presetMap[presetName]; var renderer = ps.GetComponent<ParticleSystemRenderer>(); if (renderer != null) { if (!originalMaterials.ContainsKey(renderer)) { originalMaterials[renderer] = renderer.sharedMaterials.Length > 0 ? renderer.sharedMaterials[0] : null; } renderer.material = preset.material; renderer.supportsLighting = preset.supportsLighting; renderer.enableGPUInstancing = preset.useGPUInstancing; } } } public void ApplyPresetToVFX(VisualEffect vfx, string presetName) { if (presetMap.ContainsKey(presetName)) { var preset = presetMap[presetName]; vfx.SetTexture("MainTexture", preset.material.mainTexture); } } public void RestoreOriginalMaterial(Renderer renderer) { if (originalMaterials.ContainsKey(renderer)) { renderer.material = originalMaterials[renderer]; originalMaterials.Remove(renderer); } } public void ApplyPresetToGroup(GameObject groupRoot, string presetName) { var particleSystems = groupRoot.GetComponentsInChildren<ParticleSystem>(); foreach (var ps in particleSystems) { ApplyPresetToParticleSystem(ps, presetName); } var vfxSystems = groupRoot.GetComponentsInChildren<VisualEffect>(); foreach (var vfx in vfxSystems) { ApplyPresetToVFX(vfx, presetName); } } public Material CreateCustomParticleMaterial(string shaderName, Color color, float alpha = 1.0f) { Shader shader = Shader.Find(shaderName); if (shader == null) { Debug.LogError($"Shader {shaderName} not found!"); return null; } Material material = new Material(shader); material.SetColor("_BaseColor", new Color(color.r, color.g, color.b, alpha)); material.SetFloat("_Surface", 1f); material.SetFloat("_Blend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); material.SetFloat("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); return material; } public void AdjustMaterialParameters(Renderer renderer, Color color, float smoothness = 0.5f, float metallic = 0f) { Material material = renderer.sharedMaterial; if (material.HasProperty("_BaseColor")) material.SetColor("_BaseColor", color); if (material.HasProperty("_Smoothness")) material.SetFloat("_Smoothness", smoothness); if (material.HasProperty("_Metallic")) material.SetFloat("_Metallic", metallic); } public List<string> GetAvailablePresets() { List<string> presets = new List<string>(); foreach (var kvp in presetMap) { presets.Add(kvp.Key); } return presets; } public bool IsMaterialGPUInstancingCompatible(Material material) { if (material == null) return false; return material.enableInstancing; } }
|
实践练习
10.11 练习1:创建基础粒子效果
目标:创建一个简单的火焰粒子效果
步骤:
- 创建ParticleSystem对象
- 配置发射模块(速率、数量)
- 设置粒子生命周期和大小
- 应用URP兼容的材质
- 调整颜色渐变实现火焰效果
参数设置:
- Duration: 5s, Looping: true
- Start Lifetime: 2-3s
- Start Speed: 0-2
- Start Size: 0.5-1.5
- Color over Lifetime: Red-Yellow-White gradient
10.12 练习2:实现VFX Graph雨滴效果
目标:使用VFX Graph创建下雨效果
步骤:
- 创建VFX Graph资源
- 设置Spawn Context生成雨滴
- 配置Update Context实现重力下落
- 添加Output Context渲染雨滴
- 调整参数实现自然的下雨效果
技术要点:
- 使用Position Over Lifetime设置雨滴轨迹
- 应用URP兼容的VFX材质
- 配置合适的发射速率
10.13 练习3:粒子光照交互
目标:实现粒子与场景光照的交互
步骤:
- 创建粒子系统并应用Lit材质
- 在场景中添加光源
- 配置粒子接收光照
- 观察光照对粒子的影响
- 调整光照参数优化效果
材质设置:
- Shader: Universal Render Pipeline/Particles/Lit
- Enable lighting on particle renderer
10.14 练习4:GPU Instancing优化测试
目标:测试GPU Instancing对粒子性能的影响
步骤:
- 创建大量粒子系统
- 启用GPU Instancing
- 测量性能差异
- 对比启用/禁用Instancing的性能
- 记录Draw Call数量变化
测试配置:
- 粒子数量:1000, 5000, 10000
- Instancing:开启/关闭对比
10.15 练习5:半透明粒子渲染顺序
目标:解决半透明粒子的渲染顺序问题
步骤:
- 创建多个半透明粒子系统
- 调整渲染队列设置
- 配置排序优先级
- 测试不同距离下的渲染顺序
- 确保正确的透明度混合
排序设置:
- Render Queue: Transparent
- Sort Mode: Distance or Front to Back
10.16 练习6:性能监控与优化
目标:实现粒子系统性能监控
步骤:
- 编写性能监控脚本
- 实时监控粒子数量
- 跟踪帧时间和Draw Call
- 实现性能警告系统
- 添加自动优化功能
监控指标:
- 粒子数量
- 平均帧时间
- 内存使用
- Draw Call数量
总结
第10章全面介绍了URP中的粒子系统与VFX Graph集成。粒子系统是游戏开发中不可或缺的视觉效果工具,而URP为粒子系统提供了优化的渲染管线支持。
关键要点总结:
- URP粒子Shader:使用URP兼容的粒子Shader以获得最佳效果
- VFX Graph集成:Visual Effect Graph与URP深度集成,提供强大的视觉创作能力
- 光照交互:粒子可以与URP光照系统交互,实现更真实的视觉效果
- GPU Instancing:通过GPU实例化优化大量粒子的渲染性能
- 性能监控:实现性能监控以确保粒子系统在目标平台上流畅运行
- 渲染顺序:正确处理半透明粒子的渲染顺序以避免视觉问题
粒子系统在URP中的实现需要考虑性能、视觉质量和平台兼容性等多个方面。通过合理配置粒子参数、选择合适的材质和优化渲染设置,可以创建出既美观又高效的粒子效果。
下一章将深入探讨URP的底层架构,分析UniversalRenderPipeline类的设计原理和实现机制。