#pragma once // Note: See "main.cpp" for headers. // This file was extracted to improve the organization of the code, // but it is still compiled in the "main.cpp" translation unit, // because both files have mostly the same dependencies (OpenGL, GLM, FreeType). #include "fwd.hpp" #include "ofMain.h" #include "ofUtils.h" #include #include #ifdef TARGET_OPENGLES //#include //#include //#include //#include #endif #include #include FT_FREETYPE_H #include FT_MULTIPLE_MASTERS_H #ifndef F26DOT6_TO_DOUBLE #define F26DOT6_TO_DOUBLE(x) (1 / 64. * double(x)) #endif #ifndef F16DOT16_TO_DOUBLE #define F16DOT16_TO_DOUBLE(x) (1 / 65536. * double(x)) #endif #ifndef DOUBLE_TO_F16DOT16 #define DOUBLE_TO_F16DOT16(x) FT_Fixed(65536. * x) #endif namespace ofxGPUFont { inline FT_Fixed double2f16dot16(double x){ return DOUBLE_TO_F16DOT16(x); } inline FT_Fixed float2f16dot16(float x){ return DOUBLE_TO_F16DOT16(x); } inline int32_t calculateUpperPowerOfTwo(int32_t v){ v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } inline bool isPowerOfTwo(int i){ return (i & (i - 1)) == 0; } // Decodes the first Unicode code point from the null-terminated UTF-8 string *text and advances *text to point at the next code point. // If the encoding is invalid, advances *text by one byte and returns 0. // *text should not be empty, because it will be advanced past the null terminator. inline uint32_t decodeCharcodes(const char * * text){ uint8_t first = static_cast ((*text)[0]); // Fast-path for ASCII. if(first < 128){ (*text)++; return static_cast (first); } // This could probably be optimized a bit. uint32_t result; int size; if((first & 0xE0) == 0xC0){ // 110xxxxx result = first & 0x1F; size = 2; }else if((first & 0xF0) == 0xE0){ // 1110xxxx result = first & 0x0F; size = 3; }else if((first & 0xF8) == 0xF0){ // 11110xxx result = first & 0x07; size = 4; }else{ // Invalid encoding. (*text)++; return 0; } for(int i = 1; i < size; i++){ uint8_t value = static_cast ((*text)[i]); // Invalid encoding (also catches a null terminator in the middle of a code point). if((value & 0xC0) != 0x80){ // 10xxxxxx (*text)++; return 0; } result = (result << 6) | (value & 0x3F); } (*text) += size; return result; } inline uint32_t decodeCharcode(const char * text){ uint8_t first = static_cast ((text)[0]); // Fast-path for ASCII. if(first < 128){ return static_cast (first); } // This could probably be optimized a bit. uint32_t result; int size; if((first & 0xE0) == 0xC0){ // 110xxxxx result = first & 0x1F; size = 2; }else if((first & 0xF0) == 0xE0){ // 1110xxxx result = first & 0x0F; size = 3; }else if((first & 0xF8) == 0xF0){ // 11110xxx result = first & 0x07; size = 4; }else{ // Invalid encoding. return 0; } for(int i = 1; i < size; i++){ uint8_t value = static_cast ((text)[i]); // Invalid encoding (also catches a null terminator in the middle of a code point). if((value & 0xC0) != 0x80){ // 10xxxxxx return 0; } result = (result << 6) | (value & 0x3F); } return result; } template inline void hash_combine(std::size_t & seed, const T & v){ std::hash hasher; seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); } typedef std::vector FontVariationCoords; struct GlyphIdentity { uint32_t charcode; FontVariationCoords coords; // we need a comparison operator, to use GlyphIdentity in our unordered_map as key bool operator==(const GlyphIdentity & other) const { return this->charcode == other.charcode && this->coords == other.coords; } }; struct GlyphAppearance { uint32_t charcode; std::array color; float fontSize_px; float letterSpacing; }; } // create hash for GlyphIdentity, so we can use it in our unordered_map as key namespace std { template <> struct hash { std::size_t operator()(const ofxGPUFont::GlyphIdentity & k) const { using std::size_t; using std::hash; using std::string; size_t seed = 0; ofxGPUFont::hash_combine(seed, k.charcode); for(const auto & coord : k.coords){ ofxGPUFont::hash_combine(seed, coord); } return seed; } }; } namespace ofxGPUFont { class Font { struct Glyph { FT_UInt index; int32_t bufferIndex; int32_t curveCount; // Important glyph metrics in font units. FT_Pos width, height; FT_Pos bearingX; FT_Pos bearingY; FT_Pos advance; }; struct BufferGlyph { int32_t start, count; // range of bezier curves belonging to this glyph }; struct BufferCurve { glm::float32_t x0, y0, x1, y1, x2, y2; }; // keep this, just because I wrote it // though, we don't need it as openGL takes care // of managing the units // //static set gl_texture_units; //static set gl_texture_units_freed; //static int claimFreeTextureName(){ //if(gl_texture_units_freed.size() == 0){ //int unit = gl_texture_units.size(); //gl_texture_units.insert(unit); //return unit; //}else{ //auto unit_it = gl_texture_units_freed.begin(); //int unit = *unit_it; //gl_texture_units.insert(unit); //gl_texture_units_freed.erase(unit_it); //return unit; //} //} public: struct BufferVertex { float x, y, u, v; int32_t bufferIndex; float r, g, b, a; }; // // p0----------p1 // | | // | /\ | // | / \ | // | /____\ | // | / \ | // | | // p3----------p2 // // also has bufferIndex & uv coords struct BoundingBox { glm::vec4 p0; glm::vec4 p1; glm::vec4 p2; glm::vec4 p3; float u0; float u1; float v0; float v1; int32_t bufferIndex; void multiply(const glm::mat4 & mat){ p0 = mat * p0; p1 = mat * p1; p2 = mat * p2; p3 = mat * p3; } }; /// A structure to model a given axis of a variable font. struct FontVariationAxisParameters { /// The name of the variation axis. const char * name; /// The axis's minimum coordinate value. double minValue; /// The axis's maximum coordinate value. double maxValue; /// The axis's default coordinate value. FreeType computes meaningful default values for Adobe MM fonts. double defaultValue; }; struct FontVariationAxis { const char * name; double value; }; static bool setFontVariationAxes(FT_Library & library, FT_Face & face, FontVariationCoords & coords){ bool success = false; if(face->face_flags & FT_FACE_FLAG_MULTIPLE_MASTERS){ FT_MM_Var * master = NULL; if(FT_Get_MM_Var(face, &master) != 0){ return false; } if(master && master->num_axis){ if(FT_Set_Var_Design_Coordinates(face, FT_UInt(coords.size()), &coords[0]) != 0){ success = false; } } FT_Done_MM_Var(library, master); } return success; } bool setFontVariationAxes(FT_Library & library, FontVariationCoords & coords){ return Font::setFontVariationAxes(library, face, coords); } static bool setFontVariationAxes(FT_Library & library, FT_Face & face, const std::vector & variationAxes){ bool success = false; if(face->face_flags & FT_FACE_FLAG_MULTIPLE_MASTERS){ FT_MM_Var * master = NULL; if(FT_Get_MM_Var(face, &master) != 0){ return false; } if(master && master->num_axis){ std::vector coords(master->num_axis); if(FT_Get_Var_Design_Coordinates(face, FT_UInt(coords.size()), &coords[0]) == 0){ for(FT_UInt i = 0; i < master->num_axis; ++i){ for(const auto & variationAxis : variationAxes){ if(strcmp(variationAxis.name, master->axis[i].name) == 0){ coords[i] = DOUBLE_TO_F16DOT16(variationAxis.value); success = true; break; } } } } if(FT_Set_Var_Design_Coordinates(face, FT_UInt(coords.size()), &coords[0]) != 0){ success = false; } } FT_Done_MM_Var(library, master); } return success; } bool setFontVariationAxes(FT_Library & library, const std::vector & variationAxes){ return Font::setFontVariationAxes(library, face, variationAxes); } static bool setFontVariationAxis(FT_Library & library, FT_Face & face, const char * name, double coordinate){ bool success = false; if(face->face_flags & FT_FACE_FLAG_MULTIPLE_MASTERS){ FT_MM_Var * master = NULL; if(FT_Get_MM_Var(face, &master)){ return false; } if(master && master->num_axis){ std::vector coords(master->num_axis); if(!FT_Get_Var_Design_Coordinates(face, FT_UInt(coords.size()), &coords[0])){ for(FT_UInt i = 0; i < master->num_axis; ++i){ if(!strcmp(name, master->axis[i].name)){ coords[i] = DOUBLE_TO_F16DOT16(coordinate); success = true; break; } } } if(FT_Set_Var_Design_Coordinates(face, FT_UInt(coords.size()), &coords[0])){ success = false; } } FT_Done_MM_Var(library, master); } return success; } bool setFontVariationAxis(FT_Library & library, const std::string & name, float coordinate){ return Font::setFontVariationAxis(library, face, name.c_str(), coordinate); } static bool listFontVariationAxes(std::vector & axes, FT_Library & library, FT_Face & face){ if(face->face_flags & FT_FACE_FLAG_MULTIPLE_MASTERS){ FT_MM_Var * master = NULL; if(FT_Get_MM_Var(face, &master)){ return false; } axes.resize(master->num_axis); for(FT_UInt i = 0; i < master->num_axis; i++){ FontVariationAxisParameters & axis = axes[i]; axis.name = master->axis[i].name; axis.minValue = F16DOT16_TO_DOUBLE(master->axis[i].minimum); axis.maxValue = F16DOT16_TO_DOUBLE(master->axis[i].maximum); axis.defaultValue = F16DOT16_TO_DOUBLE(master->axis[i].def); } FT_Done_MM_Var(library, master); return true; } return false; } bool listFontVariationAxes(std::vector & axes, FT_Library & library){ return Font::listFontVariationAxes(axes, library, face); } static FT_Face loadFace(FT_Library library, const std::string & filename, std::string & error){ FT_Face face = NULL; //string pwd = ofSystem("pwd"); //pwd.erase(std::remove_if(pwd.begin(), pwd.end(), ::isspace), pwd.end()); //string fontPath = pwd + "/" + filename; FT_Error ftError = FT_New_Face(library, filename.c_str(), 0, &face); if(ftError){ const char * ftErrorStr = FT_Error_String(ftError); if(ftErrorStr){ error = std::string(ftErrorStr); }else{ // Fallback in case FT_Error_String returns NULL (e.g. if there // was an error or FT was compiled without error strings). std::stringstream stream; stream << "Error " << ftError; error = stream.str(); } return NULL; } if(!(face->face_flags & FT_FACE_FLAG_SCALABLE)){ error = "non-scalable fonts are not supported"; FT_Done_Face(face); return NULL; } return face; } // If hinting is enabled, worldSize must be an integer and defines the font size in pixels used for hinting. // Otherwise, worldSize can be an arbitrary floating-point value. Font(FT_Face face, int gpuTextureOffset, float worldSize = 1.0f, bool hinting = false) : face(face), hinting(hinting), worldSize(worldSize), gpuTextureOffset(gpuTextureOffset){ // TODO: modularize init, so we can initialize with settings and text if(gl_max_texture_size == -1){ glGetIntegerv(GL_MAX_TEXTURE_SIZE, &gl_max_texture_size); cout << "GL_MAX_TEXTURE_SIZE: " << ofToString(gl_max_texture_size) << endl; } if(hinting){ loadFlags = FT_LOAD_NO_BITMAP; kerningMode = FT_KERNING_DEFAULT; emSize = worldSize * 64; FT_Error error = FT_Set_Pixel_Sizes(face, 0, static_cast (std::ceil(worldSize))); if(error){ std::cerr << "[font] error while setting pixel size: " << error << std::endl; } }else{ loadFlags = FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP; kerningMode = FT_KERNING_UNSCALED; emSize = face->units_per_EM; } //int amounters = 0; //for(int i = 0; i < 1024; i++){ //testGL(10 + i); //amounters = i + 1; //cout << "added " << ofToString(amounters) << " LOL" << endl; //bool hasError = false; //GLenum err; //while((err = glGetError()) != GL_NO_ERROR){ //hasError = true; //// Process/log the error. //} //if(hasError){ //cout << "WHOOOPS" << endl; //break; //} //} glGenVertexArrays(1, &vao); glGenBuffers(1, &vbo); glGenBuffers(1, &ebo); #ifndef TARGET_OPENGLES glGenTextures(1, &glyphTextureName); glGenTextures(1, &curveTextureName); glGenBuffers(1, &glyphBufferName); glGenBuffers(1, &curveBufferName); #else glGenTextures(1, &glyphBufferName); glGenTextures(1, &curveBufferName); #endif glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, false, sizeof(BufferVertex), (void *)offsetof(BufferVertex, x)); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 2, GL_FLOAT, false, sizeof(BufferVertex), (void *)offsetof(BufferVertex, u)); glEnableVertexAttribArray(2); glVertexAttribIPointer(2, 1, GL_INT, sizeof(BufferVertex), (void *)offsetof(BufferVertex, bufferIndex)); glEnableVertexAttribArray(3); glVertexAttribPointer(3, 4, GL_FLOAT, false, sizeof(BufferVertex), (void *)offsetof(BufferVertex, r)); glBindVertexArray(0); //uploadBuffers(); #ifndef TARGET_OPENGLES glBindTexture(GL_TEXTURE_BUFFER, glyphTextureName); glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32I, glyphBufferName); glBindTexture(GL_TEXTURE_BUFFER, 0); glBindTexture(GL_TEXTURE_BUFFER, curveTextureName); glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, curveBufferName); glBindTexture(GL_TEXTURE_BUFFER, 0); #else #endif } int amountCurveTextureLayers = 1; int widthCurveTextureLayer = 0; int previousCurveBufferSize = 0; int bufferCurveSize = (sizeof(BufferCurve) / (sizeof(glm::float32_t) * 1)); void generateCurveTextureLayers(int amountCurves){ //cout << "generateCurveTextureLayers()" << endl; int curveBufferSize = amountCurves * bufferCurveSize; curveBufferSize = calculateUpperPowerOfTwo(curveBufferSize); int overshoot = curveBufferSize % gl_max_texture_size == 0 ? 0 : 1; int neededLayers = (curveBufferSize / gl_max_texture_size) + overshoot; // LOL //cout << "\tlayers needed: " << ofToString(neededLayers) << endl //<< "\tcurveBufferSize p2: " << ofToString(curveBufferSize) << endl //<< "\tcurveBufferSize or: " << ofToString(bufferCurves.size()) << endl //<< "\tsize of BufferCurve: " << ofToString(bufferCurveSize) << endl //<< "\ttotal glyphs: " << ofToString(glyphs.size()) << endl //; if(neededLayers != amountCurveTextureLayers || curveBufferSize > previousCurveBufferSize){ glDeleteTextures(1, &curveBufferName); glGenTextures(1, &curveBufferName); //glActiveTexture(GL_TEXTURE0 + curveBufferUnit); glBindTexture(GL_TEXTURE_2D_ARRAY, curveBufferName); if(neededLayers == 1){ widthCurveTextureLayer = curveBufferSize; glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RG32F, curveBufferSize, 1, neededLayers); }else{ //cout << ofToString(neededLayers) << "layers needed NEW" << endl; if(neededLayers > 4){ //neededLayers = 4; // TODO: implement multiple arrays // LOL }else{ } widthCurveTextureLayer = gl_max_texture_size; glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RG32F, gl_max_texture_size, 1, neededLayers); } glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glBindTexture(GL_TEXTURE_2D_ARRAY, 0); //glActiveTexture(GL_TEXTURE0); amountCurveTextureLayers = neededLayers; } previousCurveBufferSize = curveBufferSize; } void uploadCurveTextureLayers(){ glActiveTexture(GL_TEXTURE0 + curveBufferUnit); glBindTexture(GL_TEXTURE_2D_ARRAY, curveBufferName); for(int i = 0; i < amountCurveTextureLayers; i++){ int width = widthCurveTextureLayer; int height = 1; int third = (int)std::floor(gl_max_texture_size / 3); int layerPointer = (i * third) - (third % bufferCurveSize) + 1; //int layerPointer = i == 0 ? 0 : ////5461; //(i * gl_max_texture_size / int(3)) - ((gl_max_texture_size / int(3)) % bufferCurveSize) + 1; //cout << ofToString(i) << " - layer pointer " << ofToString(layerPointer) << endl; glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, width, height, 1, GL_RG, GL_FLOAT, &bufferCurves[layerPointer]); } glActiveTexture(GL_TEXTURE0); } ~Font(){ glDeleteVertexArrays(1, &vao); glDeleteBuffers(1, &vbo); glDeleteBuffers(1, &ebo); #ifndef TARGET_OPENGLES glDeleteTextures(1, &glyphTextureName); glDeleteTextures(1, &curveTextureName); glDeleteBuffers(1, &glyphBufferName); glDeleteBuffers(1, &curveBufferName); #else glDeleteTextures(1, &glyphBufferName); glDeleteTextures(1, &curveBufferName); #endif FT_Done_Face(face); } public: void setWorldSize(FT_Library & library, float worldSize){ if(worldSize == this->worldSize){ return; } this->worldSize = worldSize; if(!hinting){ return; } // We have to rebuild our buffers, because the outline coordinates can // change because of hinting. emSize = worldSize * 64; { // error scope FT_Error error = FT_Set_Pixel_Sizes(face, 0, static_cast (std::ceil(worldSize))); if(error){ std::cerr << "[font] error while setting pixel size: " << error << std::endl; } } bufferGlyphs.clear(); bufferCurves.clear(); for(auto it = glyphs.begin(); it != glyphs.end();){ uint32_t charcode = it->first.charcode; FT_UInt glyphIndex = it->second.index; setFontVariationAxes(library, const_cast (it->first.coords)); // danger? FT_Error error = FT_Load_Glyph(face, glyphIndex, loadFlags); if(error){ std::cerr << "[font] error while loading glyph for character " << charcode << ": " << error << std::endl; it = glyphs.erase(it); continue; } // This call will overwrite the glyph in the glyphs map. However, it // cannot invalidate the iterator because the glyph is already in // the map if we are here. buildGlyph(GlyphIdentity{charcode, it->first.coords}, glyphIndex); it++; } dirtyBuffers = true; //uploadBuffers(); } void prepareGlyphsForText(const std::vector & variationText, FT_Library & library, bool forceChange = false){ bool changed = false; if(forceChange){ bufferGlyphs.clear(); bufferCurves.clear(); glyphs.clear(); } int i = 0; stringstream text; for(const auto & glyphIdentity : variationText){ const uint32_t & charcode = glyphIdentity.charcode; if(charcode == '\0'){ // do we need this? break; } //cout << "yes, this is running" << endl; if(charcode == '\r' || charcode == '\n'){ //cout << "but charcode is r or n" << endl; continue; } if(glyphs.count(glyphIdentity) != 0){ continue; } setFontVariationAxes(library, const_cast (glyphIdentity.coords)); // danger? FT_UInt glyphIndex = FT_Get_Char_Index(face, charcode); if(!glyphIndex){ continue; } FT_Error error = FT_Load_Glyph(face, glyphIndex, loadFlags); if(error){ std::cerr << "[font] error while loading glyph for character " << charcode << ": " << error << std::endl; continue; } //text << (char)glyphIdentity.charcode //<< " " << ofToString(glyphIdentity.coords[0]) << " " //<< " (" << ofToString(bufferCurves.size()) << ")" << endl; buildGlyph(glyphIdentity, glyphIndex); changed = true; i++; } //cout << text.str() << endl; if(changed){ dirtyBuffers = true; glyphBufferUnit = 1; curveBufferUnit = 2; generateCurveTextureLayers(bufferCurves.size()); // Reupload the full buffer contents. To make this even more // dynamic, the buffers could be overallocated and only the added // data could be uploaded. uploadBuffers(); } } private: bool initializedGlyphsBufferTexture = false; bool initializedCurvesBufferTexture = false; bool dirtyBuffers = false; int calcTextureOffset(){ return gpuTextureOffset * 2; } void testGL(GLuint texture = 10){ //int size = 0; //glGetIntegerv(GL_MAX_TEXTURE_SIZE, &size); //cout << "GL_MAX_TEXTURE_SIZE: " << ofToString(size) << endl; //size = 0; //glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &size); //cout << "GL_MAX_ARRAY_TEXTURE_LAYERS: " << ofToString(size) << endl; //size = 0; GLsizei width = gl_max_texture_size; GLsizei height = 1; GLsizei layerCount = 4; GLsizei mipLevelCount = 1; //GLsizei bufSize = 42; //GLint * params[bufSize]; //for(int i = 0; i < bufSize; i++){ //params[i] = 0; //} //glGetInternalformativ(GL_TEXTURE_2D_ARRAY, //GL_RG32F, //GL_MAX_DEPTH, //bufSize / 2, //params[0]); //glGetInternalformativ(GL_TEXTURE_2D_ARRAY, //GL_RG32F, //GL_MAX_LAYERS, //bufSize / 2, //params[21]); //stringstream s; //s << "and here:" << endl; //for(int i = 0; i < bufSize; i++){ //s << ofToString(i) << ": " << ofToString(params[i]) << endl; //} //cout << s.str(); // Read you texels here. In the current example, we have 2*2*2 = 8 texels, with each texel being 4 GLubytes. GLubyte texels[layerCount][width * height]; for(int l = 0; l < layerCount; l++){ for(int i = 0; i < width * height; i++){ texels[l][i] = 127; } } glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D_ARRAY, texture); // Allocate the storage. glTexStorage3D(GL_TEXTURE_2D_ARRAY, mipLevelCount, GL_RG32F, width, height, layerCount); // Upload pixel data. // The first 0 refers to the mipmap level (level 0, since there's only 1) // The following 2 zeroes refers to the x and y offsets in case you only want to specify a subrectangle. // The final 0 refers to the layer index offset (we start from index 0 and have 2 levels). // Altogether you can specify a 3D box subset of the overall texture, but only one mip level at a time. for(int i = 0; i < (int)layerCount; i++){ //cout << "OFFSET " << ofToString(i) << endl; glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, width, height, layerCount, GL_RG, GL_FLOAT, texels[i]); } // Always set reasonable texture parameters glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } void uploadBuffers(){ if(dirtyBuffers){ //cout << "bufferGlyphs.size(): " << bufferGlyphs.size() << endl; //cout << "bufferCurves.size(): " << bufferCurves.size() << endl; #ifndef TARGET_OPENGLES glBindBuffer(GL_TEXTURE_BUFFER, glyphBufferName); glBufferData(GL_TEXTURE_BUFFER, sizeof(BufferGlyph) * bufferGlyphs.size(), bufferGlyphs.data(), GL_STATIC_DRAW); glBindBuffer(GL_TEXTURE_BUFFER, 0); glBindBuffer(GL_TEXTURE_BUFFER, curveBufferName); glBufferData(GL_TEXTURE_BUFFER, sizeof(BufferCurve) * bufferCurves.size(), bufferCurves.data(), GL_STATIC_DRAW); glBindBuffer(GL_TEXTURE_BUFFER, 0); #else int size; //glGetIntegerv(GL_MAX_SHADER_STORAGE_BLOCK_SIZE, &size); //cout << "GL_MAX_SHADER_STORAGE_BLOCK_SIZE: " << ofToString(size) << endl; int bufferGlyphsSize = bufferGlyphs.size() * sizeof(BufferGlyph); if(!isPowerOfTwo(bufferGlyphsSize)){ bufferGlyphsSize = calculateUpperPowerOfTwo(bufferGlyphsSize); } glActiveTexture(GL_TEXTURE0 + glyphBufferUnit); glBindTexture(GL_TEXTURE_2D, glyphBufferName); if(!initializedGlyphsBufferTexture){ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); initializedGlyphsBufferTexture = true; } glTexImage2D(GL_TEXTURE_2D, 0, GL_RG32I, bufferGlyphsSize, 1, 0, GL_RG_INTEGER, GL_INT, bufferGlyphs.data()); //int bufferCurvesSize = bufferCurves.size() * bufferCurveSize; //if(!isPowerOfTwo(bufferCurvesSize)){ //bufferCurvesSize = calculateUpperPowerOfTwo(bufferCurvesSize); //} //glActiveTexture(GL_TEXTURE0 + curveBufferUnit); //glBindTexture(GL_TEXTURE_2D, curveBufferName); //if(!initializedCurvesBufferTexture){ //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, //GL_NEAREST); //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, //GL_NEAREST); //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); //initializedCurvesBufferTexture = true; //} //glTexImage2D(GL_TEXTURE_2D, //0, //GL_RG32F, //bufferCurvesSize, //1, //0, //GL_RG, //GL_FLOAT, //bufferCurves.data()); glActiveTexture(GL_TEXTURE0); uploadCurveTextureLayers(); #endif dirtyBuffers = false; } } void buildGlyph(const GlyphIdentity & glyphIdentity, FT_UInt glyphIndex){ BufferGlyph bufferGlyph; bufferGlyph.start = static_cast (bufferCurves.size()); short start = 0; for(int i = 0; i < face->glyph->outline.n_contours; i++){ // Note: The end indices in face->glyph->outline.contours are inclusive. convertContour(bufferCurves, &face->glyph->outline, start, face->glyph->outline.contours[i], emSize); start = face->glyph->outline.contours[i] + 1; } bufferGlyph.count = static_cast (bufferCurves.size()) - bufferGlyph.start; int32_t bufferIndex = static_cast (bufferGlyphs.size()); bufferGlyphs.push_back(bufferGlyph); Glyph glyph; glyph.index = glyphIndex; glyph.bufferIndex = bufferIndex; glyph.curveCount = bufferGlyph.count; glyph.width = face->glyph->metrics.width; glyph.height = face->glyph->metrics.height; glyph.bearingX = face->glyph->metrics.horiBearingX; glyph.bearingY = face->glyph->metrics.horiBearingY; glyph.advance = face->glyph->metrics.horiAdvance; glyphs[glyphIdentity] = glyph; } // This function takes a single contour (defined by firstIndex and // lastIndex, both inclusive) from outline and converts it into individual // quadratic bezier curves, which are added to the curves vector. void convertContour(std::vector & all_curves, const FT_Outline * outline, short firstIndex, short lastIndex, float emSize){ // we might have to flip the direction // so let's buffer our buffercurves in a buffer curves of buffercurves std::vector curves; // See https://freetype.org/freetype2/docs/glyphs/glyphs-6.html // for a detailed description of the outline format. // // In short, a contour is a list of points describing line segments // and quadratic or cubic bezier curves that form a closed shape. // // TrueType fonts only contain quadratic bezier curves. OpenType fonts // may contain outline data in TrueType format or in Compact Font // Format, which also allows cubic beziers. However, in FreeType it is // (theoretically) possible to mix the two types of bezier curves, so // we handle both at the same time. // // Each point in the contour has a tag specifying its type // (FT_CURVE_TAG_ON, FT_CURVE_TAG_CONIC or FT_CURVE_TAG_CUBIC). // FT_CURVE_TAG_ON points sit exactly on the outline, whereas the // other types are control points for quadratic/conic bezier curves, // which in general do not sit exactly on the outline and are also // called off points. // // Some examples of the basic segments: // ON - ON ... line segment // ON - CONIC - ON ... quadratic bezier curve // ON - CUBIC - CUBIC - ON ... cubic bezier curve // // Cubic bezier curves must always be described by two CUBIC points // inbetween two ON points. For the points used in the TrueType format // (ON, CONIC) there is a special rule, that two consecutive points of // the same type imply a virtual point of the opposite type at their // exact midpoint. // // For example the sequence ON - CONIC - CONIC - ON describes two // quadratic bezier curves where the virtual point forms the joining // end point of the two curves: ON - CONIC - [ON] - CONIC - ON. // // Similarly the sequence ON - ON can be thought of as a line segment // or a quadratic bezier curve (ON - [CONIC] - ON). Because the // virtual point is at the exact middle of the two endpoints, the // bezier curve is identical to the line segment. // // The font shader only supports quadratic bezier curves, so we use // this virtual point rule to represent line segments as quadratic // bezier curves. // // Cubic bezier curves are slightly more difficult, since they have a // higher degree than the shader supports. Each cubic curve is // approximated by two quadratic curves according to the following // paper. This preserves C1-continuity (location of and tangents at // the end points of the cubic curve) and the paper even proves that // splitting at the parametric center minimizes the error due to the // degree reduction. One could also analyze the approximation error // and split the cubic curve, if the error is too large. However, // almost all fonts use "nice" cubic curves, resulting in very small // errors already (see also the section on Font Design in the paper). // // Quadratic Approximation of Cubic Curves // Nghia Truong, Cem Yuksel, Larry Seiler // https://ttnghia.github.io/pdf/QuadraticApproximation.pdf // https://doi.org/10.1145/3406178 if(firstIndex == lastIndex){ return; } short dIndex = 1; if(outline->flags & FT_OUTLINE_REVERSE_FILL){ short tmpIndex = lastIndex; lastIndex = firstIndex; firstIndex = tmpIndex; dIndex = -1; } auto convert = [emSize](const FT_Vector & v){ return glm::vec2( (float)v.x / emSize, (float)v.y / emSize ); }; auto makeMidpoint = [](const glm::vec2 & a, const glm::vec2 & b){ return 0.5f * (a + b); }; // Get Direction of a point pair. // Notice that we can ignore the middle point. auto getDirection = [](const glm::vec2 & p0, const glm::vec2 & p1){ return (p1.x - p0.x) * (p1.y + p0.y); }; // keep track of collected direction float direction = 0; // We will inject getDirection in makeCurve, // so anytime the curve is made it updates the direction. // This is less error prone than calling it separately. // wisdom: Any error you cannot make, you will not make. :) auto makeCurve = [&direction, getDirection](const glm::vec2 & p0, const glm::vec2 & p1, const glm::vec2 & p2){ BufferCurve result; result.x0 = p0.x; result.y0 = p0.y; result.x1 = p1.x; result.y1 = p1.y; result.x2 = p2.x; result.y2 = p2.y; direction += getDirection(p0, p2); return result; }; // Find a point that is on the curve and remove it from the list. glm::vec2 first; bool firstOnCurve = (outline->tags[firstIndex] & FT_CURVE_TAG_ON); if(firstOnCurve){ first = convert(outline->points[firstIndex]); firstIndex += dIndex; }else{ bool lastOnCurve = (outline->tags[lastIndex] & FT_CURVE_TAG_ON); if(lastOnCurve){ first = convert(outline->points[lastIndex]); lastIndex -= dIndex; }else{ first = makeMidpoint(convert(outline->points[firstIndex]), convert(outline->points[lastIndex])); // This is a virtual point, so we don't have to remove it. } } glm::vec2 start = first; glm::vec2 control = first; glm::vec2 previous = first; char previousTag = FT_CURVE_TAG_ON; for(short index = firstIndex; index != lastIndex + dIndex; index += dIndex){ glm::vec2 current = convert(outline->points[index]); char currentTag = FT_CURVE_TAG(outline->tags[index]); if(currentTag == FT_CURVE_TAG_CUBIC){ // No-op, wait for more points. control = previous; }else if(currentTag == FT_CURVE_TAG_ON){ if(previousTag == FT_CURVE_TAG_CUBIC){ glm::vec2 & b0 = start; glm::vec2 & b1 = control; glm::vec2 & b2 = previous; glm::vec2 & b3 = current; glm::vec2 c0 = b0 + 0.75f * (b1 - b0); glm::vec2 c1 = b3 + 0.75f * (b2 - b3); glm::vec2 d = makeMidpoint(c0, c1); curves.push_back(makeCurve(b0, c0, d)); curves.push_back(makeCurve(d, c1, b3)); }else if(previousTag == FT_CURVE_TAG_ON){ // Linear segment. curves.push_back(makeCurve(previous, makeMidpoint(previous, current), current)); }else{ // Regular bezier curve. curves.push_back(makeCurve(start, previous, current)); } start = current; control = current; }else{ /* currentTag == FT_CURVE_TAG_CONIC */ if(previousTag == FT_CURVE_TAG_ON){ // No-op, wait for third point. }else{ // Create virtual on point. glm::vec2 mid = makeMidpoint(previous, current); curves.push_back(makeCurve(start, previous, mid)); start = mid; control = mid; } } previous = current; previousTag = currentTag; } // Close the contour. if(previousTag == FT_CURVE_TAG_CUBIC){ glm::vec2 & b0 = start; glm::vec2 & b1 = control; glm::vec2 & b2 = previous; glm::vec2 & b3 = first; glm::vec2 c0 = b0 + 0.75f * (b1 - b0); glm::vec2 c1 = b3 + 0.75f * (b2 - b3); glm::vec2 d = makeMidpoint(c0, c1); curves.push_back(makeCurve(b0, c0, d)); curves.push_back(makeCurve(d, c1, b3)); }else if(previousTag == FT_CURVE_TAG_ON){ // Linear segment. curves.push_back(makeCurve(previous, makeMidpoint(previous, first), first)); }else{ curves.push_back(makeCurve(start, previous, first)); } if(direction < 0){ for(BufferCurve & curve : curves){ float tmp_x = curve.x0; float tmp_y = curve.y0; curve.x0 = curve.x2; curve.y0 = curve.y2; curve.x2 = tmp_x; curve.y2 = tmp_y; } std::reverse(curves.begin(), curves.end()); } all_curves.insert(all_curves.end(), curves.begin(), curves.end()); } public: void drawSetup(){ GLint location; #ifndef TARGET_OPENGLES location = glGetUniformLocation(program, "glyphs"); glUniform1i(location, 0); location = glGetUniformLocation(program, "curves"); glUniform1i(location, 1); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_BUFFER, glyphTextureName); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_BUFFER, curveTextureName); glActiveTexture(GL_TEXTURE0); #else glyphsUniformLocation = glGetUniformLocation(program, "glyphs"); glUniform1i(glyphsUniformLocation, glyphBufferUnit); curvesUniformLocation = glGetUniformLocation(program, "curves"); glUniform1i(curvesUniformLocation, curveBufferUnit); glActiveTexture(GL_TEXTURE0 + glyphBufferUnit); glBindTexture(GL_TEXTURE_2D, glyphBufferName); glActiveTexture(GL_TEXTURE0 + curveBufferUnit); glBindTexture(GL_TEXTURE_2D_ARRAY, curveBufferName); glActiveTexture(GL_TEXTURE0); #endif } float getLineHeight(float fontSize_px = 1){ float out = ((float)face->height * fontSize_px) / (float)face->units_per_EM * worldSize; return out; } float getAscender(float fontSize_px = 1){ float out = ((float)face->ascender * fontSize_px) / (float)face->units_per_EM * worldSize; return out; } float getDescender(float fontSize_px = 1){ float out = ((float)face->descender * fontSize_px) / (float)face->units_per_EM * worldSize; return out; } void collectBoundingBoxes(const std::vector & variationText, const std::vector & variationTextAppearance, BoundingBox & boundingBox, std::vector & boundingBoxes, float & advanceY, const bool vFlip = false, const float fontSize_px = 42, const float lineHeightMultiplier = 1, const float wrapWidth = 0){ float advanceX = 0; FT_UInt previous = 0; float minX = FLT_MAX; float minY = FLT_MAX; float maxX = -FLT_MAX; float maxY = -FLT_MAX; int i = 0; for(const GlyphIdentity & glyphIdentity : variationText){ const GlyphAppearance & glyphAppearance = variationTextAppearance[i]; if(glyphIdentity.charcode == '\0'){ break; } float letterFontSize_px = glyphAppearance.fontSize_px; const uint32_t & charcode = glyphIdentity.charcode; if(charcode == '\r'){ i++; continue; } if(charcode == '\n'){ advanceX = 0; advanceY += getLineHeight(fontSize_px) * lineHeightMultiplier; if(!vFlip){ advanceY *= -1; } if(hinting){ advanceY = std::round(advanceY); } i++; continue; } auto glyphIt = glyphs.find(glyphIdentity); Glyph & glyph = (glyphIt == glyphs.end()) ? glyphs.begin()->second : glyphIt->second; // in case we have no glyph, draw first one? // NOTE: should we do this? if(previous != 0 && glyph.index != 0){ FT_Vector kerning; FT_Error error = FT_Get_Kerning(face, previous, glyph.index, kerningMode, &kerning); if(!error){ advanceX += (((float)kerning.x * letterFontSize_px)) / emSize * worldSize; } } float letterSpacing = advanceX == 0 ? 0 : glyphAppearance.letterSpacing; advanceX += (letterSpacing * letterFontSize_px) * worldSize; // Do not emit quad for empty glyphs (whitespace). if(glyph.curveCount){ FT_Pos d = (FT_Pos)(emSize * dilation); float u0 = (float)(glyph.bearingX - d) / emSize; float v0 = (float)(glyph.bearingY - glyph.height - d) / emSize; float u1 = (float)(glyph.bearingX + glyph.width + d) / emSize; float v1 = (float)(glyph.bearingY + d) / emSize; float x0 = advanceX + u0 * worldSize * letterFontSize_px; float y0 = v0 * worldSize * letterFontSize_px; float x1 = advanceX + u1 * worldSize * letterFontSize_px; float y1 = v1 * worldSize * letterFontSize_px; if(vFlip){ float _v = v0; v0 = v1; v1 = _v; y0 = -v0 * worldSize * letterFontSize_px; y1 = -v1 * worldSize * letterFontSize_px; } y0 += advanceY; y1 += advanceY; minX = min(x0, min(x1, minX)); maxX = max(x0, max(x1, maxX)); minY = min(y0, min(y1, minY)); maxY = max(y0, max(y1, maxY)); boundingBoxes.push_back({ glm::vec4(x0, y0, 0, 1), glm::vec4(x1, y0, 0, 1), glm::vec4(x1, y1, 0, 1), glm::vec4(x0, y1, 0, 1), u0, u1, v0, v1, glyph.bufferIndex }); } advanceX += ((float)glyph.advance * letterFontSize_px) / emSize * worldSize; previous = glyph.index; i++; } boundingBox.p0 = glm::vec4(minX, minY, 0, 1); boundingBox.p1 = glm::vec4(maxX, minY, 0, 1); boundingBox.p2 = glm::vec4(maxX, maxY, 0, 1); boundingBox.p3 = glm::vec4(minX, maxY, 0, 1); advanceY += getLineHeight(fontSize_px) * lineHeightMultiplier; if(!vFlip){ advanceY *= -1; } if(hinting){ advanceY = std::round(advanceY); } } void collectVerticesAndIndices(const ofNode & node, const std::vector & variationTextAppearance, std::vector & boundingBoxes, std::vector & vertices, std::vector & indices){ int i = 0; for(BoundingBox & bb : boundingBoxes){ bool hasGoodCharacter = false; while(!hasGoodCharacter && i < variationTextAppearance.size() - 1){ if(variationTextAppearance[i].charcode == '\0' || variationTextAppearance[i].charcode == '\r' || variationTextAppearance[i].charcode == '\n'){ i++; }else{ hasGoodCharacter = true; } } bb.multiply(node.getGlobalTransformMatrix()); int32_t base = static_cast (vertices.size()); const std::array & color = variationTextAppearance[i].color; vertices.push_back(BufferVertex{bb.p0.x, bb.p0.y, bb.u0, bb.v0, bb.bufferIndex, color[0], color[1], color[2], color[3]}); vertices.push_back(BufferVertex{bb.p1.x, bb.p1.y, bb.u1, bb.v0, bb.bufferIndex, color[0], color[1], color[2], color[3]}); vertices.push_back(BufferVertex{bb.p2.x, bb.p2.y, bb.u1, bb.v1, bb.bufferIndex, color[0], color[1], color[2], color[3]}); vertices.push_back(BufferVertex{bb.p3.x, bb.p3.y, bb.u0, bb.v1, bb.bufferIndex, color[0], color[1], color[2], color[3]}); indices.insert(indices.end(), {base, base + 1, base + 2, base + 2, base + 3, base}); i++; } } void draw(const std::vector & vertices, const std::vector & indices){ glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(BufferVertex) * vertices.size(), vertices.data(), GL_STREAM_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(int32_t) * indices.size(), indices.data(), GL_STREAM_DRAW); glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0); glBindVertexArray(0); } private: FT_Face face; // Whether hinting is enabled for this instance. // Note that hinting changes how we operate FreeType: // If hinting is not enabled, we scale all coordinates ourselves (see comment for emSize). // If hinting is enabled, we must let FreeType scale the outlines for the hinting to work properly. // The variables loadFlags and kerningMode are set in the constructor and control this scaling behavior. bool hinting; FT_Int32 loadFlags; FT_Kerning_Mode kerningMode; // Size of the em square used to convert metrics into em-relative values, // which can then be scaled to the worldSize. We do the scaling ourselves in // floating point to support arbitrary world sizes (whereas the fixed-point // numbers used by FreeType do not have enough resolution if the world size // is small). // Following the FreeType convention, if hinting (and therefore scaling) is enabled, // this value is in 1/64th of a pixel (compatible with 26.6 fixed point numbers). // If hinting/scaling is not enabled, this value is in font units. float emSize; float worldSize; GLuint vao, vbo, ebo; #ifndef TARGET_OPENGLES GLuint glyphTextureName, curveTextureName; #else GLint glyphsUniformLocation, curvesUniformLocation; #endif GLuint glyphBufferName, curveBufferName; GLuint glyphBufferUnit, curveBufferUnit; std::vector bufferGlyphs; std::vector bufferCurves; std::vector bufferCurveLayerPointers = {0}; std::unordered_map glyphs; static GLint gl_max_texture_size; public: // ID of the shader program to use. GLuint program = 0; // The glyph quads are expanded by this amount to enable proper // anti-aliasing. Value is relative to emSize. float dilation = 0; int gpuTextureOffset = 0; }; static std::shared_ptr loadFont(FT_Library & library, const std::string & filename, int gpuTextureOffset = 0, float worldSize = 1.0f, bool hinting = false){ std::string error; FT_Face face = Font::loadFace(library, filename, error); if(error != ""){ std::cerr << "ofxGPUFont::font.hpp[" << __LINE__ << "] FT failed to load " << filename << ": " << error << std::endl; return std::shared_ptr {}; }else{ std::cout << "ofxGPUFont::font.hpp[" << __LINE__ << "] FT loaded " << filename << ": " << error << std::endl; } return std::make_shared (face, gpuTextureOffset, worldSize, hinting); } static void initializeFont(FT_Library & library, const std::string & filename, shared_ptr & mainFont, int gpuTextureOffset){ cout << "initializeFont" << endl; auto font = loadFont(library, filename, gpuTextureOffset); if(!font){ return; } font->dilation = 0.1f; mainFont = std::move(font); } }