Last active
October 6, 2024 09:26
-
-
Save jakubtomsu/5ee4fdfee23df893f6f61b4692dcf895 to your computer and use it in GitHub Desktop.
Example font renderer using fontstash and sokol_gfx in Odin
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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