mirror of
https://github.com/DragonMinded/bemaniutils.git
synced 2026-07-26 13:03:17 -05:00
Switch to 4x4 matrix and 3D point for all transform operations. Should behave identically to before.
This commit is contained in:
parent
f0792067b7
commit
debc7b3aac
|
|
@ -1,4 +1,3 @@
|
|||
import math
|
||||
import multiprocessing
|
||||
import signal
|
||||
from PIL import Image # type: ignore
|
||||
|
|
@ -432,8 +431,8 @@ 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.
|
||||
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)
|
||||
xscale = 1.0 / inverse.xscale
|
||||
yscale = 1.0 / inverse.yscale
|
||||
|
||||
# These are used for picking the various sample points for SSAA method below.
|
||||
xswing = 0.5 * max(1.0, xscale)
|
||||
|
|
@ -446,7 +445,7 @@ def pixel_renderer(
|
|||
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()
|
||||
aax, aay, _ = aaloc.as_tuple()
|
||||
if not (aax <= 0 or aay <= 0 or aax >= (texwidth - 1) or aay >= (texheight - 1)):
|
||||
bilinear = True
|
||||
|
||||
|
|
@ -454,7 +453,7 @@ def pixel_renderer(
|
|||
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()
|
||||
aax, aay, _ = texloc.as_tuple()
|
||||
aaxrem = texloc.x - aax
|
||||
aayrem = texloc.y - aay
|
||||
|
||||
|
|
@ -498,7 +497,7 @@ def pixel_renderer(
|
|||
for addy in ypoints:
|
||||
for addx in xpoints:
|
||||
texloc = inverse.multiply_point(Point(imgx + addx, imgy + addy))
|
||||
aax, aay = texloc.as_tuple()
|
||||
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.
|
||||
|
|
@ -541,7 +540,7 @@ def pixel_renderer(
|
|||
else:
|
||||
# Calculate what texture pixel data goes here.
|
||||
texloc = inverse.multiply_point(Point(imgx + 0.5, imgy + 0.5))
|
||||
texx, texy = texloc.as_tuple()
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from PIL import Image # type: ignore
|
||||
from typing import Optional, Tuple
|
||||
from typing import Optional
|
||||
|
||||
from ..types import Color, Matrix
|
||||
|
||||
from ..types import Color, Matrix, Point
|
||||
|
||||
def affine_composite(
|
||||
img: Image.Image,
|
||||
|
|
@ -11,7 +12,7 @@ def affine_composite(
|
|||
mask: Optional[Image.Image],
|
||||
blendfunc: int,
|
||||
texture: Image.Image,
|
||||
single_threaded: bool = False,
|
||||
enable_aa: bool = True,
|
||||
single_threaded: bool = ...,
|
||||
enable_aa: bool = ...,
|
||||
) -> Image.Image:
|
||||
...
|
||||
|
|
|
|||
|
|
@ -11,16 +11,18 @@ cdef extern struct floatcolor_t:
|
|||
float a;
|
||||
|
||||
cdef extern struct matrix_t:
|
||||
float a;
|
||||
float b;
|
||||
float c;
|
||||
float d;
|
||||
float tx;
|
||||
float ty;
|
||||
|
||||
cdef extern struct point_t:
|
||||
float x;
|
||||
float y;
|
||||
float a11;
|
||||
float a12;
|
||||
float a13;
|
||||
float a21;
|
||||
float a22;
|
||||
float a23;
|
||||
float a31;
|
||||
float a32;
|
||||
float a33;
|
||||
float a41;
|
||||
float a42;
|
||||
float a43;
|
||||
|
||||
cdef extern int affine_composite_fast(
|
||||
unsigned char *imgdata,
|
||||
|
|
@ -106,7 +108,12 @@ def affine_composite(
|
|||
# Convert classes to C structs.
|
||||
cdef floatcolor_t c_addcolor = floatcolor_t(r=add_color.r, g=add_color.g, b=add_color.b, a=add_color.a)
|
||||
cdef floatcolor_t c_multcolor = floatcolor_t(r=mult_color.r, g=mult_color.g, b=mult_color.b, a=mult_color.a)
|
||||
cdef matrix_t c_inverse = matrix_t(a=inverse.a, b=inverse.b, c=inverse.c, d=inverse.d, tx=inverse.tx, ty=inverse.ty)
|
||||
cdef matrix_t c_inverse = matrix_t(
|
||||
a11=inverse.a11, a12=inverse.a12, a13=inverse.a13,
|
||||
a21=inverse.a21, a22=inverse.a22, a23=inverse.a23,
|
||||
a31=inverse.a31, a32=inverse.a32, a33=inverse.a33,
|
||||
a41=inverse.a41, a42=inverse.a42, a43=inverse.a43,
|
||||
)
|
||||
cdef unsigned int threads = 1 if single_threaded else multiprocessing.cpu_count()
|
||||
|
||||
# Call the C++ function.
|
||||
|
|
|
|||
|
|
@ -24,29 +24,46 @@ extern "C"
|
|||
typedef struct point {
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
|
||||
struct point add(struct point other) {
|
||||
return (struct point){
|
||||
x + other.x,
|
||||
y + other.y,
|
||||
z + other.z,
|
||||
};
|
||||
};
|
||||
} point_t;
|
||||
|
||||
typedef struct matrix {
|
||||
float a;
|
||||
float b;
|
||||
float c;
|
||||
float d;
|
||||
float tx;
|
||||
float ty;
|
||||
float a11;
|
||||
float a12;
|
||||
float a13;
|
||||
float a21;
|
||||
float a22;
|
||||
float a23;
|
||||
float a31;
|
||||
float a32;
|
||||
float a33;
|
||||
float a41;
|
||||
float a42;
|
||||
float a43;
|
||||
|
||||
point_t multiply_point(point_t point) {
|
||||
return (point_t){
|
||||
(a * point.x) + (c * point.y) + tx,
|
||||
(b * point.x) + (d * point.y) + ty,
|
||||
(a11 * point.x) + (a21 * point.y) + (a31 * point.z) + a41,
|
||||
(a12 * point.x) + (a22 * point.y) + (a32 * point.z) + a42,
|
||||
(a13 * point.x) + (a23 * point.y) + (a33 * point.z) + a43,
|
||||
};
|
||||
}
|
||||
|
||||
float xscale() {
|
||||
return sqrt((a11 * a11) + (a12 * a12) + (a13 * a13));
|
||||
}
|
||||
|
||||
float yscale() {
|
||||
return sqrt((a21 * a21) + (a22 * a22) + (a23 * a23));
|
||||
}
|
||||
} matrix_t;
|
||||
|
||||
typedef struct work {
|
||||
|
|
@ -253,8 +270,8 @@ 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 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);
|
||||
float xscale = 1.0 / work->inverse.xscale();
|
||||
float yscale = 1.0 / work->inverse.yscale();
|
||||
|
||||
// These are used for picking the various sample points for SSAA method below.
|
||||
float xswing = 0.5 * fmax(1.0, xscale);
|
||||
|
|
|
|||
|
|
@ -96,10 +96,10 @@ class Mask:
|
|||
|
||||
class PlacedObject:
|
||||
# An object that occupies the screen at some depth.
|
||||
def __init__(self, object_id: int, depth: int, rotation_offset: Point, transform: Matrix, mult_color: Color, add_color: Color, blend: int, mask: Optional[Mask]) -> None:
|
||||
def __init__(self, object_id: int, depth: int, rotation_origin: Point, transform: Matrix, mult_color: Color, add_color: Color, blend: int, mask: Optional[Mask]) -> None:
|
||||
self.__object_id = object_id
|
||||
self.__depth = depth
|
||||
self.rotation_offset = rotation_offset
|
||||
self.rotation_origin = rotation_origin
|
||||
self.transform = transform
|
||||
self.mult_color = mult_color
|
||||
self.add_color = add_color
|
||||
|
|
@ -129,7 +129,7 @@ class PlacedShape(PlacedObject):
|
|||
self,
|
||||
object_id: int,
|
||||
depth: int,
|
||||
rotation_offset: Point,
|
||||
rotation_origin: Point,
|
||||
transform: Matrix,
|
||||
mult_color: Color,
|
||||
add_color: Color,
|
||||
|
|
@ -137,7 +137,7 @@ class PlacedShape(PlacedObject):
|
|||
mask: Optional[Mask],
|
||||
source: RegisteredShape,
|
||||
) -> None:
|
||||
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend, mask)
|
||||
super().__init__(object_id, depth, rotation_origin, transform, mult_color, add_color, blend, mask)
|
||||
self.__source = source
|
||||
|
||||
@property
|
||||
|
|
@ -155,7 +155,7 @@ class PlacedClip(PlacedObject):
|
|||
self,
|
||||
object_id: int,
|
||||
depth: int,
|
||||
rotation_offset: Point,
|
||||
rotation_origin: Point,
|
||||
transform: Matrix,
|
||||
mult_color: Color,
|
||||
add_color: Color,
|
||||
|
|
@ -163,7 +163,7 @@ class PlacedClip(PlacedObject):
|
|||
mask: Optional[Mask],
|
||||
source: RegisteredClip,
|
||||
) -> None:
|
||||
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend, mask)
|
||||
super().__init__(object_id, depth, rotation_origin, transform, mult_color, add_color, blend, mask)
|
||||
self.placed_objects: List[PlacedObject] = []
|
||||
self.frame: int = 0
|
||||
self.unplayed_tags: List[int] = [i for i in range(len(source.tags))]
|
||||
|
|
@ -254,7 +254,7 @@ class PlacedImage(PlacedObject):
|
|||
self,
|
||||
object_id: int,
|
||||
depth: int,
|
||||
rotation_offset: Point,
|
||||
rotation_origin: Point,
|
||||
transform: Matrix,
|
||||
mult_color: Color,
|
||||
add_color: Color,
|
||||
|
|
@ -262,7 +262,7 @@ class PlacedImage(PlacedObject):
|
|||
mask: Optional[Mask],
|
||||
source: RegisteredImage,
|
||||
) -> None:
|
||||
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend, mask)
|
||||
super().__init__(object_id, depth, rotation_origin, transform, mult_color, add_color, blend, mask)
|
||||
self.__source = source
|
||||
|
||||
@property
|
||||
|
|
@ -279,7 +279,7 @@ class PlacedDummy(PlacedObject):
|
|||
self,
|
||||
object_id: int,
|
||||
depth: int,
|
||||
rotation_offset: Point,
|
||||
rotation_origin: Point,
|
||||
transform: Matrix,
|
||||
mult_color: Color,
|
||||
add_color: Color,
|
||||
|
|
@ -287,7 +287,7 @@ class PlacedDummy(PlacedObject):
|
|||
mask: Optional[Mask],
|
||||
source: RegisteredDummy,
|
||||
) -> None:
|
||||
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend, mask)
|
||||
super().__init__(object_id, depth, rotation_origin, transform, mult_color, add_color, blend, mask)
|
||||
self.__source = source
|
||||
|
||||
@property
|
||||
|
|
@ -721,7 +721,7 @@ class AFPRenderer(VerboseOutput):
|
|||
new_mult_color = tag.mult_color or obj.mult_color
|
||||
new_add_color = tag.add_color or obj.add_color
|
||||
new_transform = tag.transform or obj.transform
|
||||
new_rotation_offset = tag.rotation_offset or obj.rotation_offset
|
||||
new_rotation_origin = tag.rotation_origin or obj.rotation_origin
|
||||
new_blend = tag.blend or obj.blend
|
||||
|
||||
if tag.source_tag_id is not None and tag.source_tag_id != obj.source.tag_id:
|
||||
|
|
@ -733,7 +733,7 @@ class AFPRenderer(VerboseOutput):
|
|||
operating_clip.placed_objects[i] = PlacedShape(
|
||||
obj.object_id,
|
||||
obj.depth,
|
||||
new_rotation_offset,
|
||||
new_rotation_origin,
|
||||
new_transform,
|
||||
new_mult_color,
|
||||
new_add_color,
|
||||
|
|
@ -748,7 +748,7 @@ class AFPRenderer(VerboseOutput):
|
|||
operating_clip.placed_objects[i] = PlacedImage(
|
||||
obj.object_id,
|
||||
obj.depth,
|
||||
new_rotation_offset,
|
||||
new_rotation_origin,
|
||||
new_transform,
|
||||
new_mult_color,
|
||||
new_add_color,
|
||||
|
|
@ -763,7 +763,7 @@ class AFPRenderer(VerboseOutput):
|
|||
new_clip = PlacedClip(
|
||||
tag.object_id,
|
||||
tag.depth,
|
||||
new_rotation_offset,
|
||||
new_rotation_origin,
|
||||
new_transform,
|
||||
new_mult_color,
|
||||
new_add_color,
|
||||
|
|
@ -779,7 +779,7 @@ class AFPRenderer(VerboseOutput):
|
|||
operating_clip.placed_objects[i] = PlacedDummy(
|
||||
obj.object_id,
|
||||
obj.depth,
|
||||
new_rotation_offset,
|
||||
new_rotation_origin,
|
||||
new_transform,
|
||||
new_mult_color,
|
||||
new_add_color,
|
||||
|
|
@ -798,7 +798,7 @@ class AFPRenderer(VerboseOutput):
|
|||
obj.mult_color = new_mult_color
|
||||
obj.add_color = new_add_color
|
||||
obj.transform = new_transform
|
||||
obj.rotation_offset = new_rotation_offset
|
||||
obj.rotation_origin = new_rotation_origin
|
||||
obj.blend = new_blend
|
||||
return None, True
|
||||
|
||||
|
|
@ -818,7 +818,7 @@ class AFPRenderer(VerboseOutput):
|
|||
PlacedShape(
|
||||
tag.object_id,
|
||||
tag.depth,
|
||||
tag.rotation_offset or Point.identity(),
|
||||
tag.rotation_origin or Point.identity(),
|
||||
tag.transform or Matrix.identity(),
|
||||
tag.mult_color or Color(1.0, 1.0, 1.0, 1.0),
|
||||
tag.add_color or Color(0.0, 0.0, 0.0, 0.0),
|
||||
|
|
@ -835,7 +835,7 @@ class AFPRenderer(VerboseOutput):
|
|||
PlacedImage(
|
||||
tag.object_id,
|
||||
tag.depth,
|
||||
tag.rotation_offset or Point.identity(),
|
||||
tag.rotation_origin or Point.identity(),
|
||||
tag.transform or Matrix.identity(),
|
||||
tag.mult_color or Color(1.0, 1.0, 1.0, 1.0),
|
||||
tag.add_color or Color(0.0, 0.0, 0.0, 0.0),
|
||||
|
|
@ -851,7 +851,7 @@ class AFPRenderer(VerboseOutput):
|
|||
placed_clip = PlacedClip(
|
||||
tag.object_id,
|
||||
tag.depth,
|
||||
tag.rotation_offset or Point.identity(),
|
||||
tag.rotation_origin or Point.identity(),
|
||||
tag.transform or Matrix.identity(),
|
||||
tag.mult_color or Color(1.0, 1.0, 1.0, 1.0),
|
||||
tag.add_color or Color(0.0, 0.0, 0.0, 0.0),
|
||||
|
|
@ -875,7 +875,7 @@ class AFPRenderer(VerboseOutput):
|
|||
PlacedDummy(
|
||||
tag.object_id,
|
||||
tag.depth,
|
||||
tag.rotation_offset or Point.identity(),
|
||||
tag.rotation_origin or Point.identity(),
|
||||
tag.transform or Matrix.identity(),
|
||||
tag.mult_color or Color(1.0, 1.0, 1.0, 1.0),
|
||||
tag.add_color or Color(0.0, 0.0, 0.0, 0.0),
|
||||
|
|
@ -1012,7 +1012,7 @@ class AFPRenderer(VerboseOutput):
|
|||
self.vprint(f"{prefix} Rendering placed object ID {renderable.object_id} from sprite {renderable.source.tag_id} onto Depth {renderable.depth}")
|
||||
|
||||
# Compute the affine transformation matrix for this object.
|
||||
transform = renderable.transform.multiply(parent_transform).translate(Point.identity().subtract(renderable.rotation_offset))
|
||||
transform = renderable.transform.multiply(parent_transform).translate(Point.identity().subtract(renderable.rotation_origin))
|
||||
|
||||
# Calculate blending and blend color if it is present.
|
||||
mult_color = (renderable.mult_color or Color(1.0, 1.0, 1.0, 1.0)).multiply(parent_mult_color)
|
||||
|
|
@ -1331,7 +1331,7 @@ class AFPRenderer(VerboseOutput):
|
|||
frameno: int = 0
|
||||
|
||||
# Calculate actual size based on given movie transform.
|
||||
actual_size = movie_transform.multiply_point(Point(swf.location.width, swf.location.height)).as_tuple()
|
||||
resized_width, resized_height, _ = movie_transform.multiply_point(Point(swf.location.width, swf.location.height)).as_tuple()
|
||||
|
||||
# TODO: If the location top/left is nonzero, we need move the root transform
|
||||
# so that the correct viewport is rendered.
|
||||
|
|
@ -1360,7 +1360,7 @@ class AFPRenderer(VerboseOutput):
|
|||
# Stretch the image to make sure it fits the entire frame.
|
||||
imgwidth = float(background_image.width)
|
||||
imgheight = float(background_image.height)
|
||||
background_matrix = Matrix(
|
||||
background_matrix = Matrix.affine(
|
||||
a=swf.location.width / imgwidth,
|
||||
b=0,
|
||||
c=0,
|
||||
|
|
@ -1388,10 +1388,10 @@ class AFPRenderer(VerboseOutput):
|
|||
-1,
|
||||
# The coordinates of the rectangle of the shape in screen space.
|
||||
[
|
||||
Point(0.0, 0.0),
|
||||
Point(imgwidth, 0.0),
|
||||
Point(0, 0),
|
||||
Point(imgwidth, 0),
|
||||
Point(imgwidth, imgheight),
|
||||
Point(0.0, imgheight),
|
||||
Point(0, imgheight),
|
||||
],
|
||||
# The coordinates of the original texture in UV space (we don't use this).
|
||||
[
|
||||
|
|
@ -1419,7 +1419,7 @@ class AFPRenderer(VerboseOutput):
|
|||
)
|
||||
|
||||
# Create the root mask for where to draw the root clip.
|
||||
movie_mask = Image.new("RGBA", actual_size, color=(255, 0, 0, 255))
|
||||
movie_mask = Image.new("RGBA", (resized_width, resized_height), color=(255, 0, 0, 255))
|
||||
|
||||
# These could possibly be overwritten from an external source of we wanted.
|
||||
actual_mult_color = Color(1.0, 1.0, 1.0, 1.0)
|
||||
|
|
@ -1452,7 +1452,7 @@ class AFPRenderer(VerboseOutput):
|
|||
if changed or last_rendered_frame is None:
|
||||
# Now, render out the placed objects.
|
||||
color = swf.color or Color(0.0, 0.0, 0.0, 0.0)
|
||||
curimage = Image.new("RGBA", actual_size, color=color.as_tuple())
|
||||
curimage = Image.new("RGBA", (resized_width, resized_height), color=color.as_tuple())
|
||||
curimage = self.__render_object(curimage, root_clip, movie_transform, movie_mask, actual_mult_color, actual_add_color, actual_blend, only_depths=only_depths)
|
||||
else:
|
||||
# Nothing changed, make a copy of the previous render.
|
||||
|
|
|
|||
|
|
@ -230,14 +230,21 @@ class AP2DefineButtonTag(Tag):
|
|||
|
||||
|
||||
class AP2PlaceCameraTag(Tag):
|
||||
def __init__(self) -> None:
|
||||
# TODO: I need to figure out what camera placements actually DO, and take the
|
||||
# values that I parsed out store them here...
|
||||
def __init__(self, camera_id: int, center: Optional[Point], focal_length: float) -> None:
|
||||
super().__init__(None)
|
||||
|
||||
# This is not actually Tag ID, just a way to refer to the camera. Confusing, I know.
|
||||
# Probably this happened when they hacked 3D into the format.
|
||||
self.camera_id = camera_id
|
||||
self.center = center
|
||||
self.focal_length = focal_length
|
||||
|
||||
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||
return {
|
||||
**super().as_dict(*args, **kwargs),
|
||||
"camera_id": self.camera_id,
|
||||
'center': self.center.as_dict(*args, **kwargs) if self.center is not None else None,
|
||||
"focal_length": self.focal_length,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -283,7 +290,7 @@ class AP2PlaceObjectTag(Tag):
|
|||
blend: Optional[int],
|
||||
update: bool,
|
||||
transform: Optional[Matrix],
|
||||
rotation_offset: Optional[Point],
|
||||
rotation_origin: Optional[Point],
|
||||
mult_color: Optional[Color],
|
||||
add_color: Optional[Color],
|
||||
triggers: Dict[int, List[ByteCode]],
|
||||
|
|
@ -315,7 +322,7 @@ class AP2PlaceObjectTag(Tag):
|
|||
|
||||
# Whether there is a transform matrix to apply before placing/updating this object or not.
|
||||
self.transform = transform
|
||||
self.rotation_offset = rotation_offset
|
||||
self.rotation_origin = rotation_origin
|
||||
|
||||
# If there is a color to blend with the sprite/shape when drawing.
|
||||
self.mult_color = mult_color
|
||||
|
|
@ -338,7 +345,7 @@ class AP2PlaceObjectTag(Tag):
|
|||
'blend': self.blend,
|
||||
'update': self.update,
|
||||
'transform': self.transform.as_dict(*args, **kwargs) if self.transform is not None else None,
|
||||
'rotation_offset': self.rotation_offset.as_dict(*args, **kwargs) if self.rotation_offset is not None else None,
|
||||
'rotation_origin': self.rotation_origin.as_dict(*args, **kwargs) if self.rotation_origin is not None else None,
|
||||
'mult_color': self.mult_color.as_dict(*args, **kwargs) if self.mult_color is not None else None,
|
||||
'add_color': self.add_color.as_dict(*args, **kwargs) if self.add_color is not None else None,
|
||||
'triggers': {i: [b.as_dict(*args, **kwargs) for b in t] for (i, t) in self.triggers.items()}
|
||||
|
|
@ -1134,6 +1141,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
|
||||
# Handle transformation matrix.
|
||||
transform = Matrix.identity()
|
||||
transform_set = False
|
||||
|
||||
if flags & 0x100:
|
||||
# Has scale component.
|
||||
|
|
@ -1144,6 +1152,8 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
|
||||
transform.a = float(a_int) / 1024.0
|
||||
transform.d = float(d_int) / 1024.0
|
||||
transform_set = True
|
||||
|
||||
self.vprint(f"{prefix} Transform Matrix A: {transform.a}, D: {transform.d}")
|
||||
|
||||
if flags & 0x200:
|
||||
|
|
@ -1155,6 +1165,8 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
|
||||
transform.b = float(b_int) / 1024.0
|
||||
transform.c = float(c_int) / 1024.0
|
||||
transform_set = True
|
||||
|
||||
self.vprint(f"{prefix} Transform Matrix B: {transform.b}, C: {transform.c}")
|
||||
|
||||
if flags & 0x400:
|
||||
|
|
@ -1166,6 +1178,8 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
|
||||
transform.tx = float(tx_int) / 20.0
|
||||
transform.ty = float(ty_int) / 20.0
|
||||
transform_set = True
|
||||
|
||||
self.vprint(f"{prefix} Transform Matrix TX: {transform.tx}, TY: {transform.ty}")
|
||||
|
||||
# Handle object colors
|
||||
|
|
@ -1325,7 +1339,9 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
# I don't know however as I've not encountered data with this bit.
|
||||
self.vprint(f"{prefix} Unknown Filter data Count: {count}, Size: {filter_size}")
|
||||
|
||||
rotation_offset = None
|
||||
rotation_origin = Point(0.0, 0.0, 0.0)
|
||||
rotation_origin_set = False
|
||||
|
||||
if flags & 0x1000000:
|
||||
# I am certain that this is the rotation origin, as treating it as such works for
|
||||
# basically all files.
|
||||
|
|
@ -1334,26 +1350,34 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
self.add_coverage(dataoffset + running_pointer, 8)
|
||||
running_pointer += 8
|
||||
|
||||
rotation_offset = Point(float(x) / 20.0, float(y) / 20.0)
|
||||
self.vprint(f"{prefix} Rotation Origin: {rotation_offset}")
|
||||
rotation_origin.x = float(x) / 20.0
|
||||
rotation_origin.y = float(y) / 20.0
|
||||
rotation_origin_set = True
|
||||
|
||||
self.vprint(f"{prefix} Rotation XY Origin: {rotation_origin.x}, {rotation_origin.y}")
|
||||
|
||||
if flags & 0x200000000:
|
||||
# TODO: This might be z rotation origin? I've only seen it on files that have a place
|
||||
# camera, and its setting a local value that is close to the rotation origin
|
||||
# x and y constants.
|
||||
# This is Z rotation origin.
|
||||
unhandled_flags &= ~0x200000000
|
||||
z_int = struct.unpack("<i", datachunk[running_pointer:(running_pointer + 4)])[0]
|
||||
self.add_coverage(dataoffset + running_pointer, 4)
|
||||
running_pointer += 4
|
||||
|
||||
z = float(z_int) / 20.0
|
||||
self.vprint(f"{prefix} Unknown Rotation Origin Float: {z}")
|
||||
rotation_origin.z = float(z_int) / 20.0
|
||||
rotation_origin_set = True
|
||||
|
||||
self.vprint(f"{prefix} Rotation Z Origin: {rotation_origin.z}")
|
||||
|
||||
if flags & 0x2000000:
|
||||
# Same as above, but initializing to 0, 0 instead of from data.
|
||||
# Same as above, but initializing to 0, 0, 0 instead of from data.
|
||||
unhandled_flags &= ~0x2000000
|
||||
rotation_offset = Point(0.0, 0.0)
|
||||
self.vprint(f"{prefix} Rotation Origin: {rotation_offset}")
|
||||
|
||||
rotation_origin.x = 0.0
|
||||
rotation_origin.y = 0.0
|
||||
rotation_origin.z = 0.0
|
||||
rotation_origin_set = True
|
||||
|
||||
self.vprint(f"{prefix} Rotation XYZ Origin: {rotation_origin.x}, {rotation_origin.y}, {rotation_origin.z}")
|
||||
|
||||
if flags & 0x40000:
|
||||
# This appears in newer IIDX to be an alternative method for populating
|
||||
|
|
@ -1374,6 +1398,8 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
|
||||
transform.a = float(a_int) / 32768.0
|
||||
transform.d = float(d_int) / 32768.0
|
||||
transform_set = True
|
||||
|
||||
self.vprint(f"{prefix} Transform Matrix A: {transform.a}, D: {transform.d}")
|
||||
|
||||
if flags & 0x80000:
|
||||
|
|
@ -1386,6 +1412,8 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
|
||||
transform.b = float(b_int) / 32768.0
|
||||
transform.c = float(c_int) / 32768.0
|
||||
transform_set = True
|
||||
|
||||
self.vprint(f"{prefix} Transform Matrix B: {transform.b}, C: {transform.c}")
|
||||
|
||||
if flags & 0x100000:
|
||||
|
|
@ -1405,34 +1433,43 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
running_pointer += catchup
|
||||
|
||||
if flags & 0x8000000:
|
||||
# TODO: Some unknown float. This might be the "tz" value for a 3D matrix.
|
||||
# This is the translation offset "z" for a 3D transform matrix.
|
||||
unhandled_flags &= ~0x8000000
|
||||
unk_5 = struct.unpack("<i", datachunk[running_pointer:(running_pointer + 4)])[0]
|
||||
tz_int = struct.unpack("<i", datachunk[running_pointer:(running_pointer + 4)])[0]
|
||||
self.add_coverage(dataoffset + running_pointer, 4)
|
||||
running_pointer += 4
|
||||
|
||||
unk_5_f = unk_5 / 20.0
|
||||
transform.tz = tz_int / 20.0
|
||||
transform_set = True
|
||||
|
||||
self.vprint(f"{prefix} Unk 5: {unk_5_f}")
|
||||
self.vprint(f"{prefix} Translate Z offset: {transform.tz}")
|
||||
|
||||
if flags & 0x10000000:
|
||||
# TODO: Absolutely no idea, the games that use this reuse the transform
|
||||
# matrix variables but then don't put them in the same spot, so
|
||||
# this might be a 3D transform matrix? Would make sense given the
|
||||
# unknown float above as well as the unknown rotation origin above
|
||||
# as well as the newly-discovered AP2_PLACE_CAMERA tag.
|
||||
# This is a 3x3 grid of initializers for a 3D transform matrix. It appears that
|
||||
# files also include the A/D and B/C pairs that match the correct locations in
|
||||
# previous transform parsing sections, possibly for backwards compatibility?
|
||||
unhandled_flags &= ~0x10000000
|
||||
ints = struct.unpack("<iiiiiiiii", datachunk[running_pointer:(running_pointer + 36)])
|
||||
self.add_coverage(dataoffset + running_pointer, 36)
|
||||
running_pointer += 36
|
||||
|
||||
floats = [float(i) / 1024.0 for i in ints]
|
||||
floats = [x / 1024.0 for x in ints]
|
||||
if False:
|
||||
# TODO: Start actually doing perspective projections when needed.
|
||||
transform.a11 = floats[0]
|
||||
transform.a12 = floats[1]
|
||||
transform.a13 = floats[2]
|
||||
transform.a21 = floats[3]
|
||||
transform.a22 = floats[4]
|
||||
transform.a23 = floats[5]
|
||||
transform.a31 = floats[6]
|
||||
transform.a32 = floats[7]
|
||||
transform.a33 = floats[8]
|
||||
|
||||
self.vprint(f"{prefix} Unknown 3D Transform Matrix: {', '.join(str(f) for f in floats)}")
|
||||
self.vprint(f"{prefix} 3D Transform Matrix: {', '.join(str(f) for f in floats)}")
|
||||
|
||||
if flags & 0x20000000:
|
||||
# TODO: Again, absolutely no idea, gets passed into a function and I
|
||||
# don't see how its used.
|
||||
# TODO: Again, absolutely no idea, gets passed into a function and I don't see how its used.
|
||||
unhandled_flags &= ~0x20000000
|
||||
unk_a, unk_b, unk_c = struct.unpack("<hbb", datachunk[running_pointer:(running_pointer + 4)])
|
||||
self.add_coverage(dataoffset + running_pointer, 4)
|
||||
|
|
@ -1524,9 +1561,19 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
else:
|
||||
self.vprint(f"{prefix} Ignore color information")
|
||||
if flags & 0x4000000:
|
||||
self.vprint(f"{prefix} Use 3D transform matrix?")
|
||||
self.vprint(f"{prefix} Use 3D transform system")
|
||||
else:
|
||||
self.vprint(f"{prefix} Ignore 3D transform matrix?")
|
||||
self.vprint(f"{prefix} Use 2D transform system")
|
||||
|
||||
# Unset any previously set 3D transforms. Files shouldn't include both 3D
|
||||
# transforms AND the old 2D transform flag, but let's respect that bit.
|
||||
rotation_origin.z = 0.0
|
||||
transform.a13 = 0.0
|
||||
transform.a23 = 0.0
|
||||
transform.a31 = 0.0
|
||||
transform.a32 = 0.0
|
||||
transform.a33 = 1.0
|
||||
transform.a43 = 0.0
|
||||
|
||||
if unhandled_flags != 0:
|
||||
raise Exception(f"Did not handle {hex(unhandled_flags)} flag bits!")
|
||||
|
|
@ -1543,8 +1590,8 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
label_name=label_name,
|
||||
blend=blend,
|
||||
update=True if (flags & 0x1) else False,
|
||||
transform=transform if (flags & 0x4) else None,
|
||||
rotation_offset=rotation_offset,
|
||||
transform=transform if (transform_set and (flags & 0x4)) else None,
|
||||
rotation_origin=rotation_origin if (rotation_origin_set and (flags & 0x4)) else None,
|
||||
mult_color=multcolor if (flags & 0x8) else None,
|
||||
add_color=addcolor if (flags & 0x8) else None,
|
||||
triggers=bytecodes,
|
||||
|
|
@ -1750,7 +1797,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
chunk_offset += 48
|
||||
unprocessed_flags &= ~0x4
|
||||
|
||||
matrix1 = Matrix(
|
||||
matrix1 = Matrix.affine(
|
||||
a=a1,
|
||||
b=b1,
|
||||
c=c1,
|
||||
|
|
@ -1759,7 +1806,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
ty=ty1,
|
||||
)
|
||||
|
||||
matrix2 = Matrix(
|
||||
matrix2 = Matrix.affine(
|
||||
a=a2,
|
||||
b=b2,
|
||||
c=c2,
|
||||
|
|
@ -1981,43 +2028,37 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||
|
||||
return AP2DefineButtonTag(button_id)
|
||||
elif tagid == AP2Tag.AP2_PLACE_CAMERA:
|
||||
flags, unk1, = struct.unpack("<HH", ap2data[dataoffset:(dataoffset + 4)])
|
||||
flags, camera_id, = struct.unpack("<HH", ap2data[dataoffset:(dataoffset + 4)])
|
||||
self.add_coverage(dataoffset, 4)
|
||||
running_data_ptr = dataoffset + 4
|
||||
|
||||
self.vprint(f"{prefix} Flags: {hex(flags)}, Unknown: {unk1}")
|
||||
self.vprint(f"{prefix} Flags: {hex(flags)}, Camera ID: {camera_id}")
|
||||
|
||||
if flags & 1:
|
||||
center = None
|
||||
if flags & 0x1:
|
||||
i1, i2, i3 = struct.unpack("<iii", ap2data[running_data_ptr:(running_data_ptr + 12)])
|
||||
self.add_coverage(running_data_ptr, 12)
|
||||
running_data_ptr += 12
|
||||
|
||||
# These appear to be camera x, y, z coordinates.
|
||||
f1 = i1 / 20.0
|
||||
f2 = i2 / 20.0
|
||||
f3 = i3 / 20.0
|
||||
|
||||
self.vprint(f"{prefix} Unknown Floats: {f1}, {f2}, {f3}")
|
||||
# This is the camera's X/Y/Z position in the scene, looking "down" at the canvas.
|
||||
center = Point(i1 / 20.0, i2 / 20.0, i3 / 20.0)
|
||||
self.vprint(f"{prefix} Camera Center: {center}")
|
||||
|
||||
focal_length = 0.0
|
||||
if flags & 0x2:
|
||||
i4 = struct.unpack("<i", ap2data[running_data_ptr:(running_data_ptr + 4)])[0]
|
||||
self.add_coverage(running_data_ptr, 4)
|
||||
running_data_ptr += 4
|
||||
|
||||
# I have no idea what this is for. The game adds this to f3 above for
|
||||
# some stored calculation.
|
||||
f4 = i4 / 20.0
|
||||
# This is the focal length of the camera, used to construct the FOV.
|
||||
focal_length = i4 / 20.0
|
||||
|
||||
self.vprint(f"{prefix} Unknown Float: {f4}")
|
||||
else:
|
||||
# The games I've looked at will take the stored value of a previously
|
||||
# parsed place camera for f4 if this is set to zero.
|
||||
pass
|
||||
self.vprint(f"{prefix} Focal Length: {focal_length}")
|
||||
|
||||
if dataoffset + size != running_data_ptr:
|
||||
raise Exception(f"Failed to parse {dataoffset + size - running_data_ptr} bytes of data!")
|
||||
|
||||
return AP2PlaceCameraTag()
|
||||
return AP2PlaceCameraTag(camera_id, center, focal_length)
|
||||
elif tagid == AP2Tag.AP2_IMAGE:
|
||||
if size != 8:
|
||||
raise Exception(f"Invalid size {size} to get data from AP2_IMAGE!")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
from typing import Any, Dict, Tuple
|
||||
import math
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
|
||||
class Color:
|
||||
|
|
@ -48,36 +50,41 @@ class Color:
|
|||
|
||||
|
||||
class Point:
|
||||
# A simple 2D point.
|
||||
def __init__(self, x: float, y: float) -> None:
|
||||
# A simple 3D point. For ease of construction, the Z can be left out
|
||||
# at which point it is assumed to be zero.
|
||||
def __init__(self, x: float, y: float, z: float = 0.0) -> None:
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.z = z
|
||||
|
||||
@staticmethod
|
||||
def identity() -> "Point":
|
||||
return Point(0.0, 0.0)
|
||||
return Point(0.0, 0.0, 0.0)
|
||||
|
||||
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||
return {
|
||||
'x': self.x,
|
||||
'y': self.y,
|
||||
'z': self.z,
|
||||
}
|
||||
|
||||
def as_tuple(self) -> Tuple[int, int]:
|
||||
return (int(round(self.x, 5)), int(round(self.y, 5)))
|
||||
def as_tuple(self) -> Tuple[int, int, int]:
|
||||
return (int(round(self.x, 5)), int(round(self.y, 5)), int(round(self.z, 5)))
|
||||
|
||||
def add(self, other: "Point") -> "Point":
|
||||
x = self.x + other.x
|
||||
y = self.y + other.y
|
||||
return Point(x, y)
|
||||
z = self.z + other.z
|
||||
return Point(x, y, z)
|
||||
|
||||
def subtract(self, other: "Point") -> "Point":
|
||||
x = self.x - other.x
|
||||
y = self.y - other.y
|
||||
return Point(x, y)
|
||||
z = self.z - other.z
|
||||
return Point(x, y, z)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"x: {round(self.x, 5)}, y: {round(self.y, 5)}"
|
||||
return f"x: {round(self.x, 5)}, y: {round(self.y, 5)}, z: {round(self.z, 5)}"
|
||||
|
||||
|
||||
class Rectangle:
|
||||
|
|
@ -113,6 +120,12 @@ class Rectangle:
|
|||
|
||||
|
||||
class Matrix:
|
||||
# A transformation matrix that can be used to calculate both affine and perspective
|
||||
# transforms. This is a 4x4 matrix where the final column is assumed to be 0, 0, 0, 1.
|
||||
# Note that for ease of construction and use with 2D-only parts of the rendering engine
|
||||
# this is capable of being used as a standard 2D affine transformation matrix, as documented
|
||||
# in the next paragraph.
|
||||
|
||||
# The classic SWF matrix. Technically it is missing the third column, but that
|
||||
# column never changes and thus can be omitted. Includes operations for multiplying
|
||||
# 2D points as well as other matrixes and inverting itself. This is how SWF (and
|
||||
|
|
@ -124,72 +137,280 @@ class Matrix:
|
|||
# | c d 0 |
|
||||
# | tx ty 1 |
|
||||
|
||||
def __init__(self, a: float, b: float, c: float, d: float, tx: float, ty: float) -> None:
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
self.d = d
|
||||
self.tx = tx
|
||||
self.ty = ty
|
||||
def __init__(
|
||||
self, *,
|
||||
a11: float, a12: float, a13: float,
|
||||
a21: float, a22: float, a23: float,
|
||||
a31: float, a32: float, a33: float,
|
||||
a41: float, a42: float, a43: float,
|
||||
) -> None:
|
||||
self.a11 = a11
|
||||
self.a12 = a12
|
||||
self.a13 = a13
|
||||
self.a21 = a21
|
||||
self.a22 = a22
|
||||
self.a23 = a23
|
||||
self.a31 = a31
|
||||
self.a32 = a32
|
||||
self.a33 = a33
|
||||
self.a41 = a41
|
||||
self.a42 = a42
|
||||
self.a43 = a43
|
||||
|
||||
@staticmethod
|
||||
def identity() -> "Matrix":
|
||||
return Matrix(a=1.0, b=0.0, c=0.0, d=1.0, tx=0.0, ty=0.0)
|
||||
return Matrix(
|
||||
a11=1.0, a12=0.0, a13=0.0,
|
||||
a21=0.0, a22=1.0, a23=0.0,
|
||||
a31=0.0, a32=0.0, a33=1.0,
|
||||
a41=0.0, a42=0.0, a43=0.0,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def affine(*, a: float, b: float, c: float, d: float, tx: float, ty: float) -> "Matrix":
|
||||
return Matrix(
|
||||
a11=a, a12=b, a13=0.0,
|
||||
a21=c, a22=d, a23=0.0,
|
||||
a31=0.0, a32=0.0, a33=1.0,
|
||||
a41=tx, a42=ty, a43=0.0,
|
||||
)
|
||||
|
||||
def __is_affine(self) -> bool:
|
||||
return (
|
||||
round(abs(self.a13), 5) == 0.0 and
|
||||
round(abs(self.a23), 5) == 0.0 and
|
||||
round(abs(self.a31), 5) == 0.0 and
|
||||
round(abs(self.a32), 5) == 0.0 and
|
||||
round(self.a33, 5) == 1.0 and
|
||||
round(abs(self.a43), 5) == 0.0
|
||||
)
|
||||
|
||||
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||
return {
|
||||
'a': self.a,
|
||||
'b': self.b,
|
||||
'c': self.c,
|
||||
'd': self.d,
|
||||
'tx': self.tx,
|
||||
'ty': self.ty,
|
||||
}
|
||||
if self.__is_affine:
|
||||
return {
|
||||
'a': self.a,
|
||||
'b': self.b,
|
||||
'c': self.c,
|
||||
'd': self.d,
|
||||
'tx': self.tx,
|
||||
'ty': self.ty,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'a11': self.a11,
|
||||
'a12': self.a12,
|
||||
'a13': self.a13,
|
||||
'a21': self.a21,
|
||||
'a22': self.a22,
|
||||
'a23': self.a23,
|
||||
'a31': self.a31,
|
||||
'a32': self.a32,
|
||||
'a33': self.a33,
|
||||
'a41': self.a41,
|
||||
'a42': self.a42,
|
||||
'a43': self.a43,
|
||||
}
|
||||
|
||||
@property
|
||||
def xscale(self) -> float:
|
||||
return math.sqrt((self.a11 * self.a11) + (self.a12 * self.a12) + (self.a13 * self.a13))
|
||||
|
||||
@property
|
||||
def yscale(self) -> float:
|
||||
return math.sqrt((self.a21 * self.a21) + (self.a22 * self.a22) + (self.a23 * self.a23))
|
||||
|
||||
@property
|
||||
def a(self) -> float:
|
||||
return self.a11
|
||||
|
||||
@a.setter
|
||||
def a(self, val: float) -> None:
|
||||
self.a11 = val
|
||||
|
||||
@property
|
||||
def b(self) -> float:
|
||||
return self.a12
|
||||
|
||||
@b.setter
|
||||
def b(self, val: float) -> None:
|
||||
self.a12 = val
|
||||
|
||||
@property
|
||||
def c(self) -> float:
|
||||
return self.a21
|
||||
|
||||
@c.setter
|
||||
def c(self, val: float) -> None:
|
||||
self.a21 = val
|
||||
|
||||
@property
|
||||
def d(self) -> float:
|
||||
return self.a22
|
||||
|
||||
@d.setter
|
||||
def d(self, val: float) -> None:
|
||||
self.a22 = val
|
||||
|
||||
@property
|
||||
def tx(self) -> float:
|
||||
return self.a41
|
||||
|
||||
@tx.setter
|
||||
def tx(self, val: float) -> None:
|
||||
self.a41 = val
|
||||
|
||||
@property
|
||||
def ty(self) -> float:
|
||||
return self.a42
|
||||
|
||||
@ty.setter
|
||||
def ty(self, val: float) -> None:
|
||||
self.a42 = val
|
||||
|
||||
@property
|
||||
def tz(self) -> float:
|
||||
return self.a43
|
||||
|
||||
@tz.setter
|
||||
def tz(self, val: float) -> None:
|
||||
self.a43 = val
|
||||
|
||||
def multiply_point(self, point: Point) -> Point:
|
||||
return Point(
|
||||
x=(self.a * point.x) + (self.c * point.y) + self.tx,
|
||||
y=(self.b * point.x) + (self.d * point.y) + self.ty,
|
||||
x=(self.a11 * point.x) + (self.a21 * point.y) + (self.a31 * point.z) + self.a41,
|
||||
y=(self.a12 * point.x) + (self.a22 * point.y) + (self.a32 * point.z) + self.a42,
|
||||
z=(self.a13 * point.x) + (self.a23 * point.y) + (self.a33 * point.z) + self.a43,
|
||||
)
|
||||
|
||||
def translate(self, point: Point) -> "Matrix":
|
||||
new_point = self.multiply_point(point)
|
||||
return Matrix(
|
||||
a=self.a,
|
||||
b=self.b,
|
||||
c=self.c,
|
||||
d=self.d,
|
||||
tx=new_point.x,
|
||||
ty=new_point.y,
|
||||
a11=self.a11,
|
||||
a12=self.a12,
|
||||
a13=self.a13,
|
||||
a21=self.a21,
|
||||
a22=self.a22,
|
||||
a23=self.a23,
|
||||
a31=self.a31,
|
||||
a32=self.a32,
|
||||
a33=self.a33,
|
||||
a41=new_point.x,
|
||||
a42=new_point.y,
|
||||
a43=new_point.z,
|
||||
)
|
||||
|
||||
def multiply(self, other: "Matrix") -> "Matrix":
|
||||
return Matrix(
|
||||
a=self.a * other.a + self.b * other.c,
|
||||
b=self.a * other.b + self.b * other.d,
|
||||
c=self.c * other.a + self.d * other.c,
|
||||
d=self.c * other.b + self.d * other.d,
|
||||
tx=self.tx * other.a + self.ty * other.c + other.tx,
|
||||
ty=self.tx * other.b + self.ty * other.d + other.ty,
|
||||
a11=self.a11 * other.a11 + self.a12 * other.a21 + self.a13 * other.a31,
|
||||
a12=self.a11 * other.a12 + self.a12 * other.a22 + self.a13 * other.a32,
|
||||
a13=self.a11 * other.a13 + self.a12 * other.a23 + self.a13 * other.a33,
|
||||
|
||||
a21=self.a21 * other.a11 + self.a22 * other.a21 + self.a23 * other.a31,
|
||||
a22=self.a21 * other.a12 + self.a22 * other.a22 + self.a23 * other.a32,
|
||||
a23=self.a21 * other.a13 + self.a22 * other.a23 + self.a23 * other.a33,
|
||||
|
||||
a31=self.a31 * other.a11 + self.a32 * other.a21 + self.a33 * other.a31,
|
||||
a32=self.a31 * other.a12 + self.a32 * other.a22 + self.a33 * other.a32,
|
||||
a33=self.a31 * other.a13 + self.a32 * other.a23 + self.a33 * other.a33,
|
||||
|
||||
a41=self.a41 * other.a11 + self.a42 * other.a21 + self.a43 * other.a31 + other.a41,
|
||||
a42=self.a41 * other.a12 + self.a42 * other.a22 + self.a43 * other.a32 + other.a42,
|
||||
a43=self.a41 * other.a13 + self.a42 * other.a23 + self.a43 * other.a33 + other.a43,
|
||||
)
|
||||
|
||||
def inverse(self) -> "Matrix":
|
||||
denom = (self.a * self.d - self.b * self.c)
|
||||
# Use gauss-jordan eliminiation to invert the matrix.
|
||||
size = 4
|
||||
m = [
|
||||
[self.a11, self.a12, self.a13, 0.0],
|
||||
[self.a21, self.a22, self.a23, 0.0],
|
||||
[self.a31, self.a32, self.a33, 0.0],
|
||||
[self.a41, self.a42, self.a43, 1.0],
|
||||
]
|
||||
inverse: List[List[float]] = [[1 if row == col else 0 for col in range(size)] for row in range(size)]
|
||||
|
||||
try:
|
||||
return Matrix(
|
||||
a=self.d / denom,
|
||||
b=-self.b / denom,
|
||||
c=-self.c / denom,
|
||||
d=self.a / denom,
|
||||
tx=(self.c * self.ty - self.d * self.tx) / denom,
|
||||
ty=-(self.a * self.ty - self.b * self.tx) / denom,
|
||||
)
|
||||
except ZeroDivisionError:
|
||||
pass
|
||||
# First, get upper triangle of the matrix.
|
||||
for col in range(size):
|
||||
if col < size - 1:
|
||||
numbers = [m[row][col] for row in range(col, size)]
|
||||
if all(n == 0 for n in numbers):
|
||||
print("HOO!")
|
||||
raise ZeroDivisionError(f"Matrix({self}) cannot be inverted!")
|
||||
|
||||
# This happens if one of the scaling factors is zero.
|
||||
raise ZeroDivisionError(f"Matrix({self}) cannot be inverted!")
|
||||
# Reorder the matrix until all nonzero numbers are at the top.
|
||||
for row in range(col, size - 1):
|
||||
# First, make sure all non-zero values are at the top.
|
||||
nrow = row - col
|
||||
if numbers[nrow] == 0:
|
||||
# Put this at the end.
|
||||
numbers = [
|
||||
*numbers[:nrow],
|
||||
*numbers[(nrow + 1):],
|
||||
numbers[nrow],
|
||||
]
|
||||
m = [
|
||||
*m[:row],
|
||||
*m[(row + 1):],
|
||||
m[row],
|
||||
]
|
||||
inverse = [
|
||||
*inverse[:row],
|
||||
*inverse[(row + 1):],
|
||||
inverse[row],
|
||||
]
|
||||
row += 1
|
||||
|
||||
# Now, figure out what multiplier we need to make every
|
||||
# other entry zero.
|
||||
major = m[col][col]
|
||||
|
||||
for row in range(size):
|
||||
if row == col:
|
||||
continue
|
||||
if m[row][col] != 0:
|
||||
factor = -(m[row][col] / major)
|
||||
|
||||
m = [
|
||||
*m[:row],
|
||||
[m[row][i] + m[col][i] * factor for i in range(size)],
|
||||
*m[(row + 1):],
|
||||
]
|
||||
inverse = [
|
||||
*inverse[:row],
|
||||
[inverse[row][i] + inverse[col][i] * factor for i in range(size)],
|
||||
*inverse[(row + 1):],
|
||||
]
|
||||
|
||||
# Finally, divide the current column to make it a unit.
|
||||
factor = 1 / m[col][col]
|
||||
m = [
|
||||
*m[:col],
|
||||
[e * factor for e in m[col]],
|
||||
*m[(col + 1):],
|
||||
]
|
||||
inverse = [
|
||||
*inverse[:col],
|
||||
[e * factor for e in inverse[col]],
|
||||
*inverse[(col + 1):],
|
||||
]
|
||||
|
||||
# Technically the rest of the matrix that we don't care about could have values other
|
||||
# than 0.0, 0.0, 0.0, 1.0 but in practice that's because of floating point errors
|
||||
# accumulating so we simply trust the math and discard those values.
|
||||
return Matrix(
|
||||
a11=inverse[0][0], a12=inverse[0][1], a13=inverse[0][2],
|
||||
a21=inverse[1][0], a22=inverse[1][1], a23=inverse[1][2],
|
||||
a31=inverse[2][0], a32=inverse[2][1], a33=inverse[2][2],
|
||||
a41=inverse[3][0], a42=inverse[3][1], a43=inverse[3][2],
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"a: {round(self.a, 5)}, b: {round(self.b, 5)}, c: {round(self.c, 5)}, d: {round(self.d, 5)}, tx: {round(self.tx, 5)}, ty: {round(self.ty, 5)}"
|
||||
if self.__is_affine:
|
||||
return f"a: {round(self.a, 5)}, b: {round(self.b, 5)}, c: {round(self.c, 5)}, d: {round(self.d, 5)}, tx: {round(self.tx, 5)}, ty: {round(self.ty, 5)}"
|
||||
else:
|
||||
return "; ".join([
|
||||
f"a11: {round(self.a11, 5)}, a12: {round(self.a12, 5)}, a13: {round(self.a13, 5)}",
|
||||
f"a21: {round(self.a21, 5)}, a22: {round(self.a22, 5)}, a23: {round(self.a23, 5)}",
|
||||
f"a31: {round(self.a31, 5)}, a32: {round(self.a32, 5)}, a33: {round(self.a33, 5)}",
|
||||
f"a41: {round(self.a41, 5)}, a42: {round(self.a42, 5)}, a43: {round(self.a43, 5)}",
|
||||
])
|
||||
|
|
|
|||
|
|
@ -610,7 +610,7 @@ def render_path(
|
|||
requested_height *= scale_height
|
||||
|
||||
# Calculate the overall view matrix based on the requested width/height.
|
||||
transform = Matrix(
|
||||
transform = Matrix.affine(
|
||||
a=requested_width / swf_location.width,
|
||||
b=0.0,
|
||||
c=0.0,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user