diff --git a/scalenx/epx.slangp b/scalenx/epx.slangp new file mode 100644 index 0000000..e746a24 --- /dev/null +++ b/scalenx/epx.slangp @@ -0,0 +1,6 @@ +shaders = 1 + +shader0 = shaders/epx.slang +filter_linear0 = false +scale_type0 = source +scale0 = 2.0 diff --git a/scalenx/mmpx.slangp b/scalenx/mmpx.slangp new file mode 100644 index 0000000..ef44068 --- /dev/null +++ b/scalenx/mmpx.slangp @@ -0,0 +1,6 @@ +shaders = 1 + +shader0 = shaders/mmpx.slang +filter_linear0 = false +scale_type0 = source +scale0 = 2.0 diff --git a/scalenx/shaders/epx.slang b/scalenx/shaders/epx.slang new file mode 100644 index 0000000..2affb61 --- /dev/null +++ b/scalenx/shaders/epx.slang @@ -0,0 +1,88 @@ +#version 450 + +// EPX (Eric's Pixel Scaler) +// based on the description from Wikipedia: +// https://en.wikipedia.org/wiki/Pixel-art_scaling_algorithms#EPX/Scale2%C3%97/AdvMAME2%C3%97 +// adapted for slang by hunterk +// license GPL, I think + +layout(push_constant) uniform Push +{ + vec4 SourceSize; + vec4 OriginalSize; + vec4 OutputSize; + uint FrameCount; +} params; + +layout(std140, set = 0, binding = 0) uniform UBO +{ + mat4 MVP; +} global; + +#pragma stage vertex +layout(location = 0) in vec4 Position; +layout(location = 1) in vec2 TexCoord; +layout(location = 0) out vec2 vTexCoord; + +void main() +{ + gl_Position = global.MVP * Position; + vTexCoord = TexCoord * 1.00001; +} + +#pragma stage fragment +layout(location = 0) in vec2 vTexCoord; +layout(location = 0) out vec4 FragColor; +layout(set = 0, binding = 2) uniform sampler2D Source; + +bool same(vec3 B, vec3 A0){ + return all(equal(B, A0)); +} + +bool notsame(vec3 B, vec3 A0){ + return any(notEqual(B, A0)); +} + +// sample with coordinate offsets +#define TEX(c,d) texture(Source, vTexCoord.xy + vec2(c,d) * params.SourceSize.zw).rgb + +void main() +{ +// The algorithm looks at the current pixel and the 4 surrounding cardinal pixels +// ___|_A_|___ +// _C_|_P_|_B_ +// | D | + +// Our current pixel, P + vec3 P = TEX( 0., 0.); + +// Input pixels + vec3 A = TEX( 0., 1.); + vec3 B = TEX( 1., 0.); + vec3 D = TEX( 0.,-1.); + vec3 C = TEX(-1., 0.); + +// Output: 2x2 grid. Default to the current pixel color (Nearest magnification) +// ___one_|_two___ +// three | four + vec3 one = P; + vec3 two = P; + vec3 three = P; + vec3 four = P; + +// EPX algorithm rules: +// IF C==A AND C!=D AND A!=B => 1=A +// IF A==B AND A!=C AND B!=D => 2=B +// IF D==C AND D!=B AND C!=A => 3=C +// IF B==D AND B!=A AND D!=C => 4=D + + one = (same(C, D) && notsame(C, A) && notsame(C, B)) ? C : P; + two = (same(D, B) && notsame(D, C) && notsame(D, A)) ? D : P; + three = (same(A, C) && notsame(A, B) && notsame(A, D)) ? A : P; + four = (same(B, A) && notsame(B, D) && notsame(B, C)) ? B : P; + + vec2 px = fract(vTexCoord * params.SourceSize.xy); +// split the texels into 4 and assign one of our output pixels to each + FragColor.rgb = (px.x < 0.5) ? (px.y < 0.5 ? one : three) : (px.y < 0.5 ? two : four); + FragColor.a = 1.0; +} diff --git a/scalenx/shaders/mmpx.slang b/scalenx/shaders/mmpx.slang new file mode 100644 index 0000000..9d625a6 --- /dev/null +++ b/scalenx/shaders/mmpx.slang @@ -0,0 +1,303 @@ +#version 450 + +// MMPX +// by Morgan McGuire and Mara Gagiu +// https://casual-effects.com/research/McGuire2021PixelArt/ +// License: MIT +// adapted for slang by hunterk + +layout(push_constant) uniform Push +{ + vec4 SourceSize; + vec4 OriginalSize; + vec4 OutputSize; + uint FrameCount; +} params; + +layout(std140, set = 0, binding = 0) uniform UBO +{ + mat4 MVP; +} global; + +#pragma stage vertex +layout(location = 0) in vec4 Position; +layout(location = 1) in vec2 TexCoord; +layout(location = 0) out vec2 vTexCoord; + +void main() +{ + gl_Position = global.MVP * Position; + vTexCoord = TexCoord; +} + +#pragma stage fragment +layout(location = 0) in vec2 vTexCoord; +layout(location = 0) out vec4 FragColor; +layout(set = 0, binding = 2) uniform sampler2D Source; + +float luma(vec3 col){ + return dot(col, vec3(0.2126, 0.7152, 0.0722)); +} + +// I tried using a hash to speed these up but it didn't really help +bool same(vec3 B, vec3 A0){ + return all(equal(B, A0)); +} + +bool notsame(vec3 B, vec3 A0){ + return any(notEqual(B, A0)); +} + +bool all_eq2(vec3 B, vec3 A0, vec3 A1) { + return (same(B,A0) && same(B,A1)); +} + +bool all_eq3(vec3 B, vec3 A0, vec3 A1, vec3 A2) { + return (same(B,A0) && same(B,A1) && same(B,A2)); +} + +bool all_eq4(vec3 B, vec3 A0, vec3 A1, vec3 A2, vec3 A3) { + return (same(B,A0) && same(B,A1) && same(B,A2) && same(B,A3)); +} + +bool any_eq3(vec3 B, vec3 A0, vec3 A1, vec3 A2) { + return (same(B,A0) || same(B,A1) || same(B,A2)); +} + +bool none_eq2(vec3 B, vec3 A0, vec3 A1) { + return (notsame(B,A0) && notsame(B,A1)); +} + +bool none_eq4(vec3 B, vec3 A0, vec3 A1, vec3 A2, vec3 A3) { + return (notsame(B,A0) && notsame(B,A1) && notsame(B,A2) && notsame(B,A3)); +} + +#define src(c,d) texture(Source, vTexCoord + vec2(c,d) * params.SourceSize.zw).rgb + +void main() +{ +// these do nothing, but just for consistency with the original code... + float srcX = 0.; + float srcY = 0.; + +// Our current pixel + vec3 E = src(srcX+0.,srcY+0.); + +// Input: A-I central 3x3 grid + vec3 A = src(srcX-1.,srcY-1.); + vec3 B = src(srcX+0.,srcY-1.); + vec3 C = src(srcX+1.,srcY-1.); + + vec3 D = src(srcX-1.,srcY+0.); + vec3 F = src(srcX+1.,srcY+0.); + + vec3 G = src(srcX-1.,srcY+1.); + vec3 H = src(srcX+0.,srcY+1.); + vec3 I = src(srcX+1.,srcY+1.); + +// Default to Nearest magnification + vec3 J = E; + vec3 K = E; + vec3 L = E; + vec3 M = E; +// Go ahead and put this here so we can use an early return on the next +// line to save some cycles + FragColor = vec4(E, 1.0); + +// Skip constant 3x3 centers and just use nearest-neighbor +// them. This gives a good speedup on spritesheets with +// lots of padding and full screen images with large +// constant regions such as skies. +// EDIT: this is a wash for me, but we'll keep it around + if(same(E,A) && same(E,B) && same(E,C) && same(E,D) && same(E,F) && same(E,G) && same(E,H) && same(E,I)) return; + +// Read additional values at the tips of the diamond pattern + vec3 P = src(srcX+0.,srcY-2.); + vec3 Q = src(srcX-2.,srcY+0.); + vec3 R = src(srcX+2.,srcY+0.); + vec3 S = src(srcX+0.,srcY+2.); + +// Precompute luminances + float Bl = luma(B); + float Dl = luma(D); + float El = luma(E); + float Fl = luma(F); + float Hl = luma(H); + +// Round some corners and fill in 1:1 slopes, but preserve +// sharp right angles. +// +// In each expression, the left clause is from +// EPX and the others are new. EPX +// recognizes 1:1 single-pixel lines because it +// applies the rounding only to the LINE, and not +// to the background (it looks at the mirrored +// side). It thus fails on thick 1:1 edges +// because it rounds *both* sides and produces an +// aliased edge shifted by 1 dst pixel. (This +// also yields the mushroom-shaped arrow heads, +// where that 1-pixel offset runs up against the +// 2-pixel aligned end; this is an inherent +// problem with 2X in-palette scaling.) +// +// The 2nd clause clauses avoid *double* diagonal +// filling on 1:1 slopes to prevent them becoming +// aliased again. It does this by breaking +// symmetry ties using luminance when working with +// thick features (it allows thin and transparent +// features to pass always). +// +// The 3rd clause seeks to preserve square corners +// by considering the center value before +// rounding. +// +// The 4th clause identifies 1-pixel bumps on +// straight lines that are darker than their +// background, such as the tail on a pixel art +// "4", and prevents them from being rounded. This +// corrects for asymmetry in this case that the +// luminance tie breaker introduced. + +// .------------ 1st ------------. .----- 2nd ---------. .------ 3rd -----. .--------------- 4th -----------------------. + if (((same(D,B) && notsame(D,H) && notsame(D,F))) && ((El>=Dl) || same(E,A)) && any_eq3(E,A,C,G) && ((El