Introducing the OmniScale (beta) algorithm to SameBoy

This commit is contained in:
Lior Halphon 2016-06-09 00:06:55 +03:00
parent 8a3e0c3f24
commit 94ea44da0c
12 changed files with 287 additions and 6 deletions

View File

@ -23,6 +23,8 @@
@"Scale4x",
@"AAScale2x",
@"AAScale4x",
@"OmniScale",
@"AAOmniScale",
];
}
return filters;

View File

@ -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];

View File

@ -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

View File

@ -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;

View File

@ -38,9 +38,9 @@
</textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6pP-kK-EEC">
<rect key="frame" x="30" y="223" width="245" height="26"/>
<popUpButtonCell key="cell" type="push" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" id="I1w-05-lGl">
<popUpButtonCell key="cell" type="push" title="Nearest Neighbor (Pixelated)" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="neN-eo-LA7" id="I1w-05-lGl">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="xDC-0T-Qg9">
<items>
<menuItem title="Nearest Neighbor (Pixelated)" id="neN-eo-LA7">
@ -54,6 +54,12 @@
<menuItem title="Anti-aliased Scale4x" id="zJR-ER-Ygo">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="OmniScale (Beta, any factor)" id="doe-kf-quG">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Anti-aliased OmniScale" id="uZy-BK-VyB">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
</items>
</menu>
</popUpButtonCell>

View File

@ -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.

39
SCALING.md Normal file
View File

@ -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.

119
Shaders/AAOmniScale.fsh Normal file
View File

@ -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;
}

View File

@ -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));

View File

@ -5,6 +5,7 @@ uniform bool uMixPrevious;
uniform vec2 uResolution;
const vec2 textureDimensions = vec2(160, 144);
#line 1
{filter}
void main() {

107
Shaders/OmniScale.fsh Normal file
View File

@ -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;
}

View File

@ -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));