From 73a36e17c2e2239ad19ce00fad2230db6441b504 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Mon, 9 Aug 2021 19:09:00 +0000 Subject: [PATCH] Fix anti-aliasing of rectangles so that they are always crisp. --- bemani/format/afp/blend/blend.py | 66 +++++++++++++----------- bemani/format/afp/blend/blendcpp.pyi | 4 +- bemani/format/afp/blend/blendcpp.pyx | 12 ++--- bemani/format/afp/blend/blendcppimpl.cxx | 29 ++++++++--- bemani/format/afp/render.py | 38 ++++++++++---- bemani/format/afp/types/__init__.py | 2 + bemani/format/afp/types/aa.py | 8 +++ 7 files changed, 102 insertions(+), 57 deletions(-) create mode 100644 bemani/format/afp/types/aa.py diff --git a/bemani/format/afp/blend/blend.py b/bemani/format/afp/blend/blend.py index 83c3773..4a9a0ae 100644 --- a/bemani/format/afp/blend/blend.py +++ b/bemani/format/afp/blend/blend.py @@ -1,9 +1,9 @@ import multiprocessing import signal from PIL import Image # type: ignore -from typing import Any, Callable, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, List, Optional, Sequence, Union -from ..types import Color, Matrix, Point +from ..types import Color, Matrix, Point, AAMode from .perspective import perspective_calculate @@ -201,14 +201,14 @@ def pixel_renderer( texheight: int, xscale: float, yscale: float, - callback: Callable[[Point], Tuple[Optional[Point], bool]], + callback: Callable[[Point], Optional[Point]], add_color: Color, mult_color: Color, blendfunc: int, imgbytes: Union[bytes, bytearray], texbytes: Union[bytes, bytearray], maskbytes: Optional[Union[bytes, bytearray]], - enable_aa: bool, + aa_mode: int, ) -> Sequence[int]: # Determine offset maskoff = imgx + (imgy * imgwidth) @@ -218,7 +218,7 @@ def pixel_renderer( # This pixel is masked off! return imgbytes[imgoff:(imgoff + 4)] - if enable_aa: + if aa_mode != AAMode.NONE: r = 0 g = 0 b = 0 @@ -229,17 +229,21 @@ 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, xscale) - yswing = 0.5 * max(1.0, yscale) + if aa_mode == AAMode.UNSCALED_SSAA_ONLY: + xswing = 0.5 + yswing = 0.5 + else: + 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] # First, figure out if we can use bilinear resampling. bilinear = False - if xscale >= 1.0 and yscale >= 1.0: - aaloc, enable_bilinear = callback(Point(imgx + 0.5, imgy + 0.5)) - if aaloc is not None and enable_bilinear: + if aa_mode == AAMode.SSAA_OR_BILINEAR and xscale >= 1.0 and yscale >= 1.0: + aaloc = callback(Point(imgx + 0.5, imgy + 0.5)) + if aaloc is not None: aax, aay, _ = aaloc.as_tuple() if not (aax <= 0 or aay <= 0 or aax >= (texwidth - 1) or aay >= (texheight - 1)): bilinear = True @@ -247,7 +251,7 @@ def pixel_renderer( # Now perform the desired AA operation. if bilinear: # Calculate the pixel we're after, and what percentage into the pixel we are. - texloc, _ = callback(Point(imgx + 0.5, imgy + 0.5)) + texloc = callback(Point(imgx + 0.5, imgy + 0.5)) if texloc is None: raise Exception("Logic error!") aax, aay, _ = texloc.as_tuple() @@ -293,7 +297,7 @@ def pixel_renderer( else: for addy in ypoints: for addx in xpoints: - texloc, _ = callback(Point(imgx + addx, imgy + addy)) + texloc = callback(Point(imgx + addx, imgy + addy)) denom += 1 if texloc is None: @@ -340,7 +344,7 @@ def pixel_renderer( return blend_point(add_color, mult_color, average, imgbytes[imgoff:(imgoff + 4)], blendfunc) else: # Calculate what texture pixel data goes here. - texloc, _ = callback(Point(imgx + 0.5, imgy + 0.5)) + texloc = callback(Point(imgx + 0.5, imgy + 0.5)) if texloc is None: return imgbytes[imgoff:(imgoff + 4)] @@ -370,7 +374,7 @@ def affine_line_renderer( imgbytes: Union[bytes, bytearray], texbytes: Union[bytes, bytearray], maskbytes: Optional[Union[bytes, bytearray]], - enable_aa: bool, + aa_mode: int, ) -> None: while True: imgy = work.get() @@ -392,14 +396,14 @@ def affine_line_renderer( texheight, 1.0 / inverse.xscale, 1.0 / inverse.yscale, - lambda point: (inverse.multiply_point(point), True), + lambda point: inverse.multiply_point(point), add_color, mult_color, blendfunc, imgbytes, texbytes, maskbytes, - enable_aa, + aa_mode, ) results.put((imgy, bytes(rowbytes))) @@ -414,7 +418,7 @@ def affine_composite( blendfunc: int, texture: Image.Image, single_threaded: bool = False, - enable_aa: bool = True, + aa_mode: int = AAMode.SSAA_OR_BILINEAR, ) -> Image.Image: # Calculate the inverse so we can map canvas space back to texture space. try: @@ -477,14 +481,14 @@ def affine_composite( texheight, 1.0 / inverse.xscale, 1.0 / inverse.yscale, - lambda point: (inverse.multiply_point(point), True), + lambda point: inverse.multiply_point(point), add_color, mult_color, blendfunc, imgbytes, texbytes, maskbytes, - enable_aa, + aa_mode, ) img = Image.frombytes('RGBA', (imgwidth, imgheight), bytes(imgbytes)) @@ -529,7 +533,7 @@ def affine_composite( imgbytes, texbytes, maskbytes, - enable_aa, + aa_mode, ), ) procs.append(proc) @@ -581,15 +585,15 @@ def perspective_line_renderer( imgbytes: Union[bytes, bytearray], texbytes: Union[bytes, bytearray], maskbytes: Optional[Union[bytes, bytearray]], - enable_aa: bool, + aa_mode: int, ) -> None: - def perspective_inverse(imgpoint: Point) -> Tuple[Optional[Point], bool]: + def perspective_inverse(imgpoint: Point) -> Optional[Point]: # Calculate the texture coordinate with our perspective interpolation. texdiv = inverse.multiply_point(imgpoint) if texdiv.z <= 0.0: - return None, False + return None - return Point(texdiv.x / texdiv.z, texdiv.y / texdiv.z), False + return Point(texdiv.x / texdiv.z, texdiv.y / texdiv.z) while True: imgy = work.get() @@ -618,7 +622,7 @@ def perspective_line_renderer( imgbytes, texbytes, maskbytes, - enable_aa, + aa_mode, ) results.put((imgy, bytes(rowbytes))) @@ -635,7 +639,7 @@ def perspective_composite( blendfunc: int, texture: Image.Image, single_threaded: bool = False, - enable_aa: bool = True, + aa_mode: int = AAMode.SSAA_ONLY, ) -> Image.Image: # Warn if we have an unsupported blend. if blendfunc not in {0, 1, 2, 3, 8, 9, 70, 256, 257}: @@ -664,13 +668,13 @@ def perspective_composite( else: maskbytes = None - def perspective_inverse(imgpoint: Point) -> Tuple[Optional[Point], bool]: + def perspective_inverse(imgpoint: Point) -> Optional[Point]: # Calculate the texture coordinate with our perspective interpolation. texdiv = inverse_matrix.multiply_point(imgpoint) if texdiv.z <= 0.0: - return None, False + return None - return Point(texdiv.x / texdiv.z, texdiv.y / texdiv.z), False + return Point(texdiv.x / texdiv.z, texdiv.y / texdiv.z) cores = multiprocessing.cpu_count() if single_threaded or cores < 2: @@ -703,7 +707,7 @@ def perspective_composite( imgbytes, texbytes, maskbytes, - enable_aa, + aa_mode, ) img = Image.frombytes('RGBA', (imgwidth, imgheight), bytes(imgbytes)) @@ -750,7 +754,7 @@ def perspective_composite( imgbytes, texbytes, maskbytes, - enable_aa, + aa_mode, ), ) procs.append(proc) diff --git a/bemani/format/afp/blend/blendcpp.pyi b/bemani/format/afp/blend/blendcpp.pyi index 9af27be..29b3715 100644 --- a/bemani/format/afp/blend/blendcpp.pyi +++ b/bemani/format/afp/blend/blendcpp.pyi @@ -13,7 +13,7 @@ def affine_composite( blendfunc: int, texture: Image.Image, single_threaded: bool = ..., - enable_aa: bool = ..., + aa_mode: int = ... ) -> Image.Image: ... @@ -29,6 +29,6 @@ def perspective_composite( blendfunc: int, texture: Image.Image, single_threaded: bool = ..., - enable_aa: bool = ..., + aa_mode: int = ... ) -> Image.Image: ... diff --git a/bemani/format/afp/blend/blendcpp.pyx b/bemani/format/afp/blend/blendcpp.pyx index 71eb7b1..2be9c93 100644 --- a/bemani/format/afp/blend/blendcpp.pyx +++ b/bemani/format/afp/blend/blendcpp.pyx @@ -2,7 +2,7 @@ import multiprocessing from PIL import Image # type: ignore from typing import Optional, Tuple -from ..types import Color, Matrix, Point +from ..types import Color, Matrix, Point, AAMode from .perspective import perspective_calculate cdef extern struct floatcolor_t: @@ -45,7 +45,7 @@ cdef extern int composite_fast( unsigned int texwidth, unsigned int texheight, unsigned int threads, - unsigned int enable_aa + unsigned int aa_mode ) def affine_composite( @@ -57,7 +57,7 @@ def affine_composite( blendfunc: int, texture: Image.Image, single_threaded: bool = False, - enable_aa: bool = True, + aa_mode: int = AAMode.SSAA_OR_BILINEAR, ) -> Image.Image: # Calculate the inverse so we can map canvas space back to texture space. try: @@ -141,7 +141,7 @@ def affine_composite( texwidth, texheight, threads, - 1 if enable_aa else 0, + aa_mode, ) if errors != 0: raise Exception("Error raised in C++!") @@ -164,7 +164,7 @@ def perspective_composite( blendfunc: int, texture: Image.Image, single_threaded: bool = False, - enable_aa: bool = True, + aa_mode: int = AAMode.SSAA_ONLY, ) -> Image.Image: if blendfunc not in {0, 1, 2, 3, 8, 9, 70, 256, 257}: print(f"WARNING: Unsupported blend {blendfunc}") @@ -229,7 +229,7 @@ def perspective_composite( texwidth, texheight, threads, - 1 if enable_aa else 0, + aa_mode, ) if errors != 0: raise Exception("Error raised in C++!") diff --git a/bemani/format/afp/blend/blendcppimpl.cxx b/bemani/format/afp/blend/blendcppimpl.cxx index 605c6be..570920d 100644 --- a/bemani/format/afp/blend/blendcppimpl.cxx +++ b/bemani/format/afp/blend/blendcppimpl.cxx @@ -5,6 +5,11 @@ #define MIN_THREAD_WORK 10 +#define AA_MODE_NONE 0 +#define AA_MODE_UNSCALED_SSAA_ONLY 1 +#define AA_MODE_SSAA_ONLY 2 +#define AA_MODE_SSAA_OR_BILINEAR 3 + extern "C" { typedef struct intcolor { @@ -77,7 +82,7 @@ extern "C" floatcolor_t mult_color; int blendfunc; pthread_t *thread; - int enable_aa; + int aa_mode; } work_t; inline unsigned char clamp(float color) { @@ -265,8 +270,16 @@ 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, work->xscale); - float yswing = 0.5 * fmax(1.0, work->yscale); + float xswing; + float yswing; + + if (work->aa_mode == AA_MODE_UNSCALED_SSAA_ONLY) { + xswing = 0.5; + yswing = 0.5; + } else { + xswing = 0.5 * fmax(1.0, work->xscale); + yswing = 0.5 * fmax(1.0, work->yscale); + } for (unsigned int imgy = work->miny; imgy < work->maxy; imgy++) { for (unsigned int imgx = work->minx; imgx < work->maxx; imgx++) { @@ -280,7 +293,7 @@ extern "C" } // Blend for simple anti-aliasing. - if (work->enable_aa) { + if (work->aa_mode != AA_MODE_NONE) { // Calculate what texture pixel data goes here. int r = 0; int g = 0; @@ -292,7 +305,7 @@ extern "C" // First, figure out if we can use bilinear resampling. Bilinear seems to look // awful on perspective transforms, so disable it for all of them. int bilinear = 0; - if (!work->use_perspective && work->xscale >= 1.0 && work->yscale >= 1.0) { + if (work->aa_mode == AA_MODE_SSAA_OR_BILINEAR && work->xscale >= 1.0 && work->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; @@ -503,7 +516,7 @@ extern "C" unsigned int texwidth, unsigned int texheight, unsigned int threads, - unsigned int enable_aa + unsigned int aa_mode ) { // Cast to a usable type. intcolor_t *imgdata = (intcolor_t *)imgbytes; @@ -528,7 +541,7 @@ extern "C" work.add_color = add_color; work.mult_color = mult_color; work.blendfunc = blendfunc; - work.enable_aa = enable_aa; + work.aa_mode = aa_mode; work.use_perspective = use_perspective; chunk_composite_fast(&work); @@ -575,7 +588,7 @@ extern "C" work->mult_color = mult_color; work->blendfunc = blendfunc; work->thread = thread; - work->enable_aa = enable_aa; + work->aa_mode = aa_mode; work->use_perspective = use_perspective; if (me) diff --git a/bemani/format/afp/render.py b/bemani/format/afp/render.py index d4aec57..1d8094f 100644 --- a/bemani/format/afp/render.py +++ b/bemani/format/afp/render.py @@ -22,6 +22,7 @@ from .types import ( Matrix, Point, Rectangle, + AAMode, AP2Trigger, AP2Action, PushAction, @@ -1046,7 +1047,7 @@ class AFPRenderer(VerboseOutput): 257, mask.rectangle, single_threaded=self.__single_threaded, - enable_aa=False, + aa_mode=AAMode.NONE, ) elif projection == AP2PlaceObjectTag.PROJECTION_PERSPECTIVE: if self.__camera is None: @@ -1060,7 +1061,7 @@ class AFPRenderer(VerboseOutput): 257, mask.rectangle, single_threaded=self.__single_threaded, - enable_aa=False, + aa_mode=AAMode.NONE, ) else: calculated_mask = perspective_composite( @@ -1074,7 +1075,7 @@ class AFPRenderer(VerboseOutput): 257, mask.rectangle, single_threaded=self.__single_threaded, - enable_aa=False, + aa_mode=AAMode.NONE, ) # Composite it onto the current mask. @@ -1087,7 +1088,7 @@ class AFPRenderer(VerboseOutput): 256, calculated_mask, single_threaded=self.__single_threaded, - enable_aa=False, + aa_mode=AAMode.NONE, ) def __render_object( @@ -1162,6 +1163,7 @@ class AFPRenderer(VerboseOutput): print("WARNING: Unhandled UV coordinate color!") texture = None + rectangle = False if params.flags & 0x2: # We need to look up the texture for this. if params.region not in self.textures: @@ -1203,9 +1205,15 @@ class AFPRenderer(VerboseOutput): shape.rectangle = Image.new('RGBA', (int(right - left), int(bottom - top)), (params.blend.as_tuple())) texture = shape.rectangle + rectangle = True if texture is not None: if projection == AP2PlaceObjectTag.PROJECTION_AFFINE: + if self.__enable_aa: + aamode = AAMode.UNSCALED_SSAA_ONLY if rectangle else AAMode.SSAA_OR_BILINEAR + else: + aamode = AAMode.NONE + img = affine_composite( img, add_color, @@ -1215,10 +1223,15 @@ class AFPRenderer(VerboseOutput): blend, texture, single_threaded=self.__single_threaded, - enable_aa=self.__enable_aa, + aa_mode=aamode, ) elif projection == AP2PlaceObjectTag.PROJECTION_PERSPECTIVE: if self.__camera is None: + if self.__enable_aa: + aamode = AAMode.UNSCALED_SSAA_ONLY if rectangle else AAMode.SSAA_OR_BILINEAR + else: + aamode = AAMode.NONE + print("WARNING: Element requests perspective projection but no camera exists!") img = affine_composite( img, @@ -1229,9 +1242,14 @@ class AFPRenderer(VerboseOutput): blend, texture, single_threaded=self.__single_threaded, - enable_aa=self.__enable_aa, + aa_mode=aamode, ) else: + if self.__enable_aa: + aamode = AAMode.UNSCALED_SSAA_ONLY if rectangle else AAMode.SSAA_ONLY + else: + aamode = AAMode.NONE + img = perspective_composite( img, add_color, @@ -1243,7 +1261,7 @@ class AFPRenderer(VerboseOutput): blend, texture, single_threaded=self.__single_threaded, - enable_aa=self.__enable_aa, + aa_mode=aamode, ) elif isinstance(renderable, PlacedImage): @@ -1263,7 +1281,7 @@ class AFPRenderer(VerboseOutput): blend, texture, single_threaded=self.__single_threaded, - enable_aa=self.__enable_aa, + aa_mode=AAMode.SSAA_OR_BILINEAR if self.__enable_aa else AAMode.NONE, ) elif projection == AP2PlaceObjectTag.PROJECTION_PERSPECTIVE: if self.__camera is None: @@ -1277,7 +1295,7 @@ class AFPRenderer(VerboseOutput): blend, texture, single_threaded=self.__single_threaded, - enable_aa=self.__enable_aa, + aa_mode=AAMode.SSAA_OR_BILINEAR if self.__enable_aa else AAMode.NONE, ) else: img = perspective_composite( @@ -1291,7 +1309,7 @@ class AFPRenderer(VerboseOutput): blend, texture, single_threaded=self.__single_threaded, - enable_aa=self.__enable_aa, + aa_mode=AAMode.SSAA_ONLY if self.__enable_aa else AAMode.NONE, ) elif isinstance(renderable, PlacedDummy): # Nothing to do! diff --git a/bemani/format/afp/types/__init__.py b/bemani/format/afp/types/__init__.py index 4bf783a..7232cd9 100644 --- a/bemani/format/afp/types/__init__.py +++ b/bemani/format/afp/types/__init__.py @@ -87,6 +87,7 @@ from .statement import ( AndIf, OrIf, ) +from .aa import AAMode __all__ = [ @@ -176,4 +177,5 @@ __all__ = [ 'GetURL2Action', 'StartDragAction', 'DefineFunction2Action', + 'AAMode', ] diff --git a/bemani/format/afp/types/aa.py b/bemani/format/afp/types/aa.py new file mode 100644 index 0000000..e34217d --- /dev/null +++ b/bemani/format/afp/types/aa.py @@ -0,0 +1,8 @@ +from typing_extensions import Final + + +class AAMode: + NONE: Final[int] = 0 + UNSCALED_SSAA_ONLY: Final[int] = 1 + SSAA_ONLY: Final[int] = 2 + SSAA_OR_BILINEAR: Final[int] = 3