精灵图集(Sprite Atlas)简介
在Unity中,通常渲染一个纹理会调用一次DrawCall。一个2D项目,可能包含大量的纹理,如果绘制每个纹理都调用一次DrawCall,这会占用过多的资源,从而影响整个应用程序的性能,看下面一个例子。
在Unity中新建一个场景,为了排除其他干扰,删掉Camera和Directional Light,添加两个Image为它们赋值相同的Sprite

可以看到此时的DrawCall为1
![]()
将上面两个Image的Sprite更换成不同的

这时DrawCall变成了2
![]()
为了降低性能消耗,我们可以使用精灵图集(Sprite Atlas)技术,它能够将多个纹理(texture)合并成一个大纹理,当访问图集中的多个纹理时,也只需要调用一次DrawCall 。听起来挺不错,看一下如何使用。
什么是DrawCall?
游戏中的图片模型最终是要给到显卡去渲染的,图片开始是要从硬盘读到内存里面去的,然后CPU通知GPU要开始渲染,这一次通知就是一次 Drawcall。通知的时候还要把渲染数据告诉GPU,因此会消耗一些性能。想要了解更多可以参考文章 渲染管线基础
这是上面用到的两张图片,需要的自取


如何打包图集
在Unity2017之前,打包图集使用的是旧版工具SpritePacker,后来引入了新的SpriteAtlas,并且Unity2020.1之后的版本都已经放弃使用旧版的SpritePacker了,因此本文主要讲解如何使用SpriteAtlas打包图集。
在使用之前,需要先安装2D Sprite插件,具体步骤为 Window -> Package Manager,将Packages选择为Unity Registry,搜索2D Sprite并且下载,如下图所示:

安装完成后,开启图集打包的功能, Edit -> Project Settings -> Editor -> Sprite Packer-> Mode,默认是Disable,我们可以选择下面四种模式之一(不同版本的选项可能并不一致,我用的是Unity2020.3):

- Disabled:禁用图集相关功能。
- Sprite Atlas V1 - Enabled For Builds:仅当发布(Build)的时候构建图集,在Editor和Play模式中引用原始纹理而非图集中的纹理。
- Sprite Atlas V1 - Always Enabled:在运行时(Build和Play模式)引用图集中的纹理,在Editor模式中引用原始纹理。
- Sprite Atlas V2 (Experimental) - Enabled :Unity2020.3版本这个功能还处于实验中,默认在运行时引用图集中的纹理。Sprite Atlas V1 不支持缓存服务器(Cache Server),Unity只能将打包的图集数据存储在
Library/AtlasCache文件夹中,并且也不能有依赖项,不支持命名对象导入器(named objects importer),而Sprite Atlas V2提供了对上述功能的支持。具体可以参考 Unity Manual:Sprite Atlas Version 2
这里选用Sprite Atlas V2,选择好之后,创建一个图集,Create -> 2D -> Sprite Atlas.

我们看看新建的图集包含哪些属性,选中新建的图集,下面是它的属性面板


对于其中比较模糊的概念,单独拎出来讲解
图集类型(Type)
图集分为主图集和变体图集,那什么是Variant(变体图集)呢?切换成Variant模式看一下,截取属性面板中部分不同的内容

可以发现,它多了Master Atlas和Scale属性。
- Master Atlas:设置主图集内容的副本作为自己的内容。
- Scale:设置变体图集的缩放因子,介于0.1~1之间。
变体图集的作用是什么呢?它的主要目的是创建与主图集不同分辨率的图集,主图集中Sprite的分辨率 * Scale(缩放因子)得到的结果就是变体图集中Sprite的分辨率,它自身不包含Objects for Packing属性,因此变体图集中的内容都是主图集的副本。
当项目中既有主图集,又有该主图集的变体图集时,可以使用这两个图集中任意一个的Sprite。如果要自动从变体图集而不是主图集中加载Sprite,那就仅为变体图集启用Include in Build选项,并关闭主图集的这个选项。
Include in Build
Unity在打包的项目中会包含图集,并且在运行时自动加载它们,取消勾选图集的Include in Build选项可以禁用此行为。
如果禁用Include in Build,Unity仍会将图集打包到项目Assets文件夹中的** .spriteatlas * 文件中,只是运行的时候不会加载到内存中。因此,当精灵引用已禁用的图集中的纹理,由于引用纹理不可用或未加载(not available or loaded),该纹理将无法被找到,引用它的图片将显示为空白。此时要加载精灵图集,必须使用脚本通过后期绑定(Late Binding)执行此操作。(后期绑定稍后会讲)
使用和加载图集资源
将Sprite添加进图集前需要将自身的Texture Type设置为Sprite(2D and UI),然后才能将其加入图集的Objects for Packing进行打包,添加后记得点击Pack Preview按钮保存更改(因版本而异,使用Sprite Atlas V1不需要点击),打包完成后我们可以对图集进行预览。

