Typography improvements
This commit is contained in:
@@ -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
|
- Continue source cleanup/refactoring. Pass 1 done
|
||||||
- Support a separate sound shader `.slang` file in shader packages.
|
- Support a separate sound shader `.slang` file in shader packages.
|
||||||
- Add WebView2
|
- Add WebView2
|
||||||
|
- move to MSDF, typography rasterisation
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr int kTextSdfSpread = 20;
|
constexpr int kTextSdfSpread = 20;
|
||||||
constexpr unsigned kTextSdfBlurPasses = 1;
|
|
||||||
constexpr float kTextFontPixelSize = 144.0f;
|
constexpr float kTextFontPixelSize = 144.0f;
|
||||||
constexpr float kTextLayoutPadding = 48.0f;
|
constexpr float kTextLayoutPadding = 48.0f;
|
||||||
|
constexpr float kSdfInfinity = 1.0e20f;
|
||||||
|
|
||||||
class GdiplusSession
|
class GdiplusSession
|
||||||
{
|
{
|
||||||
@@ -54,128 +54,115 @@ std::wstring Utf8ToWide(const std::string& text)
|
|||||||
return wide;
|
return wide;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<unsigned char> BuildLocalSdf(const std::vector<unsigned char>& alpha, unsigned width, unsigned height)
|
void DistanceTransform1D(const std::vector<float>& input, std::vector<float>& output, unsigned count)
|
||||||
{
|
{
|
||||||
std::vector<unsigned char> sdf(static_cast<std::size_t>(width) * height * 4, 0);
|
std::vector<unsigned> locations(count, 0);
|
||||||
|
std::vector<float> boundaries(static_cast<std::size_t>(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<float>(q * q)) - (input[location] + static_cast<float>(location * location))) /
|
||||||
|
(2.0f * static_cast<float>(q) - 2.0f * static_cast<float>(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<float>(q))
|
||||||
|
++segment;
|
||||||
|
const unsigned location = locations[segment];
|
||||||
|
const float delta = static_cast<float>(q) - static_cast<float>(location);
|
||||||
|
output[q] = delta * delta + input[location];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<float> DistanceTransform2D(const std::vector<unsigned char>& targetMask, unsigned width, unsigned height)
|
||||||
|
{
|
||||||
|
std::vector<float> rowInput(width, 0.0f);
|
||||||
|
std::vector<float> rowOutput(width, 0.0f);
|
||||||
|
std::vector<float> columnInput(height, 0.0f);
|
||||||
|
std::vector<float> columnOutput(height, 0.0f);
|
||||||
|
std::vector<float> rowDistance(static_cast<std::size_t>(width) * height, 0.0f);
|
||||||
|
std::vector<float> distance(static_cast<std::size_t>(width) * height, 0.0f);
|
||||||
|
|
||||||
for (unsigned y = 0; y < height; ++y)
|
for (unsigned y = 0; y < height; ++y)
|
||||||
{
|
{
|
||||||
for (unsigned x = 0; x < width; ++x)
|
for (unsigned x = 0; x < width; ++x)
|
||||||
{
|
rowInput[x] = targetMask[static_cast<std::size_t>(y) * width + x] ? 0.0f : kSdfInfinity;
|
||||||
const bool inside = alpha[static_cast<std::size_t>(y) * width + x] > 127;
|
DistanceTransform1D(rowInput, rowOutput, width);
|
||||||
int bestDistanceSq = kTextSdfSpread * kTextSdfSpread;
|
for (unsigned x = 0; x < width; ++x)
|
||||||
for (int oy = -kTextSdfSpread; oy <= kTextSdfSpread; ++oy)
|
rowDistance[static_cast<std::size_t>(y) * width + x] = rowOutput[x];
|
||||||
{
|
|
||||||
const int sy = static_cast<int>(y) + oy;
|
|
||||||
if (sy < 0 || sy >= static_cast<int>(height))
|
|
||||||
continue;
|
|
||||||
for (int ox = -kTextSdfSpread; ox <= kTextSdfSpread; ++ox)
|
|
||||||
{
|
|
||||||
const int sx = static_cast<int>(x) + ox;
|
|
||||||
if (sx < 0 || sx >= static_cast<int>(width))
|
|
||||||
continue;
|
|
||||||
const bool sampleInside = alpha[static_cast<std::size_t>(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<float>(bestDistanceSq));
|
for (unsigned x = 0; x < width; ++x)
|
||||||
const float signedDistance = (inside ? 1.0f : -1.0f) * distance;
|
{
|
||||||
float normalized = 0.5f + signedDistance / static_cast<float>(kTextSdfSpread * 2);
|
for (unsigned y = 0; y < height; ++y)
|
||||||
const unsigned char sourceAlpha = alpha[static_cast<std::size_t>(y) * width + x];
|
columnInput[y] = rowDistance[static_cast<std::size_t>(y) * width + x];
|
||||||
if (sourceAlpha > 0 && sourceAlpha < 255)
|
DistanceTransform1D(columnInput, columnOutput, height);
|
||||||
normalized = static_cast<float>(sourceAlpha) / 255.0f;
|
for (unsigned y = 0; y < height; ++y)
|
||||||
if (normalized < 0.0f)
|
distance[static_cast<std::size_t>(y) * width + x] = columnOutput[y];
|
||||||
normalized = 0.0f;
|
}
|
||||||
if (normalized > 1.0f)
|
|
||||||
normalized = 1.0f;
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> BuildTextSdfTexture(const std::vector<unsigned char>& alpha, unsigned width, unsigned height)
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> insideMask(static_cast<std::size_t>(width) * height, 0);
|
||||||
|
std::vector<unsigned char> outsideMask(static_cast<std::size_t>(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<float> distanceToInside = DistanceTransform2D(insideMask, width, height);
|
||||||
|
const std::vector<float> distanceToOutside = DistanceTransform2D(outsideMask, width, height);
|
||||||
|
std::vector<unsigned char> sdf(static_cast<std::size_t>(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<std::size_t>(y) * width + x;
|
||||||
|
const float signedDistance = std::sqrt(distanceToOutside[source]) - std::sqrt(distanceToInside[source]);
|
||||||
|
const float normalized = std::clamp(
|
||||||
|
0.5f + signedDistance / static_cast<float>(kTextSdfSpread * 2),
|
||||||
|
0.0f,
|
||||||
|
1.0f);
|
||||||
const unsigned char value = static_cast<unsigned char>(normalized * 255.0f + 0.5f);
|
const unsigned char value = static_cast<unsigned char>(normalized * 255.0f + 0.5f);
|
||||||
const std::size_t out = (static_cast<std::size_t>(y) * width + x) * 4;
|
const std::size_t out = (static_cast<std::size_t>(flippedY) * width + x) * 4;
|
||||||
sdf[out + 0] = value;
|
sdf[out + 0] = value;
|
||||||
sdf[out + 1] = value;
|
sdf[out + 1] = value;
|
||||||
sdf[out + 2] = value;
|
sdf[out + 2] = value;
|
||||||
sdf[out + 3] = value;
|
sdf[out + 3] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sdf;
|
return sdf;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<unsigned char> BuildTextCoverageTexture(const std::vector<unsigned char>& alpha, unsigned width, unsigned height)
|
|
||||||
{
|
|
||||||
std::vector<unsigned char> coverage(static_cast<std::size_t>(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<std::size_t>(y) * width + x];
|
|
||||||
const std::size_t out = (static_cast<std::size_t>(y) * width + x) * 4;
|
|
||||||
coverage[out + 0] = value;
|
|
||||||
coverage[out + 1] = value;
|
|
||||||
coverage[out + 2] = value;
|
|
||||||
coverage[out + 3] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return coverage;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<unsigned char> FlipTextTextureForShaderUv(const std::vector<unsigned char>& pixels, unsigned width, unsigned height)
|
|
||||||
{
|
|
||||||
std::vector<unsigned char> flipped(pixels.size(), 0);
|
|
||||||
const std::size_t stride = static_cast<std::size_t>(width) * 4;
|
|
||||||
for (unsigned y = 0; y < height; ++y)
|
|
||||||
{
|
|
||||||
const std::size_t srcOffset = static_cast<std::size_t>(y) * stride;
|
|
||||||
const std::size_t dstOffset = static_cast<std::size_t>(height - 1 - y) * stride;
|
|
||||||
std::memcpy(flipped.data() + dstOffset, pixels.data() + srcOffset, stride);
|
|
||||||
}
|
|
||||||
return flipped;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<unsigned char> BlurTextSdf(const std::vector<unsigned char>& pixels, unsigned width, unsigned height, unsigned passes)
|
|
||||||
{
|
|
||||||
std::vector<unsigned char> current = pixels;
|
|
||||||
std::vector<unsigned char> 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<int>(y) + oy;
|
|
||||||
if (sy < 0 || sy >= static_cast<int>(height))
|
|
||||||
continue;
|
|
||||||
for (int ox = -1; ox <= 1; ++ox)
|
|
||||||
{
|
|
||||||
const int sx = static_cast<int>(x) + ox;
|
|
||||||
if (sx < 0 || sx >= static_cast<int>(width))
|
|
||||||
continue;
|
|
||||||
const unsigned weight = (ox == 0 && oy == 0) ? 4u : ((ox == 0 || oy == 0) ? 2u : 1u);
|
|
||||||
const std::size_t sample = (static_cast<std::size_t>(sy) * width + sx) * 4;
|
|
||||||
weightedTotal += static_cast<unsigned>(current[sample]) * weight;
|
|
||||||
weightSum += weight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsigned char value = static_cast<unsigned char>((weightedTotal + weightSum / 2) / weightSum);
|
|
||||||
const std::size_t out = (static_cast<std::size_t>(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<unsigned char>& alpha, const std::vector<unsigned char>& sdf, unsigned width, unsigned height)
|
void WriteTextMaskDebugDump(const std::string& text, const std::vector<unsigned char>& alpha, const std::vector<unsigned char>& sdf, unsigned width, unsigned height)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -308,9 +295,7 @@ bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& font
|
|||||||
alpha[static_cast<std::size_t>(y) * kTextTextureWidth + x] = static_cast<unsigned char>(luminance);
|
alpha[static_cast<std::size_t>(y) * kTextTextureWidth + x] = static_cast<unsigned char>(luminance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sdf = BuildTextCoverageTexture(alpha, kTextTextureWidth, kTextTextureHeight);
|
sdf = BuildTextSdfTexture(alpha, kTextTextureWidth, kTextTextureHeight);
|
||||||
sdf = BlurTextSdf(sdf, kTextTextureWidth, kTextTextureHeight, kTextSdfBlurPasses);
|
|
||||||
sdf = FlipTextTextureForShaderUv(sdf, kTextTextureWidth, kTextTextureHeight);
|
|
||||||
WriteTextMaskDebugDump(text, alpha, sdf, kTextTextureWidth, kTextTextureHeight);
|
WriteTextMaskDebugDump(text, alpha, sdf, kTextTextureWidth, kTextTextureHeight);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
constexpr unsigned kTextTextureWidth = 2048;
|
constexpr unsigned kTextTextureWidth = 4096;
|
||||||
constexpr unsigned kTextTextureHeight = 256;
|
constexpr unsigned kTextTextureHeight = 512;
|
||||||
|
|
||||||
bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& fontPath, std::vector<unsigned char>& sdf, std::string& error);
|
bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& fontPath, std::vector<unsigned char>& sdf, std::string& error);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"type": "float",
|
"type": "float",
|
||||||
"default": 0.42,
|
"default": 0.42,
|
||||||
"min": 0.1,
|
"min": 0.1,
|
||||||
"max": 1.5,
|
"max": 3,
|
||||||
"step": 0.01
|
"step": 0.01
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ float4 compositeOver(float4 baseColor, float4 overColor)
|
|||||||
return float4(outRgb, outAlpha);
|
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)
|
float4 shadeVideo(ShaderContext context)
|
||||||
{
|
{
|
||||||
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
||||||
@@ -19,21 +25,26 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float2 textUv = (context.uv - position) / safeTextSize;
|
float2 textUv = (context.uv - position) / safeTextSize;
|
||||||
bool insideTextRect = textUv.x >= 0.0 && textUv.x <= 1.0 && textUv.y >= 0.0 && textUv.y <= 1.0;
|
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 distance = insideTextRect ? sampleTitleText(textUv) : 0.0;
|
||||||
float edge = 0.02;
|
float edge = 0.5;
|
||||||
float aa = max(fwidth(mask) * 1.5, 0.002);
|
float aa = max(fwidth(distance) * (1.75 + softness * 5.0), 0.0025);
|
||||||
float fill = smoothstep(edge - aa, edge + aa, mask);
|
float2 pixelTextUv = (1.0 / resolution) / safeTextSize;
|
||||||
float shadowRadius = min((outlineWidth + softness) * 0.025, 0.018);
|
float2 sampleOffset = pixelTextUv * 0.38;
|
||||||
float shadow = 0.0;
|
float fill = (
|
||||||
if (shadowRadius > 0.0001)
|
sdfCoverage(textUv, edge, aa) * 2.0 +
|
||||||
{
|
sdfCoverage(textUv + float2(sampleOffset.x, sampleOffset.y), edge, aa) +
|
||||||
shadow = max(shadow, sampleTitleText(textUv + float2(shadowRadius, shadowRadius)));
|
sdfCoverage(textUv + float2(-sampleOffset.x, sampleOffset.y), edge, aa) +
|
||||||
shadow = max(shadow, sampleTitleText(textUv + float2(-shadowRadius, shadowRadius)));
|
sdfCoverage(textUv + float2(sampleOffset.x, -sampleOffset.y), edge, aa) +
|
||||||
shadow = max(shadow, sampleTitleText(textUv + float2(shadowRadius, -shadowRadius)));
|
sdfCoverage(textUv + float2(-sampleOffset.x, -sampleOffset.y), edge, aa)) / 6.0;
|
||||||
shadow = max(shadow, sampleTitleText(textUv + float2(-shadowRadius, -shadowRadius)));
|
float outlineDistance = outlineWidth * 0.16;
|
||||||
}
|
float outlineEdge = edge - outlineDistance;
|
||||||
shadow = smoothstep(edge - aa, edge + aa, shadow) * (0.35 + softness);
|
float outline = (
|
||||||
float outlineAlpha = saturate(shadow * (1.0 - fill)) * outlineColor.a;
|
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 fillAlpha = fill * fillColor.a;
|
||||||
float textAlpha = max(fillAlpha, outlineAlpha);
|
float textAlpha = max(fillAlpha, outlineAlpha);
|
||||||
if (textAlpha <= 0.0001)
|
if (textAlpha <= 0.0001)
|
||||||
|
|||||||
Reference in New Issue
Block a user