Blurred rounded rectangles
raphlinus.github.io
raphlinus.github.io
Also a demonstration of how slow that technique is. I can run stunning games with entire cities of buildings and people and cars and mountains in the distance and trees and grass and clouds all running at 60fps or faster. Or I can run some SDF that runs at 0.2 to 3 fps on the same machine.
Don't get me wrong, I'm blown away by those shaders but they aren't remotely performant.
This particular technique might be okay but you'd still arguably be better running it on 4 quads that make a frame. There's no reason you want to be computing pixels in the middle of the frame where there is no shadow.
This one is intended to be used as part of a 2D renderer. It would be easy to write this up as a fragment shader (and the parameter calculation can be done in either the vertex shader on the CPU). I believe, without having tested it yet, that it would be lightning fast, especially because the heart of the erf approximation can be done with inversesqrt.
I'm also working on a compute-centric renderer, and for that I expect it to be even faster. There, I'll break the scene into tiles, and for each tile there can be an analysis of what's inside. So tiles on the interior of a large blurred rounded rectangle can be solid colors, the edges can just compute a 1d function, etc, and only the corners with the full version.
Keep in mind, what's done in many, if not most, rendering pipelines is to render the rounded rect into a buffer, run a Gaussian blur shader over that buffer into another one (often two or more stages for the separable version of the kernel or something like a dual Kawase blur). Even the simple fragment shader version should massively outperform that.
Or how fast! SDFs can compute approximations of volumetric effects that on a regular raytracing engine would take a few seconds to render.
Additionally, games have been using (baked) 2D SDFs [1] for ages to render world-space (and recently even plain screen-space) text, it's plausible to use the same technique to generate other kinds of shapes.
It can be very useful for UI elements since you can have just a few source assets, and with some shader parameters you get fully animatable effects such as drop shadows, glow, and even normals.
[1]: https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007...
2D signed distance fields are also different, since as textures, each shader fragment is still looking at the same pixels it would have seen before and is just able to do a little extra work with the values it finds to create sharp text.
It’s not really fair to compare SDF with rasterizing using a game engine that has bounding volumes. They are different things.
Some of the terrain & grass SDF examples run faster than any raster engine can ever do it.
> you’d still arguably be better running it on 4 quads
Nothing about the article precludes doing that, right? The technique would work without modification if you attach it to some quads and leave out the middle. Probably even better yet, just exclude the middle from any calculations in the shader and use 1 quad...
https://kylebashour.com/posts/finding-the-real-iphone-x-corn...
The other consideration is the style of writing the code. If you were writing for a sequential processor with fast branching, it would be very tempting to write "if in the corner, compute this. If exterior on the edge, compute that. etc." This might save quite a bit in the number of "actual work" operations, but is much more likely to compile into branches, which on a GPU (or SIMD or, likely, any modern CPU) the will cost more than the work saved.
So it's very idiomatic when writing shader code to use min and max to combine a bunch of cases into a unified code path that can be executed as a straight line.
You might ask how max() and min() are implemented with a branchless model. Sometimes the GPU has a native instruction for it, and a "sufficiently stupid compiler" might not be able to recognize the branch and turn it into the corresponding max/min.
The modern reality is that most all GPUs all have conditional move instructions which allow them to do some amount of branchless conditional across vector lanes like "x >= 1.0 ? x : 0.0;" without incurring the penalty of true flow control.
However, some are still uncomfortable with trusting the compiler to recognize and support this, especially on mobile chipsets with poor quality compilers. Others still just prefer the coding style of the idiomatic branchless expressions, since it's what they're used to.
[1] Footnote: On super old GPUs, like those in the Direct3D 8 era, flow control was emulated completely through branchless systems. The native machine ISA was something like a series r=lerp(A,B,C)+D instructions, and flow control amounted to clever abuse of this paradigm -- lerping to 0.0 or 1.0 can get you a form of conditional move.
> [reciprocal square root] it is particularly well supported in SIMD and GPU and is generally about the same speed as simple division.
Curious, Raph - why is the erf using f64? Reciprocal square root is well supported for single precision, but not double precision. And the spline fit constants in there are single precision anyway. I’m guessing it’d be a lot faster with no harmful effects as f32. (Seems to work fine on ShaderToy BTW).
Also curious if erf() might be overkill? Did you compare to using a smoothstep()? What are the quality indicators you’re looking for? It seems like I get very close to the same results as your erf approximation if I use smoothstep(0., blurwidth, sqrt(d)) where d is the SDF distance to the box. (With the added benefit that I automatically have a strict bound on the blur.)
I didn't compare smoothstep. It's worth doing an analysis of the tradeoff between performance and quality. In any case, I think in practice this erf approximation will be plenty fast, and probably a bit better quality, especially in the tail region.