Edit: Update here!



Starling Framework's spritesheets are described by the TextureAtlas XML files.

Recently I have donated to the KenneyLand crowdfund and Kenney (www.kenney.nl) has sent me an asset pack containing a lot of spritesheets and other great assets. The spritesheets also came with XML files next to them. I currently use Unity3D as my main development tool and wanted to use these spritesheets for some of my game ideas.

I will now explain how I wrote a script to let me easily slice 2D sprites.

We will use the UI pack: Space extension by Kenney as an example. The spritesheet looks like this:

uipackSpace_sheet

As you can see, it is not evenly divided, so the grid slicer will not work. Also, the pixels are mostly touching, so the automatic slicing will not work either. To our luck, it comes with an XML file:
<TextureAtlas imagePath="sheet.png">
 <SubTexture name="barHorizontal_blue_left.png" x="400" y="78" width="6" height="26"/>
 <SubTexture name="barHorizontal_blue_mid.png" x="388" y="420" width="16" height="26"/>
 <SubTexture name="barHorizontal_blue_right.png" x="400" y="0" width="6" height="26"/>
 <SubTexture name="barHorizontal_green_left.png" x="400" y="104" width="6" height="26"/>
 <SubTexture name="barHorizontal_green_mid.png" x="386" y="366" width="16" height="26"/>
 <SubTexture name="barHorizontal_green_right.png" x="400" y="26" width="6" height="26"/>
 <!-- ... -->
 <SubTexture name="squareWhite.png" x="384" y="476" width="19" height="26"/>
 <SubTexture name="squareYellow.png" x="380" y="236" width="19" height="26"/>
 <SubTexture name="square_shadow.png" x="386" y="288" width="19" height="26"/>
</TextureAtlas>
We could use this XML file to manually create the rectangles... just kidding.

Implementation

Here is what the script will do:
  • Add a context menu item to TextureImporter inspector
  • This item will open a window to configure slicing
  • Then the window will close itself after slicing is completed.


If you do not care about the implementation details, you can skip to the script.

New Script
In your project explorer, create an 'Editor' folder for the script, if it does not exist already. This folder needs to be named 'Editor' because that's how Unity3D knows that these scripts will be used only for editor-specific functions. More information can be found here.

In there, create a C# script. I named mine 'TextureAtlasSlicer'. Here is what it looks like now:
using UnityEditor;
public class TextureAtlasSlicer : EditorWindow {
// TODO
}


Adding a custom context menu item
We need to use the MenuItem attribute. This menuitem's name should start with "CONTEXT/TextureImporter/", because that is how Unity3D understands that it is a context menu, and it is for the TextureImporter inspector.

using UnityEditor;
using UnityEngine;
public class TextureAtlasSlicer : EditorWindow {
[MenuItem("CONTEXT/TextureImporter/Slice Sprite Using XML")]
public static void SliceUsingXML(MenuCommand command)
{
TextureImporter textureImporter = command.context as TextureImporter;
TextureAtlasSlicer window = ScriptableObject.CreateInstance<TextureAtlasSlicer>();
window.importer = textureImporter;
window.ShowUtility();
}
[MenuItem("CONTEXT/TextureImporter/Slice Sprite Using XML", true)]
public static bool ValidateSliceUsingXML(MenuCommand command)
{
TextureImporter textureImporter = command.context as TextureImporter;
//valid only if the texture type is 'sprite' or 'advanced'.
return textureImporter && textureImporter.textureType == TextureImporterType.Sprite ||
textureImporter.textureType == TextureImporterType.Advanced;
}
public TextureImporter importer;
public TextureAtlasSlicer()
{
title = "Texture Atlas Slicer";
}
}


The ValidateSliceUsingXML function tells Unity3D to enable the menu item only for 'sprite' or 'advanced' texture types.

The actual function, SliceUsingXML is used to open a window and save the reference to the TextureImporter, as it will be used by the window to do the slicing. The TextureImporter is extracted out of the MenuCommand.

Here is what it looks like: Slice Sprite Context Menu

Configuring the slicing


2014-09-28 21_36_56-Unity - Untitled - 2DJointEditors - PC, Mac & Linux Standalone_

[SerializeField] private TextAsset xmlAsset;
public SpriteAlignment spriteAlignment = SpriteAlignment.Center;
public Vector2 customOffset = new Vector2(0.5f, 0.5f);
public void OnGUI() {
xmlAsset = EditorGUILayout.ObjectField("XML Source", xmlAsset, typeof (TextAsset), false) as TextAsset;
spriteAlignment = (SpriteAlignment) EditorGUILayout.EnumPopup("Pivot", spriteAlignment);
bool enabled = GUI.enabled;
if (spriteAlignment != SpriteAlignment.Custom) {
GUI.enabled = false;
}
EditorGUILayout.Vector2Field("Custom Offset", customOffset);
GUI.enabled = enabled;
if (xmlAsset == null) {
GUI.enabled = false;
}
if (GUILayout.Button("Slice")) {
//PerformSlice();
}
GUI.enabled = enabled;
}
view raw gistfile1.cs hosted with ❤ by GitHub


The xmlAsset field is used to choose which file will be used for slicing.

We can also customize the spriteAlignment and customOffset, which will set the default pivot for the slices.

And finally, the Slice button, that's why we're here!

Slicing
private void PerformSlice()
{
XmlDocument document = new XmlDocument();
document.LoadXml(xmlAsset.text);
XmlElement root = document.DocumentElement;
if (root.Name == "TextureAtlas") {
bool failed = false;
Texture2D texture = AssetDatabase.LoadMainAssetAtPath(importer.assetPath) as Texture2D;
int textureHeight = texture.height;
List<SpriteMetaData> metaDataList = new List<SpriteMetaData>();
foreach (XmlNode childNode in root.ChildNodes)
{
if (childNode.Name == "SubTexture") {
try {
int width = Convert.ToInt32(childNode.Attributes["width"].Value);
int height = Convert.ToInt32(childNode.Attributes["height"].Value);
int x = Convert.ToInt32(childNode.Attributes["x"].Value);
int y = textureHeight - (height + Convert.ToInt32(childNode.Attributes["y"].Value));
SpriteMetaData spriteMetaData = new SpriteMetaData
{
alignment = (int)spriteAlignment,
border = new Vector4(),
name = childNode.Attributes["name"].Value,
pivot = GetPivotValue(spriteAlignment, customOffset),
rect = new Rect(x, y, width, height)
};
metaDataList.Add(spriteMetaData);
}
catch (Exception exception) {
failed = true;
Debug.LogException(exception);
}
}
else
{
Debug.LogError("Child nodes should be named 'SubTexture' !");
failed = true;
}
}
if (!failed) {
importer.spriteImportMode = SpriteImportMode.Multiple;
importer.spritesheet = metaDataList.ToArray();
EditorUtility.SetDirty(importer);
try
{
AssetDatabase.StartAssetEditing();
AssetDatabase.ImportAsset(importer.assetPath);
}
finally
{
AssetDatabase.StopAssetEditing();
Close();
}
}
}
else
{
Debug.LogError("XML needs to have a 'TextureAtlas' root node!");
}
}
view raw gistfile1.cs hosted with ❤ by GitHub


We use the XmlDocument#LoadXml method in order to create an XmlDocument object in c#.

As specified above, the XML document has a TextureAtlas node as root, and SubTexture child nodes. Each SubTexture has five attributes: the name and the rectangle definitions (x, y, width, height).

In Unity3D, for textures, the y coordinate is 0 at the bottom. This is not the case for SubTexture rectangles, so we need to fix that. In order to get the texture height, we need to load the image file as a texture (line 10 in gist). Then, the y value for the rectangle is (texture height - (SubTexture.height + SubTexture.y)) (line 22 in gist).

