From 830f32814ece070c010bb6f567c64d1a1473c427 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Sat, 17 Apr 2021 23:30:46 +0000 Subject: [PATCH] Clean up the code some in preparation for trying to get affine transformations working. --- bemani/format/afp/render.py | 176 ++++++++++++++++++++++-------------- 1 file changed, 107 insertions(+), 69 deletions(-) diff --git a/bemani/format/afp/render.py b/bemani/format/afp/render.py index c31cc59..66b4689 100644 --- a/bemani/format/afp/render.py +++ b/bemani/format/afp/render.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Tuple, Optional +from typing import Dict, List, Tuple, Optional from PIL import Image # type: ignore from .swf import SWF, Frame, Tag, AP2ShapeTag, AP2DefineSpriteTag, AP2PlaceObjectTag, AP2RemoveObjectTag, AP2DoActionTag, AP2DefineFontTag, AP2DefineEditTextTag @@ -8,60 +8,92 @@ from .util import VerboseOutput class Clip: + # A movie clip that we are rendering, frame by frame. These are manifest by the root + # SWF as well as AP2DefineSpriteTags which are essentially embedded movie clips. def __init__(self, tag_id: Optional[int], frames: List[Frame], tags: List[Tag]) -> None: self.tag_id = tag_id self.frames = frames self.tags = tags self.frameno = 0 + @property def frame(self) -> Frame: + # The current frame object. + if self.finished: + raise Exception("Logic error!") return self.frames[self.frameno] def advance(self) -> None: - if not self.finished(): + # Advance the clip by one frame after we finished processing that frame. + if not self.finished: self.frameno += 1 + @property def finished(self) -> bool: + # Whether we've hit the end of the clip or not. return self.frameno == len(self.frames) + @property def running(self) -> bool: - return not self.finished() + # Whether we are still running. + return self.frameno < len(self.frames) + + def __repr__(self) -> str: + return f"Clip(tag_id={self.tag_id}, frames={len(self.frames)}, frameno={self.frameno})" class PlacedObject: - def __init__(self, parent_sprite: Optional[int], tag: AP2PlaceObjectTag) -> None: - self.parent_sprite = parent_sprite + # An object that occupies the screen at some depth. Placed by an AP2PlaceObjectTag + # that is inside the root SWF or an AP2DefineSpriteTag (essentially an embedded + # movie clip). + def __init__(self, parent_clip: Optional[int], tag: AP2PlaceObjectTag) -> None: + self.parent_clip = parent_clip self.tag = tag + @property + def depth(self) -> int: + return self.tag.depth + + @property + def object_id(self) -> int: + return self.tag.object_id + + def __repr__(self) -> str: + return f"PlacedObject(parent_clip={self.parent_clip}, object_id={self.object_id}, depth={self.depth})" + class AFPRenderer(VerboseOutput): - def __init__(self, shapes: Dict[str, Shape] = {}, textures: Dict[str, Any] = {}, swfs: Dict[str, SWF] = {}) -> None: + def __init__(self, shapes: Dict[str, Shape] = {}, textures: Dict[str, Image.Image] = {}, swfs: Dict[str, SWF] = {}) -> None: super().__init__() self.shapes: Dict[str, Shape] = shapes - self.textures: Dict[str, Any] = textures + self.textures: Dict[str, Image.Image] = textures self.swfs: Dict[str, SWF] = swfs # Internal render parameters self.__visible_tag: Optional[int] = None - self.__ided_tags: Dict[int, Tag] = {} self.__registered_shapes: Dict[int, Shape] = {} self.__placed_objects: List[PlacedObject] = [] def add_shape(self, name: str, data: Shape) -> None: + # Register a named shape with the renderer. if not data.parsed: data.parse() self.shapes[name] = data - def add_texture(self, name: str, data: Any) -> None: + def add_texture(self, name: str, data: Image.Image) -> None: + # Register a named texture (already loaded PIL image) with the renderer. self.textures[name] = data def add_swf(self, name: str, data: SWF) -> None: + # Register a named SWF with the renderer. if not data.parsed: data.parse() self.swfs[name] = data - def render_path(self, path: str, verbose: bool = False) -> Tuple[int, List[Any]]: + def render_path(self, path: str, verbose: bool = False) -> Tuple[int, List[Image.Image]]: + # Given a path to a SWF root animation or an exported animation inside a SWF, + # attempt to render it to a list of frames, one per image. components = path.split(".") if len(components) > 2: @@ -76,6 +108,7 @@ class AFPRenderer(VerboseOutput): raise Exception(f'{path} not found in registered SWFs!') def list_paths(self, verbose: bool = False) -> List[str]: + # Given the loaded animations, return a list of possible paths to render. paths: List[str] = [] for name, swf in self.swfs.items(): @@ -86,14 +119,20 @@ class AFPRenderer(VerboseOutput): return paths - def __place(self, tag: Tag, parent_sprite: Optional[int], prefix: str = "") -> List[Clip]: + def __place(self, tag: Tag, parent_clip: Optional[int], prefix: str = "") -> List[Clip]: + # "Place" a tag on the screen. Most of the time, this means performing the action of the tag, + # such as defining a shape (registering it with our shape list) or adding/removing an object. if isinstance(tag, AP2ShapeTag): self.vprint(f"{prefix} Loading {tag.reference} into shape slot {tag.id}") if tag.reference not in self.shapes: raise Exception(f"Cannot find shape reference {tag.reference}!") + if tag.id in self.__registered_shapes: + raise Exception(f"Cannot register {tag.reference} as shape slot {tag.id} is already taken!") self.__registered_shapes[tag.id] = self.shapes[tag.reference] + + # No additional movie clips were spawned. return [] elif isinstance(tag, AP2DefineSpriteTag): self.vprint(f"{prefix} Registering Sprite Tag {tag.id}") @@ -103,14 +142,14 @@ class AFPRenderer(VerboseOutput): clips: List[Clip] = [clip] # Now, we need to run the first frame of this clip, since that's this frame. - if clip.running(): - frame = clip.frame() - if frame.num_tags > 0: - self.vprint(f"{prefix} First Frame Initialization, Start Frame: {frame.start_tag_offset}, Num Frames: {frame.num_tags}") - for child in clip.tags[frame.start_tag_offset:(frame.start_tag_offset + frame.num_tags)]: - clips.extend(self.__place(child, parent_sprite=tag.id, prefix=" ")) + if clip.running: + if clip.frame.num_tags > 0: + self.vprint(f"{prefix} First Frame Initialization, Start Frame: {clip.frame.start_tag_offset}, Num Frames: {clip.frame.num_tags}") + for child in clip.tags[clip.frame.start_tag_offset:(clip.frame.start_tag_offset + clip.frame.num_tags)]: + clips.extend(self.__place(child, parent_clip=tag.id, prefix=prefix + " ")) - # Finally, return the new clips we registered. + # Finally, return the new clips we registered, including any that were done + # in recursive calls to __place. return clips elif isinstance(tag, AP2PlaceObjectTag): if tag.update: @@ -118,7 +157,7 @@ class AFPRenderer(VerboseOutput): updated = False for obj in self.__placed_objects: - if obj.tag.object_id == tag.object_id and obj.tag.depth == tag.depth: + if obj.object_id == tag.object_id and obj.depth == tag.depth: # As far as I can tell, pretty much only color and matrix stuff can be updated. obj.tag.mult_color = tag.mult_color or obj.tag.mult_color obj.tag.add_color = tag.add_color or obj.tag.add_color @@ -127,31 +166,43 @@ class AFPRenderer(VerboseOutput): updated = True if not updated: - raise Exception("Couldn't find tag to update!") + raise Exception(f"Couldn't find tag {tag.object_id} on depth {tag.depth} to update!") else: self.vprint(f"{prefix} Placing Object ID {tag.object_id} onto Depth {tag.depth}") - self.__placed_objects.append(PlacedObject(parent_sprite, tag)) + self.__placed_objects.append(PlacedObject(parent_clip, tag)) + + # TODO: Handle ON_LOAD triggers for this object. Many of these are just calls into + # the game to set the current frame that we're on, but sometimes its important. - # TODO: Handle triggers for this object. return [] elif isinstance(tag, AP2RemoveObjectTag): self.vprint(f"{prefix} Removing Object ID {tag.object_id} from Depth {tag.depth}") if tag.object_id != 0: # Remove the identified object by object ID and depth. + old_len = len(self.__placed_objects) + self.__placed_objects = [ - o for o in self.__placed_objects - if o.tag.object_id == tag.object_id and o.tag.depth == tag.depth + obj for obj in self.__placed_objects + if not(obj.object_id == tag.object_id and obj.depth == tag.depth) ] + + # We should have removed at least one objct. + if len(self.__placed_objects) == old_len: + raise Exception(f"Couldn't find object to remove by ID {tag.object_id} and depth {tag.depth}!") else: - # Remove the last placed object at this depth. + # Remove the last placed object at this depth. The placed objects list isn't + # ordered so much as apppending to the list means the last placed object at a + # depth comes last. for i in range(len(self.__placed_objects)): real_index = len(self.__placed_objects) - (i + 1) - if self.__placed_objects[real_index].tag.depth == tag.depth: + if self.__placed_objects[real_index].depth == tag.depth: self.__placed_objects = self.__placed_objects[:real_index] + self.__placed_objects[(real_index + 1):] break + else: + raise Exception(f"Couldn't find a recently-placed object to remove on depth {tag.depth}!") return [] elif isinstance(tag, AP2DoActionTag): @@ -166,7 +217,7 @@ class AFPRenderer(VerboseOutput): else: raise Exception(f"Failed to process tag: {tag}") - def __render_object(self, img: Any, tag: AP2PlaceObjectTag, parent_transform: Matrix, parent_origin: Point) -> Any: + def __render_object(self, img: Image.Image, tag: AP2PlaceObjectTag, parent_transform: Matrix, parent_origin: Point) -> Image.Image: if tag.source_tag_id is None: self.vprint(" Nothing to render!") return img @@ -189,11 +240,19 @@ class AFPRenderer(VerboseOutput): # Look up source shape. if tag.source_tag_id not in self.__registered_shapes: # This is probably a sprite placement reference. + found_one = False for obj in self.__placed_objects: - if obj.parent_sprite == tag.source_tag_id: - self.vprint(f" Rendering placed object ID {obj.tag.object_id} from sprite {obj.parent_sprite} onto Depth {obj.tag.depth}") + if obj.parent_clip == tag.source_tag_id: + self.vprint(f" Rendering placed object ID {obj.object_id} from sprite {obj.parent_clip} onto Depth {obj.depth}") img = self.__render_object(img, obj.tag, transform, origin) + found_one = True + + if not found_one: + raise Exception(f"Couldn't find parent clip {obj.parent_clip} to render animation out of!") + return img + + # This is a shape draw reference. shape = self.__registered_shapes[tag.source_tag_id] for params in shape.draw_params: @@ -226,8 +285,8 @@ class AFPRenderer(VerboseOutput): img.alpha_composite(texture, cutin.as_tuple(), cutoff.as_tuple()) return img - def __render(self, swf: SWF, export_tag: Optional[str]) -> Tuple[int, List[Any]]: - # If we are rendering only an exported tag, we want to perform the actions of the + def __render(self, swf: SWF, export_tag: Optional[str]) -> Tuple[int, List[Image.Image]]: + # If we are rendering an exported tag, we want to perform the actions of the # rest of the SWF but not update any layers as a result. self.__visible_tag = None if export_tag is not None: @@ -236,40 +295,18 @@ class AFPRenderer(VerboseOutput): raise Exception(f'{export_tag} is not exported by {swf.exported_name}!') self.__visible_tag = swf.exported_tags[export_tag] - # Now, we need to make an index of each ID'd tag. - self.__ided_tags = {} - - def get_children(tag: Tag) -> List[Tag]: - children: List[Tag] = [] - - for child in tag.children(): - children.extend(get_children(child)) - children.append(tag) - return children - - all_children: List[Tag] = [] - for tag in swf.tags: - all_children.extend(get_children(tag)) - - for child in all_children: - if child.id is not None: - if child.id in self.__ided_tags: - raise Exception(f"Already have a Tag ID {child.id}!") - self.__ided_tags[child.id] = child - - # TODO: Now, we have to resolve imports. - pass + # TODO: We have to resolve imports. # Now, let's go through each frame, performing actions as necessary. spf = 1.0 / swf.fps - frames: List[Any] = [] + frames: List[Image.Image] = [] frameno: int = 0 - clips: List[Clip] = [Clip(None, swf.frames, swf.tags)] + clips: List[Clip] = [Clip(None, swf.frames, swf.tags)] if len(swf.frames) > 0 else [] # Reset any registered shapes. self.__registered_shapes = {} - while any(c.running() for c in clips): + while any(c.running for c in clips): # Create a new image to render into. time = spf * float(frameno) color = swf.color or Color(0.0, 0.0, 0.0, 0.0) @@ -279,24 +316,22 @@ class AFPRenderer(VerboseOutput): # Go through all registered clips, place all needed tags. newclips: List[Clip] = [] for clip in clips: - if clip.finished(): - continue - - frame = clip.frame() - if frame.num_tags > 0: - self.vprint(f" Sprite Tag ID: {clip.tag_id}, Start Frame: {frame.start_tag_offset}, Num Frames: {frame.num_tags}") - for tag in clip.tags[frame.start_tag_offset:(frame.start_tag_offset + frame.num_tags)]: - newclips.extend(self.__place(tag, parent_sprite=clip.tag_id)) + if clip.frame.num_tags > 0: + self.vprint(f" Sprite Tag ID: {clip.tag_id}, Start Frame: {clip.frame.start_tag_offset}, Num Frames: {clip.frame.num_tags}") + for tag in clip.tags[clip.frame.start_tag_offset:(clip.frame.start_tag_offset + clip.frame.num_tags)]: + newclips.extend(self.__place(tag, parent_clip=clip.tag_id)) # Add any new clips that we should process next frame. clips.extend(newclips) - # Now, render out the placed objects. - for obj in sorted(self.__placed_objects, key=lambda o: o.tag.depth): - if self.__visible_tag != obj.parent_sprite: + # Now, render out the placed objects. We sort by depth so that we can + # get the layering correct, but its important to preserve the original + # insertion order for delete requests. + for obj in sorted(self.__placed_objects, key=lambda obj: obj.depth): + if self.__visible_tag != obj.parent_clip: continue - self.vprint(f" Rendering placed object ID {obj.tag.object_id} from sprite {obj.parent_sprite} onto Depth {obj.tag.depth}") + self.vprint(f" Rendering placed object ID {obj.object_id} from sprite {obj.parent_clip} onto Depth {obj.depth}") curimage = self.__render_object(curimage, obj.tag, Matrix.identity(), Point.identity()) # Advance all the clips and frame now that we processed and rendered them. @@ -305,4 +340,7 @@ class AFPRenderer(VerboseOutput): frames.append(curimage) frameno += 1 + # Garbage collect any clips that we're finished with. + clips = [c for c in clips if c.running] + return int(spf * 1000.0), frames