From 5e8c9dbf1b417fa98ba11391f61ac447cf6c48e4 Mon Sep 17 00:00:00 2001 From: Brendan Hansen Date: Sat, 8 May 2021 13:31:32 -0500 Subject: [PATCH] added basic bitmap font rendering --- site/js/game.js | 4 ++ src/build.onyx | 1 + src/font/bitmap_font.onyx | 111 ++++++++++++++++++++++++++++++++++++++ src/res/font.data | Bin 0 -> 262144 bytes src/tower.onyx | 90 ++++++++++++++++++++++++++++--- 5 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 src/font/bitmap_font.onyx create mode 100644 src/res/font.data diff --git a/site/js/game.js b/site/js/game.js index 08f959c..b5f89e8 100644 --- a/site/js/game.js +++ b/site/js/game.js @@ -12,4 +12,8 @@ window.ONYX_MODULES.push({ window.requestAnimationFrame(loop); }, + + time_now: function() { + return Date.now(); + }, }); diff --git a/src/build.onyx b/src/build.onyx index 3869d20..373d6be 100644 --- a/src/build.onyx +++ b/src/build.onyx @@ -8,4 +8,5 @@ #load "modules/js_events/module" #load "modules/immediate_mode/module" +#load "src/font/bitmap_font" #load "src/tower" diff --git a/src/font/bitmap_font.onyx b/src/font/bitmap_font.onyx new file mode 100644 index 0000000..1ec0cb5 --- /dev/null +++ b/src/font/bitmap_font.onyx @@ -0,0 +1,111 @@ +/* + +Bitmap Fonts + +Simple bitmap font renderer. Splits an image of all font characters a set of glyphs +that can be used to render individual characters / text. + +When creating a bitmap font, you need to provide the font texture, and the order of the +glyphs in the texture in a string. You can use non-ASCII characters in the string using +'\xAA' as a character. + +The font data provided is an RGBA byte array, with interlaced components. +In other words the data should look like: + + R G B A R G B A R G B A ... + +The top left (0-th) pixel is taken to be the separator color. It is used to determine the +outline of every glyph. Glyphs are read from the top left, left to right, top to bottom. +The separator color must be present on the entire left and top sides of the glyph. + +*/ + + + + + +package bitmap_font +use package core + +Bitmap_Font :: struct { + font_texture : Bitmap_Font_Texture; + + glyphs : map.Map(u32, Glyph); + + Glyph :: struct { + x0, y0, x1, y1: f32; + } + + get_glyph :: (use bmp: ^Bitmap_Font, char: u8) -> ^Glyph { + return map.get_ptr(^glyphs, ~~char); + } +} + +Bitmap_Font_Texture :: struct { + Components_Per_Pixel :: 4; // NOTE: RGBA only + + data : [] u8; + width, height : u32; + + // NOTE: Assumes pixels are laid out in RGBA format. + get_pixel :: (use texture: ^Bitmap_Font_Texture, x: u32, y: u32) -> u32 { + return (cast(u32) data[(y * width + x) * Components_Per_Pixel + 0] << 24) + | (cast(u32) data[(y * width + x) * Components_Per_Pixel + 1] << 16) + | (cast(u32) data[(y * width + x) * Components_Per_Pixel + 2] << 8) + | (cast(u32) data[(y * width + x) * Components_Per_Pixel + 3] << 0); + } +} + + +// NOTE: Takes the raw RGBA font data. Therefore, font_data.count is expected to +// be a multiple of 4 in length, with interlaced red, green, blue and alpha components. +// The font data can be load from another source using this method, which is why it +// is preferred. +bitmap_font_create :: (bft: Bitmap_Font_Texture, glyph_str: str) -> Bitmap_Font { + assert(bft.data.count <= bft.width * bft.height * Bitmap_Font_Texture.Components_Per_Pixel, "Bad font size."); + + bmp: Bitmap_Font; + bmp.font_texture = bft; + map.init(^bmp.glyphs, .{0,0,0,0}); + + success := bitmap_font_prepare_glyphs(^bmp, glyph_str); + assert(success, "Failed to load glyphs out of font."); + + return bmp; +} + +bitmap_font_prepare_glyphs :: (use bmp: ^Bitmap_Font, glyph_str: str) -> bool { + separator_color := font_texture->get_pixel(0, 0); + + g_x0, g_y0: u32; + g_x0, g_y0 = 0, 0; + + for glyph_char: glyph_str { + // These will need to be converted to floating point when they are inserted into the glyph, + // but it is easier to think about them as actual pixel coordinates when parsing the glyphs. + x0, y0, x1, y1: u32; + x0, y0 = g_x0 + 1, g_y0 + 1; + x1, y1 = x0, y0; + + while font_texture->get_pixel(x1, y0) != separator_color do x1 += 1; + while font_texture->get_pixel(x0, y1) != separator_color do y1 += 1; + + map.put(^glyphs, ~~glyph_char, .{ + x0 = cast(f32) x0 / ~~font_texture.width, + y0 = cast(f32) y0 / ~~font_texture.height, + x1 = cast(f32) x1 / ~~font_texture.width, + y1 = cast(f32) y1 / ~~font_texture.height, + }); + + g_x0 = x1 + 1; + reset := g_x0 >= font_texture.width; + if !reset do reset = font_texture->get_pixel(g_x0, g_y0) != separator_color; + + if reset { + g_x0 = 0; + g_y0 = y1 + 1; + } + } + + return true; +} diff --git a/src/res/font.data b/src/res/font.data new file mode 100644 index 0000000000000000000000000000000000000000..96b8ec4c06cf978b1e45ef5306f0df325b39fab2 GIT binary patch literal 262144 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 void #foreign "game" "start_loop" --- start_loop(); } +init_font :: () { + font_data := #file_contents "src/res/font.data"; + + bft := bitmap_font.Bitmap_Font_Texture.{ + data = font_data, + width = 256, + height = 256, + }; + + font = bitmap_font.bitmap_font_create(bft, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 \xff"); + + font_texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, font_texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 256, 0, gl.RGBA, gl.UNSIGNED_BYTE, font_data); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.bindTexture(gl.TEXTURE_2D, -1); +} + +last_time := 0; #export "loop" () { + time_now :: () -> i32 #foreign "game" "time_now" --- + + if last_time == 0 do last_time = time_now(); + + now := time_now(); + dt := cast(f32) (now - last_time) / 1000.0f; + last_time = now; + poll_events(); + update(dt); draw(); } +window_width := 0 +window_height := 0 poll_events :: () { for event: events.consume() do switch event.kind { case .MouseDown do println("Mouse was down!"); @@ -28,19 +68,57 @@ poll_events :: () { case .Resize { printf("Window was resized to: %i %i\n", event.resize.width, event.resize.height); + window_width = event.resize.width; + window_height = event.resize.height; + gl.setSize(event.resize.width, event.resize.height); gl.viewport(0, 0, event.resize.width, event.resize.height); - gfx.use_ortho_projection(0, ~~event.resize.width, 0, ~~event.resize.height); } } } +update :: (dt: f32) { +} + draw :: () { - gl.clearColor(0, 0, 0, 1); + gl.clearColor(0.1, 0.1, 0.1, 1); gl.clear(gl.COLOR_BUFFER_BIT); - gfx.quad(.{ 0, 0 }, .{ 400, 400 }, color=.{ 1, 0, 1 }); - gfx.quad(.{ 20, 0 }, .{ 400, 400 }, color=.{ 1, 1, 1 }); + gfx.use_ortho_projection(0, ~~window_width, 0, ~~window_height); + + draw_text("Hello World", 100, 100, 128); + draw_text("something else...", 100, 230, 32); + gfx.flush(); } + + + + +draw_text :: (text: str, x: f32, y: f32, size := 32.0f) { + + gl.bindTexture(gl.TEXTURE_2D, font_texture); + gfx.set_texture(0); + gfx.use_ortho_projection(0, ~~window_width, 0, ~~window_height); + + for char: text { + glyph := font->get_glyph(char); + + if glyph == null { + glyph = font->get_glyph(255); + assert(glyph != null, "NO NULL GLYPH"); + } + + gfx.textured_quad( + .{ x, y }, + .{ size, size }, + .{ glyph.x0, glyph.y0 }, + .{ glyph.x1 - glyph.x0, glyph.y1 - glyph.y0 }); + + x += size; + } + + gfx.flush(); + gl.bindTexture(gl.TEXTURE_2D, -1); +} -- 2.25.1