3
3

Voxel Terrain using Squint

18d 9h ago by lemmy.ml/u/yogthos in clojure@lemmy.ml

the code is embedded in the url

https://squint-cljs.github.io/squint/?src=gzip%3AH4sIAAAAAAAAE81Z3XLbNha%2B91OcdW%2FIZEmTsiXb8qZtGie7nm1n22SyvfDkAhJBEREFcknIIp3JTB%2Bi77L3%2Byh9kh38EABJyFKvEjVTS8T5zjk4%2FwC9BKewQWVQk0cM03jin3jdow2q1zCNY%2F%2Fk5OYG%2Fvj9tz9%2B%2Fw1%2BroolTrYVyoHhqkKEqpUv9e9EKEw7bYIMk1XG4L6B9sMJgJdjBve0Ae8MGr1T%2FwTUh7Z8pXWslAQ%2B1mc%2FIZaFP99xVgDec%2FCewXkEXrdSE8of0QbOoSS%2Br%2BGcbjqma2ESXo4oJ1P%2Bf028LGrFdCpIXWwuQiXQ5hNfjUi955wPbX2YjcTGfbFmL9cDsZ1CLVw5hEZ7uMQXe7WPHcrPYI8RJvFedeLrAZ%2BrKf%2BhQkKGQvDtssiLCu4zEw8VeMuCJuD9DTI4n%2FoAk6l2fPcRi9PIB7iI3Iszvhhf7VmNo8jn9j%2BP5M4CjQgnRuMB5mIqMJdXBiP4ROFsL%2BZSYuIoMhjOJw7jfZhJLHWLLywM5xOHUwdmjvMai2%2BTSWSWVyMzmu1mMHGyMlbl4uNLLZ7jn4DMOsh0qiGcjVvhngMuBw6Iw6tD9o8vJkP7nx%2B2v9FM8InCy4P2nwzsHzl1M%2Fb3nsPE8rPgI3QzoIXLKyo4Mjg%2FwiuX0Z%2F1ypVl4iO9MkyLOIwOecWGHOuVCxsjvTI95JWZjZFemR30Svy0V5Y52pTgpRTuHz6AtyEUJjyUCWXw4Pu%2B6jALwoKikn%2FrjKQsyHHKBOnkoq%2FCkMaTIhY%2BxLOjKFc%2BXA0I5UIlSqqcBGQR3aBS1N%2F3hLKrl1WF2pDvVk8Oun36aoAQNZejNOx8cgB34iUFIxtcw73px6Jm6%2BemgytziWqe8W8b1EA0sKs3GAoaaO1mAQAkaVRXt0YAaJQ3ADxUY2bZgNNnfm9R71Tw6vecjIvrzU%2B3%2F%2FoJasy25Rcem56eqIRZl4g%2BoBq8cFlhxPDrHG8wZXwoSorlVnw%2FlTSnwl5ejdlfwAsDkoB87sPpQ9HgPOjIbKqatTkOy6ImjBTUIFLS4MRFyorSUEUuChHcT5M83tEEN4bo%2Bvr62kWXkLrMUWsIF3mxXEvKEJUlpsmrjOQJRyyKpLXN4ncovzMloWnxtCET8jC2IodpG%2FIfDk0lkYjI086Yc2HDG1aU8zgqmxtuF%2FlNhOX8mzRNbxZouV5VxZYm82q1QF70V%2FFfOPNvSpQkhK7ms7IBAVsUVYKroEIJ2dbzi7K5SQvK5vGkbGBT0KIu0RLfPAaEG3ceR1EU3ZQFoQxXAX7AlNVzWlDcV5%2Fhhr0qKOP7Vzv99eW7W3gBm%2BIBw%2F%2F%2BC7%2BcvYYXwLYV5b%2Fenr2BFyoXj3WE4NtLwHfLCmMKi22a4urrSkJVcWuhYZAghvgmESs28GmuspEfWUhuKth8yRr1tf%2BcbFaCxfD5YpteuegX2%2FR84ni%2B01%2BtMXeemaefzcBdYV4%2FA7kBuDfzti4lK8xU%2BP%2FQ3iX9FBiVCvlRCvCMoBRXv5KEZRy4IzQpdoYu69P9Q5Z8Q6g6Rhd9O8GnS2%2FFR6%2BqhtEtq5KvNtNZXGxHRHDTkcLpRNYu3WA6N3TZf7dBK3zLn3A%2BO9NN%2BGexTfUuVIR6ocR3jLpJAcCrsFC3Hy%2F2x4oa%2BWU4wpjoWbJmtKh1776MKHQwDWeDxTYdzUsmxEYjgZNcR95utKTDL%2FusRpVe6PknXoiS5DWvPD%2BSmmGKKxMJcCqJT%2Fvx2qsS%2F%2BaxCKKsQYVpgqsvWCy69EoqtAsecMXIEuVBTiiGe2lTFQI7PuHwNtkuCsbLxrLIRRLuMkz5iGsv2iHdMj1A8bmJk%2FXGpHahFhRUh2BeFCXcrxUV6zuqSFM%2BIKn5imktfcmsMaFsqbiGdmGLlkOW3KZiuCzyHkWFl9uK81zCWszvik7Lkx9Tpbg%2FA1lupIvvl2gT8KlgE%2FCWvwkyQHSVY8iKijwW1FSyT%2FM1bmtZBHR%2BiDSQKu4g%2B%2FAZvnflZE1omREV%2FvpCRMixjglFPSDitx0DonqJchxkAJOLKDRlOSE1Q3SJ4SqyH9frNpDjaNS8eTO7uJ7dvlLFMExJnivNNZkdGBlJEszvyD7WZ3dWwnbl1B7OCewsf8rBWcEJn4T7MfMof8WWonITOGfokT93BMej3mMvAGRZLmrJ03vWWfFxfGarCdVUyiEOqpJPTEEjb%2FwC%2Fo9z9wWaT3eboNkHajuQkCRhIq4cgEqcFeQJRIo4IEECWqOWIn9KStLNB94ZR2ihao%2B69%2FUwrQvTdlt0Ygh90Jb1zrgDgVtMhqqV6U8Eje3OWqrtPdfO8J4BgaRxHMT5p2779K2ib%2FfQbxpzNEb8toSXpLrx9dX3HljrgrUHYajk%2BeqhVe84KUvjxj57bvZtsJXDgWLRJZabNhODomLPQ0uUNKEEn4kfHv2usg0c08u2rBZCXSI8Ry%2FqCiDpgE7VpP760LzHAk6Zw5JSy7I%2B7gTPeaEQdUS0A1VSojCK%2BPWwLkSh6E7D%2BUpUc0NTbll%2FZNNVP4JoeLp%2FhTa4Ql%2FXqeKpo8ZS6HsC8M3HGj7Nu0oBk%2BnM1OV5636sZmSAyyvrqeyb%2FGAQ6iv1uQo24Ld2YfS5u1sitNwyLT0tqh2qkh%2FQcs3%2Fcn9pBjyl3wpx9tNteVvsaJ%2BwKNbvS0hRXuPeQ0EpHmvxOapZkFZog7tjlhfSYscnxVvEzF0WomSDGKGr7xSZYNP3%2FN85F9HZvrhvD42R2zJBDAfS9%2FYpje%2B9bwEzWrDuRpQvf28sx%2B85o%2FPucCUrBy3YC55XxmnC0T7oVwf6nCWjRapipPGiMFgT0l0swxgSprN6oMEwpEZ6iH2L6wbJfw8gYfao2infjBQH0ZatFVHcBPveS7DB5nplTLNv97Jv97MX78Yc7N32UQm01z36GOzyT39RGbDPceyZMFAZKilGAlWh2COxvypene6VYelxtJTgKCkyaFzzg%2B1690jgnB9sl7ph8qLu2PlBz%2BbSHt9yOsVBvOEY%2Bs4RfEPn2gy0IdQFhFVHacFPW12lWaEN5r4oVZXpVR5xgzc%2Bi6mDiZ1CvYB3BuW4WozcKMSRFDz%2BduXY%2BiB4Hy5nPbI9OaXUdEW%2FtWRB1c4%2B1mcV%2Fs8W1%2BylbEMFfSNMrY3bd4bVrFSb6vWpO8776%2B1Rrn6VIZrkOFjjFu7LCtc1Tr4D%2BWJIHfwSzO0n%2Flo96wHlYF64kRQ0%2BIFPbqpl8VelXbrw0HgBgs%2FpP3H766lvfr%2BsqmL3vjw142OXKnuCyOPyz8VLzT383434c%2Ff%2FSQnBkyJejkT8iFPmEDEKbsE8for37Yi3YHA086DP3eL8lnPuwP18EsiJ9arYxr05gAv2AV%2FbwF6K6qBxwX4ZwmyJGmj3pu6hOV5U4v3ILU7RNmcmfAV5QAsG35uMNmV6nOys2lqx7yjNg9nWkB5RYdT1Wei6VDUX%2BGvcJjyAwStRxQjK7dQV%2Bu27mO3x2JanXC0HD3vuvqNELIvN8ftfRyP5c23kmCbyf4cliyYwJwAA

@yogthos Wow! Damn, that's compact. Q/E appear to do pitch rather than yaw... But damned impressive.

turned out better than I anticipated, and yeah goofed up Q/E, but was too lazy to fix when I realized it 😅

(def map-size 512)
(def map-mask 511)

;; ── Procedural terrain ──────────────────────────────────────────

(defn terrain-height [x y]
  (let [nx (/ x map-size)
        ny (/ y map-size)
        pi js/Math.PI]
    (+ (* 30 (js/Math.sin (* nx 3 pi)))
       (* 35 (js/Math.sin (* ny 2.7 pi)))
       (* 25 (* (js/Math.cos (* nx 5 pi)) (js/Math.sin (* ny 4.3 pi))))
       (* 18 (js/Math.sin (* (+ nx ny) 6 pi)))
       (* 15 (* (js/Math.sin (* nx 9 pi)) (js/Math.cos (* ny 8 pi))))
       (* 10 (* (js/Math.sin (* nx 14 pi)) (js/Math.sin (* ny 13 pi))))
       (* 6  (* (js/Math.cos (* nx 21 pi)) (js/Math.cos (* ny 19 pi))))
       85)))

(defn height->color [h]
  (let [r (cond (< h 35)  25
                (< h 50)  40
                (< h 60)  180
                (< h 100) (+ 30  (* (- h 60)  1.2))
                (< h 145) (+ 78  (* (- h 100) 0.6))
                (< h 175) (+ 100 (* (- h 145) 1.1))
                (< h 210) (+ 140 (* (- h 175) 1.5))
                :else     220)
        g (cond (< h 35)  (+ 30  (* h 2.5))
                (< h 50)  (+ 117 (* (- h 35) 2.5))
                (< h 60)  (+ 155 (* (- h 50) 1.5))
                (< h 100) (+ 70  (* (- h 60) 1.8))
                (< h 145) (+ 142 (* (- h 100) 0.3))
                (< h 175) (+ 105 (* (- h 145) 0.7))
                (< h 210) (+ 120 (* (- h 175) 0.8))
                :else     (+ 200 (* (- h 210) 0.3)))
        b (cond (< h 35)  (+ 100 (* h 3.5))
                (< h 50)  (+ 170 (* (- h 35) 2.5))
                (< h 60)  (+ 80  (* (- h 50) 1.5))
                (< h 100) (+ 30  (* (- h 60) 1.0))
                (< h 145) (+ 30  (* (- h 100) 0.3))
                (< h 175) (+ 40  (* (- h 145) 0.5))
                (< h 210) (+ 60  (* (- h 175) 0.6))
                :else     (+ 210 (* (- h 210) 0.3)))
        clamp (fn [v] (min 255 (int v)))]
    (bit-or (bit-shift-left 255 24)
            (bit-shift-left (clamp b) 16)
            (bit-shift-left (clamp g) 8)
            (clamp r))))

(def heightmap (js/Uint8Array. (* map-size map-size)))
(def colormap  (js/Uint32Array. (* map-size map-size)))

(dotimes [y map-size]
  (dotimes [x map-size]
    (let [h   (max 0 (min 255 (int (terrain-height x y))))
          idx (+ (* y map-size) x)]
      (aset heightmap idx h)
      (aset colormap idx (height->color h)))))

;; ── DOM setup ───────────────────────────────────────────────────

(let [canvas (.createElement js/document "canvas")]
  (set! (.-id canvas) "voxel-canvas")
  (set! (.-style.position canvas) "fixed")
  (set! (.-style.top canvas) "0")
  (set! (.-style.left canvas) "0")
  (set! (.-style.zIndex canvas) "9999")
  (set! (.-style.display canvas) "block")
  (.appendChild (.-body js/document) canvas))

(let [info (.createElement js/document "div")]
  (set! (.-id info) "voxel-info")
  (set! (.-style info)
    "position:fixed;top:10px;left:10px;color:#fff;background:rgba(0,0,0,0.6);padding:6px 10px;border-radius:4px;font:12px monospace;z-index:10000;pointer-events:none")
  (set! (.-textContent info) "WASD = move · Q/E = turn · R/F = height")
  (.appendChild (.-body js/document) info))

