From 25d77aa94ffa4a1079170b2b274a0de8080d2aec Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 25 May 2025 12:14:48 +0200 Subject: [PATCH 01/21] Initial work --- .../sprites/sprite_list_geometry_fs.glsl | 8 ++--- .../sprites/sprite_list_simple_fs.glsl | 22 +++++++++++++ .../sprites/sprite_list_simple_vs.glsl | 32 +++++++++++++++++++ arcade/sprite_list/sprite_list.py | 12 +++++-- 4 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl create mode 100644 arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl diff --git a/arcade/resources/system/shaders/sprites/sprite_list_geometry_fs.glsl b/arcade/resources/system/shaders/sprites/sprite_list_geometry_fs.glsl index 0149f48c24..7032ae354f 100644 --- a/arcade/resources/system/shaders/sprites/sprite_list_geometry_fs.glsl +++ b/arcade/resources/system/shaders/sprites/sprite_list_geometry_fs.glsl @@ -11,11 +11,11 @@ in vec4 gs_color; out vec4 f_color; void main() { - vec4 basecolor = texture(sprite_texture, gs_uv); - basecolor *= gs_color * spritelist_color; + vec4 base_color = texture(sprite_texture, gs_uv); + base_color *= gs_color * spritelist_color; // Alpha test - if (basecolor.a == 0.0) { + if (base_color.a == 0.0) { discard; } - f_color = basecolor; + f_color = base_color; } diff --git a/arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl b/arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl new file mode 100644 index 0000000000..33c8d7b397 --- /dev/null +++ b/arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl @@ -0,0 +1,22 @@ +#version 330 +// vert/frag only version of the sprite list shader + +// Texture atlas +uniform sampler2D sprite_texture; +// Global color set on the sprite list +uniform vec4 spritelist_color; + +in vec2 uv; +in vec4 color; + +out vec4 f_color; + +void main() { + vec4 base_color = texture(sprite_texture, uv); + base_color *= color * spritelist_color; + // Alpha test + if (base_color.a == 0.0) { + discard; + } + f_color = base_color; +} diff --git a/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl new file mode 100644 index 0000000000..1d8bacb85c --- /dev/null +++ b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl @@ -0,0 +1,32 @@ +#version 330 +// vert/frag only version of the sprite list shader + +// Texture atlas +uniform sampler2D sprite_texture; +// Texture containing UVs for the entire atlas +uniform sampler2D uv_texture; +// How much half-pixel offset to apply to the UVs. +// 0.0 is no offset, 1.0 is half a pixel offset +uniform float uv_offset_bias; + +in vec3 in_pos; +in float in_angle; +in vec2 in_size; +in float in_texture; +in vec4 in_color; +// + +out vec2 uv; +out vec4 color; + +#include :system:shaders/lib/sprite.glsl + +void main() { + // Read texture coordinates from UV texture here + vec2 uv0, uv1, uv2, uv3; + getSpriteUVs(uv_texture, int(in_texture), uv0, uv1, uv2, uv3); + + // TODO: Half pixel offset + // TODO: Position, rotation, size, color, uvs + +} diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index 8c07f359ff..b77544cc82 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -301,9 +301,6 @@ def _init_deferred(self) -> None: return self.ctx = get_window().ctx - self.program = self.ctx.sprite_list_program_cull - if not self._atlas: - self._atlas = self.ctx.default_atlas # Buffers for each sprite attribute (read by shader) with initial capacity self._sprite_pos_buf = self.ctx.buffer(reserve=self._buf_capacity * 12) # 3 x 32 bit floats @@ -327,11 +324,20 @@ def _init_deferred(self) -> None: ["in_color"], ), ] + # Geometry shader version + self.program = self.ctx.sprite_list_program_cull + if not self._atlas: + self._atlas = self.ctx.default_atlas self._geometry = self.ctx.geometry( contents, index_buffer=self._sprite_index_buf, index_element_size=4, # 32 bit integers ) + # Vertex / fragment shader version + program = self.ctx.load_program( + vertex_shader=":system:shaders/sprites/sprite_list_simple_vs.glsl", + fragment_shader=":system:shaders/sprites/sprite_list_simple_fs.glsl", + ) self._initialized = True From 0139268f5eca1e5682b8047d58b09b7ffbfeefe0 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Mon, 26 May 2025 01:48:05 +0200 Subject: [PATCH 02/21] Initial attempt separating out spritelist data --- .../sprites/sprite_list_simple_vs.glsl | 17 +- arcade/sprite_list/collision.py | 2 +- arcade/sprite_list/sprite_list.py | 735 +++++++++++------- 3 files changed, 451 insertions(+), 303 deletions(-) diff --git a/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl index 1d8bacb85c..46616b9d5e 100644 --- a/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl +++ b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl @@ -9,12 +9,14 @@ uniform sampler2D uv_texture; // 0.0 is no offset, 1.0 is half a pixel offset uniform float uv_offset_bias; -in vec3 in_pos; -in float in_angle; -in vec2 in_size; -in float in_texture; -in vec4 in_color; -// +// Per sprite/instance data +in vec3 in_instance_pos; +in float in_instance_angle; +in vec2 in_instance_size; +in float in_instance_texture; // NOTE: This is a float due to compatibility +in vec4 in_instance_color; +// Instanced geometry (rectangle as triangle strip) +in vec2 in_pos; out vec2 uv; out vec4 color; @@ -24,9 +26,8 @@ out vec4 color; void main() { // Read texture coordinates from UV texture here vec2 uv0, uv1, uv2, uv3; - getSpriteUVs(uv_texture, int(in_texture), uv0, uv1, uv2, uv3); + getSpriteUVs(uv_texture, int(in_instance_texture), uv0, uv1, uv2, uv3); // TODO: Half pixel offset // TODO: Position, rotation, size, color, uvs - } diff --git a/arcade/sprite_list/collision.py b/arcade/sprite_list/collision.py index 60330bb1eb..8cc674512e 100644 --- a/arcade/sprite_list/collision.py +++ b/arcade/sprite_list/collision.py @@ -150,7 +150,7 @@ def _get_nearby_sprites( # Run the transform shader emitting sprites close to the configured position and size. # This runs in a query so we can measure the number of sprites emitted. with ctx.collision_query: - sprite_list.geometry.transform( # type: ignore + sprite_list.sprite_data.geometry.transform( # type: ignore ctx.collision_detection_program, buffer, vertices=sprite_count, diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index b77544cc82..d429b6bd64 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -28,7 +28,7 @@ from arcade.utils import copy_dunders_unimplemented if TYPE_CHECKING: - from arcade import DefaultTextureAtlas, Texture + from arcade import Texture, ArcadeContext from arcade.texture_atlas import TextureAtlasBase # The default capacity from spritelists @@ -253,17 +253,7 @@ def __init__( # Index buffer self._sprite_index_data = array("i", [0] * self._idx_capacity) - # Define and annotate storage space for buffers - self._sprite_pos_buf: Buffer | None = None - self._sprite_size_buf: Buffer | None = None - self._sprite_angle_buf: Buffer | None = None - self._sprite_color_buf: Buffer | None = None - self._sprite_texture_buf: Buffer | None = None - - # Index buffer - self._sprite_index_buf: Buffer | None = None - - self._geometry: Geometry | None = None + self._spritelist_data: SpriteListData # Flags for signaling if a buffer needs to be written to the OpenGL buffer self._sprite_pos_changed: bool = False @@ -301,47 +291,15 @@ def _init_deferred(self) -> None: return self.ctx = get_window().ctx - - # Buffers for each sprite attribute (read by shader) with initial capacity - self._sprite_pos_buf = self.ctx.buffer(reserve=self._buf_capacity * 12) # 3 x 32 bit floats - self._sprite_size_buf = self.ctx.buffer(reserve=self._buf_capacity * 8) # 2 x 32 bit floats - self._sprite_angle_buf = self.ctx.buffer(reserve=self._buf_capacity * 4) # 32 bit float - self._sprite_color_buf = self.ctx.buffer(reserve=self._buf_capacity * 4) # 4 x bytes colors - self._sprite_texture_buf = self.ctx.buffer(reserve=self._buf_capacity * 4) # 32 bit int - # Index buffer - self._sprite_index_buf = self.ctx.buffer( - reserve=self._idx_capacity * 4 - ) # 32 bit unsigned integers - - contents = [ - gl.BufferDescription(self._sprite_pos_buf, "3f", ["in_pos"]), - gl.BufferDescription(self._sprite_size_buf, "2f", ["in_size"]), - gl.BufferDescription(self._sprite_angle_buf, "1f", ["in_angle"]), - gl.BufferDescription(self._sprite_texture_buf, "1f", ["in_texture"]), - gl.BufferDescription( - self._sprite_color_buf, - "4f1", - ["in_color"], - ), - ] - # Geometry shader version - self.program = self.ctx.sprite_list_program_cull if not self._atlas: self._atlas = self.ctx.default_atlas - self._geometry = self.ctx.geometry( - contents, - index_buffer=self._sprite_index_buf, - index_element_size=4, # 32 bit integers + self._spritelist_data = SpriteListBufferData( + self.ctx, capacity=self._buf_capacity, atlas=self._atlas ) - # Vertex / fragment shader version - program = self.ctx.load_program( - vertex_shader=":system:shaders/sprites/sprite_list_simple_vs.glsl", - fragment_shader=":system:shaders/sprites/sprite_list_simple_fs.glsl", - ) - self._initialized = True # Load all the textures and write texture coordinates into buffers. + # This is important for lazy spritelists. for sprite in self.sprite_list: if sprite._texture is None: raise ValueError("Attempting to use a sprite without a texture") @@ -508,131 +466,12 @@ def atlas(self) -> TextureAtlasBase | None: return self._atlas @property - def geometry(self) -> Geometry: - """ - Returns the internal OpenGL geometry for this spritelist. - This can be used to execute custom shaders with the - spritelist data. - - One or multiple of the following inputs must be defined in your vertex shader:: - - in vec2 in_pos; - in float in_angle; - in vec2 in_size; - in float in_texture; - in vec4 in_color; - """ + def sprite_data(self) -> SpriteListData: + """Get the sprite data for this spritelist.""" if not self._initialized: self.initialize() - return self._geometry # type: ignore - - @property - def buffer_positions(self) -> Buffer: - """ - Get the internal OpenGL position buffer for this spritelist. - - The buffer contains 32 bit float values with - x, y and z positions. These are the center positions - for each sprite. - - This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` - instance with name ``in_pos``. - """ - if self._sprite_pos_buf is None: - raise ValueError("SpriteList is not initialized") - return self._sprite_pos_buf - - @property - def buffer_sizes(self) -> Buffer: - """ - Get the internal OpenGL size buffer for this spritelist. - - The buffer contains 32 bit float width and height values. - - This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` - instance with name ``in_size``. - """ - if self._sprite_size_buf is None: - raise ValueError("SpriteList is not initialized") - return self._sprite_size_buf - - @property - def buffer_angles(self) -> Buffer: - """ - Get the internal OpenGL angle buffer for the spritelist. - - This buffer contains a series of 32 bit floats - representing the rotation angle for each sprite in degrees. - - This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` - instance with name ``in_angle``. - """ - if self._sprite_angle_buf is None: - raise ValueError("SpriteList is not initialized") - return self._sprite_angle_buf - - @property - def buffer_colors(self) -> Buffer: - """ - Get the internal OpenGL color buffer for this spritelist. - - This buffer contains a series of 32 bit floats representing - the RGBA color for each sprite. 4 x floats = RGBA. - - - This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` - instance with name ``in_color``. - """ - if self._sprite_color_buf is None: - raise ValueError("SpriteList is not initialized") - return self._sprite_color_buf - - @property - def buffer_textures(self) -> Buffer: - """ - Get the internal openGL texture id buffer for the spritelist. - - This buffer contains a series of single 32 bit floats referencing - a texture ID. This ID references a texture in the texture - atlas assigned to this spritelist. The ID is used to look up - texture coordinates in a 32bit floating point texture the - texture atlas provides. This system makes sure we can resize - and rebuild a texture atlas without having to rebuild every - single spritelist. - - This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` - instance with name ``in_texture``. - - Note that it should ideally an unsigned integer, but due to - compatibility we store them as 32 bit floats. We cast them - to integers in the shader. - """ - if self._sprite_texture_buf is None: - raise ValueError("SpriteList is not initialized") - return self._sprite_texture_buf - - @property - def buffer_indices(self) -> Buffer: - """ - Get the internal index buffer for this spritelist. - - The data in the other buffers are not in the correct order - matching ``spritelist[i]``. The index buffer has to be - used used to resolve the right order. It simply contains - a series of integers referencing locations in the other buffers. - - Also note that the length of this buffer might be bigger than - the number of sprites. Rely on ``len(spritelist)`` for the - correct length. - - This index buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` - instance and will be automatically be applied the the input buffers - when rendering or transforming. - """ - if self._sprite_index_buf is None: - raise ValueError("SpriteList is not initialized") - return self._sprite_index_buf + return self._spritelist_data # type: ignore[return-value] def _next_slot(self) -> int: """ @@ -876,7 +715,6 @@ def insert(self, index: int, sprite: SpriteType) -> None: self._update_all(sprite) # Allocate room in the index buffer - self._normalize_index_buffer() # idx_slot = self._sprite_index_slots self._sprite_index_slots += 1 self._grow_index_buffer() @@ -888,9 +726,6 @@ def insert(self, index: int, sprite: SpriteType) -> None: def reverse(self) -> None: """Reverses the current list in-place""" - # Ensure the index buffer is normalized - self._normalize_index_buffer() - # Reverse the sprites and index buffer self.sprite_list.reverse() # This seems to be the reasonable way to reverse a subset of an array @@ -906,9 +741,6 @@ def shuffle(self) -> None: # to shuffle the sprite_list and index buffer in # in the same operation. We don't change the sprite buffers - # Make sure the index buffer is the same length as the sprite list - self._normalize_index_buffer() - # zip index and sprite into pairs and shuffle pairs = list(zip(self.sprite_list, self._sprite_index_data)) random.shuffle(pairs) @@ -951,9 +783,6 @@ def create_y_pos_comparison(sprite): reverse: If set to ``True`` the sprites will be sorted in reverse """ - # Ensure the index buffer is normalized - self._normalize_index_buffer() - # In-place sort the spritelist self.sprite_list.sort(key=key, reverse=reverse) # Loop over the sorted sprites and assign new values in index buffer @@ -1056,35 +885,25 @@ def write_sprite_buffers_to_gpu(self) -> None: self._write_sprite_buffers_to_gpu() def _write_sprite_buffers_to_gpu(self) -> None: - if self._sprite_pos_changed and self._sprite_pos_buf: - self._sprite_pos_buf.orphan() - self._sprite_pos_buf.write(self._sprite_pos_data) - self._sprite_pos_changed = False - - if self._sprite_size_changed and self._sprite_size_buf: - self._sprite_size_buf.orphan() - self._sprite_size_buf.write(self._sprite_size_data) - self._sprite_size_changed = False - - if self._sprite_angle_changed and self._sprite_angle_buf: - self._sprite_angle_buf.orphan() - self._sprite_angle_buf.write(self._sprite_angle_data) - self._sprite_angle_changed = False - - if self._sprite_color_changed and self._sprite_color_buf: - self._sprite_color_buf.orphan() - self._sprite_color_buf.write(self._sprite_color_data) - self._sprite_color_changed = False - - if self._sprite_texture_changed and self._sprite_texture_buf: - self._sprite_texture_buf.orphan() - self._sprite_texture_buf.write(self._sprite_texture_data) - self._sprite_texture_changed = False + if not self._initialized: + self._init_deferred() - if self._sprite_index_changed and self._sprite_index_buf: - self._sprite_index_buf.orphan() - self._sprite_index_buf.write(self._sprite_index_data) - self._sprite_index_changed = False + self._spritelist_data.write_sprite_buffers_to_gpu( + # Buffer data + self._sprite_pos_data, + self._sprite_size_data, + self._sprite_angle_data, + self._sprite_color_data, + self._sprite_texture_data, + self._sprite_index_data, + # Changed flags + self._sprite_pos_changed, + self._sprite_size_changed, + self._sprite_angle_changed, + self._sprite_color_changed, + self._sprite_texture_changed, + self._sprite_index_changed, + ) def initialize(self) -> None: """ @@ -1113,69 +932,18 @@ def draw( return self._init_deferred() - if not self.program: - raise ValueError("Attempting to render without shader program.") self._write_sprite_buffers_to_gpu() - - prev_blend_func = self.ctx.blend_func - if self._blend: - self.ctx.enable(self.ctx.BLEND) - # Set custom blend function or revert to default - if blend_function is not None: - self.ctx.blend_func = blend_function - else: - self.ctx.blend_func = self.ctx.BLEND_DEFAULT - else: - self.ctx.disable(self.ctx.BLEND) - - # Workarounds for Optional[TextureAtlas] + slow . lookup speed - atlas: DefaultTextureAtlas = self.atlas # type: ignore - atlas_texture: Texture2D = atlas.texture - - # Set custom filter or reset to default - if filter: - if hasattr( - filter, - "__len__", - ): # assume it's a collection - if len(cast(Sized, filter)) != 2: - raise ValueError("Can't use sequence of length != 2") - atlas_texture.filter = tuple(filter) # type: ignore - else: # assume it's an int - atlas_texture.filter = cast(OpenGlFilter, (filter, filter)) - else: - # Handle the pixelated shortcut if filter is not set - if pixelated: - atlas_texture.filter = self.ctx.NEAREST, self.ctx.NEAREST - else: - atlas_texture.filter = self.DEFAULT_TEXTURE_FILTER - - self.program["spritelist_color"] = self._color - - # Control center pixel interpolation: - # 0.0 = raw interpolation using texture corners - # 1.0 = center pixel interpolation - if self.ctx.NEAREST in atlas_texture.filter: - self.program.set_uniform_safe("uv_offset_bias", 0.0) - else: - self.program.set_uniform_safe("uv_offset_bias", 1.0) - - atlas_texture.use(0) - atlas.use_uv_texture(1) - if not self._geometry: - raise ValueError("Attempting to render without '_geometry' field being set.") - self._geometry.render( - self.program, - mode=self.ctx.POINTS, - vertices=self._sprite_index_slots, + self._spritelist_data.render( + atlas=self._atlas, # type: ignore + count=self._sprite_index_slots, + color=self._color, + default_texture_filter=self.DEFAULT_TEXTURE_FILTER, + filter=filter, + pixelated=pixelated, + blend_function=blend_function, + blend=self._blend, ) - # Leave global states to default - if self._blend: - self.ctx.disable(self.ctx.BLEND) - if blend_function is not None: - self.ctx.blend_func = prev_blend_func - def draw_hit_boxes( self, color: RGBOrA255 = (0, 0, 0, 255), line_thickness: float = 1.0 ) -> None: @@ -1196,25 +964,6 @@ def draw_hit_boxes( arcade.draw_lines(points, color=converted_color, line_width=line_thickness) - def _normalize_index_buffer(self) -> None: - """ - Removes unused slots in the index buffer. - The other buffers don't need this because they re-use slots. - New sprites on the other hand always needs to be added - to the end of the index buffer to preserve order - """ - # NOTE: Currently we keep the index buffer normalized - # but we can increase the performance in the future - # delaying normalization. - # Need counter for how many slots are used in index buffer. - # 1) Sort the deleted indices (descending) and pop() them in a loop - # 2) Create a new array.array and manually copy every - # item in the list except the deleted index slots - # 3) Use a transform (gpu) to trim the index buffer and - # read this buffer back into a new array using array.from_bytes - # NOTE: Right now the index buffer is always normalized - pass - def _grow_sprite_buffers(self) -> None: """Double the internal buffer sizes""" # Resize sprite buffers if needed @@ -1233,12 +982,7 @@ def _grow_sprite_buffers(self) -> None: self._sprite_texture_data.extend([0] * extend_by) if self._initialized: - # Proper initialization implies these buffers are allocated - self._sprite_pos_buf.orphan(double=True) # type: ignore - self._sprite_size_buf.orphan(double=True) # type: ignore - self._sprite_angle_buf.orphan(double=True) # type: ignore - self._sprite_color_buf.orphan(double=True) # type: ignore - self._sprite_texture_buf.orphan(double=True) # type: ignore + self._spritelist_data.grow_sprite_buffers() self._sprite_pos_changed = True self._sprite_size_changed = True @@ -1248,6 +992,7 @@ def _grow_sprite_buffers(self) -> None: def _grow_index_buffer(self) -> None: # Extend the index buffer capacity if needed + # TODO: We might not need this any more since index buffer is always normalized if self._sprite_index_slots <= self._idx_capacity: return @@ -1255,8 +1000,8 @@ def _grow_index_buffer(self) -> None: self._idx_capacity = self._idx_capacity * 2 self._sprite_index_data.extend([0] * extend_by) - if self._initialized and self._sprite_index_buf: - self._sprite_index_buf.orphan(size=self._idx_capacity * 4) + if self._initialized: + self._spritelist_data.grow_index_buffer() self._sprite_index_changed = True @@ -1453,3 +1198,405 @@ def _update_angle(self, sprite: SpriteType) -> None: slot = self.sprite_slot[sprite] self._sprite_angle_data[slot] = sprite._angle self._sprite_angle_changed = True + + +class SpriteListData: + """Base class for sprite list data.""" + + def __init__(self, ctx: ArcadeContext, capacity: int) -> None: + self.ctx = ctx + self._buf_capacity = capacity + self._idx_capacity = capacity + + def write_sprite_buffers_to_gpu( + self, + # The data itself + sprite_pos_data, + sprite_size_data, + sprite_angle_data, + sprite_color_data, + sprite_texture_data, + sprite_index_data, + # Changed flags + sprite_pos_changed: bool = True, + sprite_size_changed: bool = True, + sprite_angle_changed: bool = True, + sprite_color_changed: bool = True, + sprite_texture_changed: bool = True, + sprite_index_changed: bool = True, + ) -> None: + """ + Write the sprite buffers to the GPU. + + Args: + sprite_pos_data: Array of sprite positions. + sprite_size_data: Array of sprite sizes. + sprite_angle_data: Array of sprite angles. + sprite_color_data: Array of sprite colors. + sprite_texture_data: Array of sprite texture IDs. + sprite_index_data: Array of sprite indices. + sprite_pos_changed: Whether the position data has changed. + sprite_size_changed: Whether the size data has changed. + sprite_angle_changed: Whether the angle data has changed. + sprite_color_changed: Whether the color data has changed. + sprite_texture_changed: Whether the texture data has changed. + sprite_index_changed: Whether the index data has changed. + """ + raise NotImplementedError("This method should be implemented in subclasses.") + + def grow_sprite_buffers(self) -> None: + """ + Grow the sprite buffer to accommodate more sprites. + + This method is called when the internal buffer capacity is exceeded. + It should increase the buffer size and prepare for more sprites. + """ + raise NotImplementedError("This method should be implemented in subclasses.") + + def grow_index_buffer(self) -> None: + """ + Grow the index buffer to accommodate more sprites. + + This method is called when the internal index buffer capacity is exceeded. + It should increase the index buffer size and prepare for more sprites. + """ + raise NotImplementedError("This method should be implemented in subclasses.") + + def render( + self, + *, + atlas: TextureAtlasBase, + count: int, + color: tuple[float, float, float, float], + default_texture_filter: OpenGlFilter, + filter: PyGLenum | OpenGlFilter | None = None, + pixelated: bool | None = None, + blend_function: BlendFunction | None = None, + blend: bool = True, + ) -> None: + """ + Render the sprite list using the provided shader program. + + Args: + filter: Texture filter to use. + pixelated: Whether to use pixelated rendering. + blend_function: Blend function to use for rendering. + """ + raise NotImplementedError("This method should be implemented in subclasses.") + + +class SpriteListBufferData(SpriteListData): + """Container for all gpu data used by the SpriteList.""" + + def __init__(self, ctx: ArcadeContext, capacity: int, atlas: TextureAtlasBase) -> None: + self.ctx = ctx + self._buf_capacity = capacity + self._idx_capacity = capacity + self._atlas = atlas + + # Buffers for each sprite attribute (read by shader) with initial capacity + self._sprite_pos_buf = self.ctx.buffer(reserve=self._buf_capacity * 12) # 3 x 32 bit floats + self._sprite_size_buf = self.ctx.buffer(reserve=self._buf_capacity * 8) # 2 x 32 bit floats + self._sprite_angle_buf = self.ctx.buffer(reserve=self._buf_capacity * 4) # 32 bit float + self._sprite_color_buf = self.ctx.buffer(reserve=self._buf_capacity * 4) # 4 x bytes colors + self._sprite_texture_buf = self.ctx.buffer(reserve=self._buf_capacity * 4) # 32 bit int + # Index buffer + self._sprite_index_buf = self.ctx.buffer( + reserve=self._idx_capacity * 4 + ) # 32 bit unsigned integers + + contents = [ + gl.BufferDescription(self._sprite_pos_buf, "3f", ["in_pos"]), + gl.BufferDescription(self._sprite_size_buf, "2f", ["in_size"]), + gl.BufferDescription(self._sprite_angle_buf, "1f", ["in_angle"]), + gl.BufferDescription(self._sprite_texture_buf, "1f", ["in_texture"]), + gl.BufferDescription( + self._sprite_color_buf, + "4f1", + ["in_color"], + ), + ] + # Geometry shader version + self.program = self.ctx.sprite_list_program_cull + if not self._atlas: + self._atlas = self.ctx.default_atlas + self._geometry = self.ctx.geometry( + contents, + index_buffer=self._sprite_index_buf, + index_element_size=4, # 32 bit integers + ) + + @property + def geometry(self) -> Geometry: + """ + Returns the internal OpenGL geometry for this spritelist. + This can be used to execute custom shaders with the + spritelist data. + + One or multiple of the following inputs must be defined in your vertex shader:: + + in vec2 in_pos; + in float in_angle; + in vec2 in_size; + in float in_texture; + in vec4 in_color; + """ + return self._geometry # type: ignore + + @property + def buffer_positions(self) -> Buffer: + """ + Get the internal OpenGL position buffer for this spritelist. + + The buffer contains 32 bit float values with + x, y and z positions. These are the center positions + for each sprite. + + This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` + instance with name ``in_pos``. + """ + return self._sprite_pos_buf + + @property + def buffer_sizes(self) -> Buffer: + """ + Get the internal OpenGL size buffer for this spritelist. + + The buffer contains 32 bit float width and height values. + + This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` + instance with name ``in_size``. + """ + if self._sprite_size_buf is None: + raise ValueError("SpriteList is not initialized") + return self._sprite_size_buf + + @property + def buffer_angles(self) -> Buffer: + """ + Get the internal OpenGL angle buffer for the spritelist. + + This buffer contains a series of 32 bit floats + representing the rotation angle for each sprite in degrees. + + This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` + instance with name ``in_angle``. + """ + return self._sprite_angle_buf + + @property + def buffer_colors(self) -> Buffer: + """ + Get the internal OpenGL color buffer for this spritelist. + + This buffer contains a series of 32 bit floats representing + the RGBA color for each sprite. 4 x floats = RGBA. + + This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` + instance with name ``in_color``. + """ + return self._sprite_color_buf + + @property + def buffer_textures(self) -> Buffer: + """ + Get the internal openGL texture id buffer for the spritelist. + + This buffer contains a series of single 32 bit floats referencing + a texture ID. This ID references a texture in the texture + atlas assigned to this spritelist. The ID is used to look up + texture coordinates in a 32bit floating point texture the + texture atlas provides. This system makes sure we can resize + and rebuild a texture atlas without having to rebuild every + single spritelist. + + This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` + instance with name ``in_texture``. + + Note that it should ideally an unsigned integer, but due to + compatibility we store them as 32 bit floats. We cast them + to integers in the shader. + """ + return self._sprite_texture_buf + + @property + def buffer_indices(self) -> Buffer: + """ + Get the internal index buffer for this spritelist. + + The data in the other buffers are not in the correct order + matching ``spritelist[i]``. The index buffer has to be + used used to resolve the right order. It simply contains + a series of integers referencing locations in the other buffers. + + Also note that the length of this buffer might be bigger than + the number of sprites. Rely on ``len(spritelist)`` for the + correct length. + + This index buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` + instance and will be automatically be applied the the input buffers + when rendering or transforming. + """ + return self._sprite_index_buf + + def write_sprite_buffers_to_gpu( + self, + # The data itself + sprite_pos_data, + sprite_size_data, + sprite_angle_data, + sprite_color_data, + sprite_texture_data, + sprite_index_data, + # Changed flags + sprite_pos_changed: bool = True, + sprite_size_changed: bool = True, + sprite_angle_changed: bool = True, + sprite_color_changed: bool = True, + sprite_texture_changed: bool = True, + sprite_index_changed: bool = True, + ) -> None: + """ + Write the sprite buffers to the GPU. + + Args: + sprite_pos_data: Array of sprite positions. + sprite_size_data: Array of sprite sizes. + sprite_angle_data: Array of sprite angles. + sprite_color_data: Array of sprite colors. + sprite_texture_data: Array of sprite texture IDs. + sprite_index_data: Array of sprite indices. + sprite_pos_changed: Whether the position data has changed. + sprite_size_changed: Whether the size data has changed. + sprite_angle_changed: Whether the angle data has changed. + sprite_color_changed: Whether the color data has changed. + sprite_texture_changed: Whether the texture data has changed. + sprite_index_changed: Whether the index data has changed. + """ + if sprite_pos_changed: + self._sprite_pos_buf.orphan() + self._sprite_pos_buf.write(sprite_pos_data) + self._sprite_pos_changed = False + + if sprite_size_changed: + self._sprite_size_buf.orphan() + self._sprite_size_buf.write(sprite_size_data) + self._sprite_size_changed = False + + if sprite_angle_changed: + self._sprite_angle_buf.orphan() + self._sprite_angle_buf.write(sprite_angle_data) + self._sprite_angle_changed = False + + if sprite_color_changed: + self._sprite_color_buf.orphan() + self._sprite_color_buf.write(sprite_color_data) + self._sprite_color_changed = False + + if sprite_texture_changed: + self._sprite_texture_buf.orphan() + self._sprite_texture_buf.write(sprite_texture_data) + self._sprite_texture_changed = False + + if sprite_index_changed: + self._sprite_index_buf.orphan() + self._sprite_index_buf.write(sprite_index_data) + self._sprite_index_changed = False + + def grow_sprite_buffers(self) -> None: + # Proper initialization implies these buffers are allocated + self._sprite_pos_buf.orphan(double=True) + self._sprite_size_buf.orphan(double=True) + self._sprite_angle_buf.orphan(double=True) + self._sprite_color_buf.orphan(double=True) + self._sprite_texture_buf.orphan(double=True) + + def grow_index_buffer(self) -> None: + self._sprite_index_buf.orphan(double=True) + + def render( + self, + *, + atlas: TextureAtlasBase, + count: int, + color: tuple[float, float, float, float], + default_texture_filter: OpenGlFilter, + filter: PyGLenum | OpenGlFilter | None = None, + pixelated: bool | None = None, + blend_function: BlendFunction | None = None, + blend: bool = True, + ) -> None: + """ + Render the sprite list using the provided shader program. + + Args: + filter: Texture filter to use. + pixelated: Whether to use pixelated rendering. + blend_function: Blend function to use for rendering. + """ + if not self.program: + raise ValueError("Attempting to render without shader program.") + + prev_blend_func = self.ctx.blend_func + if blend: + self.ctx.enable(self.ctx.BLEND) + # Set custom blend function or revert to default + if blend_function is not None: + self.ctx.blend_func = blend_function + else: + self.ctx.blend_func = self.ctx.BLEND_DEFAULT + else: + self.ctx.disable(self.ctx.BLEND) + + atlas_texture: Texture2D = atlas.texture + + # Set custom filter or reset to default + if filter: + if hasattr( + filter, + "__len__", + ): # assume it's a collection + if len(cast(Sized, filter)) != 2: + raise ValueError("Can't use sequence of length != 2") + atlas_texture.filter = tuple(filter) # type: ignore + else: # assume it's an int + atlas_texture.filter = cast(OpenGlFilter, (filter, filter)) + else: + # Handle the pixelated shortcut if filter is not set + if pixelated: + atlas_texture.filter = self.ctx.NEAREST, self.ctx.NEAREST + else: + atlas_texture.filter = default_texture_filter + + self.program["spritelist_color"] = color + + # Control center pixel interpolation: + # 0.0 = raw interpolation using texture corners + # 1.0 = center pixel interpolation + if self.ctx.NEAREST in atlas_texture.filter: + self.program.set_uniform_safe("uv_offset_bias", 0.0) + else: + self.program.set_uniform_safe("uv_offset_bias", 1.0) + + atlas_texture.use(0) + atlas.use_uv_texture(1) + if not self._geometry: + raise ValueError("Attempting to render without '_geometry' field being set.") + self._geometry.render( + self.program, + mode=self.ctx.POINTS, + vertices=count, + ) + + # Leave global states to default + if blend: + self.ctx.disable(self.ctx.BLEND) + if blend_function is not None: + self.ctx.blend_func = prev_blend_func + + +# Program without geo shader for later +# program = self.ctx.load_program( +# vertex_shader=":system:shaders/sprites/sprite_list_simple_vs.glsl", +# fragment_shader=":system:shaders/sprites/sprite_list_simple_fs.glsl", +# ) From 2173d950c5218dd908fb0091f3c12011aae14139 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Wed, 28 May 2025 23:49:33 +0200 Subject: [PATCH 03/21] More spritelist rendering work * Merge pos and angle buffers * Prepare data for non-geo shaders * Create renderer for non-geo shaders --- arcade/context.py | 13 + .../sprites/sprite_list_geometry_vs.glsl | 7 +- .../sprites/sprite_list_simple_vs.glsl | 12 +- arcade/sprite_list/sprite_list.py | 265 ++++++++++++------ 4 files changed, 204 insertions(+), 93 deletions(-) diff --git a/arcade/context.py b/arcade/context.py index 58d5f94b5f..4cbc54bec7 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -99,6 +99,19 @@ def __init__( self.sprite_list_program_cull["sprite_texture"] = 0 self.sprite_list_program_cull["uv_texture"] = 1 + # self.sprite_list_program_no_geo = self.load_program( + # vertex_shader=":system:shaders/sprites/sprite_list_simple_vs.glsl", + # fragment_shader=":system:shaders/sprites/sprite_list_simple_fs.glsl", + # ) + # self.sprite_list_program_no_geo["sprite_texture"] = 0 + # self.sprite_list_program_no_geo["uv_texture"] = 1 + # # Per-instance data + # self.sprite_list_program_no_geo["pos_data"] = 2 + # self.sprite_list_program_no_geo["size_data"] = 3 + # self.sprite_list_program_no_geo["color_data"] = 4 + # self.sprite_list_program_no_geo["texture_id_data"] = 5 + # self.sprite_list_program_no_geo["index_data"] = 6 + self.sprite_program_single = self.load_program( vertex_shader=":system:shaders/sprites/sprite_single_vs.glsl", geometry_shader=":system:shaders/sprites/sprite_list_geometry_no_cull_geo.glsl", diff --git a/arcade/resources/system/shaders/sprites/sprite_list_geometry_vs.glsl b/arcade/resources/system/shaders/sprites/sprite_list_geometry_vs.glsl index ddc169c8ec..9f6c8fe247 100644 --- a/arcade/resources/system/shaders/sprites/sprite_list_geometry_vs.glsl +++ b/arcade/resources/system/shaders/sprites/sprite_list_geometry_vs.glsl @@ -1,7 +1,6 @@ #version 330 -in vec3 in_pos; -in float in_angle; +in vec4 in_pos; in vec2 in_size; in float in_texture; in vec4 in_color; @@ -12,8 +11,8 @@ out vec2 v_size; out float v_texture; void main() { - gl_Position = vec4(in_pos, 1.0); - v_angle = in_angle; + gl_Position = vec4(in_pos.xyz, 1.0); + v_angle = in_pos.w; v_color = in_color; v_size = in_size; v_texture = in_texture; diff --git a/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl index 46616b9d5e..234af9a52a 100644 --- a/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl +++ b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl @@ -5,16 +5,20 @@ uniform sampler2D sprite_texture; // Texture containing UVs for the entire atlas uniform sampler2D uv_texture; + +// Per instance data +uniform sampler2D pos_data; +uniform sampler2D size_data; +uniform sampler2D color_data; +uniform isampler2D texture_id_data; +uniform isampler2D index_data; + // How much half-pixel offset to apply to the UVs. // 0.0 is no offset, 1.0 is half a pixel offset uniform float uv_offset_bias; // Per sprite/instance data in vec3 in_instance_pos; -in float in_instance_angle; -in vec2 in_instance_size; -in float in_instance_texture; // NOTE: This is a float due to compatibility -in vec4 in_instance_color; // Instanced geometry (rectangle as triangle strip) in vec2 in_pos; diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index d429b6bd64..6e300bcc7a 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -245,9 +245,10 @@ def __init__( self.sprite_slot: dict[SpriteType, int] = dict() # Python representation of buffer data - self._sprite_pos_data = array("f", [0] * self._buf_capacity * 3) + # NOTE: The number of components must be 1, 2 or 4. 3 floats is not supported + # for most iGPUs due to alignment issues. + self._sprite_pos_angle_data = array("f", [0] * self._buf_capacity * 4) self._sprite_size_data = array("f", [0] * self._buf_capacity * 2) - self._sprite_angle_data = array("f", [0] * self._buf_capacity) self._sprite_color_data = array("B", [0] * self._buf_capacity * 4) self._sprite_texture_data = array("f", [0] * self._buf_capacity) # Index buffer @@ -256,9 +257,8 @@ def __init__( self._spritelist_data: SpriteListData # Flags for signaling if a buffer needs to be written to the OpenGL buffer - self._sprite_pos_changed: bool = False + self._sprite_pos_angle_changed: bool = False self._sprite_size_changed: bool = False - self._sprite_angle_changed: bool = False self._sprite_color_changed: bool = False self._sprite_texture_changed: bool = False self._sprite_index_changed: bool = False @@ -310,9 +310,8 @@ def _init_deferred(self) -> None: for texture in sprite.textures or []: self._atlas.add(texture) - self._sprite_pos_changed = True + self._sprite_pos_angle_changed = True self._sprite_size_changed = True - self._sprite_angle_changed = True self._sprite_color_changed = True self._sprite_texture_changed = True self._sprite_index_changed = True @@ -542,9 +541,8 @@ def clear(self, *, capacity: int | None = None, deep: bool = True) -> None: # Reset buffers # Python representation of buffer data - self._sprite_pos_data = array("f", [0] * self._buf_capacity * 3) + self._sprite_pos_angle_data = array("f", [0] * self._buf_capacity * 4) self._sprite_size_data = array("f", [0] * self._buf_capacity * 2) - self._sprite_angle_data = array("f", [0] * self._buf_capacity) self._sprite_color_data = array("B", [0] * self._buf_capacity * 4) self._sprite_texture_data = array("f", [0] * self._buf_capacity) # Index buffer @@ -890,16 +888,14 @@ def _write_sprite_buffers_to_gpu(self) -> None: self._spritelist_data.write_sprite_buffers_to_gpu( # Buffer data - self._sprite_pos_data, + self._sprite_pos_angle_data, self._sprite_size_data, - self._sprite_angle_data, self._sprite_color_data, self._sprite_texture_data, self._sprite_index_data, # Changed flags - self._sprite_pos_changed, + self._sprite_pos_angle_changed, self._sprite_size_changed, - self._sprite_angle_changed, self._sprite_color_changed, self._sprite_texture_changed, self._sprite_index_changed, @@ -975,18 +971,16 @@ def _grow_sprite_buffers(self) -> None: self._buf_capacity = self._buf_capacity * 2 # Extend the buffers so we don't lose the old data - self._sprite_pos_data.extend([0] * extend_by * 3) + self._sprite_pos_angle_data.extend([0] * extend_by * 4) self._sprite_size_data.extend([0] * extend_by * 2) - self._sprite_angle_data.extend([0] * extend_by) self._sprite_color_data.extend([0] * extend_by * 4) self._sprite_texture_data.extend([0] * extend_by) if self._initialized: self._spritelist_data.grow_sprite_buffers() - self._sprite_pos_changed = True + self._sprite_pos_angle_changed = True self._sprite_size_changed = True - self._sprite_angle_changed = True self._sprite_color_changed = True self._sprite_texture_changed = True @@ -1015,17 +1009,16 @@ def _update_all(self, sprite: SpriteType) -> None: """ slot = self.sprite_slot[sprite] # position - self._sprite_pos_data[slot * 3] = sprite._position[0] - self._sprite_pos_data[slot * 3 + 1] = sprite._position[1] - self._sprite_pos_data[slot * 3 + 2] = sprite._depth - self._sprite_pos_changed = True + self._sprite_pos_angle_data[slot * 4] = sprite._position[0] + self._sprite_pos_angle_data[slot * 4 + 1] = sprite._position[1] + self._sprite_pos_angle_data[slot * 4 + 2] = sprite._depth + self._sprite_pos_angle_data[slot * 4 + 3] = sprite._angle + self._sprite_pos_angle_changed = True # size self._sprite_size_data[slot * 2] = sprite._width self._sprite_size_data[slot * 2 + 1] = sprite._height self._sprite_size_changed = True # angle - self._sprite_angle_data[slot] = sprite._angle - self._sprite_angle_changed = True # color self._sprite_color_data[slot * 4] = sprite._color[0] self._sprite_color_data[slot * 4 + 1] = sprite._color[1] @@ -1088,9 +1081,9 @@ def _update_position(self, sprite: SpriteType) -> None: sprite: Sprite to update. """ slot = self.sprite_slot[sprite] - self._sprite_pos_data[slot * 3] = sprite._position[0] - self._sprite_pos_data[slot * 3 + 1] = sprite._position[1] - self._sprite_pos_changed = True + self._sprite_pos_angle_data[slot * 4] = sprite._position[0] + self._sprite_pos_angle_data[slot * 4 + 1] = sprite._position[1] + self._sprite_pos_angle_changed = True def _update_position_x(self, sprite: SpriteType) -> None: """ @@ -1104,8 +1097,8 @@ def _update_position_x(self, sprite: SpriteType) -> None: sprite: Sprite to update. """ slot = self.sprite_slot[sprite] - self._sprite_pos_data[slot * 3] = sprite._position[0] - self._sprite_pos_changed = True + self._sprite_pos_angle_data[slot * 4] = sprite._position[0] + self._sprite_pos_angle_changed = True def _update_position_y(self, sprite: SpriteType) -> None: """ @@ -1119,8 +1112,8 @@ def _update_position_y(self, sprite: SpriteType) -> None: sprite: Sprite to update. """ slot = self.sprite_slot[sprite] - self._sprite_pos_data[slot * 3 + 1] = sprite._position[1] - self._sprite_pos_changed = True + self._sprite_pos_angle_data[slot * 4 + 1] = sprite._position[1] + self._sprite_pos_angle_changed = True def _update_depth(self, sprite: SpriteType) -> None: """ @@ -1131,8 +1124,8 @@ def _update_depth(self, sprite: SpriteType) -> None: sprite: Sprite to update. """ slot = self.sprite_slot[sprite] - self._sprite_pos_data[slot * 3 + 2] = sprite._depth - self._sprite_pos_changed = True + self._sprite_pos_angle_data[slot * 4 + 2] = sprite._depth + self._sprite_pos_angle_changed = True def _update_color(self, sprite: SpriteType) -> None: """ @@ -1196,8 +1189,8 @@ def _update_angle(self, sprite: SpriteType) -> None: sprite: Sprite to update. """ slot = self.sprite_slot[sprite] - self._sprite_angle_data[slot] = sprite._angle - self._sprite_angle_changed = True + self._sprite_pos_angle_data[slot * 4 + 3] = sprite._angle + self._sprite_pos_angle_changed = True class SpriteListData: @@ -1211,16 +1204,14 @@ def __init__(self, ctx: ArcadeContext, capacity: int) -> None: def write_sprite_buffers_to_gpu( self, # The data itself - sprite_pos_data, + sprite_pos_angle_data, sprite_size_data, - sprite_angle_data, sprite_color_data, sprite_texture_data, sprite_index_data, # Changed flags - sprite_pos_changed: bool = True, + sprite_pos_angle_changed: bool = True, sprite_size_changed: bool = True, - sprite_angle_changed: bool = True, sprite_color_changed: bool = True, sprite_texture_changed: bool = True, sprite_index_changed: bool = True, @@ -1229,15 +1220,13 @@ def write_sprite_buffers_to_gpu( Write the sprite buffers to the GPU. Args: - sprite_pos_data: Array of sprite positions. + sprite_pos_angle_data: Array of sprite positions. sprite_size_data: Array of sprite sizes. - sprite_angle_data: Array of sprite angles. sprite_color_data: Array of sprite colors. sprite_texture_data: Array of sprite texture IDs. sprite_index_data: Array of sprite indices. - sprite_pos_changed: Whether the position data has changed. + sprite_pos_angle_changed: Whether the position data has changed. sprite_size_changed: Whether the size data has changed. - sprite_angle_changed: Whether the angle data has changed. sprite_color_changed: Whether the color data has changed. sprite_texture_changed: Whether the texture data has changed. sprite_index_changed: Whether the index data has changed. @@ -1295,9 +1284,10 @@ def __init__(self, ctx: ArcadeContext, capacity: int, atlas: TextureAtlasBase) - self._atlas = atlas # Buffers for each sprite attribute (read by shader) with initial capacity - self._sprite_pos_buf = self.ctx.buffer(reserve=self._buf_capacity * 12) # 3 x 32 bit floats + self._sprite_pos_angle_buf = self.ctx.buffer( + reserve=self._buf_capacity * 16 + ) # 4 x 32 bit floats self._sprite_size_buf = self.ctx.buffer(reserve=self._buf_capacity * 8) # 2 x 32 bit floats - self._sprite_angle_buf = self.ctx.buffer(reserve=self._buf_capacity * 4) # 32 bit float self._sprite_color_buf = self.ctx.buffer(reserve=self._buf_capacity * 4) # 4 x bytes colors self._sprite_texture_buf = self.ctx.buffer(reserve=self._buf_capacity * 4) # 32 bit int # Index buffer @@ -1306,9 +1296,8 @@ def __init__(self, ctx: ArcadeContext, capacity: int, atlas: TextureAtlasBase) - ) # 32 bit unsigned integers contents = [ - gl.BufferDescription(self._sprite_pos_buf, "3f", ["in_pos"]), + gl.BufferDescription(self._sprite_pos_angle_buf, "4f", ["in_pos"]), gl.BufferDescription(self._sprite_size_buf, "2f", ["in_size"]), - gl.BufferDescription(self._sprite_angle_buf, "1f", ["in_angle"]), gl.BufferDescription(self._sprite_texture_buf, "1f", ["in_texture"]), gl.BufferDescription( self._sprite_color_buf, @@ -1344,7 +1333,7 @@ def geometry(self) -> Geometry: return self._geometry # type: ignore @property - def buffer_positions(self) -> Buffer: + def buffer_positions_angle(self) -> Buffer: """ Get the internal OpenGL position buffer for this spritelist. @@ -1355,7 +1344,7 @@ def buffer_positions(self) -> Buffer: This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` instance with name ``in_pos``. """ - return self._sprite_pos_buf + return self._sprite_pos_angle_buf @property def buffer_sizes(self) -> Buffer: @@ -1371,19 +1360,6 @@ def buffer_sizes(self) -> Buffer: raise ValueError("SpriteList is not initialized") return self._sprite_size_buf - @property - def buffer_angles(self) -> Buffer: - """ - Get the internal OpenGL angle buffer for the spritelist. - - This buffer contains a series of 32 bit floats - representing the rotation angle for each sprite in degrees. - - This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` - instance with name ``in_angle``. - """ - return self._sprite_angle_buf - @property def buffer_colors(self) -> Buffer: """ @@ -1442,16 +1418,14 @@ def buffer_indices(self) -> Buffer: def write_sprite_buffers_to_gpu( self, # The data itself - sprite_pos_data, + sprite_pos_angle_data, sprite_size_data, - sprite_angle_data, sprite_color_data, sprite_texture_data, sprite_index_data, # Changed flags - sprite_pos_changed: bool = True, + sprite_pos_angle_changed: bool = True, sprite_size_changed: bool = True, - sprite_angle_changed: bool = True, sprite_color_changed: bool = True, sprite_texture_changed: bool = True, sprite_index_changed: bool = True, @@ -1460,34 +1434,26 @@ def write_sprite_buffers_to_gpu( Write the sprite buffers to the GPU. Args: - sprite_pos_data: Array of sprite positions. + sprite_pos_angle_data: Array of sprite positions. sprite_size_data: Array of sprite sizes. - sprite_angle_data: Array of sprite angles. sprite_color_data: Array of sprite colors. sprite_texture_data: Array of sprite texture IDs. sprite_index_data: Array of sprite indices. - sprite_pos_changed: Whether the position data has changed. sprite_size_changed: Whether the size data has changed. - sprite_angle_changed: Whether the angle data has changed. sprite_color_changed: Whether the color data has changed. sprite_texture_changed: Whether the texture data has changed. sprite_index_changed: Whether the index data has changed. """ - if sprite_pos_changed: - self._sprite_pos_buf.orphan() - self._sprite_pos_buf.write(sprite_pos_data) - self._sprite_pos_changed = False + if sprite_pos_angle_changed: + self._sprite_pos_angle_buf.orphan() + self._sprite_pos_angle_buf.write(sprite_pos_angle_data) + self._sprite_pos_angle_changed = False if sprite_size_changed: self._sprite_size_buf.orphan() self._sprite_size_buf.write(sprite_size_data) self._sprite_size_changed = False - if sprite_angle_changed: - self._sprite_angle_buf.orphan() - self._sprite_angle_buf.write(sprite_angle_data) - self._sprite_angle_changed = False - if sprite_color_changed: self._sprite_color_buf.orphan() self._sprite_color_buf.write(sprite_color_data) @@ -1504,10 +1470,8 @@ def write_sprite_buffers_to_gpu( self._sprite_index_changed = False def grow_sprite_buffers(self) -> None: - # Proper initialization implies these buffers are allocated - self._sprite_pos_buf.orphan(double=True) + self._sprite_pos_angle_buf.orphan(double=True) self._sprite_size_buf.orphan(double=True) - self._sprite_angle_buf.orphan(double=True) self._sprite_color_buf.orphan(double=True) self._sprite_texture_buf.orphan(double=True) @@ -1595,8 +1559,139 @@ def render( self.ctx.blend_func = prev_blend_func -# Program without geo shader for later -# program = self.ctx.load_program( -# vertex_shader=":system:shaders/sprites/sprite_list_simple_vs.glsl", -# fragment_shader=":system:shaders/sprites/sprite_list_simple_fs.glsl", -# ) +class SpriteListTextureData(SpriteListData): + """Container for all gpu data used by the SpriteList without buffers.""" + + def __init__(self, ctx: ArcadeContext, capacity: int, atlas: TextureAtlasBase) -> None: + self.ctx = ctx + self._buf_capacity = capacity + self._idx_capacity = capacity + self._atlas = atlas + + # Program without geo shader + self.program = self.ctx.load_program( + vertex_shader=":system:shaders/sprites/sprite_list_simple_vs.glsl", + fragment_shader=":system:shaders/sprites/sprite_list_simple_fs.glsl", + ) + self._geometry: Geometry | None = None + self._atlas = atlas or self.ctx.default_atlas + + # Texture buffers for per-sprite data. These are looked up using gl_InstanceID + self._pos_angle_texture = self.ctx.texture(size=(capacity, 4), dtype="f4") + self._size_texture = self.ctx.texture(size=(capacity, 2), dtype="f4") + self._color_texture = self.ctx.texture(size=(capacity, 4), dtype="f4") + self._texture_id_texture = self.ctx.texture(size=(capacity, 1), dtype="I4") + self._index_texture = self.ctx.texture(size=(capacity, 1), dtype="I4") + + def write_sprite_buffers_to_gpu( + self, + # The data itself + sprite_pos_angle_data, + sprite_size_data, + sprite_color_data, + sprite_texture_data, + sprite_index_data, + # Changed flags + sprite_pos_angle_changed: bool = True, + sprite_size_changed: bool = True, + sprite_color_changed: bool = True, + sprite_texture_changed: bool = True, + sprite_index_changed: bool = True, + ) -> None: + pass + + def grow_sprite_buffers(self) -> None: + """Double the internal storage""" + # Double the capacity + self._buf_capacity = self._buf_capacity * 2 + + # Extend the textures so we don't lose the old data + self._pos_angle_texture.resize((self._buf_capacity, 4)) + self._size_texture.resize((self._buf_capacity, 2)) + self._color_texture.resize((self._buf_capacity, 4)) + self._texture_id_texture.resize((self._buf_capacity, 1)) + + def grow_index_buffer(self) -> None: + """Double the internal index buffer storage""" + self._idx_capacity = self._idx_capacity * 2 + self._index_texture.resize((self._buf_capacity, 1)) + + def render( + self, + *, + atlas: TextureAtlasBase, + count: int, + color: tuple[float, float, float, float], + default_texture_filter: OpenGlFilter, + filter: PyGLenum | OpenGlFilter | None = None, + pixelated: bool | None = None, + blend_function: BlendFunction | None = None, + blend: bool = True, + ) -> None: + """Render the sprite list using the provided shader program.""" + if not self.program: + raise ValueError("Attempting to render without shader program.") + + prev_blend_func = self.ctx.blend_func + if blend: + self.ctx.enable(self.ctx.BLEND) + # Set custom blend function or revert to default + if blend_function is not None: + self.ctx.blend_func = blend_function + else: + self.ctx.blend_func = self.ctx.BLEND_DEFAULT + else: + self.ctx.disable(self.ctx.BLEND) + + atlas_texture: Texture2D = atlas.texture + + # Set custom filter or reset to default + if filter: + if hasattr( + filter, + "__len__", + ): # assume it's a collection + if len(cast(Sized, filter)) != 2: + raise ValueError("Can't use sequence of length != 2") + atlas_texture.filter = tuple(filter) # type: ignore + else: # assume it's an int + atlas_texture.filter = cast(OpenGlFilter, (filter, filter)) + else: + # Handle the pixelated shortcut if filter is not set + if pixelated: + atlas_texture.filter = self.ctx.NEAREST, self.ctx.NEAREST + else: + atlas_texture.filter = default_texture_filter + + self.program["spritelist_color"] = color + + # Control center pixel interpolation: + # 0.0 = raw interpolation using texture corners + # 1.0 = center pixel interpolation + if self.ctx.NEAREST in atlas_texture.filter: + self.program.set_uniform_safe("uv_offset_bias", 0.0) + else: + self.program.set_uniform_safe("uv_offset_bias", 1.0) + + atlas_texture.use(0) + atlas.use_uv_texture(1) + # Per-instance data + self._pos_angle_texture.use(2) + self._size_texture.use(3) + self._color_texture.use(4) + self._texture_id_texture.use(5) + self._index_texture.use(6) + + if not self._geometry: + raise ValueError("Attempting to render without '_geometry' field being set.") + self._geometry.render( + self.program, + mode=self.ctx.POINTS, + vertices=count, + ) + + # Leave global states to default + if blend: + self.ctx.disable(self.ctx.BLEND) + if blend_function is not None: + self.ctx.blend_func = prev_blend_func From 17921d2e0fa4fae1e7bd3cdbd5c793084bcbe7e5 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 00:29:24 +0200 Subject: [PATCH 04/21] More work .. --- arcade/context.py | 24 +++--- .../resources/system/shaders/lib/sprite.glsl | 25 ++++++- .../sprites/sprite_list_simple_fs.glsl | 2 +- .../sprites/sprite_list_simple_vs.glsl | 52 ++++++++++++- arcade/sprite_list/sprite_list.py | 73 ++++++++++++++++--- 5 files changed, 147 insertions(+), 29 deletions(-) diff --git a/arcade/context.py b/arcade/context.py index 4cbc54bec7..7440bd5f75 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -99,18 +99,18 @@ def __init__( self.sprite_list_program_cull["sprite_texture"] = 0 self.sprite_list_program_cull["uv_texture"] = 1 - # self.sprite_list_program_no_geo = self.load_program( - # vertex_shader=":system:shaders/sprites/sprite_list_simple_vs.glsl", - # fragment_shader=":system:shaders/sprites/sprite_list_simple_fs.glsl", - # ) - # self.sprite_list_program_no_geo["sprite_texture"] = 0 - # self.sprite_list_program_no_geo["uv_texture"] = 1 - # # Per-instance data - # self.sprite_list_program_no_geo["pos_data"] = 2 - # self.sprite_list_program_no_geo["size_data"] = 3 - # self.sprite_list_program_no_geo["color_data"] = 4 - # self.sprite_list_program_no_geo["texture_id_data"] = 5 - # self.sprite_list_program_no_geo["index_data"] = 6 + self.sprite_list_program_no_geo = self.load_program( + vertex_shader=":system:shaders/sprites/sprite_list_simple_vs.glsl", + fragment_shader=":system:shaders/sprites/sprite_list_simple_fs.glsl", + ) + self.sprite_list_program_no_geo["sprite_texture"] = 0 + self.sprite_list_program_no_geo["uv_texture"] = 1 + # Per-instance data + self.sprite_list_program_no_geo["pos_data"] = 2 + self.sprite_list_program_no_geo["size_data"] = 3 + self.sprite_list_program_no_geo["color_data"] = 4 + self.sprite_list_program_no_geo["texture_id_data"] = 5 + self.sprite_list_program_no_geo["index_data"] = 6 self.sprite_program_single = self.load_program( vertex_shader=":system:shaders/sprites/sprite_single_vs.glsl", diff --git a/arcade/resources/system/shaders/lib/sprite.glsl b/arcade/resources/system/shaders/lib/sprite.glsl index f56908f248..27b228bf66 100644 --- a/arcade/resources/system/shaders/lib/sprite.glsl +++ b/arcade/resources/system/shaders/lib/sprite.glsl @@ -1,4 +1,4 @@ -// Fetch texture coordiantes from uv texture +// Fetch texture coordinates from uv texture void getSpriteUVs(sampler2D uvData, int texture_id, out vec2 uv0, out vec2 uv1, out vec2 uv2, out vec2 uv3) { texture_id *= 2; // Calculate the position in the texture. Basic "line wrapping". @@ -14,3 +14,26 @@ void getSpriteUVs(sampler2D uvData, int texture_id, out vec2 uv0, out vec2 uv1, uv2 = data_2.xy; uv3 = data_2.zw; } + +// Functions for fetching per-instance data from textures. +// These are used with the shader program that uses instancing to render sprites +// meaning there is no geo shader involved. This should work for WebGL. +vec4 getInstancePosRot(sampler2D posData, int index) { + return texelFetch(posData, ivec2(index, 0), 0); +} + +vec2 getInstanceSize(sampler2D sizeData, int index) { + return texelFetch(sizeData, ivec2(index, 0), 0).xy; +} + +vec4 getInstanceColor(sampler2D colorData, int index) { + return texelFetch(colorData, ivec2(index, 0), 0); +} + +int getInstanceTextureId(isampler2D textureIdData, int index) { + return texelFetch(textureIdData, ivec2(index, 0), 0).x; +} + +int getInstanceIndex(isampler2D indexData, int index) { + return texelFetch(indexData, ivec2(index, 0), 0).x; +} diff --git a/arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl b/arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl index 33c8d7b397..61eddda6ce 100644 --- a/arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl +++ b/arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl @@ -18,5 +18,5 @@ void main() { if (base_color.a == 0.0) { discard; } - f_color = base_color; + f_color = base_color + vec4(0.5, 0.5, 0.5, 1.0); } diff --git a/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl index 234af9a52a..55b78d6ee4 100644 --- a/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl +++ b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl @@ -1,6 +1,11 @@ #version 330 // vert/frag only version of the sprite list shader +uniform WindowBlock { + mat4 projection; + mat4 view; +} window; + // Texture atlas uniform sampler2D sprite_texture; // Texture containing UVs for the entire atlas @@ -17,21 +22,60 @@ uniform isampler2D index_data; // 0.0 is no offset, 1.0 is half a pixel offset uniform float uv_offset_bias; -// Per sprite/instance data -in vec3 in_instance_pos; // Instanced geometry (rectangle as triangle strip) in vec2 in_pos; +// Output to frag shader out vec2 uv; out vec4 color; #include :system:shaders/lib/sprite.glsl void main() { + // Reading per-instance data from textures. + // First we need take the index texture into account to get the correct rendering order. + int index = getInstanceIndex(index_data, gl_InstanceID); + vec4 pos_rot = getInstancePosRot(pos_data, index); + vec2 size = getInstanceSize(size_data, index); + vec4 color = getInstanceColor(color_data, index); + int texture_id = getInstanceTextureId(texture_id_data, index); // Read texture coordinates from UV texture here vec2 uv0, uv1, uv2, uv3; - getSpriteUVs(uv_texture, int(in_instance_texture), uv0, uv1, uv2, uv3); + getSpriteUVs(uv_texture, texture_id, uv0, uv1, uv2, uv3); + + vec3 center = pos_rot.xyz; + float angle = radians(pos_rot.w); + mat2 rot = mat2( + cos(angle), -sin(angle), + sin(angle), cos(angle) + ); + + mat4 mvp = window.projection * window.view; // TODO: Half pixel offset - // TODO: Position, rotation, size, color, uvs + + int vertex_id = gl_VertexID % 4; + color = color; + switch (vertex_id) { + case 0: + // Upper left + gl_Position = mvp * vec4(rot * size + center.xy, center.z, 1.0); + uv = uv0; + break; + case 1: + // lower left + gl_Position = mvp * vec4(rot * size + center.xy, center.z, 1.0); + uv = uv2; + break; + case 2: + // upper right + gl_Position = mvp * vec4(rot * size + center.xy, center.z, 1.0); + uv = uv1; + break; + case 3: + // lower right + gl_Position = mvp * vec4(rot * size + center.xy, center.z, 1.0); + uv = uv3; + break; + } } diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index 6e300bcc7a..f9969b6d38 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -20,7 +20,7 @@ ) from arcade import Sprite, SpriteType, SpriteType_co, get_window, gl -from arcade.gl import Program, Texture2D +from arcade.gl import Program, Texture2D, BufferDescription from arcade.gl.buffer import Buffer from arcade.gl.types import BlendFunction, OpenGlFilter, PyGLenum from arcade.gl.vertex_array import Geometry @@ -293,9 +293,17 @@ def _init_deferred(self) -> None: self.ctx = get_window().ctx if not self._atlas: self._atlas = self.ctx.default_atlas - self._spritelist_data = SpriteListBufferData( + + # NOTE: Instantiate the appropriate spritelist data class here + # Desktop GL (with geo shader) + # self._spritelist_data = SpriteListBufferData( + # self.ctx, capacity=self._buf_capacity, atlas=self._atlas + # ) + # WebGL (without geo shader) + self._spritelist_data = SpriteListTextureData( self.ctx, capacity=self._buf_capacity, atlas=self._atlas ) + self._initialized = True # Load all the textures and write texture coordinates into buffers. @@ -1569,19 +1577,29 @@ def __init__(self, ctx: ArcadeContext, capacity: int, atlas: TextureAtlasBase) - self._atlas = atlas # Program without geo shader - self.program = self.ctx.load_program( - vertex_shader=":system:shaders/sprites/sprite_list_simple_vs.glsl", - fragment_shader=":system:shaders/sprites/sprite_list_simple_fs.glsl", - ) - self._geometry: Geometry | None = None + self.program = self.ctx.sprite_list_program_no_geo self._atlas = atlas or self.ctx.default_atlas + # fmt: off + self._instance_buffer = self.ctx.buffer( + data=array("f", [ + -0.5, +0.5, # Upper left + -0.5, -0.5, # lower left + +0.5, +0.5, # upper right + +0.5, -0.5, # lower right + ]), + ) + # fmt: on + self._geometry = self.ctx.geometry( + [BufferDescription(self._instance_buffer, "2f", ["in_pos"], instanced=True)], + ) + # Texture buffers for per-sprite data. These are looked up using gl_InstanceID self._pos_angle_texture = self.ctx.texture(size=(capacity, 4), dtype="f4") self._size_texture = self.ctx.texture(size=(capacity, 2), dtype="f4") self._color_texture = self.ctx.texture(size=(capacity, 4), dtype="f4") - self._texture_id_texture = self.ctx.texture(size=(capacity, 1), dtype="I4") - self._index_texture = self.ctx.texture(size=(capacity, 1), dtype="I4") + self._texture_id_texture = self.ctx.texture(size=(capacity, 1), dtype="i4") + self._index_texture = self.ctx.texture(size=(capacity, 1), dtype="i4") def write_sprite_buffers_to_gpu( self, @@ -1598,7 +1616,40 @@ def write_sprite_buffers_to_gpu( sprite_texture_changed: bool = True, sprite_index_changed: bool = True, ) -> None: - pass + """ + Write the sprite buffers to the GPU. + + Args: + sprite_pos_angle_data: Array of sprite positions. + sprite_size_data: Array of sprite sizes. + sprite_color_data: Array of sprite colors. + sprite_texture_data: Array of sprite texture IDs. + sprite_index_data: Array of sprite indices. + sprite_pos_angle_changed: Whether the position data has changed. + sprite_size_changed: Whether the size data has changed. + sprite_color_changed: Whether the color data has changed. + sprite_texture_changed: Whether the texture data has changed. + sprite_index_changed: Whether the index data has changed. + """ + if sprite_pos_angle_changed: + self._pos_angle_texture.write(sprite_pos_angle_data) + self._sprite_pos_angle_changed = False + + if sprite_size_changed: + self._size_texture.write(sprite_size_data) + self._sprite_size_changed = False + + if sprite_color_changed: + self._color_texture.write(sprite_color_data) + self._sprite_color_changed = False + + if sprite_texture_changed: + self._texture_id_texture.write(sprite_texture_data) + self._sprite_texture_changed = False + + if sprite_index_changed: + self._index_texture.write(sprite_index_data) + self._sprite_index_changed = False def grow_sprite_buffers(self) -> None: """Double the internal storage""" @@ -1687,7 +1738,7 @@ def render( self._geometry.render( self.program, mode=self.ctx.POINTS, - vertices=count, + instances=count, ) # Leave global states to default From 2f363f528105a32ad3452e02cdd54fc290777cb1 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 00:39:42 +0200 Subject: [PATCH 05/21] Fix texture formats --- arcade/sprite_list/sprite_list.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index f9969b6d38..98e7f8ed7b 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -1595,11 +1595,11 @@ def __init__(self, ctx: ArcadeContext, capacity: int, atlas: TextureAtlasBase) - ) # Texture buffers for per-sprite data. These are looked up using gl_InstanceID - self._pos_angle_texture = self.ctx.texture(size=(capacity, 4), dtype="f4") - self._size_texture = self.ctx.texture(size=(capacity, 2), dtype="f4") - self._color_texture = self.ctx.texture(size=(capacity, 4), dtype="f4") - self._texture_id_texture = self.ctx.texture(size=(capacity, 1), dtype="i4") - self._index_texture = self.ctx.texture(size=(capacity, 1), dtype="i4") + self._pos_angle_texture = self.ctx.texture(size=(capacity, 1), components=4, dtype="f4") + self._size_texture = self.ctx.texture(size=(capacity, 1), components=2, dtype="f4") + self._color_texture = self.ctx.texture(size=(capacity, 1), components=4, dtype="f1") + self._texture_id_texture = self.ctx.texture(size=(capacity, 1), components=1, dtype="i4") + self._index_texture = self.ctx.texture(size=(capacity, 1), components=1, dtype="i4") def write_sprite_buffers_to_gpu( self, @@ -1657,9 +1657,9 @@ def grow_sprite_buffers(self) -> None: self._buf_capacity = self._buf_capacity * 2 # Extend the textures so we don't lose the old data - self._pos_angle_texture.resize((self._buf_capacity, 4)) - self._size_texture.resize((self._buf_capacity, 2)) - self._color_texture.resize((self._buf_capacity, 4)) + self._pos_angle_texture.resize((self._buf_capacity, 1)) + self._size_texture.resize((self._buf_capacity, 1)) + self._color_texture.resize((self._buf_capacity, 1)) self._texture_id_texture.resize((self._buf_capacity, 1)) def grow_index_buffer(self) -> None: From b28537f281e3c27671e5633f734501716268c2af Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 13:48:16 +0200 Subject: [PATCH 06/21] First working version --- .../resources/system/shaders/lib/sprite.glsl | 4 +- .../sprite_list_geometry_cull_geo.glsl | 2 +- .../sprites/sprite_list_simple_fs.glsl | 11 ++--- .../sprites/sprite_list_simple_vs.glsl | 33 ++++----------- arcade/sprite_list/sprite_list.py | 42 +++++++++++++------ 5 files changed, 45 insertions(+), 47 deletions(-) diff --git a/arcade/resources/system/shaders/lib/sprite.glsl b/arcade/resources/system/shaders/lib/sprite.glsl index 27b228bf66..cfc1a1aa1c 100644 --- a/arcade/resources/system/shaders/lib/sprite.glsl +++ b/arcade/resources/system/shaders/lib/sprite.glsl @@ -30,8 +30,8 @@ vec4 getInstanceColor(sampler2D colorData, int index) { return texelFetch(colorData, ivec2(index, 0), 0); } -int getInstanceTextureId(isampler2D textureIdData, int index) { - return texelFetch(textureIdData, ivec2(index, 0), 0).x; +int getInstanceTextureId(sampler2D textureIdData, int index) { + return int(texelFetch(textureIdData, ivec2(index, 0), 0).x); } int getInstanceIndex(isampler2D indexData, int index) { diff --git a/arcade/resources/system/shaders/sprites/sprite_list_geometry_cull_geo.glsl b/arcade/resources/system/shaders/sprites/sprite_list_geometry_cull_geo.glsl index eadbfdea61..644be8aaaf 100644 --- a/arcade/resources/system/shaders/sprites/sprite_list_geometry_cull_geo.glsl +++ b/arcade/resources/system/shaders/sprites/sprite_list_geometry_cull_geo.glsl @@ -42,7 +42,7 @@ void main() { mat4 mvp = window.projection * window.view; // Do viewport culling for sprites. // We do this in normalized device coordinates to make it simple - // apply projection to the center point. This is important so we get zooming/scrollig right + // apply projection to the center point. This is important so we get zooming/scrolling right vec2 ct = (mvp * vec4(center.xy, 0.0, 1.0)).xy; // We can get away with cheaper calculation of size // The length of the diagonal is the cheapest estimation in case rotation is applied diff --git a/arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl b/arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl index 61eddda6ce..5f6a8ee4c3 100644 --- a/arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl +++ b/arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl @@ -6,17 +6,18 @@ uniform sampler2D sprite_texture; // Global color set on the sprite list uniform vec4 spritelist_color; -in vec2 uv; -in vec4 color; +in vec2 v_uv; +in vec4 v_color; out vec4 f_color; void main() { - vec4 base_color = texture(sprite_texture, uv); - base_color *= color * spritelist_color; + // vec4 base_color = v_color; + vec4 base_color = texture(sprite_texture, v_uv); + base_color *= v_color * spritelist_color; // Alpha test if (base_color.a == 0.0) { discard; } - f_color = base_color + vec4(0.5, 0.5, 0.5, 1.0); + f_color = base_color; } diff --git a/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl index 55b78d6ee4..eb5696032e 100644 --- a/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl +++ b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl @@ -15,7 +15,7 @@ uniform sampler2D uv_texture; uniform sampler2D pos_data; uniform sampler2D size_data; uniform sampler2D color_data; -uniform isampler2D texture_id_data; +uniform sampler2D texture_id_data; uniform isampler2D index_data; // How much half-pixel offset to apply to the UVs. @@ -26,8 +26,8 @@ uniform float uv_offset_bias; in vec2 in_pos; // Output to frag shader -out vec2 uv; -out vec4 color; +out vec2 v_uv; +out vec4 v_color; #include :system:shaders/lib/sprite.glsl @@ -55,27 +55,8 @@ void main() { // TODO: Half pixel offset int vertex_id = gl_VertexID % 4; - color = color; - switch (vertex_id) { - case 0: - // Upper left - gl_Position = mvp * vec4(rot * size + center.xy, center.z, 1.0); - uv = uv0; - break; - case 1: - // lower left - gl_Position = mvp * vec4(rot * size + center.xy, center.z, 1.0); - uv = uv2; - break; - case 2: - // upper right - gl_Position = mvp * vec4(rot * size + center.xy, center.z, 1.0); - uv = uv1; - break; - case 3: - // lower right - gl_Position = mvp * vec4(rot * size + center.xy, center.z, 1.0); - uv = uv3; - break; - } + vec2 uvs[4] = {uv0, uv2, uv1, uv3}; + v_color = color; + gl_Position = mvp * vec4(rot * (in_pos * size) + center.xy, 0.0, 1.0); + v_uv = uvs[vertex_id]; } diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index 98e7f8ed7b..c9c42b0377 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -908,6 +908,11 @@ def _write_sprite_buffers_to_gpu(self) -> None: self._sprite_texture_changed, self._sprite_index_changed, ) + self._sprite_pos_angle_changed = False + self._sprite_size_changed = False + self._sprite_color_changed = False + self._sprite_texture_changed = False + self._sprite_index_changed = False def initialize(self) -> None: """ @@ -974,6 +979,7 @@ def _grow_sprite_buffers(self) -> None: if self._sprite_buffer_slots <= self._buf_capacity: return + print("Growing sprite buffers...") # Double the capacity extend_by = self._buf_capacity self._buf_capacity = self._buf_capacity * 2 @@ -1591,16 +1597,20 @@ def __init__(self, ctx: ArcadeContext, capacity: int, atlas: TextureAtlasBase) - ) # fmt: on self._geometry = self.ctx.geometry( - [BufferDescription(self._instance_buffer, "2f", ["in_pos"], instanced=True)], + [BufferDescription(self._instance_buffer, "2f", ["in_pos"])], + mode=self.ctx.TRIANGLE_STRIP, ) # Texture buffers for per-sprite data. These are looked up using gl_InstanceID self._pos_angle_texture = self.ctx.texture(size=(capacity, 1), components=4, dtype="f4") self._size_texture = self.ctx.texture(size=(capacity, 1), components=2, dtype="f4") self._color_texture = self.ctx.texture(size=(capacity, 1), components=4, dtype="f1") - self._texture_id_texture = self.ctx.texture(size=(capacity, 1), components=1, dtype="i4") + self._texture_id_texture = self.ctx.texture(size=(capacity, 1), components=1, dtype="f4") self._index_texture = self.ctx.texture(size=(capacity, 1), components=1, dtype="i4") + # Debugging + self._query = self.ctx.query(primitives=True) + def write_sprite_buffers_to_gpu( self, # The data itself @@ -1633,26 +1643,22 @@ def write_sprite_buffers_to_gpu( """ if sprite_pos_angle_changed: self._pos_angle_texture.write(sprite_pos_angle_data) - self._sprite_pos_angle_changed = False if sprite_size_changed: self._size_texture.write(sprite_size_data) - self._sprite_size_changed = False if sprite_color_changed: self._color_texture.write(sprite_color_data) - self._sprite_color_changed = False if sprite_texture_changed: self._texture_id_texture.write(sprite_texture_data) - self._sprite_texture_changed = False if sprite_index_changed: self._index_texture.write(sprite_index_data) - self._sprite_index_changed = False def grow_sprite_buffers(self) -> None: """Double the internal storage""" + print(f"Growing sprite buffers from {self._buf_capacity} to", self._buf_capacity * 2) # Double the capacity self._buf_capacity = self._buf_capacity * 2 @@ -1714,7 +1720,10 @@ def render( else: atlas_texture.filter = default_texture_filter - self.program["spritelist_color"] = color + try: + self.program["spritelist_color"] = color + except KeyError: + pass # Control center pixel interpolation: # 0.0 = raw interpolation using texture corners @@ -1735,11 +1744,18 @@ def render( if not self._geometry: raise ValueError("Attempting to render without '_geometry' field being set.") - self._geometry.render( - self.program, - mode=self.ctx.POINTS, - instances=count, - ) + + # a = array("i") + # a.frombytes(self._index_texture.read()) + # print("Buffer", a) + + with self._query: + self._geometry.render( + self.program, + # mode=self.ctx.TRIANGLE_STRIP, + instances=count, + ) + # print(f"Primitives rendered: {self._query.primitives_generated} (count: {count})") # Leave global states to default if blend: From d324e03b4f1398ad89f04961d8d49259352988a0 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 13:55:39 +0200 Subject: [PATCH 07/21] Remove debug prints in atlas --- arcade/texture_atlas/atlas_default.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arcade/texture_atlas/atlas_default.py b/arcade/texture_atlas/atlas_default.py index 77a60174f0..52ec23823e 100644 --- a/arcade/texture_atlas/atlas_default.py +++ b/arcade/texture_atlas/atlas_default.py @@ -667,7 +667,7 @@ def resize(self, size: tuple[int, int], force=False) -> None: force: Force a resize even if the size is the same """ - print("Resizing atlas from", self._size, "to", size) + # print("Resizing atlas from", self._size, "to", size) # Only resize if the size actually changed if size == self._size and not force: @@ -746,7 +746,7 @@ def rebuild(self) -> None: This method also tries to organize the textures more efficiently ordering them by size. The texture ids will persist so the sprite list doesn't need to be rebuilt. """ - print("Rebuilding atlas") + # print("Rebuilding atlas") # Hold a reference to the old textures textures = self.textures From 780cf7c39c8fa0c7c1512e547075d7abdc40033c Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 13:56:04 +0200 Subject: [PATCH 08/21] Remove debug code in texture data --- arcade/sprite_list/sprite_list.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index c9c42b0377..9a605b21ba 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -1608,9 +1608,6 @@ def __init__(self, ctx: ArcadeContext, capacity: int, atlas: TextureAtlasBase) - self._texture_id_texture = self.ctx.texture(size=(capacity, 1), components=1, dtype="f4") self._index_texture = self.ctx.texture(size=(capacity, 1), components=1, dtype="i4") - # Debugging - self._query = self.ctx.query(primitives=True) - def write_sprite_buffers_to_gpu( self, # The data itself @@ -1745,17 +1742,10 @@ def render( if not self._geometry: raise ValueError("Attempting to render without '_geometry' field being set.") - # a = array("i") - # a.frombytes(self._index_texture.read()) - # print("Buffer", a) - - with self._query: - self._geometry.render( - self.program, - # mode=self.ctx.TRIANGLE_STRIP, - instances=count, - ) - # print(f"Primitives rendered: {self._query.primitives_generated} (count: {count})") + self._geometry.render( + self.program, + instances=count, + ) # Leave global states to default if blend: From 7f47ba63aeb8104234c6633e82ae8471689dec32 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 16:15:51 +0200 Subject: [PATCH 09/21] Support 1M sprites --- .../resources/system/shaders/lib/sprite.glsl | 10 +++---- arcade/sprite_list/sprite_list.py | 27 ++++++++++++------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/arcade/resources/system/shaders/lib/sprite.glsl b/arcade/resources/system/shaders/lib/sprite.glsl index cfc1a1aa1c..bc96369108 100644 --- a/arcade/resources/system/shaders/lib/sprite.glsl +++ b/arcade/resources/system/shaders/lib/sprite.glsl @@ -19,21 +19,21 @@ void getSpriteUVs(sampler2D uvData, int texture_id, out vec2 uv0, out vec2 uv1, // These are used with the shader program that uses instancing to render sprites // meaning there is no geo shader involved. This should work for WebGL. vec4 getInstancePosRot(sampler2D posData, int index) { - return texelFetch(posData, ivec2(index, 0), 0); + return texelFetch(posData, ivec2(index % 256, index / 256), 0); } vec2 getInstanceSize(sampler2D sizeData, int index) { - return texelFetch(sizeData, ivec2(index, 0), 0).xy; + return texelFetch(sizeData, ivec2(index % 256, index / 256), 0).xy; } vec4 getInstanceColor(sampler2D colorData, int index) { - return texelFetch(colorData, ivec2(index, 0), 0); + return texelFetch(colorData, ivec2(index % 256, index / 256), 0); } int getInstanceTextureId(sampler2D textureIdData, int index) { - return int(texelFetch(textureIdData, ivec2(index, 0), 0).x); + return int(texelFetch(textureIdData, ivec2(index % 256, index / 256), 0).x); } int getInstanceIndex(isampler2D indexData, int index) { - return texelFetch(indexData, ivec2(index, 0), 0).x; + return texelFetch(indexData, ivec2(index % 256, index / 256), 0).x; } diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index 9a605b21ba..e73172d21a 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -31,8 +31,16 @@ from arcade import Texture, ArcadeContext from arcade.texture_atlas import TextureAtlasBase -# The default capacity from spritelists -_DEFAULT_CAPACITY = 100 + +def _align_capacity(capacity: int) -> int: + """ + Aligns the capacity to be a multiple of 256. + This is important to make the data compatible with different + types of storage such as buffers and textures. + """ + if capacity <= 0: + return 256 + return (capacity + 255) // 256 * 256 class SpriteSequence(Collection[SpriteType_co]): @@ -227,10 +235,11 @@ def __init__( self._blend = True self._color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0) + capacity = _align_capacity(capacity) # The initial capacity of the spritelist buffers (internal) - self._buf_capacity = abs(capacity) or _DEFAULT_CAPACITY + self._buf_capacity = capacity # The initial capacity of the index buffer (internal) - self._idx_capacity = abs(capacity) or _DEFAULT_CAPACITY + self._idx_capacity = capacity # The number of slots used in the sprite buffer self._sprite_buffer_slots = 0 # Number of slots used in the index buffer @@ -1660,15 +1669,15 @@ def grow_sprite_buffers(self) -> None: self._buf_capacity = self._buf_capacity * 2 # Extend the textures so we don't lose the old data - self._pos_angle_texture.resize((self._buf_capacity, 1)) - self._size_texture.resize((self._buf_capacity, 1)) - self._color_texture.resize((self._buf_capacity, 1)) - self._texture_id_texture.resize((self._buf_capacity, 1)) + self._pos_angle_texture.resize((256, self._buf_capacity // 256)) + self._size_texture.resize((256, self._buf_capacity // 256)) + self._color_texture.resize((256, self._buf_capacity // 256)) + self._texture_id_texture.resize((256, self._buf_capacity // 256)) def grow_index_buffer(self) -> None: """Double the internal index buffer storage""" self._idx_capacity = self._idx_capacity * 2 - self._index_texture.resize((self._buf_capacity, 1)) + self._index_texture.resize((256, self._idx_capacity // 256)) def render( self, From b2137922fb39730583880246513867548f9e1771 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 16:33:20 +0200 Subject: [PATCH 10/21] Remove debug stuff --- arcade/sprite_list/sprite_list.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index e73172d21a..beaadb8d9c 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -617,7 +617,6 @@ def append(self, sprite: SpriteType) -> None: Args: sprite: Sprite to add to the list. """ - # print(f"{id(self)} : {id(sprite)} append") if sprite in self.sprite_slot: raise ValueError("Sprite already in SpriteList") @@ -988,7 +987,6 @@ def _grow_sprite_buffers(self) -> None: if self._sprite_buffer_slots <= self._buf_capacity: return - print("Growing sprite buffers...") # Double the capacity extend_by = self._buf_capacity self._buf_capacity = self._buf_capacity * 2 @@ -1567,8 +1565,6 @@ def render( atlas_texture.use(0) atlas.use_uv_texture(1) - if not self._geometry: - raise ValueError("Attempting to render without '_geometry' field being set.") self._geometry.render( self.program, mode=self.ctx.POINTS, @@ -1664,7 +1660,6 @@ def write_sprite_buffers_to_gpu( def grow_sprite_buffers(self) -> None: """Double the internal storage""" - print(f"Growing sprite buffers from {self._buf_capacity} to", self._buf_capacity * 2) # Double the capacity self._buf_capacity = self._buf_capacity * 2 @@ -1748,9 +1743,6 @@ def render( self._texture_id_texture.use(5) self._index_texture.use(6) - if not self._geometry: - raise ValueError("Attempting to render without '_geometry' field being set.") - self._geometry.render( self.program, instances=count, From 3c2cc9fca6658c98ff65d5d5c9b610e8d60abc6d Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 17:47:45 +0200 Subject: [PATCH 11/21] Rewrite gpu collision for spritelists --- .../shaders/collision/col_trans_vs.glsl | 2 +- arcade/sprite_list/collision.py | 45 +------ arcade/sprite_list/sprite_list.py | 110 +++++++++++++++++- 3 files changed, 107 insertions(+), 50 deletions(-) diff --git a/arcade/resources/system/shaders/collision/col_trans_vs.glsl b/arcade/resources/system/shaders/collision/col_trans_vs.glsl index 9a54e2f12a..a2167aa975 100644 --- a/arcade/resources/system/shaders/collision/col_trans_vs.glsl +++ b/arcade/resources/system/shaders/collision/col_trans_vs.glsl @@ -1,7 +1,7 @@ #version 330 // A simple passthrough shader forwarding data to the geomtry shader -in vec3 in_pos; +in vec4 in_pos; in vec2 in_size; out vec2 pos; diff --git a/arcade/sprite_list/collision.py b/arcade/sprite_list/collision.py index 8cc674512e..1138fda7bf 100644 --- a/arcade/sprite_list/collision.py +++ b/arcade/sprite_list/collision.py @@ -134,50 +134,7 @@ def _get_nearby_sprites( sprite_count = len(sprite_list) if sprite_count == 0: return [] - - # Update the position and size to check - ctx = get_window().ctx - sprite_list._write_sprite_buffers_to_gpu() - - ctx.collision_detection_program["check_pos"] = sprite.position - ctx.collision_detection_program["check_size"] = sprite.width, sprite.height - - # Ensure the result buffer can fit all the sprites (worst case) - buffer = ctx.collision_buffer - if buffer.size < sprite_count * 4: - buffer.orphan(size=sprite_count * 4) - - # Run the transform shader emitting sprites close to the configured position and size. - # This runs in a query so we can measure the number of sprites emitted. - with ctx.collision_query: - sprite_list.sprite_data.geometry.transform( # type: ignore - ctx.collision_detection_program, - buffer, - vertices=sprite_count, - ) - - # Store the number of sprites emitted - emit_count = ctx.collision_query.primitives_generated - # print( - # emit_count, - # ctx.collision_query.time_elapsed, - # ctx.collision_query.time_elapsed / 1_000_000_000, - # ) - - # If no sprites emitted we can just return an empty list - if emit_count == 0: - return [] - - # # Debug block for transform data to keep around - # print("emit_count", emit_count) - # data = buffer.read(size=emit_count * 4) - # print("bytes", data) - # print("data", struct.unpack(f'{emit_count}i', data)) - - # .. otherwise build and return a list of the sprites selected by the transform - return [ - sprite_list[i] for i in struct.unpack(f"{emit_count}i", buffer.read(size=emit_count * 4)) - ] + return sprite_list.get_nearby_sprites_gpu(sprite.position, sprite.size) def check_for_collision_with_list( diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index beaadb8d9c..fdf5298768 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -8,6 +8,7 @@ from __future__ import annotations import random +import struct from abc import abstractmethod from array import array from collections import deque @@ -139,6 +140,23 @@ def draw_hit_boxes( """ ... + @abstractmethod + def get_nearby_sprites_gpu(self, pos: Point2, size: Point2) -> list[SpriteType_co]: + """ + Get a list of sprites that are nearby the given position and size + using the gpu. No spatial hashing is needed. This is a very fast method + to find nearby sprites in large spritelists but is very expensive + if the method is called many times per frame or if the sprite list + is small. + + Args: + pos: The position to check for nearby sprites. + size: The size of the area to check for nearby sprites. + Returns: + A list of sprites nearby the given position and size. + """ + ... + @abstractmethod def _write_sprite_buffers_to_gpu(self) -> None: ... @@ -305,13 +323,13 @@ def _init_deferred(self) -> None: # NOTE: Instantiate the appropriate spritelist data class here # Desktop GL (with geo shader) - # self._spritelist_data = SpriteListBufferData( - # self.ctx, capacity=self._buf_capacity, atlas=self._atlas - # ) - # WebGL (without geo shader) - self._spritelist_data = SpriteListTextureData( + self._spritelist_data = SpriteListBufferData( self.ctx, capacity=self._buf_capacity, atlas=self._atlas ) + # WebGL (without geo shader) + # self._spritelist_data = SpriteListTextureData( + # self.ctx, capacity=self._buf_capacity, atlas=self._atlas + # ) self._initialized = True @@ -981,6 +999,30 @@ def draw_hit_boxes( arcade.draw_lines(points, color=converted_color, line_width=line_thickness) + def get_nearby_sprites_gpu(self, pos: Point2, size: Point2) -> list[SpriteType]: + """ + Get a list of sprites that are nearby the given position and size + using the gpu. No spatial hashing is needed. This is a very fast method + to find nearby sprites in large spritelists but is very expensive + if the method is called many times per frame or if the sprite list + is small. + + Args: + pos: The position to check for nearby sprites. + size: The size of the area to check for nearby sprites. + Returns: + A list of sprites nearby the given position and size. + """ + if not self._initialized: + self._init_deferred() + + if len(self.sprite_list) == 0: + return [] + + self._write_sprite_buffers_to_gpu() + indices = self._spritelist_data.get_nearby_sprite_indices(pos, size, len(self.sprite_list)) + return [self.sprite_list[i] for i in indices] + def _grow_sprite_buffers(self) -> None: """Double the internal buffer sizes""" # Resize sprite buffers if needed @@ -1294,6 +1336,19 @@ def render( """ raise NotImplementedError("This method should be implemented in subclasses.") + def get_nearby_sprite_indices(self, pos: Point2, size: Point2, length: int) -> list[int]: + """ + Get indices of sprites that are nearby the given position and size. + + Args: + pos: The position to check for nearby sprites. + size: The size of the area to check for nearby sprites. + length: The number of sprites in the list. + Returns: + A list of indices of nearby sprites. + """ + raise NotImplementedError("This method should be implemented in subclasses.") + class SpriteListBufferData(SpriteListData): """Container for all gpu data used by the SpriteList.""" @@ -1577,6 +1632,39 @@ def render( if blend_function is not None: self.ctx.blend_func = prev_blend_func + def get_nearby_sprite_indices(self, pos: Point2, size: Point2, length: int) -> list[int]: + """ + Get indices of sprites that are nearby the given position and size. + + Args: + pos: The position to check for nearby sprites. + size: The size of the area to check for nearby sprites. + length: The number of sprites in the spritelist. + Returns: + A list of indices of nearby sprites. + """ + ctx = self.ctx + ctx.collision_detection_program["check_pos"] = pos + ctx.collision_detection_program["check_size"] = size + + # Ensure the result buffer can fit all the sprites (worst case) + buffer = ctx.collision_buffer + # NOTE: Right now the limit is 1000 hits + # Run the transform shader emitting sprites close to the configured position and size. + # This runs in a query so we can measure the number of sprites emitted. + with ctx.collision_query: + self._geometry.transform( # type: ignore + ctx.collision_detection_program, + buffer, + vertices=length, + ) + + # Store the number of sprites emitted + emit_count = ctx.collision_query.primitives_generated + if emit_count == 0: + return [] + return [i for i in struct.unpack(f"{emit_count}i", buffer.read(size=emit_count * 4))] + class SpriteListTextureData(SpriteListData): """Container for all gpu data used by the SpriteList without buffers.""" @@ -1753,3 +1841,15 @@ def render( self.ctx.disable(self.ctx.BLEND) if blend_function is not None: self.ctx.blend_func = prev_blend_func + + # def get_nearby_sprite_indices(self, pos: Point2, size: Point2, length: int) -> list[int]: + # """ + # Get indices of sprites that are nearby the given position and size. + + # Args: + # pos: The position to check for nearby sprites. + # size: The size of the area to check for nearby sprites. + # length: The number of sprites in the spritelist. + # Returns: + # A list of indices of nearby sprites. + # """ From af370232d30d2de7bab35e62aa394670d57cefc3 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 18:43:45 +0200 Subject: [PATCH 12/21] Working gpu collision with texture backend --- arcade/context.py | 11 ++++ .../shaders/collision/col_tex_trans_gs.glsl | 33 +++++++++++ .../shaders/collision/col_tex_trans_vs.glsl | 20 +++++++ .../shaders/collision/col_trans_gs.glsl | 1 + .../shaders/collision/col_trans_vs.glsl | 2 +- arcade/sprite_list/sprite_list.py | 59 ++++++++++++------- 6 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 arcade/resources/system/shaders/collision/col_tex_trans_gs.glsl create mode 100644 arcade/resources/system/shaders/collision/col_tex_trans_vs.glsl diff --git a/arcade/context.py b/arcade/context.py index 7440bd5f75..78073f1dbb 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -157,11 +157,22 @@ def __init__( self.atlas_resize_program["texcoords_old"] = 2 self.atlas_resize_program["texcoords_new"] = 3 + # NOTE: These should not be created when WebGL is used # SpriteList collision resources + # Buffer version of the collision detection program. self.collision_detection_program = self.load_program( vertex_shader=":system:shaders/collision/col_trans_vs.glsl", geometry_shader=":system:shaders/collision/col_trans_gs.glsl", ) + # Texture version of the collision detection program. + self.collision_detection_program_simple = self.load_program( + vertex_shader=":system:shaders/collision/col_tex_trans_vs.glsl", + geometry_shader=":system:shaders/collision/col_tex_trans_gs.glsl", + ) + self.collision_detection_program_simple["pos_angle_data"] = 0 + self.collision_detection_program_simple["size_data"] = 1 + self.collision_detection_program_simple["index_data"] = 2 + self.collision_buffer = self.buffer(reserve=1024 * 4) self.collision_query = self.query(samples=False, time=False, primitives=True) diff --git a/arcade/resources/system/shaders/collision/col_tex_trans_gs.glsl b/arcade/resources/system/shaders/collision/col_tex_trans_gs.glsl new file mode 100644 index 0000000000..ac2e453dad --- /dev/null +++ b/arcade/resources/system/shaders/collision/col_tex_trans_gs.glsl @@ -0,0 +1,33 @@ +#version 330 +// Texture version if collision shader + +layout(points) in; +layout(points, max_vertices=1) out; + +uniform vec2 check_pos; +uniform vec2 check_size; + +in vec2 pos[]; +in vec2 size[]; + +out int spriteIndex; + +void main() { + // Calculate the distance between the sprite center + // and the point we want to check + float dist = distance(pos[0], check_pos); + + // Get the maximum x and y size + // max() works per component + vec2 size = max(size[0], check_size); + + // Destroy the sprite if too far away + if (dist < max(size.x, size.y) * 1.42) { + // Set the sprite index to the current primitive id + // We are only processing points, so it will match + // the spritelist index + spriteIndex = int(gl_PrimitiveIDIn); + EmitVertex(); + } + +} diff --git a/arcade/resources/system/shaders/collision/col_tex_trans_vs.glsl b/arcade/resources/system/shaders/collision/col_tex_trans_vs.glsl new file mode 100644 index 0000000000..d638489af0 --- /dev/null +++ b/arcade/resources/system/shaders/collision/col_tex_trans_vs.glsl @@ -0,0 +1,20 @@ +#version 330 +// Texture version if collision shader + +#include :system:shaders/lib/sprite.glsl + +uniform sampler2D pos_angle_data; +uniform sampler2D size_data; +uniform isampler2D index_data; + +out vec2 pos; +out vec2 size; + +void main() { + int index = getInstanceIndex(index_data, gl_VertexID); + vec4 _pos_rot = getInstancePosRot(pos_angle_data, index); + vec2 _size = getInstanceSize(size_data, index); + + pos = _pos_rot.xy; + size = _size.xy; +} diff --git a/arcade/resources/system/shaders/collision/col_trans_gs.glsl b/arcade/resources/system/shaders/collision/col_trans_gs.glsl index 98524c0098..068ce68e0d 100644 --- a/arcade/resources/system/shaders/collision/col_trans_gs.glsl +++ b/arcade/resources/system/shaders/collision/col_trans_gs.glsl @@ -1,4 +1,5 @@ #version 330 +// Buffer version if collision shader layout(points) in; layout(points, max_vertices=1) out; diff --git a/arcade/resources/system/shaders/collision/col_trans_vs.glsl b/arcade/resources/system/shaders/collision/col_trans_vs.glsl index a2167aa975..9474fbf8a1 100644 --- a/arcade/resources/system/shaders/collision/col_trans_vs.glsl +++ b/arcade/resources/system/shaders/collision/col_trans_vs.glsl @@ -1,5 +1,5 @@ #version 330 -// A simple passthrough shader forwarding data to the geomtry shader +// Buffer version if collision shader in vec4 in_pos; in vec2 in_size; diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index fdf5298768..c8c49c7bd1 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -323,14 +323,13 @@ def _init_deferred(self) -> None: # NOTE: Instantiate the appropriate spritelist data class here # Desktop GL (with geo shader) - self._spritelist_data = SpriteListBufferData( - self.ctx, capacity=self._buf_capacity, atlas=self._atlas - ) - # WebGL (without geo shader) - # self._spritelist_data = SpriteListTextureData( + # self._spritelist_data = SpriteListBufferData( # self.ctx, capacity=self._buf_capacity, atlas=self._atlas # ) - + # WebGL (without geo shader) + self._spritelist_data = SpriteListTextureData( + self.ctx, capacity=self._buf_capacity, atlas=self._atlas + ) self._initialized = True # Load all the textures and write texture coordinates into buffers. @@ -1646,12 +1645,7 @@ def get_nearby_sprite_indices(self, pos: Point2, size: Point2, length: int) -> l ctx = self.ctx ctx.collision_detection_program["check_pos"] = pos ctx.collision_detection_program["check_size"] = size - - # Ensure the result buffer can fit all the sprites (worst case) buffer = ctx.collision_buffer - # NOTE: Right now the limit is 1000 hits - # Run the transform shader emitting sprites close to the configured position and size. - # This runs in a query so we can measure the number of sprites emitted. with ctx.collision_query: self._geometry.transform( # type: ignore ctx.collision_detection_program, @@ -1842,14 +1836,35 @@ def render( if blend_function is not None: self.ctx.blend_func = prev_blend_func - # def get_nearby_sprite_indices(self, pos: Point2, size: Point2, length: int) -> list[int]: - # """ - # Get indices of sprites that are nearby the given position and size. - - # Args: - # pos: The position to check for nearby sprites. - # size: The size of the area to check for nearby sprites. - # length: The number of sprites in the spritelist. - # Returns: - # A list of indices of nearby sprites. - # """ + def get_nearby_sprite_indices(self, pos: Point2, size: Point2, length: int) -> list[int]: + """ + Get indices of sprites that are nearby the given position and size. + + Args: + pos: The position to check for nearby sprites. + size: The size of the area to check for nearby sprites. + length: The number of sprites in the spritelist. + Returns: + A list of indices of nearby sprites. + """ + ctx = self.ctx + buffer = ctx.collision_buffer + program = ctx.collision_detection_program_simple + program["check_pos"] = pos + program["check_size"] = size + + self._pos_angle_texture.use(0) + self._size_texture.use(1) + self._index_texture.use(2) + + with ctx.collision_query: + ctx.geometry_empty.transform( + program, + buffer, + vertices=length, + ) + emit_count = ctx.collision_query.primitives_generated + print(f"Collision query emitted {emit_count} sprites") + if emit_count == 0: + return [] + return [i for i in struct.unpack(f"{emit_count}i", buffer.read(size=emit_count * 4))] From 877afd183f448a3cdbb55f988bb4c481fa5561ad Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 18:45:31 +0200 Subject: [PATCH 13/21] Remove debug print --- arcade/sprite_list/sprite_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index c8c49c7bd1..859f8d0aab 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -1864,7 +1864,7 @@ def get_nearby_sprite_indices(self, pos: Point2, size: Point2, length: int) -> l vertices=length, ) emit_count = ctx.collision_query.primitives_generated - print(f"Collision query emitted {emit_count} sprites") + # print(f"Collision query emitted {emit_count} sprites") if emit_count == 0: return [] return [i for i in struct.unpack(f"{emit_count}i", buffer.read(size=emit_count * 4))] From 48c79827279ff03ac5e8a2660be74b00038e1630 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 18:55:42 +0200 Subject: [PATCH 14/21] GLSL: avoid C-style initialization --- .../resources/system/shaders/sprites/sprite_list_simple_vs.glsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl index eb5696032e..f1c9989a7d 100644 --- a/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl +++ b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl @@ -55,7 +55,7 @@ void main() { // TODO: Half pixel offset int vertex_id = gl_VertexID % 4; - vec2 uvs[4] = {uv0, uv2, uv1, uv3}; + vec2 uvs[4] = vec2[4](uv0, uv2, uv1, uv3); v_color = color; gl_Position = mvp * vec4(rot * (in_pos * size) + center.xy, 0.0, 1.0); v_uv = uvs[vertex_id]; From fa15390cf7e748b9a19f7918d0c27f92c7d8a840 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 20:51:05 +0200 Subject: [PATCH 15/21] Fix tests and typing --- arcade/sprite_list/collision.py | 4 - arcade/sprite_list/sprite_list.py | 213 +++++++++++------- tests/unit/spritelist/test_spritelist.py | 16 +- .../spritelist/test_spritelist_buffers.py | 78 +++---- tests/unit/spritelist/test_spritelist_lazy.py | 7 +- 5 files changed, 182 insertions(+), 136 deletions(-) diff --git a/arcade/sprite_list/collision.py b/arcade/sprite_list/collision.py index 1138fda7bf..48f8323e18 100644 --- a/arcade/sprite_list/collision.py +++ b/arcade/sprite_list/collision.py @@ -1,9 +1,5 @@ -import struct from collections.abc import Iterable -from arcade import ( - get_window, -) from arcade.geometry import ( are_polygons_intersecting, is_point_in_polygon, diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index 859f8d0aab..a25b233143 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -21,15 +21,15 @@ ) from arcade import Sprite, SpriteType, SpriteType_co, get_window, gl -from arcade.gl import Program, Texture2D, BufferDescription +from arcade.gl import BufferDescription, Program, Texture2D from arcade.gl.buffer import Buffer from arcade.gl.types import BlendFunction, OpenGlFilter, PyGLenum from arcade.gl.vertex_array import Geometry -from arcade.types import RGBA255, Color, Point2, RGBANormalized, RGBOrA255, RGBOrANormalized +from arcade.types import RGBA255, Color, Point2, Point, RGBANormalized, RGBOrA255, RGBOrANormalized from arcade.utils import copy_dunders_unimplemented if TYPE_CHECKING: - from arcade import Texture, ArcadeContext + from arcade import ArcadeContext, Texture from arcade.texture_atlas import TextureAtlasBase @@ -141,7 +141,7 @@ def draw_hit_boxes( ... @abstractmethod - def get_nearby_sprites_gpu(self, pos: Point2, size: Point2) -> list[SpriteType_co]: + def get_nearby_sprites_gpu(self, pos: Point, size: Point) -> list[SpriteType_co]: """ Get a list of sprites that are nearby the given position and size using the gpu. No spatial hashing is needed. This is a very fast method @@ -281,7 +281,7 @@ def __init__( # Index buffer self._sprite_index_data = array("i", [0] * self._idx_capacity) - self._spritelist_data: SpriteListData + self._data: SpriteListData | None = None # Flags for signaling if a buffer needs to be written to the OpenGL buffer self._sprite_pos_angle_changed: bool = False @@ -323,13 +323,11 @@ def _init_deferred(self) -> None: # NOTE: Instantiate the appropriate spritelist data class here # Desktop GL (with geo shader) - # self._spritelist_data = SpriteListBufferData( + # self._data = SpriteListBufferData( # self.ctx, capacity=self._buf_capacity, atlas=self._atlas # ) # WebGL (without geo shader) - self._spritelist_data = SpriteListTextureData( - self.ctx, capacity=self._buf_capacity, atlas=self._atlas - ) + self._data = SpriteListTextureData(self.ctx, capacity=self._buf_capacity, atlas=self._atlas) self._initialized = True # Load all the textures and write texture coordinates into buffers. @@ -499,12 +497,12 @@ def atlas(self) -> TextureAtlasBase | None: return self._atlas @property - def sprite_data(self) -> SpriteListData: + def data(self) -> SpriteListData: """Get the sprite data for this spritelist.""" if not self._initialized: self.initialize() - return self._spritelist_data # type: ignore[return-value] + return self._data # type: ignore[return-value] def _next_slot(self) -> int: """ @@ -565,7 +563,7 @@ def clear(self, *, capacity: int | None = None, deep: bool = True) -> None: self.spatial_hash = SpatialHash(cell_size=self._spatial_hash_cell_size) # Clear the slot_idx and slot info and other states - capacity = abs(capacity or self._buf_capacity) + capacity = _align_capacity(capacity or self._buf_capacity) self._buf_capacity = capacity self._idx_capacity = capacity @@ -919,7 +917,7 @@ def _write_sprite_buffers_to_gpu(self) -> None: if not self._initialized: self._init_deferred() - self._spritelist_data.write_sprite_buffers_to_gpu( + self.data.write_sprite_buffers_to_gpu( # Buffer data self._sprite_pos_angle_data, self._sprite_size_data, @@ -967,7 +965,7 @@ def draw( self._init_deferred() self._write_sprite_buffers_to_gpu() - self._spritelist_data.render( + self.data.render( atlas=self._atlas, # type: ignore count=self._sprite_index_slots, color=self._color, @@ -998,7 +996,7 @@ def draw_hit_boxes( arcade.draw_lines(points, color=converted_color, line_width=line_thickness) - def get_nearby_sprites_gpu(self, pos: Point2, size: Point2) -> list[SpriteType]: + def get_nearby_sprites_gpu(self, pos: Point, size: Point) -> list[SpriteType]: """ Get a list of sprites that are nearby the given position and size using the gpu. No spatial hashing is needed. This is a very fast method @@ -1019,7 +1017,7 @@ def get_nearby_sprites_gpu(self, pos: Point2, size: Point2) -> list[SpriteType]: return [] self._write_sprite_buffers_to_gpu() - indices = self._spritelist_data.get_nearby_sprite_indices(pos, size, len(self.sprite_list)) + indices = self.data.get_nearby_sprite_indices(pos, size, len(self.sprite_list)) return [self.sprite_list[i] for i in indices] def _grow_sprite_buffers(self) -> None: @@ -1039,7 +1037,7 @@ def _grow_sprite_buffers(self) -> None: self._sprite_texture_data.extend([0] * extend_by) if self._initialized: - self._spritelist_data.grow_sprite_buffers() + self.data.grow_sprite_buffers() self._sprite_pos_angle_changed = True self._sprite_size_changed = True @@ -1057,7 +1055,7 @@ def _grow_index_buffer(self) -> None: self._sprite_index_data.extend([0] * extend_by) if self._initialized: - self._spritelist_data.grow_index_buffer() + self.data.grow_index_buffer() self._sprite_index_changed = True @@ -1263,6 +1261,53 @@ def __init__(self, ctx: ArcadeContext, capacity: int) -> None: self._buf_capacity = capacity self._idx_capacity = capacity + # Generic GPU storage for sprite data + self._storage_pos_angle: Buffer | Texture2D + self._storage_size: Buffer | Texture2D + self._storage_color: Buffer | Texture2D + self._storage_texture_id: Buffer | Texture2D + self._storage_index: Buffer | Texture2D + + @property + def storage_positions_angle(self) -> Buffer | Texture2D: + """ + Returns the buffer for sprite positions and angles. + This is a buffer of 4 x 32 bit floats (x, y, depth, angle). + """ + return self._storage_pos_angle + + @property + def storage_size(self) -> Buffer | Texture2D: + """ + Returns the buffer for sprite sizes. + This is a buffer of 2 x 32 bit floats (width, height). + """ + return self._storage_size + + @property + def storage_color(self) -> Buffer | Texture2D: + """ + Returns the buffer for sprite colors. + This is a buffer of 4 x bytes (r, g, b, a). + """ + return self._storage_color + + @property + def storage_texture_id(self) -> Buffer | Texture2D: + """ + Returns the buffer for sprite texture IDs. + This is a buffer of 32 bit integers (texture ID). + """ + return self._storage_texture_id + + @property + def storage_index(self) -> Buffer | Texture2D: + """ + Returns the buffer for sprite indices. + This is a buffer of 32 bit unsigned integers (sprite index). + """ + return self._storage_index + def write_sprite_buffers_to_gpu( self, # The data itself @@ -1335,7 +1380,7 @@ def render( """ raise NotImplementedError("This method should be implemented in subclasses.") - def get_nearby_sprite_indices(self, pos: Point2, size: Point2, length: int) -> list[int]: + def get_nearby_sprite_indices(self, pos: Point, size: Point, length: int) -> list[int]: """ Get indices of sprites that are nearby the given position and size. @@ -1359,23 +1404,29 @@ def __init__(self, ctx: ArcadeContext, capacity: int, atlas: TextureAtlasBase) - self._atlas = atlas # Buffers for each sprite attribute (read by shader) with initial capacity - self._sprite_pos_angle_buf = self.ctx.buffer( + self._storage_pos_angle: Buffer = self.ctx.buffer( reserve=self._buf_capacity * 16 ) # 4 x 32 bit floats - self._sprite_size_buf = self.ctx.buffer(reserve=self._buf_capacity * 8) # 2 x 32 bit floats - self._sprite_color_buf = self.ctx.buffer(reserve=self._buf_capacity * 4) # 4 x bytes colors - self._sprite_texture_buf = self.ctx.buffer(reserve=self._buf_capacity * 4) # 32 bit int + self._storage_size: Buffer = self.ctx.buffer( + reserve=self._buf_capacity * 8 + ) # 2 x 32 bit floats + self._storage_color: Buffer = self.ctx.buffer( + reserve=self._buf_capacity * 4 + ) # 4 x bytes colors + self._storage_texture_id: Buffer = self.ctx.buffer( + reserve=self._buf_capacity * 4 + ) # 32 bit int # Index buffer - self._sprite_index_buf = self.ctx.buffer( + self._storage_index: Buffer = self.ctx.buffer( reserve=self._idx_capacity * 4 ) # 32 bit unsigned integers contents = [ - gl.BufferDescription(self._sprite_pos_angle_buf, "4f", ["in_pos"]), - gl.BufferDescription(self._sprite_size_buf, "2f", ["in_size"]), - gl.BufferDescription(self._sprite_texture_buf, "1f", ["in_texture"]), + gl.BufferDescription(self._storage_pos_angle, "4f", ["in_pos"]), + gl.BufferDescription(self._storage_size, "2f", ["in_size"]), + gl.BufferDescription(self._storage_texture_id, "1f", ["in_texture"]), gl.BufferDescription( - self._sprite_color_buf, + self._storage_color, "4f1", ["in_color"], ), @@ -1386,7 +1437,7 @@ def __init__(self, ctx: ArcadeContext, capacity: int, atlas: TextureAtlasBase) - self._atlas = self.ctx.default_atlas self._geometry = self.ctx.geometry( contents, - index_buffer=self._sprite_index_buf, + index_buffer=self._storage_index, index_element_size=4, # 32 bit integers ) @@ -1405,7 +1456,7 @@ def geometry(self) -> Geometry: in float in_texture; in vec4 in_color; """ - return self._geometry # type: ignore + return self._geometry @property def buffer_positions_angle(self) -> Buffer: @@ -1419,7 +1470,7 @@ def buffer_positions_angle(self) -> Buffer: This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` instance with name ``in_pos``. """ - return self._sprite_pos_angle_buf + return self._storage_pos_angle @property def buffer_sizes(self) -> Buffer: @@ -1431,9 +1482,7 @@ def buffer_sizes(self) -> Buffer: This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` instance with name ``in_size``. """ - if self._sprite_size_buf is None: - raise ValueError("SpriteList is not initialized") - return self._sprite_size_buf + return self._storage_size @property def buffer_colors(self) -> Buffer: @@ -1446,7 +1495,7 @@ def buffer_colors(self) -> Buffer: This buffer is attached to the :py:attr:`~arcade.SpriteList.geometry` instance with name ``in_color``. """ - return self._sprite_color_buf + return self._storage_color @property def buffer_textures(self) -> Buffer: @@ -1468,7 +1517,7 @@ def buffer_textures(self) -> Buffer: compatibility we store them as 32 bit floats. We cast them to integers in the shader. """ - return self._sprite_texture_buf + return self._storage_texture_id @property def buffer_indices(self) -> Buffer: @@ -1488,7 +1537,7 @@ def buffer_indices(self) -> Buffer: instance and will be automatically be applied the the input buffers when rendering or transforming. """ - return self._sprite_index_buf + return self._storage_index def write_sprite_buffers_to_gpu( self, @@ -1520,38 +1569,38 @@ def write_sprite_buffers_to_gpu( sprite_index_changed: Whether the index data has changed. """ if sprite_pos_angle_changed: - self._sprite_pos_angle_buf.orphan() - self._sprite_pos_angle_buf.write(sprite_pos_angle_data) + self._storage_pos_angle.orphan() + self._storage_pos_angle.write(sprite_pos_angle_data) self._sprite_pos_angle_changed = False if sprite_size_changed: - self._sprite_size_buf.orphan() - self._sprite_size_buf.write(sprite_size_data) + self._storage_size.orphan() + self._storage_size.write(sprite_size_data) self._sprite_size_changed = False if sprite_color_changed: - self._sprite_color_buf.orphan() - self._sprite_color_buf.write(sprite_color_data) + self._storage_color.orphan() + self._storage_color.write(sprite_color_data) self._sprite_color_changed = False if sprite_texture_changed: - self._sprite_texture_buf.orphan() - self._sprite_texture_buf.write(sprite_texture_data) + self._storage_texture_id.orphan() + self._storage_texture_id.write(sprite_texture_data) self._sprite_texture_changed = False if sprite_index_changed: - self._sprite_index_buf.orphan() - self._sprite_index_buf.write(sprite_index_data) + self._storage_index.orphan() + self._storage_index.write(sprite_index_data) self._sprite_index_changed = False def grow_sprite_buffers(self) -> None: - self._sprite_pos_angle_buf.orphan(double=True) - self._sprite_size_buf.orphan(double=True) - self._sprite_color_buf.orphan(double=True) - self._sprite_texture_buf.orphan(double=True) + self._storage_pos_angle.orphan(double=True) + self._storage_size.orphan(double=True) + self._storage_color.orphan(double=True) + self._storage_texture_id.orphan(double=True) def grow_index_buffer(self) -> None: - self._sprite_index_buf.orphan(double=True) + self._storage_index.orphan(double=True) def render( self, @@ -1631,7 +1680,7 @@ def render( if blend_function is not None: self.ctx.blend_func = prev_blend_func - def get_nearby_sprite_indices(self, pos: Point2, size: Point2, length: int) -> list[int]: + def get_nearby_sprite_indices(self, pos: Point, size: Point, length: int) -> list[int]: """ Get indices of sprites that are nearby the given position and size. @@ -1689,11 +1738,21 @@ def __init__(self, ctx: ArcadeContext, capacity: int, atlas: TextureAtlasBase) - ) # Texture buffers for per-sprite data. These are looked up using gl_InstanceID - self._pos_angle_texture = self.ctx.texture(size=(capacity, 1), components=4, dtype="f4") - self._size_texture = self.ctx.texture(size=(capacity, 1), components=2, dtype="f4") - self._color_texture = self.ctx.texture(size=(capacity, 1), components=4, dtype="f1") - self._texture_id_texture = self.ctx.texture(size=(capacity, 1), components=1, dtype="f4") - self._index_texture = self.ctx.texture(size=(capacity, 1), components=1, dtype="i4") + self._storage_pos_angle: Texture2D = self.ctx.texture( + size=(capacity, 1), components=4, dtype="f4" + ) + self._storage_size: Texture2D = self.ctx.texture( + size=(capacity, 1), components=2, dtype="f4" + ) + self._storage_color: Texture2D = self.ctx.texture( + size=(capacity, 1), components=4, dtype="f1" + ) + self._storage_texture_id: Texture2D = self.ctx.texture( + size=(capacity, 1), components=1, dtype="f4" + ) + self._storage_index: Texture2D = self.ctx.texture( + size=(capacity, 1), components=1, dtype="i4" + ) def write_sprite_buffers_to_gpu( self, @@ -1726,19 +1785,19 @@ def write_sprite_buffers_to_gpu( sprite_index_changed: Whether the index data has changed. """ if sprite_pos_angle_changed: - self._pos_angle_texture.write(sprite_pos_angle_data) + self._storage_pos_angle.write(sprite_pos_angle_data) if sprite_size_changed: - self._size_texture.write(sprite_size_data) + self._storage_size.write(sprite_size_data) if sprite_color_changed: - self._color_texture.write(sprite_color_data) + self._storage_color.write(sprite_color_data) if sprite_texture_changed: - self._texture_id_texture.write(sprite_texture_data) + self._storage_texture_id.write(sprite_texture_data) if sprite_index_changed: - self._index_texture.write(sprite_index_data) + self._storage_index.write(sprite_index_data) def grow_sprite_buffers(self) -> None: """Double the internal storage""" @@ -1746,15 +1805,15 @@ def grow_sprite_buffers(self) -> None: self._buf_capacity = self._buf_capacity * 2 # Extend the textures so we don't lose the old data - self._pos_angle_texture.resize((256, self._buf_capacity // 256)) - self._size_texture.resize((256, self._buf_capacity // 256)) - self._color_texture.resize((256, self._buf_capacity // 256)) - self._texture_id_texture.resize((256, self._buf_capacity // 256)) + self._storage_pos_angle.resize((256, self._buf_capacity // 256)) + self._storage_size.resize((256, self._buf_capacity // 256)) + self._storage_color.resize((256, self._buf_capacity // 256)) + self._storage_texture_id.resize((256, self._buf_capacity // 256)) def grow_index_buffer(self) -> None: """Double the internal index buffer storage""" self._idx_capacity = self._idx_capacity * 2 - self._index_texture.resize((256, self._idx_capacity // 256)) + self._storage_index.resize((256, self._idx_capacity // 256)) def render( self, @@ -1819,11 +1878,11 @@ def render( atlas_texture.use(0) atlas.use_uv_texture(1) # Per-instance data - self._pos_angle_texture.use(2) - self._size_texture.use(3) - self._color_texture.use(4) - self._texture_id_texture.use(5) - self._index_texture.use(6) + self._storage_pos_angle.use(2) + self._storage_size.use(3) + self._storage_color.use(4) + self._storage_texture_id.use(5) + self._storage_index.use(6) self._geometry.render( self.program, @@ -1836,7 +1895,7 @@ def render( if blend_function is not None: self.ctx.blend_func = prev_blend_func - def get_nearby_sprite_indices(self, pos: Point2, size: Point2, length: int) -> list[int]: + def get_nearby_sprite_indices(self, pos: Point, size: Point, length: int) -> list[int]: """ Get indices of sprites that are nearby the given position and size. @@ -1853,9 +1912,9 @@ def get_nearby_sprite_indices(self, pos: Point2, size: Point2, length: int) -> l program["check_pos"] = pos program["check_size"] = size - self._pos_angle_texture.use(0) - self._size_texture.use(1) - self._index_texture.use(2) + self._storage_pos_angle.use(0) + self._storage_size.use(1) + self._storage_index.use(2) with ctx.collision_query: ctx.geometry_empty.transform( diff --git a/tests/unit/spritelist/test_spritelist.py b/tests/unit/spritelist/test_spritelist.py index 53187f2ee5..b0f371eced 100644 --- a/tests/unit/spritelist/test_spritelist.py +++ b/tests/unit/spritelist/test_spritelist.py @@ -184,7 +184,7 @@ def test_can_shuffle(ctx): # Ensure the index buffer is referring to the correct slots # Raw buffer from OpenGL index_data = struct.unpack( - f"{num_sprites}i", spritelist._sprite_index_buf.read()[: num_sprites * 4] + f"{num_sprites}i", spritelist.data.storage_index.read()[: num_sprites * 4] ) for i, sprite in enumerate(spritelist): # Check if slots are updated @@ -221,14 +221,14 @@ def test_sort(ctx): assert spritelist._sprite_index_data[0:3] == array("f", [0, 1, 2]) -@pytest.mark.parametrize("capacity", (128, 512, 1024)) +@pytest.mark.parametrize("capacity", (256, 512, 1024)) def test_clear(ctx, capacity): sp = arcade.SpriteList(capacity=capacity) sp.clear(capacity=None) assert len(sp._sprite_index_data) == capacity - assert len(sp._sprite_pos_data) == capacity * 3 - assert sp._sprite_index_buf.size == capacity * 4 - assert sp._sprite_pos_buf.size == capacity * 4 * 3 + assert len(sp._sprite_pos_angle_data) == capacity * 4 + assert len(sp.data.storage_index.read()) == capacity * 4 + assert len(sp.data.storage_positions_angle.read()) == capacity * 4 * 4 sp.extend(make_named_sprites(capacity)) sp.clear(capacity=capacity) @@ -237,9 +237,9 @@ def test_clear(ctx, capacity): assert sp._sprite_buffer_slots == 0 assert sp.atlas is not None assert len(sp._sprite_index_data) == capacity - assert len(sp._sprite_pos_data) == capacity * 3 - assert sp._sprite_index_buf.size == capacity * 4 - assert sp._sprite_pos_buf.size == capacity * 4 * 3 + assert len(sp._sprite_pos_angle_data) == capacity * 4 + assert len(sp.data.storage_index.read()) == capacity * 4 + assert len(sp.data.storage_positions_angle.read()) == capacity * 4 * 4 def test_color(): diff --git a/tests/unit/spritelist/test_spritelist_buffers.py b/tests/unit/spritelist/test_spritelist_buffers.py index 5b8aa89a3c..2063565c8c 100644 --- a/tests/unit/spritelist/test_spritelist_buffers.py +++ b/tests/unit/spritelist/test_spritelist_buffers.py @@ -10,19 +10,17 @@ def check_buff_sizes(sp: arcade.SpriteList): # Buffers capacity = sp._buf_capacity - assert sp._sprite_pos_buf.size == 12 * capacity # 3 floats - assert sp._sprite_angle_buf.size == 4 * capacity # 1 float - assert sp._sprite_color_buf.size == 4 * capacity # 4 floats - assert sp._sprite_size_buf.size == 8 * capacity # 2 floats - assert sp._sprite_texture_buf.size == 4 * capacity # 1 int + assert len(sp.data.storage_positions_angle.read()) == 16 * capacity # 3 floats + assert len(sp.data.storage_color.read()) == 4 * capacity # 4 floats + assert len(sp.data.storage_size.read()) == 8 * capacity # 2 floats + assert len(sp.data.storage_texture_id.read()) == 4 * capacity # 1 int # Arrays - assert len(sp._sprite_pos_data) == 3 * capacity # 3 floats - assert len(sp._sprite_angle_data) == 1 * capacity # 1 float + assert len(sp._sprite_pos_angle_data) == 4 * capacity # 3 floats assert len(sp._sprite_color_data) == 4 * capacity # 1 float assert len(sp._sprite_size_data) == 2 * capacity # 2 floats assert len(sp._sprite_texture_data) == 1 * capacity # 1 int # Index buffer - assert sp._sprite_index_buf.size == 4 * sp._idx_capacity # 1 int + assert len(sp.data.storage_index.read()) == 4 * sp._idx_capacity # 1 int assert len(sp._sprite_index_data) == 1 * sp._idx_capacity # 1 int # Slots assert len(sp.sprite_slot) == len(sp) @@ -40,11 +38,11 @@ def test_buffer_sizes(ctx: arcade.ArcadeContext): arcade.color.GREEN, arcade.color.BLUE, ) - positions = ( - (0, 1, 2), - (10, 11, 12), - (20, 21, 22), - (30, 31, 32), + positions_angle = ( + (0, 1, 2, 0), + (10, 11, 12, 1), + (20, 21, 22, 2), + (30, 31, 32, 3), ) sizes = ( (10, 20), @@ -52,77 +50,73 @@ def test_buffer_sizes(ctx: arcade.ArcadeContext): (50, 60), (70, 80), ) - angles = (0, 90, 180, 270) sprites = [] - for color, size, angle, pos in zip(colors, sizes, angles, positions): + for color, size, pos_angle in zip(colors, sizes, positions_angle): sprite = arcade.SpriteSolidColor( *size, - center_x=pos[0], - center_y=pos[1], + center_x=pos_angle[0], + center_y=pos_angle[1], color=color, - angle=angle, + angle=pos_angle[3], ) - sprite.depth = pos[2] + sprite.depth = pos_angle[2] sprites.append(sprite) sp: arcade.SpriteList = arcade.SpriteList(capacity=1) # Initial capacity - assert sp._buf_capacity == 1 - assert sp._idx_capacity == 1 + assert sp._buf_capacity == 256 + assert sp._idx_capacity == 256 check_buff_sizes(sp) # After adding a sprite filling the capacity (1) sp.append(sprites[0]) - assert sp._buf_capacity == 1 - assert sp._idx_capacity == 1 + assert sp._buf_capacity == 256 + assert sp._idx_capacity == 256 check_buff_sizes(sp) # Adding one more sprite should double the capacity (2) sp.append(sprites[1]) - assert sp._buf_capacity == 2 - assert sp._idx_capacity == 2 + assert sp._buf_capacity == 256 + assert sp._idx_capacity == 256 check_buff_sizes(sp) # Adding 1 more sprites to pass max capacity (3) sp.append(sprites[2]) - assert sp._buf_capacity == 4 - assert sp._idx_capacity == 4 + assert sp._buf_capacity == 256 + assert sp._idx_capacity == 256 check_buff_sizes(sp) # Adding 1 more sprites to pass max capacity (4) sp.append(sprites[3]) - assert sp._buf_capacity == 4 - assert sp._idx_capacity == 4 + assert sp._buf_capacity == 256 + assert sp._idx_capacity == 256 check_buff_sizes(sp) sp.write_sprite_buffers_to_gpu() # Test the contents of the arrays and buffers. # Prepare expected data - expected_pos_data = struct.pack("12f", *[v for p in positions for v in p]) + expected_pos_data = struct.pack("16f", *[v for p in positions_angle for v in p]) expected_color_data = struct.pack("16B", *[v for c in colors for v in c]) expected_size_data = struct.pack("8f", *[v for s in sizes for v in s]) - expected_angle_data = struct.pack("4f", *angles) expected_texture_data = struct.pack( "4f", *[ctx.default_atlas.get_texture_id(sprite.texture) for sprite in sprites], ) # Check the buffers - assert sp._sprite_pos_buf.read() == expected_pos_data - assert sp._sprite_color_buf.read() == expected_color_data - assert sp._sprite_size_buf.read() == expected_size_data - assert sp._sprite_angle_buf.read() == expected_angle_data - assert sp._sprite_texture_buf.read() == expected_texture_data + assert sp.data.storage_positions_angle.read().startswith(expected_pos_data) + assert sp.data.storage_color.read().startswith(expected_color_data) + assert sp.data.storage_size.read().startswith(expected_size_data) + assert sp.data.storage_texture_id.read().startswith(expected_texture_data) # Check arrays - assert sp._sprite_pos_data.tobytes() == expected_pos_data - assert sp._sprite_color_data.tobytes() == expected_color_data - assert sp._sprite_size_data.tobytes() == expected_size_data - assert sp._sprite_angle_data.tobytes() == expected_angle_data - assert sp._sprite_texture_data.tobytes() == expected_texture_data + assert sp._sprite_pos_angle_data.tobytes().startswith(expected_pos_data) + assert sp._sprite_color_data.tobytes().startswith(expected_color_data) + assert sp._sprite_size_data.tobytes().startswith(expected_size_data) + assert sp._sprite_texture_data.tobytes().startswith(expected_texture_data) # Index buffer - assert sp._sprite_index_buf.read() == struct.pack("4i", 0, 1, 2, 3) + assert sp.data.storage_index.read().startswith(struct.pack("4i", 0, 1, 2, 3)) diff --git a/tests/unit/spritelist/test_spritelist_lazy.py b/tests/unit/spritelist/test_spritelist_lazy.py index 014600d186..b373e8160c 100644 --- a/tests/unit/spritelist/test_spritelist_lazy.py +++ b/tests/unit/spritelist/test_spritelist_lazy.py @@ -8,9 +8,7 @@ def test_create_lazy_equals_true(): spritelist = arcade.SpriteList(lazy=True, use_spatial_hash=True) # Make sure OpenGL abstractions are not created - assert spritelist._sprite_pos_buf == None - assert spritelist._geometry == None - assert spritelist.atlas is None + assert spritelist._data is None # Make sure CPU-only behavior still works correctly for x in range(100): @@ -37,8 +35,7 @@ def test_manual_initialization_after_lazy_equals_true(window): # Make sure initialization still worked correctly. spritelist.initialize() assert spritelist._initialized - assert spritelist._sprite_pos_buf - assert spritelist._geometry + assert spritelist._data assert isinstance(spritelist.atlas, DefaultTextureAtlas) # Uncomment the next line and set a breakpoint on it to From 24fda212786bb4bb8955d9f475f41d62d260509a Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 20:55:07 +0200 Subject: [PATCH 16/21] Type fix after mypy upgrade --- arcade/examples/sections_demo_3.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arcade/examples/sections_demo_3.py b/arcade/examples/sections_demo_3.py index a05640b0cd..d95a1084d3 100644 --- a/arcade/examples/sections_demo_3.py +++ b/arcade/examples/sections_demo_3.py @@ -87,7 +87,7 @@ def draw_button(self): def on_resize(self, width: int, height: int): """set position on screen resize""" self.left = width // 3 - self.bottom = (height // 2) - self.height // 2 + self.bottom = (height // 2) - self.height // 2 # type: ignore pos = self.left + self.width / 2, self.bottom + self.height / 2 self.button.position = pos @@ -203,7 +203,7 @@ def on_mouse_press(self, x: float, y: float, button: int, modifiers: int): def on_resize(self, width: int, height: int): # stick to the right - self.left = width - self.width + self.left = width - self.width # type: ignore self.height = height - self.view.info_bar.height self.button_stop.position = self.left + self.width / 2, self.top - 80 From 4810b67d5f523e7bb87ddf92d6df669be9801a8c Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 20:57:00 +0200 Subject: [PATCH 17/21] import order --- arcade/sprite_list/sprite_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index a25b233143..51b2aaf11d 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -25,7 +25,7 @@ from arcade.gl.buffer import Buffer from arcade.gl.types import BlendFunction, OpenGlFilter, PyGLenum from arcade.gl.vertex_array import Geometry -from arcade.types import RGBA255, Color, Point2, Point, RGBANormalized, RGBOrA255, RGBOrANormalized +from arcade.types import RGBA255, Color, Point, Point2, RGBANormalized, RGBOrA255, RGBOrANormalized from arcade.utils import copy_dunders_unimplemented if TYPE_CHECKING: From 91a95eaca70261ec2a6440054937373476ad85a9 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 21:15:33 +0200 Subject: [PATCH 18/21] test: Add WindowProxy.register_event_type --- tests/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 4310fb6288..513f5c9baf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -348,6 +348,10 @@ def center_window(self): def set_vsync(self, vsync): self.window.set_vsync(vsync) + @staticmethod + def register_event_type(*args, **kwargs): + pass + @property def default_camera(self): """ From 8ead822a56f659ff94a19a629cce39f3b4ede36a Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 21:24:41 +0200 Subject: [PATCH 19/21] test: Skip spritelist interaction for now --- tests/integration/examples/test_examples.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/examples/test_examples.py b/tests/integration/examples/test_examples.py index f80c900048..f440805a79 100644 --- a/tests/integration/examples/test_examples.py +++ b/tests/integration/examples/test_examples.py @@ -37,6 +37,7 @@ "multisample", # Anything requiring multisampling we can't run in unit test "indirect", # Indirect rendering cannot be run in unit test "bindless", # Bindless textures cannot be run in unit test + "spritelist_interaction", # Currently only works for spritelist buffer backend. Not textures. ] From 0349f926e5218780b63888779f36faa48c1e64d6 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 22:25:48 +0200 Subject: [PATCH 20/21] Remaining sprite work - UV bias - Single sprite rendering - Cache more stuff in context --- arcade/context.py | 30 ++++++++ arcade/draw/rect.py | 9 ++- .../sprites/sprite_list_simple_vs.glsl | 15 +++- .../sprites/sprite_single_simple_vs.glsl | 70 +++++++++++++++++++ arcade/sprite_list/sprite_list.py | 16 +---- 5 files changed, 119 insertions(+), 21 deletions(-) create mode 100644 arcade/resources/system/shaders/sprites/sprite_single_simple_vs.glsl diff --git a/arcade/context.py b/arcade/context.py index 78073f1dbb..2e714fa08f 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -3,6 +3,7 @@ Contains pre-loaded programs """ +from array import array from collections.abc import Iterable, Sequence from pathlib import Path from typing import Any @@ -112,6 +113,7 @@ def __init__( self.sprite_list_program_no_geo["texture_id_data"] = 5 self.sprite_list_program_no_geo["index_data"] = 6 + # Geo shader single sprite program self.sprite_program_single = self.load_program( vertex_shader=":system:shaders/sprites/sprite_single_vs.glsl", geometry_shader=":system:shaders/sprites/sprite_list_geometry_no_cull_geo.glsl", @@ -120,6 +122,34 @@ def __init__( self.sprite_program_single["sprite_texture"] = 0 self.sprite_program_single["uv_texture"] = 1 self.sprite_program_single["spritelist_color"] = 1.0, 1.0, 1.0, 1.0 + # Non-geometry shader single sprite program + self.sprite_program_single_simple = self.load_program( + vertex_shader=":system:shaders/sprites/sprite_single_simple_vs.glsl", + fragment_shader=":system:shaders/sprites/sprite_list_simple_fs.glsl", + ) + self.sprite_program_single_simple["sprite_texture"] = 0 + self.sprite_program_single_simple["uv_texture"] = 1 + self.sprite_program_single_simple["spritelist_color"] = 1.0, 1.0, 1.0, 1.0 + + # fmt: off + self.spritelist_geometry_simple = self.geometry( + [ + BufferDescription( + self.buffer( + data=array("f", [ + -0.5, +0.5, # Upper left + -0.5, -0.5, # lower left + +0.5, +0.5, # upper right + +0.5, -0.5, # lower right + ]) + ), + "2f", + ["in_pos"] + ), + ], + mode=self.TRIANGLE_STRIP, + ) + # fmt: on # Shapes self.shape_line_program: Program = self.load_program( diff --git a/arcade/draw/rect.py b/arcade/draw/rect.py index c7790d68b4..0fc3d832ee 100644 --- a/arcade/draw/rect.py +++ b/arcade/draw/rect.py @@ -54,7 +54,7 @@ def draw_texture_rect( ctx.disable(ctx.BLEND) atlas = atlas or ctx.default_atlas - program = ctx.sprite_program_single + program = ctx.sprite_program_single_simple texture_id, _ = atlas.add(texture) if pixelated: @@ -68,14 +68,13 @@ def draw_texture_rect( atlas.use_uv_texture(unit=1) geometry = ctx.geometry_empty - program["pos"] = rect.x, rect.y, 0 + program["pos_rot"] = rect.x, rect.y, 0, angle program["color"] = color.normalized program["size"] = rect.width, rect.height - program["angle"] = angle - program["texture_id"] = float(texture_id) + program["texture_id"] = texture_id program["spritelist_color"] = 1.0, 1.0, 1.0, alpha_normalized - geometry.render(program, mode=gl.POINTS, vertices=1) + geometry.render(program, mode=gl.TRIANGLE_STRIP, vertices=4) if blend: ctx.disable(ctx.BLEND) diff --git a/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl index f1c9989a7d..fe6a852118 100644 --- a/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl +++ b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl @@ -52,7 +52,20 @@ void main() { mat4 mvp = window.projection * window.view; - // TODO: Half pixel offset + // Apply half pixel offset modified by bias. + // What bias to set depends on the texture filtering mode. + // Linear can have 1.0 bias while nearest should have 0.0 (unless scale is 1:1) + // uvs ( + // 0.0, 0.0, + // 1.0, 0.0, + // 0.0, 1.0, + // 1.0, 1.0 + // ) + vec2 hp = 0.5 / vec2(textureSize(sprite_texture, 0)) * uv_offset_bias; + uv0 += hp; + uv1 += vec2(-hp.x, hp.y); + uv2 += vec2(hp.x, -hp.y); + uv3 += -hp; int vertex_id = gl_VertexID % 4; vec2 uvs[4] = vec2[4](uv0, uv2, uv1, uv3); diff --git a/arcade/resources/system/shaders/sprites/sprite_single_simple_vs.glsl b/arcade/resources/system/shaders/sprites/sprite_single_simple_vs.glsl new file mode 100644 index 0000000000..c3a9821f8d --- /dev/null +++ b/arcade/resources/system/shaders/sprites/sprite_single_simple_vs.glsl @@ -0,0 +1,70 @@ +#version 330 +// vert/frag only version of the sprite list shader + +uniform WindowBlock { + mat4 projection; + mat4 view; +} window; + +// Texture atlas +uniform sampler2D sprite_texture; +// Texture containing UVs for the entire atlas +uniform sampler2D uv_texture; + +uniform vec4 pos_rot; // rect.x, rect.y, 0, angle +uniform vec4 color; // color.normalized +uniform vec2 size; // rect.width, rect.height +uniform int texture_id; + +// How much half-pixel offset to apply to the UVs. +// 0.0 is no offset, 1.0 is half a pixel offset +uniform float uv_offset_bias; + +// Output to frag shader +out vec2 v_uv; +out vec4 v_color; + +#include :system:shaders/lib/sprite.glsl + + +const vec2 vertices[4] = vec2[4]( + vec2(-0.5, +0.5), // Upper left + vec2(-0.5, -0.5), // lower left + vec2(+0.5, +0.5), // upper right + vec2(+0.5, -0.5) // lower right +); + +void main() { + vec2 uv0, uv1, uv2, uv3; + getSpriteUVs(uv_texture, texture_id, uv0, uv1, uv2, uv3); + + vec3 center = pos_rot.xyz; + float angle = radians(pos_rot.w); + mat2 rot = mat2( + cos(angle), -sin(angle), + sin(angle), cos(angle) + ); + + mat4 mvp = window.projection * window.view; + + // Apply half pixel offset modified by bias. + // What bias to set depends on the texture filtering mode. + // Linear can have 1.0 bias while nearest should have 0.0 (unless scale is 1:1) + // uvs ( + // 0.0, 0.0, + // 1.0, 0.0, + // 0.0, 1.0, + // 1.0, 1.0 + // ) + vec2 hp = 0.5 / vec2(textureSize(sprite_texture, 0)) * uv_offset_bias; + uv0 += hp; + uv1 += vec2(-hp.x, hp.y); + uv2 += vec2(hp.x, -hp.y); + uv3 += -hp; + + int vertex_id = gl_VertexID % 4; + vec2 uvs[4] = vec2[4](uv0, uv2, uv1, uv3); + v_color = color; + gl_Position = mvp * vec4(rot * (vertices[vertex_id] * size) + center.xy, 0.0, 1.0); + v_uv = uvs[vertex_id]; +} diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index 51b2aaf11d..040ab0ad1d 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -1721,21 +1721,7 @@ def __init__(self, ctx: ArcadeContext, capacity: int, atlas: TextureAtlasBase) - # Program without geo shader self.program = self.ctx.sprite_list_program_no_geo self._atlas = atlas or self.ctx.default_atlas - - # fmt: off - self._instance_buffer = self.ctx.buffer( - data=array("f", [ - -0.5, +0.5, # Upper left - -0.5, -0.5, # lower left - +0.5, +0.5, # upper right - +0.5, -0.5, # lower right - ]), - ) - # fmt: on - self._geometry = self.ctx.geometry( - [BufferDescription(self._instance_buffer, "2f", ["in_pos"])], - mode=self.ctx.TRIANGLE_STRIP, - ) + self._geometry = self.ctx.spritelist_geometry_simple # Texture buffers for per-sprite data. These are looked up using gl_InstanceID self._storage_pos_angle: Texture2D = self.ctx.texture( From 53e093366fd9e557ec8d3e60d7d8e88888cd2e78 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Thu, 29 May 2025 22:27:37 +0200 Subject: [PATCH 21/21] lint --- arcade/sprite_list/sprite_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index 040ab0ad1d..83887a83b6 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -21,7 +21,7 @@ ) from arcade import Sprite, SpriteType, SpriteType_co, get_window, gl -from arcade.gl import BufferDescription, Program, Texture2D +from arcade.gl import Program, Texture2D from arcade.gl.buffer import Buffer from arcade.gl.types import BlendFunction, OpenGlFilter, PyGLenum from arcade.gl.vertex_array import Geometry