feat: 添加MSDF字体支持并重构文本渲染系统

实现MSDF字体渲染功能,包括:
1. 新增MSDF字体构建工具,支持从TTF生成MSDF图集
2. 重构字体加载器和文本渲染器以支持MSDF
3. 添加MSDF文本渲染示例程序
4. 移除旧的TrueType字体渲染实现
5. 优化VAO顶点属性绑定设置

MSDF字体提供高质量的可缩放文本渲染,同时保持运行时性能。构建工具将TTF转换为带元数据的PNG图集,运行时只需加载预生成的纹理。重构后的文本渲染系统更简洁高效,支持中文等复杂字符集。
This commit is contained in:
ChestnutYueyue 2026-02-23 21:30:44 +08:00
parent 5158fd713e
commit bbefd83664
29 changed files with 3964 additions and 6719 deletions

View File

@ -142,13 +142,14 @@ private:
};
// ---------------------------------------------------------------------------
// FontLoader - 字体加载器
// FontLoader - 字体加载器 (MSDF)
// ---------------------------------------------------------------------------
/**
* @brief
*
* TrueType .ttf, .otf
* MSDF PNG
* 使 msdfgen PNG
*/
class FontLoader : public AssetLoader<FontAsset> {
public:
@ -157,6 +158,24 @@ public:
bool canLoad(const std::string &path) const override;
AssetType type() const override { return AssetType::Font; }
std::vector<std::string> extensions() const override;
private:
/**
* @brief PNG
*/
bool parseEmbeddedMetadata(const std::string &path, FontAsset *asset);
/**
* @brief tEXt chunk
*/
bool readTextChunk(const std::vector<u8> &pngData,
const std::string &keyword,
std::vector<u8> &output);
/**
* @brief JSON
*/
bool parseJsonMetadata(const std::string &json, FontAsset *asset);
};
// ---------------------------------------------------------------------------

View File

@ -1,15 +1,32 @@
#pragma once
#include <extra2d/asset/asset.h>
#include <extra2d/core/types.h>
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <unordered_map>
#include <vector>
#include <string>
namespace extra2d {
/**
* @brief
* @brief
*/
struct GlyphInfo {
char32_t codepoint = 0; ///< Unicode 码点
glm::vec2 uvMin; ///< UV 左下角
glm::vec2 uvMax; ///< UV 右上角
glm::vec2 size; ///< 字符尺寸(像素)
glm::vec2 bearing; ///< 基线偏移
float advance = 0.0f; ///< 前进宽度
};
/**
* @brief MSDF
*
* TrueType字体数据
* 使 Pimpl stbtt_fontinfo
* MSDF PNG
* 使 msdfgen PNG
*/
class FontAsset : public Asset {
public:
@ -17,46 +34,152 @@ public:
~FontAsset() override;
AssetType type() const override { return AssetType::Font; }
bool loaded() const override;
size_t memSize() const override { return data_.size(); }
size_t memSize() const override;
/**
* @brief
* @param pixels
* @return
* @brief ID
* @return OpenGL ID
*/
float scaleForPixelHeight(float pixels) const;
GLuint textureId() const { return textureId_; }
/**
* @brief
* @return
* @brief
* @param codepoint Unicode
* @return nullptr
*/
const u8 *data() const { return data_.data(); }
const GlyphInfo* getGlyph(char32_t codepoint) const;
/**
* @brief
* @return
* @brief
* @param codepoint Unicode
* @return true
*/
size_t dataSize() const { return data_.size(); }
bool hasGlyph(char32_t codepoint) const;
/**
* @brief
* @param data
* @return true
* @brief
* @param text
* @return
*/
bool setData(std::vector<u8> data);
std::vector<char32_t> getMissingChars(const std::u32string& text) const;
/**
* @brief
* @brief
* @param text
* @return true
*/
bool canRender(const std::u32string& text) const;
/**
* @brief
* @param text
* @param scale
* @return
*/
glm::vec2 measureText(const std::u32string& text, float scale = 1.0f) const;
/**
* @brief
* @return
*/
int fontSize() const { return fontSize_; }
/**
* @brief
* @return
*/
float pxRange() const { return pxRange_; }
/**
* @brief
* @return
*/
int atlasWidth() const { return atlasWidth_; }
/**
* @brief
* @return
*/
int atlasHeight() const { return atlasHeight_; }
/**
* @brief
* @return
*/
int lineHeight() const { return lineHeight_; }
/**
* @brief 线
* @return 线
*/
int baseline() const { return baseline_; }
/**
* @brief ID
* @param id ID
*/
void setTextureId(GLuint id) { textureId_ = id; }
/**
* @brief
* @param size
*/
void setFontSize(int size) { fontSize_ = size; }
/**
* @brief
* @param range
*/
void setPxRange(float range) { pxRange_ = range; }
/**
* @brief
* @param width
* @param height
*/
void setAtlasSize(int width, int height) {
atlasWidth_ = width;
atlasHeight_ = height;
}
/**
* @brief
* @param height
*/
void setLineHeight(int height) { lineHeight_ = height; }
/**
* @brief 线
* @param baseline 线
*/
void setBaseline(int baseline) { baseline_ = baseline; }
/**
* @brief
* @param glyph
*/
void addGlyph(const GlyphInfo& glyph);
/**
* @brief
*/
void markLoaded();
/**
* @brief
*/
void release();
private:
std::vector<u8> data_;
class Impl;
Unique<Impl> impl_;
GLuint textureId_ = 0;
int fontSize_ = 48;
float pxRange_ = 4.0f;
int atlasWidth_ = 2048;
int atlasHeight_ = 2048;
int lineHeight_ = 60;
int baseline_ = 12;
std::unordered_map<char32_t, GlyphInfo> glyphs_;
};
} // namespace extra2d

View File

@ -1,184 +0,0 @@
#pragma once
#include <extra2d/asset/asset.h>
#include <extra2d/core/types.h>
#include <glad/glad.h>
#include <glm/glm.hpp>
#include <unordered_map>
#include <vector>
#include <string>
namespace extra2d {
/**
* @brief
*/
struct GlyphInfo {
char32_t codepoint = 0; ///< Unicode 码点
glm::vec2 uvMin; ///< UV 左下角
glm::vec2 uvMax; ///< UV 右上角
glm::vec2 size; ///< 字符尺寸(像素)
glm::vec2 bearing; ///< 基线偏移
float advance = 0.0f; ///< 前进宽度
};
/**
* @brief MSDF
*
* MSDF PNG
*/
class MSDFFontAsset : public Asset {
public:
MSDFFontAsset();
~MSDFFontAsset() override;
AssetType type() const override { return AssetType::Font; }
bool loaded() const override;
size_t memSize() const override;
/**
* @brief ID
* @return OpenGL ID
*/
GLuint textureId() const { return textureId_; }
/**
* @brief
* @param codepoint Unicode
* @return nullptr
*/
const GlyphInfo* getGlyph(char32_t codepoint) const;
/**
* @brief
* @param codepoint Unicode
* @return true
*/
bool hasGlyph(char32_t codepoint) const;
/**
* @brief
* @param text
* @return
*/
std::vector<char32_t> getMissingChars(const std::u32string& text) const;
/**
* @brief
* @param text
* @return true
*/
bool canRender(const std::u32string& text) const;
/**
* @brief
* @param text
* @param scale
* @return
*/
glm::vec2 measureText(const std::u32string& text, float scale = 1.0f) const;
/**
* @brief
* @return
*/
int fontSize() const { return fontSize_; }
/**
* @brief
* @return
*/
float pxRange() const { return pxRange_; }
/**
* @brief
* @return
*/
int atlasWidth() const { return atlasWidth_; }
/**
* @brief
* @return
*/
int atlasHeight() const { return atlasHeight_; }
/**
* @brief
* @return
*/
int lineHeight() const { return lineHeight_; }
/**
* @brief 线
* @return 线
*/
int baseline() const { return baseline_; }
/**
* @brief ID
* @param id ID
*/
void setTextureId(GLuint id) { textureId_ = id; }
/**
* @brief
* @param size
*/
void setFontSize(int size) { fontSize_ = size; }
/**
* @brief
* @param range
*/
void setPxRange(float range) { pxRange_ = range; }
/**
* @brief
* @param width
* @param height
*/
void setAtlasSize(int width, int height) {
atlasWidth_ = width;
atlasHeight_ = height;
}
/**
* @brief
* @param height
*/
void setLineHeight(int height) { lineHeight_ = height; }
/**
* @brief 线
* @param baseline 线
*/
void setBaseline(int baseline) { baseline_ = baseline; }
/**
* @brief
* @param glyph
*/
void addGlyph(const GlyphInfo& glyph);
/**
* @brief
*/
void markLoaded();
/**
* @brief
*/
void release();
private:
GLuint textureId_ = 0;
int fontSize_ = 48;
float pxRange_ = 4.0f;
int atlasWidth_ = 2048;
int atlasHeight_ = 2048;
int lineHeight_ = 60;
int baseline_ = 12;
std::unordered_map<char32_t, GlyphInfo> glyphs_;
};
} // namespace extra2d

View File

@ -1,41 +0,0 @@
#pragma once
#include <extra2d/asset/asset_loader.h>
#include <extra2d/asset/msdf_font_asset.h>
#include <string>
namespace extra2d {
/**
* @brief MSDF
*
* MSDF PNG
*/
class MSDFFontLoader : public AssetLoader<MSDFFontAsset> {
public:
Ref<MSDFFontAsset> load(const std::string& path) override;
Ref<MSDFFontAsset> loadFromMemory(const u8* data, size_t size) override;
bool canLoad(const std::string& path) const override;
AssetType type() const override { return AssetType::Font; }
std::vector<std::string> extensions() const override;
private:
/**
* @brief PNG
*/
bool parseEmbeddedMetadata(const std::string& path, MSDFFontAsset* asset);
/**
* @brief tEXt chunk
*/
bool readTextChunk(const std::vector<u8>& pngData,
const std::string& keyword,
std::vector<u8>& output);
/**
* @brief JSON
*/
bool parseJsonMetadata(const std::string& json, MSDFFontAsset* asset);
};
} // namespace extra2d

View File

@ -1,118 +0,0 @@
#pragma once
#include <extra2d/asset/msdf_font_asset.h>
#include <extra2d/render/material.h>
#include <extra2d/core/types.h>
#include <glm/glm.hpp>
#include <string>
namespace extra2d {
/**
* @brief MSDF
*
*
*/
class MSDFTextRenderer {
public:
MSDFTextRenderer();
~MSDFTextRenderer();
/**
* @brief
* @return true
*/
bool init();
/**
* @brief
*/
void shutdown();
/**
* @brief
* @param font
*/
void setFont(Ref<MSDFFontAsset> font) { font_ = font; }
/**
* @brief
* @return
*/
Ref<MSDFFontAsset> font() const { return font_; }
/**
* @brief
* @param text UTF-32
*/
void setText(const std::u32string& text) { text_ = text; }
/**
* @brief UTF-8
* @param utf8Text UTF-8
*/
void setText(const std::string& utf8Text);
/**
* @brief
* @param pos
*/
void setPosition(const glm::vec2& pos) { position_ = pos; }
/**
* @brief
* @param size
*/
void setFontSize(float size);
/**
* @brief
* @param color
*/
void setColor(const Color& color) { color_ = color; }
/**
* @brief
* @param material
*/
void setMaterial(Ref<MaterialInstance> material) { material_ = material; }
/**
* @brief
* @return
*/
glm::vec2 getSize() const;
/**
* @brief
* @param projection
*/
void render(const glm::mat4& projection);
private:
Ref<MSDFFontAsset> font_;
std::u32string text_;
glm::vec2 position_ = glm::vec2(0.0f);
float fontSize_ = 48.0f;
float scale_ = 1.0f;
Color color_ = Colors::White;
Ref<MaterialInstance> material_;
struct Vertex {
glm::vec2 position;
glm::vec2 texCoord;
};
std::vector<Vertex> vertices_;
std::vector<uint32_t> indices_;
GLuint vao_ = 0;
GLuint vbo_ = 0;
GLuint ibo_ = 0;
GLuint shader_ = 0;
void updateGeometry();
void draw();
bool createShader();
};
} // namespace extra2d

View File

