Text rendering
Some checks failed
CI / React UI Build (push) Successful in 40s
CI / Native Windows Build And Tests (push) Failing after 2m28s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
2026-05-21 15:39:37 +10:00
parent 09efe2d6a0
commit df0a77ef01
8 changed files with 107 additions and 52 deletions

View File

@@ -111,8 +111,12 @@ std::vector<unsigned char> BuildRuntimeShaderGlobalParamsStd140(
break;
case ShaderParameterType::Text:
{
const auto scaleIt = artifact.textTextureWidthScales.find(definition.id);
AppendStd140Float(buffer, scaleIt == artifact.textTextureWidthScales.end() ? 1.0f : scaleIt->second);
const auto metricsIt = artifact.textTextureMetrics.find(definition.id);
const RuntimeTextTextureMetrics metrics = metricsIt == artifact.textTextureMetrics.end()
? RuntimeTextTextureMetrics()
: metricsIt->second;
AppendStd140Float(buffer, metrics.activeWidthScale);
AppendStd140Float(buffer, metrics.aspect);
break;
}
case ShaderParameterType::Trigger:

View File

@@ -12,9 +12,9 @@
namespace
{
constexpr GLuint kFirstTextTextureUnit = 8;
constexpr unsigned kTextTextureHeight = 128;
constexpr unsigned kTextTexturePadding = 8;
constexpr double kFontPixelsPerEm = 96.0;
constexpr unsigned kTextTextureHeight = 256;
constexpr unsigned kTextTexturePadding = 16;
constexpr double kFontPixelsPerEm = 192.0;
std::string ReadTextFile(const std::filesystem::path& path)
{
@@ -105,16 +105,20 @@ void RuntimeTextTextureCache::UpdateArtifactState(const RuntimeShaderArtifact& a
void RuntimeTextTextureCache::RefreshTextTextures(RuntimeShaderArtifact* artifactState)
{
if (artifactState)
artifactState->textTextureWidthScales.clear();
artifactState->textTextureMetrics.clear();
for (TextTexture& textTexture : mTextTextures)
{
EnsureTextTexture(textTexture);
if (artifactState)
{
const float scale = textTexture.width == 0
RuntimeTextTextureMetrics metrics;
metrics.activeWidthScale = textTexture.width == 0
? 1.0f
: static_cast<float>(textTexture.liveWidth) / static_cast<float>(textTexture.width);
artifactState->textTextureWidthScales[textTexture.parameterId] = scale;
metrics.aspect = textTexture.height == 0
? 1.0f
: static_cast<float>(textTexture.liveWidth) / static_cast<float>(textTexture.height);
artifactState->textTextureMetrics[textTexture.parameterId] = metrics;
}
}
}
@@ -336,7 +340,7 @@ bool RuntimeTextTextureCache::EnsureTextTexture(TextTexture& texture)
unsigned width = 0;
unsigned height = 0;
unsigned liveWidth = 1;
std::vector<unsigned char> pixels = ComposeTextMask(*atlas, texture, text, width, height, liveWidth);
std::vector<unsigned char> pixels = ComposeTextTexture(*atlas, texture, text, width, height, liveWidth);
if (pixels.empty() || width == 0 || height == 0)
return false;
@@ -353,11 +357,11 @@ bool RuntimeTextTextureCache::EnsureTextTexture(TextTexture& texture)
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);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, static_cast<GLsizei>(width), static_cast<GLsizei>(height), 0, GL_RED, GL_UNSIGNED_BYTE, pixels.data());
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast<GLsizei>(width), static_cast<GLsizei>(height), 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
}
else
{
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height), GL_RED, GL_UNSIGNED_BYTE, pixels.data());
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height), GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
}
glBindTexture(GL_TEXTURE_2D, 0);
glPixelStorei(GL_UNPACK_ALIGNMENT, previousUnpackAlignment);
@@ -370,7 +374,7 @@ bool RuntimeTextTextureCache::EnsureTextTexture(TextTexture& texture)
return texture.texture != 0;
}
std::vector<unsigned char> RuntimeTextTextureCache::ComposeTextMask(const Atlas& atlas, const TextTexture& texture, const std::string& text, unsigned& width, unsigned& height, unsigned& liveWidth) const
std::vector<unsigned char> RuntimeTextTextureCache::ComposeTextTexture(const Atlas& atlas, const TextTexture& texture, const std::string& text, unsigned& width, unsigned& height, unsigned& liveWidth) const
{
double advance = 0.0;
for (unsigned char character : text)
@@ -384,7 +388,7 @@ std::vector<unsigned char> RuntimeTextTextureCache::ComposeTextMask(const Atlas&
liveWidth = (std::max)(1u, static_cast<unsigned>(std::ceil(advance * kFontPixelsPerEm)) + kTextTexturePadding * 2u);
width = (std::max)(fixedWidth, liveWidth);
height = kTextTextureHeight;
std::vector<unsigned char> mask(static_cast<std::size_t>(width) * height, 0);
std::vector<unsigned char> texturePixels(static_cast<std::size_t>(width) * height * 4u, 0);
const double baseline = kTextTexturePadding + (-atlas.ascender * kFontPixelsPerEm);
double penX = static_cast<double>(kTextTexturePadding);
@@ -417,8 +421,11 @@ std::vector<unsigned char> RuntimeTextTextureCache::ComposeTextMask(const Atlas&
const double u = (static_cast<double>(x) + 0.5 - destLeft) / destWidth;
const double atlasX = glyph.atlasLeft + u * (glyph.atlasRight - glyph.atlasLeft);
const double atlasY = glyph.atlasTop + v * (glyph.atlasBottom - glyph.atlasTop);
unsigned char& destination = mask[static_cast<std::size_t>(y) * width + static_cast<std::size_t>(x)];
destination = (std::max)(destination, SampleAtlasAlpha(atlas, atlasX, atlasY));
unsigned char sample[4] = {};
SampleAtlasPixel(atlas, atlasX, atlasY, sample);
unsigned char* destination = texturePixels.data() + (static_cast<std::size_t>(y) * width + static_cast<std::size_t>(x)) * 4u;
for (unsigned channel = 0; channel < 4u; ++channel)
destination[channel] = (std::max)(destination[channel], sample[channel]);
}
}
}
@@ -428,13 +435,13 @@ std::vector<unsigned char> RuntimeTextTextureCache::ComposeTextMask(const Atlas&
for (unsigned y = 0; y < height / 2u; ++y)
{
unsigned char* topRow = mask.data() + static_cast<std::size_t>(y) * width;
unsigned char* bottomRow = mask.data() + static_cast<std::size_t>(height - 1u - y) * width;
for (unsigned x = 0; x < width; ++x)
unsigned char* topRow = texturePixels.data() + static_cast<std::size_t>(y) * width * 4u;
unsigned char* bottomRow = texturePixels.data() + static_cast<std::size_t>(height - 1u - y) * width * 4u;
for (unsigned x = 0; x < width * 4u; ++x)
std::swap(topRow[x], bottomRow[x]);
}
return mask;
return texturePixels;
}
const RuntimeTextTextureCache::Atlas* RuntimeTextTextureCache::FindAtlas(const std::string& fontId) const
@@ -463,12 +470,28 @@ std::string RuntimeTextTextureCache::DefaultTextValue(const RuntimeShaderArtifac
return std::string();
}
unsigned char RuntimeTextTextureCache::SampleAtlasAlpha(const Atlas& atlas, double x, double y)
void RuntimeTextTextureCache::SampleAtlasPixel(const Atlas& atlas, double x, double y, unsigned char* rgba)
{
const int ix = (std::max)(0, (std::min)(static_cast<int>(atlas.width) - 1, static_cast<int>(std::floor(x))));
const int iy = (std::max)(0, (std::min)(static_cast<int>(atlas.height) - 1, static_cast<int>(std::floor(y))));
const std::size_t pixelOffset = (static_cast<std::size_t>(iy) * atlas.width + static_cast<std::size_t>(ix)) * 4u;
return atlas.rgbaPixels[pixelOffset + 3u];
const double clampedX = (std::max)(0.0, (std::min)(static_cast<double>(atlas.width) - 1.0, x));
const double clampedY = (std::max)(0.0, (std::min)(static_cast<double>(atlas.height) - 1.0, y));
const int x0 = static_cast<int>(std::floor(clampedX));
const int y0 = static_cast<int>(std::floor(clampedY));
const int x1 = (std::min)(static_cast<int>(atlas.width) - 1, x0 + 1);
const int y1 = (std::min)(static_cast<int>(atlas.height) - 1, y0 + 1);
const double tx = clampedX - static_cast<double>(x0);
const double ty = clampedY - static_cast<double>(y0);
const auto channelAt = [&atlas](int sx, int sy, unsigned channel) {
const std::size_t pixelOffset = (static_cast<std::size_t>(sy) * atlas.width + static_cast<std::size_t>(sx)) * 4u;
return static_cast<double>(atlas.rgbaPixels[pixelOffset + channel]);
};
for (unsigned channel = 0; channel < 4u; ++channel)
{
const double top = channelAt(x0, y0, channel) * (1.0 - tx) + channelAt(x1, y0, channel) * tx;
const double bottom = channelAt(x0, y1, channel) * (1.0 - tx) + channelAt(x1, y1, channel) * tx;
const double value = top * (1.0 - ty) + bottom * ty;
rgba[channel] = static_cast<unsigned char>((std::max)(0.0, (std::min)(255.0, std::round(value))));
}
}
void RuntimeTextTextureCache::DestroyTexture(TextTexture& texture)

View File

@@ -66,11 +66,11 @@ private:
bool LoadAtlasJson(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const;
bool LoadAtlasImage(const RenderCadenceCompositor::FontAtlasBuildOutput& output, Atlas& atlas, std::string& error) const;
bool EnsureTextTexture(TextTexture& texture);
std::vector<unsigned char> ComposeTextMask(const Atlas& atlas, const TextTexture& texture, const std::string& text, unsigned& width, unsigned& height, unsigned& liveWidth) const;
std::vector<unsigned char> ComposeTextTexture(const Atlas& atlas, const TextTexture& texture, const std::string& text, unsigned& width, unsigned& height, unsigned& liveWidth) const;
const Atlas* FindAtlas(const std::string& fontId) const;
static const ShaderParameterValue* FindParameterValue(const RuntimeShaderArtifact& artifact, const std::string& parameterId);
static std::string DefaultTextValue(const RuntimeShaderArtifact& artifact, const std::string& parameterId);
static unsigned char SampleAtlasAlpha(const Atlas& atlas, double x, double y);
static void SampleAtlasPixel(const Atlas& atlas, double x, double y, unsigned char* rgba);
static void DestroyTexture(TextTexture& texture);
RuntimeShaderArtifact mArtifact;

View File

@@ -12,8 +12,8 @@ struct FontAtlasBuildConfig
{
std::filesystem::path repoRoot;
std::filesystem::path cacheRoot;
double sizePixelsPerEm = 64.0;
double pixelRange = 4.0;
double sizePixelsPerEm = 128.0;
double pixelRange = 8.0;
std::string atlasType = "mtsdf";
};

View File

@@ -15,6 +15,12 @@ struct RuntimeShaderPassArtifact
std::string outputName;
};
struct RuntimeTextTextureMetrics
{
float activeWidthScale = 1.0f;
float aspect = 1.0f;
};
struct RuntimeShaderArtifact
{
std::string layerId;
@@ -25,6 +31,6 @@ struct RuntimeShaderArtifact
std::string message;
std::vector<ShaderParameterDefinition> parameterDefinitions;
std::map<std::string, ShaderParameterValue> parameterValues;
std::map<std::string, float> textTextureWidthScales;
std::map<std::string, RuntimeTextTextureMetrics> textTextureMetrics;
std::vector<RenderCadenceCompositor::FontAtlasBuildOutput> fontAtlases;
};

