From df0a77ef0145ca2acea3aefe84cb100bacbb3e4b Mon Sep 17 00:00:00 2001 From: Aiden Date: Thu, 21 May 2026 15:39:37 +1000 Subject: [PATCH] Text rendering --- shaders/text-overlay/shader.json | 10 +-- shaders/text-overlay/shader.slang | 30 +++++---- src/render/runtime/RuntimeShaderParams.cpp | 8 ++- .../runtime/RuntimeTextTextureCache.cpp | 67 +++++++++++++------ src/render/runtime/RuntimeTextTextureCache.h | 4 +- src/runtime/FontAtlasBuilder.h | 4 +- src/runtime/RuntimeShaderArtifact.h | 8 ++- src/shader/ShaderCompiler.cpp | 28 ++++++-- 8 files changed, 107 insertions(+), 52 deletions(-) diff --git a/shaders/text-overlay/shader.json b/shaders/text-overlay/shader.json index 3831fa9..d78d8af 100644 --- a/shaders/text-overlay/shader.json +++ b/shaders/text-overlay/shader.json @@ -46,7 +46,7 @@ "id": "scale", "label": "Scale", "type": "float", - "default": 0.42, + "default": 1, "min": 0.1, "max": 3, "step": 0.01, @@ -72,7 +72,7 @@ 0, 0, 0, - 0.8 + 1 ], "description": "Text outline color and alpha." }, @@ -80,10 +80,10 @@ "id": "outlineWidth", "label": "Outline Width", "type": "float", - "default": 0.12, + "default": 0.22, "min": 0, - "max": 0.5, - "step": 0.01, + "max": 1, + "step": 0.02, "description": "Width of the SDF outline around the text." }, { diff --git a/shaders/text-overlay/shader.slang b/shaders/text-overlay/shader.slang index 7000c80..3434c7b 100644 --- a/shaders/text-overlay/shader.slang +++ b/shaders/text-overlay/shader.slang @@ -16,11 +16,18 @@ float sdfCoverage(float2 uv, float edge, float aa) return smoothstep(edge - aa, edge + aa, distance); } +float coverage(float distance, float edge, float aa) +{ + return smoothstep(edge - aa, edge + aa, distance); +} + float4 shadeVideo(ShaderContext context) { float2 resolution = max(context.outputResolution, float2(1.0, 1.0)); float aspect = resolution.x / resolution.y; - float2 textSize = float2(0.72 * scale, 0.09 * scale * aspect); + float textHeight = 0.09 * scale * aspect; + float textWidth = textHeight * max(titleTextTextureAspect, 0.01) / aspect; + float2 textSize = float2(textWidth, textHeight); float2 safeTextSize = max(textSize, float2(0.0001, 0.0001)); float2 textUv = (context.uv - position) / safeTextSize; bool insideTextRect = textUv.x >= 0.0 && textUv.x <= 1.0 && textUv.y >= 0.0 && textUv.y <= 1.0; @@ -30,20 +37,15 @@ float4 shadeVideo(ShaderContext context) float aa = max(fwidth(distance) * (1.75 + softness * 5.0), 0.0025); float2 pixelTextUv = (1.0 / resolution) / safeTextSize; float2 sampleOffset = pixelTextUv * 0.38; + float msdfDistance = sampleTitleTextMsdf(textUv); float fill = ( - sdfCoverage(textUv, edge, aa) * 2.0 + - sdfCoverage(textUv + float2(sampleOffset.x, sampleOffset.y), edge, aa) + - sdfCoverage(textUv + float2(-sampleOffset.x, sampleOffset.y), edge, aa) + - sdfCoverage(textUv + float2(sampleOffset.x, -sampleOffset.y), edge, aa) + - sdfCoverage(textUv + float2(-sampleOffset.x, -sampleOffset.y), edge, aa)) / 6.0; - float outlineDistance = outlineWidth * 0.16; - float outlineEdge = edge - outlineDistance; - float outline = ( - sdfCoverage(textUv, outlineEdge, aa) * 2.0 + - sdfCoverage(textUv + float2(sampleOffset.x, sampleOffset.y), outlineEdge, aa) + - sdfCoverage(textUv + float2(-sampleOffset.x, sampleOffset.y), outlineEdge, aa) + - sdfCoverage(textUv + float2(sampleOffset.x, -sampleOffset.y), outlineEdge, aa) + - sdfCoverage(textUv + float2(-sampleOffset.x, -sampleOffset.y), outlineEdge, aa)) / 6.0; + coverage(msdfDistance, edge, aa) * 2.0 + + coverage(sampleTitleTextMsdf(textUv + float2(sampleOffset.x, sampleOffset.y)), edge, aa) + + coverage(sampleTitleTextMsdf(textUv + float2(-sampleOffset.x, sampleOffset.y)), edge, aa) + + coverage(sampleTitleTextMsdf(textUv + float2(sampleOffset.x, -sampleOffset.y)), edge, aa) + + coverage(sampleTitleTextMsdf(textUv + float2(-sampleOffset.x, -sampleOffset.y)), edge, aa)) / 6.0; + float outlineEdge = edge - min(outlineWidth * 0.7, 0.48); + float outline = coverage(distance, outlineEdge, aa); float outlineAlpha = saturate(outline - fill) * outlineColor.a; float fillAlpha = fill * fillColor.a; float textAlpha = max(fillAlpha, outlineAlpha); diff --git a/src/render/runtime/RuntimeShaderParams.cpp b/src/render/runtime/RuntimeShaderParams.cpp index b88eabc..5ad2d80 100644 --- a/src/render/runtime/RuntimeShaderParams.cpp +++ b/src/render/runtime/RuntimeShaderParams.cpp @@ -111,8 +111,12 @@ std::vector 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: diff --git a/src/render/runtime/RuntimeTextTextureCache.cpp b/src/render/runtime/RuntimeTextTextureCache.cpp index cce3603..07e02d8 100644 --- a/src/render/runtime/RuntimeTextTextureCache.cpp +++ b/src/render/runtime/RuntimeTextTextureCache.cpp @@ -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(textTexture.liveWidth) / static_cast(textTexture.width); - artifactState->textTextureWidthScales[textTexture.parameterId] = scale; + metrics.aspect = textTexture.height == 0 + ? 1.0f + : static_cast(textTexture.liveWidth) / static_cast(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 pixels = ComposeTextMask(*atlas, texture, text, width, height, liveWidth); + std::vector 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(width), static_cast(height), 0, GL_RED, GL_UNSIGNED_BYTE, pixels.data()); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast(width), static_cast(height), 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); } else { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast(width), static_cast(height), GL_RED, GL_UNSIGNED_BYTE, pixels.data()); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast(width), static_cast(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 RuntimeTextTextureCache::ComposeTextMask(const Atlas& atlas, const TextTexture& texture, const std::string& text, unsigned& width, unsigned& height, unsigned& liveWidth) const +std::vector 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 RuntimeTextTextureCache::ComposeTextMask(const Atlas& liveWidth = (std::max)(1u, static_cast(std::ceil(advance * kFontPixelsPerEm)) + kTextTexturePadding * 2u); width = (std::max)(fixedWidth, liveWidth); height = kTextTextureHeight; - std::vector mask(static_cast(width) * height, 0); + std::vector texturePixels(static_cast(width) * height * 4u, 0); const double baseline = kTextTexturePadding + (-atlas.ascender * kFontPixelsPerEm); double penX = static_cast(kTextTexturePadding); @@ -417,8 +421,11 @@ std::vector RuntimeTextTextureCache::ComposeTextMask(const Atlas& const double u = (static_cast(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(y) * width + static_cast(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(y) * width + static_cast(x)) * 4u; + for (unsigned channel = 0; channel < 4u; ++channel) + destination[channel] = (std::max)(destination[channel], sample[channel]); } } } @@ -428,13 +435,13 @@ std::vector RuntimeTextTextureCache::ComposeTextMask(const Atlas& for (unsigned y = 0; y < height / 2u; ++y) { - unsigned char* topRow = mask.data() + static_cast(y) * width; - unsigned char* bottomRow = mask.data() + static_cast(height - 1u - y) * width; - for (unsigned x = 0; x < width; ++x) + unsigned char* topRow = texturePixels.data() + static_cast(y) * width * 4u; + unsigned char* bottomRow = texturePixels.data() + static_cast(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(atlas.width) - 1, static_cast(std::floor(x)))); - const int iy = (std::max)(0, (std::min)(static_cast(atlas.height) - 1, static_cast(std::floor(y)))); - const std::size_t pixelOffset = (static_cast(iy) * atlas.width + static_cast(ix)) * 4u; - return atlas.rgbaPixels[pixelOffset + 3u]; + const double clampedX = (std::max)(0.0, (std::min)(static_cast(atlas.width) - 1.0, x)); + const double clampedY = (std::max)(0.0, (std::min)(static_cast(atlas.height) - 1.0, y)); + const int x0 = static_cast(std::floor(clampedX)); + const int y0 = static_cast(std::floor(clampedY)); + const int x1 = (std::min)(static_cast(atlas.width) - 1, x0 + 1); + const int y1 = (std::min)(static_cast(atlas.height) - 1, y0 + 1); + const double tx = clampedX - static_cast(x0); + const double ty = clampedY - static_cast(y0); + + const auto channelAt = [&atlas](int sx, int sy, unsigned channel) { + const std::size_t pixelOffset = (static_cast(sy) * atlas.width + static_cast(sx)) * 4u; + return static_cast(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((std::max)(0.0, (std::min)(255.0, std::round(value)))); + } } void RuntimeTextTextureCache::DestroyTexture(TextTexture& texture) diff --git a/src/render/runtime/RuntimeTextTextureCache.h b/src/render/runtime/RuntimeTextTextureCache.h index e1b75ae..9ecf761 100644 --- a/src/render/runtime/RuntimeTextTextureCache.h +++ b/src/render/runtime/RuntimeTextTextureCache.h @@ -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 ComposeTextMask(const Atlas& atlas, const TextTexture& texture, const std::string& text, unsigned& width, unsigned& height, unsigned& liveWidth) const; + std::vector 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; diff --git a/src/runtime/FontAtlasBuilder.h b/src/runtime/FontAtlasBuilder.h index eca5726..7160c42 100644 --- a/src/runtime/FontAtlasBuilder.h +++ b/src/runtime/FontAtlasBuilder.h @@ -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"; }; diff --git a/src/runtime/RuntimeShaderArtifact.h b/src/runtime/RuntimeShaderArtifact.h index 65a8bb2..192ed37 100644 --- a/src/runtime/RuntimeShaderArtifact.h +++ b/src/runtime/RuntimeShaderArtifact.h @@ -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 parameterDefinitions; std::map parameterValues; - std::map textTextureWidthScales; + std::map textTextureMetrics; std::vector fontAtlases; }; diff --git a/src/shader/ShaderCompiler.cpp b/src/shader/ShaderCompiler.cpp index 4152b34..f87834b 100644 --- a/src/shader/ShaderCompiler.cpp +++ b/src/shader/ShaderCompiler.cpp @@ -53,7 +53,8 @@ std::string BuildParameterUniforms(const std::vector& { 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& 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"