back
57 comments
This article talks about a pretty full-featured ray tracer. If you just want to play around with some ideas and have fun, you can make different tradeoffs and still get very nice and fast renderings. Here's a ray tracer that I made that's GPU-accelerated and can make some nice looking images quickly in 1000 lines of code. https://www.shadertoy.com/view/4ddcRn The tradeoff is that it's all procedural graphics. So there are no triangle meshes and it would also be a bit tricky to implement a bidirectional ray tracer like this.
Whoaaa. If you look from the bottom up through the glass floor, it looks crazy awesome.

I think that's the most impressive little demo I've seen in ... quite some time. I can think of more impressive ones, but they're all extraordinarily complicated in comparison.

How'd you do the glass if there are no meshes? I'm about to pass out, else I'd dig in.

I'm especially curious how you're getting the refraction right. I forget my basic physics, but is it as simple as deflecting the ray slightly? But then how does it work on a curved surface? I guess it happens per pixel, so it shouldn't be too surprising, but... still, I was expecting a lot more aliasing than this: https://i.imgur.com/fdDVcCT.jpg

I guess I've never seen a real time raytracer that happened to trace through ripply glass, which is why it feels so neat.

The sibling comment explains it too, but here's a simple description: The object still has a geometric definition, it just isn't a mesh. It has equations describing the curves. Yes, per pixel, using the curve geometry, you calculate where the ray intersects that curve, and at what angle to figure out the deflection from there.

Think of a simplification in 2D. Sunlight comes from directly vertical above a sine wave. For every x-coordinate where you cast a sun ray, you just need the slope of the sine-wave at that x-coordinate to calculate the angle for what will happen at the intersection.

A mesh isn't any fundamental unit of physics or geometry, it's just our customary way of approaching the rendering to get good-enough fast-enough results from our current hardware.

Not the person you're replying to, but in general refraction is pretty easy to do in a ray tracer. When the ray hits the object you use the normal vector, the incident ray direction, and the index of refraction of the material (or materials, if you're transitioning from one substance to another with different index of refractions) to calculate the new ray direction and continue from there. Usually there's some amount of reflection, too (the amount varying based on the angle of the incoming ray with respect to the surface), so you might spawn two rays: one continuing through the object, and the other bouncing off.

What's much harder to do is simulating the light that's refracted or reflected off of objects (called "caustics"), like the light on the bottom of a swimming pool. To do that in a physically correct way generally requires falling back on some kind of global illumination technique like path tracing or photon mapping.

The geometry in my raytracer can either be ray-traced primitives, like spheres or boxes, or it can be "signed distance functions" (SDFs), which let you define all kinda of crazy shapes. Inigo Quilez does a good job of explaining SDFs here: https://www.iquilezles.org/www/articles/distfunctions/distfu... The refraction math in my code is around line 799 or 809 depending on what you're looking for. There are a few errors in the refraction code in this version. :/ But caustics are handled well by my renderer.
If you think that shader is impressive this one will blow your mind: https://www.shadertoy.com/view/3lsSzf
Pity I can only vote once, this is amazing work, thanks for sharing.
Are there any good resources for learning how to render dichroic thin films with multiple internal reflections and refraction indexes? Basically, I would love to render something like this[1] in real time.

[1] https://www.youtube.com/watch?v=BVQ34NlTi-E?mute=1

Look into spectral rendering.

For a very simple form, you can sample a specific wavelength with each ray, trace it using wavelength-dependent refraction indices (e.g., from the Sellmeier equation) whenever you compute refraction or internal reflection. Then tint the ray by the color and visible intensity of the wavelength when averaging it into the image pixel.

For a lot more detail, see e.g., "Efficient Spectral Rendering on the GPU for Predictive Rendering" (https://hal.inria.fr/hal-03331619/document).

A naive spectral path tracer can be less complex (code wise) than a "normal" one. You just trace each photon with its wavelength instead of tracing RGB (or some other full spectrum) per path. Since this abstraction is closer to physics, it makes a lot of code easier to reason about. Wavelength dependent refraction is straightforward, you just use the index of refraction for the photon you are tracing.

I find this code base extremely easy to read despite not being a C++ (It has that Carmackian quality). Pretty easy to use to port to the language of your choice. https://github.com/TomCrypto/Lambda To get the kind of effect you see in the youtube video you'd render diamond or something with extreme refraction.

This is far from real time though of course. It will run an order of magnitude slower than anything you can do with full spectrum rays. So doing it in realtime would probably be just like subsurface scattering is done in real time: you just have to cheat. Perhaps there can be some inspiration from the real thing though.

I'm just an amateur who played around with rendering in various ways, raytraced and rasterized. In order to get the results similar to the video (without hacks), you're gonna need caustics, refraction and reflections, all together with realistic lightning. You're not gonna be able to get that looking nice without a physically accurate render engine, which will most likely be using raytracing. And raytracing (compared to rasterization) is not gonna be able to run anywhere near real-time (more like at least seconds per frame, if you really ramp down the samples [image will be more noisy]).
Or you can fit one on a business card:

https://www.kevinbeason.com/smallpt/

Also what I was thinking of. Fabien Sanglard did a nice article[0] about a business card raytracer Andrew Kensler made awhile back, too (HN discussion[1]).

[0] https://fabiensanglard.net/rayTracing_back_of_business_card/

[1] https://news.ycombinator.com/item?id=6425714

That's a big business card. :)
> you could write your own scheduler or use a library

I never wrote ray tracers, but in my experience the best way to parallelize things is usually “none of the above”.

C++ runtime has a built-in scheduler, OpenMP https://www.openmp.org/ If the algorithm needs to dynamically spawn tasks (graph search, flood fill or similar), Windows kernel has another one, see SubmitThreadpoolWork API.

These things tend to work better than third-party libraries, and are way easier to consume than developing custom schedulers.

> C++ runtime has a built-in scheduler, OpenMP

Is OpenMP part of the C++ standard these days?

I don't think so. I believe OpenMP qualifies as a (very mature and widely supported) library as well as language extension.

Assuming it meets your needs it has (AFAIK) support from all the major compilers (Intel, MS, GCC, LLVM) as well as being able to abstract over GPUs and other sorts of hardware.

No, but it’s supported by all modern compilers. In practice, that’s almost as good as being a part of the language standard.

The level of support varies, but OpenMP 2.0 standard is supported by all 4 of them.

There are a few more than 4.
Technically, sure. But practically — what’s your estimation how many C++ programmers target anything besides these 4, gcc, clang, vc++ or intel? My estimation is [ 0.1% .. 1% ]
Plenty, for example HPC tends to favour IBM's XL compilers.

Embedded tends to have their own vendor forks, even if based on GCC/clang, it isn't the same thing.

Then we have game consoles as well.

Intel TBB has a steep initial learning curve, but is incredible once you've passed that initial hump. If I was working on a greenfield project, I woudl start with TBB.
> Now you want to actually trace something. There is at least three ways to it:

> * write all intersection and BVH code by yourself;

> * build and use open-source third-party library;

> * use something like Intel Embree.

How many more useful ways are there? Pretty much the only better thing that comes to my mind is "study something like Embree and then write a shader compiler that produces tracing code on-the-fly", and even that perhaps might not be actually better (although it's my understanding that OSL may actually be doing something like this).

Nice resource but the headline really brings up the question about what language we should use to "quantify" code. I'd go with something like C or even Fortran: expressive enough to do this kind of work but low-level enough that we can easily imaging the assembly code behind it.
I often feel sorry people are shy about inventing new languages and using them in production nowadays. I'd rather have a reasonably different language for every class of problems which would eliminate boilerplate code.
Sounds like it'd be great for job security, too.
This article is very good! It covers many beautiful techniques and has many beautiful images. However, I would quibble with the title; if you want to know how much boilerplate code you need to write a ray-tracer, you will not find the answer in it.

I wrote the following programs in an attempt to get a better answer to that question.

http://canonical.org/~kragen/sw/aspmisc/my-very-first-raytra... four pages of C

http://canonical.org/~kragen/sw/dev3/raytracer1k.clj 1K of Clojure, condensed down from

http://canonical.org/~kragen/sw/dev3/circle.clj a page of unfactored but not obfuscated Clojure, translated into

http://canonical.org/~kragen/sw/dev3/circle.js a page of JS in node.js

https://gitlab.com/kragen/bubbleos/blob/master/yeso/sdf.lua a page of Lua using signed distance function raymarching that runs in real time at a lousy framerate; to run it, git clone the repo, install the prerequisites listed in the README, and run `make` in the yeso directory, before running sdf.lua.

But none of these can load triangle meshes, render caustics, render area light sources, average multiple samples, do bidirectional rendering, etc.

Reznik says, "A lot of projects using something like PPM file format, which is very simple, but in the end, you need to write ... extra code for this simplicity (like tone mapping, converting from floating point values to uint8, etc.)," but the amount of extra code required is really very minimal:

    /* PPM P6 file format; see <http://netpbm.sourceforge.net/doc/ppm.html> */

    static void
    output_header(int ww, int hh)
    { printf("P6\n%d %d\n255\n", ww, hh); }

    static unsigned char
    byte(double dd) { return dd > 1 ? 255 : dd < 0 ? 0 : dd * 255 + 0.5; }

    static void
    encode_color(color co)
    { putchar(byte(co.x)); putchar(byte(co.y)); putchar(byte(co.z)); }
But on some platforms you don't need even that much; my Clojure raytracer[s] just use[s] javax.imageio.ImageIO/write to encode a JPEG. (Admittedly, that one in particular cheats pretty shamelessly on tone mapping!)
> like tone mapping

You can can even add a fairly basic kind of Reinhard tone mapping, plus a 2.2 gamma and output dithering if you change the second function to:

    static unsigned char
    byte(double dd) { return pow(dd / (1.0 + dd), 1.0/2.2) * 255.0 + drand48(); }
Having spent over a decade working on one of the major commercial renderers, I'll say that there can be a lot of complexity involved in a production renderer, but you can still get pretty far with a surprisingly small amount of code.
This is awesome! I should have put something like that in there, but I wouldn't even have thought of the dithering, and since I don't know much about graphics I didn't know about Reinhard tone mapping.

There's an interesting thing that happens where sometimes if you constrain yourself to 1% or 0.1% of the code you would normally use, you end up getting interesting uncontrolled effects. cf. http://canonical.org/~kragen/bytebeat

The underlying algorithms of ray tracing, ignoring the details of the graphics system and IO, really are very simple (Snell's law). The 1K of clojure example is pretty much that.

All the rest is pretty uninteresting, in my opinion- the various tricks to make better rendering in shorter times is mostly grad-level math and some computer engineering, while the asset loading is a long solved problem with ultra-mature libraries.

None of my examples actually implement Snell's law, just diffuse and sometimes specular reflection, but I agree that adding Snell's law (and thus refraction) does not add much complexity. Recently it's become fashionable to use the Fresnel equations and "more filmic" tone mapping to get less plasticky images, too.

I think my interests are probably pretty different from yours, because I find the details of the graphics system and I/O pretty interesting, in particular how they can be improved. I also think the various tricks for better rendering in shorter time are super interesting, and even the question of how to do liability loading has some very interesting new answers, like FlatBuffers. Probably the liability-loading strategies that we came up with in the previous millennium for spinning rust that transferred 40 megabytes per second after an 8 000 000 nanosecond seek time are not optimal for SSDs that transfer 4000 megabytes per second with a 1000 nanosecond "seek time".

Another thing worth mentioning is the question of how you model the 3-D shapes you want to render in the first place, which includes considerations of HCI, sometimes physics simulation, and so on. One of the most interesting things about SDFs is the tempting possibility of an efficiently renderable representation that naturally supports operations like CSG and filleting (though not usually both at once!)

That is to say, I think the stuff these minimal raytracers leave out is actually very interesting indeed. But I also think it's very interesting to see what's left over when you do leave it out: a relatively small amount of math and code that can still produce visually arresting images.

I'm confused- don't all ray tracers use snell's law or is there a different equation for reflection (https://en.wikipedia.org/wiki/Specular_reflection)? Nearly everybody I've worked with who's implemented a ray tracer says there's only one thing that matters, and that's implementing the reflection equation (I mean for first order ray tracing, not interesting stuff).

As for asset loading, like I said, I think it's a solved problem. When I start Cyperpunk 2077 on my machine (fresh, no cache), it takes less than 30 seconds to get to a playable state and my SSD is basically streaming data into RAM as fast as it can.

What do you mean by liability loading and liability pipeline?
Nice! APL-family languages are really great at this kind of thing; I should see how small I can squinch a raytracer with just NumPy. Stevan Apter's programs (like that one) constantly make me wish I knew K: even if I don't want my code to look like that, it seems like it would be great to be able to type it in that way.

That also leads to a one-page OCaml version and a two-page C++ version by Jon D. Harrop (the Flying Frog guy), both also using PPM output and rendering only spheres, much like most of my examples: https://web.archive.org/web/20070605173709/http://www.ffcons...

He also wrote an SML version https://web.archive.org/web/20070522030632/http://www.ffcons... and a Java version, and others ported it to Scheme and Common Lisp https://web.archive.org/web/20070515185350/http://www.ffcons..., those these seem to be lost now.

Still others ported it to Io http://mike-austin.com/io/ray.io and Factor https://github.com/factor/factor/blob/master/extra/benchmark... (3 pages).

In https://www.pouet.net/prod.php?which=83222 Holtsetio wrote a raytracer in two pages of MySQL SQL, but it's sort of obfuscated. It emits the output in BMP format (almost as simple as PPM) and uses lots of imperative MySQL extensions to SQL, and it ended up as 10 KiB instead of the 1 KiB of my Clojure version. But it handles triangles, not just spheres, so it can render the Stanford bunny.

https://github.com/chunky/sqlraytracer/blob/master/raytracer... is instead about three pages of SQL, using recursive CTEs instead of imperative assignments, and supporting only spheres but with different materials, like my C version. He's using recursive CTEs as implemented in Postgres: compliant with ANSI SQL but not implemented in most other SQL engines. In particular, he started out with SQLite but it wasn't strong enough.

Is it fair to think of programming in K as directly writing bytecode for an array VM?
No

EDIT: it may look gibberish but it's still a structured source file, that requires (simple) lexical analysis etc

You may be happy to learn that in modern Node, the 3200x2400 example which used to take 6.8 seconds now runs in 0.25 seconds. I unfortunately don't have a clojure setupt to compare against.
For me (on AMD Ryzen 9 5950X, Arch Linux, Clojure 1.10.3 [OpenJDK 17.0.1] and NodeJS 17.4.0), circle.js takes ~0.15 seconds and the circle.clj takes ~0.19 seconds. After running circle.clj 100 times in a running JVM though, it takes the time down to ~0.092 seconds for each run.
Well of course I now have to ask how long it takes in node after running the script a hundred times
Does NodeJS do any on-the-fly optimization like the JVM does?
Yes, if you dig around V8 performance profiles you'll see some massive "Optimizing Code" blocks that appear at the bottom of a frequently-hit routine once, then subsequent calls to that same routine will run faster.

That being said, while I haven't investigated this super thoroughly, in my experience the delta is usually small.

That's astounding! Maybe V8 is catching up to LuaJIT's performance?
On the PPM side, a very easy alternative is to use stb_image (even though I abhor header only libs), or OS specific ones like GDI+ / CoreImage, if using something like C++.

On .NET side, I used ImageSharp.

Thanks for sharing the Clojure version of a RayTracer.

I'm glad you enjoyed it!
Really annoying that he never gives a single answer to the question posed in the title of the article. Such a troll for a title . .
read this book: http://raytracerchallenge.com/

I'm halfway through and at 1531 lines in Elixir including the tests via doctest :)