- Home /
Pixel-perfect custom sprite pivot point
I'm playing around with some of the new 2D features introduced with Unity 4.3. For the main character, I've got a sprite sheet with varying frame dimensions. When the character stands still he uses smaller frames, but when he's swinging his sword around he needs more space.
My problem is that when I try to position the pivot point using the mouse it tends to get floating point values, not corresponding to exact pixel values. This prevents the animation from running pixel perfect and it all looks rather lame as the character kind of floats around with each frame.
The only possible solution I've come up with is to measure the pixel position in photoshop, recalculate that into a fractional value corresponding to the pivot position depending on the size of the frame and enter those numbers into the custom pivot x and y fields in the sprite editor. Obviously, that's not a very practical workflow.
The optimal workflow would be if I could simply get the pivot point to snap to exact pixel values, e.g., using the CMD key like snapping in the scene view. Is there another way to achieve what I'm after?
I know I could also use a single frame dimension, e.g. 64x64, use grid-slicing with a non-custom pivot point and position the sprite within that frame in photoshop to achieve pixel perfect pivot point, but that tends to produce a lot of white space in the textures because all frames must conform to the largest frame possible. Also it kind of goes against the whole idea of being able to set the pivot point on a per-frame basis.
The only possible solution I've come up with is to measure the pixel position in photoshop, recalculate that into a fractional value corresponding to the pivot position depending on the size of the frame and enter those numbers into the custom pivot x and y fields in the sprite editor. Obviously, that's not a very practical workflow.
I don't know of a solution to your issue I'm afraid, but that workflow issue you mention sounds like a good thing to write an editor script for. Sometimes the time taken to write the script can outweigh the cost of entering numbers 'by hand', if there is a lot of work to do.
Thanks for the suggestion Dave!
I'd really like to implement a auto-snap feature similar to the neat code snippet provided by karl_ in response to this question. That editor extension manipulates game objects in the scene view using the Selection class, but would it possible to write an editor extension to access the state and pivot points of the sprite editor window? How is the sprite editor exposed through the API, I can't seem to find anything about it in the documentation.
I am looking for this feature too. I am also considering to implement it myself.
Here's an example of how to modify the pivot point of a sprite (or many) from script, I think it could be useful:
http://answers.unity3d.com/questions/761009/unity2d-sprite-editor-multiple-pivot-point-edit.html
Enhancing the Sprite Editor would be very cool, but I am not sure it will be possible. If you find any information about it please leave a comment.
Hello, I know this is an old thread, but just thought I would check on the off chance someone did end up making an snap to pixel script.
Thanks!
Answer by sandygk · Jul 24, 2015 at 03:32 PM
I ended up doing mine (find the code below).
To use it, you just need to add the script inside the Editor Folder (be sure the file is called SpritePivotPositioner.cs). Then, in Unity, go to Windows -> Sprite Pivot Positioner. Select the sprite you want to modify. You'll notice the sprite has a small square, that's the current pivot, if the square is red it means the pivot is not in a pixel perfect position (otherwise it would be green). Click anywhere on the sprite to select a new pivot and click Apply Changes and that's it. I hope it helps. If you have any doubts just give me a shout.
using System;
using UnityEditor;
using UnityEngine;
internal class SpritePivotPositioner : EditorWindow
{
#region Private Fields
private static Texture2D _rectTexture;
private static bool _newPivotSelected;
private static int _newPivotXPixel;
private static int _newPivotYPixel;
private static Sprite _sprite;
private static float _frameMargin = 2;
private static int _scale = 6;
private static Color _borderColor = Color.gray;
private static float _shade = 0.7f;
private static Color _frameColor = new Color(_shade, _shade, _shade);
private static Color _currentPivotColor = Color.green;
private static Color _mousePosColor = Color.gray;
private static Color _newPivotColor = Color.blue;
private static Color _misplacedPivotColor = Color.red;
private float _outerFrameWidth;
private float _outerFrameHeight;
private float _innerFrameWidth;
private float _innerFrameHeight;
private float _x;
private float _y;
#endregion Private Fields
#region Public Methods
[MenuItem("Window/Sprite Pivot Positioner")]
public static void ShowWindow()
{
EditorWindow.GetWindow(typeof(SpritePivotPositioner));
}
#endregion Public Methods
#region Private Methods
private void Update()
{
Repaint();
}
private void OnGUI()
{
if (!SelectSprite()) return;
DrawSprite();
DrawMousePos();
DrawNewPivot();
DrawCurrentPivot();
ProcessMouseClick();
ProcessApplyButton();
}
private bool SelectSprite()
{
if (Selection.activeObject != null &&
Selection.activeObject is UnityEngine.Sprite)
{
if (_sprite != (Sprite)Selection.activeObject)
{
_sprite = (Sprite)Selection.activeObject;
_newPivotSelected = false;
}
return true;
}
return false;
}
private void DrawSprite()
{
_innerFrameWidth = _sprite.rect.width * _scale;
_innerFrameHeight = _sprite.rect.height * _scale;
_outerFrameWidth = _innerFrameWidth + 2 * _frameMargin;
_outerFrameHeight = _innerFrameHeight + 2 * _frameMargin;
var rect = EditorGUILayout.GetControlRect(true, _outerFrameHeight);
_x = rect.min.x;
_y = rect.min.y;
//draw the rect that fills the scroll:
GUIExtensions.DrawRect(new Rect(_x, _y, _outerFrameWidth, _outerFrameHeight), _borderColor, ref _rectTexture);
//draw the background colour of each frame:
_x += _frameMargin;
_y += _frameMargin;
GUIExtensions.DrawRect(new Rect(_x, _y, _innerFrameWidth, _innerFrameHeight), _frameColor, ref _rectTexture);
//draw the sprite
Texture texture = _sprite.texture;
Rect textureRect = _sprite.textureRect;
var textureCoords = new Rect(textureRect.x / texture.width, textureRect.y / texture.height,
textureRect.width / texture.width, textureRect.height / texture.height);
var positionRect = new Rect(_x, _y, _innerFrameWidth, _innerFrameHeight);
GUI.DrawTextureWithTexCoords(positionRect, texture, textureCoords);
}
private void DrawPixel(float x, float y, Color color, bool invertY = true)
{
bool intergerPosition = (x == Math.Floor(x) && y == Math.Floor(y));
x = _x + x * _scale;
if (invertY) y = _sprite.rect.height - 1 - y;
y = _y + y * _scale;
GUIExtensions.DrawRect(new Rect(x, y, _scale, _scale), intergerPosition ? color : _misplacedPivotColor, ref _rectTexture);
}
private void DrawCurrentPivot()
{
float x = _sprite.pivot.x;
float y = _sprite.pivot.y;
DrawPixel(x, y, _currentPivotColor);
}
private void DrawNewPivot()
{
if (!_newPivotSelected) return;
DrawPixel(_newPivotXPixel, _newPivotYPixel, _newPivotColor);
}
private void DrawMousePos()
{
int x, y;
if (GetMousePixel(out x, out y))
{
DrawPixel(x, y, _mousePosColor, false);
}
}
private bool GetMousePixel(out int x, out int y)
{
x = (int)((Event.current.mousePosition.x - _x) / _scale);
y = (int)((Event.current.mousePosition.y - _y) / _scale);
return x >= 0 && x < _sprite.rect.width &&
y >= 0 && y < _sprite.rect.height;
}
private void ProcessMouseClick()
{
int x, y;
if (GetMousePixel(out x, out y) &&
Event.current.isMouse)
{
_newPivotSelected = true;
_newPivotXPixel = x;
_newPivotYPixel = (int)_sprite.rect.height - 1 - y;
}
}
private void ProcessApplyButton()
{
GUI.enabled = _newPivotSelected;
if (!GUILayout.Button("Apply Changes")) return;
GUI.enabled = true;
string path = AssetDatabase.GetAssetPath(_sprite.texture);
var textureImporter = AssetImporter.GetAtPath(path) as TextureImporter;
var spritesheet = textureImporter.spritesheet;
for (int i = 0; i < spritesheet.Length; i++)
{
if (spritesheet[i].name != _sprite.name)
continue;
textureImporter.isReadable = true;
var spriteMetaData = spritesheet[i];
spriteMetaData.alignment = (int)SpriteAlignment.Custom;
float xFraction = _newPivotXPixel / (float)_sprite.rect.width;
float yFraction = _newPivotYPixel / (float)_sprite.rect.height;
spriteMetaData.pivot = new Vector2(xFraction, yFraction);
spritesheet[i] = spriteMetaData;
textureImporter.spritesheet = spritesheet;
textureImporter.isReadable = false; //apparently this must be before the AssetDatabase.ImportAsset(...) call
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
break;
}
_newPivotSelected = false;
}
private void OnDestroy()
{
_rectTexture = null;
}
#endregion Private Methods
}
public class GUIExtensions
{
static private GUIStyle _rectStyle;
//I am passing the rectTexture rather than using a local
//static variable because it will leak memory otherwise
public static void DrawRect(Rect position, Color color, ref Texture2D _rectTexture)
{
if (_rectTexture == null)
{
_rectTexture = new Texture2D(1, 1);
_rectTexture.hideFlags = HideFlags.DontSaveInEditor;
}
if (_rectStyle == null)
{
_rectStyle = new GUIStyle();
}
_rectTexture.SetPixel(0, 0, color);
_rectTexture.Apply();
_rectStyle.normal.background = _rectTexture;
GUI.Box(position, GUIContent.none, _rectStyle);
}
}
Thanks very much for sharing your script!
I'm getting this error however: Assets/Editor/SpritePivotPositioner.cs(92,17): error CS0103: The name `GUIExtensions' does not exist in the current context
Is GUIExtensions so kind of plugin?
Sorry, I forgot to include the class GUIExtensions in the script. It should work fine now.
Brilliant, thanks again. This is exactly what I was after!
Can I buy you a beer online? :)
This script is exactly what I was looking for, however, the window appears to be quite large on screen. I went into the script and altered the "_scale" but that made pixel snapping very inaccurate. Is there a way to have this window be resizable?
Answer by Avash · May 01, 2016 at 07:59 PM
It shows only an empty tab.
After following the instructions in OP's post, what you want to do is create a Sprite Sheet (Sprite using "$$anonymous$$ultiple" mode). After your various sprites are sliced, in the Project tab you will have the original image file, and if you expand it, the virtual sprites are all listed below. You want to have any of these virtual sprites selected before you select Windows -> Sprite Pivot Positioner. Did it work?