@ -1,57 +1,19 @@
#pragma once
#include <extra2d/asset/font_asset.h>
#include <extra2d/render/render_types.h>
#include <extra2d/render/render_device.h>
#include <extra2d/render/buffer.h>
#include <extra2d/render/vao.h>
#include <extra2d/render/material.h>
#include <glad/glad.h>
#include <glm/mat4x4.hpp>
#include <glm/vec2.hpp>
#include <glm/vec4.hpp>
#include <memory>
#include <unordered_map>
namespace extra2d {
/**
* @brief
*/
struct GlyphInfo {
char32_t codepoint = 0;
glm::vec2 uvMin = glm::vec2(0.0f);
glm::vec2 uvMax = glm::vec2(0.0f);
glm::vec2 size = glm::vec2(0.0f);
glm::vec2 bearing = glm::vec2(0.0f);
float advance = 0.0f;
};
/**
* @brief
*/
class FontAtlas {
public:
FontAtlas();
~FontAtlas();
bool create(int atlasWidth = 2048, int atlasHeight = 2048);
void destroy();
bool isValid() const { return texture_ != 0; }
GLuint texture() const { return texture_; }
int width() const { return width_; }
int height() const { return height_; }
void addGlyph(char32_t codepoint, const GlyphInfo& info);
const GlyphInfo* getGlyph(char32_t codepoint) const;
bool hasGlyph(char32_t codepoint) const;
private:
GLuint texture_ = 0;
int width_ = 0;
int height_ = 0;
std::unordered_map<char32_t, GlyphInfo> glyphs_;
};
/**
* @brief
*/
@ -69,7 +31,6 @@ struct TextConfig {
glm::vec4 color = glm::vec4(1.0f);
float outlineWidth = 0.0f;
glm::vec4 outlineColor = glm::vec4(0.0f);
float pxRange = 4.0f;
};
/**
@ -80,8 +41,7 @@ struct TextConfig {
class TextRenderer {
public:
static constexpr size_t MAX_VERTICES = 65536;
static constexpr size_t VERTICES_PER_CHAR = 4;
static constexpr size_t INDICES_PER_CHAR = 6;
static constexpr size_t VERTICES_PER_CHAR = 6;
TextRenderer();
~TextRenderer();
@ -104,13 +64,13 @@ public:
/**
* @brief
*/
void drawText(FontAtlas* font, const String32& text,
void drawText(FontAsset* font, const String32& text,
float x, float y, const TextConfig& config = {});
/**
* @brief
*/
glm::vec2 measureText(FontAtlas* font, const String32& text, float fontSize);
glm::vec2 measureText(FontAsset* font, const String32& text, float fontSize);
/**
* @brief
@ -122,18 +82,23 @@ public:
*/
uint32 drawCalls() const { return drawCalls_; }
/**
* @brief
*/
void setMaterial(Ref<MaterialInstance> material) { material_ = material; }
private:
std::unique_ptr<VAO> vao_;
std::unique_ptr<VertexBuffer> vbo_;
std::unique_ptr<IndexBuffer> ibo_;
Array<TextVertex> vertices_;
size_t vertexCount_ = 0;
GLuint shader_ = 0;
glm::mat4 viewProjection_;
FontAtlas* currentFont_ = nullptr;
FontAsset* currentFont_ = nullptr;
uint32 drawCalls_ = 0;
Ref<MaterialInstance> material_;
bool createShader();
void flush();

View File

@ -0,0 +1,375 @@
#pragma once
#include <extra2d/asset/font_asset.h>
#include <extra2d/core/color.h>
#include <extra2d/core/math_types.h>
#include <extra2d/core/service_interface.h>
#include <extra2d/render/camera.h>
#include <extra2d/render/render_module.h>
#include <extra2d/render/render_types.h>
#include <extra2d/render/renderer.h>
#include <extra2d/render/text_renderer.h>
#include <extra2d/render/texture.h>
#include <glm/mat4x4.hpp>
#include <functional>
#include <memory>
#include <string>
#include <vector>
namespace extra2d {
/**
* @brief
*
* RenderStats
*/
struct RenderServiceStats {
u32 drawCalls = 0;
u32 spriteCount = 0;
u32 textChars = 0;
u32 frameCount = 0;
f32 frameTime = 0.0f;
f32 fps = 0.0f;
};
/**
* @brief
* @param renderService
* @param dt
*/
using RenderCallback = std::function<void(class IRenderService *renderService, f32 dt)>;
/**
* @brief
*
* 2D API
*
*/
class IRenderService : public IService {
public:
virtual ~IRenderService() = default;
/**
* @brief
* @param callback
*/
virtual void onRender(RenderCallback callback) = 0;
/**
* @brief
* @param color
*/
virtual void setClearColor(const Color &color) = 0;
/**
* @brief
* @param clearColor
*/
virtual void beginFrame(const Color &clearColor = Colors::Black) = 0;
/**
* @brief
*/
virtual void endFrame() = 0;
/**
* @brief
* @param viewProjection
*/
virtual void setViewProjection(const glm::mat4 &viewProjection) = 0;
/**
* @brief
* @return
*/
virtual glm::mat4 viewProjection() const = 0;
/**
* @brief
* @param camera
*/
virtual void setCamera(const Camera &camera) = 0;
// =========================================================================
// 图形绘制 API
// =========================================================================
/**
* @brief 线
* @param start
* @param end
* @param color
* @param width 线
*/
virtual void drawLine(const Vec2 &start, const Vec2 &end, const Color &color,
float width = 1.0f) = 0;
/**
* @brief
* @param rect
* @param color
* @param width 线
*/
virtual void drawRect(const Rect &rect, const Color &color,
float width = 1.0f) = 0;
/**
* @brief
* @param rect
* @param color
*/
virtual void fillRect(const Rect &rect, const Color &color) = 0;
/**
* @brief
* @param center
* @param radius
* @param color
* @param segments
* @param width 线
*/
virtual void drawCircle(const Vec2 &center, float radius, const Color &color,
int segments = 32, float width = 1.0f) = 0;
/**
* @brief
* @param center
* @param radius
* @param color
* @param segments
*/
virtual void fillCircle(const Vec2 &center, float radius, const Color &color,
int segments = 32) = 0;
/**
* @brief
*/
virtual void drawTriangle(const Vec2 &p1, const Vec2 &p2, const Vec2 &p3,
const Color &color, float width = 1.0f) = 0;
/**
* @brief
*/
virtual void fillTriangle(const Vec2 &p1, const Vec2 &p2, const Vec2 &p3,
const Color &color) = 0;
/**
* @brief
*/
virtual void drawPolygon(const std::vector<Vec2> &points, const Color &color,
float width = 1.0f) = 0;
/**
* @brief
*/
virtual void fillPolygon(const std::vector<Vec2> &points,
const Color &color) = 0;
// =========================================================================
// 精灵绘制 API
// =========================================================================
/**
* @brief
* @param texture
* @param destRect
* @param srcRect
* @param color
* @param rotation
* @param anchor
*/
virtual void drawSprite(const Texture &texture, const Rect &destRect,
const Rect &srcRect,
const Color &color = Colors::White,
float rotation = 0.0f,
const Vec2 &anchor = Vec2(0.5f, 0.5f)) = 0;
/**
* @brief
*/
virtual void drawSprite(const Texture &texture, const Vec2 &position,
const Color &color = Colors::White) = 0;
/**
* @brief
*/
virtual void drawSprite(const Texture &texture, const Vec2 &position,
const Vec2 &scale,
const Color &color = Colors::White) = 0;
// =========================================================================
// 文本绘制 API
// =========================================================================
/**
* @brief
* @param font
* @param text UTF-32
* @param x X
* @param y Y
* @param config
*/
virtual void drawText(FontAsset *font, const String32 &text, float x, float y,
const TextConfig &config = {}) = 0;
/**
* @brief UTF-8
* @param font
* @param text UTF-8
* @param x X
* @param y Y
* @param config
*/
virtual void drawText(FontAsset *font, const std::string &text, float x,
float y, const TextConfig &config = {}) = 0;
/**
* @brief
* @param font
* @param text
* @param fontSize
* @return
*/
virtual glm::vec2 measureText(FontAsset *font, const String32 &text,
float fontSize) = 0;
// =========================================================================
// 统计信息
// =========================================================================
/**
* @brief
* @return
*/
virtual RenderServiceStats stats() const = 0;
/**
* @brief
*/
virtual void resetStats() = 0;
/**
* @brief
* @return
*/
virtual RenderDevice &device() = 0;
/**
* @brief
* @return
*/
virtual Renderer *renderer() = 0;
/**
* @brief
* @return
*/
virtual TextRenderer *textRenderer() = 0;
};
/**
* @brief
*
* Renderer TextRenderer
* RenderModule ILogger
*
*/
class RenderService : public IRenderService {
public:
RenderService();
~RenderService() override;
ServiceInfo info() const override {
ServiceInfo i;
i.name = "RenderService";
i.priority = ServicePriority::Resource;
i.enabled = true;
return i;
}
/**
* @brief
*/
std::vector<std::type_index> deps() const override {
return {std::type_index(typeid(ILogger))};
}
/**
* @brief
*/
std::vector<std::type_index> needsModules() const override {
return {std::type_index(typeid(RenderModule))};
}
bool init() override;
void shutdown() override;
void onRender(RenderCallback callback) override;
void setClearColor(const Color &color) override;
void beginFrame(const Color &clearColor = Colors::Black) override;
void endFrame() override;
void setViewProjection(const glm::mat4 &viewProjection) override;
glm::mat4 viewProjection() const override;
void setCamera(const Camera &camera) override;
void drawLine(const Vec2 &start, const Vec2 &end, const Color &color,
float width = 1.0f) override;
void drawRect(const Rect &rect, const Color &color,
float width = 1.0f) override;
void fillRect(const Rect &rect, const Color &color) override;
void drawCircle(const Vec2 &center, float radius, const Color &color,
int segments = 32, float width = 1.0f) override;
void fillCircle(const Vec2 &center, float radius, const Color &color,
int segments = 32) override;
void drawTriangle(const Vec2 &p1, const Vec2 &p2, const Vec2 &p3,
const Color &color, float width = 1.0f) override;
void fillTriangle(const Vec2 &p1, const Vec2 &p2, const Vec2 &p3,
const Color &color) override;
void drawPolygon(const std::vector<Vec2> &points, const Color &color,
float width = 1.0f) override;
void fillPolygon(const std::vector<Vec2> &points,
const Color &color) override;
void drawSprite(const Texture &texture, const Rect &destRect,
const Rect &srcRect, const Color &color = Colors::White,
float rotation = 0.0f,
const Vec2 &anchor = Vec2(0.5f, 0.5f)) override;
void drawSprite(const Texture &texture, const Vec2 &position,
const Color &color = Colors::White) override;
void drawSprite(const Texture &texture, const Vec2 &position,
const Vec2 &scale,
const Color &color = Colors::White) override;
void drawText(FontAsset *font, const String32 &text, float x, float y,
const TextConfig &config = {}) override;
void drawText(FontAsset *font, const std::string &text, float x, float y,
const TextConfig &config = {}) override;
glm::vec2 measureText(FontAsset *font, const String32 &text,
float fontSize) override;
RenderServiceStats stats() const override;
void resetStats() override;
RenderDevice &device() override;
Renderer *renderer() override;
TextRenderer *textRenderer() override;
void update(f32 dt) override;
private:
std::unique_ptr<Renderer> renderer_;
std::unique_ptr<TextRenderer> textRenderer_;
RenderServiceStats stats_;
RenderCallback renderCallback_;
Color clearColor_{0.1f, 0.1f, 0.15f, 1.0f};
bool inFrame_ = false;
f32 frameStartTime_ = 0.0f;
f32 fpsAccumulator_ = 0.0f;
u32 fpsFrameCount_ = 0;
glm::vec4 toVec4(const Color &color) const;
};
} // namespace extra2d

File diff suppressed because it is too large Load Diff

View File

@ -1,623 +0,0 @@
// stb_rect_pack.h - v1.01 - public domain - rectangle packing
// Sean Barrett 2014
//
// Useful for e.g. packing rectangular textures into an atlas.
// Does not do rotation.
//
// Before #including,
//
// #define STB_RECT_PACK_IMPLEMENTATION
//
// in the file that you want to have the implementation.
//
// Not necessarily the awesomest packing method, but better than
// the totally naive one in stb_truetype (which is primarily what
// this is meant to replace).
//
// Has only had a few tests run, may have issues.
//
// More docs to come.
//
// No memory allocations; uses qsort() and assert() from stdlib.
// Can override those by defining STBRP_SORT and STBRP_ASSERT.
//
// This library currently uses the Skyline Bottom-Left algorithm.
//
// Please note: better rectangle packers are welcome! Please
// implement them to the same API, but with a different init
// function.
//
// Credits
//
// Library
// Sean Barrett
// Minor features
// Martins Mozeiko
// github:IntellectualKitty
//
// Bugfixes / warning fixes
// Jeremy Jaussaud
// Fabian Giesen
//
// Version history:
//
// 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in public section
// 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles
// 0.99 (2019-02-07) warning fixes
// 0.11 (2017-03-03) return packing success/fail result
// 0.10 (2016-10-25) remove cast-away-const to avoid warnings
// 0.09 (2016-08-27) fix compiler warnings
// 0.08 (2015-09-13) really fix bug with empty rects (w=0 or h=0)
// 0.07 (2015-09-13) fix bug with empty rects (w=0 or h=0)
// 0.06 (2015-04-15) added STBRP_SORT to allow replacing qsort
// 0.05: added STBRP_ASSERT to allow replacing assert
// 0.04: fixed minor bug in STBRP_LARGE_RECTS support
// 0.01: initial release
//
// LICENSE
//
// See end of file for license information.
//////////////////////////////////////////////////////////////////////////////
//
// INCLUDE SECTION
//
#ifndef STB_INCLUDE_STB_RECT_PACK_H
#define STB_INCLUDE_STB_RECT_PACK_H
#define STB_RECT_PACK_VERSION 1
#ifdef STBRP_STATIC
#define STBRP_DEF static
#else
#define STBRP_DEF extern
#endif
#ifdef __cplusplus
extern "C" {
#endif
typedef struct stbrp_context stbrp_context;
typedef struct stbrp_node stbrp_node;
typedef struct stbrp_rect stbrp_rect;
typedef int stbrp_coord;
#define STBRP__MAXVAL 0x7fffffff
// Mostly for internal use, but this is the maximum supported coordinate value.
STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects);
// Assign packed locations to rectangles. The rectangles are of type
// 'stbrp_rect' defined below, stored in the array 'rects', and there
// are 'num_rects' many of them.
//
// Rectangles which are successfully packed have the 'was_packed' flag
// set to a non-zero value and 'x' and 'y' store the minimum location
// on each axis (i.e. bottom-left in cartesian coordinates, top-left
// if you imagine y increasing downwards). Rectangles which do not fit
// have the 'was_packed' flag set to 0.
//
// You should not try to access the 'rects' array from another thread
// while this function is running, as the function temporarily reorders
// the array while it executes.
//
// To pack into another rectangle, you need to call stbrp_init_target
// again. To continue packing into the same rectangle, you can call
// this function again. Calling this multiple times with multiple rect
// arrays will probably produce worse packing results than calling it
// a single time with the full rectangle array, but the option is
// available.
//
// The function returns 1 if all of the rectangles were successfully
// packed and 0 otherwise.
struct stbrp_rect
{
// reserved for your use:
int id;
// input:
stbrp_coord w, h;
// output:
stbrp_coord x, y;
int was_packed; // non-zero if valid packing
}; // 16 bytes, nominally
STBRP_DEF void stbrp_init_target (stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes);
// Initialize a rectangle packer to:
// pack a rectangle that is 'width' by 'height' in dimensions
// using temporary storage provided by the array 'nodes', which is 'num_nodes' long
//
// You must call this function every time you start packing into a new target.
//
// There is no "shutdown" function. The 'nodes' memory must stay valid for
// the following stbrp_pack_rects() call (or calls), but can be freed after
// the call (or calls) finish.
//
// Note: to guarantee best results, either:
// 1. make sure 'num_nodes' >= 'width'
// or 2. call stbrp_allow_out_of_mem() defined below with 'allow_out_of_mem = 1'
//
// If you don't do either of the above things, widths will be quantized to multiples
// of small integers to guarantee the algorithm doesn't run out of temporary storage.
//
// If you do #2, then the non-quantized algorithm will be used, but the algorithm
// may run out of temporary storage and be unable to pack some rectangles.
STBRP_DEF void stbrp_setup_allow_out_of_mem (stbrp_context *context, int allow_out_of_mem);
// Optionally call this function after init but before doing any packing to
// change the handling of the out-of-temp-memory scenario, described above.
// If you call init again, this will be reset to the default (false).
STBRP_DEF void stbrp_setup_heuristic (stbrp_context *context, int heuristic);
// Optionally select which packing heuristic the library should use. Different
// heuristics will produce better/worse results for different data sets.
// If you call init again, this will be reset to the default.
enum
{
STBRP_HEURISTIC_Skyline_default=0,
STBRP_HEURISTIC_Skyline_BL_sortHeight = STBRP_HEURISTIC_Skyline_default,
STBRP_HEURISTIC_Skyline_BF_sortHeight
};
//////////////////////////////////////////////////////////////////////////////
//
// the details of the following structures don't matter to you, but they must
// be visible so you can handle the memory allocations for them
struct stbrp_node
{
stbrp_coord x,y;
stbrp_node *next;
};
struct stbrp_context
{
int width;
int height;
int align;
int init_mode;
int heuristic;
int num_nodes;
stbrp_node *active_head;
stbrp_node *free_head;
stbrp_node extra[2]; // we allocate two extra nodes so optimal user-node-count is 'width' not 'width+2'
};
#ifdef __cplusplus
}
#endif
#endif
//////////////////////////////////////////////////////////////////////////////
//
// IMPLEMENTATION SECTION
//
#ifdef STB_RECT_PACK_IMPLEMENTATION
#ifndef STBRP_SORT
#include <stdlib.h>
#define STBRP_SORT qsort
#endif
#ifndef STBRP_ASSERT
#include <assert.h>
#define STBRP_ASSERT assert
#endif
#ifdef _MSC_VER
#define STBRP__NOTUSED(v) (void)(v)
#define STBRP__CDECL __cdecl
#else
#define STBRP__NOTUSED(v) (void)sizeof(v)
#define STBRP__CDECL
#endif
enum
{
STBRP__INIT_skyline = 1
};
STBRP_DEF void stbrp_setup_heuristic(stbrp_context *context, int heuristic)
{
switch (context->init_mode) {
case STBRP__INIT_skyline:
STBRP_ASSERT(heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight || heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight);
context->heuristic = heuristic;
break;
default:
STBRP_ASSERT(0);
}
}
STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_out_of_mem)
{
if (allow_out_of_mem)
// if it's ok to run out of memory, then don't bother aligning them;
// this gives better packing, but may fail due to OOM (even though
// the rectangles easily fit). @TODO a smarter approach would be to only
// quantize once we've hit OOM, then we could get rid of this parameter.
context->align = 1;
else {
// if it's not ok to run out of memory, then quantize the widths
// so that num_nodes is always enough nodes.
//
// I.e. num_nodes * align >= width
// align >= width / num_nodes
// align = ceil(width/num_nodes)
context->align = (context->width + context->num_nodes-1) / context->num_nodes;
}
}
STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes)
{
int i;
for (i=0; i < num_nodes-1; ++i)
nodes[i].next = &nodes[i+1];
nodes[i].next = NULL;
context->init_mode = STBRP__INIT_skyline;
context->heuristic = STBRP_HEURISTIC_Skyline_default;
context->free_head = &nodes[0];
context->active_head = &context->extra[0];
context->width = width;
context->height = height;
context->num_nodes = num_nodes;
stbrp_setup_allow_out_of_mem(context, 0);
// node 0 is the full width, node 1 is the sentinel (lets us not store width explicitly)
context->extra[0].x = 0;
context->extra[0].y = 0;
context->extra[0].next = &context->extra[1];
context->extra[1].x = (stbrp_coord) width;
context->extra[1].y = (1<<30);
context->extra[1].next = NULL;
}
// find minimum y position if it starts at x1
static int stbrp__skyline_find_min_y(stbrp_context *c, stbrp_node *first, int x0, int width, int *pwaste)
{
stbrp_node *node = first;
int x1 = x0 + width;
int min_y, visited_width, waste_area;
STBRP__NOTUSED(c);
STBRP_ASSERT(first->x <= x0);
#if 0
// skip in case we're past the node
while (node->next->x <= x0)
++node;
#else
STBRP_ASSERT(node->next->x > x0); // we ended up handling this in the caller for efficiency
#endif
STBRP_ASSERT(node->x <= x0);
min_y = 0;
waste_area = 0;
visited_width = 0;
while (node->x < x1) {
if (node->y > min_y) {
// raise min_y higher.
// we've accounted for all waste up to min_y,
// but we'll now add more waste for everything we've visted
waste_area += visited_width * (node->y - min_y);
min_y = node->y;
// the first time through, visited_width might be reduced
if (node->x < x0)
visited_width += node->next->x - x0;
else
visited_width += node->next->x - node->x;
} else {
// add waste area
int under_width = node->next->x - node->x;
if (under_width + visited_width > width)
under_width = width - visited_width;
waste_area += under_width * (min_y - node->y);
visited_width += under_width;
}
node = node->next;
}
*pwaste = waste_area;
return min_y;
}
typedef struct
{
int x,y;
stbrp_node **prev_link;
} stbrp__findresult;
static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int width, int height)
{
int best_waste = (1<<30), best_x, best_y = (1 << 30);
stbrp__findresult fr;
stbrp_node **prev, *node, *tail, **best = NULL;
// align to multiple of c->align
width = (width + c->align - 1);
width -= width % c->align;
STBRP_ASSERT(width % c->align == 0);
// if it can't possibly fit, bail immediately
if (width > c->width || height > c->height) {
fr.prev_link = NULL;
fr.x = fr.y = 0;
return fr;
}
node = c->active_head;
prev = &c->active_head;
while (node->x + width <= c->width) {
int y,waste;
y = stbrp__skyline_find_min_y(c, node, node->x, width, &waste);
if (c->heuristic == STBRP_HEURISTIC_Skyline_BL_sortHeight) { // actually just want to test BL
// bottom left
if (y < best_y) {
best_y = y;
best = prev;
}
} else {
// best-fit
if (y + height <= c->height) {
// can only use it if it first vertically
if (y < best_y || (y == best_y && waste < best_waste)) {
best_y = y;
best_waste = waste;
best = prev;
}
}
}
prev = &node->next;
node = node->next;
}
best_x = (best == NULL) ? 0 : (*best)->x;
// if doing best-fit (BF), we also have to try aligning right edge to each node position
//
// e.g, if fitting
//
// ____________________
// |____________________|
//
// into
//
// | |
// | ____________|
// |____________|
//
// then right-aligned reduces waste, but bottom-left BL is always chooses left-aligned
//
// This makes BF take about 2x the time
if (c->heuristic == STBRP_HEURISTIC_Skyline_BF_sortHeight) {
tail = c->active_head;
node = c->active_head;
prev = &c->active_head;
// find first node that's admissible
while (tail->x < width)
tail = tail->next;
while (tail) {
int xpos = tail->x - width;
int y,waste;
STBRP_ASSERT(xpos >= 0);
// find the left position that matches this
while (node->next->x <= xpos) {
prev = &node->next;
node = node->next;
}
STBRP_ASSERT(node->next->x > xpos && node->x <= xpos);
y = stbrp__skyline_find_min_y(c, node, xpos, width, &waste);
if (y + height <= c->height) {
if (y <= best_y) {
if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) {
best_x = xpos;
STBRP_ASSERT(y <= best_y);
best_y = y;
best_waste = waste;
best = prev;
}
}
}
tail = tail->next;
}
}
fr.prev_link = best;
fr.x = best_x;
fr.y = best_y;
return fr;
}
static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, int width, int height)
{
// find best position according to heuristic
stbrp__findresult res = stbrp__skyline_find_best_pos(context, width, height);
stbrp_node *node, *cur;
// bail if:
// 1. it failed
// 2. the best node doesn't fit (we don't always check this)
// 3. we're out of memory
if (res.prev_link == NULL || res.y + height > context->height || context->free_head == NULL) {
res.prev_link = NULL;
return res;
}
// on success, create new node
node = context->free_head;
node->x = (stbrp_coord) res.x;
node->y = (stbrp_coord) (res.y + height);
context->free_head = node->next;
// insert the new node into the right starting point, and
// let 'cur' point to the remaining nodes needing to be
// stiched back in
cur = *res.prev_link;
if (cur->x < res.x) {
// preserve the existing one, so start testing with the next one
stbrp_node *next = cur->next;
cur->next = node;
cur = next;
} else {
*res.prev_link = node;
}
// from here, traverse cur and free the nodes, until we get to one
// that shouldn't be freed
while (cur->next && cur->next->x <= res.x + width) {
stbrp_node *next = cur->next;
// move the current node to the free list
cur->next = context->free_head;
context->free_head = cur;
cur = next;
}
// stitch the list back in
node->next = cur;
if (cur->x < res.x + width)
cur->x = (stbrp_coord) (res.x + width);
#ifdef _DEBUG
cur = context->active_head;
while (cur->x < context->width) {
STBRP_ASSERT(cur->x < cur->next->x);
cur = cur->next;
}
STBRP_ASSERT(cur->next == NULL);
{
int count=0;
cur = context->active_head;
while (cur) {
cur = cur->next;
++count;
}
cur = context->free_head;
while (cur) {
cur = cur->next;
++count;
}
STBRP_ASSERT(count == context->num_nodes+2);
}
#endif
return res;
}
static int STBRP__CDECL rect_height_compare(const void *a, const void *b)
{
const stbrp_rect *p = (const stbrp_rect *) a;
const stbrp_rect *q = (const stbrp_rect *) b;
if (p->h > q->h)
return -1;
if (p->h < q->h)
return 1;
return (p->w > q->w) ? -1 : (p->w < q->w);
}
static int STBRP__CDECL rect_original_order(const void *a, const void *b)
{
const stbrp_rect *p = (const stbrp_rect *) a;
const stbrp_rect *q = (const stbrp_rect *) b;
return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed);
}
STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects)
{
int i, all_rects_packed = 1;
// we use the 'was_packed' field internally to allow sorting/unsorting
for (i=0; i < num_rects; ++i) {
rects[i].was_packed = i;
}
// sort according to heuristic
STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_height_compare);
for (i=0; i < num_rects; ++i) {
if (rects[i].w == 0 || rects[i].h == 0) {
rects[i].x = rects[i].y = 0; // empty rect needs no space
} else {
stbrp__findresult fr = stbrp__skyline_pack_rectangle(context, rects[i].w, rects[i].h);
if (fr.prev_link) {
rects[i].x = (stbrp_coord) fr.x;
rects[i].y = (stbrp_coord) fr.y;
} else {
rects[i].x = rects[i].y = STBRP__MAXVAL;
}
}
}
// unsort
STBRP_SORT(rects, num_rects, sizeof(rects[0]), rect_original_order);
// set was_packed flags and all_rects_packed status
for (i=0; i < num_rects; ++i) {
rects[i].was_packed = !(rects[i].x == STBRP__MAXVAL && rects[i].y == STBRP__MAXVAL);
if (!rects[i].was_packed)
all_rects_packed = 0;
}
// return the all_rects_packed status
return all_rects_packed;
}
#endif
/*
------------------------------------------------------------------------------
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2017 Sean Barrett
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------------------------
*/

