From ff1ad05ba47cb508206b078fc0174799facc0c46 Mon Sep 17 00:00:00 2001 From: MrLetsplay2003 Date: Sun, 16 Oct 2022 14:30:12 +0200 Subject: [PATCH] Key mappings, basic text rendering --- CMakeLists.txt | 10 +- dependencies/microtar | 2 +- src/kekengine/cpp/defaults.cpp | 53 +++ src/kekengine/cpp/engine.cpp | 89 ++--- src/kekengine/cpp/fonts.cpp | 344 ++++++++++++++++++ src/kekengine/cpp/input.cpp | 30 ++ src/kekengine/include/color.h | 38 ++ src/kekengine/include/constants.h | 12 + src/kekengine/include/defaults.h | 7 + src/kekengine/include/fonts.h | 159 ++++++++ src/kekengine/include/input.h | 16 + src/kekengine/include/internal.h | 4 + src/kekengine/include/utils.h | 2 + .../res/font/MaredivRegular-yeg3.ttf | Bin 0 -> 36756 bytes src/kekengine/res/shader/text/fragment.glsl | 10 + src/kekengine/res/shader/text/vertex.glsl | 13 + 16 files changed, 724 insertions(+), 65 deletions(-) create mode 100644 src/kekengine/cpp/defaults.cpp create mode 100644 src/kekengine/cpp/fonts.cpp create mode 100644 src/kekengine/include/color.h create mode 100644 src/kekengine/include/defaults.h create mode 100644 src/kekengine/include/fonts.h create mode 100644 src/kekengine/res/font/MaredivRegular-yeg3.ttf create mode 100644 src/kekengine/res/shader/text/fragment.glsl create mode 100644 src/kekengine/res/shader/text/vertex.glsl diff --git a/CMakeLists.txt b/CMakeLists.txt index acffba6..14b7a40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,8 +59,13 @@ add_library(kekengine OBJECT ${KEKENGINE_SOURCE_FILES}) set_property(TARGET kekengine PROPERTY POSITION_INDEPENDENT_CODE 1) add_dependencies(kekengine kekengine_res) -set_property(TARGET microtar PROPERTY POSITION_INDEPENDENT_CODE 1) -target_link_libraries(kekengine PUBLIC microtar) +target_link_libraries(kekengine PUBLIC microtar_static) + +# Freetype +find_package(PkgConfig REQUIRED) +pkg_check_modules(FREETYPE REQUIRED freetype2) +target_link_libraries(kekengine PUBLIC ${FREETYPE_LIBRARIES}) +target_include_directories(kekengine PRIVATE ${FREETYPE_INCLUDE_DIRS}) if(UNIX) target_link_libraries(kekengine PUBLIC glfw GLEW GL) @@ -118,7 +123,6 @@ if(${KEKENGINE_BUILD_KEKGAME}) add_executable(kekgame ${KEKGAME_SOURCE_FILES}) add_dependencies(kekgame kekgame_res) - if(WIN32) target_link_options(kekgame PUBLIC -static-libgcc -static-libstdc++ -static) endif() diff --git a/dependencies/microtar b/dependencies/microtar index b240d95..66dbf3c 160000 --- a/dependencies/microtar +++ b/dependencies/microtar @@ -1 +1 @@ -Subproject commit b240d953b12dc87c625743a9fb55634b0f0f3608 +Subproject commit 66dbf3cf8623b522d6413c6e994ec45f66288bd5 diff --git a/src/kekengine/cpp/defaults.cpp b/src/kekengine/cpp/defaults.cpp new file mode 100644 index 0000000..c5e8c2a --- /dev/null +++ b/src/kekengine/cpp/defaults.cpp @@ -0,0 +1,53 @@ +#include "input.h" +#include "internal.h" + +namespace kek::Defaults { + +static int + keyForward, + keyBackward, + keyLeft, + keyRight, + keyUp, + keyDown; + +void defaultInput(GLFWwindow *window, void *data) { + if(Input::getKeyState(keyForward) == GLFW_PRESS) { + kekData.activeCamera->translate(kekData.activeCamera->direction * KEK_NOCLIP_SPEED * kekData.lastFrameTime); + } + + if(Input::getKeyState(keyBackward) == GLFW_PRESS) { + kekData.activeCamera->translate(kekData.activeCamera->direction * -KEK_NOCLIP_SPEED * kekData.lastFrameTime); + } + + if(Input::getKeyState(keyLeft) == GLFW_PRESS) { + glm::vec3 camRight = glm::normalize(glm::cross(kekData.activeCamera->direction, glm::vec3(0.0f, 1.0f, 0.0f))); + kekData.activeCamera->translate(-camRight * KEK_NOCLIP_SPEED * kekData.lastFrameTime); + } + + if(Input::getKeyState(keyRight) == GLFW_PRESS) { + glm::vec3 camRight = glm::normalize(glm::cross(kekData.activeCamera->direction, glm::vec3(0.0f, 1.0f, 0.0f))); + kekData.activeCamera->translate(camRight * KEK_NOCLIP_SPEED * kekData.lastFrameTime); + } + + if(Input::getKeyState(keyUp) == GLFW_PRESS) { + kekData.activeCamera->translateY(KEK_NOCLIP_SPEED * kekData.lastFrameTime); + } + + if(Input::getKeyState(keyDown) == GLFW_PRESS) { + kekData.activeCamera->translateY(-KEK_NOCLIP_SPEED * kekData.lastFrameTime); + } +} + +void init() { + keyForward = Input::createKeyMapping("Forward", GLFW_KEY_W); + keyBackward = Input::createKeyMapping("Backward", GLFW_KEY_S); + keyLeft = Input::createKeyMapping("Left", GLFW_KEY_A); + keyRight = Input::createKeyMapping("Right", GLFW_KEY_D); + keyUp = Input::createKeyMapping("Up", GLFW_KEY_SPACE); + keyDown = Input::createKeyMapping("Down", GLFW_KEY_LEFT_CONTROL); + + Input::addPeriodicCallback(PeriodicCallback(defaultInput, nullptr)); +} + +} diff --git a/src/kekengine/cpp/engine.cpp b/src/kekengine/cpp/engine.cpp index cec0d9e..bfb72b3 100644 --- a/src/kekengine/cpp/engine.cpp +++ b/src/kekengine/cpp/engine.cpp @@ -21,11 +21,14 @@ #include "constants.h" #include "gameobject.h" #include "scene.h" +#include "defaults.h" kek::KekData kek::kekData; namespace kek::Engine { +static TextObject *fpsText; + static void framebufferSizeCallback(GLFWwindow *window, int w, int h) { glViewport(0, 0, w, h); kekData.screenWidth = w; @@ -160,10 +163,21 @@ int init() { kekData.shader = new Shader("shader/mesh/vertex.glsl", "shader/mesh/fragment.glsl"); kekData.shader->initLighting(); + FT_Error error = FT_Init_FreeType(&kekData.freetype); + if(error) { + ErrorDialog::showError("Failed to initialize FreeType: " + std::string(FT_Error_String(error))); + glfwTerminate(); + return KEK_ERROR; + } + + Defaults::init(); + fpsText = new TextObject(new Font("font/MaredivRegular-yeg3.ttf"), "HELLO WORLD!"); + return KEK_SUCCESS; } int start() { + int prevTime = 0; while(!glfwWindowShouldClose(kekData.window)) { auto start = std::chrono::high_resolution_clock::now(); @@ -175,32 +189,6 @@ int start() { cb.second(kekData.window); } - if(glfwGetKey(kekData.window, GLFW_KEY_W) == GLFW_PRESS) { - kekData.activeCamera->translate(kekData.activeCamera->direction * KEK_NOCLIP_SPEED * kekData.lastFrameTime); - } - - if(glfwGetKey(kekData.window, GLFW_KEY_S) == GLFW_PRESS) { - kekData.activeCamera->translate(kekData.activeCamera->direction * -KEK_NOCLIP_SPEED * kekData.lastFrameTime); - } - - if(glfwGetKey(kekData.window, GLFW_KEY_A) == GLFW_PRESS) { - glm::vec3 camRight = glm::normalize(glm::cross(kekData.activeCamera->direction, glm::vec3(0.0f, 1.0f, 0.0f))); - kekData.activeCamera->translate(-camRight * KEK_NOCLIP_SPEED * kekData.lastFrameTime); - } - - if(glfwGetKey(kekData.window, GLFW_KEY_D) == GLFW_PRESS) { - glm::vec3 camRight = glm::normalize(glm::cross(kekData.activeCamera->direction, glm::vec3(0.0f, 1.0f, 0.0f))); - kekData.activeCamera->translate(camRight * KEK_NOCLIP_SPEED * kekData.lastFrameTime); - } - - if(glfwGetKey(kekData.window, GLFW_KEY_SPACE) == GLFW_PRESS) { - kekData.activeCamera->translateY(KEK_NOCLIP_SPEED * kekData.lastFrameTime); - } - - if(glfwGetKey(kekData.window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) { - kekData.activeCamera->translateY(-KEK_NOCLIP_SPEED * kekData.lastFrameTime); - } - if(glfwGetKey(kekData.window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { break; } @@ -243,39 +231,6 @@ int start() { int numPointLights = 0, numDirectionalLights = 0, numSpotLights = 0; for(Light *light : shaderLights) { - /*std::string prefix = "lights[" + std::to_string(i) + "]."; - - switch(light->getType()) { - case LightType::POINT: - { - PointLight *l = (PointLight *) light; - glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "color").c_str()), 1, glm::value_ptr(l->color)); - glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "position").c_str()), 1, glm::value_ptr(l->getPosition())); - glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "attenuation").c_str()), 1, glm::value_ptr(glm::vec3(l->constant, l->linear, l->quadratic))); - numPointLights++; - break; - } - case LightType::DIRECTIONAL: - { - DirectionalLight *l = (DirectionalLight *) light; - glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "color").c_str()), 1, glm::value_ptr(l->color)); - glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "direction").c_str()), 1, glm::value_ptr(l->direction)); - numDirectionalLights++; - break; - } - case LightType::SPOT: - { - SpotLight *l = (SpotLight *) light; - glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "color").c_str()), 1, glm::value_ptr(l->color)); - glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "position").c_str()), 1, glm::value_ptr(l->getPosition())); - glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "direction").c_str()), 1, glm::value_ptr(l->direction)); - glUniform3fv(glGetUniformLocation(kekData.shader->id, (prefix + "attenuation").c_str()), 1, glm::value_ptr(glm::vec3(l->constant, l->linear, l->quadratic))); - glUniform2fv(glGetUniformLocation(kekData.shader->id, (prefix + "cutoff").c_str()), 1, glm::value_ptr(glm::vec2(glm::cos(l->innerCutoff), glm::cos(l->outerCutoff)))); - numSpotLights++; - break; - } - }*/ - ShaderLight shLight; //shLight.color = light->color; memcpy(shLight.color, glm::value_ptr(light->color), sizeof(shLight.color)); @@ -321,6 +276,18 @@ int start() { if(kekData.activeScene) kekData.activeScene->draw(kekData.shader); + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + int time = (int) (glfwGetTime() * 10); + if(time != prevTime) fpsText->setText(std::to_string((int) floor(1.0f / kekData.lastFrameTime)) + " (" + std::to_string(kekData.lastFrameTime * 1000) + ")"); + prevTime = time; + fpsText->getFont()->drawText(fpsText, 0, fpsText->getMetrics(24).height, 24, Colors::RED); + + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + // Swap buffers and poll window events glfwSwapBuffers(kekData.window); glfwPollEvents(); @@ -328,8 +295,8 @@ int start() { auto end = std::chrono::high_resolution_clock::now(); std::chrono::duration secsTaken = end - start; kekData.lastFrameTime = secsTaken.count(); - std::cout << "FT: " << kekData.lastFrameTime << std::endl; - std::cout << "FR: " << (1.0f / kekData.lastFrameTime) << std::endl; + //std::cout << "FT: " << kekData.lastFrameTime << '\n'; + //std::cout << "FR: " << (1.0f / kekData.lastFrameTime) << '\n'; } return KEK_SUCCESS; diff --git a/src/kekengine/cpp/fonts.cpp b/src/kekengine/cpp/fonts.cpp new file mode 100644 index 0000000..73f6da7 --- /dev/null +++ b/src/kekengine/cpp/fonts.cpp @@ -0,0 +1,344 @@ +#include "fonts.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "shader.h" +#include "engine.h" +#include "constants.h" +#include "internal.h" + +namespace kek { + +Shader *fontShader = nullptr; + +TextObject::TextObject(Font *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; +} + +Font *TextObject::getFont() { + return font; +} + +void TextObject::setText(std::string text) { + this->text = text; + loadChars(); +} + +std::string TextObject::getText() { + return text; +} + +TextMetrics TextObject::getMetrics(int sizePixels) { + float sizeRatio = (float) sizePixels / KEK_FONT_RESOLUTION; + return TextMetrics((int) (sizeRatio * offsetX), (int) (sizeRatio * offsetY), (int) (sizeRatio * width), (int) (sizeRatio * height)); +} + +static std::wstring_convert, char32_t> utf32cvt; + +struct RenderChar { + float data[24]; + + RenderChar(float xPos, float yPos, float w, float h, float texL, float texR, float texU, float texD) { + float coords[] = { + xPos, yPos, texL, texU, + xPos, yPos + h, texL, texD, + xPos + w, yPos, texR, texU, + xPos + w, yPos, texR, texU, + xPos, yPos + h, texL, texD, + xPos + w, yPos + h, texR, texD + }; + for(int i = 0; i < 24; i++) data[i] = coords[i]; + } +}; + +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), NULL, 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); +} + +void TextObject::loadChars() { + std::map> chars; + std::u32string str = utf32cvt.from_bytes(text); + + 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; + + 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 + } + + this->offsetX = offX; + this->offsetY = offY; + this->width = x; + this->height = maxH; + + 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; + + for(unsigned int r = 0; r < face->glyph->bitmap.rows; r++) { + memcpy( + textureBytes + row * KEK_FONT_RESOLUTION * KEK_FONT_BITMAP_WIDTH + col * KEK_FONT_RESOLUTION + r * KEK_FONT_BITMAP_WIDTH, + face->glyph->bitmap.buffer + r * face->glyph->bitmap.width, + face->glyph->bitmap.width); + } + + 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; +} + +TextObject *Font::createText(std::string text) { + return new TextObject(this, text); +} + +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); +} + +} diff --git a/src/kekengine/cpp/input.cpp b/src/kekengine/cpp/input.cpp index aa19341..4144451 100644 --- a/src/kekengine/cpp/input.cpp +++ b/src/kekengine/cpp/input.cpp @@ -38,4 +38,34 @@ void removeMouseListener(InputListener listener) { kekData.mouseCallbacks.erase(listener); } +KeyMapping createKeyMapping(std::string name, GLFWKey defaultKey) { + if(name == KEK_INVALID_KEY_MAPPING_NAME) return KEK_INVALID_KEY_MAPPING; + KeyMapping id = nextID++; + KeyMappingData d; + d.name = name; + d.key = defaultKey; + kekData.keyMappings.emplace(id, d); + return id; +} + +void reassignKeyMapping(KeyMapping mapping, GLFWKey key) { + auto it = kekData.keyMappings.find(mapping); + if(it == kekData.keyMappings.end()) return; + KeyMappingData d = it->second; + d.key = key; + it->second = d; +} + +KeyMappingData getKeyMapping(KeyMapping mapping) { + auto it = kekData.keyMappings.find(mapping); + if(it == kekData.keyMappings.end()) return {.name = KEK_INVALID_KEY_MAPPING_NAME}; + return it->second; +} + +GLFWKeyState getKeyState(KeyMapping mapping) { + auto it = kekData.keyMappings.find(mapping); + if(it == kekData.keyMappings.end()) return -1; + return glfwGetKey(kekData.window, it->second.key); +} + } diff --git a/src/kekengine/include/color.h b/src/kekengine/include/color.h new file mode 100644 index 0000000..c28de98 --- /dev/null +++ b/src/kekengine/include/color.h @@ -0,0 +1,38 @@ +#pragma once + +namespace kek { + +struct Color { + float r, g, b, a; + + constexpr Color(float r, float g, float b, float a): r(r), g(g), b(b), a(a) {} + + constexpr Color(float r, float g, float b): Color(r, g, b, 1.0) {} + + constexpr Color(): Color(0,0,0,1) {} + + float *valuePointer() { + return &r; + } + +}; + +class Colors { + +public: + static constexpr Color RED = Color(1.0, 0.0, 0.0); + static constexpr Color ORANGE = Color(1.0, 0.5, 0.0); + static constexpr Color YELLOW = Color(1.0, 1.0, 0.0); + static constexpr Color GREEN = Color(0.0, 1.0, 0.0); + static constexpr Color CYAN = Color(0.0, 1.0, 1.0); + static constexpr Color BLUE = Color(0.0, 0.0, 1.0); + static constexpr Color PURPLE = Color(0.5, 0.0, 0.5); + static constexpr Color MAGENTA = Color(1.0, 0.0, 1.0); + static constexpr Color GRAY = Color(0.5, 0.5, 0.5); + static constexpr Color WHITE = Color(1.0, 1.0, 1.0); + static constexpr Color BLACK = Color(0.0, 0.0, 0.0); + + Colors() = delete; +}; + +} \ No newline at end of file diff --git a/src/kekengine/include/constants.h b/src/kekengine/include/constants.h index cbb2ced..7eaf920 100644 --- a/src/kekengine/include/constants.h +++ b/src/kekengine/include/constants.h @@ -23,3 +23,15 @@ #define KEK_LIGHT_MAX_DISTANCE 30 #define KEK_LIGHT_MAX_DISTANCE_SQUARED (KEK_LIGHT_MAX_DISTANCE * KEK_LIGHT_MAX_DISTANCE) + +#define KEK_INVALID_KEY_MAPPING_NAME "INVALID" +#define KEK_INVALID_KEY_MAPPING -1 + +#define KEK_FONT_RESOLUTION 64 +#define KEK_FONT_BITMAP_WIDTH_BLOCKS 16 +#define KEK_FONT_BITMAP_HEIGHT_BLOCKS 16 +#define KEK_FONT_BITMAP_WIDTH (KEK_FONT_BITMAP_WIDTH_BLOCKS * KEK_FONT_RESOLUTION) +#define KEK_FONT_BITMAP_HEIGHT (KEK_FONT_BITMAP_HEIGHT_BLOCKS * KEK_FONT_RESOLUTION) +#define KEK_FONT_BITMAP_CHAR_BITS 8 // = log2(KEK_FONT_BITMAP_WIDTH_BLOCKS * KEK_FONT_BITMAP_HEIGHT_BLOCKS) +#define KEK_FONT_BITMAP_CHAR_MASK 0xFF // = KEK_FONT_BITMAP_CHAR_BITS 1s in binary +#define KEK_TEXT_BLOCK_SIZE 8 diff --git a/src/kekengine/include/defaults.h b/src/kekengine/include/defaults.h new file mode 100644 index 0000000..223fe53 --- /dev/null +++ b/src/kekengine/include/defaults.h @@ -0,0 +1,7 @@ +#pragma once + +namespace kek::Defaults { + +void init(); + +} diff --git a/src/kekengine/include/fonts.h b/src/kekengine/include/fonts.h new file mode 100644 index 0000000..a13aad3 --- /dev/null +++ b/src/kekengine/include/fonts.h @@ -0,0 +1,159 @@ +#pragma once + +#include +#include FT_FREETYPE_H +#include + +#include +#include + +#include "color.h" +#include "resource.h" + +namespace kek { + +struct Character { + unsigned int textureX; + unsigned int textureY; + glm::ivec2 size; + glm::ivec2 bearing; + unsigned int advance; +}; + +struct CharacterBlock { + unsigned int textureID; + std::map characters; +}; + +struct TextMetrics { + int offsetX; + int offsetY; + int width; + int height; + + TextMetrics(int offsetX, int offsetY, unsigned int width, unsigned int height) { + this->offsetX = offsetX; + this->offsetY = offsetY; + this->width = width; + this->height = height; + } +}; + +struct FontMetrics { + int lineHeight; + int ascender; + int descender; + + FontMetrics(int lineHeight, int ascender, int descender) { + this->lineHeight = lineHeight; + this->ascender = ascender; + this->descender = descender; + } +}; + +class Font; + +struct TextBlock { + unsigned int characterBlock; + unsigned int length; + + unsigned int bufferSize; + unsigned int vao; + unsigned int vbo; +}; + +class TextObject { + +private: + Font *font; + std::string text; + + int offsetX; + int offsetY; + int width; + int height; + +public: + std::map blocks; + + TextObject(Font *font, std::string text); + + ~TextObject(); + + TextObject(const TextObject &) = delete; + + TextObject &operator=(const TextObject &) = delete; + + TextObject(TextObject &&other); + + TextObject &operator=(TextObject &&other); + + Font *getFont(); + + void setText(std::string text); + + std::string getText(); + + TextMetrics getMetrics(int sizePixels); + +private: + void destroy(); + + void allocateBuffer(TextBlock *block, int numChars); + + void loadChars(); + +}; + +class Font { + +private: + int lineHeight; + int ascender; + int descender; + + MemoryBuffer *fileMemory; + FT_Face face; + + std::map characterBlocks; + +public: + + Font(std::string path); + + ~Font(); + + CharacterBlock getCharacterBlock(unsigned int block); + + TextObject *createText(std::string text); + + FontMetrics getDefaultMetrics(); + + FontMetrics getMetrics(int sizePixels); + + // Draws text normally. + // y specifies the baseline of the drawn text + void drawText(TextObject *textObject, int x, int y, int sizePixels, Color color); + + // Draws text normally. + // y specifies the baseline of the drawn text + void drawText(TextObject *textObject, glm::mat4 projection, int x, int y, int sizePixels, Color color); + + // Draws text, with (x,y) being the top-left corner of the text drawn + void drawTextFromOrigin(TextObject *textObject, int x, int y, int sizePixels, Color color); + + // Draws text, with (x,y) being the top-left corner of the text drawn + void drawTextFromOrigin(TextObject *textObject, glm::mat4 projection, int x, int y, int sizePixels, Color color); + + // Draws text centered around (x,y) + void drawTextCentered(TextObject *textObject, int x, int y, int sizePixels, Color color); + + // Draws text centered around (x,y) + void drawTextCentered(TextObject *textObject, glm::mat4 projection, int x, int y, int sizePixels, Color color); + +private: + CharacterBlock generateCharacterBlock(unsigned int block); + +}; + +} diff --git a/src/kekengine/include/input.h b/src/kekengine/include/input.h index 71443e7..d1c2ff5 100644 --- a/src/kekengine/include/input.h +++ b/src/kekengine/include/input.h @@ -12,6 +12,14 @@ typedef generic_callable_t KeyCallback; // key typedef generic_callable_t MouseCallback; // mouseCallback(GLFWwindow *window, double x, double y) typedef unsigned int InputListener; +typedef unsigned int GLFWKey; +typedef unsigned int GLFWKeyState; +typedef unsigned int KeyMapping; + +struct KeyMappingData { + std::string name; + GLFWKey key; +}; } @@ -29,4 +37,12 @@ InputListener addMouseListener(MouseCallback callback); void removeMouseListener(InputListener listener); +KeyMapping createKeyMapping(std::string name, GLFWKey defaultKey); + +void reassignKeyMapping(KeyMapping mapping, GLFWKey key); + +KeyMappingData getKeyMapping(KeyMapping mapping); + +GLFWKeyState getKeyState(KeyMapping mapping); + } diff --git a/src/kekengine/include/internal.h b/src/kekengine/include/internal.h index 278c733..d2b36ca 100644 --- a/src/kekengine/include/internal.h +++ b/src/kekengine/include/internal.h @@ -8,6 +8,7 @@ #include "camera.h" #include "scene.h" #include "texture.h" +#include "fonts.h" namespace kek { @@ -15,6 +16,7 @@ struct KekData { std::map periodicCallbacks; std::map keyCallbacks; std::map mouseCallbacks; + std::map keyMappings; GLFWwindow *window; Shader *shader; @@ -28,6 +30,8 @@ struct KekData { float lastFrameTime; std::map> loadedTextures; + + FT_Library freetype; }; extern KekData kekData; diff --git a/src/kekengine/include/utils.h b/src/kekengine/include/utils.h index 82a8add..2254596 100644 --- a/src/kekengine/include/utils.h +++ b/src/kekengine/include/utils.h @@ -16,6 +16,8 @@ struct generic_callable_t { generic_function_t function; void *data; + generic_callable_t(generic_function_t function, void *data): function(function), data(data) {} + void operator()(Args... args) { function(args..., data); } diff --git a/src/kekengine/res/font/MaredivRegular-yeg3.ttf b/src/kekengine/res/font/MaredivRegular-yeg3.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e24ae15cfd4a24a1d606389861744ce49dee84ab GIT binary patch literal 36756 zcmeHQdyHJweLk}f?|O}G{K89tFTcjU6SGkV>m4LSR+-gI0;kABmw=)u1S?s2UUgKvk4l4OLB>MoEf`aMp8I>R|JqXz|G>GzP3V1M zV61<#)HS%%x%Q(dZx4+g+Vk_@?Y`ByrO!E6x_G$QKbS1(e*$A)MY&-Z9hd#rvUarJ zMY()yLY3KfMr*mBwO-@Xg3g2+AJNLB`uRCH#`Ns_#-g#MfxQ-nyPc+Wa{8ymMFH<{YH(pY8WQaG$%)B{K^auO^1& zCtHB7aE@2IdKRxnl|UnZgg!{g(taG&cKLeI3(j#&R}aRZ&AGhJ(s|7Hb1bi`W;-sN z7oV%m@hqaFTKqIFtM1rVNwb+bq*FbPa=Ip-4}`&jc_<8+;K*p?is+0Vjrme_WhLl0KbxU8CEvigbJm)Rp7+h&glN7mS8 z`YQDWFAp}T)|>j{GjDuT7L4eZzd~~E0ax0 zUs6h5y7aQkmS47Nrfp`yIcA*1Jzr=mbQjhYZZF(hc%(31I2g@ak#r|FSIl#qckXBB z{{GzGp8KnFe|qjK=e~6A(7B;={pTK^y>NEHZ2Q0Uo%qp-e>(B%iN8Masvp4 z=coVCezyI6qWiN79X^Mg|BQ33SE0O()u$8X z0hH&lqri~%yHLK4@-oU>&MmqC_iy-#afg} zlpkTKLjTHFom+)^)i+Qu_5$#^09Y<~+qui)TN7haC?917-L2rR$# z1j=`vyJ!sx=3R8!xr=wAyyn~`=cC+-@+=C*eykg16y+u7K7Jm`APVSw9M_Uzlpi{G zX(tNCT>83mmx1qP&pY=Chf+d$4!10rcRBc2!Q3nQP~LRzlP_Zv4|-QZj;roM!I-OG za_$=NyylnAU3)PK=&jj+0(@&Qm-jW-9dWJ)JbQ4h2fTWI?p!Z$_uh%}EXrx;*8a%3 zb)bJe@U8!ea~mE*IqBTS5|;OsC?}k|an!k+5){z5c{9qlo!fLd%1h3D3iYReee-!J zpt%M0mZzP&Wev)Aox8OU;+rg0tD?6w;Xw(_OtNynawj>&i!}W_h+^s!2i;@nJs8v-S*o5 z9%}pk83^6A%)z@BZ2!$Mw_?He1$X?9o4W3SEoc*?I< zP~XG7^Xw5%fi-EXCPkjBR)0LcZ5MatGcV4(NGXVj=b9DaGO9B#f;x8Qk{UI3;k>KD zex3Tc7IWwDp{_)aHTjb#5ekpag^B%=h&VV;=aQ^Y@=~=Q_w{>saJC9ZNlL51mDku$ zYOHx~(xR=J{gjt?DZ#E6VPu^TtwU_^hu|M63d%|@ye_UnpwaS54{5Q#vQsmXn>wa35YJH)m3ZtSL7O_1bchEzVaDIr0i~)9evk zKM8MH7^9WktR?&G5s#rzV$DKs#M6G964T=-BIhuk+mHM4evIKDpYj;uWN%6*#M25t zhP2`jM~id)nBgeO+FXoS47Wp@^!%KUvMa42EfNE@Nd1r_$H=n^CFy+TMW1`9tJ%Xo z8u$!FBg;N$`99>6Wm!lhe`*iUH`YbRHc!{{=|HxLHTG_ZSMTq}>yj)^u@c19=s;rD zakR!#6PXogsU5N9tt)(-8D|V>AkM1F2cQ!C2sxlZZY5pmM7B|_oD!oH-&8l8ZJDV7 zLLv2g0n4rJZdINJt9|B7Z_1Gv#F5;+kFa%i^_gB?(N*dFG*?JmCx%EjIk8f$6Y^$E zo`L5XW$>r2Q$!wxx0`YfYtG~~FLUtE29ctu;5;Ic_RViFH%3q>@#>jgpIzd}&}<($ zWhALzWJZbU3q0j@l05P(3KQi>wmDj>XYg)hs*sPkpK0KOl!5c)cf>CIs-*=Ykq`5 zZ=qWZDit4Up9uId1Yw=vY=4CwV;Et(h%Wr2Q0mwQJPm9{FO0oH^3pcsLatVVy~@|i zT+2O}p>{m0x=ow$z5tO}qbo)ycZaOoc99xa=HMqy%Ou?x8lZ~hqfDfig*4TquI7!* zlBpagt3W;TL*9xF)mgcj)V=n32gO;m5=RO>C6e#jXz288_o5eUuXte|e}bZBIa?Wx zlCqC=+(&%Oll~gl(9_3ej&juE3otrp_1(|=neiI?>;Lo~QxTgPyFg`&Y7Hnqc5I{6 zGcTTgh2Mn49DQtZ7gg4Ihs#+5#3j}@ziAP@a$R+gbov$O(ljMKt8RYXp|+GOBo%S6 z&<0+2S=pXhg|p>n6}nu6Ytcs%QjZmql}P#28?#3wNyt^{+EGQx>(kqvI~UsD?_fkh zI#3%1Sy<;g#$f0$dn8{rnqX>0Z|e9Q<)$8Xi;Pb)8q_vdrD9u+(IgTd4LJg>;7<13n}I`Z}C2`S@?@^C#Re(#}Ledwvt zysEq;?J4>dpY$uxffq1xR8!9tYvaPKXE35A)H<)Et=(l8Vpu#*g>2^WTAXG?M2wkI zlD=v~n2EhYhBRgy>|v=*@)J+eua<{>HYW0+)SkLRyHX>nX?f0MiTfj4Wynt6nw5nk zU8luIHThMoQ5p0lFK5xqwWlLOpH1n}l!+s#b$YX8VD0hSbHm6-NtZK-g0s?0n6XCX z#1lE2?2+bVd($%QWVCrkZzD&IAZJDiCDDqLkChb2ixfZ=XUeShWd-~vJcG=^s#%5$ zn5+&oV;Sl{j}(ejubs!x*9v<0kO%GKscX!l7ut5=*=VGDQIRGVy+AFgmwNGevtt`% zY<0^#^@?S@Aln&s=!LVXVe^aN5xUp*PZ~Ibqhemn-QtBcrM331RD5VD`L~ebuG8)n z{H{|r)N>neOOqkTCMliZy+B7k@=5YDEgEj!cbLS%YspJgY^1G$9oy>5Qmr$yLs=Hm z6EAAi{43Bkd*nzXO~urubM{d`JWs!OiaBuvZAaMy1@kA9B_l6LYSt{IEE}=*o~Wy| zc|_UnyJ`2c-OczI=^1@UKy0L{*q0CZ`$h4S{;y0(!_94m^XQz;vL zmpskt!TLJ)`U*0Za|>yCy(s4q8>Fn+MRFXakGZDYd4&?jT4mnCd5{YuYWB`rXX?_4 z<0QjtDPQb|#K%6?-m{cZ*ds6Em5^0ppRJKv)QM1NIhS6P`YE4jL=?>`@(svcCkN7= z@<`(IJUrr=qqI(pn$JeOKHHCjZj}FN9gZYk(o18NxXi~>JLF^=&eD9&5*~UJvlh?U z?_26o%`1{bwQLV>&#;{QSiDGyeblx0%UE`C=je>}pu)E_*7O%5#-k2CX=^HWC0J#- zn=%{a?sH=s#S~LrXrD{FDFP}nT1;i=APZ?6_r1v!X6gRACb=4dh zpVO`(&u7R)oyLAttuvniiHIe(NOf+9U2^DiCjEt|co#{8^g8nOM~z_3Q5wNoF@(IG z6AQ%5ku0PjYStHa3heV&$dzYShjmdrRePOf<=?a2OYye!FKI`!r9BVI!6lC5CVbLw zxexdvJdJQ62HJLY^}r6TSi^GoN_9WhAkr!#Da{8=CDg9OHJoYM4kyo)cO5J#m3oZN3hTn@ULgakHU5UOX zKh<2NCD!D?B9GHK^`UmnHT#A~c;OH4P!4{R? z@b6aFgrwXt$R}gz~eE9R0rNZ-q!YSp5PI)Mx^$7aU) z?`)|BE5VL76VybZnzFcLj6?2d+tslRkyifxfH6T2X7u^*ipdMUjj?=_1pUxFU65L( zwPbUgf=bzUeXhOMk8RdvWhv5)Wigg%s zI<}b(Ee2!{NmZ7{e5oAYUS~ObFw!SheqxR3Jmu>(CmxbGgEwqi`z+Du!3KsNK9%t- z(PF~ZsGR+ItaaIt7H09!8)zTygXHP*a>k_J;~D)$IEg>z8|XvL^^)|=^FkbZ&G1=< zp0N0M=FTXNXU>n}8S?O532?63iE^CuklweDqjAN&V7n@ulrs3M)ojx${@p@(9(!#S zg_a|~)Bdeea%zE8Q1W6&Z!Q-0Q5O1xuk|hr*XsG?cS%Ful0BBe`XB=(6vlevKJ@&K zOwAnz*2sl;m+)$!X!?VFRyp?w|F41^ltJ?Nn5vYKebj?wrKhv)d#QV6pwqA9S>9)4 z`WROv*3vS4o8A$89Q*qqqw>-vZrxlYl}fO>8xQg#ZA!rp`C1M^KO;yu=C1*-7rlX% zosVKa^0CmgMl4#(c4wwuU)#>H z2|q!M^Oy0kGWC%c2_ce&SiZoTg`O;5@OiHdzhY8+P!BUly_eg{d8B-IUjFXb_6bYt z-J|QyMcJ(SE`?g92N4f(YHySGM%R+^&er^nIo0w)*t2|KEOE4dU@Q&3lWwG&Je|!Q z7p0=L!*9ZRmh<-oyc@fSKVDSMs8=__Dz9O5(~j~l+11Wr=SJAI&z!Y8Nm8PnmBAQq z=8KdCdpO?l1Rvy`Wv(QR(32)<6D3O{lr{Ic0ZUo0bGWC^vodi|Zl7PhRuw(8Wh0EK zCS@CGGeiz@2=l5vgUHPx)tX}lrH$FIn)lH}nvIAVh8$IECiiU2VW8L`PKZ0k&bBl` zzAUnwR%i1X`xZ{KZAA=mz^m-%I}0&o(+sZZ`3!t3_h~7$r+X0XaaEHJ5s`NKhJ-ZW z>zYhv_Aq~*7tc6z&5I|rn3nB ze!P*L~QZ6P+%Ey{K^nHOp zCr4R-=Ed0~(h8%W`6s*yWotqg*IGVdScN@nrnG6yQJ+LXZ&F@U?ryZMraqs((VDIR z)Ppoi%E}d#mx!bI!3XEDpUXpE zktxjg8ME~)uS^kY>`lUNha1?O`lmN3x`?QpqnM=;UF@?lk<($9Ar}5|Fxi zSdPcX>x|dG*0##$mfjCfzmjKryFXb9{8b-iTV^U^Ayvf){Vjc8lJ_iHv&<9W_n4L5 zf7G$E{3gzg;-VZ$>1YQZUrWAHiGx?={~TpFL;b7$pl^1zzk|SccI&=pz`4pO;!69G zz99r%@nS#uHS;!a&C~x68B@h7V%qXP;8$X4gnE@fzqh>sriQzzW^|=k6#`l`&5GRH z#TILTPu|x~%XA*YAN!#_x$nCIay9j71~ctTBxRk3IF!9vH!H*9)n4D>X$z=wW;t>} z$>Qd)f7Vj=Q9gmKBT+L_XoaREjNazFl(G?#YT+vD`qFy1WP)b!$rETxMVh?CpYyD~ zhost-=ddovFEs6A-`*SK_dyNz8p5sn40?dDQ6uyWyBk8(v!9ysm_1g#o#TIt<*#^O zQq)=Q04~mE@j24oovdqd&KC_K5`Ae>mKN>wn>Nmj@372!p(%SCOR_LBk5uGJKE15t zg@w%DItUd}NI%sXGhI2VkyYxaCPP)`@(&GPIno)~!2O}&YvL&-^;Py*@TNwxbeNY( z=Z{sfq8PV#WBmI=ABxqhGf?wLm3>0pLYuUl4DUEqVB_wA+0A0WMtKeTIMr_q%8_Hg zHK2YQ(c%npr)BbKk!zrpv40Y_Gi!rY!*@TH@qsm$uGYC$(r-U*SouyvrsSkf4Oju< zlxme0F@#sc8Z(c`mOf|=7V1cPl7=h)hKlxd#8#exyra?7uAb%ax~=JRc$T9bnKB^d zqgR*XIq)TNAx$kxj(on#XUy+yPuCI~9o7-r%JM2GnBC|P@!qZO4!)V#CJN!xZz24y zD9+gaJ@^Ls@B?9C<}K^UtV{l~ruzdzKSo*}wH!ksY)XW8DZnHO&Y$KWCl!i#z(MypaCNXb3gpq5LjcO`cL-A{Tv| z}3HO)^?p@r9ih_iP!NRT9avoJHSdJ|Is0g&+Lg7DIZbmz+6*@y-!ZE?kIv zh%DVesz; zU+%n6%hrrpSmS`?P*xba$vcP*L`-)HhK^OA}*fHCQF!UoUU$&v9GzxAqn zb<#}znNK9ioinH*)ohy{WB+M#r;_DC(hq6Ue&%gxQ##%6q;HF}H`b4#3N{on)X)dD zY`$$PlRxK?mZzot82h^)nX4DaO2b+e4I8)#qaP9b5-ICEd@F%{26K<)wgztw&@bpG zq%R+-&N!05TS{*iN59`zteXz(`{bHm6H6}Ej1!$xPS2e^)U)(U6w+(f--!~1YKqy^ zxKc*3p0j@-7CnzwWV0`hpseqdYa%!Bve>P|OF2xDN`I0(Y!k11;pGSp(8>)nwk&;r1W z{8N56tWMUW=h%#3P3ARXqkdKAe?zRS7i#PyxSm>G>8Q7QD^n=9hWIg!F#k&T%6PhC zsC@oeFVp$_W$&itNKdSkQZkSyDKhsHAN5J^@@T9FBjPKXsoL|b_jgZ7jTEGH@)U+R z(|DfhQB6B(rfR<*?mUL~2bet@tTfUqvnQ#tP{x=t+rm|;1AWV@diF^AZh*W`9L%Sv zJ@biI&1JWKr1unW%(InJ?>9eG1`@} zq?&f;x61TV`n&3AZESd7M*WZ;xl&@%BoAgDM!NirXnibd2CwK>Pd>tHR!+;Y&D|f8 zD)EyNS8ej6Ov0w?9tV5X!`ky#^w-$RaF{S_6mRpGGDqoe=qx!LazF(w&f*O1FlZ%~8tWT& z`V;+;w8A@F71Rillt_WRg;LZxzhM?_CH?S@Snm5m@)>%%BqnugzkCh^DK}jqCgM{p zXop9AhkNl=xSIK)wzLZ+H_~8D8ssW@O^fyyGF9MFdJWKB-I zcc}b5Aa~o9W4MEf#=tiG%Wml9@3A5&NRNDDl>TqPc{}O%P#BidXyX5l&@Z_CFNCNM z%I-g_2klI)Q!egh`SmpV?L0o40DH$>0TaV<&Wem{bvv}ccb z=}9DKFz#DU+8%l-PO3iMXEs-z!#9<|O`V&no%c&4uy z5?#{bb&QU36z6)ok%EtV{KlMPs(!W1k<>%w44z>oDDqw^%*((U&(5V{v<(NaGAmZ* zO6o1jm8N_9n`JroOyp_O2b*)g`Cg=H>$AV+&z6;tKcqKNpgz4l>hYu=L`gFxJNrd7 z|9#vVyP0mw|4&(lZb?^st%d}$srQG|$MC;9AU-X(LvPG5wqK8YmME!vp{JGnUmKu@ zns0#uyx;uDo<)5Eqnr3t+krX|7w&A?CVg@jinx}|{76NtQ3(DR$EQ&PrC5)o&VE^> z;vEm}Q5xJ;5e<1TE{$)XRUOs5PARy@@X~l>{_pXKJ4Y$%NsYhbmL50;`*`zz``aR| z;D`?`N~)3nFJ#~1)mR%DtA2IVU%P3_E}T#P6}9=!T>HIl^L}OAY(L=;i~Z)lfkiw= zkCSclQD|OzwR7gf;Ptcy_0F}}&ypWemsOEAaTbG5q9i^w7NU#?E1 zM(QzEjnPwJf2TBi_}$OvVH75xrwvQ){fuPE<4O$?LB2-B4H7&2^#YTBxqt7dquJl_ z;z>${E`9+$Wa*-@-MH@S`{a5$shStL=D=9j~?H8arNR z#~wTOx*I{Q*KM{VsjapCb#}boj_d8X!Hyg4c!M2pbbXk0qr20NoO`3Y&)Pff_!y2i z*~pvixXF&6vg2kuZn5L7?pDn0GbDZ1v(1j%?RbYB@3gCTT6>2bKW)dm?D!cw-ebpm z?f6+c-ef}FJATfN58Ck|JMOfRJFWe&9Urmdqjr4EuI{on{&V5NH+=>M zRs8kY&@NzgxgfM#(7q(Jx$0aV+HH8Qa&2hyS3;XYdjZC53vK@2qj!aNCnz2W?S&Zg z)zI!LY;@0tHmDT-A+*nPH@CEg_F{Kk%jVEt;?}i{g!T$|b<3aoHk|FemhU5D71|a; zjx{a+9NGo9q-7?wTilY?g`wSwHrG*(Z*w1Oy&|;R-36^%Lwf-w~?Imt^$9F<|g}bkFg>OTa z?#|-YiOEA#BSXWb+yfB_q>Ge^Q$2A1sdbPwh=6_9W@Wo`H$6;ZkXGdQ)%j9!#0G zDeMgp-U#%5oE+$%PIm7b87(CTMoPmGjlSaa$k2FkDj6(J4@`|rmVjhavU&2B+tD*H zF*>rhm`siwERH7Rkc#C^T&#msWeQgkRaJNT^vkyA4(=h2a|!~=;-9|#CUOf zO;S8KP@F6!yGMqG0JeX8&T8v*5LGClHz z;-)gP%{{f1tchE{Gkb7D#pH(tMX;|*(j zgPpcc6#;sGG1)pXHU{d+)~#!kd-v@g9T`aaCdT?l#*;oU7%<7m1qL4ob6{;w>6w@s z>YcPHgEj-SrzU#gQ@z_s8H!7u+Sfm@cVv9XJoM1ibn@f`9AfMcyyIZ9yEu6Jftz*= zj@~@}(9o7GZmXMclkSk4awBfY4ZD&{+!tICeZ%PMcVjre9w~dBLyE=u6s`?o?0%%X zB4+G!qt;t_t@13X8v7t-PPu7NnQ-Hvzs~h=QK+E4-L93~{XjGYE(3;a1hW#1ORTpa zNQyv6RD01k;r8H|HM0lvCoq!ol_1+R5@Rp^?Fn3_jh^SCu@zcKF0RCvTmnu9z?pb< z1K$YrK;0ccs{}sTIJ&S16;1LXNKP#zCc`xF(E^j0>$S28S2pAP7I%9XLkUK4buZ>6 zxH94nqBV-Pq)1E`)QRsS^*u(n2yTNxcSN}vZR(Z!9|8}r`w^V=qj-BwgAO%2VAAw~ z-+mNXZ8Nl2Nuvi;dkt|H{?l=feTV6UJ~M{7!Z2nCy|t(-;T^|l>T@5C^fGGp5RR0a zX9MUR#XtHckyDdv&|icmc#YcQ9D4E)&M80T;oQ_NW0*@s2`CX0Tf91m*|gh!lX?V4 z*&<7_Yciy**6KarLo8mev=p^RzuIK8EA^LPE7OBm+h}s6x}1y8W_z^vsA;npHFp55 zb?yel+yA3$^QKZ=Z;5@10YK|K%lWa83y;WIetJ%cse81U*nQ09FXlZ)IR{UFw z`d;|QZnND0&iZUrzv-A~UV?$hdXmLQJmZXhP1}->&kN$x18P&S82vcqG8pmjxEVRr zA@f!*GLEMu-%H$6nB5QBdkxnRa4MTH(^8rzLBVH?G4pZel7r~qZ4%rL8{LFW