Talking to Three.js from CLJS

Original Pen

This is a port of the excellent pen by ycw into clojurescript. Three.js has an API which is all about mutation and chaining (check all the set, get calls!), which makes it particularly challenging to port directly to CLJS.

ycw also used a fun technique, where they defined a custom property $x on the groups of meshes, allowing gsap to read and overwrite this position via a single property, even though the value is held and written in a nested property of the group.

First things first.

Let's import all the libraries we'll be using from skypack and expose them to window so CLJS can get its hands on them. As far as I know, there isn't a good way to do this just yet directly from cljs..? I guess we could use the dynamic import(url).then(...) API as well, but can leave that for next time - would be an interesting challenge on its own.

<script type="module">
import * as $3 from 'https://cdn.skypack.dev/three@0.125.0/build/three.module.js?min';
import { OrbitControls } from 'https://cdn.skypack.dev/three@0.125.0/examples/jsm/controls/OrbitControls.js?min';
import { EffectComposer } from 'https://cdn.skypack.dev/three@0.125.0/examples/jsm/postprocessing/EffectComposer.js?min';
import { RenderPass } from 'https://cdn.skypack.dev/three@0.125.0/examples/jsm/postprocessing/RenderPass.js?min';
import { UnrealBloomPass } from 'https://cdn.skypack.dev/three@0.125.0/examples/jsm/postprocessing/UnrealBloomPass.js?min';
import { gsap } from 'https://cdn.skypack.dev/gsap?min';

window.libs = {
    $3,
    OrbitControls,
    EffectComposer,
    RenderPass,
    UnrealBloomPass,
    gsap
};
</script>

Start doing things.

(ns sketch.core
  ;; Klipse has some magic where it loads applied science's interop library
  ;; somehow, just by being here. Cool!
  (:require [applied-science.js-interop :as j]))

(defn sketch []
  ;; Boot
  (let [{:keys [OrbitControls
                EffectComposer
                RenderPass
                UnrealBloomPass
                gsap
                $3]} (j/lookup (j/get js/window :libs))
        el (js/document.getElementById "mc")
        renderer ($3.WebGLRenderer. #js {:canvas    el
                                         :antialias true})
        scene ($3.Scene.)
        camera ($3.PerspectiveCamera. 75 2 0.1 1000)
        controls (OrbitControls. camera el)]
    (js/window.addEventListener "resize"
                                (fn []
                                  (let [w (.-clientWidth el)
                                        h (.-clientHeight el)]
                                    (doto renderer
                                      (.setSize w h false)
                                      (.setPixelRatio (.-devicePixelRatio js/window)))
                                    (doto camera
                                      (j/assoc! :aspect (/ w h))
                                      (.updateProjectionMatrix)))))
    (.dispatchEvent js/window (js/window.Event. "resize"))

    ;; Setup

    (j/call-in camera [:position :set] -2 8 -2)

    (let [steps 1000
          extrude-steps (* 1024 2)
          extrude-depth 0.5
          half-extrude-depth (/ extrude-depth 2)
          thickness 1
          half-thickness (/ thickness 2)
          repetition 4
          scale-x 100
          light0 (doto ($3.DirectionalLight. "white" 1)
                   (j/call-in [:position :set] 2 -2 -2))
          _ (j/call scene :add light0)
          light1 (doto ($3.DirectionalLight. "white" 1)
                   (j/call-in [:position :set] -2 2 2))
          _ (j/call scene :add light1)
          curve ($3.CurvePath.)
          shape ($3.Shape.)]

      (loop [i 0
             prev ($3.Vector3.)
             curr ($3.Vector3.)]
        (let [t (/ i steps)
              ft (js/Math.sin (* t
                                 (.-PI js/Math)
                                 2
                                 repetition))
              x (* scale-x (- t 0.5))
              y ft]
          (if (zero? i)
            (do
              (j/call prev :set x y 0)
              (recur (inc i)
                     prev
                     curr))
            (when (< i steps)
              (let [curr ($3.Vector3. x y 0)
                    line ($3.LineCurve3. prev curr)]
                (j/call curve :add line)
                (recur (inc i)
                       curr
                       nil))))))

      (doto shape
        (j/call :moveTo (- half-extrude-depth) (- half-thickness))
        (j/call :lineTo (- half-extrude-depth) half-thickness)
        (j/call :lineTo half-extrude-depth half-thickness)
        (j/call :lineTo half-extrude-depth (- half-thickness))
        (j/call :lineTo (- half-extrude-depth) (- half-thickness)))

      (let [geom ($3.ExtrudeGeometry. shape #js {:extrudePath  curve
                                                 :bevelEnabled false
                                                 :steps        extrude-steps})
            gs (reduce (fn [acc i]
                         (let [I 16
                               color (doto ($3.Color.)
                                       (j/call :setHSL (* 2 (/ i I)) 0.8 0.5))
                               mat ($3.MeshPhongMaterial. #js {:color color})
                               mesh ($3.Mesh. geom #js [nil mat])
                               g ($3.Group.)]
                           (j/call g :add mesh)
                           (js/Object.defineProperty
                             g
                             "$x"
                             #js {:get #(j/get-in g [:position :x])
                                  :set #(j/assoc-in! g [:position :x] %)})
                           (j/assoc-in! mesh [:position :z] (-> i
                                                                (/ (- I 1))
                                                                (- 0.5)
                                                                (* 20)))
                           (j/assoc-in! mesh [:position :x] (mod i 8))
                           (j/call scene :add g)
                           (j/call acc :push g)
                           acc))
                       #js []
                       (range 16))
            res (j/call renderer :getDrawingBufferSize ($3.Vector2.))
            composer (EffectComposer. renderer)]
        (j/call js/window :addEventListener "resize"
                (fn []
                  (let [{:keys [clientWidth clientHeight]} (j/lookup el)]
                    (j/call renderer :getDrawingBufferSize res)
                    (j/call composer :setPixelRatio (j/get js/window :devicePixelRatio))
                    (j/call composer :setSize clientWidth clientHeight))))
        (doto composer
          (j/call :addPass (RenderPass. scene camera))
          (j/call :addPass (UnrealBloomPass. res 1 0.5 0.1)))
        (j/call renderer :setAnimationLoop (fn []
                                             (j/call composer :render)
                                             (j/call controls :update)))
        (j/call gsap :to gs #js {:$x       (/ scale-x repetition)
                                 :duration 1
                                 :ease     "none"
                                 :repeat   -1})))


    :done))

(sketch)


Ouptut


Tags: cljs 3d

Copyright © 2023 Dan Peddle RSS
Powered by Cryogen