Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AtlasEngine: Improve appearance of curly underlines #17501

Merged
merged 1 commit into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??? did we learn something new about const here?

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
Loading