From 501f7752af336aff4c3771106095fa62487e84f4 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 2 Jul 2024 01:24:17 +0200 Subject: [PATCH] AtlasEngine: Improve appearance of curly underlines --- src/renderer/atlas/BackendD3D.cpp | 2 +- src/renderer/atlas/shader_ps.hlsl | 65 ++++++++++++++++++++++--------- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index 9a83b75d233..a9e81422d0a 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -306,7 +306,7 @@ void BackendD3D::_updateFontDependents(const RenderingPayload& p) // it being simple to implement and robust against more peculiar fonts with unusually large/small descenders, etc. // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom, which is why `position` has a min(). const auto height = std::max(3, duBottom + duHeight - duTop); - const auto position = std::min(duTop, cellHeight - height - duHeight); + const auto position = std::min(duTop, cellHeight - height); _curlyLineHalfHeight = height * 0.5f; _curlyUnderline.position = gsl::narrow_cast(position); diff --git a/src/renderer/atlas/shader_ps.hlsl b/src/renderer/atlas/shader_ps.hlsl index d8d894be13e..f52a13f48eb 100644 --- a/src/renderer/atlas/shader_ps.hlsl +++ b/src/renderer/atlas/shader_ps.hlsl @@ -37,7 +37,7 @@ Output main(PSData data) : SV_Target { case SHADING_TYPE_TEXT_BACKGROUND: { - const float2 cell = data.position.xy / backgroundCellSize; + float2 cell = data.position.xy / backgroundCellSize; color = all(cell < backgroundCellCount) ? background[cell] : backgroundColor; weights = float4(1, 1, 1, 1); break; @@ -45,13 +45,13 @@ Output main(PSData data) : SV_Target case SHADING_TYPE_TEXT_GRAYSCALE: { // These are independent of the glyph texture and could be moved to the vertex shader or CPU side of things. - const float4 foreground = premultiplyColor(data.color); - const float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb); - const float intensity = DWrite_CalcColorIntensity(data.color.rgb); + float4 foreground = premultiplyColor(data.color); + float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb); + float intensity = DWrite_CalcColorIntensity(data.color.rgb); // These aren't. - const float4 glyph = glyphAtlas[data.texcoord]; - const float contrasted = DWrite_EnhanceContrast(glyph.a, blendEnhancedContrast); - const float alphaCorrected = DWrite_ApplyAlphaCorrection(contrasted, intensity, gammaRatios); + float4 glyph = glyphAtlas[data.texcoord]; + float contrasted = DWrite_EnhanceContrast(glyph.a, blendEnhancedContrast); + float alphaCorrected = DWrite_ApplyAlphaCorrection(contrasted, intensity, gammaRatios); color = alphaCorrected * foreground; weights = color.aaaa; break; @@ -59,11 +59,11 @@ Output main(PSData data) : SV_Target case SHADING_TYPE_TEXT_CLEARTYPE: { // These are independent of the glyph texture and could be moved to the vertex shader or CPU side of things. - const float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb); + float blendEnhancedContrast = DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, data.color.rgb); // These aren't. - const float4 glyph = glyphAtlas[data.texcoord]; - const float3 contrasted = DWrite_EnhanceContrast3(glyph.rgb, blendEnhancedContrast); - const float3 alphaCorrected = DWrite_ApplyAlphaCorrection3(contrasted, data.color.rgb, gammaRatios); + float4 glyph = glyphAtlas[data.texcoord]; + float3 contrasted = DWrite_EnhanceContrast3(glyph.rgb, blendEnhancedContrast); + float3 alphaCorrected = DWrite_ApplyAlphaCorrection3(contrasted, data.color.rgb, gammaRatios); weights = float4(alphaCorrected * data.color.a, 1); color = weights * data.color; break; @@ -157,26 +157,53 @@ Output main(PSData data) : SV_Target } case SHADING_TYPE_DOTTED_LINE: { - const bool on = frac(data.position.x / (3.0f * underlineWidth * data.renditionScale.x)) < (1.0f / 3.0f); + bool on = frac(data.position.x / (3.0f * underlineWidth * data.renditionScale.x)) < (1.0f / 3.0f); color = on * premultiplyColor(data.color); weights = color.aaaa; break; } case SHADING_TYPE_DASHED_LINE: { - const bool on = frac(data.position.x / (6.0f * underlineWidth * data.renditionScale.x)) < (4.0f / 6.0f); + bool on = frac(data.position.x / (6.0f * underlineWidth * data.renditionScale.x)) < (4.0f / 6.0f); color = on * premultiplyColor(data.color); weights = color.aaaa; break; } case SHADING_TYPE_CURLY_LINE: { - const float strokeWidthHalf = doubleUnderlineWidth * data.renditionScale.y * 0.5f; - const float amp = (curlyLineHalfHeight - strokeWidthHalf) * data.renditionScale.y; - const float freq = data.renditionScale.x / curlyLineHalfHeight * 1.57079632679489661923f; - const float s = sin(data.position.x * freq) * amp; - const float d = abs(curlyLineHalfHeight - data.texcoord.y - s); - const float a = 1 - saturate(d - strokeWidthHalf); + // The curly line has the same thickness as a double underline. + // We halve it to make the math a bit easier. + float strokeWidthHalf = doubleUnderlineWidth * data.renditionScale.y * 0.5f; + float amplitude = (curlyLineHalfHeight - strokeWidthHalf) * data.renditionScale.y; + // We multiply the frequency by pi/2 to get a sine wave which has an integer period. + // This makes every period of the wave look exactly the same. + float frequency = data.renditionScale.x / curlyLineHalfHeight * 1.57079632679489661923f; + // At very small sizes, like when the wave is just 3px tall and 1px wide, it'll look too fat and/or blurry. + // Because we multiplied our frequency with pi, the extrema of the curve and its intersections with the + // centerline always occur right between two pixels. This causes both to be lit with the same color. + // By adding a small phase shift, we can break this symmetry up. It'll make the wave look a lot more crispy. + float phase = 1.57079632679489661923f; + float sine = sin(data.position.x * frequency + phase); + // We use the distance to the sine curve as its alpha value - the closer the more opaque. + // To give it a smooth appearance we don't want to simply calculate the vertical distance to the curve: + // abs(pixel.y - sin(pixel.x)) + // + // ...because while a pixel may be vertically far away it may be horizontally close to the sine curve. + // We need a proper distance calculation. This makes a large difference at especially small font sizes. + // + // While calculating the distance to a sine curve is complex, calculating the distance to its tangent is easy, + // because tangents are straight lines and line-point distance are trivial. The tangent of sin(x) is cos(x). + // The line-point distance is the vertical distance multiplied by the cos(angle) of the line. + // To turn out tangent cos(x) into an angle we need to calculate atan(cos(x)). This nets us: + // abs(pixel.y - sin(pixel.x)) * cos(atan(cos(pixel.x)) + // + // The expanded sine form of cos(atan(cos(x))) is 1 / sqrt(2 - sin(x)^2), which results in: + // abs(pixel.y - sin(pixel.x)) * rsqrt(2 - sin(pixel.x)^2) + float distance = abs(curlyLineHalfHeight - data.texcoord.y - sine * amplitude) * rsqrt(2 - sine * sine); + // Since pixel coordinates are always offset by half a pixel (i.e. data.texcoord is 1.5f, 2.5f, 3.5f, ...) + // the distance is also off by half a pixel. We undo that by adding half a pixel to the distance. + // This gives the line its proper thickness appearance. + float a = 1 - saturate(distance - strokeWidthHalf + 0.5f); color = a * premultiplyColor(data.color); weights = color.aaaa; break;