@woosh/meep-engine 2.142.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 (28) 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/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
  18. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +3 -1
  19. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +12 -7
  20. package/src/engine/physics/ecs/JointSerializationAdapter.d.ts +29 -0
  21. package/src/engine/physics/ecs/JointSerializationAdapter.d.ts.map +1 -0
  22. package/src/engine/physics/ecs/JointSerializationAdapter.js +72 -0
  23. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  24. package/src/engine/physics/narrowphase/narrowphase_step.js +20 -13
  25. package/src/engine/physics/narrowphase/refine_ray_hit.js +2 -2
  26. package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts +8 -7
  27. package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts.map +1 -1
  28. package/src/engine/physics/narrowphase/sphere_sphere_contact.js +8 -7
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Pure JavaScript game engine. Fully featured and production ready.",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "2.142.0",
9
+ "version": "2.143.0",
10
10
  "main": "build/meep.module.js",
11
11
  "module": "build/meep.module.js",
12
12
  "exports": {
@@ -33,7 +33,7 @@ export class CapsuleShape3D extends AbstractShape3D {
33
33
  */
34
34
  equals(other: CapsuleShape3D): boolean;
35
35
  /**
36
- * Fast type-check marker, mirroring `UnitSphereShape3D.prototype.isUnitSphereShape3D`
36
+ * Fast type-check marker, mirroring `SphereShape3D.prototype.isSphereShape3D`
37
37
  * and `BoxShape3D.prototype.isBoxShape3D`. Lets the physics narrowphase
38
38
  * dispatch capsule-sphere / capsule-capsule / capsule-box pairs to
39
39
  * closed-form helpers (capsule = Minkowski sum of segment + sphere; all
@@ -210,7 +210,7 @@ export class CapsuleShape3D extends AbstractShape3D {
210
210
  }
211
211
 
212
212
  /**
213
- * Fast type-check marker, mirroring `UnitSphereShape3D.prototype.isUnitSphereShape3D`
213
+ * Fast type-check marker, mirroring `SphereShape3D.prototype.isSphereShape3D`
214
214
  * and `BoxShape3D.prototype.isBoxShape3D`. Lets the physics narrowphase
215
215
  * dispatch capsule-sphere / capsule-capsule / capsule-box pairs to
216
216
  * closed-form helpers (capsule = Minkowski sum of segment + sphere; all
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Sphere of arbitrary `radius`, centred at the body's local origin.
3
+ *
4
+ * Mirrors the {@link BoxShape3D} (arbitrary half-extents) ↔
5
+ * {@link UnitCubeShape3D} (fixed 0.5) relationship: this is the general
6
+ * parametrised sphere, and {@link UnitSphereShape3D} is the `radius = 1`
7
+ * special case kept for the JSON `'sphere'` tag and the shared `INSTANCE`
8
+ * singleton.
9
+ *
10
+ * The physics narrowphase recognises spheres via the shared `isSphereShape3D`
11
+ * prototype marker and reads `radius`.
12
+ *
13
+ * @author Alex Goldring
14
+ * @copyright Company Named Limited (c) 2026
15
+ */
16
+ export class SphereShape3D extends AbstractShape3D {
17
+ /**
18
+ * Convenience constructor.
19
+ * @param {number} radius
20
+ * @returns {SphereShape3D}
21
+ */
22
+ static from(radius: number): SphereShape3D;
23
+ /**
24
+ * Sphere radius, in the body's local frame.
25
+ * @type {number}
26
+ */
27
+ radius: number;
28
+ support(result: any, result_offset: any, direction_x: any, direction_y: any, direction_z: any): void;
29
+ compute_bounding_box(result: any): void;
30
+ nearest_point_on_surface(result: any, reference: any): void;
31
+ signed_distance_gradient_at_point(result: any, point: any): number;
32
+ signed_distance_at_point(point: any): number;
33
+ contains_point(point: any): boolean;
34
+ sample_random_point_in_volume(result: any, result_offset: any, random: any): void;
35
+ /**
36
+ * Fast type-check marker. Lets the physics narrowphase short-circuit
37
+ * sphere-involved pairs to closed-form solvers (reading `radius`) rather than
38
+ * running GJK + EPA on a smooth surface (the EPA-degenerate case). `true` for
39
+ * both {@link SphereShape3D} and {@link UnitSphereShape3D} (via the prototype
40
+ * chain).
41
+ * @readonly
42
+ * @type {boolean}
43
+ */
44
+ readonly isSphereShape3D: boolean;
45
+ }
46
+ import { AbstractShape3D } from "./AbstractShape3D.js";
47
+ //# sourceMappingURL=SphereShape3D.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SphereShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/SphereShape3D.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;GAcG;AACH;IAUI;;;;OAIG;IACH,oBAHW,MAAM,GACJ,aAAa,CAMzB;IAhBG;;;OAGG;IACH,QAFU,MAAM,CAED;IAwBnB,qGAQC;IAED,wCAQC;IAED,4DAaC;IAED,mEAEC;IAED,6CAEC;IAED,oCAOC;IAED,kFAMC;IAOL;;;;;;;;OAQG;IACH,0BAFU,OAAO,CAEsB;CAXtC;gCAhH+B,sBAAsB"}
@@ -0,0 +1,127 @@
1
+ import { computeHashFloat } from "../../../primitives/numbers/computeHashFloat.js";
2
+ import { randomPointInSphere } from "../../random/randomPointInSphere.js";
3
+ import { v3_length } from "../../vec3/v3_length.js";
4
+ import { AbstractShape3D } from "./AbstractShape3D.js";
5
+ import { compute_signed_distance_gradient_by_sampling } from "./util/compute_signed_distance_gradient_by_sampling.js";
6
+
7
+ /**
8
+ * Sphere of arbitrary `radius`, centred at the body's local origin.
9
+ *
10
+ * Mirrors the {@link BoxShape3D} (arbitrary half-extents) ↔
11
+ * {@link UnitCubeShape3D} (fixed 0.5) relationship: this is the general
12
+ * parametrised sphere, and {@link UnitSphereShape3D} is the `radius = 1`
13
+ * special case kept for the JSON `'sphere'` tag and the shared `INSTANCE`
14
+ * singleton.
15
+ *
16
+ * The physics narrowphase recognises spheres via the shared `isSphereShape3D`
17
+ * prototype marker and reads `radius`.
18
+ *
19
+ * @author Alex Goldring
20
+ * @copyright Company Named Limited (c) 2026
21
+ */
22
+ export class SphereShape3D extends AbstractShape3D {
23
+ constructor() {
24
+ super();
25
+ /**
26
+ * Sphere radius, in the body's local frame.
27
+ * @type {number}
28
+ */
29
+ this.radius = 1;
30
+ }
31
+
32
+ /**
33
+ * Convenience constructor.
34
+ * @param {number} radius
35
+ * @returns {SphereShape3D}
36
+ */
37
+ static from(radius) {
38
+ const s = new SphereShape3D();
39
+ s.radius = radius;
40
+ return s;
41
+ }
42
+
43
+ get volume() {
44
+ const r = this.radius;
45
+ return (4 / 3) * Math.PI * r * r * r;
46
+ }
47
+
48
+ get surface_area() {
49
+ const r = this.radius;
50
+ return 4 * Math.PI * r * r;
51
+ }
52
+
53
+ support(result, result_offset, direction_x, direction_y, direction_z) {
54
+ // Farthest point along `direction` on a sphere of this radius. Matches
55
+ // the unit-sphere convention (no normalisation — callers pass a unit
56
+ // direction); for radius r it is simply the direction scaled by r.
57
+ const r = this.radius;
58
+ result[result_offset] = direction_x * r;
59
+ result[result_offset + 1] = direction_y * r;
60
+ result[result_offset + 2] = direction_z * r;
61
+ }
62
+
63
+ compute_bounding_box(result) {
64
+ const r = this.radius;
65
+ result[0] = -r;
66
+ result[1] = -r;
67
+ result[2] = -r;
68
+ result[3] = r;
69
+ result[4] = r;
70
+ result[5] = r;
71
+ }
72
+
73
+ nearest_point_on_surface(result, reference) {
74
+ const r_x = reference[0];
75
+ const r_y = reference[1];
76
+ const r_z = reference[2];
77
+
78
+ const len = v3_length(r_x, r_y, r_z);
79
+ // At the centre the nearest surface point is undefined; return the
80
+ // centre rather than NaN/Inf.
81
+ const d = len > 0 ? this.radius / len : 0;
82
+
83
+ result[0] = r_x * d;
84
+ result[1] = r_y * d;
85
+ result[2] = r_z * d;
86
+ }
87
+
88
+ signed_distance_gradient_at_point(result, point) {
89
+ return compute_signed_distance_gradient_by_sampling(result, this, point);
90
+ }
91
+
92
+ signed_distance_at_point(point) {
93
+ return v3_length(point[0], point[1], point[2]) - this.radius;
94
+ }
95
+
96
+ contains_point(point) {
97
+ const x = point[0];
98
+ const y = point[1];
99
+ const z = point[2];
100
+ const r = this.radius;
101
+
102
+ return (x * x + y * y + z * z) < r * r;
103
+ }
104
+
105
+ sample_random_point_in_volume(result, result_offset, random) {
106
+ randomPointInSphere(random, result, result_offset);
107
+ const r = this.radius;
108
+ result[result_offset] *= r;
109
+ result[result_offset + 1] *= r;
110
+ result[result_offset + 2] *= r;
111
+ }
112
+
113
+ hash() {
114
+ return computeHashFloat(this.radius);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Fast type-check marker. Lets the physics narrowphase short-circuit
120
+ * sphere-involved pairs to closed-form solvers (reading `radius`) rather than
121
+ * running GJK + EPA on a smooth surface (the EPA-degenerate case). `true` for
122
+ * both {@link SphereShape3D} and {@link UnitSphereShape3D} (via the prototype
123
+ * chain).
124
+ * @readonly
125
+ * @type {boolean}
126
+ */
127
+ SphereShape3D.prototype.isSphereShape3D = true;
@@ -1,27 +1,39 @@
1
1
  /**
2
- * Sphere with diameter of 2 (radius = 1)
2
+ * Sphere with radius 1 (diameter 2), centred at the origin.
3
+ *
4
+ * @deprecated Use {@link SphereShape3D} instead — UnitSphereShape3D is now a
5
+ * thin subclass that just pins `radius` to `1`. All geometry (`support`,
6
+ * `signed_distance_at_point`, `volume`, `surface_area`,
7
+ * `nearest_point_on_surface`, ...) is inherited from {@link SphereShape3D}.
8
+ *
9
+ * Kept only for:
10
+ * - the `UnitSphereShape3D.INSTANCE` shared singleton that scenes, colliders,
11
+ * and tests embed by reference
12
+ * - the JSON `'sphere'` type-tag round-trip in `shape_to_type` /
13
+ * `type_adapters` (an `instanceof` check, which is why we still need a
14
+ * distinct subclass rather than a constant `SphereShape3D` instance)
15
+ *
16
+ * For new code prefer:
17
+ * - `new SphereShape3D()` — also defaults to `radius = 1`
18
+ * - `SphereShape3D.from(radius)` — express by radius
3
19
  */
4
- export class UnitSphereShape3D extends AbstractShape3D {
5
- support(result: any, result_offset: any, direction_x: any, direction_y: any, direction_z: any): void;
6
- compute_bounding_box(result: any): void;
7
- nearest_point_on_surface(result: any, reference: any): void;
8
- signed_distance_gradient_at_point(result: any, point: any): number;
9
- signed_distance_at_point(point: any): number;
10
- contains_point(point: any): boolean;
11
- sample_random_point_in_volume(result: any, result_offset: any, random: any): void;
20
+ export class UnitSphereShape3D extends SphereShape3D {
12
21
  /**
13
- * Fast type-check marker (mirrors {@link Transform.prototype.isTransform}). Lets
14
- * downstream code dispatch on shape kind without instanceof / import coupling.
15
- * Use case: physics narrowphase short-circuits sphere-sphere to a closed-form
16
- * solution rather than running GJK+EPA on a smooth surface (which is the EPA
17
- * degenerate case).
22
+ * Shared singleton. Many call sites JSON deserialisation, the default
23
+ * {@link Collider} shape, tests embed this by reference rather than
24
+ * allocating a fresh sphere.
25
+ * @type {UnitSphereShape3D}
26
+ */
27
+ static INSTANCE: UnitSphereShape3D;
28
+ /**
29
+ * @deprecated Prefer dispatching on `isSphereShape3D`, which is `true` for both
30
+ * UnitSphereShape3D (inherited via prototype chain) and {@link SphereShape3D}.
31
+ * Retained so any older external code that already narrowed via this marker
32
+ * keeps working.
18
33
  * @readonly
19
34
  * @type {boolean}
20
35
  */
21
36
  readonly isUnitSphereShape3D: boolean;
22
37
  }
23
- export namespace UnitSphereShape3D {
24
- let INSTANCE: UnitSphereShape3D;
25
- }
26
- import { AbstractShape3D } from "./AbstractShape3D.js";
38
+ import { SphereShape3D } from "./SphereShape3D.js";
27
39
  //# sourceMappingURL=UnitSphereShape3D.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"UnitSphereShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/UnitSphereShape3D.js"],"names":[],"mappings":"AAKA;;GAEG;AACH;IASI,qGAQC;IAED,wCAQC;IAED,4DAYC;IAED,mEAEC;IAED,6CAMC;IAED,oCAMC;IAED,kFAEC;IASL;;;;;;;;OAQG;IACH,8BAFU,OAAO,CAE8B;CAb9C;;;;gCA5E+B,sBAAsB"}
1
+ {"version":3,"file":"UnitSphereShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/UnitSphereShape3D.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH;IAKI;;;;;OAKG;IACH,iBAFU,iBAAiB,CAEe;IAG9C;;;;;;;OAOG;IACH,8BAFU,OAAO,CAE8B;CAV9C;8BAjC6B,oBAAoB"}
@@ -1,92 +1,44 @@
1
- import { randomPointInSphere } from "../../random/randomPointInSphere.js";
2
- import { v3_length } from "../../vec3/v3_length.js";
3
- import { AbstractShape3D } from "./AbstractShape3D.js";
4
- import { compute_signed_distance_gradient_by_sampling } from "./util/compute_signed_distance_gradient_by_sampling.js";
5
-
6
- /**
7
- * Sphere with diameter of 2 (radius = 1)
8
- */
9
- export class UnitSphereShape3D extends AbstractShape3D {
10
- get volume() {
11
- return (4 / 3) * Math.PI;
12
- }
13
-
14
- get surface_area() {
15
- return Math.PI * 4;
16
- }
17
-
18
- support(result, result_offset, direction_x, direction_y, direction_z) {
19
-
20
- // since the sphere has a radius of 1, we don't have to multiply by it
21
-
22
- result[result_offset] = direction_x;
23
- result[result_offset + 1] = direction_y;
24
- result[result_offset + 2] = direction_z;
25
-
26
- }
27
-
28
- compute_bounding_box(result) {
29
- result[0] = -1;
30
- result[1] = -1;
31
- result[2] = -1;
32
-
33
- result[3] = 1;
34
- result[4] = 1;
35
- result[5] = 1;
36
- }
37
-
38
- nearest_point_on_surface(result, reference) {
39
-
40
- const r_x = reference[0];
41
- const r_y = reference[1];
42
- const r_z = reference[2];
43
-
44
- // normalize vector to 1 radius
45
- const d = 1 / v3_length(r_x, r_y, r_z);
46
-
47
- result[0] = r_x * d;
48
- result[1] = r_y * d;
49
- result[2] = r_z * d;
50
- }
51
-
52
- signed_distance_gradient_at_point(result, point) {
53
- return compute_signed_distance_gradient_by_sampling(result, this, point);
54
- }
55
-
56
- signed_distance_at_point(point) {
57
- return v3_length(
58
- point[0],
59
- point[1],
60
- point[2]
61
- ) - 1;
62
- }
63
-
64
- contains_point(point) {
65
- const x = point[0];
66
- const y = point[1];
67
- const z = point[2];
68
-
69
- return (x * x + y * y + z * z) < 1;
70
- }
71
-
72
- sample_random_point_in_volume(result, result_offset, random) {
73
- randomPointInSphere(random, result, result_offset);
74
- }
75
-
76
- hash() {
77
- return 13;
78
- }
79
- }
80
-
81
- UnitSphereShape3D.INSTANCE = new UnitSphereShape3D();
82
-
83
- /**
84
- * Fast type-check marker (mirrors {@link Transform.prototype.isTransform}). Lets
85
- * downstream code dispatch on shape kind without instanceof / import coupling.
86
- * Use case: physics narrowphase short-circuits sphere-sphere to a closed-form
87
- * solution rather than running GJK+EPA on a smooth surface (which is the EPA
88
- * degenerate case).
89
- * @readonly
90
- * @type {boolean}
91
- */
92
- UnitSphereShape3D.prototype.isUnitSphereShape3D = true;
1
+ import { SphereShape3D } from "./SphereShape3D.js";
2
+
3
+ /**
4
+ * Sphere with radius 1 (diameter 2), centred at the origin.
5
+ *
6
+ * @deprecated Use {@link SphereShape3D} instead — UnitSphereShape3D is now a
7
+ * thin subclass that just pins `radius` to `1`. All geometry (`support`,
8
+ * `signed_distance_at_point`, `volume`, `surface_area`,
9
+ * `nearest_point_on_surface`, ...) is inherited from {@link SphereShape3D}.
10
+ *
11
+ * Kept only for:
12
+ * - the `UnitSphereShape3D.INSTANCE` shared singleton that scenes, colliders,
13
+ * and tests embed by reference
14
+ * - the JSON `'sphere'` type-tag round-trip in `shape_to_type` /
15
+ * `type_adapters` (an `instanceof` check, which is why we still need a
16
+ * distinct subclass rather than a constant `SphereShape3D` instance)
17
+ *
18
+ * For new code prefer:
19
+ * - `new SphereShape3D()` — also defaults to `radius = 1`
20
+ * - `SphereShape3D.from(radius)` express by radius
21
+ */
22
+ export class UnitSphereShape3D extends SphereShape3D {
23
+ // No constructor / no method overrides on purpose. SphereShape3D's
24
+ // constructor already sets radius to 1, and every geometry method is
25
+ // correct for that case verbatim.
26
+
27
+ /**
28
+ * Shared singleton. Many call sites — JSON deserialisation, the default
29
+ * {@link Collider} shape, tests — embed this by reference rather than
30
+ * allocating a fresh sphere.
31
+ * @type {UnitSphereShape3D}
32
+ */
33
+ static INSTANCE = new UnitSphereShape3D();
34
+ }
35
+
36
+ /**
37
+ * @deprecated Prefer dispatching on `isSphereShape3D`, which is `true` for both
38
+ * UnitSphereShape3D (inherited via prototype chain) and {@link SphereShape3D}.
39
+ * Retained so any older external code that already narrowed via this marker
40
+ * keeps working.
41
+ * @readonly
42
+ * @type {boolean}
43
+ */
44
+ UnitSphereShape3D.prototype.isUnitSphereShape3D = true;
@@ -1 +1 @@
1
- {"version":3,"file":"shape_to_type.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/geom/3d/shape/json/shape_to_type.js"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,uDAFa,MAAM,CAgBlB"}
1
+ {"version":3,"file":"shape_to_type.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/geom/3d/shape/json/shape_to_type.js"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,uDAFa,MAAM,CAkBlB"}
@@ -1,8 +1,8 @@
1
1
  import { CapsuleShape3D } from "../CapsuleShape3D.js";
2
+ import { SphereShape3D } from "../SphereShape3D.js";
2
3
  import { TransformedShape3D } from "../TransformedShape3D.js";
3
4
  import { UnionShape3D } from "../UnionShape3D.js";
4
5
  import { UnitCubeShape3D } from "../UnitCubeShape3D.js";
5
- import { UnitSphereShape3D } from "../UnitSphereShape3D.js";
6
6
 
7
7
  /**
8
8
  *
@@ -14,7 +14,9 @@ export function shape_to_type(shape) {
14
14
  return 'union';
15
15
  } else if (shape instanceof UnitCubeShape3D) {
16
16
  return 'cube';
17
- } else if (shape instanceof UnitSphereShape3D) {
17
+ } else if (shape instanceof SphereShape3D) {
18
+ // Covers UnitSphereShape3D (radius 1) and SphereShape3D (arbitrary);
19
+ // the 'sphere' adapter carries the radius.
18
20
  return 'sphere';
19
21
  } else if (shape instanceof TransformedShape3D) {
20
22
  return 'transform';
@@ -4,8 +4,17 @@ export namespace type_adapters {
4
4
  function write(): {};
5
5
  }
6
6
  namespace sphere {
7
- function read(): UnitSphereShape3D;
8
- function write(): {};
7
+ function read({ radius }?: {
8
+ radius: any;
9
+ }): SphereShape3D;
10
+ /**
11
+ * @param {SphereShape3D} object
12
+ */
13
+ function write(object: SphereShape3D): {
14
+ radius?: undefined;
15
+ } | {
16
+ radius: number;
17
+ };
9
18
  }
10
19
  namespace capsule {
11
20
  function read({ radius, height }: {
@@ -49,7 +58,7 @@ export namespace type_adapters {
49
58
  }
50
59
  }
51
60
  import { UnitCubeShape3D } from "../UnitCubeShape3D.js";
52
- import { UnitSphereShape3D } from "../UnitSphereShape3D.js";
61
+ import { SphereShape3D } from "../SphereShape3D.js";
53
62
  import { CapsuleShape3D } from "../CapsuleShape3D.js";
54
63
  import { TransformedShape3D } from "../TransformedShape3D.js";
55
64
  import { UnionShape3D } from "../UnionShape3D.js";
@@ -1 +1 @@
1
- {"version":3,"file":"type_adapters.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/geom/3d/shape/json/type_adapters.js"],"names":[],"mappings":";;QAUQ,iCAEC;QACD,qBAEC;;;QAGD,mCAEC;QACD,qBAEC;;;QAGD;;;2BAEC;QACD;;;WAGG;QACH;;;UAKC;;;QAGD;;;+BAIC;QACD;;;WAGG;QACH;;;UAKC;;;QAGD;;yBAIC;QACD;;;WAGG;QACH;;UAIC;;;gCApEuB,uBAAuB;kCACrB,yBAAyB;+BAJ5B,sBAAsB;mCAClB,0BAA0B;6BAChC,oBAAoB"}
1
+ {"version":3,"file":"type_adapters.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/geom/3d/shape/json/type_adapters.js"],"names":[],"mappings":";;QAWQ,iCAEC;QACD,qBAEC;;;QAGD;;0BAQC;QACD;;WAEG;QACH;;;;UAIC;;;QAGD;;;2BAEC;QACD;;;WAGG;QACH;;;UAKC;;;QAGD;;;+BAIC;QACD;;;WAGG;QACH;;;UAKC;;;QAGD;;yBAIC;QACD;;;WAGG;QACH;;UAIC;;;gCA/EuB,uBAAuB;8BAHzB,qBAAqB;+BADpB,sBAAsB;mCAElB,0BAA0B;6BAChC,oBAAoB"}
@@ -1,4 +1,5 @@
1
1
  import { CapsuleShape3D } from "../CapsuleShape3D.js";
2
+ import { SphereShape3D } from "../SphereShape3D.js";
2
3
  import { TransformedShape3D } from "../TransformedShape3D.js";
3
4
  import { UnionShape3D } from "../UnionShape3D.js";
4
5
  import { UnitCubeShape3D } from "../UnitCubeShape3D.js";
@@ -16,11 +17,22 @@ export const type_adapters = {
16
17
  }
17
18
  },
18
19
  'sphere': {
19
- read() {
20
- return UnitSphereShape3D.INSTANCE;
20
+ read({ radius } = {}) {
21
+ // Backward-compatible: legacy 'sphere' entries have no radius and
22
+ // map to the unit-sphere singleton. A radius of 1 is also treated
23
+ // as the unit sphere so re-saved unit spheres keep sharing INSTANCE.
24
+ if (radius === undefined || radius === 1) {
25
+ return UnitSphereShape3D.INSTANCE;
26
+ }
27
+ return SphereShape3D.from(radius);
21
28
  },
22
- write() {
23
- return {};
29
+ /**
30
+ * @param {SphereShape3D} object
31
+ */
32
+ write(object) {
33
+ // Omit radius for the unit case to keep the serialized form
34
+ // byte-identical to the pre-SphereShape3D 'sphere' tag.
35
+ return object.radius === 1 ? {} : { radius: object.radius };
24
36
  }
25
37
  },
26
38
  'capsule': {
@@ -8,10 +8,10 @@ import { AttachmentSockets } from "../../../../../engine/ecs/sockets/AttachmentS
8
8
  import { Transform } from "../../../../../engine/ecs/transform/Transform.js";
9
9
  import { DrawMode } from "../../../../../engine/graphics/ecs/mesh-v2/DrawMode.js";
10
10
  import { ShadedGeometry } from "../../../../../engine/graphics/ecs/mesh-v2/ShadedGeometry.js";
11
+ import { SphereShape3D } from "../SphereShape3D.js";
11
12
  import { TransformedShape3D } from "../TransformedShape3D.js";
12
13
  import { UnionShape3D } from "../UnionShape3D.js";
13
14
  import { UnitCubeShape3D } from "../UnitCubeShape3D.js";
14
- import { UnitSphereShape3D } from "../UnitSphereShape3D.js";
15
15
 
16
16
  const g_unit_sphere = makeHelperSphereGeometry(0.5, 32);
17
17
  const g_unit_cube = makeHelperBoxGeometry(1, 1, 1);
@@ -149,7 +149,7 @@ export function shape_to_visual_entity(shape, ecd) {
149
149
  return composite_shape_to_entity(shape, ecd);
150
150
  } else if (shape instanceof UnitCubeShape3D) {
151
151
  return cube_shape_to_entity(shape, ecd);
152
- } else if (shape instanceof UnitSphereShape3D) {
152
+ } else if (shape instanceof SphereShape3D) {
153
153
  return sphere_shape_to_entity(shape, ecd);
154
154
  } else if (shape instanceof TransformedShape3D) {
155
155
  return transformed_shape_to_entity(shape, ecd);
@@ -0,0 +1,255 @@
1
+ # Collision handling — construction plan
2
+
3
+ Companion to DESIGN.md (base controller) and TODO.md. This is the
4
+ roadmap for taking the controller's collision response from the current
5
+ 2.5D split to a unified, slope-aware, anti-tunnelling mover modelled on
6
+ the Quake → Source `TryPlayerMove` / `CategorizePosition` lineage —
7
+ using the now-accurate `shapeCast` / `raycast` the physics engine
8
+ gained over the P1–P6 narrowphase work.
9
+
10
+ Every phase is **guard-test-first**: write the failing spec that pins
11
+ the behaviour, then implement until green, then run the full
12
+ first-person suite. Phases are ordered so the controller stays shippable
13
+ and tested after each one — no flag day.
14
+
15
+ ---
16
+
17
+ ## 1. Goal & non-goals
18
+
19
+ **Goal.** A single swept move-and-slide that handles walls, floors,
20
+ ceilings, ramps, and getting unstuck — with true surface normals — so
21
+ abilities (Slide, WallRun, Mantle) inherit correct collision for free
22
+ by routing their motion through it.
23
+
24
+ **Non-goals.**
25
+ - Bunny-hop / strafe-jump emergent acceleration. We deliberately
26
+ *preserve* momentum (Mirror's Edge / modern-CoD model), not *amplify*
27
+ it (Quake / Titanfall model). See the momentum note in DESIGN.md.
28
+ - A full dynamics solver for the player. The player stays a
29
+ `KinematicPosition` body; the controller owns the Transform.
30
+ - Replacing the abilities' bespoke motion models — only giving them a
31
+ correct shared mover to call.
32
+
33
+ ---
34
+
35
+ ## 2. Current state (one paragraph)
36
+
37
+ `_integrateVerticalAndResolveGround` does a **2.5D split**: gravity →
38
+ horizontal-only swept slide (`_moveAndSlide`, 4-iteration collide-and-
39
+ slide, `SKIN = 0.005`, `CAST_STEP_HEIGHT = 0.05`) → a *direct* vertical
40
+ position add → a downward-ray ground resolver that snaps a scalar Y.
41
+ The slide projects against true narrowphase normals (good), but: the
42
+ ground resolver discards the surface normal (no slopes), vertical motion
43
+ isn't swept (tunnels through thin floors, no ceiling detection), corners
44
+ re-project planes independently (no crease/dead-stop), and there's no
45
+ depenetration. Full analysis in the review that preceded this doc.
46
+
47
+ ---
48
+
49
+ ## 3. Physics capabilities we can now lean on
50
+
51
+ The P1–P6 narrowphase + query work made the two primitives we depend on
52
+ narrowphase-exact. This plan is only feasible because of it.
53
+
54
+ | Capability | Status | Source |
55
+ |---|---|---|
56
+ | `raycast` exact surface **normal** (sphere/box/capsule/mesh/heightmap) | ✅ landed | refine_ray_hit P1–P3 (`6f931b4`, `5cc2e3e`, `1a2a602`, `4c2292c`) |
57
+ | `shapeCast` true contact **normal at TOI** via MPR (not broadphase −dir) | ✅ landed | MPR normal-recovery in `shape_cast.js` |
58
+ | `shapeCast` **start-in-contact** → returns `t=0` + valid normal | ✅ landed | `shape_cast.js:195`, normal probe `:350` |
59
+ | `shapeCast` analytic slab-narrowing (long-sweep accuracy/perf) | ✅ landed | `shape_cast.js:203` |
60
+ | EPA adaptive tolerance (no silent sub-mm failure) | ✅ landed | P2.2 (`8ff15a6`) |
61
+ | MPR fallback when EPA doesn't converge | ✅ landed | P3.1 (`419b94a`) |
62
+ | box-box edge-edge closest-pair (not centre-midpoint) | ✅ landed | P3.2 (`419b94a`) |
63
+ | capsule-vs-triangle closed-form multi-point manifold | ✅ landed | P1.1c (`eaa75fa`) |
64
+ | GJK separating-axis cache (cheap warm casts) | ✅ landed | P2.1 (`60ca759`) |
65
+ | `overlap(shape,pos,rot,out,off,filter)` → body-id list | ✅ landed | `overlap_shape.js` |
66
+ | `compute_penetration(...)` → depth + B→A direction | ⚠️ **internal-only** | `compute_penetration.js` — not on PhysicsSystem |
67
+
68
+ Query contract notes the plan relies on:
69
+ - `shapeCast` `result.t` is a **world distance** when the ray direction
70
+ is unit-length and `tMax = sweepLength` (how `_moveAndSlide` already
71
+ uses it). New casts must keep that convention.
72
+ - `result.position` is the **swept shape centre**, not the surface
73
+ contact point. We never need the contact point in this plan; flagged
74
+ in case edge/foot placement ever does.
75
+ - `result.normal` is the **target's outward** surface normal.
76
+
77
+ ---
78
+
79
+ ## 4. Target architecture
80
+
81
+ Replace the 2.5D split with a **unified swept solve + ground
82
+ categorize**, the Source pattern:
83
+
84
+ ```
85
+ move(dt):
86
+ applyGravityToVelocity() # vy -= g·dt
87
+ delta = velocity · dt # full 3D, gravity folded in
88
+ collideAndSlide(delta) # swept, all axes, crease-aware
89
+ categorizeGround() # short down-probe → grounded + normal
90
+ if grounded and walkable: stayOnGround() # snap to surface, zero vy
91
+ ```
92
+
93
+ Three pieces:
94
+
95
+ 1. **`collideAndSlide(delta)`** — the existing 4-iteration loop,
96
+ generalised to 3D and made crease-aware (two-plane seam projection,
97
+ three-plane dead-stop). Gravity is part of `delta`, so floors and
98
+ ceilings are just contact planes — anti-tunnelling falls out.
99
+
100
+ 2. **`categorizeGround()`** — a short **downward `shapeCast`** (the
101
+ capsule swept down by `SKIN + groundProbeBand`, e.g. ~6 cm). Returns
102
+ `{grounded, surfaceY, normal}`. Replaces the scalar-Y resolver as the
103
+ authoritative ground source when physics is present; the host
104
+ resolver / flat-ground stays as the no-physics fallback, behind one
105
+ `_probeGround()` function so callers never branch (uniform flow).
106
+
107
+ 3. **`stayOnGround()`** — when categorize says grounded on a walkable
108
+ plane, snap `position.y` to `surfaceY` and zero `vy`. This is what
109
+ kills the SKIN-vs-snap **bounce** (the bug we hit earlier): grounded
110
+ is a *band test + active snap*, not a strict `y <= testY`
111
+ inequality, so landing at `floor + SKIN` doesn't re-flag airborne.
112
+
113
+ Walkability gate: `groundNormal.y >= MIN_WALK_NORMAL` (≈0.7, ~45°). On a
114
+ steeper face the player isn't grounded — they slide, because the slope
115
+ is just another clip plane in `collideAndSlide` and gravity carries them
116
+ down it.
117
+
118
+ ---
119
+
120
+ ## 5. Phased plan
121
+
122
+ ### Phase 1 — Ground normal + slope policy
123
+ *Review gap #1. Highest feel/realism leverage; lowest risk.*
124
+
125
+ - Add `_probeGround(runtime, bodyTransform) → {grounded, surfaceY, normal}`.
126
+ Physics path: downward `shapeCast` of the capsule, read `hit.normal`.
127
+ No-physics path: existing resolver / flat-ground, normal = `(0,1,0)`.
128
+ - Write `state.groundNormal`. Add `MIN_WALK_NORMAL` to config.
129
+ - Reproject grounded horizontal velocity onto the ground plane
130
+ (`v -= (v·n)·n`) so ramps don't launch (downhill) or clip (uphill).
131
+ - Steep face → not grounded → slides under gravity.
132
+
133
+ **Guard tests** (new `GroundSlope.spec.js`): walk up a 30° ramp keeps
134
+ speed and stays grounded; stand on shallow slope = grounded with correct
135
+ normal; 60° face = not grounded, player slides downhill, not glued.
136
+
137
+ **Risk:** low. Self-contained; resolver fallback unchanged for existing
138
+ tests.
139
+
140
+ ### Phase 2 — Crease / corner handling
141
+ *Review gap #3. Cheap correctness win in the existing loop.*
142
+
143
+ - Track planes hit within one `_moveAndSlide` call. On a second plane
144
+ that re-violates the first, project the residual onto the seam
145
+ `normalize(n₁ × n₂)`. On a third plane in one frame, zero the residual
146
+ (and the into-wall velocity) — Quake's dead-stop.
147
+
148
+ **Guard tests** (extend `MoveAndSlide.spec.js`): oblique (non-axis)
149
+ inside corner stops cleanly with no residual leak and no tick-to-tick
150
+ chatter; narrow wedge dead-stops instead of squirting through.
151
+
152
+ **Risk:** low–medium. Pure addition to the slide loop; existing
153
+ single-plane tests must stay green.
154
+
155
+ ### Phase 3 — Unify vertical into the swept solve
156
+ *Review gap #2 (anti-tunnel). The structural change; do it after 1 & 2
157
+ so slope + crease are already trustworthy.*
158
+
159
+ - Fold gravity-driven vertical into the swept `delta`; remove the direct
160
+ `position.y += vy·dt` add.
161
+ - Replace the strict-inequality resolver snap with
162
+ `categorizeGround()` + `stayOnGround()` (§4).
163
+ - Route Slide / WallRun vertical through the same mover (TODO's "ability
164
+ motion routing" — they stop hand-integrating gravity).
165
+
166
+ **Guard tests** (new `VerticalSweep.spec.js`): drop from height onto a
167
+ thin physics slab — no tunnelling, lands on top; jump straight into a
168
+ ceiling — stops, doesn't pass through; **the stationary-on-floor case
169
+ stays at y≈0 across 120 ticks** (generalise the earlier no-bounce repro,
170
+ now without an escape pass to mask it).
171
+
172
+ **Risk:** medium–high. Changes the contract `_integrate…` exposes to
173
+ abilities; they're updated in the same phase. This is the phase most
174
+ likely to surface a physics gap (see §6).
175
+
176
+ ### Phase 4 — Depenetration safety net
177
+ *Review gap #4. Minimal, not the rolled-back ring-buffer escape.*
178
+
179
+ - One bounded nudge: if the capsule starts a tick overlapping (detect
180
+ via `shapeCast` `t=0`, or `overlap()`), push out along the contact
181
+ normal by `depth + SKIN`, capped at ~4 iterations. No ring buffer, no
182
+ teleport, no rescue signal — just don't stay solid.
183
+ - **Blocked on §6 requirement #1** for a clean depth. Without it, the
184
+ fallback is a fixed-step nudge along the normal (no exact depth),
185
+ which is cruder but unblocks the phase.
186
+
187
+ **Guard tests** (new `Depenetration.spec.js`): spawn the player
188
+ straddling a box surface → within a few ticks they're outside; a body
189
+ that spawns on top of a standing player → player ends up clear; deep
190
+ burial terminates (no infinite loop), doesn't teleport.
191
+
192
+ **Risk:** low if requirement #1 lands; medium otherwise (cruder nudge
193
+ needs more tuning).
194
+
195
+ ### Phase 5 — Real step-up *(optional / deferrable)*
196
+ *Review gap #5.*
197
+
198
+ - Source-style `raise → move → trace-down` for stairs above the 5 cm
199
+ implicit step, OR keep stairs as the Mantle ability's job and document
200
+ the curb ceiling. Likely **defer** — Mantle already covers ledges; a
201
+ decision, not necessarily code.
202
+
203
+ ---
204
+
205
+ ## 6. Unsatisfied physics requirements
206
+
207
+ Things I'd want from the physics engine to build §5 cleanly. None block
208
+ Phases 1–3; #1 shapes Phase 4.
209
+
210
+ 1. **Public penetration/contact query — `PhysicsSystem.computePenetration`
211
+ (or `contact`).** `compute_penetration(out_dir, shapeA, posA, rotA,
212
+ shapeB, posB, rotB) → depth` already exists and is exactly right, but
213
+ it's an **internal narrowphase utility, not exposed** on
214
+ `PhysicsSystem`. Phase 4 depenetration wants `{normal, depth}` for the
215
+ player capsule vs each overlapping body. Today I can get the *fact* of
216
+ overlap (`overlap()` → ids) and a *normal* (`shapeCast` `t=0`), but
217
+ **not the depth** — `shapeCast` discards it at `t=0`. Wiring this
218
+ through is small (the function is written and tested) and is the
219
+ single highest-value API add for the controller. **Priority: HIGH.**
220
+
221
+ 2. **`overlap()` that can optionally emit per-body `{normal, depth}`,
222
+ not just ids.** Subsumed by #1 if I call `computePenetration` per
223
+ returned id — acceptable. Only worth doing natively if profiling
224
+ shows the per-body re-query matters. **Priority: LOW** (covered by #1).
225
+
226
+ 3. **Contact *point* (not just swept-centre) from `shapeCast`.** Not
227
+ needed for this plan, but any future foot-placement / ledge-edge or
228
+ multi-point ground balancing wants the world contact point.
229
+ `result.position` is the swept centre today. **Priority: LOW.**
230
+
231
+ 4. **Multi-contact ground manifold from one query.** `shapeCast` returns
232
+ the single closest-`t` contact. A capsule straddling the seam between
233
+ two boxes gets one normal, not both — fine for v1 grounded snap, a
234
+ limitation for precise edge-balancing. **Priority: LOW.**
235
+
236
+ If only one thing gets built: **#1**. It turns Phase 4 from "tune a
237
+ blind nudge" into "push out by the exact reported depth," and it's
238
+ mostly plumbing over code that already exists and is tested.
239
+
240
+ ---
241
+
242
+ ## 7. Constants (ours ← reference lineage)
243
+
244
+ | Constant | Plan value | Reference | Note |
245
+ |---|---|---|---|
246
+ | slide iterations | 4 | Quake `numbumps` 4 | unchanged |
247
+ | `SKIN` | 0.005 m | Fauerby `veryCloseDistance` ~0.005 | unchanged |
248
+ | `MIN_WALK_NORMAL` | ~0.7 (≈45°) | Quake3 `MIN_WALK_NORMAL` 0.7 / Source `normal.z ≥ 0.7` | **new, Phase 1** |
249
+ | ground-probe band | ~0.06 m | Source `StayOnGround` ~2 u down-snap | **new, Phase 3** |
250
+ | crease dead-stop | zero on 3rd plane | Quake `SV_FlyMove` | **new, Phase 2** |
251
+ | step height | 0.05 m (implicit) | Source `sv_stepsize` 18 u (~0.34 m) | Phase 5 decision |
252
+
253
+ Overbounce: we keep the clip at effective `1.0` gated on into-wall
254
+ (`v·n < 0`), relying on `SKIN` for clearance, rather than Quake3's
255
+ `OVERCLIP 1.001` nudge. No change planned — equivalent in practice.
@@ -1 +1 @@
1
- {"version":3,"file":"AmbientOcclusionPostProcessEffect.d.ts","sourceRoot":"","sources":["../../../../../../../../src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js"],"names":[],"mappings":"AAoBA;IAIQ,WAA0C;IAI1C,2BAQE;IAyBF;;;;OAIG;IACH,sBAA0B;IAE1B;;;;OAIG;IACH,2BAA6B;IAG7B;;;;OAIG;IACH,yBAA4B;IAE5B;;;;OAIG;IACH,wBAA2B;IAE3B,8CAUE;IAEF;;;;OAIG;IACH,uBAAwB;IAExB;;;;;OAKG;IACH,2BASE;IAKF;;;;OAIG;IACH,0BAAwD;IAK5D;;;OAGG;IACH,2BAEC;IAED;;;OAGG;IACH,wBAEC;IAED;;;;OAIG;IACH,oBAeC;IAED,iBAEC;IAED,2BAkCC;IAED,uBASC;IAED;;;OAGG;IACH,iBAwBC;IAED;;;OAGG;IACH,4BAFW,OAAO,QAiCjB;IAED;;;;OAIG;IACH,gCASC;IAED;;;;OAIG;IACH,mCAUC;IAGD,oCAcC;IAED,wBA2CC;IAED,yBAiBC;CAEJ;6BA/Y4B,uCAAuC;+BAD7D,OAAO;6CAAP,OAAO"}
1
+ {"version":3,"file":"AmbientOcclusionPostProcessEffect.d.ts","sourceRoot":"","sources":["../../../../../../../../src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js"],"names":[],"mappings":"AAoBA;IAIQ,WAA0C;IAI1C,2BAQE;IAyBF;;;;OAIG;IACH,sBAA0B;IAE1B;;;;OAIG;IACH,2BAA6B;IAG7B;;;;OAIG;IACH,yBAA4B;IAE5B;;;;OAIG;IACH,wBAA2B;IAE3B,8CAUE;IAEF;;;;OAIG;IACH,uBAAwB;IAExB;;;;;OAKG;IACH,2BASE;IAKF;;;;OAIG;IACH,0BAAwD;IAK5D;;;OAGG;IACH,2BAEC;IAED;;;OAGG;IACH,wBAEC;IAED;;;;OAIG;IACH,oBAeC;IAED,iBAEC;IAED,2BAoCC;IAED,uBASC;IAED;;;OAGG;IACH,iBAwBC;IAED;;;OAGG;IACH,4BAFW,OAAO,QAiCjB;IAED;;;;OAIG;IACH,gCASC;IAED;;;;OAIG;IACH,mCAUC;IAGD,oCAcC;IAED,wBA2CC;IAED,yBAiBC;CAEJ;6BAjZ4B,uCAAuC;+BAD7D,OAAO;6CAAP,OAAO"}
@@ -187,7 +187,9 @@ export class AmbientOcclusionPostProcessEffect extends EnginePlugin {
187
187
 
188
188
  const camera = this.__render_camera;
189
189
 
190
- uniforms.size.value.set(this.__render_target.width, this.__render_target.height);
190
+ // AO renders at half-res but samples the full-res depth buffer, so `size` is the full (depth)
191
+ // resolution -- all texel math then lands on depth texel centres rather than texel boundaries
192
+ uniforms.size.value.set(this.__composit_layer.renderTarget.width, this.__composit_layer.renderTarget.height);
191
193
 
192
194
  // setup camera
193
195
  const near = camera.near;
@@ -217,8 +217,8 @@ const SAOShader = {
217
217
  // radius over which an occluder's contribution fades to zero at the edge. Must be in [0, 1]. (GTAO)
218
218
  const float GTAO_EFFECT_FALLOFF_RANGE = 0.615;
219
219
 
220
- float getOccludedFraction( const in vec3 centerViewPosition ) {
221
- vec3 centerViewNormal = getViewNormal( centerViewPosition, vUv );
220
+ float getOccludedFraction( const in vec3 centerViewPosition, const in vec2 centerUv ) {
221
+ vec3 centerViewNormal = getViewNormal( centerViewPosition, centerUv );
222
222
 
223
223
  // sample origin lifted slightly off the surface along the normal (see SURFACE_BIAS)
224
224
  vec3 sampleOrigin = centerViewPosition + centerViewNormal * ( kernelRadius * SURFACE_BIAS );
@@ -267,7 +267,7 @@ const SAOShader = {
267
267
  vec2 sampleUv = ( clip.xy / clip.w ) * 0.5 + 0.5;
268
268
 
269
269
  // screen-space offset from the centre, in pixels
270
- vec2 offsetPixels = ( sampleUv - vUv ) * size;
270
+ vec2 offsetPixels = ( sampleUv - centerUv ) * size;
271
271
 
272
272
  // push forward along the screen direction by the self-occlusion margin (extends the
273
273
  // screen-space radius), then snap to the nearest pixel centre so the depth fetch is
@@ -276,7 +276,7 @@ const SAOShader = {
276
276
  offsetPixels += offsetPixels / max( offsetLength, EPSILON ) * SELF_OCCLUSION_MARGIN_PIXELS;
277
277
  offsetPixels = round( offsetPixels );
278
278
 
279
- sampleUv = vUv + offsetPixels / size;
279
+ sampleUv = centerUv + offsetPixels / size;
280
280
 
281
281
  // samples outside the screen have no depth to test against
282
282
  if ( sampleUv.x < 0.0 || sampleUv.x > 1.0 || sampleUv.y < 0.0 || sampleUv.y > 1.0 ) {
@@ -316,15 +316,20 @@ const SAOShader = {
316
316
  }
317
317
 
318
318
  void main() {
319
- float centerDepth = getDepth( vUv );
319
+ // snap to the centre of the full-res depth texel we sample. The half-res pass otherwise
320
+ // lands on texel boundaries, where NEAREST biases consistently to one side and the
321
+ // reconstructed position disagrees with the sampled depth -> asymmetric edge self-occlusion.
322
+ vec2 centerUv = ( floor( vUv * size ) + 0.5 ) / size;
323
+
324
+ float centerDepth = getDepth( centerUv );
320
325
  if( centerDepth >= ( 1.0 - EPSILON ) ) {
321
326
  discard;
322
327
  }
323
328
 
324
329
  float centerViewZ = getViewZ( centerDepth );
325
- vec3 viewPosition = getViewPosition( vUv, centerDepth, centerViewZ );
330
+ vec3 viewPosition = getViewPosition( centerUv, centerDepth, centerViewZ );
326
331
 
327
- float occlusion = getOccludedFraction( viewPosition );
332
+ float occlusion = getOccludedFraction( viewPosition, centerUv );
328
333
 
329
334
  // intensity is an artistic power: 1.0 leaves the physical estimate untouched, higher
330
335
  // darkens occluded areas while leaving fully-lit areas at 1.0
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Binary serialization for {@link Joint}.
3
+ *
4
+ * Writes the configurable / persistent fields: the two anchor entities, the
5
+ * local anchor points and basis frames, and the per-DOF mode + limit / spring /
6
+ * motor config. Transient runtime state owned by the {@link PhysicsSystem} —
7
+ * the warm-start accumulator (`dofImpulse`) and the resolved `_bodyIdA` /
8
+ * `_bodyIdB` / `_jointId` — is deliberately excluded; it is recomputed on
9
+ * `link_joint`, and serialising it would tie the stream to a particular world
10
+ * instance.
11
+ */
12
+ export class JointSerializationAdapter extends BinaryClassSerializationAdapter<any> {
13
+ constructor();
14
+ klass: typeof Joint;
15
+ version: number;
16
+ /**
17
+ * @param {BinaryBuffer} buffer
18
+ * @param {Joint} value
19
+ */
20
+ serialize(buffer: BinaryBuffer, value: Joint): void;
21
+ /**
22
+ * @param {BinaryBuffer} buffer
23
+ * @param {Joint} value
24
+ */
25
+ deserialize(buffer: BinaryBuffer, value: Joint): void;
26
+ }
27
+ import { BinaryClassSerializationAdapter } from "../../ecs/storage/binary/BinaryClassSerializationAdapter.js";
28
+ import { Joint } from "./Joint.js";
29
+ //# sourceMappingURL=JointSerializationAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JointSerializationAdapter.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/JointSerializationAdapter.js"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH;;IAEI,oBAAc;IACd,gBAAY;IAEZ;;;OAGG;IACH,uCAFW,KAAK,QAwBf;IAED;;;OAGG;IACH,yCAFW,KAAK,QAqBf;CACJ;gDAvE+C,6DAA6D;sBACvF,YAAY"}
@@ -0,0 +1,72 @@
1
+ import { BinaryClassSerializationAdapter } from "../../ecs/storage/binary/BinaryClassSerializationAdapter.js";
2
+ import { Joint } from "./Joint.js";
3
+
4
+ /**
5
+ * Binary serialization for {@link Joint}.
6
+ *
7
+ * Writes the configurable / persistent fields: the two anchor entities, the
8
+ * local anchor points and basis frames, and the per-DOF mode + limit / spring /
9
+ * motor config. Transient runtime state owned by the {@link PhysicsSystem} —
10
+ * the warm-start accumulator (`dofImpulse`) and the resolved `_bodyIdA` /
11
+ * `_bodyIdB` / `_jointId` — is deliberately excluded; it is recomputed on
12
+ * `link_joint`, and serialising it would tie the stream to a particular world
13
+ * instance.
14
+ */
15
+ export class JointSerializationAdapter extends BinaryClassSerializationAdapter {
16
+
17
+ klass = Joint;
18
+ version = 0;
19
+
20
+ /**
21
+ * @param {BinaryBuffer} buffer
22
+ * @param {Joint} value
23
+ */
24
+ serialize(buffer, value) {
25
+ // Anchor entities — entityB may be JOINT_WORLD (−1), so signed.
26
+ buffer.writeInt32(value.entityA);
27
+ buffer.writeInt32(value.entityB);
28
+
29
+ const la = value.localAnchorA, lb = value.localAnchorB;
30
+ buffer.writeFloat64(la.x); buffer.writeFloat64(la.y); buffer.writeFloat64(la.z);
31
+ buffer.writeFloat64(lb.x); buffer.writeFloat64(lb.y); buffer.writeFloat64(lb.z);
32
+
33
+ const ba = value.localBasisA, bb = value.localBasisB;
34
+ buffer.writeFloat64(ba[0]); buffer.writeFloat64(ba[1]); buffer.writeFloat64(ba[2]); buffer.writeFloat64(ba[3]);
35
+ buffer.writeFloat64(bb[0]); buffer.writeFloat64(bb[1]); buffer.writeFloat64(bb[2]); buffer.writeFloat64(bb[3]);
36
+
37
+ for (let i = 0; i < 6; i++) buffer.writeUint8(value.dofMode[i]);
38
+ for (let i = 0; i < 6; i++) buffer.writeFloat64(value.dofLowerLimit[i]);
39
+ for (let i = 0; i < 6; i++) buffer.writeFloat64(value.dofUpperLimit[i]);
40
+ for (let i = 0; i < 6; i++) buffer.writeFloat64(value.dofStiffness[i]);
41
+ for (let i = 0; i < 6; i++) buffer.writeFloat64(value.dofDamping[i]);
42
+ for (let i = 0; i < 6; i++) buffer.writeFloat64(value.dofMotorTargetVelocity[i]);
43
+ for (let i = 0; i < 6; i++) buffer.writeFloat64(value.dofMotorMaxForce[i]);
44
+
45
+ buffer.writeUint8(value.swingTwist ? 1 : 0);
46
+ }
47
+
48
+ /**
49
+ * @param {BinaryBuffer} buffer
50
+ * @param {Joint} value
51
+ */
52
+ deserialize(buffer, value) {
53
+ value.entityA = buffer.readInt32();
54
+ value.entityB = buffer.readInt32();
55
+
56
+ value.localAnchorA.set(buffer.readFloat64(), buffer.readFloat64(), buffer.readFloat64());
57
+ value.localAnchorB.set(buffer.readFloat64(), buffer.readFloat64(), buffer.readFloat64());
58
+
59
+ value.localBasisA.set(buffer.readFloat64(), buffer.readFloat64(), buffer.readFloat64(), buffer.readFloat64());
60
+ value.localBasisB.set(buffer.readFloat64(), buffer.readFloat64(), buffer.readFloat64(), buffer.readFloat64());
61
+
62
+ for (let i = 0; i < 6; i++) value.dofMode[i] = buffer.readUint8();
63
+ for (let i = 0; i < 6; i++) value.dofLowerLimit[i] = buffer.readFloat64();
64
+ for (let i = 0; i < 6; i++) value.dofUpperLimit[i] = buffer.readFloat64();
65
+ for (let i = 0; i < 6; i++) value.dofStiffness[i] = buffer.readFloat64();
66
+ for (let i = 0; i < 6; i++) value.dofDamping[i] = buffer.readFloat64();
67
+ for (let i = 0; i < 6; i++) value.dofMotorTargetVelocity[i] = buffer.readFloat64();
68
+ for (let i = 0; i < 6; i++) value.dofMotorMaxForce[i] = buffer.readFloat64();
69
+
70
+ value.swingTwist = buffer.readUint8() !== 0;
71
+ }
72
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"AA6uCA;;;;;;;;;;;GAWG;AACH,uFALW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,CAAC,QAyJlE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,uEAJW,MAAM,UACN,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,UACjD,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,QAiD3D"}
1
+ {"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"AAovCA;;;;;;;;;;;GAWG;AACH,uFALW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,CAAC,QAyJlE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,uEAJW,MAAM,UACN,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,UACjD,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,QAiD3D"}
@@ -358,8 +358,10 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
358
358
  const shapeA = colA.shape;
359
359
  const shapeB = colB.shape;
360
360
 
361
- const isSphereA = shapeA.isUnitSphereShape3D === true;
362
- const isSphereB = shapeB.isUnitSphereShape3D === true;
361
+ // isSphereShape3D covers both UnitSphereShape3D (fixed radius 1) and
362
+ // SphereShape3D (arbitrary radius). Both expose `radius`.
363
+ const isSphereA = shapeA.isSphereShape3D === true;
364
+ const isSphereB = shapeB.isSphereShape3D === true;
363
365
  // isBoxShape3D covers both UnitCubeShape3D (fixed 0.5) and BoxShape3D
364
366
  // (arbitrary half-extents). Both expose `half_extents` as a Vector3.
365
367
  const isBoxA = shapeA.isBoxShape3D === true;
@@ -369,12 +371,13 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
369
371
 
370
372
  // sphere-sphere
371
373
  if (isSphereA && isSphereB) {
374
+ const ra = shapeA.radius, rb = shapeB.radius;
372
375
 
373
376
  const ok = sphere_sphere_contact(
374
377
  sphere_result,
375
378
  trA.position.x, trA.position.y, trA.position.z,
376
379
  trB.position.x, trB.position.y, trB.position.z,
377
- 1, 1
380
+ ra, rb
378
381
  );
379
382
 
380
383
  if (!ok) return count;
@@ -383,23 +386,25 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
383
386
 
384
387
  // Sphere-sphere produces exactly one contact per pair; fid = 1
385
388
  // identifies it as a real feature (distinguishes from "no info" = 0)
386
- // and is trivially stable across frames.
389
+ // and is trivially stable across frames. Witnesses are the surface
390
+ // points along the (unit) normal, scaled by each sphere's radius.
387
391
  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,
392
+ trA.position.x - nx * ra, trA.position.y - ny * ra, trA.position.z - nz * ra,
393
+ trB.position.x + nx * rb, trB.position.y + ny * rb, trB.position.z + nz * rb,
390
394
  nx, ny, nz, sphere_result[3], 1);
391
395
  }
392
396
 
393
397
  // sphere ↔ box
394
398
  if ((isSphereA && isBoxB) || (isBoxA && isSphereB)) {
395
- const sphereTr = isSphereA ? trA : trB;
399
+ const sphereTr = isSphereA ? trA : trB;
400
+ const sphereShape = isSphereA ? shapeA : shapeB;
396
401
  const boxTr = isSphereA ? trB : trA;
397
402
  const boxShape = isSphereA ? shapeB : shapeA;
398
403
  const bh = boxShape.half_extents;
399
404
 
400
405
  const ok = sphere_box_contact(
401
406
  closed_form_result,
402
- sphereTr.position.x, sphereTr.position.y, sphereTr.position.z, 1,
407
+ sphereTr.position.x, sphereTr.position.y, sphereTr.position.z, sphereShape.radius,
403
408
  boxTr.position.x, boxTr.position.y, boxTr.position.z,
404
409
  boxTr.rotation.x, boxTr.rotation.y, boxTr.rotation.z, boxTr.rotation.w,
405
410
  bh.x, bh.y, bh.z
@@ -491,12 +496,13 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
491
496
  const capsuleTr = isCapsuleA ? trA : trB;
492
497
  const capsuleShape = isCapsuleA ? colA.shape : colB.shape;
493
498
  const sphereTr = isCapsuleA ? trB : trA;
499
+ const sphereShape = isCapsuleA ? colB.shape : colA.shape;
494
500
  const ok = capsule_sphere_contact(
495
501
  closed_form_result,
496
502
  capsuleTr.position.x, capsuleTr.position.y, capsuleTr.position.z,
497
503
  capsuleTr.rotation.x, capsuleTr.rotation.y, capsuleTr.rotation.z, capsuleTr.rotation.w,
498
504
  capsuleShape.radius, capsuleShape.height * 0.5,
499
- sphereTr.position.x, sphereTr.position.y, sphereTr.position.z, 1
505
+ sphereTr.position.x, sphereTr.position.y, sphereTr.position.z, sphereShape.radius
500
506
  );
501
507
  if (!ok) return count;
502
508
  let nx = closed_form_result[0], ny = closed_form_result[1], nz = closed_form_result[2];
@@ -639,14 +645,15 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
639
645
  const c_pos_y = concave_tr.position.y;
640
646
  const c_pos_z = concave_tr.position.z;
641
647
 
642
- // Sphere fast-path: when the convex side is a UnitSphereShape3D we
643
- // bypass GJK+EPA entirely per triangle and use the closed-form
648
+ // Sphere fast-path: when the convex side is a sphere we bypass GJK+EPA
649
+ // entirely per triangle and use the closed-form
644
650
  // {@link sphere_triangle_contact}. This avoids the EPA precision
645
651
  // wall on Triangle3D (whose support function is degenerate along
646
652
  // the face normal — all 3 vertices project to the same value),
647
653
  // which was producing noisy depths at small penetrations and
648
654
  // letting dropped spheres tunnel through heightmaps / meshes.
649
- const isSphereConvex = convex_col.shape.isUnitSphereShape3D === true;
655
+ const isSphereConvex = convex_col.shape.isSphereShape3D === true;
656
+ const sphere_radius = isSphereConvex ? convex_col.shape.radius : 0;
650
657
 
651
658
  // Box fast-path: closed-form {@link box_triangle_contact} via SAT
652
659
  // over 13 axes + polygon clipping for face-vs-face contacts.
@@ -743,7 +750,7 @@ function dispatch_pair(count, colA, trA, colB, trB, gjk_axis_buf = null, gjk_axi
743
750
 
744
751
  const ok = sphere_triangle_contact(
745
752
  sphere_triangle_result,
746
- convex_wx, convex_wy, convex_wz, 1,
753
+ convex_wx, convex_wy, convex_wz, sphere_radius,
747
754
  ax_w, ay_w, az_w,
748
755
  bx_w, by_w, bz_w,
749
756
  cx_w, cy_w, cz_w
@@ -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.