From 2c81aeec6e12d5f8b77d10451ca88a7af62f8b77 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sat, 28 Feb 2026 23:52:18 +0000 Subject: [PATCH] font: string rendering --- font.lua | 95 +++++++++++++++++++++++++++++++ font/terminus_128x128_8x16.data | Bin 0 -> 16384 bytes font/terminus_128x64_6x12.data | Bin 0 -> 8192 bytes font/terminus_256x128_10x18.data | Bin 0 -> 32768 bytes font/terminus_256x128_12x24.data | Bin 0 -> 32768 bytes main.lua | 7 +++ pixel_font.glsl | 19 +++++++ vertex_font.glsl | 21 +++++++ 8 files changed, 142 insertions(+) create mode 100644 font.lua create mode 100644 font/terminus_128x128_8x16.data create mode 100644 font/terminus_128x64_6x12.data create mode 100644 font/terminus_256x128_10x18.data create mode 100644 font/terminus_256x128_12x24.data create mode 100644 pixel_font.glsl create mode 100644 vertex_font.glsl diff --git a/font.lua b/font.lua new file mode 100644 index 0000000..c9f3c20 --- /dev/null +++ b/font.lua @@ -0,0 +1,95 @@ +local _math = require "_math" +local mat4 = _math.mat4 + +local pixel_font_data = love.filesystem.newFileData("pixel_font.glsl") +local vertex_font_data = love.filesystem.newFileData("vertex_font.glsl") +local shader_font = love.graphics.newShader(pixel_font_data, vertex_font_data) + +local fonts = { + ter_6x12 = { + path = "font/terminus_128x64_6x12.data", + texture_width = 128, + texture_height = 64, + glyph_width = 6, + glyph_height = 12, + }, + ter_8x16 = { + path = "font/terminus_128x128_8x16.data", + texture_width = 128, + texture_height = 128, + glyph_width = 8, + glyph_height = 16, + }, + ter_10x18 = { + path = "font/terminus_256x128_10x18.data", + texture_width = 256, + texture_height = 128, + glyph_width = 10, + glyph_height = 18, + }, + ter_12x24 = { + path = "font/terminus_256x128_12x24.data", + texture_width = 256, + texture_height = 128, + glyph_width = 12, + glyph_height = 24, + } +} + +local load_font = function(desc) + local file_data = love.filesystem.newFileData(desc.path) + local image_data = love.image.newImageData(desc.texture_width, desc.texture_height, "r8", file_data) + local texture = love.graphics.newTexture(image_data) + texture:setFilter("nearest", "nearest") + local font = { + texture = texture, + texture_width = desc.texture_width, + texture_height = desc.texture_height, + glyph_width = desc.glyph_width, + glyph_height = desc.glyph_height, + stride = math.floor(desc.texture_width / desc.glyph_width), + } + return font +end + +local glyph_coordinate = function(font, ord) + local c = ord - 32 + local x = c % font.stride + local y = math.floor(c / font.stride) + return {x, y} +end + +local glyph_transform = function(font, x, y) + local transform = ( + mat4.scaling(font.glyph_width, font.glyph_height, 0) + * mat4.translation(x, -y, 0) + * mat4.scaling(2 / 1024, 2 / 1024, 0) + * mat4.translation(-1, 1, 0) + ) + return transform +end + +local draw_string = function(font, s, x, y) + love.graphics.setCanvas() + love.graphics.setShader(shader_font) + shader_font:send("texture_sampler", font.texture) + love.graphics.setDepthMode("always", false) + shader_font:send("cell", {font.glyph_width / font.texture_width, font.glyph_height / font.texture_height}) + + for i = 1, #s do + local c = s:byte(i) + if c ~= 32 then + shader_font:send("transform", "column", glyph_transform(font, x, y).data) + shader_font:send("glyph", glyph_coordinate(font, c)) + love.graphics.drawFromShader(screen_index_buffer, 3 * 2, 1, 1) + end + + x = x + font.glyph_width + end +end + +return { + fonts = fonts, + load_font = load_font, + draw_string = draw_string, +} diff --git a/font/terminus_128x128_8x16.data b/font/terminus_128x128_8x16.data new file mode 100644 index 0000000000000000000000000000000000000000..00b05559e7726a900d166edf430ef3ea773540f4 GIT binary patch literal 16384 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 GoWL)-)&&3n literal 0 HcmV?d00001 diff --git a/font/terminus_256x128_10x18.data b/font/terminus_256x128_10x18.data new file mode 100644 index 0000000000000000000000000000000000000000..246d05a44084bc585c86e02b6e630ccad2a9c515 GIT binary patch literal 32768 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 literal 0 HcmV?d00001 diff --git a/main.lua b/main.lua index 639e4d9..ee6f79d 100644 --- a/main.lua +++ b/main.lua @@ -16,6 +16,9 @@ local scene_noodle = require 'scene.noodle.noodle' local scene_sci_fi_ship = require 'scene.sci_fi_ship.sci_fi_ship' local scene_shadow_test = require 'scene.shadow_test.shadow_test' +local font = require 'font' +local terminus_font + local scenes = { sci_fi_ship = { descriptor = scene_sci_fi_ship.descriptor, @@ -80,6 +83,8 @@ function love.load(args) g_shadow_canvas = love.graphics.newCanvas(2048, 2048, {format = "r32f"}) load_screen_shader() + + terminus_font = font.load_font(font.fonts.ter_10x18) end local rotation = 0.0 @@ -128,4 +133,6 @@ function love.draw() -- love.graphics.setShader(screen_shader) -- screen_shader:send("g_sampler", g_shadow_canvas) -- love.graphics.drawFromShader(screen_index_buffer, 3 * 2, 1, 1) + + font.draw_string(terminus_font, "asdf test 1234", 10, 10) end diff --git a/pixel_font.glsl b/pixel_font.glsl new file mode 100644 index 0000000..27acd84 --- /dev/null +++ b/pixel_font.glsl @@ -0,0 +1,19 @@ +#pragma language glsl3 + +layout (location = 0) out vec4 g_color; + +uniform sampler2D texture_sampler; + +in vec4 PixelTexture; + +uniform vec2 cell; +uniform vec2 glyph; + +void pixelmain() +{ + vec4 sample = texture(texture_sampler, PixelTexture.xy * cell + cell * glyph); + float px = sample.x == 0.0 ? 0.0 : 1.0; + + g_color = vec4(vec3(px), 1.0); + //g_color = vec4(1, 0, 0, 1.0); +} diff --git a/vertex_font.glsl b/vertex_font.glsl new file mode 100644 index 0000000..7ef4162 --- /dev/null +++ b/vertex_font.glsl @@ -0,0 +1,21 @@ +#pragma language glsl3 + +const vec2 vtx[4] = vec2[](vec2(-1.0, 1.0), // tl + vec2( 1.0, 1.0), // tr + vec2( 1.0, -1.0), // br + vec2(-1.0, -1.0)); // bl + +uniform mat4 transform; + +out vec4 PixelTexture; + +void vertexmain() +{ + vec2 vertex = vtx[gl_VertexID]; + + PixelTexture = vec4(vertex * vec2(0.5, -0.5) + 0.5, 0, 0); + + vertex = vertex * vec2(0.5, 0.5) + vec2(0.5, -0.5); + + love_Position = transform * vec4(vertex, 0.0, 1.0); +}