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
|