@woosh/meep-engine 2.141.0 → 2.143.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/CapsuleShape3D.d.ts +1 -1
  3. package/src/core/geom/3d/shape/CapsuleShape3D.js +1 -1
  4. package/src/core/geom/3d/shape/SphereShape3D.d.ts +47 -0
  5. package/src/core/geom/3d/shape/SphereShape3D.d.ts.map +1 -0
  6. package/src/core/geom/3d/shape/SphereShape3D.js +127 -0
  7. package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts +30 -18
  8. package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts.map +1 -1
  9. package/src/core/geom/3d/shape/UnitSphereShape3D.js +44 -92
  10. package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
  11. package/src/core/geom/3d/shape/json/shape_to_type.js +4 -2
  12. package/src/core/geom/3d/shape/json/type_adapters.d.ts +12 -3
  13. package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
  14. package/src/core/geom/3d/shape/json/type_adapters.js +16 -4
  15. package/src/core/geom/3d/shape/util/shape_to_visual_entity.js +2 -2
  16. package/src/engine/control/first-person/DESIGN_COLLISION.md +255 -0
  17. package/src/engine/control/first-person/prototype_first_person_controller.js +5 -0
  18. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
  19. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +70 -43
  20. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts +12 -22
  21. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts.map +1 -1
  22. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +345 -186
  23. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts +44 -0
  24. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts.map +1 -0
  25. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.js +151 -0
  26. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts +14 -0
  27. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts.map +1 -0
  28. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.js +78 -0
  29. package/src/engine/physics/PLAN.md +705 -578
  30. package/src/engine/physics/REVIEW_003.md +166 -0
  31. package/src/engine/physics/constraint/solve_constraints.d.ts +24 -2
  32. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  33. package/src/engine/physics/constraint/solve_constraints.js +402 -165
  34. package/src/engine/physics/ecs/Joint.d.ts +115 -0
  35. package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
  36. package/src/engine/physics/ecs/Joint.js +168 -0
  37. package/src/engine/physics/ecs/JointSerializationAdapter.d.ts +29 -0
  38. package/src/engine/physics/ecs/JointSerializationAdapter.d.ts.map +1 -0
  39. package/src/engine/physics/ecs/JointSerializationAdapter.js +72 -0
  40. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  41. package/src/engine/physics/narrowphase/narrowphase_step.js +20 -13
  42. package/src/engine/physics/narrowphase/ray_shapes.d.ts +66 -0
  43. package/src/engine/physics/narrowphase/ray_shapes.d.ts.map +1 -0
  44. package/src/engine/physics/narrowphase/ray_shapes.js +187 -0
  45. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +16 -0
  46. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -0
  47. package/src/engine/physics/narrowphase/refine_ray_concave.js +145 -0
  48. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +39 -0
  49. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -0
  50. package/src/engine/physics/narrowphase/refine_ray_hit.js +78 -0
  51. package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts +8 -7
  52. package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts.map +1 -1
  53. package/src/engine/physics/narrowphase/sphere_sphere_contact.js +8 -7
  54. package/src/engine/physics/queries/raycast.d.ts +11 -9
  55. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  56. package/src/engine/physics/queries/raycast.js +108 -159
  57. package/src/engine/physics/vehicle/RaycastVehicle.d.ts +114 -0
  58. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -0
  59. package/src/engine/physics/vehicle/RaycastVehicle.js +333 -0
@@ -0,0 +1,187 @@
1
+ /**
2
+ * # Local-frame ray ↔ primitive intersections (raycast narrowphase)
3
+ *
4
+ * Each function intersects a ray, **expressed in the shape's local frame**,
5
+ * against a canonical primitive at the origin (sphere at the origin; box
6
+ * axis-aligned spanning `[-h, +h]`; capsule along the local Y axis). They
7
+ * return the hit distance `t` along the ray (`Infinity` on a miss) and write
8
+ * the **local** outward surface normal (unit) into `outNormal[0..2]`.
9
+ *
10
+ * The ray-narrowphase dispatch transforms the world ray into the body's local
11
+ * frame once (rotate by the inverse body rotation — a unit direction stays
12
+ * unit, so `t` is preserved), calls the matching primitive, then rotates the
13
+ * returned local normal back to world. Keeping the primitives canonical lets
14
+ * the box and capsule tests be axis-aligned (cheap slab / cylinder math) and
15
+ * shares a single transform across them.
16
+ *
17
+ * Conventions:
18
+ * - the ray direction is unit length (the caller guarantees it);
19
+ * - the first surface crossing at or after the origin within `tMax` is
20
+ * returned; a ray starting inside the shape returns its exit crossing;
21
+ * - the normal is the geometric outward surface normal at the hit.
22
+ *
23
+ * @author Alex Goldring
24
+ * @copyright Company Named Limited (c) 2026
25
+ */
26
+
27
+ /**
28
+ * Ray vs a sphere of radius `r` centred at the local origin.
29
+ *
30
+ * @param {Float64Array} outNormal length-3, written on hit
31
+ * @param {number} ox @param {number} oy @param {number} oz ray origin (local)
32
+ * @param {number} dx @param {number} dy @param {number} dz ray dir (local, unit)
33
+ * @param {number} tMax
34
+ * @param {number} r sphere radius
35
+ * @returns {number} hit distance, or `Infinity` on miss
36
+ */
37
+ export function ray_sphere_local(outNormal, ox, oy, oz, dx, dy, dz, tMax, r) {
38
+ // |o + t·d|² = r², d unit → t² + 2(o·d)t + (|o|² − r²) = 0.
39
+ const b = ox * dx + oy * dy + oz * dz;
40
+ const c = ox * ox + oy * oy + oz * oz - r * r;
41
+ const disc = b * b - c;
42
+ if (disc < 0) return Infinity;
43
+ const sq = Math.sqrt(disc);
44
+ let t = -b - sq; // near root (entry)
45
+ if (t < 0) t = -b + sq; // origin inside the sphere → far root (exit)
46
+ if (t < 0 || t > tMax) return Infinity;
47
+ const inv = 1 / r;
48
+ outNormal[0] = (ox + dx * t) * inv;
49
+ outNormal[1] = (oy + dy * t) * inv;
50
+ outNormal[2] = (oz + dz * t) * inv;
51
+ return t;
52
+ }
53
+
54
+ /**
55
+ * Ray vs an axis-aligned box spanning `[-hx,hx] × [-hy,hy] × [-hz,hz]` at the
56
+ * local origin (the canonical pose of {@link BoxShape3D}). Slab method, with
57
+ * the entry (or, origin-inside, exit) face's outward normal.
58
+ *
59
+ * @param {Float64Array} outNormal length-3, written on hit
60
+ * @param {number} ox @param {number} oy @param {number} oz ray origin (local)
61
+ * @param {number} dx @param {number} dy @param {number} dz ray dir (local, unit)
62
+ * @param {number} tMax
63
+ * @param {number} hx @param {number} hy @param {number} hz half-extents
64
+ * @returns {number} hit distance, or `Infinity` on miss
65
+ */
66
+ export function ray_box_local(outNormal, ox, oy, oz, dx, dy, dz, tMax, hx, hy, hz) {
67
+ let tmin = -Infinity, tmax = Infinity;
68
+ let enterAxis = 0, enterSign = 0, exitAxis = 0, exitSign = 0;
69
+
70
+ // X slab.
71
+ if (dx !== 0) {
72
+ const inv = 1 / dx;
73
+ let tn = (-hx - ox) * inv, tf = (hx - ox) * inv, nsign = -1;
74
+ if (tn > tf) { const tmp = tn; tn = tf; tf = tmp; nsign = 1; }
75
+ if (tn > tmin) { tmin = tn; enterAxis = 0; enterSign = nsign; }
76
+ if (tf < tmax) { tmax = tf; exitAxis = 0; exitSign = -nsign; }
77
+ } else if (ox < -hx || ox > hx) {
78
+ return Infinity;
79
+ }
80
+ // Y slab.
81
+ if (dy !== 0) {
82
+ const inv = 1 / dy;
83
+ let tn = (-hy - oy) * inv, tf = (hy - oy) * inv, nsign = -1;
84
+ if (tn > tf) { const tmp = tn; tn = tf; tf = tmp; nsign = 1; }
85
+ if (tn > tmin) { tmin = tn; enterAxis = 1; enterSign = nsign; }
86
+ if (tf < tmax) { tmax = tf; exitAxis = 1; exitSign = -nsign; }
87
+ } else if (oy < -hy || oy > hy) {
88
+ return Infinity;
89
+ }
90
+ // Z slab.
91
+ if (dz !== 0) {
92
+ const inv = 1 / dz;
93
+ let tn = (-hz - oz) * inv, tf = (hz - oz) * inv, nsign = -1;
94
+ if (tn > tf) { const tmp = tn; tn = tf; tf = tmp; nsign = 1; }
95
+ if (tn > tmin) { tmin = tn; enterAxis = 2; enterSign = nsign; }
96
+ if (tf < tmax) { tmax = tf; exitAxis = 2; exitSign = -nsign; }
97
+ } else if (oz < -hz || oz > hz) {
98
+ return Infinity;
99
+ }
100
+
101
+ if (tmax < tmin || tmax < 0) return Infinity;
102
+
103
+ let t, axis, sign;
104
+ if (tmin >= 0) { t = tmin; axis = enterAxis; sign = enterSign; }
105
+ else { t = tmax; axis = exitAxis; sign = exitSign; } // origin inside the box
106
+
107
+ if (t > tMax) return Infinity;
108
+ outNormal[0] = 0; outNormal[1] = 0; outNormal[2] = 0;
109
+ outNormal[axis] = sign;
110
+ return t;
111
+ }
112
+
113
+ /**
114
+ * Ray vs a capsule along the local Y axis: a cylinder of radius `r` over
115
+ * `y ∈ [−hh, hh]` capped by hemispheres of radius `r` at `(0, ±hh, 0)` — the
116
+ * canonical pose of {@link CapsuleShape3D} (`hh = height/2`). Tests the
117
+ * infinite cylinder (clamped to the segment) and the two cap spheres, taking
118
+ * the nearest valid crossing.
119
+ *
120
+ * @param {Float64Array} outNormal length-3, written on hit
121
+ * @param {number} ox @param {number} oy @param {number} oz ray origin (local)
122
+ * @param {number} dx @param {number} dy @param {number} dz ray dir (local, unit)
123
+ * @param {number} tMax
124
+ * @param {number} r capsule radius
125
+ * @param {number} hh half-height of the cylindrical section (`height/2`)
126
+ * @returns {number} hit distance, or `Infinity` on miss
127
+ */
128
+ export function ray_capsule_local(outNormal, ox, oy, oz, dx, dy, dz, tMax, r, hh) {
129
+ let best = Infinity;
130
+ let nx = 0, ny = 0, nz = 0;
131
+ const r2 = r * r;
132
+ const inv_r = 1 / r;
133
+
134
+ // --- Cylinder side (infinite cylinder about Y, projected to XZ). ---
135
+ const a = dx * dx + dz * dz;
136
+ if (a > 1e-12) {
137
+ const b = ox * dx + oz * dz;
138
+ const c = ox * ox + oz * oz - r2;
139
+ const disc = b * b - a * c;
140
+ if (disc >= 0) {
141
+ const sq = Math.sqrt(disc);
142
+ const inv_a = 1 / a;
143
+ // Both roots; the smaller non-negative one whose hit lies on the
144
+ // cylindrical section is the side contact.
145
+ const roots = (-b - sq) * inv_a;
146
+ const rootf = (-b + sq) * inv_a;
147
+ for (let i = 0; i < 2; i++) {
148
+ const t = i === 0 ? roots : rootf;
149
+ if (t < 0 || t >= best || t > tMax) continue;
150
+ const y = oy + dy * t;
151
+ if (y < -hh || y > hh) continue; // off the cylinder, onto a cap
152
+ best = t;
153
+ const px = ox + dx * t, pz = oz + dz * t;
154
+ nx = px * inv_r; ny = 0; nz = pz * inv_r;
155
+ break; // roots is the nearer; if it qualified we're done
156
+ }
157
+ }
158
+ }
159
+
160
+ // --- Cap spheres at (0, ±hh, 0), each valid only on its outer hemisphere. ---
161
+ for (let cap = 0; cap < 2; cap++) {
162
+ const cy = cap === 0 ? hh : -hh;
163
+ const oyc = oy - cy;
164
+ const bb = ox * dx + oyc * dy + oz * dz;
165
+ const cc = ox * ox + oyc * oyc + oz * oz - r2;
166
+ const disc = bb * bb - cc;
167
+ if (disc < 0) continue;
168
+ const sq = Math.sqrt(disc);
169
+ const tn = -bb - sq, tf = -bb + sq;
170
+ for (let i = 0; i < 2; i++) {
171
+ const t = i === 0 ? tn : tf;
172
+ if (t < 0 || t >= best || t > tMax) continue;
173
+ const hy = oy + dy * t;
174
+ // Only the hemisphere beyond the segment end belongs to this cap.
175
+ if (cap === 0 ? hy < hh : hy > -hh) continue;
176
+ best = t;
177
+ nx = (ox + dx * t) * inv_r;
178
+ ny = (hy - cy) * inv_r;
179
+ nz = (oz + dz * t) * inv_r;
180
+ break;
181
+ }
182
+ }
183
+
184
+ if (best === Infinity) return Infinity;
185
+ outNormal[0] = nx; outNormal[1] = ny; outNormal[2] = nz;
186
+ return best;
187
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @param {AbstractShape3D} shape a concave shape (`is_convex === false`)
3
+ * @param {{x:number,y:number,z:number}} position
4
+ * @param {ArrayLike<number>} rotation body rotation quaternion (x,y,z,w)
5
+ * @param {number} ox @param {number} oy @param {number} oz ray origin (world)
6
+ * @param {number} dx @param {number} dy @param {number} dz ray dir (world, unit)
7
+ * @param {number} tMax
8
+ * @param {Float64Array|Vector3} outNormal world surface normal, written on hit
9
+ * @returns {number} hit distance, or `Infinity` on miss
10
+ */
11
+ export function refine_ray_concave(shape: AbstractShape3D, position: {
12
+ x: number;
13
+ y: number;
14
+ z: number;
15
+ }, rotation: ArrayLike<number>, ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, tMax: number, outNormal: Float64Array | Vector3): number;
16
+ //# sourceMappingURL=refine_ray_concave.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refine_ray_concave.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/refine_ray_concave.js"],"names":[],"mappings":"AAsCA;;;;;;;;;GASG;AACH,qEARW;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,YAC5B,UAAU,MAAM,CAAC,MACjB,MAAM,MAAa,MAAM,MAAa,MAAM,MAC5C,MAAM,MAAa,MAAM,MAAa,MAAM,QAC5C,MAAM,aACN,YAAY,UAAQ,GAClB,MAAM,CAkGlB"}
@@ -0,0 +1,145 @@
1
+ import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
2
+ import { TRIANGLE_FLOAT_STRIDE } from "./decomposition/triangle_buffer_layout.js";
3
+ import { mesh_enumerate_triangles } from "./decomposition/mesh_enumerate_triangles.js";
4
+ import { heightmap_enumerate_triangles } from "./decomposition/heightmap_enumerate_triangles.js";
5
+
6
+ /**
7
+ * # Ray vs a concave shape (mesh / heightmap)
8
+ *
9
+ * Refines a ray hit against a non-convex collider by triangle decomposition,
10
+ * mirroring the contact narrowphase's concave path. Working in the shape's
11
+ * local frame:
12
+ * 1. clip the ray to the shape's local bounding box (a quick reject, and a
13
+ * finite query segment even when `tMax` is unbounded);
14
+ * 2. enumerate the triangles overlapping that segment's AABB
15
+ * (`mesh_enumerate_triangles` / `heightmap_enumerate_triangles`);
16
+ * 3. two-sided Möller–Trumbore each, keeping the nearest crossing;
17
+ * 4. return its distance and the world surface normal (the triangle's
18
+ * geometric normal, oriented to face the ray).
19
+ *
20
+ * The triangle scratch grows on saturation and is bounded by {@link MAX_TRIS}
21
+ * (a ray crossing more triangles than that in one mesh is not a realistic
22
+ * raycast — it would mean skewering a 65k-triangle mesh lengthwise).
23
+ *
24
+ * @author Alex Goldring
25
+ * @copyright Company Named Limited (c) 2026
26
+ */
27
+
28
+ const lo = new Float64Array(3); // ray origin, shape-local
29
+ const ld = new Float64Array(3); // ray direction, shape-local
30
+ const bounds = new Float64Array(6); // shape local AABB
31
+
32
+ const INITIAL_TRIS = 1024;
33
+ const MAX_TRIS = 1 << 16;
34
+ let tri_buf = new Float64Array(INITIAL_TRIS * TRIANGLE_FLOAT_STRIDE);
35
+
36
+ const EPS = 1e-12;
37
+ const AABB_FATTEN = 1e-5; // guards triangles whose AABB just abuts the segment
38
+
39
+ /**
40
+ * @param {AbstractShape3D} shape a concave shape (`is_convex === false`)
41
+ * @param {{x:number,y:number,z:number}} position
42
+ * @param {ArrayLike<number>} rotation body rotation quaternion (x,y,z,w)
43
+ * @param {number} ox @param {number} oy @param {number} oz ray origin (world)
44
+ * @param {number} dx @param {number} dy @param {number} dz ray dir (world, unit)
45
+ * @param {number} tMax
46
+ * @param {Float64Array|Vector3} outNormal world surface normal, written on hit
47
+ * @returns {number} hit distance, or `Infinity` on miss
48
+ */
49
+ export function refine_ray_concave(shape, position, rotation, ox, oy, oz, dx, dy, dz, tMax, outNormal) {
50
+ const qx = rotation[0], qy = rotation[1], qz = rotation[2], qw = rotation[3];
51
+ // World → local (inverse body rotation; unit dir stays unit → t preserved).
52
+ v3_quat3_apply(lo, 0, ox - position.x, oy - position.y, oz - position.z, -qx, -qy, -qz, qw);
53
+ v3_quat3_apply(ld, 0, dx, dy, dz, -qx, -qy, -qz, qw);
54
+ const lox = lo[0], loy = lo[1], loz = lo[2];
55
+ const ldx = ld[0], ldy = ld[1], ldz = ld[2];
56
+
57
+ // Clip the local ray to the shape's local AABB → segment [tEnter, tExit].
58
+ shape.compute_bounding_box(bounds);
59
+ let tEnter = 0, tExit = tMax;
60
+ if (ldx > EPS || ldx < -EPS) {
61
+ const inv = 1 / ldx;
62
+ let t1 = (bounds[0] - lox) * inv, t2 = (bounds[3] - lox) * inv;
63
+ if (t1 > t2) { const s = t1; t1 = t2; t2 = s; }
64
+ if (t1 > tEnter) tEnter = t1;
65
+ if (t2 < tExit) tExit = t2;
66
+ } else if (lox < bounds[0] || lox > bounds[3]) return Infinity;
67
+ if (ldy > EPS || ldy < -EPS) {
68
+ const inv = 1 / ldy;
69
+ let t1 = (bounds[1] - loy) * inv, t2 = (bounds[4] - loy) * inv;
70
+ if (t1 > t2) { const s = t1; t1 = t2; t2 = s; }
71
+ if (t1 > tEnter) tEnter = t1;
72
+ if (t2 < tExit) tExit = t2;
73
+ } else if (loy < bounds[1] || loy > bounds[4]) return Infinity;
74
+ if (ldz > EPS || ldz < -EPS) {
75
+ const inv = 1 / ldz;
76
+ let t1 = (bounds[2] - loz) * inv, t2 = (bounds[5] - loz) * inv;
77
+ if (t1 > t2) { const s = t1; t1 = t2; t2 = s; }
78
+ if (t1 > tEnter) tEnter = t1;
79
+ if (t2 < tExit) tExit = t2;
80
+ } else if (loz < bounds[2] || loz > bounds[5]) return Infinity;
81
+ if (tExit < tEnter) return Infinity; // ray misses the shape's AABB entirely
82
+
83
+ // Query AABB = bbox of the clipped local segment.
84
+ const sax = lox + ldx * tEnter, say = loy + ldy * tEnter, saz = loz + ldz * tEnter;
85
+ const sbx = lox + ldx * tExit, sby = loy + ldy * tExit, sbz = loz + ldz * tExit;
86
+ const qminx = (sax < sbx ? sax : sbx) - AABB_FATTEN;
87
+ const qminy = (say < sby ? say : sby) - AABB_FATTEN;
88
+ const qminz = (saz < sbz ? saz : sbz) - AABB_FATTEN;
89
+ const qmaxx = (sax > sbx ? sax : sbx) + AABB_FATTEN;
90
+ const qmaxy = (say > sby ? say : sby) + AABB_FATTEN;
91
+ const qmaxz = (saz > sbz ? saz : sbz) + AABB_FATTEN;
92
+
93
+ const enumerate = shape.isHeightMapShape3D === true ? heightmap_enumerate_triangles : mesh_enumerate_triangles;
94
+ let cap = (tri_buf.length / TRIANGLE_FLOAT_STRIDE) | 0;
95
+ let count = enumerate(tri_buf, 0, shape, qminx, qminy, qminz, qmaxx, qmaxy, qmaxz);
96
+ while (count === cap && cap < MAX_TRIS) {
97
+ cap = cap * 2 < MAX_TRIS ? cap * 2 : MAX_TRIS;
98
+ tri_buf = new Float64Array(cap * TRIANGLE_FLOAT_STRIDE);
99
+ count = enumerate(tri_buf, 0, shape, qminx, qminy, qminz, qmaxx, qmaxy, qmaxz);
100
+ }
101
+
102
+ // Two-sided Möller–Trumbore over the candidates; keep the nearest.
103
+ let best = tMax;
104
+ let found = false;
105
+ let nx = 0, ny = 0, nz = 0;
106
+ for (let i = 0; i < count; i++) {
107
+ const o = i * TRIANGLE_FLOAT_STRIDE;
108
+ const ax = tri_buf[o], ay = tri_buf[o + 1], az = tri_buf[o + 2];
109
+ const e1x = tri_buf[o + 3] - ax, e1y = tri_buf[o + 4] - ay, e1z = tri_buf[o + 5] - az;
110
+ const e2x = tri_buf[o + 6] - ax, e2y = tri_buf[o + 7] - ay, e2z = tri_buf[o + 8] - az;
111
+
112
+ const px = ldy * e2z - ldz * e2y, py = ldz * e2x - ldx * e2z, pz = ldx * e2y - ldy * e2x;
113
+ const det = e1x * px + e1y * py + e1z * pz;
114
+ if (det < EPS && det > -EPS) continue; // ray parallel to the triangle
115
+ const invDet = 1 / det;
116
+
117
+ const tvx = lox - ax, tvy = loy - ay, tvz = loz - az;
118
+ const u = (tvx * px + tvy * py + tvz * pz) * invDet;
119
+ if (u < 0 || u > 1) continue;
120
+
121
+ const wx = tvy * e1z - tvz * e1y, wy = tvz * e1x - tvx * e1z, wz = tvx * e1y - tvy * e1x;
122
+ const v = (ldx * wx + ldy * wy + ldz * wz) * invDet;
123
+ if (v < 0 || u + v > 1) continue;
124
+
125
+ const t = (e2x * wx + e2y * wy + e2z * wz) * invDet;
126
+ if (t < 0 || t >= best) continue;
127
+
128
+ best = t;
129
+ found = true;
130
+ // Geometric normal e1 × e2 (local, unnormalised).
131
+ nx = e1y * e2z - e1z * e2y;
132
+ ny = e1z * e2x - e1x * e2z;
133
+ nz = e1x * e2y - e1y * e2x;
134
+ }
135
+
136
+ if (!found) return Infinity;
137
+
138
+ // Normalise, orient to face the ray, then rotate the local normal to world.
139
+ let nl = Math.sqrt(nx * nx + ny * ny + nz * nz);
140
+ if (nl === 0) nl = 1;
141
+ nx /= nl; ny /= nl; nz /= nl;
142
+ if (nx * ldx + ny * ldy + nz * ldz > 0) { nx = -nx; ny = -ny; nz = -nz; }
143
+ v3_quat3_apply(outNormal, 0, nx, ny, nz, qx, qy, qz, qw);
144
+ return best;
145
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Refine a broadphase ray hit against a body's true shape geometry.
3
+ *
4
+ * Transforms the world ray into the shape's local frame once (rotate by the
5
+ * inverse body rotation — a unit direction stays unit, so the hit distance is
6
+ * preserved), runs the matching canonical primitive ({@link ray_shapes}), and
7
+ * rotates the returned local normal back to world. Spheres are
8
+ * rotation-invariant, so they only need the translation.
9
+ *
10
+ * Handles the convex primitives (sphere / box / capsule). Concave shapes
11
+ * (mesh / heightmap) and arbitrary composite convex shapes return
12
+ * {@link RAY_REFINE_UNSUPPORTED} — the concave path is wired in separately, and
13
+ * composites fall back to the broadphase AABB hit (no regression).
14
+ *
15
+ * @param {AbstractShape3D} shape
16
+ * @param {{x:number,y:number,z:number}} position body world position
17
+ * @param {ArrayLike<number>} rotation body world rotation quaternion (x,y,z,w)
18
+ * @param {number} ox @param {number} oy @param {number} oz ray origin (world)
19
+ * @param {number} dx @param {number} dy @param {number} dz ray dir (world, unit)
20
+ * @param {number} tMax
21
+ * @param {Float64Array|Vector3} outNormal world surface normal, written on a
22
+ * finite hit
23
+ * @returns {number} hit distance, `Infinity` on a refined miss, or
24
+ * {@link RAY_REFINE_UNSUPPORTED} when the shape has no exact ray test here
25
+ */
26
+ export function refine_ray_hit(shape: AbstractShape3D, position: {
27
+ x: number;
28
+ y: number;
29
+ z: number;
30
+ }, rotation: ArrayLike<number>, ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, tMax: number, outNormal: Float64Array | Vector3): number;
31
+ /**
32
+ * Sentinel returned by {@link refine_ray_hit} for a shape it has no exact ray
33
+ * test for (composite / arbitrary convex). The caller should keep the
34
+ * broadphase AABB result for that leaf rather than treat it as a miss.
35
+ * Distinct from `Infinity` (a refined miss) and any finite hit distance.
36
+ * @type {number}
37
+ */
38
+ export const RAY_REFINE_UNSUPPORTED: number;
39
+ //# sourceMappingURL=refine_ray_hit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refine_ray_hit.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/refine_ray_hit.js"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,iEAVW;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,YAC5B,UAAU,MAAM,CAAC,MACjB,MAAM,MAAa,MAAM,MAAa,MAAM,MAC5C,MAAM,MAAa,MAAM,MAAa,MAAM,QAC5C,MAAM,aACN,YAAY,UAAQ,GAElB,MAAM,CAsClB;AAzED;;;;;;GAMG;AACH,qCAFU,MAAM,CAEyB"}
@@ -0,0 +1,78 @@
1
+ import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
2
+ import { ray_sphere_local, ray_box_local, ray_capsule_local } from "./ray_shapes.js";
3
+ import { refine_ray_concave } from "./refine_ray_concave.js";
4
+
5
+ /**
6
+ * Sentinel returned by {@link refine_ray_hit} for a shape it has no exact ray
7
+ * test for (composite / arbitrary convex). The caller should keep the
8
+ * broadphase AABB result for that leaf rather than treat it as a miss.
9
+ * Distinct from `Infinity` (a refined miss) and any finite hit distance.
10
+ * @type {number}
11
+ */
12
+ export const RAY_REFINE_UNSUPPORTED = -1;
13
+
14
+ const lo = new Float64Array(3); // ray origin in shape-local frame
15
+ const ld = new Float64Array(3); // ray direction in shape-local frame
16
+ const ln = new Float64Array(3); // surface normal in shape-local frame
17
+
18
+ /**
19
+ * Refine a broadphase ray hit against a body's true shape geometry.
20
+ *
21
+ * Transforms the world ray into the shape's local frame once (rotate by the
22
+ * inverse body rotation — a unit direction stays unit, so the hit distance is
23
+ * preserved), runs the matching canonical primitive ({@link ray_shapes}), and
24
+ * rotates the returned local normal back to world. Spheres are
25
+ * rotation-invariant, so they only need the translation.
26
+ *
27
+ * Handles the convex primitives (sphere / box / capsule). Concave shapes
28
+ * (mesh / heightmap) and arbitrary composite convex shapes return
29
+ * {@link RAY_REFINE_UNSUPPORTED} — the concave path is wired in separately, and
30
+ * composites fall back to the broadphase AABB hit (no regression).
31
+ *
32
+ * @param {AbstractShape3D} shape
33
+ * @param {{x:number,y:number,z:number}} position body world position
34
+ * @param {ArrayLike<number>} rotation body world rotation quaternion (x,y,z,w)
35
+ * @param {number} ox @param {number} oy @param {number} oz ray origin (world)
36
+ * @param {number} dx @param {number} dy @param {number} dz ray dir (world, unit)
37
+ * @param {number} tMax
38
+ * @param {Float64Array|Vector3} outNormal world surface normal, written on a
39
+ * finite hit
40
+ * @returns {number} hit distance, `Infinity` on a refined miss, or
41
+ * {@link RAY_REFINE_UNSUPPORTED} when the shape has no exact ray test here
42
+ */
43
+ export function refine_ray_hit(shape, position, rotation, ox, oy, oz, dx, dy, dz, tMax, outNormal) {
44
+ if (shape.isSphereShape3D === true) {
45
+ // Rotation-invariant: translate into the sphere's frame; the local
46
+ // normal is already the world normal.
47
+ return ray_sphere_local(outNormal, ox - position.x, oy - position.y, oz - position.z, dx, dy, dz, tMax, shape.radius);
48
+ }
49
+
50
+ if (shape.isBoxShape3D === true || shape.isCapsuleShape3D === true) {
51
+ const qx = rotation[0], qy = rotation[1], qz = rotation[2], qw = rotation[3];
52
+ // World → local: rotate by the inverse (conjugate) body rotation.
53
+ v3_quat3_apply(lo, 0, ox - position.x, oy - position.y, oz - position.z, -qx, -qy, -qz, qw);
54
+ v3_quat3_apply(ld, 0, dx, dy, dz, -qx, -qy, -qz, qw);
55
+
56
+ let t;
57
+ if (shape.isBoxShape3D === true) {
58
+ const h = shape.half_extents;
59
+ t = ray_box_local(ln, lo[0], lo[1], lo[2], ld[0], ld[1], ld[2], tMax, h.x, h.y, h.z);
60
+ } else {
61
+ t = ray_capsule_local(ln, lo[0], lo[1], lo[2], ld[0], ld[1], ld[2], tMax, shape.radius, shape.height * 0.5);
62
+ }
63
+ if (t === Infinity) return Infinity;
64
+
65
+ // Local normal → world: rotate by the body rotation.
66
+ v3_quat3_apply(outNormal, 0, ln[0], ln[1], ln[2], qx, qy, qz, qw);
67
+ return t;
68
+ }
69
+
70
+ // Concave (mesh / heightmap): triangle decomposition.
71
+ if (shape.is_convex === false) {
72
+ return refine_ray_concave(shape, position, rotation, ox, oy, oz, dx, dy, dz, tMax, outNormal);
73
+ }
74
+
75
+ // Arbitrary composite convex (TransformedShape3D / UnionShape3D / …):
76
+ // no exact ray test yet — caller falls back to the broadphase AABB hit.
77
+ return RAY_REFINE_UNSUPPORTED;
78
+ }
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Closed-form contact generation for two unit spheres positioned in world
3
- * space. Returns whether the spheres overlap. On overlap, `out` is populated
4
- * with `[nx, ny, nz, depth]` where the normal points from B toward A and
5
- * `depth` is the positive penetration distance.
2
+ * Closed-form contact generation for two spheres positioned in world space.
3
+ * Returns whether the spheres overlap. On overlap, `out` is populated with
4
+ * `[nx, ny, nz, depth]` where the normal (unit length) points from B toward A
5
+ * and `depth` is the positive penetration distance.
6
6
  *
7
- * Both spheres have radius 1 (the {@link UnitSphereShape3D} convention); any
8
- * scaling on the body's transform is irrelevant under our "no scale on
9
- * physics transforms" assumption.
7
+ * Each sphere's radius is passed explicitly (`radius_a` / `radius_b`) — the
8
+ * caller reads it from the shape (`SphereShape3D.radius`; 1 for the
9
+ * {@link UnitSphereShape3D} special case). Any scaling on the body's transform
10
+ * is irrelevant under our "no scale on physics transforms" assumption.
10
11
  *
11
12
  * Centres-coincident is a singular case for the general normal but is
12
13
  * resolved here by picking +X as a deterministic tie-break direction.
@@ -1 +1 @@
1
- {"version":3,"file":"sphere_sphere_contact.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/sphere_sphere_contact.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,2CAXW,MAAM,EAAE,GAAC,YAAY,MACrB,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,YACN,MAAM,YACN,MAAM,GACJ,OAAO,CA4BnB"}
1
+ {"version":3,"file":"sphere_sphere_contact.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/sphere_sphere_contact.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,2CAXW,MAAM,EAAE,GAAC,YAAY,MACrB,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,YACN,MAAM,YACN,MAAM,GACJ,OAAO,CA4BnB"}
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Closed-form contact generation for two unit spheres positioned in world
3
- * space. Returns whether the spheres overlap. On overlap, `out` is populated
4
- * with `[nx, ny, nz, depth]` where the normal points from B toward A and
5
- * `depth` is the positive penetration distance.
2
+ * Closed-form contact generation for two spheres positioned in world space.
3
+ * Returns whether the spheres overlap. On overlap, `out` is populated with
4
+ * `[nx, ny, nz, depth]` where the normal (unit length) points from B toward A
5
+ * and `depth` is the positive penetration distance.
6
6
  *
7
- * Both spheres have radius 1 (the {@link UnitSphereShape3D} convention); any
8
- * scaling on the body's transform is irrelevant under our "no scale on
9
- * physics transforms" assumption.
7
+ * Each sphere's radius is passed explicitly (`radius_a` / `radius_b`) — the
8
+ * caller reads it from the shape (`SphereShape3D.radius`; 1 for the
9
+ * {@link UnitSphereShape3D} special case). Any scaling on the body's transform
10
+ * is irrelevant under our "no scale on physics transforms" assumption.
10
11
  *
11
12
  * Centres-coincident is a singular case for the general normal but is
12
13
  * resolved here by picking +X as a deterministic tie-break direction.
@@ -1,19 +1,21 @@
1
1
  /**
2
2
  * Raycast against both broadphase trees (static + dynamic) of a
3
- * {@link PhysicsSystem}, filling `result` with the nearest hit and returning
4
- * `true` on hit, `false` on miss.
3
+ * {@link PhysicsSystem}, refined against each candidate's true shape geometry.
4
+ * Fills `result` with the nearest hit and returns `true` on hit, `false` on
5
+ * miss. `result.t` is the exact surface distance and `result.normal` the true
6
+ * surface normal for sphere / box / capsule / mesh / heightmap colliders;
7
+ * composite convex shapes (no exact ray test yet) fall back to the broadphase
8
+ * AABB hit + AABB-face normal.
5
9
  *
6
- * Hit normal is the AABB face normalexact for AABB-shaped colliders,
7
- * a stable approximation for general convex shapes (an upcoming narrowphase
8
- * refinement pass will replace this with the true shape normal at the same
9
- * call site, no API change).
10
+ * Multi-collider bodies resolve their primary (first-attached) colliderthe
11
+ * BVH leaf encodes only `body_id`; per-collider rays need the leaf user-data to
12
+ * carry the collider index (future work).
10
13
  *
11
14
  * @param {PhysicsSystem} system
12
15
  * @param {Ray3} ray origin + unit direction + `tMax`
13
16
  * @param {PhysicsSurfacePoint} result populated on hit; untouched on miss
14
- * @param {(entity:number, collider:Collider)=>boolean} [filter] mandatory in
15
- * contract; defaults to {@link returnTrue} (accept everything). Called once
16
- * per BVH leaf that crosses the ray.
17
+ * @param {(entity:number, collider:Collider)=>boolean} [filter] called once per
18
+ * crossing leaf; defaults to {@link returnTrue}.
17
19
  * @returns {boolean} true on hit, false on miss
18
20
  */
19
21
  export function raycast(system: PhysicsSystem, ray: Ray3, result: PhysicsSurfacePoint, filter?: (entity: number, collider: Collider) => boolean): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"raycast.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/queries/raycast.js"],"names":[],"mappings":"AA2HA;;;;;;;;;;;;;;;;;GAiBG;AACH,yGALmB,MAAM,yBAAsB,OAAO,GAGzC,OAAO,CA6GnB"}
1
+ {"version":3,"file":"raycast.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/queries/raycast.js"],"names":[],"mappings":"AA2IA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,yGAJmB,MAAM,yBAAsB,OAAO,GAEzC,OAAO,CAwCnB"}