Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Signed distance functions in 46 lines of Python (vgel.me)
283 points by todsacerdoti on Dec 20, 2022 | hide | past | favorite | 24 comments


Great write up. If anyone wants to explore SDFs more, check out Inigo Quilez - he's a master of this stuff and a great explainer.

Painting a Character with Maths: https://www.youtube.com/watch?v=8--5LwHRhjk

And plenty more on his website in article form: https://iquilezles.org/articles/

Finally, Shadertoy, a graphics shader playground where you can see (and try!) a lot of this kind of stuff: https://www.shadertoy.com/


For completeness, check Inigo's shadertoy page: https://www.shadertoy.com/user/iq


Adding in with a toy site I built for people interested in something like Shadertoy but for ascii graphics like the article: https://textshader.com

EDIT: Translating the article's code over to Javascript with some minor changes yields: https://textshader.com/#default.032468fa7db1ddd9602b9d2516aa...


I recommend the video wherein you can paint a character using easy mathematics.


Check out https://bauble.studio too


Excellent. You might be interested in a Python library that I wrote for generating 3D meshes (STL files) from SDFs : https://github.com/fogleman/sdf

It just uses marching cubes for triangulation but the SDFs are all numpy'd and the SDF is evaluated in batches on multiple threads so it's relatively fast.

I wrote it because I was a little frustrated with OpenSCAD. SDFs make it trivial to do things like rounded corners, for example.


Oh man, this is great. I'm a nuclear engineer and an old but widely used program called MCNP relies on a constructive solid geometry approach (first intended for punch cards). I've been wishing for a way to go between a Python approach and MCNP for constructing geometry and this will make it very easy!


Really nice, I've played around with SDFs trying to make Fluid Simulations, which after much hair pulling I realized hadd particles only "pushing" one way on eachother, because I didn't think that GPUs are unable to have different jobs write back to shared memory. Well, I just found a fluid asset that was 1000x better than what I made.

The shape library looks pretty comprehensive, and the calling conventions are nice!


Do you have some numbers on how fast this actually is? I know it’ll be hard to give specifics, but is this fast enough to convert a somewhat complex sdf into a mesh in (near) real-time? Could you for example build a modelling app around this that uses sdfs as an underlying data model, and uses this as an in between step to make rendering faster?


You can change the resolution. A coarse resolution might be adequate for fairly quick feedback. Then you can crank up the resolution when you're ready to generate the final output. Or you could even progressively update the preview. I wouldn't call it realtime though. Like you wouldn't be able to animate a changing SDF.


Thanking you kindly because I have used this library to generate a few things for 3D printing - although I have slunk back to OpenSCAD a few times because the super-quick preview is extremely helpful when iterating on something.


This is lovely. I will be playing with it soon!


Kudos. Usually these type of "in N lines of Python"-type things proceed to call a 3rd-party library like "sklearn" or "matplotlib" to do all the heavy lifting. Fun, educational article.


There is a SDF renderer in Taichi Lang examples: https://github.com/taichi-dev/taichi/blob/master/python/taic...

Not so short as OP's version(46 lines vs 171 lines), but it renders a nice looking image: https://github.com/taichi-dev/taichi-release-tests/blob/mast...


I guess it's not important to the rendering, but it would be nice if the author explained what "signed distance function" is specifically.

* The distance to the boundary of a circle is abs(sqrt(x*2 + y*2) - radius) ... so as you would expect on the outside, but it's also positive on the inside of the circle

* The distance to the circle is max(0, sqrt(x*2 + y*2) - radius) ... so as you would expect on the outside, but it's 0 on the inside

* The signed distance function of the circle is sqrt(x*2 + y*2) - radius ... so as you would expect on the outside, but it's negative on the inside

Technically the regular distance function would do for the flat looking rendering in the article but you have to calculate the signed distance function on the way there first anyway.

But the signed distance function does have a lot of interesting features you can't recover from the regular distance function [1]. One is that you can recover the surface normal from it by taking the gradient - exactly what was used for getting the normal vector in the article! Another property, not relevant here but very nice, is you can use (point on surface, signed distance) as a coordinate system in the space near to the surface. This gives you a nice formula for the integral of a function in a vicinity of the surface.

[1] https://en.wikipedia.org/wiki/Signed_distance_function#Prope...


> The distance to the circle is max(0, sqrt(x*2 + y*2) - radius) ... so as you would expect on the outside, but it's 0 on the inside

That is the distance to a disc.


Oops you are right! That's a depressing reminder how long it's been since I was a research mathematician (when I wouldn't have made that mistake... probably).

In fairness, "circle" is fairly often used to refer to the 2d shape, even sometimes in real maths contexts e.g. "squaring the circle" in classical geometry is never referred to as "squaring the disc". In fact I suspect use of "circle" to refer exclusively to the 1d boundary probably comes a very long time after the word first existed in the English language. And even when I was a research mathematician, I never would've corrected someone if it was really clear from the context that they were talking about the 2d shape (which, to be honest, is true of my comment above).


This is a beautifully written article. Excellent job!

I have seen some of Inigo Quilez tutorials on this subject [1] but I feel like this is a much better introduction for those of us without too much interest in learning GLSL :-). On the other hand, Quilez's tutorials are amazing and show what can be accomplished with this technique!

1: https://iquilezles.org/live/


Excuse me if this is a newbie question, but

> In sample(x, y), we only care about the ray of light that passes through (x, y, camera_z). So why bother with all the other rays? We'll shoot the ray backwards! We'll start it at (x, y, camera_z), and at each step, query the SDF to get the distance from the ray's current point to the scene (in any direction). If the distance is less than some threshold, we hit the scene! Otherwise, we can safely "march" the ray forward by whatever distance is returned, since we know the scene is at least that distance away in the forward direction.

Doesn't this assume all the rays are only in the z direction? We are assuming the light source is directly in front of the camera? The code actually only increments z so it's basically only capable of determining occlusion, right?


The blog's explanation with sun and light ray is confusing. Ray marching is for finding the interception of the object surface and the camera’s viewing direction. Whether there’s a sun or light is irrelevant.

The ray and the light source are two different things. The ray is from the camera to a point in the space. The light source is at a different location; it’s for computing light reflection on the object.

It's kind of weird and confusing to increment z only. It should increment the ray. E.g.

    camera_pos = vec3(0.0, 0.0, -5.0) // eye is -5.0 in front of the screen
    screen_pos = vec3(x, y, 0.0)      // screen plane lays at 0.0 on z-axis.
    ray_direction = normalize(screen_pos - camera_pos)
For ray marching along a ray, each step extends the ray by a certain amount.

    marched_distance = 0.0
    for 0 to N
        ray_end = camera_pos + marched_distance * ray_direction
        ray_end_to_object_distance = sdf_sphere(ray_end)
        if (ray_end_to_object_distance < 0.0001)
            return ray_end  // ray_end hit the object surface
        marched_distance += ray_end_to_object_distance
    return null  // ray never met the object after N steps.
The object's reflection at the returned ray_end point can be computed using the surface normal at the point, the light source position, and the camera_pos.


From what I see, it's a simplification: rays start from (x, y, -10) and follow the z axis. This is very limited and I guess you'll lose the perspective.

Exercise for the reader: - Update the sample function, pass the ray direction as argument. - Compute the direction for each ray.

Rays should have a slightly different direction, based on the camera field of view. When you move the camera back or increase the field of view, the donut should get smaller.

Next exercise: move the camera around the donut (but still looking at it).


It's an orthographic projection. Ever play an isometric game?

https://en.wikipedia.org/wiki/Orthographic_projection#/media...


Nice! I've been conjuring up a little library for using braille Unicode characters to draw in the terminal which I plan to write a blog post on and release Soon™, and this is a pretty cool idea for a demo. I'll have to try my best to resist implementing it, because I've been delaying it by having waaay to much fun making demos already [0], but that's a good problem to have, I think :)

[0] https://raw.githubusercontent.com/pedrovhb/brailliant/main/e... (warning, 8mb unoptimized animated svg)


I tried rendering something with braille characters, but for whatever reason using Konsole in Archlinux, despite trying multiple fixed-width fonts, the no-dots (empty) braille character rendered with a different width than all the others, and trying other (fixed width or not) space characters instead also didn't help. This of course messing up the alignment. Maddening!

When making a fixed width font and adding the braille characters, the first priority should be to make all 256 (also the empty one) of them the same width! And a VTE should of course prioritize rendering fixed width as fixed width. How did that get screwed up?

Now that I think of it, I could have used one of the others but made the font fg color the same as the bg color. But that wouldn't look good if then copypasting it into a plain text editor.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: