Unity性能优化之特效合并
特效合并,意思是说将粒子所用的零碎图片,以shader为单位合并成一张图集,好处就是可以降低draw call。试想,合并前每个粒子使用一个material,而每一个material就要占用一个drawcall,而合并后多个粒子可以用同一个material,这样就降低了drawcall,提升了性能。
转载请注明出处:http://www.cnblogs.com/jietian331/p/8625078.html
合并工具的代码如下:
1 using System; 2 using System.Collections.Generic; 3 using System.IO; 4 using System.Linq; 5 using System.Text; 6 using UnityEditor; 7 using UnityEngine; 8 9 namespace AssetBundle 10 { 11 public class ParticleSystemCombiner : ScriptableObject 12 { 13 public const string 14 AtlasFolder = "Assets/Cloth/Resources/ParticleSystemAtlas", 15 ParticleAtlasPath = "Assets/Cloth/Resources/ParticleSystemAtlas/particle_atlas.prefab", 16 SettingFilepath = "Assets/Editor/ParticleSystemCombinerSetting.csv"; 17 18 static string[] EffectObjFolders = new string[] 19 { 20 "Assets/Cloth/Resources/Effect/Cloth", 21 "Assets/Cloth/Resources/Model/Equip", 22 }; 23 24 static ParticleAtlases s_atlasesData; 25 static List<string> s_materials; 26 static Dictionary<string, int> s_texturesSize; 27 28 static ParticleAtlases AtlasesData 29 { 30 get 31 { 32 if (s_atlasesData == null) 33 s_atlasesData = AssetDatabase.LoadAssetAtPath<ParticleAtlases>(ParticleAtlasPath); 34 return s_atlasesData; 35 } 36 } 37 38 static Dictionary<string, int> TexturesSize 39 { 40 get 41 { 42 if (s_texturesSize == null) 43 { 44 s_texturesSize = new Dictionary<string, int>(); 45 46 string[] lines = File.ReadAllLines(SettingFilepath); 47 bool decode = false; 48 foreach (var line in lines) 49 { 50 if (!decode) 51 { 52 if (line.StartsWith("# Texture Size")) 53 { 54 decode = true; 55 } 56 } 57 else 58 { 59 if (line.StartsWith("#")) 60 { 61 decode = false; 62 } 63 } 64 65 if (decode) 66 { 67 string[] words = line.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 68 if (words.Length > 1) 69 { 70 string name = words[0]; 71 int size; 72 int.TryParse(words[1], out size); 73 if (size != 0) 74 s_texturesSize[name] = size; 75 } 76 } 77 } 78 } 79 return s_texturesSize; 80 } 81 } 82 83 84 static List<string> NotCombineTextures 85 { 86 get 87 { 88 List<string> list = new List<string>(); 89 string[] lines = File.ReadAllLines(SettingFilepath); 90 bool decode = false; 91 foreach (var line in lines) 92 { 93 if (!decode) 94 { 95 if (line.StartsWith("# Not Combine Textures")) 96 { 97 decode = true; 98 } 99 } 100 else 101 { 102 if (line.StartsWith("#")) 103 { 104 decode = false; 105 } 106 } 107 108 if (decode) 109 { 110 string[] words = line.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 111 if (words.Length > 0 && !string.IsNullOrEmpty(words[0])) 112 { 113 list.Add(words[0]); 114 } 115 } 116 } 117 return list; 118 } 119 } 120 121 #region for build 122 123 public static void ClearCache() 124 { 125 s_atlasesData = null; 126 s_materials = null; 127 } 128 129 public static List<ParticleAtlases.TextureItem> GetParticlesUsedAtlas(GameObject effectObj) 130 { 131 List<ParticleAtlases.TextureItem> texturesData = new List<ParticleAtlases.TextureItem>(); 132 ParticleSystem[] particles = effectObj.GetComponentsInChildren<ParticleSystem>(true); 133 134 foreach (ParticleSystem ps in particles) 135 { 136 ParticleSystemRenderer render = ps.GetComponent<ParticleSystemRenderer>(); 137 if (!render || !render.sharedMaterial) 138 { 139 Debug.LogWarning("Particle no material: " + ps.name); 140 continue; 141 } 142 143 Texture texture = render.sharedMaterial.mainTexture; 144 if (ps.textureSheetAnimation.enabled || !texture) 145 continue; 146 147 foreach (var atlasData in AtlasesData.Atlases) 148 { 149 foreach (var t in atlasData.Textures) 150 { 151 if (t.Name == texture.name) 152 { 153 texturesData.Add(t); 154 break; 155 } 156 } 157 } 158 } 159 160 return texturesData; 161 } 162 163 public static void ProcessEffectObj(GameObject obj) 164 { 165 ParticleSystem[] particles = obj.GetComponentsInChildren<ParticleSystem>(true); 166 167 foreach (ParticleSystem ps in particles) 168 { 169 ParticleSystemRenderer render = ps.GetComponent<ParticleSystemRenderer>(); 170 if (!render || !render.sharedMaterial) 171 { 172 Debug.LogWarning("Particle no material: " + ps.name); 173 continue; 174 } 175 176 Texture texture = render.sharedMaterial.mainTexture; 177 if (ps.textureSheetAnimation.enabled || !texture) 178 continue; 179 180 ParticleAtlases.Atlas target = AtlasesData.Atlases.FirstOrDefault(a => a.Textures.Any(t => t.Name == texture.name && t.ShaderName == render.sharedMaterial.shader.name)); 181 182 if (target != null) 183 { 184 ParticleLoader loader = ps.GetComponent<ParticleLoader>(); 185 if (!loader) 186 loader = ps.gameObject.AddComponent<ParticleLoader>(); 187 loader.TextureName = texture.name; 188 loader.ShaderName = render.sharedMaterial.shader.name; 189 render.sharedMaterial = null; 190 191 if (!ps.trails.enabled) 192 render.trailMaterial = null; 193 } 194 } 195 196 EditorUtility.SetDirty(obj); 197 AssetDatabase.SaveAssets(); 198 } 199 200 public static List<string> GetAllMaterials() 201 { 202 if (s_materials != null) 203 return s_materials; 204 205 List<string> effects; 206 Dictionary<Shader, List<ParticleSystem>> dic = GetAllParticles(out effects); 207 s_materials = new List<string>(); 208 209 foreach (var pair in dic) 210 { 211 foreach (ParticleSystem ps in pair.Value) 212 { 213 ParticleSystemRenderer r = ps.GetComponent<ParticleSystemRenderer>(); 214 string path = AssetDatabase.GetAssetPath(r.sharedMaterial); 215 if (!s_materials.Contains(path)) 216 s_materials.Add(path); 217 } 218 } 219 220 return s_materials; 221 } 222 223 #endregion 224 225 226 [MenuItem("BuildTool/AssetBundle/Combine Particle System")] 227 static void Init() 228 { 229 CombineAllEffectTextures(); 230 231 EditorUtility.DisplayDialog("finished", "All work finished.", "ok"); 232 } 233 234 static Dictionary<Shader, List<ParticleSystem>> GetAllParticles(out List<string> effectObjs) 235 { 236 // 获取所有的粒子 237 List<ParticleSystem> particlesList = new List<ParticleSystem>(); 238 List<string> objPaths = new List<string>(); 239 foreach (var effectObjFolder in EffectObjFolders) 240 { 241 string[] paths = Directory.GetFiles(effectObjFolder, "*.prefab", SearchOption.AllDirectories); 242 objPaths.AddRange(paths); 243 } 244 effectObjs = new List<string>(); 245 246 foreach (string path in objPaths) 247 { 248 string pathFixed = path.Replace("\\", "/"); 249 effectObjs.Add(pathFixed); 250 GameObject obj = AssetDatabase.LoadAssetAtPath<GameObject>(pathFixed); 251 ParticleSystem[] particles = obj.GetComponentsInChildren<ParticleSystem>(true); 252 253 foreach (ParticleSystem ps in particles) 254 { 255 ParticleSystemRenderer r = ps.GetComponent<ParticleSystemRenderer>(); 256 bool needCombine = !ps.textureSheetAnimation.enabled 257 && r.sharedMaterial 258 && r.sharedMaterial.shader.name != "Particles/Alpha Blended Premultiply" 259 && r.sharedMaterial.mainTexture 260 && r.sharedMaterial.mainTexture.width == r.sharedMaterial.mainTexture.height; 261 if (needCombine) 262 particlesList.Add(ps); 263 } 264 } 265 266 // 分类 267 Dictionary<Shader, List<ParticleSystem>> dic = new Dictionary<Shader, List<ParticleSystem>>(); 268 foreach (ParticleSystem ps in particlesList) 269 { 270 ParticleSystemRenderer r = ps.GetComponent<ParticleSystemRenderer>(); 271 var shader = r.sharedMaterial.shader; 272 if (!dic.ContainsKey(shader)) 273 dic[shader] = new List<ParticleSystem>(); 274 dic[shader].Add(ps); 275 } 276 277 return dic; 278 } 279 280 public static List<string> CombineAllEffectTextures() 281 { 282 List<string> atlases = new List<string>(); 283 284 // 获取所有的粒子 285 List<string> effects; 286 Dictionary<Shader, List<ParticleSystem>> dic = GetAllParticles(out effects); 287 288 // combine 289 Dictionary<Texture2D, Material> dictTextures = new Dictionary<Texture2D, Material>(); 290 List<ParticleAtlases.Atlas> atlasesData = new List<ParticleAtlases.Atlas>(); 291 List<string> notCombineTextures = NotCombineTextures; 292 293 foreach (var pair in dic) 294 { 295 // get textures 296 dictTextures.Clear(); 297 foreach (ParticleSystem ps in pair.Value) 298 { 299 ParticleSystemRenderer r = ps.GetComponent<ParticleSystemRenderer>(); 300 Texture2D texture = (Texture2D)r.sharedMaterial.mainTexture; 301 if (!notCombineTextures.Contains(texture.name) && !dictTextures.ContainsKey(texture)) 302 dictTextures.Add(texture, r.sharedMaterial); 303 } 304 305 if (dictTextures.Count < 2) 306 continue; 307 308 Texture2D[] texturesArray = dictTextures.Keys.ToArray(); 309 310 // combine texture 311 string atlasName = string.Format("ParticleAtlas_{0}", Path.GetFileNameWithoutExtension(pair.Key.name)); 312 string atlasPath = string.Format("{0}/{1}.png", AtlasFolder, atlasName); 313 Uploader.CreateDirectory(atlasPath); 314 Rect[] rects; 315 Vector2[] textureSizes; 316 Texture2D atlas = CombineTextures(texturesArray, atlasPath, out rects, out textureSizes); 317 atlases.Add(atlasPath); 318 319 // create material 320 string matPath = string.Format("{0}/{1}.mat", AtlasFolder, atlasName); 321 Material mat = AssetDatabase.LoadAssetAtPath<Material>(matPath); 322 if (mat == null) 323 { 324 mat = new Material(pair.Key); 325 AssetDatabase.CreateAsset(mat, matPath); 326 } 327 mat.mainTexture = atlas; 328 329 // get config 330 ParticleAtlases.TextureItem[] texturesData = new ParticleAtlases.TextureItem[texturesArray.Length]; 331 for (int i = 0; i < texturesArray.Length; i++) 332 { 333 Rect rect = rects[i]; 334 Texture2D texture2D = texturesArray[i]; 335 Vector2 textureSize = textureSizes[i]; // will resize temp texture, so cann't use texture2D.width 336 int numTilesX = (int)(atlas.width / textureSize.x); 337 int numTilesY = (int)(atlas.height / textureSize.y); 338 int colIndex = (int)(rect.x * numTilesX); 339 int rowIndex = (int)(numTilesY - 1 - rect.y * numTilesY); 340 int index = rowIndex * numTilesX + colIndex; 341 342 // get color 343 Material oldMat = dictTextures[texture2D]; 344 Color32 oldColor = oldMat.GetColor("_TintColor"); 345 string strColor = string.Format("{0}_{1}_{2}_{3}", oldColor.r, oldColor.g, oldColor.b, oldColor.a); 346 347 string shaderName = oldMat.shader.name; 348 349 int depth = oldMat.renderQueue; 350 351 texturesData[i] = new ParticleAtlases.TextureItem() 352 { 353 Color = oldColor, 354 Depth = depth, 355 Index = index, 356 Name = texture2D.name, 357 NumTilesX = numTilesX, 358 NumTilesY = numTilesY, 359 ShaderName = shaderName, 360 }; 361 } 362 363 ParticleAtlases.Atlas atlasData = new ParticleAtlases.Atlas() 364 { 365 Material = mat, 366 Textures = texturesData, 367 }; 368 atlasesData.Add(atlasData); 369 } 370 371 GameObject prefabObj = AssetDatabase.LoadAssetAtPath<GameObject>(ParticleAtlasPath); 372 if (!prefabObj) 373 prefabObj = PrefabUtility.CreatePrefab(ParticleAtlasPath, new GameObject()); 374 ParticleAtlases atlasesCom = prefabObj.GetComponent<ParticleAtlases>(); 375 if (!atlasesCom) 376 atlasesCom = prefabObj.AddComponent<ParticleAtlases>(); 377 atlasesCom.Atlases = atlasesData.ToArray(); 378 prefabObj.name = Path.GetFileNameWithoutExtension(ParticleAtlasPath); 379 380 EditorUtility.SetDirty(prefabObj); 381 AssetDatabase.SaveAssets(); 382 383 Debug.Log("All cloth effect textures combine finished!"); 384 385 return atlases; 386 } 387 388 static Texture2D CombineTextures(Texture2D[] textures, string path, out Rect[] rects, out Vector2[] textureSizes) 389 { 390 if (textures == null || textures.Length < 1) 391 { 392 Debug.LogError("None textures"); 393 rects = null; 394 textureSizes = null; 395 return null; 396 } 397 398 string tempFolderName = "_TempImages"; 399 string tempFolder = "Assets/" + tempFolderName; 400 AssetDatabase.DeleteAsset(tempFolder); 401 AssetDatabase.CreateFolder("Assets", tempFolderName); 402 403 List<string> newPaths = new List<string>(); 404 var newTextures = new Texture2D[textures.Length]; 405 textureSizes = new Vector2[textures.Length]; 406 407 // 将原来的图片复制一份出来 408 for (int i = 0; i < textures.Length; i++) 409 { 410 string texPath = AssetDatabase.GetAssetPath(textures[i]); 411 if (File.Exists(texPath)) 412 { 413 string newPath = string.Format("{0}/{1}", tempFolder, Path.GetFileName(texPath)); 414 AssetDatabase.CopyAsset(texPath, newPath); 415 newPaths.Add(newPath); 416 } 417 else 418 { 419 Debug.Log("File not exists: " + texPath); 420 } 421 } 422 423 // make it readable 424 for (int i = 0; i < newPaths.Count; i++) 425 { 426 string newPath = newPaths[i]; 427 SetSourceTextureReadalbe(newPath); 428 Texture2D t = AssetDatabase.LoadAssetAtPath<Texture2D>(newPath); 429 textureSizes[i] = new Vector2(t.width, t.height); 430 431 // 去掉边缘的一个像素 432 if (t.width > 4) 433 { 434 for (int j = 0; j < t.width; j++) 435 { 436 t.SetPixel(j, 0, new Color(0, 0, 0, 0)); 437 t.SetPixel(j, 1, new Color(0, 0, 0, 0)); 438 t.SetPixel(j, t.height - 1, new Color(0, 0, 0, 0)); 439 t.SetPixel(j, t.height - 2, new Color(0, 0, 0, 0)); 440 } 441 } 442 443 if (t.height > 4) 444 { 445 for (int z = 0; z < t.height; z++) 446 { 447 t.SetPixel(0, z, new Color(0, 0, 0, 0)); 448 t.SetPixel(1, z, new Color(0, 0, 0, 0)); 449 t.SetPixel(t.width - 1, z, new Color(0, 0, 0, 0)); 450 t.SetPixel(t.width - 2, z, new Color(0, 0, 0, 0)); 451 } 452 } 453 454 newTextures[i] = t; 455 } 456 457 // pack 458 Texture2D atlas = new Texture2D(1, 1, TextureFormat.ARGB32, false); 459 rects = atlas.PackTextures(newTextures, 0, 4096, false); 460 461 // save 462 byte[] bytes = atlas.EncodeToPNG(); 463 File.WriteAllBytes(path, bytes); 464 AssetDatabase.Refresh(); 465 AssetDatabase.SaveAssets(); 466 467 // setting 468 TextureCompresser.CompressRGBA(path); 469 470 // 删除临时目录 471 AssetDatabase.DeleteAsset(tempFolder); 472 473 AssetDatabase.Refresh(); 474 AssetDatabase.SaveAssets(); 475 476 return AssetDatabase.LoadAssetAtPath<Texture2D>(path); 477 } 478 479 static void SetSourceTextureReadalbe(string path) 480 { 481 string name = Path.GetFileNameWithoutExtension(path); 482 int maxSize; 483 TexturesSize.TryGetValue(name, out maxSize); 484 if (maxSize == 0) 485 maxSize = 64; 486 487 bool readable = true; 488 TextureImporterNPOTScale npotScale = TextureImporterNPOTScale.ToNearest; 489 TextureWrapMode wrapMode = TextureWrapMode.Clamp; 490 TextureImporterCompression compression = TextureImporterCompression.Uncompressed; 491 492 bool changed = false; 493 494 var importer = (TextureImporter)AssetImporter.GetAtPath(path); 495 TextureImporterSettings settings = new TextureImporterSettings(); 496 importer.ReadTextureSettings(settings); 497 498 settings.alphaIsTransparency = true; 499 settings.mipmapEnabled = false; 500 501 if (settings.readable != readable) 502 { 503 settings.readable = readable; 504 changed = true; 505 } 506 507 if (settings.npotScale != npotScale) 508 { 509 settings.npotScale = npotScale; 510 changed = true; 511 } 512 513 if (settings.wrapMode != wrapMode) 514 { 515 settings.wrapMode = wrapMode; 516 changed = true; 517 } 518 519 if (importer.maxTextureSize != maxSize) 520 { 521 importer.maxTextureSize = maxSize; 522 changed = true; 523 } 524 525 if (importer.textureCompression != compression) 526 { 527 importer.textureCompression = compression; 528 changed = true; 529 } 530 531 // set platform overriten as false 532 var androidSetting = importer.GetPlatformTextureSettings("Android"); 533 var iosSetting = importer.GetPlatformTextureSettings("iPhone"); 534 var pcSetting = importer.GetPlatformTextureSettings("Standalone"); 535 if (androidSetting.overridden) 536 { 537 androidSetting.overridden = false; 538 changed = true; 539 } 540 if (iosSetting.overridden) 541 { 542 iosSetting.overridden = false; 543 changed = true; 544 } 545 if (pcSetting.overridden) 546 { 547 pcSetting.overridden = false; 548 changed = true; 549 } 550 importer.SetPlatformTextureSettings(androidSetting); 551 importer.SetPlatformTextureSettings(iosSetting); 552 importer.SetPlatformTextureSettings(pcSetting); 553 554 if (changed) 555 { 556 importer.SetTextureSettings(settings); 557 AssetDatabase.ImportAsset(path); 558 } 559 } 560 561 } 562 }
加载的代码:
1 using UnityEngine; 2 3 public partial class ParticleLoader : MonoBehaviour 4 { 5 public string TextureName; 6 public string ShaderName; 7 }
1 using Common; 2 using UnityEngine; 3 4 public partial class ParticleLoader : MonoBehaviour 5 { 6 static ParticleAtlases s_atlases; 7 8 9 // 加载图集 10 public static void Initialize() 11 { 12 BundleLoader.Singleton.LoadAssetBundle("atlases/particlesystematlas.u", r => 13 { 14 GameObject[] objs = r.Bundle.LoadAllAssets<GameObject>(); 15 s_atlases = objs[0].GetComponent<ParticleAtlases>(); 16 }); 17 } 18 19 void OnEnable() 20 { 21 Load(); 22 } 23 24 void Load() 25 { 26 ParticleSystemRenderer renderer = GetComponent<ParticleSystemRenderer>(); 27 if (!renderer) 28 return; 29 30 ParticleAtlases.Atlas targetAtlas = null; 31 ParticleAtlases.TextureItem targetTextureConfig = null; 32 33 // 找对应的配置 34 for (int i = 0; i < s_atlases.Atlases.Length; i++) 35 { 36 ParticleAtlases.Atlas atlasData = s_atlases.Atlases[i]; 37 for (int j = 0; j < atlasData.Textures.Length; j++) 38 { 39 ParticleAtlases.TextureItem textureData = atlasData.Textures[j]; 40 if (textureData.Name == this.TextureName && textureData.ShaderName == this.ShaderName) 41 { 42 targetAtlas = atlasData; 43 targetTextureConfig = textureData; 44 break; 45 } 46 } 47 48 if (targetAtlas != null) 49 break; 50 } 51 52 if (targetAtlas != null && targetTextureConfig != null) 53 { 54 ParticleSystem ps = GetComponent<ParticleSystem>(); 55 56 renderer.sharedMaterial = targetAtlas.Material; // 渲染 57 58 SetTextureSheet(ps, targetTextureConfig); // 设置格子 59 60 SetStartColor(ps, renderer, targetTextureConfig.Color); // 设置颜色 61 62 SetDepth(renderer, targetTextureConfig); // 排序 63 } 64 } 65 66 // 设置格子 67 void SetTextureSheet(ParticleSystem ps, ParticleAtlases.TextureItem targetTextureConfig) 68 { 69 var tsa = ps.textureSheetAnimation; 70 float curveConstant = (float)targetTextureConfig.Index / targetTextureConfig.NumTilesX / targetTextureConfig.NumTilesY; 71 tsa.enabled = true; 72 tsa.numTilesX = targetTextureConfig.NumTilesX; 73 tsa.numTilesY = targetTextureConfig.NumTilesY; 74 tsa.animation = ParticleSystemAnimationType.WholeSheet; 75 tsa.startFrame = new ParticleSystem.MinMaxCurve(0); 76 tsa.frameOverTime = new ParticleSystem.MinMaxCurve(curveConstant); 77 tsa.cycleCount = 1; 78 } 79 80 // 设置颜色 81 void SetStartColor(ParticleSystem ps, ParticleSystemRenderer renderer, Color matColor) 82 { 83 var main = ps.main; 84 switch (main.startColor.mode) 85 { 86 case ParticleSystemGradientMode.Color: 87 case ParticleSystemGradientMode.Gradient: 88 case ParticleSystemGradientMode.RandomColor: 89 case ParticleSystemGradientMode.TwoGradients: 90 var targetColor = main.startColor.color * matColor; 91 main.startColor = new UnityEngine.ParticleSystem.MinMaxGradient(targetColor); 92 break; 93 94 case ParticleSystemGradientMode.TwoColors: 95 var colorMin = main.startColor.colorMin * matColor; 96 var colorMax = main.startColor.colorMax * matColor; 97 main.startColor = new UnityEngine.ParticleSystem.MinMaxGradient(colorMin, colorMax); 98 break; 99 100 default: 101 Debug.LogError("Unknown mode: " + main.startColor.mode); 102 break; 103 } 104 105 renderer.sharedMaterial.SetColor("_TintColor", Color.white); 106 } 107 108 // 排序 109 void SetDepth(ParticleSystemRenderer renderer, ParticleAtlases.TextureItem targetTextureConfig) 110 { 111 int depth = targetTextureConfig.Depth; 112 if (depth > 0 && renderer.sharedMaterial.renderQueue < depth) 113 renderer.sharedMaterial.renderQueue = depth; 114 } 115 116 }
1 using System; 2 using UnityEngine; 3 4 public class ParticleAtlases : MonoBehaviour 5 { 6 public Atlas[] Atlases; 7 8 [Serializable] 9 public class Atlas 10 { 11 public Material Material; 12 public TextureItem[] Textures; 13 } 14 15 [Serializable] 16 public class TextureItem 17 { 18 public string Name; 19 public string ShaderName; 20 public int NumTilesX; 21 public int NumTilesY; 22 public int Index; 23 public Color Color; 24 public int Depth; 25 } 26 }
合并后的图集如下:
合并后的粒子如下:
效果如下: