Introducing the OmniScale (beta) algorithm to SameBoy
This commit is contained in:
parent
8a3e0c3f24
commit
94ea44da0c
@ -23,6 +23,8 @@
|
|||||||
@"Scale4x",
|
@"Scale4x",
|
||||||
@"AAScale2x",
|
@"AAScale2x",
|
||||||
@"AAScale4x",
|
@"AAScale4x",
|
||||||
|
@"OmniScale",
|
||||||
|
@"AAOmniScale",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return filters;
|
return filters;
|
||||||
|
@ -41,7 +41,6 @@ void main(void) {\n\
|
|||||||
if (self) {
|
if (self) {
|
||||||
// Program
|
// Program
|
||||||
NSString *fragment_shader = [[self class] shaderSourceForName:@"MasterShader"];
|
NSString *fragment_shader = [[self class] shaderSourceForName:@"MasterShader"];
|
||||||
fragment_shader = [fragment_shader stringByReplacingOccurrencesOfString:@"\n" withString:@""];
|
|
||||||
fragment_shader = [fragment_shader stringByReplacingOccurrencesOfString:@"{filter}"
|
fragment_shader = [fragment_shader stringByReplacingOccurrencesOfString:@"{filter}"
|
||||||
withString:[[self class] shaderSourceForName:shaderName]];
|
withString:[[self class] shaderSourceForName:shaderName]];
|
||||||
program = [[self class] programWithVertexShader:vertex_shader fragmentShader:fragment_shader];
|
program = [[self class] programWithVertexShader:vertex_shader fragmentShader:fragment_shader];
|
||||||
|
@ -6,6 +6,6 @@
|
|||||||
- (void) flip;
|
- (void) flip;
|
||||||
- (uint32_t *) pixels;
|
- (uint32_t *) pixels;
|
||||||
@property GB_gameboy_t *gb;
|
@property GB_gameboy_t *gb;
|
||||||
@property BOOL shouldBlendFrameWithPrevious;
|
@property (nonatomic) BOOL shouldBlendFrameWithPrevious;
|
||||||
@property GBShader *shader;
|
@property GBShader *shader;
|
||||||
@end
|
@end
|
||||||
|
@ -22,9 +22,16 @@ static GBShader *shader = nil;
|
|||||||
|
|
||||||
- (void) filterChanged
|
- (void) filterChanged
|
||||||
{
|
{
|
||||||
|
[self setNeedsDisplay:YES];
|
||||||
self.shader = nil;
|
self.shader = nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void) setShouldBlendFrameWithPrevious:(BOOL)shouldBlendFrameWithPrevious
|
||||||
|
{
|
||||||
|
_shouldBlendFrameWithPrevious = shouldBlendFrameWithPrevious;
|
||||||
|
[self setNeedsDisplay:YES];
|
||||||
|
}
|
||||||
|
|
||||||
- (unsigned char) numberOfBuffers
|
- (unsigned char) numberOfBuffers
|
||||||
{
|
{
|
||||||
return _shouldBlendFrameWithPrevious? 3 : 2;
|
return _shouldBlendFrameWithPrevious? 3 : 2;
|
||||||
|
@ -38,9 +38,9 @@
|
|||||||
</textField>
|
</textField>
|
||||||
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6pP-kK-EEC">
|
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6pP-kK-EEC">
|
||||||
<rect key="frame" x="30" y="223" width="245" height="26"/>
|
<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"/>
|
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="menu"/>
|
||||||
<menu key="menu" id="xDC-0T-Qg9">
|
<menu key="menu" id="xDC-0T-Qg9">
|
||||||
<items>
|
<items>
|
||||||
<menuItem title="Nearest Neighbor (Pixelated)" id="neN-eo-LA7">
|
<menuItem title="Nearest Neighbor (Pixelated)" id="neN-eo-LA7">
|
||||||
@ -54,6 +54,12 @@
|
|||||||
<menuItem title="Anti-aliased Scale4x" id="zJR-ER-Ygo">
|
<menuItem title="Anti-aliased Scale4x" id="zJR-ER-Ygo">
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
</menuItem>
|
</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>
|
</items>
|
||||||
</menu>
|
</menu>
|
||||||
</popUpButtonCell>
|
</popUpButtonCell>
|
||||||
|
@ -25,6 +25,7 @@ Features currently supported only with the Cocoa version:
|
|||||||
* Battery save support
|
* Battery save support
|
||||||
* Save states
|
* Save states
|
||||||
* Optional frame blending
|
* Optional frame blending
|
||||||
|
* Several [scaling algorithms](SCALING.md) (Including exclusive algorithms like OmniScale and Anti-aliased Scale2x)
|
||||||
|
|
||||||
## Compatibility
|
## 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.
|
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
39
SCALING.md
Normal 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
119
Shaders/AAOmniScale.fsh
Normal 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;
|
||||||
|
}
|
@ -2,7 +2,7 @@ vec4 filter(sampler2D image)
|
|||||||
{
|
{
|
||||||
vec2 texCoord = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y) / uResolution;
|
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 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 q12 = texture2D(image, vec2(floor(pixel.x) / textureDimensions.x, ceil(pixel.y) / textureDimensions.y));
|
||||||
|
@ -5,6 +5,7 @@ uniform bool uMixPrevious;
|
|||||||
uniform vec2 uResolution;
|
uniform vec2 uResolution;
|
||||||
const vec2 textureDimensions = vec2(160, 144);
|
const vec2 textureDimensions = vec2(160, 144);
|
||||||
|
|
||||||
|
#line 1
|
||||||
{filter}
|
{filter}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
107
Shaders/OmniScale.fsh
Normal file
107
Shaders/OmniScale.fsh
Normal 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;
|
||||||
|
}
|
@ -2,7 +2,7 @@ vec4 filter(sampler2D image)
|
|||||||
{
|
{
|
||||||
vec2 texCoord = vec2(gl_FragCoord.x, uResolution.y - gl_FragCoord.y) / uResolution;
|
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 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 q12 = texture2D(image, vec2(floor(pixel.x) / textureDimensions.x, ceil(pixel.y) / textureDimensions.y));
|
||||||
|
Loading…
Reference in New Issue
Block a user