Let’s take an honest look at our result so far:

image.png

This has one major issue, being that even in a perfect world where the dots would for sure be circles (which they can’t be because The dots are not real), they are still at the mercy of surfaces they touch.

We know that to keep dots of the same size, we compare the rate of change of UVs using dFdx() and dFdy() and scale relative to the resulting values. This works great to ensure objects far away have dots of larger size, but quickly faces issues with surfaces angled away from the camera. In that sometimes, dFdy() and dFdx() will wildly disagree, what should we do then…?

Solution 1 - choose the larger value

This one is just fun and leads to a very psychedelic result which I simply wanted to share. It could be the aesthetic you’re looking for, but probably not:

image.png

Simply put, when you walk on a floor, the further away the floor is, the stronger the angle. Since we bias strongly against this effect, far away dots on said floor feel larger than they should be, and as you approach them they seem to shrink. You essentially get this reverse fish eye effect where the elements on the side of the screen often feel more consistent and clear than the elements far away in the middle.

Solution 2 - choose the smaller value

image.png

This approach works most of the time, but does lead to aliasing issue at much larger dots sizes than expected.

Solution 3 - A mix of the two

In my final code, I do bias towards the smaller value but not completely, choosing a 75-25 split

	[...]
// use the UV frequency to keep all UVs within power of 2
// of one another
vec2 uvFrequency = getUVFrequency();
float spacing = uvFrequency.x * .25 + uvFrequency.y * .75;
spacing *= exp2(scale);
	[...]

image.png

this leads to the version seen at first which doesn’t suffer with Aliasing as quickly but still obviously doesn’t look like perfect dots all around.

Solution 4 - Enforce screen sized UVs

Now obviously we can’t have screen-spaced UVs, that would lead to all the issues we’ve been trying to move away from by snapping our dithering onto the textures themselves. But perhaps we could use dFdx and dFdy to force UVs to move at the same rate vertically and horizontally right?

Something like this..?

vec2 flattenUVs()
{
    // Estimate screen-space gradients of the UVs
    vec2 dx = dFdx(inUV);
    vec2 dy = dFdy(inUV);

    // Compute the length (magnitude) of the change in each direction
    float lenX = length(dx);
    float lenY = length(dy);

    // Normalize by the average (or max) to make both axes scale evenly
    float avgLen = (lenX + lenY) * 0.5;

    return (inUV - 0.5) / vec2(lenX, lenY) * avgLen + 0.5;
}