SKILL DETAIL
sprite-editor
unity-technologies/skills/sprite-editor
This skill edits Unity sprite metadata such as rects, borders, pivots, and outlines. Since this data lives inside the importer and cannot be edited directly, it requires running C# through a live Editor. The skill generates C# editor scripts that use ISpriteEditorDataProvider APIs, working with TextureImporter, PSBImporter, and custom importers. Operations include modifying name, rect, border, or pivot; adding or removing sprites; and setting outlines. All generated scripts must follow the Safe Core Pattern with mandatory capability checks to prevent data corruption. After execution, verify results in the Unity console and Project window.
Installation
npx skills add https://github.com/unity-technologies/skills --skill sprite-editor
技能檔案
SKILL.md
最近同步 · 2026年8月27日
references/api_reference.md›
# Unity Sprite Editor API Reference
## Table of Contents
- [Core Interface: ISpriteEditorDataProvider](#core-interface-ispriteeditordataprovider)
- [Properties](#properties)
- [Core Methods](#core-methods)
- [Callbacks](#callbacks)
- [Additional Data Providers](#additional-data-providers)
- [ISpriteNameFileIdDataProvider](#ispritenamefileiddataprovider)
- [ISpriteOutlineDataProvider](#ispriteoutlinedataprovider)
- [ISpritePhysicsOutlineDataProvider](#ispritephysicsoutlinedataprovider)
- [ISpriteBoneDataProvider](#ispritebonedataprovider)
- [ISpriteMeshDataProvider](#ispritemeshdataprovider)
- [ITextureDataProvider](#itexturedataprovider)
- [ISecondaryTextureDataProvider](#isecondarytexturedataprovider)
- [ISpriteFrameEditCapability](#ispriteframeeditcapability)
- [SpriteRect Properties](#spriterect-properties)
- [Common Patterns](#common-patterns)
- [Modifying Sprite Properties](#modifying-sprite-properties)
- [Working with Selection](#working-with-selection)
- [Version Considerations](#version-considerations)
## Core Interface: ISpriteEditorDataProvider
Main interface for editing sprite data. See [templates.md](templates.md) for the standard initialization and usage pattern.
### Properties
- `SpriteImportMode spriteImportMode` - How sprite data will be imported
- `float pixelsPerUnit` - Pixels per unit in world space
- `UnityObject targetObject` - Object providing the data
### Core Methods
- `SpriteRect[] GetSpriteRects()` - Returns array of SpriteRect
- `void SetSpriteRects(SpriteRect[] spriteRects)` - Updates sprite rectangles
- `void Apply()` - Applies changed data
- `void InitSpriteEditorDataProvider()` - Initializes the provider
- `T GetDataProvider<T>()` - Gets additional data providers
- `bool HasDataProvider(Type type)` - Checks if provider type is supported
### Callbacks
- `void RegisterDataChangeCallback(Action<ISpriteEditorDataProvider> action)`
- `void UnregisterDataChangeCallback(Action<ISpriteEditorDataProvider> action)`
## Additional Data Providers
### ISpriteNameFileIdDataProvider
Maps sprite names to file IDs (required for Unity 2021.2+ when adding/removing sprites).
```csharp
var nameFileIdProvider = dataProvider.GetDataProvider<ISpriteNameFileIdDataProvider>();
IEnumerable<SpriteNameFileIdPair> pairs = nameFileIdProvider.GetNameFileIdPairs();
nameFileIdProvider.SetNameFileIdPairs(updatedPairs);
```
### ISpriteOutlineDataProvider
Manages outline data for sprite tessellation.
```csharp
var outlineProvider = dataProvider.GetDataProvider<ISpriteOutlineDataProvider>();
List<Vector2[]> outlines = outlineProvider.GetOutlines(spriteGuid);
outlineProvider.SetOutlines(spriteGuid, newOutlines);
float tessellation = outlineProvider.GetTessellationDetail(spriteGuid);
outlineProvider.SetTessellationDetail(spriteGuid, 0.5f); // 0-1 range
```
### ISpritePhysicsOutlineDataProvider
Manages physics outlines for Polygon Collider 2D.
```csharp
var physicsProvider = dataProvider.GetDataProvider<ISpritePhysicsOutlineDataProvider>();
List<Vector2[]> physicsOutlines = physicsProvider.GetOutlines(spriteGuid);
physicsProvider.SetOutlines(spriteGuid, newPhysicsOutlines);
float tessellation = physicsProvider.GetTessellationDetail(spriteGuid);
physicsProvider.SetTessellationDetail(spriteGuid, 0.5f);
```
### ISpriteBoneDataProvider
Manages bone data for 2D animation.
```csharp
var boneProvider = dataProvider.GetDataProvider<ISpriteBoneDataProvider>();
List<SpriteBone> bones = boneProvider.GetBones(spriteGuid);
boneProvider.SetBones(spriteGuid, updatedBones);
```
### ISpriteMeshDataProvider
Manages custom sprite mesh data (vertices, indices, edges).
```csharp
var meshProvider = dataProvider.GetDataProvider<ISpriteMeshDataProvider>();
Vertex2DMetaData[] vertices = meshProvider.GetVertices(spriteGuid);
int[] indices = meshProvider.GetIndices(spriteGuid);
Vector2Int[] edges = meshProvider.GetEdges(spriteGuid);
meshProvider.SetVertices(spriteGuid, newVertices);
meshProvider.SetIndices(spriteGuid, newIndices);
meshProvider.SetEdges(spriteGuid, newEdges);
```
### ITextureDataProvider
Provides texture data for Sprite Editor.
```csharp
var textureProvider = dataProvider.GetDataProvider<ITextureDataProvider>();
Texture2D texture = textureProvider.texture;
Texture2D preview = textureProvider.previewTexture;
textureProvider.GetTextureActualWidthAndHeight(out int width, out int height);
Texture2D readable = textureProvider.GetReadableTexture2D();
```
### ISecondaryTextureDataProvider
Manages secondary textures.
```csharp
var secondaryProvider = dataProvider.GetDataProvider<ISecondaryTextureDataProvider>();
SecondarySpriteTexture[] textures = secondaryProvider.textures;
secondaryProvider.textures = newTextures;
```
### ISpriteFrameEditCapability
Controls sprite frame editing capabilities.
```csharp
var capabilityProvider = dataProvider.GetDataProvider<ISpriteFrameEditCapability>();
EditCapability capability = capabilityProvider.GetEditCapability();
capabilityProvider.SetEditCapability(newCapability);
```
## SpriteRect Properties
Key properties that can be modified on `SpriteRect`:
- `string name` - Sprite name
- `GUID spriteID` - Unique identifier (matches sprite asset's `GetSpriteID()`)
- `Rect rect` - Position and size in texture
- `Vector2 pivot` - Pivot point (0-1 range, relative to rect)
- `SpriteAlignment alignment` - Alignment preset (BottomLeft, Center, Custom, etc.)
- `Vector4 border` - 9-slice border (left, bottom, right, top)
**Note**: The `spriteID` property matches the GUID returned by calling `GetSpriteID()` on a sprite asset at runtime. This allows matching between editor-time configuration and runtime sprites.
## Common Patterns
See [templates.md](templates.md) for code patterns including:
- Safe Core Pattern with capability checks
- Modifying sprite properties
- Working with selection
## Version Considerations
See [background.md](background.md#version-specific-requirements) for Unity version-specific requirements (ISpriteNameFileIdDataProvider in 2021.2+).
references/background.md›
# Sprite Editor Background Information
## Table of Contents
- [Why Use ISpriteEditorDataProvider](#why-use-ispriteeditordataprovider)
- [Original vs Imported Image Sizes](#original-vs-imported-image-sizes)
- [The Critical Distinction](#the-critical-distinction)
- [Important Implications](#important-implications)
- [Critical for Slicing Operations](#critical-for-slicing-operations)
- [Importer Compatibility](#importer-compatibility)
- [TextureImporter Configuration](#textureimporter-configuration)
- [Other Importers](#other-importers)
- [Data Provider Initialization](#data-provider-initialization)
- [Version-Specific Requirements](#version-specific-requirements)
- [Unity 2021.2+](#unity-20212)
- [Unity 2021.1 and Earlier](#unity-20211-and-earlier)
## Why Use ISpriteEditorDataProvider
**ISpriteEditorDataProvider provides a unified interface** that works across all importer types (TextureImporter, PSBImporter, custom importers). This ensures:
- Scripts work consistently regardless of importer type
- Changes are properly communicated to the importer
- Sprite metadata is handled correctly
**Never access importer-specific properties directly.** Always use ISpriteEditorDataProvider for compatibility.
## Original vs Imported Image Sizes
### The Critical Distinction
**Sprite data is always based on the original image size**, not the imported Texture2D size.
**Original Image Size:**
- The dimensions of the source image file before import
- Example: 4096x4096 PNG file
**Imported Texture2D Size:**
- The actual texture size after import
- Can be **smaller** due to:
- Platform-specific texture size limitations (e.g., mobile max 2048x2048)
- Texture compression settings
- Max texture size in import settings
### Important Implications
1. **All sprite data uses original coordinates:**
- Sprite rectangles (rect)
- Borders (for 9-slicing)
- Pivots
- Outline coordinates
2. **Example:**
- Original image: 4096x4096 PNG
- Imported texture: 2048x2048 (due to max size setting)
- Sprite rect: `(0, 0, 4096, 4096)` ← Still uses original dimensions!
3. **Unity handles scaling internally** when rendering sprites
4. **Always work in original image coordinate space** when editing sprite data
### Critical for Slicing Operations
When performing slicing (automatic, grid, isometric), you **MUST ensure the texture being sliced matches the original source image size**.
If the imported Texture2D is smaller than original:
- Slicing coordinates will be incorrect
- Sprite rectangles won't align with intended regions
- The operation will fail
**Solution:** Use `GetTextureToSlice` utility (see scripts/README.md) to ensure correct texture dimensions for slicing.
## Importer Compatibility
### TextureImporter Configuration
For TextureImporter to support sprites:
- `textureType` must be `TextureImporterType.Sprite`
- `spriteImportMode` must be `SpriteImportMode.Multiple` for multiple sprites
The pre-flight check automatically configures these settings.
### Other Importers
PSBImporter and custom importers may have different configuration requirements. Always verify ISpriteEditorDataProvider support before attempting sprite operations.
### Data Provider Initialization
See [templates.md](templates.md) for the standard initialization pattern. If `dataProvider` is null, the importer does not support sprite editing.
## Version-Specific Requirements
### Unity 2021.2+
Adding or removing sprites requires additional steps:
```csharp
var nameFileIdProvider = dataProvider.GetDataProvider<ISpriteNameFileIdDataProvider>();
if (nameFileIdProvider != null)
{
// Get existing name-file ID pairs
var nameFileIdPairs = nameFileIdProvider.GetNameFileIdPairs();
// Update pairs when adding/removing sprites
// Add new pair: nameFileIdPairs.Add(new SpriteNameFileIdPair(name, fileId));
// Remove pair: nameFileIdPairs.RemoveAll(p => p.name == spriteName);
nameFileIdProvider.SetNameFileIdPairs(nameFileIdPairs);
}
```
### Unity 2021.1 and Earlier
ISpriteNameFileIdDataProvider does not exist. Simply use SetSpriteRects() without additional steps.
references/templates.md›
# Sprite Editor Code Templates
## Safe Core Pattern (MANDATORY)
Types are fully qualified because this runs through `eval`, which rejects `using` directives.
If you save it as a `.cs` file instead, add `using UnityEditor.U2D.Sprites;` and shorten them.
Use this structure for all sprite modification tasks.
**CRITICAL:** If capability checks fail, the script MUST return immediately. NEVER bypass capability checks even if you suspect the API might work - this can cause data corruption and violates Unity's data provider contract.
```csharp
// 1. Get and Init Data Provider
var importer = UnityEditor.AssetImporter.GetAtPath(assetPath);
var factory = new UnityEditor.U2D.Sprites.SpriteDataProviderFactories();
factory.Init();
var dataProvider = factory.GetSpriteEditorDataProviderFromObject(importer);
dataProvider.InitSpriteEditorDataProvider();
// 2. MANDATORY: Check Capabilities - ABORT if not supported
var editCapability = dataProvider.GetDataProvider<UnityEditor.U2D.Sprites.ISpriteFrameEditCapability>();
if (editCapability == null)
{
throw new System.Exception("Edit capability not supported by importer. Operation aborted.");
}
var capability = editCapability.GetEditCapability();
// Check for: EditSpriteName, EditSpriteRect, EditBorder, EditPivot, CreateAndDeleteSprite
if (!capability.HasCapability(UnityEditor.U2D.Sprites.EEditCapability.EditSpriteName))
{
throw new System.Exception("Operation not supported by importer. User action aborted.");
}
// 3. Read and Modify
var spriteRects = dataProvider.GetSpriteRects();
// ... logic here ...
// 4. Apply and Reimport
dataProvider.SetSpriteRects(spriteRects);
dataProvider.Apply();
importer.SaveAndReimport();
```
## Capability Check Pattern
Before performing any modification operation, check if the importer supports it. **ABORT the user action if the capability is not supported.**
**DO NOT rationalize bypassing this check.** Even if you believe the API might accept the operation, capability checks are mandatory for data integrity. Return immediately on failure - no exceptions.
```csharp
var editCapability = dataProvider.GetDataProvider<UnityEditor.U2D.Sprites.ISpriteFrameEditCapability>();
if (editCapability == null)
{
throw new System.Exception("Edit capability not supported by importer. User action aborted.");
return;
}
var capability = editCapability.GetEditCapability();
if (!capability.HasCapability(UnityEditor.U2D.Sprites.EEditCapability.EditSpriteName)) // Adjust based on task
{
throw new System.Exception("Importer does not support the requested operation. User action aborted.");
return;
}
```
### Available Capabilities
- `EditSpriteName` - Modify sprite names
- `EditSpriteRect` - Modify sprite rectangles
- `EditBorder` - Modify 9-slice borders
- `EditPivot` - Modify pivot points
- `CreateAndDeleteSprite` - Add/remove sprites or perform slicing
scripts/AutomaticSliceTexture.cs›
using System;
using UnityEditor.U2D.Sprites;
namespace Editor
{
static public partial class SpriteEditorUtility
{
/// <summary>
/// Automatically slices a texture by detecting visible pixel regions and creating sprite rectangles.
/// Uses Unity's internal automatic sprite detection algorithm to find sprite boundaries.
/// </summary>
/// <param name="spriteDataProvider">The sprite data provider for the texture.</param>
/// <param name="textureProvider">The texture data provider.</param>
/// <param name="minRectSize">Minimum size in pixels for detected sprite rectangles.</param>
/// <param name="extrudeSize">Number of pixels to extrude (expand) sprite boundaries.</param>
/// <param name="addNewSpriteMethod">Method for handling existing sprites (DeleteAll, Smart, Safe).</param>
/// <param name="nameGenerator">Function to generate sprite names based on index.</param>
/// <param name="kOverlapTolerance">Tolerance for detecting overlapping sprites.</param>
/// <param name="kBestFitTolerance">Tolerance for best-fit matching.</param>
/// <param name="bestFit">Whether to use best-fit algorithm for overlap detection.</param>
/// <returns>True if slicing succeeded, false if texture is not readable.</returns>
static public bool AutomaticSliceTexture(ISpriteEditorDataProvider spriteDataProvider, ITextureDataProvider textureProvider,
int minRectSize, int extrudeSize, AddNewSpriteMethod addNewSpriteMethod, Func<int, string> nameGenerator,
float kOverlapTolerance= 0.00001f, float kBestFitTolerance = 0.5f, bool bestFit = false)
{
var texture = GetTextureToSlice(textureProvider);
if (texture == null)
{
return false;
}
var rects = UnityEditorInternal.InternalSpriteUtility.GenerateAutomaticSpriteRectangles(texture, minRectSize, extrudeSize);
var newRects = GenerateNewSpriteRects(spriteDataProvider, rects, addNewSpriteMethod, nameGenerator, kOverlapTolerance, kBestFitTolerance, bestFit);
spriteDataProvider.SetSpriteRects(newRects.ToArray());
return true;
}
}
}
scripts/GenerateNewSpriteRects.cs›
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.U2D.Sprites;
using UnityEngine;
namespace Editor
{
static public partial class SpriteEditorUtility
{
public enum AddNewSpriteMethod
{
DeleteAll, // Remove all existing sprites and create new ones
Smart, // Update overlapping sprites, add non-overlapping ones
Safe // Only add sprites that don't overlap with existing ones
}
/// <summary>
/// Generates new sprite rectangles from a collection of rects, handling existing sprites based on the specified method.
/// Automatically assigns unique names using the provided name generator function.
/// </summary>
/// <param name="spriteDataProvider">The sprite data provider containing existing sprites.</param>
/// <param name="rects">Collection of rectangles to create sprites from.</param>
/// <param name="addNewSpriteMethod">Strategy for handling existing sprites.</param>
/// <param name="nameGenerator">Function that takes an index and returns a sprite name.</param>
/// <param name="kOverlapTolerance">Minimum overlap area ratio to consider sprites overlapping.</param>
/// <param name="kBestFitTolerance">Maximum overlap ratio difference for best-fit matching.</param>
/// <param name="bestFit">If true, finds the best matching existing sprite; if false, uses first match.</param>
/// <returns>List of sprite rectangles ready to be set on the data provider.</returns>
public static List<SpriteRect> GenerateNewSpriteRects(ISpriteEditorDataProvider spriteDataProvider, IEnumerable<Rect> rects, AddNewSpriteMethod addNewSpriteMethod, Func<int, string> nameGenerator,
float kOverlapTolerance= 0.00001f, float kBestFitTolerance = 0.5f, bool bestFit = false)
{
const int k_NameFindBreakLimit = 1000000;
var existingSpriteRects = spriteDataProvider.GetSpriteRects();
List<SpriteRect> newRects = new List<SpriteRect>();
HashSet<string> existingNames = new HashSet<string>();
Func<Rect, SpriteRect> newRectLambda = (frame) =>
{
int nameIndex = existingNames.Count;
var spriteName = "";
while (nameIndex < k_NameFindBreakLimit)
{
spriteName = nameGenerator(nameIndex++);
if (!existingNames.Contains(spriteName))
break;
}
if(nameIndex >= k_NameFindBreakLimit)
{
Debug.LogError("Failed to generate unique sprite name for automatic slicing. Please check the name generator function.");
return null;
}
existingNames.Add(spriteName);
return new SpriteRect()
{
name = spriteName,
alignment = SpriteAlignment.Center,
rect = frame,
};
};
Action<Rect> deleteAllSliceMethodLambda = (frame) =>
{
var newRect = newRectLambda(frame);
if (newRect != null)
{
newRects.Add(newRect);
}
};
Action<Rect> smartSliceMethodLambda = (frame) =>
{
var outSprite = GetExistingOverlappingSprite(spriteDataProvider, frame, kOverlapTolerance, kBestFitTolerance, bestFit);
if (outSprite != -1)
{
var existingRect = existingSpriteRects[outSprite];
existingRect.rect = frame;
if (existingNames.Contains(existingRect.name))
{
// Handle name conflict by renaming the previous sprite
var conflictRect = newRects.FindIndex(x => x.name == existingRect.name);
if (conflictRect != -1)
{
int nameIndex = existingNames.Count;
var spriteName = "";
while (nameIndex < k_NameFindBreakLimit)
{
spriteName = nameGenerator(nameIndex++);
if (!existingNames.Contains(spriteName))
break;
}
if(nameIndex >= k_NameFindBreakLimit)
{
Debug.LogError("Failed to generate unique sprite name for automatic slicing. Removing conflicting sprite.");
newRects.RemoveAt(conflictRect);
}
else
newRects[conflictRect].name = spriteName;
}
}
else
existingNames.Add(existingRect.name);
newRects.Add(existingRect);
}
else
{
var newRect = newRectLambda(frame);
if (newRect != null)
{
newRects.Add(newRect);
}
}
};
Action<Rect> safeSliceMethodLambda = (frame) =>
{
var outSprite = GetExistingOverlappingSprite(spriteDataProvider, frame, kOverlapTolerance, kBestFitTolerance, bestFit);
if (outSprite == -1)
{
var newRect = newRectLambda(frame);
if (newRect != null)
{
newRects.Add(newRect);
}
}
};
Action<Rect> sliceMethodLambda = safeSliceMethodLambda;
switch (addNewSpriteMethod)
{
case AddNewSpriteMethod.DeleteAll:
sliceMethodLambda = deleteAllSliceMethodLambda;
break;
case AddNewSpriteMethod.Smart:
sliceMethodLambda = smartSliceMethodLambda;
break;
case AddNewSpriteMethod.Safe:
// Preserve all existing sprites
foreach(var existingRect in existingSpriteRects)
{
existingNames.Add(existingRect.name);
}
newRects.AddRange(existingSpriteRects);
break;
}
foreach (var frame in rects)
{
sliceMethodLambda(frame);
}
return newRects;
}
private static int GetExistingOverlappingSprite(ISpriteEditorDataProvider dataProvider, Rect rect, float kOverlapTolerance= 0.00001f, float kBestFitTolerance = 0.5f, bool bestFit = false)
{
var spriteRects = dataProvider.GetSpriteRects();
var count = spriteRects.Length;
var bestRect = -1;
var rectArea = rect.width * rect.height;
if (rectArea < kOverlapTolerance)
return bestRect;
var bestRatio = float.MaxValue;
var bestArea = float.MaxValue;
for (int i = 0; i < count; i++)
{
Rect existingRect = spriteRects[i].rect;
if (existingRect.Overlaps(rect))
{
if (bestFit)
{
var dx = Math.Min(rect.xMax, existingRect.xMax) - Math.Max(rect.xMin, existingRect.xMin);
var dy = Math.Min(rect.yMax, existingRect.yMax) - Math.Max(rect.yMin, existingRect.yMin);
var overlapArea = dx * dy;
var overlapRatio = Math.Abs((overlapArea / rectArea) - 1.0f);
var existingArea = existingRect.width * existingRect.height;
if (overlapRatio < bestRatio || (overlapRatio < kOverlapTolerance && existingArea < bestArea))
{
bestRatio = overlapRatio;
if (overlapRatio < kOverlapTolerance)
bestArea = existingArea;
bestRect = i;
}
}
else
{
bestRect = i;
break;
}
}
}
if (bestFit && bestRatio > kBestFitTolerance)
return -1;
return bestRect;
}
}
}
scripts/GetTextureSourceImageSize.cs›
using UnityEditor;
using UnityEditor.U2D.Sprites;
using UnityEngine;
namespace Editor
{
static public partial class SpriteEditorUtility
{
/// <summary>
/// Get the original source image size of a texture, which is different from the texture size when the texture is imported with "Max Size" smaller than the original image size.
/// This method will try to get the original source image size from TextureImporter, if it fails, it will return the texture size as fallback.
/// </summary>
/// <param name="texture"></param>
/// <param name="width"></param>
/// <param name="height"></param>
static public void GetTextureSourceImageSize(Texture2D texture, out int width, out int height)
{
SpriteDataProviderFactories factories = new SpriteDataProviderFactories();
factories.Init();
var dataProvider = factories.GetSpriteEditorDataProviderFromObject(texture);
var textureDataProvider = dataProvider?.GetDataProvider<ITextureDataProvider>();
if(textureDataProvider != null)
{
textureDataProvider.GetTextureActualWidthAndHeight(out width, out height);
return;
}
width = texture.width;
height = texture.height;
}
}
}
scripts/GetTextureToSlice.cs›
using UnityEditor;
using UnityEditor.U2D.Sprites;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
namespace Editor
{
static public partial class SpriteEditorUtility
{
/// <summary>
/// Gets a readable texture for slicing operations, upscaling if necessary to match original dimensions.
/// This ensures slicing is performed on the original image size, not the imported texture size.
/// </summary>
/// <param name="textureDataProvider">The texture data provider.</param>
/// <returns>A readable Texture2D at original dimensions, or null if texture is not readable.</returns>
static public Texture2D GetTextureToSlice(ITextureDataProvider textureDataProvider)
{
textureDataProvider.GetTextureActualWidthAndHeight(out var width, out var height);
var readableTexture = textureDataProvider.GetReadableTexture2D();
if (readableTexture == null || (readableTexture.width == width && readableTexture.height == height))
return readableTexture;
// Upscale the imported texture to match original dimensions for accurate slicing
var texture = CreateTemporaryDuplicate(readableTexture, width, height);
texture.hideFlags = HideFlags.HideAndDontSave;
return texture;
}
/// <summary>
/// Creates a temporary duplicate of a texture at a specified size using RenderTexture.
/// Used internally to upscale textures for slicing operations.
/// </summary>
public static Texture2D CreateTemporaryDuplicate(Texture2D original, int width, int height)
{
if (!ShaderUtil.hardwareSupportsRectRenderTexture || !(bool) (Object) original)
return null;
RenderTexture active = RenderTexture.active;
RenderTexture temporary = RenderTexture.GetTemporary(width, height, 0, SystemInfo.GetGraphicsFormat(DefaultFormat.LDR));
Graphics.Blit(original, temporary);
RenderTexture.active = temporary;
bool flag = width >= SystemInfo.maxTextureSize || height >= SystemInfo.maxTextureSize;
Texture2D temporaryDuplicate = new Texture2D(width, height, TextureFormat.RGBA32, original.mipmapCount > 1 | flag);
temporaryDuplicate.ReadPixels(new Rect(0.0f, 0.0f, width, height), 0, 0);
temporaryDuplicate.Apply();
RenderTexture.ReleaseTemporary(temporary);
temporaryDuplicate.alphaIsTransparency = original.alphaIsTransparency;
return temporaryDuplicate;
}
}
}
scripts/GridSliceTexture.cs›
using System;
using UnityEngine;
using UnityEditor.U2D.Sprites;
namespace Editor
{
static public partial class SpriteEditorUtility
{
/// <summary>
/// Slices a texture into a regular grid of sprite rectangles based on specified cell size, offset, and padding.
/// Optionally keeps or discards empty rectangles based on pixel alpha values.
/// </summary>
/// <param name="spriteDataProvider">The sprite data provider for the texture.</param>
/// <param name="textureProvider">The texture data provider.</param>
/// <param name="offset">The offset from the top-left corner to start the grid.</param>
/// <param name="size">The size of each grid cell (width x height).</param>
/// <param name="padding">The padding between grid cells.</param>
/// <param name="addNewSpriteMethod">Method for handling existing sprites (DeleteAll, Smart, Safe).</param>
/// <param name="nameGenerator">Function to generate sprite names based on index.</param>
/// <param name="keepEmptyRects">Whether to keep sprites with no visible pixels.</param>
/// <param name="kOverlapTolerance">Tolerance for detecting overlapping sprites.</param>
/// <param name="kBestFitTolerance">Tolerance for best-fit matching.</param>
/// <param name="bestFit">Whether to use best-fit algorithm for overlap detection.</param>
/// <returns>True if slicing succeeded, false if texture is not readable.</returns>
static public bool GridSliceTexture(ISpriteEditorDataProvider spriteDataProvider, ITextureDataProvider textureProvider, Vector2 offset, Vector2 size, Vector2 padding,
AddNewSpriteMethod addNewSpriteMethod, Func<int, string> nameGenerator,
bool keepEmptyRects =false, float kOverlapTolerance= 0.00001f, float kBestFitTolerance = 0.5f, bool bestFit = false)
{
var textureToUse = GetTextureToSlice(textureProvider);
if (textureToUse == null)
{
return false;
}
var rects = UnityEditorInternal.InternalSpriteUtility.GenerateGridSpriteRectangles(textureToUse, offset, size, padding, keepEmptyRects);
var newRects = GenerateNewSpriteRects(spriteDataProvider, rects, addNewSpriteMethod, nameGenerator, kOverlapTolerance, kBestFitTolerance, bestFit);
spriteDataProvider.SetSpriteRects(newRects.ToArray());
return true;
}
}
}
scripts/IsometricSliceTexture.cs›
using System;
using System.Collections.Generic;
using UnityEditor.U2D.Sprites;
using UnityEngine;
namespace Editor
{
static public class IsometricSliceUtility
{
/// <summary>
/// Slices a texture into isometric tiles based on specified size and offset.
/// Creates sprite rectangles in an isometric diamond pattern and optionally sets diamond-shaped outlines.
/// </summary>
/// <param name="spriteDataProvider">The sprite data provider for the texture.</param>
/// <param name="textureProvider">The texture data provider.</param>
/// <param name="size">The size of each isometric tile (width x height).</param>
/// <param name="offset">The offset from the top-left corner to start slicing.</param>
/// <param name="alignment">The alignment value for sprites.</param>
/// <param name="pivot">The pivot point for sprites.</param>
/// <param name="slicingMethod">Method for handling existing sprites (DeleteAll, Smart, Safe).</param>
/// <param name="nameGenerator">Function to generate sprite names based on index.</param>
/// <param name="kOverlapTolerance">Tolerance for detecting overlapping sprites.</param>
/// <param name="kBestFitTolerance">Tolerance for best-fit matching.</param>
/// <param name="bestFit">Whether to use best-fit algorithm for overlap detection.</param>
/// <param name="keepEmptyRects">Whether to keep sprites with no visible pixels.</param>
/// <param name="isAlternate">Whether to start with alternating row offset.</param>
/// <returns>True if slicing succeeded, false if texture is not readable.</returns>
static public bool IsometricSliceTexture(ISpriteEditorDataProvider spriteDataProvider, ITextureDataProvider textureProvider,
Vector2 size, Vector2 offset, int alignment, Vector2 pivot, SpriteEditorUtility.AddNewSpriteMethod slicingMethod, Func<int, string> nameGenerator,
float kOverlapTolerance = 0.00001f, float kBestFitTolerance = 0.5f, bool bestFit = false, bool keepEmptyRects = false, bool isAlternate = false)
{
var texture = SpriteEditorUtility.GetTextureToSlice(textureProvider);
if (texture == null)
{
return false;
}
var rects = GetIsometricRects(texture, size, offset, isAlternate, keepEmptyRects);
var newRects = SpriteEditorUtility.GenerateNewSpriteRects(spriteDataProvider, rects, slicingMethod, nameGenerator, kOverlapTolerance, kBestFitTolerance, bestFit);
spriteDataProvider.SetSpriteRects(newRects.ToArray());
// Set diamond-shaped outlines for isometric sprites
var outlineDataProvider = spriteDataProvider.GetDataProvider<ISpriteOutlineDataProvider>();
if (outlineDataProvider != null)
{
List<Vector2[]> outlines = new List<Vector2[]>(4);
outlines.Add(new[] {
new Vector2(0.0f, -size.y / 2),
new Vector2(size.x / 2, 0.0f),
new Vector2(0.0f, size.y / 2),
new Vector2(-size.x / 2, 0.0f)
});
foreach (var rect in newRects)
{
outlineDataProvider.SetOutlines(rect.spriteID, outlines);
}
}
return true;
}
private static bool PixelHasAlpha(int x, int y, int width, bool[] alphaPixelCache)
{
var index = y * width + x;
return alphaPixelCache[index];
}
/// <summary>
/// Generates rectangles for isometric tile slicing by walking the texture in an isometric pattern.
/// Optionally filters out empty rectangles based on alpha pixel density.
/// </summary>
public static IEnumerable<Rect> GetIsometricRects(Texture2D textureToUse, Vector2 size, Vector2 offset, bool isAlternate, bool keepEmptyRects)
{
var alphaPixelCache = new bool[textureToUse.width * textureToUse.height];
Color32[] pixels = textureToUse.GetPixels32();
for (int i = 0; i < pixels.Length; i++)
alphaPixelCache[i] = pixels[i].a != 0;
var gradient = (size.x / 2) / (size.y / 2);
bool isAlt = isAlternate;
float x = offset.x;
if (isAlt)
x += size.x / 2;
float y = textureToUse.height - offset.y;
while (y - size.y >= 0)
{
while (x + size.x <= textureToUse.width)
{
var rect = new Rect(x, y - size.y, size.x, size.y);
if (!keepEmptyRects)
{
// Check if the isometric diamond area has sufficient alpha pixels
int sx = (int)rect.x;
int sy = (int)rect.y;
int width = (int)size.x;
int odd = ((int)size.y) % 2;
int topY = ((int)size.y / 2) - 1;
int bottomY = topY + odd;
int totalPixels = 0;
int alphaPixels = 0;
// Sample pixels in diamond shape
for (int ry = 0; ry <= topY; ry++)
{
var pixelOffset = Mathf.CeilToInt(gradient * ry);
for (int rx = pixelOffset; rx < width - pixelOffset; ++rx)
{
if (PixelHasAlpha(sx + rx, sy + topY - ry, textureToUse.width, alphaPixelCache))
alphaPixels++;
if (PixelHasAlpha(sx + rx, sy + bottomY + ry, textureToUse.width, alphaPixelCache))
alphaPixels++;
totalPixels += 2;
}
}
if (odd > 0)
{
int ry = topY + 1;
for (int rx = 0; rx < size.x; ++rx)
{
if (PixelHasAlpha(sx + rx, sy + ry, textureToUse.width, alphaPixelCache))
alphaPixels++;
totalPixels++;
}
}
if (totalPixels > 0 && ((float)alphaPixels) / totalPixels > 0.01f)
yield return rect;
}
else
yield return rect;
x += size.x;
}
isAlt = !isAlt;
x = offset.x;
if (isAlt)
x += size.x / 2;
y -= size.y / 2;
}
}
}
}
scripts/README.md›
# Sprite Editor Utility Examples
Reference implementations demonstrating sprite editing operations. Each file contains a single method for token efficiency.
## Basic Operations
### GetTextureSourceImageSize.cs
Gets the original source image dimensions (before import).
**Use when:** You need to know the true image dimensions, not the imported texture size.
**Key APIs:** ITextureDataProvider.GetTextureActualWidthAndHeight()
### SpriteToPng.cs
Exports a sprite to PNG byte array by rendering its mesh geometry.
**Use when:** Extracting individual sprites from sprite sheets.
**Key concepts:** Renders sprite vertices, UVs, and triangles. Handles tight packing and custom meshes.
### SetPivotExample.cs
Demonstrates setting sprite pivots using both predefined alignments and custom pivot positions.
**Use when:** Changing sprite pivot points.
**Key methods:**
- `SpriteEditorUtility.SetCustomPivot()` - Sets custom pivot position. Must set `alignment = SpriteAlignment.Custom` and `pivot = Vector2`
- `SpriteEditorUtility.SetPivot()` - Sets predefined alignment (Center, BottomLeft, TopRight, etc.)
**Important:** Always set `alignment` field when changing pivots. Custom pivots require `SpriteAlignment.Custom`.
## Slicing Operations
### GetTextureToSlice.cs
Prepares a readable texture for slicing, upscaling to original dimensions if needed.
**Use when:** Before any slicing operation to ensure correct coordinates.
**Key concept:** See [../references/background.md](../references/background.md#critical-for-slicing-operations) for why this is necessary.
### AutomaticSliceTexture.cs
Automatically detects sprite regions using Unity's built-in detection algorithm.
**Use when:** Slicing sprite sheets where sprites have transparent borders.
**Key APIs:**
- UnityEditorInternal.InternalSpriteUtility.GenerateAutomaticSpriteRectangles()
- GenerateNewSpriteRects() for sprite management
### GridSliceTexture.cs
Slices textures into regular grid patterns.
**Use when:** Sprite sheet has evenly-spaced sprites (e.g., animation frames, tile sets).
**Parameters:** offset, size, padding, keepEmptyRects
**Key APIs:** UnityEditorInternal.InternalSpriteUtility.GenerateGridSpriteRectangles()
### IsometricSliceTexture.cs
Slices textures into isometric diamond-pattern tiles.
**Use when:** Working with isometric tile sets (e.g., isometric RPG tiles).
**Key features:**
- Diamond-shaped outline generation
- Empty tile detection based on alpha pixels
- Alternating row offset support
**Key APIs:** ISpriteOutlineDataProvider for diamond outlines
### GenerateNewSpriteRects.cs
Core utility for managing sprite rectangles during slicing operations.
**Three modes:**
- **DeleteAll**: Replace all existing sprites with new ones
- **Smart**: Update overlapping sprites, add non-overlapping ones
- **Safe**: Only add sprites that don't overlap with existing sprites
**Key features:**
- Automatic sprite naming with conflict resolution
- Overlap detection (with tolerance and best-fit options)
- Preserves existing sprites in Safe/Smart modes
**Use when:** Implementing custom slicing logic or managing sprite updates.
## Usage Pattern
All slicing utilities follow this pattern:
```csharp
// 1. Get texture at original size
var texture = GetTextureToSlice(textureProvider);
// 2. Generate rectangles
var rects = [algorithm to generate Rect collection];
// 3. Convert to SpriteRects with management logic
var newRects = GenerateNewSpriteRects(
spriteDataProvider,
rects,
addNewSpriteMethod,
nameGenerator
);
// 4. Apply to data provider
spriteDataProvider.SetSpriteRects(newRects.ToArray());
```
## Name Generator Examples
The `nameGenerator` parameter is a function that takes an integer index and returns a sprite name string.
### Using Asset Filename from Data Provider
```csharp
string assetPath = AssetDatabase.GetAssetPath(spriteDataProvider.targetObject);
string filename = !string.IsNullOrEmpty(assetPath)
? System.IO.Path.GetFileNameWithoutExtension(assetPath)
: "sprite";
Func<int, string> nameGenerator = (index) => $"{filename}_{index}";
// Produces: character_sheet_0, character_sheet_1, etc. (or sprite_0, sprite_1 if no path)
```
### Simple Numbered Names
```csharp
Func<int, string> nameGenerator = (index) => $"sprite_{index}";
// Produces: sprite_0, sprite_1, sprite_2, etc.
```
## Important Notes
- All coordinates are in **original image space** (see [../references/background.md](../references/background.md#original-vs-imported-image-sizes))
- Use GetTextureToSlice before any slicing operation
- GenerateNewSpriteRects handles name conflicts and overlap detection
- For Unity 2021.2+ requirements, see [../references/background.md](../references/background.md#version-specific-requirements)
scripts/SetPivotExample.cs›
using UnityEditor;
using UnityEditor.U2D.Sprites;
using UnityEngine;
namespace Editor
{
static public partial class SpriteEditorUtility
{
/// <summary>
/// Sets a custom pivot point for a specific sprite within a sprite editor data provider.
/// The pivot is specified as a normalized Vector2 where (0,0) is bottom-left and (1,1) is top-right.
/// </summary>
/// <param name="dataProvider">The sprite editor data provider containing the sprite data.</param>
/// <param name="sprite">The GUID of the sprite to modify.</param>
/// <param name="pivot">The normalized pivot position (0-1 range for both x and y).</param>
/// <returns>True if the sprite was found and the pivot was set successfully; otherwise, false.</returns>
public static bool SetCustomPivot(ISpriteEditorDataProvider dataProvider, GUID sprite, Vector2 pivot)
{
var rects = dataProvider.GetSpriteRects();
for (int i = 0; i < rects.Length; ++i)
{
if (rects[i].spriteID == sprite)
{
rects[i].pivot = pivot;
rects[i].alignment = SpriteAlignment.Custom;
dataProvider.SetSpriteRects(rects);
return true;
}
}
return false;
}
/// <summary>
/// Sets a predefined pivot alignment for a specific sprite within a sprite editor data provider.
/// Uses Unity's built-in alignment options (e.g., Center, TopLeft, BottomRight).
/// </summary>
/// <param name="dataProvider">The sprite editor data provider containing the sprite data.</param>
/// <param name="sprite">The GUID of the sprite to modify.</param>
/// <param name="alignment">The predefined sprite alignment to apply.</param>
/// <returns>True if the sprite was found and the alignment was set successfully; otherwise, false.</returns>
public static bool SetPivot(ISpriteEditorDataProvider dataProvider, GUID sprite, SpriteAlignment alignment)
{
var rects = dataProvider.GetSpriteRects();
for (int i = 0; i < rects.Length; ++i)
{
if (rects[i].spriteID == sprite)
{
rects[i].alignment = alignment;
dataProvider.SetSpriteRects(rects);
return true;
}
}
return false;
}
}
}scripts/SpriteToPng.cs›
using UnityEditor;
using UnityEngine;
namespace Editor
{
static public partial class SpriteEditorUtility
{
static public byte[] SpriteToPng(Sprite sprite)
{
Texture2D texture = sprite.texture;
Rect rect = sprite.textureRect;
// Create a temporary RenderTexture
RenderTexture renderTexture = RenderTexture.GetTemporary(
(int)rect.width,
(int)rect.height,
0,
RenderTextureFormat.ARGB32,
RenderTextureReadWrite.Default);
// Save the current active RenderTexture
RenderTexture previousActive = RenderTexture.active;
RenderTexture.active = renderTexture;
GL.Clear(true, true, Color.clear);
// Get sprite vertices and UVs
Vector2[] vertices = sprite.vertices;
Vector2[] uvs = sprite.uv;
ushort[] triangles = sprite.triangles;
// Calculate bounds for centering
Vector2 min = new Vector2(float.MaxValue, float.MaxValue);
Vector2 max = new Vector2(float.MinValue, float.MinValue);
foreach (var v in vertices)
{
min = Vector2.Min(min, v);
max = Vector2.Max(max, v);
}
Vector2 size = max - min;
Vector2 offset = -min;
// Create material for rendering the sprite with proper alpha
Material mat = new Material(Shader.Find("UI/Default"));
mat.mainTexture = texture;
// Render the sprite mesh
GL.PushMatrix();
GL.LoadPixelMatrix(0, rect.width, 0, rect.height);
mat.SetPass(0);
GL.Begin(GL.TRIANGLES);
for (int i = 0; i < triangles.Length; i += 3)
{
for (int j = 0; j < 3; j++)
{
int idx = triangles[i + j];
Vector2 vertex = vertices[idx];
Vector2 uv = uvs[idx];
// Transform vertex to render texture space
float x = (vertex.x + offset.x) * rect.width / size.x;
float y = (vertex.y + offset.y) * rect.height / size.y;
GL.TexCoord2(uv.x, uv.y);
GL.Vertex3(x, y, 0);
}
}
GL.End();
GL.PopMatrix();
// Read pixels from RenderTexture into a new Texture2D
Texture2D croppedTexture = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.ARGB32, false);
croppedTexture.ReadPixels(new Rect(0, 0, rect.width, rect.height), 0, 0);
croppedTexture.Apply();
// Restore the previous RenderTexture and clean up
RenderTexture.active = previousActive;
RenderTexture.ReleaseTemporary(renderTexture);
Object.DestroyImmediate(mat);
return croppedTexture.EncodeToPNG();
}
}
}
SKILL.md›
---
name: sprite-editor
description: Edits Unity sprite properties by generating C# editor scripts using ISpriteEditorDataProvider APIs. Handles sprite rectangles, borders, pivots, outlines, and slicing operations (automatic, grid, isometric). Use when working with sprite assets, sprite sheets, texture atlases, or sprite slicing.
modes: [agent, ask]
---
# Sprite Editor
Sprite metadata (rects, borders, pivots, outlines) lives inside the importer, not in a file
you can edit — reaching it means running C# through a live Editor.
**The `unity-cli` skill owns getting you there** — installing the CLI, confirming a connected
Editor, adding the project's `com.unity.pipeline` package, telling a genuinely absent Editor
apart from one stuck in Safe Mode, and discovering the Editor's command catalog. Follow it
first; don't re-derive any of it here.
Two things it can't know for you:
- **You need `eval` in particular**, not just a reachable Editor. Confirm it appears in the
catalog — its presence depends on the Pipeline package version, not on the CLI.
- **Never hand-edit a `.meta` file to change sprite metadata.** The importer owns that data
and the capability checks below exist to prevent corruption, so an unreachable Editor is a
stop, not a cue to improvise.
Run C# through the connected Editor with the `eval` command. Discover its parameter shape
from `unity command --format json` rather than assuming one — the inline form is
`unity command eval --code '<snippet>'`, and some Pipeline versions also register
`eval_file` for running a snippet from a file. **Check the catalog before reaching for
`eval_file`; it is frequently absent.** `unity command` defaults to a 30 second timeout.
Generates C# editor scripts to manipulate Unity sprites using ISpriteEditorDataProvider. Works with TextureImporter, PSBImporter, and custom importers.
### Passing C# to `eval`
`eval` compiles a **statement block, not a file**. Two consequences, both of which cause a
compile error rather than a warning:
- **No `using` directives.** The compiler reads `using UnityEngine;` as a resource-disposal
statement and rejects it (`CS0210`).
- **Types must be fully qualified.** A bare `AssetDatabase` or `Volume` does not resolve
(`CS0246` / `CS0103`), and a bare `Object` is ambiguous with `object` (`CS0104`).
Where a snippet below is written as a file — with usings, for readability, or because it is
meant to be saved into the project — qualify the types before passing it to `eval`.
## Workflow
All generated scripts must follow the Safe Core Pattern in [references/templates.md](references/templates.md), which includes MANDATORY capability checks. NEVER attempt operations if capability checks fail - this prevents data corruption. After execution, verify results in Unity console and Project window.
## Common Operations
**Modify Name/Rect/Border/Pivot:** Update corresponding `SpriteRect` fields (see scripts/SetPivotExample.cs for pivot examples)
- Requires: `EditSpriteName`, `EditSpriteRect`, `EditBorder`, or `EditPivot`
**Add/Remove/Slice:** Create or filter `SpriteRect` array (see [references/background.md](references/background.md) for Unity 2021.2+ requirements)
- Requires: `CreateAndDeleteSprite`
**Set Outlines:** Get `ISpriteOutlineDataProvider` → Call `SetOutlines()` with GUID + Vector2 arrays
## Important Notes
- Do NOT use AssetPostprocessor or MenuItem patterns
- Generate standalone snippets only — no `AssetPostprocessor`, no `MenuItem`
- **Enum assignments:** Always use enum values and cast to numeric types. Never use raw numbers.
- ✅ Correct: `(int)SpriteAlignment.Center`
- ❌ Wrong: `1` (magic number)