initital commit

This commit is contained in:
jrkb 2023-03-31 16:45:23 +02:00
commit 6661f7eb0f
19 changed files with 2400 additions and 0 deletions

9
example/src/defer.hpp Normal file
View file

@ -0,0 +1,9 @@
// Source: https://stackoverflow.com/a/42060129
#ifndef defer
struct defer_dummy {};
template <class F> struct deferrer { F f; ~deferrer() { f(); } };
template <class F> deferrer<F> 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

View file

@ -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 <ft2build.h>
#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 <FT_Fixed> 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<FontVariationAxis> &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 <FT_UInt>(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 <FT_UInt>(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 <int32_t>(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 <int32_t>(bufferCurves.size()) - bufferGlyph.start;
int32_t bufferIndex = static_cast <int32_t>(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 <BufferCurve> & 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 <uint8_t>((*text)[0]);
// Fast-path for ASCII.
if(first < 128){
(*text)++;
return static_cast <uint32_t>(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 <uint8_t>((*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 <BufferVertex> vertices;
std::vector <int32_t> 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 <int32_t>(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 <float>::infinity();
bb.minY = +std::numeric_limits <float>::infinity();
bb.maxX = -std::numeric_limits <float>::infinity();
bb.maxY = -std::numeric_limits <float>::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 <BufferGlyph> bufferGlyphs;
std::vector <BufferCurve> bufferCurves;
std::unordered_map <uint32_t, Glyph> 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;
};

View file

@ -0,0 +1,18 @@
#pragma once
#define GLM_FORCE_RADIANS
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
#include <glm/mat4x4.hpp>
#include <glm/trigonometric.hpp>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/matrix_access.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/hash.hpp>
#include <glm/gtx/string_cast.hpp>
#include <glm/gtx/transform.hpp>

View file

@ -0,0 +1,717 @@
#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
#include <sstream>
#include <string>
#include <unordered_map>
#include <vector>
#include <defer.hpp>
#include "ofMain.h"
#include <ft2build.h>
#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 <double>::quiet_NaN();
wrapY = std::numeric_limits <double>::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 <double>::quiet_NaN();
wrapY = std::numeric_limits <double>::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 <double>::quiet_NaN();
wrapY = std::numeric_limits <double>::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 <double>()), glm::vec3(0, 0, 1));
glm::mat3 ry = glm::rotate(float(deltaY / size * glm::pi <double>()), 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 <double>()), glm::vec3(0, 1, 0));
glm::mat3 ry = glm::rotate(float(deltaY / size * glm::pi <double>()), 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> shaderCatalog;
std::shared_ptr <ShaderCatalog::Entry> backgroundShader;
std::shared_ptr <ShaderCatalog::Entry> fontShader;
std::unique_ptr <Font> mainFont;
std::unique_ptr <Font> helpFont;
std::vector <std::unique_ptr <Font> > otherFonts;
std::vector <float> otherWghts;
std::vector <float> 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 <Font> 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 <Font>{};
}
return std::make_unique <Font>(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 <ShaderCatalog>("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;
}

View file

@ -0,0 +1,191 @@
#include "shader_catalog.hpp"
#include <chrono>
#include <fstream>
#include <iostream>
#include <mutex>
#include <unordered_map>
#include <vector>
#include <defer.hpp>
//#include <glad/glad.h>
// 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 <std::string, std::chrono::steady_clock::time_point> updates;
public:
void requestUpdate(const std::string & name){
using namespace std::chrono_literals;
std::lock_guard <std::mutex> guard(mutex);
updates[name] = std::chrono::steady_clock::now() + 50ms;
}
std::vector <std::string> collectDueUpdates(){
std::lock_guard <std::mutex> guard(mutex);
std::vector <std::string> 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 <std::string, std::shared_ptr <Entry> > 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 <Entry> 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 <Entry>(program);
entries[name] = entry;
return entry;
}
void update(){
std::vector <std::string> 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 <Impl>(dir)){
}
ShaderCatalog::~ShaderCatalog(){
}
std::shared_ptr <ShaderCatalog::Entry> ShaderCatalog::get(const std::string & name){
return impl->get(name);
}
void ShaderCatalog::update(){
impl->update();
}

View file

@ -0,0 +1,32 @@
#pragma once
#include <string>
#include <memory>
#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 <Entry> get(const std::string & name);
void update();
private:
class Impl;
std::unique_ptr <Impl> impl;
};

21
example/src/main.cpp Normal file
View file

@ -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());
}

191
example/src/ofApp.cpp Normal file
View file

@ -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 dont 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, arent
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 dont know yet where to
start. I hope to capture something essential.)DONE";
shaderCatalog = std::make_unique <ShaderCatalog>("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){
}

97
example/src/ofApp.h Normal file
View file

@ -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 <Font> 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 <Font>{};
}
return std::make_unique <Font>(face, worldSize, hinting);
}
static void tryUpdateMainFont(FT_Library & library,
const std::string & filename,
const string & mainText,
unique_ptr <Font> & 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> 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> shaderCatalog;
std::shared_ptr <ShaderCatalog::Entry> backgroundShader;
std::shared_ptr <ShaderCatalog::Entry> 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;
};