diff --git a/bemani/format/afp/blend/blend.py b/bemani/format/afp/blend/blend.py index 20f201f..28a81fc 100644 --- a/bemani/format/afp/blend/blend.py +++ b/bemani/format/afp/blend/blend.py @@ -127,6 +127,7 @@ def affine_composite( blendfunc: int, texture: Image.Image, single_threaded: bool = False, + enable_aa: bool = True, ) -> Image.Image: # Calculate the inverse so we can map canvas space back to texture space. try: @@ -181,21 +182,59 @@ def affine_composite( for imgx in range(minx, maxx): # Determine offset imgoff = imgx + (imgy * imgwidth) - - # Calculate what texture pixel data goes here. - texloc = inverse.multiply_point(Point(float(imgx + 0.5), float(imgy + 0.5))) - texx, texy = texloc.as_tuple() - - # If we're out of bounds, don't update. - if texx < 0 or texy < 0 or texx >= texwidth or texy >= texheight: - continue - - # Blend it. - texoff = texx + (texy * texwidth) if maskmap is not None and maskmap[imgoff] == 0: # This pixel is masked off! continue - imgmap[imgoff] = blend_point(add_color, mult_color, texmap[texoff], imgmap[imgoff], blendfunc) + + if enable_aa: + r = 0 + g = 0 + b = 0 + a = 0 + count = 0 + + xswing = abs(0.5 / inverse.a) + yswing = abs(0.5 / inverse.d) + + 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() + + # If we're out of bounds, don't update. + if aax < 0 or aay < 0 or aax >= texwidth or aay >= texheight: + continue + + # Grab the values to average, for SSAA. + texoff = aax + (aay * texwidth) + r += texmap[texoff][0] + g += texmap[texoff][1] + b += texmap[texoff][2] + a += texmap[texoff][3] + count += 1 + + if count == 0: + # None of the samples existed in-bounds. + continue + + # Average the pixels. + average = [r // count, g // count, b // count, a // count] + imgmap[imgoff] = blend_point(add_color, mult_color, average, imgmap[imgoff], blendfunc) + else: + # Calculate what texture pixel data goes here. + texloc = inverse.multiply_point(Point(imgx + 0.5, imgy + 0.5)) + texx, texy = texloc.as_tuple() + + # If we're out of bounds, don't update. + if texx < 0 or texy < 0 or texx >= texwidth or texy >= texheight: + continue + + # Blend it. + texoff = texx + (texy * texwidth) + imgmap[imgoff] = blend_point(add_color, mult_color, texmap[texoff], imgmap[imgoff], blendfunc) img.putdata(imgmap) else: @@ -239,6 +278,7 @@ def affine_composite( imgbytes, texbytes, maskbytes, + enable_aa, ), ) procs.append(proc) @@ -317,6 +357,7 @@ def pixel_renderer( imgbytes: bytes, texbytes: bytes, maskbytes: Optional[bytes], + enable_aa: bool, ) -> None: while True: imgy = work.get() @@ -330,23 +371,62 @@ def pixel_renderer( if imgx < minx or imgx >= maxx: result.append(imgbytes[(imgoff * 4):((imgoff + 1) * 4)]) continue - - # Calculate what texture pixel data goes here. - texloc = inverse.multiply_point(Point(float(imgx + 0.5), float(imgy + 0.5))) - texx, texy = texloc.as_tuple() - - # If we're out of bounds, don't update. - if texx < 0 or texy < 0 or texx >= texwidth or texy >= texheight: - result.append(imgbytes[(imgoff * 4):((imgoff + 1) * 4)]) - continue - - # Blend it. - texoff = texx + (texy * texwidth) if maskbytes is not None and maskbytes[imgoff] == 0: # This pixel is masked off! result.append(imgbytes[(imgoff * 4):((imgoff + 1) * 4)]) continue - result.append(blend_point(add_color, mult_color, texbytes[(texoff * 4):((texoff + 1) * 4)], imgbytes[(imgoff * 4):((imgoff + 1) * 4)], blendfunc)) + + if enable_aa: + r = 0 + g = 0 + b = 0 + a = 0 + count = 0 + + xswing = abs(0.5 / inverse.a) + yswing = abs(0.5 / inverse.d) + + 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() + + # If we're out of bounds, don't update. + if aax < 0 or aay < 0 or aax >= texwidth or aay >= texheight: + continue + + # Grab the values to average, for SSAA. + texoff = (aax + (aay * texwidth)) * 4 + r += texbytes[texoff] + g += texbytes[texoff + 1] + b += texbytes[texoff + 2] + a += texbytes[texoff + 3] + count += 1 + + if count == 0: + # None of the samples existed in-bounds. + result.append(imgbytes[(imgoff * 4):((imgoff + 1) * 4)]) + continue + + # Average the pixels. + average = [r // count, g // count, b // count, a // count] + result.append(blend_point(add_color, mult_color, average, imgbytes[(imgoff * 4):((imgoff + 1) * 4)], blendfunc)) + else: + # Calculate what texture pixel data goes here. + texloc = inverse.multiply_point(Point(imgx + 0.5, imgy + 0.5)) + texx, texy = texloc.as_tuple() + + # If we're out of bounds, don't update. + if texx < 0 or texy < 0 or texx >= texwidth or texy >= texheight: + result.append(imgbytes[(imgoff * 4):((imgoff + 1) * 4)]) + continue + + # Blend it. + texoff = texx + (texy * texwidth) + result.append(blend_point(add_color, mult_color, texbytes[(texoff * 4):((texoff + 1) * 4)], imgbytes[(imgoff * 4):((imgoff + 1) * 4)], blendfunc)) linebytes = bytes([channel for pixel in result for channel in pixel]) results.put((imgy, linebytes)) diff --git a/bemani/format/afp/blend/blendcpp.pyi b/bemani/format/afp/blend/blendcpp.pyi index 7af6366..a03c6f9 100644 --- a/bemani/format/afp/blend/blendcpp.pyi +++ b/bemani/format/afp/blend/blendcpp.pyi @@ -12,5 +12,6 @@ def affine_composite( blendfunc: int, texture: Image.Image, single_threaded: bool = False, + enable_aa: bool = True, ) -> Image.Image: ... diff --git a/bemani/format/afp/blend/blendcpp.pyx b/bemani/format/afp/blend/blendcpp.pyx index 3c7525d..e2598b3 100644 --- a/bemani/format/afp/blend/blendcpp.pyx +++ b/bemani/format/afp/blend/blendcpp.pyx @@ -38,7 +38,8 @@ cdef extern int affine_composite_fast( unsigned char *texdata, unsigned int texwidth, unsigned int texheight, - unsigned int threads + unsigned int threads, + unsigned int enable_aa, ) def affine_composite( @@ -50,6 +51,7 @@ def affine_composite( blendfunc: int, texture: Image.Image, single_threaded: bool = False, + enable_aa: bool = True, ) -> Image.Image: # Calculate the inverse so we can map canvas space back to texture space. try: @@ -125,6 +127,7 @@ def affine_composite( texwidth, texheight, threads, + 1 if enable_aa else 0, ) 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 d130e6d..2613319 100644 --- a/bemani/format/afp/blend/blendcppimpl.cxx +++ b/bemani/format/afp/blend/blendcppimpl.cxx @@ -65,6 +65,7 @@ extern "C" floatcolor_t mult_color; int blendfunc; pthread_t *thread; + int enable_aa; } work_t; inline unsigned char clamp(float color) { @@ -253,23 +254,75 @@ extern "C" // Determine offset. unsigned int imgoff = imgx + (imgy * work->imgwidth); - // Calculate what texture pixel data goes here. - point_t texloc = work->inverse.multiply_point((point_t){(float)imgx + (float)0.5, (float)imgy + (float)0.5}); - int texx = texloc.x; - 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) { - continue; - } - - // Blend it. - unsigned int texoff = texx + (texy * work->texwidth); + // If we are masked off, don't do any other calculations. if (work->maskdata != NULL && work->maskdata[imgoff] == 0) { // This pixel is masked off! continue; } - work->imgdata[imgoff] = blend_point(work->add_color, work->mult_color, work->texdata[texoff], work->imgdata[imgoff], work->blendfunc); + + // Blend for simple anti-aliasing. + if (work->enable_aa) { + // Calculate what texture pixel data goes here. + int r = 0; + int g = 0; + int b = 0; + int a = 0; + int count = 0; + + float xswing = fabs(0.5 / work->inverse.a); + float yswing = fabs(0.5 / work->inverse.d); + + 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. + if (aax < 0 or aay < 0 or aax >= (int)work->texwidth or aay >= (int)work->texheight) { + continue; + } + + // Grab the values to average, for SSAA. + unsigned int texoff = aax + (aay * work->texwidth); + r += work->texdata[texoff].r; + g += work->texdata[texoff].g; + b += work->texdata[texoff].b; + a += work->texdata[texoff].a; + count ++; + } + } + + if (count == 0) { + // None of the samples existed in-bounds. + continue; + } + + // Average the pixels. + intcolor_t average = (intcolor_t){ + (unsigned char)(r / count), + (unsigned char)(g / count), + (unsigned char)(b / count), + (unsigned char)(a / count), + }; + + // Blend it. + work->imgdata[imgoff] = blend_point(work->add_color, work->mult_color, average, work->imgdata[imgoff], work->blendfunc); + } else { + // Grab the center of the pixel to get the color. + point_t texloc = work->inverse.multiply_point((point_t){(float)imgx + (float)0.5, (float)imgy + (float)0.5}); + int texx = texloc.x; + 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) { + continue; + } + + // Blend it. + unsigned int texoff = texx + (texy * work->texwidth); + work->imgdata[imgoff] = blend_point(work->add_color, work->mult_color, work->texdata[texoff], work->imgdata[imgoff], work->blendfunc); + } } } } @@ -296,7 +349,8 @@ extern "C" unsigned char *texbytes, unsigned int texwidth, unsigned int texheight, - unsigned int threads + unsigned int threads, + unsigned int enable_aa ) { // Cast to a usable type. intcolor_t *imgdata = (intcolor_t *)imgbytes; @@ -319,6 +373,7 @@ extern "C" work.add_color = add_color; work.mult_color = mult_color; work.blendfunc = blendfunc; + work.enable_aa = enable_aa; chunk_composite_fast(&work); } else { @@ -362,6 +417,7 @@ extern "C" work->mult_color = mult_color; work->blendfunc = blendfunc; work->thread = thread; + work->enable_aa = enable_aa; if (me) { diff --git a/bemani/format/afp/render.py b/bemani/format/afp/render.py index 9324f3b..d555701 100644 --- a/bemani/format/afp/render.py +++ b/bemani/format/afp/render.py @@ -397,11 +397,12 @@ MissingThis = object() class AFPRenderer(VerboseOutput): - def __init__(self, shapes: Dict[str, Shape] = {}, textures: Dict[str, Image.Image] = {}, swfs: Dict[str, SWF] = {}, single_threaded: bool = False) -> None: + def __init__(self, shapes: Dict[str, Shape] = {}, textures: Dict[str, Image.Image] = {}, swfs: Dict[str, SWF] = {}, single_threaded: bool = False, enable_aa: bool = True) -> None: super().__init__() # Options for rendering self.__single_threaded = single_threaded + self.__enable_aa = enable_aa # Library of shapes (draw instructions), textures (actual images) and swfs (us and other files for imports). self.shapes: Dict[str, Shape] = shapes @@ -958,6 +959,7 @@ class AFPRenderer(VerboseOutput): 257, mask.rectangle, single_threaded=self.__single_threaded, + enable_aa=False, ) # Composite it onto the current mask. @@ -970,6 +972,7 @@ class AFPRenderer(VerboseOutput): 256, calculated_mask, single_threaded=self.__single_threaded, + enable_aa=False, ) def __render_object( @@ -1041,11 +1044,13 @@ class AFPRenderer(VerboseOutput): print("WARNING: Unhandled UV coordinate color!") texture = None + enable_aa = False if params.flags & 0x2: # We need to look up the texture for this. if params.region not in self.textures: raise Exception(f"Cannot find texture reference {params.region}!") texture = self.textures[params.region] + enable_aa = self.__enable_aa if params.flags & 0x8: # TODO: This texture gets further blended somehow? Not sure this is ever used. @@ -1082,7 +1087,7 @@ class AFPRenderer(VerboseOutput): texture = shape.rectangle if texture is not None: - img = affine_composite(img, add_color, mult_color, transform, mask, blend, texture, single_threaded=self.__single_threaded) + img = affine_composite(img, add_color, mult_color, transform, mask, blend, texture, single_threaded=self.__single_threaded, enable_aa=enable_aa) elif isinstance(renderable, PlacedImage): if only_depths is not None and renderable.depth not in only_depths: # Not on the correct depth plane. @@ -1090,7 +1095,7 @@ class AFPRenderer(VerboseOutput): # This is a shape draw reference. texture = self.textures[renderable.source.reference] - img = affine_composite(img, add_color, mult_color, transform, mask, blend, texture, single_threaded=self.__single_threaded) + img = affine_composite(img, add_color, mult_color, transform, mask, blend, texture, single_threaded=self.__single_threaded, enable_aa=self.__enable_aa) elif isinstance(renderable, PlacedDummy): # Nothing to do! pass diff --git a/bemani/utils/afputils.py b/bemani/utils/afputils.py index 244f5cf..52b68cb 100644 --- a/bemani/utils/afputils.py +++ b/bemani/utils/afputils.py @@ -523,6 +523,7 @@ def render_path( output: str, *, disable_threads: bool = False, + disable_anti_aliasing: bool = False, background_color: Optional[str] = None, background_image: Optional[str] = None, force_aspect_ratio: Optional[str] = None, @@ -532,7 +533,7 @@ def render_path( only_frames: Optional[str] = None, verbose: bool = False, ) -> int: - renderer = AFPRenderer(single_threaded=disable_threads) + renderer = AFPRenderer(single_threaded=disable_threads, enable_aa=not disable_anti_aliasing) load_containers(renderer, containers, need_extras=True, verbose=verbose) # Verify the correct params. @@ -918,6 +919,11 @@ def main() -> int: action="store_true", help="Disable multi-threaded rendering.", ) + render_parser.add_argument( + "--disable-anti-aliasing", + action="store_true", + help="Disable anti-aliased rendering.", + ) render_parser.add_argument( "-v", "--verbose", @@ -982,6 +988,7 @@ def main() -> int: args.path, args.output, disable_threads=args.disable_threads, + disable_anti_aliasing=args.disable_anti_aliasing, background_color=args.background_color, background_image=args.background_image, force_aspect_ratio=args.force_aspect_ratio,