File: simple_atlas.py

package info (click to toggle)
python-moderngl-window 3.1.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 69,096 kB
  • sloc: python: 12,076; makefile: 21
file content (169 lines) | stat: -rw-r--r-- 5,025 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
"""
Simple row based texture atlas created for fast runtime allocation.

* This atlas is partly based on the texture atlas in the Arcade project
* The allocator is based on Pyglet's row allocator

https://github.com/pyglet/pyglet/blob/master/pyglet/image/atlas.py
https://github.com/pythonarcade/arcade/blob/development/arcade/texture_atlas.py
"""

import moderngl

from .base import BaseImage


class AllocatorException(Exception):
    pass


class _Row:
    """
    A row in the texture atlas.
    """

    __slots__ = ("x", "y", "y2", "max_height")

    def __init__(self, y: int, max_height: int) -> None:
        self.x = 0
        self.y = y
        self.max_height = max_height
        self.y2 = y

    def add(self, width: int, height: int) -> tuple[int, int]:
        """Add a region to the row and return the position"""
        if width <= 0 or height <= 0:
            raise AllocatorException("Cannot allocate size: [{}, {}]".format(width, height))
        if height > self.max_height:
            raise AllocatorException("Cannot allocate past the max height")

        x, y = self.x, self.y
        self.x += width
        # Keep track of the highest y value for compaction
        self.y2 = max(self.y + height, self.y2)
        return x, y

    def compact(self) -> None:
        """
        Compacts the row to the smallest height.
        Should only be done once when the row is filled before adding a new row.
        """
        self.max_height = self.y2 - self.y


class Allocator:
    """Row based allocator"""

    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height
        # First row covers the entire height until compacted
        # when a new row is added
        self.rows = [_Row(0, self.height)]

    def alloc(self, width: int, height: int) -> tuple[int, int]:
        """
        Allocate a region.

        Returns:
            tuple[int, int]: The x,y location
        Raises:
            AllocatorException: if no more space
        """
        # Check if we have room in existing rows
        for row in self.rows:
            # Can we add the region to the end if this row?
            if self.width - row.x >= width and row.max_height >= height:
                return row.add(width, height)

        # Can we make a new row?
        if self.width >= width and self.height - row.y2 >= height:
            # Compact the last row
            row.compact()
            # New row continuing from y2 with a remaining of the height as the max
            new_row = _Row(row.y2, self.height - row.y2)
            self.rows.append(new_row)
            # Allocate the are in the new row
            return new_row.add(width, height)

        raise AllocatorException("No more space in {} for box [{}, {}]".format(self, width, height))


class TextureAtlas:
    """
    A simple texture atlas using a row based allocation.
    There are more efficient ways to pack textures, but this
    is normally sufficient for dynamic atlases were textures
    are added on the fly runtime.
    """

    def __init__(
        self,
        ctx: moderngl.Context,
        width: int,
        height: int,
        components: int = 4,
        border: int = 1,
        auto_resize: bool = True,
    ):
        # Basic properties
        self._ctx = ctx
        self._width = width
        self._height = height
        self._components = components
        self._border = border
        self._auto_resize = auto_resize

        # The physical size limit for the current hardware
        self._max_size: tuple[int, int] = self._ctx.info["GL_MAX_VIEWPORT_DIMS"]

        # Atlas content
        self._texture = self._ctx.texture(self.size, components=self._components)

        # We want to be able to render into the atlas texture
        self._fbo = self._ctx.framebuffer(color_attachments=[self._texture])
        self._allocator = Allocator(width, height)

    @property
    def ctx(self) -> moderngl.Context:
        """The moderngl contex this atlas belongs to"""
        return self._ctx

    @property
    def textrue(self) -> moderngl.Texture:
        """The moderngl texture with the atlas contents"""
        return self._texture

    @property
    def width(self) -> int:
        """int: Width of the atlas in pixels"""
        return self._width

    @property
    def height(self) -> int:
        """int: Height of the atlas in pixels"""
        return self._height

    @property
    def size(self) -> tuple[int, int]:
        """tuple[int, int]: The size of he atlas (width, height)"""
        return self._width, self._height

    @property
    def max_size(self) -> tuple[int, int]:
        """
        tuple[int,int]: The maximum size of the atlas in pixels (x, y)
        """
        return self._max_size

    def add(self, image: BaseImage) -> None:
        pass

    def remove(self, image: BaseImage) -> None:
        pass

    def resize(self, width: int, height: int) -> None:
        pass

    def rebuild(self) -> None:
        pass