From 6661f7eb0f024aee901634456dedc7fea9bc42f5 Mon Sep 17 00:00:00 2001 From: themancalledjakob Date: Fri, 31 Mar 2023 16:45:23 +0200 Subject: [PATCH] initital commit --- .gitignore | 56 ++ addon_config.mk | 65 ++ data/ofxGPUFont/shaders/GL3/background.frag | 12 + data/ofxGPUFont/shaders/GL3/background.vert | 15 + data/ofxGPUFont/shaders/GL3/font.frag | 156 ++++ data/ofxGPUFont/shaders/GL3/font.vert | 19 + example/addons.make | 1 + example/clean.sh | 14 + example/src/defer.hpp | 9 + example/src/gpufont/font.hpp | 767 ++++++++++++++++++++ example/src/gpufont/glm.hpp | 18 + example/src/gpufont/maingpufont.hpp | 717 ++++++++++++++++++ example/src/gpufont/shader_catalog.cpp | 191 +++++ example/src/gpufont/shader_catalog.hpp | 32 + example/src/main.cpp | 21 + example/src/ofApp.cpp | 191 +++++ example/src/ofApp.h | 97 +++ src/glm.hpp | 18 + src/ofxGPUFont.h | 1 + 19 files changed, 2400 insertions(+) create mode 100644 .gitignore create mode 100644 addon_config.mk create mode 100644 data/ofxGPUFont/shaders/GL3/background.frag create mode 100644 data/ofxGPUFont/shaders/GL3/background.vert create mode 100644 data/ofxGPUFont/shaders/GL3/font.frag create mode 100644 data/ofxGPUFont/shaders/GL3/font.vert create mode 100644 example/addons.make create mode 100755 example/clean.sh create mode 100644 example/src/defer.hpp create mode 100644 example/src/gpufont/font.hpp create mode 100644 example/src/gpufont/glm.hpp create mode 100644 example/src/gpufont/maingpufont.hpp create mode 100644 example/src/gpufont/shader_catalog.cpp create mode 100644 example/src/gpufont/shader_catalog.hpp create mode 100644 example/src/main.cpp create mode 100644 example/src/ofApp.cpp create mode 100644 example/src/ofApp.h create mode 100644 src/glm.hpp create mode 100644 src/ofxGPUFont.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d33f28 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# don't push project files +*.cbp +*.sln +*.vcx* +*.workspace* +config.make +Makefile +*.xcodeproj +*.plist +*.xcconfig +*.suo + +*.depend +*.layout +*.mode*v3 +*.pbxuser +*.app* +*.DS_* +*.xcworkspacedata +xcuserdata/ +project.xcworkspace + +*.opensdf +*.sdf +*.suo +*.ipch + +.svn/ +obj/ +bin/ +build/ +!data/ + +.cache +.ccls-cache + +# don't push backup files/directories +*.bk + +# vim +*.swp +*.swo + +# qtcreator +*.qbs +*.qbs.user +build-* + +compile_commands.json +compile_commands* +.vroot +.ycm_extra_conf.py +*.nohup + +# prevent accidental leaks +celines-fonts diff --git a/addon_config.mk b/addon_config.mk new file mode 100644 index 0000000..779f044 --- /dev/null +++ b/addon_config.mk @@ -0,0 +1,65 @@ +# All variables and this file are optional, if they are not present the PG and the +# makefiles will try to parse the correct values from the file system. +# +# Variables that specify exclusions can use % as a wildcard to specify that anything in +# that position will match. A partial path can also be specified to, for example, exclude +# a whole folder from the parsed paths from the file system +# +# Variables can be specified using = or += +# = will clear the contents of that variable both specified from the file or the ones parsed +# from the file system +# += will add the values to the previous ones in the file or the ones parsed from the file +# system +# +# The PG can be used to detect errors in this file, just create a new project with this addon +# and the PG will write to the console the kind of error and in which line it is + +meta: + ADDON_NAME = ofxGPUFont + ADDON_DESCRIPTION = draw font shapes with gpu + ADDON_AUTHOR = Jakob Schloetter + ADDON_TAGS = "typography font" + ADDON_URL = https://gitlab.com/pointerstudio/utils/ofxGPUFont + +common: + # dependencies with other addons, a list of them separated by spaces + # or use += in several lines + # ADDON_DEPENDENCIES = + + # include search paths, this will be usually parsed from the file system + # but if the addon or addon libraries need special search paths they can be + # specified here separated by spaces or one per line using += + # ADDON_INCLUDES = + + # any special flag that should be passed to the compiler when using this + # addon + # ADDON_CFLAGS = + + # any special flag that should be passed to the linker when using this + # addon, also used for system libraries with -lname + # ADDON_LDFLAGS = + + # linux only, any library that should be included in the project using + # pkg-config + # ADDON_PKG_CONFIG_LIBRARIES = + + # osx/iOS only, any framework that should be included in the project + # ADDON_FRAMEWORKS = + + # source files, these will be usually parsed from the file system looking + # in the src folders in libs and the root of the addon. if your addon needs + # to include files in different places or a different set of files per platform + # they can be specified here + # ADDON_SOURCES = + + # some addons need resources to be copied to the bin/data folder of the project + # specify here any files that need to be copied, you can use wildcards like * and ? + # ADDON_DATA = + ADDON_DATA = data/ofxGPUFont + + # when parsing the file system looking for libraries exclude this for all or + # a specific platform + # ADDON_LIBS_EXCLUDE = + +emscripten: +linux64: diff --git a/data/ofxGPUFont/shaders/GL3/background.frag b/data/ofxGPUFont/shaders/GL3/background.frag new file mode 100644 index 0000000..6d8baa7 --- /dev/null +++ b/data/ofxGPUFont/shaders/GL3/background.frag @@ -0,0 +1,12 @@ +#version 330 core + +in vec2 position; + +out vec3 color; + +void main() { + float t = (position.y + 1.0) / 2.0; + vec3 bottom = vec3(75.0, 55.0, 201.0) / 255.0; + vec3 top = vec3(0.0, 12.0, 0.0) / 255.0; + color = mix(bottom, top, t); +} diff --git a/data/ofxGPUFont/shaders/GL3/background.vert b/data/ofxGPUFont/shaders/GL3/background.vert new file mode 100644 index 0000000..c6b01be --- /dev/null +++ b/data/ofxGPUFont/shaders/GL3/background.vert @@ -0,0 +1,15 @@ +#version 330 core + +const vec2 vertices[4] = vec2[4]( + vec2(-1.0, -1.0), + vec2( 1.0, -1.0), + vec2(-1.0, 1.0), + vec2( 1.0, 1.0) +); + +out vec2 position; + +void main() { + position = vertices[gl_VertexID]; + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); +} diff --git a/data/ofxGPUFont/shaders/GL3/font.frag b/data/ofxGPUFont/shaders/GL3/font.frag new file mode 100644 index 0000000..71c33f0 --- /dev/null +++ b/data/ofxGPUFont/shaders/GL3/font.frag @@ -0,0 +1,156 @@ +#version 330 core + +// Based on: http://wdobbie.com/post/gpu-text-rendering-with-vector-textures/ + +struct Glyph { + int start, count; +}; + +struct Curve { + vec2 p0, p1, p2; +}; + +uniform isamplerBuffer glyphs; +uniform samplerBuffer curves; +uniform vec4 color; + + +// Controls for debugging and exploring: + +// Size of the window (in pixels) used for 1-dimensional anti-aliasing along each rays. +// 0 - no anti-aliasing +// 1 - normal anti-aliasing +// >=2 - exaggerated effect +uniform float antiAliasingWindowSize = 1.0; + +// Enable a second ray along the y-axis to achieve 2-dimensional anti-aliasing. +uniform bool enableSuperSamplingAntiAliasing = true; + +// Draw control points for debugging (green - on curve, magenta - off curve). +uniform bool enableControlPointsVisualization = false; + + +in vec2 uv; +flat in int bufferIndex; + +out vec4 result; + +Glyph loadGlyph(int index) { + Glyph result; + ivec2 data = texelFetch(glyphs, index).xy; + result.start = data.x; + result.count = data.y; + return result; +} + +Curve loadCurve(int index) { + Curve result; + result.p0 = texelFetch(curves, 3*index+0).xy; + result.p1 = texelFetch(curves, 3*index+1).xy; + result.p2 = texelFetch(curves, 3*index+2).xy; + return result; +} + +float computeCoverage(float inverseDiameter, vec2 p0, vec2 p1, vec2 p2) { + if (p0.y > 0 && p1.y > 0 && p2.y > 0) return 0.0; + if (p0.y < 0 && p1.y < 0 && p2.y < 0) return 0.0; + + // Note: Simplified from abc formula by extracting a factor of (-2) from b. + vec2 a = p0 - 2*p1 + p2; + vec2 b = p0 - p1; + vec2 c = p0; + + float t0, t1; + if (abs(a.y) >= 1e-5) { + // Quadratic segment, solve abc formula to find roots. + float radicand = b.y*b.y - a.y*c.y; + if (radicand <= 0) return 0.0; + + float s = sqrt(radicand); + t0 = (b.y - s) / a.y; + t1 = (b.y + s) / a.y; + } else { + // Linear segment, avoid division by a.y, which is near zero. + // There is only one root, so we have to decide which variable to + // assign it to based on the direction of the segment, to ensure that + // the ray always exits the shape at t0 and enters at t1. For a + // quadratic segment this works 'automatically', see readme. + float t = p0.y / (p0.y - p2.y); + if (p0.y < p2.y) { + t0 = -1.0; + t1 = t; + } else { + t0 = t; + t1 = -1.0; + } + } + + float alpha = 0; + + if (t0 >= 0 && t0 < 1) { + float x = (a.x*t0 - 2.0*b.x)*t0 + c.x; + alpha += clamp(x * inverseDiameter + 0.5, 0, 1); + } + + if (t1 >= 0 && t1 < 1) { + float x = (a.x*t1 - 2.0*b.x)*t1 + c.x; + alpha -= clamp(x * inverseDiameter + 0.5, 0, 1); + } + + return alpha; +} + +vec2 rotate(vec2 v) { + return vec2(v.y, -v.x); +} + +void main() { + float alpha = 0; + + // Inverse of the diameter of a pixel in uv units for anti-aliasing. + vec2 inverseDiameter = 1.0 / (antiAliasingWindowSize * fwidth(uv)); + + Glyph glyph = loadGlyph(bufferIndex); + for (int i = 0; i < glyph.count; i++) { + Curve curve = loadCurve(glyph.start + i); + + vec2 p0 = curve.p0 - uv; + vec2 p1 = curve.p1 - uv; + vec2 p2 = curve.p2 - uv; + + alpha += computeCoverage(inverseDiameter.x, p0, p1, p2); + if (enableSuperSamplingAntiAliasing) { + alpha += computeCoverage(inverseDiameter.y, rotate(p0), rotate(p1), rotate(p2)); + } + } + + if (enableSuperSamplingAntiAliasing) { + alpha *= 0.5; + } + + alpha = clamp(alpha, 0.0, 1.0); + result = color * alpha; + + if (enableControlPointsVisualization) { + // Visualize control points. + vec2 fw = fwidth(uv); + float r = 4.0 * 0.5 * (fw.x + fw.y); + for (int i = 0; i < glyph.count; i++) { + Curve curve = loadCurve(glyph.start + i); + + vec2 p0 = curve.p0 - uv; + vec2 p1 = curve.p1 - uv; + vec2 p2 = curve.p2 - uv; + + if (dot(p0, p0) < r*r || dot(p2, p2) < r*r) { + result = vec4(0, 1, 0, 1); + return; + } + + if (dot(p1, p1) < r*r) { + result = vec4(1, 0, 1, 1); + return; + } + } + } +} diff --git a/data/ofxGPUFont/shaders/GL3/font.vert b/data/ofxGPUFont/shaders/GL3/font.vert new file mode 100644 index 0000000..5cd10fa --- /dev/null +++ b/data/ofxGPUFont/shaders/GL3/font.vert @@ -0,0 +1,19 @@ +#version 330 core + +uniform mat4 projection; +uniform mat4 view; +uniform mat4 model; +uniform float z; + +layout (location = 0) in vec2 vertexPosition; +layout (location = 1) in vec2 vertexUV; +layout (location = 2) in int vertexIndex; + +out vec2 uv; +flat out int bufferIndex; + +void main() { + gl_Position = projection * view * model * vec4(vertexPosition, z, 1); + uv = vertexUV; + bufferIndex = vertexIndex; +} diff --git a/example/addons.make b/example/addons.make new file mode 100644 index 0000000..67ec9ee --- /dev/null +++ b/example/addons.make @@ -0,0 +1 @@ +ofxGPUFont diff --git a/example/clean.sh b/example/clean.sh new file mode 100755 index 0000000..f1cfc89 --- /dev/null +++ b/example/clean.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +PREVIOUS_DIR=$(pwd) + +cd $DIR + +emmake make clean && make clean && rm -rf obj && rm -rf ../../obj +rm -rf ../../../libs/openFrameworksCompiled/lib/linux64/obj +rm -rf ../../../libs/openFrameworksCompiled/lib/linux64/libopenFrameworks.a +rm -rf ../../../libs/openFrameworksCompiled/lib/emscripten/obj +rm -rf ../../../libs/openFrameworksCompiled/lib/emscripten/libopenFrameworks.bc + +cd $PREVIOUS_DIR diff --git a/example/src/defer.hpp b/example/src/defer.hpp new file mode 100644 index 0000000..ab25465 --- /dev/null +++ b/example/src/defer.hpp @@ -0,0 +1,9 @@ +// Source: https://stackoverflow.com/a/42060129 +#ifndef defer +struct defer_dummy {}; +template struct deferrer { F f; ~deferrer() { f(); } }; +template deferrer operator*(defer_dummy, F f) { return {f}; } +#define DEFER_(LINE) zz_defer##LINE +#define DEFER(LINE) DEFER_(LINE) +#define defer auto DEFER(__LINE__) = defer_dummy{} *[&]() +#endif // defer diff --git a/example/src/gpufont/font.hpp b/example/src/gpufont/font.hpp new file mode 100644 index 0000000..0b97aee --- /dev/null +++ b/example/src/gpufont/font.hpp @@ -0,0 +1,767 @@ +// 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 "ofMain.h" + +#include +#include FT_FREETYPE_H +#include FT_MULTIPLE_MASTERS_H + +#define F26DOT6_TO_DOUBLE(x) (1 / 64. * double(x)) +#define F16DOT16_TO_DOUBLE(x) (1 / 65536. * double(x)) +#define DOUBLE_TO_F16DOT16(x) FT_Fixed(65536. * x) + +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 { + float x0, y0, x1, y1, x2, y2; + }; + + struct BufferVertex { + float x, y, u, v; + int32_t bufferIndex; + }; + + public: + + 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 listFontVariationAxes(std::vector &axes, FreetypeHandle *library, FontHandle *font) { + //if (font->face->face_flags&FT_FACE_FLAG_MULTIPLE_MASTERS) { + //FT_MM_Var *master = NULL; + //if (FT_Get_MM_Var(font->face, &master)) + //return false; + //axes.resize(master->num_axis); + //for (FT_UInt i = 0; i < master->num_axis; i++) { + //FontVariationAxis &axis = axes[i]; + //axis.name = master->axis[i].name; + //axis.minValue = master->axis[i].minimum; + //axis.maxValue = master->axis[i].maximum; + //axis.defaultValue = master->axis[i].def; + //} + //FT_Done_MM_Var(library->library, master); + //return true; + //} + //return false; +//} + static FT_Face loadFace(FT_Library library, const std::string & filename, std::string & error){ + FT_Face face = NULL; + + string pwd = ofSystem("pwd"); + cout << "pwd: " << pwd << endl; + pwd.erase(std::remove_if(pwd.begin(), pwd.end(), ::isspace), pwd.end()); + cout << "fontPAth: " << pwd << "/" << filename << endl; + string fontPath = pwd + "/" + filename; + + FT_Error ftError = FT_New_Face(library, fontPath.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, float worldSize = 1.0f, bool hinting = false) : face(face), worldSize(worldSize), hinting(hinting){ + + 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; + } + + glGenVertexArrays(1, &vao); + + glGenBuffers(1, &vbo); + glGenBuffers(1, &ebo); + + glGenTextures(1, &glyphTexture); + glGenTextures(1, &curveTexture); + + glGenBuffers(1, &glyphBuffer); + glGenBuffers(1, &curveBuffer); + + 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)); + glBindVertexArray(0); + + { + uint32_t charcode = 0; + FT_UInt glyphIndex = 0; + FT_Error error = FT_Load_Glyph(face, glyphIndex, loadFlags); + if(error){ + std::cerr << "[font] error while loading undefined glyph: " << error << std::endl; + // Continue, because we always want an entry for the undefined glyph in our glyphs map! + } + + buildGlyph(charcode, glyphIndex); + } + + for(uint32_t charcode = 32; charcode < 128; charcode++){ + 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; + } + + buildGlyph(charcode, glyphIndex); + } + + uploadBuffers(); + + glBindTexture(GL_TEXTURE_BUFFER, glyphTexture); + glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32I, glyphBuffer); + glBindTexture(GL_TEXTURE_BUFFER, 0); + + glBindTexture(GL_TEXTURE_BUFFER, curveTexture); + glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, curveBuffer); + glBindTexture(GL_TEXTURE_BUFFER, 0); + } + + ~Font(){ + glDeleteVertexArrays(1, &vao); + + glDeleteBuffers(1, &vbo); + glDeleteBuffers(1, &ebo); + + glDeleteTextures(1, &glyphTexture); + glDeleteTextures(1, &curveTexture); + + glDeleteBuffers(1, &glyphBuffer); + glDeleteBuffers(1, &curveBuffer); + + FT_Done_Face(face); + } + + public: + void setWorldSize(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; + 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; + FT_UInt glyphIndex = it->second.index; + + 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(charcode, glyphIndex); + it++; + } + + uploadBuffers(); + } + + void prepareGlyphsForText(const std::string & text){ + bool changed = false; + + for(const char * textIt = text.c_str(); *textIt != '\0';){ + uint32_t charcode = decodeCharcode(&textIt); + + if(charcode == '\r' || charcode == '\n'){ + continue; + } + if(glyphs.count(charcode) != 0){ + continue; + } + + 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; + } + + buildGlyph(charcode, glyphIndex); + changed = true; + } + + if(changed){ + // 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: + void uploadBuffers(){ + glBindBuffer(GL_TEXTURE_BUFFER, glyphBuffer); + glBufferData(GL_TEXTURE_BUFFER, sizeof(BufferGlyph) * bufferGlyphs.size(), bufferGlyphs.data(), GL_STATIC_DRAW); + glBindBuffer(GL_TEXTURE_BUFFER, 0); + + glBindBuffer(GL_TEXTURE_BUFFER, curveBuffer); + glBufferData(GL_TEXTURE_BUFFER, sizeof(BufferCurve) * bufferCurves.size(), bufferCurves.data(), GL_STATIC_DRAW); + glBindBuffer(GL_TEXTURE_BUFFER, 0); + } + + void buildGlyph(uint32_t charcode, 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[charcode] = 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 & curves, const FT_Outline * outline, short firstIndex, short lastIndex, float emSize){ + // 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); + }; + + auto makeCurve = [](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; + 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)); + } + } + + // 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. + uint32_t decodeCharcode(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; + } + + public: + void drawSetup(){ + GLint location; + + location = glGetUniformLocation(program, "glyphs"); + glUniform1i(location, 0); + location = glGetUniformLocation(program, "curves"); + glUniform1i(location, 1); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_BUFFER, glyphTexture); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_BUFFER, curveTexture); + + glActiveTexture(GL_TEXTURE0); + } + + void draw(float x, float y, float z, const std::string & text){ + float originalX = x; + + glBindVertexArray(vao); + + std::vector vertices; + std::vector indices; + + FT_UInt previous = 0; + for(const char * textIt = text.c_str(); *textIt != '\0';){ + uint32_t charcode = decodeCharcode(&textIt); + + if(charcode == '\r'){ + continue; + } + + if(charcode == '\n'){ + x = originalX; + y -= (float)face->height / (float)face->units_per_EM * worldSize; + if(hinting){ + y = std::round(y); + } + continue; + } + + auto glyphIt = glyphs.find(charcode); + Glyph & glyph = (glyphIt == glyphs.end()) ? glyphs[0] : glyphIt->second; + + if(previous != 0 && glyph.index != 0){ + FT_Vector kerning; + FT_Error error = FT_Get_Kerning(face, previous, glyph.index, kerningMode, &kerning); + if(!error){ + x += (float)kerning.x / emSize * 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 = x + u0 * worldSize; + float y0 = y + v0 * worldSize; + float x1 = x + u1 * worldSize; + float y1 = y + v1 * worldSize; + + int32_t base = static_cast (vertices.size()); + vertices.push_back(BufferVertex{x0, y0, u0, v0, glyph.bufferIndex}); + vertices.push_back(BufferVertex{x1, y0, u1, v0, glyph.bufferIndex}); + vertices.push_back(BufferVertex{x1, y1, u1, v1, glyph.bufferIndex}); + vertices.push_back(BufferVertex{x0, y1, u0, v1, glyph.bufferIndex}); + indices.insert(indices.end(), {base, base + 1, base + 2, base + 2, base + 3, base}); + } + + x += (float)glyph.advance / emSize * worldSize; + previous = glyph.index; + } + + 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); + } + + struct BoundingBox { + float minX, minY, maxX, maxY; + }; + + BoundingBox measure(float x, float y, const std::string & text){ + BoundingBox bb; + bb.minX = +std::numeric_limits ::infinity(); + bb.minY = +std::numeric_limits ::infinity(); + bb.maxX = -std::numeric_limits ::infinity(); + bb.maxY = -std::numeric_limits ::infinity(); + + float originalX = x; + + FT_UInt previous = 0; + for(const char * textIt = text.c_str(); *textIt != '\0';){ + uint32_t charcode = decodeCharcode(&textIt); + + if(charcode == '\r'){ + continue; + } + + if(charcode == '\n'){ + x = originalX; + y -= (float)face->height / (float)face->units_per_EM * worldSize; + if(hinting){ + y = std::round(y); + } + continue; + } + + auto glyphIt = glyphs.find(charcode); + Glyph & glyph = (glyphIt == glyphs.end()) ? glyphs[0] : glyphIt->second; + + if(previous != 0 && glyph.index != 0){ + FT_Vector kerning; + FT_Error error = FT_Get_Kerning(face, previous, glyph.index, kerningMode, &kerning); + if(!error){ + x += (float)kerning.x / emSize * worldSize; + } + } + + // Note: Do not apply dilation here, we want to calculate exact bounds. + float u0 = (float)(glyph.bearingX) / emSize; + float v0 = (float)(glyph.bearingY - glyph.height) / emSize; + float u1 = (float)(glyph.bearingX + glyph.width) / emSize; + float v1 = (float)(glyph.bearingY) / emSize; + + float x0 = x + u0 * worldSize; + float y0 = y + v0 * worldSize; + float x1 = x + u1 * worldSize; + float y1 = y + v1 * worldSize; + + if(x0 < bb.minX){ + bb.minX = x0; + } + if(y0 < bb.minY){ + bb.minY = y0; + } + if(x1 > bb.maxX){ + bb.maxX = x1; + } + if(y1 > bb.maxY){ + bb.maxY = y1; + } + + x += (float)glyph.advance / emSize * worldSize; + previous = glyph.index; + } + + return bb; + } + + 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; + GLuint glyphTexture, curveTexture; + GLuint glyphBuffer, curveBuffer; + + std::vector bufferGlyphs; + std::vector bufferCurves; + std::unordered_map glyphs; + + 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; +}; diff --git a/example/src/gpufont/glm.hpp b/example/src/gpufont/glm.hpp new file mode 100644 index 0000000..e6c9e80 --- /dev/null +++ b/example/src/gpufont/glm.hpp @@ -0,0 +1,18 @@ +#pragma once + +#define GLM_FORCE_RADIANS +#define GLM_ENABLE_EXPERIMENTAL + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include diff --git a/example/src/gpufont/maingpufont.hpp b/example/src/gpufont/maingpufont.hpp new file mode 100644 index 0000000..16b4935 --- /dev/null +++ b/example/src/gpufont/maingpufont.hpp @@ -0,0 +1,717 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ofMain.h" + +#include +#include FT_FREETYPE_H +#include FT_MULTIPLE_MASTERS_H + +#include "glm.hpp" + +#include "shader_catalog.hpp" + +#define F26DOT6_TO_DOUBLE(x) (1 / 64. * double(x)) +#define F16DOT16_TO_DOUBLE(x) (1 / 65536. * double(x)) +#define DOUBLE_TO_F16DOT16(x) FT_Fixed(65536. * x) + +#include "font.hpp" + +float wght = 700.0; +float wghtStep = 10.0; +std::string currentFontPath = ""; + +struct Transform { + float fovy = glm::radians(60.0f); + float distance = 0.42f; + glm::mat3 rotation = glm::mat3(1.0f); + glm::vec3 position = glm::vec3(0.0f); + + glm::mat4 getProjectionMatrix(float aspect){ + return glm::perspective( /* fovy = */ glm::radians(60.0f), aspect, 0.002f, 12.000f); + } + + glm::mat4 getViewMatrix(){ + auto translation = glm::translate(position); + return glm::lookAt(glm::vec3(0, 0, distance), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0)) * glm::mat4(rotation) * translation; + } +}; + +struct DragController { + enum class Action { + NONE, + TRANSLATE, + ROTATE_TURNTABLE, + ROTATE_TRACKBALL + }; + + Transform * transform = nullptr; + int activeButton = -1; + Action activeAction = Action::NONE; + + double dragX, dragY; + double wrapX, wrapY; + double virtualX, virtualY; + glm::vec3 dragTarget; + + void reset(){ + // Reset transform. + *transform = Transform{}; + + // Cancel active action, if any. + activeButton = -1; + activeAction = Action::NONE; + } + + bool unprojectMousePositionToXYPlane(GLFWwindow * window, double x, double y, glm::vec3 & result){ + int iwidth = 0, iheight = 0; + glfwGetWindowSize(window, &iwidth, &iheight); + + double width = iwidth; + double height = iheight; + + glm::mat4 projection = transform->getProjectionMatrix(float(width / height)); + glm::mat4 view = transform->getViewMatrix(); + + double relX = x / width * 2.0 - 1.0; + double relY = y / height * 2.0 - 1.0; + + glm::vec4 clipPos = glm::vec4(float(relX), -float(relY), 0.5f, 1.0f); + glm::vec4 worldPos = glm::inverse(projection * view) * clipPos; + worldPos *= 1.0f / worldPos.w; + + glm::vec3 pos = glm::vec3(glm::column(glm::inverse(view), 3)); + glm::vec3 dir = glm::normalize(glm::vec3(worldPos) - pos); + float t = -pos.z / dir.z; + + result = pos + t * dir; + return t > 0.0f; + } + + void onMouseButton(GLFWwindow * window, int button, int action, int mods){ + if(action == GLFW_PRESS && activeButton == -1){ + activeButton = button; + + if(mods & GLFW_MOD_CONTROL){ + activeAction = Action::TRANSLATE; + }else{ + if(activeButton == GLFW_MOUSE_BUTTON_2){ + activeAction = Action::TRANSLATE; + }else if(activeButton == GLFW_MOUSE_BUTTON_3){ + activeAction = Action::ROTATE_TURNTABLE; + }else{ + activeAction = Action::ROTATE_TRACKBALL; + } + } + + glfwGetCursorPos(window, &dragX, &dragY); + wrapX = std::numeric_limits ::quiet_NaN(); + wrapY = std::numeric_limits ::quiet_NaN(); + virtualX = dragX; + virtualY = dragY; + + glm::vec3 target; + bool ok = unprojectMousePositionToXYPlane(window, dragX, dragY, target); + dragTarget = ok ? target : glm::vec3(); + }else if(action == GLFW_RELEASE && activeButton == button){ + activeButton = -1; + activeAction = Action::NONE; + dragX = 0.0; + dragY = 0.0; + wrapX = std::numeric_limits ::quiet_NaN(); + wrapY = std::numeric_limits ::quiet_NaN(); + virtualX = 0.0; + virtualY = 0.0; + dragTarget = glm::vec3(); + } + } + + void onCursorPos(GLFWwindow * window, double x, double y){ + if(activeAction == Action::NONE){ + return; + } + + int iwidth = 0, iheight = 0; + glfwGetWindowSize(window, &iwidth, &iheight); + + double width = iwidth; + double height = iheight; + + double deltaX = x - dragX; + double deltaY = y - dragY; + + if(!std::isnan(wrapX) && !std::isnan(wrapY)){ + double wrapDeltaX = x - wrapX; + double wrapDeltaY = y - wrapY; + if(wrapDeltaX * wrapDeltaX + wrapDeltaY * wrapDeltaY < deltaX * deltaX + deltaY * deltaY){ + deltaX = wrapDeltaX; + deltaY = wrapDeltaY; + wrapX = std::numeric_limits ::quiet_NaN(); + wrapY = std::numeric_limits ::quiet_NaN(); + } + } + + dragX = x; + dragY = y; + + double targetX = x; + double targetY = y; + bool changed = false; + if(targetX < 0){ + targetX += width - 1; + changed = true; + }else if(targetX >= width){ + targetX -= width - 1; + changed = true; + } + if(targetY < 0){ + targetY += height - 1; + changed = true; + }else if(targetY >= height){ + targetY -= height - 1; + changed = true; + } + if(changed){ + glfwSetCursorPos(window, targetX, targetY); + wrapX = targetX; + wrapY = targetY; + } + + if(activeAction == Action::TRANSLATE){ + virtualX += deltaX; + virtualY += deltaY; + + glm::vec3 target; + bool ok = unprojectMousePositionToXYPlane(window, virtualX, virtualY, target); + if(ok){ + float x = transform->position.x; + float y = transform->position.y; + glm::vec3 delta = target - dragTarget; + transform->position.x = glm::clamp(x + delta.x, -4.0f, 4.0f); + transform->position.y = glm::clamp(y + delta.y, -4.0f, 4.0f); + } + }else if(activeAction == Action::ROTATE_TURNTABLE){ + double size = glm::min(width, height); + glm::mat3 rx = glm::rotate(float(deltaX / size * glm::pi ()), glm::vec3(0, 0, 1)); + glm::mat3 ry = glm::rotate(float(deltaY / size * glm::pi ()), glm::vec3(1, 0, 0)); + transform->rotation = ry * transform->rotation * rx; + }else if(activeAction == Action::ROTATE_TRACKBALL){ + double size = glm::min(width, height); + glm::mat3 rx = glm::rotate(float(deltaX / size * glm::pi ()), glm::vec3(0, 1, 0)); + glm::mat3 ry = glm::rotate(float(deltaY / size * glm::pi ()), glm::vec3(1, 0, 0)); + transform->rotation = ry * rx * transform->rotation; + } + } + + void onScroll(GLFWwindow * window, double xOffset, double yOffset){ + float factor = glm::clamp(1.0 - float(yOffset) / 10.0, 0.1, 1.9); + transform->distance = glm::clamp(transform->distance * factor, 0.010f, 10.000f); + } +}; + +namespace { +FT_Library library; + +Transform transform; +DragController dragController; + +// Empty VAO used when the vertex shader has no input and only uses gl_VertexID, +// because OpenGL still requires a non-zero VAO to be bound for the draw call. +GLuint emptyVAO; + +std::unique_ptr shaderCatalog; +std::shared_ptr backgroundShader; +std::shared_ptr fontShader; + +std::unique_ptr mainFont; +std::unique_ptr helpFont; +std::vector > otherFonts; +std::vector otherWghts; +std::vector otherSteps; +static const int N_OTHER_FONTS = 5; + +constexpr float helpFontBaseSize = 20.0f; + +int antiAliasingWindowSize = 1; +bool enableSuperSamplingAntiAliasing = true; +bool enableControlPointsVisualization = false; + +bool showHelp = true; + +Font::BoundingBox bb; +std::string mainTexta = + R"DONE(AVM AAIn the center of Fedora, that gray stone metropolis, stands a metal building +[from Invisible Cities by Italo Calvino])DONE"; +std::string mainText = + R"DONE(In the center of Fedora, that gray stone metropolis, stands a metal building +with a crystal globe in every room. Looking into each globe, you see a blue +city, the model of a different Fedora. These are the forms the city could have +taken if, for one reason or another, it had not become what we see today. In +every age someone, looking at Fedora as it was, imagined a way of making it the +ideal city, but while he constructed his miniature model, Fedora was already no +longer the same as before, and what had been until yesterday a possible future +became only a toy in a glass globe. + +The building with the globes is now Fedora's museum: every inhabitant visits it, +chooses the city that corresponds to his desires, contemplates it, imagining his +reflection in the medusa pond that would have collected the waters of the canal +(if it had not been dried up), the view from the high canopied box along the +avenue reserved for elephants (now banished from the city), the fun of sliding +down the spiral, twisting minaret (which never found a pedestal from which to +rise). + +On the map of your empire, O Great Khan, there must be room both for the big, +stone Fedora and the little Fedoras in glass globes. Not because they are all +equally real, but because they are only assumptions. The one contains what is +accepted as necessary when it is not yet so; the others, what is imagined as +possible and, a moment later, is possible no longer. + +On the map of your empire, O Great Khan, there must be room both for the big, +stone Fedora and the little Fedoras in glass globes. Not because they are all +equally real, but because they are only assumptions. The one contains what is +accepted as necessary when it is not yet so; the others, what is imagined as +possible and, a moment later, is possible no longer. + +On the map of your empire, O Great Khan, there must be room both for the big, +stone Fedora and the little Fedoras in glass globes. Not because they are all +equally real, but because they are only assumptions. The one contains what is +accepted as necessary when it is not yet so; the others, what is imagined as +possible and, a moment later, is possible no longer. + +On the map of your empire, O Great Khan, there must be room both for the big, +stone Fedora and the little Fedoras in glass globes. Not because they are all +equally real, but because they are only assumptions. The one contains what is +accepted as necessary when it is not yet so; the others, what is imagined as +possible and, a moment later, is possible no longer. + +The building with the globes is now Fedora's museum: every inhabitant visits it, +chooses the city that corresponds to his desires, contemplates it, imagining his +reflection in the medusa pond that would have collected the waters of the canal +(if it had not been dried up), the view from the high canopied box along the +avenue reserved for elephants (now banished from the city), the fun of sliding +down the spiral, twisting minaret (which never found a pedestal from which to +rise). + +The building with the globes is now Fedora's museum: every inhabitant visits it, +chooses the city that corresponds to his desires, contemplates it, imagining his +reflection in the medusa pond that would have collected the waters of the canal +(if it had not been dried up), the view from the high canopied box along the +avenue reserved for elephants (now banished from the city), the fun of sliding +down the spiral, twisting minaret (which never found a pedestal from which to +rise). + +On the map of your empire, O Great Khan, there must be room both for the big, +stone Fedora and the little Fedoras in glass globes. Not because they are all +equally real, but because they are only assumptions. The one contains what is +accepted as necessary when it is not yet so; the others, what is imagined as +possible and, a moment later, is possible no longer. + +On the map of your empire, O Great Khan, there must be room both for the big, +stone Fedora and the little Fedoras in glass globes. Not because they are all +equally real, but because they are only assumptions. The one contains what is +accepted as necessary when it is not yet so; the others, what is imagined as +possible and, a moment later, is possible no longer. + +On the map of your empire, O Great Khan, there must be room both for the big, +stone Fedora and the little Fedoras in glass globes. Not because they are all +equally real, but because they are only assumptions. The one contains what is +accepted as necessary when it is not yet so; the others, what is imagined as +possible and, a moment later, is possible no longer. + +On the map of your empire, O Great Khan, there must be room both for the big, +stone Fedora and the little Fedoras in glass globes. Not because they are all +equally real, but because they are only assumptions. The one contains what is +accepted as necessary when it is not yet so; the others, what is imagined as +possible and, a moment later, is possible no longer. +[from Invisible Cities by Italo Calvino])DONE"; + +} + +void updateWght(float & value, float & step){ + value += step; + + if(value >= 700.0){ + if(step > 0){ + step *= -1; + } + value += step; + } + if(value <= 100.0){ + if(step < 0){ + step *= -1; + } + value += step; + } +} + +int currentIndexx = -1; + +static std::unique_ptr loadFont(const std::string & filename, float worldSize = 1.0f, bool hinting = false){ + std::string error; + FT_Face face = Font::loadFace(library, filename, error); + float & wghtValue = currentIndexx < 0 ? wght : otherWghts[currentIndexx]; + bool success = Font::setFontVariationAxis(library, face, "Weight", wghtValue); + //if(success){ + //std::cout << "lol, success?" << wght << std::endl; + //}else{ + //std::cout << "godverdomme" << wght << std::endl; + //} + if(error != ""){ + std::cerr << "[font] failed to load " << filename << ": " << error << std::endl; + return std::unique_ptr {}; + } + + return std::make_unique (face, worldSize, hinting); +} + +static void tryUpdateMainFont(const std::string & filename){ + { + currentIndexx = -1; + auto font = loadFont(filename, 0.05f); + if(!font){ + return; + } + + font->dilation = 0.1f; + + font->prepareGlyphsForText(mainText); + + mainFont = std::move(font); + bb = mainFont->measure(0, 0, mainText); + } + updateWght(wght, wghtStep); + + for(int i = 0; i < N_OTHER_FONTS; ++i){ + if(otherWghts.size() == i){ + int w = i * 213456; + while(w > 700){ + w -= 700; + } + otherWghts.push_back(float(w)); + otherSteps.push_back(float(wghtStep)); + } + currentIndexx = i; + auto font = loadFont(filename, 0.05f); + if(!font){ + return; + } + updateWght(otherWghts[i], otherSteps[i]); + //std::cout << "updateWght(" << otherWghts[i] << "," << otherSteps[i] << ");" << std::endl; + + font->dilation = 0.1f; + + font->prepareGlyphsForText(mainText); + + if(otherFonts.size() == i){ + otherFonts.push_back(std::move(font)); + }else{ + otherFonts[i] = std::move(font); + } + } +} + +static void mouseButtonCallback(GLFWwindow * window, int button, int action, int mods){ + dragController.onMouseButton(window, button, action, mods); +} + +static void cursorPosCallback(GLFWwindow * window, double x, double y){ + dragController.onCursorPos(window, x, y); +} + +static void scrollCallback(GLFWwindow * window, double xOffset, double yOffset){ + dragController.onScroll(window, xOffset, yOffset); +} + +static void keyCallback(GLFWwindow * window, int key, int scancode, int action, int mods){ + if(action != GLFW_PRESS){ + return; + } + switch(key){ + case GLFW_KEY_R: + dragController.reset(); + break; + + case GLFW_KEY_C: + enableControlPointsVisualization = !enableControlPointsVisualization; + break; + + case GLFW_KEY_A: + enableSuperSamplingAntiAliasing = !enableSuperSamplingAntiAliasing; + break; + + case GLFW_KEY_0: + antiAliasingWindowSize = 0; + break; + + case GLFW_KEY_1: + antiAliasingWindowSize = 1; + break; + + case GLFW_KEY_2: + antiAliasingWindowSize = 20; + break; + + case GLFW_KEY_3: + antiAliasingWindowSize = 40; + break; + + case GLFW_KEY_S: + antiAliasingWindowSize = 1; + enableSuperSamplingAntiAliasing = true; + break; + + case GLFW_KEY_H: + showHelp = !showHelp; + break; + } +} + +static void dropCallback(GLFWwindow * window, int pathCount, const char * paths[]){ + if(pathCount == 0){ + return; + } + currentFontPath = paths[0]; + tryUpdateMainFont(paths[0]); +} + +int main(int argc, char * argv[]){ + if(!glfwInit()){ + std::cerr << "ERROR: failed to initialize GLFW" << std::endl; + return 1; + } + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); + glfwWindowHint(GLFW_SRGB_CAPABLE, GLFW_TRUE); + + GLFWwindow * window = glfwCreateWindow(1600, 900, "GPU Font Rendering Demo", nullptr, nullptr); + if(!window){ + std::cerr << "ERROR: failed to create GLFW window" << std::endl; + glfwTerminate(); + return 1; + } + + glfwMakeContextCurrent(window); + + if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){ + std::cerr << "ERROR: failed to initialize OpenGL context" << std::endl; + glfwTerminate(); + return 1; + } + + { + FT_Error error = FT_Init_FreeType(&library); + if(error){ + std::cerr << "ERROR: failed to initialize FreeType" << std::endl; + glfwTerminate(); + return 1; + } + } + + dragController.transform = &transform; + glfwSetMouseButtonCallback(window, mouseButtonCallback); + glfwSetCursorPosCallback(window, cursorPosCallback); + glfwSetScrollCallback(window, scrollCallback); + glfwSetKeyCallback(window, keyCallback); + glfwSetDropCallback(window, dropCallback); + + glGenVertexArrays(1, &emptyVAO); + + shaderCatalog = std::make_unique ("shaders"); + backgroundShader = shaderCatalog->get("background"); + fontShader = shaderCatalog->get("font"); + + currentFontPath = "fonts/SourceSerifPro-Regular.otf"; + tryUpdateMainFont(currentFontPath); + + { + float xscale, yscale; + glfwGetWindowContentScale(window, &xscale, &yscale); + float worldSize = std::ceil(helpFontBaseSize * yscale); + helpFont = loadFont("fonts/SourceSansPro-Semibold.otf", worldSize, true); + } + + while(!glfwWindowShouldClose(window)){ + tryUpdateMainFont(currentFontPath); + shaderCatalog->update(); + + glfwPollEvents(); + + int width, height; + glfwGetFramebufferSize(window, &width, &height); + glViewport(0, 0, width, height); + + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + GLuint location; + + glm::mat4 projection = transform.getProjectionMatrix((float)width / height); + glm::mat4 view = transform.getViewMatrix(); + glm::mat4 model = glm::mat4(1.0f); + + { // Draw background. + GLuint program = backgroundShader->program; + glUseProgram(program); + glBindVertexArray(emptyVAO); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + glUseProgram(0); + } + + // Uses premultiplied-alpha. + glEnable(GL_BLEND); + glBlendEquation(GL_FUNC_ADD); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + for(int i = N_OTHER_FONTS - 1; i >= 0; i--){ + if(otherFonts.size() > i){ + //std::cout << "drawing " << i << " with " << otherWghts[i] << std::endl; + auto & otherFont = otherFonts[i]; + GLuint program = fontShader->program; + glUseProgram(program); + + otherFont->program = program; + otherFont->drawSetup(); + + location = glGetUniformLocation(program, "projection"); + glUniformMatrix4fv(location, 1, false, glm::value_ptr(projection)); + location = glGetUniformLocation(program, "view"); + glUniformMatrix4fv(location, 1, false, glm::value_ptr(view)); + location = glGetUniformLocation(program, "model"); + glUniformMatrix4fv(location, 1, false, glm::value_ptr(model)); + + location = glGetUniformLocation(program, "color"); + float r = float(i) / N_OTHER_FONTS; + float g = sin(42.143 * float(i) + otherWghts[i] * 0.0001) * 0.5 + 0.5; + float b = cos(3.143 * float(i) + otherWghts[i] * 0.0005) * 0.1 + 0.1; + glUniform4f(location, r, 1.0f - r, 1.0f - r, 1.0f); + float z = (i + 1) * -0.1; + location = glGetUniformLocation(program, "z"); + glUniform1f(location, z); + + location = glGetUniformLocation(program, "antiAliasingWindowSize"); + glUniform1f(location, (float)antiAliasingWindowSize); + location = glGetUniformLocation(program, "enableSuperSamplingAntiAliasing"); + glUniform1i(location, enableSuperSamplingAntiAliasing); + location = glGetUniformLocation(program, "enableControlPointsVisualization"); + glUniform1i(location, enableControlPointsVisualization); + + float cx = 0.5f * (bb.minX + bb.maxX); + float cy = 0.5f * (bb.minY + bb.maxY); + otherFont->draw(-cx, -cy, 0, mainText); + glUseProgram(0); + } + } + if(mainFont){ + GLuint program = fontShader->program; + glUseProgram(program); + + mainFont->program = program; + mainFont->drawSetup(); + + location = glGetUniformLocation(program, "projection"); + glUniformMatrix4fv(location, 1, false, glm::value_ptr(projection)); + location = glGetUniformLocation(program, "view"); + glUniformMatrix4fv(location, 1, false, glm::value_ptr(view)); + location = glGetUniformLocation(program, "model"); + glUniformMatrix4fv(location, 1, false, glm::value_ptr(model)); + float z = 0; + location = glGetUniformLocation(program, "z"); + glUniform1f(location, z); + + location = glGetUniformLocation(program, "color"); + glUniform4f(location, 1.0f, 1.0f, 1.0f, 1.0f); + + location = glGetUniformLocation(program, "antiAliasingWindowSize"); + glUniform1f(location, (float)antiAliasingWindowSize); + location = glGetUniformLocation(program, "enableSuperSamplingAntiAliasing"); + glUniform1i(location, enableSuperSamplingAntiAliasing); + location = glGetUniformLocation(program, "enableControlPointsVisualization"); + glUniform1i(location, enableControlPointsVisualization); + + float cx = 0.5f * (bb.minX + bb.maxX); + float cy = 0.5f * (bb.minY + bb.maxY); + mainFont->draw(-cx, -cy, 0, mainText); + glUseProgram(0); + } + + if(helpFont && showHelp){ + GLuint program = fontShader->program; + glUseProgram(program); + + helpFont->program = program; + helpFont->drawSetup(); + + glm::mat4 projection = glm::ortho(0.0f, (float)width, 0.0f, (float)height, -1.0f, 1.0f); + glm::mat4 view = glm::mat4(1.0f); + glm::mat4 model = glm::mat4(1.0f); + + location = glGetUniformLocation(program, "projection"); + glUniformMatrix4fv(location, 1, false, glm::value_ptr(projection)); + location = glGetUniformLocation(program, "view"); + glUniformMatrix4fv(location, 1, false, glm::value_ptr(view)); + location = glGetUniformLocation(program, "model"); + glUniformMatrix4fv(location, 1, false, glm::value_ptr(model)); + + location = glGetUniformLocation(program, "color"); + float r = 200, g = 35, b = 220, a = 0.8; + glUniform4f(location, r * a / 255.0f, g * a / 255.0f, b * a / 255.0f, a); + + location = glGetUniformLocation(program, "antiAliasingWindowSize"); + glUniform1f(location, 1.0f); + location = glGetUniformLocation(program, "enableSuperSamplingAntiAliasing"); + glUniform1i(location, true); + location = glGetUniformLocation(program, "enableControlPointsVisualization"); + glUniform1i(location, false); + + std::stringstream stream; + stream << "Drag and drop a .ttf or .otf file to change the font\n"; + stream << "\n"; + stream << "right drag (or CTRL drag) - move\n"; + stream << "left drag - trackball rotate\n"; + stream << "middle drag - turntable rotate\n"; + stream << "scroll wheel - zoom\n"; + stream << "\n"; + stream << "0, 1, 2, 3 - change anti-aliasing window size: " << antiAliasingWindowSize << " pixel" << ((antiAliasingWindowSize != 1) ? "s" : "") << "\n"; + stream << glfwGetKeyName(GLFW_KEY_A, 0) << " - " << (enableSuperSamplingAntiAliasing ? "disable" : "enable") << " 2D anti-aliasing\n"; + stream << "(using another ray along the y-axis)\n"; + stream << glfwGetKeyName(GLFW_KEY_S, 0) << " - reset anti-aliasing settings\n"; + stream << glfwGetKeyName(GLFW_KEY_C, 0) << " - " << (enableControlPointsVisualization ? "disable" : "enable") << " control points\n"; + stream << glfwGetKeyName(GLFW_KEY_R, 0) << " - reset view\n"; + stream << glfwGetKeyName(GLFW_KEY_H, 0) << " - toggle help\n"; + + std::string helpText = stream.str(); + helpFont->prepareGlyphsForText(helpText); + + float xscale, yscale; + glfwGetWindowContentScale(window, &xscale, &yscale); + helpFont->setWorldSize(std::ceil(helpFontBaseSize * yscale)); + + auto bb = helpFont->measure(0, 0, helpText); + helpFont->draw(10 - bb.minX, height - 10 - bb.maxY, 0, helpText); + glUseProgram(0); + } + + glDisable(GL_BLEND); + + glfwSwapBuffers(window); + } + + // Clean up OpenGL resources before termination. + mainFont = nullptr; + helpFont = nullptr; + + glfwTerminate(); + return 0; +} diff --git a/example/src/gpufont/shader_catalog.cpp b/example/src/gpufont/shader_catalog.cpp new file mode 100644 index 0000000..165ff4d --- /dev/null +++ b/example/src/gpufont/shader_catalog.cpp @@ -0,0 +1,191 @@ +#include "shader_catalog.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +//#include + +// UpdateList keeps track of which entries need to be updated. +// The actual update is slightly delayed to avoid reading a partially written file. +// It is threadsafe to allow safe communication with the asynchronous file watcher callback. +class UpdateList { + std::mutex mutex; + std::unordered_map updates; + + public: + void requestUpdate(const std::string & name){ + using namespace std::chrono_literals; + std::lock_guard guard(mutex); + updates[name] = std::chrono::steady_clock::now() + 50ms; + } + + std::vector collectDueUpdates(){ + std::lock_guard guard(mutex); + std::vector result; + auto now = std::chrono::steady_clock::now(); + for(auto it = updates.begin(); it != updates.end();){ + if(it->second < now){ + result.push_back(it->first); + it = updates.erase(it); + }else{ + ++it; + } + } + return result; + } +}; + +class ShaderCatalog::Impl { + private: + std::string dir; + std::unordered_map > entries; + + UpdateList list; + + public: + Impl(const std::string & dir) : dir(dir){ + } + + private: + std::string readFile(const std::string & filename, std::string & error){ + std::ifstream stream(filename, std::ios::binary); + if(!stream){ + error = "failed to open: " + filename; + return ""; + } + + stream.seekg(0, std::istream::end); + size_t size = stream.tellg(); + stream.seekg(0, std::istream::beg); + + std::string result = std::string(size, 0); + stream.read(&result[0], size); + if(!stream){ + error = "failed to read: " + filename; + return ""; + } + + return result; + } + + GLuint compile(const std::string & name, std::string & error){ + std::string vertexData = readFile(dir + "/" + name + ".vert", error); + if(error != ""){ + return 0; + } + + std::string fragmentData = readFile(dir + "/" + name + ".frag", error); + if(error != ""){ + return 0; + } + + GLint success = 0; + + const char * vertexSource = vertexData.c_str(); + GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); + defer {glDeleteShader(vertexShader); + }; + glShaderSource(vertexShader, 1, &vertexSource, nullptr); + glCompileShader(vertexShader); + + glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); + if(!success){ + char log[1024]; + GLsizei length = 0; + glGetShaderInfoLog(vertexShader, sizeof(log), &length, log); + error = "failed to compile vertex shader " + name + ":\n\n" + log; + return 0; + } + + const char * fragmentSource = fragmentData.c_str(); + GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); + defer {glDeleteShader(fragmentShader); + }; + glShaderSource(fragmentShader, 1, &fragmentSource, nullptr); + glCompileShader(fragmentShader); + + glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); + if(!success){ + char log[1024]; + GLsizei length = 0; + glGetShaderInfoLog(fragmentShader, sizeof(log), &length, log); + error = "failed to compile fragment shader " + name + ":\n\n" + log; + return 0; + } + + GLuint program = glCreateProgram(); + glAttachShader(program, vertexShader); + glAttachShader(program, fragmentShader); + glLinkProgram(program); + + glGetProgramiv(program, GL_LINK_STATUS, &success); + if(!success){ + char log[1024]; + GLsizei length = 0; + glGetProgramInfoLog(program, sizeof(log), &length, log); + glDeleteProgram(program); + error = "failed to compile program " + name + ":\n\n" + log; + return 0; + } + + return program; + } + + public: + std::shared_ptr get(const std::string & name){ + auto it = entries.find(name); + if(it != entries.end()){ + return it->second; + } + + std::string error; + GLuint program = compile(name, error); + if(error != ""){ + std::cerr << "[shader] " << error << std::endl; + } + + auto entry = std::make_shared (program); + entries[name] = entry; + return entry; + } + + void update(){ + std::vector updates = list.collectDueUpdates(); + for(const std::string & name : updates){ + auto it = entries.find(name); + if(it == entries.end()){ + continue; + } + + std::string error; + GLuint program = compile(name, error); + if(error != ""){ + std::cerr << "[shader] " << error << std::endl; + }else{ + std::cerr << "[shader] reloaded " << name << std::endl; + glDeleteProgram(it->second->program); + it->second->program = program; + } + } + } +}; + +ShaderCatalog::ShaderCatalog(const std::string & dir) : impl(std::make_unique (dir)){ +} + +ShaderCatalog::~ShaderCatalog(){ +} + +std::shared_ptr ShaderCatalog::get(const std::string & name){ + return impl->get(name); +} + +void ShaderCatalog::update(){ + impl->update(); +} diff --git a/example/src/gpufont/shader_catalog.hpp b/example/src/gpufont/shader_catalog.hpp new file mode 100644 index 0000000..e303e0e --- /dev/null +++ b/example/src/gpufont/shader_catalog.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include "ofMain.h" + +// A shader catalog loads and compiles shaders from a directory. Vertex and +// fragment shaders are matched based on their filename (e.g. example.vert and +// example.frag are loaded and linked together to form the "example" program). +// Whenever a shader file changes on disk, the corresponding program is +// recompiled and relinked. +class ShaderCatalog { + public: + struct Entry { + unsigned int program; + + Entry() : program(0){ + } + Entry(unsigned int program) : program(program){ + } + }; + + ShaderCatalog(const std::string & dir); + ~ShaderCatalog(); + + std::shared_ptr get(const std::string & name); + void update(); + + private: + class Impl; + std::unique_ptr impl; +}; diff --git a/example/src/main.cpp b/example/src/main.cpp new file mode 100644 index 0000000..7a3543d --- /dev/null +++ b/example/src/main.cpp @@ -0,0 +1,21 @@ +#include "ofMain.h" +#include "ofApp.h" + +//======================================================================== +int main(){ + #ifdef OF_TARGET_OPENGLES + ofGLESWindowSettings settings; + //settings.setSize(1920, 1080); + settings.glesVersion = 3; + #else + ofGLWindowSettings settings; + settings.setSize(1920, 1080); + settings.setGLVersion(3, 3); + #endif + ofCreateWindow(settings); + + // this kicks off the running of my app + // can be OF_WINDOW or OF_FULLSCREEN + // pass in width and height too: + ofRunApp(new ofApp()); +} diff --git a/example/src/ofApp.cpp b/example/src/ofApp.cpp new file mode 100644 index 0000000..3997915 --- /dev/null +++ b/example/src/ofApp.cpp @@ -0,0 +1,191 @@ +#include "ofApp.h" + +//-------------------------------------------------------------- +void ofApp::setup(){ + + mainText = R"DONE(Some things are hard to write about. Take soil, +for instance. Soil, Oxford dictionary reads, “is the +upper layer of earth in which plants grow, a black or +dark brown material typically consisting of a mixture +of organic remains, clay, and rock particles”. +I wonder why I have chosen to write +about soil, as I don’t seem to have had many +encounters with it. Perhaps because of that? +Intuiting its importance, but never minding it? +So far it has appeared rather distant. Absent +from my thoughts and words. But now, when +I think of it, I see a charismatic substance. +Soil, according to Merriam-Webster, is +“1. firm land: earth; + 2. a) the upper layer of earth that may be dug + or plowed and in which plants grow; and + b) the superficial unconsolidated and + usually weathered part of the mantle of + a planet and especially of the earth”. + +The Soil Science Society of America defines soil +as “a mixture of minerals, dead and living +organisms (organic materials), air, and water.” +The “soil” entry in Encyclopaedia Britannica starts +with “soil is the biologically active, +porous medium […] serving as a reservoir of +water and nutrients, as a medium for the filtration +and breakdown of injurious wastes, and as +a participant in the cycling of carbon and other +elements through the global ecosystem.” +Such nebulous definitions, though, aren’t +most definitions unsettled? (all definitions are +blasphemy!)… like “soil is (a mix of) all and nothing, +doing anything and everything, everywhere”. How +one defines or translates soil by posing a question +always affects the answer. Soil can mean earth, +ground, dirt, clay, turf, humus, silt, loam, land, +clod, terra, territory, landscape, country, a political +power base, an aspect of divinity, a terrain +to cultivate, or a resource to be exploited… This +is going to be difficult. I don’t know yet where to +start. I hope to capture something essential.)DONE"; + + shaderCatalog = std::make_unique ("data/ofxGPUFont/shaders/GL3"); + backgroundShader = shaderCatalog->get("background"); + fontShader = shaderCatalog->get("font"); + + { + FT_Error error = FT_Init_FreeType(&library); + if(error){ + std::cerr << "ERROR: failed to initialize FreeType" << std::endl; + ofExit(); + } + } + + currentFontPath = "data/celines-fonts/Version-1-var.ttf"; + tryUpdateMainFont(library, + currentFontPath, + mainText, + font, + bb); + + transform = Transform(); +} + +//-------------------------------------------------------------- +void ofApp::update(){ + +} + +//-------------------------------------------------------------- +void ofApp::draw(){ + GLuint location; + + int width = ofGetWidth(); + int height = ofGetHeight(); + + glm::mat4 projection = transform.getProjectionMatrix((float)width / height); + glm::mat4 view = transform.getViewMatrix(); + glm::mat4 model = glm::mat4(1.0f); + + { // Draw background. + GLuint program = backgroundShader->program; + glUseProgram(program); + glBindVertexArray(emptyVAO); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + glUseProgram(0); + } + + // Uses premultiplied-alpha. + glEnable(GL_BLEND); + glBlendEquation(GL_FUNC_ADD); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + if(font){ + GLuint program = fontShader->program; + glUseProgram(program); + + font->program = program; + font->drawSetup(); + + location = glGetUniformLocation(program, "projection"); + glUniformMatrix4fv(location, 1, false, glm::value_ptr(projection)); + location = glGetUniformLocation(program, "view"); + glUniformMatrix4fv(location, 1, false, glm::value_ptr(view)); + location = glGetUniformLocation(program, "model"); + glUniformMatrix4fv(location, 1, false, glm::value_ptr(model)); + float z = 0; + location = glGetUniformLocation(program, "z"); + glUniform1f(location, z); + + location = glGetUniformLocation(program, "color"); + glUniform4f(location, 1.0f, 1.0f, 1.0f, 1.0f); + + location = glGetUniformLocation(program, "antiAliasingWindowSize"); + glUniform1f(location, (float)antiAliasingWindowSize); + location = glGetUniformLocation(program, "enableSuperSamplingAntiAliasing"); + glUniform1i(location, enableSuperSamplingAntiAliasing); + location = glGetUniformLocation(program, "enableControlPointsVisualization"); + glUniform1i(location, enableControlPointsVisualization); + + float cx = 0.5f * (bb.minX + bb.maxX); + float cy = 0.5f * (bb.minY + bb.maxY); + font->draw(-cx, -cy, 0, mainText); + glUseProgram(0); + } + + glDisable(GL_BLEND); + +} + +//-------------------------------------------------------------- +void ofApp::keyPressed(int key){ + +} + +//-------------------------------------------------------------- +void ofApp::keyReleased(int key){ + +} + +//-------------------------------------------------------------- +void ofApp::mouseMoved(int x, int y){ + +} + +//-------------------------------------------------------------- +void ofApp::mouseDragged(int x, int y, int button){ + +} + +//-------------------------------------------------------------- +void ofApp::mousePressed(int x, int y, int button){ + +} + +//-------------------------------------------------------------- +void ofApp::mouseReleased(int x, int y, int button){ + +} + +//-------------------------------------------------------------- +void ofApp::mouseEntered(int x, int y){ + +} + +//-------------------------------------------------------------- +void ofApp::mouseExited(int x, int y){ + +} + +//-------------------------------------------------------------- +void ofApp::windowResized(int w, int h){ + +} + +//-------------------------------------------------------------- +void ofApp::gotMessage(ofMessage msg){ + +} + +//-------------------------------------------------------------- +void ofApp::dragEvent(ofDragInfo dragInfo){ + +} diff --git a/example/src/ofApp.h b/example/src/ofApp.h new file mode 100644 index 0000000..1c9ad98 --- /dev/null +++ b/example/src/ofApp.h @@ -0,0 +1,97 @@ +#pragma once + +#include "ofMain.h" +#include "ofxGPUFont.h" +#include "gpufont/font.hpp" +#include "gpufont/shader_catalog.hpp" + +static std::unique_ptr loadFont(FT_Library & library, const std::string & filename, float worldSize = 1.0f, bool hinting = false){ + std::string error; + FT_Face face = Font::loadFace(library, filename, error); + if(error != ""){ + std::cerr << "[font] failed to load " << filename << ": " << error << std::endl; + return std::unique_ptr {}; + } + + return std::make_unique (face, worldSize, hinting); +} + +static void tryUpdateMainFont(FT_Library & library, + const std::string & filename, + const string & mainText, + unique_ptr & mainFont, + Font::BoundingBox & bb){ + { + auto font = loadFont(library, filename, 0.05f); + if(!font){ + return; + } + + font->dilation = 0.1f; + + font->prepareGlyphsForText(mainText); + + mainFont = std::move(font); + bb = mainFont->measure(0, 0, mainText); + } +} +class ofApp : public ofBaseApp { + + public: + struct Transform { + float fovy = glm::radians(60.0f); + float distance = 0.42f; + glm::mat3 rotation = glm::mat3(1.0f); + glm::vec3 position = glm::vec3(0.0f); + + glm::mat4 getProjectionMatrix(float aspect){ + return glm::perspective( /* fovy = */ glm::radians(60.0f), aspect, 0.002f, 12.000f); + } + + glm::mat4 getViewMatrix(){ + auto translation = glm::translate(position); + return glm::lookAt(glm::vec3(0, 0, distance), glm::vec3(0, 0, 0), glm::vec3(0, 1, 0)) * glm::mat4(rotation) * translation; + } + }; + void setup(); + void update(); + void draw(); + + void keyPressed(int key); + void keyReleased(int key); + void mouseMoved(int x, int y); + void mouseDragged(int x, int y, int button); + void mousePressed(int x, int y, int button); + void mouseReleased(int x, int y, int button); + void mouseEntered(int x, int y); + void mouseExited(int x, int y); + void windowResized(int w, int h); + void dragEvent(ofDragInfo dragInfo); + void gotMessage(ofMessage msg); + + unique_ptr font; + FT_Library library; + +// Empty VAO used when the vertex shader has no input and only uses gl_VertexID, +// because OpenGL still requires a non-zero VAO to be bound for the draw call. + GLuint emptyVAO; + + std::unique_ptr shaderCatalog; + std::shared_ptr backgroundShader; + std::shared_ptr fontShader; + + Font::BoundingBox bb; + + Transform transform; + + string currentFontPath; + string mainText; + + float helpFontBaseSize = 20.0f; + + int antiAliasingWindowSize = 1; + bool enableSuperSamplingAntiAliasing = true; + bool enableControlPointsVisualization = false; + + bool showHelp = true; +}; diff --git a/src/glm.hpp b/src/glm.hpp new file mode 100644 index 0000000..e6c9e80 --- /dev/null +++ b/src/glm.hpp @@ -0,0 +1,18 @@ +#pragma once + +#define GLM_FORCE_RADIANS +#define GLM_ENABLE_EXPERIMENTAL + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include diff --git a/src/ofxGPUFont.h b/src/ofxGPUFont.h new file mode 100644 index 0000000..1a42a12 --- /dev/null +++ b/src/ofxGPUFont.h @@ -0,0 +1 @@ +#pragma one