@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.
- package/package.json +1 -1
- package/src/core/geom/3d/shape/CapsuleShape3D.d.ts +1 -1
- package/src/core/geom/3d/shape/CapsuleShape3D.js +1 -1
- package/src/core/geom/3d/shape/SphereShape3D.d.ts +47 -0
- package/src/core/geom/3d/shape/SphereShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/SphereShape3D.js +127 -0
- package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts +30 -18
- package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/UnitSphereShape3D.js +44 -92
- package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/shape_to_type.js +4 -2
- package/src/core/geom/3d/shape/json/type_adapters.d.ts +12 -3
- package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/type_adapters.js +16 -4
- package/src/core/geom/3d/shape/util/shape_to_visual_entity.js +2 -2
- package/src/engine/control/first-person/DESIGN_COLLISION.md +255 -0
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +3 -1
- package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +12 -7
- package/src/engine/physics/ecs/JointSerializationAdapter.d.ts +29 -0
- package/src/engine/physics/ecs/JointSerializationAdapter.d.ts.map +1 -0
- package/src/engine/physics/ecs/JointSerializationAdapter.js +72 -0
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +20 -13
- package/src/engine/physics/narrowphase/refine_ray_hit.js +2 -2
- package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts +8 -7
- package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts.map +1 -1
- 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.
|
|
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 `
|
|
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 `
|
|
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
|
|
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
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
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
|
-
|
|
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":"
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
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,
|
|
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
|
|
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(
|
|
8
|
-
|
|
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 {
|
|
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":";;
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
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.
|
package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
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 -
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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":"
|
|
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
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
643
|
-
//
|
|
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.
|
|
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,
|
|
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.
|
|
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,
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|