Skip to content

Commit 535a28c

Browse files
authored
Atlas tweaks again (#2187)
* Missing content in atlas ABC * Texture interpolation using half pixels * Allow force resize of texture atlase * Test AtlasRegion * AtlasRegion: Raise RuntimeError instead of ValueErrror * Atlas: Allow force resizing * Atlas: Update type annotations * Remove NinePatchTexture.from_rect * Fix ninepatch. It can't use center pixels for now * import order
1 parent a557f72 commit 535a28c

File tree

8 files changed

+256
-83
lines changed

8 files changed

+256
-83
lines changed

arcade/gui/nine_patch.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import arcade
66
import arcade.gl as gl
7-
from arcade.types.rect import Rect
87

98

109
class NinePatchTexture:
@@ -96,7 +95,7 @@ def __init__(
9695
# References for the texture
9796
self._atlas = atlas or self.ctx.default_atlas
9897
self._texture = texture
99-
self._set_texture(texture)
98+
self._add_to_atlas(texture)
10099

101100
# pixel texture co-ordinate start and end of central box.
102101
self._left = left
@@ -106,15 +105,6 @@ def __init__(
106105

107106
self._check_sizes()
108107

109-
@classmethod
110-
def from_rect(
111-
cls, rect: Rect, texture: arcade.Texture, atlas: Optional[arcade.DefaultTextureAtlas] = None
112-
) -> NinePatchTexture:
113-
"""Construct a new SpriteSolidColor from a :py:class:`~arcade.types.rect.Rect`."""
114-
return cls(
115-
int(rect.left), int(rect.right), int(rect.bottom), int(rect.top), texture, atlas=atlas
116-
)
117-
118108
@property
119109
def ctx(self) -> arcade.ArcadeContext:
120110
"""The OpenGL context."""
@@ -127,7 +117,8 @@ def texture(self) -> arcade.Texture:
127117

128118
@texture.setter
129119
def texture(self, texture: arcade.Texture):
130-
self._set_texture(texture)
120+
self._texture = texture
121+
self._add_to_atlas(texture)
131122

132123
@property
133124
def program(self) -> gl.program.Program:
@@ -142,15 +133,14 @@ def program(self) -> gl.program.Program:
142133
def program(self, program: gl.program.Program):
143134
self._program = program
144135

145-
def _set_texture(self, texture: arcade.Texture):
136+
def _add_to_atlas(self, texture: arcade.Texture):
146137
"""
147138
Internal method for setting the texture.
148139
149140
It ensures the texture is added to the global atlas.
150141
"""
151142
if not self._atlas.has_texture(texture):
152143
self._atlas.add(texture)
153-
self._texture = texture
154144

155145
@property
156146
def left(self) -> int:

arcade/resources/system/shaders/gui/nine_patch_gs.glsl

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,28 @@ void main() {
6565
// )
6666
// Get texture coordinates
6767
vec2 uv0, uv1, uv2, uv3;
68-
getSpriteUVs(uv_texture, int(texture_id), uv0, uv1, uv2, uv3);
6968
vec2 atlas_size = vec2(textureSize(sprite_texture, 0));
70-
// Corner offsets (upper left, upper, right, lower left, lower right)
71-
// This is the global texture coordiante offset in the entire atlas
69+
getSpriteUVs(uv_texture, int(texture_id), uv0, uv1, uv2, uv3);
70+
// TODO: Do center pixel interpolation. Revert by 0.5 pixels for now
71+
vec2 half_px = 0.5 / atlas_size;
72+
uv0 -= half_px;
73+
uv1 += vec2(half_px.x, -half_px.y);
74+
uv2 += vec2(-half_px.x, half_px.y);
75+
uv3 += half_px;
76+
77+
// Local corner offsets in pixels
7278
float left = start.x;
7379
float right = t_size.x - end.x;
7480
float top = t_size.y - end.y;
7581
float bottom = start.y;
82+
// UV offsets to the inner rectangle in the patch
83+
// This is the global texture coordiante offset in the entire atlas
84+
vec2 c1 = vec2(left, top) / atlas_size; // Upper left corner
85+
vec2 c2 = vec2(right, top) / atlas_size; // Upper right corner
86+
vec2 c3 = vec2(left, bottom) / atlas_size; // Lower left corner
87+
vec2 c4 = vec2(right, bottom) / atlas_size; // Lower right corner
7688

77-
vec2 c1 = vec2(left, top) / atlas_size;
78-
vec2 c2 = vec2(right, top) / atlas_size;
79-
vec2 c3 = vec2(left, bottom) / atlas_size;
80-
vec2 c4 = vec2(right, bottom) / atlas_size;
81-
89+
// Texture coordinates for all the points in the patch
8290
vec2 t1 = uv0;
8391
vec2 t2 = uv0 + vec2(c1.x, 0.0);
8492
vec2 t3 = uv1 - vec2(c2.x, 0.0);
@@ -100,9 +108,8 @@ void main() {
100108
vec2 t16 = uv3;
101109

102110
mat4 mvp = window.projection * window.view;
103-
// First row - two fixed corners + strechy middle - 8 vertices
104-
// NOTE: This should ideally be done with 3 strips
105-
// Upper left corner
111+
// First row - two fixed corners + strechy middle
112+
// Upper left corner. Fixed size.
106113
gl_Position = mvp * vec4(p1, 0.0, 1.0);
107114
uv = t1;
108115
EmitVertex();
@@ -117,7 +124,7 @@ void main() {
117124
EmitVertex();
118125
EndPrimitive();
119126

120-
// Upper middle part
127+
// Upper middle part streches on x axis
121128
gl_Position = mvp * vec4(p2, 0.0, 1.0);
122129
uv = t2;
123130
EmitVertex();
@@ -132,7 +139,7 @@ void main() {
132139
EmitVertex();
133140
EndPrimitive();
134141

135-
// Upper right corner
142+
// Upper right corner. Fixed size
136143
gl_Position = mvp * vec4(p3, 0.0, 1.0);
137144
uv = t3;
138145
EmitVertex();
@@ -147,8 +154,8 @@ void main() {
147154
EmitVertex();
148155
EndPrimitive();
149156

150-
// middle row - three strechy parts - 8 vertices
151-
// left border
157+
// Middle row: Two strechy sides + strechy middle
158+
// left border steching on y axis
152159
gl_Position = mvp * vec4(p5, 0.0, 1.0);
153160
uv = t5;
154161
EmitVertex();
@@ -163,7 +170,7 @@ void main() {
163170
EmitVertex();
164171
EndPrimitive();
165172

166-
// Center area
173+
// Center strechy area
167174
gl_Position = mvp * vec4(p6, 0.0, 1.0);
168175
uv = t6;
169176
EmitVertex();
@@ -178,7 +185,7 @@ void main() {
178185
EmitVertex();
179186
EndPrimitive();
180187

181-
// Right border
188+
// Right border. Steches on y axis
182189
gl_Position = mvp * vec4(p7, 0.0, 1.0);
183190
uv = t7;
184191
EmitVertex();
@@ -193,8 +200,8 @@ void main() {
193200
EmitVertex();
194201
EndPrimitive();
195202

196-
// last row - two fixed corners + strechy middle - 8 vertices
197-
// Lower left corner
203+
// Bottom row: two fixed corners + strechy middle
204+
// Lower left corner. Fixed size
198205
gl_Position = mvp * vec4(p9, 0.0, 1.0);
199206
uv = t9;
200207
EmitVertex();
@@ -209,7 +216,7 @@ void main() {
209216
EmitVertex();
210217
EndPrimitive();
211218

212-
// Lower middle part
219+
// Lower middle part. Streches on x axis
213220
gl_Position = mvp * vec4(p10, 0.0, 1.0);
214221
uv = t10;
215222
EmitVertex();
@@ -224,7 +231,7 @@ void main() {
224231
EmitVertex();
225232
EndPrimitive();
226233

227-
// Lower right corner
234+
// Lower right corner. Fixed size
228235
gl_Position = mvp * vec4(p11, 0.0, 1.0);
229236
uv = t11;
230237
EmitVertex();

arcade/texture_atlas/atlas_default.py

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,8 @@
55
from pathlib import Path
66
from typing import (
77
TYPE_CHECKING,
8-
Dict,
9-
List,
108
Optional,
119
Sequence,
12-
Tuple,
1310
Union,
1411
)
1512
from weakref import WeakSet, WeakValueDictionary, finalize
@@ -94,7 +91,7 @@ class DefaultTextureAtlas(TextureAtlasBase):
9491

9592
def __init__(
9693
self,
97-
size: Tuple[int, int],
94+
size: tuple[int, int],
9895
*,
9996
border: int = 1,
10097
textures: Optional[Sequence["Texture"]] = None,
@@ -104,7 +101,7 @@ def __init__(
104101
):
105102
self._ctx = ctx or get_window().ctx
106103
self._max_size = self._ctx.info.MAX_VIEWPORT_DIMS
107-
self._size: Tuple[int, int] = size
104+
self._size: tuple[int, int] = size
108105
self._allocator = Allocator(*self._size)
109106
self._auto_resize = auto_resize
110107
self._capacity = capacity
@@ -143,8 +140,8 @@ def __init__(
143140
# The texture regions are clones of the image regions with transforms applied
144141
# in order to map the same image using different orders or texture coordinates.
145142
# The key is the cache name for a texture
146-
self._image_regions: Dict[str, AtlasRegion] = dict()
147-
self._texture_regions: Dict[str, AtlasRegion] = dict()
143+
self._image_regions: dict[str, AtlasRegion] = dict()
144+
self._texture_regions: dict[str, AtlasRegion] = dict()
148145

149146
# Ref counter for images and textures. Per atlas we need to keep
150147
# track of ho many times an image is used in textures to determine
@@ -162,7 +159,7 @@ def __init__(
162159
# All textures added to the atlas
163160
self._textures: WeakSet[Texture] = WeakSet()
164161
# atlas_name: Set of textures with matching atlas name
165-
self._unique_textures: Dict[str, WeakSet["Texture"]] = dict()
162+
self._unique_textures: dict[str, WeakSet["Texture"]] = dict()
166163

167164
# Add all the textures
168165
for tex in textures or []:
@@ -187,7 +184,7 @@ def max_height(self) -> int:
187184
return self._max_size[1]
188185

189186
@property
190-
def max_size(self) -> Tuple[int, int]:
187+
def max_size(self) -> tuple[int, int]:
191188
"""
192189
The maximum size of the atlas in pixels (x, y)
193190
"""
@@ -227,7 +224,7 @@ def texture_uv_texture(self) -> "Texture2D":
227224
return self._texture_uvs.texture
228225

229226
@property
230-
def textures(self) -> List["Texture"]:
227+
def textures(self) -> list["Texture"]:
231228
"""
232229
All textures instance added to the atlas regardless
233230
of their internal state. See :py:meth:`unique_textures``
@@ -236,7 +233,7 @@ def textures(self) -> List["Texture"]:
236233
return list(self._textures)
237234

238235
@property
239-
def unique_textures(self) -> List["Texture"]:
236+
def unique_textures(self) -> list["Texture"]:
240237
"""
241238
All unique textures in the atlas.
242239
@@ -245,23 +242,23 @@ def unique_textures(self) -> List["Texture"]:
245242
can be found in :py:meth:`textures`.
246243
"""
247244
# Grab the first texture from each set
248-
textures: List[Texture] = []
245+
textures: list[Texture] = []
249246
for tex_set in self._unique_textures.values():
250247
if len(tex_set) == 0:
251248
raise RuntimeError("Empty set in unique textures")
252249
textures.append(next(iter(tex_set)))
253250
return textures
254251

255252
@property
256-
def images(self) -> List["ImageData"]:
253+
def images(self) -> list["ImageData"]:
257254
"""
258255
Return a list of all the images in the atlas.
259256
260257
A new list is constructed from the internal weak set of images.
261258
"""
262259
return list(self._images.values())
263260

264-
def add(self, texture: "Texture") -> Tuple[int, AtlasRegion]:
261+
def add(self, texture: "Texture") -> tuple[int, AtlasRegion]:
265262
"""
266263
Add a texture to the atlas.
267264
@@ -271,7 +268,7 @@ def add(self, texture: "Texture") -> Tuple[int, AtlasRegion]:
271268
"""
272269
return self._add(texture)
273270

274-
def _add(self, texture: "Texture", create_finalizer=True) -> Tuple[int, AtlasRegion]:
271+
def _add(self, texture: "Texture", create_finalizer=True) -> tuple[int, AtlasRegion]:
275272
"""
276273
Internal add method with additional control. We we rebuild the atlas
277274
we don't want to create finalizers for the texture or they will be
@@ -367,7 +364,7 @@ def remove(self, texture: "Texture") -> None:
367364
"and let the python garbage collector handle the removal."
368365
)
369366

370-
def _allocate_texture(self, texture: "Texture") -> Tuple[int, AtlasRegion]:
367+
def _allocate_texture(self, texture: "Texture") -> tuple[int, AtlasRegion]:
371368
"""
372369
Add or update a unique texture in the atlas.
373370
This is mainly responsible for updating the texture coordinates
@@ -391,7 +388,7 @@ def _allocate_texture(self, texture: "Texture") -> Tuple[int, AtlasRegion]:
391388

392389
return slot, texture_region
393390

394-
def _allocate_image(self, image_data: "ImageData") -> Tuple[int, int, int, AtlasRegion]:
391+
def _allocate_image(self, image_data: "ImageData") -> tuple[int, int, int, AtlasRegion]:
395392
"""
396393
Attempts to allocate space for an image in the atlas or
397394
update the existing space for the image.
@@ -593,7 +590,7 @@ def has_image(self, image_data: "ImageData") -> bool:
593590
"""Check if an image is already in the atlas"""
594591
return image_data.hash in self._images
595592

596-
def resize(self, size: Tuple[int, int]) -> None:
593+
def resize(self, size: tuple[int, int], force=False) -> None:
597594
"""
598595
Resize the atlas.
599596
@@ -607,19 +604,21 @@ def resize(self, size: Tuple[int, int]) -> None:
607604
undefined state.
608605
609606
:param size: The new size
607+
:param force: Force a resize even if the size is the same
610608
"""
611609
LOG.info("[%s] Resizing atlas from %s to %s", id(self), self._size, size)
612610
# print("Resizing atlas from", self._size, "to", size)
613611

614612
# Only resize if the size actually changed
615-
if size == self._size:
613+
if size == self._size and not force:
616614
return
617615

618616
self._check_size(size)
619617
resize_start = time.perf_counter()
620618

621619
# Keep a reference to the old atlas texture so we can copy it into the new one
622620
atlas_texture_old = self._texture
621+
atlas_texture_old.filter = self._ctx.NEAREST, self._ctx.NEAREST
623622
self._size = size
624623

625624
# Create new image uv data temporarily keeping the old one
@@ -729,7 +728,7 @@ def use_uv_texture(self, unit: int = 0) -> None:
729728
def render_into(
730729
self,
731730
texture: "Texture",
732-
projection: Optional[Tuple[float, float, float, float]] = None,
731+
projection: Optional[tuple[float, float, float, float]] = None,
733732
):
734733
"""
735734
Render directly into a sub-section of the atlas.
@@ -812,7 +811,7 @@ def to_image(
812811
flip: bool = False,
813812
components: int = 4,
814813
draw_borders: bool = False,
815-
border_color: Tuple[int, int, int] = (255, 0, 0),
814+
border_color: tuple[int, int, int] = (255, 0, 0),
816815
) -> Image.Image:
817816
"""
818817
Convert the atlas to a Pillow image.
@@ -854,7 +853,7 @@ def show(
854853
flip: bool = False,
855854
components: int = 4,
856855
draw_borders: bool = False,
857-
border_color: Tuple[int, int, int] = (255, 0, 0),
856+
border_color: tuple[int, int, int] = (255, 0, 0),
858857
) -> None:
859858
"""
860859
Show the texture atlas using Pillow.
@@ -880,7 +879,7 @@ def save(
880879
flip: bool = False,
881880
components: int = 4,
882881
draw_borders: bool = False,
883-
border_color: Tuple[int, int, int] = (255, 0, 0),
882+
border_color: tuple[int, int, int] = (255, 0, 0),
884883
) -> None:
885884
"""
886885
Save the texture atlas to a png.
@@ -901,7 +900,7 @@ def save(
901900
border_color=border_color,
902901
).save(path, format="png")
903902

904-
def _check_size(self, size: Tuple[int, int]) -> None:
903+
def _check_size(self, size: tuple[int, int]) -> None:
905904
"""Check it the atlas exceeds the hardware limitations"""
906905
if size[0] > self._max_size[0] or size[1] > self._max_size[1]:
907906
raise Exception(

0 commit comments

Comments
 (0)