加载图集
图集可以放在Resources文件夹中加载,也可以打包到AssetBundle中进行加载。
我们依然使用上面例子中的两个Image,假设打包的图集位于Resources文件夹中并且名为FirstAtlas,图集中的两张Sprite名字分别为Message和Lovely,场景中的两个Image分别是firstImg和secondImg,挂载脚本并为Image赋值。我们可以通过下面代码获取图集并且加载里面的Sprite。
SpriteAtlas atlas = Resources.Load<SpriteAtlas>("FirstAtlas"); //图集名称
Sprite message = atlas.GetSprite("Message"); //精灵体名称
Sprite lovely = atlas.GetSprite("Lovely");
firstImg.sprite = message;
secondImg.sprite = lovely;
运行后可以看到,虽然有两张Sprite不同的图片,但它们的DrawCall依然为1,说明合理使用图集确实能降低DrawCall。

![]()
接下来看一下怎么通过AB包加载,代码如下,效果和上图一致 。
//AB包保存路径及名称
AssetBundle bundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + @"\AssetBundles\myatlas");
//从AB包中加载图集
SpriteAtlas atlas = bundle.LoadAsset<SpriteAtlas>("FirstAtlas");
Sprite message = atlas.GetSprite("Message");
Sprite lovely = atlas.GetSprite("Lovely");
firstImg.sprite = message;
secondImg.sprite = lovely;
后期绑定(Late Binding)
前面我们提到,一个图集如果没有勾选Include in Build选项,只能通过后期绑定来加载里面的Sprite。所谓后期绑定,就是Unity在加载资源时,对于引用到的Sprite,Unity由于不知道Sprite和图集的引用关系,所以会发送一个请求加载这个Sprite的事件。使用者需要监听这个事件(即SpriteAtlasManager.atlasRequested回调),然后把这个Sprite所属的图集返回给Unity。这也就意味着,使用者需要自己加载图集。
依然以上面的两个Image为例,我们将它们的Source Image分别设置为Lovely和Message,并且取消勾选图集的Include in Build选项,在Editor模式下,图片虽然能正常显示,但运行项目后,会发现图片成了空白的。打包程序并且运行,查看它的Profile,发现图集并没有被加载进内存。这也印证了前面说的禁用Include in Build后Unity将不在运行时自动加载图集,引用的Sprite将不可见。

与此同时还出现了一条警告信息 ⚠️,它提示我们图集的atlasRequested事件没有被监听。

SpriteMananger是在运行期间管理SpriteAtlas的一个类,它包含atlasRequested和atlasRegisterer两个事件:
atlasRequested事件
当一个Sprite打包进了图集但是在运行期间无法找到该图集的位置时触发,通常情况下是因为没有勾选Include in Build选项。用户可以通过Action委托在必要的时候进行响应,进行响应的时候需要使用SpriteAtlas参数,下面是这个事件的构造
//string:精灵体打包图集的名称
//Action委托会自动加载图集
public static event Action<string, Action<SpriteAtlas>> atlasRequested;
atlasRegisterer事件
当atlasRequested中的Action委托调用后执行。
public static event Action<SpriteAtlas> atlasRegistered;
了解了这两个事件,我们继续上面的例子。既然Unity警告没有监听atlasRequested事件,那我们注册这个事件。
private void OnEnable()
{
//注册事件
SpriteAtlasManager.atlasRequested += RequestAtlas;
}
private void OnDisable()
{
//注销事件
SpriteAtlasManager.atlasRequested -= RequestAtlas;
}
private void RequestAtlas(string atlasName, Action<SpriteAtlas> callback)
{
//获取指定图集并且调用委托
//具体代码根据项目决定,我是将图集放在Resources文件夹
var sa = Resources.Load<SpriteAtlas>(atlasName);
callback(sa);
}
现在Image能够正常显示了!
我们也可以根据需要实现SpriteAtlasManager.atlasRegistered事件,这里就不作说明了。
后期绑定更多时候是和AssetBundle一起使用的,比如我们将UI元素Image放在一个AB包中,Image中引用的图集放在另一个AB包中,我们加载Image的时候并不需要知道两个AB包之间的依赖,这时可以通过后期绑定来解决这个问题。
需要注意的问题
图集虽然能够有效降低DrawCall,但使用不当容易造成内存资源的浪费。当图集中有一个Sprite处于活跃状态时,Unity会加载该Sprite所属图集中的所有纹理。如果一个图集中的有许多纹理,就算场景中只有一个纹理被引用,那么整个图集都会被加载,会造成较大的内存消耗。
假设场景中有一张Image,Image的Sprite被打包到一个图集中,这个图集中还包含其他资源。


程序运行的时候可以看到整张图集都被加载进了内存,虽然只引用了其中一张贴图。

为了解决这个问题,可以将场景中激活的所有或大多数纹理都打包到同一个图集,最好根据纹理的常见用途将分别打包成多个较小的图集,不过这样也可能造成DrawCall增加 。
当打包的图集空白空间过多时,可以手动减少打包图集的大小,从而减少占用的内存。通过调整图集Max Texture Size值,然后选择Pack Preview重新生成图集。

当 Max Texture Size 值小于精灵图集纹理的当前尺寸时,Unity 会减小打包纹理尺寸以尽可能与设置的 Max Texture Size 匹配,并自动修剪掉任何多余的空白空间。如果选择的一些精灵纹理超过精灵图集的 Max Texture Size 设置,则精灵图集将忽略 Max Texture Size 设置,并保持能够包含精灵纹理原始尺寸而需要的最小尺寸,此时编辑器也会报错。

但打包的图集仍然是可用的,只是大小与设置的不一致。
总结

参考
[1] Unity Manual: Sprite Atlas



被折叠的 条评论
为什么被折叠?



