Source code for arcade.texture_atlas.uv_data

"""
A helper class to keep track of texture coordinates stored in a texture.
"""

from __future__ import annotations

from array import array
from collections import deque
from typing import TYPE_CHECKING

from .base import (
    UV_TEXTURE_WIDTH,
    TexCoords,
)

if TYPE_CHECKING:
    from arcade import ArcadeContext
    from arcade.gl import Texture2D


[docs] class UVData: """ A container for float32 texture coordinates stored in a texture. Each texture coordinate has a slot/index in the texture and is looked up by a shader to obtain the texture coordinates. The shader would look up texture coordinates by the id of the texture. The purpose of this system is to: - Greatly increase the performance of the texture atlas - Greatly simplify the system - Allow images to move freely around the atlas without having to update the vertex buffers. Meaning we can allow re-building and re-sizing. The resize can even be done in the GPU by rendering the old atlas into the new one. - Avoid spending lots of time packing texture data into buffers - Avoid spending lots of buffer memory Args: ctx: The Arcade context capacity: The number of textures the atlas keeps track of. This is multiplied by 4096. Meaning capacity=2 is 8192 textures. """ def __init__(self, ctx: ArcadeContext, capacity: int): self._ctx = ctx self._capacity = capacity self._num_slots = UV_TEXTURE_WIDTH * capacity self._dirty = False # The GPU resource self._texture = self._ctx.texture( (UV_TEXTURE_WIDTH, self._num_slots * 2 // UV_TEXTURE_WIDTH), components=4, dtype="f4", ) self._texture.filter = self._ctx.NEAREST, self._ctx.NEAREST # Python resources: data + tracker for slots # 8 floats per texture (4 x vec2 coordinates) self._data = array("f", [0] * self._num_slots * 8) self._slots: dict[str, int] = dict() self._slots_free = deque(i for i in range(0, self._num_slots))
[docs] def clone_with_slots(self) -> UVData: """ Clone the UVData with the current slot data. The UV data itself and the opengl texture is not cloned. This is useful when we are re-building the atlas since we can't lost the slot data since the indices in the uv texture must not change. If they change the entire spritelist must be updated. """ clone = UVData(self._ctx, self._capacity) clone._slots = self._slots clone._slots_free = self._slots_free clone._dirty = True return clone
@property def num_slots(self) -> int: """The amount of texture coordinates (x4) this UVData can hold""" return self._num_slots @property def num_free_slots(self) -> int: """The amount of free texture coordinates slots""" return len(self._slots_free) @property def texture(self) -> Texture2D: """The opengl texture containing the texture coordinates""" return self._texture
[docs] def get_slot_or_raise(self, name: str) -> int: """ Get the slot for a texture by name or raise an exception Args: name: The name of the texture """ slot = self._slots.get(name) if slot is None: raise Exception(f"Texture '{name}' not found in UVData") return slot
[docs] def get_existing_or_free_slot(self, name: str) -> int: """ Get the slot for a texture by name or a free slot. Getting existing slots is useful when the resize or re-build the atlas. Args: name: The name of the texture """ slot = self._slots.get(name) if slot is not None: return slot try: slot = self._slots_free.popleft() self._slots[name] = slot return slot except IndexError: raise Exception( "No more free slots in the UV texture." f"Max number of textures: {self._num_slots}." "Consider creating a texture atlas with a larger capacity." )
[docs] def free_slot_by_name(self, name: str) -> None: """ Free a slot for a texture by name. If the slot is not found no action is taken Args: name: The name of the texture """ try: slot = self._slots.pop(name) self._slots_free.appendleft(slot) except KeyError: return
[docs] def set_slot_data(self, slot: int, data: TexCoords) -> None: """ Update the texture coordinates for a slot. Args: slot: The slot to update data: The texture coordinates """ self._data[slot * 8 : slot * 8 + 8] = array("f", data) self._dirty = True
[docs] def write_to_texture(self) -> None: """Write the texture coordinates to the opengl texture if dirty""" if self._dirty: self._texture.write(self._data, 0) self._dirty = False
def __len__(self) -> int: return len(self._slots)