@woosh/meep-engine 2.142.0 → 2.144.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 (64) 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/PointShape3D.d.ts +1 -0
  5. package/src/core/geom/3d/shape/PointShape3D.d.ts.map +1 -1
  6. package/src/core/geom/3d/shape/PointShape3D.js +11 -0
  7. package/src/core/geom/3d/shape/SphereShape3D.d.ts +48 -0
  8. package/src/core/geom/3d/shape/SphereShape3D.d.ts.map +1 -0
  9. package/src/core/geom/3d/shape/SphereShape3D.js +131 -0
  10. package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts +30 -18
  11. package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts.map +1 -1
  12. package/src/core/geom/3d/shape/UnitSphereShape3D.js +44 -92
  13. package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
  14. package/src/core/geom/3d/shape/json/shape_to_type.js +4 -2
  15. package/src/core/geom/3d/shape/json/type_adapters.d.ts +12 -3
  16. package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
  17. package/src/core/geom/3d/shape/json/type_adapters.js +16 -4
  18. package/src/core/geom/3d/shape/util/shape_to_visual_entity.js +2 -2
  19. package/src/engine/control/first-person/DESIGN_COLLISION.md +302 -0
  20. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +91 -58
  21. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  22. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1814 -1789
  23. package/src/engine/control/first-person/TODO.md +17 -32
  24. package/src/engine/control/first-person/collision/KinematicMover.d.ts +176 -0
  25. package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -0
  26. package/src/engine/control/first-person/collision/KinematicMover.js +424 -0
  27. package/src/engine/control/first-person/prototype_first_person_controller.js +65 -0
  28. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
  29. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +3 -1
  30. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +30 -16
  31. package/src/engine/physics/PLAN.md +94 -32
  32. package/src/engine/physics/contact/ManifoldStore.d.ts +28 -2
  33. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  34. package/src/engine/physics/contact/ManifoldStore.js +37 -3
  35. package/src/engine/physics/contact/combine_material.d.ts +30 -0
  36. package/src/engine/physics/contact/combine_material.d.ts.map +1 -0
  37. package/src/engine/physics/contact/combine_material.js +35 -0
  38. package/src/engine/physics/ecs/Collider.d.ts +15 -0
  39. package/src/engine/physics/ecs/Collider.d.ts.map +1 -1
  40. package/src/engine/physics/ecs/Collider.js +34 -0
  41. package/src/engine/physics/ecs/Joint.d.ts +18 -0
  42. package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
  43. package/src/engine/physics/ecs/Joint.js +70 -0
  44. package/src/engine/physics/ecs/JointSerializationAdapter.d.ts +29 -0
  45. package/src/engine/physics/ecs/JointSerializationAdapter.d.ts.map +1 -0
  46. package/src/engine/physics/ecs/JointSerializationAdapter.js +72 -0
  47. package/src/engine/physics/ecs/PhysicsSystem.d.ts +9 -4
  48. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  49. package/src/engine/physics/ecs/PhysicsSystem.js +9 -4
  50. package/src/engine/physics/ecs/RigidBody.d.ts +15 -0
  51. package/src/engine/physics/ecs/RigidBody.d.ts.map +1 -1
  52. package/src/engine/physics/ecs/RigidBody.js +46 -0
  53. package/src/engine/physics/narrowphase/compute_penetration.d.ts +41 -41
  54. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
  55. package/src/engine/physics/narrowphase/compute_penetration.js +96 -169
  56. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +52 -0
  57. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  58. package/src/engine/physics/narrowphase/narrowphase_step.js +150 -16
  59. package/src/engine/physics/narrowphase/refine_ray_hit.js +2 -2
  60. package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts +8 -7
  61. package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts.map +1 -1
  62. package/src/engine/physics/narrowphase/sphere_sphere_contact.js +8 -7
  63. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  64. package/src/engine/physics/solver/solve_contacts.js +10 -21
@@ -1,6 +1,7 @@
1
1
  import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
2
2
  import { Triangle3D } from "../../../core/geom/3d/shape/Triangle3D.js";
3
3
  import { body_id_index } from "../body/BodyStorage.js";
4
+ import { combine_friction, combine_restitution } from "../contact/combine_material.js";
4
5
  import { CONTACT_STRIDE, MAX_CONTACTS_PER_MANIFOLD } from "../contact/ManifoldStore.js";
