[HN Gopher] Perfecting anti-aliasing on signed distance functions
       ___________________________________________________________________
        
       Perfecting anti-aliasing on signed distance functions
        
       Author : ibobev
       Score  : 91 points
       Date   : 2025-08-04 10:55 UTC (12 hours ago)
        
 (HTM) web link (blog.pkh.me)
 (TXT) w3m dump (blog.pkh.me)
        
       | mxfh wrote:
       | The minute black area on the inner part of the sector getting
       | perceptually boosted with the same ramp width like the outer area
       | is effectively how an outline on a shape would behave, not two
       | shapes with no stroke width. I would expect the output brightness
       | should scale with the volume/depth under a pixel in the 3d
       | visualization.
       | 
       | Is this intentional? To me this is an opiniated (aka artistic
       | preference) feature preserving method not _the_ perfect one.
       | 
       | Btw the _common_ visualization has a source and an author:
       | 
       | https://iquilezles.org/articles/distfunctions2d/
       | https://www.shadertoy.com/playlist/MXdSRf
        
         | Retr0id wrote:
         | > The minute black area on the inner part of the sector
         | 
         | I'm not grasping what you're referring to here.
        
           | mxfh wrote:
           | That Pac-Man "mouth" is collapsing to a constant width line
           | like halfway in for the last for examples for me.
           | 
           | Having some weird mid-length discontinuity in the edge
           | direction for me. Not just perceptually.
           | 
           | Maybe I'm misunderstanding something here or have a different
           | idea what the goal of that exercise is, but I would expect
           | some pixels to turn near white near the center towards that
           | gap sector.
        
       | NohatCoder wrote:
       | Reminds me that I found an alternative way of sampling an SDF:
       | 
       | First take a sample in each corner of the pixel to be rendered
       | (s1 s2 s3 s4), then compute:                   coverage=0.5 +
       | (s1+s2+s3+s4)/(abs(s1)+abs(s2)+abs(s3)+abs(s4))/2
       | 
       | It is a good approximation, and it keeps on working no matter how
       | you scale and stretch the field.
       | 
       | Relative to the standard method it is expensive to calculate. But
       | for a modern GPU it is still a very light workload to do this
       | once per screen pixel.
        
         | brookman64k wrote:
         | Would that be done in two passes? 1. Render the image shifted
         | by 0.5 pixels in both directions (plus one additional row &
         | column). 2. Apply above formula to each pixel (4 reads, 1
         | write).
        
           | NohatCoder wrote:
           | You certainly could imagine doing that, but as long as the
           | initial evaluation is fairly cheap (say a texture lookup), I
           | don't see the extra pass being worth it.
        
           | ralferoo wrote:
           | That'd be one way of doing it.
           | 
           | You don't technically need 4 reads per pixel either, for
           | instance you can process a 7x7 group with a 64-count thread
           | group. Each thread does 1 read, and then fetches the other 3
           | values from its neighbours and calculates the average. Then
           | the 7x7 subset of the 8x8 write their values.
           | 
           | You could integrate this into the first pass too, but then
           | there would be duplication on the overlapped areas of each
           | block. Depending on the complexity of the first pass, it
           | still might be more efficient to do that than an extra pass.
           | 
           | Knowing that it's only the edges that are shared between
           | threads, you could expand the work of each thread to do
           | multiple pixels so that each thread group covers more pixels
           | the reduce the number of pixels sampled multiple times. How
           | much you do this by depends on register pressure, it's
           | probably not worth doing more than 4 pixels per thread but
           | YMMV.
        
         | shiandow wrote:
         | Technically that only requires calculating one extra row and
         | column of pixels.
         | 
         | It is indeed scale invariant but I think you can do better, you
         | should have enough to make it invariant to any linear
         | transformation. The calculation will be more complex but that
         | is nothing compared to evaluating the SDF
        
           | NohatCoder wrote:
           | I do believe that it is already invariant to linear
           | transformations the way you want, i.e. we can evaluate the
           | corners of an arbitrary parallelogram instead of a square and
           | get a similar coverage estimate.
        
             | shiandow wrote:
             | Similar maybe but it can't be the same surely? Just pick
             | some function like f(x,y) = x-1 and start rotating it
             | around your centre pixel, the average (s1+s2+s3+s4) will be
             | the same (since it's a linear function) but there's no way
             | those absolute values will remain constant.
             | 
             | You should be pretty close though. For a linear function
             | you can just calculate the distance to the 0 line, which is
             | invariant to any linear transformation that leaves that
             | line where it is (which is what you want). This is just the
             | function value divided by the norm of the gradient. Both of
             | which you can estimate from those 4 points. This gives
             | something like                   dx = (s2 - s1 + s4 - s3)
             | dy = (s3 - s1 + s4 - s2)         f  = (s1+s2+s3+s4)/4
             | dist = f / sqrt(dx*dx + dy*dy)
        
               | NohatCoder wrote:
               | My function approximate coverage of a square pixel, so
               | indeed if you rotate a line around it at a certain
               | distance that line will clip the corners at some angles
               | and be clear of the pixel at other angles.
        
       | talkingtab wrote:
       | A very good example of SDF thinking, using signed distance fields
       | in shaders. Both shaders and SDF are new to me and very
       | interesting. Another example of what is being done is MSDF here:
       | https://github.com/Chlumsky/msdfgen.
        
         | mxfh wrote:
         | That what I wondering, for sharp narrow corners like with that
         | pac-man mouth center and in font rendering
         | composite/multichannel is probably the better approach for any
         | situation where the there is potential for self-intersection of
         | the distance field in concave situations.
         | https://lambdacube3d.wordpress.com/2014/11/12/playing-around...
        
       | WithinReason wrote:
       | Instead of OKLAB isn't it simpler to just use a linear color
       | space and only do gamma correction at the very end?
        
         | badlibrarian wrote:
         | Simpler and worse in this application.
        
           | yorwba wrote:
           | If the application is simulating a crisp higher-resolution
           | image that was slightly blurred while downscaling to the
           | output resolution, a linear color space is exactly the right
           | choice. Yes, it means bright objects on a dark background
           | will look larger than the reverse, but that's just a fact of
           | human light sensitivity. If the blurring is purely optical,
           | with no pixels in between, a small light in the dark can
           | still create a large halo, whereas there's no corresponding
           | anti-halo for dark spots in a well-lit room.
           | 
           | On the other hand, if you want something that looks roughly
           | the same no matter which color you use, counteracting such
           | oddities of perception is certainly unavoidable.
        
       | Const-me wrote:
       | Good article, but I believe it lacks information what
       | specifically these magical dFdx, dFdy, and fwidth = abs(dFdx) +
       | abs(dFdy) functions are computing.
       | 
       | The following stackexchange answer addresses that question rather
       | well: https://gamedev.stackexchange.com/a/130933/3355 As you see,
       | dFdx and dFdx are not exactly derivatives, these are discrete
       | screen-space approximations of these derivativities. Very cheap
       | to compute due to the weird execution model of pixel shaders
       | running in hardware GPUs.
        
         | mananaysiempre wrote:
         | If you've ever sampled a texture in a shader, then you know
         | what those are, so it's probably fair to include them in the
         | prerequisites for the article. But yes, those are intended to
         | be approximate screen-space derivatives of whatever quantity
         | you plug into them, and (I believe) on basically any hardware
         | the approximation in question is a single-sided first
         | difference, because the particular fragment (single-pixel
         | contribution) you're computing always exists in a 2x2 group of
         | screen-space neighbours executing in lockstep.
        
       | david-gpu wrote:
       | This looks a lot like some line anti aliasing I had to hack
       | together many years ago when a customer started complaining
       | loudly about the lack of hardware support for it. I think I had
       | something like a week to put together three different
       | alternatives for them to pick from, and this was the winner. It
       | looked the best by far.
       | 
       | Years later my boss was telling me how satisfied he was that he
       | could throw any problem in my general direction and it would be
       | gone in no time. There is nothing like the risk of losing his
       | work permit to motivate a young guy to work himself down to a
       | crisp, all for peanuts.
        
       | jeremyscanvic wrote:
       | Really interesting write-up! I'm not very familiar with signed
       | distance functions but aliasing is a major part of my PhD and
       | this is really insightful to me!
        
       | pcwalton wrote:
       | Mathematically, what you want to do here is to calculate the area
       | of the pixel square (or circle; however you want to approximate
       | it) that the shape covers. In this case a linear ramp actually
       | approximates the true value better than smoothstep does. (I had
       | the derivation worked out at some point; I don't have it handy,
       | unfortunately.) Of course, beauty is in the eye of the beholder,
       | and aesthetically one might prefer smoothstep.
       | 
       | By the way, since the article mentions ellipse distance
       | approximations, the fastest way to approximate distance to an
       | ellipse is to use a trick I came up with based on a paper from
       | 1994 [1]:
       | https://github.com/servo/webrender/blob/c4bd5b47d8f5cd684334...
       | Unless it's changed recently, this is what Firefox uses for
       | border radius.
       | 
       | [1]: http://mesh.brown.edu/taubin/pdfs/Taubin-tog94.pdf
        
       ___________________________________________________________________
       (page generated 2025-08-04 23:01 UTC)