From 94ea44da0c6b00c771534b58b246628bb3e7c998 Mon Sep 17 00:00:00 2001 From: Lior Halphon Date: Thu, 9 Jun 2016 00:06:55 +0300 Subject: [PATCH] Introducing the OmniScale (beta) algorithm to SameBoy --- Cocoa/GBPreferencesWindow.m | 2 + Cocoa/GBShader.m | 1 - Cocoa/GBView.h | 2 +- Cocoa/GBView.m | 7 +++ Cocoa/Preferences.xib | 10 ++- README.md | 1 + SCALING.md | 39 ++++++++++++ Shaders/AAOmniScale.fsh | 119 ++++++++++++++++++++++++++++++++++++ Shaders/Bilinear.fsh | 2 +- Shaders/MasterShader.fsh | 1 + Shaders/OmniScale.fsh | 107 ++++++++++++++++++++++++++++++++ Shaders/SmoothBilinear.fsh | 2 +- 12 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 SCALING.md create mode 100644 Shaders/AAOmniScale.fsh create mode 100644 Shaders/OmniScale.fsh diff --git a/Cocoa/GBPreferencesWindow.m b/Cocoa/GBPreferencesWindow.m index 362abd1..d7dd576 100644 --- a/Cocoa/GBPreferencesWindow.m +++ b/Cocoa/GBPreferencesWindow.m @@ -23,6 +23,8 @@ @"Scale4x", @"AAScale2x", @"AAScale4x", + @"OmniScale", + @"AAOmniScale", ]; } return filters; diff --git a/Cocoa/GBShader.m b/Cocoa/GBShader.m index a3aa64e..d0922e7 100644 --- a/Cocoa/GBShader.m +++ b/Cocoa/GBShader.m @@ -41,7 +41,6 @@ void main(void) {\n\ if (self) { // Program NSString *fragment_shader = [[self class] shaderSourceForName:@"MasterShader"]; - fragment_shader = [fragment_shader stringByReplacingOccurrencesOfString:@"\n" withString:@""]; fragment_shader = [fragment_shader stringByReplacingOccurrencesOfString:@"{filter}" withString:[[self class] shaderSourceForName:shaderName]]; program = [[self class] programWithVertexShader:vertex_shader fragmentShader:fragment_shader]; diff --git a/Cocoa/GBView.h b/Cocoa/GBView.h index bd042f2..a2baada 100644 --- a/Cocoa/GBView.h +++ b/Cocoa/GBView.h @@ -6,6 +6,6 @@ - (void) flip; - (uint32_t *) pixels; @property GB_gameboy_t *gb; -@property BOOL shouldBlendFrameWithPrevious; +@property (nonatomic) BOOL shouldBlendFrameWithPrevious; @property GBShader *shader; @end diff --git a/Cocoa/GBView.m b/Cocoa/GBView.m index 0f67aa3..b0657c1 100644 --- a/Cocoa/GBView.m +++ b/Cocoa/GBView.m @@ -22,9 +22,16 @@ static GBShader *shader = nil; - (void) filterChanged { + [self setNeedsDisplay:YES]; self.shader = nil; } +- (void) setShouldBlendFrameWithPrevious:(BOOL)shouldBlendFrameWithPrevious +{ + _shouldBlendFrameWithPrevious = shouldBlendFrameWithPrevious; + [self setNeedsDisplay:YES]; +} + - (unsigned char) numberOfBuffers { return _shouldBlendFrameWithPrevious? 3 : 2; diff --git a/Cocoa/Preferences.xib b/Cocoa/Preferences.xib index 4b8f7cd..99066ad 100644 --- a/Cocoa/Preferences.xib +++ b/Cocoa/Preferences.xib @@ -38,9 +38,9 @@ - + - + @@ -54,6 +54,12 @@ + + + + + + diff --git a/README.md b/README.md index d946297..3428388 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Features currently supported only with the Cocoa version: * Battery save support * Save states * Optional frame blending + * Several [scaling algorithms](SCALING.md) (Including exclusive algorithms like OmniScale and Anti-aliased Scale2x) ## Compatibility While SameBoy passes many of [blargg's test ROMs](http://gbdev.gg8.se/wiki/articles/Test_ROMs#Blargg.27s_tests), some games fail to run correctly. SameBoy is still relatively early in its development and accuracy and compatibility will be improved. diff --git a/SCALING.md b/SCALING.md new file mode 100644 index 0000000..e3839c2 --- /dev/null +++ b/SCALING.md @@ -0,0 +1,39 @@ +# Scaling + +Starting with version 0.4, the Cocoa version of SameBoy supports several GPU-accelerated scaling algorithms, some of which made their premiere at SameBoy. This document describes the algorithms supported by SameBoy. + +## General-purpose Scaling Algorithms +Common algorithms that were not made specifically for pixel art + +### Nearest Neighbor +A simple pixelated scaling algorithm we all know and love. This is the default filter. + +### Bilinear +An algorithm that fills "missing" pixels using a bilinear interpolation, causing a blurry image + +### Smooth Bilinear +A variant of bilinear filtering that applies a smooth curve to the bilinear interpolation. The results look similar to the algorithm Apple uses when scaling non-Retina graphics for Retina Displays. + +## The ScaleNx Family +The ScaleNx family is a group of algorithm that scales pixel art by the specified factor using simple pattern-based rules. The Scale3x algorithm is not yet supported in SameBoy. + +### Scale2x +The most simple algorithm of the family. It scales the image by a 2x factor without introducing new colors. + +### Scale4x +This algorithm applies the Scale2x algorithm twice to scale the image by a 4x factor. + +### Anti-aliased Scale2x +A variant of Scale2x exclusive to SameBoy that blends the Scale2x output with the Nearest Neighbor output. The specific traits of Scale2x makes this blend produce nicely looking anti-aliased output. + +### Anti-aliased Scale4x +Another exclusive algorithm that works by applying the Anti-aliased Scale2x algorithm twice + +## The OmniScale Family (beta) +OmniScale is an exclusive algorithm developed for SameBoy. It combines pattern-based rules with a unique locally paletted bilinear filtering technique to scale an image by any factor, including non-integer factors. The algorithm is currently in beta, and its pattern-based rule do not currently detect 30- and 60-degree diagonals, making them look jaggy. + +### OmniScale +The base version of the algorithm, which generates aliased output with very few new colors introduced. + +### Anti-aliased OmniScale +A variant of OmniScale that produces anti-aliased output using 2x super-sampling. \ No newline at end of file diff --git a/Shaders/AAOmniScale.fsh b/Shaders/AAOmniScale.fsh new file mode 100644 index 0000000..53caa1f --- /dev/null +++ b/Shaders/AAOmniScale.fsh @@ -0,0 +1,119 @@ + +float quickDistance(vec4 a, vec4 b) +{ + return abs(a.x - b.x) + abs(a.y - b.y) + abs(a.z - b.z); +} + +vec4 omniScale(sampler2D image, vec2 texCoord) +{ + vec2 pixel = texCoord * textureDimensions - vec2(0.5, 0.5); + + vec4 q11 = texture2D(image, vec2(floor(pixel.x) / textureDimensions.x, floor(pixel.y) / textureDimensions.y)); + vec4 q12 = texture2D(image, vec2(floor(pixel.x) / textureDimensions.x, ceil(pixel.y) / textureDimensions.y)); + vec4 q21 = texture2D(image, vec2(ceil(pixel.x) / textureDimensions.x, floor(pixel.y) / textureDimensions.y)); + vec4 q22 = texture2D(image, vec2(ceil(pixel.x) / textureDimensions.x, ceil(pixel.y) / textureDimensions.y)); + + vec2 pos = fract(pixel); + + /* Special handling for diaonals */ + bool hasDownDiagonal = false; + bool hasUpDiagonal = false; + if (q12 == q21 && q11 != q22) hasUpDiagonal = true; + else if (q12 != q21 && q11 == q22) hasDownDiagonal = true; + else if (q12 == q21 && q11 == q22) { + if (q11 == q12) return q11; + int diagonalBias = 0; + for (float y = -1.0; y < 3.0; y++) { + for (float x = -1.0; x < 3.0; x++) { + vec4 color = texture2D(image, (pixel + vec2(x, y)) / textureDimensions); + if (color == q11) diagonalBias++; + if (color == q12) diagonalBias--; + } + } + if (diagonalBias <= 0) { + hasDownDiagonal = true; + } + if (diagonalBias >= 0) { + hasUpDiagonal = true; + } + } + + if (hasUpDiagonal || hasDownDiagonal) { + vec4 downDiagonalResult, upDiagonalResult; + + if (hasUpDiagonal) { + float diagonalPos = pos.x + pos.y; + + if (diagonalPos < 0.5) { + upDiagonalResult = q11; + } + else if (diagonalPos > 1.5) { + upDiagonalResult = q22; + } + else { + upDiagonalResult = q12; + } + } + + if (hasDownDiagonal) { + float diagonalPos = 1.0 - pos.x + pos.y; + + if (diagonalPos < 0.5) { + downDiagonalResult = q21; + } + else if (diagonalPos > 1.5) { + downDiagonalResult = q12; + } + else { + downDiagonalResult = q11; + } + } + + if (!hasUpDiagonal) return downDiagonalResult; + if (!hasDownDiagonal) return upDiagonalResult; + return mix(downDiagonalResult, upDiagonalResult, 0.5); + } + + vec4 r1 = mix(q11, q21, fract(pos.x)); + vec4 r2 = mix(q12, q22, fract(pos.x)); + + vec4 unqunatized = mix(r1, r2, fract(pos.y)); + + float q11d = quickDistance(unqunatized, q11); + float q21d = quickDistance(unqunatized, q21); + float q12d = quickDistance(unqunatized, q12); + float q22d = quickDistance(unqunatized, q22); + + float best = min(q11d, + min(q21d, + min(q12d, + q22d))); + + if (q11d == best) { + return q11; + } + + if (q21d == best) { + return q21; + } + + if (q12d == best) { + return q12; + } + + return q22; +} + +vec4 filter(sampler2D image) +{ + vec2 texCoord = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y) / uResolution; + vec2 pixel = vec2(1.0, 1.0) / uResolution; + // 4-pixel super sampling + + vec4 q11 = omniScale(image, texCoord + pixel * vec2(-0.25, -0.25)); + vec4 q21 = omniScale(image, texCoord + pixel * vec2(+0.25, -0.25)); + vec4 q12 = omniScale(image, texCoord + pixel * vec2(-0.25, +0.25)); + vec4 q22 = omniScale(image, texCoord + pixel * vec2(+0.25, +0.25)); + + return (q11 + q21 + q12 + q22) / 4.0; +} \ No newline at end of file diff --git a/Shaders/Bilinear.fsh b/Shaders/Bilinear.fsh index 6a604e9..dfe1b1e 100644 --- a/Shaders/Bilinear.fsh +++ b/Shaders/Bilinear.fsh @@ -2,7 +2,7 @@ vec4 filter(sampler2D image) { vec2 texCoord = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y) / uResolution; - vec2 pixel = texCoord * textureDimensions; + vec2 pixel = texCoord * textureDimensions - vec2(0.5, 0.5); vec4 q11 = texture2D(image, vec2(floor(pixel.x) / textureDimensions.x, floor(pixel.y) / textureDimensions.y)); vec4 q12 = texture2D(image, vec2(floor(pixel.x) / textureDimensions.x, ceil(pixel.y) / textureDimensions.y)); diff --git a/Shaders/MasterShader.fsh b/Shaders/MasterShader.fsh index 3821e48..11a714d 100644 --- a/Shaders/MasterShader.fsh +++ b/Shaders/MasterShader.fsh @@ -5,6 +5,7 @@ uniform bool uMixPrevious; uniform vec2 uResolution; const vec2 textureDimensions = vec2(160, 144); +#line 1 {filter} void main() { diff --git a/Shaders/OmniScale.fsh b/Shaders/OmniScale.fsh new file mode 100644 index 0000000..1bfdd6c --- /dev/null +++ b/Shaders/OmniScale.fsh @@ -0,0 +1,107 @@ + +float quickDistance(vec4 a, vec4 b) +{ + return abs(a.x - b.x) + abs(a.y - b.y) + abs(a.z - b.z); +} + +vec4 filter(sampler2D image) +{ + vec2 texCoord = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y) / uResolution; + + vec2 pixel = texCoord * textureDimensions - vec2(0.5, 0.5); + + vec4 q11 = texture2D(image, (pixel ) / textureDimensions); + vec4 q12 = texture2D(image, (pixel + vec2(0.0, 1.0)) / textureDimensions); + vec4 q21 = texture2D(image, (pixel + vec2(1.0, 0.0)) / textureDimensions); + vec4 q22 = texture2D(image, (pixel + vec2(1.0, 1.0)) / textureDimensions); + + vec2 pos = fract(pixel); + + /* Special handling for diaonals */ + bool hasDownDiagonal = false; + bool hasUpDiagonal = false; + if (q12 == q21 && q11 != q22) hasUpDiagonal = true; + else if (q12 != q21 && q11 == q22) hasDownDiagonal = true; + else if (q12 == q21 && q11 == q22) { + if (q11 == q12) return q11; + int diagonalBias = 0; + for (float y = -1.0; y < 3.0; y++) { + for (float x = -1.0; x < 3.0; x++) { + vec4 color = texture2D(image, (pixel + vec2(x, y)) / textureDimensions); + if (color == q11) diagonalBias++; + if (color == q12) diagonalBias--; + } + } + if (diagonalBias <= 0) { + hasDownDiagonal = true; + } + if (diagonalBias >= 0) { + hasUpDiagonal = true; + } + } + + if (hasUpDiagonal || hasDownDiagonal) { + vec4 downDiagonalResult, upDiagonalResult; + + if (hasUpDiagonal) { + float diagonalPos = pos.x + pos.y; + + if (diagonalPos < 0.5) { + upDiagonalResult = q11; + } + else if (diagonalPos > 1.5) { + upDiagonalResult = q22; + } + else { + upDiagonalResult = q12; + } + } + + if (hasDownDiagonal) { + float diagonalPos = 1.0 - pos.x + pos.y; + + if (diagonalPos < 0.5) { + downDiagonalResult = q21; + } + else if (diagonalPos > 1.5) { + downDiagonalResult = q12; + } + else { + downDiagonalResult = q11; + } + } + + if (!hasUpDiagonal) return downDiagonalResult; + if (!hasDownDiagonal) return upDiagonalResult; + return mix(downDiagonalResult, upDiagonalResult, 0.5); + } + + vec4 r1 = mix(q11, q21, fract(pos.x)); + vec4 r2 = mix(q12, q22, fract(pos.x)); + + vec4 unqunatized = mix(r1, r2, fract(pos.y)); + + float q11d = quickDistance(unqunatized, q11); + float q21d = quickDistance(unqunatized, q21); + float q12d = quickDistance(unqunatized, q12); + float q22d = quickDistance(unqunatized, q22); + + float best = min(q11d, + min(q21d, + min(q12d, + q22d))); + + if (q11d == best) { + return q11; + } + + if (q21d == best) { + return q21; + } + + if (q12d == best) { + return q12; + } + + return q22; +} \ No newline at end of file diff --git a/Shaders/SmoothBilinear.fsh b/Shaders/SmoothBilinear.fsh index 2489a03..391cba4 100644 --- a/Shaders/SmoothBilinear.fsh +++ b/Shaders/SmoothBilinear.fsh @@ -2,7 +2,7 @@ vec4 filter(sampler2D image) { vec2 texCoord = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y) / uResolution; - vec2 pixel = texCoord * textureDimensions; + vec2 pixel = texCoord * textureDimensions - vec2(0.5, 0.5); vec4 q11 = texture2D(image, vec2(floor(pixel.x) / textureDimensions.x, floor(pixel.y) / textureDimensions.y)); vec4 q12 = texture2D(image, vec2(floor(pixel.x) / textureDimensions.x, ceil(pixel.y) / textureDimensions.y));