diff --git a/README.md b/README.md index c59682d..8cd003a 100644 --- a/README.md +++ b/README.md @@ -254,3 +254,4 @@ If neither variable is set, the workflow falls back to the repo-local defaults u - Continue source cleanup/refactoring. Pass 1 done - Support a separate sound shader `.slang` file in shader packages. - Add WebView2 +- move to MSDF, typography rasterisation \ No newline at end of file diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/TextRasterizer.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/TextRasterizer.cpp index f633885..b5e11c9 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/TextRasterizer.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/TextRasterizer.cpp @@ -13,9 +13,9 @@ namespace { constexpr int kTextSdfSpread = 20; -constexpr unsigned kTextSdfBlurPasses = 1; constexpr float kTextFontPixelSize = 144.0f; constexpr float kTextLayoutPadding = 48.0f; +constexpr float kSdfInfinity = 1.0e20f; class GdiplusSession { @@ -54,128 +54,115 @@ std::wstring Utf8ToWide(const std::string& text) return wide; } -std::vector BuildLocalSdf(const std::vector& alpha, unsigned width, unsigned height) +void DistanceTransform1D(const std::vector& input, std::vector& output, unsigned count) { - std::vector sdf(static_cast(width) * height * 4, 0); + std::vector locations(count, 0); + std::vector boundaries(static_cast(count) + 1, 0.0f); + + unsigned segment = 0; + locations[0] = 0; + boundaries[0] = -kSdfInfinity; + boundaries[1] = kSdfInfinity; + + for (unsigned q = 1; q < count; ++q) + { + float intersection = 0.0f; + for (;;) + { + const unsigned location = locations[segment]; + intersection = + ((input[q] + static_cast(q * q)) - (input[location] + static_cast(location * location))) / + (2.0f * static_cast(q) - 2.0f * static_cast(location)); + if (intersection > boundaries[segment] || segment == 0) + break; + --segment; + } + + ++segment; + locations[segment] = q; + boundaries[segment] = intersection; + boundaries[segment + 1] = kSdfInfinity; + } + + segment = 0; + for (unsigned q = 0; q < count; ++q) + { + while (boundaries[segment + 1] < static_cast(q)) + ++segment; + const unsigned location = locations[segment]; + const float delta = static_cast(q) - static_cast(location); + output[q] = delta * delta + input[location]; + } +} + +std::vector DistanceTransform2D(const std::vector& targetMask, unsigned width, unsigned height) +{ + std::vector rowInput(width, 0.0f); + std::vector rowOutput(width, 0.0f); + std::vector columnInput(height, 0.0f); + std::vector columnOutput(height, 0.0f); + std::vector rowDistance(static_cast(width) * height, 0.0f); + std::vector distance(static_cast(width) * height, 0.0f); + 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; - } - } + rowInput[x] = targetMask[static_cast(y) * width + x] ? 0.0f : kSdfInfinity; + DistanceTransform1D(rowInput, rowOutput, width); + for (unsigned x = 0; x < width; ++x) + rowDistance[static_cast(y) * width + x] = rowOutput[x]; + } - 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; + for (unsigned x = 0; x < width; ++x) + { + for (unsigned y = 0; y < height; ++y) + columnInput[y] = rowDistance[static_cast(y) * width + x]; + DistanceTransform1D(columnInput, columnOutput, height); + for (unsigned y = 0; y < height; ++y) + distance[static_cast(y) * width + x] = columnOutput[y]; + } + + return distance; +} + +std::vector BuildTextSdfTexture(const std::vector& alpha, unsigned width, unsigned height) +{ + std::vector insideMask(static_cast(width) * height, 0); + std::vector outsideMask(static_cast(width) * height, 0); + for (std::size_t index = 0; index < alpha.size(); ++index) + { + const bool inside = alpha[index] > 127; + insideMask[index] = inside ? 1 : 0; + outsideMask[index] = inside ? 0 : 1; + } + + const std::vector distanceToInside = DistanceTransform2D(insideMask, width, height); + const std::vector distanceToOutside = DistanceTransform2D(outsideMask, width, height); + std::vector sdf(static_cast(width) * height * 4, 0); + + for (unsigned y = 0; y < height; ++y) + { + const unsigned flippedY = height - 1 - y; + for (unsigned x = 0; x < width; ++x) + { + const std::size_t source = static_cast(y) * width + x; + const float signedDistance = std::sqrt(distanceToOutside[source]) - std::sqrt(distanceToInside[source]); + const float normalized = std::clamp( + 0.5f + signedDistance / static_cast(kTextSdfSpread * 2), + 0.0f, + 1.0f); const unsigned char value = static_cast(normalized * 255.0f + 0.5f); - const std::size_t out = (static_cast(y) * width + x) * 4; + const std::size_t out = (static_cast(flippedY) * 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 @@ -308,9 +295,7 @@ bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& font 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); + sdf = BuildTextSdfTexture(alpha, kTextTextureWidth, kTextTextureHeight); WriteTextMaskDebugDump(text, alpha, sdf, kTextTextureWidth, kTextTextureHeight); return true; } diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/TextRasterizer.h b/apps/LoopThroughWithOpenGLCompositing/gl/TextRasterizer.h index 284249c..577ca67 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/TextRasterizer.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/TextRasterizer.h @@ -4,7 +4,7 @@ #include #include -constexpr unsigned kTextTextureWidth = 2048; -constexpr unsigned kTextTextureHeight = 256; +constexpr unsigned kTextTextureWidth = 4096; +constexpr unsigned kTextTextureHeight = 512; bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& fontPath, std::vector& sdf, std::string& error); diff --git a/shaders/text-overlay/shader.json b/shaders/text-overlay/shader.json index e5b4e52..34daad2 100644 --- a/shaders/text-overlay/shader.json +++ b/shaders/text-overlay/shader.json @@ -34,7 +34,7 @@ "type": "float", "default": 0.42, "min": 0.1, - "max": 1.5, + "max": 3, "step": 0.01 }, { diff --git a/shaders/text-overlay/shader.slang b/shaders/text-overlay/shader.slang index 1f627cc..7000c80 100644 --- a/shaders/text-overlay/shader.slang +++ b/shaders/text-overlay/shader.slang @@ -10,6 +10,12 @@ float4 compositeOver(float4 baseColor, float4 overColor) return float4(outRgb, outAlpha); } +float sdfCoverage(float2 uv, float edge, float aa) +{ + float distance = sampleTitleText(uv); + return smoothstep(edge - aa, edge + aa, distance); +} + float4 shadeVideo(ShaderContext context) { float2 resolution = max(context.outputResolution, float2(1.0, 1.0)); @@ -19,21 +25,26 @@ float4 shadeVideo(ShaderContext context) float2 textUv = (context.uv - position) / safeTextSize; bool insideTextRect = textUv.x >= 0.0 && textUv.x <= 1.0 && textUv.y >= 0.0 && textUv.y <= 1.0; - float mask = insideTextRect ? sampleTitleText(textUv) : 0.0; - float edge = 0.02; - float aa = max(fwidth(mask) * 1.5, 0.002); - float fill = smoothstep(edge - aa, edge + aa, mask); - float shadowRadius = min((outlineWidth + softness) * 0.025, 0.018); - float shadow = 0.0; - if (shadowRadius > 0.0001) - { - shadow = max(shadow, sampleTitleText(textUv + float2(shadowRadius, shadowRadius))); - shadow = max(shadow, sampleTitleText(textUv + float2(-shadowRadius, shadowRadius))); - shadow = max(shadow, sampleTitleText(textUv + float2(shadowRadius, -shadowRadius))); - shadow = max(shadow, sampleTitleText(textUv + float2(-shadowRadius, -shadowRadius))); - } - shadow = smoothstep(edge - aa, edge + aa, shadow) * (0.35 + softness); - float outlineAlpha = saturate(shadow * (1.0 - fill)) * outlineColor.a; + float distance = insideTextRect ? sampleTitleText(textUv) : 0.0; + float edge = 0.5; + float aa = max(fwidth(distance) * (1.75 + softness * 5.0), 0.0025); + float2 pixelTextUv = (1.0 / resolution) / safeTextSize; + float2 sampleOffset = pixelTextUv * 0.38; + 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; + float outlineAlpha = saturate(outline - fill) * outlineColor.a; float fillAlpha = fill * fillColor.a; float textAlpha = max(fillAlpha, outlineAlpha); if (textAlpha <= 0.0001)