;; ── Screen buffer ───────────────────────────────────────────────

(def screen-data
  (atom {:canvas    nil
         :ctx       nil
         :img-data  nil
         :buf8      nil
         :buf32     nil
         :w         0
         :h         0}))

(defn resize-screen []
  (let [canvas (.getElementById js/document "voxel-canvas")
        w      (.-innerWidth js/window)
        h      (.-innerHeight js/window)]
    (set! (.-width canvas) w)
    (set! (.-height canvas) h)
    (let [ctx      (.getContext canvas "2d")
          img-data (.createImageData ctx w h)
          buf      (.-buffer (.-data img-data))]
      (reset! screen-data
              {:canvas   canvas
               :ctx      ctx
               :img-data img-data
               :buf8     (js/Uint8Array. buf)
               :buf32    (js/Uint32Array. buf)
               :w        w
               :h        h}))))

(resize-screen)
(.addEventListener js/window "resize" resize-screen)

;; ── Voxel space renderer ────────────────────────────────────────

(defn draw-vertical-line [buf32 screen-w x ytop ybottom col]
  (when (< ytop ybottom)
    (let [yt (max 0 (int ytop))
          yb (int ybottom)]
      (loop [k      yt
             offset (+ (* yt screen-w) (int x))]
        (when (< k yb)
          (aset buf32 offset col)
          (recur (inc k) (+ offset screen-w)))))))

(defn render-voxel-space [cam-x cam-y cam-h angle horizon]
  (let [{:keys [ctx img-data buf8 buf32 w h]} @screen-data
        sinphi   (js/Math.sin angle)
        cosphi   (js/Math.cos angle)
        scale-h  240.0
        distance 800.0
        sky-color 0xFF6496DC]
    (.fill buf32 sky-color)
    (let [hiddeny (js/Int32Array. w)]
      (dotimes [i w]
        (aset hiddeny i h))
      (loop [z      1.0
             deltaz 1.0]
        (when (< z distance)
          (let [cosz     (* cosphi z)
                sinz     (* sinphi z)
                pleft-x  (+ (- (- cosz) sinz) cam-x)
                pleft-y  (+ (- sinz cosz) cam-y)
                pright-x (+ (- cosz sinz) cam-x)
                pright-y (+ (- (- sinz) cosz) cam-y)
                dx       (/ (- pright-x pleft-x) w)
                dy       (/ (- pright-y pleft-y) w)
                invz     (* (/ 1.0 z) scale-h)]
            (dotimes [i w]
              (let [sx     (+ pleft-x (* i dx))
                    sy     (+ pleft-y (* i dy))
                    mx     (bit-and (int sx) map-mask)
                    my     (bit-and (int sy) map-mask)
                    map-h  (aget heightmap (+ (* my map-size) mx))
                    ybuf   (aget hiddeny i)
                    hs     (+ (* (- cam-h map-h) invz) horizon)]
                (when (< hs ybuf)
                  (draw-vertical-line buf32 w i hs ybuf
                    (aget colormap (+ (* my map-size) mx)))
                  (aset hiddeny i hs)))))
          (recur (+ z deltaz) (+ deltaz 0.005))))
      (.set (.-data img-data) buf8)
      (.putImageData ctx img-data 0 0))))

