added bitmap font and FPS rendering master
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Tue, 11 May 2021 19:42:24 +0000 (14:42 -0500)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Tue, 11 May 2021 19:42:24 +0000 (14:42 -0500)
site/js/game.js
site/sand_toy.wasm
src/build.onyx
src/font/bitmap_font.onyx [new file with mode: 0644]
src/res/font.data [new file with mode: 0644]
src/res/font_2.data [new file with mode: 0644]
src/sand_toy.onyx

index 122d816c262bf2db03c7a9fa77892a3a22da14a1..cdbb24bee59c8f745c508eb448c033aa34122032 100644 (file)
@@ -12,4 +12,8 @@ window.ONYX_MODULES.push({
 \r
         window.requestAnimationFrame(loop);\r
     },\r
+\r
+    time_now: function() {\r
+        return Date.now();\r
+    },\r
 });\r
index 7f687fa774da28392268b1ce6af3df59e022786c..3afcf053b71453d9a2b37bb1a362dbef58f73a59 100644 (file)
Binary files a/site/sand_toy.wasm and b/site/sand_toy.wasm differ
index 2d09ee65d4cf9d37d09bf54deac55325e531f006..4227945b54de0e936be18aad0499cf35ed422a0e 100644 (file)
@@ -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 (file)
index 0000000..c83548a
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..00ac0e0
Binary files /dev/null and b/src/res/font_2.data differ
index aebc1c42ebdeee6432711cfe36d743e91a33d064..8fbeae4d960baf07561b196d9ddd5822d90aa776 100644 (file)
@@ -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();
 }