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.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 glfw.waitEvents();

View File

@ -4,15 +4,15 @@ const std = @import("std");
const ftc = @import("freetype/c.zig");
const gl = @import("opengl.zig");
const gb = @import("gb_math.zig");
const ftgl = @import("freetype-gl/c.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,
font: *ftgl.texture_font_t,
atlas: *ftgl.texture_atlas_t,
program: gl.Program,
tex: gl.Texture,
const CharList = std.ArrayListUnmanaged(Char);
const Char = struct {
@ -23,96 +23,58 @@ const Char = struct {
};
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,
const atlas = ftgl.texture_atlas_new(512, 512, 1);
if (atlas == null) return error.FontAtlasFail;
errdefer ftgl.texture_atlas_delete(atlas);
const font = ftgl.texture_font_new_from_memory(
atlas,
48,
face_ttf,
face_ttf.len,
0,
&face,
) != 0) {
return error.FreetypeFaceFailed;
}
);
if (font == null) return error.FontInitFail;
errdefer ftgl.texture_font_delete(font);
_ = 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 all visible ASCII characters.
var i: u8 = 32;
while (i < 127) : (i += 1) {
// 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;
}
}
// Generate the texture
// Build our 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,
);
errdefer tex.destroy();
const binding = try tex.bind(.@"2D");
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();
try binding.image2D(
0,
.Red,
@intCast(c_int, atlas.*.width),
@intCast(c_int, atlas.*.height),
0,
.Red,
.UnsignedByte,
atlas.*.data,
);
// Create our shader
const program = try gl.Program.createVF(
@embedFile("../shaders/text.v.glsl"),
@embedFile("../shaders/text.f.glsl"),
@embedFile("../shaders/text-atlas.v.glsl"),
@embedFile("../shaders/text-atlas.f.glsl"),
);
var res = TextRenderer{
.alloc = alloc,
.ft = ft,
.face = face,
.chars = chars,
.font = font,
.atlas = atlas,
.program = program,
.vao = vao,
.vbo = vbo,
.projection = undefined,
.tex = tex,
};
// 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 {
// 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", .{});
ftgl.texture_font_delete(self.font);
ftgl.texture_atlas_delete(self.atlas);
self.* = undefined;
}
@ -152,44 +108,74 @@ pub fn render(
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();
const r = color[0];
const g = color[1];
const b = color[2];
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;
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 ypos = y - ((char.size[1] - char.bearing[1]) * scale);
const w = char.size[0] * scale;
const h = char.size[1] * scale;
const x0 = curx + @intToFloat(f32, glyph.offset_x);
const y0 = y + @intToFloat(f32, glyph.offset_y);
const x1 = x0 + @intToFloat(f32, glyph.width);
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{
.{ xpos, ypos + h, 0.0, 0.0 },
.{ xpos, ypos, 0.0, 1.0 },
.{ xpos + w, ypos, 1.0, 1.0 },
std.log.info("CHAR ch={} x0={} y0={} x1={} y1={}", .{ c, x0, y0, x1, y1 });
.{ xpos, ypos + h, 0.0, 0.0 },
.{ xpos + w, ypos, 1.0, 1.0 },
.{ xpos + w, ypos + h, 1.0, 0.0 },
const vert = [6][9]f32{
.{ x0, y0, 0, s0, t0, r, g, b, a },
.{ 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");
defer texbind.unbind();
var bind = try self.vbo.bind(.ArrayBuffer);
try bind.setSubData(0, vert);
bind.unbind();
vertices.appendAssumeCapacity(vert);
try gl.drawArrays(gl.c.GL_TRIANGLES, 0, 6);
curx += @intToFloat(f32, char.advance >> 6) * scale;
curx += glyph.advance_x;
}
}
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();
}

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);
defer flags.deinit();
try flags.appendSlice(&.{
"-std=c99",
"-O0",
"-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,
) !void {
const info = dataInfo(data);
std.log.info("SET DATA {}", .{
info.size,
});
c.glBufferData(@enumToInt(b.target), info.size, info.ptr, @enumToInt(usage));
try errors.getError();
}
@ -83,7 +86,7 @@ pub const Binding = struct {
.ptr = data,
},
.Slice => .{
.size = @sizeOf(ptr.child) * data.len,
.size = @intCast(isize, @sizeOf(ptr.child) * data.len),
.ptr = data.ptr,
},
else => {