From 1e072c56070fe48b298a57c620aeced18368049d Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 12 Jul 2026 19:45:09 +0300 Subject: [PATCH] add configurable padding to sprites --- build.zig | 17 +++-- libs/stb/src/stb_truetype.zig | 4 +- src/graphics.zig | 135 ++++++++++++++++++++++++---------- 3 files changed, 111 insertions(+), 45 deletions(-) diff --git a/build.zig b/build.zig index 27c8766..94c6221 100644 --- a/build.zig +++ b/build.zig @@ -52,16 +52,21 @@ pub fn build(b: *std.Build) !void { exe_mod.addImport("sokol", mod_sokol); const dep_shdc = dep_sokol.builder.dependency("shdc", .{}); + + var slang = sokol.shdc.Slang{}; + if (is_wasm) { + slang.wgsl = true; + } else { + slang.glsl410 = true; + slang.hlsl4 = true; + slang.metal_macos = true; + slang.glsl300es = true; + } const mod_shader = try sokol.shdc.createModule(b, "shader", mod_sokol, .{ .shdc_dep = dep_shdc, .input = "src/shader.glsl", .output = "src/shader.zig", - .slang = .{ - .glsl410 = true, - .hlsl4 = true, - .metal_macos = true, - .glsl300es = true, - }, + .slang = slang }); mod_shader.addImport("math", math_mod); exe_mod.addImport("shader", mod_shader); diff --git a/libs/stb/src/stb_truetype.zig b/libs/stb/src/stb_truetype.zig index 97621c4..a63cfe6 100644 --- a/libs/stb/src/stb_truetype.zig +++ b/libs/stb/src/stb_truetype.zig @@ -109,8 +109,8 @@ pub const Font = struct { return result; } - pub fn getGlyphKernAdvance(self: Font, prev_glyph: u32, next_glyph: u21) i32 { - return c.stbtt_GetGlyphKernAdvance(&self.info, prev_glyph, next_glyph); + pub fn getGlyphKernAdvance(self: Font, prev_glyph: u32, next_glyph: u32) i32 { + return c.stbtt_GetGlyphKernAdvance(&self.info, @intCast(prev_glyph), @intCast(next_glyph)); } }; diff --git a/src/graphics.zig b/src/graphics.zig index ad2e385..6961596 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -71,9 +71,10 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { const shader = sg.makeShader(shd.mainShaderDesc(sg.queryBackend())); assert(shader.id != sg.invalid_id); + // TODO: Make sampler configurable. const default_sampler = sg.makeSampler(.{ - .min_filter = .NEAREST, - .mag_filter = .NEAREST, + .min_filter = .LINEAR, + .mag_filter = .LINEAR, .label = "default-sampler" }); assert(default_sampler.id != sg.invalid_id); @@ -200,7 +201,8 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { nil_sprite.* = Sprite{ .spritesheet = self.nil_spritesheet, .data = .none, - .position = null + .position = null, + .padding = 0 }; } @@ -710,7 +712,8 @@ pub fn draw(opts: DrawOptions) void { } const SpriteOptions = struct { - spritesheet: ?Spritesheet.Id = null + spritesheet: ?Spritesheet.Id = null, + padding: u32 = 0, }; pub fn initSprite(opts: SpriteOptions) Sprite.Id { @@ -723,6 +726,7 @@ pub fn initSprite(opts: SpriteOptions) Sprite.Id { const sprite = self.sprites.getAssumeExists(id); sprite.* = .{ .spritesheet = opts.spritesheet orelse self.default_spritesheet, + .padding = opts.padding, .data = null, .position = null }; @@ -856,8 +860,8 @@ fn repackSpritesheet(id: Spritesheet.Id) void { for (0.., sprites.items) |i, sprite| { const sprite_data = sprite.data.?; rects[i] = .{ - .w = @intCast(sprite_data.width), - .h = @intCast(sprite_data.height), + .w = @intCast(sprite_data.width + sprite.padding * 2), + .h = @intCast(sprite_data.height + sprite.padding * 2), }; } @@ -874,8 +878,8 @@ fn repackSpritesheet(id: Spritesheet.Id) void { for (0.., sprites.items) |i, sprite| { assert(rects[i].was_packed == 1); - const x: u32 = @intCast(rects[i].x); - const y: u32 = @intCast(rects[i].y); + const x: u32 = @as(u32, @intCast(rects[i].x)) + sprite.padding; + const y: u32 = @as(u32, @intCast(rects[i].y)) + sprite.padding; sprite.position = .initFromInt(u32, x, y); } @@ -941,10 +945,18 @@ fn rebuildSpritesheetTextureIfNeeded(id: Spritesheet.Id) void { const sprite_data = sprite.data orelse continue; const sprite_position = sprite.position orelse continue; + const x: u32 = @intFromFloat(sprite_position.x); + const y: u32 = @intFromFloat(sprite_position.y); + image_data.setRectZero( + x - sprite.padding, + y - sprite.padding, + sprite_data.width + sprite.padding * 2, + sprite_data.height + sprite.padding * 2 + ); image_data.copyFrom( sprite_data, - @intFromFloat(sprite_position.x), - @intFromFloat(sprite_position.y), + x, + y, 0, 0, sprite_data.width, @@ -968,14 +980,16 @@ fn ensureSpritePacked(id: Sprite.Id) bool { if (spritesheet.packer) |*packer| { var rects: [1]STBRectPack.Rect = .{ .{ - .w = @intCast(sprite_data.width), - .h = @intCast(sprite_data.height), + .w = @intCast(sprite_data.width + sprite.padding * 2), + .h = @intCast(sprite_data.height + sprite.padding * 2), } }; if (packer.packRects(&rects)) { // Suboptimal sprite packing, will repack at end of frame assert(rects[0].was_packed == 1); - sprite.position = .initFromInt(i32, rects[0].x, rects[0].y); + const x: u32 = @as(u32, @intCast(rects[0].x)) + sprite.padding; + const y: u32 = @as(u32, @intCast(rects[0].y)) + sprite.padding; + sprite.position = .initFromInt(u32, x, y); spritesheet.needs_repack = true; } else { log.debug("Failed to pack a single sprite to spritesheet", .{}); @@ -1168,6 +1182,7 @@ pub fn drawText(id: Font.Id, opts: DrawTextOptions) void { var current_point = opts.pos; current_point.y += baseline; + var prev_glyph: ?u32 = null; var cursor: usize = 0; while (cursor < opts.text.len) { const codepoint_len = std.unicode.utf8ByteSequenceLength(opts.text[cursor]) catch |e| { @@ -1194,39 +1209,49 @@ pub fn drawText(id: Font.Id, opts: DrawTextOptions) void { glyph = font.cache.get(glyph_id); glyph.box = stb_font.getGlyphBitmapBox(glyph_key.index, glyph_key.scale_x, glyph_key.scale_y); - assert(glyph.sprite == self.nil_sprite); - glyph.sprite = initSprite(.{ .spritesheet = font.spritesheet }); - const width: u32 = @intCast(glyph.box.x1 - glyph.box.x0); const height: u32 = @intCast(glyph.box.y1 - glyph.box.y0); + if (width > 0 and height > 0) { + assert(glyph.sprite == self.nil_sprite); + glyph.sprite = initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 }); - assert(width < Font.Glyph.max_width); - assert(height < Font.Glyph.max_height); + assert(width < Font.Glyph.max_width); + assert(height < Font.Glyph.max_height); - var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined; - const bitmap = ImageData{ - .pixels = .{ .r8 = (&bitmap_buffer).ptr }, - .width = width, - .height = height - }; - stb_font.makeGlyphBitmap( - bitmap.pixels.?.r8, - width, height, - width, - glyph_key.scale_x, glyph_key.scale_y, - glyph_key.index - ); - setSprite(glyph.sprite, .{ .image = bitmap }); + var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined; + const bitmap = ImageData{ + .pixels = .{ .r8 = (&bitmap_buffer).ptr }, + .width = width, + .height = height + }; + stb_font.makeGlyphBitmap( + bitmap.pixels.?.r8, + width, height, + width, + glyph_key.scale_x, glyph_key.scale_y, + glyph_key.index + ); + setSprite(glyph.sprite, .{ .image = bitmap }); + } else { + glyph.sprite = self.nil_sprite; + } } glyph.used_this_frame = true; + if (prev_glyph != null) { + const kerning = stb_font.getGlyphKernAdvance(prev_glyph.?, glyph_index); + current_point.x += @as(f32, @floatFromInt(kerning)) * scale; + } + + if (glyph.sprite != self.nil_sprite) { + const glyph_size = Vec2.initFromInt(i32, glyph.box.x1 - glyph.box.x0, glyph.box.y1 - glyph.box.y0); + const glyph_pos = current_point.sub(.initFromInt(i32, glyph.box.x0, -glyph.box.y0)); + drawSprite(glyph.sprite, glyph_pos, glyph_size, opts.color); + } + const hmetrics = stb_font.getGlyphHMetrics(glyph_index); - - const glyph_size = Vec2.initFromInt(i32, glyph.box.x1 - glyph.box.x0, glyph.box.y1 - glyph.box.y0); - const glyph_pos = current_point.sub(.initFromInt(i32, glyph.box.x0, -glyph.box.y0)); - drawSprite(glyph.sprite, glyph_pos, glyph_size, opts.color); - current_point.x += @as(f32, @floatFromInt(hmetrics.advance_width)) * scale; + prev_glyph = glyph_index; } } @@ -1324,6 +1349,41 @@ pub const ImageData = struct { } } + pub fn setRect(self: ImageData, x: u32, y: u32, width: u32, height: u32, pixel: []const u8) void { + if (self.pixels == null) { + return; + } + + for (0..height) |oy| { + const iy = y + oy; + if (iy >= self.height) { + continue; + } + + for (0..width) |ox| { + const ix = x + ox; + if (ix >= self.width) { + continue; + } + + const dest_pixel = self.getPixel(@intCast(ix), @intCast(iy)); + @memcpy(dest_pixel, pixel); + } + } + } + + pub fn setRectZero(self: ImageData, x: u32, y: u32, width: u32, height: u32) void { + if (self.pixels == null) { + return; + } + + const pixel = switch (self.pixels.?) { + .rgba8 => &[_]u8{ 0, 0, 0, 0 }, + .r8 => &[_]u8{ 0 } + }; + self.setRect(x, y, width, height, pixel); + } + pub fn copyFrom( self: ImageData, source: ImageData, @@ -1382,6 +1442,7 @@ pub const Sprite = struct { spritesheet: Spritesheet.Id, data: ?ImageData, position: ?Vec2, + padding: u32, const max_sprites = 128; const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_sprites-1), u8, Sprite);