From 85606fece9c8532426bf1ce8d9ad2ff9bcebb95b Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Sun, 16 May 2021 00:20:23 +0000 Subject: [PATCH] Move affine transform function out into blend for slight cPython boost and also preparation for threading. --- bemani/format/afp/blend.py | 100 +++++++++++++++++++++++++++++++++++- bemani/format/afp/render.py | 72 ++------------------------ 2 files changed, 101 insertions(+), 71 deletions(-) diff --git a/bemani/format/afp/blend.py b/bemani/format/afp/blend.py index 0ae94d5..02f26db 100644 --- a/bemani/format/afp/blend.py +++ b/bemani/format/afp/blend.py @@ -1,6 +1,7 @@ -from typing import Tuple +from PIL import Image # type: ignore +from typing import List, Tuple -from .types.generic import Color +from .types.generic import Color, Matrix, Point def clamp(color: float) -> int: @@ -153,3 +154,98 @@ def blend_multiply( clamp(255 * ((dest[2] / 255.0) * (src[2] / 255.0))), clamp(255 * ((dest[3] / 255.0) * (src[3] / 255.0))), ) + + +def affine_composite( + img: Image.Image, + add_color: Tuple[int, int, int, int], + mult_color: Color, + transform: Matrix, + inverse: Matrix, + origin: Point, + blendfunc: int, + texture: Image.Image, +) -> List[Tuple[int, int, int, int]]: + # Get the data in an easier to manipulate and faster to update fashion. + imgmap = list(img.getdata()) + texmap = list(texture.getdata()) + + # Warn if we have an unsupported blend. + if blendfunc not in {0, 2, 3, 8, 9, 70}: + print(f"WARNING: Unsupported blend {blendfunc}") + + # These are calculated properties and caching them outside of the loop + # speeds things up a bit. + imgwidth = img.width + imgheight = img.height + texwidth = texture.width + texheight = texture.height + + # Calculate the maximum range of update this texture can possibly reside in. + pix1 = transform.multiply_point(Point.identity().subtract(origin)) + pix2 = transform.multiply_point(Point.identity().subtract(origin).add(Point(texwidth, 0))) + pix3 = transform.multiply_point(Point.identity().subtract(origin).add(Point(0, texheight))) + pix4 = transform.multiply_point(Point.identity().subtract(origin).add(Point(texwidth, texheight))) + + # Map this to the rectangle we need to sweep in the rendering image. + minx = max(int(min(pix1.x, pix2.x, pix3.x, pix4.x)), 0) + maxx = min(int(max(pix1.x, pix2.x, pix3.x, pix4.x)) + 1, imgwidth) + miny = max(int(min(pix1.y, pix2.y, pix3.y, pix4.y)), 0) + maxy = min(int(max(pix1.y, pix2.y, pix3.y, pix4.y)) + 1, imgheight) + + for imgy in range(miny, maxy): + for imgx in range(minx, maxx): + # Determine offset + imgoff = imgx + (imgy * imgwidth) + + # Blit this pixel. + imgmap[imgoff] = affine_blend_point(imgx, imgy, imgwidth, imgheight, add_color, mult_color, imgmap[imgoff], inverse, origin, blendfunc, texwidth, texheight, texmap) + + return imgmap + +def affine_blend_point( + imgx: int, + imgy: int, + imgwidth: int, + imgheight: int, + add_color: Tuple[int, int, int, int], + mult_color: Color, + dest_color: Tuple[int, int, int, int], + inverse: Matrix, + origin: Point, + blendfunc: int, + texwidth: int, + texheight: int, + texmap: List[Tuple[int, int, int, int]], +) -> Tuple[int, int, int, int]: + # Calculate what texture pixel data goes here. + texloc = inverse.multiply_point(Point(float(imgx), float(imgy))).add(origin) + 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: + return dest_color + + # Blend it. + texoff = texx + (texy * texwidth) + + if blendfunc == 3: + return blend_multiply(dest_color, texmap[texoff], mult_color, add_color) + # TODO: blend mode 4, which is "screen" blending according to SWF references. I've only seen this + # in Jubeat and it implements it using OpenGL equation Src * (1 - Dst) + Dst * 1. + # TODO: blend mode 5, which is "lighten" blending according to SWF references. Jubeat does not + # premultiply by alpha, but the GL/DX equation is max(Src * As, Dst * 1). + # TODO: blend mode 6, which is "darken" blending according to SWF references. Jubeat does not + # premultiply by alpha, but the GL/DX equation is min(Src * As, Dst * 1). + # TODO: blend mode 10, which is "invert" according to SWF references. The only game I could find + # that implemented this had equation Src * (1 - Dst) + Dst * (1 - As). + # TODO: blend mode 13, which is "overlay" according to SWF references. The equation seems to be + # Src * Dst + Dst * Src but Jubeat thinks it should be Src * Dst + Dst * (1 - As). + elif blendfunc == 8: + return blend_addition(dest_color, texmap[texoff], mult_color, add_color) + elif blendfunc == 9 or blendfunc == 70: + return blend_subtraction(dest_color, texmap[texoff], mult_color, add_color) + # TODO: blend mode 75, which is not in the SWF spec and appears to have the equation + # Src * (1 - Dst) + Dst * (1 - Src). + else: + return blend_normal(dest_color, texmap[texoff], mult_color, add_color) diff --git a/bemani/format/afp/render.py b/bemani/format/afp/render.py index 78b8cd7..05d54dc 100644 --- a/bemani/format/afp/render.py +++ b/bemani/format/afp/render.py @@ -1,7 +1,7 @@ from typing import Dict, List, Tuple, Optional, Union from PIL import Image # type: ignore -from .blend import blend_normal, blend_addition, blend_subtraction, blend_multiply +from .blend import affine_composite from .swf import SWF, Frame, Tag, AP2ShapeTag, AP2DefineSpriteTag, AP2PlaceObjectTag, AP2RemoveObjectTag, AP2DoActionTag, AP2DefineFontTag, AP2DefineEditTextTag from .types import Color, Matrix, Point from .geo import Shape, DrawParams @@ -448,74 +448,8 @@ class AFPRenderer(VerboseOutput): img.alpha_composite(texture, cutin.as_tuple(), cutoff.as_tuple()) else: - # Now, render out the texture. - imgmap = list(img.getdata()) - texmap = list(texture.getdata()) - - # These are calculated properties and caching them outside of the loop - # speeds things up a bit. - imgwidth = img.width - imgheight = img.height - texwidth = texture.width - texheight = texture.height - - # Calculate the maximum range of update this texture can possibly reside in. - pix1 = transform.multiply_point(Point.identity().subtract(origin)) - pix2 = transform.multiply_point(Point.identity().subtract(origin).add(Point(texwidth, 0))) - pix3 = transform.multiply_point(Point.identity().subtract(origin).add(Point(0, texheight))) - pix4 = transform.multiply_point(Point.identity().subtract(origin).add(Point(texwidth, texheight))) - - # Map this to the rectangle we need to sweep in the rendering image. - minx = max(int(min(pix1.x, pix2.x, pix3.x, pix4.x)), 0) - maxx = min(int(max(pix1.x, pix2.x, pix3.x, pix4.x)) + 1, imgwidth) - miny = max(int(min(pix1.y, pix2.y, pix3.y, pix4.y)), 0) - maxy = min(int(max(pix1.y, pix2.y, pix3.y, pix4.y)) + 1, imgheight) - - announced = False - for imgy in range(miny, maxy): - 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), float(imgy))).add(origin) - 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 blend == 0 or blend == 2: - imgmap[imgoff] = blend_normal(imgmap[imgoff], texmap[texoff], mult_color, add_color) - elif blend == 3: - imgmap[imgoff] = blend_multiply(imgmap[imgoff], texmap[texoff], mult_color, add_color) - # TODO: blend mode 4, which is "screen" blending according to SWF references. I've only seen this - # in Jubeat and it implements it using OpenGL equation Src * (1 - Dst) + Dst * 1. - # TODO: blend mode 5, which is "lighten" blending according to SWF references. Jubeat does not - # premultiply by alpha, but the GL/DX equation is max(Src * As, Dst * 1). - # TODO: blend mode 6, which is "darken" blending according to SWF references. Jubeat does not - # premultiply by alpha, but the GL/DX equation is min(Src * As, Dst * 1). - # TODO: blend mode 10, which is "invert" according to SWF references. The only game I could find - # that implemented this had equation Src * (1 - Dst) + Dst * (1 - As). - # TODO: blend mode 13, which is "overlay" according to SWF references. The equation seems to be - # Src * Dst + Dst * Src but Jubeat thinks it should be Src * Dst + Dst * (1 - As). - elif blend == 8: - imgmap[imgoff] = blend_addition(imgmap[imgoff], texmap[texoff], mult_color, add_color) - elif blend == 9 or blend == 70: - imgmap[imgoff] = blend_subtraction(imgmap[imgoff], texmap[texoff], mult_color, add_color) - # TODO: blend mode 75, which is not in the SWF spec and appears to have the equation - # Src * (1 - Dst) + Dst * (1 - Src). - else: - if not announced: - # Don't print it for every pixel. - print(f"WARNING: Unsupported blend {blend}") - announced = True - imgmap[imgoff] = blend_normal(imgmap[imgoff], texmap[texoff], mult_color, add_color) - - img.putdata(imgmap) + # We can't, so do the slow render that's correct. + img.putdata(affine_composite(img, add_color, mult_color, transform, inverse, origin, blend, texture)) else: raise Exception(f"Unknown placed object type to render {renderable}!")