Exemple #1
0
def make_sprite_node(
    sprite: Texture,
    size: tuple = None,
    name: str = None,
    is_two_sided: bool = True,
    is_transparent: bool = True,
    parent: NodePath = None,
    position: Vec3 = None,
    scale: float = 0.0,
) -> NodePath:
    """Make flat single-sprite node out of provided data"""
    # Using WM_clamp instead of WM_mirror, to avoid issue with black 1-pixel
    # bars appearing on top of spritesheet randomly.
    # Idk if this needs to have possibility to override it #TODO
    sprite.set_wrap_u(Texture.WM_clamp)
    sprite.set_wrap_v(Texture.WM_clamp)

    # Creating CardMaker frame
    card = CardMaker(name or sprite.get_name() or "sprite")
    # This will fail if texture has been generated with no set_orig_file_size()
    size = size or (sprite.get_orig_file_x_size(),
                    sprite.get_orig_file_y_size())

    # Been told that its not in pixels, thus accepting either 1, 2 or 4 values
    # Kinda jank, I know
    if len(size) > 3:
        card.set_frame(-size[0], size[1], -size[2], size[3])
    elif len(size) > 1:
        card.set_frame(-size[0], size[0], -size[1], size[1])
    else:
        card.set_frame(-size[0], size[0], -size[0], size[0])

    parent = parent or NodePath()
    node = parent.attach_new_node(card.generate())
    node.set_texture(sprite)

    # Making it possible to utilize texture's alpha channel settings
    # This is a float from 0 to 1, but I dont think there is a point to only
    # show half of actual object's transparency.
    # Do it manually afterwards if thats what you need
    if is_transparent:
        node.set_transparency(1)
    # Enabling ability to render texture on both front and back of card
    if is_two_sided:
        node.set_two_sided(True)
    # Setting object's position. This is done relatively to parent, thus if you
    # didnt pass any, it may be a bit janky
    if position:
        node.set_pos(*position)

    if scale and scale > 0:
        node.set_scale(scale)

    return node
Exemple #2
0
def get_images(spritesheet: Texture, sprite_sizes: tuple) -> list:
    """Cut provided spritesheet texture into separate PNMImage objects"""
    if _has_remainder(spritesheet, sprite_sizes):
        raise exceptions.InvalidSpriteSize(spritesheet.get_name(),
                                           sprite_sizes)

    sprite_x, sprite_y = sprite_sizes
    columns, rows = _get_columns_and_rows(spritesheet, sprite_sizes)

    # This is safety check to ensure there wont be any weird effects during cutting,
    # caused by texture autorescale. In order to circuimvent this, you need to
    # set "textures-power-2 none" in your Config.rpc.
    # There seem to be setters and getters to deal with it on per-texture basis,
    # but thus far I couldnt figure out how to make them work properly #TODO
    if spritesheet.getTexturesPower2():
        if not _is_power_of_two(columns) or not _is_power_of_two(rows):
            raise exceptions.InvalidSpriteSize(spritesheet.get_name(),
                                               sprite_sizes)

    # Extract texture's image from memory
    sheet_image = Image()
    spritesheet.store(sheet_image)

    images = []
    for row in range(0, rows):
        log.debug(f"Processing row{row}")
        for column in range(0, columns):
            log.debug(f"Processing column {column}")
            # THIS WAS BUGGED - I HAD TO FLIP IT
            x = column * sprite_x
            y = row * sprite_y
            # passing amount of channels is important to allow transparency
            pic = Image(sprite_x, sprite_y, sheet_image.get_num_channels())
            pic.blendSubImage(sheet_image, 0, 0, x, y, sprite_x, sprite_y, 1.0)
            images.append(pic)

    log.debug(f"Got following images: {images}")
    return images
