189 lines
6.1 KiB
Clojure
189 lines
6.1 KiB
Clojure
(ns quil.middlewares.navigation-3d
|
|
(:require [quil.core :as q]))
|
|
|
|
(def ^:private missing-navigation-key-error
|
|
(str "state map is missing :navigation-3d key. "
|
|
"Did you accidentally removed it from the state in "
|
|
":update or any other handler?"))
|
|
|
|
(defn- assert-state-has-navigation
|
|
"Asserts that state map contains :navigation-2d object."
|
|
[state]
|
|
(when-not (:navigation-3d state)
|
|
(throw #?(:clj (RuntimeException. missing-navigation-key-error)
|
|
:cljs (js/Error. missing-navigation-key-error)))))
|
|
|
|
(defn- default-position
|
|
"Default position configuration. Check default configuration in
|
|
'camera' function."
|
|
[]
|
|
{:position [(/ (q/width) 2.0)
|
|
(/ (q/height) 2.0)
|
|
(/ (q/height) 2.0 (q/tan (/ (* q/PI 60.0) 360.0)))]
|
|
:straight [0 0 -1]
|
|
:up [0 1 0]})
|
|
|
|
(defn- rotate-by-axis-and-angle
|
|
"Rotates vector v by angle with axis.
|
|
Formula is taken from wiki:
|
|
http://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle"
|
|
[v axis angle]
|
|
(let [[a-x a-y a-z] axis
|
|
[x y z] v
|
|
cs (q/cos angle)
|
|
-cs (- 1 cs)
|
|
sn (q/sin angle)
|
|
; Matrix is
|
|
; [a b c]
|
|
; [d e f]
|
|
; [g h i]
|
|
a (+ cs (* a-x a-x -cs))
|
|
b (- (* a-x a-y -cs)
|
|
(* a-z sn))
|
|
c (+ (* a-x a-z -cs)
|
|
(* a-y sn))
|
|
d (+ (* a-x a-y -cs)
|
|
(* a-z sn))
|
|
e (+ cs (* a-y a-y -cs))
|
|
f (- (* a-y a-z -cs)
|
|
(* a-x sn))
|
|
g (- (* a-x a-z -cs)
|
|
(* a-y sn))
|
|
h (+ (* a-y a-z -cs)
|
|
(* a-x sn))
|
|
i (+ cs (* a-z a-z -cs))]
|
|
[(+ (* a x) (* b y) (* c z))
|
|
(+ (* d x) (* e y) (* f z))
|
|
(+ (* g x) (* h y) (* i z))]))
|
|
|
|
(defn- rotate-lr
|
|
"Rotates nav-3d configuration left-right. angle positive - rotate right,
|
|
negative - left."
|
|
[nav-3d angle]
|
|
(update-in nav-3d [:straight] rotate-by-axis-and-angle (:up nav-3d) angle))
|
|
|
|
(defn- cross-product
|
|
"Vector cross-product: http://en.wikipedia.org/wiki/Cross_product"
|
|
[[u1 u2 u3] [v1 v2 v3]]
|
|
[(- (* u2 v3) (* u3 v2))
|
|
(- (* u3 v1) (* u1 v3))
|
|
(- (* u1 v2) (* u2 v1))])
|
|
|
|
(defn- v-mult
|
|
"Multiply vector v by scalar mult."
|
|
[v mult]
|
|
(mapv #(* % mult) v))
|
|
|
|
(defn- v-plus
|
|
"Sum of 2 vectors."
|
|
[v1 v2]
|
|
(mapv + v1 v2))
|
|
|
|
(defn- v-opposite
|
|
"Returns vector opposite to vector v."
|
|
[v]
|
|
(v-mult v -1))
|
|
|
|
(defn- v-normalize
|
|
"Normalize vector, returning vector
|
|
which has same direction but with norm equals to 1."
|
|
[v]
|
|
(let [norm (->> (map q/sq v)
|
|
(apply +)
|
|
(q/sqrt))]
|
|
(v-mult v (/ norm))))
|
|
|
|
(defn- rotate-ud
|
|
"Rotates nav-3d configuration up-down."
|
|
[nav-3d angle]
|
|
(let [axis (cross-product (:straight nav-3d) (:up nav-3d))
|
|
rotate #(rotate-by-axis-and-angle % axis angle)]
|
|
(-> nav-3d
|
|
(update-in [:straight] rotate)
|
|
(update-in [:up] rotate))))
|
|
|
|
(defn- rotate
|
|
"Mouse handler function which rotates nav-3d configuration.
|
|
It uses mouse from event object and pixels-in-360 to calculate
|
|
angles to rotate."
|
|
[state event pixels-in-360]
|
|
(assert-state-has-navigation state)
|
|
(if (= 0 (:p-x event) (:p-y event))
|
|
state
|
|
(let [dx (- (:p-x event) (:x event))
|
|
dy (- (:y event) (:p-y event))
|
|
angle-lr (q/map-range dx 0 pixels-in-360 0 q/TWO-PI)
|
|
angle-ud (q/map-range dy 0 pixels-in-360 0 q/TWO-PI)]
|
|
(update-in state [:navigation-3d]
|
|
#(-> %
|
|
(rotate-lr angle-lr)
|
|
(rotate-ud angle-ud))))))
|
|
|
|
(def ^:private space (keyword " "))
|
|
|
|
(defn- move
|
|
"Keyboard handler function which moves nav-3d configuration.
|
|
It uses keyboard key from event object to determing in which
|
|
direction to move."
|
|
[state event step-size]
|
|
(assert-state-has-navigation state)
|
|
(let [{:keys [up straight]} (:navigation-3d state)]
|
|
(if-let [dir (condp = (:key event)
|
|
:w straight
|
|
:s (v-opposite straight)
|
|
space (v-opposite up)
|
|
:z up
|
|
:d (cross-product straight up)
|
|
:a (cross-product up straight)
|
|
nil)]
|
|
(update-in state [:navigation-3d :position]
|
|
#(v-plus % (v-mult dir step-size)))
|
|
state)))
|
|
|
|
(defn- setup-3d-nav
|
|
"Custom 'setup' function which creates initial position
|
|
configuration and puts it to the state map."
|
|
[user-setup user-settings]
|
|
(let [initial-state (-> user-settings
|
|
(select-keys [:straight :up :position])
|
|
(->> (merge (default-position)))
|
|
(update-in [:straight] v-normalize)
|
|
(update-in [:up] v-normalize))]
|
|
(update-in (user-setup) [:navigation-3d]
|
|
#(merge initial-state %))))
|
|
|
|
(defn navigation-3d
|
|
"Enables navigation in 3D space. Similar to how it is done in
|
|
shooters: WASD navigation, space is go up, z is go down,
|
|
drag mouse to look around."
|
|
[options]
|
|
(let [; 3d-navigation related user settings
|
|
user-settings (:navigation-3d options)
|
|
pixels-in-360 (:pixels-in-360 user-settings 1000)
|
|
step-size (:step-size user-settings 20)
|
|
rotate-on (:rotate-on user-settings :mouse-dragged)
|
|
|
|
; user-provided handlers which will be overridden
|
|
; by 3d-navigation
|
|
draw (:draw options (fn [state]))
|
|
key-pressed (:key-pressed options (fn [state _] state))
|
|
rotate-on-fn (rotate-on options (fn [state _] state))
|
|
setup (:setup options (fn [] {}))]
|
|
(assoc options
|
|
|
|
:setup (partial setup-3d-nav setup user-settings)
|
|
|
|
:draw (fn [state]
|
|
(assert-state-has-navigation state)
|
|
(let [{[c-x c-y c-z] :straight
|
|
[u-x u-y u-z] :up
|
|
[p-x p-y p-z] :position} (:navigation-3d state)]
|
|
(q/camera p-x p-y p-z (+ p-x c-x) (+ p-y c-y) (+ p-z c-z) u-x u-y u-z))
|
|
(draw state))
|
|
|
|
:key-pressed (fn [state event]
|
|
(key-pressed (move state event step-size) event))
|
|
|
|
rotate-on (fn [state event]
|
|
(rotate-on-fn (rotate state event pixels-in-360) event)) )))
|