#define _CRT_SECURE_NO_WARNINGS #define STBI_FAILURE_USERMSG #define STB_IMAGE_IMPLEMENTATION #include #define SBTT_STATIC #define STB_TRUETYPE_IMPLEMENTATION #include //#define WT_RENDER_FONT_FILE #ifdef WT_RENDER_FONT_FILE #define STB_IMAGE_WRITE_IMPLEMENTATION #include #endif #include #include "Dengine/AssetManager.h" #include "Dengine/Debug.h" #include "Dengine/MemoryArena.h" #include "Dengine/OpenGL.h" #include "Dengine/Platform.h" /* ********************************* * Hash Table Operations ********************************* */ INTERNAL HashTableEntry *const getFreeHashSlot(HashTable *const table, MemoryArena_ *const arena, const char *const key) { u32 hashIndex = common_getHashIndex(key, table->size); HashTableEntry *result = &table->entries[hashIndex]; if (result->key) { while (result->next) { if (common_strcmp(result->key, key) == 0) { // TODO(doyle): Error hash item already exists ASSERT(INVALID_CODE_PATH); } result = result->next; } result->next = MEMORY_PUSH_STRUCT(arena, HashTableEntry); result = result->next; } /* Check if platform_mem_alloc failed(), otherwise always true if hash table * entries initialised, assign key to hash entry */ if (result) { // +1 Null terminator i32 keyLen = common_strlen(key) + 1; result->key = memory_pushBytes(arena, keyLen * sizeof(char)); common_strncpy(result->key, key, keyLen); } return result; } INTERNAL HashTableEntry *const getEntryFromHash(HashTable *const table, const char *const key) { u32 hashIndex = common_getHashIndex(key, table->size); HashTableEntry *result = &table->entries[hashIndex]; if (result->key) { while (result && common_strcmp(result->key, key) != 0) result = result->next; } return result; } /* ********************************* * Texture Operations ********************************* */ INTERNAL SubTexture *getFreeAtlasSubTexSlot(TexAtlas *const atlas, MemoryArena_ *const arena, const char *const key) { HashTableEntry *entry = getFreeHashSlot(&atlas->subTex, arena, key); if (entry) { entry->data = MEMORY_PUSH_STRUCT(arena, SubTexture); SubTexture *result = CAST(SubTexture *)entry->data; return result; } else { return NULL; } } const SubTexture asset_getAtlasSubTex(TexAtlas *const atlas, const char *const key) { HashTableEntry *entry = getEntryFromHash(&atlas->subTex, key); SubTexture result = {0}; if (entry) { result = *(CAST(SubTexture *) entry->data); return result; } DEBUG_LOG("asset_getAtlasSubTex() failed: Sub texture does not exist"); return result; } Texture *asset_getTex(AssetManager *const assetManager, const char *const key) { HashTableEntry *entry = getEntryFromHash(&assetManager->textures, key); Texture *result = NULL; if (entry) result = CAST(Texture *)entry->data; return result; } TexAtlas *asset_getFreeTexAtlasSlot(AssetManager *const assetManager, MemoryArena_ *arena, const char *const key, i32 numSubTex) { HashTableEntry *const entry = getFreeHashSlot(&assetManager->texAtlas, arena, key); if (entry) { entry->data = MEMORY_PUSH_STRUCT(arena, TexAtlas); TexAtlas *result = CAST(TexAtlas *) entry->data; if (result) { result->subTex.size = numSubTex; result->subTex.entries = memory_pushBytes(arena, numSubTex * sizeof(HashTableEntry)); if (!result->subTex.entries) { // TODO(doyle): Mem free /* PLATFORM_MEM_FREE(arena, result, sizeof(TexAtlas)); result = NULL; */ } } return result; } else { return NULL; } } TexAtlas *asset_getTexAtlas(AssetManager *const assetManager, const char *const key) { HashTableEntry *entry = getEntryFromHash(&assetManager->texAtlas, key); TexAtlas *result = NULL; if (entry) result = CAST(TexAtlas *)entry->data; return result; } Texture *asset_getFreeTexSlot(AssetManager *const assetManager, MemoryArena_ *const arena, const char *const key) { HashTableEntry *const entry = getFreeHashSlot(&assetManager->textures, arena, key); if (entry) { entry->data = MEMORY_PUSH_STRUCT(arena, Texture); Texture *result = CAST(Texture *) entry->data; return result; } else { return NULL; } } Texture *asset_loadTextureImage(AssetManager *assetManager, MemoryArena_ *arena, const char *const path, const char *const key) { /* Open the texture image */ i32 imgWidth, imgHeight, bytesPerPixel; stbi_set_flip_vertically_on_load(TRUE); u8 *image = stbi_load(path, &imgWidth, &imgHeight, &bytesPerPixel, 0); #ifdef DENGINE_DEBUG if (imgWidth != imgHeight) { printf( "asset_loadTextureImage() warning: Sprite sheet is not square: " "%dx%dpx\n", imgWidth, imgHeight); } #endif if (!image) { printf("stdbi_load() failed: %s\n", stbi_failure_reason()); return NULL; } Texture *result = asset_getFreeTexSlot(assetManager, arena, key); *result = texture_gen(CAST(GLuint)(imgWidth), CAST(GLuint)(imgHeight), CAST(GLint)(bytesPerPixel), image); GL_CHECK_ERROR(); stbi_image_free(image); return result; } /* ********************************* * Animation Asset Managing ********************************* */ INTERNAL Animation *getFreeAnimationSlot(AssetManager *const assetManager, MemoryArena_ *const arena, const char *const key) { HashTableEntry *entry = getFreeHashSlot(&assetManager->anims, arena, key); if (entry) { entry->data = MEMORY_PUSH_STRUCT(arena, Animation); Animation *result = CAST(Animation *) entry->data; return result; } else { return NULL; } } void asset_addAnimation(AssetManager *const assetManager, MemoryArena_ *const arena, const char *const animName, TexAtlas *const atlas, char **const subTextureNames, const i32 numSubTextures, const f32 frameDuration) { Animation *anim = getFreeAnimationSlot(assetManager, arena, animName); /* Use same animation ptr for name from entry key */ HashTableEntry *entry = getEntryFromHash(&assetManager->anims, animName); anim->name = entry->key; anim->atlas = atlas; anim->frameDuration = frameDuration; anim->numFrames = numSubTextures; anim->frameList = memory_pushBytes(arena, numSubTextures * sizeof(char *)); for (i32 i = 0; i < numSubTextures; i++) { anim->frameList[i] = subTextureNames[i]; } } Animation *asset_getAnim(AssetManager *const assetManager, const char *const key) { HashTableEntry *entry = getEntryFromHash(&assetManager->anims, key); Animation *result = NULL; if (entry) result = CAST(Animation *)entry->data; return result; } /* ********************************* * XML Operations ********************************* */ enum XmlTokenType { xmltokentype_unknown, xmltokentype_openArrow, xmltokentype_closeArrow, xmltokentype_name, xmltokentype_value, xmltokentype_equals, xmltokentype_quotes, xmltokentype_backslash, xmltokentype_count, }; typedef struct XmlToken { // TODO(doyle): Dynamic size string in tokens maybe. enum XmlTokenType type; char string[128]; i32 len; } XmlToken; INTERNAL XmlToken *const tokeniseXmlBuffer(MemoryArena_ *const arena, const char *const buffer, const i32 bufferSize, int *const numTokens) { XmlToken *xmlTokens = memory_pushBytes(arena, 8192 * sizeof(XmlToken)); i32 tokenIndex = 0; for (i32 i = 0; i < bufferSize; i++) { char c = (CAST(char *) buffer)[i]; switch (c) { case '<': case '>': case '=': case '/': { enum XmlTokenType type = xmltokentype_unknown; if (c == '<') { type = xmltokentype_openArrow; } else if (c == '>') { type = xmltokentype_closeArrow; } else if (c == '=') { type = xmltokentype_equals; } else { type = xmltokentype_backslash; } xmlTokens[tokenIndex].type = type; xmlTokens[tokenIndex].len = 1; tokenIndex++; break; } case '"': { xmlTokens[tokenIndex].type = xmltokentype_value; for (i32 j = i + 1; j < bufferSize; j++) { char c = (CAST(char *) buffer)[j]; if (c == '"') { break; } else { xmlTokens[tokenIndex].string[xmlTokens[tokenIndex].len++] = c; #ifdef DENGINE_DEBUG ASSERT(xmlTokens[tokenIndex].len < ARRAY_COUNT(xmlTokens[tokenIndex].string)); #endif } } // NOTE(doyle): +1 to skip the closing quotes i += (xmlTokens[tokenIndex].len + 1); tokenIndex++; break; } default: { if ((c >= 'a' && c <= 'z') || c >= 'A' && c <= 'Z') { xmlTokens[tokenIndex].type = xmltokentype_name; for (i32 j = i; j < bufferSize; j++) { char c = (CAST(char *) buffer)[j]; if (c == ' ' || c == '=' || c == '>' || c == '<' || c == '\\') { break; } else { xmlTokens[tokenIndex] .string[xmlTokens[tokenIndex].len++] = c; #ifdef DENGINE_DEBUG ASSERT(xmlTokens[tokenIndex].len < ARRAY_COUNT(xmlTokens[tokenIndex].string)); #endif } } i += xmlTokens[tokenIndex].len; tokenIndex++; } break; } } } // TODO(doyle): Dynamic token allocation *numTokens = 8192; return xmlTokens; } INTERNAL XmlNode *const buildXmlTree(MemoryArena_ *const arena, XmlToken *const xmlTokens, const i32 numTokens) { XmlNode *root = MEMORY_PUSH_STRUCT(arena, XmlNode); if (!root) return NULL; XmlNode *node = root; node->parent = node; // NOTE(doyle): Used for when closing a node with many children. We // automatically assign the next child after each child close within // a group. Hence on the last child, we open another node but the next // token indicates the group is closing- we need to set the last child's // next reference to NULL XmlNode *prevNode = NULL; for (i32 i = 0; i < numTokens; i++) { XmlToken *token = &xmlTokens[i]; switch (token->type) { case xmltokentype_openArrow: { /* Open arrows indicate closing parent node or new node name */ XmlToken *nextToken = &xmlTokens[++i]; if (nextToken->type == xmltokentype_backslash) { nextToken = &xmlTokens[++i]; if (common_strcmp(nextToken->string, node->parent->name) == 0) { node->parent->isClosed = TRUE; if (prevNode) { prevNode->next = NULL; } XmlNode *parent = node->parent; // TODO(doyle): Mem free //PLATFORM_MEM_FREE(arena, node, sizeof(XmlNode)); node = node->parent; } else { #ifdef DENGINE_DEBUG DEBUG_LOG( "Closing xml node name does not match parent name"); #endif } } else if (nextToken->type == xmltokentype_name) { node->name = nextToken->string; } else { #ifdef DENGINE_DEBUG DEBUG_LOG("Unexpected token type after open arrow"); #endif } token = nextToken; break; } case xmltokentype_name: { // TODO(doyle): Store latest attribute pointer so we aren't always // chasing the linked list each iteration. Do the same for children // node /* Xml Attributes are a linked list, get first free entry */ XmlAttribute *attribute = &node->attribute; if (attribute->init) { while (attribute->next) attribute = attribute->next; attribute->next = MEMORY_PUSH_STRUCT(arena, XmlAttribute); attribute = attribute->next; } /* Just plain text is a node attribute name */ attribute->name = token->string; /* Followed by the value */ token = &xmlTokens[++i]; attribute->value = token->string; attribute->init = TRUE; break; } case xmltokentype_closeArrow: { XmlToken prevToken = xmlTokens[i - 1]; prevNode = node; /* Closed node means we can return to parent */ if (prevToken.type == xmltokentype_backslash) { node->isClosed = TRUE; node = node->parent; } if (!node->isClosed) { /* Unclosed node means next fields will be children of node */ /* If the first child is free allocate, otherwise we have to * iterate through the child's next node(s) */ if (!node->child) { // TODO(doyle): Mem alloc error checking node->child = MEMORY_PUSH_STRUCT(arena, XmlNode); node->child->parent = node; node = node->child; } else { XmlNode *nodeToCheck = node->child; while (nodeToCheck->next) nodeToCheck = nodeToCheck->next; nodeToCheck->next = MEMORY_PUSH_STRUCT(arena, XmlNode); nodeToCheck->next->parent = node; node = nodeToCheck->next; } } break; } default: { break; } } } return root; } INTERNAL void parseXmlTreeToGame(AssetManager *assetManager, MemoryArena_ *arena, XmlNode *root) { XmlNode *node = root; while (node) { /* ********************************* * Branch on node names ********************************* */ if (common_strcmp(node->name, "TextureAtlas") == 0) { XmlNode *atlasXmlNode = node; TexAtlas *atlas = NULL; if (common_strcmp(node->attribute.name, "imagePath") == 0) { /* ********************************************** * Create a texture atlas with imageName as key ********************************************** */ char *imageName = atlasXmlNode->attribute.value; i32 numSubTex = 1024; atlas = asset_getFreeTexAtlasSlot(assetManager, arena, imageName, numSubTex); if (!atlas) { DEBUG_LOG( "parseXmlTreeToGame() failed: Could not get free atlas " "entry"); return; } /* ************************************************* * Load a texture to hash, with imageName as key ************************************************* */ char *dataDir = "data/textures/WorldTraveller/"; i32 dataDirLen = common_strlen(dataDir); i32 imageNameLen = common_strlen(imageName); i32 totalPathLen = (dataDirLen + imageNameLen) + 1; char *imagePath = memory_pushBytes(arena, totalPathLen * sizeof(char)); common_strncat(imagePath, dataDir, dataDirLen); common_strncat(imagePath, imageName, imageNameLen); Texture *tex = asset_loadTextureImage(assetManager, arena, imagePath, imageName); if (!tex) { DEBUG_LOG("parseXmlTreeToGame() failed: Could not load image"); // TODO(doyle): Mem free //PLATFORM_MEM_FREE(arena, imagePath, // totalPathLen * sizeof(char)); return; } // TODO(doyle): Mem free //PLATFORM_MEM_FREE(arena, imagePath, // totalPathLen * sizeof(char)); atlas->tex = tex; /* ************************************************* * Iterate over XML attributes ************************************************* */ XmlNode *atlasChildNode = atlasXmlNode->child; while (atlasChildNode) { if (common_strcmp(atlasChildNode->name, "SubTexture") == 0) { XmlAttribute *subTexAttrib = &atlasChildNode->attribute; char *key = NULL; SubTexture subTex = {0}; while (subTexAttrib) { // TODO(doyle): Work around for now in xml reading, // reading the last node closing node not being // merged to the parent if (!subTexAttrib->name) continue; if (common_strcmp(subTexAttrib->name, "name") == 0) { char *value = subTexAttrib->value; key = value; } else if (common_strcmp(subTexAttrib->name, "x") == 0) { char *value = subTexAttrib->value; i32 valueLen = common_strlen(value); i32 intValue = common_atoi(value, valueLen); subTex.rect.pos.x = CAST(f32) intValue; } else if (common_strcmp(subTexAttrib->name, "y") == 0) { char *value = subTexAttrib->value; i32 valueLen = common_strlen(value); i32 intValue = common_atoi(value, valueLen); subTex.rect.pos.y = CAST(f32) intValue; } else if (common_strcmp(subTexAttrib->name, "width") == 0) { char *value = subTexAttrib->value; i32 valueLen = common_strlen(value); i32 intValue = common_atoi(value, valueLen); subTex.rect.size.w = CAST(f32) intValue; } else if (common_strcmp(subTexAttrib->name, "height") == 0) { char *value = subTexAttrib->value; i32 valueLen = common_strlen(value); i32 intValue = common_atoi(value, valueLen); subTex.rect.size.h = CAST(f32) intValue; } else if (common_strcmp(subTexAttrib->name, "hand_offset_x") == 0) { char *value = subTexAttrib->value; i32 valueLen = common_strlen(value); i32 intValue = common_atoi(value, valueLen); subTex.offset.x = CAST(f32) intValue; } else if (common_strcmp(subTexAttrib->name, "hand_offset_y") == 0) { char *value = subTexAttrib->value; i32 valueLen = common_strlen(value); i32 intValue = common_atoi(value, valueLen); subTex.offset.y = CAST(f32) intValue; } else { #ifdef DENGINE_DEBUG DEBUG_LOG( "Unsupported xml attribute in SubTexture"); #endif } subTexAttrib = subTexAttrib->next; } // TODO(doyle): XML specifies 0,0 top left, we // prefer 0,0 bottom right, so offset by size since 0,0 // is top left and size creates a bounding box below it subTex.rect.pos.y = 1024 - subTex.rect.pos.y; subTex.rect.pos.y -= subTex.rect.size.h; subTex.offset.y = subTex.rect.size.h - subTex.offset.y; #ifdef DENGINE_DEBUG ASSERT(key); #endif SubTexture *subTexInHash = getFreeAtlasSubTexSlot(atlas, arena, key); *subTexInHash = subTex; } else { #ifdef DENGINE_DEBUG DEBUG_LOG("Unsupported xml node name not parsed"); #endif } atlasChildNode = atlasChildNode->next; } } else { #ifdef DENGINE_DEBUG DEBUG_LOG("Unsupported xml node"); #endif } } else { #ifdef DENGINE_DEBUG DEBUG_LOG("Unsupported xml node name not parsed"); #endif } node = node->next; } } INTERNAL void recursiveFreeXmlTree(MemoryArena_ *const arena, XmlNode *node) { if (!node) { return; } else { // NOTE(doyle): First attribute is statically allocated, only if there's // more attributes do we dynamically allocate XmlAttribute *attrib = node->attribute.next; while (attrib) { XmlAttribute *next = attrib->next; attrib->name = NULL; attrib->value = NULL; // TODO(doyle): Mem free // PLATFORM_MEM_FREE(arena, attrib, sizeof(XmlAttribute)); attrib = next; } recursiveFreeXmlTree(arena, node->child); recursiveFreeXmlTree(arena, node->next); node->name = NULL; node->isClosed = FALSE; // TODO(doyle): Mem free // PLATFORM_MEM_FREE(arena, node, sizeof(XmlNode)); node = NULL; } } INTERNAL void freeXmlData(MemoryArena_ *const arena, XmlToken *tokens, const i32 numTokens, XmlNode *tree) { if (tree) recursiveFreeXmlTree(arena, tree); // TODO(doyle): Mem free // if (tokens) PLATFORM_MEM_FREE(arena, tokens, numTokens * sizeof(XmlToken)); } /* ********************************* * Everything else ********************************* */ const i32 asset_loadXmlFile(AssetManager *const assetManager, MemoryArena_ *const arena, const PlatformFileRead *const fileRead) { i32 result = 0; /* Tokenise buffer */ i32 numTokens = 0; XmlToken *xmlTokens = tokeniseXmlBuffer(arena, fileRead->buffer, fileRead->size, &numTokens); /* Build XML tree from tokens */ XmlNode *xmlTree = buildXmlTree(arena, xmlTokens, numTokens); if (xmlTree) { /* Parse XML tree to game structures */ parseXmlTreeToGame(assetManager, arena, xmlTree); } else { result = -1; } /* Free data */ freeXmlData(arena, xmlTokens, numTokens, xmlTree); return result; } AudioVorbis *const asset_getVorbis(AssetManager *const assetManager, const char *const key) { HashTableEntry *entry = getEntryFromHash(&assetManager->audio, key); AudioVorbis *result = NULL; if (entry) result = CAST(AudioVorbis *)entry->data; return result; } const i32 asset_loadVorbis(AssetManager *assetManager, MemoryArena_ *arena, const char *const path, const char *const key) { HashTableEntry *entry = getFreeHashSlot(&assetManager->audio, arena, key); if (!entry) return -1; // TODO(doyle): Remember to free vorbis file if we remove from memory PlatformFileRead fileRead = {0}; platform_readFileToBuffer(arena, path, &fileRead); entry->data = MEMORY_PUSH_STRUCT(arena, AudioVorbis); i32 error; AudioVorbis *audio = CAST(AudioVorbis *) entry->data; audio->file = stb_vorbis_open_memory(fileRead.buffer, fileRead.size, &error, NULL); if (!audio->file) { printf("stb_vorbis_open_memory() failed: Error code %d\n", error); // TODO(doyle): Mem free // platform_closeFileRead(arena, &fileRead); stb_vorbis_close(audio->file); return 0; } audio->name = entry->key; audio->info = stb_vorbis_get_info(audio->file); audio->lengthInSamples = stb_vorbis_stream_length_in_samples(audio->file); audio->lengthInSeconds = stb_vorbis_stream_length_in_seconds(audio->file); audio->data = CAST(u8 *) fileRead.buffer; audio->size = fileRead.size; return 0; } INTERNAL GLuint createShaderFromPath(MemoryArena_ *arena, const char *const path, GLuint shadertype) { PlatformFileRead file = {0}; // TODO(doyle): Revise platform reads i32 status = platform_readFileToBuffer(arena, path, &file); if (status) return status; const GLchar *source = CAST(char *)file.buffer; GLuint result = glCreateShader(shadertype); glShaderSource(result, 1, &source, NULL); glCompileShader(result); GLint success; GLchar infoLog[512]; glGetShaderiv(result, GL_COMPILE_STATUS, &success); if (!success) { glGetShaderInfoLog(result, 512, NULL, infoLog); printf("glCompileShader() failed: %s\n", infoLog); } platform_closeFileRead(arena, &file); return result; } INTERNAL u32 shaderLoadProgram(const GLuint vertexShader, const GLuint fragmentShader) { u32 result = glCreateProgram(); glAttachShader(result, vertexShader); glAttachShader(result, fragmentShader); glLinkProgram(result); GL_CHECK_ERROR(); glDeleteShader(fragmentShader); glDeleteShader(vertexShader); GL_CHECK_ERROR(); GLint success; GLchar infoLog[512]; glGetProgramiv(result, GL_LINK_STATUS, &success); GL_CHECK_ERROR(); if (result == 0) { glGetProgramInfoLog(result, 512, NULL, infoLog); printf("glLinkProgram failed: %s\n", infoLog); ASSERT(TRUE); } GL_CHECK_ERROR(); return result; } u32 asset_getShader(AssetManager *assetManager, const enum ShaderList type) { if (type < shaderlist_count) return assetManager->shaders[type]; #ifdef DENGINE_DEBUG ASSERT(INVALID_CODE_PATH); #endif return -1; } const i32 asset_loadShaderFiles(AssetManager *assetManager, MemoryArena_ *arena, const char *const vertexPath, const char *const fragmentPath, const enum ShaderList type) { GLuint vertexShader = createShaderFromPath(arena, vertexPath, GL_VERTEX_SHADER); GLuint fragmentShader = createShaderFromPath(arena, fragmentPath, GL_FRAGMENT_SHADER); u32 shaderId = shaderLoadProgram(vertexShader, fragmentShader); if (shaderId == 0) return -1; assetManager->shaders[type] = shaderId; return 0; } /* Individual glyph bitmap generated from STB used for creating a font sheet */ typedef struct GlyphBitmap { v2 dimensions; u32 *pixels; i32 codepoint; } GlyphBitmap; const i32 asset_loadTTFont(AssetManager *assetManager, MemoryArena_ *arena, const char *filePath) { PlatformFileRead fontFileRead = {0}; i32 result = platform_readFileToBuffer(arena, filePath, &fontFileRead); if (result) return result; stbtt_fontinfo fontInfo = {0}; stbtt_InitFont(&fontInfo, fontFileRead.buffer, stbtt_GetFontOffsetForIndex(fontFileRead.buffer, 0)); /* **************************************** * Initialise assetmanager font reference **************************************** */ Font *font = &assetManager->font; font->codepointRange = V2i(32, 127); v2 codepointRange = font->codepointRange; const i32 numGlyphs = CAST(i32)(codepointRange.y - codepointRange.x); GlyphBitmap *glyphBitmaps = memory_pushBytes(arena, numGlyphs * sizeof(GlyphBitmap)); v2 largestGlyphDimension = V2(0, 0); const f32 targetFontHeight = 15.0f; f32 scaleY = stbtt_ScaleForPixelHeight(&fontInfo, targetFontHeight); i32 ascent, descent, lineGap; stbtt_GetFontVMetrics(&fontInfo, &ascent, &descent, &lineGap); ascent = CAST(i32)(ascent * scaleY); descent = CAST(i32)(descent * scaleY); lineGap = CAST(i32)(lineGap * scaleY); font->metrics = CAST(FontMetrics){ascent, descent, lineGap}; font->charMetrics = memory_pushBytes(arena, numGlyphs * sizeof(CharMetrics)); /* ************************************************************ * Use STB_TrueType to generate a series of bitmap characters ************************************************************ */ i32 glyphIndex = 0; for (i32 codepoint = CAST(i32) codepointRange.x; codepoint < CAST(i32) codepointRange.y; codepoint++) { // NOTE(doyle): ScaleX if not specified, is then calculated based on the // ScaleY component i32 width, height, xOffset, yOffset; u8 *const monoBitmap = stbtt_GetCodepointBitmap(&fontInfo, 0, scaleY, codepoint, &width, &height, &xOffset, &yOffset); u8 *source = monoBitmap; u32 *colorBitmap = memory_pushBytes(arena, width * height * sizeof(u32)); u32 *dest = colorBitmap; // NOTE(doyle): STB generates 1 byte per pixel bitmaps, we use 4bpp, so // duplicate the alpha byte (essentially) for all RGBA components for (i32 y = 0; y < height; y++) { for (i32 x = 0; x < width; x++) { u8 monoByte = *source++; *dest++ = (monoByte << 24) | (monoByte << 16) | (monoByte << 8) | (monoByte << 0); } } /* Get individual character metrics */ i32 advance, leftSideBearing; stbtt_GetCodepointHMetrics(&fontInfo, codepoint, &advance, &leftSideBearing); advance = CAST(i32)(advance * scaleY); leftSideBearing = CAST(i32)(leftSideBearing * scaleY); font->charMetrics[glyphIndex] = CAST(CharMetrics){advance, leftSideBearing, NULL, V2i(xOffset, yOffset), V2i(width, height)}; /* Store bitmap into intermediate storage */ stbtt_FreeBitmap(monoBitmap, NULL); // TODO(doyle): Dimensions is used twice in font->trueSize and this glyphBitmaps[glyphIndex].dimensions = V2i(width, height); glyphBitmaps[glyphIndex].codepoint = codepoint; glyphBitmaps[glyphIndex++].pixels = colorBitmap; if (height > CAST(f32)largestGlyphDimension.h) largestGlyphDimension.h = CAST(f32)height; if (width > CAST(f32)largestGlyphDimension.w) largestGlyphDimension.w = CAST(f32)width; #ifdef DENGINE_DEBUG if ((largestGlyphDimension.h - CAST(i32)targetFontHeight) >= 50) { printf( "asset_loadTTFont() warning: The loaded font file has a glyph " "considerably larger than our target .. font packing is " "unoptimal\n"); } #endif } /* * NOTE(doyle): Use rasterised TTF bitmap-characters combine them all into * one bitmap as an atlas. We determine how many glyphs we can fit per row * by determining the largest glyph size we have. * * For the amount of glyphs we fit per row, we iterate through them and * write each row of the glyph adjacent to the next until we finish writing * all pixels for the glyphs, then move onto the next set of glyphs. */ font->maxSize = largestGlyphDimension; i32 glyphsPerRow = (MAX_TEXTURE_SIZE / CAST(i32)font->maxSize.w); #ifdef DENGINE_DEBUG i32 glyphsPerCol = MAX_TEXTURE_SIZE / CAST(i32)font->maxSize.h; if ((glyphsPerRow * glyphsPerCol) <= numGlyphs) { printf( "asset_loadTTFont() warning: The target font height creates a " "glyph sheet that exceeds the available space!"); ASSERT(INVALID_CODE_PATH); } #endif i32 bitmapSize = SQUARED(TARGET_TEXTURE_SIZE) * TARGET_BYTES_PER_PIXEL; u32 *fontBitmap = memory_pushBytes(arena, bitmapSize * sizeof(u32)); const i32 pitch = MAX_TEXTURE_SIZE * TARGET_BYTES_PER_PIXEL; // Check value to determine when a row of glyphs is completely printed i32 verticalPixelsBlitted = 0; i32 startingGlyphIndex = 0; i32 glyphsRemaining = numGlyphs; i32 glyphsOnCurrRow = (glyphsPerRow < glyphsRemaining) ? glyphsPerRow : glyphsRemaining; // TODO(doyle): We copy over the bitmap direct to the font sheet, should we // align the baselines up so we don't need to do baseline adjusting at // render? char charToEncode = CAST(char)codepointRange.x; i32 numSubTex = numGlyphs; TexAtlas *fontAtlas = asset_getFreeTexAtlasSlot(assetManager, arena, "font", numSubTex); /* ********************************************************* * Load individual glyph bitmap data into one font bitmap ********************************************************* */ for (i32 row = 0; row < MAX_TEXTURE_SIZE; row++) { u32 *destRow = fontBitmap + (row * MAX_TEXTURE_SIZE); for (i32 glyphIndex = 0; glyphIndex < glyphsOnCurrRow; glyphIndex++) { i32 activeGlyphIndex = startingGlyphIndex + glyphIndex; GlyphBitmap activeGlyph = glyphBitmaps[activeGlyphIndex]; /* Store the location of glyph into atlas */ if (verticalPixelsBlitted == 0) { v2 origin = V2(CAST(f32)(glyphIndex * font->maxSize.w), CAST(f32) row); // NOTE(doyle): Since charToEncode starts from 0 and we record // all ascii characters, charToEncode represents the character // 1:1 const char key[2] = {charToEncode, 0}; SubTexture *subTex = getFreeAtlasSubTexSlot(fontAtlas, arena, key); subTex->rect = CAST(Rect){origin, font->maxSize}; charToEncode++; } /* Copy over exactly one row of pixels */ i32 numPixelsToPad = CAST(i32)font->maxSize.w; if (verticalPixelsBlitted < activeGlyph.dimensions.h) { const i32 srcPitch = CAST(i32) activeGlyph.dimensions.w * verticalPixelsBlitted; const u32 *src = activeGlyph.pixels + srcPitch; const i32 numPixelsToCopy = CAST(i32)activeGlyph.dimensions.w; for (i32 count = 0; count < numPixelsToCopy; count++) *destRow++ = *src++; /* * NOTE(doyle): If the glyph is smaller than largest glyph * available size, don't advance src pointer any further * (NULL/mixes up rows), instead just advance the final bitmap * pointer by the remaining distance */ numPixelsToPad = CAST(i32)(font->maxSize.w - activeGlyph.dimensions.w); } destRow += numPixelsToPad; } /* A row of glyphs has been fully formed on the atlas */ if (verticalPixelsBlitted++ >= font->maxSize.h) { verticalPixelsBlitted = 0; startingGlyphIndex += glyphsPerRow; glyphsRemaining -= glyphsPerRow; if (glyphsRemaining <= 0) break; else if (glyphsRemaining <= glyphsPerRow) { // NOTE(doyle): This allows us to modify the glyph iterator to // prevent over-running of the available glyphs glyphsOnCurrRow = glyphsRemaining; } } } /* ******************************************* * Generate and store font bitmap to assets ******************************************* */ Texture *tex = asset_getFreeTexSlot(assetManager, arena, "font"); *tex = texture_gen(MAX_TEXTURE_SIZE, MAX_TEXTURE_SIZE, 4, CAST(u8 *) fontBitmap); #ifdef WT_RENDER_FONT_FILE /* save out a 4 channel image */ stbi_write_png("out.png", MAX_TEXTURE_SIZE, MAX_TEXTURE_SIZE, 4, fontBitmap, MAX_TEXTURE_SIZE * 4); #endif // TODO(doyle): Mem free // PLATFORM_MEM_FREE(arena, fontBitmap, bitmapSize); fontAtlas->tex = tex; font->atlas = fontAtlas; // NOTE(doyle): Formula derived from STB Font font->verticalSpacing = font->metrics.ascent - font->metrics.descent + font->metrics.lineGap; for (i32 i = 0; i < numGlyphs; i++) { i32 glyphBitmapSizeInBytes = CAST(i32) glyphBitmaps[i].dimensions.w * CAST(i32) glyphBitmaps[i].dimensions.h * sizeof(u32); // TODO(doyle): Mem free // PLATFORM_MEM_FREE(arena, glyphBitmaps[i].pixels, glyphBitmapSizeInBytes); } // TODO(doyle): Mem free // PLATFORM_MEM_FREE(arena, glyphBitmaps, numGlyphs * sizeof(GlyphBitmap)); platform_closeFileRead(arena, &fontFileRead); return 0; } const v2 asset_stringDimInPixels(const Font *const font, const char *const string) { v2 stringDim = V2(0, 0); for (i32 i = 0; i < common_strlen(string); i++) { i32 codepoint = string[i]; #ifdef DENGINE_DEBUG ASSERT(codepoint >= font->codepointRange.x && codepoint <= font->codepointRange.y) #endif i32 relativeIndex = CAST(i32)(codepoint - font->codepointRange.x); v2 charDim = font->charMetrics[relativeIndex].trueSize; stringDim.x += charDim.x; stringDim.y = (charDim.y > stringDim.y) ? charDim.y : stringDim.y; } return stringDim; } void asset_unitTest(MemoryArena_ *arena) { PlatformFileRead xmlFileRead = {0}; i32 result = platform_readFileToBuffer( arena, "data/textures/WorldTraveller/ClaudeSprite.xml", &xmlFileRead); if (result) { DEBUG_LOG( "unitTest() error: Could not load XML file for memory free test"); } else { /* Tokenise buffer */ i32 numTokens = 0; XmlToken *xmlTokens = tokeniseXmlBuffer(arena, xmlFileRead.buffer, xmlFileRead.size, &numTokens); /* Build XML tree from tokens */ XmlNode *xmlTree = buildXmlTree(arena, xmlTokens, numTokens); freeXmlData(arena, xmlTokens, numTokens, xmlTree); } }