sfw/sfw/render_core/font.cpp
2024-01-04 12:12:57 +01:00

600 lines
17 KiB
C++

// font framework. original code by Vassvik (UNLICENSED)
// - rlyeh, public domain.
//
// [x] embedded default font (bm mini).
// [x] oversampling, texture dimensions.
// [x] utf8, unicode ranges.
// [x] markup opcodes.
// [x] faces (italic, bold, regular, cjk), colors and sizes.
// [x] unicode ranges from dear-imgui (@ocornut allowed to mit-0 relicense the data tables).
// [*] alignment. kinda hacky. revisit some day.
// [ ] underlining, soft/hard shadows, outlines.
// [ ] clip/wrap/overflow regions.
// [ ] text-shaping, text-layout.
// [ ] text-wrapping.
// [ ] optimizations.
//
// ## language families that could be merged on a single texture alias
// - EU+EL+RU
// - AR+HE+RU
// - TH+VI+TW
// - ZH
// - JP
// - KR
// -----------------------------------------------------------------------------
// bm-mini.zip (public domain font)
// http://bitmapmania.m78.com
// cooz@m78.com
#include "font.h"
#include "window.h"
#include "3rd_glad.h"
#define STB_TRUETYPE_IMPLEMENTATION
#include "3rd_stb_truetype.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include "font_data_bm_mini.inc.h"
#include "font_data_tables.inc.h"
#include "render_core/image.h"
#include "render_core/texture.h"
void Font::load_default(const float size, const uint32_t flags) {
font_face_from_mem(bm_mini_ttf, 20176, size, flags);
}
float Font::get_scale() const {
return _scale;
}
void Font::set_scale(const float p_scale) {
_scale = p_scale;
}
void Font::font_face_from_mem(const void *ttf_data, uint32_t ttf_len, float font_size, uint32_t flags) {
if (font_size <= 0 || font_size > 72) {
return;
}
if (!ttf_data || !ttf_len) {
return;
}
if (!(flags & FONT_ASCII)) {
flags |= FONT_ASCII; // ensure this minimal range [0020-00FF] is almost always in
}
_initialized = 1;
// load .ttf into a bitmap using stb_truetype.h
int dim = flags & FONT_4096 ? 4096 : flags & FONT_2048 ? 2048
: flags & FONT_1024 ? 1024
: 512;
_width = dim;
_height = dim;
// change size [h1(largest) to h3(regular) to h6(smallest)]
_font_size = font_size;
_scale = 1.0000f;
// figure out what ranges we're about to bake
#define MERGE_TABLE(table) \
do { \
for (unsigned i = 0; table[i]; i += 2) { \
uint64_t begin = table[i + 0], end = table[i + 1]; \
for (unsigned j = begin; j <= end; ++j) { \
sorted.push_back(j); \
} \
} \
} while (0)
#define MERGE_PACKED_TABLE(codepoint_begin, table, table_size) \
do { \
for (int i = 0, begin = codepoint_begin, end = table_size; i < end; i++) { \
sorted.push_back((unsigned)(begin + table[i])); \
begin += table[i]; \
} \
} while (0)
Vector<uint64_t> sorted;
if (flags & FONT_ASCII) {
MERGE_TABLE(table_common);
}
if (flags & FONT_EM) {
MERGE_TABLE(table_emoji);
}
if (flags & FONT_EU) {
MERGE_TABLE(table_eastern_europe);
}
if (flags & FONT_RU) {
MERGE_TABLE(table_western_europe);
}
if (flags & FONT_EL) {
MERGE_TABLE(table_western_europe);
}
if (flags & FONT_AR) {
MERGE_TABLE(table_middle_east);
}
if (flags & FONT_HE) {
MERGE_TABLE(table_middle_east);
}
if (flags & FONT_TH) {
MERGE_TABLE(table_thai);
}
if (flags & FONT_VI) {
MERGE_TABLE(table_vietnamese);
}
if (flags & FONT_KR) {
MERGE_TABLE(table_korean);
}
if (flags & FONT_JP) {
MERGE_TABLE(table_chinese_japanese_common);
MERGE_PACKED_TABLE(0x4E00, packed_table_japanese, 2999);
}
if (flags & FONT_ZH) {
MERGE_TABLE(table_chinese_japanese_common);
MERGE_PACKED_TABLE(0x4E00, packed_table_chinese, 2500);
} // zh-simplified
if (flags & FONT_ZH) {
MERGE_TABLE(table_chinese_punctuation);
} // both zh-simplified and zh-full
// if(flags & FONT_ZH) { MERGE_TABLE(table_chinese_full); } // zh-full
sorted.sort();
for (int i = 0; i < sorted.size(); ++i) {
uint64_t current_element = sorted[i];
for (int j = i + 1; j < sorted.size(); ++j) {
if (sorted[j] == current_element) {
sorted.remove(j);
--j;
} else {
break;
}
}
}
// pack and create bitmap
//unsigned char *bitmap = memnew_arr(unsigned char, _height * _width);
Vector<uint8_t> bitmap_data;
bitmap_data.resize(_height * _width);
unsigned char *bitmap = (unsigned char *)bitmap_data.ptrw();
int charCount = sorted[sorted.size() - 1] - sorted[0] + 1; // 0xEFFFF;
_cdata = (stbtt_packedchar *)calloc(1, sizeof(stbtt_packedchar) * charCount);
_iter2cp = memnew_arr(unsigned int, charCount);
_cp2iter = memnew_arr(unsigned int, charCount);
for (int i = 0; i < charCount; ++i) {
_iter2cp[i] = _cp2iter[i] = 0xFFFD; // default invalid glyph
}
// find first ch./co
{
stbtt_fontinfo info = { 0 };
stbtt_InitFont(&info, (const unsigned char *)ttf_data, stbtt_GetFontOffsetForIndex((const unsigned char *)ttf_data, 0));
for (int i = 0, end = sorted.size() - 1; i < end; ++i) {
unsigned glyph = sorted[i];
if (!stbtt_FindGlyphIndex(&info, glyph))
continue;
_begin = glyph;
break;
}
}
stbtt_pack_context pc;
if (!stbtt_PackBegin(&pc, bitmap, _width, _height, 0, 1, NULL)) {
ERR_FAIL_COND("Failed to initialize atlas font");
}
stbtt_PackSetOversampling(&pc, flags & FONT_OVERSAMPLE_X ? 2 : 1, flags & FONT_OVERSAMPLE_Y ? 2 : 1); /*useful on small fonts*/
unsigned int count = 0;
for (int i = 0, num = sorted.size() - 1; i < num; ++i) {
uint64_t begin = sorted[i], end = sorted[i];
while (i < (num - 1) && (sorted[i + 1] - sorted[i]) == 1) {
end = sorted[++i];
}
//printf("(%d,%d)", (unsigned)begin, (unsigned)end);
if (begin < _begin)
continue;
if (stbtt_PackFontRange(&pc, (const unsigned char *)ttf_data, 0, _font_size, begin, end - begin + 1, (stbtt_packedchar *)_cdata + begin - _begin)) {
for (uint64_t cp = begin; cp <= end; ++cp) {
// unicode->index runtime lookup
_cp2iter[cp - _begin] = count;
_iter2cp[count++] = cp;
}
} else {
ERR_PRINT("!Failed to pack atlas font. Likely out of texture mem.");
}
}
stbtt_PackEnd(&pc);
_num_glyphs = count;
// calculate vertical font metrics
stbtt_fontinfo info = { 0 };
stbtt_InitFont(&info, (const unsigned char *)ttf_data, stbtt_GetFontOffsetForIndex((const unsigned char *)ttf_data, 0));
int a, d, l;
if (!stbtt_GetFontVMetricsOS2(&info, &a, &d, &l)) {
stbtt_GetFontVMetrics(&info, &a, &d, &l);
}
_ascent = a;
_descent = d;
_linegap = l;
_linedist = (a - d + l);
_factor = (_font_size / (_ascent - _descent));
// save some gpu memory by truncating unused vertical space in atlas texture
{
int max_y1 = 0;
for (unsigned int i = 0; i < _num_glyphs; i++) {
int cp = _iter2cp[i];
if (cp == 0xFFFD)
continue;
stbtt_packedchar *cd = &_cdata[cp - _begin];
if (cd->y1 > max_y1) {
max_y1 = cd->y1;
}
}
// cut away the unused part of the bitmap
_height = max_y1 + 1;
}
//LOG_MSG("Font atlas size %dx%d (GL_R, %5.2fKiB) (%u glyphs)\n", _width, _height, _width * _height / 1024.f, _num_glyphs);
/*
// setup and upload font bitmap texture
glGenTextures(1, &f->texture_fontdata);
glBindTexture(GL_TEXTURE_2D, f->texture_fontdata);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, _width, _height, 0, GL_RED, GL_UNSIGNED_BYTE, bitmap);
*/
// last chance to inspect the font atlases
//if (flag("--font-debug"))
//String pngname = "font_debug" + itos(index) + " .png";
//stbi_write_png(pngname.utf8().get_data(), _width, _height, 1, bitmap, 0);
if (!_image.is_valid()) {
_image.instance();
}
bitmap_data.resize(_width * _height);
_image->create(_width, _height, false, Image::FORMAT_L8, bitmap_data);
if (!_texture.is_valid()) {
_texture.instance();
}
_texture->create_from_image(_image);
_texture_offsets.resize(_num_glyphs);
// remap larger 0xFFFF unicodes into smaller NUM_GLYPHS glyphs
for (unsigned i = 0; i < _num_glyphs; i++) {
unsigned cp = _iter2cp[i];
if (cp == 0xFFFD) {
continue;
}
stbtt_packedchar *cd = &_cdata[cp - _begin];
// if(cd->x1==cd->x0) { _iter2cp[i] = _cp2iter[cp - _begin] = 0xFFFD; continue; }
TextureOffset offset;
offset.x = cd->x0 / (double)_width;
offset.y = cd->y0 / (double)_height;
offset.w = (cd->x1 - cd->x0) / (double)_width;
offset.h = (cd->y1 - cd->y0) / (double)_height;
offset.xoff = cd->xoff / (double)_width;
offset.yoff = cd->yoff / (double)_height;
offset.woff = cd->xoff2 / (double)_width;
offset.hoff = cd->yoff2 / (double)_height;
_texture_offsets.write[i] = offset;
}
/*
float *texture_offsets = memnew_arr(float, 8 * _num_glyphs);
// remap larger 0xFFFF unicodes into smaller NUM_GLYPHS glyphs
for (unsigned i = 0, count = 0; i < _num_glyphs; i++) {
unsigned cp = _iter2cp[i];
if (cp == 0xFFFD)
continue;
stbtt_packedchar *cd = &_cdata[cp - _begin];
// if(cd->x1==cd->x0) { _iter2cp[i] = _cp2iter[cp - _begin] = 0xFFFD; continue; }
int k1 = 0 * _num_glyphs + count;
int k2 = 1 * _num_glyphs + count;
++count;
texture_offsets[4 * k1 + 0] = cd->x0 / (double)_width;
texture_offsets[4 * k1 + 1] = cd->y0 / (double)_height;
texture_offsets[4 * k1 + 2] = (cd->x1 - cd->x0) / (double)_width;
texture_offsets[4 * k1 + 3] = (cd->y1 - cd->y0) / (double)_height;
texture_offsets[4 * k2 + 0] = cd->xoff / (double)_width;
texture_offsets[4 * k2 + 1] = cd->yoff / (double)_height;
texture_offsets[4 * k2 + 2] = cd->xoff2 / (double)_width;
texture_offsets[4 * k2 + 3] = cd->yoff2 / (double)_height;
}
glUniform1i(glGetUniformLocation(f->program->get_program(), "sampler_font"), 0);
glUniform1i(glGetUniformLocation(f->program->get_program(), "sampler_meta"), 1);
glUniform1i(glGetUniformLocation(f->program->get_program(), "sampler_colors"), 2);
glUniform2f(glGetUniformLocation(f->program->get_program(), "res_bitmap"), _width, _height);
glUniform2f(glGetUniformLocation(f->program->get_program(), "res_meta"), _num_glyphs, 2);
glUniform1f(glGetUniformLocation(f->program->get_program(), "num_colors"), FONT_MAX_COLORS);
(void)flags;
*/
}
void Font::font_face(const char *filename_ttf, float font_size, unsigned flags) {
/*
font_init();
int len;
const char *buffer = vfs_load(filename_ttf, &len);
if (!buffer)
buffer = file_load(filename_ttf, &len);
font_face_from_mem(tag, buffer, len, font_size, flags);
*/
}
void Font::font_draw_cmd(const float *glyph_data, int glyph_idx, float factor, Vector2 offset) {
/*
// Backup GL state
GLint last_program, last_vertex_array;
GLint last_texture0, last_texture1, last_texture2;
GLint last_blend_src, last_blend_dst;
GLint last_blend_equation_rgb, last_blend_equation_alpha;
glGetIntegerv(GL_CURRENT_PROGRAM, &last_program);
glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &last_vertex_array);
glActiveTexture(GL_TEXTURE0);
glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture0);
glActiveTexture(GL_TEXTURE1);
glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture1);
glActiveTexture(GL_TEXTURE2);
glGetIntegerv(GL_TEXTURE_BINDING_1D, &last_texture2);
glGetIntegerv(GL_BLEND_SRC, &last_blend_src);
glGetIntegerv(GL_BLEND_DST, &last_blend_dst);
glGetIntegerv(GL_BLEND_EQUATION_RGB, &last_blend_equation_rgb);
glGetIntegerv(GL_BLEND_EQUATION_ALPHA, &last_blend_equation_alpha);
GLboolean last_enable_blend = glIsEnabled(GL_BLEND);
GLboolean last_enable_depth_test = glIsEnabled(GL_DEPTH_TEST);
// Setup render state: alpha-blending enabled, no depth testing and bind textures
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_DEPTH_TEST);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, f->texture_fontdata);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, f->texture_offsets);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_1D, f->texture_colors);
// update bindings
glBindVertexArray(f->vao);
// update uniforms
glUseProgram(f->program->get_program());
glUniform1f(glGetUniformLocation(f->program->get_program(), "scale_factor"), factor);
glUniform2fv(glGetUniformLocation(f->program->get_program(), "string_offset"), 1, &offset.x);
glUniform1f(glGetUniformLocation(f->program->get_program(), "offset_firstline"), _ascent * _factor);
GLint dims[4] = { 0 };
glGetIntegerv(GL_VIEWPORT, dims);
glUniform2f(glGetUniformLocation(f->program->get_program(), "resolution"), dims[2], dims[3]);
// actual uploading
glBindBuffer(GL_ARRAY_BUFFER, f->vbo_instances);
glBufferSubData(GL_ARRAY_BUFFER, 0, 4 * 4 * glyph_idx, glyph_data);
// actual drawing
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, glyph_idx);
// Restore modified GL state
glUseProgram(last_program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, last_texture0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, last_texture1);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_1D, last_texture2);
glBlendEquationSeparate(last_blend_equation_rgb, last_blend_equation_alpha);
glBindVertexArray(last_vertex_array);
glBlendFunc(last_blend_src, last_blend_dst);
(last_enable_depth_test ? glEnable(GL_DEPTH_TEST) : glDisable(GL_DEPTH_TEST));
(last_enable_blend ? glEnable(GL_BLEND) : glDisable(GL_BLEND));
*/
}
// 1. call font_face() if it's the first time it's called.
// 1. parse the string and update the instance vbo, then upload it
// 1. draw the string
Vector2 Font::font_draw_ex(const String &text, Vector2 offset, const char *col, void (*draw_cmd)(const float *, int, float, Vector2)) {
/*
font_init();
// sanity checks
int len = text.length();
if (len >= FONT_MAX_STRING_LEN) {
return Vector2(0, 0);
}
// pre-init
static __thread float *text_glyph_data = NULL;
if (!text_glyph_data) {
text_glyph_data = memnew_arr(float, 4 * FONT_MAX_STRING_LEN);
}
// ready
font_t *f = &fonts[0];
int S = 3;
uint32_t color = 0;
float X = 0, Y = 0, W = 0, L = _ascent * _factor * _scale[S], LL = L; // LL=largest linedist
offset.y = -offset.y; // invert y polarity
// parse string
float *t = text_glyph_data;
for (int i = 0, end = text.length(); i < end; ++i) {
uint32_t ch = text[i];
if (ch == '\n') {
// change cursor, advance y, record largest x as width, increase height
if (X > W)
W = X;
X = 0.0;
Y -= _linedist * _factor * _scale[S];
if (i + 1 == end) { //@hack: ensures we terminate the height at the correct position
Y -= (_descent + _linegap) * _factor * _scale[S];
}
continue;
}
if (ch >= 1 && ch <= 6) {
// flush previous state
if (draw_cmd)
draw_cmd(f, text_glyph_data, (t - text_glyph_data) / 4, _scale[S], offset);
t = text_glyph_data;
// reposition offset to align new baseline
// @fixme:
// offset.y += (_linedist - _linegap) * ( _scale[ch] - _scale[S] );
// change size
S = ch;
L = _ascent * _factor * _scale[S];
if (L > LL)
LL = L;
continue;
}
if (ch >= 0x1a && ch <= 0x1f) {
color = ch - 0x1a;
continue;
}
if (ch >= 0x10 && ch <= 0x19) {
if (fonts[ch - 0x10].initialized) {
// flush previous state
if (draw_cmd)
draw_cmd(f, text_glyph_data, (t - text_glyph_data) / 4, _scale[S], offset);
t = text_glyph_data;
// change face
f = &fonts[ch - 0x10];
}
continue;
}
// convert to vbo data
int cp = ch - _begin; // _cp2iter[ch - _begin];
//if(cp == 0xFFFD) continue;
//if (cp > _num_glyphs) continue;
*t++ = X;
*t++ = Y;
*t++ = _cp2iter[cp];
*t++ = col ? col[i] : color;
X += _cdata[cp].xadvance * _scale[S];
}
if (draw_cmd)
draw_cmd(f, text_glyph_data, (t - text_glyph_data) / 4, _scale[S], offset);
//if(strstr(text, "fps")) printf("(%f,%f) (%f) L:%f LINEDIST:%f\n", X, Y, W, L, _linedist);
return Vector2(W * W > X * X ? W : X, Y * Y > LL * LL ? Y : LL).abs();
*/
return Vector2();
}
// Return cursor
Vector2 Font::font_xy() {
return gotoxy;
}
// Relocate cursor
void Font::font_goto(float x, float y) {
gotoxy = Vector2(x, y);
}
// Calculate the size of a string, in the pixel size specified. Count stray newlines too.
Vector2 Font::font_rect(const String &str) {
return font_draw_ex(str, gotoxy, NULL, NULL);
}
Font::font_metrics_t Font::font_metrics(const String &text) {
font_metrics_t m = { 0 };
/*
// parse string
for (int i = 0, end = text.length(); i < end; ++i) {
uint32_t ch = text[i];
if (ch >= 1 && ch <= 6) {
S = ch;
continue;
}
if (ch >= 0x1a && ch <= 0x1f) {
if (fonts[ch - 0x1a].initialized) {
// change face
f = &fonts[ch - 0x1a];
}
continue;
}
}
*/
m.ascent = _ascent * _factor * _scale;
m.descent = _descent * _factor * _scale;
m.linegap = _linegap * _factor * _scale;
m.linedist = _linedist * _factor * _scale;
return m;
}
int Font::get_atlas_width() {
return _width;
}
int Font::get_atlas_height() {
return _height;
}
Ref<Image> Font::get_image() {
return _image;
}
Ref<Texture> Font::get_texture() {
return _texture;
}
Font::Font() {
_initialized = false;
}