starting FontAtlas

This commit is contained in:
Mitchell Hashimoto
2022-04-05 17:57:09 -07:00
parent c4fb335a6b
commit e33aeea723
4 changed files with 193 additions and 3 deletions

View File

@ -47,5 +47,6 @@ pub fn build(b: *std.build.Builder) !void {
// Tests // Tests
const test_step = b.step("test", "Run all tests"); const test_step = b.step("test", "Run all tests");
const lib_tests = b.addTest("src/main.zig"); const lib_tests = b.addTest("src/main.zig");
ftlib.link(lib_tests);
test_step.dependOn(&lib_tests.step); test_step.dependOn(&lib_tests.step);
} }

View File

@ -26,9 +26,10 @@ data: []u8,
/// Width and height of the atlas texture. The current implementation is /// Width and height of the atlas texture. The current implementation is
/// always square so this is both the width and the height. /// always square so this is both the width and the height.
size: u32, size: u32 = 0,
nodes: std.ArrayListUnmanaged(Node), /// The nodes (rectangles) of available space.
nodes: std.ArrayListUnmanaged(Node) = .{},
const Node = struct { const Node = struct {
x: u32, x: u32,
@ -41,6 +42,8 @@ pub const Error = error{
AtlasFull, AtlasFull,
}; };
/// A region within the texture atlas. These can be acquired using the
/// "reserve" function. A region reservation is required to write data.
pub const Region = struct { pub const Region = struct {
x: u32, x: u32,
y: u32, y: u32,
@ -121,7 +124,7 @@ pub fn reserve(self: *Atlas, alloc: Allocator, width: u32, height: u32) !Region
if (node.x < (prev.x + prev.width)) { if (node.x < (prev.x + prev.width)) {
const shrink = prev.x + prev.width - node.x; const shrink = prev.x + prev.width - node.x;
node.x += shrink; node.x += shrink;
node.width -= shrink; node.width -|= shrink;
if (node.width <= 0) { if (node.width <= 0) {
_ = self.nodes.orderedRemove(i); _ = self.nodes.orderedRemove(i);
i -= 1; i -= 1;

185
src/FontAtlas.zig Normal file
View File

@ -0,0 +1,185 @@
//! Implements font loading and rendering into a texture atlas, using
//! Atlas as the backing implementation. The FontAtlas represents a single
//! face with a single size.
const FontAtlas = @This();
const std = @import("std");
const assert = std.debug.assert;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const ftc = @import("freetype/c.zig");
const Atlas = @import("Atlas.zig");
const ftok = ftc.FT_Err_Ok;
const log = std.log.scoped(.font_atlas);
/// The texture atlas where all the font glyphs are rendered.
/// This is NOT owned by the FontAtlas, deinitialization must
/// be manually done.
atlas: Atlas,
/// The glyphs that are loaded into the atlas, keyed by codepoint.
glyphs: std.AutoHashMapUnmanaged(u32, Glyph),
/// The FreeType library
ft_library: ftc.FT_Library,
/// Our font face.
ft_face: ftc.FT_Face = null,
/// Information about a single glyph.
pub const Glyph = struct {
/// width of glyph in pixels
width: u32,
/// height of glyph in pixels
height: u32,
/// left bearing
offset_x: i32,
/// top bearing
offset_y: i32,
/// normalized x, y (s, t) coordinates
s0: f32,
t0: f32,
s1: f32,
t1: f32,
/// horizontal position to increase drawing position for strings
advance_x: f32,
};
pub fn init(atlas: Atlas) !FontAtlas {
var res = FontAtlas{
.atlas = atlas,
.ft_library = undefined,
.glyphs = .{},
};
if (ftc.FT_Init_FreeType(&res.ft_library) != ftok)
return error.FreeTypeInitFailed;
return res;
}
pub fn deinit(self: *FontAtlas, alloc: Allocator) void {
self.glyphs.deinit(alloc);
if (self.ft_face != null) {
if (ftc.FT_Done_Face(self.ft_face) != ftok)
log.err("failed to clean up font face", .{});
}
if (ftc.FT_Done_FreeType(self.ft_library) != ftok)
log.err("failed to clean up FreeType", .{});
self.* = undefined;
}
/// Loads a font to use for the atlas.
///
/// This can only be called if a font is not already loaded.
pub fn loadFaceFromMemory(self: *FontAtlas, source: [:0]const u8, size: u32) !void {
assert(self.ft_face == null);
if (ftc.FT_New_Memory_Face(
self.ft_library,
source.ptr,
@intCast(c_long, source.len),
0,
&self.ft_face,
) != ftok) return error.FaceLoadFailed;
errdefer {
_ = ftc.FT_Done_Face(self.ft_face);
self.ft_face = null;
}
if (ftc.FT_Select_Charmap(self.ft_face, ftc.FT_ENCODING_UNICODE) != ftok)
return error.FaceLoadFailed;
if (ftc.FT_Set_Pixel_Sizes(self.ft_face, size, size) != ftok)
return error.FaceLoadFailed;
}
/// Add a glyph to the font atlas. The codepoint can be either a u8 or
/// []const u8 depending on if you know it is ASCII or must be UTF-8 decoded.
pub fn addGlyph(self: *FontAtlas, alloc: Allocator, codepoint: anytype) !void {
assert(self.ft_face != null);
// We need a UTF32 codepoint for freetype
const utf32 = switch (@TypeOf(codepoint)) {
comptime_int, u8 => @intCast(u32, codepoint),
[]const u8 => @intCast(u32, try std.unicode.utfDecode(codepoint)),
else => @compileError("invalid codepoint type"),
};
const glyph_index = ftc.FT_Get_Char_Index(self.ft_face, utf32);
// TODO: probably not an error because we want to add a box.
if (glyph_index == 0) return error.CodepointNotFound;
if (ftc.FT_Load_Glyph(
self.ft_face,
glyph_index,
ftc.FT_LOAD_RENDER,
) != ftok) return error.LoadGlyphFailed;
const glyph = self.ft_face.*.glyph;
const bitmap = glyph.*.bitmap;
const src_w = bitmap.width;
const src_h = bitmap.rows;
const tgt_w = src_w;
const tgt_h = src_h;
const region = try self.atlas.reserve(alloc, tgt_w, tgt_h);
// Build our buffer
const buffer = try alloc.alloc(u8, tgt_w * tgt_h);
defer alloc.free(buffer);
var dst_ptr = buffer;
var src_ptr = bitmap.buffer;
var i: usize = 0;
while (i < src_h) : (i += 1) {
std.mem.copy(u8, dst_ptr, src_ptr[0..bitmap.width]);
dst_ptr = dst_ptr[tgt_w..];
src_ptr += @intCast(usize, bitmap.pitch);
}
// Write the glyph information into the atlas
assert(region.width == tgt_w);
assert(region.height == tgt_h);
self.atlas.set(region, buffer);
const gop = try self.glyphs.getOrPut(alloc, utf32);
gop.value_ptr.* = .{
.width = tgt_w,
.height = tgt_h,
.offset_x = glyph.*.bitmap_left,
.offset_y = glyph.*.bitmap_top,
.s0 = @intToFloat(f32, region.x) / @intToFloat(f32, self.atlas.size),
.t0 = @intToFloat(f32, region.y) / @intToFloat(f32, self.atlas.size),
.s1 = @intToFloat(f32, region.x + tgt_w) / @intToFloat(f32, self.atlas.size),
.t1 = @intToFloat(f32, region.y + tgt_h) / @intToFloat(f32, self.atlas.size),
.advance_x = @intToFloat(f32, glyph.*.advance.x),
};
}
test {
const alloc = testing.allocator;
var font = try init(try Atlas.init(alloc, 512));
defer font.deinit(alloc);
defer font.atlas.deinit(alloc);
try font.loadFaceFromMemory(testFont, 48);
// Generate all visible ASCII
var i: u8 = 32;
while (i < 127) : (i += 1) {
try font.addGlyph(alloc, i);
}
}
const testFont = @embedFile("../fonts/Inconsolata-Regular.ttf");

View File

@ -20,4 +20,5 @@ pub fn main() !void {
test { test {
_ = @import("Atlas.zig"); _ = @import("Atlas.zig");
_ = @import("FontAtlas.zig");
} }