feat: 添加MSDF字体支持并重构文本渲染系统
实现MSDF字体渲染功能,包括: 1. 新增MSDF字体构建工具,支持从TTF生成MSDF图集 2. 重构字体加载器和文本渲染器以支持MSDF 3. 添加MSDF文本渲染示例程序 4. 移除旧的TrueType字体渲染实现 5. 优化VAO顶点属性绑定设置 MSDF字体提供高质量的可缩放文本渲染,同时保持运行时性能。构建工具将TTF转换为带元数据的PNG图集,运行时只需加载预生成的纹理。重构后的文本渲染系统更简洁高效,支持中文等复杂字符集。
This commit is contained in:
parent
5158fd713e
commit
bbefd83664
|
|
@ -142,21 +142,40 @@ private:
|
|||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FontLoader - 字体加载器
|
||||
// FontLoader - 字体加载器 (MSDF)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief 字体加载器
|
||||
*
|
||||
* 加载 TrueType 字体文件(.ttf, .otf)。
|
||||
* 加载内嵌元数据的 MSDF PNG 图集。
|
||||
* 构建时使用 msdfgen 生成图集,运行时只加载预生成的 PNG。
|
||||
*/
|
||||
class FontLoader : public AssetLoader<FontAsset> {
|
||||
public:
|
||||
Ref<FontAsset> load(const std::string &path) override;
|
||||
Ref<FontAsset> 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;
|
||||
Ref<FontAsset> load(const std::string &path) override;
|
||||
Ref<FontAsset> 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, 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);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 ¢er, 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 ¢er, 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 ¢er, float radius, const Color &color,
|
||||
int segments = 32, float width = 1.0f) override;
|
||||
void fillCircle(const Vec2 ¢er, 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
|
|
@ -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
|
|
@ -124,42 +124,167 @@ 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);
|
||||
return nullptr;
|
||||
}
|
||||
return loadFromMemory(data.data(), data.size());
|
||||
auto asset = ptr::make<FontAsset>();
|
||||
|
||||
if (!parseEmbeddedMetadata(path, asset.get())) {
|
||||
E2D_ERROR(CAT_ASSET, "Failed to parse MSDF font metadata: {}", path);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
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();
|
||||
return std::find(exts.begin(), exts.end(), ext) != exts.end();
|
||||
std::string ext = getExtension(path);
|
||||
auto exts = extensions();
|
||||
return std::find(exts.begin(), exts.end(), ext) != exts.end();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
return false;
|
||||
const GlyphInfo* FontAsset::getGlyph(char32_t codepoint) const {
|
||||
auto it = glyphs_.find(codepoint);
|
||||
if (it != glyphs_.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
data_ = std::move(data);
|
||||
bool FontAsset::hasGlyph(char32_t codepoint) const {
|
||||
return glyphs_.find(codepoint) != glyphs_.end();
|
||||
}
|
||||
|
||||
if (!stbtt_InitFont(&impl_->info, data_.data(), 0)) {
|
||||
data_.clear();
|
||||
impl_->initialized = false;
|
||||
setState(AssetState::Failed);
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
|
||||
impl_->initialized = true;
|
||||
setState(AssetState::Loaded);
|
||||
bool FontAsset::canRender(const std::u32string& text) const {
|
||||
for (char32_t c : text) {
|
||||
if (!hasGlyph(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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 ¢er, float radius,
|
||||
const Color &color, int segments, float width) {
|
||||
if (renderer_) {
|
||||
renderer_->drawCircle(center, radius, color, segments, width);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderService::fillCircle(const Vec2 ¢er, 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
|
||||
|
|
@ -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 |
|
|
@ -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()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 594 KiB |
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
12
xmake.lua
12
xmake.lua
|
|
@ -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
|
||||
|
||||
-- ==============================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue