use premultiplied alpha to avoid black fringes on edges

This commit is contained in:
Rokas Puzonas 2026-09-16 02:53:04 +03:00
parent e317e21cfe
commit 1b20016642
2 changed files with 76 additions and 12 deletions

View File

@ -334,6 +334,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
const clear_color = Color.rgb(40, 40, 40); const clear_color = Color.rgb(40, 40, 40);
Gfx.setClearColor(clear_color); Gfx.setClearColor(clear_color);
Gfx.setDefaultSampler(.linear);
const tile_size = Vec2.init(48, 48); const tile_size = Vec2.init(48, 48);
var canvas = CanvasScaler{ var canvas = CanvasScaler{

View File

@ -96,9 +96,11 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
s[0] = .{ s[0] = .{
.blend = .{ .blend = .{
.enabled = true, .enabled = true,
.src_factor_rgb = .SRC_ALPHA,
.src_factor_rgb = .ONE,
.dst_factor_rgb = .ONE_MINUS_SRC_ALPHA, .dst_factor_rgb = .ONE_MINUS_SRC_ALPHA,
.op_rgb = .ADD, .op_rgb = .ADD,
.src_factor_alpha = .ONE, .src_factor_alpha = .ONE,
.dst_factor_alpha = .ONE_MINUS_SRC_ALPHA, .dst_factor_alpha = .ONE_MINUS_SRC_ALPHA,
.op_alpha = .ADD, .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 self = &g_state;
const texture = self.textures.get(id) orelse { const texture = self.textures.get(id) orelse {
log.warn("Attempt to set texture that doesn't exist: {}", .{id}); log.warn("Attempt to set texture that doesn't exist: {}", .{id});
return; return;
}; };
const texture_data = opts.data;
const pixel_count = texture_data.width*texture_data.height; const pixel_count = texture_data.width*texture_data.height;
var image_data: sg.ImageData = .{}; 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{ var new_image_desc = sg.ImageDesc{
.type = ._2D, .type = ._2D,
.width = @intCast(texture_data.width), .width = @intCast(texture_data.width),
@ -874,7 +885,8 @@ pub fn deinitSprite(id: Sprite.Id) void {
const SetSpriteOptions = struct { const SetSpriteOptions = struct {
image: ImageData, image: ImageData,
rect: ?Rect = null rect: ?Rect = null,
premultiplied: bool = false
}; };
pub fn setSprite(id: Sprite.Id, opts: SetSpriteOptions) void { 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; var old_size: ?Vec2 = null;
if (sprite.data) |old_sprite_data| { if (sprite.data) |old_sprite_data| {
old_size = Vec2.initFromInt(u32, old_sprite_data.width, old_sprite_data.height); 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); 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 { fn rebuildSpritesheetTextureIfNeeded(id: Spritesheet.Id) void {
const self = &g_state; const self = &g_state;
const spritesheet = self.spritesheets.get(id) orelse return; const spritesheet = self.spritesheets.get(id) orelse return;
@ -1072,9 +1090,43 @@ fn rebuildSpritesheetTextureIfNeeded(id: Spritesheet.Id) void {
sprite_data.width, sprite_data.width,
sprite_data.height 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; spritesheet.needs_texture_rebuild = false;
} }
@ -1125,17 +1177,11 @@ pub fn getSpriteUVRect(id: Sprite.Id) ?Rect {
assert(sprite.position != null); assert(sprite.position != null);
const sprite_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height); const sprite_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height);
var uv_rect = Rect{ return Rect{
.pos = sprite.position.?.divide(spritesheet.size), .pos = sprite.position.?.divide(spritesheet.size),
.size = sprite_size.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 { 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; const pixel_index = y * self.width + x;
if (self.pixels) |pixels| { if (self.pixels) |pixels| {
return switch (pixels) { return switch (pixels) {
@ -1330,6 +1376,23 @@ pub const ImageData = struct {
self.setRect(x, y, width, height, pixel); 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( pub fn copyFrom(
self: ImageData, self: ImageData,
source: ImageData, source: ImageData,