use a font atlas!

This commit is contained in:
Mitchell Hashimoto
2022-04-04 22:24:02 -07:00
parent 7e42a0c17e
commit 684fb64705
8 changed files with 337 additions and 116 deletions

12
shaders/text-atlas.f.glsl Normal file
View File

@ -0,0 +1,12 @@
#version 330 core
in vec2 TexCoords;
in vec4 VertexColor;
uniform sampler2D text;
void main()
{
float a = texture(text, TexCoords).r;
gl_FragColor = vec4(VertexColor.rgb, VertexColor.a*a);
}

17
shaders/text-atlas.v.glsl Normal file
View File

@ -0,0 +1,17 @@
#version 330 core
layout (location = 0) in vec3 vertex;
layout (location = 1) in vec2 tex_coord;
layout (location = 2) in vec4 color;
out vec2 TexCoords;
out vec4 VertexColor;
uniform mat4 projection;
void main()
{
gl_Position = projection * vec4(vertex, 1.0);
TexCoords = tex_coord.xy;
VertexColor = color;
}

View File

@ -74,7 +74,8 @@ pub fn run(self: App) !void {
gl.clearColor(0.2, 0.3, 0.3, 1.0); gl.clearColor(0.2, 0.3, 0.3, 1.0);
gl.clear(gl.c.GL_COLOR_BUFFER_BIT); gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
try self.text.render("sh $ /bin/bash -c \"echo hello\"", 25.0, 25.0, 1.0, .{ 0.5, 0.8, 0.2 }); try self.text.render("sh $ /bin/bash -c \"echo hello\"", 25.0, 25.0, .{ 0.5, 0.8, 0.2 });
//try self.text.render("hi", 25.0, 25.0, .{ 0.5, 0.8, 0.2 });
try self.window.swapBuffers(); try self.window.swapBuffers();
try glfw.waitEvents(); try glfw.waitEvents();

View File

@ -4,15 +4,15 @@ const std = @import("std");
const ftc = @import("freetype/c.zig"); const ftc = @import("freetype/c.zig");
const gl = @import("opengl.zig"); const gl = @import("opengl.zig");
const gb = @import("gb_math.zig"); const gb = @import("gb_math.zig");
const ftgl = @import("freetype-gl/c.zig");
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
ft: ftc.FT_Library,
face: ftc.FT_Face,
chars: CharList,
vao: gl.VertexArray = undefined,
vbo: gl.Buffer = undefined,
program: gl.Program = undefined,
projection: gb.gbMat4 = undefined, projection: gb.gbMat4 = undefined,
font: *ftgl.texture_font_t,
atlas: *ftgl.texture_atlas_t,
program: gl.Program,
tex: gl.Texture,
const CharList = std.ArrayListUnmanaged(Char); const CharList = std.ArrayListUnmanaged(Char);
const Char = struct { const Char = struct {
@ -23,96 +23,58 @@ const Char = struct {
}; };
pub fn init(alloc: std.mem.Allocator) !TextRenderer { pub fn init(alloc: std.mem.Allocator) !TextRenderer {
var ft: ftc.FT_Library = undefined; const atlas = ftgl.texture_atlas_new(512, 512, 1);
if (ftc.FT_Init_FreeType(&ft) != 0) { if (atlas == null) return error.FontAtlasFail;
return error.FreetypeInitFailed; errdefer ftgl.texture_atlas_delete(atlas);
} const font = ftgl.texture_font_new_from_memory(
atlas,
var face: ftc.FT_Face = undefined; 48,
if (ftc.FT_New_Memory_Face(
ft,
face_ttf, face_ttf,
face_ttf.len, face_ttf.len,
0, );
&face, if (font == null) return error.FontInitFail;
) != 0) { errdefer ftgl.texture_font_delete(font);
return error.FreetypeFaceFailed;
}
_ = ftc.FT_Set_Pixel_Sizes(face, 0, 48); // Load all visible ASCII characters.
var i: u8 = 32;
// disable byte-alignment restriction while (i < 127) : (i += 1) {
try gl.pixelStore(gl.c.GL_UNPACK_ALIGNMENT, 1);
// Pre-render all the ASCII characters
var chars = try CharList.initCapacity(alloc, 128);
var i: usize = 0;
while (i < chars.capacity) : (i += 1) {
// Load the character // Load the character
if (ftc.FT_Load_Char(face, i, ftc.FT_LOAD_RENDER) != 0) { if (ftgl.texture_font_load_glyph(font, &i) == 0) {
return error.GlyphLoadFailed; return error.GlyphLoadFailed;
} }
}
// Generate the texture // Build our texture
const tex = try gl.Texture.create(); const tex = try gl.Texture.create();
var binding = try tex.bind(.@"2D"); errdefer tex.destroy();
defer binding.unbind(); const binding = try tex.bind(.@"2D");
try binding.image2D(
0,
.Red,
@intCast(c_int, face.*.glyph.*.bitmap.width),
@intCast(c_int, face.*.glyph.*.bitmap.rows),
0,
.Red,
.UnsignedByte,
face.*.glyph.*.bitmap.buffer,
);
try binding.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE); try binding.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
try binding.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE); try binding.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
try binding.parameter(.MinFilter, gl.c.GL_LINEAR); try binding.parameter(.MinFilter, gl.c.GL_LINEAR);
try binding.parameter(.MagFilter, gl.c.GL_LINEAR); try binding.parameter(.MagFilter, gl.c.GL_LINEAR);
try binding.image2D(
// Store the character 0,
chars.appendAssumeCapacity(.{ .Red,
.tex = tex, @intCast(c_int, atlas.*.width),
.size = .{ @intCast(c_int, atlas.*.height),
@intToFloat(f32, face.*.glyph.*.bitmap.width), 0,
@intToFloat(f32, face.*.glyph.*.bitmap.rows), .Red,
}, .UnsignedByte,
.bearing = .{ atlas.*.data,
@intToFloat(f32, face.*.glyph.*.bitmap_left), );
@intToFloat(f32, face.*.glyph.*.bitmap_top),
},
.advance = @intCast(c_uint, face.*.glyph.*.advance.x),
});
}
// Configure VAO/VBO for glyph rendering
const vao = try gl.VertexArray.create();
const vbo = try gl.Buffer.create();
try vao.bind();
var binding = try vbo.bind(.ArrayBuffer);
try binding.setDataNull([6 * 4]f32, .DynamicDraw);
try binding.enableVertexAttribArray(0);
try binding.vertexAttribPointer(0, 4, gl.c.GL_FLOAT, false, 4 * @sizeOf(f32), null);
binding.unbind();
try gl.VertexArray.unbind();
// Create our shader // Create our shader
const program = try gl.Program.createVF( const program = try gl.Program.createVF(
@embedFile("../shaders/text.v.glsl"), @embedFile("../shaders/text-atlas.v.glsl"),
@embedFile("../shaders/text.f.glsl"), @embedFile("../shaders/text-atlas.f.glsl"),
); );
var res = TextRenderer{ var res = TextRenderer{
.alloc = alloc, .alloc = alloc,
.ft = ft, .font = font,
.face = face, .atlas = atlas,
.chars = chars,
.program = program, .program = program,
.vao = vao, .tex = tex,
.vbo = vbo,
.projection = undefined,
}; };
// Update the initialize size so we have some projection. We // Update the initialize size so we have some projection. We
@ -123,14 +85,8 @@ pub fn init(alloc: std.mem.Allocator) !TextRenderer {
} }
pub fn deinit(self: *TextRenderer) void { pub fn deinit(self: *TextRenderer) void {
// TODO: delete textures ftgl.texture_font_delete(self.font);
self.chars.deinit(self.alloc); ftgl.texture_atlas_delete(self.atlas);
if (ftc.FT_Done_Face(self.face) != 0)
std.log.err("freetype face deinitialization failed", .{});
if (ftc.FT_Done_FreeType(self.ft) != 0)
std.log.err("freetype library deinitialization failed", .{});
self.* = undefined; self.* = undefined;
} }
@ -152,44 +108,74 @@ pub fn render(
text: []const u8, text: []const u8,
x: f32, x: f32,
y: f32, y: f32,
scale: f32,
color: @Vector(3, f32), color: @Vector(3, f32),
) !void { ) !void {
try self.program.use(); const r = color[0];
try self.program.setUniform("textColor", color); const g = color[1];
try gl.Texture.active(gl.c.GL_TEXTURE0); const b = color[2];
try self.vao.bind(); const a: f32 = 1.0;
var vertices: std.ArrayListUnmanaged([6][9]f32) = .{};
try vertices.ensureUnusedCapacity(self.alloc, text.len);
defer vertices.deinit(self.alloc);
var curx: f32 = x; var curx: f32 = x;
for (text) |c| { for (text) |c| {
const char = self.chars.items[c]; if (ftgl.texture_font_get_glyph(self.font, &c)) |glyph_ptr| {
const glyph = glyph_ptr.*;
const kerning = 0; // for now
curx += kerning;
const xpos = curx + (char.bearing[0] * scale); const x0 = curx + @intToFloat(f32, glyph.offset_x);
const ypos = y - ((char.size[1] - char.bearing[1]) * scale); const y0 = y + @intToFloat(f32, glyph.offset_y);
const w = char.size[0] * scale; const x1 = x0 + @intToFloat(f32, glyph.width);
const h = char.size[1] * scale; const y1 = y0 - @intToFloat(f32, glyph.height);
const s0 = glyph.s0;
const t0 = glyph.t0;
const s1 = glyph.s1;
const t1 = glyph.t1;
const vert = [6][4]f32{ std.log.info("CHAR ch={} x0={} y0={} x1={} y1={}", .{ c, x0, y0, x1, y1 });
.{ xpos, ypos + h, 0.0, 0.0 },
.{ xpos, ypos, 0.0, 1.0 },
.{ xpos + w, ypos, 1.0, 1.0 },
.{ xpos, ypos + h, 0.0, 0.0 }, const vert = [6][9]f32{
.{ xpos + w, ypos, 1.0, 1.0 }, .{ x0, y0, 0, s0, t0, r, g, b, a },
.{ xpos + w, ypos + h, 1.0, 0.0 }, .{ x0, y1, 0, s0, t1, r, g, b, a },
.{ x1, y1, 0, s1, t1, r, g, b, a },
.{ x0, y0, 0, s0, t0, r, g, b, a },
.{ x1, y1, 0, s1, t1, r, g, b, a },
.{ x1, y0, 0, s1, t0, r, g, b, a },
}; };
var texbind = try char.tex.bind(.@"2D"); vertices.appendAssumeCapacity(vert);
defer texbind.unbind();
var bind = try self.vbo.bind(.ArrayBuffer);
try bind.setSubData(0, vert);
bind.unbind();
try gl.drawArrays(gl.c.GL_TRIANGLES, 0, 6); curx += glyph.advance_x;
}
curx += @intToFloat(f32, char.advance >> 6) * scale;
} }
try self.program.use();
// Bind our texture and set our data
try gl.Texture.active(gl.c.GL_TEXTURE0);
var texbind = try self.tex.bind(.@"2D");
defer texbind.unbind();
// Configure VAO/VBO for glyph rendering
const vao = try gl.VertexArray.create();
defer vao.destroy();
try vao.bind();
const vbo = try gl.Buffer.create();
defer vbo.destroy();
var binding = try vbo.bind(.ArrayBuffer);
defer binding.unbind();
try binding.setData(vertices.items, .DynamicDraw);
try binding.enableVertexAttribArray(0);
try binding.vertexAttribPointer(0, 3, gl.c.GL_FLOAT, false, 9 * @sizeOf(f32), null);
try binding.enableVertexAttribArray(1);
try binding.vertexAttribPointer(1, 2, gl.c.GL_FLOAT, false, 9 * @sizeOf(f32), @intToPtr(*const anyopaque, 3 * @sizeOf(f32)));
try binding.enableVertexAttribArray(2);
try binding.vertexAttribPointer(2, 4, gl.c.GL_FLOAT, false, 9 * @sizeOf(f32), @intToPtr(*const anyopaque, 5 * @sizeOf(f32)));
try gl.drawArrays(gl.c.GL_TRIANGLES, 0, @intCast(c_int, vertices.items.len * 6));
try gl.VertexArray.unbind(); try gl.VertexArray.unbind();
} }

