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

Quick idea for improving text antialiasing #137

Open
asbjornlystrup opened this issue Jun 17, 2021 · 7 comments
Open

Quick idea for improving text antialiasing #137

asbjornlystrup opened this issue Jun 17, 2021 · 7 comments

Comments

@asbjornlystrup
Copy link

asbjornlystrup commented Jun 17, 2021

I'm not sure if you've updated the antialiasing code recently, as I'm using an older version, but after finding a nice way to do antialiasing of shader-drawn circles, I wanted to try it out on your text as well, because the text is SDF-based, and the circle's antialiasing is distance-based as well.

In my current version of Troika, you do return length(fwidth(vTroikaGlyphUV * vTroikaGlyphDimensions)) * 0.5; for the aa distance. This has the artifact that when viewing at an angle, the leftmost and rightmost edges get a somewhat visible blur:
image

I changed it to a scalar; the distance, just like in my circle code: return fwidth(distance) * 0.5; With the distance passed in from troikaGetFragDistValue like this: float distance = troikaGetFragDistValue(); float aaDist = troikaGetAADist(distance);
Doing it this way I don't get the blurring artifact:
image

The 0.5 factor can be increased for a smoother antialiasing. E.g. 0.8:
image

I thought maybe you were unaware of this method just like me, so thought I'd share it. My "implementation" does have some strange artifacts though (see below), and maybe this is why you opted for the other method. Also, I'm not exactly sure how you use the aaDist variable in the shader code, so you might be better equipped at implementing this. Maybe there's some code that should be changed or removed because of this change for example, that could improve my "implementation" further.

image

I found this method of doing it in bgolus's post here: https://forum.unity.com/threads/antialiasing-circle-shader.432119/ where there's some more info. He also says that you can do for example length(vec2(dFdx(distance), dFdy(distance))) instead of fwidth(distance) for even greater precision, although I'm not sure if this is negligible or not.

@lojjic
Copy link
Collaborator

lojjic commented Jun 17, 2021

Thanks for the suggestion! I'll definitely look into this, as I've noticed those blurry edges at oblique angles too and would like to eliminate them.

Theoretically I think both the change in distance and the change in the vTroikaGlyphUV * vTroikaGlyphDimensions should be equivalent since they both represent a distance in font-space units, but there must be some subtle difference there I've overlooked. If I can avoid the artifacts you showed, then I'll be happy to integrate your change. :)

@asbjornlystrup
Copy link
Author

Great! The change is as simple as that one line (and passing in that distance argument to acommodate it). If you still would like the raw code, I can send it when I'm working tomorrow.

Theoretically I think both the change in distance and the change in the vTroikaGlyphUV * vTroikaGlyphDimensions should be equivalent since they both represent a distance in font-space units

I don't exactly understand how the derivatives would be the same. distance is the SDF distance I guess, but the other is a scaled uv position. I haven't looked at the code and how they compare, but it feels like their derivatives should be different. As far as I know fwidth just takes the difference of the specific input value passed in as argument with those same specific input values in the neighbor fragments, which is possible because the neighbor fragments execute at the same time in parallel. I ran a test earlier today with gl_FragColor.rgb = vec3(fwidth(gl_FragColor.x + gl_FragColor.g + gl_FragColor.b)); to see if it rendered edges like in edge detection/a sobel filter, to try to confirm that fact a bit, and it did.

@lojjic
Copy link
Collaborator

lojjic commented Jun 22, 2021

Thanks for the discussion, this stuff breaks my brain sometimes. ;)

When I said they are "equivalent" I meant that they both represent a distance in the same units, but you're right they're not exactly the same.

Backing up, what we want here is the potential rate of change between fragments. Using the change in the distance variable like you're suggesting will often be accurate for that, but in some cases it will be highly inaccurate. This is because the SDF from which that value is sampled is non-uniform; it curves and switches its direction of change across the glyph. Therefore the derivative of distance between neighboring fragments can end up being much too small, even zero, like where the distance field switches from increasing to decreasing halfway between glyph paths, or much too large, like when the text is displayed at small sizes and the fragment grid overlays the SDF at unpredictable locations. I suspect that explains at least some of the artifacts you are seeing.

So how can we determine the potential rate of change without using distance? Well that "scaled uv position" as you call it is essentially an x/y coordinate within the glyph's rect, using the same units as distance. The derivative of that x/y gives us a rate of potential travel between fragments. Overall that gives a better result than using distance, because it is based on a uniform rate of change across the glyph, and we therefore don't get artifacts and it's more accurate at small sizes.

But, as you've seen, where it breaks down is at oblique angles; in those cases the rate of potential x/y change in the vertical is much different than that of the horizontal. And so we end up using a single value for the potential rate of change that is too large in one direction and too small in the other, giving blurry and/or choppy lines depending on the SDF's actual direction of change at that fragment.

So yeah, there's definitely room for improvement in the oblique case, but just using the derivative of distance isn't sufficient on its own. I'm open to ideas to improve this of course. One possibility could be to try to detect the scenario (large text size at an oblique angle, close to a path) and use the distance derivative only in that one case.

@asbjornlystrup
Copy link
Author

asbjornlystrup commented Jun 23, 2021

Yeah, this stuff is confusing to think about, haha. I can see the distance field being less smooth, yeah.

Those glitchy pixels might just be because the fragments out there have some weird distance values.

It's not incredibly choppy from far away, but you can definitely see the UVs being better. Perhaps interpolating between the distance variable and the UVs dependent on camera distance would work well; as in choose more of fwidth(distance) when close, and more fwidth(uv) when far away.

For comparison (didn't capture the two at the exact same camera position and orientation, but I think it shows the difference well anyway):
Distance (0.8 factor):
image
UVs:
image

@canadaduane
Copy link

I'm noticing that this modification significantly improves the result for custom text transformations. Here is some text, modified to follow a curve using createDerivedMaterial (see this technique). This uses current troika-three-text shaders:

image

Here is the same screenshot, but with @asbjornlystrup's modification to troikaGetAADist:

image

In the second shot, you can see the extremely stretched "GREAT!" is quite a bit crisper.

@canadaduane
Copy link

canadaduane commented Oct 9, 2021

Anecdotally: I've also found that tweaking the constant to 0.25 (from 0.5) makes a nice crisp edge as well:

return length(fwidth(vTroikaGlyphUV * vTroikaGlyphDimensions)) * 0.25;

@lojjic
Copy link
Collaborator

lojjic commented Oct 10, 2021

Thanks @canadaduane, I can definitely see the improvement in the stretched parts. I can also see the ugly artifacts popping up there between the L's in HELLO, which we'd definitely need to figure out before adopting this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants