特效合并,意思是说将粒子所用的零碎图片,以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 }
ParticleSystemCombiner
加载的代码:
1 using UnityEngine;
2
3 public partial class ParticleLoader : MonoBehaviour
4 {
5 public string TextureName;
6 public string ShaderName;
7 }
ParticleLoader
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 }
ParticleLoader
合并后的图集如下:
合并后的粒子如下:
效果如下: