#include "fonts.h" #include #include #include #include #include #include #include #include "constants.h" #include "engine.h" #include "internal.h" #include "shader.h" #include "unicode.h" namespace kek { Shader *fontShader = nullptr; TextObject::TextObject(std::shared_ptr font, std::string text) { assert(text.length() > 0); this->font = font; this->text = text; loadChars(); } TextObject::~TextObject() { destroy(); } void TextObject::destroy() { for(auto o : blocks) { glDeleteVertexArrays(1, &o.second.vao); glDeleteBuffers(1, &o.second.vbo); } blocks.clear(); } TextObject::TextObject(TextObject &&other) : blocks(other.blocks) { other.blocks.clear(); } TextObject &TextObject::operator=(TextObject &&other) { if(this != &other) { destroy(); std::swap(blocks, other.blocks); } return *this; } std::shared_ptr TextObject::getFont() { return font; } void TextObject::setText(std::string text) { this->text = text; loadChars(); } std::string TextObject::getText() { return text; } TextMetrics TextObject::getMetrics(unsigned int offset, unsigned int length, int sizePixels) { TextMetrics metrics = measureChars(Unicode::convertStdToU32(text), offset, length, nullptr); float sizeRatio = (float) sizePixels / KEK_FONT_RESOLUTION; return TextMetrics((int) (sizeRatio * metrics.offsetX), (int) (sizeRatio * metrics.offsetY), (int) (sizeRatio * metrics.width), (int) (sizeRatio * metrics.height)); } TextMetrics TextObject::getMetrics(int sizePixels) { float sizeRatio = (float) sizePixels / KEK_FONT_RESOLUTION; return TextMetrics((int) (sizeRatio * unscaledMetrics.offsetX), (int) (sizeRatio * unscaledMetrics.offsetY), (int) (sizeRatio * unscaledMetrics.width), (int) (sizeRatio * unscaledMetrics.height)); } void TextObject::allocateBuffer(TextBlock *block, int numChars) { unsigned int targetSize = (numChars + KEK_TEXT_BLOCK_SIZE - 1) / KEK_TEXT_BLOCK_SIZE * KEK_TEXT_BLOCK_SIZE; // ceil(numChars / KEK_TEXT_BLOCK_SIZE) * KEK_TEXT_BLOCK_SIZE block->bufferSize = targetSize; glGenVertexArrays(1, &block->vao); glGenBuffers(1, &block->vbo); glBindVertexArray(block->vao); glBindBuffer(GL_ARRAY_BUFFER, block->vbo); glBufferData(GL_ARRAY_BUFFER, targetSize * sizeof(RenderChar), nullptr, GL_DYNAMIC_DRAW); // 6 verts/char, 4 floats/vertex glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *) (2 * sizeof(float))); glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); } TextMetrics TextObject::measureChars(std::u32string str, unsigned int offset, unsigned int length, std::map> *chars) { // TODO: returned metrics are incorrect when there are multiple lines of text if(offset != 0 || length != str.length()) { length = std::min(length, (unsigned int) (str.length() - offset)); str = str.substr(offset, length); } float x = 0, y = 0; int lineHeight = font->getDefaultMetrics().lineHeight; int offX = -1, offY = 0, maxH = 0; for(char32_t codepoint : str) { if(codepoint == U'\n') { y += lineHeight; x = 0; continue; } unsigned int charBlock = codepoint >> KEK_FONT_BITMAP_CHAR_BITS; CharacterBlock block = font->getCharacterBlock(charBlock); Character ch = block.characters[codepoint & KEK_FONT_BITMAP_CHAR_MASK]; float xPos = x + ch.bearing.x; float yPos = y - ch.bearing.y; if(offX == -1) offX = ch.bearing.x; if(ch.bearing.y > offY) offY = ch.bearing.y; float w = ch.size.x; float h = ch.size.y; if(h > maxH) maxH = h; if(chars != nullptr) { auto &_chars = *chars; float texL = (ch.textureX) / (float) KEK_FONT_BITMAP_WIDTH; float texR = (ch.textureX + ch.size.x) / (float) KEK_FONT_BITMAP_WIDTH; float texU = (ch.textureY) / (float) KEK_FONT_BITMAP_HEIGHT; float texD = (ch.textureY + ch.size.y) / (float) KEK_FONT_BITMAP_HEIGHT; RenderChar rCh = RenderChar(xPos, yPos, w, h, texL, texR, texU, texD); auto charsForBlock = _chars.find(charBlock); if(charsForBlock != _chars.end()) { charsForBlock->second.push_back(rCh); } else { std::vector chs; chs.push_back(rCh); _chars[charBlock] = chs; } } x += (ch.advance >> 6); // == ch.advance / 64 } return TextMetrics(offX, offY, x, maxH); } void TextObject::loadChars() { std::map> chars; std::u32string str = Unicode::convertStdToU32(text); TextMetrics metrics = measureChars(str, 0, str.length(), &chars); this->unscaledMetrics = metrics; auto it = chars.begin(); while(it != chars.end()) { auto oldTextBlock = blocks.find(it->first); int textLength = it->second.size(); if(oldTextBlock != blocks.end()) { // Update buffer TextBlock *oldBlock = &oldTextBlock->second; if(textLength != 0 && (textLength > (int) oldBlock->bufferSize || textLength < (int) oldBlock->bufferSize - 2 * KEK_TEXT_BLOCK_SIZE)) { // String is either too large for the current buffer or at least one block smaller, reallocate // TODO: no need to delete, just call glBufferData glDeleteVertexArrays(1, &oldBlock->vao); glDeleteBuffers(1, &oldBlock->vbo); allocateBuffer(oldBlock, text.length()); } oldBlock->length = textLength; glNamedBufferSubData(oldBlock->vbo, 0, textLength * sizeof(RenderChar), &it->second[0]); } else { // Allocate Buffer TextBlock block; allocateBuffer(&block, textLength); block.length = textLength; block.characterBlock = it->first; glNamedBufferSubData(block.vbo, 0, textLength * sizeof(RenderChar), &it->second[0]); blocks[it->first] = block; } it++; } auto it2 = blocks.begin(); while(it2 != blocks.end()) { if(chars.find(it2->first) == chars.end()) { it2 = blocks.erase(it2); } else { it2++; } } } Font::Font(std::string path) { glPixelStorei(GL_UNPACK_ALIGNMENT, 1); fileMemory = Resource::loadResource(path); if(!fileMemory) { std::cerr << "Failed to load font: " << path << std::endl; throw std::exception(); } if(FT_Error err = FT_New_Memory_Face(kekData.freetype, (FT_Byte *) fileMemory->buffer, fileMemory->length, 0, &face)) { std::cout << "Failed to load font: " << FT_Error_String(err) << std::endl; return; } FT_Set_Pixel_Sizes(face, 0, KEK_FONT_RESOLUTION); FT_Select_Charmap(face, FT_Encoding::FT_ENCODING_UNICODE); lineHeight = face->size->metrics.height; ascender = face->size->metrics.ascender; descender = face->size->metrics.descender; if(!fontShader) fontShader = new Shader("shader/text/vertex.glsl", "shader/text/fragment.glsl"); } Font::~Font() { FT_Done_Face(face); delete fileMemory; } CharacterBlock Font::getCharacterBlock(unsigned int block) { auto b = characterBlocks.find(block); if(b == characterBlocks.end()) return generateCharacterBlock(block); return b->second; } CharacterBlock Font::generateCharacterBlock(unsigned int block) { CharacterBlock chBlock; unsigned char *textureBytes = new unsigned char[KEK_FONT_BITMAP_WIDTH * KEK_FONT_BITMAP_HEIGHT]; // grayscale (1 byte per pixel) texture memset(textureBytes, 0, KEK_FONT_BITMAP_WIDTH * KEK_FONT_BITMAP_HEIGHT); for(unsigned int c = 0; c < KEK_FONT_BITMAP_WIDTH_BLOCKS * KEK_FONT_BITMAP_HEIGHT_BLOCKS; c++) { if(FT_Error err = FT_Load_Char(face, (block << KEK_FONT_BITMAP_CHAR_BITS) + c, FT_LOAD_RENDER)) { std::cout << "Failed to load glyph: " << FT_Error_String(err) << std::endl; continue; } int col = c % KEK_FONT_BITMAP_WIDTH_BLOCKS; int row = c / KEK_FONT_BITMAP_WIDTH_BLOCKS; // TODO: handle wide characters more gracefully for(unsigned int r = 0; r < std::min((unsigned int) KEK_FONT_RESOLUTION, face->glyph->bitmap.rows); r++) { size_t offset = row * KEK_FONT_RESOLUTION * KEK_FONT_BITMAP_WIDTH + col * KEK_FONT_RESOLUTION + r * KEK_FONT_BITMAP_WIDTH; size_t len = glm::min((unsigned int) KEK_FONT_RESOLUTION, face->glyph->bitmap.width); memcpy(textureBytes + offset, face->glyph->bitmap.buffer + r * face->glyph->bitmap.width, len); } Character ch; ch.textureX = col * KEK_FONT_RESOLUTION; ch.textureY = row * KEK_FONT_RESOLUTION; ch.size = glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows); ch.bearing = glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top); ch.advance = face->glyph->advance.x; chBlock.characters[c] = ch; } // stbi_write_png(("/home/mr/Desktop/bitmap_" + std::to_string(block) + ".png").c_str(), KEK_FONT_BITMAP_WIDTH, KEK_FONT_BITMAP_HEIGHT, 1, textureBytes, KEK_FONT_BITMAP_WIDTH); glGenTextures(1, &chBlock.textureID); glBindTexture(GL_TEXTURE_2D, chBlock.textureID); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, KEK_FONT_BITMAP_WIDTH, KEK_FONT_BITMAP_HEIGHT, 0, GL_RED, GL_UNSIGNED_BYTE, textureBytes); glGenerateMipmap(GL_TEXTURE_2D); delete[] textureBytes; characterBlocks[block] = chBlock; return chBlock; } FontMetrics Font::getDefaultMetrics() { return FontMetrics(lineHeight >> 6, ascender >> 6, descender >> 6); } FontMetrics Font::getMetrics(int sizePixels) { float sizeRatio = (float) sizePixels / KEK_FONT_RESOLUTION; return FontMetrics(((int) (sizeRatio * lineHeight)) >> 6, ((int) (sizeRatio * ascender)) >> 6, ((int) (sizeRatio * descender)) >> 6); } void Font::drawText(TextObject *textObject, int x, int y, int sizePixels, Color color) { glm::mat4 projection = glm::ortho(0.0f, (float) kekData.screenWidth, (float) kekData.screenHeight, 0.0f); Font::drawText(textObject, projection, x, y, sizePixels, color); } void Font::drawText(TextObject *textObject, glm::mat4 projection, int x, int y, int sizePixels, Color color) { fontShader->use(); for(auto b : textObject->blocks) { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, getCharacterBlock(b.second.characterBlock).textureID); glUniform4fv(glGetUniformLocation(fontShader->id, "textColor"), 1, color.valuePointer()); glUniform1i(glGetUniformLocation(fontShader->id, "charTexture"), 0); glBindVertexArray(b.second.vao); glUniformMatrix4fv(glGetUniformLocation(fontShader->id, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); glUniform2fv(glGetUniformLocation(fontShader->id, "offset"), 1, glm::value_ptr(glm::vec2(x, y))); glUniform1f(glGetUniformLocation(fontShader->id, "scale"), (float) sizePixels / KEK_FONT_RESOLUTION); glDrawArrays(GL_TRIANGLES, 0, 6 * b.second.length); } } void Font::drawTextFromOrigin(TextObject *textObject, int x, int y, int sizePixels, Color color) { TextMetrics metrics = textObject->getMetrics(sizePixels); Font::drawText(textObject, x - metrics.offsetX, y + metrics.offsetY, sizePixels, color); } void Font::drawTextFromOrigin(TextObject *textObject, glm::mat4 projection, int x, int y, int sizePixels, Color color) { TextMetrics metrics = textObject->getMetrics(sizePixels); Font::drawText(textObject, projection, x - metrics.offsetX, y + metrics.offsetY, sizePixels, color); } void Font::drawTextCentered(TextObject *textObject, int x, int y, int sizePixels, Color color) { TextMetrics metrics = textObject->getMetrics(sizePixels); Font::drawText(textObject, x - metrics.offsetX + metrics.width / 2, y + metrics.offsetY - metrics.height / 2, sizePixels, color); } void Font::drawTextCentered(TextObject *textObject, glm::mat4 projection, int x, int y, int sizePixels, Color color) { TextMetrics metrics = textObject->getMetrics(sizePixels); Font::drawText(textObject, projection, x - metrics.offsetX + metrics.width / 2, y + metrics.offsetY - metrics.height / 2, sizePixels, color); } std::shared_ptr Font::load(std::string fontPath) { std::shared_ptr ret = nullptr; auto iter = kekData.loadedFonts.find(fontPath); if(iter != kekData.loadedFonts.end()) ret = iter->second.lock(); if(!ret) { ret = std::make_shared(fontPath); kekData.loadedFonts.emplace(fontPath, ret); } return ret; } }