Skip to content

Don't use geo shader for nine patch #2668

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 230 additions & 35 deletions arcade/gui/nine_patch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from array import array

import arcade
import arcade.gl as gl
from arcade.gl import Buffer, BufferDescription, Geometry, Program
from arcade.math import Vec2
from arcade.texture_atlas.base import TextureAtlasBase
from arcade.types import Rect


class NinePatchTexture:
Expand Down Expand Up @@ -74,6 +78,7 @@ def __init__(
self._initialized = False
self._texture = texture
self._custom_atlas = atlas
self._geometry_cache: tuple[int, int, int, int, Rect] | None = None

# pixel texture co-ordinate start and end of central box.
self._left = left
Expand All @@ -84,37 +89,16 @@ def __init__(
self._check_sizes()

# Created in _init_deferred
self._program: gl.program.Program
self._geometry: gl.Geometry
self._buffer: Buffer
self._program: Program
self._geometry: Geometry
self._ctx: arcade.ArcadeContext
self._atlas: TextureAtlasBase
try:
self._init_deferred()
except Exception:
pass

def _init_deferred(self):
"""Deferred initialization when lazy loaded"""
self._ctx = arcade.get_window().ctx
# TODO: Cache in context?
self._program = self._ctx.load_program(
vertex_shader=":system:shaders/gui/nine_patch_vs.glsl",
geometry_shader=":system:shaders/gui/nine_patch_gs.glsl",
fragment_shader=":system:shaders/gui/nine_patch_fs.glsl",
)
# Configure texture channels
self._program.set_uniform_safe("uv_texture", 0)
self._program["sprite_texture"] = 1

# TODO: Cache in context?
self._geometry = self._ctx.geometry()

# References for the texture
self._atlas = self._custom_atlas or self._ctx.default_atlas
self._add_to_atlas(self.texture)

self._initialized = True

def initialize(self) -> None:
"""
Manually initialize the NinePatchTexture if it was lazy loaded.
Expand All @@ -141,7 +125,7 @@ def texture(self, texture: arcade.Texture):
self._add_to_atlas(texture)

@property
def program(self) -> gl.program.Program:
def program(self) -> Program:
"""Get or set the shader program.

Returns the default shader if no other shader is assigned.
Expand All @@ -152,7 +136,7 @@ def program(self) -> gl.program.Program:
return self._program

@program.setter
def program(self, program: gl.program.Program):
def program(self, program: Program):
if not self._initialized:
raise RuntimeError("The NinePatchTexture has not been initialized")

Expand Down Expand Up @@ -241,26 +225,22 @@ def draw_rect(
if not self._initialized:
self._init_deferred()

self._create_geometry(rect)

if blend:
self._ctx.enable_only(self._ctx.BLEND)
else:
self._ctx.disable(self._ctx.BLEND)

self.program.set_uniform_safe("texture_id", self._atlas.get_texture_id(self._texture))
if pixelated:
self._atlas.texture.filter = self._ctx.NEAREST, self._ctx.NEAREST
else:
self._atlas.texture.filter = self._ctx.LINEAR, self._ctx.LINEAR

self.program["position"] = rect.bottom_left
self.program["start"] = self._left, self._bottom
self.program["end"] = self.width - self._right, self.height - self._top
self.program["size"] = rect.size
self.program["t_size"] = self._texture.size

self._atlas.use_uv_texture(0)
self._atlas.texture.use(1)
self._geometry.render(self._program, vertices=1)

self._geometry.render(self._program)

if blend:
self._ctx.disable(self._ctx.BLEND)
Expand All @@ -282,3 +262,218 @@ def _check_sizes(self):
raise ValueError("Left and right border must be smaller than texture width")
if self._bottom + self._top > self._texture.height:
raise ValueError("Bottom and top border must be smaller than texture height")

def _init_deferred(self):
"""Deferred initialization when lazy loaded"""
self._ctx = arcade.get_window().ctx
# TODO: Cache in context?
self._program = self._ctx.load_program(
vertex_shader=":system:shaders/gui/nine_patch_vs.glsl",
fragment_shader=":system:shaders/gui/nine_patch_fs.glsl",
)
# Configure texture channels
self._program.set_uniform_safe("uv_texture", 0)
self._program["sprite_texture"] = 1

# 4 byte floats * 4 floats * 4 vertices * 9 patches
self._buffer = self._ctx.buffer(reserve=576)
# fmt: off
self._ibo = self._ctx.buffer(
data=array("i",
[
# Triangulate the patches
# First rot
0, 1, 2,
3, 1, 2,

4, 5, 6,
7, 5, 6,

8, 9, 10,
11, 9, 10,

# Middle row
12, 13, 14,
15, 13, 14,

16, 17, 18,
19, 17, 18,

20, 21, 22,
23, 21, 22,

# Bottom row
24, 25, 26,
27, 25, 26,

28, 29, 30,
31, 29, 30,

32, 33, 34,
35, 33, 34,
]
),
)
# fmt: on
self._geometry = self._ctx.geometry(
content=[BufferDescription(self._buffer, "2f 2f", ["in_position", "in_uv"])],
index_buffer=self._ibo,
mode=self._ctx.TRIANGLES,
index_element_size=4,
)

# References for the texture
self._atlas = self._custom_atlas or self._ctx.default_atlas
self._add_to_atlas(self.texture)

# NOTE: Important to create geometry after the texture is added to the atlas
# self._create_geometry(LBWH(0, 0, self.width, self.height))
self._initialized = True

def _create_geometry(self, rect: Rect):
"""Create vertices for the 9-patch texture."""
# NOTE: This was ported from glsl geometry shader to python
# Simulate old uniforms
cache_key = (self._left, self._right, self._bottom, self._top, rect)
if cache_key == self._geometry_cache:
return
self._geometry_cache = cache_key

position = rect.bottom_left
start = Vec2(self._left, self._bottom)
end = Vec2(self.width - self._right, self.height - self._top)
size = rect.size
t_size = Vec2(*self._texture.size)
atlas_size = Vec2(*self._atlas.size)

# Patch points starting from upper left row by row
p1 = position + Vec2(0.0, size.y)
p2 = position + Vec2(start.x, size.y)
p3 = position + Vec2(size.x - (t_size.x - end.x), size.y)
p4 = position + Vec2(size.x, size.y)

y = size.y - (t_size.y - end.y)
p5 = position + Vec2(0.0, y)
p6 = position + Vec2(start.x, y)
p7 = position + Vec2(size.x - (t_size.x - end.x), y)
p8 = position + Vec2(size.x, y)

p9 = position + Vec2(0.0, start.y)
p10 = position + Vec2(start.x, start.y)
p11 = position + Vec2(size.x - (t_size.x - end.x), start.y)
p12 = position + Vec2(size.x, start.y)

p13 = position + Vec2(0.0, 0.0)
p14 = position + Vec2(start.x, 0.0)
p15 = position + Vec2(size.x - (t_size.x - end.x), 0.0)
p16 = position + Vec2(size.x, 0.0)

# <AtlasRegion
# x=1 y=1
# width=100 height=100
# uvs=(
# 0.001953125, 0.001953125,
# 0.197265625, 0.001953125,
# 0.001953125, 0.197265625,
# 0.197265625, 0.197265625,
# )
# Get texture coordinates
# vec2 uv0, uv1, uv2, uv3
region = self._atlas.get_texture_region_info(self._texture.atlas_name)
tex_coords = region.texture_coordinates
uv0 = Vec2(tex_coords[0], tex_coords[1])
uv1 = Vec2(tex_coords[2], tex_coords[3])
uv2 = Vec2(tex_coords[4], tex_coords[5])
uv3 = Vec2(tex_coords[6], tex_coords[7])

# Local corner offsets in pixels
left = start.x
right = t_size.x - end.x
top = t_size.y - end.y
bottom = start.y

# UV offsets to the inner rectangle in the patch
# This is the global texture coordinate offset in the entire atlas
c1 = Vec2(left, top) / atlas_size # Upper left corner
c2 = Vec2(right, top) / atlas_size # Upper right corner
c3 = Vec2(left, bottom) / atlas_size # Lower left corner
c4 = Vec2(right, bottom) / atlas_size # Lower right corner

# Texture coordinates for all the points in the patch
t1 = uv0
t2 = uv0 + Vec2(c1.x, 0.0)
t3 = uv1 - Vec2(c2.x, 0.0)
t4 = uv1

t5 = uv0 + Vec2(0.0, c1.y)
t6 = uv0 + c1
t7 = uv1 + Vec2(-c2.x, c2.y)
t8 = uv1 + Vec2(0.0, c2.y)

t9 = uv2 - Vec2(0.0, c3.y)
t10 = uv2 + Vec2(c3.x, -c3.y)
t11 = uv3 - c4
t12 = uv3 - Vec2(0.0, c4.y)

t13 = uv2
t14 = uv2 + Vec2(c3.x, 0.0)
t15 = uv3 - Vec2(c4.x, 0.0)
t16 = uv3

# fmt: off
primitives = [
# First row - two fixed corners + stretchy middle
# Upper left corner. Fixed size.
p1, t1,
p5, t5,
p2, t2,
p6, t6,
# Upper middle part stretches on x axis
p2, t2,
p6, t6,
p3, t3,
p7, t7,
# Upper right corner. Fixed size
p3, t3,
p7, t7,
p4, t4,
p8, t8,

# Middle row: Two stretchy sides + stretchy middle
# left border sketching on y axis
p5, t5,
p9, t9,
p6, t6,
p10, t10,
# Center stretchy area
p6, t6,
p10, t10,
p7, t7,
p11, t11,
# Right border. Stenches on y axis
p7, t7,
p11, t11,
p8, t8,
p12, t12,

# Bottom row: two fixed corners + stretchy middle
# Lower left corner. Fixed size.
p9, t9,
p13, t13,
p10, t10,
p14, t14,
# Lower middle part stretches on x axis
p10, t10,
p14, t14,
p11, t11,
p15, t15,
# Lower right corner. Fixed size
p11, t11,
p15, t15,
p12, t12,
p16, t16,
]
# fmt: on

data = array("f", [coord for point in primitives for coord in point])
self._buffer.write(data.tobytes())
4 changes: 2 additions & 2 deletions arcade/resources/system/shaders/gui/nine_patch_fs.glsl
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#version 330

uniform sampler2D sprite_texture;
out vec4 f_color;
out vec4 fragColor;

in vec2 uv;

void main() {
f_color = texture(sprite_texture, uv);
fragColor = texture(sprite_texture, uv);
}
Loading
Loading