diff --git a/build.zig b/build.zig index 29121dea..ca496e8a 100644 --- a/build.zig +++ b/build.zig @@ -3,6 +3,7 @@ const builtin = @import("builtin"); pub const gpu = @import("gpu/build.zig"); const gpu_dawn = @import("gpu-dawn/build.zig"); pub const glfw = @import("glfw/build.zig"); +const freetype = @import("freetype/build.zig"); const Pkg = std.build.Pkg; pub fn build(b: *std.build.Builder) void { @@ -38,7 +39,7 @@ pub fn build(b: *std.build.Builder) void { .{ .name = "instanced-cube", .packages = &[_]Pkg{Packages.zmath} }, .{ .name = "advanced-gen-texture-light", .packages = &[_]Pkg{Packages.zmath} }, .{ .name = "fractal-cube", .packages = &[_]Pkg{Packages.zmath} }, - .{ .name = "gkurve", .packages = &[_]Pkg{ Packages.zmath, Packages.zigimg } }, + .{ .name = "gkurve", .packages = &[_]Pkg{ Packages.zmath, Packages.zigimg, freetype.pkg } }, .{ .name = "textured-cube", .packages = &[_]Pkg{ Packages.zmath, Packages.zigimg } }, }) |example| { // FIXME: this is workaround for a problem that some examples (having the std_platform_only=true field) as @@ -59,6 +60,11 @@ pub fn build(b: *std.build.Builder) void { }, ); example_app.setBuildMode(mode); + inline for (example.packages) |p| { + if (std.mem.eql(u8, p.name, freetype.pkg.name)) + freetype.link(example_app.b, example_app.step, .{}); + } + example_app.link(options); example_app.install(); diff --git a/examples/gkurve/main.zig b/examples/gkurve/main.zig index 76c21ea7..809f33d1 100644 --- a/examples/gkurve/main.zig +++ b/examples/gkurve/main.zig @@ -1,5 +1,5 @@ // TODO: -// - handle textures better witexture atlas +// - handle textures better with texture atlas // - handle adding and removing triangles and quads better const std = @import("std"); @@ -10,11 +10,15 @@ const zigimg = @import("zigimg"); const glfw = @import("glfw"); const draw = @import("draw.zig"); const Atlas = @import("atlas.zig").Atlas; +const ft = @import("freetype"); +const Label = @import("text.zig"); pub const options = mach.Options{ .width = 640, .height = 480 }; pub const App = @This(); +const AtlasRGB8 = Atlas(zigimg.color.Rgba32); + pipeline: gpu.RenderPipeline, queue: gpu.Queue, vertex_buffer: gpu.Buffer, @@ -26,18 +30,17 @@ frag_uniform_buffer: gpu.Buffer, fragment_uniform_list: std.ArrayList(draw.FragUniform), update_frag_uniform_buffer: bool, bind_group: gpu.BindGroup, +texture_atlas_data: AtlasRGB8, pub fn init(app: *App, engine: *mach.Engine) !void { try engine.core.setSizeLimits(.{ .width = 20, .height = 20 }, .{ .width = null, .height = null }); const queue = engine.gpu_driver.device.getQueue(); - const AtlasRGB8 = Atlas(zigimg.color.Rgba32); // TODO: Refactor texture atlas size number - var texture_atlas_data: AtlasRGB8 = try AtlasRGB8.init(engine.allocator, 640); - defer texture_atlas_data.deinit(engine.allocator); - const atlas_size = gpu.Extent3D{ .width = texture_atlas_data.size, .height = texture_atlas_data.size }; - const atlas_float_size = @intToFloat(f32, texture_atlas_data.size); + app.texture_atlas_data = try AtlasRGB8.init(engine.allocator, 1280); + const atlas_size = gpu.Extent3D{ .width = app.texture_atlas_data.size, .height = app.texture_atlas_data.size }; + const atlas_float_size = @intToFloat(f32, app.texture_atlas_data.size); const texture = engine.gpu_driver.device.createTexture(&.{ .size = atlas_size, @@ -56,37 +59,46 @@ pub fn init(app: *App, engine: *mach.Engine) !void { const img = try zigimg.Image.fromMemory(engine.allocator, @embedFile("../assets/gotta-go-fast.png")); defer img.deinit(); - const atlas_img_region = try texture_atlas_data.reserve(engine.allocator, @truncate(u32, img.width), @truncate(u32, img.height)); + const atlas_img_region = try app.texture_atlas_data.reserve(engine.allocator, @truncate(u32, img.width), @truncate(u32, img.height)); const img_uv_data = atlas_img_region.getUVData(atlas_float_size); switch (img.pixels.?) { - .Rgba32 => |pixels| texture_atlas_data.set(atlas_img_region, pixels), + .Rgba32 => |pixels| app.texture_atlas_data.set(atlas_img_region, pixels), .Rgb24 => |pixels| { const data = try rgb24ToRgba32(engine.allocator, pixels); defer data.deinit(engine.allocator); - texture_atlas_data.set(atlas_img_region, data.Rgba32); + app.texture_atlas_data.set(atlas_img_region, data.Rgba32); }, else => @panic("unsupported image color format"), } const white_tex_scale = 80; - const atlas_white_region = try texture_atlas_data.reserve(engine.allocator, white_tex_scale, white_tex_scale); + const atlas_white_region = try app.texture_atlas_data.reserve(engine.allocator, white_tex_scale, white_tex_scale); const white_texture_uv_data = atlas_white_region.getUVData(atlas_float_size); var white_tex_data = try engine.allocator.alloc(zigimg.color.Rgba32, white_tex_scale * white_tex_scale); std.mem.set(zigimg.color.Rgba32, white_tex_data, zigimg.color.Rgba32.initRGB(0xff, 0xff, 0xff)); - texture_atlas_data.set(atlas_white_region, white_tex_data); + app.texture_atlas_data.set(atlas_white_region, white_tex_data); + + app.vertices = try std.ArrayList(draw.Vertex).initCapacity(engine.allocator, 9); + app.fragment_uniform_list = try std.ArrayList(draw.FragUniform).initCapacity(engine.allocator, 3); + + // Quick test for using freetype + const lib = try ft.Library.init(); + defer lib.deinit(); + + var label = try Label.init(lib, "freetype/upstream/assets/FiraSans-Regular.ttf", 0, 40, engine.allocator); + defer label.deinit(); + + try label.print(app, "All your game's bases are belong to us", .{}, @Vector(2, f32){ 0, 420 }, @Vector(4, f32){ 1, 1, 1, 1 }); queue.writeTexture( &.{ .texture = texture }, &data_layout, - &.{ .width = texture_atlas_data.size, .height = texture_atlas_data.size }, + &.{ .width = app.texture_atlas_data.size, .height = app.texture_atlas_data.size }, zigimg.color.Rgba32, - texture_atlas_data.data, + app.texture_atlas_data.data, ); - app.vertices = try std.ArrayList(draw.Vertex).initCapacity(engine.allocator, 9); - app.fragment_uniform_list = try std.ArrayList(draw.FragUniform).initCapacity(engine.allocator, 3); - const wsize = try engine.core.getWindowSize(); const window_width = @intToFloat(f32, wsize.width); const window_height = @intToFloat(f32, wsize.height); @@ -99,8 +111,8 @@ pub fn init(app: *App, engine: *mach.Engine) !void { // try draw.equilateralTriangle(app, .{ window_width / 2, window_height / 2 }, triangle_scale, .{}, img_uv_data); // try draw.equilateralTriangle(app, .{ window_width / 2, window_height / 2 - triangle_scale }, triangle_scale, .{ .type = .concave }, img_uv_data); // try draw.equilateralTriangle(app, .{ window_width / 2 - triangle_scale, window_height / 2 - triangle_scale / 2 }, triangle_scale, .{ .type = .convex }, white_texture_uv_data); - // try draw.quad(app, .{ 0, 0 }, .{ 200, 200 }, .{}, img_uv_data); - try draw.circle(app, .{ window_width / 2, window_height / 2 }, window_height / 2 - 10, .{ 0, 0.5, 0.75, 1.0 }, white_texture_uv_data); + // try draw.quad(app, .{ 0, 0 }, .{ 480, 480 }, .{}, .{ .bottom_left = .{ 0, 0 }, .width_and_height = .{ 1, 1 } }); + // try draw.circle(app, .{ window_width / 2, window_height / 2 }, window_height / 2 - 10, .{ 0, 0.5, 0.75, 1.0 }, white_texture_uv_data); const vs_module = engine.gpu_driver.device.createShaderModule(&.{ .label = "my vertex shader", @@ -224,13 +236,14 @@ pub fn init(app: *App, engine: *mach.Engine) !void { bgl.release(); } -pub fn deinit(app: *App, _: *mach.Engine) void { +pub fn deinit(app: *App, engine: *mach.Engine) void { app.vertex_buffer.release(); app.vertex_uniform_buffer.release(); app.frag_uniform_buffer.release(); app.bind_group.release(); app.vertices.deinit(); app.fragment_uniform_list.deinit(); + app.texture_atlas_data.deinit(engine.allocator); } pub fn update(app: *App, engine: *mach.Engine) !bool { diff --git a/examples/gkurve/text.zig b/examples/gkurve/text.zig new file mode 100644 index 00000000..c21ba6dc --- /dev/null +++ b/examples/gkurve/text.zig @@ -0,0 +1,125 @@ +//! At the moment we use only rgba32, but maybe it could be useful to use also other types + +const std = @import("std"); +const ft = @import("freetype"); +const zigimg = @import("zigimg"); +const Atlas = @import("atlas.zig").Atlas; +const UVData = @import("atlas.zig").UVData; +const App = @import("main.zig").App; +const draw = @import("draw.zig"); + +pub const Label = @This(); + +const Vec2 = @Vector(2, f32); +const Vec4 = @Vector(4, f32); + +const GlyphInfo = struct { + uv_data: UVData, + metrics: ft.Glyph.GlyphMetrics, +}; + +face: ft.Face, +size: i32, +char_map: std.AutoHashMap(u8, GlyphInfo), +allocator: std.mem.Allocator, + +const WriterContext = struct { + label: *Label, + app: *App, + position: Vec2, + text_color: Vec4, +}; +const Writer = std.io.Writer(WriterContext, ft.Error, write); + +pub fn writer(label: *Label, app: *App, position: Vec2, text_color: Vec4) Writer { + return Writer{ + .context = .{ + .label = label, + .app = app, + .position = position, + .text_color = text_color, + }, + }; +} + +pub fn init(lib: ft.Library, font_path: []const u8, face_index: i32, char_size: i32, allocator: std.mem.Allocator) !Label { + return Label{ + .face = try lib.newFace(font_path, face_index), + .size = char_size, + .char_map = std.AutoHashMap(u8, GlyphInfo).init(allocator), + .allocator = allocator, + }; +} + +pub fn deinit(label: *Label) void { + label.face.deinit(); + label.char_map.deinit(); +} + +// FIXME: union ft.error and hashmap error +fn write(ctx: WriterContext, bytes: []const u8) ft.Error!usize { + var offset = Vec2{ 0, 0 }; + for (bytes) |char| { + switch (char) { + '\n' => { + offset[0] = 0; + offset[1] -= @intToFloat(f32, ctx.label.face.sizeMetrics().?.height >> 6); + }, + ' ' => { + const v = ctx.label.char_map.getOrPut(char) catch unreachable; + if (!v.found_existing) { + try ctx.label.face.setCharSize(ctx.label.size * 64, 0, 50, 0); + try ctx.label.face.loadChar(char, .{ .render = true }); + const glyph = ctx.label.face.glyph; + v.value_ptr.* = GlyphInfo{ + .uv_data = undefined, + .metrics = glyph.metrics(), + }; + } + offset[0] += @intToFloat(f32, v.value_ptr.metrics.horiAdvance >> 6); + }, + else => { + const v = ctx.label.char_map.getOrPut(char) catch unreachable; + if (!v.found_existing) { + try ctx.label.face.setCharSize(ctx.label.size * 64, 0, 50, 0); + try ctx.label.face.loadChar(char, .{ .render = true }); + const glyph = ctx.label.face.glyph; + const glyph_bitmap = glyph.bitmap(); + const glyph_width = glyph_bitmap.width(); + const glyph_height = glyph_bitmap.rows(); + var glyph_data = ctx.label.allocator.alloc(zigimg.color.Rgba32, glyph_width * glyph_height) catch unreachable; + defer ctx.label.allocator.free(glyph_data); + const glyph_buffer = glyph_bitmap.buffer(); + for (glyph_data) |*data, i| { + const x = i % glyph_width; + const y = i / glyph_width; + const glyph_col = glyph_buffer[y * glyph_width + x]; + data.* = zigimg.color.Rgba32.initRGB(glyph_col, glyph_col, glyph_col); + } + const glyph_atlas_region = ctx.app.texture_atlas_data.reserve(ctx.label.allocator, glyph_width, glyph_height) catch unreachable; + const glyph_uv_data = glyph_atlas_region.getUVData(@intToFloat(f32, ctx.app.texture_atlas_data.size)); + ctx.app.texture_atlas_data.set(glyph_atlas_region, glyph_data); + + v.value_ptr.* = GlyphInfo{ + .uv_data = glyph_uv_data, + .metrics = glyph.metrics(), + }; + } + draw.quad( + ctx.app, + ctx.position + offset + Vec2{ @intToFloat(f32, v.value_ptr.metrics.horiBearingX >> 6), @intToFloat(f32, (v.value_ptr.metrics.horiBearingY - v.value_ptr.metrics.height) >> 6) }, + .{ @intToFloat(f32, v.value_ptr.metrics.width >> 6), @intToFloat(f32, v.value_ptr.metrics.height >> 6) }, + .{ .blend_color = ctx.text_color }, + v.value_ptr.uv_data, + ) catch unreachable; + offset[0] += @intToFloat(f32, v.value_ptr.metrics.horiAdvance >> 6); + }, + } + } + return bytes.len; +} + +pub fn print(label: *Label, app: *App, comptime fmt: []const u8, args: anytype, position: Vec2, text_color: Vec4) !void { + const w = writer(label, app, position, text_color); + try w.print(fmt, args); +}