diff --git a/arcade/context.py b/arcade/context.py index 58d5f94b5f..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 @@ -99,6 +100,20 @@ 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 + + # 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", @@ -107,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( @@ -144,11 +187,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/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/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 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 9a54e2f12a..9474fbf8a1 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 +// Buffer version if collision shader -in vec3 in_pos; +in vec4 in_pos; in vec2 in_size; out vec2 pos; diff --git a/arcade/resources/system/shaders/lib/sprite.glsl b/arcade/resources/system/shaders/lib/sprite.glsl index f56908f248..bc96369108 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 % 256, index / 256), 0); +} + +vec2 getInstanceSize(sampler2D sizeData, int index) { + return texelFetch(sizeData, ivec2(index % 256, index / 256), 0).xy; +} + +vec4 getInstanceColor(sampler2D colorData, int index) { + return texelFetch(colorData, ivec2(index % 256, index / 256), 0); +} + +int getInstanceTextureId(sampler2D textureIdData, int index) { + return int(texelFetch(textureIdData, ivec2(index % 256, index / 256), 0).x); +} + +int getInstanceIndex(isampler2D indexData, int index) { + return texelFetch(indexData, ivec2(index % 256, index / 256), 0).x; +} 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_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_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_fs.glsl b/arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl new file mode 100644 index 0000000000..5f6a8ee4c3 --- /dev/null +++ b/arcade/resources/system/shaders/sprites/sprite_list_simple_fs.glsl @@ -0,0 +1,23 @@ +#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 v_uv; +in vec4 v_color; + +out vec4 f_color; + +void main() { + // 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; +} 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..fe6a852118 --- /dev/null +++ b/arcade/resources/system/shaders/sprites/sprite_list_simple_vs.glsl @@ -0,0 +1,75 @@ +#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; + +// Per instance data +uniform sampler2D pos_data; +uniform sampler2D size_data; +uniform sampler2D color_data; +uniform sampler2D 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; + +// Instanced geometry (rectangle as triangle strip) +in vec2 in_pos; + +// Output to frag shader +out vec2 v_uv; +out vec4 v_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, 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 * (in_pos * size) + center.xy, 0.0, 1.0); + v_uv = uvs[vertex_id]; +} 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/collision.py b/arcade/sprite_list/collision.py index 60330bb1eb..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, @@ -134,50 +130,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.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 8c07f359ff..83887a83b6 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 @@ -24,15 +25,23 @@ 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, Point, Point2, RGBANormalized, RGBOrA255, RGBOrANormalized from arcade.utils import copy_dunders_unimplemented if TYPE_CHECKING: - from arcade import DefaultTextureAtlas, Texture + from arcade import ArcadeContext, Texture 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]): @@ -131,6 +140,23 @@ def draw_hit_boxes( """ ... + @abstractmethod + 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 + 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: ... @@ -227,10 +253,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 @@ -245,30 +272,20 @@ 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 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._data: SpriteListData | None = None # 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 @@ -301,41 +318,20 @@ 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 - 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"], - ), - ] - self._geometry = self.ctx.geometry( - contents, - index_buffer=self._sprite_index_buf, - index_element_size=4, # 32 bit integers - ) - + # NOTE: Instantiate the appropriate spritelist data class here + # Desktop GL (with geo shader) + # self._data = SpriteListBufferData( + # self.ctx, capacity=self._buf_capacity, atlas=self._atlas + # ) + # WebGL (without geo shader) + 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. + # 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") @@ -346,9 +342,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 @@ -502,131 +497,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 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._data # type: ignore[return-value] def _next_slot(self) -> int: """ @@ -687,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 @@ -697,9 +573,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 @@ -757,7 +632,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") @@ -870,7 +744,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() @@ -882,9 +755,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 @@ -900,9 +770,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) @@ -945,9 +812,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 @@ -1050,35 +914,28 @@ 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.data.write_sprite_buffers_to_gpu( + # Buffer data + self._sprite_pos_angle_data, + self._sprite_size_data, + self._sprite_color_data, + self._sprite_texture_data, + self._sprite_index_data, + # Changed flags + self._sprite_pos_angle_changed, + self._sprite_size_changed, + self._sprite_color_changed, + 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: """ @@ -1107,69 +964,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.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: @@ -1190,24 +996,29 @@ 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 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 + 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.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""" @@ -1220,28 +1031,22 @@ 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: - # 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._sprite_pos_changed = True + self.data.grow_sprite_buffers() + + 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 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 @@ -1249,8 +1054,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.data.grow_index_buffer() self._sprite_index_changed = True @@ -1264,17 +1069,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] @@ -1337,9 +1141,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: """ @@ -1353,8 +1157,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: """ @@ -1368,8 +1172,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: """ @@ -1380,8 +1184,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: """ @@ -1445,5 +1249,667 @@ 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: + """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 + + # 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 + 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: + """ + 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. + """ + 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.") + + 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. + + 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.""" + + 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._storage_pos_angle: Buffer = self.ctx.buffer( + reserve=self._buf_capacity * 16 + ) # 4 x 32 bit floats + 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._storage_index: Buffer = self.ctx.buffer( + reserve=self._idx_capacity * 4 + ) # 32 bit unsigned integers + + contents = [ + 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._storage_color, + "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._storage_index, + 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 + + @property + def buffer_positions_angle(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._storage_pos_angle + + @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``. + """ + return self._storage_size + + @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._storage_color + + @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._storage_texture_id + + @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._storage_index + + 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: + """ + 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_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._storage_pos_angle.orphan() + self._storage_pos_angle.write(sprite_pos_angle_data) + self._sprite_pos_angle_changed = False + + if sprite_size_changed: + self._storage_size.orphan() + self._storage_size.write(sprite_size_data) + self._sprite_size_changed = False + + if sprite_color_changed: + self._storage_color.orphan() + self._storage_color.write(sprite_color_data) + self._sprite_color_changed = False + + if sprite_texture_changed: + self._storage_texture_id.orphan() + self._storage_texture_id.write(sprite_texture_data) + self._sprite_texture_changed = False + + if sprite_index_changed: + self._storage_index.orphan() + self._storage_index.write(sprite_index_data) + self._sprite_index_changed = False + + def grow_sprite_buffers(self) -> None: + 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._storage_index.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) + 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 + + 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. + + 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 + buffer = ctx.collision_buffer + 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.""" + + 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.sprite_list_program_no_geo + self._atlas = atlas or self.ctx.default_atlas + 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( + 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, + # 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: + """ + 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._storage_pos_angle.write(sprite_pos_angle_data) + + if sprite_size_changed: + self._storage_size.write(sprite_size_data) + + if sprite_color_changed: + self._storage_color.write(sprite_color_data) + + if sprite_texture_changed: + self._storage_texture_id.write(sprite_texture_data) + + if sprite_index_changed: + self._storage_index.write(sprite_index_data) + + 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._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._storage_index.resize((256, self._idx_capacity // 256)) + + 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 + + try: + self.program["spritelist_color"] = color + except KeyError: + pass + + # 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._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, + instances=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 + + 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. + + 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._storage_pos_angle.use(0) + self._storage_size.use(1) + self._storage_index.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))] 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 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): """ 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. ] 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