mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(renderer): msdf text renderer
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[submodule "third_party/msdfgen"]
|
||||
path = third_party/msdfgen
|
||||
url = https://github.com/Chlumsky/msdfgen.git
|
||||
+14
-2
@@ -18,6 +18,16 @@ pkg_check_modules(GLES2 REQUIRED glesv2)
|
||||
pkg_check_modules(WAYLAND_EGL REQUIRED wayland-egl)
|
||||
pkg_check_modules(FREETYPE2 REQUIRED freetype2)
|
||||
pkg_check_modules(HARFBUZZ REQUIRED harfbuzz)
|
||||
|
||||
# --- msdfgen (vendored) ---
|
||||
set(MSDFGEN_CORE_ONLY OFF CACHE BOOL "" FORCE)
|
||||
set(MSDFGEN_BUILD_STANDALONE OFF CACHE BOOL "" FORCE)
|
||||
set(MSDFGEN_USE_VCPKG OFF CACHE BOOL "" FORCE)
|
||||
set(MSDFGEN_USE_SKIA OFF CACHE BOOL "" FORCE)
|
||||
set(MSDFGEN_DISABLE_SVG ON CACHE BOOL "" FORCE)
|
||||
set(MSDFGEN_DISABLE_PNG ON CACHE BOOL "" FORCE)
|
||||
set(MSDFGEN_INSTALL OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(third_party/msdfgen)
|
||||
pkg_get_variable(WAYLAND_PROTOCOLS_PKGDATADIR wayland-protocols pkgdatadir)
|
||||
file(REAL_PATH "${WAYLAND_PROTOCOLS_PKGDATADIR}" WAYLAND_PROTOCOLS_PKGDATADIR)
|
||||
|
||||
@@ -126,8 +136,8 @@ add_executable(noctalia
|
||||
src/render/LinearGradientProgram.cpp
|
||||
src/render/RoundedRectProgram.cpp
|
||||
src/render/ShaderProgram.cpp
|
||||
src/render/TextProgram.cpp
|
||||
src/render/TextRenderer.cpp
|
||||
src/render/MsdfTextProgram.cpp
|
||||
src/render/MsdfTextRenderer.cpp
|
||||
src/shell/BarShell.cpp
|
||||
src/wayland/LayerSurface.cpp
|
||||
src/wayland/WaylandConnection.cpp
|
||||
@@ -151,6 +161,8 @@ target_include_directories(noctalia PRIVATE
|
||||
)
|
||||
target_link_libraries(noctalia PRIVATE
|
||||
SDBusCpp::sdbus-c++
|
||||
msdfgen::msdfgen-core
|
||||
msdfgen::msdfgen-ext
|
||||
${WAYLAND_CLIENT_LIBRARIES}
|
||||
${EGL_LIBRARIES}
|
||||
${GLES2_LIBRARIES}
|
||||
|
||||
@@ -131,7 +131,8 @@ void GlRenderer::render(std::uint32_t width, std::uint32_t height) {
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
constexpr auto kLabel = "Noctalia";
|
||||
const auto labelMetrics = m_textRenderer.measure(kLabel);
|
||||
constexpr float kFontSize = 14.0f;
|
||||
const auto labelMetrics = m_textRenderer.measure(kLabel, kFontSize);
|
||||
const float labelX = (static_cast<float>(width) - labelMetrics.width) * 0.5f;
|
||||
const float labelHeight = labelMetrics.bottom - labelMetrics.top;
|
||||
const float labelBaseline =
|
||||
@@ -176,6 +177,7 @@ void GlRenderer::render(std::uint32_t width, std::uint32_t height) {
|
||||
labelX,
|
||||
labelBaseline,
|
||||
kLabel,
|
||||
kFontSize,
|
||||
kRosePinePalette.text);
|
||||
|
||||
if (eglSwapBuffers(m_eglDisplay, m_eglSurface) != EGL_TRUE) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "render/LinearGradientProgram.hpp"
|
||||
#include "render/RoundedRectProgram.hpp"
|
||||
#include "render/Renderer.hpp"
|
||||
#include "render/TextRenderer.hpp"
|
||||
#include "render/MsdfTextRenderer.hpp"
|
||||
|
||||
#include <EGL/egl.h>
|
||||
|
||||
@@ -32,7 +32,7 @@ private:
|
||||
EGLSurface m_eglSurface = EGL_NO_SURFACE;
|
||||
LinearGradientProgram m_linearGradientProgram;
|
||||
RoundedRectProgram m_roundedRectProgram;
|
||||
TextRenderer m_textRenderer;
|
||||
MsdfTextRenderer m_textRenderer;
|
||||
std::uint32_t m_surfaceWidth = 0;
|
||||
std::uint32_t m_surfaceHeight = 0;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "render/TextProgram.hpp"
|
||||
#include "render/MsdfTextProgram.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <stdexcept>
|
||||
@@ -31,18 +31,25 @@ precision highp float;
|
||||
|
||||
uniform sampler2D u_texture;
|
||||
uniform vec4 u_color;
|
||||
uniform float u_px_range;
|
||||
varying vec2 v_texcoord;
|
||||
|
||||
float median(float r, float g, float b) {
|
||||
return max(min(r, g), min(max(r, g), b));
|
||||
}
|
||||
|
||||
void main() {
|
||||
float alpha = texture2D(u_texture, v_texcoord).a;
|
||||
alpha = pow(clamp(alpha, 0.0, 1.0), 0.9);
|
||||
gl_FragColor = vec4(u_color.rgb, u_color.a * alpha);
|
||||
vec3 msd = texture2D(u_texture, v_texcoord).rgb;
|
||||
float sd = median(msd.r, msd.g, msd.b);
|
||||
float screenPxDist = u_px_range * (sd - 0.5);
|
||||
float opacity = clamp(screenPxDist + 0.5, 0.0, 1.0);
|
||||
gl_FragColor = vec4(u_color.rgb, u_color.a * opacity);
|
||||
}
|
||||
)";
|
||||
|
||||
} // namespace
|
||||
|
||||
void TextProgram::ensureInitialized() {
|
||||
void MsdfTextProgram::ensureInitialized() {
|
||||
if (m_program.isValid()) {
|
||||
return;
|
||||
}
|
||||
@@ -52,6 +59,7 @@ void TextProgram::ensureInitialized() {
|
||||
m_texCoordLocation = glGetAttribLocation(m_program.id(), "a_texcoord");
|
||||
m_surfaceSizeLocation = glGetUniformLocation(m_program.id(), "u_surface_size");
|
||||
m_rectLocation = glGetUniformLocation(m_program.id(), "u_rect");
|
||||
m_pxRangeLocation = glGetUniformLocation(m_program.id(), "u_px_range");
|
||||
m_colorLocation = glGetUniformLocation(m_program.id(), "u_color");
|
||||
m_samplerLocation = glGetUniformLocation(m_program.id(), "u_texture");
|
||||
|
||||
@@ -59,34 +67,37 @@ void TextProgram::ensureInitialized() {
|
||||
m_texCoordLocation < 0 ||
|
||||
m_surfaceSizeLocation < 0 ||
|
||||
m_rectLocation < 0 ||
|
||||
m_pxRangeLocation < 0 ||
|
||||
m_colorLocation < 0 ||
|
||||
m_samplerLocation < 0) {
|
||||
throw std::runtime_error("failed to query text shader locations");
|
||||
throw std::runtime_error("failed to query MSDF text shader locations");
|
||||
}
|
||||
}
|
||||
|
||||
void TextProgram::destroy() {
|
||||
void MsdfTextProgram::destroy() {
|
||||
m_program.destroy();
|
||||
m_positionLocation = -1;
|
||||
m_texCoordLocation = -1;
|
||||
m_surfaceSizeLocation = -1;
|
||||
m_rectLocation = -1;
|
||||
m_pxRangeLocation = -1;
|
||||
m_colorLocation = -1;
|
||||
m_samplerLocation = -1;
|
||||
}
|
||||
|
||||
void TextProgram::draw(GLuint texture,
|
||||
float surfaceWidth,
|
||||
float surfaceHeight,
|
||||
float x,
|
||||
float y,
|
||||
float width,
|
||||
float height,
|
||||
float u0,
|
||||
float v0,
|
||||
float u1,
|
||||
float v1,
|
||||
const Color& color) const {
|
||||
void MsdfTextProgram::draw(GLuint texture,
|
||||
float surfaceWidth,
|
||||
float surfaceHeight,
|
||||
float x,
|
||||
float y,
|
||||
float width,
|
||||
float height,
|
||||
float u0,
|
||||
float v0,
|
||||
float u1,
|
||||
float v1,
|
||||
float pxRange,
|
||||
const Color& color) const {
|
||||
if (!m_program.isValid() || texture == 0 || width <= 0.0f || height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
@@ -112,15 +123,18 @@ void TextProgram::draw(GLuint texture,
|
||||
glUseProgram(m_program.id());
|
||||
glUniform2f(m_surfaceSizeLocation, surfaceWidth, surfaceHeight);
|
||||
glUniform4f(m_rectLocation, x, y, width, height);
|
||||
glUniform1f(m_pxRangeLocation, pxRange);
|
||||
glUniform4f(m_colorLocation, color.r, color.g, color.b, color.a);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glUniform1i(m_samplerLocation, 0);
|
||||
glVertexAttribPointer(m_positionLocation, 2, GL_FLOAT, GL_FALSE, 0, positions.data());
|
||||
glVertexAttribPointer(m_texCoordLocation, 2, GL_FLOAT, GL_FALSE, 0, texcoords.data());
|
||||
glEnableVertexAttribArray(m_positionLocation);
|
||||
glEnableVertexAttribArray(m_texCoordLocation);
|
||||
const auto posAttr = static_cast<GLuint>(m_positionLocation);
|
||||
const auto texAttr = static_cast<GLuint>(m_texCoordLocation);
|
||||
glVertexAttribPointer(posAttr, 2, GL_FLOAT, GL_FALSE, 0, positions.data());
|
||||
glVertexAttribPointer(texAttr, 2, GL_FLOAT, GL_FALSE, 0, texcoords.data());
|
||||
glEnableVertexAttribArray(posAttr);
|
||||
glEnableVertexAttribArray(texAttr);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
glDisableVertexAttribArray(m_positionLocation);
|
||||
glDisableVertexAttribArray(m_texCoordLocation);
|
||||
glDisableVertexAttribArray(posAttr);
|
||||
glDisableVertexAttribArray(texAttr);
|
||||
}
|
||||
@@ -5,13 +5,13 @@
|
||||
|
||||
#include <GLES2/gl2.h>
|
||||
|
||||
class TextProgram {
|
||||
class MsdfTextProgram {
|
||||
public:
|
||||
TextProgram() = default;
|
||||
~TextProgram() = default;
|
||||
MsdfTextProgram() = default;
|
||||
~MsdfTextProgram() = default;
|
||||
|
||||
TextProgram(const TextProgram&) = delete;
|
||||
TextProgram& operator=(const TextProgram&) = delete;
|
||||
MsdfTextProgram(const MsdfTextProgram&) = delete;
|
||||
MsdfTextProgram& operator=(const MsdfTextProgram&) = delete;
|
||||
|
||||
void ensureInitialized();
|
||||
void destroy();
|
||||
@@ -27,6 +27,7 @@ public:
|
||||
float v0,
|
||||
float u1,
|
||||
float v1,
|
||||
float pxRange,
|
||||
const Color& color) const;
|
||||
|
||||
private:
|
||||
@@ -35,6 +36,7 @@ private:
|
||||
GLint m_texCoordLocation = -1;
|
||||
GLint m_surfaceSizeLocation = -1;
|
||||
GLint m_rectLocation = -1;
|
||||
GLint m_pxRangeLocation = -1;
|
||||
GLint m_colorLocation = -1;
|
||||
GLint m_samplerLocation = -1;
|
||||
};
|
||||
@@ -0,0 +1,365 @@
|
||||
#include "render/MsdfTextRenderer.hpp"
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wshadow"
|
||||
#include <msdfgen.h>
|
||||
#include <msdfgen-ext.h>
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kDefaultFontPath = "/usr/share/fonts/google-roboto/Roboto-Medium.ttf";
|
||||
constexpr float kAtlasEmSize = 48.0f;
|
||||
constexpr double kDistanceRange = 4.0;
|
||||
constexpr int kGlyphPadding = 2;
|
||||
|
||||
} // namespace
|
||||
|
||||
MsdfTextRenderer::MsdfTextRenderer() = default;
|
||||
|
||||
MsdfTextRenderer::~MsdfTextRenderer() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
void MsdfTextRenderer::initialize() {
|
||||
if (m_face != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (FT_Init_FreeType(&m_library) != 0) {
|
||||
throw std::runtime_error("FT_Init_FreeType failed");
|
||||
}
|
||||
|
||||
if (FT_New_Face(m_library, kDefaultFontPath, 0, &m_face) != 0) {
|
||||
throw std::runtime_error("FT_New_Face failed");
|
||||
}
|
||||
|
||||
if (FT_Set_Pixel_Sizes(m_face, 0, static_cast<FT_UInt>(kAtlasEmSize)) != 0) {
|
||||
throw std::runtime_error("FT_Set_Pixel_Sizes failed");
|
||||
}
|
||||
|
||||
m_hbFont = hb_ft_font_create_referenced(m_face);
|
||||
if (m_hbFont == nullptr) {
|
||||
throw std::runtime_error("hb_ft_font_create_referenced failed");
|
||||
}
|
||||
m_currentShapingSize = kAtlasEmSize;
|
||||
|
||||
m_fontHandle = msdfgen::adoptFreetypeFont(m_face);
|
||||
if (m_fontHandle == nullptr) {
|
||||
throw std::runtime_error("msdfgen::adoptFreetypeFont failed");
|
||||
}
|
||||
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
ensureAtlasInitialized();
|
||||
m_program.ensureInitialized();
|
||||
}
|
||||
|
||||
MsdfTextRenderer::TextMetrics MsdfTextRenderer::measure(std::string_view text, float fontSize) {
|
||||
if (text.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
setShapingSize(fontSize);
|
||||
|
||||
hb_buffer_t* buffer = hb_buffer_create();
|
||||
if (buffer == nullptr) {
|
||||
throw std::runtime_error("hb_buffer_create failed");
|
||||
}
|
||||
|
||||
hb_buffer_add_utf8(buffer, text.data(), static_cast<int>(text.size()), 0, static_cast<int>(text.size()));
|
||||
hb_buffer_guess_segment_properties(buffer);
|
||||
hb_shape(m_hbFont, buffer, nullptr, 0);
|
||||
|
||||
unsigned int glyphCount = 0;
|
||||
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(buffer, &glyphCount);
|
||||
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(buffer, &glyphCount);
|
||||
|
||||
const float scale = fontSize / kAtlasEmSize;
|
||||
float width = 0.0f;
|
||||
float penX = 0.0f;
|
||||
float minTop = 0.0f;
|
||||
float maxBottom = 0.0f;
|
||||
bool hasBounds = false;
|
||||
|
||||
for (unsigned int i = 0; i < glyphCount; ++i) {
|
||||
const auto glyphIndex = glyphInfos[i].codepoint;
|
||||
Glyph& glyph = loadGlyph(glyphIndex);
|
||||
const float xOffset = static_cast<float>(glyphPositions[i].x_offset) / 64.0f;
|
||||
const float yOffset = static_cast<float>(glyphPositions[i].y_offset) / 64.0f;
|
||||
const float glyphLeft = penX + xOffset + glyph.bearingX * scale;
|
||||
const float glyphTop = -yOffset - glyph.bearingY * scale;
|
||||
const float glyphBottom = glyphTop + glyph.atlasHeight * scale;
|
||||
const float glyphRight = glyphLeft + glyph.atlasWidth * scale;
|
||||
|
||||
if (glyph.atlasWidth > 0.0f && glyph.atlasHeight > 0.0f) {
|
||||
if (!hasBounds) {
|
||||
minTop = glyphTop;
|
||||
maxBottom = glyphBottom;
|
||||
width = glyphRight;
|
||||
hasBounds = true;
|
||||
} else {
|
||||
minTop = std::min(minTop, glyphTop);
|
||||
maxBottom = std::max(maxBottom, glyphBottom);
|
||||
width = std::max(width, glyphRight);
|
||||
}
|
||||
}
|
||||
|
||||
penX += static_cast<float>(glyphPositions[i].x_advance) / 64.0f;
|
||||
}
|
||||
|
||||
width = std::max(width, penX);
|
||||
|
||||
hb_buffer_destroy(buffer);
|
||||
|
||||
return TextMetrics{
|
||||
.width = width,
|
||||
.top = minTop,
|
||||
.bottom = maxBottom,
|
||||
};
|
||||
}
|
||||
|
||||
void MsdfTextRenderer::draw(float surfaceWidth,
|
||||
float surfaceHeight,
|
||||
float x,
|
||||
float baselineY,
|
||||
std::string_view text,
|
||||
float fontSize,
|
||||
const Color& color) {
|
||||
if (text.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShapingSize(fontSize);
|
||||
|
||||
hb_buffer_t* buffer = hb_buffer_create();
|
||||
if (buffer == nullptr) {
|
||||
throw std::runtime_error("hb_buffer_create failed");
|
||||
}
|
||||
|
||||
hb_buffer_add_utf8(buffer, text.data(), static_cast<int>(text.size()), 0, static_cast<int>(text.size()));
|
||||
hb_buffer_guess_segment_properties(buffer);
|
||||
hb_shape(m_hbFont, buffer, nullptr, 0);
|
||||
|
||||
unsigned int glyphCount = 0;
|
||||
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(buffer, &glyphCount);
|
||||
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(buffer, &glyphCount);
|
||||
|
||||
const float scale = fontSize / kAtlasEmSize;
|
||||
const float pxRange = std::max(static_cast<float>(kDistanceRange) * scale, 1.0f);
|
||||
float penX = x;
|
||||
float penY = std::round(baselineY);
|
||||
|
||||
for (unsigned int i = 0; i < glyphCount; ++i) {
|
||||
const auto glyphIndex = glyphInfos[i].codepoint;
|
||||
Glyph& glyph = loadGlyph(glyphIndex);
|
||||
|
||||
if (glyph.atlasWidth > 0.0f && glyph.atlasHeight > 0.0f) {
|
||||
const float xOffset = static_cast<float>(glyphPositions[i].x_offset) / 64.0f;
|
||||
const float yOffset = static_cast<float>(glyphPositions[i].y_offset) / 64.0f;
|
||||
const float glyphX = penX + xOffset + glyph.bearingX * scale;
|
||||
const float glyphY = penY - yOffset - glyph.bearingY * scale;
|
||||
const float glyphW = glyph.atlasWidth * scale;
|
||||
const float glyphH = glyph.atlasHeight * scale;
|
||||
|
||||
m_program.draw(
|
||||
m_atlasTexture,
|
||||
surfaceWidth,
|
||||
surfaceHeight,
|
||||
glyphX,
|
||||
glyphY,
|
||||
glyphW,
|
||||
glyphH,
|
||||
glyph.u0,
|
||||
glyph.v0,
|
||||
glyph.u1,
|
||||
glyph.v1,
|
||||
pxRange,
|
||||
color);
|
||||
}
|
||||
|
||||
penX += static_cast<float>(glyphPositions[i].x_advance) / 64.0f;
|
||||
penY -= static_cast<float>(glyphPositions[i].y_advance) / 64.0f;
|
||||
}
|
||||
|
||||
hb_buffer_destroy(buffer);
|
||||
}
|
||||
|
||||
void MsdfTextRenderer::cleanup() {
|
||||
if (m_atlasTexture != 0) {
|
||||
glDeleteTextures(1, &m_atlasTexture);
|
||||
m_atlasTexture = 0;
|
||||
}
|
||||
m_glyphs.clear();
|
||||
m_atlasCursorX = 1;
|
||||
m_atlasCursorY = 1;
|
||||
m_atlasRowHeight = 0;
|
||||
|
||||
m_program.destroy();
|
||||
|
||||
if (m_fontHandle != nullptr) {
|
||||
msdfgen::destroyFont(m_fontHandle);
|
||||
m_fontHandle = nullptr;
|
||||
}
|
||||
|
||||
if (m_hbFont != nullptr) {
|
||||
hb_font_destroy(m_hbFont);
|
||||
m_hbFont = nullptr;
|
||||
}
|
||||
|
||||
if (m_face != nullptr) {
|
||||
FT_Done_Face(m_face);
|
||||
m_face = nullptr;
|
||||
}
|
||||
|
||||
if (m_library != nullptr) {
|
||||
FT_Done_FreeType(m_library);
|
||||
m_library = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void MsdfTextRenderer::ensureAtlasInitialized() {
|
||||
if (m_atlasTexture != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
glGenTextures(1, &m_atlasTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_atlasTexture);
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_RGB,
|
||||
m_atlasWidth,
|
||||
m_atlasHeight,
|
||||
0,
|
||||
GL_RGB,
|
||||
GL_UNSIGNED_BYTE,
|
||||
nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
}
|
||||
|
||||
void MsdfTextRenderer::setShapingSize(float fontSize) {
|
||||
if (std::abs(fontSize - m_currentShapingSize) < 0.01f) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (FT_Set_Pixel_Sizes(m_face, 0, static_cast<FT_UInt>(fontSize)) != 0) {
|
||||
throw std::runtime_error("FT_Set_Pixel_Sizes failed");
|
||||
}
|
||||
|
||||
if (m_hbFont != nullptr) {
|
||||
hb_font_destroy(m_hbFont);
|
||||
}
|
||||
m_hbFont = hb_ft_font_create_referenced(m_face);
|
||||
if (m_hbFont == nullptr) {
|
||||
throw std::runtime_error("hb_ft_font_create_referenced failed");
|
||||
}
|
||||
|
||||
m_currentShapingSize = fontSize;
|
||||
}
|
||||
|
||||
MsdfTextRenderer::Glyph& MsdfTextRenderer::loadGlyph(std::uint32_t glyphIndex) {
|
||||
if (auto it = m_glyphs.find(glyphIndex); it != m_glyphs.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
msdfgen::Shape shape;
|
||||
double advance = 0.0;
|
||||
if (!msdfgen::loadGlyph(shape, m_fontHandle, msdfgen::GlyphIndex(glyphIndex),
|
||||
msdfgen::FONT_SCALING_EM_NORMALIZED, &advance)) {
|
||||
auto [it, _] = m_glyphs.emplace(glyphIndex, Glyph{});
|
||||
return it->second;
|
||||
}
|
||||
|
||||
if (shape.contours.empty()) {
|
||||
auto [it, _] = m_glyphs.emplace(glyphIndex, Glyph{});
|
||||
return it->second;
|
||||
}
|
||||
|
||||
shape.normalize();
|
||||
msdfgen::Shape::Bounds bounds = shape.getBounds();
|
||||
|
||||
const auto emSize = static_cast<double>(kAtlasEmSize);
|
||||
const double pxLeft = bounds.l * emSize - kDistanceRange;
|
||||
const double pxBottom = bounds.b * emSize - kDistanceRange;
|
||||
const double pxRight = bounds.r * emSize + kDistanceRange;
|
||||
const double pxTop = bounds.t * emSize + kDistanceRange;
|
||||
|
||||
const int glyphW = std::max(1, static_cast<int>(std::ceil(pxRight - pxLeft)));
|
||||
const int glyphH = std::max(1, static_cast<int>(std::ceil(pxTop - pxBottom)));
|
||||
|
||||
msdfgen::edgeColoringSimple(shape, 3.0);
|
||||
|
||||
msdfgen::Bitmap<float, 3> msdf(glyphW, glyphH);
|
||||
const msdfgen::Projection projection(
|
||||
msdfgen::Vector2(emSize, emSize),
|
||||
msdfgen::Vector2(-pxLeft / emSize, -pxBottom / emSize));
|
||||
const msdfgen::Range range(kDistanceRange / emSize);
|
||||
msdfgen::generateMSDF(msdf, shape, projection, range);
|
||||
|
||||
const auto pixelCount = static_cast<std::size_t>(glyphW * glyphH * 3);
|
||||
std::vector<unsigned char> pixels(pixelCount);
|
||||
for (int y = 0; y < glyphH; ++y) {
|
||||
const int srcY = glyphH - 1 - y;
|
||||
for (int x = 0; x < glyphW; ++x) {
|
||||
const auto dstIdx = static_cast<std::size_t>((y * glyphW + x) * 3);
|
||||
const float* pixel = msdf(x, srcY);
|
||||
pixels[dstIdx + 0] = msdfgen::pixelFloatToByte(pixel[0]);
|
||||
pixels[dstIdx + 1] = msdfgen::pixelFloatToByte(pixel[1]);
|
||||
pixels[dstIdx + 2] = msdfgen::pixelFloatToByte(pixel[2]);
|
||||
}
|
||||
}
|
||||
|
||||
const int paddedW = glyphW + kGlyphPadding * 2;
|
||||
const int paddedH = glyphH + kGlyphPadding * 2;
|
||||
|
||||
if (m_atlasCursorX + paddedW > m_atlasWidth) {
|
||||
m_atlasCursorX = 1;
|
||||
m_atlasCursorY += m_atlasRowHeight + 1;
|
||||
m_atlasRowHeight = 0;
|
||||
}
|
||||
|
||||
if (m_atlasCursorY + paddedH > m_atlasHeight) {
|
||||
throw std::runtime_error("MSDF text atlas is full");
|
||||
}
|
||||
|
||||
const int destX = m_atlasCursorX + kGlyphPadding;
|
||||
const int destY = m_atlasCursorY + kGlyphPadding;
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, m_atlasTexture);
|
||||
glTexSubImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
destX,
|
||||
destY,
|
||||
glyphW,
|
||||
glyphH,
|
||||
GL_RGB,
|
||||
GL_UNSIGNED_BYTE,
|
||||
pixels.data());
|
||||
|
||||
Glyph glyph{
|
||||
.atlasWidth = static_cast<float>(glyphW),
|
||||
.atlasHeight = static_cast<float>(glyphH),
|
||||
.bearingX = static_cast<float>(pxLeft),
|
||||
.bearingY = static_cast<float>(pxTop),
|
||||
.u0 = static_cast<float>(destX) / static_cast<float>(m_atlasWidth),
|
||||
.v0 = static_cast<float>(destY) / static_cast<float>(m_atlasHeight),
|
||||
.u1 = static_cast<float>(destX + glyphW) / static_cast<float>(m_atlasWidth),
|
||||
.v1 = static_cast<float>(destY + glyphH) / static_cast<float>(m_atlasHeight),
|
||||
};
|
||||
|
||||
m_atlasCursorX += paddedW + 1;
|
||||
m_atlasRowHeight = std::max(m_atlasRowHeight, paddedH);
|
||||
|
||||
auto [it, _] = m_glyphs.emplace(glyphIndex, glyph);
|
||||
return it->second;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "render/Color.hpp"
|
||||
#include "render/TextProgram.hpp"
|
||||
#include "render/MsdfTextProgram.hpp"
|
||||
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
@@ -11,7 +11,11 @@
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
class TextRenderer {
|
||||
namespace msdfgen {
|
||||
class FontHandle;
|
||||
}
|
||||
|
||||
class MsdfTextRenderer {
|
||||
public:
|
||||
struct TextMetrics {
|
||||
float width = 0.0f;
|
||||
@@ -19,26 +23,27 @@ public:
|
||||
float bottom = 0.0f;
|
||||
};
|
||||
|
||||
TextRenderer();
|
||||
~TextRenderer();
|
||||
MsdfTextRenderer();
|
||||
~MsdfTextRenderer();
|
||||
|
||||
TextRenderer(const TextRenderer&) = delete;
|
||||
TextRenderer& operator=(const TextRenderer&) = delete;
|
||||
MsdfTextRenderer(const MsdfTextRenderer&) = delete;
|
||||
MsdfTextRenderer& operator=(const MsdfTextRenderer&) = delete;
|
||||
|
||||
void initialize();
|
||||
[[nodiscard]] TextMetrics measure(std::string_view text);
|
||||
[[nodiscard]] TextMetrics measure(std::string_view text, float fontSize);
|
||||
void draw(float surfaceWidth,
|
||||
float surfaceHeight,
|
||||
float x,
|
||||
float baselineY,
|
||||
std::string_view text,
|
||||
float fontSize,
|
||||
const Color& color);
|
||||
void cleanup();
|
||||
|
||||
private:
|
||||
struct Glyph {
|
||||
float width = 0.0f;
|
||||
float height = 0.0f;
|
||||
float atlasWidth = 0.0f;
|
||||
float atlasHeight = 0.0f;
|
||||
float bearingX = 0.0f;
|
||||
float bearingY = 0.0f;
|
||||
float u0 = 0.0f;
|
||||
@@ -49,16 +54,19 @@ private:
|
||||
|
||||
Glyph& loadGlyph(std::uint32_t glyphIndex);
|
||||
void ensureAtlasInitialized();
|
||||
void setShapingSize(float fontSize);
|
||||
|
||||
FT_Library m_library = nullptr;
|
||||
FT_Face m_face = nullptr;
|
||||
hb_font_t* m_hbFont = nullptr;
|
||||
TextProgram m_program;
|
||||
msdfgen::FontHandle* m_fontHandle = nullptr;
|
||||
MsdfTextProgram m_program;
|
||||
GLuint m_atlasTexture = 0;
|
||||
int m_atlasWidth = 1024;
|
||||
int m_atlasHeight = 1024;
|
||||
int m_atlasWidth = 2048;
|
||||
int m_atlasHeight = 2048;
|
||||
int m_atlasCursorX = 1;
|
||||
int m_atlasCursorY = 1;
|
||||
int m_atlasRowHeight = 0;
|
||||
float m_currentShapingSize = 0.0f;
|
||||
std::unordered_map<std::uint32_t, Glyph> m_glyphs;
|
||||
};
|
||||
@@ -1,290 +0,0 @@
|
||||
#include "render/TextRenderer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kDefaultFontPath = "/usr/share/fonts/google-roboto/Roboto-Medium.ttf";
|
||||
constexpr unsigned int kFontPixelSize = 14;
|
||||
constexpr float kRasterScale = 1.0f;
|
||||
constexpr int kGlyphPadding = 1;
|
||||
|
||||
} // namespace
|
||||
|
||||
TextRenderer::TextRenderer() = default;
|
||||
|
||||
TextRenderer::~TextRenderer() {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
void TextRenderer::initialize() {
|
||||
if (m_face != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (FT_Init_FreeType(&m_library) != 0) {
|
||||
throw std::runtime_error("FT_Init_FreeType failed");
|
||||
}
|
||||
|
||||
if (FT_New_Face(m_library, kDefaultFontPath, 0, &m_face) != 0) {
|
||||
throw std::runtime_error("FT_New_Face failed");
|
||||
}
|
||||
|
||||
if (FT_Set_Pixel_Sizes(m_face, 0, static_cast<FT_UInt>(kFontPixelSize)) != 0) {
|
||||
throw std::runtime_error("FT_Set_Pixel_Sizes failed");
|
||||
}
|
||||
|
||||
m_hbFont = hb_ft_font_create_referenced(m_face);
|
||||
if (m_hbFont == nullptr) {
|
||||
throw std::runtime_error("hb_ft_font_create_referenced failed");
|
||||
}
|
||||
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
ensureAtlasInitialized();
|
||||
m_program.ensureInitialized();
|
||||
}
|
||||
|
||||
TextRenderer::TextMetrics TextRenderer::measure(std::string_view text) {
|
||||
if (text.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
hb_buffer_t* buffer = hb_buffer_create();
|
||||
if (buffer == nullptr) {
|
||||
throw std::runtime_error("hb_buffer_create failed");
|
||||
}
|
||||
|
||||
hb_buffer_add_utf8(buffer, text.data(), static_cast<int>(text.size()), 0, static_cast<int>(text.size()));
|
||||
hb_buffer_guess_segment_properties(buffer);
|
||||
hb_shape(m_hbFont, buffer, nullptr, 0);
|
||||
|
||||
unsigned int glyphCount = 0;
|
||||
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(buffer, &glyphCount);
|
||||
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(buffer, &glyphCount);
|
||||
|
||||
float width = 0.0f;
|
||||
float penX = 0.0f;
|
||||
float minTop = 0.0f;
|
||||
float maxBottom = 0.0f;
|
||||
bool hasBounds = false;
|
||||
|
||||
for (unsigned int i = 0; i < glyphCount; ++i) {
|
||||
const auto glyphIndex = glyphInfos[i].codepoint;
|
||||
Glyph& glyph = loadGlyph(glyphIndex);
|
||||
const float xOffset = static_cast<float>(glyphPositions[i].x_offset) / 64.0f / kRasterScale;
|
||||
const float yOffset = static_cast<float>(glyphPositions[i].y_offset) / 64.0f / kRasterScale;
|
||||
const float glyphLeft = penX + xOffset + glyph.bearingX;
|
||||
const float glyphTop = -yOffset - glyph.bearingY;
|
||||
const float glyphBottom = glyphTop + glyph.height;
|
||||
const float glyphRight = glyphLeft + glyph.width;
|
||||
|
||||
if (glyph.width > 0.0f && glyph.height > 0.0f) {
|
||||
if (!hasBounds) {
|
||||
minTop = glyphTop;
|
||||
maxBottom = glyphBottom;
|
||||
width = glyphRight;
|
||||
hasBounds = true;
|
||||
} else {
|
||||
minTop = std::min(minTop, glyphTop);
|
||||
maxBottom = std::max(maxBottom, glyphBottom);
|
||||
width = std::max(width, glyphRight);
|
||||
}
|
||||
}
|
||||
|
||||
penX += static_cast<float>(glyphPositions[i].x_advance) / 64.0f / kRasterScale;
|
||||
}
|
||||
|
||||
width = std::max(width, penX);
|
||||
|
||||
hb_buffer_destroy(buffer);
|
||||
|
||||
return TextMetrics{
|
||||
.width = width,
|
||||
.top = minTop,
|
||||
.bottom = maxBottom,
|
||||
};
|
||||
}
|
||||
|
||||
void TextRenderer::draw(float surfaceWidth,
|
||||
float surfaceHeight,
|
||||
float x,
|
||||
float baselineY,
|
||||
std::string_view text,
|
||||
const Color& color) {
|
||||
if (text.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
hb_buffer_t* buffer = hb_buffer_create();
|
||||
if (buffer == nullptr) {
|
||||
throw std::runtime_error("hb_buffer_create failed");
|
||||
}
|
||||
|
||||
hb_buffer_add_utf8(buffer, text.data(), static_cast<int>(text.size()), 0, static_cast<int>(text.size()));
|
||||
hb_buffer_guess_segment_properties(buffer);
|
||||
hb_shape(m_hbFont, buffer, nullptr, 0);
|
||||
|
||||
unsigned int glyphCount = 0;
|
||||
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(buffer, &glyphCount);
|
||||
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(buffer, &glyphCount);
|
||||
|
||||
float penX = x;
|
||||
float penY = std::round(baselineY);
|
||||
|
||||
for (unsigned int i = 0; i < glyphCount; ++i) {
|
||||
const auto glyphIndex = glyphInfos[i].codepoint;
|
||||
Glyph& glyph = loadGlyph(glyphIndex);
|
||||
|
||||
const float xOffset = static_cast<float>(glyphPositions[i].x_offset) / 64.0f / kRasterScale;
|
||||
const float yOffset = static_cast<float>(glyphPositions[i].y_offset) / 64.0f / kRasterScale;
|
||||
const float glyphX = std::round(penX + xOffset + glyph.bearingX);
|
||||
const float glyphY = std::round(penY - yOffset - glyph.bearingY);
|
||||
|
||||
m_program.draw(
|
||||
m_atlasTexture,
|
||||
surfaceWidth,
|
||||
surfaceHeight,
|
||||
glyphX,
|
||||
glyphY,
|
||||
glyph.width,
|
||||
glyph.height,
|
||||
glyph.u0,
|
||||
glyph.v0,
|
||||
glyph.u1,
|
||||
glyph.v1,
|
||||
color);
|
||||
|
||||
penX += static_cast<float>(glyphPositions[i].x_advance) / 64.0f / kRasterScale;
|
||||
penY -= static_cast<float>(glyphPositions[i].y_advance) / 64.0f / kRasterScale;
|
||||
}
|
||||
|
||||
hb_buffer_destroy(buffer);
|
||||
}
|
||||
|
||||
void TextRenderer::cleanup() {
|
||||
if (m_atlasTexture != 0) {
|
||||
glDeleteTextures(1, &m_atlasTexture);
|
||||
m_atlasTexture = 0;
|
||||
}
|
||||
m_glyphs.clear();
|
||||
m_atlasCursorX = 1;
|
||||
m_atlasCursorY = 1;
|
||||
m_atlasRowHeight = 0;
|
||||
|
||||
m_program.destroy();
|
||||
|
||||
if (m_hbFont != nullptr) {
|
||||
hb_font_destroy(m_hbFont);
|
||||
m_hbFont = nullptr;
|
||||
}
|
||||
|
||||
if (m_face != nullptr) {
|
||||
FT_Done_Face(m_face);
|
||||
m_face = nullptr;
|
||||
}
|
||||
|
||||
if (m_library != nullptr) {
|
||||
FT_Done_FreeType(m_library);
|
||||
m_library = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void TextRenderer::ensureAtlasInitialized() {
|
||||
if (m_atlasTexture != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
glGenTextures(1, &m_atlasTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, m_atlasTexture);
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_ALPHA,
|
||||
m_atlasWidth,
|
||||
m_atlasHeight,
|
||||
0,
|
||||
GL_ALPHA,
|
||||
GL_UNSIGNED_BYTE,
|
||||
nullptr);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
}
|
||||
|
||||
TextRenderer::Glyph& TextRenderer::loadGlyph(std::uint32_t glyphIndex) {
|
||||
if (auto it = m_glyphs.find(glyphIndex); it != m_glyphs.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
if (FT_Load_Glyph(m_face, glyphIndex, FT_LOAD_DEFAULT | FT_LOAD_TARGET_LIGHT) != 0) {
|
||||
throw std::runtime_error("FT_Load_Glyph failed");
|
||||
}
|
||||
|
||||
if (FT_Render_Glyph(m_face->glyph, FT_RENDER_MODE_NORMAL) != 0) {
|
||||
throw std::runtime_error("FT_Render_Glyph failed");
|
||||
}
|
||||
|
||||
const FT_GlyphSlot slot = m_face->glyph;
|
||||
const int glyphPixelWidth = static_cast<int>(slot->bitmap.width);
|
||||
const int paddedWidth = glyphPixelWidth + (kGlyphPadding * 2);
|
||||
const int paddedHeight = static_cast<int>(slot->bitmap.rows) + (kGlyphPadding * 2);
|
||||
|
||||
if (m_atlasCursorX + paddedWidth > m_atlasWidth) {
|
||||
m_atlasCursorX = 1;
|
||||
m_atlasCursorY += m_atlasRowHeight + 1;
|
||||
m_atlasRowHeight = 0;
|
||||
}
|
||||
|
||||
if (m_atlasCursorY + paddedHeight > m_atlasHeight) {
|
||||
throw std::runtime_error("text atlas is full");
|
||||
}
|
||||
|
||||
std::vector<unsigned char> paddedBitmap(static_cast<std::size_t>(paddedWidth * paddedHeight), 0);
|
||||
|
||||
for (unsigned int row = 0; row < slot->bitmap.rows; ++row) {
|
||||
const auto* src = slot->bitmap.buffer + row * slot->bitmap.pitch;
|
||||
auto* dst = paddedBitmap.data()
|
||||
+ ((static_cast<std::size_t>(row) + static_cast<std::size_t>(kGlyphPadding))
|
||||
* static_cast<std::size_t>(paddedWidth))
|
||||
+ static_cast<std::size_t>(kGlyphPadding);
|
||||
std::memcpy(dst, src, static_cast<std::size_t>(slot->bitmap.width));
|
||||
}
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, m_atlasTexture);
|
||||
glTexSubImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
m_atlasCursorX,
|
||||
m_atlasCursorY,
|
||||
paddedWidth,
|
||||
paddedHeight,
|
||||
GL_ALPHA,
|
||||
GL_UNSIGNED_BYTE,
|
||||
paddedBitmap.data());
|
||||
|
||||
Glyph glyph{
|
||||
.width = static_cast<float>(glyphPixelWidth) / kRasterScale,
|
||||
.height = static_cast<float>(slot->bitmap.rows) / kRasterScale,
|
||||
.bearingX = static_cast<float>(slot->bitmap_left) / kRasterScale,
|
||||
.bearingY = static_cast<float>(slot->bitmap_top) / kRasterScale,
|
||||
.u0 = (static_cast<float>(m_atlasCursorX + kGlyphPadding) + 0.5f) / static_cast<float>(m_atlasWidth),
|
||||
.v0 = (static_cast<float>(m_atlasCursorY + kGlyphPadding) + 0.5f) / static_cast<float>(m_atlasHeight),
|
||||
.u1 = (static_cast<float>(m_atlasCursorX + kGlyphPadding + glyphPixelWidth) - 0.5f)
|
||||
/ static_cast<float>(m_atlasWidth),
|
||||
.v1 = (static_cast<float>(m_atlasCursorY + kGlyphPadding + slot->bitmap.rows) - 0.5f)
|
||||
/ static_cast<float>(m_atlasHeight),
|
||||
};
|
||||
|
||||
m_atlasCursorX += paddedWidth + 1;
|
||||
m_atlasRowHeight = std::max(m_atlasRowHeight, paddedHeight);
|
||||
|
||||
auto [it, _] = m_glyphs.emplace(glyphIndex, glyph);
|
||||
return it->second;
|
||||
}
|
||||
+1
Submodule third_party/msdfgen added at 85e8b3d47b
Reference in New Issue
Block a user