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
(ns me.grison.raytraclj.vec
(:require [clojure.core :as clj]))
(defn mute [op
[^float x1 ^float y1 ^float z1]
[^float x2 ^float y2 ^float z2]]
[(op x1 x2) (op y1 y2) (op z1 z2)])
(defn + [v1 v2]
(mute clj/+ v1 v2))
(defn - [v1 v2]
(if (number? v2)
(map #(clj/- v2 %) v1)
(mute clj/- v1 v2)))
(defn * [v1 v2]
(if (number? v2)
(map #(clj/* v2 %) v1)
(mute clj/* v1 v2)))
(defn / [v1 v2]
(if (number? v2)
(if (zero? v2)
1
(* v1 (clj// 1 v2)))
(mute clj// v1 v2)))
(defn • [v1 v2]
(reduce clj/+ (* v1 v2)))
(defn ⨯ [[x1 y1 z1] [x2 y2 z3]]
[(clj/- (clj/* y1 z3) (clj/* z1 y2))
(clj/- (clj/* z1 x2) (clj/* x1 z3))
(clj/- (clj/* x1 y2) (clj/* y1 x2))])
(defn squared-length [[x y z]]
(clj/+ (clj/* x x) (clj/* y y) (clj/* z z)))
(defn length [v]
(Math/sqrt (squared-length v)))
(defn unit-vector [v]
(let [l (length v)]
(map #(clj// % l) v)))
(defn x [v]
(first v))
(defn y [v]
(second v))
(defn z [v]
(last v))
(defn reflect [v n]
(let [m (* n (clj/* (• v n) 2))]
(- v m)))
(defn refract [v n ni-over-nt]
(let [uv (unit-vector v)
dt (• uv n)
discriminant (clj/- 1.0
(clj/* ni-over-nt ni-over-nt (clj/- 1 (clj/* dt dt))))]
(when (pos? discriminant)
(let [uv-ndt (- uv (* n dt))
n-discrim (* n (Math/sqrt discriminant))]
(- (* uv-ndt ni-over-nt) n-discrim)))))