From: Brendan Hansen Date: Tue, 11 May 2021 19:42:24 +0000 (-0500) Subject: added bitmap font and FPS rendering X-Git-Url: https://git.brendanfh.com/?a=commitdiff_plain;h=HEAD;p=sand-toy.git added bitmap font and FPS rendering --- diff --git a/site/js/game.js b/site/js/game.js index 122d816..cdbb24b 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/site/sand_toy.wasm b/site/sand_toy.wasm index 7f687fa..3afcf05 100644 Binary files a/site/sand_toy.wasm and b/site/sand_toy.wasm differ diff --git a/src/build.onyx b/src/build.onyx index 2d09ee6..4227945 100644 --- a/src/build.onyx +++ b/src/build.onyx @@ -13,3 +13,4 @@ #load "src/simulation" #load "src/sand_toy" +#load "src/font/bitmap_font" diff --git a/src/font/bitmap_font.onyx b/src/font/bitmap_font.onyx new file mode 100644 index 0000000..c83548a --- /dev/null +++ b/src/font/bitmap_font.onyx @@ -0,0 +1,134 @@ +/* + +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 boundaries of the glyph. + +*/ + + + + + +package bitmap_font +use package core + +Bitmap_Font :: struct { + font_texture : Bitmap_Font_Texture; + + em: f32; // Width of 'M' + + glyphs : map.Map(u32, Glyph); + + Glyph :: struct { + x0, y0, x1, y1: f32; + w, h: 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.data)[y * width + x]; + + // SPEED: All of these values could be read simultaneously by treating the data + // array as a slice of u32, and then not multiplying by Components_Per_Pixel. + // + // 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, charset: 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,0,0}); + + success := bitmap_font_prepare_glyphs(^bmp, charset); + assert(success, "Failed to load glyphs out of font."); + + return bmp; +} + +bitmap_font_prepare_glyphs :: (use bmp: ^Bitmap_Font, charset: str) -> bool { + separator_color := font_texture->get_pixel(0, 0); + + g_x0, g_y0: u32; + g_x0, g_y0 = 0, 0; + + for char: charset { + if g_y0 >= font_texture.height do return false; + + // 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, ~~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, + w = cast(f32) (x1 - x0) / ~~font_texture.width, + h = cast(f32) (y1 - y0) / ~~font_texture.height, + }); + + g_x0 = x1 + 1; + + // CLEANUP: This is gross because Onyx does not support short circuit boolean operators, + // and because g_x0 being greater than font_texture.width would potentially overflow in + // get_pixel, they need to be checked in two different steps instead of with '||'. + 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; + } + } + + M_glyph := map.get_ptr(^glyphs, #char "M"); + if M_glyph != null { + em = M_glyph.w * ~~font_texture.width; + } else { + // ROBUSTNESS: If there is no 'M' in the character set, then just use 16. It will probably be very wrong. + em = 16; + } + + return true; +} diff --git a/src/res/font.data b/src/res/font.data new file mode 100644 index 0000000..615eafb Binary files /dev/null and b/src/res/font.data differ diff --git a/src/res/font_2.data b/src/res/font_2.data new file mode 100644 index 0000000..00ac0e0 Binary files /dev/null and b/src/res/font_2.data differ diff --git a/src/sand_toy.onyx b/src/sand_toy.onyx index aebc1c4..8fbeae4 100644 --- a/src/sand_toy.onyx +++ b/src/sand_toy.onyx @@ -3,6 +3,8 @@ use package core #private_file events :: package js_events #private_file gl :: package gl +#private_file bitmap_font :: package bitmap_font + BAR_HEIGHT :: 32.0f particle_board: ParticleBoard; @@ -12,6 +14,9 @@ world_height :: 400 world_texture : gl.GLTexture world_texture_data : [] u8 +font_texture : gl.GLTexture +font : bitmap_font.Bitmap_Font + window_width := 0 window_height := 0 @@ -38,9 +43,35 @@ init :: () { gfx.immediate_renderer_init(); + init_font(); + + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + particle_board = create_board(world_width, world_height); } +init_font :: () { + font_data := #file_contents "src/res/font_2.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); +} + poll_events :: () { for event: events.consume() do switch event.kind { case .Resize { @@ -121,7 +152,18 @@ remove_particles :: (x: i32, y: i32, radius: i32) { } } -update :: () { +fps := 0; +fps_timer := 1.0f; +frames := 0 +update :: (dt: f32) { + fps_timer -= dt; + frames += 1; + if fps_timer <= 0 { + fps = frames; + frames = 0; + fps_timer = 1.0f; + } + if mouse_down { spawn_particles(~~mouse_x, ~~mouse_y, 20, selected_particle); } @@ -167,6 +209,10 @@ draw :: () { gl.bindTexture(gl.TEXTURE_2D, -1); + fps_buffer : [16] u8; + fps_string := conv.str_format("FPS: %i", ~~fps_buffer, fps); + draw_text(fps_string, 0, BAR_HEIGHT); + draw_selection_bar :: () { gfx.rect(.{ 0, 0 }, .{ ~~window_width, BAR_HEIGHT }, color=.{.2,.2,.2}); @@ -194,10 +240,46 @@ draw :: () { } } +draw_text :: (text: str, x: f32, y: f32, size := 32.0f, color := gfx.Color4.{1,1,1}) { + + gl.bindTexture(gl.TEXTURE_2D, font_texture); + gfx.set_texture(0); + + for char: text { + glyph := font->get_glyph(char); + + if glyph == null { + glyph = font->get_glyph(255); + assert(glyph != null, "NO NULL GLYPH"); + } + + gfx.textured_rect( + .{ x, y }, + .{ glyph.w * size * font.em, glyph.h * size * font.em }, + .{ glyph.x0, glyph.y0 }, + .{ glyph.x1 - glyph.x0, glyph.y1 - glyph.y0 }, + color = color); + + x += glyph.w * size * font.em; + } + + gfx.flush(); + gl.bindTexture(gl.TEXTURE_2D, -1); +} + + +last_time := 0; #export "loop" () { - poll_events(); + time_now :: () -> i32 #foreign "game" "time_now" --- + + if last_time == 0 do last_time = time_now(); - update(); + now := time_now(); + dt := cast(f32) (now - last_time) / 1000.0f; + last_time = now; + + poll_events(); + update(dt); draw(); }