196
src/TextRenderer2.zig Normal file
View File

@ -0,0 +1,196 @@
const TextRenderer = @This();
const std = @import("std");
const ftc = @import("freetype/c.zig");
const gl = @import("opengl.zig");
const gb = @import("gb_math.zig");
alloc: std.mem.Allocator,
ft: ftc.FT_Library,
face: ftc.FT_Face,
chars: CharList,
vao: gl.VertexArray = undefined,
vbo: gl.Buffer = undefined,
program: gl.Program = undefined,
projection: gb.gbMat4 = undefined,
const CharList = std.ArrayListUnmanaged(Char);
const Char = struct {
tex: gl.Texture,
size: @Vector(2, f32),
bearing: @Vector(2, f32),
advance: c_uint,
};
pub fn init(alloc: std.mem.Allocator) !TextRenderer {
var ft: ftc.FT_Library = undefined;
if (ftc.FT_Init_FreeType(&ft) != 0) {
return error.FreetypeInitFailed;
}
var face: ftc.FT_Face = undefined;
if (ftc.FT_New_Memory_Face(
ft,
face_ttf,
face_ttf.len,
0,
&face,
) != 0) {
return error.FreetypeFaceFailed;
}
_ = ftc.FT_Set_Pixel_Sizes(face, 0, 48);
// disable byte-alignment restriction
try gl.pixelStore(gl.c.GL_UNPACK_ALIGNMENT, 1);
// Pre-render all the ASCII characters
var chars = try CharList.initCapacity(alloc, 128);
var i: usize = 0;
while (i < chars.capacity) : (i += 1) {
// Load the character
if (ftc.FT_Load_Char(face, i, ftc.FT_LOAD_RENDER) != 0) {
return error.GlyphLoadFailed;
}
// Generate the texture
const tex = try gl.Texture.create();
var binding = try tex.bind(.@"2D");
defer binding.unbind();
try binding.image2D(
0,
.Red,
@intCast(c_int, face.*.glyph.*.bitmap.width),
@intCast(c_int, face.*.glyph.*.bitmap.rows),
0,
.Red,
.UnsignedByte,
face.*.glyph.*.bitmap.buffer,
);
try binding.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
try binding.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
try binding.parameter(.MinFilter, gl.c.GL_LINEAR);
try binding.parameter(.MagFilter, gl.c.GL_LINEAR);
// Store the character
chars.appendAssumeCapacity(.{
.tex = tex,
.size = .{
@intToFloat(f32, face.*.glyph.*.bitmap.width),
@intToFloat(f32, face.*.glyph.*.bitmap.rows),
},
.bearing = .{
@intToFloat(f32, face.*.glyph.*.bitmap_left),
@intToFloat(f32, face.*.glyph.*.bitmap_top),
},
.advance = @intCast(c_uint, face.*.glyph.*.advance.x),
});
}
// Configure VAO/VBO for glyph rendering
const vao = try gl.VertexArray.create();
const vbo = try gl.Buffer.create();
try vao.bind();
var binding = try vbo.bind(.ArrayBuffer);
try binding.setDataNull([6 * 4]f32, .DynamicDraw);
try binding.enableVertexAttribArray(0);
try binding.vertexAttribPointer(0, 4, gl.c.GL_FLOAT, false, 4 * @sizeOf(f32), null);
binding.unbind();
try gl.VertexArray.unbind();
// Create our shader
const program = try gl.Program.createVF(
@embedFile("../shaders/text.v.glsl"),
@embedFile("../shaders/text.f.glsl"),
);
var res = TextRenderer{
.alloc = alloc,
.ft = ft,
.face = face,
.chars = chars,
.program = program,
.vao = vao,
.vbo = vbo,
.projection = undefined,
};
// Update the initialize size so we have some projection. We
// expect this will get updated almost immediately.
try res.setScreenSize(3000, 1666);
return res;
}
pub fn deinit(self: *TextRenderer) void {
// TODO: delete textures
self.chars.deinit(self.alloc);
if (ftc.FT_Done_Face(self.face) != 0)
std.log.err("freetype face deinitialization failed", .{});
if (ftc.FT_Done_FreeType(self.ft) != 0)
std.log.err("freetype library deinitialization failed", .{});
self.* = undefined;
}
pub fn setScreenSize(self: *TextRenderer, w: i32, h: i32) !void {
gb.gb_mat4_ortho2d(
&self.projection,
0,
@intToFloat(f32, w),
0,
@intToFloat(f32, h),
);
try self.program.use();
try self.program.setUniform("projection", self.projection);
}
pub fn render(
self: TextRenderer,
text: []const u8,
x: f32,
y: f32,
scale: f32,
color: @Vector(3, f32),
) !void {
try self.program.use();
try self.program.setUniform("textColor", color);
try gl.Texture.active(gl.c.GL_TEXTURE0);
try self.vao.bind();
var curx: f32 = x;
for (text) |c| {
const char = self.chars.items[c];
const xpos = curx + (char.bearing[0] * scale);
const ypos = y - ((char.size[1] - char.bearing[1]) * scale);
const w = char.size[0] * scale;
const h = char.size[1] * scale;
const vert = [6][4]f32{
.{ xpos, ypos + h, 0.0, 0.0 },
.{ xpos, ypos, 0.0, 1.0 },
.{ xpos + w, ypos, 1.0, 1.0 },
.{ xpos, ypos + h, 0.0, 0.0 },
.{ xpos + w, ypos, 1.0, 1.0 },
.{ xpos + w, ypos + h, 1.0, 0.0 },
};
var texbind = try char.tex.bind(.@"2D");
defer texbind.unbind();
var bind = try self.vbo.bind(.ArrayBuffer);
try bind.setSubData(0, vert);
bind.unbind();
try gl.drawArrays(gl.c.GL_TRIANGLES, 0, 6);
curx += @intToFloat(f32, char.advance >> 6) * scale;
}
try gl.VertexArray.unbind();
}
const face_ttf = @embedFile("../fonts/Inconsolata-Regular.ttf");

