I was preparing an article about Clojure and buddy (it is coming) and then my friend David told me:

You already know your backend, APIs and stuff. Why don’t you get out of your comfort zone and do some 3D.

He was right 🤠

I thought about writing a raytracer because it’s cool and I know nothing, so I searched on the web and found this gem named Ray Tracing in One Weekend.

Then I told my friend David, I’m gonna write this raytracer but in Clojure and he told me:

Dude I already sent you this book two days ago.

Fair enough, so I just launched IntelliJ and started to code.

As the book suggests, it took me the weekend. But hey, it was a lot of fun, learned a lot (and forgot pretty quickly 😅, I wouldn’t be able to come up with all these formulas by heart). But it opens my perspective on other fields in IT.

OMG this is way harder to get right than backends! I had some problem of reflections, how do you debug that? ahah so much fun. I’ve thought multiple times boy you’re so dumb, and I think I am 🤪.

The code is on agrison/raytraclj, I’ll just post the latest version I’ve committed below.

The final scene from the first book takes around 35 minutes to render on my computer (Ryzen 9 3900X) with multi-threading enabled.

So this is fairly slow, the C++ implementation in the book took around 14 minutes. I did not try to improve that much the performance. Just tinkered with Aparapi to move some computations on the GPU but was not able to. I should look at neanderthal, maybe it can help, I don’t know.

All in all, just do it if you have some time, it was a cool experiment, and the book is a gem ❤.

Edit:

Someone pointed me to the clojure2d project on Github (via Reddit) and digging in it I fount that it contains an implementation of the same ray tracer. The implementation in there is way faster than mine (based on fastmaths) and it has real-time rendering on an image canvas.

You can take a look at it over there: https://github.com/Clojure2D/clojure2d-examples/tree/master/src/rt_in_weekend

Just be sure to change map to pmap at line 80 of file ch12_random_scene.clj , it’s like 5 to 6 times faster and it renders on a canvas view in real time, really great work!

I definitely need to change my code to use fastmaths to see what improvements it gives!

Until next time!

camera.clj

( ns me.grison.raytraclj.camera ( :require [ me.grison.raytraclj.ray :as ray ] [ me.grison.raytraclj.vec :as vec ])) ( defn random-in-unit-disk [] ( loop [ p ( vec/- ( vec/* [( rand ) ( rand ) 0 ] 2 ) [ 1 1 0 ])] ( if ( < ( vec/ • p p ) 1 ) p ( recur ( vec/- ( vec/* [( rand ) ( rand ) 0 ] 2 ) [ 1 1 0 ]))))) ( defn make [ look-from look-at vup vfov aspect aperture focus-dist ] ( let [ lens-radius ( / aperture 2 ) theta ( / ( * vfov Math/PI ) 180 ) half-height ( Math/tan ( / theta 2 )) half-width ( * aspect half-height ) origin look-from w ( vec/unit-vector ( vec/- look-from look-at )) u ( vec/unit-vector ( vec/ ⨯ vup w )) v ( vec/ ⨯ w u ) lower-left-corner ( vec/- ( vec/- ( vec/- origin ( vec/* u ( * half-width focus-dist ))) ( vec/* v ( * half-height focus-dist ))) ( vec/* w focus-dist )) horizontal ( vec/* u ( * 2 half-width focus-dist )) vertical ( vec/* v ( * 2 half-height focus-dist ))] { :lower-left-corner lower-left-corner :horizontal horizontal :vertical vertical :origin origin :lens-radius lens-radius :u u :v v :w w })) ( defn get-ray [{ :keys [ lower-left-corner horizontal vertical origin lens-radius u v ]} s t ] ( let [ rd ( vec/* ( random-in-unit-disk ) lens-radius ) offset ( vec/+ ( vec/* u ( vec/x rd )) ( vec/* v ( vec/y rd )))] ( ray/make ( vec/+ origin offset ) ( vec/+ lower-left-corner ( vec/- ( vec/- ( vec/+ ( vec/* horizontal s ) ( vec/* vertical t )) origin ) offset )))))

hitable.clj

( ns me.grison.raytraclj.hitable ( :require [ me.grison.raytraclj.vec :as vec ] [ me.grison.raytraclj.ray :as ray ])) ( defprotocol Hitable ( hit [ this r t-min t-max ])) ( defn hit-record [ r t center radius material ] ( let [ p ( ray/point-at-parameter r t )] { :t t :p p :normal ( vec// ( vec/- p center ) radius ) :material material })) ( defrecord Sphere [ center radius material ] Hitable ( hit [ this r t-min t-max ] ( let [ oc ( vec/- ( ray/origin r ) ( :center this )) a ( vec/squared-length ( ray/direction r )) half-b ( vec/ • oc ( ray/direction r )) c ( - ( vec/squared-length oc ) ( * ( :radius this ) ( :radius this ))) discriminant ( - ( * half-b half-b ) ( * a c ))] ( when ( pos? discriminant ) ( let [ root ( Math/sqrt discriminant ) temp ( / ( - ( - half-b ) root ) a )] ( if ( and ( < temp t-max ) ( > temp t-min )) ( hit-record r temp ( :center this ) ( :radius this ) ( :material this )) ( let [ temp ( / ( + ( - half-b ) root ) a )] ( when ( and ( < temp t-max ) ( > temp t-min )) ( hit-record r temp ( :center this ) ( :radius this ) ( :material this )))))))))) ( defn hits [ world r t-min t-max ] ( let [ closest-so-far ( atom t-max ) record ( atom nil )] ( doseq [ i ( range 0 ( count world ))] ( do ( if-let [ rec ( hit ( get world i ) r t-min @ closest-so-far )] ( do ( reset! closest-so-far ( :t rec )) ( reset! record rec ))))) @ record ))

material.clj

( ns me.grison.raytraclj.material ( :require [ me.grison.raytraclj.vec :as vec ] [ me.grison.raytraclj.ray :as ray ])) ( defn random-in-unit-sphere [] ( loop [ p ( vec/- ( vec/* [( rand ) ( rand ) ( rand )] 2.0 ) [ 1.0 1.0 1.0 ])] ( if ( > ( vec/squared-length p ) 1.0 ) p ( recur ( vec/- ( vec/* [( rand ) ( rand ) ( rand )] 2.0 ) [ 1.0 1.0 1.0 ]))))) ( defprotocol Material ( scatter [ this r-in rec ])) ( defrecord Lambertian [ a ] Material ( scatter [ this r-in rec ] ( let [ target ( vec/+ ( vec/+ ( :p rec ) ( :normal rec )) ( random-in-unit-sphere )) scattered ( ray/make ( :p rec ) ( vec/- target ( :p rec )))] { :ok true :attenuation ( :a this ) :scattered scattered }))) ( defrecord Metal [ a f ] Material ( scatter [ this r-in rec ] ( let [ fuzz ( if ( < f 1 ) f 1 ) reflected ( vec/reflect ( vec/unit-vector ( :direction r-in )) ( :normal rec )) scattered ( ray/make ( :p rec ) ( vec/+ reflected ( vec/* ( random-in-unit-sphere ) fuzz ))) final ( vec/ • ( :direction scattered ) ( :normal rec ))] { :ok ( pos? final ) :attenuation ( :a this ) :scattered scattered }))) ( defn schlick [ cosine ref-idx ] ( let [ r0 ( / ( - 1 ref-idx ) ( + 1 ref-idx )) r0 ( * r0 r0 )] ( + r0 ( * ( - 1 r0 ) ( Math/pow ( - 1 cosine ) 5 ))))) ( defrecord Dielectric [ ref-idx ] Material ( scatter [ this r-in rec ] ( let [ reflected ( vec/reflect ( :direction r-in ) ( :normal rec )) attenuation [ 1.0 1.0 1.0 ] dot-dir-normal ( vec/ • ( :direction r-in ) ( :normal rec )) ni-over-nt ( if ( pos? dot-dir-normal ) ( :ref-idx this ) ( / 1.0 ( :ref-idx this ))) outward-normal ( if ( pos? dot-dir-normal ) ( vec/- [ 0 0 0 ] ( :normal rec )) ( :normal rec )) cosine ( if ( pos? dot-dir-normal ) ( / ( * ( :ref-idx this ) ( vec/ • ( :direction r-in ) ( :normal rec ))) ( vec/length ( :direction r-in ))) ( / ( - ( vec/ • ( :direction r-in ) ( :normal rec ))) ( vec/length ( :direction r-in )))) refracted ( vec/refract ( :direction r-in ) outward-normal ni-over-nt ) reflect-prob ( if ( not ( nil? refracted )) ( schlick cosine ( :ref-idx this )) 1.0 )] ( if ( < ( rand ) reflect-prob ) { :ok true :attenuation attenuation :scattered ( ray/make ( :p rec ) reflected )} { :ok true :attenuation attenuation :scattered ( ray/make ( :p rec ) refracted )}))))

ray.clj

( ns me.grison.raytraclj.ray ( :require [ me.grison.raytraclj.vec :as vec ])) ( defn make [ origin direction ] { :origin origin :direction direction }) ( defn origin [ ray ] ( :origin ray )) ( defn direction [ ray ] ( :direction ray )) ( defn point-at-parameter [ ray t ] ( vec/+ ( origin ray ) ( vec/* ( direction ray ) t ))) ( defn hit-sphere [ center radius { :keys [ origin direction ]}] ( let [ oc ( vec/- origin center ) a ( vec/ • direction direction ) b ( * 2.0 ( vec/ • oc direction )) c ( - ( vec/ • oc oc ) ( * radius radius )) discriminant ( - ( * b b ) ( * 4 a c ))] ( if ( neg? discriminant ) -1.0 ( / ( - ( - b ) ( Math/sqrt discriminant )) ( * 2.0 a )))))

vec.clj