From 1b200166425dd9bad2f7bb330eebe930c28c4c47 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Wed, 16 Sep 2026 02:53:04 +0300 Subject: [PATCH] use premultiplied alpha to avoid black fringes on edges --- src/app.zig | 1 + src/platform/graphics.zig | 87 +++++++++++++++++++++++++++++++++------ 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/app.zig b/src/app.zig index 78df39b..b09f86c 100644 --- a/src/app.zig +++ b/src/app.zig @@ -334,6 +334,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { const clear_color = Color.rgb(40, 40, 40); Gfx.setClearColor(clear_color); + Gfx.setDefaultSampler(.linear); const tile_size = Vec2.init(48, 48); var canvas = CanvasScaler{ diff --git a/src/platform/graphics.zig b/src/platform/graphics.zig index 4a30bf0..24d6046 100644 --- a/src/platform/graphics.zig +++ b/src/platform/graphics.zig @@ -96,9 +96,11 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { s[0] = .{ .blend = .{ .enabled = true, - .src_factor_rgb = .SRC_ALPHA, + + .src_factor_rgb = .ONE, .dst_factor_rgb = .ONE_MINUS_SRC_ALPHA, .op_rgb = .ADD, + .src_factor_alpha = .ONE, .dst_factor_alpha = .ONE_MINUS_SRC_ALPHA, .op_alpha = .ADD, @@ -646,13 +648,19 @@ pub fn deinitTexture(id: Texture.Id) void { } } -pub fn setTexture(id: Texture.Id, texture_data: ImageData) void { +const SetTextureOptions = struct { + data: ImageData, + premultiplied: bool = false +}; + +pub fn setTexture(id: Texture.Id, opts: SetTextureOptions) void { const self = &g_state; const texture = self.textures.get(id) orelse { log.warn("Attempt to set texture that doesn't exist: {}", .{id}); return; }; + const texture_data = opts.data; const pixel_count = texture_data.width*texture_data.height; var image_data: sg.ImageData = .{}; @@ -671,6 +679,9 @@ pub fn setTexture(id: Texture.Id, texture_data: ImageData) void { } } + // TODO: Apply premultiply, will need scratch buffer + assert(opts.premultiplied); + var new_image_desc = sg.ImageDesc{ .type = ._2D, .width = @intCast(texture_data.width), @@ -874,7 +885,8 @@ pub fn deinitSprite(id: Sprite.Id) void { const SetSpriteOptions = struct { image: ImageData, - rect: ?Rect = null + rect: ?Rect = null, + premultiplied: bool = false }; pub fn setSprite(id: Sprite.Id, opts: SetSpriteOptions) void { @@ -925,6 +937,10 @@ pub fn setSprite(id: Sprite.Id, opts: SetSpriteOptions) void { }; } + if (!opts.premultiplied) { + sprite_data.premultiplyAlpha(); + } + var old_size: ?Vec2 = null; if (sprite.data) |old_sprite_data| { old_size = Vec2.initFromInt(u32, old_sprite_data.width, old_sprite_data.height); @@ -1026,6 +1042,8 @@ fn repackSpritesheetIfNeeded(id: Spritesheet.Id) void { repackSpritesheet(id); } +// TODO: This function is a bit large, there are some easy things that could be moved to separate function. +// e.g. extruding padding. fn rebuildSpritesheetTextureIfNeeded(id: Spritesheet.Id) void { const self = &g_state; const spritesheet = self.spritesheets.get(id) orelse return; @@ -1072,9 +1090,43 @@ fn rebuildSpritesheetTextureIfNeeded(id: Spritesheet.Id) void { sprite_data.width, sprite_data.height ); + if (sprite.padding > 0) { + for (0..sprite_data.width) |ox| { + const pixel = image_data.getPixel(x+ox, y); + for (0..sprite.padding) |i| { + @memcpy(image_data.getPixel(x+ox, y-(i+1)), pixel); + } + } + + for (0..sprite_data.width) |ox| { + const pixel = image_data.getPixel(x+ox, y+sprite_data.height-1); + for (0..sprite.padding) |i| { + @memcpy(image_data.getPixel(x+ox, y+sprite_data.height+i), pixel); + } + } + + for (0..sprite_data.height) |oy| { + const pixel = image_data.getPixel(x, y+oy); + for (0..sprite.padding) |i| { + @memcpy(image_data.getPixel(x-(i+1), y+oy), pixel); + } + } + + for (0..sprite_data.height) |oy| { + const pixel = image_data.getPixel(x+sprite_data.width-1, y+oy); + for (0..sprite.padding) |i| { + @memcpy(image_data.getPixel(x+sprite_data.width+i, y+oy), pixel); + } + } + + // TODO: Add padding to corners. + } } - setTexture(spritesheet.texture, image_data); + setTexture(spritesheet.texture, .{ + .data = image_data, + .premultiplied = true + }); spritesheet.needs_texture_rebuild = false; } @@ -1125,17 +1177,11 @@ pub fn getSpriteUVRect(id: Sprite.Id) ?Rect { assert(sprite.position != null); - const sprite_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height); - var uv_rect = Rect{ + return Rect{ .pos = sprite.position.?.divide(spritesheet.size), .size = sprite_size.divide(spritesheet.size), }; - - const texel_size = Vec2.init(1, 1).divide(spritesheet.size); - uv_rect = uv_rect.shrink(texel_size.divideScalar(8)); - - return uv_rect; } pub fn drawSpriteQuad(id: Sprite.Id, points: Quad, color: Color) void { @@ -1283,7 +1329,7 @@ pub const ImageData = struct { } } - pub fn getPixel(self: ImageData, x: u32, y: u32) []u8 { + pub fn getPixel(self: ImageData, x: usize, y: usize) []u8 { const pixel_index = y * self.width + x; if (self.pixels) |pixels| { return switch (pixels) { @@ -1330,6 +1376,23 @@ pub const ImageData = struct { self.setRect(x, y, width, height, pixel); } + pub fn premultiplyAlpha(self: ImageData) void { + const pixels = self.pixels orelse return; + if (pixels != .rgba8) return; + const rgba8_pixels = pixels.rgba8; + + for (0..self.height) |y| { + for (0..self.width) |x| { + const pixel_index = y * self.width + x; + const rgba8_pixel = rgba8_pixels[(4*pixel_index)..][0..4]; + + inline for (0..3) |i| { + rgba8_pixel[i] = @intCast(@as(u16, rgba8_pixel[i]) * rgba8_pixel[3] / 255); + } + } + } + } + pub fn copyFrom( self: ImageData, source: ImageData,