Understanding the wavy-like shape in alpha-testing magnification for text rendering

Started by
11 comments, last by JoeJ 2 weeks, 1 day ago

In the original Valve paper about SDF, they compare their paper to alpha-testing (as can seen in the video here or the paper here) the alpha Alpha tested version is a lot sharper than bilinear filtering, but it has a wavy like shape near edges as seen on the image to the left below:

I would appreciate if someone can point me to why it is the way it is.
On another note (which may or may not be related to the original question) why is SDF scalable, or more specifically why does interpolation works in the case of SDF but does not work in the normal case? I am mostly interested really in the math behind it, reading about it online, seems to suggest SDF is similar to Contour lines. I am not entirely sure I see the connection. Any points or links are also highly appreciated, as I feel I am lacking a lot in the basics of text-rendering.

Best,
Ali

None

Advertisement

Although i have lots of related experience, i'm still puzzled myself a bit by those questions. I guess somebody else can give concrete answers, but share my observations and conclusions is better than nothing til then.

AliAbdulKareem said:
On another note (which may or may not be related to the original question) why is SDF scalable, or more specifically why does interpolation works in the case of SDF but does not work in the normal case?

I'd say that's because SDF has continuous information everywhere, while density (or alpha of a font letter) is clipped between 0 and one. So we have gradual density information only on the edges. Thus, when using any kernel to calculate gradient, or any filtering approach, to find a sub pixel value (e.g. 0.5 to define a boundary, or a contour at any other value), we likely access clipped values of either 0 or 1, which causes wavy artifacts.

Contrary, with SDF the distance values lie between -/+ infinity, and filtering / calculating gradients is more robust. Artifacts happen only if the signal is too complex to be represented by resolution of the SDF, like in the cases where two edges meet at angles in your lambda example.

Thinking of it, this may well answer both questions.

No mystery there. Magnifying the texture a lot will give it a round-edged blocky look from bilinear filtering. Alpha-testing is tracing the edge that has a value of 0.5 (or whatever you choose) of the result of magnification. The result is a sharp-looking wavy edge.

I think the idea of relying on SDF to allow reducing the texture size by big factors is exaggerated. The technique does allow you a certain play in size without noticeable quality loss, but eventually you will get this look when you -say- zoom enough into the text.

Fixes to this exist, but they add additional data, so it kinda defeats the purpose of size reductions.

Wessam Bahnassi said:
No mystery there. Magnifying the texture a lot will give it a round-edged blocky look from bilinear filtering.

@Wessam Bahnassi Would you mind to elaborate? I am specifically asking why is it wavy, why not blurry, not blocky, not looking weird or anything like that, the fact it is repetitive and identical, means there is a math equation being substituted with x=0.5 and also, it is very probably an equation resulting from bilinear interpolation (as this is the only thing going on). I am just trying to understand how that shape is being generated, and why it is the way it is.

Also what I meant about SDF is not it is infinitely scalable, but the fact it is scalable does not make sense to me since the color of a raster bitmap is also between 0 to 1, why is it that interpolating the distance function is much better than interpolating the color of a pixel? again, I am interested in the math behind it really rather than anything else.

Thanks

None

JoeJ said:
I'd say that's because SDF has continuous information everywhere, while density (or alpha of a font letter) is clipped between 0 and one. So we have gradual density information only on the edges.

Would you mind to elaborate, I am not sure I understand what you mean here, because SDF values are also between 0 to 1 (but this is because it is normalized, rather than clamped, but isn't that the same for Alpha?)

JoeJ said:
Thus, when using any kernel to calculate gradient, or any filtering approach, to find a sub pixel value (e.g. 0.5 to define a boundary, or a contour at any other value), we likely access clipped values of either 0 or 1, which causes wavy artifacts.

I am probably missing something here, would you mind to expand on this? by kernels do you mean bilinear filtering in this case? (I still don't understand why is it blocky in the standard raster bitmap but smooth with sdf).

None

AliAbdulKareem said:
Would you mind to elaborate, I am not sure I understand what you mean here, because SDF values are also between 0 to 1 (but this is because it is normalized, rather than clamped, but isn't that the same for Alpha?)

The range of values does not matter. It's about the clipping. For density (or alpha) we are at some point either full or empty, the values remain constant and contain no information about a linear gradient we construct under the assumption the sampled function is linear:

That's ‘clipped to full’ values on the left, and ‘clipped to empty’ values on the right. Only in the middle is one value between the bound range, like on the edge of a font character.
Trying to reconstruct the signal smoothly shows a wavy blue line. (Using a higher order filter to make it smooth.)
If you sample close to the clipped values, the clipping pulls your result towards the average, although it should exceed the clipping range to avoid the curvy waves. (No matter if we use linear or higher order filters)

The same spot using SDF would look like this:

There is no clipping. Distance keeps changing linearly.
The reconstruction is a straight line.
At any point you can construct a gradient and get a good estimate about the point where distance is zero.

Contrary, if you sample density in the middle of the letter, you can't estimate where the edge is. Because all your samples are just one.

That's usually the reason to use SDF over density.
But avoiding the wavy patterns is a good reason too. Less obvious, but following the same reasoning.

I did not think of it before, but this also means:

We could increase the quality of all alpha testing we do in games, for zero runtime cost!

Convert alpha maps to SDF,
do iterative optimization to grow the distance values until we have distance at least 4 pixels around the edges.
Convert back to alpha, which now looks softer but can still give the same sharpness for the alpha test.

I'm very certain this just works and has no downsides.
Any game engine should add this to its texture preprocessing toolset.
No more jaggy fences and leafs. \:D/
(But maybe they already do this.)

Here is something that hopefully makes you better visualize the source of the waviness. It's not the best quality SDF, but should work. The red trace on the bilinearly enlarged SDF represents the cutoff of the alpha-test at 127/255.

Enlarged SDF

It's worth noting that to get the best quality from this SDF approach the SDF should be calculated from the binary image at a much higher resolution than the final texture, and downsampled to the final resolution. This will avoid some of the step artifacts. The Valve paper used 4096px images if I recall.

Wessam Bahnassi said:
Here is something that hopefully makes you better visualize the source of the waviness.

That's a different kind of wave. In the OPs example, the waves happen within a single pixel, but in yours it's basically aliasing across a larger region of pixels.

Wessam Bahnassi said:
It's not the best quality SDF

Indeed. This usually happens if you generate SDF from image / volume data, not form vector data. It's inaccurate because the exact boundary where distance is zero is unknown, so the aliasing propagates to the SDF as well.

My proposal requires to fix this. I would need to look up my code for a details, but the optimization is about matching gradient and distance estimate across a kernel of 3x3 pixel for example, for some number of iterations. Which reduces / fixes your jaggies and the SDF becomes smooth. It also reduces / fixes the subpixel waves shown in OP iirc.

I did this to generate high quality SDF from a density volume obtained from voxelization, and the SDF was then used for a kind of upscaling. I could not find related resources, which all used vector data to generate a SDF. But turned out it's not really difficult to turn image or volume data into high quality SDF.

Advertisement