Advertisement
Guest User

Unity Pixel Perfect Rotation Script

a guest
Feb 20th, 2018
2,607
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 16.26 KB | None | 0 0
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4.  
  5. // How To Use:
  6. // ------------
  7. // 1. Attach this script to the object that you want to rotate (it must have a SpriteRenderer attached to it)
  8. // 2. Call SetRotate(degrees) to rotate the sprite
  9. // 3. If you use the SetRotate method with a pivot, use the referenced Vector3 to translate your sprite after the rotation
  10. //    by adding the result to the sprite's transform.localPosition
  11.  
  12. [RequireComponent(typeof(SpriteRenderer))]
  13. public class PixelRotate : MonoBehaviour {
  14.  
  15.     private SpriteRenderer spriteRenderer;
  16.     private Texture2D referenceTexture; // The 8x scaled image used for sampling during the rotation
  17.     private Texture2D spriteTexture; // The final image that gets set on the sprite
  18.     private float Angle { get; set; } // I don't understand C# lol
  19.  
  20.     // Width and Height in pixels of original sprite
  21.     // We can use this for rotating around a pivot
  22.     private int originalWidth;
  23.     private int originalHeight;
  24.     private int xPad;
  25.     private int yPad;
  26.  
  27.     // Used when scaling
  28.     private Rect scaledTexBounds;
  29.  
  30.     // Use this for initialization
  31.     void Awake () {
  32.         spriteRenderer = GetComponent<SpriteRenderer>();
  33.  
  34.         Sprite original = spriteRenderer.sprite;
  35.  
  36.         int xoff = (int)original.rect.x;
  37.         int yoff = (int)original.rect.y;
  38.  
  39.         int totalWidth = (int)original.texture.width;
  40.         int totalHeight = (int)original.texture.height;
  41.         int spriteWidth = (int)original.rect.width;
  42.         int spriteHeight = (int)original.rect.height;
  43.  
  44.         originalWidth = spriteWidth;
  45.         originalHeight = spriteHeight;
  46.  
  47.         // Get temporary render texture to get access to sprite pixels
  48.         RenderTexture tmp = RenderTexture.GetTemporary(totalWidth, totalHeight, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear);
  49.         Graphics.Blit(original.texture, tmp);
  50.         RenderTexture old = RenderTexture.active;
  51.         RenderTexture.active = tmp;
  52.  
  53.         // Calculate diagonal to ensure corners aren't cuttoff when rotating
  54.         float diag = Mathf.Sqrt(spriteWidth * spriteWidth + spriteHeight * spriteHeight);
  55.         int iDiag = Mathf.RoundToInt(diag + 0.5f); // Round up
  56.  
  57.         // Make iDiag even to ensure centering works
  58.         if (iDiag % 2 == 1) iDiag++;
  59.  
  60.         // Represent the lower left corner of where the original sprite sits on the padded texture
  61.         xPad = (iDiag - originalWidth) / 2;
  62.         yPad = (iDiag - originalHeight) / 2;
  63.  
  64.         referenceTexture = new Texture2D(iDiag, iDiag);
  65.         referenceTexture.filterMode = FilterMode.Point;
  66.  
  67.         // Set background to transparent
  68.         Color transparent = new Color(0, 0, 0, 0);
  69.         Color[] colors = referenceTexture.GetPixels();
  70.         for(int i = 0; i < colors.Length; i++)
  71.         {
  72.             colors[i] = transparent;
  73.         }
  74.         referenceTexture.SetPixels(colors);
  75.  
  76.         // Draw the sprite to the reference texture
  77.         referenceTexture.ReadPixels(original.rect, iDiag / 2 - spriteWidth / 2, iDiag / 2 - spriteHeight / 2);
  78.         referenceTexture.Apply();
  79.  
  80.         RenderTexture.active = old;
  81.         RenderTexture.ReleaseTemporary(tmp);
  82.  
  83.         // The texture that's attached to the sprite. Copy the reference texture (unscaled) to the sprite texturer
  84.         spriteTexture = new Texture2D(referenceTexture.width, referenceTexture.height);
  85.         spriteTexture.filterMode = FilterMode.Point;
  86.         spriteTexture.SetPixels32(referenceTexture.GetPixels32());
  87.         spriteTexture.Apply();
  88.  
  89.         // The sprite is now no longer attached to a spritesheet and is instead the isolated SpriteTexture
  90.         spriteRenderer.sprite = Sprite.Create(spriteTexture, new Rect(0, 0, spriteTexture.width, spriteTexture.height), new Vector2(0.5f, 0.5f), 16);
  91.  
  92.         // Scale for a total of 8x (note: this only needs to be done once, so no performance is lost. The only
  93.         // issue is memory, but for small sprites this isn't an issue)
  94.         scaledTexBounds = new Rect(xPad, yPad, originalWidth + 1, originalHeight + 1);
  95.         Scale2x();
  96.         Scale2x();
  97.         Scale2x();
  98.     }
  99.  
  100.     // Rotates the sprite about its center by the specified number of degrees
  101.     public void SetRotate(float degrees)
  102.     {
  103.         Vector3 trash = new Vector3();
  104.         SetRotate(degrees, new Vector2(originalWidth / 2, originalHeight / 2), out trash);
  105.     }
  106.  
  107.     // Pivot represents a PIXEL position on the ORIGINAL sprite
  108.     // Out Vector local represents the translation in transform local space
  109.     // Add local to your sprite transform after the rotation is finished
  110.     public void SetRotate(float degrees, Vector2 pivot, out Vector3 local)
  111.     {
  112.         Angle = degrees;
  113.  
  114.         // Negate the degrees to get a counter-clockwise rotation
  115.         // This algorithm rotates clockwise by default, so we
  116.         // need a negative angle to rotate CCW properly.
  117.         float radians = -Mathf.Deg2Rad * degrees;
  118.  
  119.         int width = spriteTexture.width;
  120.         int height = spriteTexture.height;
  121.  
  122.         Color32[] pixels = referenceTexture.GetPixels32();
  123.         Color32[] newPixels = spriteTexture.GetPixels32();
  124.  
  125.         int xcenter = width / 2;
  126.         int ycenter = height / 2;
  127.  
  128.         float sin = Mathf.Sin(radians);
  129.         float cos = Mathf.Cos(radians);
  130.  
  131.         // Checking for the best offset is too slow, so instead we use 4 in both the x and y as the rotation offset
  132.         //Vector2 offset = bestOffset(referenceTexture, radians);
  133.         //int xOff = (int)offset.x;
  134.         //int yOff = (int)offset.y;
  135.         int xOff = 4;
  136.         int yOff = 4;
  137.  
  138.         for (int x = 0; x < width; x++)
  139.         {
  140.             for (int y = 0; y < height; y++)
  141.             {
  142.                 // Make sure all pixels have a color
  143.                 newPixels[x + y * width] = new Color(0, 0, 0, 0);
  144.  
  145.                 // Rotate around center point
  146.                 int xx = (int)((x - xcenter) * cos + (y - ycenter) * -sin) + xcenter;
  147.                 int yy = (int)((x - xcenter) * sin + (y - ycenter) * cos) + ycenter;
  148.  
  149.                 // Make sure the rotated point falls within the bounds of the texture
  150.                 // Note: Because we padded the original texture with transparent borders,
  151.                 // no pixel on the original sprite will fall outside the bounds of the texture.
  152.                 if (xx >= 0 && xx < width && yy >= 0 && yy < height)
  153.                 {
  154.                     newPixels[x + y * width] = pixels[xx * 8 + xOff + ((yy * 8 + yOff) * width * 8)];
  155.                 }
  156.             }
  157.         }
  158.  
  159.         // Apply the pixels and reset the sprite
  160.         spriteTexture.SetPixels32(newPixels);
  161.         spriteTexture.Apply();
  162.  
  163.         spriteRenderer.sprite = Sprite.Create(spriteTexture, new Rect(0, 0, spriteTexture.width, spriteTexture.height), new Vector2(0.5f, 0.5f), 16);
  164.  
  165.         // Pivot adjustment
  166.         int xPiv = (int)pivot.x;
  167.         int yPiv = (int)pivot.y;
  168.  
  169.         // Translate to padded texture
  170.         xPiv += xPad;
  171.         yPiv += yPad;
  172.  
  173.         Sprite sprite = spriteRenderer.sprite;
  174.  
  175.         // Where the pivot is in sprite coords
  176.         float xPos = (xPiv / (float)width) * sprite.bounds.extents.x * 2 - sprite.bounds.extents.x;
  177.         float yPos = (yPiv / (float)height) * sprite.bounds.extents.y * 2 - sprite.bounds.extents.y;
  178.  
  179.         // Negate back to original angle (we need true CCW rotation which is why we negate the angle again)
  180.         radians = -radians;
  181.         cos = Mathf.Cos(radians);
  182.         sin = Mathf.Sin(radians);
  183.  
  184.         // See where the rotation takes the pivot
  185.         int xRot = (int)((xPiv - xcenter) * cos + (yPiv - ycenter) * -sin) + xcenter;
  186.         int yRot = (int)((xPiv - xcenter) * sin + (yPiv - ycenter) * cos) + ycenter;
  187.  
  188.         // Piv coords now transformed into sprite coords
  189.         float xSprite = (xRot / (float)width) * sprite.bounds.extents.x * 2 - sprite.bounds.extents.x;
  190.         float ySprite = (yRot / (float)height) * sprite.bounds.extents.y * 2 - sprite.bounds.extents.y;
  191.        
  192.         // Return the difference between where the pivot should be and where it ended up
  193.         local = new Vector3(xPos - xSprite, yPos - ySprite, 0);
  194.     }
  195.  
  196.     public float GetAngle()
  197.     {
  198.         return Angle;
  199.     }
  200.  
  201.     // Scaling algorithm used in RotSprite
  202.     // See link below for documentation on how this algorithm works
  203.     // https://github.com/alteredgenome/grafx2/issues/385
  204.     private void Scale2x()
  205.     {
  206.         int width = referenceTexture.width;
  207.         int height = referenceTexture.height;
  208.  
  209.         Texture2D scaledTex = new Texture2D(width * 2, height * 2);
  210.         scaledTex.filterMode = FilterMode.Point;
  211.         Color32[] colors = referenceTexture.GetPixels32();
  212.         Color32[] scaledCols = scaledTex.GetPixels32();
  213.  
  214.         int scaledWidth = width * 2;
  215.  
  216.         for(int x = 0; x < width; x++)
  217.         {
  218.             for (int y = 0; y < height; y++)
  219.             {
  220.                 int x1 = x * 2;
  221.                 int x2 = x1 + 1;
  222.                 int y1 = y * 2;
  223.                 int y2 = y1 + 1;
  224.                
  225.                 // If we're on a pixel that's apart of the padding, then we don't have to calculate the scale
  226.                 if (x < scaledTexBounds.x - 1 || x > scaledTexBounds.x + scaledTexBounds.width ||
  227.                     y < scaledTexBounds.y - 1 || y > scaledTexBounds.y + scaledTexBounds.height)
  228.                 {
  229.                     // Set 4 corresponding pixels to blank
  230.                     scaledCols[x1 + y1 * scaledWidth] = new Color32(0, 0, 0, 0);
  231.                     scaledCols[x2 + y1 * scaledWidth] = new Color32(0, 0, 0, 0);
  232.                     scaledCols[x1 + y2 * scaledWidth] = new Color32(0, 0, 0, 0);
  233.                     scaledCols[x2 + y2 * scaledWidth] = new Color32(0, 0, 0, 0);
  234.                     continue;
  235.                 }
  236.  
  237.                 // Obtain 9 neighbor pixels
  238.                 // cA cB cC
  239.                 // cD cE cF
  240.                 // cG cH cI
  241.  
  242.                 Color32 cA = colors[x - 1 + (y + 1) * width];
  243.                 Color32 cB = colors[x + (y + 1) * width];
  244.                 Color32 cC = colors[x + 1 + (y + 1) * width];
  245.                 Color32 cD = colors[x - 1 +  y * width];
  246.                 Color32 cE = colors[x + y * width];
  247.                 Color32 cF = colors[x + 1 + y * width];
  248.                 Color32 cG = colors[x - 1 + (y - 1) * width];
  249.                 Color32 cH = colors[x + (y - 1) * width];
  250.                 Color32 cI = colors[x + 1 + (y - 1) * width];
  251.  
  252.                 // Outputs
  253.                 // oA oB
  254.                 // oC oD
  255.                 Color32 oA;
  256.                 Color32 oB;
  257.                 Color32 oC;
  258.                 Color32 oD;
  259.  
  260.                 if (different(cD, cF, cE) && different(cB, cH, cE) &&
  261.                     ((similar(cE, cD, cE) || similar(cE, cH, cE) || similar(cE, cF, cE) || similar(cE, cB, cE) ||
  262.                     ((different(cA, cI, cE) || similar(cE, cG, cE) || similar(cE, cC, cE)) &&
  263.                     (different(cG, cC, cE) || similar(cE, cI, cE) || similar(cE, cA, cE))))))
  264.                 {
  265.                     oA = ((similar(cB, cD, cE) && (different(cE, cA, cE) && (different(cE, cA, cE) || different(cE, cI, cE) || different(cB, cC, cE) || different(cD, cG, cE)))) ? cB : cE);
  266.                     oB = ((similar(cB, cF, cE) && (different(cE, cC, cE) && (different(cE, cC, cE) || different(cE, cG, cE) || different(cF, cI, cE) || different(cB, cA, cE)))) ? cF : cE);
  267.                     oC = ((similar(cD, cH, cE) && (different(cE, cG, cE) && (different(cE, cG, cE) || different(cE, cC, cE) || different(cD, cA, cE) || different(cH, cI, cE)))) ? cD : cE);
  268.                     oD = ((similar(cH, cF, cE) && (different(cE, cI, cE) && (different(cE, cI, cE) || different(cE, cA, cE) || different(cH, cG, cE) || different(cF, cC, cE)))) ? cH : cE);
  269.  
  270.                 }
  271.                 else
  272.                 {
  273.                     oA = cE;
  274.                     oB = cE;
  275.                     oC = cE;
  276.                     oD = cE;
  277.                 }
  278.  
  279.                
  280.                 scaledCols[x1 + y2 * scaledWidth] = oA; // oA
  281.                 scaledCols[x2 + y2 * scaledWidth] = oB; // oB
  282.                 scaledCols[x1 + y1 * scaledWidth] = oC; // oC
  283.                 scaledCols[x2 + y1 * scaledWidth] = oD; // oD
  284.             }
  285.         }
  286.  
  287.         scaledTex.SetPixels32(scaledCols);
  288.         scaledTex.Apply();
  289.  
  290.         referenceTexture = scaledTex;
  291.         scaledTexBounds.x *= 2;
  292.         scaledTexBounds.y *= 2;
  293.         scaledTexBounds.width *= 2;
  294.         scaledTexBounds.height *= 2;
  295.     }
  296.  
  297.     private float distance(Color32 c1, Color32 c2)
  298.     {
  299.         // IF the colors are the same, their distance is 0
  300.         if (eq(c1,c2)) return 0;
  301.         return Mathf.Abs(c1.r - c2.r) + Mathf.Abs(c1.g - c2.g) + Mathf.Abs(c1.b - c2.b);
  302.     }
  303.  
  304.     private bool similar(Color32 c1, Color32 c2, Color32 reference)
  305.     {
  306.         if (eq(c1, c2)) return true;
  307.         float d12 = distance(c1, c2);
  308.         float d1r = distance(c1, reference);
  309.         float d2r = distance(c2, reference);
  310.         return d12 <= d1r && d12 <= d2r;
  311.     }
  312.  
  313.     private bool different(Color32 c1, Color32 c2, Color32 reference)
  314.     {
  315.         return !similar(c1, c2, reference);
  316.     }
  317.  
  318.     private bool eq(Color32 c1, Color32 c2)
  319.     {
  320.         return c1.r == c2.r && c1.g == c2.g && c1.b == c2.b;
  321.     }
  322.  
  323.     // Tex is the 8x scaled texture
  324.     // TOO SLOW TOO SLOW TOO SLOW TOO SLOW
  325.     //
  326.     // This algorithm calculates the best rotation offsets to use on the scaled image
  327.     // by reducing the squared error of rotated pixels and their neighbor's colors
  328.     private Vector2 bestOffset(Texture2D tex, float radians)
  329.     {
  330.         float cos = Mathf.Cos(radians);
  331.         float sin = Mathf.Sin(radians);
  332.         int xcenter = tex.width / 2;
  333.         int ycenter = tex.height / 2;
  334.  
  335.         Color32[] colors = tex.GetPixels32();
  336.  
  337.         float xBest = 0;
  338.         float yBest = 0;
  339.         float lowestError = float.MaxValue;
  340.         for(int xOff = 0; xOff < 8; xOff++)
  341.         {
  342.             for(int yOff = 0; yOff < 8; yOff++)
  343.             {
  344.                 float error = 0;
  345.                
  346.                 // Loop through pixels and get squared error
  347.                 for(int x = xOff; x < tex.width; x+=8)
  348.                 {
  349.                     for(int y = yOff; y < tex.height; y+=8)
  350.                     {
  351.                         int xx = (int)((x - xcenter) * cos + (y - ycenter) * -sin) + xcenter;
  352.                         int yy = (int)((x - xcenter) * sin + (y - ycenter) * cos) + ycenter;
  353.  
  354.                         if (xx < 0 || xx >= tex.width || yy < 0 || yy >= tex.height) continue;
  355.  
  356.                         error += neighborSquaredError(xx, yy, tex);
  357.                     }
  358.                 }
  359.  
  360.                 // If the accumulated error is less than the best lowest error, then we found better rotation offsets
  361.                 if(error < lowestError)
  362.                 {
  363.                     lowestError = error;
  364.                     xBest = xOff;
  365.                     yBest = yOff;
  366.                 }
  367.             }
  368.         }
  369.  
  370.         return new Vector2(xBest, yBest);
  371.     }
  372.    
  373.     private float neighborSquaredError(int x, int y, Texture2D tex)
  374.     {
  375.         Color32[] colors = tex.GetPixels32();
  376.         Color32 col = colors[x + y * tex.width];
  377.  
  378.         float error = 0;
  379.  
  380.         // Left Pixel
  381.         int xx = x - 1;
  382.         int yy = y;
  383.  
  384.         if(xx >= 0)
  385.         {
  386.             error += sqrError(col, colors[xx + yy * tex.width]);
  387.         }
  388.  
  389.         // Right Pixel
  390.         xx = x + 1;
  391.  
  392.         if(xx < tex.width)
  393.         {
  394.             error += sqrError(col, colors[xx + yy * tex.width]);
  395.         }
  396.  
  397.         // Top Pixel
  398.         xx = x;
  399.         yy = y + 1;
  400.  
  401.         if (yy < tex.height)
  402.         {
  403.             error += sqrError(col, colors[xx + yy * tex.width]);
  404.         }
  405.  
  406.         // Bottom Pixel
  407.         yy = y - 1;
  408.  
  409.         if (yy >= 0)
  410.         {
  411.             error += sqrError(col, colors[xx + yy * tex.width]);
  412.         }
  413.         return error;
  414.     }
  415.  
  416.     private float sqrError(Color32 c1, Color32 c2)
  417.     {
  418.         float r = c1.r - c2.r;
  419.         float g = c1.g - c2.g;
  420.         float b = c1.b - c2.b;
  421.         float a = c1.a - c2.a;
  422.         return r * r + g * g + b * b + a * a;
  423.     }
  424. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement