This turned into a rabbit hole for me hopefully it helps answer your question, I recommend trying out the various functions on the ShaderToy website. Just drop them in and change the call to "sdCircle". I used the values R=0.75 and r=0.35 to test them out.
Part 1: Defining what an SDF means
Signed Distance Field or SDF, in this context, is a function that returns the signed distance from a point to a primitive. The returned value is always a float that can take on 3 context specific meanings.
- zero: This is on the surface, while it is rare to hit exactly on the surface of the primitive we can approximate a hit with a simple inequality like
abs(d)<sigma
where d is the signed distance and sigma is some small value like 0.001.
- negative: In this context negative values represent hits inside the primitive but choosing the meaning of a negative return value is context specific. Smaller (more negative) values represent values that are deeper in the interior of the primitive.
- positive: Again this is context specific, but in this case this is a hit outside the primitive and larger values mean it is farther away from the primitive.
Stop for a second and take another look at the first case, "zero". This may seem unimportant at first glance but it turns out that by defining a generic base primitive and then changing the value of sigma we can easily compute a primitive of any size. In the case of a torus we could have a very thin torus or one that is so big it envelopes itself. Then by adding one more variable representing the large Radius of the torus, the torus can now be changed to fit our needs as desired just by controlling two variables.
Part 2: Defining the primitive
Take a look at IQ's 2D function for a circle.
float sdCircle( vec2 p, float r ){ return length(p) - r; }
All this function is doing is computing the "distance" using the length function of our point "p" from our circle (which is sitting at the origin) then subtracting our radius "r". And easy as that any point greater then radius is positive, any point less then radius is negative.
This is almost a torus already, we are just missing the signed distance for the inside diameter of the 2D torus. But we already know that is really just some sigma value from our original circle, so all we have to do is add a second radius and compute the sigma.
So:
float sdTorus2D( vec2 p, float R, float r )
{
float q = length(p)-R;
return abs(q)-r;
}
No need to recalculate the distance, so just figure out sigma to our second circle with abs(q)-r
.
Part 3: Going 3D
If you stop and think about it for a second (ok...maybe more then a second) then it is easy to realize that our 2D torus is just a slice of a 3D torus. When you think about it like that then we can take another slice of our torus in 2D on its edge just by rotating our torus 90 degrees. This should result in two SDF circles side by side.
But turning the torus on its side is really just a matter of computing our signed distances in the opposite order...lets try:
float sdTorus2DSide( vec2 p, float R, float r )
{
vec2 q = vec2(abs(p.x)-R,p.y);
return length(q)-r;
}
The two different views essentially compute the distance in two different ways, so by combining them we should be able to view our torus in 3D from any angle just by adding an extra component "z" so that both computations can be independent of each other. Then combine our two radii into a vec2 and we end up with a function that looks just like IQ's.
float sdTorus( vec3 p, vec2 t )
{
vec2 q = vec2(length(p.xz)-t.x,p.y);
return length(q)-t.y;
}
This would still look two dimensional in the "circle" shadertoy, but now we can vary z to look at the torus slices. Or rotate our view point and see it emerge from the 2D world. Neat stuff!
BTW you can go through this same process with many primitives and get many different shapes as IQ has demonstrated on his website.