Voxel Terrain using Squint
18d 9h ago by lemmy.ml/u/yogthos in clojure@lemmy.ml@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))