From 092c4b697295d78a93706ae70e6e3ac767f18dfd Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Sun, 13 Jun 2021 18:24:18 +0000 Subject: [PATCH] Implement bilinear AA for scaled up sprites to get rid of boxy artifacting. --- bemani/format/afp/blend/blend.py | 125 +++++++++++----- bemani/format/afp/blend/blendcppimpl.cxx | 180 ++++++++++++++++------- 2 files changed, 216 insertions(+), 89 deletions(-) diff --git a/bemani/format/afp/blend/blend.py b/bemani/format/afp/blend/blend.py index 09d29d9..0d37676 100644 --- a/bemani/format/afp/blend/blend.py +++ b/bemani/format/afp/blend/blend.py @@ -432,52 +432,109 @@ def pixel_renderer( # Essentially what we're doing here is calculating the scale, clamping it at 1.0 as the # minimum and then setting the AA sample swing accordingly. This has the effect of anti-aliasing # scaled up images a bit softer than would otherwise be achieved. - xswing = 0.5 * max(1.0, 1.0 / math.sqrt(inverse.a * inverse.a + inverse.b * inverse.b)) - yswing = 0.5 * max(1.0, 1.0 / math.sqrt(inverse.c * inverse.c + inverse.d * inverse.d)) + xscale = 1.0 / math.sqrt(inverse.a * inverse.a + inverse.b * inverse.b) + yscale = 1.0 / math.sqrt(inverse.c * inverse.c + inverse.d * inverse.d) + + # These are used for picking the various sample points for SSAA method below. + xswing = 0.5 * max(1.0, xscale) + yswing = 0.5 * max(1.0, yscale) xpoints = [0.5 - xswing, 0.5 - (xswing / 2.0), 0.5, 0.5 + (xswing / 2.0), 0.5 + xswing] ypoints = [0.5 - yswing, 0.5 - (yswing / 2.0), 0.5, 0.5 + (yswing / 2.0), 0.5 + yswing] - for addy in ypoints: - for addx in xpoints: - texloc = inverse.multiply_point(Point(imgx + addx, imgy + addy)) - aax, aay = texloc.as_tuple() + # First, figure out if we can use bilinear resampling. + bilinear = False + if xscale >= 1.0 and yscale >= 1.0: + aaloc = inverse.multiply_point(Point(imgx + 0.5, imgy + 0.5)) + aax, aay = aaloc.as_tuple() + if not (aax <= 0 or aay <= 0 or aax >= (texwidth - 1) or aay >= (texheight - 1)): + bilinear = True - # If we're out of bounds, don't update. Factor this in, however, so we can get partial - # transparency to the pixel that is already there. - denom += 1 - if aax < 0 or aay < 0 or aax >= texwidth or aay >= texheight: - continue + # Now perform the desired AA operation. + if bilinear: + # Calculate the pixel we're after, and what percentage into the pixel we are. + texloc = inverse.multiply_point(Point(imgx + 0.5, imgy + 0.5)) + aax, aay = texloc.as_tuple() + aaxrem = texloc.x - aax + aayrem = texloc.y - aay - # Grab the values to average, for SSAA. Make sure to factor in alpha as a poor-man's - # blend to ensure that partial transparency pixel values don't unnecessarily factor - # into average calculations. - texoff = (aax + (aay * texwidth)) * 4 + # Find the four pixels that we can interpolate from. The first number is the x, and second is y. + tex00 = (aax + (aay * texwidth)) * 4 + tex10 = tex00 + 4 + tex01 = (aax + ((aay + 1) * texwidth)) * 4 + tex11 = tex01 + 4 - # If this is a fully transparent pixel, the below formulas work out to adding nothing - # so we should skip this altogether. - if texbytes[texoff + 3] == 0: - continue + # Calculate various scaling factors based on alpha and percentage. + tex00percent = texbytes[tex00 + 3] / 255.0 + tex10percent = texbytes[tex10 + 3] / 255.0 + tex01percent = texbytes[tex01 + 3] / 255.0 + tex11percent = texbytes[tex11 + 3] / 255.0 - apercent = texbytes[texoff + 3] / 255.0 - r += int(texbytes[texoff] * apercent) - g += int(texbytes[texoff + 1] * apercent) - b += int(texbytes[texoff + 2] * apercent) - a += texbytes[texoff + 3] - count += 1 + y0percent = (tex00percent * (1.0 - aaxrem)) + (tex10percent * aaxrem) + y1percent = (tex01percent * (1.0 - aaxrem)) + (tex11percent * aaxrem) + finalpercent = (y0percent * (1.0 - aayrem)) + (y1percent * aayrem) - if count == 0: - # None of the samples existed in-bounds. - return imgbytes[imgoff:(imgoff + 4)] + if finalpercent <= 0.0: + # This pixel would be blank, so we avoid dividing by zero. + average = [255, 255, 255, 0] + else: + # Interpolate in the X direction on both Y axis. + y0r = ((texbytes[tex00] * tex00percent * (1.0 - aaxrem)) + (texbytes[tex10] * tex10percent * aaxrem)) + y0g = ((texbytes[tex00 + 1] * tex00percent * (1.0 - aaxrem)) + (texbytes[tex10 + 1] * tex10percent * aaxrem)) + y0b = ((texbytes[tex00 + 2] * tex00percent * (1.0 - aaxrem)) + (texbytes[tex10 + 2] * tex10percent * aaxrem)) - # Average the pixels. Make sure to divide out the alpha in preparation for blending. - alpha = a // denom + y1r = ((texbytes[tex01] * tex01percent * (1.0 - aaxrem)) + (texbytes[tex11] * tex11percent * aaxrem)) + y1g = ((texbytes[tex01 + 1] * tex01percent * (1.0 - aaxrem)) + (texbytes[tex11 + 1] * tex11percent * aaxrem)) + y1b = ((texbytes[tex01 + 2] * tex01percent * (1.0 - aaxrem)) + (texbytes[tex11 + 2] * tex11percent * aaxrem)) - if alpha == 0: - average = [255, 255, 255, alpha] + # Now interpolate the Y direction to get the final pixel value. + average = [ + int(((y0r * (1.0 - aayrem)) + (y1r * aayrem)) / finalpercent), + int(((y0g * (1.0 - aayrem)) + (y1g * aayrem)) / finalpercent), + int(((y0b * (1.0 - aayrem)) + (y1b * aayrem)) / finalpercent), + int(finalpercent * 255), + ] else: - apercent = alpha / 255.0 - average = [int((r / denom) / apercent), int((g / denom) / apercent), int((b / denom) / apercent), alpha] + for addy in ypoints: + for addx in xpoints: + texloc = inverse.multiply_point(Point(imgx + addx, imgy + addy)) + aax, aay = texloc.as_tuple() + + # If we're out of bounds, don't update. Factor this in, however, so we can get partial + # transparency to the pixel that is already there. + denom += 1 + if aax < 0 or aay < 0 or aax >= texwidth or aay >= texheight: + continue + + # Grab the values to average, for SSAA. Make sure to factor in alpha as a poor-man's + # blend to ensure that partial transparency pixel values don't unnecessarily factor + # into average calculations. + texoff = (aax + (aay * texwidth)) * 4 + + # If this is a fully transparent pixel, the below formulas work out to adding nothing + # so we should skip this altogether. + if texbytes[texoff + 3] == 0: + continue + + apercent = texbytes[texoff + 3] / 255.0 + r += int(texbytes[texoff] * apercent) + g += int(texbytes[texoff + 1] * apercent) + b += int(texbytes[texoff + 2] * apercent) + a += texbytes[texoff + 3] + count += 1 + + if count == 0: + # None of the samples existed in-bounds. + return imgbytes[imgoff:(imgoff + 4)] + + # Average the pixels. Make sure to divide out the alpha in preparation for blending. + alpha = a // denom + + if alpha == 0: + average = [255, 255, 255, alpha] + else: + apercent = alpha / 255.0 + average = [int((r / denom) / apercent), int((g / denom) / apercent), int((b / denom) / apercent), alpha] # Finally, blend it with the destination. return blend_point(add_color, mult_color, average, imgbytes[imgoff:(imgoff + 4)], blendfunc) diff --git a/bemani/format/afp/blend/blendcppimpl.cxx b/bemani/format/afp/blend/blendcppimpl.cxx index ead3deb..b448248 100644 --- a/bemani/format/afp/blend/blendcppimpl.cxx +++ b/bemani/format/afp/blend/blendcppimpl.cxx @@ -253,8 +253,12 @@ extern "C" // costs us almost nothing. Essentially what we're doing here is calculating the scale, clamping it at 1.0 as the // minimum and then setting the AA sample swing accordingly. This has the effect of anti-aliasing scaled up images // a bit softer than would otherwise be achieved. - float xswing = 0.5 * fmax(1.0, 1.0 / sqrt(work->inverse.a * work->inverse.a + work->inverse.b * work->inverse.b)); - float yswing = 0.5 * fmax(1.0, 1.0 / sqrt(work->inverse.c * work->inverse.c + work->inverse.d * work->inverse.d)); + float xscale = 1.0 / sqrt(work->inverse.a * work->inverse.a + work->inverse.b * work->inverse.b); + float yscale = 1.0 / sqrt(work->inverse.c * work->inverse.c + work->inverse.d * work->inverse.d); + + // These are used for picking the various sample points for SSAA method below. + float xswing = 0.5 * fmax(1.0, xscale); + float yswing = 0.5 * fmax(1.0, yscale); for (unsigned int imgy = work->miny; imgy < work->maxy; imgy++) { for (unsigned int imgx = work->minx; imgx < work->maxx; imgx++) { @@ -277,65 +281,131 @@ extern "C" int count = 0; int denom = 0; - for (float addy = 0.5 - yswing; addy <= 0.5 + yswing; addy += yswing / 2.0) { - for (float addx = 0.5 - xswing; addx <= 0.5 + xswing; addx += xswing / 2.0) { - point_t texloc = work->inverse.multiply_point((point_t){(float)imgx + addx, (float)imgy + addy}); - int aax = texloc.x; - int aay = texloc.y; + // First, figure out if we can use bilinear resampling. + int bilinear = 0; + if (xscale >= 1.0 && yscale >= 1.0) { + point_t aaloc = work->inverse.multiply_point((point_t){(float)(imgx + 0.5), (float)(imgy + 0.5)}); + int aax = aaloc.x; + int aay = aaloc.y; - // If we're out of bounds, don't update. Factor this in, however, so we can get partial - // transparency to the pixel that is already there. - denom ++; - if (aax < 0 or aay < 0 or aax >= (int)work->texwidth or aay >= (int)work->texheight) { - continue; - } - - // Grab the values to average, for SSAA. Make sure to factor in alpha as a poor-man's - // blend to ensure that partial transparency pixel values don't unnecessarily factor - // into average calculations. - unsigned int texoff = aax + (aay * work->texwidth); - - // If this is a fully transparent pixel, the below formulas work out to adding nothing - // so we should skip this altogether. - if (work->texdata[texoff].a == 0) { - continue; - } - - float apercent = work->texdata[texoff].a / 255.0; - r += (int)(work->texdata[texoff].r * apercent); - g += (int)(work->texdata[texoff].g * apercent); - b += (int)(work->texdata[texoff].b * apercent); - a += work->texdata[texoff].a; - count ++; + if (!(aax <= 0 || aay <= 0 || aax >= ((int)work->texwidth - 1) || aay >= ((int)work->texheight - 1))) { + bilinear = 1; } } - if (count == 0) { - // None of the samples existed in-bounds. - continue; - } - - // Average the pixels. Make sure to divide out the alpha in preparation for blending. - unsigned char alpha = (unsigned char)(a / denom); + // Now perform the desired AA operation. intcolor_t average; + if (bilinear) { + // Calculate the pixel we're after, and what percentage into the pixel we are. + point_t texloc = work->inverse.multiply_point((point_t){(float)(imgx + 0.5), (float)(imgy + 0.5)}); + int aax = texloc.x; + int aay = texloc.y; + float aaxrem = texloc.x - (float)aax; + float aayrem = texloc.y - (float)aay; - if (alpha == 0) { - // Samples existed in bounds, but with zero alpha. - average = (intcolor_t){ - 255, - 255, - 255, - alpha, - }; + // Find the four pixels that we can interpolate from. The first number is the x, and second is y. + unsigned int tex00 = aax + (aay * work->texwidth); + unsigned int tex10 = tex00 + 1; + unsigned int tex01 = aax + ((aay + 1) * work->texwidth); + unsigned int tex11 = tex01 + 1; + + // Calculate various scaling factors based on alpha and percentage. + float tex00percent = work->texdata[tex00].a / 255.0; + float tex10percent = work->texdata[tex10].a / 255.0; + float tex01percent = work->texdata[tex01].a / 255.0; + float tex11percent = work->texdata[tex11].a / 255.0; + + float y0percent = (tex00percent * (1.0 - aaxrem)) + (tex10percent * aaxrem); + float y1percent = (tex01percent * (1.0 - aaxrem)) + (tex11percent * aaxrem); + float finalpercent = (y0percent * (1.0 - aayrem)) + (y1percent * aayrem); + + if (finalpercent <= 0.0) { + // This pixel would be blank, so we avoid dividing by zero. + average = (intcolor_t){ + 255, + 255, + 255, + 0, + }; + } else { + // Interpolate in the X direction on both Y axis. + float y0r = ((work->texdata[tex00].r * tex00percent * (1.0 - aaxrem)) + (work->texdata[tex10].r * tex10percent * aaxrem)); + float y0g = ((work->texdata[tex00].g * tex00percent * (1.0 - aaxrem)) + (work->texdata[tex10].g * tex10percent * aaxrem)); + float y0b = ((work->texdata[tex00].b * tex00percent * (1.0 - aaxrem)) + (work->texdata[tex10].b * tex10percent * aaxrem)); + + + float y1r = ((work->texdata[tex01].r * tex01percent * (1.0 - aaxrem)) + (work->texdata[tex11].r * tex11percent * aaxrem)); + float y1g = ((work->texdata[tex01].g * tex01percent * (1.0 - aaxrem)) + (work->texdata[tex11].g * tex11percent * aaxrem)); + float y1b = ((work->texdata[tex01].b * tex01percent * (1.0 - aaxrem)) + (work->texdata[tex11].b * tex11percent * aaxrem)); + + // Now interpolate the Y direction to get the final pixel value. + average = (intcolor_t){ + (unsigned char)(((y0r * (1.0 - aayrem)) + (y1r * aayrem)) / finalpercent), + (unsigned char)(((y0g * (1.0 - aayrem)) + (y1g * aayrem)) / finalpercent), + (unsigned char)(((y0b * (1.0 - aayrem)) + (y1b * aayrem)) / finalpercent), + (unsigned char)(finalpercent * 255), + }; + } } else { - // Samples existed in bounds, with some alpha component, un-premultiply it. - float apercent = alpha / 255.0; - average = (intcolor_t){ - (unsigned char)((r / denom) / apercent), - (unsigned char)((g / denom) / apercent), - (unsigned char)((b / denom) / apercent), - alpha, - }; + for (float addy = 0.5 - yswing; addy <= 0.5 + yswing; addy += yswing / 2.0) { + for (float addx = 0.5 - xswing; addx <= 0.5 + xswing; addx += xswing / 2.0) { + point_t texloc = work->inverse.multiply_point((point_t){(float)imgx + addx, (float)imgy + addy}); + int aax = texloc.x; + int aay = texloc.y; + + // If we're out of bounds, don't update. Factor this in, however, so we can get partial + // transparency to the pixel that is already there. + denom ++; + if (aax < 0 || aay < 0 || aax >= (int)work->texwidth || aay >= (int)work->texheight) { + continue; + } + + // Grab the values to average, for SSAA. Make sure to factor in alpha as a poor-man's + // blend to ensure that partial transparency pixel values don't unnecessarily factor + // into average calculations. + unsigned int texoff = aax + (aay * work->texwidth); + + // If this is a fully transparent pixel, the below formulas work out to adding nothing + // so we should skip this altogether. + if (work->texdata[texoff].a == 0) { + continue; + } + + float apercent = work->texdata[texoff].a / 255.0; + r += (int)(work->texdata[texoff].r * apercent); + g += (int)(work->texdata[texoff].g * apercent); + b += (int)(work->texdata[texoff].b * apercent); + a += work->texdata[texoff].a; + count ++; + } + } + + if (count == 0) { + // None of the samples existed in-bounds. + continue; + } + + // Average the pixels. Make sure to divide out the alpha in preparation for blending. + unsigned char alpha = (unsigned char)(a / denom); + + if (alpha == 0) { + // Samples existed in bounds, but with zero alpha. + average = (intcolor_t){ + 255, + 255, + 255, + 0, + }; + } else { + // Samples existed in bounds, with some alpha component, un-premultiply it. + float apercent = alpha / 255.0; + average = (intcolor_t){ + (unsigned char)((r / denom) / apercent), + (unsigned char)((g / denom) / apercent), + (unsigned char)((b / denom) / apercent), + alpha, + }; + } } // Blend it. @@ -347,7 +417,7 @@ extern "C" int texy = texloc.y; // If we're out of bounds, don't update. - if (texx < 0 or texy < 0 or texx >= (int)work->texwidth or texy >= (int)work->texheight) { + if (texx < 0 || texy < 0 || texx >= (int)work->texwidth || texy >= (int)work->texheight) { continue; }