diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e19bba..e140346 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,7 +57,6 @@ set(TOWNENGINE_SOURCE_FILES townengine/util.c townengine/util.h townengine/rendering.c townengine/rendering.h townengine/input/input.c townengine/input.h - townengine/text.c townengine/text.h townengine/camera.c townengine/camera.h townengine/textures/textures.c diff --git a/apps/testgame/scenes/title.c b/apps/testgame/scenes/title.c index e532dc4..02b8fc6 100644 --- a/apps/testgame/scenes/title.c +++ b/apps/testgame/scenes/title.c @@ -17,6 +17,34 @@ static void title_tick(struct state *state) { m_sprite("/assets/title.png", ((t_frect) { (RENDER_BASE_WIDTH / 2) - (320 / 2), 64, 320, 128 })); + + + /* draw the tick count as an example of dynamic text */ + size_t text_str_len = snprintf(NULL, 0, "%ld", state->ctx->tick_count) + 1; + char *text_str = cmalloc(text_str_len); + snprintf(text_str, text_str_len, "%ld", state->ctx->tick_count); + + const char *font = "fonts/kenney-pixel.ttf"; + int text_h = 32; + int text_w = get_text_width(text_str, text_h, font); + + push_rectangle( + (t_frect) { + .x = 0, + .y = 0, + .w = (float)text_w, + .h = (float)text_h, + }, + (t_color) { 0, 0, 0, 255 } + ); + push_text( + text_str, + (t_fvec2){ 0, 0 }, + text_h, + (t_color) { 255, 255, 255, 255 }, + font + ); + free(text_str); } diff --git a/data/fonts/kenney-pixel.ttf b/data/fonts/kenney-pixel.ttf new file mode 100755 index 0000000..0020733 Binary files /dev/null and b/data/fonts/kenney-pixel.ttf differ diff --git a/townengine/config.h b/townengine/config.h index 11d647f..273b8ba 100644 --- a/townengine/config.h +++ b/townengine/config.h @@ -1,6 +1,10 @@ #ifndef CONFIG_H #define CONFIG_H + +#include + + /* * this file is for configuration values which are to be set at * compile time. generally speaking, it's for things that would be unwise to @@ -28,6 +32,10 @@ #define AUDIO_FREQUENCY 48000 #define AUDIO_N_CHANNELS 2 +#define TEXT_FONT_TEXTURE_SIZE 1024 +#define TEXT_FONT_OVERSAMPLING 4 +#define TEXT_FONT_FILTERING GL_LINEAR + /* 1024 * 1024 */ /* #define UMKA_STACK_SIZE 1048576 */ diff --git a/townengine/context.h b/townengine/context.h index 3b078dd..f393c0f 100644 --- a/townengine/context.h +++ b/townengine/context.h @@ -2,6 +2,7 @@ #define CONTEXT_H +#include "rendering/internal_api.h" #include "textures/internal_api.h" #include "input.h" @@ -21,6 +22,7 @@ typedef struct context { struct primitive_2d *render_queue_2d; struct mesh_batch_item *uncolored_mesh_batches; + struct text_cache text_cache; struct audio_channel_item *audio_channels; SDL_AudioDeviceID audio_device; diff --git a/townengine/main.c b/townengine/main.c index a2d9840..48e6fcb 100644 --- a/townengine/main.c +++ b/townengine/main.c @@ -356,12 +356,14 @@ static bool initialize(void) { /* rendering */ /* these are dynamic arrays and will be allocated lazily by stb_ds */ ctx.render_queue_2d = NULL; + ctx.uncolored_mesh_batches = NULL; textures_cache_init(&ctx.texture_cache, ctx.window); if (TTF_Init() < 0) { CRY_SDL("SDL_ttf initialization failed."); goto fail; } + text_cache_init(&ctx.text_cache); /* input */ input_state_init(&ctx.input); @@ -391,7 +393,15 @@ static void clean_up(void) { input_state_deinit(&ctx.input); + /* if you're gonna remove this, it's also being done in rendering.c */ + for (size_t i = 0; i < arrlenu(ctx.render_queue_2d); ++i) { + if (ctx.render_queue_2d[i].type == PRIMITIVE_2D_TEXT) { + free(ctx.render_queue_2d[i].text.text); + } + } arrfree(ctx.render_queue_2d); + + text_cache_deinit(&ctx.text_cache); textures_cache_deinit(&ctx.texture_cache); SDL_DestroyMutex(game_object_mutex); diff --git a/townengine/rendering.c b/townengine/rendering.c index 6b2c969..f163c36 100644 --- a/townengine/rendering.c +++ b/townengine/rendering.c @@ -2,6 +2,7 @@ #include "rendering/sprites.h" #include "rendering/triangles.h" #include "rendering/circles.h" +#include "rendering/text.h" #include "textures/internal_api.h" #include "townengine/context.h" @@ -19,6 +20,14 @@ static t_matrix4 camera_look_at_matrix; void render_queue_clear(void) { + /* this doesn't even _deserve_ a TODO */ + /* if you're gonna remove it, this is also being done in main.c */ + for (size_t i = 0; i < arrlenu(ctx.render_queue_2d); ++i) { + if (ctx.render_queue_2d[i].type == PRIMITIVE_2D_TEXT) { + free(ctx.render_queue_2d[i].text.text); + } + } + /* since i don't intend to free the queues, */ /* it's faster and simpler to just "start over" */ /* and start overwriting the existing data */ @@ -103,6 +112,9 @@ static void render_2d(void) { case PRIMITIVE_2D_CIRCLE: render_circle(¤t->circle); break; + case PRIMITIVE_2D_TEXT: + render_text(¤t->text); + break; } } } diff --git a/townengine/rendering.h b/townengine/rendering.h index 2aa527e..1efeb18 100644 --- a/townengine/rendering.h +++ b/townengine/rendering.h @@ -34,6 +34,11 @@ void push_rectangle(t_frect rect, t_color color); /* pushes a filled circle onto the circle render queue */ void push_circle(t_fvec2 position, float radius, t_color color); +void text_cache_init(struct text_cache *cache); +void text_cache_deinit(struct text_cache *cache); +void push_text(char *string, t_fvec2 position, int height_px, t_color color, const char *font_path); +int get_text_width(char *string, int height_px, const char *font_path); + /* pushes a textured 3d triangle onto the render queue */ /* vertices are in absolute coordinates, relative to world origin */ /* texture coordinates are in pixels */ diff --git a/townengine/rendering/internal_api.h b/townengine/rendering/internal_api.h index 4049e52..f3afd13 100644 --- a/townengine/rendering/internal_api.h +++ b/townengine/rendering/internal_api.h @@ -34,10 +34,19 @@ struct circle_primitive { t_fvec2 position; }; +struct text_primitive { + t_color color; + t_fvec2 position; + char *text; + const char *font; + int height_px; +}; + enum primitive_2d_type { PRIMITIVE_2D_SPRITE, PRIMITIVE_2D_RECT, PRIMITIVE_2D_CIRCLE, + PRIMITIVE_2D_TEXT, }; struct primitive_2d { @@ -47,6 +56,7 @@ struct primitive_2d { struct sprite_primitive sprite; struct rect_primitive rect; struct circle_primitive circle; + struct text_primitive text; }; }; @@ -85,6 +95,10 @@ struct mesh_batch_item { struct mesh_batch value; }; +struct text_cache { + struct font_data **data; +}; + /* renders the background, then the primitives in all render queues */ void render(void); diff --git a/townengine/rendering/text.h b/townengine/rendering/text.h new file mode 100644 index 0000000..2f6d962 --- /dev/null +++ b/townengine/rendering/text.h @@ -0,0 +1,248 @@ +/* a rendering.c mixin */ +#ifndef TEXT_H +#define TEXT_H + + +#include "../util.h" +#include "townengine/config.h" +#include "townengine/context.h" + +#include +#include + + +#define ASCII_START 32 +#define ASCII_END 128 +#define NUM_DISPLAY_ASCII ((ASCII_END - ASCII_START) + 1) + + +struct font_data { + stbtt_packedchar char_data[NUM_DISPLAY_ASCII]; + stbtt_fontinfo info; + + const char *file_path; + unsigned char *file_bytes; + size_t file_bytes_len; + + GLuint texture; + + int height_px; + float scale_factor; + int ascent; + int descent; + int line_gap; +}; + + +static struct font_data *text_load_font_data(const char *path, int height_px) { + struct font_data *font_data = ccalloc(1, sizeof *font_data); + font_data->file_path = path; + font_data->height_px = height_px; + + unsigned char* bitmap = ccalloc(TEXT_FONT_TEXTURE_SIZE * TEXT_FONT_TEXTURE_SIZE, 1); + + { + unsigned char *buf = NULL; + int64_t buf_len = file_to_bytes(path, &buf); + stbtt_InitFont(&font_data->info, buf, stbtt_GetFontOffsetForIndex(buf, 0)); + + /* might as well get these now, for later */ + font_data->file_bytes = buf; + font_data->file_bytes_len = buf_len; + font_data->scale_factor = stbtt_ScaleForPixelHeight(&font_data->info, (float)height_px); + stbtt_GetFontVMetrics( + &font_data->info, + &font_data->ascent, + &font_data->descent, + &font_data->line_gap + ); + font_data->ascent = (int)((float)font_data->ascent * font_data->scale_factor); + font_data->descent = (int)((float)font_data->descent * font_data->scale_factor); + font_data->line_gap = (int)((float)font_data->line_gap * font_data->scale_factor); + + stbtt_pack_context pctx; + stbtt_PackBegin(&pctx, bitmap, TEXT_FONT_TEXTURE_SIZE, TEXT_FONT_TEXTURE_SIZE, 0, 1, NULL); + stbtt_PackSetOversampling(&pctx, TEXT_FONT_OVERSAMPLING, TEXT_FONT_OVERSAMPLING); + stbtt_PackFontRange(&pctx, buf, 0, (float)height_px, ASCII_START, NUM_DISPLAY_ASCII, font_data->char_data); + stbtt_PackEnd(&pctx); + } + + glGenTextures(1, &font_data->texture); + glBindTexture(GL_TEXTURE_2D, font_data->texture); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_ALPHA, + TEXT_FONT_TEXTURE_SIZE, + TEXT_FONT_TEXTURE_SIZE, + 0, + GL_ALPHA, + GL_UNSIGNED_BYTE, + bitmap + ); + free(bitmap); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, TEXT_FONT_FILTERING); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, TEXT_FONT_FILTERING); + glBindTexture(GL_TEXTURE_2D, 0); + + + return font_data; +} + + +static void text_destroy_font_data(struct font_data *font_data) { + free(font_data->file_bytes); + glDeleteTextures(1, &font_data->texture); + free(font_data); +} + + +static void text_draw_with(struct font_data* font_data, char* text, t_fvec2 position, t_color color) { + glBindTexture(GL_TEXTURE_2D, font_data->texture); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthFunc(GL_ALWAYS); + glDepthMask(GL_FALSE); + glDisable(GL_ALPHA_TEST); + + glColor4ub(color.r, color.g, color.b, color.a); + + glBegin(GL_QUADS); + for (const char *p = text; *p != '\0'; ++p) { + const char c = *p; + + /* outside the range of what we want to display */ + //if (c < ASCII_START || c > ASCII_END) + if (c < ASCII_START) + continue; + + /* stb_truetype.h conveniently provides everything we need to draw here! */ + stbtt_aligned_quad quad; + stbtt_GetPackedQuad( + font_data->char_data, + TEXT_FONT_TEXTURE_SIZE, + TEXT_FONT_TEXTURE_SIZE, + c - ASCII_START, + &position.x, + &position.y, + &quad, + true + ); + + /* have to do this so the "origin" is at the top left */ + /* maybe there's a better way, or maybe this isn't a good idea... */ + /* who knows */ + quad.y0 += (float)font_data->ascent; + quad.y1 += (float)font_data->ascent; + + /* TODO: you know... */ + glTexCoord2f(quad.s0, quad.t0); + glVertex2f(quad.x0, quad.y0); + glTexCoord2f(quad.s1, quad.t0); + glVertex2f(quad.x1, quad.y0); + glTexCoord2f(quad.s1, quad.t1); + glVertex2f(quad.x1, quad.y1); + glTexCoord2f(quad.s0, quad.t1); + glVertex2f(quad.x0, quad.y1); + } + glEnd(); + + glColor4ub(255, 255, 255, 255); + glBindTexture(GL_TEXTURE_2D, 0); +} + + +static void ensure_font_cache(const char *font_path, int height_px) { + /* HACK: stupid, bad, don't do this */ + bool is_cached = false; + for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) { + struct font_data *font_data = ctx.text_cache.data[i]; + if ((strcmp(font_path, font_data->file_path) == 0) && height_px == font_data->height_px) { + is_cached = true; + break; + } + } + if (!is_cached) { + struct font_data *new_font_data = text_load_font_data(font_path, height_px); + arrput(ctx.text_cache.data, new_font_data); + } +} + + +static struct font_data *get_font_data(const char *font_path, int height_px) { + struct font_data *font_data = NULL; + for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) { + font_data = ctx.text_cache.data[i]; + if ((strcmp(font_path, font_data->file_path) == 0) && height_px == font_data->height_px) { + break; + } + } + return font_data; +} + + +static void render_text(const struct text_primitive *text) { + struct font_data *font_data = get_font_data(text->font, text->height_px); + text_draw_with(font_data, text->text, text->position, text->color); +} + + +void text_cache_init(struct text_cache *cache) { + arrsetlen(cache->data, 0); +} + + +void text_cache_deinit(struct text_cache *cache) { + for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) { + text_destroy_font_data(ctx.text_cache.data[i]); + } + + arrfree(cache->data); +} + + +void push_text(char *string, t_fvec2 position, int height_px, t_color color, const char *font_path) { + ensure_font_cache(font_path, height_px); + + /* the string might not be around by the time it's used, so copy it */ + /* TODO: arena */ + /* NOTE: can we trust strlen? */ + char *dup_string = cmalloc(strlen(string) + 1); + strcpy(dup_string, string); + + struct text_primitive text = { + .color = color, + .position = position, + .text = dup_string, + .font = font_path, + .height_px = height_px, + }; + + struct primitive_2d primitive = { + .type = PRIMITIVE_2D_TEXT, + .text = text, + }; + + arrput(ctx.render_queue_2d, primitive); +} + + +int get_text_width(char *string, int height_px, const char *font_path) { + ensure_font_cache(font_path, height_px); + struct font_data *font_data = get_font_data(font_path, height_px); + + int length = 0; + for (const char *p = string; *p != '\0'; ++p) { + const char c = *p; + int advance_width = 0; + int left_side_bearing = 0; + stbtt_GetCodepointHMetrics(&font_data->info, (int)c, &advance_width, &left_side_bearing); + length += advance_width; + } + + return (int)((float)length * font_data->scale_factor); +} + + +#endif diff --git a/townengine/text.c b/townengine/text.c deleted file mode 100644 index 6c36ea0..0000000 --- a/townengine/text.c +++ /dev/null @@ -1,5 +0,0 @@ -#include "text.h" - -#include "SDL.h" -#include "SDL_ttf.h" - diff --git a/townengine/text.h b/townengine/text.h deleted file mode 100644 index 9b7467c..0000000 --- a/townengine/text.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef TEXT_H -#define TEXT_H - - -#include "util.h" - - -struct text { - char *text; - t_color color; - int ptsize; -}; - - -struct text_cache_item { - char *key; - struct text *value; -}; - - -struct text_cache { - struct text_cache_item *hash; -}; - - -#endif diff --git a/townengine/util.c b/townengine/util.c index a598679..56cce38 100644 --- a/townengine/util.c +++ b/townengine/util.c @@ -5,12 +5,12 @@ #include #define STB_DS_IMPLEMENTATION #define STBDS_ASSERT SDL_assert -#define STBDS_REALLOC(c,p,s) crealloc(p, s) -#define STBDS_FREE(c,p) free(p) #include #define STB_RECT_PACK_IMPLEMENTATION #define STBRP_ASSERT SDL_assert #include +#define STB_TRUETYPE_IMPLEMENTATION +#include #include #include