Skip to content

Instantly share code, notes, and snippets.

@jakubtomsu
Last active October 6, 2024 09:26
Show Gist options
  • Save jakubtomsu/5ee4fdfee23df893f6f61b4692dcf895 to your computer and use it in GitHub Desktop.
Save jakubtomsu/5ee4fdfee23df893f6f61b4692dcf895 to your computer and use it in GitHub Desktop.
Example font renderer using fontstash and sokol_gfx in Odin
// This is an example usage of vendor:fontstash with sokol_gfx.
// By Jakub Tomšů
//
// https://gist.github.com/jakubtomsu/5ee4fdfee23df893f6f61b4692dcf895
//
// This won't compile on it's own, but it contains all of the interesting parts.
// It should be pretty obvious how to modify it to your needs, if not let me know.
//
// The genral per-frame work is this:
// - renderer_draw_text appends quads to a cpu-side buffer
// - the quads get uploaded to the gpu as an per-instance vertex buffer
// - update atlas if dirty or resized.
// Note: sokol doesn't have partial writes, we always need to destroy it and create it again.
// - draw all characters in a single instanced draw call
package game
import "../shaders"
import sg "../sokol/gfx"
import "core:fmt" // <-- this contains output of sokol-shdc
import "vendor:fontstash"
RENDERER_DEFAULT_FONT_ATLAS_SIZE :: 512
Renderer :: struct {
font_ctx: fontstash.FontContext,
font_indexes: [Text_Font]i32,
font_atlas_size: [2]i32,
// Fixed size array with len, you can use a dynamic array as well
font_instances: Array(BUDGET_FONT_QUADS, Renderer_Font_Instance),
font_pip: sg.Pipeline,
font_instance_buf: sg.Buffer,
font_ibuf: sg.Buffer,
font_img: sg.Image,
}
// Quad
Renderer_Font_Instance :: struct {
pos_min: [2]f32,
pos_max: [2]f32,
uv_min: [2]u16,
uv_max: [2]u16,
color: [4]u8,
}
Text_Align_Horizontal :: enum u8 {
Left = u8(fontstash.AlignHorizontal.LEFT),
Center = u8(fontstash.AlignHorizontal.CENTER),
Right = u8(fontstash.AlignHorizontal.RIGHT),
}
Text_Align_Vertical :: enum u8 {
Top = u8(fontstash.AlignVertical.TOP),
Middle = u8(fontstash.AlignVertical.MIDDLE),
Bottom = u8(fontstash.AlignVertical.BOTTOM),
Baseline = u8(fontstash.AlignVertical.BASELINE),
}
Text_Font :: enum u8 {
Default = 0,
Debug,
}
@(rodata)
_text_font_path := [Text_Font]string {
.Default = "assets/Noto_Sans_JP/static/NotoSansJP-Regular.ttf",
.Debug = "assets/JetBrainsMono-Regular.ttf",
}
renderer_draw_text :: proc(
ren: ^Renderer,
text: string,
pos: Vec2,
size: f32 = 12,
color := COLOR_WHITE,
blur: f32 = 0,
spacing: f32 = 0,
font: Text_Font = .Default,
align_h: Text_Align_Horizontal = .Left,
align_v: Text_Align_Vertical = .Baseline,
) {
col := color_to_color8(color)
// Easier than dealing with fontstash state stack...
state := fontstash.__getState(&ren.font_ctx)
state^ = {
size = size,
blur = blur,
spacing = spacing,
font = 0,
ah = fontstash.AlignHorizontal(align_h),
av = fontstash.AlignVertical(align_v),
}
inv_screen := 1.0 / vec_cast(f32, ren.screen_size)
for iter := fontstash.TextIterInit(&ren.font_ctx, pos.x, pos.y, text); true; {
quad: fontstash.Quad
fontstash.TextIterNext(&ren.font_ctx, &iter, &quad) or_break
ds_append(
&ren.font_instances,
Renderer_Font_Instance {
// Transform quads into NDC
pos_min = (Vec2{quad.x0, quad.y0} * inv_screen) * 2.0 - 1.0,
pos_max = (Vec2{quad.x1, quad.y1} * inv_screen) * 2.0 - 1.0,
uv_min = {norm_pack_fast(u16, quad.s0), norm_pack_fast(u16, quad.t0)},
uv_max = {norm_pack_fast(u16, quad.s1), norm_pack_fast(u16, quad.t1)},
color = col,
},
)
}
}
renderer_init :: proc(ren: ^Renderer) {
fontstash.Init(&ren.font_ctx, RENDERER_DEFAULT_FONT_ATLAS_SIZE, RENDERER_DEFAULT_FONT_ATLAS_SIZE, .BOTTOMLEFT)
for font in Text_Font {
index := fontstash.AddFontPath(&ren.font_ctx, fmt.tprint(font), _text_font_path[font])
ren.font_indexes[font] = i32(index)
}
ren.font_pip = sg.make_pipeline(
{
label = "font-pip",
shader = sg.make_shader(shaders.font_shader_desc(sg.query_backend())),
layout = {
buffers = {0 = {step_func = .PER_INSTANCE}},
attrs = {
shaders.ATTR_font_vs_inst_pos_min = {format = .FLOAT2},
shaders.ATTR_font_vs_inst_pos_max = {format = .FLOAT2},
shaders.ATTR_font_vs_inst_uv_min = {format = .USHORT2N},
shaders.ATTR_font_vs_inst_uv_max = {format = .USHORT2N},
shaders.ATTR_font_vs_inst_color = {format = .UBYTE4N},
},
},
index_type = .UINT16,
color_count = 1,
colors = {0 = {pixel_format = RENDERER_GBUF_COLOR_PIXEL_FORMAT, blend = RENDERER_PREMULTIPLIED_ALPHA_BLEND_STATE}},
depth = {pixel_format = .DEPTH, write_enabled = false, compare = .ALWAYS},
cull_mode = .NONE,
},
)
ren.font_instance_buf = sg.make_buffer(
{label = "font-instance-buf", type = .VERTEXBUFFER, usage = .STREAM, size = size_of(ren.font_instances.data)},
)
ren.font_ibuf = sg.make_buffer(
{label = "font-ibuf", type = .INDEXBUFFER, usage = .IMMUTABLE, data = sg_range_slice([]u16{0, 1, 2, 1, 2, 3})},
)
// ...
}
renderer_update :: proc(ren: ^Renderer) {
// Update atlas and fontstash
{
if ren.font_instances.len > 0 {
sg.update_buffer(ren.font_instance_buf, sg_range_slice(ds_slice(&ren.font_instances)))
}
atlas_size := IVec2{i32(ren.font_ctx.width), i32(ren.font_ctx.height)}
dirty_texture :=
ren.font_ctx.dirtyRect[0] < ren.font_ctx.dirtyRect[2] && ren.font_ctx.dirtyRect[1] < ren.font_ctx.dirtyRect[3]
// Note dirty_texture: sokol doesn't have partial texture updates
if ren.font_atlas_size != atlas_size || dirty_texture {
ren.font_atlas_size = atlas_size
sg.destroy_image(ren.font_img)
ren.font_img = sg.make_image(
{
label = "font-img",
type = ._2D,
width = atlas_size.x,
height = atlas_size.y,
num_slices = 1,
num_mipmaps = 1,
pixel_format = .R8,
sample_count = 1,
usage = .IMMUTABLE,
data = {subimage = {0 = {0 = sg_range_slice(ren.font_ctx.textureData)}}},
},
)
}
ren.font_ctx.state_count = 0
fontstash.Reset(&ren.font_ctx)
}
// Draw characters
if ren.font_instances.len > 0 {
sg.apply_pipeline(ren.font_pip)
// clamp to border
sg.apply_bindings(
{
vertex_buffers = {0 = ren.font_instance_buf},
index_buffer = ren.font_ibuf,
fs = {
images = {shaders.SLOT_font_tex = ren.font_img},
samplers = {shaders.SLOT_font_smp = ren.linear_clamp_to_edge_smp},
},
},
)
sg.draw(0, 6, ren.font_instances.len)
ren.font_instances.len = 0
}
// swapchain pass...
}
// This file should live in the shaders/ directory along with the shaders.odin generated by sokol-shdc
// build with this on windows:
// sokol-tools-bin\\bin\\win32\\sokol-shdc.exe -i source/shaders/shaders.glsl -o source/shaders/shaders.odin -l hlsl5 -f sokol_odin
@header package shaders
@header import sg "../sokol/gfx"
@vs font_vs
in vec2 inst_pos_min;
in vec2 inst_pos_max;
in vec2 inst_uv_min;
in vec2 inst_uv_max;
in vec4 inst_color;
out vec2 vert_uv;
out vec4 vert_color;
void main() {
// Note: we could change the quad definition to use offset+size instead for both position and UVs instead of the bit hacks.
bool left = bool(gl_VertexIndex & 1);
bool bottom = bool((gl_VertexIndex >> 1) & 1);
vec2 pos = vec2(left ? inst_pos_min.x : inst_pos_max.x, bottom ? inst_pos_min.y : inst_pos_max.y);
vec2 uv = vec2(left ? inst_uv_min.x : inst_uv_max.x, bottom ? inst_uv_min.y : inst_uv_max.y);
gl_Position = vec4(pos, 0.0, 1.0);
vert_uv = uv;
vert_color = inst_color;
}
@end
@fs font_fs
in vec2 vert_uv;
in vec4 vert_color;
out vec4 frag_color;
uniform texture2D font_tex;
uniform sampler font_smp;
void main() {
frag_color = vert_color * vec4(texture(sampler2D(font_tex, font_smp), vert_uv).r);
}
@end
@program font font_vs font_fs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment