Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.
- Fixed fast sprites passing through thin walls and platforms in `PhysicsEngineSimple` and `PhysicsEnginePlatformer`. A sprite that moved farther than its own size plus the wall's thickness in one frame jumped over it, for example a player falling fast onto a thin platform. A move longer than the sprite is now checked in steps shorter than the sprite, so it stops at the first wall in the way. Moves shorter than the sprite work exactly as before.
- Fixed collision checks with `CollisionMethod.GPU` (also chosen automatically for lists of over 1500 sprites without a spatial hash) missing hit boxes that reach beyond a sprite's drawn size, such as a melee attack's reach. The GPU only knows the sprites' drawn sizes, so it now also uses the checking sprite's hit box, and each `SpriteList` keeps track of how far its sprites' hit boxes reach beyond their sizes.
- Fixed drawing inside `TextureAtlas.render_into()` when the atlas had to rebuild to make room, for example to add a new texture being drawn. A full atlas with freed space rebuilds instead of growing, which moved the texture being drawn into, so the drawing landed on other textures and what was drawn before was lost. While rendering into it, the atlas now grows instead if it can, and a rebuild keeps what was drawn and moves the drawing area with the texture.
- Fixed a `SpriteList` on WebGL only drawing its first 256 sprites when its storage was created with a capacity over 256: for example `SpriteList(capacity=1000)`, `clear()` on a list that had grown past 256 sprites, or a lazy list with more than 256 sprites before it was first drawn ([#2909](https://github.com/pythonarcade/arcade/issues/2909)).

### New Features
- Added `arcade.sweep_sprite(sprite, dx, dy, sprite_list)`, which checks the whole path of a moving sprite and returns a `SweepInfo` for the first sprite it would hit (the `sprite`, how far along the move as a `fraction` and `distance`, and the surface `normal`), or `None`. Fast sprites can't pass through thin walls this way. A sprite that already overlaps one is an immediate hit. Added the `sprite_bullets_sweep` example comparing it with a plain collision check.
Expand Down
24 changes: 9 additions & 15 deletions arcade/sprite_list/sprite_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -1865,21 +1865,15 @@ def __init__(self, ctx: ArcadeContext, capacity: int, atlas: TextureAtlasBase) -
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"
)
# The shader reads the data as rows of 256 sprites (see lib/sprite.glsl),
# and the grow methods keep that layout, so create the textures in it.
# A single row wider than 256 would only draw the first 256 sprites.
size = (256, _align_capacity(capacity) // 256)
self._storage_pos_angle: Texture2D = self.ctx.texture(size=size, components=4, dtype="f4")
self._storage_size: Texture2D = self.ctx.texture(size=size, components=2, dtype="f4")
self._storage_color: Texture2D = self.ctx.texture(size=size, components=4, dtype="f1")
self._storage_texture_id: Texture2D = self.ctx.texture(size=size, components=1, dtype="f4")
self._storage_index: Texture2D = self.ctx.texture(size=size, components=1, dtype="i4")

def write_sprite_buffers_to_gpu(
self,
Expand Down
74 changes: 74 additions & 0 deletions tests/unit/spritelist/test_spritelist_texture_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""
On WebGL, a SpriteList keeps its sprite data in textures instead of buffers.
These tests use that storage on desktop OpenGL, so they run in CI.

The shader reads the textures as rows of 256 sprites. They used to be
created as a single row as wide as the capacity, so a list whose storage
was created with a capacity over 256 only drew its first 256 sprites.
"""

import pytest

import arcade
from arcade.sprite_list import sprite_list as sprite_list_module


@pytest.fixture
def texture_storage(monkeypatch):
"""New SpriteLists use the WebGL storage, in textures"""
monkeypatch.setattr(
sprite_list_module, "SpriteListBufferData", sprite_list_module.SpriteListTextureData
)


def _add_pixels(sprites, count):
for i in range(count):
x, y = (i % 30) * 3 + 1.5, (i // 30) * 3 + 1.5
sprites.append(arcade.SpriteSolidColor(1, 1, center_x=x, center_y=y))


def _drawn(ctx, sprites):
"""How many pixels the sprites cover, one per sprite"""
fbo = ctx.framebuffer(color_attachments=[ctx.texture((100, 100), components=4)])
with fbo.activate():
fbo.clear()
sprites.draw()
data = fbo.read(components=4)
return sum(1 for i in range(3, len(data), 4) if data[i] > 0)


@pytest.mark.parametrize("capacity", [100, 256, 1000, 1024, 3000])
def test_created_with_capacity(ctx, texture_storage, capacity):
sprites = arcade.SpriteList(capacity=capacity)
assert isinstance(sprites._data, sprite_list_module.SpriteListTextureData)
_add_pixels(sprites, 600)
assert _drawn(ctx, sprites) == 600


def test_clear_after_growing(ctx, texture_storage):
"""clear() keeps the grown capacity and creates the storage again"""
sprites = arcade.SpriteList()
_add_pixels(sprites, 300)
sprites.clear()
_add_pixels(sprites, 400)
assert _drawn(ctx, sprites) == 400


def test_lazy_list_grown_before_first_draw(ctx, texture_storage):
"""A lazy list creates its storage at the capacity it has grown to"""
sprites = arcade.SpriteList(lazy=True)
_add_pixels(sprites, 400)
assert _drawn(ctx, sprites) == 400


def test_storage_is_rows_of_256(ctx, texture_storage):
sprites = arcade.SpriteList(capacity=1024)
data = sprites._data
for texture in (
data._storage_pos_angle,
data._storage_size,
data._storage_color,
data._storage_texture_id,
data._storage_index,
):
assert texture.size == (256, 4)
Loading