You must log in or register to comment.
@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))


