From 4cc5365f2790466898af72debfce5e1f4c84991f Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Thu, 1 Oct 2026 14:48:24 -0500 Subject: [PATCH] Fix GPU collision missing sprites flipped with negative scale The GPU collision geometry shaders compared sizes with max(size[0], check_size). A sprite flipped with a negative scale has a negative width or height, so max() picked the other sprite's size and the flipped sprite could be rejected even when the two overlapped. This is the GPU counterpart of #2892. Use abs() on both sizes, in the buffer and texture versions of the shader. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../shaders/collision/col_tex_trans_gs.glsl | 3 +- .../shaders/collision/col_trans_gs.glsl | 3 +- tests/unit/sprite/test_sprite_collision.py | 35 +++++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daa785ca44..6effa80dc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page. - Fixed `SpriteList.pop()` with a negative index other than `-1` (such as `pop(-2)`) drawing the wrong sprites: the removed sprite stayed on screen and another sprite disappeared. - Fixed `SpriteList.rescale()` moving the list's center while rescaling, so sprites after the first were scaled around the wrong point. - Fixed `SpriteList.preload_textures()` raising `AttributeError` on a lazy sprite list that hadn't been drawn yet. It now preloads into the atlas the list will use. +- Fixed GPU collision checks (`CollisionMethod.GPU`, also used automatically for sprite lists over 1500 sprites without a spatial hash) missing sprites flipped with a negative scale, whose negative width or height made them look smaller than they are. ### New Features - Added `HitBox.get_adjusted_bounds()`, which returns the cached `(left, right, bottom, top)` bounds of the adjusted hit box points. diff --git a/arcade/resources/system/shaders/collision/col_tex_trans_gs.glsl b/arcade/resources/system/shaders/collision/col_tex_trans_gs.glsl index ac2e453dad..9313299d0e 100644 --- a/arcade/resources/system/shaders/collision/col_tex_trans_gs.glsl +++ b/arcade/resources/system/shaders/collision/col_tex_trans_gs.glsl @@ -19,7 +19,8 @@ void main() { // Get the maximum x and y size // max() works per component - vec2 size = max(size[0], check_size); + // Sizes are negative for sprites flipped with a negative scale + vec2 size = max(abs(size[0]), abs(check_size)); // Destroy the sprite if too far away if (dist < max(size.x, size.y) * 1.42) { diff --git a/arcade/resources/system/shaders/collision/col_trans_gs.glsl b/arcade/resources/system/shaders/collision/col_trans_gs.glsl index 068ce68e0d..0d50665140 100644 --- a/arcade/resources/system/shaders/collision/col_trans_gs.glsl +++ b/arcade/resources/system/shaders/collision/col_trans_gs.glsl @@ -19,7 +19,8 @@ void main() { // Get the maximum x and y size // max() works per component - vec2 size = max(size[0], check_size); + // Sizes are negative for sprites flipped with a negative scale + vec2 size = max(abs(size[0]), abs(check_size)); // Destroy the sprite if too far away if (dist < max(size.x, size.y) * 1.42) { diff --git a/tests/unit/sprite/test_sprite_collision.py b/tests/unit/sprite/test_sprite_collision.py index de52f7bd16..b984f78a90 100644 --- a/tests/unit/sprite/test_sprite_collision.py +++ b/tests/unit/sprite/test_sprite_collision.py @@ -499,6 +499,41 @@ def test_check_for_collision_between_lists_type_errors(window): arcade.check_for_collision_between_lists(arcade.SpriteList(), "moo") +@pytest.mark.parametrize( + "wall_scale, bullet_scale", + [ + ((1, 1), (1, 1)), + ((-1, 1), (1, 1)), + ((1, -1), (1, 1)), + ((-1, -1), (1, 1)), + ((1, 1), (-1, -1)), + ((-1, -1), (-1, -1)), + ], +) +def test_gpu_collision_flipped_sprites(window, wall_scale, bullet_scale): + """The GPU path finds sprites flipped with a negative scale""" + if window.ctx._gl_api == "webgl": + pytest.skip("GPU collision isn't supported on WebGL") + + # A long wall and a small bullet near its end, so only the wall's own + # size can bring them close enough + wall = arcade.SpriteSolidColor(200, 20) + wall.scale = wall_scale + walls = arcade.SpriteList() + walls.append(wall) + bullet = arcade.SpriteSolidColor(4, 4, center_x=80) + bullet.scale = bullet_scale + + assert walls.get_nearby_sprites_gpu(bullet.position, bullet.size) == [wall] + gpu = arcade.CollisionMethod.GPU + assert arcade.check_for_collision_with_list(bullet, walls, method=gpu) == [wall] + + # And still not when it's far away + bullet.position = 400, 0 + assert walls.get_nearby_sprites_gpu(bullet.position, bullet.size) == [] + assert arcade.check_for_collision_with_list(bullet, walls, method=gpu) == [] + + def test_check_for_collision_with_list(window): # TODO: Check that the right collision function is called internally a = arcade.SpriteSolidColor(50, 50, color=arcade.csscolor.RED)