在设计精灵图形时,可以方便地为每个角色使用单独的纹理文件。但是,精灵纹理的很大一部分通常会被图形元素之间的空白空间所占据,而此空间将导致运行时浪费视频内存。为了获得最佳性能,最好将几个精灵纹理中的图形紧密地打包在一个称为图集的纹理中。Unity 提供了一个 Sprite Packer 实用程序来自动执行从各个精灵纹理生成图集的过程。
Unity 在后台处理精灵图集纹理的生成和使用,这样用户就不需要进行手动分配。可选择在进入播放模式时或在构建期间打包图集,而图集一旦生成,便会从图集获取精灵对象的图形。
用户需要在纹理导入器 (Texture Importer) 中指定一个打包标签 (Packing Tag),从而为该纹理的精灵启用打包。
The Sprite Packer is disabled by default but you can configure it from the Editor window (menu: Edit > Project Settings, then select the Editor category). The sprite packing mode can be changed from Disabled to Enabled for Builds (packing is used for builds but not Play mode) or Always Enabled (packing is enabled for both Play mode and builds).
If you open the Sprite Packer window (menu: Window > 2D > Sprite Packer) and click the Pack button in the top-left corner, you will see the arrangement of the textures packed within the atlas.
如果在 Project 面板中选择一个精灵,该精灵也会突出显示以显示其在图集内的位置。轮廓实际上是渲染网格轮廓,还定义了用于紧密打包的区域。
Sprite Packer 窗口顶部的工具栏有许多影响打包和查看的控件。Pack 按钮可启动打包操作,但如果图集自上次打包后没有更改,则不会强制进行任何更新。(按__自定义 Sprite Packer__ 中所述实现自定义打包策略时,将出现一个相关的 Repack 按钮)。View Atlas 和 Page # 菜单允许选择在窗口中显示哪个图集的哪个页面(如果没有足够的空间来容纳最大纹理大小内的所有精灵,则单个图集可能拆分成多个“页面”)。页面编号旁边的菜单用于选择对图集使用的“打包策略”(见下文)。在工具栏的右侧有两个控件用于缩放视图以及在图集的颜色和 Alpha 显示之间切换。
Sprite Packer 使用__打包策略__来决定如何将精灵分配到图集。您可以创建自己的打包策略(见下文),但也可以随时使用 Default Packer Policy、Tight Packer Policy 或 Tight Rotate Enabled Sprite Packer Policy 选项。通过使用这些策略,纹理导入器中的 Packing Tag 属性可直接选择将精灵打包到的图集的名称,因此具有相同打包标签 (Packing Tag) 的所有精灵将打包到同一图集内。然后,根据纹理导入设置对图集进行进一步排序,使图集符合用户为源纹理进行的任何设置。具有相同纹理压缩设置的精灵将尽可能分组到相同的图集。
DefaultPackerPolicy 选项足以满足大多数用途,但如果需要,也可以实施自己的自定义打包策略。为此,需要实现编辑器脚本中的一个类的 UnityEditor.Sprites.IPackerPolicy 接口。此接口需要以下方法:
DefaultPackerPolicy 默认使用矩形打包(请参阅 SpritePackingMode)。如果正在创建纹理空间效果或想要使用不同的网格来渲染精灵,此设置将非常有用。自定义策略可覆盖此设置,并改用紧密打包。
using System;
using System.Linq;
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
public class DefaultPackerPolicySample : UnityEditor.Sprites.IPackerPolicy
{
protected class Entry
{
public Sprite sprite;
public UnityEditor.Sprites.AtlasSettings settings;
public string atlasName;
public SpritePackingMode packingMode;
public int anisoLevel;
}
private const uint kDefaultPaddingPower = 3; // 适合基础级别和两个 Mip 级别。
public virtual int GetVersion() { return 1; }
protected virtual string TagPrefix { get { return"[TIGHT]"; } }
protected virtual bool AllowTightWhenTagged { get { return true; } }
protected virtual bool AllowRotationFlipping { get { return false; } }
public static bool IsCompressedFormat(TextureFormat fmt)
{
if (fmt >= TextureFormat.DXT1 && fmt <= TextureFormat.DXT5)
return true;
if (fmt >= TextureFormat.DXT1Crunched && fmt <= TextureFormat.DXT5Crunched)
return true;
if (fmt >= TextureFormat.PVRTC_RGB2 && fmt <= TextureFormat.PVRTC_RGBA4)
return true;
if (fmt == TextureFormat.ETC_RGB4)
return true;
if (fmt >= TextureFormat.ATC_RGB4 && fmt <= TextureFormat.ATC_RGBA8)
return true;
if (fmt >= TextureFormat.EAC_R && fmt <= TextureFormat.EAC_RG_SIGNED)
return true;
if (fmt >= TextureFormat.ETC2_RGB && fmt <= TextureFormat.ETC2_RGBA8)
return true;
if (fmt >= TextureFormat.ASTC_RGB_4x4 && fmt <= TextureFormat.ASTC_RGBA_12x12)
return true;
if (fmt >= TextureFormat.DXT1Crunched && fmt <= TextureFormat.DXT5Crunched)
return true;
return false;
}
public void OnGroupAtlases(BuildTarget target, UnityEditor.Sprites.PackerJob job, int[] textureImporterInstanceIDs)
{
List<Entry> entries = new List<Entry>();
foreach (int instanceID in textureImporterInstanceIDs)
{
TextureImporter ti = EditorUtility.InstanceIDToObject(instanceID) as TextureImporter;
TextureFormat desiredFormat;
ColorSpace colorSpace;
int compressionQuality;
ti.ReadTextureImportInstructions(target, out desiredFormat, out colorSpace, out compressionQuality);
TextureImporterSettings tis = new TextureImporterSettings();
ti.ReadTextureSettings(tis);
Sprite[] sprites =
AssetDatabase.LoadAllAssetRepresentationsAtPath(ti.assetPath)
.Select(x => x as Sprite)
.Where(x => x != null)
.ToArray();
foreach (Sprite sprite in sprites)
{
Entry entry = new Entry();
entry.sprite = sprite;
entry.settings.format = desiredFormat;
entry.settings.colorSpace = colorSpace;
// 仅针对压缩格式使用压缩质量进行稍后分组。否则,将其保留为空。
entry.settings.compressionQuality = IsCompressedFormat(desiredFormat) ? compressionQuality : 0;
entry.settings.filterMode = Enum.IsDefined(typeof(FilterMode), ti.filterMode)
? ti.filterMode
: FilterMode.Bilinear;
entry.settings.maxWidth = 2048;
entry.settings.maxHeight = 2048;
entry.settings.generateMipMaps = ti.mipmapEnabled;
entry.settings.enableRotation = AllowRotationFlipping;
if (ti.mipmapEnabled)
entry.settings.paddingPower = kDefaultPaddingPower;
else
entry.settings.paddingPower = (uint)EditorSettings.spritePackerPaddingPower;
#if ENABLE_ANDROID_ATLAS_ETC1_COMPRESSION
entry.settings.allowsAlphaSplitting = ti.GetAllowsAlphaSplitting ();
#endif //ENABLE_ANDROID_ATLAS_ETC1_COMPRESSION
entry.atlasName = ParseAtlasName(ti.spritePackingTag);
entry.packingMode = GetPackingMode(ti.spritePackingTag, tis.spriteMeshType);
entry.anisoLevel = ti.anisoLevel;
entries.Add(entry);
}
Resources.UnloadAsset(ti);
}
// 首先根据图集名称将精灵分成几组
var atlasGroups =
from e in entries
group e by e.atlasName;
foreach (var atlasGroup in atlasGroups)
{
int page = 0;
// 然后根据纹理设置将这些组拆分为更小的组
var settingsGroups =
from t in atlasGroup
group t by t.settings;
foreach (var settingsGroup in settingsGroups)
{
string atlasName = atlasGroup.Key;
if (settingsGroups.Count() > 1)
atlasName += string.Format(" (Group {0})", page);
UnityEditor.Sprites.AtlasSettings settings = settingsGroup.Key;
settings.anisoLevel = 1;
// 使用此图集内所有条目的最高各向异性级别
if (settings.generateMipMaps)
foreach (Entry entry in settingsGroup)
if (entry.anisoLevel > settings.anisoLevel)
settings.anisoLevel = entry.anisoLevel;
job.AddAtlas(atlasName, settings);
foreach (Entry entry in settingsGroup)
{
job.AssignToAtlas(atlasName, entry.sprite, entry.packingMode, SpritePackingRotation.None);
}
++page;
}
}
}
protected bool IsTagPrefixed(string packingTag)
{
packingTag = packingTag.Trim();
if (packingTag.Length < TagPrefix.Length)
return false;
return (packingTag.Substring(0, TagPrefix.Length) == TagPrefix);
}
private string ParseAtlasName(string packingTag)
{
string name = packingTag.Trim();
if (IsTagPrefixed(name))
name = name.Substring(TagPrefix.Length).Trim();
return (name.Length == 0) ? "(unnamed)" : name;
}
private SpritePackingMode GetPackingMode(string packingTag, SpriteMeshType meshType)
{
if (meshType == SpriteMeshType.Tight)
if (IsTagPrefixed(packingTag) == AllowTightWhenTagged)
return SpritePackingMode.Tight;
return SpritePackingMode.Rectangle;
}
}
using System;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.Sprites;
using System.Collections.Generic;
// 除非打包标签包含 "[RECT]",否则 TightPackerPolicy 将对非矩形精灵进行紧密打包。
class TightPackerPolicySample : DefaultPackerPolicySample
{
protected override string TagPrefix { get { return "[RECT]"; } }
protected override bool AllowTightWhenTagged { get { return false; } }
protected override bool AllowRotationFlipping { get { return false; } }
}
using System;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.Sprites;
using System.Collections.Generic;
// 除非打包标签包含 "[RECT]",否则 TightPackerPolicy 将对非矩形精灵进行紧密打包。
class TightRotateEnabledSpritePackerPolicySample : DefaultPackerPolicySample
{
protected override string TagPrefix { get { return "[RECT]"; } }
protected override bool AllowTightWhenTagged { get { return false; } }
protected override bool AllowRotationFlipping { get { return true; } }
}