View File

@ -24,6 +24,8 @@ pub fn link(
var flags = std.ArrayList([]const u8).init(b.allocator); var flags = std.ArrayList([]const u8).init(b.allocator);
defer flags.deinit(); defer flags.deinit();
try flags.appendSlice(&.{ try flags.appendSlice(&.{
"-std=c99",
"-O0",
"-DGL_WITH_GLAD", "-DGL_WITH_GLAD",
}); });

4
src/freetype-gl/c.zig Normal file
View File

@ -0,0 +1,4 @@
pub usingnamespace @cImport({
@cInclude("texture-atlas.h");
@cInclude("texture-font.h");
});

View File

@ -40,6 +40,9 @@ pub const Binding = struct {
usage: Usage, usage: Usage,
) !void { ) !void {
const info = dataInfo(data); const info = dataInfo(data);
std.log.info("SET DATA {}", .{
info.size,
});
c.glBufferData(@enumToInt(b.target), info.size, info.ptr, @enumToInt(usage)); c.glBufferData(@enumToInt(b.target), info.size, info.ptr, @enumToInt(usage));
try errors.getError(); try errors.getError();
} }
@ -83,7 +86,7 @@ pub const Binding = struct {
.ptr = data, .ptr = data,
}, },
.Slice => .{ .Slice => .{
.size = @sizeOf(ptr.child) * data.len, .size = @intCast(isize, @sizeOf(ptr.child) * data.len),
.ptr = data.ptr, .ptr = data.ptr,
}, },
else => { else => {