.背景
首先,为什么需要动态图集?主要有两个原因:
游戏中的icon数量相当大,数量达到上千个。如果打成静态图集,需要好几张2048x2048的图集。运行时内存占用较大。同时,在这种情况下,由于icon会跨几个图集,在几个图集间穿插渲染有可能会提高drawcall。
若icon不打图集,每个icon就是一个单独的drawcall,会导致drawcall很高。
而动态图集,就是为了解决以上提到的问题。它的主要思路就是在运行时将需要使用的icon动态更新到一张共享的图集中去。由于同时渲染的icon数量往往远远小于实际icon的资源数量,因此通常一张1024或者2048大小的动态图集即可以满足大多数的游戏需求。该方法的好处在于,内存在可控范围,并且icon可以共享纹理从而省drawcall。
Unity动态图集技术
在 Unity 中,动态图集(Dynamic Atlas)技术是一种优化纹理资源管理和渲染性能的方法。动态图集通过将多个小纹理动态合并到一个大纹理中,减少了渲染过程中纹理切换的次数,从而提高了渲染效率。以下是关于 Unity 动态图集技术的详细介绍和实现方法。
动态图集的优势
- 减少纹理切换:在渲染过程中,频繁的纹理切换会导致性能下降。通过将多个小纹理合并到一个大纹理中,可以减少纹理切换的次数。
- 提高渲染效率:减少纹理切换和批处理次数,可以显著提高渲染效率,特别是在移动设备上。
- 简化资源管理:动态图集可以简化纹理资源的管理,使得纹理的加载和使用更加高效。
实现动态图集
在 Unity 中实现动态图集可以通过以下几种方法:
1. 使用 Unity 的 Sprite Atlas
Unity 提供了内置的 Sprite Atlas 工具,可以方便地创建和管理图集。以下是使用 Sprite Atlas 的步骤:
-
创建 Sprite Atlas:
- 在 Unity 编辑器中,右键点击项目窗口,选择
Create > 2D > Sprite Atlas
创建一个新的 Sprite Atlas。
- 在 Unity 编辑器中,右键点击项目窗口,选择
-
添加精灵到图集:
- 选择创建的 Sprite Atlas,在 Inspector 窗口中,点击
Objects for Packing
下的+
按钮,将需要合并的精灵(Sprite)拖动到列表中。
- 选择创建的 Sprite Atlas,在 Inspector 窗口中,点击
-
打包图集:
- 点击
Pack Preview
按钮,Unity 会自动将精灵合并到一个图集中。
- 点击
-
使用图集中的精灵:
- 在代码或编辑器中使用图集中的精灵时,Unity 会自动从图集中提取相应的纹理。
2. 动态生成图集
如果需要在运行时动态生成图集,可以使用 Texture2D
和 Rect
类来实现。以下是一个简单的示例,展示了如何在运行时动态生成图集:
using UnityEngine;
using System.Collections.Generic;public class DynamicAtlas : MonoBehaviour
{public List<Texture2D> textures; // 要合并的纹理列表public int atlasWidth = 1024; // 图集宽度public int atlasHeight = 1024; // 图集高度private Texture2D atlasTexture;private List<Rect> uvRects;void Start(){GenerateAtlas();}void GenerateAtlas(){atlasTexture = new Texture2D(atlasWidth, atlasHeight);uvRects = new List<Rect>();for (int i = 0; i < textures.Count; i++){Texture2D texture = textures[i];Rect uvRect = new Rect(0, 0, texture.width, texture.height);uvRects.Add(uvRect);atlasTexture.SetPixels((int)uvRect.x, (int)uvRect.y, texture.width, texture.height, texture.GetPixels());}atlasTexture.Apply();}void OnGUI(){// 在屏幕上显示图集GUI.DrawTexture(new Rect(0, 0, atlasWidth, atlasHeight), atlasTexture);}
}
3. 使用第三方插件
除了 Unity 内置的工具和自定义实现外,还有一些第三方插件可以帮助你更方便地创建和管理动态图集。例如:
- TexturePacker:一个流行的纹理打包工具,支持多种平台和格式,可以与 Unity 无缝集成。
- Asset Store 插件:Unity Asset Store 上有许多插件提供了动态图集的功能,可以根据需要选择合适的插件。
动态图集的注意事项
- 纹理大小限制:在创建图集时,需要注意图集的大小限制。不同平台对纹理大小有不同的限制,通常建议图集的大小不超过 2048x2048 或 4096x4096。
- 纹理压缩:为了减少内存占用和提高加载速度,可以对图集进行纹理压缩。Unity 提供了多种纹理压缩格式,可以根据平台选择合适的压缩格式。
- 纹理边缘处理:在合并纹理时,需要注意纹理边缘的处理,避免出现纹理缝
动态图集pipeline
游戏应用逻辑根据资源路径向动态图集服务请求icon。
如果该icon已经在该图集中,直接返回给程序使用;否则进入第3步。
去硬盘上请求加载该icon。
加载完成以后将该icon载入内存。
动态图集服务会根据某种分块算法,在其剩余可用的区域里划分一块矩形区域,同时将第4步中的icon像素拷贝到该矩形区域。
将关联第5步中矩形区域的sprite返回给应用程序使用。
在该6个步骤中,第5步是核心步骤,因为分块算法的好坏和像素的拷贝直接决定该pipeline的性能瓶颈。
动态生成图集流程
动态生成图集(Dynamic Atlas)的流程涉及多个步骤,包括纹理的收集、图集的创建、纹理的合并以及图集的使用。以下是一个详细的流程和示例代码,展示如何在 Unity 中动态生成图集。
动态生成图集的流程
- 收集纹理:收集需要合并到图集中的所有纹理。
- 创建图集:创建一个空的图集纹理(Texture2D)。
- 合并纹理:将收集到的纹理逐个合并到图集中,并记录每个纹理在图集中的位置(UV 坐标)。
- 应用图集:将生成的图集应用到需要使用的对象上。
示例代码
以下是一个完整的示例代码,展示了如何在 Unity 中动态生成图集:
using UnityEngine;
using System.Collections.Generic;public class DynamicAtlasGenerator : MonoBehaviour
{public List<Texture2D> textures; // 要合并的纹理列表public int atlasWidth = 1024; // 图集宽度public int atlasHeight = 1024; // 图集高度public int padding = 2; // 纹理之间的间距private Texture2D atlasTexture;private List<Rect> uvRects;void Start(){GenerateAtlas();}void GenerateAtlas(){// 创建一个空的图集纹理atlasTexture = new Texture2D(atlasWidth, atlasHeight, TextureFormat.RGBA32, false);uvRects = new List<Rect>();// 初始化图集纹理为透明Color[] clearPixels = new Color[atlasWidth * atlasHeight];for (int i = 0; i < clearPixels.Length; i++){clearPixels[i] = Color.clear;}atlasTexture.SetPixels(clearPixels);// 当前图集的填充位置int currentX = padding;int currentY = padding;int maxRowHeight = 0;// 合并纹理到图集中foreach (Texture2D texture in textures){if (currentX + texture.width + padding > atlasWidth){// 换行currentX = padding;currentY += maxRowHeight + padding;maxRowHeight = 0;}if (currentY + texture.height + padding > atlasHeight){Debug.LogError("图集空间不足,无法容纳所有纹理");break;}// 复制纹理到图集atlasTexture.SetPixels(currentX, currentY, texture.width, texture.height, texture.GetPixels());// 记录纹理在图集中的位置Rect uvRect = new Rect((float)currentX / atlasWidth, (float)currentY / atlasHeight, (float)texture.width / atlasWidth, (float)texture.height / atlasHeight);uvRects.Add(uvRect);// 更新当前填充位置currentX += texture.width + padding;maxRowHeight = Mathf.Max(maxRowHeight, texture.height);}// 应用图集纹理atlasTexture.Apply();// 在屏幕上显示图集GameObject quad = GameObject.CreatePrimitive(PrimitiveType.Quad);quad.transform.localScale = new Vector3(10, 10, 1);quad.GetComponent<Renderer>().material.mainTexture = atlasTexture;}void OnGUI(){// 在屏幕上显示图集GUI.DrawTexture(new Rect(0, 0, atlasWidth, atlasHeight), atlasTexture);}
}
详细解释
-
收集纹理:
public List<Texture2D> textures;
:定义一个列表来存储需要合并的纹理。
-
创建图集:
atlasTexture = new Texture2D(atlasWidth, atlasHeight, TextureFormat.RGBA32, false);
:创建一个空的图集纹理。- 初始化图集纹理为透明:使用
SetPixels
方法将图集纹理初始化为透明。
-
合并纹理:
- 使用嵌套的循环遍历所有纹理,并将它们逐个复制到图集中。
- 记录每个纹理在图集中的位置(UV 坐标),以便后续使用。
-
应用图集:
atlasTexture.Apply();
:调用Apply
方法将所有的像素更改应用到图集纹理中。- 在屏幕上显示图集:创建一个四边形(Quad)并将图集纹理应用到其材质上,以便在场景中可视化图集。
使用图集中的纹理
在生成图集后,你可能需要在游戏中使用图集中的纹理。为了方便管理和使用这些纹理,可以创建一个简单的管理器类来存储纹理的 UV 坐标,并提供获取纹理 UV 坐标的方法。
以下是一个示例,展示如何使用生成的图集和 UV 坐标:
using UnityEngine;
using System.Collections.Generic;public class DynamicAtlasManager : MonoBehaviour
{public DynamicAtlasGenerator atlasGenerator; // 引用动态图集生成器private Dictionary<string, Rect> textureUVs; // 存储纹理名称和 UV 坐标的字典void Start(){textureUVs = new Dictionary<string, Rect>();// 假设纹理名称与列表中的索引对应for (int i = 0; i < atlasGenerator.textures.Count; i++){string textureName = atlasGenerator.textures[i].name;Rect uvRect = atlasGenerator.uvRects[i];textureUVs.Add(textureName, uvRect);}}public Rect GetTextureUV(string textureName){if (textureUVs.ContainsKey(textureName)){return textureUVs[textureName];}else{Debug.LogError("纹理名称不存在:" + textureName);return Rect.zero;}}
}
使用图集中的纹理示例
假设你有一个 2D 游戏,并且需要在精灵渲染器(SpriteRenderer)中使用图集中的纹理。以下是一个示例,展示如何使用 DynamicAtlasManager
获取纹理的 UV 坐标,并应用到精灵渲染器中:
using UnityEngine;public class SpriteFromAtlas : MonoBehaviour
{public DynamicAtlasManager atlasManager; // 引用动态图集管理器public string textureName; // 要使用的纹理名称void Start(){// 获取图集纹理Texture2D atlasTexture = atlasManager.atlasGenerator.atlasTexture;// 获取纹理的 UV 坐标Rect uvRect = atlasManager.GetTextureUV(textureName);// 创建一个新的精灵Sprite sprite = Sprite.Create(atlasTexture, new Rect(uvRect.x * atlasTexture.width, uvRect.y * atlasTexture.height, uvRect.width * atlasTexture.width, uvRect.height * atlasTexture.height), new Vector2(0.5f, 0.5f));// 应用精灵到精灵渲染器SpriteRenderer spriteRenderer = GetComponent<SpriteRenderer>();spriteRenderer.sprite = sprite;}
}
详细解释
-
DynamicAtlasManager:
textureUVs
:一个字典,用于存储纹理名称和对应的 UV 坐标。Start
方法:在开始时,将DynamicAtlasGenerator
中的纹理名称和 UV 坐标存储到字典中。GetTextureUV
方法:提供一个方法,根据纹理名称获取对应的 UV 坐标。
-
SpriteFromAtlas:
atlasManager
:引用DynamicAtlasManager
,用于获取图集和 UV 坐标。textureName
:要使用的纹理名称。Start
方法:在开始时,获取图集纹理和对应的 UV 坐标,创建一个新的精灵,并将其应用到精灵渲染器中。
总结
动态生成图集是一种有效的优化技术,可以减少纹理切换,提高渲染效率。通过收集纹理、创建图集、合并纹理和应用图集,可以在 Unity 中实现动态图集的生成和使用。通过管理器类,可以方便地管理和使用图集中的纹理,从而在游戏中实现高效的纹理管理和渲染。
动态生成图集性能问题
在 Unity 中动态生成图集虽然可以带来很多好处,但也可能引发一些性能问题。以下是一些常见的性能问题及其解决方案:
性能问题
-
图集生成时间:
- 动态生成图集需要时间,特别是当有大量纹理需要合并时,可能会导致明显的延迟。
- 解决方案:尽量在非关键帧或加载屏幕期间生成图集,避免在游戏运行时频繁生成图集。
-
内存占用:
- 动态生成图集会占用额外的内存,特别是当图集尺寸较大时。
- 解决方案:合理规划图集的尺寸,避免生成过大的图集。可以考虑将图集分割成多个较小的图集。
-
纹理上传到 GPU:
- 每次生成或更新图集后,需要将图集纹理上传到 GPU,这可能会导致性能瓶颈。
- 解决方案:尽量减少图集的更新频率,避免频繁上传纹理到 GPU。
-
纹理拼接和边缘问题:
- 在合并纹理时,可能会出现纹理拼接不当或边缘渗色的问题。
- 解决方案:在生成图集时添加适当的边距(padding),并使用纹理边缘扩展技术。
优化策略
-
预生成图集:
- 尽量在编辑器中预生成图集,而不是在运行时动态生成。可以使用 Unity 的
Sprite Packer
或第三方工具如TexturePacker
来预生成图集。
- 尽量在编辑器中预生成图集,而不是在运行时动态生成。可以使用 Unity 的
-
异步生成图集:
- 如果必须在运行时生成图集,可以考虑使用异步方法来生成图集,避免阻塞主线程。
- 示例代码:
using UnityEngine; using System.Collections; using System.Collections.Generic;public class AsyncDynamicAtlasGenerator : MonoBehaviour {public List<Texture2D> textures;public int atlasWidth = 1024;public int atlasHeight = 1024;public int padding = 2;private Texture2D atlasTexture;private List<Rect> uvRects;void Start(){StartCoroutine(GenerateAtlasAsync());}IEnumerator GenerateAtlasAsync(){atlasTexture = new Texture2D(atlasWidth, atlasHeight, TextureFormat.RGBA32, false);uvRects = new List<Rect>();Color[] clearPixels = new Color[atlasWidth * atlasHeight];for (int i = 0; i < clearPixels.Length; i++){clearPixels[i] = Color.clear;}atlasTexture.SetPixels(clearPixels);int currentX = padding;int currentY = padding;int maxRowHeight = 0;foreach (Texture2D texture in textures){if (currentX + texture.width + padding > atlasWidth){currentX = padding;currentY += maxRowHeight + padding;maxRowHeight = 0;}if (currentY + texture.height + padding > atlasHeight){Debug.LogError("图集空间不足,无法容纳所有纹理");break;}atlasTexture.SetPixels(currentX, currentY, texture.width, texture.height, texture.GetPixels());Rect uvRect = new Rect((float)currentX / atlasWidth, (float)currentY / atlasHeight, (float)texture.width / atlasWidth, (float)texture.height / atlasHeight);uvRects.Add(uvRect);currentX += texture.width + padding;maxRowHeight = Mathf.Max(maxRowHeight, texture.height);yield return null; // 每次处理一个纹理后暂停,避免阻塞主线程}atlasTexture.Apply();}void OnGUI(){GUI.DrawTexture(new Rect(0, 0, atlasWidth, atlasHeight), atlasTexture);} }
-
分块生成图集:
分块生成图集是一种将大图集分割成多个较小图集的方法,以减少单个图集的生成和更新时间。这种方法可以有效地管理内存和提高性能。
以下是一个示例代码,展示如何实现分块生成图集:
using UnityEngine;
using System.Collections.Generic;public class ChunkedAtlasGenerator : MonoBehaviour
{public List<Texture2D> textures; // 要合并的纹理列表public int chunkSize = 512; // 每个图集块的大小public int padding = 2; // 纹理之间的间距private List<Texture2D> atlasTextures; // 存储生成的图集块private List<List<Rect>> uvRectsList; // 存储每个图集块中纹理的 UV 坐标void Start(){GenerateChunkedAtlas();}void GenerateChunkedAtlas(){atlasTextures = new List<Texture2D>();uvRectsList = new List<List<Rect>>();int currentX = padding;int currentY = padding;int maxRowHeight = 0;Texture2D currentAtlas = CreateNewAtlas();List<Rect> currentUVRects = new List<Rect>();foreach (Texture2D texture in textures){if (currentX + texture.width + padding > chunkSize){// 换行currentX = padding;currentY += maxRowHeight + padding;maxRowHeight = 0;}if (currentY + texture.height + padding > chunkSize){// 当前图集块已满,创建新的图集块atlasTextures.Add(currentAtlas);uvRectsList.Add(currentUVRects);currentAtlas = CreateNewAtlas();currentUVRects = new List<Rect>();currentX = padding;currentY = padding;maxRowHeight = 0;}// 复制纹理到当前图集块currentAtlas.SetPixels(currentX, currentY, texture.width, texture.height, texture.GetPixels());// 记录纹理在当前图集块中的位置Rect uvRect = new Rect((float)currentX / chunkSize, (float)currentY / chunkSize, (float)texture.width / chunkSize, (float)texture.height / chunkSize);currentUVRects.Add(uvRect);// 更新当前填充位置currentX += texture.width + padding;maxRowHeight = Mathf.Max(maxRowHeight, texture.height);}// 添加最后一个图集块atlasTextures.Add(currentAtlas);uvRectsList.Add(currentUVRects);// 应用所有图集块foreach (var atlas in atlasTextures){atlas.Apply();}}Texture2D CreateNewAtlas(){Texture2D atlas = new Texture2D(chunkSize, chunkSize, TextureFormat.RGBA32, false);Color[] clearPixels = new Color[chunkSize * chunkSize];for (int i = 0; i < clearPixels.Length; i++){clearPixels[i] = Color.clear;}atlas.SetPixels(clearPixels);return atlas;}void OnGUI(){// 在屏幕上显示所有图集块for (int i = 0; i < atlasTextures.Count; i++){GUI.DrawTexture(new Rect(i * chunkSize, 0, chunkSize, chunkSize), atlasTextures[i]);}}
}
详细解释
-
创建新图集块:
CreateNewAtlas
方法用于创建一个新的图集块,并初始化为透明。
-
生成分块图集:
GenerateChunkedAtlas
方法遍历所有纹理,将它们逐个复制到当前图集块中。- 如果当前图集块已满,则创建一个新的图集块,并继续复制剩余的纹理。
-
记录 UV 坐标:
- 每个纹理在图集块中的位置(UV 坐标)被记录下来,以便后续使用。
-
应用图集块:
- 调用
Apply
方法将所有的像素更改应用到每个图集块中。
- 调用
-
显示图集块:
- 在
OnGUI
方法中,使用GUI.DrawTexture
方法在屏幕上显示所有生成的图集块。
- 在