mirror of https://github.com/bsnes-emu/bsnes.git
Introducing the OmniScale (beta) algorithm to SameBoy
This commit is contained in:
parent
8a3e0c3f24
commit
94ea44da0c
|
@ -23,6 +23,8 @@
|
|||
@"Scale4x",
|
||||
@"AAScale2x",
|
||||
@"AAScale4x",
|
||||
@"OmniScale",
|
||||
@"AAOmniScale",
|
||||
];
|
||||
}
|
||||
return filters;
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
|
@ -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 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));
|
||||
|
|
|
@ -5,6 +5,7 @@ uniform bool uMixPrevious;
|
|||
uniform vec2 uResolution;
|
||||
const vec2 textureDimensions = vec2(160, 144);
|
||||
|
||||
#line 1
|
||||
{filter}
|
||||
|
||||
void main() {
|
||||
|
|
|
@ -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 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));
|
||||
|
|
Loading…
Reference in New Issue