#include "TextRasterizer.h" #include #include #include #include #include #include #include #include namespace { constexpr int kTextSdfSpread = 20; constexpr unsigned kTextSdfBlurPasses = 1; constexpr float kTextFontPixelSize = 144.0f; constexpr float kTextLayoutPadding = 48.0f; class GdiplusSession { public: GdiplusSession() { Gdiplus::GdiplusStartupInput startupInput; mStarted = Gdiplus::GdiplusStartup(&mToken, &startupInput, NULL) == Gdiplus::Ok; } ~GdiplusSession() { if (mStarted) Gdiplus::GdiplusShutdown(mToken); } GdiplusSession(const GdiplusSession&) = delete; GdiplusSession& operator=(const GdiplusSession&) = delete; bool started() const { return mStarted; } private: ULONG_PTR mToken = 0; bool mStarted = false; }; std::wstring Utf8ToWide(const std::string& text) { if (text.empty()) return std::wstring(); const int required = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, NULL, 0); if (required <= 1) return std::wstring(); std::wstring wide(static_cast(required - 1), L'\0'); MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, wide.data(), required); return wide; } std::vector BuildLocalSdf(const std::vector& alpha, unsigned width, unsigned height) { std::vector sdf(static_cast(width) * height * 4, 0); for (unsigned y = 0; y < height; ++y) { for (unsigned x = 0; x < width; ++x) { const bool inside = alpha[static_cast(y) * width + x] > 127; int bestDistanceSq = kTextSdfSpread * kTextSdfSpread; for (int oy = -kTextSdfSpread; oy <= kTextSdfSpread; ++oy) { const int sy = static_cast(y) + oy; if (sy < 0 || sy >= static_cast(height)) continue; for (int ox = -kTextSdfSpread; ox <= kTextSdfSpread; ++ox) { const int sx = static_cast(x) + ox; if (sx < 0 || sx >= static_cast(width)) continue; const bool sampleInside = alpha[static_cast(sy) * width + sx] > 127; if (sampleInside == inside) continue; const int distanceSq = ox * ox + oy * oy; if (distanceSq < bestDistanceSq) bestDistanceSq = distanceSq; } } const float distance = std::sqrt(static_cast(bestDistanceSq)); const float signedDistance = (inside ? 1.0f : -1.0f) * distance; float normalized = 0.5f + signedDistance / static_cast(kTextSdfSpread * 2); const unsigned char sourceAlpha = alpha[static_cast(y) * width + x]; if (sourceAlpha > 0 && sourceAlpha < 255) normalized = static_cast(sourceAlpha) / 255.0f; if (normalized < 0.0f) normalized = 0.0f; if (normalized > 1.0f) normalized = 1.0f; const unsigned char value = static_cast(normalized * 255.0f + 0.5f); const std::size_t out = (static_cast(y) * width + x) * 4; sdf[out + 0] = value; sdf[out + 1] = value; sdf[out + 2] = value; sdf[out + 3] = value; } } return sdf; } std::vector BuildTextCoverageTexture(const std::vector& alpha, unsigned width, unsigned height) { std::vector coverage(static_cast(width) * height * 4, 0); for (unsigned y = 0; y < height; ++y) { for (unsigned x = 0; x < width; ++x) { const unsigned char value = alpha[static_cast(y) * width + x]; const std::size_t out = (static_cast(y) * width + x) * 4; coverage[out + 0] = value; coverage[out + 1] = value; coverage[out + 2] = value; coverage[out + 3] = value; } } return coverage; } std::vector FlipTextTextureForShaderUv(const std::vector& pixels, unsigned width, unsigned height) { std::vector flipped(pixels.size(), 0); const std::size_t stride = static_cast(width) * 4; for (unsigned y = 0; y < height; ++y) { const std::size_t srcOffset = static_cast(y) * stride; const std::size_t dstOffset = static_cast(height - 1 - y) * stride; std::memcpy(flipped.data() + dstOffset, pixels.data() + srcOffset, stride); } return flipped; } std::vector BlurTextSdf(const std::vector& pixels, unsigned width, unsigned height, unsigned passes) { std::vector current = pixels; std::vector next(pixels.size(), 0); for (unsigned pass = 0; pass < passes; ++pass) { for (unsigned y = 0; y < height; ++y) { for (unsigned x = 0; x < width; ++x) { unsigned weightedTotal = 0; unsigned weightSum = 0; for (int oy = -1; oy <= 1; ++oy) { const int sy = static_cast(y) + oy; if (sy < 0 || sy >= static_cast(height)) continue; for (int ox = -1; ox <= 1; ++ox) { const int sx = static_cast(x) + ox; if (sx < 0 || sx >= static_cast(width)) continue; const unsigned weight = (ox == 0 && oy == 0) ? 4u : ((ox == 0 || oy == 0) ? 2u : 1u); const std::size_t sample = (static_cast(sy) * width + sx) * 4; weightedTotal += static_cast(current[sample]) * weight; weightSum += weight; } } const unsigned char value = static_cast((weightedTotal + weightSum / 2) / weightSum); const std::size_t out = (static_cast(y) * width + x) * 4; next[out + 0] = value; next[out + 1] = value; next[out + 2] = value; next[out + 3] = value; } } current.swap(next); } return current; } void WriteTextMaskDebugDump(const std::string& text, const std::vector& alpha, const std::vector& sdf, unsigned width, unsigned height) { try { std::filesystem::path debugDir = std::filesystem::current_path() / "runtime"; std::filesystem::create_directories(debugDir); auto writePgm = [width, height](const std::filesystem::path& path, const std::vector& gray, std::size_t stride) { std::ofstream out(path, std::ios::binary); if (!out) return; out << "P5\n" << width << " " << height << "\n255\n"; for (unsigned y = 0; y < height; ++y) { for (unsigned x = 0; x < width; ++x) out.put(static_cast(gray[(static_cast(y) * width + x) * stride])); } }; writePgm(debugDir / "text-mask-alpha-debug.pgm", alpha, 1); writePgm(debugDir / "text-mask-sdf-debug.pgm", sdf, 4); unsigned alphaMin = 255; unsigned alphaMax = 0; unsigned sdfMin = 255; unsigned sdfMax = 0; std::size_t alphaLit = 0; std::size_t sdfLit = 0; for (unsigned char value : alpha) { alphaMin = std::min(alphaMin, value); alphaMax = std::max(alphaMax, value); if (value > 0) ++alphaLit; } for (std::size_t index = 0; index < sdf.size(); index += 4) { const unsigned char value = sdf[index]; sdfMin = std::min(sdfMin, value); sdfMax = std::max(sdfMax, value); if (value > 127) ++sdfLit; } std::ostringstream message; message << "Text mask debug for '" << text << "': alpha min/max/lit=" << alphaMin << "/" << alphaMax << "/" << alphaLit << ", sdf min/max/gt127=" << sdfMin << "/" << sdfMax << "/" << sdfLit << "\n"; OutputDebugStringA(message.str().c_str()); } catch (...) { OutputDebugStringA("Failed to write text mask debug dump.\n"); } } } bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& fontPath, std::vector& sdf, std::string& error) { GdiplusSession gdiplus; if (!gdiplus.started()) { error = "Could not start GDI+ for text rendering."; return false; } Gdiplus::PrivateFontCollection fontCollection; Gdiplus::FontFamily fallbackFamily(L"Arial"); Gdiplus::FontFamily* fontFamily = &fallbackFamily; std::unique_ptr families; const std::wstring wideFontPath = fontPath.empty() ? std::wstring() : fontPath.wstring(); if (!wideFontPath.empty()) { if (fontCollection.AddFontFile(wideFontPath.c_str()) != Gdiplus::Ok) { error = "Could not load packaged font file for text rendering: " + fontPath.string(); return false; } const INT familyCount = fontCollection.GetFamilyCount(); if (familyCount <= 0) { error = "Packaged font did not contain a usable font family: " + fontPath.string(); return false; } families.reset(new Gdiplus::FontFamily[familyCount]); INT found = 0; if (fontCollection.GetFamilies(familyCount, families.get(), &found) != Gdiplus::Ok || found <= 0) { error = "Could not read the packaged font family: " + fontPath.string(); return false; } fontFamily = &families[0]; } Gdiplus::Bitmap bitmap(kTextTextureWidth, kTextTextureHeight, PixelFormat32bppARGB); Gdiplus::Graphics graphics(&bitmap); graphics.SetCompositingMode(Gdiplus::CompositingModeSourceCopy); graphics.Clear(Gdiplus::Color(255, 0, 0, 0)); graphics.SetCompositingMode(Gdiplus::CompositingModeSourceOver); graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAlias); graphics.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality); Gdiplus::Font font(fontFamily, kTextFontPixelSize, Gdiplus::FontStyleRegular, Gdiplus::UnitPixel); Gdiplus::SolidBrush brush(Gdiplus::Color(255, 255, 255, 255)); Gdiplus::StringFormat format; format.SetAlignment(Gdiplus::StringAlignmentNear); format.SetLineAlignment(Gdiplus::StringAlignmentCenter); format.SetFormatFlags(Gdiplus::StringFormatFlagsNoWrap | Gdiplus::StringFormatFlagsMeasureTrailingSpaces); const Gdiplus::RectF layout( kTextLayoutPadding, 0.0f, static_cast(kTextTextureWidth) - (kTextLayoutPadding * 2.0f), static_cast(kTextTextureHeight)); const std::wstring wideText = Utf8ToWide(text); graphics.DrawString(wideText.c_str(), -1, &font, layout, &format, &brush); std::vector alpha(static_cast(kTextTextureWidth) * kTextTextureHeight, 0); for (unsigned y = 0; y < kTextTextureHeight; ++y) { for (unsigned x = 0; x < kTextTextureWidth; ++x) { Gdiplus::Color pixel; bitmap.GetPixel(x, y, &pixel); BYTE luminance = pixel.GetRed(); if (pixel.GetGreen() > luminance) luminance = pixel.GetGreen(); if (pixel.GetBlue() > luminance) luminance = pixel.GetBlue(); alpha[static_cast(y) * kTextTextureWidth + x] = static_cast(luminance); } } sdf = BuildTextCoverageTexture(alpha, kTextTextureWidth, kTextTextureHeight); sdf = BlurTextSdf(sdf, kTextTextureWidth, kTextTextureHeight, kTextSdfBlurPasses); sdf = FlipTextTextureForShaderUv(sdf, kTextTextureWidth, kTextTextureHeight); WriteTextMaskDebugDump(text, alpha, sdf, kTextTextureWidth, kTextTextureHeight); return true; }