add configurable padding to sprites

This commit is contained in:
Rokas Puzonas 2026-07-12 19:45:09 +03:00
parent e2b15895c2
commit 1e072c5607
3 changed files with 111 additions and 45 deletions

View File

@ -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);

View File

@ -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));
}
};

View File

@ -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);