Skip to content

Commit

Permalink
AtlasEngine: Improve appearance of curly underlines (microsoft#17501)
Browse files Browse the repository at this point in the history
We'd previously subtract one underline-height from the curly line
offset, even though we already had subtracted its complete height.

Additionally, the pixel shader received some fine tuning:
* Shrink the stroke width so that the anti-aliasing can be seen
  all the way up to the horizontal edges of the bounding box.
* Add a phase shift to break apart the symmetry of the curve.

Closes microsoft#17482

Co-authored-by: Carlos Zamora <[email protected]>
  • Loading branch information
lhecker and carlos-zamora authored Jul 2, 2024
1 parent c9e2007 commit ad3797a
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 20 deletions.
2 changes: 1 addition & 1 deletion src/renderer/atlas/BackendD3D.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<u16>(position);
Expand Down
65 changes: 46 additions & 19 deletions src/renderer/atlas/shader_ps.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -37,33 +37,33 @@ 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;
}
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;
}
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;
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit ad3797a

Please sign in to comment.