mirror of
https://github.com/open-goal/jak-project
synced 2026-08-21 14:54:53 -04:00
1104 lines
56 KiB
Common Lisp
1104 lines
56 KiB
Common Lisp
;;-*-Lisp-*-
|
|
(in-package goal)
|
|
(bundles "ENGINE.CGO" "GAME.CGO")
|
|
(require "engine/geometry/geometry-h.gc")
|
|
(require "engine/math/quaternion.gc")
|
|
(require "engine/gfx/font.gc")
|
|
|
|
(defun vector-flatten! ((dst vector) (src vector) (plane-normal vector))
|
|
"Get the projection of src onto a plane with the given normal
|
|
The normal should have magnitude 1.0."
|
|
(rlet ((acc :class vf)
|
|
(vf0 :class vf)
|
|
(source :class vf)
|
|
(normal :class vf)
|
|
(in-plane :class vf))
|
|
(init-vf0-vector)
|
|
(.lvf source (&-> src quad))
|
|
(.lvf normal (&-> plane-normal quad))
|
|
(.mov.vf.w in-plane vf0)
|
|
;; use the triple-product to compute this without needing
|
|
;; a slow divide.
|
|
;; vec x normal gives us a vector with the right magnitude,
|
|
;; and in plane, but rotated 90 degrees.
|
|
(.outer.product.a.vf acc source normal)
|
|
(.outer.product.b.vf in-plane normal source acc)
|
|
;; rotate by 90 about normal of plane to get the final vector.
|
|
(.outer.product.a.vf acc normal in-plane)
|
|
(.outer.product.b.vf in-plane in-plane normal acc)
|
|
(.svf (&-> dst quad) in-plane)
|
|
dst))
|
|
|
|
(defun vector-reflect! ((dst vector) (src vector) (plane-normal vector))
|
|
"Reflect src across the plane through the origin with unit normal plane-normal. dst may be
|
|
either input and receives w = 1.0. plane-normal must be unit length; the magnitude of src is
|
|
preserved only if it is."
|
|
(rlet ((acc :class vf)
|
|
(vf0 :class vf)
|
|
(source :class vf)
|
|
(normal :class vf)
|
|
(in-plane :class vf))
|
|
;; we want to split the vector into normal / tangent components.
|
|
;; let src = T + N, where T dot plane-normal = 0.
|
|
;; then the reflection is T - N = 2 * T - src.
|
|
;; we can compute T from vector-flatten!'s trick
|
|
(init-vf0-vector)
|
|
(.lvf source (&-> src quad))
|
|
(.lvf normal (&-> plane-normal quad))
|
|
(.mov.vf.w in-plane vf0)
|
|
;; triple-product to project onto the plane
|
|
(.outer.product.a.vf acc source normal)
|
|
(.outer.product.b.vf in-plane normal source acc)
|
|
(.outer.product.a.vf acc normal in-plane)
|
|
(.outer.product.b.vf in-plane in-plane normal acc)
|
|
;; add in-plane to itself to get 2 * T
|
|
(.add.vf.xyz acc in-plane in-plane)
|
|
;; subtract src.
|
|
(.sub.mul.w.vf.xyz in-plane source vf0 acc)
|
|
(.svf (&-> dst quad) in-plane)
|
|
dst))
|
|
|
|
(defun vector-reflect-flat! ((dst vector) (src vector) (plane-normal vector))
|
|
"Flatten src onto the plane, then add `plane-normal`.
|
|
This is a strange operation, since `src` is typically a velocity and is 1000's of times
|
|
larger than plane-normal. Removing the addition makes no obvious difference in collision.
|
|
My theory is that this is a slight optimization and very slightly pushes collision geometry
|
|
away from the triangle, preventing a future collision. Without this, rounding errors may
|
|
cause geometry to enter the triangle again and take another iteration.
|
|
The existance of vector-reflect-true-flat! (without this addition) acknowledges that this
|
|
version is somehow an adjust reflect-flat."
|
|
(rlet ((acc :class vf)
|
|
(vf0 :class vf)
|
|
(source :class vf)
|
|
(normal :class vf)
|
|
(in-plane :class vf))
|
|
(init-vf0-vector)
|
|
(.lvf source (&-> src quad))
|
|
(.lvf normal (&-> plane-normal quad))
|
|
(.mov.vf.w in-plane vf0)
|
|
;; triple-product to project onto plane
|
|
(.outer.product.a.vf acc source normal)
|
|
(.outer.product.b.vf in-plane normal source acc)
|
|
(.outer.product.a.vf acc normal in-plane)
|
|
(.outer.product.b.vf in-plane in-plane normal acc)
|
|
;; add normal
|
|
(.add.vf.xyz in-plane in-plane normal)
|
|
(.svf (&-> dst quad) in-plane)
|
|
dst))
|
|
|
|
(defun vector-reflect-true-flat! ((dst vector) (src vector) (plane-normal vector))
|
|
"Project src onto the plane with unit normal plane-normal; equivalent to vector-flatten!."
|
|
(rlet ((acc :class vf)
|
|
(vf0 :class vf)
|
|
(source :class vf)
|
|
(normal :class vf)
|
|
(in-plane :class vf))
|
|
(init-vf0-vector)
|
|
(.lvf source (&-> src quad))
|
|
(.lvf normal (&-> plane-normal quad))
|
|
(.mov.vf.w in-plane vf0)
|
|
(.outer.product.a.vf acc source normal)
|
|
(.outer.product.b.vf in-plane normal source acc)
|
|
(.outer.product.a.vf acc normal in-plane)
|
|
(.outer.product.b.vf in-plane in-plane normal acc)
|
|
(.svf (&-> dst quad) in-plane)
|
|
dst))
|
|
|
|
(defun vector-reflect-flat-above! ((dst vector) (src vector) (plane-normal vector))
|
|
"Project src onto the plane with unit normal plane-normal, then lift the result off the plane by
|
|
0.32 times its own length, capped at four meters. dst is used as scratch, so it must not alias
|
|
plane-normal, though it may alias src."
|
|
;; this may just have a bug, the normal-component is always 0.
|
|
;; in any case, this is a bit of a softer "bounce" than reflect.
|
|
(rlet ((acc :class vf)
|
|
(vf0 :class vf)
|
|
(source :class vf)
|
|
(normal :class vf)
|
|
(in-plane :class vf))
|
|
(init-vf0-vector)
|
|
(.lvf source (&-> src quad))
|
|
(.lvf normal (&-> plane-normal quad))
|
|
(.mov.vf.w in-plane vf0)
|
|
(.outer.product.a.vf acc source normal)
|
|
(.outer.product.b.vf in-plane normal source acc)
|
|
(.outer.product.a.vf acc normal in-plane)
|
|
(.outer.product.b.vf in-plane in-plane normal acc)
|
|
(.svf (&-> dst quad) in-plane)
|
|
;; dst is now the normal part of src
|
|
(let* ((in-plane-length (vector-length dst)) ;; len of normal component
|
|
(normal-component (vector-dot dst plane-normal)) ;; always zero?
|
|
(lift (- (* 0.02 in-plane-length) normal-component)))
|
|
;; scale down and limit the normal component
|
|
(vector+float*! dst dst plane-normal (fmin (meters 4) (* 16.0 lift))))))
|
|
|
|
(defun vector-segment-distance-point! ((point vector) (segment-start vector) (segment-end vector) (closest-point vector))
|
|
"Compute the distance from a point to the closest point on the line segment.
|
|
Write that point to closest-point unless it is #f. The projected distance along
|
|
the segment is clamped before measuring the remaining perpendicular displacement.
|
|
The return value is a true distance, not a squared one. closest-point may alias any input and
|
|
receives w = 1.0. A zero-length segment divides by zero and makes both results unusable."
|
|
(local-vars (perpendicular-distance float) (segment-length float) (along-raw float))
|
|
(rlet ((acc :class vf)
|
|
(Q :class vf)
|
|
(vf0 :class vf)
|
|
(segment :class vf)
|
|
(length-work :class vf)
|
|
(start :class vf)
|
|
(end :class vf)
|
|
(probe-point :class vf)
|
|
(start-to-point :class vf)
|
|
(along :class vf)
|
|
(closest :class vf))
|
|
(init-vf0-vector)
|
|
(nop!)
|
|
(.lvf start (&-> segment-start quad))
|
|
(.lvf end (&-> segment-end quad))
|
|
(.lvf probe-point (&-> point quad))
|
|
(.sub.vf segment end start)
|
|
(.sub.vf start-to-point probe-point start)
|
|
;; Sum the squared lanes into w, then take the square root of that one lane.
|
|
(.mul.vf length-work segment segment)
|
|
(.mul.x.vf.w acc vf0 length-work)
|
|
(.add.mul.y.vf.w acc vf0 length-work acc)
|
|
(.add.mul.z.vf.w length-work vf0 length-work acc)
|
|
(.sqrt.vf Q length-work :ftf #b11)
|
|
(.wait.vf)
|
|
(.add.vf.x length-work vf0 Q)
|
|
(.nop.vf)
|
|
(.nop.vf)
|
|
(.div.vf Q vf0 length-work :fsf #b11 :ftf #b0)
|
|
(.mov segment-length length-work)
|
|
(let ((segment-max segment-length))
|
|
(.wait.vf)
|
|
;; segment becomes the unit direction, so the dot product below is a length along it.
|
|
(.mul.vf segment segment Q)
|
|
(.mul.vf along segment start-to-point)
|
|
(let ((segment-min 0.0))
|
|
(.add.y.vf.x along along along)
|
|
(.add.z.vf.x along along along)
|
|
(.mov along-raw along)
|
|
(let ((along-clamped along-raw))
|
|
(b! (< along-clamped segment-min) clamp-done :likely-delay (set! along-clamped segment-min))
|
|
(b! (< segment-max along-clamped) clamp-done :likely-delay (set! along-clamped segment-max))
|
|
(label clamp-done)
|
|
(let ((clamped-bits along-clamped)) (.mov along clamped-bits)))))
|
|
;; segment becomes the offset from segment-start to the closest point.
|
|
(.mul.x.vf segment segment along)
|
|
(b! (= closest-point #f) measure-perpendicular :delay (.mov.vf.w closest vf0))
|
|
(.add.vf.xyz closest start segment)
|
|
(.svf (&-> closest-point quad) closest)
|
|
(label measure-perpendicular)
|
|
(.sub.vf length-work start-to-point segment)
|
|
(.mul.vf length-work length-work length-work)
|
|
(.mul.x.vf.w acc vf0 length-work)
|
|
(.add.mul.y.vf.w acc vf0 length-work acc)
|
|
(.add.mul.z.vf.w length-work vf0 length-work acc)
|
|
(.sqrt.vf Q length-work :ftf #b11)
|
|
(.wait.vf)
|
|
(.add.vf.x length-work vf0 Q)
|
|
(.nop.vf)
|
|
(.mov perpendicular-distance length-work)
|
|
perpendicular-distance))
|
|
|
|
(defun vector-line-distance ((point vector) (line-point vector) (line-point-2 vector))
|
|
"Return the distance from point to the infinite line through line-point and line-point-2. This
|
|
is a true distance, and the line is not clipped to the segment between the two points. The two
|
|
points must differ, or the normalize divides by zero."
|
|
(let* ((line-direction (vector-normalize! (vector-! (new-stack-vector0) line-point-2 line-point) 1.0))
|
|
(point-offset (vector-! (new-stack-vector0) point line-point))
|
|
(distance-along-line (vector-dot line-direction point-offset))
|
|
(projected-offset (vector-float*! (new-stack-vector0) line-direction distance-along-line)))
|
|
(vector-length (vector-! (new-stack-vector0) point-offset projected-offset))))
|
|
|
|
(defun vector-line-distance-point! ((point vector) (line-point vector) (line-point-2 vector) (closest-point vector))
|
|
"Return the distance from point to an infinite line and optionally write its closest point.
|
|
closest-point may be #f, and is not clamped to the segment between the two line points, so it
|
|
can land outside them. It receives w = 1.0 and may alias any input."
|
|
(let* ((line-direction (vector-normalize! (vector-! (new-stack-vector0) line-point-2 line-point) 1.0))
|
|
(point-offset (vector-! (new-stack-vector0) point line-point))
|
|
(distance-along-line (vector-dot line-direction point-offset))
|
|
(projected-offset (vector-float*! (new-stack-vector0) line-direction distance-along-line)))
|
|
(if closest-point (vector+! closest-point line-point projected-offset))
|
|
(vector-length (vector-! (new-stack-vector0) point-offset projected-offset))))
|
|
|
|
(defun vector-orient-by-quat! ((dst vector) (src vector) (rotation quaternion))
|
|
"Rotate src by rotation and write the result to dst. rotation must be unit; a longer quaternion
|
|
scales the result by its squared norm. dst may alias src and keeps src's w."
|
|
;; Rather than call quaternion->matrix, this builds the three rotation rows directly out of the
|
|
;; quaternion's own lanes. Each row starts as a permutation of (w, z, -y) and friends, is crossed
|
|
;; with 2q, and picks up the 1.0 on its diagonal from vf0's w.
|
|
(rlet ((acc :class vf)
|
|
(vf0 :class vf)
|
|
(quat :class vf)
|
|
(row0 :class vf)
|
|
(row1 :class vf)
|
|
(row2 :class vf)
|
|
(quat-doubled :class vf)
|
|
(vec :class vf))
|
|
(init-vf0-vector)
|
|
(.lvf quat (&-> rotation vec quad))
|
|
(.lvf vec (&-> src quad))
|
|
(.add.vf quat-doubled quat quat)
|
|
(.add.w.vf.x row0 vf0 quat)
|
|
(.add.z.vf.y row0 vf0 quat)
|
|
(.sub.y.vf.z row0 vf0 quat)
|
|
(.sub.w.vf.w row0 vf0 vf0)
|
|
(.sub.z.vf.x row1 vf0 quat)
|
|
(.add.w.vf.y row1 vf0 quat)
|
|
(.add.x.vf.z row1 vf0 quat)
|
|
(.sub.w.vf.w row1 vf0 vf0)
|
|
(.add.y.vf.x row2 vf0 quat)
|
|
(.sub.x.vf.y row2 vf0 quat)
|
|
(.add.w.vf.z row2 vf0 quat)
|
|
(.sub.w.vf.w row2 vf0 vf0)
|
|
(.outer.product.a.vf acc quat-doubled row0)
|
|
(.outer.product.b.vf row0 row0 quat-doubled acc)
|
|
(.outer.product.a.vf acc quat-doubled row1)
|
|
(.outer.product.b.vf row1 row1 quat-doubled acc)
|
|
(.outer.product.a.vf acc quat-doubled row2)
|
|
(.outer.product.b.vf row2 row2 quat-doubled acc)
|
|
(.add.w.vf.x row0 row0 vf0)
|
|
(.add.w.vf.y row1 row1 vf0)
|
|
(.add.w.vf.z row2 row2 vf0)
|
|
(.mul.w.vf acc vf0 vec)
|
|
(.add.mul.x.vf acc row0 vec acc)
|
|
(.add.mul.y.vf acc row1 vec acc)
|
|
(.add.mul.z.vf vec row2 vec acc)
|
|
(.svf (&-> dst quad) vec)
|
|
dst))
|
|
|
|
;; The "forward down" function take a direction for forward (+z) and down (-y)
|
|
;; and convert to a transform.
|
|
;; Note that the normal functions take their pitch from the forward vector, but
|
|
;; the "nopitch" ones use the pitch from the up/down. Of course, if you are
|
|
;; consistent and provide orthogonal forward/down, they do the same thing.
|
|
|
|
(defun forward-down->inv-matrix ((dst matrix) (forward vector) (down vector))
|
|
"Build an inverse rotation matrix whose +z axis follows forward and -y points toward down."
|
|
(vector-normalize-copy! (-> dst vector 2) forward 1.0)
|
|
(vector-cross! (the-as vector (-> dst vector)) (-> dst vector 2) down)
|
|
(vector-normalize! (the-as vector (-> dst vector)) 1.0)
|
|
(vector-cross! (-> dst vector 1) forward (the-as vector (-> dst vector)))
|
|
(vector-normalize! (-> dst vector 1) 1.0)
|
|
(set! (-> dst vector 3 quad) (the-as uint128 0))
|
|
(set! (-> dst vector 0 w) 0.0)
|
|
(set! (-> dst vector 1 w) 0.0)
|
|
(set! (-> dst vector 2 w) 0.0)
|
|
(set! (-> dst vector 3 w) 1.0)
|
|
dst)
|
|
|
|
(defun forward-down-nopitch->inv-matrix ((dst matrix) (forward vector) (down vector))
|
|
"Build an inverse rotation matrix from forward and down, taking pitch from down."
|
|
(vector-normalize-copy! (-> dst vector 1) down 1.0)
|
|
(vector-negate! (-> dst vector 1) (-> dst vector 1))
|
|
(vector-cross! (-> dst vector 0) (-> dst vector 1) forward)
|
|
(vector-normalize! (-> dst vector 0) 1.0)
|
|
(vector-cross! (-> dst vector 2) (-> dst vector 0) (-> dst vector 1))
|
|
(vector-normalize! (-> dst vector 2) 1.0)
|
|
(vector-zero! (-> dst vector 3))
|
|
(set! (-> dst vector 0 w) 0.0)
|
|
(set! (-> dst vector 1 w) 0.0)
|
|
(set! (-> dst vector 2 w) 0.0)
|
|
(set! (-> dst vector 3 w) 1.0)
|
|
dst)
|
|
|
|
(defun forward-up-nopitch->inv-matrix ((dst matrix) (forward vector) (up vector))
|
|
"Build an inverse rotation matrix from forward and up, taking pitch from up."
|
|
(forward-down-nopitch->inv-matrix dst forward (vector-negate! (new-stack-vector0) up)))
|
|
|
|
(defun forward-up-nopitch->quaternion ((dst quaternion) (forward vector) (up vector))
|
|
"Build a quaternion from forward and up, taking pitch from up."
|
|
(matrix->quaternion dst (forward-up-nopitch->inv-matrix (new-stack-matrix0) forward up)))
|
|
|
|
(defun forward-up->quaternion ((dst quaternion) (forward vector) (up vector))
|
|
"Build a quaternion from forward and up while retaining the pitch in forward."
|
|
(matrix->quaternion dst
|
|
(forward-down->inv-matrix (new-stack-matrix0) forward (vector-negate! (new 'stack-no-clear 'vector) up))))
|
|
|
|
(defun quaternion-from-two-vectors! ((dst quaternion) (from vector) (to vector))
|
|
"Build the shortest-arc quaternion rotating unit vector from onto unit vector to.
|
|
Parallel and antiparallel inputs are singular because the cross-product axis has zero length."
|
|
;; cross(from,to) is the axis times sin(angle) and dot(from, to) is cos(angle), so the half-angle
|
|
;; identities give the quaternion without ever forming the angle itself.
|
|
(let* ((axis (vector-cross! (new-stack-vector0) from to))
|
|
(sin-angle (vector-length axis))
|
|
(cos-angle (vector-dot from to)))
|
|
(let ((scale (/ (sqrtf (* 0.5 (- 1.0 cos-angle))) sin-angle)))
|
|
(set! (-> dst x) (* (-> axis x) scale))
|
|
(set! (-> dst y) (* (-> axis y) scale))
|
|
(set! (-> dst z) (* (-> axis z) scale)))
|
|
(set! (-> dst w) (sqrtf (* 0.5 (+ 1.0 cos-angle)))))
|
|
dst)
|
|
|
|
(defun quaternion-from-two-vectors-max-angle! ((dst quaternion) (from vector) (to vector) (max-angle float))
|
|
"Build the shortest-arc quaternion between two unit vectors, capped at max-angle rotation units."
|
|
(let* ((axis (vector-cross! (new-stack-vector0) from to))
|
|
(sin-angle (vector-length axis))
|
|
(cos-angle (vector-dot from to))
|
|
(sin-half-angle (sqrtf (* 0.5 (- 1.0 cos-angle)))))
|
|
(let ((sin-half-max (sin (* 0.5 max-angle))))
|
|
(cond
|
|
((< sin-half-max sin-half-angle) (set! sin-half-angle sin-half-max) (set! (-> dst w) (cos (* 0.5 max-angle))))
|
|
(else (set! (-> dst w) (sqrtf (* 0.5 (+ 1.0 cos-angle)))))))
|
|
(let ((scale (/ sin-half-angle sin-angle)))
|
|
(set! (-> dst x) (* (-> axis x) scale))
|
|
(set! (-> dst y) (* (-> axis y) scale))
|
|
(set! (-> dst z) (* (-> axis z) scale))))
|
|
dst)
|
|
|
|
;; Every matrix-from-two-vectors variant crosses to with from, not from with to. That is not a
|
|
;; mistake. matrix-axis-sin-cos! writes the textbook column-vector
|
|
;; Rodrigues matrix, while GOAL multiplies row vector by matrix, so we correct things for GOAL here.
|
|
;;
|
|
;; All four take unit inputs. from . to is the cosine directly, and the sine comes back from
|
|
;; sqrt(1 - cos^2), which is always nonnegative, so these build the shortest arc and never the long
|
|
;; way around.
|
|
|
|
(defun matrix-from-two-vectors! ((dst matrix) (from vector) (to vector))
|
|
"Build the shortest-arc rotation matrix between two unit vectors. dst is a full affine matrix
|
|
with zero translation."
|
|
(let* ((axis (vector-normalize! (vector-cross! (new-stack-vector0) to from) 1.0))
|
|
(cos-angle (vector-dot from to))
|
|
(sin-angle (sqrtf (- 1.0 (square cos-angle)))))
|
|
(matrix-axis-sin-cos! dst axis sin-angle cos-angle)))
|
|
|
|
(defun matrix-from-two-vectors-max-angle! ((dst matrix) (from vector) (to vector) (max-angle float))
|
|
"Build the shortest-arc rotation between two unit vectors, capped at max-angle."
|
|
(let ((axis (vector-normalize! (vector-cross! (new-stack-vector0) to from) 1.0))
|
|
(cos-angle (vector-dot from to))
|
|
(cos-max (cos max-angle)))
|
|
(if (< cos-angle cos-max)
|
|
(matrix-axis-sin-cos! dst axis (sin max-angle) cos-max)
|
|
(matrix-axis-sin-cos! dst axis (sqrtf (- 1.0 (square cos-angle))) cos-angle))))
|
|
|
|
;; note: these two interpolation functions may be equivalent to slerp.
|
|
|
|
(defun matrix-from-two-vectors-max-angle-partial! ((dst matrix) (from vector) (to vector) (max-angle float) (fraction float))
|
|
"Build a fractional shortest-arc rotation capped at max-angle. This interpolates the cosine,
|
|
which doesn't linearly interpolate the rotation angle, but is faster."
|
|
(let* ((axis (vector-normalize! (vector-cross! (new-stack-vector0) to from) 1.0))
|
|
(cos-angle (vector-dot from to))
|
|
(max-cos (cos max-angle))
|
|
(blended-cos (+ 1.0 (* (+ -1.0 cos-angle) fraction))))
|
|
(if (< blended-cos max-cos)
|
|
(matrix-axis-sin-cos! dst axis (sin max-angle) max-cos)
|
|
(matrix-axis-sin-cos! dst axis (sqrtf (- 1.0 (square blended-cos))) blended-cos))))
|
|
|
|
(defun matrix-from-two-vectors-partial-linear! ((dst matrix) (from vector) (to vector) (fraction float))
|
|
"Build a fractional shortest-arc rotation between two unit vectors by scaling the angle."
|
|
(let ((axis (vector-normalize! (vector-cross! (new-stack-vector0) to from) 1.0))
|
|
(cos-angle (vector-dot from to)))
|
|
(cond
|
|
((< 0.9999 (fabs cos-angle))
|
|
(matrix-identity! dst))
|
|
(else
|
|
(let ((blended-cos (cos (* fraction (acos cos-angle)))))
|
|
(matrix-axis-sin-cos! dst axis (sqrtf (- 1.0 (square blended-cos))) blended-cos))))))
|
|
|
|
(defun matrix-remove-z-rot ((rotation matrix) (reference matrix))
|
|
"Rotate rotation about its own third row so that its second row lines up with reference's first
|
|
row flattened into that plane, removing the roll between them. rotation is modified in place and
|
|
returned; reference is only read, and only its first row is used. Rows within 0.99999 of aligned
|
|
are left alone."
|
|
;; The target direction is reference's x axis negated and projected into the plane perpendicular
|
|
;; to rotation's z axis, so it is the closest in-plane heading to "behind the reference". The
|
|
;; correction angle comes from the cross product of rotation's y axis with that target: its
|
|
;; length is the sine and its sign relative to rotation's z axis says which way. The sine is
|
|
;; negated when the cross agrees with z, because a row-vector product by the matrix
|
|
;; matrix-axis-sin-cos! builds rotates by the negative of the angle it was handed.
|
|
(let ((target (new-stack-vector0)))
|
|
0.0
|
|
0.0
|
|
(let ((twist (new-stack-matrix0)))
|
|
(vector-negate! target (the-as vector reference))
|
|
(vector-flatten! target target (-> rotation vector 2))
|
|
(vector-normalize! target 1.0)
|
|
(let ((cos-twist (vector-dot (-> rotation vector 1) target)))
|
|
(when (< cos-twist 0.99999)
|
|
(vector-cross! target (-> rotation vector 1) target)
|
|
(let ((sin-twist (vector-length target)))
|
|
(if (< 0.0 (vector-dot target (-> rotation vector 2))) (set! sin-twist (- sin-twist)))
|
|
(matrix-axis-sin-cos! twist (-> rotation vector 2) sin-twist cos-twist))
|
|
(matrix*! rotation rotation twist)))))
|
|
rotation)
|
|
|
|
(defun matrix-rot-diff! ((axis vector) (from matrix) (to matrix))
|
|
"Return the rotation-unit angle between two rotation matrices and write its unit axis to axis.
|
|
The returned angle is never negative, because the difference quaternion is flipped to the
|
|
positive-w hemisphere first. axis is negated relative to the difference quaternion's vector part
|
|
and gets w = 1.0. Both matrices must be rotations; a scale or shear makes matrix->quaternion
|
|
meaningless."
|
|
;; When from and to are equal the difference quaternion is the identity, the axis is exactly zero,
|
|
;; and the normalize below has nothing to work with. The y = 1.0 fallback only sets y, which is
|
|
;; enough on the EE: vrsqrt of zero saturates to max-float and zero times that is still zero. The
|
|
;; PC build divides by zero instead, so x and z come back NaN. Nothing calls this.
|
|
(let ((from-rot (new-stack-quaternion0))
|
|
(to-rot (new-stack-quaternion0))
|
|
(difference (new-stack-quaternion0)))
|
|
0.0
|
|
(matrix->quaternion from-rot from)
|
|
(matrix->quaternion to-rot to)
|
|
(quaternion-conjugate! difference from-rot)
|
|
(quaternion*! difference to-rot difference)
|
|
(quaternion-normalize! difference)
|
|
(if (< (-> difference w) 0.0) (quaternion-negate! difference difference))
|
|
(let ((angle (* 2.0 (acos (-> difference w)))))
|
|
(set! (-> axis quad) (-> difference vec quad))
|
|
(vector-negate! axis axis)
|
|
(if (= (vector-normalize-ret-len! axis 1.0) 0.0) (set! (-> axis y) 1.0))
|
|
angle)))
|
|
|
|
(defun quaternion-seek ((value quaternion) (from quaternion) (to quaternion) (unused-rate float) (max-angle float))
|
|
"Rotate value toward to by at most max-angle, using only the forward axes of from and to.
|
|
unused-rate is ignored. The implementation is a bit strange and doesn't handle roll."
|
|
;; Only the third row of each matrix is read, so any roll difference between from and to is
|
|
;; discarded and value keeps its own. value is both the accumulator and the output: the step is
|
|
;; post-multiplied onto whatever it already holds, which is not required to agree with from.
|
|
(let ((from-matrix (new-stack-matrix0))
|
|
(to-matrix (new-stack-matrix0)))
|
|
(quaternion->matrix from-matrix from)
|
|
(quaternion->matrix to-matrix to)
|
|
(let ((step (new-stack-quaternion0)))
|
|
(quaternion-from-two-vectors-max-angle! step (-> from-matrix vector 2) (-> to-matrix vector 2) max-angle)
|
|
(quaternion-normalize! (quaternion*! value value step)))))
|
|
|
|
(defun vector-deg-seek ((dst vector) (from vector) (to vector) (max-angle float))
|
|
"Rotate from toward to by at most max-angle rotation units and write the result to dst. from and
|
|
to must be unit length."
|
|
(let ((step (new-stack-matrix0)))
|
|
(matrix-from-two-vectors-max-angle! step from to max-angle)
|
|
(vector-matrix*! dst from step)))
|
|
|
|
(defun vector-deg-slerp ((dst vector) (from vector) (to vector) (t float))
|
|
"Spherically interpolate from one vector direction to another by t, preserving from's magnitude.
|
|
The inputs are normalized internally."
|
|
(cond
|
|
((>= 0.0 t) (vector-copy! dst from) dst)
|
|
((>= t 1.0) (vector-copy! dst to) dst)
|
|
(else
|
|
(let ((step (new-stack-matrix0)))
|
|
(let ((from-unit (vector-normalize-copy! (new 'stack-no-clear 'vector) from 1.0))
|
|
(to-unit (vector-normalize-copy! (new 'stack-no-clear 'vector) to 1.0)))
|
|
(matrix-from-two-vectors-partial-linear! step from-unit to-unit t))
|
|
(vector-matrix*! dst from step)))))
|
|
|
|
(defun vector-vector-deg-slerp! ((dst vector) (from vector) (to vector) (t float) (up vector))
|
|
"Interpolate direction from from to to around a shared up vector while linearly blending
|
|
magnitude. Both directions are turned into full orientations against up before the slerp, so up
|
|
must not be parallel to either one."
|
|
(cond
|
|
((>= 0.0 t)
|
|
(vector-copy! dst from))
|
|
((>= t 1.0)
|
|
(vector-copy! dst to))
|
|
(else
|
|
(let* ((from-unit (vector-normalize-copy! (new 'stack-no-clear 'vector) from 1.0))
|
|
(to-unit (vector-normalize-copy! (new 'stack-no-clear 'vector) to 1.0))
|
|
(from-rot (forward-up->quaternion (new 'stack-no-clear 'quaternion) from-unit up))
|
|
(to-rot (forward-up->quaternion (new 'stack-no-clear 'quaternion) to-unit up))
|
|
(blended-rot (quaternion-slerp! (new 'stack-no-clear 'quaternion) from-rot to-rot t)))
|
|
(vector-normalize-copy! dst
|
|
(vector-z-quaternion! (new 'stack-no-clear 'vector) blended-rot)
|
|
(lerp (vector-length from) (vector-length to) t)))))
|
|
dst)
|
|
|
|
(defun normal-of-plane ((dst vector) (point-a vector) (point-b vector) (point-c vector))
|
|
"Compute a unit normal for the plane through three points. The normal follows the right-hand rule
|
|
for the points taken in a, b, c order, so reversing any two arguments flips it."
|
|
(rlet ((acc :class vf)
|
|
(Q :class vf)
|
|
(vf0 :class vf)
|
|
(a-to-b :class vf)
|
|
(c-to-b :class vf)
|
|
(vertex-b :class vf)
|
|
(normal :class vf)
|
|
(length-work :class vf))
|
|
(init-vf0-vector)
|
|
;; point-a and point-c are loaded into the registers that immediately become the two edges.
|
|
(.lvf vertex-b (&-> point-b quad))
|
|
(.lvf a-to-b (&-> point-a quad))
|
|
(.lvf c-to-b (&-> point-c quad))
|
|
(.sub.vf a-to-b vertex-b a-to-b)
|
|
(.sub.vf c-to-b vertex-b c-to-b)
|
|
(.outer.product.a.vf acc c-to-b a-to-b)
|
|
(.outer.product.b.vf normal a-to-b c-to-b acc)
|
|
(.mul.vf length-work normal normal)
|
|
(.add.y.vf.x length-work length-work length-work)
|
|
(.add.z.vf.x length-work length-work length-work)
|
|
(.isqrt.vf Q vf0 length-work :fsf #b11 :ftf #b0)
|
|
(.mov.vf.w normal vf0)
|
|
(.wait.vf)
|
|
(.mul.vf.xyz normal normal Q)
|
|
(.nop.vf)
|
|
(.nop.vf)
|
|
(.svf (&-> dst quad) normal)
|
|
dst))
|
|
|
|
(defun vector-3pt-cross! ((dst vector) (origin vector) (point-a vector) (point-b vector))
|
|
"Cross the displacements from origin to point-a and point-b without normalizing the result. The
|
|
length is therefore twice the area of the triangle, which callers use as an area or winding test
|
|
as well as a normal direction."
|
|
(rlet ((acc :class vf)
|
|
(vf0 :class vf)
|
|
(base :class vf)
|
|
(base-to-a :class vf)
|
|
(base-to-b :class vf)
|
|
(cross :class vf))
|
|
(init-vf0-vector)
|
|
(.lvf base (&-> origin quad))
|
|
(.lvf base-to-a (&-> point-a quad))
|
|
(.lvf base-to-b (&-> point-b quad))
|
|
(.add.x.vf.w cross vf0 vf0)
|
|
(.sub.vf base-to-a base-to-a base)
|
|
(.sub.vf base-to-b base-to-b base)
|
|
(.outer.product.a.vf acc base-to-a base-to-b)
|
|
(.outer.product.b.vf cross base-to-b base-to-a acc)
|
|
(.svf (&-> dst quad) cross)
|
|
dst))
|
|
|
|
(defun closest-pt-in-triangle ((dst vector) (point vector) (triangle matrix) (normal vector))
|
|
"Write the closest point on triangle to point.
|
|
The function projects onto the plane, then clamps to the nearest edge when the projection lies outside.
|
|
triangle's first three rows are the vertices and its fourth is ignored. normal must be the
|
|
triangle's own unit normal"
|
|
;; The three edge tests are cross products of the two triangle edges leaving a shared vertex with
|
|
;; the vector to point, dotted against the normal. Each dot lands in lane y so that the wide
|
|
;; register move below carries it in the high word of a 64-bit value, where its sign bit is the
|
|
;; integer sign bit. The three signs become bits 0, 1 and 2 of one mask, and the mask selects the
|
|
;; region: zero is the interior, one bit is an edge, two bits are the corner between them. All
|
|
;; three bits set is geometrically impossible for a consistent normal and shares the last branch.
|
|
;; (declare (print-asm))
|
|
(local-vars (edge-01-bits uint)
|
|
(mask-minus-1 uint)
|
|
(mask-minus-2 uint)
|
|
(mask-minus-3 uint)
|
|
(mask-minus-4 uint)
|
|
(mask-minus-5 uint)
|
|
(edge-20-bits uint)
|
|
(edge-12-bits uint))
|
|
(rlet ((acc :class vf)
|
|
(vf0 :class vf)
|
|
(plane-normal :class vf)
|
|
(v0-to-point :class vf)
|
|
(edge-01-side :class vf)
|
|
(edge-12-side :class vf)
|
|
(edge-20-side :class vf)
|
|
(edge-01-cross :class vf)
|
|
(edge-12-cross :class vf)
|
|
(edge-20-cross :class vf)
|
|
(plane-offset :class vf)
|
|
(projected :class vf)
|
|
(vertex-0 :class vf)
|
|
(vertex-1 :class vf)
|
|
(vertex-2 :class vf)
|
|
(probe-point :class vf)
|
|
(v2-to-v1 :class vf)
|
|
(point-to-v1 :class vf)
|
|
(v0-to-v1 :class vf)
|
|
(v2-to-point :class vf))
|
|
(init-vf0-vector)
|
|
(nop!)
|
|
(nop!)
|
|
(.lvf vertex-1 (&-> triangle vector 1 quad))
|
|
(nop!)
|
|
(.lvf vertex-2 (&-> triangle vector 2 quad))
|
|
(nop!)
|
|
(.lvf probe-point (&-> point quad))
|
|
(.sub.vf v2-to-v1 vertex-1 vertex-2)
|
|
(.lvf vertex-0 (&-> triangle vector 0 quad))
|
|
(.sub.vf point-to-v1 vertex-1 probe-point)
|
|
(.lvf plane-normal (&-> normal quad))
|
|
(.sub.vf v0-to-v1 vertex-1 vertex-0)
|
|
(.sub.vf v2-to-point probe-point vertex-2)
|
|
(.sub.vf v0-to-point probe-point vertex-0)
|
|
(.outer.product.a.vf acc point-to-v1 v0-to-v1)
|
|
(.outer.product.b.vf edge-01-cross v0-to-v1 point-to-v1 acc)
|
|
(.outer.product.a.vf acc v2-to-v1 point-to-v1)
|
|
(.outer.product.b.vf edge-12-cross point-to-v1 v2-to-v1 acc)
|
|
(.mul.vf edge-01-side edge-01-cross plane-normal)
|
|
(.outer.product.a.vf acc v2-to-point v0-to-point)
|
|
(.outer.product.b.vf edge-20-cross v0-to-point v2-to-point acc)
|
|
(.mul.vf edge-12-side edge-12-cross plane-normal)
|
|
(.add.x.vf.y edge-01-side edge-01-side edge-01-side)
|
|
(.mul.vf edge-20-side edge-20-cross plane-normal)
|
|
(.add.x.vf.y edge-12-side edge-12-side edge-12-side)
|
|
(.add.x.vf.y edge-20-side edge-20-side edge-20-side)
|
|
(.add.z.vf.y edge-01-side edge-01-side edge-01-side)
|
|
(.add.z.vf.y edge-12-side edge-12-side edge-12-side)
|
|
(.add.z.vf.y edge-20-side edge-20-side edge-20-side)
|
|
;; og:preserve-this these types were changed to uint to make this copy 64 bits.
|
|
(.mov edge-01-bits edge-01-side)
|
|
(.mov edge-12-bits edge-12-side)
|
|
(.mov edge-20-bits edge-20-side)
|
|
(let* ((edge-01-outside (shr (the-as int edge-01-bits) 63))
|
|
(edge-12-outside (shr (the-as int edge-12-bits) 63))
|
|
(edge-20-outside (shr (the-as int edge-20-bits) 63))
|
|
(edge-12-bit (* edge-12-outside 2))
|
|
(edge-20-bit (* edge-20-outside 4))
|
|
(outside-mask (logior (logior edge-01-outside edge-12-bit) edge-20-bit)))
|
|
(b! (nonzero? outside-mask) outside-triangle :delay (set! mask-minus-1 (the-as uint (+ outside-mask -1)))))
|
|
;; Inside. Project point onto the triangle's plane through vertex 0 with the double cross.
|
|
(.sub.vf plane-offset probe-point vertex-0)
|
|
(.mov.vf.w projected vf0)
|
|
(.outer.product.a.vf acc plane-offset plane-normal)
|
|
(.outer.product.b.vf projected plane-normal plane-offset acc)
|
|
(.outer.product.a.vf acc plane-normal projected)
|
|
(.outer.product.b.vf projected projected plane-normal acc)
|
|
(.add.vf.xyz projected projected vertex-0)
|
|
(b! #t closest-point-done :delay (.svf (&-> dst quad) projected))
|
|
(nop!)
|
|
(label outside-triangle)
|
|
(b! (nonzero? mask-minus-1) test-edge-12 :delay (set! mask-minus-2 (the-as uint (+ mask-minus-1 -1))))
|
|
(vector-segment-distance-point! point (the-as vector (-> triangle vector)) (-> triangle vector 1) dst)
|
|
(goto closest-point-done)
|
|
(label test-edge-12)
|
|
(b! (nonzero? mask-minus-2) test-vertex-1 :delay (set! mask-minus-3 (the-as uint (+ mask-minus-2 -1))))
|
|
(vector-segment-distance-point! point (-> triangle vector 1) (-> triangle vector 2) dst)
|
|
(goto closest-point-done)
|
|
(label test-vertex-1)
|
|
(b! (nonzero? mask-minus-3) test-edge-20 :delay (set! mask-minus-4 (the-as uint (+ mask-minus-3 -1))))
|
|
(let ((first-distance (vector-segment-distance-point! point (-> triangle vector 1) (the-as vector (-> triangle vector)) dst))
|
|
(second-point (new 'stack-no-clear 'vector)))
|
|
(if (< (vector-segment-distance-point! point (-> triangle vector 1) (-> triangle vector 2) second-point) first-distance)
|
|
(vector-copy! dst second-point)))
|
|
(goto closest-point-done)
|
|
(label test-edge-20)
|
|
(b! (nonzero? mask-minus-4) test-vertex-0 :delay (set! mask-minus-5 (the-as uint (+ mask-minus-4 -1))))
|
|
(vector-segment-distance-point! point (-> triangle vector 2) (the-as vector (-> triangle vector)) dst)
|
|
(goto closest-point-done)
|
|
(label test-vertex-0)
|
|
(b! (nonzero? mask-minus-5) vertex-2-region)
|
|
(let ((first-distance (vector-segment-distance-point! point (the-as vector (-> triangle vector)) (-> triangle vector 1) dst))
|
|
(second-point (new 'stack-no-clear 'vector)))
|
|
(if (< (vector-segment-distance-point! point (the-as vector (-> triangle vector)) (-> triangle vector 2) second-point) first-distance)
|
|
(vector-copy! dst second-point)))
|
|
(goto closest-point-done)
|
|
(label vertex-2-region)
|
|
(let ((first-distance (vector-segment-distance-point! point (-> triangle vector 2) (the-as vector (-> triangle vector)) dst))
|
|
(second-point (new 'stack-no-clear 'vector)))
|
|
(if (< (vector-segment-distance-point! point (-> triangle vector 2) (-> triangle vector 1) second-point) first-distance)
|
|
(vector-copy! dst second-point)))
|
|
(label closest-point-done)
|
|
0
|
|
(none)))
|
|
|
|
(defun point-in-triangle-cross ((point vector) (normal vector) (vertex-a vector) (vertex-b vector) (vertex-c vector))
|
|
"Test whether a coplanar point lies inside a triangle using consistently oriented edge crosses.
|
|
point is assumed already in the triangle's plane; nothing here measures how far off it is, so a
|
|
point anywhere along the prism through the triangle reports true. normal must point the way
|
|
normal-of-plane would for the vertices in a, b, c order, and only its direction matters. Points
|
|
exactly on an edge count as inside. Returns true when the point is inside."
|
|
;; One test per edge: cross the two vectors leaving the shared vertex, dot against the normal, and
|
|
;; ask whether the result is negative. The dot is summed into lane y so that the 64-bit register
|
|
;; move puts it in the high word of an int, where a negative float has the integer sign bit set.
|
|
;; Oring the three carriers together and testing the sign then answers "was any of the three
|
|
;; negative" in a single comparison, without three branches and without leaving the accumulator.
|
|
(local-vars (ca-bits int) (bc-bits int) (ab-bits int))
|
|
(rlet ((acc :class vf)
|
|
(plane-normal :class vf)
|
|
(a-to-point :class vf)
|
|
(bc-side :class vf)
|
|
(ab-side :class vf)
|
|
(ca-side :class vf)
|
|
(probe-point :class vf)
|
|
(c-to-b :class vf)
|
|
(point-to-b :class vf)
|
|
(a-to-b :class vf)
|
|
(c-to-point :class vf))
|
|
;; The three vertices are loaded into the registers that go on to hold the three edge results.
|
|
(.lvf ab-side (&-> vertex-b quad))
|
|
(.lvf ca-side (&-> vertex-c quad))
|
|
(.lvf probe-point (&-> point quad))
|
|
(.lvf bc-side (&-> vertex-a quad))
|
|
(.lvf plane-normal (&-> normal quad))
|
|
(.sub.vf c-to-b ab-side ca-side)
|
|
(.sub.vf point-to-b ab-side probe-point)
|
|
(.sub.vf a-to-b ab-side bc-side)
|
|
(.sub.vf c-to-point probe-point ca-side)
|
|
(.sub.vf a-to-point probe-point bc-side)
|
|
(.outer.product.a.vf acc c-to-b point-to-b)
|
|
(.outer.product.b.vf bc-side point-to-b c-to-b acc)
|
|
(.outer.product.a.vf acc point-to-b a-to-b)
|
|
(.outer.product.b.vf ab-side a-to-b point-to-b acc)
|
|
(.outer.product.a.vf acc c-to-point a-to-point)
|
|
(.outer.product.b.vf ca-side a-to-point c-to-point acc)
|
|
(.mul.vf bc-side bc-side plane-normal)
|
|
(.mul.vf ab-side ab-side plane-normal)
|
|
(.nop.vf)
|
|
(.mul.vf ca-side ca-side plane-normal)
|
|
(.add.x.vf.y bc-side bc-side bc-side)
|
|
(.add.x.vf.y ab-side ab-side ab-side)
|
|
(.add.x.vf.y ca-side ca-side ca-side)
|
|
(.nop.vf)
|
|
(.add.z.vf.y bc-side bc-side bc-side)
|
|
(.add.z.vf.y ab-side ab-side ab-side)
|
|
(.add.z.vf.y ca-side ca-side ca-side)
|
|
(.nop.vf)
|
|
(.mov bc-bits bc-side)
|
|
(.mov ab-bits ab-side)
|
|
(.mov ca-bits ca-side)
|
|
(>= (the-as int (logior (logior bc-bits ab-bits) ca-bits)) 0)))
|
|
|
|
(defun point-in-plane-<-point+normal! ((dst vector) (point vector) (normal vector))
|
|
"Construct another point in the plane through point with the supplied normal.
|
|
Two coordinates are offset and the coordinate with the largest normal component is solved from the plane equation.
|
|
The two free coordinates are offset by exactly one meter, so dst is a second plane point roughly
|
|
a meter and a half away rather than a direction. normal need not be unit."
|
|
;; Which coordinate to solve for is decided by magnitude so the division is by the largest
|
|
;; component available. Exact ties fall through to z.
|
|
(let ((plane-offset (+ (+ (* (-> normal x) (-> point x)) (* (-> normal y) (-> point y))) (* (-> normal z) (-> point z)))))
|
|
(set! (-> dst w) 1.0)
|
|
(let ((abs-x (fabs (-> normal x)))
|
|
(abs-y (fabs (-> normal y)))
|
|
(abs-z (fabs (-> normal z))))
|
|
(cond
|
|
((and (< abs-y abs-x) (< abs-z abs-x))
|
|
(set! (-> dst y) (+ (meters 1) (-> point y)))
|
|
(set! (-> dst z) (+ (meters 1) (-> point z)))
|
|
(set! (-> dst x) (/ (+ (- (- (* (-> normal y) (-> dst y))) (* (-> normal z) (-> dst z))) plane-offset) (-> normal x))))
|
|
((and (< abs-x abs-y) (< abs-z abs-y))
|
|
(set! (-> dst x) (+ (meters 1) (-> point x)))
|
|
(set! (-> dst z) (+ (meters 1) (-> point z)))
|
|
(set! (-> dst y) (/ (- (- plane-offset (* (-> normal x) (-> dst x))) (* (-> normal z) (-> dst z))) (-> normal y))))
|
|
(else
|
|
(set! (-> dst x) (+ (meters 1) (-> point x)))
|
|
(set! (-> dst y) (+ (meters 1) (-> point y)))
|
|
(set! (-> dst z) (/ (+ (- (- (* (-> normal x) (-> dst x))) (* (-> normal y) (-> dst y))) plane-offset) (-> normal z)))))))
|
|
dst)
|
|
|
|
|
|
(defun circle-circle-xz-intersect ((circle-a sphere) (circle-b sphere) (intersection-a vector) (intersection-b vector))
|
|
"Intersect two circles in the xz plane. Unused and not ported."
|
|
(format 0 "circle-circle-xz-intersect~%")
|
|
(crash!)
|
|
0)
|
|
|
|
(defun circle-test ()
|
|
"Test the circle-circle-xz-intersect function."
|
|
(let ((circle-a (new 'stack 'sphere))
|
|
(circle-b (new 'stack 'sphere))
|
|
(intersection-a (new-stack-vector0))
|
|
(intersection-b (new-stack-vector0)))
|
|
(set-vector! circle-a 0.0 0.0 0.0 1.0)
|
|
(set-vector! circle-b 100.0 0.0 0.0 10000.0)
|
|
(let ((result (circle-circle-xz-intersect circle-a circle-b intersection-a intersection-b)))
|
|
(format #t "res = ~d~%" result))
|
|
(format #t "(~f, ~f)~%" (-> intersection-a x) (-> intersection-a z))
|
|
(format #t "(~f, ~f)~%" (-> intersection-b x) (-> intersection-b z)))
|
|
(none))
|
|
|
|
(defun vector-circle-tangent-new ((point-a vector) (point-b vector) (tangent-a vector) (tangent-b vector))
|
|
"Write the xz tangent contact points from the position point-a to the circle centered on point-b
|
|
with radius point-b's w. point-a's w is ignored, so this takes a point and a circle despite both
|
|
arguments being vectors. Returns nothing, and the intersection count from
|
|
circle-circle-xz-intersect is discarded, so a point-a inside the circle leaves the outputs
|
|
untouched with no indication."
|
|
;; Thales' construction: the tangent contact points are exactly where the circle with the segment
|
|
;; from point-a to point-b as its diameter crosses the target circle, because the radius meets a
|
|
;; tangent at a right angle and any angle inscribed on a diameter is a right angle. So the whole
|
|
;; function is building that circle -- center at the midpoint, radius half the xz distance -- and
|
|
;; handing both circles to the general intersector. Only the x and z lanes take part; the
|
|
;; midpoint's y is computed and then ignored.
|
|
(rlet ((Q :class vf)
|
|
(vf0 :class vf)
|
|
(thales :class vf)
|
|
(half :class vf)
|
|
(circle-center :class vf)
|
|
(from-point :class vf)
|
|
(half-offset :class vf))
|
|
(init-vf0-vector)
|
|
(let ((thales-circle (new 'stack 'sphere)))
|
|
(set! (-> (new 'stack-no-clear 'vector) quad) (the-as uint128 0))
|
|
(set! (-> (new 'stack-no-clear 'vector) quad) (the-as uint128 0))
|
|
(let ((half-value 0.5)) (.lvf circle-center (&-> point-b quad)) (.mov half half-value))
|
|
(.lvf from-point (&-> point-a quad))
|
|
(.add.vf thales circle-center from-point)
|
|
(.sub.vf half-offset from-point circle-center)
|
|
(.mul.x.vf thales thales half)
|
|
(.mul.x.vf half-offset half-offset half)
|
|
(.mul.vf.xz half-offset half-offset half-offset)
|
|
(.add.z.vf.x half-offset half-offset half-offset)
|
|
(.sqrt.vf Q half-offset :ftf #b0)
|
|
(.wait.vf)
|
|
;; The Thales radius goes in w, which is where the sphere type keeps it.
|
|
(.mul.vf.w thales vf0 Q)
|
|
(.nop.vf)
|
|
(.nop.vf)
|
|
(.svf (&-> thales-circle quad) thales)
|
|
(circle-circle-xz-intersect (the-as sphere point-b) thales-circle tangent-a tangent-b))
|
|
0
|
|
(none)))
|
|
|
|
(defun vector-circle-tangent ((point vector) (circle vector) (tangent-a vector) (tangent-b vector))
|
|
"Write the two xz tangent points from an external point to a circle. circle's xz is the center and
|
|
its w is the radius; y is ignored throughout. point must lie outside the circle: the arc cosine of
|
|
radius over distance is not a number once the point is inside, the comparison below then fails,
|
|
and the function falls into the general case and writes nonsense."
|
|
;; The angle at the center between the direction to point and the direction to either contact point
|
|
;; is acos(radius / distance), so both contacts are the center-to-point direction turned by plus and
|
|
;; minus that angle and walked out one radius. The negated radius appears because the stored
|
|
;; displacement runs from point toward the center, the opposite of the direction being walked.
|
|
(let* ((to-center (vector-! (the-as vector (new 'stack-no-clear 'sphere)) circle point))
|
|
(center-distance (vector-xz-length to-center))
|
|
(tangent-angle (acos (/ (-> circle w) center-distance))))
|
|
(cond
|
|
;; Very small tangent angles are numerically unstable; use the perpendicular fallback.
|
|
;; The fallback returns two points a full center-distance to either side of point rather than
|
|
;; points on the circle, which is only close to right because a tiny angle means point is
|
|
;; nearly on the circle already.
|
|
((>= (degrees 3) tangent-angle)
|
|
(set! (-> tangent-a x) (- (-> point x) (-> to-center z)))
|
|
(set! (-> tangent-a y) 0.0)
|
|
(set! (-> tangent-a z) (+ (-> point z) (-> to-center x)))
|
|
(set! (-> tangent-b x) (+ (-> point x) (-> to-center z)))
|
|
(set! (-> tangent-b y) 0.0)
|
|
(set! (-> tangent-b z) (- (-> point z) (-> to-center x))))
|
|
(else
|
|
(let ((center-angle (atan (-> to-center z) (-> to-center x)))
|
|
(neg-radius (- (-> circle w)))
|
|
(sines (new 'stack-no-clear 'vector)))
|
|
(let ((cosines (new 'stack-no-clear 'vector)))
|
|
(let ((angles (new 'stack-no-clear 'vector)))
|
|
(set! (-> angles x) (- center-angle tangent-angle))
|
|
(set! (-> angles y) (+ center-angle tangent-angle))
|
|
(vector-sincos! sines cosines angles))
|
|
(set! (-> tangent-a x) (+ (-> circle x) (* neg-radius (-> cosines x))))
|
|
(set! (-> tangent-a z) (+ (-> circle z) (* neg-radius (-> sines x))))
|
|
(set! (-> tangent-b x) (+ (-> circle x) (* neg-radius (-> cosines y)))))
|
|
(set! (-> tangent-b z) (+ (-> circle z) (* neg-radius (-> sines y))))))))
|
|
0
|
|
(none))
|
|
|
|
;;;;;;;;;;;;;;;;;;;
|
|
;; curve
|
|
;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; Curves are clamped cubic nonuniform B-splines. For N control vertices, a degree-three spline
|
|
;; requires N + 4 knots: with last control index n = N - 1 and degree p = 3, the last knot index is
|
|
;; m = n + p + 1 = N + 3, so the knot count is m + 1 = N + 4. Knots must be nondecreasing.
|
|
;;
|
|
;; Valid span indices are 3 through N - 1. Span i blends controls i - 3 through i, so the first
|
|
;; span reads controls 0..3 and the last reads controls N-4..N-1. Repeating the first and last knot
|
|
;; four times gives cubic endpoint clamping and makes the curve interpolate its endpoint controls.
|
|
;; For example, seven controls use eleven knots:
|
|
;; [0 0 0 0 .25 .5 .75 1 1 1 1]
|
|
;; whose four spans select controls 0..3, 1..4, 2..5, and 3..6.
|
|
;;
|
|
;; Evaluation computes u = clamp(input * last-knot, first-knot, last-knot). Curve data uses a
|
|
;; zero first knot, so input [0, 1] maps to the full knot domain; this is not the general
|
|
;; first + input * (last - first) mapping. After locating u's span, the evaluator computes four
|
|
;; Cox-de Boor basis weights and blends that span's four controls.
|
|
;;
|
|
;; Input is parameter progress, not arc-length progress: .25 does not generally travel one quarter
|
|
;; of the curve's distance. Changing an interior knot while retaining the same controls changes the
|
|
;; basis weights of several neighboring spans and generally changes both shape and speed, not just
|
|
;; the point where one polynomial begins. Applying the same affine transform to every knot preserves
|
|
;; the curve when u receives the same transform; this evaluator handles uniform scaling directly
|
|
;; because first-knot is zero.
|
|
;;
|
|
;; The fast span candidate is for zero-origin, unit-spaced knots:
|
|
;; [0 0 0 0 1 2 3 ... last last last last]
|
|
;; When u is in [k, k+1), int(u) + 3 is the expected span: the +3 skips the first three repeated
|
|
;; zero entries before span 3. The candidate is checked against its actual knot bounds, so other
|
|
;; spacing falls back to binary search.
|
|
|
|
(defun find-knot-span ((high-span int) (low-span int) (value float) (knots (pointer float)))
|
|
"Find the knot span containing value, using an integer-derived fast candidate before binary search.
|
|
The highest candidate span comes first and the lowest second, which is the reverse of the usual
|
|
order; for a clamped cubic spline they are knot-count - 5 and 3. The returned span i means
|
|
knots[i] <= value < knots[i + 1], except for value exactly at the top knot, which is reported as
|
|
the last span rather than as out of range."
|
|
(let* ((top-knot (-> knots (+ high-span 1)))
|
|
(candidate-span (+ (the int value) 3))
|
|
(candidate-knots (&-> knots candidate-span)))
|
|
(cond
|
|
;; The upper endpoint belongs to the final span despite its half-open interval.
|
|
((= value top-knot)
|
|
high-span)
|
|
;; Unit-spaced knots make int(value) + 3 the likely span. Accept it only when its
|
|
;; actual knot interval contains value.
|
|
((and (>= value (-> candidate-knots 0))
|
|
(< value (-> candidate-knots 1)))
|
|
candidate-span)
|
|
(else
|
|
;; Maintain [search-low, search-high) around the result. This also advances across
|
|
;; repeated knots, selecting the first nonempty span whose upper knot exceeds value.
|
|
(let ((search-low low-span)
|
|
(search-high (+ high-span 1)))
|
|
(while (> (- search-high search-low) 1)
|
|
(let ((mid-span (/ (+ search-low search-high) 2)))
|
|
(if (< value (-> knots mid-span))
|
|
(set! search-high mid-span)
|
|
(set! search-low mid-span))))
|
|
search-low)))))
|
|
|
|
(defun calculate-basis-functions-vector! ((dst vector) (span int) (value float) (knots (pointer float)))
|
|
"Compute the four nonzero cubic B-spline basis weights for value in span."
|
|
;; Unrolled Cox-de Boor recurrence. left-N and right-N are the distances from value
|
|
;; to the knots on either side of the selected span.
|
|
(let* ((span-knot (&-> knots span))
|
|
(left-1 (- value (-> span-knot 0)))
|
|
(left-2 (- value (-> span-knot -1)))
|
|
(left-3 (- value (-> span-knot -2)))
|
|
(right-1 (- (-> span-knot 1) value))
|
|
(right-2 (- (-> span-knot 2) value))
|
|
(right-3 (- (-> span-knot 3) value))
|
|
;; Linear basis functions.
|
|
(linear-denominator (+ right-1 left-1))
|
|
(linear-0 (/ right-1 linear-denominator))
|
|
(linear-1 (/ left-1 linear-denominator))
|
|
;; Quadratic basis functions.
|
|
(quadratic-0 (* right-1 (/ linear-0 (+ right-1 left-2))))
|
|
(quadratic-1 (+ (* left-2 (/ linear-0 (+ right-1 left-2)))
|
|
(* right-2 (/ linear-1 (+ right-2 left-1)))))
|
|
(quadratic-2 (* left-1 (/ linear-1 (+ right-2 left-1))))
|
|
;; Cubic basis functions.
|
|
(basis-0 (* right-1 (/ quadratic-0 (+ right-1 left-3))))
|
|
(basis-1 (+ (* left-3 (/ quadratic-0 (+ right-1 left-3)))
|
|
(* right-2 (/ quadratic-1 (+ right-2 left-2)))))
|
|
(basis-2 (+ (* left-2 (/ quadratic-1 (+ right-2 left-2)))
|
|
(* right-3 (/ quadratic-2 (+ right-3 left-1)))))
|
|
(basis-3 (* left-1 (/ quadratic-2 (+ right-3 left-1)))))
|
|
(set! (-> dst x) basis-0)
|
|
(set! (-> dst y) basis-1)
|
|
(set! (-> dst z) basis-2)
|
|
(set! (-> dst w) basis-3))
|
|
dst)
|
|
|
|
(defun curve-evaluate! ((dst vector)
|
|
(input float)
|
|
(control-points (inline-array vector))
|
|
(control-point-count int)
|
|
(knots (pointer float))
|
|
(knot-count int))
|
|
"Evaluate a clamped cubic nonuniform B-spline at normalized input."
|
|
(let* ((first-knot (-> knots 0))
|
|
(last-knot (-> knots (+ knot-count -1)))
|
|
(u (fmax (fmin (* input last-knot) last-knot) first-knot))
|
|
(last-span (+ knot-count -5))
|
|
(span (find-knot-span last-span 3 u knots))
|
|
(basis (new 'stack-no-clear 'vector))
|
|
(first-control (- span 3))
|
|
(control-0 (-> control-points first-control))
|
|
(control-1 (-> control-points (+ first-control 1)))
|
|
(control-2 (-> control-points (+ first-control 2)))
|
|
(control-3 (-> control-points (+ first-control 3))))
|
|
(calculate-basis-functions-vector! basis span u knots)
|
|
(set! (-> dst x) (+ (* (-> control-0 x) (-> basis x))
|
|
(* (-> control-1 x) (-> basis y))
|
|
(* (-> control-2 x) (-> basis z))
|
|
(* (-> control-3 x) (-> basis w))))
|
|
(set! (-> dst y) (+ (* (-> control-0 y) (-> basis x))
|
|
(* (-> control-1 y) (-> basis y))
|
|
(* (-> control-2 y) (-> basis z))
|
|
(* (-> control-3 y) (-> basis w))))
|
|
(set! (-> dst z) (+ (* (-> control-0 z) (-> basis x))
|
|
(* (-> control-1 z) (-> basis y))
|
|
(* (-> control-2 z) (-> basis z))
|
|
(* (-> control-3 z) (-> basis w))))
|
|
(set! (-> dst w) 1.0))
|
|
dst)
|
|
|
|
(defun curve-get-pos! ((dst vector) (input float) (curve-data curve))
|
|
"Evaluate curve-data at normalized input and write its position to dst. dst.w becomes 1.0. The
|
|
curve's cached length field is not consulted; input is parameter progress."
|
|
(curve-evaluate! dst
|
|
input
|
|
(-> curve-data cverts)
|
|
(-> curve-data num-cverts)
|
|
(-> curve-data knots)
|
|
(-> curve-data num-knots)))
|
|
|
|
(defun curve-length ((curve-data curve))
|
|
"Estimate total curve length with three uniformly spaced parameter samples per control point.
|
|
This value does not make the curve distance-parameterized.
|
|
The estimate is the length of the inscribed polyline through those samples, so it always
|
|
undershoots the true arc length, and it undershoots more the tighter the curvature. It does not
|
|
read or update the curve's cached length field."
|
|
;; The first sample uses knots[0] rather than 0.0 as the parameter. Curve data has a zero first
|
|
;; knot, so the two agree, but a curve whose first knot is nonzero would start the walk at the
|
|
;; clamped endpoint either way.
|
|
(let ((previous-point (new-stack-vector0))
|
|
(current-point (new-stack-vector0))
|
|
(segment-count (* 3 (-> curve-data num-cverts)))
|
|
(total-length 0.0))
|
|
(curve-evaluate! current-point
|
|
(-> curve-data knots 0)
|
|
(-> curve-data cverts)
|
|
(-> curve-data num-cverts)
|
|
(-> curve-data knots)
|
|
(-> curve-data num-knots))
|
|
(dotimes (i segment-count)
|
|
(set! (-> previous-point quad) (-> current-point quad))
|
|
(curve-evaluate! current-point
|
|
(/ (the float (+ i 1)) (the float segment-count))
|
|
(-> curve-data cverts)
|
|
(-> curve-data num-cverts)
|
|
(-> curve-data knots)
|
|
(-> curve-data num-knots))
|
|
(+! total-length (vector-vector-distance previous-point current-point)))
|
|
total-length))
|
|
|
|
(defun curve-copy! ((dst curve) (src curve))
|
|
"Shallow-copy a curve descriptor, retaining its control-point and knot references. The two curves
|
|
share the same control-point and knot storage afterwards, so editing one edits both."
|
|
(set! (-> dst cverts) (-> src cverts))
|
|
(set! (-> dst num-cverts) (-> src num-cverts))
|
|
(set! (-> dst knots) (-> src knots))
|
|
(set! (-> dst num-knots) (-> src num-knots))
|
|
(set! (-> dst length) (-> src length))
|
|
dst)
|
|
|
|
(defun curve-closest-point ((curve-data curve) (point vector) (initial-input float) (search-distance float) (iterations int) (input-padding float))
|
|
"Refine a normalized parameter toward the curve point nearest point.
|
|
Each iteration compares the two ends of the current interval, moves the center toward the
|
|
better end, and halves the interval; distance arguments are converted with the estimated length.
|
|
search-distance and input-padding are world distances, divided by the estimated curve length to
|
|
reach parameter units; a search-distance of zero or less means half the curve. This is a local
|
|
refinement, not a global search: it converges on whichever nearby minimum the starting interval
|
|
brackets. The padding is subtracted before the search and added back afterwards, so the returned
|
|
parameter is not clamped and can leave zero to one even though the interior iterations are."
|
|
(local-vars (search-radius float))
|
|
(set! search-radius search-distance)
|
|
(let ((remaining iterations)
|
|
(padding input-padding)
|
|
(curve-total-length (curve-length curve-data))
|
|
(low-point (new 'stack-no-clear 'vector))
|
|
(high-point (new 'stack-no-clear 'vector)))
|
|
0.0
|
|
0.0
|
|
0.0
|
|
0.0
|
|
(let ((half-width 0.5))
|
|
0.0
|
|
(if (< 0.0 search-radius) (set! half-width (/ search-radius curve-total-length)))
|
|
(let* ((center (- initial-input (/ padding curve-total-length)))
|
|
(low-input (- center half-width))
|
|
(high-input (+ center half-width)))
|
|
(curve-get-pos! low-point low-input curve-data)
|
|
(curve-get-pos! high-point high-input curve-data)
|
|
;; Squared distances are enough, since only the comparison between the two ends matters.
|
|
(let ((low-distance-squared (vector-vector-distance-squared low-point point))
|
|
(high-distance-squared (vector-vector-distance-squared high-point point)))
|
|
(while (> remaining 0)
|
|
(+! remaining -1)
|
|
(set! half-width (* 0.5 half-width))
|
|
;; The losing end is replaced by the center, and the center steps toward the winner.
|
|
(let ((next-center (cond
|
|
((< low-distance-squared high-distance-squared)
|
|
(curve-get-pos! high-point center curve-data)
|
|
(set! high-distance-squared (vector-vector-distance-squared high-point point))
|
|
(set! high-input center)
|
|
(- center half-width))
|
|
(else
|
|
(curve-get-pos! low-point center curve-data)
|
|
(set! low-distance-squared (vector-vector-distance-squared low-point point))
|
|
(set! low-input center)
|
|
(+ center half-width)))))
|
|
(set! center (fmin 1.0 (fmax 0.0 next-center)))))
|
|
(+ (if (< low-distance-squared high-distance-squared) low-input high-input) (/ padding curve-total-length)))))))
|
|
|
|
(defun vector-plane-distance ((point vector) (plane-pt vector) (normal vector))
|
|
"Return the signed distance from point to plane-data along normal."
|
|
(vector-dot (vector-! (new 'stack-no-clear 'vector) point plane-pt) normal))
|