added basic bitmap font rendering
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Sat, 8 May 2021 18:31:32 +0000 (13:31 -0500)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Sat, 8 May 2021 18:31:32 +0000 (13:31 -0500)
site/js/game.js
src/build.onyx
src/font/bitmap_font.onyx [new file with mode: 0644]
src/res/font.data [new file with mode: 0644]
src/tower.onyx

index 08f959cf44893340af9914c8bb68fd817e998997..b5f89e82c16303bf15b65fda04356be14b205576 100644 (file)
@@ -12,4 +12,8 @@ window.ONYX_MODULES.push({
 
         window.requestAnimationFrame(loop);
     },
+
+    time_now: function() {
+        return Date.now();
+    },
 });
index 3869d2055af9adec52ad60bd16e1b6afb6941014..373d6bec477116a8c56db3a78bf91679cf4fabec 100644 (file)
@@ -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 (file)
index 0000000..1ec0cb5
--- /dev/null
@@ -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 (file)
index 0000000..96b8ec4
Binary files /dev/null and b/src/res/font.data differ
index b77f78e818fc020d0a568d2529d28acd714a830a..35d0bd3abdac3a024a2a282839745c2a31d45107 100644 (file)
@@ -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);
+}