Files
jak-project/goal_src/jak1/engine/collide/collide-func.gc
T
2026-07-26 14:43:53 -04:00

1032 lines
50 KiB
Common Lisp

;;-*-Lisp-*-
(in-package goal)
(bundles "ENGINE.CGO" "GAME.CGO")
(require "engine/math/vector.gc")
(require "kernel/gkernel-h.gc")
;; This file contains the primitive intersection functions used for collision.
;; Most take a description of primitive and a "probe"
;; The probe has an origin and a direction. The length of the direction vector is the length
;; of the probe.
;; Generally, collision functions will return the fraction of the probe to reach the primitive.
;; For example, if the probe is 5.0 long, and hits the primitive 2.0 away from the probe origin,
;; the return value (u) would be 0.4.
;; If (u) would be > 1.0, then it counts as "not intersecting" (object too far away)
;; If (u) would be < 0.0, then it counts as "not intersecting" (object behind probe)
;; If there's a miss, return COLLISION_MISS, a large negative number.
;; If we are inside of the primitive, return 0.0
;; Two VU0 idioms appear throughout this file.
;;
;; There is no dot-product instruction, so a dot product is a componentwise multiply followed by two
;; lane adds that fold the remaining components into one lane:
;;
;; (.mul.vf products left right)
;; (.add.x.vf.y products products products) ;; products.y += products.x
;; (.add.z.vf.y products products products) ;; products.y += products.z
;;
;; Which lane the adds target is not cosmetic. It determines how the result is read back out, which
;; is the second idiom.
;;
;; (.mov <int-local> <vf>) moves the register's low doubleword into a 64-bit integer register, so
;; lanes x and y land in bits 0 through 31 and 32 through 63, and bit 63 -- the integer's sign bit --
;; is the y lane's sign bit. A scalar parked in y can therefore be sign-tested with an ordinary
;; integer branch, with no floating-point compare and no wait on the VU0 pipeline. Two such words also
;; combine in a single operation:
;;
;; (logand a b) is negative <=> both y lanes are negative
;; (logior a b) is negative <=> either y lane is negative
;;
;; so three sign questions cost two ORs and one branch. That is how the inside test in
;; ray-triangle-intersect and the end-cap tests in ray-cylinder-intersect are written. Two things bite
;; here: negative zero counts as negative, and the x lane of these registers is often left holding a
;; stale product on purpose. Where a value must be compared as a float instead,
;; (.mov <float-local> <vf>) reads the x lane.
;; decomp begins
(defconstant COLLISION_MISS -100000000.0)
(#unless PC_PORT
(defun raw-ray-sphere-intersect ((radius float))
"Solve the finite ray-sphere quadratic for a sphere at the origin. radius is passed normally;
ray-relative-origin and ray-direction occupy VU0 vf1 and vf2 under the EE calling
convention. Return zero from inside the sphere and COLLISION_MISS for a miss."
(declare (asm-func float))
;; For P(t) = origin + t * direction, use the half-b quadratic
;; a = direction.direction, b = direction.origin, c = origin.origin - radius^2
;; and take t = (-b - sqrt(b^2 - a*c)) / a. The direction contains the full probe
;; displacement, so the useful interval is zero through one.
(rlet ((origin :reg vf1)
(direction :reg vf2)
(radius-vf :reg vf3)
(a-and-reciprocal :reg vf4)
(b-term :reg vf5)
(c-and-limit-term :reg vf6)
(a-times-c :reg vf7)
(b-squared :reg vf8)
(discriminant-and-root :reg vf9)
(Q :reg Q)
(result)
(a-bits)
(a-bits-float)
(b-bits)
(c-bits)
(discriminant-bits)
(limit-left-bits)
(limit-right-bits)
(unused-one)
(zero-float))
(m radius-vf radius)
(mul.vf a-and-reciprocal direction direction)
(mul.vf radius-vf radius-vf radius-vf)
(mul.vf c-and-limit-term origin origin)
(mul.vf b-term direction origin)
(add.y.vf.x a-and-reciprocal a-and-reciprocal a-and-reciprocal)
(m! result 0)
(add.x.vf.y c-and-limit-term c-and-limit-term c-and-limit-term)
(sub.x.vf.z c-and-limit-term c-and-limit-term radius-vf)
(add.z.vf.x a-and-reciprocal a-and-reciprocal a-and-reciprocal)
(add.x.vf.y b-term b-term b-term)
(m! b-bits 0)
(add.z.vf.y c-and-limit-term c-and-limit-term c-and-limit-term)
(div.w.x Q vf0 a-and-reciprocal)
(add.z.vf.y b-term b-term b-term)
(m a-bits a-and-reciprocal)
(mul.x.vf a-times-c c-and-limit-term a-and-reciprocal)
(m c-bits c-and-limit-term)
(mul.vf b-squared b-term b-term)
;; An origin inside the sphere is already in contact.
(b.lt c-bits r0 done :delay (m a-bits a-bits))
(mulq.vf.w a-and-reciprocal vf0 Q)
(sub.vf discriminant-and-root b-squared a-times-c)
(m a-bits-float a-bits)
(m zero-float b-bits)
(c.eq.s a-bits-float zero-float)
(vnop)
(b.fpt miss :delay (m b-bits b-term))
;; A nonnegative b means the ray is moving away from a sphere that starts outside it.
(sqrt.y Q discriminant-and-root)
(b.ge b-bits r0 miss :delay (m discriminant-bits discriminant-and-root))
(b.lt discriminant-bits r0 miss :delay (m! unused-one 1.0))
;; The following sign-bit identity rejects the smaller root when it lies beyond t = 1.
(add.x.vf c-and-limit-term b-term a-and-reciprocal)
(m limit-left-bits c-and-limit-term)
(mul.vf c-and-limit-term c-and-limit-term c-and-limit-term)
(mulq.vf.w discriminant-and-root vf0 Q)
(sub.vf c-and-limit-term discriminant-and-root c-and-limit-term)
(add.w.vf.y discriminant-and-root b-term discriminant-and-root)
(m limit-right-bits c-and-limit-term)
(mul.w.vf.y discriminant-and-root discriminant-and-root a-and-reciprocal)
(and limit-left-bits limit-left-bits limit-right-bits)
(b.lt limit-left-bits r0 miss :delay (sub.y.vf a-and-reciprocal vf0 discriminant-and-root))
(b done :delay (m result a-and-reciprocal))
(label miss)
(m! result COLLISION_MISS)
(label done)
result)))
(#when PC_PORT
(defun raw-ray-sphere-intersect ((radius float))
"The EE version receives the relative ray origin and direction in VU0 registers. PC callers must
use pc-port-raw-ray-sphere-implementation, which makes those arguments explicit."
(local-vars
(b-bits float)
(discriminant-bits float)
(limit-left-bits number)
(a-float float)
(a-float-copy float)
(limit-right-bits int)
(c-bits float))
(crash!)
(rlet ((Q :class vf)
(vf0 :class vf)
(origin :class vf)
(direction :class vf)
(radius-vf :class vf)
(a-and-reciprocal :class vf)
(b-term :class vf)
(c-and-limit-term :class vf)
(a-times-c :class vf)
(b-squared :class vf)
(discriminant-and-root :class vf))
(init-vf0-vector)
(.mov radius-vf radius) ;; radius-vf = radius
;; sphere is at the origin, origin is source of the ray (o)
;; direction is the ray's full probe displacement (u)
(.mul.vf a-and-reciprocal direction direction) ;; a-and-reciprocal = u.^2
(.mul.vf radius-vf radius-vf radius-vf) ;; radius-vf = r^2
(.mul.vf c-and-limit-term origin origin) ;; c-and-limit-term = o.^2
(.mul.vf b-term direction origin) ;; b-term = u . o
(.add.y.vf.x a-and-reciprocal a-and-reciprocal a-and-reciprocal)
(let ((result (the-as float 0)))
(.add.x.vf.y c-and-limit-term c-and-limit-term c-and-limit-term)
(.sub.x.vf.z c-and-limit-term c-and-limit-term radius-vf)
(.add.z.vf.x a-and-reciprocal a-and-reciprocal a-and-reciprocal)
(.add.x.vf.y b-term b-term b-term)
(let ((zero-float (the-as float 0)))
(.add.z.vf.y c-and-limit-term c-and-limit-term c-and-limit-term)
(.div.vf Q vf0 a-and-reciprocal :fsf #b11 :ftf #b0)
(.add.z.vf.y b-term b-term b-term)
(.mov a-float a-and-reciprocal)
(.mul.x.vf a-times-c c-and-limit-term a-and-reciprocal)
(.mov c-bits c-and-limit-term)
(.mul.vf b-squared b-term b-term)
(b! (< (the-as int c-bits) 0) done :delay (set! a-float-copy a-float))
(.mul.vf.w a-and-reciprocal vf0 Q)
(.sub.vf discriminant-and-root b-squared a-times-c)
(b! (= a-float-copy zero-float) miss :delay (.mov b-bits b-term)))
(.sqrt.vf Q discriminant-and-root :ftf #b1)
(b! (>= (the-as int b-bits) 0) miss :delay (.mov discriminant-bits discriminant-and-root))
(b! (< (the-as int discriminant-bits) 0) miss :delay 1.0)
(.add.x.vf c-and-limit-term b-term a-and-reciprocal)
(.mov limit-left-bits c-and-limit-term)
(.mul.vf c-and-limit-term c-and-limit-term c-and-limit-term)
(.mul.vf.w discriminant-and-root vf0 Q)
(.sub.vf c-and-limit-term discriminant-and-root c-and-limit-term)
(.add.w.vf.y discriminant-and-root b-term discriminant-and-root)
(.mov limit-right-bits c-and-limit-term)
(.mul.w.vf.y discriminant-and-root discriminant-and-root a-and-reciprocal)
(b! (< (logand (the-as uint limit-left-bits) (the-as uint limit-right-bits)) 0)
miss
:delay (.sub.y.vf a-and-reciprocal vf0 discriminant-and-root))
(b! #t done :delay (.mov result a-and-reciprocal))
(label miss)
(set! result -100000000.0)
(label done)
(the-as float result)))))
(defmacro pc-port-do-raw-ray-sphere-intersect (radius relative-origin ray-direction)
"Spill the two VU arguments so the PC implementation can receive them through the normal ABI."
`(let ((relative-origin-storage (new 'stack-no-clear 'vector))
(ray-direction-storage (new 'stack-no-clear 'vector)))
(.svf (&-> relative-origin-storage quad) ,relative-origin)
(.svf (&-> ray-direction-storage quad) ,ray-direction)
(pc-port-raw-ray-sphere-implementation ,radius relative-origin-storage ray-direction-storage)))
(defun pc-port-raw-ray-sphere-implementation ((radius float) (relative-origin vector) (ray-direction vector))
"Solve the finite ray-sphere quadratic for a sphere at the origin with explicit PC ABI arguments.
Return zero when relative-origin is inside the sphere and COLLISION_MISS when the ray points
away, has no real root, has zero squared length, or first meets the sphere beyond its endpoint."
;; With P(t) = o + t*u, the half-b form is
;; a = u.u, b = u.o, c = o.o - radius^2, discriminant = b^2 - a*c.
;; The first root is (-b - sqrt(discriminant)) / a. Since u is the complete probe displacement,
;; t is already the fraction used by the collision callers.
;;
;;
;; Which lane each coefficient lands in matters. a goes to x, since it is the only coefficient tested
;; as a float and it is also the divisor handed to the VU0 scalar pipeline, and both of those read x.
;; b and c go to y so their signs can be taken from a 64-bit integer move. a-and-reciprocal is named
;; for its second job: 1/a is parked in its w lane, which turns the final scaling of the root into a
;; single w-lane multiply.
;;
;; The rejections, in order: c below zero means the ray starts inside the sphere, which returns zero
;; rather than a miss; a equal to zero is a zero-length probe whose reciprocal would be garbage; b at
;; or above zero points away from a sphere the ray starts outside; and a negative discriminant misses
;; the sphere entirely.
;;
;; The t greater than one rejection is the subtle one. Writing L for -(b + a), the root passes one
;; exactly when L exceeds sqrt(discriminant), which requires both L above zero and L squared above
;; the discriminant. Both are sign questions, so the code forms (b + a) and
;; (discriminant - (b + a)^2), moves each y lane into an integer register and rejects when the AND of
;; the two is negative. The square root is never compared against anything.
(local-vars
(b-bits int)
(discriminant-bits int)
(limit-left-bits int)
(a-float float)
(a-float-copy float)
(limit-right-bits int)
(c-bits int))
(rlet ((Q :class vf)
(vf0 :class vf)
(origin :class vf)
(direction :class vf)
(radius-vf :class vf)
(a-and-reciprocal :class vf)
(b-term :class vf)
(c-and-limit-term :class vf)
(a-times-c :class vf)
(b-squared :class vf)
(discriminant-and-root :class vf))
(init-vf0-vector)
(.lvf origin (&-> relative-origin quad))
(.lvf direction (&-> ray-direction quad))
(.mov radius-vf radius)
(.mul.vf a-and-reciprocal direction direction)
(.mul.vf radius-vf radius-vf radius-vf) ;; r^2 in every lane
(.mul.vf c-and-limit-term origin origin)
(.mul.vf b-term direction origin)
(.add.y.vf.x a-and-reciprocal a-and-reciprocal a-and-reciprocal)
(let ((result (the-as float 0)))
(.add.x.vf.y c-and-limit-term c-and-limit-term c-and-limit-term)
;; The -r^2 term rides in the z lane, so the z fold below pays for it.
(.sub.x.vf.z c-and-limit-term c-and-limit-term radius-vf)
(.add.z.vf.x a-and-reciprocal a-and-reciprocal a-and-reciprocal) ;; a lands in x
(.add.x.vf.y b-term b-term b-term)
(let ((zero-float (the-as float 0)))
(.add.z.vf.y c-and-limit-term c-and-limit-term c-and-limit-term) ;; c lands in y
(.div.vf Q vf0 a-and-reciprocal :fsf #b11 :ftf #b0) ;; start 1/a in the scalar pipeline
(.add.z.vf.y b-term b-term b-term) ;; b lands in y
(.mov a-float a-and-reciprocal) ;; a as a float, out of the x lane
(.mul.x.vf a-times-c c-and-limit-term a-and-reciprocal) ;; a*c in y
(.mov c-bits c-and-limit-term) ;; c's sign, in bit 63
(.mul.vf b-squared b-term b-term) ;; b^2 in y
(b! (< (the-as int c-bits) 0) done :delay (set! a-float-copy a-float)) ;; in the sphere
(.mul.vf.w a-and-reciprocal vf0 Q) ;; park 1/a in the w lane
(.sub.vf discriminant-and-root b-squared a-times-c) ;; discriminant in y
(b! (= a-float-copy zero-float) miss :delay (.mov b-bits b-term)) ;; bad denominator in division
)
(.sqrt.vf Q discriminant-and-root :ftf #b1) ;; start sqrt(discriminant) from the y lane
(b! (>= (the-as int b-bits) 0) miss :delay (.mov discriminant-bits discriminant-and-root)) ;; wrong dir
(b! (< (the-as int discriminant-bits) 0) miss :delay 1.0) ;; bad sqrt
;; The two sign bits gathered below are sign(b + a) and sign(discriminant - (b + a)^2); the root
;; itself is computed in between them.
(.add.x.vf c-and-limit-term b-term a-and-reciprocal) ;; b + a in y
(.mov limit-left-bits c-and-limit-term)
(.mul.vf c-and-limit-term c-and-limit-term c-and-limit-term) ;; (b + a)^2
(.mul.vf.w discriminant-and-root vf0 Q) ;; sqrt(discriminant) into the w lane
(.sub.vf c-and-limit-term discriminant-and-root c-and-limit-term) ;; discriminant - (b + a)^2
(.add.w.vf.y discriminant-and-root b-term discriminant-and-root) ;; b + sqrt(discriminant)
(.mov limit-right-bits c-and-limit-term)
(.mul.w.vf.y discriminant-and-root discriminant-and-root a-and-reciprocal) ;; times 1/a
;; too far.
;; The delay slot negates the quotient into x, which is where the result is read.
(b! (< (logand (the-as int limit-left-bits) (the-as int limit-right-bits)) 0)
miss
:delay (.sub.y.vf a-and-reciprocal vf0 discriminant-and-root))
(b! #t done :delay (.mov result a-and-reciprocal))
(label miss)
(set! result -100000000.0)
(label done)
(the-as float result))))
(defun ray-sphere-intersect ((ray-origin vector) (ray-direction vector) (sphere-origin vector) (radius float))
"Return the first contact fraction for a finite ray against a sphere. ray-direction spans the
whole probe rather than being unit length, so zero is the ray origin and one is its end. Return
zero when the origin is inside the sphere and COLLISION_MISS when contact lies outside the probe."
;; Translate the sphere to the origin for the shared quadratic.
(rlet ((vu-relative-origin :class vf)
(vu-direction :class vf))
(.lvf vu-relative-origin (&-> ray-origin quad))
(.lvf vu-direction (&-> sphere-origin quad))
(.sub.vf vu-relative-origin vu-relative-origin vu-direction) ;; the sphere is at the origin in the actual intersection.
(.lvf vu-direction (&-> ray-direction quad))
(#if PC_PORT (pc-port-do-raw-ray-sphere-intersect radius vu-relative-origin vu-direction) (raw-ray-sphere-intersect radius))))
(defun ray-circle-intersect ((ray-origin vector) (ray-direction vector) (circle-origin vector) (radius float))
"Return the first contact fraction for a finite ray against a circle in the XZ plane. The Y
components of the origin, direction, and circle center are ignored."
(rlet ((vf0 :class vf)
(vu-relative-origin :class vf)
(vu-direction :class vf))
(init-vf0-vector)
(.lvf vu-relative-origin (&-> ray-origin quad))
(.mov.vf.y vu-relative-origin vf0)
(.lvf vu-direction (&-> circle-origin quad))
(.mov.vf.y vu-direction vf0)
(.sub.vf vu-relative-origin vu-relative-origin vu-direction)
(.lvf vu-direction (&-> ray-direction quad))
(.mov.vf.y vu-direction vf0)
(#if PC_PORT (pc-port-do-raw-ray-sphere-intersect radius vu-relative-origin vu-direction) (raw-ray-sphere-intersect radius))))
(defun ray-cylinder-intersect ((ray-origin vector)
(ray-direction vector)
(cylinder-origin vector)
(cylinder-axis vector)
(radius float)
(cylinder-length float)
(axis-point-out vector))
"Return the first contact fraction for a finite ray against the curved side of a finite cylinder.
cylinder-axis must be unit length and cylinder-length measures from cylinder-origin along that
axis. End caps are not tested. Write the corresponding point on the cylinder axis to
axis-point-out; only use that output after a nonnegative return. Return COLLISION_MISS when the
ray misses or meets the infinite cylinder beyond either end."
;; The test runs in the cylinder's frame, splitting every point into a height along the axis and an
;; offset perpendicular to it. cylinder-axis is unit length, so the perpendicular part of v is just
;; v - axis * (v . axis). Stripping the axial part off both the ray origin and the ray direction
;; leaves a two-dimensional ray against a circle in the cross section, which is what
;; raw-ray-sphere-intersect solves once the axial component is gone.
;;
;; The heights all live in y lanes, so every cap test is a sign test:
;; axial-start .y the origin's height
;; axial-travel .y the height the ray covers
;; axial-end .y their sum
;; overshoot .y a height minus cylinder-length, negative below the far cap
;;
;; The two rejections before the circle test are a slab reject only. A segment can straddle a cap and
;; still hit the side, so the decision is the contact height recomputed afterwards and checked
;; against both caps. The end caps are not surfaces: a ray entering through one is a miss, and the
;; swept-sphere callers cover that case with their vertex spheres.
(local-vars
(result float)
(axial-start-bits int)
(overshoot-bits int)
(axial-end-bits int)
(end-overshoot-bits int)
(axial-contact-bits int)
(contact-overshoot-bits int))
(rlet ((perp-origin :class vf)
(ray-origin-vf :class vf)
(direction :class vf)
(axis-point :class vf)
(axis :class vf)
(cylinder-length-vf :class vf)
(relative-origin :class vf)
(axial-start :class vf)
(axial-travel :class vf)
(axial-end :class vf)
(overshoot :class vf)
(perp-direction :class vf)
(end-overshoot :class vf)
(contact-fraction-vf :class vf))
(.lvf ray-origin-vf (&-> ray-origin quad))
(.lvf axis-point (&-> cylinder-origin quad)) ;; Walks out to the contact point on the axis.
(.sub.vf relative-origin ray-origin-vf axis-point)
(.lvf direction (&-> ray-direction quad))
(.lvf axis (&-> cylinder-axis quad))
(.mov cylinder-length-vf cylinder-length)
(.mul.vf axial-start relative-origin axis)
(.mul.vf axial-travel direction axis)
(.add.x.vf.y axial-start axial-start axial-start)
(.add.x.vf.y axial-travel axial-travel axial-travel)
(.add.z.vf.y axial-start axial-start axial-start) ;; The origin's height, in y.
(.add.z.vf.y axial-travel axial-travel axial-travel) ;; The height the ray covers, in y.
(.mul.y.vf perp-origin axis axial-start) ;; Axial part of the origin.
(.add.vf axial-end axial-travel axial-start) ;; The endpoint's height, in y.
(.sub.x.vf overshoot axial-start cylinder-length-vf)
(.mul.y.vf perp-direction axis axial-travel) ;; Axial part of the direction.
(.mov axial-start-bits axial-start)
(.sub.x.vf end-overshoot axial-end cylinder-length-vf)
(.mov axial-end-bits axial-end)
;; Both heights negative: the whole segment is below the near cap.
(let ((both-ends-below-base (logand axial-start-bits (the-as uint axial-end-bits))))
(.sub.vf perp-origin relative-origin perp-origin) ;; Down to the cross section.
(b! (< both-ends-below-base 0) miss :delay (.sub.vf perp-direction direction perp-direction)))
(.mov overshoot-bits overshoot)
(.mov end-overshoot-bits end-overshoot)
;; Neither overshoot negative: the whole segment is at or past the far cap.
(b! (>= (the-as int (logior overshoot-bits (the-as uint end-overshoot-bits))) 0) miss :delay (nop!))
;; The circle test in the cross section. perp-origin and perp-direction are the VU0 arguments.
(let ((contact-fraction (#if PC_PORT (pc-port-do-raw-ray-sphere-intersect radius perp-origin perp-direction) (raw-ray-sphere-intersect radius))))
(b! (< (the-as int contact-fraction) 0) miss :delay (.mov contact-fraction-vf contact-fraction))
(.mul.x.vf axial-travel axial-travel contact-fraction-vf)
(.add.vf axial-start axial-start axial-travel) ;; The contact height, in y.
(.mul.y.vf axis axis axial-start) ;; The axis, scaled to the contact height.
(.sub.x.vf overshoot axial-start cylinder-length-vf)
(.mov axial-contact-bits axial-start)
;; A contact off either end lies on the infinite cylinder rather than this one. The delay slots
;; finish and store the axis point, so the far-cap rejection leaves axis-point-out written; the
;; return value has to be checked before that output is used.
(b! (< (the-as int axial-contact-bits) 0) miss :delay (.add.vf.xyz axis-point axis-point axis))
(.mov contact-overshoot-bits overshoot)
(b! (>= (the-as int contact-overshoot-bits) 0) miss :delay (.svf (&-> axis-point-out quad) axis-point))
(b! #t done :delay (set! result contact-fraction)))
(label miss)
(set! result -100000000.0)
(label done)
result))
(defun ray-plane-intersect ((intersection-out vector)
(normal-out vector)
(ray-origin vector)
(ray-direction vector)
(plane-a vector)
(plane-b vector)
(plane-c vector))
"Intersect a ray with the infinite plane through plane-a, plane-b, and plane-c. Write the
intersection point and a unit plane normal to the output vectors, and return the ray parameter.
Return COLLISION_MISS without writing the outputs when the ray is parallel to the plane; this
function does not restrict the parameter to the finite zero-to-one probe interval."
;; The plane arrives as three points, so the normal is the cross product of the two edges leaving
;; plane-b, oriented as (b - c) x (b - a). A point p lies on the plane when n . (p - b) is zero, so
;; substituting p = origin + t*direction gives
;; t = (n . (b - origin)) / (n . direction)
;; and those two dot products are the only real work. Both fold into the x lane rather than y,
;; because both are read out as floats: one for the zero check and one for an ordinary scalar divide,
;; not the VU0 pipeline.
;;
;; normal-out is unit length with w set to one. The reciprocal length is started as soon as the
;; squared length is known, well before the cond decides whether it is needed, and the later
;; .wait.vf is what collects it.
;;
;; t is not clamped, so it can be negative or past one and the caller has to bound it. Neither output
;; is written on the parallel path, and three collinear points give both a zero normal and a zero
;; denominator, so they leave through that same path.
(local-vars (direction-dot float) (unused-intersection-read float) (plane-offset-dot float))
(rlet ((acc :class vf)
(Q :class vf)
(vf0 :class vf)
(edge-ba :class vf)
(edge-bc :class vf)
(vertex-b :class vf)
(normal :class vf)
(normal-length-squared :class vf)
(origin :class vf)
(direction :class vf)
(plane-offset :class vf)
(direction-normal-products :class vf))
(init-vf0-vector)
(.lvf vertex-b (&-> plane-b quad))
(.lvf edge-ba (&-> plane-a quad))
(.lvf edge-bc (&-> plane-c quad))
(.sub.vf edge-ba vertex-b edge-ba)
(.sub.vf edge-bc vertex-b edge-bc)
(.lvf origin (&-> ray-origin quad))
(.lvf direction (&-> ray-direction quad))
(.sub.vf plane-offset vertex-b origin)
(.outer.product.a.vf acc edge-bc edge-ba)
(.outer.product.b.vf normal edge-ba edge-bc acc)
(.mul.vf plane-offset plane-offset normal)
(.mul.vf direction-normal-products direction normal)
(.mul.vf normal-length-squared normal normal)
(.add.y.vf.x plane-offset plane-offset plane-offset)
(.add.y.vf.x direction-normal-products direction-normal-products direction-normal-products)
(.add.y.vf.x normal-length-squared normal-length-squared normal-length-squared)
(.add.z.vf.x plane-offset plane-offset plane-offset)
(.add.z.vf.x direction-normal-products direction-normal-products direction-normal-products)
(.add.z.vf.x normal-length-squared normal-length-squared normal-length-squared)
(.mov direction-dot direction-normal-products)
(.mov plane-offset-dot plane-offset)
(.isqrt.vf Q vf0 normal-length-squared :fsf #b11 :ftf #b0)
(let ((numerator plane-offset-dot)
(denominator direction-dot))
(cond
((!= denominator 0.0)
(let ((ray-fraction (/ numerator denominator)))
(.mov.vf.w normal vf0)
(.wait.vf)
(.mul.vf.xyz normal normal Q)
(let ((result ray-fraction))
(.mov plane-offset result)
(.svf (&-> normal-out quad) normal)
(.mul.x.vf acc direction plane-offset)
(.add.mul.w.vf.xyz direction origin vf0 acc)
(.svf (&-> intersection-out quad) direction)
(.mov unused-intersection-read direction)
result)))
(else -100000000.0)))))
(#unless PC_PORT
(defun ray-triangle-intersect ((ray-origin vector)
(ray-direction vector)
(radius float)
(triangle matrix)
(intersection-out vector)
(normal-out vector))
"Intersect a ray with the triangle formed by the first three rows of triangle. Write the plane
intersection and unit triangle normal to the output vectors. When radius rounds to a nonzero
integer, move the returned fraction earlier by radius divided by the ray length and clamp it to
zero; the inside test still uses the centerline-plane intersection. Return COLLISION_MISS for
a parallel ray, a contact behind the origin, or a point outside the triangle."
(declare (asm-func float))
(rlet ((vertex-a :reg vf1)
(vertex-b :reg vf2)
(vertex-c :reg vf3)
(edge-ba :reg vf4)
(edge-bc :reg vf5)
(normal :reg vf6)
(normal-length-squared :reg vf7)
(origin :reg vf8)
(direction :reg vf9)
(plane-offset-products :reg vf10)
(direction-normal-products :reg vf11)
(fraction-vf :reg vf12)
(intersection :reg vf13)
(edge-test-0 :reg vf14)
(edge-test-1 :reg vf15)
(edge-test-2 :reg vf16)
(direction-length-squared :reg vf17)
(radius-vf :reg vf18)
(edge-point-0 :reg vf19)
(edge-point-1 :reg vf20)
(edge-point-2 :reg vf21)
(Q :reg Q)
(denominator-bits)
(denominator-float)
(denominator-zero-test)
(numerator-bits)
(fraction-bits)
(edge-test-bits-0)
(edge-test-bits-1)
(edge-test-bits-2)
(edge-test-mask)
(radius-integer)
(radius-float)
(numerator-float)
(fraction-float)
(result))
(l.vf vertex-a triangle)
(l.vf vertex-b triangle 16)
(l.vf vertex-c triangle 32)
(l.vf origin ray-origin)
(l.vf direction ray-direction)
(sub.vf edge-ba vertex-b vertex-a)
(sub.vf edge-bc vertex-b vertex-c)
(sub.vf plane-offset-products vertex-b origin)
(mul.vf direction-length-squared direction direction)
(outer.product.a.vf acc edge-bc edge-ba)
(outer.product.b.vf normal edge-ba edge-bc acc)
(add.y.vf.x direction-length-squared direction-length-squared direction-length-squared)
(mul.vf normal-length-squared normal normal)
(mul.vf plane-offset-products plane-offset-products normal)
(mul.vf direction-normal-products direction normal)
(add.z.vf.x direction-length-squared direction-length-squared direction-length-squared)
(add.y.vf.x normal-length-squared normal-length-squared normal-length-squared)
(add.y.vf.x plane-offset-products plane-offset-products plane-offset-products)
(add.y.vf.x direction-normal-products direction-normal-products direction-normal-products)
(m radius-vf radius)
(add.z.vf.x normal-length-squared normal-length-squared normal-length-squared)
(add.z.vf.x plane-offset-products plane-offset-products plane-offset-products)
(add.z.vf.x direction-normal-products direction-normal-products direction-normal-products)
(rsqrt.w.x Q vf0 normal-length-squared)
(m denominator-bits direction-normal-products)
(m numerator-bits plane-offset-products)
(m denominator-float denominator-bits)
(m numerator-float numerator-bits)
(div.s fraction-float numerator-float denominator-float)
;; Shifting the raw denominator bits treats both +0.0 and -0.0 as zero.
(sll denominator-zero-test denominator-bits 1)
(move.w.vf normal vf0)
(b.z denominator-zero-test miss :delay (waitq))
(mulq.vf.xyz normal normal Q)
(m fraction-bits fraction-float)
(m fraction-vf fraction-bits)
(s.vf normal normal-out)
(mula.x.vf direction fraction-vf)
(madd.w.vf intersection origin vf0)
;; A plane hit is written before the finite triangle test, even when that point is later
;; rejected.
(b.lt fraction-bits r0 miss :delay (s.vf intersection intersection-out))
(sub.vf edge-point-0 vertex-b intersection)
(sub.vf edge-point-1 intersection vertex-c)
(sub.vf edge-point-2 intersection vertex-a)
(rsqrt.w.x Q vf0 direction-length-squared)
(outer.product.a.vf acc edge-bc edge-point-0)
(outer.product.b.vf edge-test-0 edge-point-0 edge-bc acc)
(outer.product.a.vf acc edge-point-0 edge-ba)
(outer.product.b.vf edge-test-1 edge-ba edge-point-0 acc)
(outer.product.a.vf acc edge-point-1 edge-point-2)
(outer.product.b.vf edge-test-2 edge-point-2 edge-point-1 acc)
(mul.vf.xyz edge-test-0 edge-test-0 normal)
(mul.vf.xyz edge-test-1 edge-test-1 normal)
(mul.vf.xyz edge-test-2 edge-test-2 normal)
(add.x.vf.y edge-test-0 edge-test-0 edge-test-0)
(add.x.vf.y edge-test-1 edge-test-1 edge-test-1)
(add.x.vf.y edge-test-2 edge-test-2 edge-test-2)
(add.z.vf.y edge-test-0 edge-test-0 edge-test-0)
(add.z.vf.y edge-test-1 edge-test-1 edge-test-1)
(add.z.vf.y edge-test-2 edge-test-2 edge-test-2)
(m edge-test-bits-0 edge-test-0)
(m edge-test-bits-1 edge-test-1)
(m edge-test-bits-2 edge-test-2)
(or edge-test-mask edge-test-bits-0 edge-test-bits-1)
(or edge-test-mask edge-test-mask edge-test-bits-2)
(b.lt edge-test-mask r0 miss :delay (nop!))
(m radius-float radius)
(cvt.w.s radius-float radius-float)
(m radius-integer radius-float)
(b.z radius-integer done :delay (nop!))
(waitq)
(mulq.vf.x radius-vf radius-vf Q)
(sub.x.vf.x fraction-vf fraction-vf radius-vf)
(max.x.vf.x fraction-vf fraction-vf vf0)
(b done :delay (m result fraction-vf))
(label miss)
(m! result COLLISION_MISS)
(label done)
result)))
(#unless PC_PORT
(defun collide-do-primitives ((sphere-start vector) (sphere-motion vector) (radius float) (triangle collide-cache-tri) (contact-out vector))
"Sweep a sphere against the three vertices and three edges of triangle and retain the earliest
contact within the finite motion. On a hit, write the contacted vertex or closest point on the
contacted edge to contact-out. Return COLLISION_MISS when none of the six boundary primitives
is hit; contact-out is undefined in that case."
(declare (asm-func float))
;; Faces are handled by moving-sphere-triangle-intersect. This function handles the rounded
;; boundary of the swept triangle: a radius sphere at each vertex and a radius cylinder along
;; each edge. The point saved for an edge hit is the closest point on the edge centerline.
(rlet ((best-fraction :reg f31)
(candidate-fraction :reg f30)
(zero :reg f28)
(best-point :reg vf31)
(edge-start :reg vf1)
(edge-vector :reg vf2)
(edge-length-squared :reg vf3)
(Q :reg Q)
(edge-direction)
(edge-length)
(edge-length-bits)
(candidate-bits)
(best-bits)
(miss-bits)
(one)
(result))
(m! edge-direction (new-stack-vector0))
(m! best-fraction 2.0)
(m! zero 0.0)
(m! candidate-bits (ray-sphere-intersect sphere-start sphere-motion (&-> triangle vertex 0) radius))
(m candidate-fraction candidate-bits)
(c.lt.s candidate-fraction zero)
(b.fpt vertex-1 :delay (nop!))
(m best-fraction candidate-bits)
(l.vf best-point triangle)
(label vertex-1)
(m! candidate-bits (ray-sphere-intersect sphere-start sphere-motion (&-> triangle vertex 1) radius))
(m candidate-fraction candidate-bits)
(c.lt.s candidate-fraction zero)
(b.fpt vertex-2 :delay (c.lt.s candidate-fraction best-fraction))
(b.fpf vertex-2 :delay (nop!))
(m best-fraction candidate-bits)
(l.vf best-point triangle 16)
(label vertex-2)
(m! candidate-bits (ray-sphere-intersect sphere-start sphere-motion (&-> triangle vertex 2) radius))
(m candidate-fraction candidate-bits)
(c.lt.s candidate-fraction zero)
(b.fpt edge-0 :delay (c.lt.s candidate-fraction best-fraction))
(b.fpf edge-0 :delay (nop!))
(m best-fraction candidate-bits)
(l.vf best-point triangle 32)
(label edge-0)
(l.vf edge-start triangle)
(l.vf edge-vector triangle 16)
(sub.vf edge-vector edge-vector edge-start)
(mul.vf edge-length-squared edge-vector edge-vector)
(add.y.vf.x edge-length-squared edge-length-squared edge-length-squared)
(add.z.vf.x edge-length-squared edge-length-squared edge-length-squared)
(rsqrt.w.x Q vf0 edge-length-squared)
(m edge-length-bits edge-length-squared)
(m edge-length edge-length-bits)
(sqrt.s edge-length edge-length)
(waitq)
(mulq.vf edge-vector edge-vector Q)
(s.vf edge-vector edge-direction)
(m edge-length-bits edge-length)
(m! candidate-bits
(ray-cylinder-intersect sphere-start
sphere-motion
(&-> triangle vertex 0)
edge-direction
radius
(the-as float edge-length-bits)
contact-out))
(m candidate-fraction candidate-bits)
(c.lt.s candidate-fraction zero)
(b.fpt edge-1 :delay (c.lt.s candidate-fraction best-fraction))
(b.fpf edge-1 :delay (nop!))
(m best-fraction candidate-bits)
(l.vf best-point contact-out)
(label edge-1)
(l.vf edge-start triangle 16)
(l.vf edge-vector triangle 32)
(sub.vf edge-vector edge-vector edge-start)
(mul.vf edge-length-squared edge-vector edge-vector)
(add.y.vf.x edge-length-squared edge-length-squared edge-length-squared)
(add.z.vf.x edge-length-squared edge-length-squared edge-length-squared)
(rsqrt.w.x Q vf0 edge-length-squared)
(m edge-length-bits edge-length-squared)
(m edge-length edge-length-bits)
(sqrt.s edge-length edge-length)
(waitq)
(mulq.vf edge-vector edge-vector Q)
(s.vf edge-vector edge-direction)
(m edge-length-bits edge-length)
(m! candidate-bits
(ray-cylinder-intersect sphere-start
sphere-motion
(&-> triangle vertex 1)
edge-direction
radius
(the-as float edge-length-bits)
contact-out))
(m candidate-fraction candidate-bits)
(c.lt.s candidate-fraction zero)
(b.fpt edge-2 :delay (c.lt.s candidate-fraction best-fraction))
(b.fpf edge-2 :delay (nop!))
(m best-fraction candidate-bits)
(l.vf best-point contact-out)
(label edge-2)
(l.vf edge-start triangle 32)
(l.vf edge-vector triangle)
(sub.vf edge-vector edge-vector edge-start)
(mul.vf edge-length-squared edge-vector edge-vector)
(add.y.vf.x edge-length-squared edge-length-squared edge-length-squared)
(add.z.vf.x edge-length-squared edge-length-squared edge-length-squared)
(rsqrt.w.x Q vf0 edge-length-squared)
(m edge-length-bits edge-length-squared)
(m edge-length edge-length-bits)
(sqrt.s edge-length edge-length)
(waitq)
(mulq.vf edge-vector edge-vector Q)
(s.vf edge-vector edge-direction)
(m edge-length-bits edge-length)
(m! candidate-bits
(ray-cylinder-intersect sphere-start
sphere-motion
(&-> triangle vertex 2)
edge-direction
radius
(the-as float edge-length-bits)
contact-out))
(m candidate-fraction candidate-bits)
(c.lt.s candidate-fraction zero)
(b.fpt finish :delay (c.lt.s candidate-fraction best-fraction))
(b.fpf finish :delay (nop!))
(m best-fraction candidate-bits)
(l.vf best-point contact-out)
(label finish)
(m! one 1.0)
(m! miss-bits COLLISION_MISS)
(c.lt.s one best-fraction)
(b.fptl return :delay (m best-fraction miss-bits))
(label return)
(m result best-fraction)
(s.vf best-point contact-out)
result)))
(#when PC_PORT
(def-mips2c collide-do-primitives (function vector vector float collide-cache-tri vector float)))
(#unless PC_PORT
(defun moving-sphere-triangle-intersect ((sphere-start vector)
(sphere-motion vector)
(radius float)
(triangle collide-cache-tri)
(contact-out vector)
(normal-out vector))
"Sweep a sphere from sphere-start along sphere-motion against triangle. First reject disjoint
swept bounds, then test the triangle face thickened by radius; when the projected face point
falls outside the triangle, test its three vertices and edges. Write the triangle contact point
and unit normal and return the earliest zero-to-one contact fraction, or COLLISION_MISS."
(declare (asm-func float))
;; The triangle is treated as a flat face with a rounded boundary. Its plane is expanded by
;; radius on both sides, then collide-do-primitives supplies the vertex spheres and edge
;; cylinders when the projected plane contact is outside the face.
(rlet ((radius-vf :reg vf1)
(triangle-min :reg vf2)
(triangle-max :reg vf3)
(sweep-min :reg vf4)
(sweep-max :reg vf5)
(sphere-end :reg vf6)
(normal-length-squared :reg vf7)
(center-at-contact :reg vf8)
(edge-point-0 :reg vf9)
(edge-point-1 :reg vf10)
(edge-a :reg vf11)
(vertex-b :reg vf12)
(edge-c :reg vf13)
(sphere-relative :reg vf14)
(motion :reg vf15)
(normal :reg vf16)
(Q :reg Q)
(bounds-a :class i128)
(bounds-b :class i128)
(bounds-mask :class i128)
(entry-bits)
(exit-bits)
(difference-bits)
(fraction-bits)
(edge-bits-0)
(edge-bits-1)
(edge-bits-2)
(edge-mask)
(result))
(l.vf sphere-relative sphere-start)
(l.vf motion sphere-motion)
(l.vf edge-a triangle)
(l.vf vertex-b triangle 16)
(l.vf edge-c triangle 32)
(m radius-vf radius)
(add.vf sphere-end sphere-relative motion)
(sub.vf edge-a edge-a vertex-b)
(sub.vf edge-c edge-c vertex-b)
(sub.vf sphere-relative sphere-relative vertex-b)
(sub.vf sphere-end sphere-end vertex-b)
;; Compare the triangle bounds with the swept segment bounds expanded by the radius.
(min.vf triangle-min edge-a vf0)
(outer.product.a.vf acc edge-c edge-a)
(outer.product.b.vf normal edge-a edge-c acc)
(max.vf triangle-max edge-a vf0)
(min.vf sweep-min sphere-relative sphere-end)
(max.vf sweep-max sphere-relative sphere-end)
(mul.vf normal-length-squared normal normal)
(min.vf triangle-min triangle-min edge-c)
(max.vf triangle-max triangle-max edge-c)
(sub.x.vf sweep-min sweep-min radius-vf)
(add.y.vf.x normal-length-squared normal-length-squared normal-length-squared)
(add.x.vf sweep-max sweep-max radius-vf)
(nop!)
(sub.vf triangle-max triangle-max sweep-min)
(add.z.vf.x normal-length-squared normal-length-squared normal-length-squared)
(sub.vf sweep-max sweep-max triangle-min)
(nop!)
(m bounds-a triangle-max)
(m bounds-b sweep-max)
(rsqrt.w.x Q vf0 normal-length-squared)
(or bounds-mask bounds-a bounds-b)
(pcgt.w bounds-mask r0 bounds-mask)
(ppach bounds-mask r0 bounds-mask)
(sll bounds-mask bounds-mask 16)
(b.nz bounds-mask miss :delay (nop!))
;; Project the motion and starting offset onto the unit normal. entry and exit are the
;; fractions at which the sphere center crosses the two radius-offset plane boundaries.
(mul.vf triangle-min normal motion)
(mul.vf triangle-max normal sphere-relative)
(add.y.vf.x triangle-min triangle-min triangle-min)
(sub.y.vf.y triangle-max vf0 triangle-max)
(add.z.vf.x triangle-min triangle-min triangle-min)
(sub.x.vf.y triangle-max triangle-max triangle-max)
(sub.z.vf.y triangle-max triangle-max triangle-max)
(add.x.vf.x triangle-max vf0 vf0)
(add.x.vf.x sweep-min vf0 vf0)
(waitq)
(mulq.vf normal normal Q)
(move.w.vf normal vf0)
(mulq.vf triangle-min triangle-min Q)
(mulq.vf sweep-min triangle-max Q)
(mulq.vf triangle-max triangle-max Q)
(s.vf normal normal-out)
(vnop)
(vnop)
(div.w.x Q vf0 triangle-min)
(add.x.vf.y triangle-max triangle-max radius-vf)
(sub.x.vf.y sweep-min sweep-min radius-vf)
(waitq)
(mulq.vf triangle-max triangle-max Q)
(mulq.vf sweep-min sweep-min Q)
(nop!)
(nop!)
(m entry-bits triangle-max)
(m exit-bits sweep-min)
(b.lt entry-bits r0 entry-negative :delay (nop!))
(b.lt exit-bits r0 start-contact :delay (nop!))
(sub difference-bits entry-bits exit-bits)
(b.lt difference-bits r0 use-entry :delay (nop!))
(sub.w.vf triangle-min sweep-min vf0)
(m difference-bits triangle-min)
(b.ge difference-bits r0 miss :delay (nop!))
(m fraction-bits sweep-min)
(mula.w.vf sphere-relative vf0)
(madd.y.vf center-at-contact motion sweep-min)
(b test-face :delay (nop!))
(label use-entry)
(sub.w.vf triangle-min triangle-max vf0)
(m difference-bits triangle-min)
(b.ge difference-bits r0 miss :delay (nop!))
(mula.w.vf sphere-relative vf0)
(madd.y.vf center-at-contact motion triangle-max)
(m fraction-bits triangle-max)
(label test-face)
(sra32 fraction-bits fraction-bits 0)
(sub.vf edge-point-0 center-at-contact edge-c)
(sub.vf edge-point-1 center-at-contact edge-a)
(outer.product.a.vf acc edge-c center-at-contact)
(outer.product.b.vf sweep-max center-at-contact edge-c acc)
(outer.product.a.vf acc center-at-contact edge-a)
(outer.product.b.vf sphere-end edge-a center-at-contact acc)
(outer.product.a.vf acc edge-point-0 edge-point-1)
(outer.product.b.vf normal-length-squared edge-point-1 edge-point-0 acc)
(mul.vf sweep-max sweep-max normal)
(mul.vf sphere-end sphere-end normal)
(mul.vf normal-length-squared normal-length-squared normal)
(add.x.vf.y sweep-max sweep-max sweep-max)
(add.x.vf.y sphere-end sphere-end sphere-end)
(add.x.vf.y normal-length-squared normal-length-squared normal-length-squared)
(add.z.vf.y sweep-max sweep-max sweep-max)
(add.z.vf.y sphere-end sphere-end sphere-end)
(add.z.vf.y normal-length-squared normal-length-squared normal-length-squared)
(m edge-bits-0 sweep-max)
(m edge-bits-1 sphere-end)
(m edge-bits-2 normal-length-squared)
(or edge-mask edge-bits-0 edge-bits-1)
(or edge-mask edge-mask edge-bits-2)
(b.lt edge-mask r0 boundary :delay (nop!))
;; Project the center onto the triangle plane with n x (n x center); the vertices above
;; are relative to vertex-b, so add vertex-b back for the world-space contact point.
(outer.product.a.vf acc center-at-contact normal)
(outer.product.b.vf sweep-max normal center-at-contact acc)
(outer.product.a.vf acc normal sweep-max)
(outer.product.b.vf sweep-max sweep-max normal acc)
(add.vf sweep-max sweep-max vertex-b)
(s.vf sweep-max contact-out)
(m result fraction-bits)
(b return :delay (nop!))
(label boundary)
(m! result (collide-do-primitives sphere-start sphere-motion radius triangle contact-out))
(b return :delay (nop!))
(label entry-negative)
(b.lt exit-bits r0 miss :delay (nop!))
(label start-contact)
;; The sphere starts within the plane slab. Test its initial projection before falling back
;; to the rounded boundary.
(sub.vf edge-point-0 sphere-relative edge-c)
(sub.vf edge-point-1 sphere-relative edge-a)
(outer.product.a.vf acc edge-c sphere-relative)
(outer.product.b.vf sweep-max sphere-relative edge-c acc)
(outer.product.a.vf acc sphere-relative edge-a)
(outer.product.b.vf sphere-end edge-a sphere-relative acc)
(outer.product.a.vf acc edge-point-0 edge-point-1)
(outer.product.b.vf normal-length-squared edge-point-1 edge-point-0 acc)
(mul.vf sweep-max sweep-max normal)
(mul.vf sphere-end sphere-end normal)
(mul.vf normal-length-squared normal-length-squared normal)
(add.x.vf.y sweep-max sweep-max sweep-max)
(add.x.vf.y sphere-end sphere-end sphere-end)
(add.x.vf.y normal-length-squared normal-length-squared normal-length-squared)
(add.z.vf.y sweep-max sweep-max sweep-max)
(add.z.vf.y sphere-end sphere-end sphere-end)
(add.z.vf.y normal-length-squared normal-length-squared normal-length-squared)
(m edge-bits-0 sweep-max)
(m edge-bits-1 sphere-end)
(m edge-bits-2 normal-length-squared)
(or edge-mask edge-bits-0 edge-bits-1)
(or edge-mask edge-mask edge-bits-2)
(b.lt edge-mask r0 boundary :delay (nop!))
(outer.product.a.vf acc sphere-relative normal)
(outer.product.b.vf sweep-max normal sphere-relative acc)
(outer.product.a.vf acc normal sweep-max)
(outer.product.b.vf sweep-max sweep-max normal acc)
(add.vf sweep-max sweep-max vertex-b)
(s.vf sweep-max contact-out)
(b return :delay (m! result 0))
(label miss)
(m! result COLLISION_MISS)
(label return)
result)))
(#when PC_PORT
(def-mips2c moving-sphere-triangle-intersect (function vector vector float collide-cache-tri vector vector float)))
(defun moving-sphere-sphere-intersect ((sphere-start vector) (sphere-motion vector) (static-sphere vector) (contact-out vector))
"Sweep a moving sphere along motion against a static sphere. Sphere vectors store center in xyz
and radius in w. Write the point on the moving sphere facing the static center at first contact
and return its fraction along motion, or COLLISION_MISS."
;; Expanding the static sphere by the moving radius reduces the sweep to a ray-sphere test.
(let ((contact-fraction (ray-sphere-intersect sphere-start sphere-motion static-sphere (+ (-> sphere-start w) (-> static-sphere w)))))
(when (>= contact-fraction 0.0)
(let ((contact-offset (vector-normalize! (vector-! (new-stack-vector0) static-sphere sphere-start) (-> sphere-start w))))
(vector+*! contact-out sphere-start sphere-motion contact-fraction)
(vector+! contact-out contact-out contact-offset)))
contact-fraction))
(defun moving-sphere-moving-sphere-intersect ((first-sphere vector) (first-motion vector) (second-sphere vector) (second-motion vector) (contact-out vector))
"Sweep two moving spheres over the same zero-to-one interval using their relative motion. Sphere
vectors store center in xyz and radius in w. Write the point on the first sphere facing the
second at first contact and return the fraction, or COLLISION_MISS."
(let ((contact-fraction (ray-sphere-intersect first-sphere
(vector-! (new-stack-vector0) first-motion second-motion)
second-sphere
(+ (-> first-sphere w) (-> second-sphere w)))))
(cond
((and (>= contact-fraction 0.0) (>= 1.0 contact-fraction))
(let ((contact-offset (vector-normalize! (vector-! (new-stack-vector0) second-sphere first-sphere) (-> first-sphere w))))
;; Use the first sphere's actual motion for the world-space contact, not the relative
;; motion used by the intersection test.
(vector+*! contact-out first-sphere first-motion contact-fraction)
(vector+! contact-out contact-out contact-offset)))
(else (set! contact-fraction COLLISION_MISS)))
contact-fraction))