Exemple #3
0
def get_textures(spritesheet: Texture,
                 sprite_sizes: tuple,
                 texture_filter: SamplerState = None) -> list:
    """Cut provided spritesheet texture into multiple textures"""
    images = get_images(
        spritesheet=spritesheet,
        sprite_sizes=sprite_sizes,
    )
    # Allowing for inheriting filter from parent in case its been set
    # This is based on magfilter, coz I cant make it inherit from one or another
    # since they all have defaults set to enumerator with non-zero value
    if texture_filter is None:
        texture_filter = spritesheet.get_magfilter()

    return to_textures(images, spritesheet.get_name(), sprite_sizes,
                       texture_filter)
Exemple #4
0
def get_offsets(spritesheet: Texture,
                sprite_sizes: tuple) -> types.SpritesheetData:
    """Fetch all available offsets from provided spritesheet."""

    # For now, this has 2 limitations, both of which are addressed as exceptions:
    # 1. Spritesheet HAS TO DIVIDE TO PROVIDED SPRITE SIZE WITHOUT REMAINDER. If
    # it doesnt cut to perfect sprites, you will get strange results during using
    # some of these sprites.
    # 2. Amount of sprite rows and columns MUST BE POWER OF 2. Otherwise - see
    # above. This is because of limitation of set_tex_offset() and set_tex_scale()
    # functions, both of which operate with floats between 0 and 1 to determine
    # texture's size and position.
    # I assume, its possible to fix both of these. But right now I have no idea how

    # As for first - I can probably add bool to enable optional cut with PNMimage
    # of all the garbage that dont fit #TODO

    log.debug(f"Fetching {sprite_sizes} offsets from {spritesheet.get_name()}")

    # Checking if our spritesheet match first limitation, mentioned above
    if _has_remainder(spritesheet, sprite_sizes):
        raise exceptions.InvalidSpriteSize(spritesheet.get_name(),
                                           sprite_sizes)

    # Determining amount of sprites in each row
    sprite_columns, sprite_rows = _get_columns_and_rows(
        spritesheet, sprite_sizes)

    # Checking if we pass second limitation from above
    if not _is_power_of_two(sprite_columns) or not _is_power_of_two(
            sprite_rows):
        raise exceptions.InvalidSpriteSize(spritesheet.get_name(),
                                           sprite_sizes)

    log.debug(f"Our sheet has {sprite_columns}x{sprite_rows} sprites")

    # idk if these should be flipped - its 3 am
    # this may backfire on values bigger than one... but it should never happen
    horizontal_offset_step = 1 / sprite_columns
    vertical_offset_step = 1 / sprite_rows
    offset_steps = LPoint2(horizontal_offset_step, vertical_offset_step)
    log.debug(f"Offset steps are {offset_steps}")

    spritesheet_offsets = []

    # We process rows backwards to make it match "from top left to bottom right"
    # style of image processing, used by most tools (and thus probs expected)
    for row in range(sprite_rows - 1, -1, -1):
        log.debug(f"Processing row {row}")
        for column in range(0, sprite_columns):
            log.debug(f"Processing column {column}")
            horizontal_offset = column * horizontal_offset_step
            vertical_offset = row * vertical_offset_step
            offsets = LPoint2(horizontal_offset, vertical_offset)
            log.debug(f"Got offsets: {offsets}")
            spritesheet_offsets.append(offsets)
    log.debug(f"Spritesheet contain following offsets: {spritesheet_offsets}")

    data = types.SpritesheetData(spritesheet, spritesheet_offsets,
                                 offset_steps)
    log.debug(f"Got following data: {data}, returning")

    return data
