Switch to 4x4 matrix and 3D point for all transform operations. Should behave identically to before.

This commit is contained in:
Jennifer Taylor 2021-07-06 21:58:32 +00:00
parent f0792067b7
commit debc7b3aac
8 changed files with 455 additions and 169 deletions

View File

@ -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:

View File

@ -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:
...

View File

@ -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.

View File

@ -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);

View File

@ -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.

View File

@ -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!")

View File

@ -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)}",
])

View File

@ -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,