5
6
  import { expanding_polytope_algorithm } from "../gjk/expanding_polytope_algorithm.js";
6
7
  import { gjk_with_axis } from "../gjk/gjk.js";
@@ -52,7 +53,7 @@ const capsule_box_multi_result = new Float64Array(CAPSULE_BOX_MAX_CONTACTS * CAP
52
53
 
53
54
  /**
54
55
  * Candidate-contact stride: wax, way, waz, wbx, wby, wbz, nx, ny, nz, depth,
55
- * feature_id.
56
+ * feature_id, friction, restitution.
56
57
  *
57
58
  * The `feature_id` (offset 10) is a stable cross-frame identifier of the
58
59
  * geometric feature pair that produced this contact — used by the
@@ -61,9 +62,26 @@ const capsule_box_multi_result = new Float64Array(CAPSULE_BOX_MAX_CONTACTS * CAP
61
62
  * corresponds to the same physical contact. A value of 0 means
62
63
  * "no feature info, fall back to position matching".
63
64
  *
65
+ * `friction` (offset 11) and `restitution` (offset 12) are the COMBINED
66
+ * coefficients for the specific (colliderA, colliderB) pair that produced this
67
+ * contact, combined here (the only place that knows the exact source collider
68
+ * on each side) and carried into the manifold so a compound body's per-collider
69
+ * materials are honoured per-contact.
70
+ *
71
+ * @type {number}
72
+ */
73
+ const CANDIDATE_STRIDE = 13;
74
+
75
+ /**
76
+ * Combined friction / restitution for the collider pair currently being
77
+ * dispatched. Set once at the top of {@link dispatch_pair} (which is called
78
+ * per collider pair) and written into every contact that call appends, so
79
+ * each contact carries the material of its actual source colliders. Module
80
+ * scratch rather than threaded through every `append_contact` call site.
64
81
  * @type {number}
65
82
  */
66
- const CANDIDATE_STRIDE = 11;
83
+ let g_pair_friction = 0;
84
+ let g_pair_restitution = 0;
67
85
 
68
86
  /**
69
87
  * Maximum number of contacts emitted into the per-pair manifold after the
@@ -194,6 +212,8 @@ function append_contact(count, wax, way, waz, wbx, wby, wbz, nx, ny, nz, depth,
194
212
  candidates[off + 6] = nx; candidates[off + 7] = ny; candidates[off + 8] = nz;
195
213
  candidates[off + 9] = depth;
196
214
  candidates[off + 10] = feature_id;
215
+ candidates[off + 11] = g_pair_friction;
216
+ candidates[off + 12] = g_pair_restitution;
197
217
 
198
218
  return count + 1;
199
219
  }
@@ -358,8 +378,24 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
358
378
  const shapeA = colA.shape;
359
379
  const shapeB = colB.shape;
360
380
 
361
- const isSphereA = shapeA.isUnitSphereShape3D === true;
362
- const isSphereB = shapeB.isUnitSphereShape3D === true;
381
+ // Per-contact materials: combine the two source colliders' coefficients
382
+ // once here (this is the only place that knows the exact collider on each
383
+ // side) and stamp them onto every contact this dispatch appends. The
384
+ // `deepest_pair_penetration` query passes bare `{shape}` adapters with no
385
+ // material fields — it never writes to a manifold, so 0 is fine there.
386
+ const fa = colA.friction, fb = colB.friction;
387
+ if (fa !== undefined && fb !== undefined) {
388
+ g_pair_friction = combine_friction(fa, fb);
389
+ g_pair_restitution = combine_restitution(colA.restitution, colB.restitution);
390
+ } else {
391
+ g_pair_friction = 0;
392
+ g_pair_restitution = 0;
393
+ }
394
+
395
+ // isSphereShape3D covers both UnitSphereShape3D (fixed radius 1) and
396
+ // SphereShape3D (arbitrary radius). Both expose `radius`.
397
+ const isSphereA = shapeA.isSphereShape3D === true;
398
+ const isSphereB = shapeB.isSphereShape3D === true;
363
399
  // isBoxShape3D covers both UnitCubeShape3D (fixed 0.5) and BoxShape3D
364
400
  // (arbitrary half-extents). Both expose `half_extents` as a Vector3.
365
401
  const isBoxA = shapeA.isBoxShape3D === true;
@@ -369,12 +405,13 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
369
405
 
370
406
  // sphere-sphere
371
407
  if (isSphereA && isSphereB) {
408
+ const ra = shapeA.radius, rb = shapeB.radius;
372
409
 
373
410
  const ok = sphere_sphere_contact(
374
411
  sphere_result,
375
412
  trA.position.x, trA.position.y, trA.position.z,
376
413
  trB.position.x, trB.position.y, trB.position.z,
377
- 1, 1
414
+ ra, rb
378
415
  );
379
416
 
380
417
  if (!ok) return count;
@@ -383,23 +420,25 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
383
420
 
384
421
  // Sphere-sphere produces exactly one contact per pair; fid = 1
385
422
  // identifies it as a real feature (distinguishes from "no info" = 0)
386
- // and is trivially stable across frames.
423
+ // and is trivially stable across frames. Witnesses are the surface
424
+ // points along the (unit) normal, scaled by each sphere's radius.
387
425
  return append_contact(count,
388
- trA.position.x - nx, trA.position.y - ny, trA.position.z - nz,
389
- trB.position.x + nx, trB.position.y + ny, trB.position.z + nz,
426
+ trA.position.x - nx * ra, trA.position.y - ny * ra, trA.position.z - nz * ra,
427
+ trB.position.x + nx * rb, trB.position.y + ny * rb, trB.position.z + nz * rb,
390
428
  nx, ny, nz, sphere_result[3], 1);
391
429
  }
392
430
 
393
431
  // sphere ↔ box
394
432
  if ((isSphereA && isBoxB) || (isBoxA && isSphereB)) {
395
- const sphereTr = isSphereA ? trA : trB;
433
+ const sphereTr = isSphereA ? trA : trB;
434
+ const sphereShape = isSphereA ? shapeA : shapeB;
396
435
  const boxTr = isSphereA ? trB : trA;
397
436
  const boxShape = isSphereA ? shapeB : shapeA;
398
437
  const bh = boxShape.half_extents;
399
438
 
400
439
  const ok = sphere_box_contact(
401
440
  closed_form_result,
402
- sphereTr.position.x, sphereTr.position.y, sphereTr.position.z, 1,
441
+ sphereTr.position.x, sphereTr.position.y, sphereTr.position.z, sphereShape.radius,
403
442
  boxTr.position.x, boxTr.position.y, boxTr.position.z,
404
443
  boxTr.rotation.x, boxTr.rotation.y, boxTr.rotation.z, boxTr.rotation.w,
405
444
  bh.x, bh.y, bh.z
@@ -491,12 +530,13 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
491
530
  const capsuleTr = isCapsuleA ? trA : trB;
492
531
  const capsuleShape = isCapsuleA ? colA.shape : colB.shape;
493
532
  const sphereTr = isCapsuleA ? trB : trA;
533
+ const sphereShape = isCapsuleA ? colB.shape : colA.shape;
494
534
  const ok = capsule_sphere_contact(
495
535
  closed_form_result,
496
536
  capsuleTr.position.x, capsuleTr.position.y, capsuleTr.position.z,
497
537
  capsuleTr.rotation.x, capsuleTr.rotation.y, capsuleTr.rotation.z, capsuleTr.rotation.w,
498
538
  capsuleShape.radius, capsuleShape.height * 0.5,
499
- sphereTr.position.x, sphereTr.position.y, sphereTr.position.z, 1
539
+ sphereTr.position.x, sphereTr.position.y, sphereTr.position.z, sphereShape.radius
500
540
  );
501
541
  if (!ok) return count;
502
542
  let nx = closed_form_result[0], ny = closed_form_result[1], nz = closed_form_result[2];
@@ -639,14 +679,15 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
639
679
  const c_pos_y = concave_tr.position.y;
640
680
  const c_pos_z = concave_tr.position.z;
641
681
 
642
- // Sphere fast-path: when the convex side is a UnitSphereShape3D we
643
- // bypass GJK+EPA entirely per triangle and use the closed-form
682
+ // Sphere fast-path: when the convex side is a sphere we bypass GJK+EPA
683
+ // entirely per triangle and use the closed-form
644
684
  // {@link sphere_triangle_contact}. This avoids the EPA precision
645
685
  // wall on Triangle3D (whose support function is degenerate along
646
686
  // the face normal — all 3 vertices project to the same value),
647
687
  // which was producing noisy depths at small penetrations and
648
688
  // letting dropped spheres tunnel through heightmaps / meshes.
649
- const isSphereConvex = convex_col.shape.isUnitSphereShape3D === true;
689
+ const isSphereConvex = convex_col.shape.isSphereShape3D === true;
690
+ const sphere_radius = isSphereConvex ? convex_col.shape.radius : 0;
650
691
 
651
692
  // Box fast-path: closed-form {@link box_triangle_contact} via SAT
652
693
  // over 13 axes + polygon clipping for face-vs-face contacts.
@@ -743,7 +784,7 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
743
784
 
744
785
  const ok = sphere_triangle_contact(
745
786
  sphere_triangle_result,
746
- convex_wx, convex_wy, convex_wz, 1,
787
+ convex_wx, convex_wy, convex_wz, sphere_radius,
747
788
  ax_w, ay_w, az_w,
748
789
  bx_w, by_w, bz_w,
749
790
  cx_w, cy_w, cz_w
@@ -1259,6 +1300,98 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
1259
1300
  );
1260
1301
  }
1261
1302
 
1303
+ // Reusable single-pair adapters for the penetration query below — no per-call
1304
+ // allocation. dispatch_pair only reads `.shape` off a collider and
1305
+ // `.position` / `.rotation` off a transform, so these minimal stand-ins are
1306
+ // all it needs.
1307
+ const _pp_colA = { shape: null };
1308
+ const _pp_colB = { shape: null };
1309
+ const _pp_trA = { position: null, rotation: null };
1310
+ const _pp_trB = { position: null, rotation: null };
1311
+
1312
+ /**
1313
+ * Cold-start GJK seed for the one-shot penetration query. Re-zeroed before each
1314
+ * call so every query is independent (gjk_with_axis treats a zero vector as a
1315
+ * cold start) — no warm-start leakage between unrelated queries, which keeps
1316
+ * the result a pure function of its inputs.
1317
+ * @type {Float64Array}
1318
+ */
1319
+ const _pp_axis = new Float64Array(3);
1320
+
1321
+ /**
1322
+ * Single-pair penetration query: the depth and world normal of the DEEPEST
1323
+ * contact the narrowphase would generate for one posed shape pair.
1324
+ *
1325
+ * Routes through the exact same {@link dispatch_pair} the contact solver
1326
+ * consumes — closed-form for every sphere / box / capsule pair (box-box via
1327
+ * SAT, so the true minimum-translation axis is found rather than the
1328
+ * centroid-seeded portal MPR would pick), triangle decomposition + closed-form
1329
+ * per triangle for convex-vs-concave, and GJK + EPA (+ MPR) for any other
1330
+ * convex pair. The deepest contact's depth is the minimum-translation distance
1331
+ * and its normal is the MTV axis, so the result is correct for every shape pair
1332
+ * the engine can build and agrees bit-for-bit with what the solver acts on.
1333
+ *
1334
+ * The normal follows the narrowphase's stored convention: a unit vector
1335
+ * pointing from B toward A — the direction to translate A to separate it.
1336
+ *
1337
+ * Concave-vs-concave is not dispatched (the narrowphase skips it) and returns
1338
+ * 0; callers needing to reject that case must check `is_convex` themselves.
1339
+ *
1340
+ * Not re-entrant: shares the module-level candidate / scratch buffers with
1341
+ * {@link narrowphase_step}. Intended for main-thread queries run outside a
1342
+ * step (depenetration, overlap depth, tooling) — never from inside one.
1343
+ *
1344
+ * @param {Float64Array|number[]} out_normal length ≥ 3; receives the unit B→A
1345
+ * normal on penetration (untouched when the return value is 0)
1346
+ * @param {AbstractShape3D} shapeA
1347
+ * @param {{x:number,y:number,z:number}} posA
1348
+ * @param {{x:number,y:number,z:number,w:number}} rotA
1349
+ * @param {AbstractShape3D} shapeB
1350
+ * @param {{x:number,y:number,z:number}} posB
1351
+ * @param {{x:number,y:number,z:number,w:number}} rotB
1352
+ * @returns {number} deepest penetration depth (> 0) or 0 if separated
1353
+ */
1354
+ export function deepest_pair_penetration(out_normal, shapeA, posA, rotA, shapeB, posB, rotB) {
1355
+ _pp_colA.shape = shapeA;
1356
+ _pp_trA.position = posA;
1357
+ _pp_trA.rotation = rotA;
1358
+ _pp_colB.shape = shapeB;
1359
+ _pp_trB.position = posB;
1360
+ _pp_trB.rotation = rotB;
1361
+
1362
+ // Cold GJK seed — one-shot query, not a warm-started per-frame manifold.
1363
+ _pp_axis[0] = 0; _pp_axis[1] = 0; _pp_axis[2] = 0;
1364
+ const n = dispatch_pair(0, _pp_colA, _pp_trA, _pp_colB, _pp_trB, _pp_axis, 0);
1365
+ if (n === 0) {
1366
+ return 0;
1367
+ }
1368
+
1369
+ // Deepest contact = the minimum-translation depth; its stored normal is the
1370
+ // separation axis. (For multi-point manifolds — box-box, capsule-box, a
1371
+ // convex straddling several mesh triangles — every point shares the
1372
+ // separating axis, so the max depth along it is the distance to separate.)
1373
+ let best_depth = -1;
1374
+ let best_off = 0;
1375
+ for (let i = 0; i < n; i++) {
1376
+ const off = i * CANDIDATE_STRIDE;
1377
+ const d = candidates[off + 9];
1378
+ if (d > best_depth) {
1379
+ best_depth = d;
1380
+ best_off = off;
1381
+ }
1382
+ }
1383
+
1384
+ if (!(best_depth > 0) || !Number.isFinite(best_depth)) {
1385
+ return 0;
1386
+ }
1387
+
1388
+ out_normal[0] = candidates[best_off + 6];
1389
+ out_normal[1] = candidates[best_off + 7];
1390
+ out_normal[2] = candidates[best_off + 8];
1391
+
1392
+ return best_depth;
1393
+ }
1394
+
1262
1395
  /**
1263
1396
  * For every pair in `pair_list`, do a cross-product over A's collider list ×
1264
1397
  * B's collider list, accumulate candidate contacts, reduce to ≤4, and write
@@ -1401,7 +1534,8 @@ export function narrowphase_step(pair_list, manifolds, lists) {
1401
1534
  candidates[off + 3], candidates[off + 4], candidates[off + 5],
1402
1535
  candidates[off + 6], candidates[off + 7], candidates[off + 8],
1403
1536
  candidates[off + 9],
1404
- candidates[off + 10]
1537
+ candidates[off + 10],
1538
+ candidates[off + 11], candidates[off + 12]
1405
1539
  );
1406
1540
  const prev_j = cand_to_prev[k];
1407
1541
  if (prev_j !== -1) {
@@ -41,10 +41,10 @@ const ln = new Float64Array(3); // surface normal in shape-local frame
41
41
  * {@link RAY_REFINE_UNSUPPORTED} when the shape has no exact ray test here
42
42
  */
43
43
  export function refine_ray_hit(shape, position, rotation, ox, oy, oz, dx, dy, dz, tMax, outNormal) {
44
- if (shape.isUnitSphereShape3D === true) {
44
+ if (shape.isSphereShape3D === true) {
45
45
  // Rotation-invariant: translate into the sphere's frame; the local
46
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, 1);
47
+ return ray_sphere_local(outNormal, ox - position.x, oy - position.y, oz - position.z, dx, dy, dz, tMax, shape.radius);
48
48
  }
49
49
 
50
50
  if (shape.isBoxShape3D === true || shape.isCapsuleShape3D === true) {
@@ -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 +1 @@
1
- {"version":3,"file":"solve_contacts.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/solver/solve_contacts.js"],"names":[],"mappings":"AAgcA;;;;;;;;;;;;;GAaG;AACH,0FAHW,MAAM,GACJ,MAAM,CA4JlB;AAED;;;;;;;;;;;;;;;GAeG;AACH,2FAuCC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wFAgEC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,iGAmFC;AAED;;;;;;;;GAQG;AACH,uFAFW,MAAM,QA2GhB;AAED;;;;;;;;;;;GAWG;AACH,yFA4DC;AAED;;;;;;;;;;;;;;GAcG;AACH,2FAFW,MAAM,QA4EhB;AAED;;;;;;;;;;;;;;;GAeG;AACH,oFAJW,MAAM,UACN,MAAM,cACN,MAAM,QAahB;AAxlCD;;;;;GAKG;AACH,0CAFU,MAAM,CAEuB;AAEvC;;;GAGG;AACH,0CAFU,MAAM,CAEsB"}
1
+ {"version":3,"file":"solve_contacts.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/solver/solve_contacts.js"],"names":[],"mappings":"AAkbA;;;;;;;;;;;;;GAaG;AACH,0FAHW,MAAM,GACJ,MAAM,CA+JlB;AAED;;;;;;;;;;;;;;;GAeG;AACH,2FAuCC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wFAgEC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,iGAmFC;AAED;;;;;;;;GAQG;AACH,uFAFW,MAAM,QA2GhB;AAED;;;;;;;;;;;GAWG;AACH,yFA4DC;AAED;;;;;;;;;;;;;;GAcG;AACH,2FAFW,MAAM,QA4EhB;AAED;;;;;;;;;;;;;;;GAeG;AACH,oFAJW,MAAM,UACN,MAAM,cACN,MAAM,QAahB;AA7kCD;;;;;GAKG;AACH,0CAFU,MAAM,CAEuB;AAEvC;;;GAGG;AACH,0CAFU,MAAM,CAEsB"}
@@ -286,25 +286,11 @@ function inv_mass_of(rb) {
286
286
  return rb.mass > 0 ? 1 / rb.mass : 0;
287
287
  }
288
288
 
289
- /**
290
- * Combine two friction coefficients (geometric mean Bullet / PhysX default).
291
- * @param {number} a
292
- * @param {number} b
293
- * @returns {number}
294
- */
295
- function combine_friction(a, b) {
296
- return Math.sqrt(a * b);
297
- }
298
-
299
- /**
300
- * Combine two restitution coefficients (maximum — Unity / common default).
301
- * @param {number} a
302
- * @param {number} b
303
- * @returns {number}
304
- */
305
- function combine_restitution(a, b) {
306
- return a > b ? a : b;
307
- }
289
+ // Friction / restitution are no longer combined here: the narrowphase combines
290
+ // the specific source-collider pair's coefficients per contact (so compound
291
+ // bodies with mixed-material colliders are honoured) and stores the result in
292
+ // the manifold (CONTACT_STRIDE offsets 14 / 15). prepare_contacts reads them
293
+ // per contact. See engine/physics/contact/combine_material.js.
308
294
 
309
295
  const scratch_clamp = new Float64Array(2);
310
296
  const scratch_inertia_a = new Float64Array(3);
@@ -520,8 +506,6 @@ export function prepare_contacts(manifolds, system, dt_sub) {
520
506
 
521
507
  const cc = manifolds.contact_count(slot);
522
508
  const slot_off = manifolds.slot_data_offset(slot);
523
- const restitution_combined = combine_restitution(colA.restitution, colB.restitution);
524
- const friction_combined = combine_friction(colA.friction, colB.friction);
525
509
 
526
510
  const pAx = trA.position.x, pAy = trA.position.y, pAz = trA.position.z;
527
511
  const pBx = trB.position.x, pBy = trB.position.y, pBz = trB.position.z;
@@ -530,6 +514,11 @@ export function prepare_contacts(manifolds, system, dt_sub) {
530
514
  for (let k = 0; k < cc; k++) {
531
515
  const off = slot_off + k * CONTACT_STRIDE;
532
516
 
517
+ // Per-contact combined materials (stamped by the narrowphase from
518
+ // this contact's specific source-collider pair).
519
+ const friction_combined = data[off + 14];
520
+ const restitution_combined = data[off + 15];
521
+
533
522
  const wax = data[off], way = data[off + 1], waz = data[off + 2];
534
523
  const wbx = data[off + 3], wby = data[off + 4], wbz = data[off + 5];
535
524
  const nx = data[off + 6], ny = data[off + 7], nz = data[off + 8];