From: Brendan Hansen Date: Sat, 8 May 2021 18:31:32 +0000 (-0500) Subject: added basic bitmap font rendering X-Git-Url: https://git.brendanfh.com/?a=commitdiff_plain;h=5e8c9dbf1b417fa98ba11391f61ac447cf6c48e4;p=tower.git added basic bitmap font rendering --- 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 0000000..96b8ec4 Binary files /dev/null and b/src/res/font.data differ diff --git a/src/tower.onyx b/src/tower.onyx index b77f78e..35d0bd3 100644 --- a/src/tower.onyx +++ b/src/tower.onyx @@ -5,22 +5,62 @@ use package core #private_file gl :: package gl #private_file gfx :: package immediate_mode -main :: (args: [] cstr) { - println("Hello World!"); +#private_file bitmap_font :: package bitmap_font + + + +font : bitmap_font.Bitmap_Font; +font_texture : gl.GLTexture; +main :: (args: [] cstr) { gl.init("game"); events.init(); gfx.immediate_renderer_init(); + init_font(); + start_loop :: () -> 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); +}