i've been working on a thing the past several days. this is an idea ive had for many years, and i'm extremely pleased with the result (though i have some more ideas and may continue working on it)
I made a pixel art upscaling algorithm, specifically for better pixel sprite rotation (but it looks pretty good just as an upscaler as well)
it upscales without adding any new colors, and when scaled down to the original resolution it displays identical to the source image. the idea is that as an artist i can draw a sprite at normal scale, ensure it will look exactly as i drew it when displayed normally, and will have a better result when rotated (not as jumbled, etc.) and also not introduce any new colors. (the ideal case for something like this is if you made art as a vector image, it'd have all the information needed to display at any angle without holes. this system is an approximation of that)
the current version takes the form of a shader, so i can just plop this onto any sprite in a game that will be rotated and it'll look a lot nicer. alternatively i could pre-process sprites with it and scale them down in-game
here are some more examples. it looks particularly good with solid lines/edges, which was the main focus
i think it looks pretty nice :)
here's the current version in the form of a godot glsl shader. (it should be fairly easy to convert to whatever system you're using, but if you want help feel free to ask!)
shader code
shader_type canvas_item;
//the color with the highest priority.
// other colors will be tested based on distance to this
// color to determine which colors take priority for overlaps.
uniform vec3 highestColor = vec3(1.,1.,1.);
//how close two colors should be to be considered "similar".
// can group shapes of visually similar colors, but creates
// some artifacting and should be kept as low as possible.
uniform float similarThreshold = 0.0;
const float scale = 4.0;
const mat3 yuv_matrix = mat3(vec3(0.299, 0.587, 0.114), vec3(-0.169, -0.331, 0.5), vec3(0.5, -0.419, -0.081));
vec3 yuv(vec3 col){
mat3 yuv = transpose(yuv_matrix);
return yuv * col;
}
bool similar(vec4 col1, vec4 col2){
return (col1.a == 0. && col2.a == 0.) || distance(col1, col2) <= similarThreshold;
}
//multiple versions because godot doesn't support function overloading
//note: inner check should ideally check between all permutations
// but this is good enough, and faster
bool similar3(vec4 col1, vec4 col2, vec4 col3){
return similar(col1, col2) && similar(col2, col3);
}
bool similar4(vec4 col1, vec4 col2, vec4 col3, vec4 col4){
return similar(col1, col2) && similar(col2, col3) && similar(col3, col4);
}
bool similar5(vec4 col1, vec4 col2, vec4 col3, vec4 col4, vec4 col5){
return similar(col1, col2) && similar(col2, col3) && similar(col3, col4) && similar(col4, col5);
}
bool higher(vec4 thisCol, vec4 otherCol){
if(similar(thisCol, otherCol)) return false;
if(thisCol.a == otherCol.a){
// return yuv(thisCol.rgb).x > yuv(otherCol.rgb).x;
// return distance(yuv(thisCol.rgb), yuv(highestColor)) < distance(yuv(otherCol.rgb), yuv(highestColor));
return distance(thisCol.rgb, highestColor) < distance(otherCol.rgb, highestColor);
} else {
return thisCol.a > otherCol.a;
}
}
void fragment() {
vec2 size = 1.0/TEXTURE_PIXEL_SIZE+0.00001; //???
vec2 px = UV*size;
vec2 local = floor(mod(px,1.0)*scale);
vec2 localDiff = (local/1.0) - vec2(1.5, 1.5);
vec2 edge = floor(local/(scale/2.0))*(scale/2.0) - vec2(1.0, 1.0);
px = ceil(px);
vec4 baseCol = texture(TEXTURE, px/size);
vec4 col = baseCol;
vec4 c = baseCol;
// vec4 c = texture(TEXTURE, floor(px+(vec2(0.5,0.5)+edge/4.0))/size);
vec2 ot = vec2(edge.x, edge.y * 4.0);
vec4 t = texture(TEXTURE, floor(px+(vec2(0.5,0.5)+ot/4.0))/size);
vec2 otl = vec2(edge.x * 4.0, edge.y * 4.0);
vec4 tl = texture(TEXTURE, floor(px+(vec2(0.5,0.5)+otl/4.0))/size);
vec2 otr = vec2(-edge.x * 4.0, edge.y * 4.0);
vec4 tr = texture(TEXTURE, floor(px+(vec2(0.5,0.5)+otr/4.0))/size);
vec2 ol = vec2(edge.x * 4.0, edge.y);
vec4 l = texture(TEXTURE, floor(px+(vec2(0.5,0.5)+ol/4.0))/size);
vec2 ob = vec2(edge.x, -edge.y * 4.0);
vec4 b = texture(TEXTURE, floor(px+(vec2(0.5,0.5)+ob/4.0))/size);
vec2 obl = vec2(edge.x * 4.0, -edge.y * 4.0);
vec4 bl = texture(TEXTURE, floor(px+(vec2(0.5,0.5)+obl/4.0))/size);
vec2 obr = vec2(-edge.x * 4.0, -edge.y * 4.0);
vec4 br = texture(TEXTURE, floor(px+(vec2(0.5,0.5)+obr/4.0))/size);
vec2 or = vec2(-edge.x * 4.0, edge.y);
vec4 r = texture(TEXTURE, floor(px+(vec2(0.5,0.5)+or/4.0))/size);
vec2 os = vec2(localDiff.x, localDiff.y);
vec4 s = texture(TEXTURE, floor(px+(vec2(0.5,0.5)+os/2.0))/size);
//checkerboard special case
if(similar5(c, tl, tr, bl, br) && similar4(t, r, b, l) && higher(t, c)){
// if(local == vec2(0.,0.)){
// col = l;
// } else if(local == vec2(3.,0.)){
// col = r;
// } else if(local == vec2(0.,3.)){
// col = l;
// } else if(local == vec2(3.,3.)){
// col = r;
// }
} else {
//corner
if(length(localDiff) > 2.1){
if(similar(t, l) && !(similar(tl, c) && !higher(t, c))){
col = t;
} else if(higher(c, l) && higher(c, t) && (similar(tr, c) || similar(bl, c)) && !similar(tl, c)){
if(higher(t, l)){
col = t;
} else {
col = l;
}
}
//edge
} else if(length(localDiff) > 1.58) {
if(similar(t, l)){
if(higher(s, c)){
col = s;
}
}
if(similar3(r, s, tl) && similar3(br, c, l) && higher(s, c)/* && !similar(b, t)*/){
col = t;
}
if(!similar(tl, c) && similar3(r, c, bl) && similar3(tr, t, l) && higher(c, l)){
col = s;
}
if(!similar(tr, c) && similar3(l, c, br) && similar3(tl, s, r) && higher(c, r)){
col = s;
}
if(similar3(b, s, tl) && similar3(br, c, t) && higher(b, c)/* && !similar(r, l)*/){
col = s;
}
if(!similar(tl, c) && similar3(tr, c, b) && similar3(t, l, bl) && higher(c, l)){
col = s;
}
if(!similar(bl, c) && similar3(br, c, t) && similar3(b, s, tl) && higher(c, s)){
col = s;
}
}
}
//set alpha back to full, for manually adjusted pixels
if(col.a > 0.00001){
col.a = 1.0;
}
COLOR = col;
}



