Last active
December 19, 2021 21:41
-
-
Save garzaa/32ec27c98218d57e41c5ee753d7c3275 to your computer and use it in GitHub Desktop.
Runtime Pixelart Smoothing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
using System.Collections.Generic; | |
using System.Collections; | |
public class AsyncTexture { | |
public int width { get; private set; } | |
public int height { get; private set; } | |
Color[] pixels; | |
public AsyncTexture(Color[] pixels, Vector2Int size) { | |
this.pixels = pixels; | |
this.width = size.x; | |
this.height = size.y; | |
} | |
public AsyncTexture(Vector2Int size) { | |
this.pixels = new Color[size.x * size.y]; | |
this.width = size.x; | |
this.height = size.y; | |
} | |
public Color GetPixel(int x, int y) { | |
// if it's within bounds, then return x and y | |
// otherwise, return a clamped value | |
if (x>=width) x = width-1; | |
else if (x<0) x = 0; | |
if (y>=height) y = height-1; | |
else if (y<0) y = 0; | |
// origin is at the bottom left | |
return pixels[y*width + x]; | |
} | |
public void SetPixel(int x, int y, Color color) { | |
if (x>=width) x = width-1; | |
else if (x<0) x = 0; | |
if (y>=height) y = height-1; | |
else if (y<0) y = 0; | |
pixels[y*width + x] = color; | |
} | |
public Color[] GetPixels() { | |
return pixels; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
public class NoSmoothSprite : MonoBehaviour { | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
using System.Collections; | |
using System.Collections.Generic; | |
public class SmoothSpriteChild : MonoBehaviour { | |
SpriteRenderer spriteRenderer; | |
SpriteSmoother spriteSmoother; | |
string spriteLastFrame; | |
public void Initialize(SpriteSmoother s) { | |
this.spriteSmoother = s; | |
spriteRenderer = GetComponent<SpriteRenderer>(); | |
spriteRenderer.sprite = spriteSmoother.GetUpscaledSprite(spriteRenderer.sprite, this); | |
spriteLastFrame = spriteRenderer.sprite.name; | |
} | |
void LateUpdate() { | |
// if the sprites differ, get the upscaled version from spritesmoother | |
// sprites may differ every frame due to some animator keying, or they may not | |
// so for performance reasons just check every frame, and optimize the check if needed | |
if (spriteRenderer.sprite.name != spriteLastFrame) { | |
spriteRenderer.sprite = spriteSmoother.GetUpscaledSprite(spriteRenderer.sprite, this); | |
} | |
spriteLastFrame = spriteRenderer.sprite.name; | |
} | |
public void ForceUpscaledSprite() { | |
spriteRenderer.sprite = spriteSmoother.GetUpscaledSprite(spriteRenderer.sprite, this); | |
spriteLastFrame = spriteRenderer.sprite.name; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using UnityEngine; | |
using System.Collections; | |
using System; | |
using System.Threading.Tasks; | |
using System.Collections.Generic; | |
// TEXTURES THIS USES HAVE TO BE READABLE IN IMPORT SETTINGS | |
// https://www.scale2x.it/algorithm | |
public class SpriteSmoother : MonoBehaviour { | |
const float scaleFactor = 4f; | |
Dictionary<string, Texture2D> upscaledTextures = new Dictionary<string, Texture2D>(); | |
Dictionary<string, Sprite> upscaledSprites = new Dictionary<string, Sprite>(); | |
HashSet<string> queuedTextures = new HashSet<string>(); | |
List<SmoothSpriteChild> children = new List<SmoothSpriteChild>(); | |
void Start() { | |
foreach (SpriteRenderer s in GetComponentsInChildren<SpriteRenderer>()) { | |
if (s.GetComponent<NoSmoothSprite>()) { | |
continue; | |
} | |
SmoothSpriteChild smoothSprite = s.gameObject.AddComponent<SmoothSpriteChild>(); | |
smoothSprite.Initialize(this); | |
children.Add(smoothSprite); | |
} | |
} | |
Task<AsyncTexture> UpscaleTexture(AsyncTexture input) { | |
return Scale4x(input); | |
} | |
async Task<AsyncTexture> Scale4x(AsyncTexture input) { | |
AsyncTexture t = await Task.Run(() => Scale2x(Scale2x(input))); | |
return t; | |
} | |
AsyncTexture Scale2x(AsyncTexture input) { | |
AsyncTexture output = new AsyncTexture(new Vector2Int(input.width*2, input.height*2)); | |
for (int x=0; x<input.width; x++) { | |
for (int y=0; y<input.height; y++) { | |
/* | |
A B C | |
D E F | |
G H I | |
E0 E1 | |
E2 E3 | |
*/ | |
Color b = input.GetPixel(x, y-1); | |
Color h = input.GetPixel(x, y+1); | |
Color d = input.GetPixel(x-1, y); | |
Color f = input.GetPixel(x+1, y); | |
Color e = input.GetPixel(x, y); | |
if (!SameColor(b, h) && !SameColor(d, f)) { | |
output.SetPixel(2*x, 2*y, SameColor(d, b) ? d : e); | |
output.SetPixel(2*x+1, 2*y, SameColor(b, f) ? f : e); | |
output.SetPixel(2*x, 2*y+1, SameColor(d, h) ? d : e); | |
output.SetPixel(2*x+1, 2*y+1, SameColor(h, f) ? f : e); | |
} else { | |
output.SetPixel(2*x, 2*y, e); | |
output.SetPixel(2*x+1, 2*y, e); | |
output.SetPixel(2*x, 2*y+1, e); | |
output.SetPixel(2*x+1, 2*y+1, e); | |
} | |
} | |
} | |
return output; | |
} | |
bool SameColor(Color a, Color b) { | |
return | |
Mathf.Approximately(a.r, b.r) | |
&& Mathf.Approximately(a.g, b.g) | |
&& Mathf.Approximately(a.b, b.b) | |
&& Mathf.Approximately(a.a, b.a); | |
} | |
public Sprite GetUpscaledSprite(Sprite s, SmoothSpriteChild caller) { | |
if (upscaledSprites.ContainsKey(s.name)) { | |
return upscaledSprites[s.name]; | |
} | |
string textureName = s.texture.name; | |
if (!upscaledTextures.ContainsKey(textureName)) { | |
if (!queuedTextures.Contains(textureName)) { | |
queuedTextures.Add(textureName); | |
Debug.Log("upscaling texture "+textureName+" from "+caller.gameObject.name); | |
AddUpscaledTexture( | |
new AsyncTexture(s.texture.GetPixels(), | |
new Vector2Int(s.texture.width, s.texture.height)), | |
textureName | |
); | |
} | |
return s; | |
} else { | |
Sprite upscaled = ExtractSprite(s, upscaledTextures[textureName]); | |
upscaledSprites.Add(s.name, upscaled); | |
return upscaled; | |
} | |
} | |
async void AddUpscaledTexture(AsyncTexture texture, string name) { | |
AsyncTexture upscaledAsync = await UpscaleTexture(texture); | |
Texture2D upscaled = new Texture2D(upscaledAsync.width, upscaledAsync.height); | |
upscaled.filterMode = FilterMode.Point; | |
upscaled.mipMapBias = -10; | |
upscaled.SetPixels(upscaledAsync.GetPixels()); | |
upscaled.Apply(true, false); | |
upscaled.name = name; | |
upscaledTextures[name] = upscaled; | |
queuedTextures.Remove(name); | |
// abort the loop if the editor exits play mode | |
if (!Application.isPlaying) return; | |
foreach (SmoothSpriteChild c in children) { | |
c.ForceUpscaledSprite(); | |
} | |
} | |
Sprite ExtractSprite(Sprite original, Texture2D upscaledAtlas) { | |
Rect spriteRect = original.rect; | |
spriteRect.position *= scaleFactor; | |
spriteRect.size *= scaleFactor; | |
Sprite upscaled = Sprite.Create( | |
upscaledAtlas, | |
spriteRect, | |
original.pivot*scaleFactor / spriteRect.size, | |
original.pixelsPerUnit * scaleFactor, | |
1, | |
SpriteMeshType.FullRect, | |
original.border, | |
false | |
); | |
upscaled.name = original.name; | |
return upscaled; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment