diff --git a/arcade/draw/rect.py b/arcade/draw/rect.py index 56918d72a2..bf254914cc 100644 --- a/arcade/draw/rect.py +++ b/arcade/draw/rect.py @@ -56,18 +56,20 @@ def draw_texture_rect( ctx.disable(ctx.BLEND) atlas = atlas or ctx.default_atlas + program = ctx.sprite_program_single texture_id, _ = atlas.add(texture) if pixelated: atlas.texture.filter = gl.NEAREST, gl.NEAREST + program.set_uniform_safe("uv_offset_bias", 0.0) else: atlas.texture.filter = gl.LINEAR, gl.LINEAR + program.set_uniform_safe("uv_offset_bias", 1.0) atlas.texture.use(unit=0) atlas.use_uv_texture(unit=1) geometry = ctx.geometry_empty - program = ctx.sprite_program_single program["pos"] = rect.center_x, rect.center_y, 0 program["color"] = color.normalized program["size"] = rect.width, rect.height diff --git a/arcade/resources/system/shaders/gui/nine_patch_gs.glsl b/arcade/resources/system/shaders/gui/nine_patch_gs.glsl index 47c089848e..dedea5fdde 100644 --- a/arcade/resources/system/shaders/gui/nine_patch_gs.glsl +++ b/arcade/resources/system/shaders/gui/nine_patch_gs.glsl @@ -67,12 +67,6 @@ void main() { vec2 uv0, uv1, uv2, uv3; vec2 atlas_size = vec2(textureSize(sprite_texture, 0)); getSpriteUVs(uv_texture, int(texture_id), uv0, uv1, uv2, uv3); - // TODO: Do center pixel interpolation. Revert by 0.5 pixels for now - vec2 half_px = 0.5 / atlas_size; - uv0 -= half_px; - uv1 += vec2(half_px.x, -half_px.y); - uv2 += vec2(-half_px.x, half_px.y); - uv3 += half_px; // Local corner offsets in pixels float left = start.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 b7e7eb58bc..fa6414096d 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 @@ -10,7 +10,13 @@ uniform WindowBlock { mat4 view; } window; +// 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 float v_angle[]; in vec4 v_color[]; @@ -49,11 +55,26 @@ void main() { vec2 uv0, uv1, uv2, uv3; getSpriteUVs(uv_texture, int(v_texture[0]), uv0, uv1, uv2, uv3); + // 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 / textureSize(sprite_texture, 0) * uv_offset_bias; + uv0 += hp; + uv1 += vec2(-hp.x, hp.y); + uv2 += vec2(hp.x, -hp.y); + uv3 += -hp; + // Set the out color for all vertices gs_color = v_color[0]; // Upper left gl_Position = mvp * vec4(rot * vec2(-hsize.x, hsize.y) + center.xy, center.z, 1.0); - gs_uv = uv0; + gs_uv = uv0; EmitVertex(); // lower left 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 ba80871855..0149f48c24 100644 --- a/arcade/resources/system/shaders/sprites/sprite_list_geometry_fs.glsl +++ b/arcade/resources/system/shaders/sprites/sprite_list_geometry_fs.glsl @@ -1,6 +1,8 @@ #version 330 +// Texture atlas uniform sampler2D sprite_texture; +// Global color set on the sprite list uniform vec4 spritelist_color; in vec2 gs_uv; @@ -11,6 +13,7 @@ out vec4 f_color; void main() { vec4 basecolor = texture(sprite_texture, gs_uv); basecolor *= gs_color * spritelist_color; + // Alpha test if (basecolor.a == 0.0) { discard; } diff --git a/arcade/resources/system/shaders/sprites/sprite_list_geometry_no_cull_geo.glsl b/arcade/resources/system/shaders/sprites/sprite_list_geometry_no_cull_geo.glsl index f03d81ef7b..6d5fd442ec 100644 --- a/arcade/resources/system/shaders/sprites/sprite_list_geometry_no_cull_geo.glsl +++ b/arcade/resources/system/shaders/sprites/sprite_list_geometry_no_cull_geo.glsl @@ -10,7 +10,13 @@ uniform WindowBlock { mat4 view; } window; +// 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 float v_angle[]; in vec4 v_color[]; @@ -36,6 +42,21 @@ void main() { vec2 uv0, uv1, uv2, uv3; getSpriteUVs(uv_texture, int(v_texture[0]), uv0, uv1, uv2, uv3); + // 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 / textureSize(sprite_texture, 0) * uv_offset_bias; + uv0 += hp; + uv1 += vec2(-hp.x, hp.y); + uv2 += vec2(hp.x, -hp.y); + uv3 += -hp; + // Set the out color for all vertices gs_color = v_color[0]; // Upper left diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index 5774fc0d87..b7b15803e4 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -1103,6 +1103,14 @@ def draw( 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: diff --git a/arcade/texture_atlas/region.py b/arcade/texture_atlas/region.py index d5a4eb1f22..a8129909c6 100644 --- a/arcade/texture_atlas/region.py +++ b/arcade/texture_atlas/region.py @@ -89,8 +89,7 @@ def __init__( # Width and height _width = self.width / atlas.width _height = self.height / atlas.height - # Half pixel correction - hp_x, hp_y = 0.5 / atlas.width, 0.5 / atlas.height + # Upper left corner. Note that are mapping the texture upside down and corners # are named as from the vertex position point of view. ul_x, ul_y = self.x / atlas.width, self.y / atlas.height @@ -98,17 +97,17 @@ def __init__( # upper_left, upper_right, lower_left, lower_right self.texture_coordinates = ( # upper_left - ul_x + hp_x, - ul_y + hp_y, + ul_x, + ul_y, # upper_right - ul_x + _width - hp_x, - ul_y + hp_y, + ul_x + _width, + ul_y, # lower_left - ul_x + hp_x, - ul_y + _height - hp_y, + ul_x, + ul_y + _height, # lower_right - ul_x + _width - hp_x, - ul_y + _height - hp_y, + ul_x + _width, + ul_y + _height, ) def verify_image_size(self, image_data: ImageData): diff --git a/tests/unit/atlas/test_region.py b/tests/unit/atlas/test_region.py index 2e9cf93912..6b99121921 100644 --- a/tests/unit/atlas/test_region.py +++ b/tests/unit/atlas/test_region.py @@ -7,8 +7,8 @@ from arcade.texture_atlas.atlas_default import DefaultTextureAtlas -def test_region_coordinates(ctx): - """Test region class.""" +def test_region_coordinates_simple(ctx): + """Basic region test.""" atlas = DefaultTextureAtlas(size=(8, 8), border=0, auto_resize=False) region = AtlasRegion(atlas=atlas, x=0, y=0, width=8, height=8) assert region.x == 0 @@ -16,25 +16,28 @@ def test_region_coordinates(ctx): assert region.width == 8 assert region.height == 8 # Simulate the half pixel location - a, b = 0.5 / 8, 1 - 0.5 / 8 + a, b = 0, 1.0 assert region.texture_coordinates == ( a, a, b, a, a, b, b, b, ) - # Above raw values: - # ( - # 0.0625, 0.0625, - # 0.9375, 0.0625, - # 0.0625, 0.9375, - # 0.9375, 0.9375) - # ) + + +def test_region_coordinates_complex(ctx): + """A more complex region test.""" + atlas = DefaultTextureAtlas(size=(16, 16), border=0, auto_resize=False) + region = AtlasRegion(atlas=atlas, x=1, y=2, width=8, height=6) + assert region.x == 1 + assert region.y == 2 + assert region.width == 8 + assert region.height == 6 + assert region.texture_coordinates == (0.0625, 0.125, 0.5625, 0.125, 0.0625, 0.5, 0.5625, 0.5) def test_verify_size(ctx): im_data = ImageData(PIL.Image.new("RGBA", (8, 8))) - texture = Texture(im_data) region = AtlasRegion(atlas=ctx.default_atlas, x=0, y=0, width=8, height=8) region.verify_image_size(im_data)