In order to get the pivot, we can use the GetPivotValue function (borrowed from the Unity3D Editor's internal SpriteEditorUtility class):

public static Vector2 GetPivotValue(SpriteAlignment alignment, Vector2 customOffset)
{
switch (alignment)
{
case SpriteAlignment.Center:
return new Vector2(0.5f, 0.5f);
case SpriteAlignment.TopLeft:
return new Vector2(0.0f, 1f);
case SpriteAlignment.TopCenter:
return new Vector2(0.5f, 1f);
case SpriteAlignment.TopRight:
return new Vector2(1f, 1f);
case SpriteAlignment.LeftCenter:
return new Vector2(0.0f, 0.5f);
case SpriteAlignment.RightCenter:
return new Vector2(1f, 0.5f);
case SpriteAlignment.BottomLeft:
return new Vector2(0.0f, 0.0f);
case SpriteAlignment.BottomCenter:
return new Vector2(0.5f, 0.0f);
case SpriteAlignment.BottomRight:
return new Vector2(1f, 0.0f);
case SpriteAlignment.Custom:
return customOffset;
default:
return Vector2.zero;
}
}
view raw gistfile1.cs hosted with ❤ by GitHub


We can use this information to create a SpriteMetaData, which basically describe the slices for a sprite (line 24).

If there were no errors, we can slice the sprite: set the spriteImportMode to Multiple, and then assign the spritesheet array. Afterwards, we just need to tell the AssetDatabase to reimport the texture, and we're done!

Done!
Here's the whole code for the script:
using System;
using System.Collections.Generic;
using System.Xml;
using UnityEditor;
using UnityEngine;
public class TextureAtlasSlicer : EditorWindow {
[MenuItem("CONTEXT/TextureImporter/Slice Sprite Using XML")]
public static void SliceUsingXML(MenuCommand command)
{
TextureImporter textureImporter = command.context as TextureImporter;
TextureAtlasSlicer window = ScriptableObject.CreateInstance<TextureAtlasSlicer>();
window.importer = textureImporter;
window.ShowUtility();
}
[MenuItem("CONTEXT/TextureImporter/Slice Sprite Using XML", true)]
public static bool ValidateSliceUsingXML(MenuCommand command)
{
TextureImporter textureImporter = command.context as TextureImporter;
//valid only if the texture type is 'sprite' or 'advanced'.
return textureImporter && textureImporter.textureType == TextureImporterType.Sprite ||
textureImporter.textureType == TextureImporterType.Advanced;
}
public TextureImporter importer;
public TextureAtlasSlicer()
{
title = "Texture Atlas Slicer";
}
[SerializeField] private TextAsset xmlAsset;
public SpriteAlignment spriteAlignment = SpriteAlignment.Center;
public Vector2 customOffset = new Vector2(0.5f, 0.5f);
public void OnGUI() {
xmlAsset = EditorGUILayout.ObjectField("XML Source", xmlAsset, typeof (TextAsset), false) as TextAsset;
spriteAlignment = (SpriteAlignment) EditorGUILayout.EnumPopup("Pivot", spriteAlignment);
bool enabled = GUI.enabled;
if (spriteAlignment != SpriteAlignment.Custom) {
GUI.enabled = false;
}
EditorGUILayout.Vector2Field("Custom Offset", customOffset);
GUI.enabled = enabled;
if (xmlAsset == null) {
GUI.enabled = false;
}
if (GUILayout.Button("Slice")) {
PerformSlice();
}
GUI.enabled = enabled;
}
private void PerformSlice()
{
XmlDocument document = new XmlDocument();
document.LoadXml(xmlAsset.text);
XmlElement root = document.DocumentElement;
if (root.Name == "TextureAtlas") {
bool failed = false;
Texture2D texture = AssetDatabase.LoadMainAssetAtPath(importer.assetPath) as Texture2D;
int textureHeight = texture.height;
List<SpriteMetaData> metaDataList = new List<SpriteMetaData>();
foreach (XmlNode childNode in root.ChildNodes)
{
if (childNode.Name == "SubTexture") {
try {
int width = Convert.ToInt32(childNode.Attributes["width"].Value);
int height = Convert.ToInt32(childNode.Attributes["height"].Value);
int x = Convert.ToInt32(childNode.Attributes["x"].Value);
int y = textureHeight - (height + Convert.ToInt32(childNode.Attributes["y"].Value));
SpriteMetaData spriteMetaData = new SpriteMetaData
{
alignment = (int)spriteAlignment,
border = new Vector4(),
name = childNode.Attributes["name"].Value,
pivot = GetPivotValue(spriteAlignment, customOffset),
rect = new Rect(x, y, width, height)
};
metaDataList.Add(spriteMetaData);
}
catch (Exception exception) {
failed = true;
Debug.LogException(exception);
}
}
else
{
Debug.LogError("Child nodes should be named 'SubTexture' !");
failed = true;
}
}
if (!failed) {
importer.spriteImportMode = SpriteImportMode.Multiple;
importer.spritesheet = metaDataList.ToArray();
EditorUtility.SetDirty(importer);
try
{
AssetDatabase.StartAssetEditing();
AssetDatabase.ImportAsset(importer.assetPath);
}
finally
{
AssetDatabase.StopAssetEditing();
Close();
}
}
}
else
{
Debug.LogError("XML needs to have a 'TextureAtlas' root node!");
}
}
//SpriteEditorUtility
public static Vector2 GetPivotValue(SpriteAlignment alignment, Vector2 customOffset)
{
switch (alignment)
{
case SpriteAlignment.Center:
return new Vector2(0.5f, 0.5f);
case SpriteAlignment.TopLeft:
return new Vector2(0.0f, 1f);
case SpriteAlignment.TopCenter:
return new Vector2(0.5f, 1f);
case SpriteAlignment.TopRight:
return new Vector2(1f, 1f);
case SpriteAlignment.LeftCenter:
return new Vector2(0.0f, 0.5f);
case SpriteAlignment.RightCenter:
return new Vector2(1f, 0.5f);
case SpriteAlignment.BottomLeft:
return new Vector2(0.0f, 0.0f);
case SpriteAlignment.BottomCenter:
return new Vector2(0.5f, 0.0f);
case SpriteAlignment.BottomRight:
return new Vector2(1f, 0.0f);
case SpriteAlignment.Custom:
return customOffset;
default:
return Vector2.zero;
}
}
}
Comments:
Add a comment

Avatar for rosdi

rosdi

Works like charm on Unity 5.6!.. Coincidentally I am using Kenney's asset too and got curious about those xmls... a quick google brought me to this page..


Avatar for Fabricio

Fabricio

The script continues to work, even on Unity 5.1.0f3 Personal. There was only one warning and I changed the line 34 to: titleContent.text = "Texture Atlas Slicer"; Thank you.


Avatar for Winseven4lyf

Winseven4lyf

Just found Kenny's art, poked around the game icons pack and found some XML. Googled how I would use this with unity, and this pops up. Thank you.


Avatar for Enlik Tjioe

Enlik Tjioe

Great job bro! I'm using some Kenney asset for Unity. With this script, i can do Spritesheet slicing faster and efficient!


Avatar for PET

PET

Hey man!

Thanks for this script. I was having the EXACT same problem :D I have the sprites from Kenney and I was really looking on how to import them considering that as you said... the sprites are touching and the auto slicer was not working. I also notice the XML... so I did a google search for "Import Unity sprites form XML" or something like that and BAM... I got here

It's funny that you use the exact same sprites and you ran into the exact same problem.

Thanks for the script!


Avatar for a a

a a

this was exactly what i was looking for, appreciate you sharing your efforts m8


Avatar for Matic Leva

Matic Leva

This script has a little shortcoming but it's nothing major. I tried importing tileset of width 2048px but Unity's default max width is 1024 which caused the script to read wrong values. Then I changed this values and script worked like charm. I know this is not directly connected to script but someone might stumble upon same problem and they can easily fix it if they read this comment. Thank you for this nice script.


Avatar for firtoz

firtoz

Interesting, could you please link to the tileset or a similar sized one? I can try to alter the script :)


Avatar for firtoz

firtoz

I have just fixed this issue in the update (check top of page ) :D