View File

@@ -53,7 +53,8 @@ std::string BuildParameterUniforms(const std::vector<ShaderParameterDefinition>&
{
if (definition.type == ShaderParameterType::Text)
{
source << "\tfloat " << definition.id << "TextureWidthScale;\n";
source << "\tfloat " << definition.id << "TextureActiveWidthScale;\n";
source << "\tfloat " << definition.id << "TextureAspect;\n";
continue;
}
if (definition.type == ShaderParameterType::Trigger)
@@ -102,17 +103,36 @@ std::string BuildTextSamplerDeclarations(const std::vector<ShaderParameterDefini
std::string BuildTextHelpers(const std::vector<ShaderParameterDefinition>& parameters)
{
std::ostringstream source;
bool emittedMedian = false;
for (const ShaderParameterDefinition& definition : parameters)
{
if (definition.type != ShaderParameterType::Text)
continue;
if (!emittedMedian)
{
source
<< "float median(float r, float g, float b)\n"
<< "{\n"
<< "\treturn max(min(r, g), min(max(r, g), b));\n"
<< "}\n\n";
emittedMedian = true;
}
const std::string suffix = CapitalizeIdentifier(definition.id);
source
<< "float sample" << suffix << "(float2 uv)\n"
<< "float4 sample" << suffix << "Mtsdf(float2 uv)\n"
<< "{\n"
<< "\tif (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)\n"
<< "\t\treturn 0.0;\n"
<< "\treturn " << definition.id << "Texture.Sample(float2(uv.x * " << definition.id << "TextureWidthScale, uv.y)).r;\n"
<< "\t\treturn float4(0.0, 0.0, 0.0, 0.0);\n"
<< "\treturn " << definition.id << "Texture.Sample(float2(uv.x * " << definition.id << "TextureActiveWidthScale, uv.y));\n"
<< "}\n\n"
<< "float sample" << suffix << "Msdf(float2 uv)\n"
<< "{\n"
<< "\tfloat4 mtsdf = sample" << suffix << "Mtsdf(uv);\n"
<< "\treturn median(mtsdf.r, mtsdf.g, mtsdf.b);\n"
<< "}\n\n"
<< "float sample" << suffix << "(float2 uv)\n"
<< "{\n"
<< "\treturn sample" << suffix << "Mtsdf(uv).a;\n"
<< "}\n\n"
<< "float4 draw" << suffix << "(float2 uv, float4 fillColor)\n"
<< "{\n"