Exemple #5
0
    def __init__(
        self,
        spritesheet: Texture,
        sprite_sizes: tuple,
        node_sizes: tuple = None,
        name: str = None,
        is_two_sided: bool = False,
        # Maybe I should rename this to "has_transparency" or "has_alpha_channel"?
        # #TODO
        is_transparent: bool = True,
        parent: NodePath = None,
        scale: float = 0.0,
        default_sprite: int = 0,
        position: Vec3 = None,
    ):

        parent = parent or NodePath()
        # name of animated object
        self.name = name or spritesheet.get_name() or "SpritesheetNode"
        self.sprite_sizes = sprite_sizes
        self.node_sizes = node_sizes or self.sprite_sizes

        sprite_data = processor.get_offsets(spritesheet, self.sprite_sizes)

        self.offsets = sprite_data.offsets

        self.node = make_sprite_node(
            sprite=spritesheet,
            # This is kept for backwards compatibility. I should probably make
            # generated objects have unified size measurement values across whole
            # library #TODO
            # size=(self.sizes[0] / 2, self.sizes[1] / 2),
            size=self.node_sizes,
            name=self.name,
            is_two_sided=is_two_sided,
            is_transparent=is_transparent,
            parent=parent,
            position=position,
            scale=scale,
        )

        # Sprite shown right now. This will crash if its value is greater than
        # amount of sprites in sheet
        self.current_sprite = default_sprite

        # okay, this does the magic
        # basically, to show the very first sprite of 2 in row, we set tex scale
        # to half (coz half is our normal char's size). If we will need to use it
        # with sprites other than first - then we also should adjust offset accordingly
        self.node.set_tex_scale(TextureStage.getDefault(),
                                *sprite_data.step_sizes)

        # now,lets say, we need to use second sprite from sheet. Just do:
        # self.node.set_tex_offset(TextureStage.getDefault(), *offsets[1])
        self.node.set_tex_offset(TextureStage.getDefault(),
                                 *self.offsets[self.current_sprite])

        self.items = {}
        # Name of item to reset playback to. If not set, playback of non-looped
        # items with reset_on_complete will stop at their last frame
        self.default_item = None
        # setting this to None may cause crashes on few rare cases, but going
        # for "idle_right" wont work for projectiles... So I technically add it
        # there for anims updater, but its meant to be overwritten at 100% cases
        self.current_item = None
        # Items to be played after current. Not implemented right now. #TODO
        # self.queue = []

        # This specifies if something plays right now or not
        self.playing = types.PlaybackState.stop

        # This used to specify amount of time left till shown item will be changed
        self.frame_time_left = 0
        # Number of item in current sequence that plays right now
        self.current_sequence_item = 0

        def play_current(event: PythonTask) -> PythonTask:
            """Taskmanager routine that plays currently shown item"""
            # Destroys the routine if node has been deleted
            if not self.node:
                return

            # If no playback is going on right now - either resetting to default
            # or just doing nothing till next frame
            if self.playing == types.PlaybackState.stop:
                return event.cont

            if self.playing == types.PlaybackState.pause:
                # This will crash if there is no current item, shouldnt happen
                if (self.items[self.current_item].reset_on_complete
                        and self.default_item
                        and self.default_item != self.current_item):
                    self.play(self.default_item)
                return event.cont

            # Getting delta time since last frame
            dt = globalClock.get_dt()
            self.frame_time_left -= dt
            if self.frame_time_left > 0:
                return event.cont

            # If amount of time passed has been more than required - resetting
            # timer and switching to next image in sequence
            # Idk if this is resource-efficient, will see #TODO
            self.frame_time_left = self.items[self.current_item].playback_speed

            self.current_sprite = self.items[self.current_item].sprites[
                self.current_sequence_item]
            self.node.set_tex_offset(TextureStage.getDefault(),
                                     *self.offsets[self.current_sprite])

            # idk how to do this better
            if (len(self.items[self.current_item].sprites) >
                    self.current_sequence_item + 1):
                self.current_sequence_item += 1
            else:
                self.current_sequence_item = 0
                # if looping is disabled - keeping last frame
                if not self.items[self.current_item].loop:
                    self.playing = types.PlaybackState.pause
                    return event.cont

            return event.cont

        base.task_mgr.add(play_current, f"Animation task of {self.name}")