File diff suppressed because it is too large Load Diff

View File

@ -124,34 +124,26 @@ void TextureLoader::setDesiredChannels(int channels) {
}
// ---------------------------------------------------------------------------
// FontLoader 实现
// FontLoader 实现 (MSDF)
// ---------------------------------------------------------------------------
Ref<FontAsset> FontLoader::load(const std::string &path) {
auto data = readFile(path);
if (data.empty()) {
E2D_ERROR(CAT_ASSET, "Failed to read font file: {}", path);
auto asset = ptr::make<FontAsset>();
if (!parseEmbeddedMetadata(path, asset.get())) {
E2D_ERROR(CAT_ASSET, "Failed to parse MSDF font metadata: {}", path);
return nullptr;
}
return loadFromMemory(data.data(), data.size());
asset->markLoaded();
return asset;
}
Ref<FontAsset> FontLoader::loadFromMemory(const u8 *data, size_t size) {
if (!data || size == 0) {
E2D_ERROR(CAT_ASSET, "MSDF font loading from memory not supported");
return nullptr;
}
auto asset = ptr::make<FontAsset>();
std::vector<u8> fontData(data, data + size);
if (!asset->setData(std::move(fontData))) {
E2D_ERROR(CAT_ASSET, "Failed to initialize font from memory");
return nullptr;
}
return asset;
}
bool FontLoader::canLoad(const std::string &path) const {
std::string ext = getExtension(path);
auto exts = extensions();
@ -159,7 +151,140 @@ bool FontLoader::canLoad(const std::string &path) const {
}
std::vector<std::string> FontLoader::extensions() const {
return {".ttf", ".otf", ".ttc"};
return {".msdf", ".png"};
}
bool FontLoader::parseEmbeddedMetadata(const std::string &path, FontAsset *asset) {
auto pngData = readFile(path);
if (pngData.empty()) {
E2D_ERROR(CAT_ASSET, "Failed to read MSDF font file: {}", path);
return false;
}
std::vector<u8> metadataJson;
if (!readTextChunk(pngData, "msdf", metadataJson)) {
E2D_ERROR(CAT_ASSET, "Failed to read MSDF metadata chunk from: {}", path);
return false;
}
std::string jsonStr(metadataJson.begin(), metadataJson.end());
if (!parseJsonMetadata(jsonStr, asset)) {
E2D_ERROR(CAT_ASSET, "Failed to parse MSDF metadata JSON from: {}", path);
return false;
}
int width, height, channels;
u8 *pixels = stbi_load_from_memory(pngData.data(), static_cast<int>(pngData.size()),
&width, &height, &channels, 4);
if (!pixels) {
E2D_ERROR(CAT_ASSET, "Failed to decode MSDF texture from: {}", path);
return false;
}
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
stbi_image_free(pixels);
asset->setTextureId(texture);
asset->setAtlasSize(width, height);
return true;
}
bool FontLoader::readTextChunk(const std::vector<u8> &pngData,
const std::string &keyword,
std::vector<u8> &output) {
if (pngData.size() < 8) return false;
if (pngData[0] != 0x89 || pngData[1] != 'P' || pngData[2] != 'N' || pngData[3] != 'G') {
return false;
}
size_t pos = 8;
while (pos < pngData.size()) {
if (pos + 8 > pngData.size()) break;
u32 length = (static_cast<u32>(pngData[pos]) << 24) |
(static_cast<u32>(pngData[pos + 1]) << 16) |
(static_cast<u32>(pngData[pos + 2]) << 8) |
static_cast<u32>(pngData[pos + 3]);
std::string chunkType(pngData.begin() + pos + 4, pngData.begin() + pos + 8);
if (chunkType == "tEXt") {
size_t dataStart = pos + 8;
size_t nullPos = dataStart;
while (nullPos < pngData.size() && pngData[nullPos] != 0) {
nullPos++;
}
std::string foundKeyword(pngData.begin() + dataStart, pngData.begin() + nullPos);
if (foundKeyword == keyword) {
output.assign(pngData.begin() + nullPos + 1, pngData.begin() + pos + 8 + length);
return true;
}
}
pos += 12 + length;
}
return false;
}
bool FontLoader::parseJsonMetadata(const std::string &json, FontAsset *asset) {
try {
auto j = nlohmann::json::parse(json);
asset->setFontSize(j.value("size", 48));
asset->setPxRange(j.value("pxRange", 4.0f));
asset->setLineHeight(j.value("lineHeight", 60));
asset->setBaseline(j.value("baseline", 12));
if (j.contains("atlas")) {
auto atlas = j["atlas"];
asset->setAtlasSize(atlas.value("width", 2048), atlas.value("height", 2048));
}
if (j.contains("glyphs")) {
for (const auto &glyphJson : j["glyphs"]) {
GlyphInfo glyph;
glyph.codepoint = glyphJson.value("unicode", 0u);
if (glyphJson.contains("atlasBounds")) {
auto bounds = glyphJson["atlasBounds"];
glyph.uvMin.x = bounds.value("left", 0.0f) / asset->atlasWidth();
glyph.uvMin.y = bounds.value("top", 0.0f) / asset->atlasHeight();
glyph.uvMax.x = bounds.value("right", 0.0f) / asset->atlasWidth();
glyph.uvMax.y = bounds.value("bottom", 0.0f) / asset->atlasHeight();
}
if (glyphJson.contains("planeBounds")) {
auto plane = glyphJson["planeBounds"];
glyph.size.x = plane.value("right", 0.0f) - plane.value("left", 0.0f);
glyph.size.y = plane.value("top", 0.0f) - plane.value("bottom", 0.0f);
glyph.bearing.x = plane.value("left", 0.0f);
glyph.bearing.y = plane.value("top", 0.0f);
}
glyph.advance = glyphJson.value("advance", 0.0f);
asset->addGlyph(glyph);
}
}
return true;
} catch (const std::exception &e) {
E2D_ERROR(CAT_ASSET, "Failed to parse MSDF metadata: {}", e.what());
return false;
}
}
// ---------------------------------------------------------------------------
@ -640,11 +765,11 @@ AssetType AssetLoaderFactory::getTypeByExtension(const std::string &extension) {
return AssetType::Texture;
}
if (ext == ".ttf" || ext == ".otf" || ext == ".ttc") {
if (ext == ".msdf" || ext == ".font.png") {
return AssetType::Font;
}
if (ext == ".vert" || ext == ".frag" || ext == ".glsl" || ext == ".vs" ||
if (ext == ".json" || ext == ".vert" || ext == ".frag" || ext == ".glsl" || ext == ".vs" ||
ext == ".fs") {
return AssetType::Shader;
}

View File

@ -1,54 +1,86 @@
#include <extra2d/asset/font_asset.h>
#define STB_TRUETYPE_IMPLEMENTATION
#include <stb/stb_truetype.h>
namespace extra2d {
class FontAsset::Impl {
public:
stbtt_fontinfo info;
bool initialized = false;
};
FontAsset::FontAsset() = default;
FontAsset::FontAsset() : impl_(ptr::unique<Impl>()) {}
FontAsset::~FontAsset() = default;
FontAsset::~FontAsset() {
release();
}
bool FontAsset::loaded() const {
return state_.load(std::memory_order_acquire) == AssetState::Loaded &&
impl_->initialized;
textureId_ != 0;
}
float FontAsset::scaleForPixelHeight(float pixels) const {
if (!impl_->initialized || data_.empty()) {
return 0.0f;
}
return stbtt_ScaleForPixelHeight(&impl_->info, pixels);
size_t FontAsset::memSize() const {
return glyphs_.size() * sizeof(GlyphInfo);
}
bool FontAsset::setData(std::vector<u8> data) {
if (data.empty()) {
const GlyphInfo* FontAsset::getGlyph(char32_t codepoint) const {
auto it = glyphs_.find(codepoint);
if (it != glyphs_.end()) {
return &it->second;
}
return nullptr;
}
bool FontAsset::hasGlyph(char32_t codepoint) const {
return glyphs_.find(codepoint) != glyphs_.end();
}
std::vector<char32_t> FontAsset::getMissingChars(const std::u32string& text) const {
std::vector<char32_t> missing;
for (char32_t c : text) {
if (!hasGlyph(c)) {
missing.push_back(c);
}
}
return missing;
}
bool FontAsset::canRender(const std::u32string& text) const {
for (char32_t c : text) {
if (!hasGlyph(c)) {
return false;
}
data_ = std::move(data);
if (!stbtt_InitFont(&impl_->info, data_.data(), 0)) {
data_.clear();
impl_->initialized = false;
setState(AssetState::Failed);
return false;
}
impl_->initialized = true;
setState(AssetState::Loaded);
return true;
}
glm::vec2 FontAsset::measureText(const std::u32string& text, float scale) const {
if (text.empty()) return glm::vec2(0.0f);
float width = 0.0f;
float maxAscent = 0.0f;
float maxDescent = 0.0f;
for (char32_t c : text) {
const GlyphInfo* glyph = getGlyph(c);
if (glyph) {
width += glyph->advance * scale;
maxAscent = glm::max(maxAscent, glyph->bearing.y * scale);
maxDescent = glm::max(maxDescent, (glyph->size.y - glyph->bearing.y) * scale);
}
}
return glm::vec2(width, maxAscent + maxDescent);
}
void FontAsset::addGlyph(const GlyphInfo& glyph) {
glyphs_[glyph.codepoint] = glyph;
}
void FontAsset::markLoaded() {
setState(AssetState::Loaded);
}
void FontAsset::release() {
data_.clear();
impl_->initialized = false;
if (textureId_ != 0) {
glDeleteTextures(1, &textureId_);
textureId_ = 0;
}
glyphs_.clear();
setState(AssetState::Unloaded);
}

View File

@ -1,87 +0,0 @@
#include <extra2d/asset/msdf_font_asset.h>
namespace extra2d {
MSDFFontAsset::MSDFFontAsset() = default;
MSDFFontAsset::~MSDFFontAsset() {
release();
}
bool MSDFFontAsset::loaded() const {
return state_.load(std::memory_order_acquire) == AssetState::Loaded &&
textureId_ != 0;
}
size_t MSDFFontAsset::memSize() const {
return glyphs_.size() * sizeof(GlyphInfo);
}
const GlyphInfo* MSDFFontAsset::getGlyph(char32_t codepoint) const {
auto it = glyphs_.find(codepoint);
if (it != glyphs_.end()) {
return &it->second;
}
return nullptr;
}
bool MSDFFontAsset::hasGlyph(char32_t codepoint) const {
return glyphs_.find(codepoint) != glyphs_.end();
}
std::vector<char32_t> MSDFFontAsset::getMissingChars(const std::u32string& text) const {
std::vector<char32_t> missing;
for (char32_t c : text) {
if (!hasGlyph(c)) {
missing.push_back(c);
}
}
return missing;
}
bool MSDFFontAsset::canRender(const std::u32string& text) const {
for (char32_t c : text) {
if (!hasGlyph(c)) {
return false;
}
}
return true;
}
glm::vec2 MSDFFontAsset::measureText(const std::u32string& text, float scale) const {
if (text.empty()) return glm::vec2(0.0f);
float width = 0.0f;
float maxAscent = 0.0f;
float maxDescent = 0.0f;
for (char32_t c : text) {
const GlyphInfo* glyph = getGlyph(c);
if (glyph) {
width += glyph->advance * scale;
maxAscent = glm::max(maxAscent, glyph->bearing.y * scale);
maxDescent = glm::max(maxDescent, (glyph->size.y - glyph->bearing.y) * scale);
}
}
return glm::vec2(width, maxAscent + maxDescent);
}
void MSDFFontAsset::addGlyph(const GlyphInfo& glyph) {
glyphs_[glyph.codepoint] = glyph;
}
void MSDFFontAsset::markLoaded() {
setState(AssetState::Loaded);
}
void MSDFFontAsset::release() {
if (textureId_ != 0) {
glDeleteTextures(1, &textureId_);
textureId_ = 0;
}
glyphs_.clear();
setState(AssetState::Unloaded);
}
} // namespace extra2d

View File

@ -1,183 +0,0 @@
#include <extra2d/asset/msdf_font_loader.h>
#include <extra2d/services/logger_service.h>
#include <json/json.hpp>
#include <fstream>
#include <glad/glad.h>
namespace extra2d {
namespace {
std::vector<u8> readFile(const std::string& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) {
return {};
}
size_t size = static_cast<size_t>(file.tellg());
file.seekg(0, std::ios::beg);
std::vector<u8> data(size);
if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
return {};
}
return data;
}
std::string getExtension(const std::string& path) {
size_t pos = path.rfind('.');
if (pos == std::string::npos) {
return "";
}
std::string ext = path.substr(pos);
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
return ext;
}
}
Ref<MSDFFontAsset> MSDFFontLoader::load(const std::string& path) {
auto asset = ptr::make<MSDFFontAsset>();
if (!parseEmbeddedMetadata(path, asset.get())) {
E2D_ERROR(CAT_ASSET, "Failed to parse MSDF font metadata: {}", path);
return nullptr;
}
asset->markLoaded();
return asset;
}
Ref<MSDFFontAsset> MSDFFontLoader::loadFromMemory(const u8* data, size_t size) {
E2D_ERROR(CAT_ASSET, "MSDF font loading from memory not supported");
return nullptr;
}
bool MSDFFontLoader::canLoad(const std::string& path) const {
std::string ext = getExtension(path);
return ext == ".msdf" || ext == ".png";
}
std::vector<std::string> MSDFFontLoader::extensions() const {
return {".msdf", ".png"};
}
bool MSDFFontLoader::parseEmbeddedMetadata(const std::string& path, MSDFFontAsset* asset) {
auto pngData = readFile(path);
if (pngData.empty()) {
return false;
}
std::vector<u8> metadataJson;
if (!readTextChunk(pngData, "msdf", metadataJson)) {
return false;
}
std::string jsonStr(metadataJson.begin(), metadataJson.end());
if (!parseJsonMetadata(jsonStr, asset)) {
return false;
}
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
asset->setTextureId(texture);
return true;
}
bool MSDFFontLoader::readTextChunk(const std::vector<u8>& pngData,
const std::string& keyword,
std::vector<u8>& output) {
if (pngData.size() < 8) return false;
if (pngData[0] != 0x89 || pngData[1] != 'P' || pngData[2] != 'N' || pngData[3] != 'G') {
return false;
}
size_t pos = 8;
while (pos < pngData.size()) {
if (pos + 8 > pngData.size()) break;
u32 length = (static_cast<u32>(pngData[pos]) << 24) |
(static_cast<u32>(pngData[pos + 1]) << 16) |
(static_cast<u32>(pngData[pos + 2]) << 8) |
static_cast<u32>(pngData[pos + 3]);
std::string chunkType(pngData.begin() + pos + 4, pngData.begin() + pos + 8);
if (chunkType == "tEXt") {
size_t dataStart = pos + 8;
size_t nullPos = dataStart;
while (nullPos < pngData.size() && pngData[nullPos] != 0) {
nullPos++;
}
std::string foundKeyword(pngData.begin() + dataStart, pngData.begin() + nullPos);
if (foundKeyword == keyword) {
output.assign(pngData.begin() + nullPos + 1, pngData.begin() + pos + 8 + length);
return true;
}
}
pos += 12 + length;
}
return false;
}
bool MSDFFontLoader::parseJsonMetadata(const std::string& json, MSDFFontAsset* asset) {
try {
auto j = nlohmann::json::parse(json);
asset->setFontSize(j.value("size", 48));
asset->setPxRange(j.value("pxRange", 4.0f));
asset->setLineHeight(j.value("lineHeight", 60));
asset->setBaseline(j.value("baseline", 12));
if (j.contains("atlas")) {
auto atlas = j["atlas"];
asset->setAtlasSize(atlas.value("width", 2048), atlas.value("height", 2048));
}
if (j.contains("glyphs")) {
for (const auto& glyphJson : j["glyphs"]) {
GlyphInfo glyph;
glyph.codepoint = glyphJson.value("unicode", 0u);
if (glyphJson.contains("atlasBounds")) {
auto bounds = glyphJson["atlasBounds"];
glyph.uvMin.x = bounds.value("left", 0.0f) / asset->atlasWidth();
glyph.uvMin.y = bounds.value("top", 0.0f) / asset->atlasHeight();
glyph.uvMax.x = bounds.value("right", 0.0f) / asset->atlasWidth();
glyph.uvMax.y = bounds.value("bottom", 0.0f) / asset->atlasHeight();
}
if (glyphJson.contains("planeBounds")) {
auto plane = glyphJson["planeBounds"];
glyph.size.x = plane.value("right", 0.0f) - plane.value("left", 0.0f);
glyph.size.y = plane.value("top", 0.0f) - plane.value("bottom", 0.0f);
glyph.bearing.x = plane.value("left", 0.0f);
glyph.bearing.y = plane.value("top", 0.0f);
}
glyph.advance = glyphJson.value("advance", 0.0f);
asset->addGlyph(glyph);
}
}
return true;
} catch (const std::exception& e) {
E2D_ERROR(CAT_ASSET, "Failed to parse MSDF metadata: {}", e.what());
return false;
}
}
} // namespace extra2d

View File

@ -1,217 +0,0 @@
#include <extra2d/render/msdf_text_renderer.h>
#include <glad/glad.h>
#include <simdutf/simdutf.h>
namespace extra2d {
static const char* MSDF_VERTEX_SHADER = R"(
#version 450 core
layout(location = 0) in vec2 a_position;
layout(location = 1) in vec2 a_texCoord;
uniform mat4 u_projection;
out vec2 v_texCoord;
void main() {
gl_Position = u_projection * vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}
)";
static const char* MSDF_FRAGMENT_SHADER = R"(
#version 450 core
in vec2 v_texCoord;
uniform sampler2D u_texture;
uniform float u_pxRange;
uniform vec4 u_color;
out vec4 fragColor;
float median(float r, float g, float b) {
return max(min(r, g), min(max(r, g), b));
}
void main() {
vec3 msdf = texture(u_texture, v_texCoord).rgb;
float sigDist = median(msdf.r, msdf.g, msdf.b);
float pxRange = u_pxRange;
float alpha = smoothstep(0.5 - 0.5/pxRange, 0.5 + 0.5/pxRange, sigDist);
fragColor = u_color * vec4(1.0, 1.0, 1.0, alpha);
}
)";
MSDFTextRenderer::MSDFTextRenderer() = default;
MSDFTextRenderer::~MSDFTextRenderer() {
shutdown();
}
bool MSDFTextRenderer::init() {
return createShader();
}
void MSDFTextRenderer::shutdown() {
if (shader_) {
glDeleteProgram(shader_);
shader_ = 0;
}
if (vao_) {
glDeleteVertexArrays(1, &vao_);
vao_ = 0;
}
if (vbo_) {
glDeleteBuffers(1, &vbo_);
vbo_ = 0;
}
if (ibo_) {
glDeleteBuffers(1, &ibo_);
ibo_ = 0;
}
}
void MSDFTextRenderer::setText(const std::string& utf8Text) {
size_t len = simdutf::utf32_length_from_utf8(utf8Text.data(), utf8Text.size());
text_.resize(len);
simdutf::convert_utf8_to_utf32(utf8Text.data(), utf8Text.size(), text_.data());
}
void MSDFTextRenderer::setFontSize(float size) {
fontSize_ = size;
if (font_) {
scale_ = size / static_cast<float>(font_->fontSize());
}
}
glm::vec2 MSDFTextRenderer::getSize() const {
if (font_) {
return font_->measureText(text_, scale_);
}
return glm::vec2(0.0f);
}
void MSDFTextRenderer::render(const glm::mat4& projection) {
if (!font_ || text_.empty()) return;
updateGeometry();
glUseProgram(shader_);
GLint projLoc = glGetUniformLocation(shader_, "u_projection");
glUniformMatrix4fv(projLoc, 1, GL_FALSE, &projection[0][0]);
GLint texLoc = glGetUniformLocation(shader_, "u_texture");
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, font_->textureId());
glUniform1i(texLoc, 0);
GLint pxRangeLoc = glGetUniformLocation(shader_, "u_pxRange");
glUniform1f(pxRangeLoc, font_->pxRange() * scale_);
GLint colorLoc = glGetUniformLocation(shader_, "u_color");
glUniform4f(colorLoc, color_.r, color_.g, color_.b, color_.a);
draw();
}
void MSDFTextRenderer::updateGeometry() {
vertices_.clear();
indices_.clear();
float x = position_.x;
float y = position_.y;
for (char32_t c : text_) {
const GlyphInfo* glyph = font_->getGlyph(c);
if (!glyph) continue;
float x0 = x + glyph->bearing.x * scale_;
float y0 = y - glyph->bearing.y * scale_;
float x1 = x0 + glyph->size.x * scale_;
float y1 = y0 + glyph->size.y * scale_;
uint32_t base = static_cast<uint32_t>(vertices_.size());
vertices_.push_back({{x0, y0}, {glyph->uvMin.x, glyph->uvMin.y}});
vertices_.push_back({{x1, y0}, {glyph->uvMax.x, glyph->uvMin.y}});
vertices_.push_back({{x1, y1}, {glyph->uvMax.x, glyph->uvMax.y}});
vertices_.push_back({{x0, y1}, {glyph->uvMin.x, glyph->uvMax.y}});
indices_.push_back(base + 0);
indices_.push_back(base + 1);
indices_.push_back(base + 2);
indices_.push_back(base + 0);
indices_.push_back(base + 2);
indices_.push_back(base + 3);
x += glyph->advance * scale_;
}
}
void MSDFTextRenderer::draw() {
if (vertices_.empty()) return;
if (!vao_) {
glGenVertexArrays(1, &vao_);
glGenBuffers(1, &vbo_);
glGenBuffers(1, &ibo_);
}
glBindVertexArray(vao_);
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
glBufferData(GL_ARRAY_BUFFER, vertices_.size() * sizeof(Vertex), vertices_.data(), GL_DYNAMIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices_.size() * sizeof(uint32_t), indices_.data(), GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position));
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoord));
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(indices_.size()), GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
}
bool MSDFTextRenderer::createShader() {
GLuint vs = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vs, 1, &MSDF_VERTEX_SHADER, nullptr);
glCompileShader(vs);
GLint success;
glGetShaderiv(vs, GL_COMPILE_STATUS, &success);
if (!success) {
glDeleteShader(vs);
return false;
}
GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, &MSDF_FRAGMENT_SHADER, nullptr);
glCompileShader(fs);
glGetShaderiv(fs, GL_COMPILE_STATUS, &success);
if (!success) {
glDeleteShader(vs);
glDeleteShader(fs);
return false;
}
shader_ = glCreateProgram();
glAttachShader(shader_, vs);
glAttachShader(shader_, fs);
glLinkProgram(shader_);
glGetProgramiv(shader_, GL_LINK_STATUS, &success);
glDeleteShader(vs);
glDeleteShader(fs);
return success == GL_TRUE;
}
} // namespace extra2d

View File

@ -52,48 +52,6 @@ void main() {
}
)";
FontAtlas::FontAtlas() = default;
FontAtlas::~FontAtlas() { destroy(); }
bool FontAtlas::create(int atlasWidth, int atlasHeight) {
width_ = atlasWidth;
height_ = atlasHeight;
glCreateTextures(GL_TEXTURE_2D, 1, &texture_);
if (texture_ == 0)
return false;
glTextureStorage2D(texture_, 1, GL_RGBA8, width_, height_);
glTextureParameteri(texture_, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTextureParameteri(texture_, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTextureParameteri(texture_, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTextureParameteri(texture_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
return true;
}
void FontAtlas::destroy() {
if (texture_ != 0) {
glDeleteTextures(1, &texture_);
texture_ = 0;
}
glyphs_.clear();
}
void FontAtlas::addGlyph(char32_t codepoint, const GlyphInfo &info) {
glyphs_[codepoint] = info;
}
const GlyphInfo *FontAtlas::getGlyph(char32_t codepoint) const {
auto it = glyphs_.find(codepoint);
return it != glyphs_.end() ? &it->second : nullptr;
}
bool FontAtlas::hasGlyph(char32_t codepoint) const {
return glyphs_.find(codepoint) != glyphs_.end();
}
TextRenderer::TextRenderer() { vertices_.resize(MAX_VERTICES); }
TextRenderer::~TextRenderer() { shutdown(); }
@ -141,9 +99,9 @@ void TextRenderer::begin(const glm::mat4 &viewProjection) {
drawCalls_ = 0;
}
void TextRenderer::drawText(FontAtlas *font, const String32 &text, float x,
void TextRenderer::drawText(FontAsset *font, const String32 &text, float x,
float y, const TextConfig &config) {
if (!font || !font->isValid() || text.empty())
if (!font || !font->loaded() || text.empty())
return;
if (font != currentFont_ ||
@ -152,7 +110,7 @@ void TextRenderer::drawText(FontAtlas *font, const String32 &text, float x,
currentFont_ = font;
}
float scale = config.fontSize / 48.0f;
float scale = config.fontSize / static_cast<float>(font->fontSize());
float cursorX = x;
float cursorY = y;
@ -173,12 +131,12 @@ void TextRenderer::drawText(FontAtlas *font, const String32 &text, float x,
}
}
glm::vec2 TextRenderer::measureText(FontAtlas *font, const String32 &text,
glm::vec2 TextRenderer::measureText(FontAsset *font, const String32 &text,
float fontSize) {
if (!font || text.empty())
return glm::vec2(0.0f);
float scale = fontSize / 48.0f;
float scale = fontSize / static_cast<float>(font->fontSize());
float width = 0.0f;
float maxWidth = 0.0f;
float height = fontSize;
@ -255,12 +213,12 @@ void TextRenderer::flush() {
glUniform1i(texLoc, 0);
GLint pxRangeLoc = glGetUniformLocation(shader_, "u_pxRange");
glUniform1f(pxRangeLoc, 4.0f);
glUniform1f(pxRangeLoc, currentFont_->pxRange());
glBindTextureUnit(0, currentFont_->texture());
glBindTextureUnit(0, currentFont_->textureId());
vao_->bind();
glDrawArrays(GL_TRIANGLE_STRIP, 0, static_cast<GLsizei>(vertexCount_));
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(vertexCount_));
E2D_RENDER_STATS().addDrawCall(static_cast<uint32>(vertexCount_),
static_cast<uint32>(vertexCount_ / 3));
@ -294,17 +252,29 @@ void TextRenderer::addChar(const GlyphInfo &glyph, float x, float y,
v.v = glyph.uvMin.y;
vertices_[vertexCount_++] = v;
v.x = x0;
v.x = x1;
v.y = y1;
v.u = glyph.uvMin.x;
v.u = glyph.uvMax.x;
v.v = glyph.uvMax.y;
vertices_[vertexCount_++] = v;
v.x = x0;
v.y = y0;
v.u = glyph.uvMin.x;
v.v = glyph.uvMin.y;
vertices_[vertexCount_++] = v;
v.x = x1;
v.y = y1;
v.u = glyph.uvMax.x;
v.v = glyph.uvMax.y;
vertices_[vertexCount_++] = v;
v.x = x0;
v.y = y1;
v.u = glyph.uvMin.x;
v.v = glyph.uvMax.y;
vertices_[vertexCount_++] = v;
}
} // namespace extra2d

View File

@ -54,7 +54,7 @@ void VAO::setFormat(const VertexFormatDesc& format) {
for (const auto& attr : format.attributes) {
setAttribFormat(attr.location, attr.type, attr.offset, attr.normalized);
setAttribBinding(attr.location, attr.location);
setAttribBinding(attr.location, 0);
enableAttrib(attr.location);
}
}

View File

@ -17,6 +17,10 @@ bool AssetService::init() {
return true;
}
info_.name = "AssetService";
info_.priority = ServicePriority::Resource;
info_.enabled = true;
setState(ServiceState::Initializing);
registerLoader<TextureAsset>(AssetLoaderFactory::createTextureLoader());

View File

@ -0,0 +1,287 @@
#include <extra2d/services/render_service.h>
#include <extra2d/core/lifecycle.h>
#include <extra2d/services/logger_service.h>
#include <chrono>
namespace extra2d {
RenderService::RenderService() {
info_.name = "RenderService";
info_.priority = ServicePriority::Resource;
info_.enabled = true;
}
RenderService::~RenderService() = default;
bool RenderService::init() {
E2D_INFO(CAT_RENDER, "初始化渲染服务...");
renderer_ = std::make_unique<Renderer>();
if (!renderer_->init()) {
E2D_ERROR(CAT_RENDER, "渲染器初始化失败");
return false;
}
textRenderer_ = std::make_unique<TextRenderer>();
if (!textRenderer_->init()) {
E2D_ERROR(CAT_RENDER, "文本渲染器初始化失败");
renderer_->shutdown();
return false;
}
setState(ServiceState::Running);
E2D_INFO(CAT_RENDER, "渲染服务初始化完成");
return true;
}
void RenderService::shutdown() {
E2D_INFO(CAT_RENDER, "关闭渲染服务...");
if (textRenderer_) {
textRenderer_->shutdown();
textRenderer_.reset();
}
if (renderer_) {
renderer_->shutdown();
renderer_.reset();
}
setState(ServiceState::Stopped);
E2D_INFO(CAT_RENDER, "渲染服务已关闭");
}
void RenderService::beginFrame(const Color &clearColor) {
if (renderer_) {
renderer_->beginFrame(clearColor);
}
inFrame_ = true;
auto now = std::chrono::high_resolution_clock::now();
frameStartTime_ = static_cast<f32>(
std::chrono::duration<double>(now.time_since_epoch()).count());
}
void RenderService::endFrame() {
if (!inFrame_) {
return;
}
if (textRenderer_) {
textRenderer_->end();
}
if (renderer_) {
renderer_->endFrame();
}
inFrame_ = false;
auto now = std::chrono::high_resolution_clock::now();
f32 frameEndTime = static_cast<f32>(
std::chrono::duration<double>(now.time_since_epoch()).count());
stats_.frameTime = frameEndTime - frameStartTime_;
stats_.frameCount++;
fpsFrameCount_++;
fpsAccumulator_ += stats_.frameTime;
if (fpsAccumulator_ >= 1.0f) {
stats_.fps = static_cast<f32>(fpsFrameCount_) / fpsAccumulator_;
fpsFrameCount_ = 0;
fpsAccumulator_ = 0.0f;
}
}
void RenderService::setViewProjection(const glm::mat4 &viewProjection) {
if (renderer_) {
renderer_->setViewProjection(viewProjection);
}
if (textRenderer_) {
textRenderer_->begin(viewProjection);
}
}
glm::mat4 RenderService::viewProjection() const {
if (renderer_) {
return renderer_->viewProjection();
}
return glm::mat4(1.0f);
}
void RenderService::setCamera(const Camera &camera) {
setViewProjection(camera.viewProjectionMatrix());
}
void RenderService::drawLine(const Vec2 &start, const Vec2 &end,
const Color &color, float width) {
if (renderer_) {
renderer_->drawLine(start, end, color, width);
}
}
void RenderService::drawRect(const Rect &rect, const Color &color,
float width) {
if (renderer_) {
renderer_->drawRect(rect, color, width);
}
}
void RenderService::fillRect(const Rect &rect, const Color &color) {
if (renderer_) {
renderer_->fillRect(rect, color);
}
}
void RenderService::drawCircle(const Vec2 &center, float radius,
const Color &color, int segments, float width) {
if (renderer_) {
renderer_->drawCircle(center, radius, color, segments, width);
}
}
void RenderService::fillCircle(const Vec2 &center, float radius,
const Color &color, int segments) {
if (renderer_) {
renderer_->fillCircle(center, radius, color, segments);
}
}
void RenderService::drawTriangle(const Vec2 &p1, const Vec2 &p2,
const Vec2 &p3, const Color &color,
float width) {
if (renderer_) {
renderer_->drawTriangle(p1, p2, p3, color, width);
}
}
void RenderService::fillTriangle(const Vec2 &p1, const Vec2 &p2,
const Vec2 &p3, const Color &color) {
if (renderer_) {
renderer_->fillTriangle(p1, p2, p3, color);
}
}
void RenderService::drawPolygon(const std::vector<Vec2> &points,
const Color &color, float width) {
if (renderer_) {
renderer_->drawPolygon(points, color, width);
}
}
void RenderService::fillPolygon(const std::vector<Vec2> &points,
const Color &color) {
if (renderer_) {
renderer_->fillPolygon(points, color);
}
}
void RenderService::drawSprite(const Texture &texture, const Rect &destRect,
const Rect &srcRect, const Color &color,
float rotation, const Vec2 &anchor) {
if (renderer_) {
renderer_->drawSprite(texture, destRect, srcRect, color, rotation, anchor);
}
}
void RenderService::drawSprite(const Texture &texture, const Vec2 &position,
const Color &color) {
if (renderer_) {
renderer_->drawSprite(texture, position, color);
}
}
void RenderService::drawSprite(const Texture &texture, const Vec2 &position,
const Vec2 &scale, const Color &color) {
if (renderer_) {
renderer_->drawSprite(texture, position, scale, color);
}
}
void RenderService::drawText(FontAsset *font, const String32 &text, float x,
float y, const TextConfig &config) {
if (textRenderer_ && font) {
::extra2d::TextConfig cfg;
cfg.fontSize = config.fontSize;
cfg.color = config.color;
cfg.outlineWidth = config.outlineWidth;
cfg.outlineColor = config.outlineColor;
textRenderer_->drawText(font, text, x, y, cfg);
stats_.textChars += static_cast<u32>(text.size());
}
}
void RenderService::drawText(FontAsset *font, const std::string &text,
float x, float y, const TextConfig &config) {
String32 text32;
for (char c : text) {
text32.push_back(static_cast<char32_t>(c));
}
drawText(font, text32, x, y, config);
}
glm::vec2 RenderService::measureText(FontAsset *font, const String32 &text,
float fontSize) {
if (textRenderer_ && font) {
return textRenderer_->measureText(font, text, fontSize);
}
return glm::vec2(0.0f);
}
RenderServiceStats RenderService::stats() const {
RenderServiceStats s = stats_;
if (renderer_) {
s.drawCalls = renderer_->drawCalls();
s.spriteCount = renderer_->spriteCount();
}
if (textRenderer_) {
s.drawCalls += textRenderer_->drawCalls();
}
return s;
}
void RenderService::resetStats() {
stats_ = RenderServiceStats{};
fpsFrameCount_ = 0;
fpsAccumulator_ = 0.0f;
}
RenderDevice &RenderService::device() { return RenderDevice::instance(); }
Renderer *RenderService::renderer() { return renderer_.get(); }
TextRenderer *RenderService::textRenderer() { return textRenderer_.get(); }
void RenderService::onRender(RenderCallback callback) {
renderCallback_ = std::move(callback);
}
void RenderService::setClearColor(const Color &color) {
clearColor_ = color;
}
void RenderService::update(f32 dt) {
stats_.textChars = 0;
if (renderCallback_) {
auto windowModule = Lifecycle::instance().module<WindowModule>();
if (windowModule) {
int width = windowModule->width();
int height = windowModule->height();
Camera camera;
camera.setViewport(0, 0, width, height);
beginFrame(clearColor_);
setCamera(camera);
renderCallback_(this, dt);
endFrame();
}
}
}
glm::vec4 RenderService::toVec4(const Color &color) const {
return glm::vec4(color.r, color.g, color.b, color.a);
}
} // namespace extra2d

View File

@ -0,0 +1,155 @@
/**
* @file main.cpp
* @brief MSDF
*
* MSDF 使 RenderService
*/
#include <extra2d/extra2d.h>
#include <extra2d/services/render_service.h>
using namespace extra2d;
namespace {
Ref<FontAsset> g_font;
} // namespace
int main(int argc, char **argv) {
E2D_INFO(CAT_APP, "============================");
E2D_INFO(CAT_APP, "MSDF Text Rendering Demo");
E2D_INFO(CAT_APP, "============================");
auto &app = Application::instance();
app.name = "MSDF Text Demo";
app.useService<ILogger, ConsoleLogger>();
app.useService<IEventService, EventService>();
app.useService<ITimerService, TimerService>();
app.useService<IAssetService, AssetService>();
app.useService<IRenderService, RenderService>();
app.useModule<WindowModule>([](WindowCfg &cfg) {
cfg.title = "Extra2D - MSDF Text Demo";
cfg.width = 1280;
cfg.height = 720;
});
app.useModule<RenderModule>([](RenderCfg &cfg) {
cfg.glMajor = 4;
cfg.glMinor = 5;
cfg.vsync = true;
});
if (!app.init()) {
E2D_ERROR(CAT_APP, "应用初始化失败!");
return -1;
}
auto renderService = app.service<IRenderService>();
if (!renderService) {
E2D_ERROR(CAT_APP, "无法获取 RenderService");
app.shutdown();
return -1;
}
auto assetService = app.service<IAssetService>();
if (!assetService) {
E2D_ERROR(CAT_APP, "无法获取 AssetService");
app.shutdown();
return -1;
}
auto fontHandle = assetService->load<FontAsset>("assets/simhei.msdf.png");
g_font = fontHandle.get();
if (!g_font || !g_font->loaded()) {
E2D_ERROR(CAT_APP, "无法加载字体");
app.shutdown();
return -1;
}
E2D_INFO(CAT_APP, "字体加载成功,字体大小: {}", g_font->fontSize());
auto eventService = app.service<IEventService>();
if (eventService) {
eventService->on(EventType::KeyPressed, [&app](Event &e) {
auto &keyEvent = std::get<KeyEvent>(e.data);
if (keyEvent.key == static_cast<i32>(Key::Escape)) {
E2D_INFO(CAT_INPUT, "ESC 键按下,退出应用");
app.quit();
}
});
}
renderService->onRender([](IRenderService *render, f32 dt) {
if (!g_font) {
return;
}
float y = 60.0f;
::extra2d::TextConfig config;
config.fontSize = 48.0f;
config.color = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);
render->drawText(g_font.get(), U"MSDF Text Rendering Demo", 20.0f, y,
config);
y += 80.0f;
config.fontSize = 32.0f;
render->drawText(g_font.get(), U"Extra2D Engine - 2D Game Engine", 20.0f, y,
config);
y += 60.0f;
config.fontSize = 24.0f;
config.color = glm::vec4(0.7f, 0.9f, 1.0f, 1.0f);
render->drawText(g_font.get(), U"High quality text rendering with MSDF",
20.0f, y, config);
y += 45.0f;
config.fontSize = 20.0f;
config.color = glm::vec4(1.0f, 0.8f, 0.4f, 1.0f);
render->drawText(g_font.get(),
U"Multi-channel Signed Distance Field technology", 20.0f,
y, config);
y += 35.0f;
config.color = glm::vec4(0.6f, 1.0f, 0.6f, 1.0f);
render->drawText(g_font.get(),
U"Provides high quality, scalable vector font rendering",
20.0f, y, config);
y += 50.0f;
config.fontSize = 28.0f;
config.color = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);
render->drawText(g_font.get(), U"Character Test:", 20.0f, y, config);
y += 45.0f;
config.fontSize = 22.0f;
config.color = glm::vec4(0.9f, 0.9f, 0.9f, 1.0f);
render->drawText(g_font.get(), U"ABCDEFGHIJKLMNOPQRSTUVWXYZ", 20.0f, y,
config);
y += 35.0f;
render->drawText(g_font.get(), U"abcdefghijklmnopqrstuvwxyz", 20.0f, y,
config);
y += 35.0f;
render->drawText(g_font.get(), U"0123456789!@#$%^&*()[]{}<>", 20.0f, y,
config);
static int frameCounter = 0;
if (++frameCounter % 60 == 0) {
auto stats = render->stats();
E2D_DEBUG(CAT_RENDER, "FPS: {:.1f}, Draw Calls: {}", stats.fps,
stats.drawCalls);
}
});
E2D_INFO(CAT_APP, "开始主循环...");
app.run();
E2D_INFO(CAT_APP, "应用结束");
app.shutdown();
return 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

View File

@ -0,0 +1,75 @@
-- ==============================================
-- MSDF Text Demo 示例 - Xmake 构建脚本
-- 支持平台: MinGW (Windows), Nintendo Switch
-- ==============================================
local example_dir = os.scriptdir()
if is_plat("mingw") then
add_requires("glm", "libsdl2", "libsdl2_mixer")
end
target("msdf_text_demo")
set_kind("binary")
add_files("main.cpp")
add_includedirs("../../Extra2D/include")
add_deps("extra2d")
if is_plat("mingw") then
add_packages("glm", "libsdl2", "libsdl2_mixer")
end
if is_plat("switch") then
set_targetdir("../../build/examples/msdf_text_demo")
after_build(function (target)
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
local elf_file = target:targetfile()
local output_dir = path.directory(elf_file)
local nacp_file = path.join(output_dir, "msdf_text_demo.nacp")
local nro_file = path.join(output_dir, "msdf_text_demo.nro")
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
if os.isfile(nacptool) and os.isfile(elf2nro) then
os.vrunv(nacptool, {"--create", "MSDF Text Demo", "Extra2D Team", "1.0.0", nacp_file})
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs})
else
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
end
print("Generated NRO: " .. nro_file)
end
end)
after_package(function (target)
local nro_file = path.join(target:targetdir(), "msdf_text_demo.nro")
local package_dir = target:packagedir()
if os.isfile(nro_file) and package_dir then
os.cp(nro_file, package_dir)
print("Copied NRO to package: " .. package_dir)
end
end)
elseif is_plat("mingw") then
set_targetdir("../../build/examples/msdf_text_demo")
after_build(function (target)
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
local target_dir = path.directory(target:targetfile())
local assets_dir = path.join(target_dir, "assets")
if not os.isdir(assets_dir) then
os.mkdir(assets_dir)
end
os.cp(path.join(romfs, "assets/**"), assets_dir)
print("Copied assets from " .. romfs .. " to " .. assets_dir)
else
print("Warning: romfs directory not found at " .. romfs)
end
end)
end
target_end()

BIN
test_output.msdf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

View File

@ -0,0 +1,77 @@
# MSDF Font Builder
将 TTF 字体转换为 MSDF PNG 图集的离线构建工具。
直接集成 msdfgen 库,无需外部依赖。
## 构建
```bash
xmake build msdf_font_builder
```
## 使用方法
```bash
# 基本用法
msdf_font_builder -i font.ttf -o font.msdf.png
# 指定字符集
msdf_font_builder -i font.ttf -o font.msdf.png -c "ABCabc123"
# 使用字符集文件
msdf_font_builder -i font.ttf -o font.msdf.png -f charset.txt
# 自定义参数
msdf_font_builder -i font.ttf -o font.msdf.png -s 64 -r 6.0 -w 4096 -h 4096
```
## 命令行选项
| 选项 | 说明 | 默认值 |
|------|------|--------|
| `-i, --input` | 输入 TTF 字体文件 | 必需 |
| `-o, --output` | 输出 PNG 图集路径 | 必需 |
| `-c, --charset` | 字符集字符串 | ASCII 可打印字符 |
| `-f, --charset-file` | 字符集文件路径 | - |
| `-s, --size` | 字体大小(像素) | 48 |
| `-r, --range` | 像素范围 | 4.0 |
| `-w, --width` | 图集宽度 | 2048 |
| `-h, --height` | 图集高度 | 2048 |
## 输出格式
生成的 PNG 文件包含嵌入的元数据(存储在 tEXt chunk 中):
```json
{
"size": 48,
"pxRange": 4.0,
"lineHeight": 60,
"baseline": 12,
"atlas": { "width": 2048, "height": 2048 },
"glyphs": [...]
}
```
## 在 Extra2D 中使用
```cpp
#include <extra2d/asset/font_asset.h>
#include <extra2d/render/text_renderer.h>
auto font = assetService->load<FontAsset>("font.msdf.png");
TextRenderer renderer;
renderer.init();
renderer.begin(projection);
renderer.drawText(font.get(), U"Hello, World!", 100, 100, { .fontSize = 32.0f });
renderer.end();
```
## 工作流程
```
构建时: TTF ──► msdfgen 库 ──► MSDF PNG (内嵌 JSON 元数据)
运行时: FontLoader ──► FontAsset ──► TextRenderer
```

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,100 @@
/**
* @file main.cpp
* @brief MSDF
*
* :
* msdf_font_builder -i <input.ttf> -o <output.png> [options]
*
* :
* -i, --input <file> TTF
* -o, --output <file> PNG
* -c, --charset <string>
* -f, --charset-file <file>
* -s, --size <pixels> 48
* -r, --range <pixels> 4.0
* -w, --width <pixels> 2048
* -h, --height <pixels> 2048
* --help
*/
#include "msdf_font_builder.h"
#include <iostream>
#include <string>
#include <cstdlib>
using namespace extra2d::tools;
void printUsage(const char* programName) {
std::cout << "MSDF Font Builder - TTF 转 MSDF PNG 图集工具\n\n";
std::cout << "用法:\n";
std::cout << " " << programName << " -i <input.ttf> -o <output.png> [options]\n\n";
std::cout << "选项:\n";
std::cout << " -i, --input <file> 输入 TTF 字体文件路径\n";
std::cout << " -o, --output <file> 输出 PNG 图集路径\n";
std::cout << " -c, --charset <string> 字符集字符串\n";
std::cout << " -f, --charset-file <file> 字符集文件路径\n";
std::cout << " -s, --size <pixels> 字体大小(默认 48\n";
std::cout << " -r, --range <pixels> 像素范围(默认 4.0\n";
std::cout << " -w, --width <pixels> 图集宽度(默认 2048\n";
std::cout << " -h, --height <pixels> 图集高度(默认 2048\n";
std::cout << " --help 显示帮助信息\n\n";
std::cout << "示例:\n";
std::cout << " " << programName << " -i font.ttf -o font.msdf.png\n";
std::cout << " " << programName << " -i font.ttf -o font.msdf.png -s 64 -c \"ABCabc123\"\n";
std::cout << " " << programName << " -i font.ttf -o font.msdf.png -f charset.txt\n";
}
int main(int argc, char* argv[]) {
MSDFFontBuilder builder;
bool showHelp = false;
int width = 2048;
int height = 2048;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--help") {
showHelp = true;
} else if ((arg == "-i" || arg == "--input") && i + 1 < argc) {
builder.setInputFont(argv[++i]);
} else if ((arg == "-o" || arg == "--output") && i + 1 < argc) {
builder.setOutputPath(argv[++i]);
} else if ((arg == "-c" || arg == "--charset") && i + 1 < argc) {
builder.setCharset(argv[++i]);
} else if ((arg == "-f" || arg == "--charset-file") && i + 1 < argc) {
builder.setCharsetFile(argv[++i]);
} else if ((arg == "-s" || arg == "--size") && i + 1 < argc) {
builder.setFontSize(std::atoi(argv[++i]));
} else if ((arg == "-r" || arg == "--range") && i + 1 < argc) {
builder.setPxRange(static_cast<float>(std::atof(argv[++i])));
} else if ((arg == "-w" || arg == "--width") && i + 1 < argc) {
width = std::atoi(argv[++i]);
} else if ((arg == "-h" || arg == "--height") && i + 1 < argc) {
height = std::atoi(argv[++i]);
} else {
std::cerr << "未知参数: " << arg << "\n";
showHelp = true;
}
}
builder.setAtlasSize(width, height);
if (showHelp) {
printUsage(argv[0]);
return 0;
}
std::cout << "MSDF Font Builder\n";
std::cout << "==================\n\n";
std::cout << "开始构建...\n\n";
if (!builder.build()) {
std::cerr << "错误: " << builder.getError() << "\n";
return 1;
}
std::cout << "成功生成 MSDF 字体图集!\n";
std::cout << "字形数量: " << builder.getMetadata().glyphs.size() << "\n";
return 0;
}

View File

@ -0,0 +1,588 @@
#include "msdf_font_builder.h"
#include <msdfgen/msdfgen.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_OUTLINE_H
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <cmath>
#include <map>
namespace extra2d {
namespace tools {
using namespace msdfgen;
/**
* @brief Freetype
*/
struct OutlineContext {
Shape* shape;
Contour* currentContour;
Point2 lastPoint;
bool hasLastPoint;
std::map<char32_t, double> advanceMap;
};
/**
* @brief MSDF
*/
struct MSDFFontBuilder::Impl {
FT_Library ftLibrary = nullptr;
FT_Face ftFace = nullptr;
OutlineContext outlineCtx;
struct GlyphBitmap {
char32_t codepoint;
Bitmap<float, 3> bitmap;
double advance;
double l, b, r, t;
double pl, pb, pr, pt;
};
std::vector<GlyphBitmap> glyphBitmaps;
};
/**
* @brief Freetype
*/
static int ftMoveTo(const FT_Vector* to, void* user) {
OutlineContext* ctx = static_cast<OutlineContext*>(user);
ctx->currentContour = &ctx->shape->addContour();
ctx->lastPoint = Point2(to->x / 64.0, to->y / 64.0);
ctx->hasLastPoint = true;
return 0;
}
/**
* @brief Freetype 线
*/
static int ftLineTo(const FT_Vector* to, void* user) {
OutlineContext* ctx = static_cast<OutlineContext*>(user);
if (ctx->currentContour && ctx->hasLastPoint) {
Point2 endPoint(to->x / 64.0, to->y / 64.0);
ctx->currentContour->addEdge(EdgeHolder(ctx->lastPoint, endPoint));
ctx->lastPoint = endPoint;
}
return 0;
}
/**
* @brief Freetype 线
*/
static int ftConicTo(const FT_Vector* control, const FT_Vector* to, void* user) {
OutlineContext* ctx = static_cast<OutlineContext*>(user);
if (ctx->currentContour && ctx->hasLastPoint) {
Point2 controlPoint(control->x / 64.0, control->y / 64.0);
Point2 endPoint(to->x / 64.0, to->y / 64.0);
ctx->currentContour->addEdge(EdgeHolder(ctx->lastPoint, controlPoint, endPoint));
ctx->lastPoint = endPoint;
}
return 0;
}
/**
* @brief Freetype 线
*/
static int ftCubicTo(const FT_Vector* control1, const FT_Vector* control2, const FT_Vector* to, void* user) {
OutlineContext* ctx = static_cast<OutlineContext*>(user);
if (ctx->currentContour && ctx->hasLastPoint) {
Point2 cp1(control1->x / 64.0, control1->y / 64.0);
Point2 cp2(control2->x / 64.0, control2->y / 64.0);
Point2 endPoint(to->x / 64.0, to->y / 64.0);
ctx->currentContour->addEdge(EdgeHolder(ctx->lastPoint, cp1, cp2, endPoint));
ctx->lastPoint = endPoint;
}
return 0;
}
MSDFFontBuilder::MSDFFontBuilder() : impl_(new Impl()) {
}
MSDFFontBuilder::~MSDFFontBuilder() {
if (impl_) {
if (impl_->ftFace) {
FT_Done_Face(impl_->ftFace);
}
if (impl_->ftLibrary) {
FT_Done_FreeType(impl_->ftLibrary);
}
delete impl_;
}
}
void MSDFFontBuilder::setInputFont(const std::string& path) {
inputFont_ = path;
}
void MSDFFontBuilder::setOutputPath(const std::string& path) {
outputPath_ = path;
}
void MSDFFontBuilder::setCharset(const std::string& charset) {
charset_ = charset;
}
void MSDFFontBuilder::setCharsetFile(const std::string& path) {
charsetFile_ = path;
}
void MSDFFontBuilder::setFontSize(int size) {
fontSize_ = size;
}
void MSDFFontBuilder::setPxRange(float range) {
pxRange_ = range;
}
void MSDFFontBuilder::setAtlasSize(int width, int height) {
atlasWidth_ = width;
atlasHeight_ = height;
}
bool MSDFFontBuilder::build() {
if (inputFont_.empty()) {
error_ = "Input font path not set";
return false;
}
if (outputPath_.empty()) {
error_ = "Output path not set";
return false;
}
if (!loadCharset()) {
return false;
}
FT_Error error = FT_Init_FreeType(&impl_->ftLibrary);
if (error) {
error_ = "Failed to initialize FreeType";
return false;
}
error = FT_New_Face(impl_->ftLibrary, inputFont_.c_str(), 0, &impl_->ftFace);
if (error) {
error_ = "Failed to load font: " + inputFont_;
return false;
}
error = FT_Set_Char_Size(impl_->ftFace, fontSize_ * 64, fontSize_ * 64, 72, 72);
if (error) {
error_ = "Failed to set font size";
return false;
}
if (!generateAllGlyphs()) {
return false;
}
if (!packGlyphs()) {
return false;
}
if (!savePngWithMetadata()) {
return false;
}
return true;
}
bool MSDFFontBuilder::loadCharset() {
if (!charsetFile_.empty()) {
std::ifstream file(charsetFile_);
if (!file) {
error_ = "Failed to open charset file: " + charsetFile_;
return false;
}
std::stringstream buffer;
buffer << file.rdbuf();
charset_ = buffer.str();
}
if (charset_.empty()) {
charset_ = " !\"#$%&'()*+,-./0123456789:;<=>?@"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`"
"abcdefghijklmnopqrstuvwxyz{|}~";
}
return true;
}
/**
* @brief Freetype msdfgen Shape
*/
static bool loadGlyphShape(FT_Face face, char32_t codepoint, Shape& shape, double& advance) {
FT_UInt glyphIndex = FT_Get_Char_Index(face, codepoint);
if (glyphIndex == 0) {
return false;
}
FT_Error error = FT_Load_Glyph(face, glyphIndex, FT_LOAD_NO_SCALE);
if (error) {
return false;
}
advance = face->glyph->metrics.horiAdvance / 64.0;
FT_Outline* outline = &face->glyph->outline;
if (outline->n_contours == 0) {
return false;
}
OutlineContext ctx;
ctx.shape = &shape;
ctx.currentContour = nullptr;
ctx.hasLastPoint = false;
FT_Outline_Funcs funcs;
funcs.move_to = ftMoveTo;
funcs.line_to = ftLineTo;
funcs.conic_to = ftConicTo;
funcs.cubic_to = ftCubicTo;
funcs.shift = 0;
funcs.delta = 0;
error = FT_Outline_Decompose(outline, &funcs, &ctx);
if (error) {
return false;
}
return true;
}
bool MSDFFontBuilder::generateAllGlyphs() {
impl_->glyphBitmaps.clear();
double unitsPerEM = impl_->ftFace->units_per_EM;
if (unitsPerEM == 0) {
unitsPerEM = 1000.0;
}
double scale = fontSize_ / unitsPerEM;
for (char c : charset_) {
char32_t codepoint = static_cast<char32_t>(static_cast<unsigned char>(c));
Shape shape;
double glyphAdvance = 0.0;
if (!loadGlyphShape(impl_->ftFace, codepoint, shape, glyphAdvance)) {
continue;
}
shape.normalize();
edgeColoringSimple(shape, 3.0);
Shape::Bounds bounds = shape.getBounds();
double l = bounds.l;
double b = bounds.b;
double r = bounds.r;
double t = bounds.t;
if (l >= r || b >= t) {
continue;
}
double pl = l * scale;
double pb = b * scale;
double pr = r * scale;
double pt = t * scale;
double range = pxRange_ / scale;
l -= range;
b -= range;
r += range;
t += range;
double width = (r - l) * scale;
double height = (t - b) * scale;
int w = static_cast<int>(std::ceil(width));
int h = static_cast<int>(std::ceil(height));
w = std::max(w, 1);
h = std::max(h, 1);
Impl::GlyphBitmap gb;
gb.codepoint = codepoint;
gb.advance = glyphAdvance * scale;
gb.l = l;
gb.b = b;
gb.r = r;
gb.t = t;
gb.pl = pl;
gb.pb = pb;
gb.pr = pr;
gb.pt = pt;
gb.bitmap = Bitmap<float, 3>(w, h);
MSDFGeneratorConfig config;
config.overlapSupport = true;
Projection projection(Vector2(scale, scale), Vector2(-l * scale, -b * scale));
Range distanceRange(range * scale);
generateMSDF(gb.bitmap, shape, projection, distanceRange, config);
impl_->glyphBitmaps.push_back(std::move(gb));
}
return !impl_->glyphBitmaps.empty();
}
bool MSDFFontBuilder::packGlyphs() {
std::vector<int> positions(impl_->glyphBitmaps.size() * 2, 0);
std::vector<std::pair<int, int>> sizes;
for (const auto& gb : impl_->glyphBitmaps) {
sizes.emplace_back(gb.bitmap.width(), gb.bitmap.height());
}
std::vector<int> indices(sizes.size());
for (size_t i = 0; i < indices.size(); ++i) {
indices[i] = static_cast<int>(i);
}
std::sort(indices.begin(), indices.end(), [&](int a, int b) {
return sizes[a].second > sizes[b].second;
});
int x = 0, y = 0;
int rowHeight = 0;
for (int idx : indices) {
int w = sizes[idx].first;
int h = sizes[idx].second;
if (x + w > atlasWidth_) {
x = 0;
y += rowHeight;
rowHeight = 0;
}
if (y + h > atlasHeight_) {
error_ = "Atlas size too small for all glyphs";
return false;
}
positions[idx * 2] = x;
positions[idx * 2 + 1] = y;
x += w;
rowHeight = std::max(rowHeight, h);
}
Bitmap<float, 3> atlas(atlasWidth_, atlasHeight_);
for (int py = 0; py < atlasHeight_; ++py) {
for (int px = 0; px < atlasWidth_; ++px) {
float* pixel = atlas(px, py);
pixel[0] = 0.0f;
pixel[1] = 0.0f;
pixel[2] = 0.0f;
}
}
for (size_t i = 0; i < impl_->glyphBitmaps.size(); ++i) {
const auto& gb = impl_->glyphBitmaps[i];
int px = positions[i * 2];
int py = positions[i * 2 + 1];
for (int gy = 0; gy < gb.bitmap.height(); ++gy) {
for (int gx = 0; gx < gb.bitmap.width(); ++gx) {
int targetY = atlasHeight_ - 1 - (py + gy);
float* pixel = atlas(px + gx, targetY);
const float* srcPixel = gb.bitmap(gx, gy);
pixel[0] = srcPixel[0];
pixel[1] = srcPixel[1];
pixel[2] = srcPixel[2];
}
}
GlyphData glyph;
glyph.codepoint = gb.codepoint;
glyph.advance = static_cast<float>(gb.advance);
glyph.left = static_cast<double>(px);
glyph.top = static_cast<double>(atlasHeight_ - py - gb.bitmap.height());
glyph.right = static_cast<double>(px + gb.bitmap.width());
glyph.bottom = static_cast<double>(atlasHeight_ - py);
glyph.uvMin.x = static_cast<float>(glyph.left / atlasWidth_);
glyph.uvMin.y = static_cast<float>(glyph.top / atlasHeight_);
glyph.uvMax.x = static_cast<float>(glyph.right / atlasWidth_);
glyph.uvMax.y = static_cast<float>(glyph.bottom / atlasHeight_);
glyph.size.x = static_cast<float>(gb.pr - gb.pl);
glyph.size.y = static_cast<float>(gb.pt - gb.pb);
glyph.bearing.x = static_cast<float>(gb.pl);
glyph.bearing.y = static_cast<float>(gb.pt);
metadata_.glyphs.push_back(glyph);
}
metadata_.fontSize = fontSize_;
metadata_.pxRange = pxRange_;
metadata_.atlasWidth = atlasWidth_;
metadata_.atlasHeight = atlasHeight_;
metadata_.lineHeight = static_cast<int>(fontSize_ * 1.25);
metadata_.baseline = static_cast<int>(fontSize_ * 0.25);
impl_->glyphBitmaps.clear();
impl_->glyphBitmaps.push_back({0, std::move(atlas), 0, 0, 0, 0, 0, 0, 0, 0, 0});
return true;
}
bool MSDFFontBuilder::savePngWithMetadata() {
if (impl_->glyphBitmaps.empty()) {
error_ = "No atlas generated";
return false;
}
const auto& atlas = impl_->glyphBitmaps[0].bitmap;
std::vector<uint8_t> rgbaData(atlasWidth_ * atlasHeight_ * 4);
for (int y = 0; y < atlasHeight_; ++y) {
for (int x = 0; x < atlasWidth_; ++x) {
const float* pixel = atlas(x, y);
int idx = (y * atlasWidth_ + x) * 4;
rgbaData[idx + 0] = static_cast<uint8_t>(std::clamp(pixel[0] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f);
rgbaData[idx + 1] = static_cast<uint8_t>(std::clamp(pixel[1] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f);
rgbaData[idx + 2] = static_cast<uint8_t>(std::clamp(pixel[2] * 0.5f + 0.5f, 0.0f, 1.0f) * 255.0f);
rgbaData[idx + 3] = 255;
}
}
std::string jsonStr = generateMetadataJson();
std::vector<uint8_t> textChunk;
std::string keyword = "msdf";
textChunk.insert(textChunk.end(), keyword.begin(), keyword.end());
textChunk.push_back(0);
textChunk.insert(textChunk.end(), jsonStr.begin(), jsonStr.end());
uint32_t length = static_cast<uint32_t>(textChunk.size());
std::vector<uint8_t> lengthBytes(4);
lengthBytes[0] = (length >> 24) & 0xFF;
lengthBytes[1] = (length >> 16) & 0xFF;
lengthBytes[2] = (length >> 8) & 0xFF;
lengthBytes[3] = length & 0xFF;
std::vector<uint8_t> crcData;
crcData.insert(crcData.end(), {'t', 'E', 'X', 't'});
crcData.insert(crcData.end(), textChunk.begin(), textChunk.end());
uint32_t crc = 0xFFFFFFFF;
for (uint8_t byte : crcData) {
crc ^= byte;
for (int i = 0; i < 8; ++i) {
crc = (crc >> 1) ^ ((crc & 1) ? 0xEDB88320 : 0);
}
}
crc ^= 0xFFFFFFFF;
std::vector<uint8_t> crcBytes(4);
crcBytes[0] = (crc >> 24) & 0xFF;
crcBytes[1] = (crc >> 16) & 0xFF;
crcBytes[2] = (crc >> 8) & 0xFF;
crcBytes[3] = crc & 0xFF;
std::string tempPng = outputPath_ + ".tmp.png";
if (!stbi_write_png(tempPng.c_str(), atlasWidth_, atlasHeight_, 4, rgbaData.data(), atlasWidth_ * 4)) {
error_ = "Failed to save PNG: " + tempPng;
return false;
}
std::ifstream inputFile(tempPng, std::ios::binary);
if (!inputFile) {
error_ = "Failed to open temporary PNG file";
return false;
}
std::vector<uint8_t> pngData((std::istreambuf_iterator<char>(inputFile)),
std::istreambuf_iterator<char>());
inputFile.close();
std::vector<uint8_t> newPng;
newPng.insert(newPng.end(), pngData.begin(), pngData.begin() + 8);
size_t pos = 8;
bool inserted = false;
while (pos < pngData.size()) {
if (pos + 8 > pngData.size()) break;
std::string chunkType(pngData.begin() + pos + 4, pngData.begin() + pos + 8);
if (!inserted && chunkType != "IHDR") {
newPng.insert(newPng.end(), lengthBytes.begin(), lengthBytes.end());
newPng.insert(newPng.end(), {'t', 'E', 'X', 't'});
newPng.insert(newPng.end(), textChunk.begin(), textChunk.end());
newPng.insert(newPng.end(), crcBytes.begin(), crcBytes.end());
inserted = true;
}
uint32_t chunkLen = (static_cast<uint32_t>(pngData[pos]) << 24) |
(static_cast<uint32_t>(pngData[pos + 1]) << 16) |
(static_cast<uint32_t>(pngData[pos + 2]) << 8) |
static_cast<uint32_t>(pngData[pos + 3]);
size_t chunkEnd = pos + 12 + chunkLen;
newPng.insert(newPng.end(), pngData.begin() + pos, pngData.begin() + chunkEnd);
pos = chunkEnd;
}
std::ofstream outputFile(outputPath_, std::ios::binary);
if (!outputFile) {
error_ = "Failed to write PNG file: " + outputPath_;
return false;
}
outputFile.write(reinterpret_cast<const char*>(newPng.data()), newPng.size());
std::remove(tempPng.c_str());
return true;
}
std::string MSDFFontBuilder::generateMetadataJson() {
nlohmann::json j;
j["size"] = metadata_.fontSize;
j["pxRange"] = metadata_.pxRange;
j["lineHeight"] = metadata_.lineHeight;
j["baseline"] = metadata_.baseline;
j["atlas"]["width"] = metadata_.atlasWidth;
j["atlas"]["height"] = metadata_.atlasHeight;
for (const auto& glyph : metadata_.glyphs) {
nlohmann::json glyphJson;
glyphJson["unicode"] = static_cast<uint32_t>(glyph.codepoint);
glyphJson["advance"] = glyph.advance;
glyphJson["atlasBounds"]["left"] = glyph.left;
glyphJson["atlasBounds"]["bottom"] = glyph.bottom;
glyphJson["atlasBounds"]["right"] = glyph.right;
glyphJson["atlasBounds"]["top"] = glyph.top;
glyphJson["planeBounds"]["left"] = glyph.bearing.x;
glyphJson["planeBounds"]["bottom"] = glyph.bearing.y - glyph.size.y;
glyphJson["planeBounds"]["right"] = glyph.bearing.x + glyph.size.x;
glyphJson["planeBounds"]["top"] = glyph.bearing.y;
j["glyphs"].push_back(glyphJson);
}
return j.dump();
}
} // namespace tools
} // namespace extra2d

View File

@ -0,0 +1,143 @@
#pragma once
#include <glm/glm.hpp>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
namespace extra2d {
namespace tools {
/**
* @brief
*/
struct GlyphData {
char32_t codepoint = 0;
glm::vec2 uvMin;
glm::vec2 uvMax;
glm::vec2 size;
glm::vec2 bearing;
float advance = 0.0f;
double left = 0.0;
double bottom = 0.0;
double right = 0.0;
double top = 0.0;
};
/**
* @brief MSDF
*/
struct MSDFMetadata {
int fontSize = 48;
float pxRange = 4.0f;
int atlasWidth = 2048;
int atlasHeight = 2048;
int lineHeight = 60;
int baseline = 12;
std::vector<GlyphData> glyphs;
};
/**
* @brief MSDF
*
* 使 msdfgen TTF MSDF PNG
*/
class MSDFFontBuilder {
public:
MSDFFontBuilder();
~MSDFFontBuilder();
/**
* @brief TTF
*/
void setInputFont(const std::string &path);
/**
* @brief PNG
*/
void setOutputPath(const std::string &path);
/**
* @brief
*/
void setCharset(const std::string &charset);
/**
* @brief
*/
void setCharsetFile(const std::string &path);
/**
* @brief
*/
void setFontSize(int size);
/**
* @brief
*/
void setPxRange(float range);
/**
* @brief
*/
void setAtlasSize(int width, int height);
/**
* @brief
*/
bool build();
/**
* @brief
*/
const std::string &getError() const { return error_; }
/**
* @brief
*/
const MSDFMetadata &getMetadata() const { return metadata_; }
private:
std::string inputFont_;
std::string outputPath_;
std::string charset_;
std::string charsetFile_;
int fontSize_ = 48;
float pxRange_ = 4.0f;
int atlasWidth_ = 2048;
int atlasHeight_ = 2048;
MSDFMetadata metadata_;
std::string error_;
struct Impl;
Impl *impl_ = nullptr;
/**
* @brief
*/
bool loadCharset();
/**
* @brief MSDF
*/
bool generateAllGlyphs();
/**
* @brief
*/
bool packGlyphs();
/**
* @brief PNG
*/
bool savePngWithMetadata();
/**
* @brief JSON
*/
std::string generateMetadataJson();
};
} // namespace tools
} // namespace extra2d

View File

@ -72,7 +72,7 @@ end
-- ==============================================
if target_plat == "mingw" then
add_requires("glm", "libsdl2", "libsdl2_mixer", "zstd", "lz4", "zlib", "libsodium")
add_requires("glm", "libsdl2", "libsdl2_mixer", "zstd", "lz4", "zlib", "libsodium", "nlohmann_json", "msdfgen", "freetype")
end
-- ==============================================
@ -85,9 +85,19 @@ includes("xmake/engine.lua")
-- 定义引擎库
define_extra2d_engine()
-- MSDF 字体构建工具
target("msdf_font_builder")
set_kind("binary")
add_files("tools/msdf_font_builder/main.cpp", "tools/msdf_font_builder/msdf_font_builder.cpp")
add_includedirs("tools/msdf_font_builder", "Extra2D/include", "Extra2D/include/stb")
add_packages("glm", "nlohmann_json", "msdfgen", "freetype")
add_defines("MSDFGEN_USE_CPP11")
set_default(false)
-- 示例程序目标(作为子项目)
if is_config("examples","true") then
includes("examples/hello_world", {rootdir = "examples/hello_world"})
includes("examples/msdf_text_demo", {rootdir = "examples/msdf_text_demo"})
end
-- ==============================================