VR with ClojureScript, three.js and Google Cardboard


Not so long ago I read that John Carmack uses Lisp for VR, and also I found experiments for Cardboard with three.js and a good article about coding with three.js for Cardboard. I thought it will be good to combine this stuff and try to play with some poor man’s VR with cheap Cardboard and a browser. And use benefits of ClojureScript like figwheel with REPL and livecoding.

So for drawing I decided to use three.js with StereoEffect.js for drawing for both eyes and with DeviceOrientationControls.js for tracking head movements.

First of all we need to prepare a scene, a camera, a renderer for both eyes and a controls for tracking head movements:

(defn get-camera
  "Creates camera with desired aspect ratio."
  (doto (js/THREE.PerspectiveCamera. 75 (/ (.-innerWidth js/window)
                                           (.-innerHeight js/window))
                                     0.1 1000)
    (.. -position (set 0 5 0))))

(defn get-canvas
  "Returns canvas that will be fullscreened after a click."
  (let [canvas (.getElementById js/document "canvas")]
    (.addEventListener canvas "click" #(.webkitRequestFullscreen canvas))

(defn get-renderer
  "Creates renderer for both eyes."
  (let [canvas (get-canvas)
        webgl (js/THREE.WebGLRenderer. #js {:canvas canvas})
        renderer (js/THREE.StereoEffect. webgl)]
    (.setSize renderer (.-innerWidth js/window) (.-innerHeight js/window))

(defn set-orientational-contorlls
  "Set in atom controlls that tracks device (and head) movements."
  [controlls camera e]
  (when (and (.-alpha e) (not @controlls))
    (let [ctrls (js/THREE.DeviceOrientationControls. camera true)]
      (.connect ctrls)
      (.update ctrls)
      (reset! controlls ctrls))))

(defn get-controlls
  "Returns atom with controlls."
  (let [controlls (atom)]
    (.addEventListener js/window "deviceorientation"
                       #(set-orientational-contorlls controlls camera %))

(def scene (js/THREE.Scene.))
(def camera (get-camera))
(def renderer (get-renderer))
(def controlls (get-controlls camera))

And then functions for rendering:

(defn do-render
  "Called on each render."

(defn render
  "Called on each render. This function not reloads on changes."
  (js/requestAnimationFrame render)
  (when @controlls
    (.update @controlls))
  (.updateProjectionMatrix camera)
  (.render renderer scene camera))

; Not reload render function when code changed:
(defonce render-started (atom false))
(when-not @render-started
  (reset! render-started true))

When we need to do some actions (change color, rotate, etc) on each render — we need to change do-render.

Well, enough with boilerplate, look at some example — two rotating rectangles, you can see this example in the video. The code isn’t good looking, three.js api not so very friendly with ClojureScript, but it readable:

(defn create-rect
  "Creates a rect with given color and xyz."
  [color x y z]
  (js/THREE.Mesh. (js/THREE.BoxGeometry. x y z)
                  (js/THREE.MeshBasicMaterial. #js {:color color})))

; Creates white rect:
(def rect (create-rect "white" 1 1 1))
(.. rect -position (set 1 1 0))
(.add scene rect)

; Creates yellow rect:
(def other-rect (create-rect "yellow" 1 2 3))
(.. other-rect -position (set -0.5 -2 0))
(.add scene other-rect)

(defn do-render
  "Called on each render."
  ; Rotates white rect:
  (set! (.. rect -rotation -x)
        (+ (.. rect -rotation -x) 0.01))
  (set! (.. rect -rotation -y)
        (+ (.. rect -rotation -y) 0.01))
  ; Rotates yellow rect:
  (set! (.. other-rect -rotation -x)
        (- (.. other-rect -rotation -x) 0.1))
  (set! (.. other-rect -rotation -y)
        (+ (.. other-rect -rotation -y) 0.1)))

Let’s see it in action, it’s not so fabulous without Cardboard, but livecoding makes it more interesting:

This way to work with Cardboard has a few problems: we can’t use the magnet trigger and we don’t have special lens distortion correction. And I guess next time I’ll try to use Cardboard SDK for Android with Clojure on Android.

Gist with the sources.

comments powered by Disqus