145 const std::shared_ptr<GraphicsDevice>& graphicsDevice)
147 if (!graphicsDevice) {
151 const auto jsonText = readTextFile(jsonPath);
152 if (!jsonText.has_value()) {
155 const std::string& text = *jsonText;
160 const std::string mapMarker =
"\"maps\":[{";
161 const size_t mapPos = text.find(mapMarker);
162 if (mapPos != std::string::npos) {
163 const size_t mapStart = text.find(
'{', mapPos);
164 const size_t mapEnd = text.find(
'}', mapStart);
165 if (mapStart != std::string::npos && mapEnd != std::string::npos && mapEnd > mapStart) {
166 const std::string mapBlock = text.substr(mapStart, mapEnd - mapStart + 1);
167 parseIntField(mapBlock,
"width", font->atlasWidth);
168 parseIntField(mapBlock,
"height", font->atlasHeight);
174 const std::string charsMarker =
"\"chars\":{";
175 const size_t charsPos = text.find(charsMarker);
176 if (charsPos != std::string::npos) {
177 const size_t objStart = text.find(
'{', charsPos);
178 const auto objEndOpt = objStart != std::string::npos ? findMatchingBrace(text, objStart) : std::nullopt;
179 if (!objEndOpt.has_value() || *objEndOpt <= objStart) {
183 const std::string charsBlock = text.substr(objStart + 1, *objEndOpt - objStart - 1);
187 const size_t keyStart = charsBlock.find(
'"', p);
188 if (keyStart == std::string::npos)
break;
189 const size_t keyEnd = charsBlock.find(
'"', keyStart + 1);
190 if (keyEnd == std::string::npos)
break;
192 if (!parseStrictInt(charsBlock.substr(keyStart + 1, keyEnd - keyStart - 1), charId)) {
196 const size_t glyphObjStart = charsBlock.find(
'{', keyEnd);
197 if (glyphObjStart == std::string::npos)
break;
198 const auto glyphObjEndOpt = findMatchingBrace(charsBlock, glyphObjStart);
199 if (!glyphObjEndOpt.has_value() || *glyphObjEndOpt <= glyphObjStart)
break;
201 const std::string block = charsBlock.substr(glyphObjStart, *glyphObjEndOpt - glyphObjStart + 1);
204 parseNumberField(block,
"x", glyph.
x);
205 parseNumberField(block,
"y", glyph.
y);
206 parseNumberField(block,
"width", glyph.
width);
207 parseNumberField(block,
"height", glyph.
height);
208 parseNumberField(block,
"xadvance", glyph.
xadvance);
209 parseNumberField(block,
"xoffset", glyph.
xoffset);
210 parseNumberField(block,
"yoffset", glyph.
yoffset);
211 font->glyphs[glyph.
id] = glyph;
213 font->lineHeight = std::max(font->lineHeight, glyph.
height);
214 p = *glyphObjEndOpt + 1;
220 const std::string kerningMarker =
"\"kerning\":{";
221 const size_t kernPos = text.find(kerningMarker);
222 if (kernPos != std::string::npos) {
223 const size_t objStart = text.find(
'{', kernPos);
224 const auto objEndOpt = objStart != std::string::npos ? findMatchingBrace(text, objStart) : std::nullopt;
225 if (objEndOpt.has_value() && *objEndOpt > objStart) {
226 const std::string block = text.substr(objStart, *objEndOpt - objStart + 1);
229 const size_t k1 = block.find(
'"', p);
230 if (k1 == std::string::npos)
break;
231 const size_t k2 = block.find(
'"', k1 + 1);
232 if (k2 == std::string::npos)
break;
234 if (!parseStrictInt(block.substr(k1 + 1, k2 - k1 - 1), left)) {
238 const size_t subObjStart = block.find(
'{', k2);
239 if (subObjStart == std::string::npos)
break;
240 const auto subObjEndOpt = findMatchingBrace(block, subObjStart);
241 if (!subObjEndOpt.has_value())
break;
242 const size_t subObjEnd = *subObjEndOpt;
243 const std::string sub = block.substr(subObjStart, subObjEnd - subObjStart + 1);
247 const size_t r1 = sub.find(
'"', q);
248 if (r1 == std::string::npos)
break;
249 const size_t r2 = sub.find(
'"', r1 + 1);
250 if (r2 == std::string::npos)
break;
252 if (!parseStrictInt(sub.substr(r1 + 1, r2 - r1 - 1), right)) {
257 const std::string numKey =
"\"" + std::to_string(right) +
"\":";
258 const size_t vPos = sub.find(numKey, r2);
259 if (vPos != std::string::npos) {
260 const size_t nStart = vPos + numKey.size();
261 size_t nEnd = nStart;
262 while (nEnd < sub.size()) {
263 const char c = sub[nEnd];
264 if ((c >=
'0' && c <=
'9') || c ==
'-' || c ==
'+' || c ==
'.') {
271 value = std::stof(sub.substr(nStart, nEnd - nStart));
272 const uint64_t key = (
static_cast<uint64_t
>(
static_cast<uint32_t
>(left)) << 32u) |
273 static_cast<uint32_t
>(right);
274 font->kerning[key] = value;
285 const std::string atlasPath = replaceExtensionWithPng(jsonPath);
289 stbi_set_flip_vertically_on_load(
false);
290 stbi_uc* pixels = stbi_load(atlasPath.c_str(), &w, &h, &channels, STBI_rgb_alpha);
291 if (!pixels || w <= 0 || h <= 0) {
294 stbi_image_free(pixels);
299 const size_t pixelCount =
static_cast<size_t>(w) *
static_cast<size_t>(h);
300 const bool likelySdfMsdf = text.find(
"\"range\":") != std::string::npos;
305 size_t midCountPos = 0;
306 size_t midCountNeg = 0;
307 std::vector<uint8_t> alphaPos(pixelCount);
308 std::vector<uint8_t> alphaNeg(pixelCount);
309 for (
size_t i = 0; i < pixelCount; ++i) {
310 const float r =
static_cast<float>(pixels[i * 4u + 0u]) / 255.0f;
311 const float g =
static_cast<float>(pixels[i * 4u + 1u]) / 255.0f;
312 const float b =
static_cast<float>(pixels[i * 4u + 2u]) / 255.0f;
313 const float sd = median3(r, g, b) - 0.5f;
314 const float covPos = std::clamp(sd * 8.0f + 0.5f, 0.0f, 1.0f);
315 const float covNeg = std::clamp((-sd) * 8.0f + 0.5f, 0.0f, 1.0f);
316 const uint8_t aPos =
static_cast<uint8_t
>(std::lround(covPos * 255.0f));
317 const uint8_t aNeg =
static_cast<uint8_t
>(std::lround(covNeg * 255.0f));
320 if (aPos > 10 && aPos < 245) {
323 if (aNeg > 10 && aNeg < 245) {
327 const bool useNeg = midCountNeg > midCountPos;
328 for (
size_t i = 0; i < pixelCount; ++i) {
329 pixels[i * 4u + 0u] = 255;
330 pixels[i * 4u + 1u] = 255;
331 pixels[i * 4u + 2u] = 255;
332 pixels[i * 4u + 3u] = useNeg ? alphaNeg[i] : alphaPos[i];
335 "Font atlas '{}' detected as SDF/MSDF; baked {} signed-distance coverage to alpha (mid-pos={}, mid-neg={}).",
337 useNeg ?
"negative" :
"positive",
346 for (
size_t i = 0; i < pixelCount; ++i) {
347 const uint8_t a = pixels[i * 4u + 3u];
348 minA = std::min(minA, a);
349 maxA = std::max(maxA, a);
352 for (
size_t i = 0; i < pixelCount; ++i) {
353 const uint8_t r = pixels[i * 4u + 0u];
354 const uint8_t g = pixels[i * 4u + 1u];
355 const uint8_t b = pixels[i * 4u + 2u];
356 const uint8_t cov = std::max(r, std::max(g, b));
357 pixels[i * 4u + 0u] = 255;
358 pixels[i * 4u + 1u] = 255;
359 pixels[i * 4u + 2u] = 255;
360 pixels[i * 4u + 3u] = cov;
362 spdlog::info(
"Font atlas '{}' had flat alpha; applied RGB->alpha bitmap fallback.", atlasPath);
365 uint8_t outMinA = 255;
367 for (
size_t i = 0; i < pixelCount; ++i) {
368 const uint8_t a = pixels[i * 4u + 3u];
369 outMinA = std::min(outMinA, a);
370 outMaxA = std::max(outMaxA, a);
372 spdlog::info(
"Font atlas '{}' output alpha range: {}..{}", atlasPath, outMinA, outMaxA);
375 options.
width =
static_cast<uint32_t
>(w);
376 options.
height =
static_cast<uint32_t
>(h);
382 options.
name =
"font-atlas";
384 auto* texture =
new Texture(graphicsDevice.get(), options);
386 const size_t dataSize = pixelCount * 4u;
387 texture->setLevelData(0,
reinterpret_cast<const uint8_t*
>(pixels), dataSize);
389 stbi_image_free(pixels);
391 font->texture = texture;
392 if (font->atlasWidth <= 0) font->atlasWidth = w;
393 if (font->atlasHeight <= 0) font->atlasHeight = h;
394 if (font->lineHeight <= 0.0f) font->lineHeight = 64.0f;
395 spdlog::info(
"Loaded bitmap font '{}': atlas={}x{}, glyphs={}, kerning={}",
396 jsonPath, font->atlasWidth, font->atlasHeight, font->glyphs.size(), font->kerning.size());