;; ── Camera ──────────────────────────────────────────────────────

(def camera
  #js {:x        256.0
       :y        256.0
       :height   78.0
       :angle    0.5
       :horizon  100.0})

(def input
  #js {:forwardBackward 0.0
       :leftRight 0.0
       :upDown 0.0
       :lookUp false
       :lookDown false})

(def last-frame  (atom (.now js/Date)))
(def animating? (atom false))

;; ── Game loop ───────────────────────────────────────────────────

(defn update-camera []
  (let [now (.now js/Date)
        dt  (* (- now @last-frame) 0.03)]
    (when (not= (.-leftRight input) 0)
      (set! (.-angle camera)
        (+ (.-angle camera) (* (.-leftRight input) 0.1 dt))))
    (when (not= (.-forwardBackward input) 0)
      (let [move (* (.-forwardBackward input) dt)]
        (set! (.-x camera)
          (- (.-x camera) (* move (js/Math.sin (.-angle camera)))))
        (set! (.-y camera)
          (- (.-y camera) (* move (js/Math.cos (.-angle camera)))))))
    (when (not= (.-upDown input) 0)
      (set! (.-height camera)
        (+ (.-height camera) (* (.-upDown input) dt))))
    (when (.-lookUp input)
      (set! (.-horizon camera)
        (+ (.-horizon camera) (* 2 dt))))
    (when (.-lookDown input)
      (set! (.-horizon camera)
        (- (.-horizon camera) (* 2 dt))))
    (let [mx     (bit-and (int (.-x camera)) map-mask)
          my     (bit-and (int (.-y camera)) map-mask)
          ground (aget heightmap (+ (* my map-size) mx))]
      (when (> (+ ground 10) (.-height camera))
        (set! (.-height camera) (+ ground 10))))
    (reset! last-frame now)))

(defn game-loop []
  (update-camera)
  (render-voxel-space
    (.-x camera) (.-y camera) (.-height camera)
    (.-angle camera) (.-horizon camera))
  (if (or (not= (.-forwardBackward input) 0)
          (not= (.-leftRight input) 0)
          (not= (.-upDown input) 0)
          (.-lookUp input)
          (.-lookDown input))
    (js/requestAnimationFrame game-loop)
    (reset! animating? false)))

;; ── Input ───────────────────────────────────────────────────────

(defn handle-key [pressed? e]
  (let [code (.-code e)
        val  (fn [v] (if pressed? v 0))]
    (cond
      (or (= code "KeyW") (= code "ArrowUp"))
      (set! (.-forwardBackward input) (val 3.0))

      (or (= code "KeyS") (= code "ArrowDown"))
      (set! (.-forwardBackward input) (val -3.0))

      (or (= code "KeyA") (= code "ArrowLeft"))
      (set! (.-leftRight input) (val 1.0))

      (or (= code "KeyD") (= code "ArrowRight"))
      (set! (.-leftRight input) (val -1.0))

      (= code "KeyR") (set! (.-upDown input) (val 2.0))
      (= code "KeyF") (set! (.-upDown input) (val -2.0))
      (= code "KeyE") (set! (.-lookUp input) pressed?)
      (= code "KeyQ") (set! (.-lookDown input) pressed?))
    (when pressed?
      (.preventDefault e)
      (when-not @animating?
        (reset! animating? true)
        (reset! last-frame (.now js/Date))
        (js/requestAnimationFrame game-loop)))))

(.addEventListener js/document "keydown" (partial handle-key true))
(.addEventListener js/document "keyup"   (partial handle-key false))

;; Initial frame
(render-voxel-space
  (.-x camera) (.-y camera) (.-height camera)
  (.-angle camera) (.-horizon camera))