package tiny_libs
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
From-scratch libraries for teaching: graphics, audio, compression, crypto, networking and more
Install
dune-project
Dependency
Authors
Maintainers
Sources
0.3.6.tar.gz
md5=7c636383d146d30ac6f2fa234a6253c8
sha512=c79f3823c5f8f57e5038eb640d487c61168b84aa07c61999d6622ef9fd0c890e2b03b4c6a7cdbbe9352a49e25dda00ac7bb14693cee8e3d7beeed251351a2af0
doc/src/tiny_libs.physics_3d/Collide3d.ml.html
Source file Collide3d.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410(* Claude Code * * Copyright (C) 2026 Yoann Padioleau * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public License * (LGPL) as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. *) (* See Collide3d.mli *) open Hitbox3d let eps = 1e-9 (*****************************************************************************) (* Closest points *) (*****************************************************************************) let clamp lo hi v = Float.max lo (Float.min hi v) let closest_on_segment ((a, b) : Vec3.t * Vec3.t) (q : Vec3.t) : Vec3.t = let ab = Vec3.sub b a in let len2 = Vec3.dot ab ab in if len2 < eps then a else Vec3.add a (Vec3.scale (clamp 0. 1. (Vec3.dot (Vec3.sub q a) ab /. len2)) ab) (* Ericson 5.1.9: the two closest points of two segments. The pair of * parameters (s, t) that minimizes |(p1 + s d1) - (p2 + t d2)|, with * the parallel case (the denominator vanishing) taken separately. *) let closest_between_segments ((p1, q1) : Vec3.t * Vec3.t) ((p2, q2) : Vec3.t * Vec3.t) : Vec3.t * Vec3.t = let d1 = Vec3.sub q1 p1 and d2 = Vec3.sub q2 p2 in let r = Vec3.sub p1 p2 in let a = Vec3.dot d1 d1 and e = Vec3.dot d2 d2 and f = Vec3.dot d2 r in if a < eps && e < eps then (p1, p2) else if a < eps then (p1, Vec3.add p2 (Vec3.scale (clamp 0. 1. (f /. e)) d2)) else let c = Vec3.dot d1 r in if e < eps then (Vec3.add p1 (Vec3.scale (clamp 0. 1. (-.c /. a)) d1), p2) else let b = Vec3.dot d1 d2 in let denom = (a *. e) -. (b *. b) in let s = if denom > eps then clamp 0. 1. (((b *. f) -. (c *. e)) /. denom) else 0. in let t = ((b *. s) +. f) /. e in (* t outside [0, 1]: clamp it and redo s for that t *) let s, t = if t < 0. then (clamp 0. 1. (-.c /. a), 0.) else if t > 1. then (clamp 0. 1. ((b -. c) /. a), 1.) else (s, t) in (Vec3.add p1 (Vec3.scale s d1), Vec3.add p2 (Vec3.scale t d2)) (* into the box's own frame, clamped to it, and back *) let closest_on_box (b : placed) (q : Vec3.t) : Vec3.t = match b.shape with | Box (hx, hy, hz) -> let lx, ly, lz = Quat.rotate (Quat.conjugate b.orientation) (Vec3.sub q b.pos) in Vec3.add b.pos (Quat.rotate b.orientation (clamp (-.hx) hx lx, clamp (-.hy) hy ly, clamp (-.hz) hz lz)) | _ -> b.pos (* Ericson 5.1.5: the closest point of a triangle, by regions *) let closest_on_triangle ((a, b, c) : Vec3.t * Vec3.t * Vec3.t) (p : Vec3.t) : Vec3.t = let ab = Vec3.sub b a and ac = Vec3.sub c a and ap = Vec3.sub p a in let d1 = Vec3.dot ab ap and d2 = Vec3.dot ac ap in if d1 <= 0. && d2 <= 0. then a else let bp = Vec3.sub p b in let d3 = Vec3.dot ab bp and d4 = Vec3.dot ac bp in if d3 >= 0. && d4 <= d3 then b else let vc = (d1 *. d4) -. (d3 *. d2) in if vc <= 0. && d1 >= 0. && d3 <= 0. then Vec3.add a (Vec3.scale (d1 /. (d1 -. d3)) ab) else let cp = Vec3.sub p c in let d5 = Vec3.dot ab cp and d6 = Vec3.dot ac cp in if d6 >= 0. && d5 <= d6 then c else let vb = (d5 *. d2) -. (d1 *. d6) in if vb <= 0. && d2 >= 0. && d6 <= 0. then Vec3.add a (Vec3.scale (d2 /. (d2 -. d6)) ac) else let va = (d3 *. d6) -. (d5 *. d4) in if va <= 0. && d4 -. d3 >= 0. && d5 -. d6 >= 0. then Vec3.add b (Vec3.scale ((d4 -. d3) /. (d4 -. d3 +. (d5 -. d6))) (Vec3.sub c b)) else let denom = 1. /. (va +. vb +. vc) in Vec3.add a (Vec3.add (Vec3.scale (vb *. denom) ab) (Vec3.scale (vc *. denom) ac)) (*****************************************************************************) (* Pairs *) (*****************************************************************************) let bounds_overlap ((alo, ahi) : Vec3.t * Vec3.t) ((blo, bhi) : Vec3.t * Vec3.t) : bool = let (ax, ay, az) = alo and (bx, by, bz) = ahi and (cx, cy, cz) = blo and (dx, dy, dz) = bhi in ax <= dx && cx <= bx && ay <= dy && cy <= by && az <= dz && cz <= bz let spheres ((ca, ra) : Vec3.t * float) ((cb, rb) : Vec3.t * float) : Contact3d.t option = let d = Vec3.sub cb ca in let len = Vec3.length d in if len >= ra +. rb then None else let normal = if len < eps then (1., 0., 0.) else Vec3.scale (1. /. len) d in let depth = ra +. rb -. len in Some (Contact3d.make ~normal ~depth ~point:(Vec3.add ca (Vec3.scale (ra -. (depth /. 2.)) normal))) (* the sphere first, the box second: the normal points from one to the * other, and a centre *inside* the box is its own case -- the way out * is then the nearest face, which no "closest point" can tell you *) let sphere_box ((c, r) : Vec3.t * float) (b : placed) : Contact3d.t option = match b.shape with | Box (hx, hy, hz) -> let cl = closest_on_box b c in let d = Vec3.sub cl c in let len = Vec3.length d in if len > eps then if len >= r then None else Some (Contact3d.make ~normal:(Vec3.scale (1. /. len) d) ~depth:(r -. len) ~point:cl) else let lx, ly, lz = Quat.rotate (Quat.conjugate b.orientation) (Vec3.sub c b.pos) in let faces = [ (hx -. Float.abs lx, (1., 0., 0.), lx); (hy -. Float.abs ly, (0., 1., 0.), ly); (hz -. Float.abs lz, (0., 0., 1.), lz) ] in let out, axis, coord = List.fold_left (fun (bo, ba, bc) (o, a, cc) -> if o < bo then (o, a, cc) else (bo, ba, bc)) (List.hd faces) (List.tl faces) in let dir = Quat.rotate b.orientation (Vec3.scale (if coord >= 0. then -1. else 1.) axis) in Some (Contact3d.make ~normal:dir ~depth:(r +. out) ~point:c) | _ -> None let sphere_capsule ((c, r) : Vec3.t * float) (cap : placed) : Contact3d.t option = match cap.shape with | Capsule (_, cr) -> spheres (c, r) (closest_on_segment (segment cap) c, cr) | _ -> None let capsules (a : placed) (b : placed) : Contact3d.t option = match (a.shape, b.shape) with | Capsule (_, ra), Capsule (_, rb) -> let pa, pb = closest_between_segments (segment a) (segment b) in spheres (pa, ra) (pb, rb) | _ -> None let sphere_triangle ((c, r) : Vec3.t * float) (tri : Vec3.t * Vec3.t * Vec3.t) : Contact3d.t option = spheres (c, r) (closest_on_triangle tri c, 0.) (* The separating axis theorem, in three dimensions: two convex shapes * miss each other if and only if some axis sees their shadows apart. * For two boxes the axes to try are the 3 + 3 face normals *and* the 9 * crosses of an edge of each -- see the .mli for what dropping those * nine does. *) let boxes ?(edge_axes = true) (a : placed) (b : placed) : Contact3d.t option = let fa = face_axes a and fb = face_axes b in let crosses = if edge_axes then List.concat_map (fun u -> List.map (fun v -> Vec3.cross u v) fb) fa else [] in let candidates = fa @ fb @ crosses in let best = ref None and separated = ref false in List.iter (fun axis -> if (not !separated) && Vec3.length axis > 1e-6 then begin let axis = Vec3.normalize axis in let alo, ahi = extent a axis and blo, bhi = extent b axis in let overlap = Float.min ahi bhi -. Float.max alo blo in if overlap <= 0. then separated := true else match !best with Some (o, _) when o <= overlap -> () | _ -> best := Some (overlap, axis) end) candidates; if !separated then None else match !best with | None -> None | Some (depth, axis) -> (* the normal points from a towards b *) let normal = if Vec3.dot (Vec3.sub b.pos a.pos) axis < 0. then Vec3.scale (-1.) axis else axis in (* the point is b's deepest, pulled back half the overlap -- * *not* the midpoint of the two supports, which for a small * box on a huge floor lands at a corner of the floor, metres * from where they touch, and gives the impulse a lever arm so * long that it vanishes. For anything resting, use [manifold]: * one point of a face is never the whole story. *) let deepest = support b (Vec3.scale (-1.) normal) in Some (Contact3d.make ~normal ~depth ~point:(Vec3.add deepest (Vec3.scale (depth /. 2.) normal))) (* A capsule is a segment with a radius, so the contact is the closest * point of that segment to the box -- found by going back and forth * between the two shapes, which converges for convex ones. See the * .mli for where this is an approximation and what pays for it. *) let box_capsule (b : placed) (cap : placed) : Contact3d.t option = match cap.shape with | Capsule (_, r) -> let seg = segment cap in let rec settle n p = if n = 0 then p else settle (n - 1) (closest_on_segment seg (closest_on_box b p)) in let p = settle 4 cap.pos in (match sphere_box (p, r) b with Some c -> Some (Contact3d.flip c) | None -> None) | _ -> None (* the plane first: its normal is the contact's, since that is the way * out of a half-space *) let plane_hitbox ((n, d) : Vec3.t * float) (p : placed) : Contact3d.t option = let n = Vec3.normalize n in let deepest = support p (Vec3.scale (-1.) n) in let along = Vec3.dot deepest n -. d in if along >= 0. then None else Some (Contact3d.make ~normal:n ~depth:(-.along) ~point:(Vec3.add deepest (Vec3.scale (-.along /. 2.) n))) (* every pair, each dispatched to the test above that knows it, with * the contact turned round when the pair arrives the other way up *) let contact (a : placed) (b : placed) : Contact3d.t option = let flipped f = match f with Some c -> Some (Contact3d.flip c) | None -> None in match (a.shape, b.shape) with | Sphere ra, Sphere rb -> spheres (a.pos, ra) (b.pos, rb) | Sphere r, Box _ -> sphere_box (a.pos, r) b | Box _, Sphere r -> flipped (sphere_box (b.pos, r) a) | Sphere r, Capsule _ -> sphere_capsule (a.pos, r) b | Capsule _, Sphere r -> flipped (sphere_capsule (b.pos, r) a) | Box _, Box _ -> boxes a b | Capsule _, Capsule _ -> capsules a b | Box _, Capsule _ -> box_capsule a b | Capsule _, Box _ -> flipped (box_capsule b a) | Plane (n, d), _ -> plane_hitbox (n, d) b | _, Plane (n, d) -> flipped (plane_hitbox (n, d) a) let touching (a : placed) (b : placed) : bool = contact a b <> None (*****************************************************************************) (* Manifolds: a whole face of contact, not one point of it *) (*****************************************************************************) (* a box's six faces, each an outward normal and its four corners in * order around it *) let box_faces (p : placed) : (Vec3.t * Vec3.t list) list = match (p.shape, face_axes p) with | Box (hx, hy, hz), [ ax; ay; az ] -> let pairs = [ ((ax, hx), (ay, hy), (az, hz)); ((ay, hy), (az, hz), (ax, hx)); ((az, hz), (ax, hx), (ay, hy)) ] in List.concat_map (fun ((n, h), (u, hu), (v, hv)) -> List.map (fun s -> let normal = Vec3.scale s n in let centre = Vec3.add p.pos (Vec3.scale (s *. h) n) in let corner su sv = Vec3.add centre (Vec3.add (Vec3.scale (su *. hu) u) (Vec3.scale (sv *. hv) v)) in (normal, [ corner 1. 1.; corner 1. (-1.); corner (-1.) (-1.); corner (-1.) 1. ])) [ 1.; -1. ]) pairs | _ -> [] (* Sutherland-Hodgman against one plane, keeping the side where * n . p <= d -- the same clipping TinyDescent.ml does * through its portals, here in 3D and against a box's sides *) let clip_by_plane (poly : Vec3.t list) (n : Vec3.t) (d : float) : Vec3.t list = let count = List.length poly in if count = 0 then [] else List.concat (List.mapi (fun i p -> let q = List.nth poly ((i + 1) mod count) in let dp = Vec3.dot n p -. d and dq = Vec3.dot n q -. d in let kept = if dp <= 0. then [ p ] else [] in let crossing = if dp > 0. = (dq > 0.) then [] else [ Vec3.add p (Vec3.scale (dp /. (dp -. dq)) (Vec3.sub q p)) ] in kept @ crossing) poly) (* at most [keep] of them: the deepest first, then whichever is * farthest from the ones already taken, so that four points still * span the face rather than huddling in one corner *) let spread_out (keep : int) (points : Contact3d.t list) : Contact3d.t list = let rec go taken left = if List.length taken >= keep || left = [] then List.rev taken else let far = List.fold_left (fun best (c : Contact3d.t) -> let d = List.fold_left (fun m (t : Contact3d.t) -> Float.min m (Vec3.length (Vec3.sub c.Contact3d.point t.Contact3d.point))) infinity taken in match best with Some (bd, _) when bd >= d -> best | _ -> Some (d, c)) None left in match far with | None -> List.rev taken | Some (_, c) -> go (c :: taken) (List.filter (fun (x : Contact3d.t) -> x != c) left) in match List.sort (fun (x : Contact3d.t) (y : Contact3d.t) -> compare y.Contact3d.depth x.Contact3d.depth) points with | [] -> [] | deepest :: rest -> go [ deepest ] rest (* two boxes: the face of one clipped against the sides of the other's, * which is what turns "they overlap, here" into "they overlap along * this whole face" -- the difference between a box that bounces and a * box that can be stacked on *) let box_manifold (a : placed) (b : placed) (c : Contact3d.t) : Contact3d.t list = let n = c.Contact3d.normal in let most_aligned faces dir = List.fold_left (fun best (fn, pts) -> let d = Vec3.dot fn dir in match best with Some (bd, _, _) when bd >= d -> best | _ -> Some (d, fn, pts)) None faces in match (most_aligned (box_faces a) n, most_aligned (box_faces b) (Vec3.scale (-1.) n)) with | Some (da, na, pa), Some (db, nb, pb) -> (* The reference face is the better aligned with the contact * normal, and the *tolerance* matters: with two crates squarely * stacked the two faces tie, and floating point breaks the tie * differently from one step to the next -- which hands the * solver a different set of four points each time, and the pile * twitches. Sticking with the first box unless the second is * clearly better keeps the manifold the same manifold (Catto * does the same, for the same reason). *) let flip = db > da +. 0.01 in let rn, ref_pts, inc_pts = if flip then (nb, pb, pa) else (na, pa, pb) in let count = List.length ref_pts in let middle = Vec3.scale (1. /. float_of_int count) (List.fold_left Vec3.add (0., 0., 0.) ref_pts) in let clipped = List.fold_left (fun poly i -> let p = List.nth ref_pts i and q = List.nth ref_pts ((i + 1) mod count) in let side = Vec3.cross (Vec3.sub q p) rn in if Vec3.length side < eps then poly else (* the plane through that edge, its normal pointing *away* * from the face -- taken from the face's middle rather * than from the corners' winding, which a box's six * faces do not all share *) let side = Vec3.normalize side in let d = Vec3.dot side p in let side, d = if Vec3.dot side middle > d then (Vec3.scale (-1.) side, -.d) else (side, d) in clip_by_plane poly side d) inc_pts (List.init count Fun.id) in let plane = Vec3.dot rn (List.hd ref_pts) in let normal = if flip then Vec3.scale (-1.) rn else rn in let points = List.filter_map (fun p -> let depth = plane -. Vec3.dot rn p in if depth >= 0. then Some (Contact3d.make ~normal ~depth ~point:p) else None) clipped in if points = [] then [ c ] else spread_out 4 points | _ -> [ c ] let manifold (a : placed) (b : placed) : Contact3d.t list = match contact a b with | None -> [] | Some c -> ( match (a.shape, b.shape) with Box _, Box _ -> box_manifold a b c | _ -> [ c ]) (*****************************************************************************) (* Rays *) (*****************************************************************************) (* claude: the arithmetic is graphics/3d/geometry's Ray, shared with the * ray tracer, which answers the whole line; physics asks for the first * hit in front, 0 when the ray starts inside. A zero direction is no * ray: it hits nothing. *) let first_hit_in_front ((t_in, t_out) : float * float) : float option = if t_out < 0. then None else Some (Float.max 0. t_in) let with_ray ~(from : Vec3.t) ~(direction : Vec3.t) (f : Ray.t -> float option) : float option = if Vec3.length direction < eps then None else f (Ray.make from direction) let ray_sphere ~(from : Vec3.t) ~(direction : Vec3.t) ((c, r) : Vec3.t * float) : float option = with_ray ~from ~direction (fun ray -> Option.bind (Ray.sphere ray (c, r)) first_hit_in_front) let ray_plane ~(from : Vec3.t) ~(direction : Vec3.t) ((n, d) : Vec3.t * float) : float option = with_ray ~from ~direction (fun ray -> match Ray.plane ray (n, d) with Some t when t >= 0. -> Some t | _ -> None) (* the slab test, in the box's own frame *) let ray_box ~(from : Vec3.t) ~(direction : Vec3.t) (b : placed) : float option = match b.shape with | Box (hx, hy, hz) -> let inv = Quat.conjugate b.orientation in with_ray ~from:(Quat.rotate inv (Vec3.sub from b.pos)) ~direction:(Quat.rotate inv direction) (fun ray -> Option.bind (Ray.box ray ((-.hx, -.hy, -.hz), (hx, hy, hz))) first_hit_in_front) | _ -> None (* a swept sphere: the side of the cylinder, then the two caps *) let ray_capsule ~(from : Vec3.t) ~(direction : Vec3.t) (cap : placed) : float option = match cap.shape with | Capsule (_, r) -> let a, b = segment cap in let u = Vec3.normalize direction in let d = Vec3.sub b a in let m = Vec3.sub from a in let dd = Vec3.dot d d and nd = Vec3.dot u d and md = Vec3.dot m d in let side = (* the quadratic of a ray against an infinite cylinder, kept * only where it lands between the two ends *) let a_ = dd -. (nd *. nd) in let b_ = dd *. Vec3.dot m u -. (nd *. md) in let c_ = (dd *. (Vec3.dot m m -. (r *. r))) -. (md *. md) in if Float.abs a_ < eps then None else let disc = (b_ *. b_) -. (a_ *. c_) in if disc < 0. then None else let t = (-.b_ -. sqrt disc) /. a_ in let t = Float.max 0. t in let along = md +. (t *. nd) in if along < 0. || along > dd then None else Some t in let caps = List.filter_map (fun c -> ray_sphere ~from ~direction (c, r)) [ a; b ] in let all = (match side with Some t -> [ t ] | None -> []) @ caps in (match all with [] -> None | t :: ts -> Some (List.fold_left Float.min t ts)) | _ -> None (* Moller and Trumbore (1997), see Ray.triangle *) let ray_triangle ~(from : Vec3.t) ~(direction : Vec3.t) (tri : Vec3.t * Vec3.t * Vec3.t) : float option = with_ray ~from ~direction (fun ray -> match Ray.triangle ray tri with Some (t, _, _) when t >= 0. -> Some t | _ -> None) let ray ~(from : Vec3.t) ~(direction : Vec3.t) (p : placed) : float option = match p.shape with | Sphere r -> ray_sphere ~from ~direction (p.pos, r) | Box _ -> ray_box ~from ~direction p | Capsule _ -> ray_capsule ~from ~direction p | Plane (n, d) -> ray_plane ~from ~direction (n, d)
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>