@woosh/meep-engine 2.145.0 → 2.146.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/HeightMapShape3D.d.ts +33 -3
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.js +486 -451
- package/src/engine/control/first-person/DESIGN_COLLISION.md +365 -352
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +1 -3
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +12 -2
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +7 -2
- package/src/engine/control/first-person/TODO.md +13 -11
- package/src/engine/control/first-person/abilities/WallJump.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallJump.js +11 -3
- package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallRun.js +12 -0
- package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
- package/src/engine/control/first-person/collision/KinematicMover.js +634 -592
- package/src/engine/control/first-person/prototype_first_person_controller.js +1003 -901
- package/src/engine/physics/PLAN.md +943 -809
- package/src/engine/physics/body/BodyStorage.d.ts +9 -0
- package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
- package/src/engine/physics/body/BodyStorage.js +23 -0
- package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
- package/src/engine/physics/broadphase/generate_pairs.js +7 -0
- package/src/engine/physics/ccd/linear_sweep.d.ts +97 -0
- package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -0
- package/src/engine/physics/ccd/linear_sweep.js +238 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +18 -3
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +59 -8
- package/src/engine/physics/ecs/RigidBodyFlags.d.ts +6 -0
- package/src/engine/physics/ecs/RigidBodyFlags.d.ts.map +1 -1
- package/src/engine/physics/ecs/RigidBodyFlags.js +6 -0
- package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -811
- package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/compute_penetration.js +325 -323
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +27 -8
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +235 -204
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +70 -13
- package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
- package/src/engine/physics/queries/overlap_shape.js +185 -183
- package/src/engine/simulation/Ticker.d.ts +14 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +136 -1
|
@@ -61,6 +61,7 @@ export class BodyStorage {
|
|
|
61
61
|
__awake_count: number;
|
|
62
62
|
__free_heap: Uint32Array;
|
|
63
63
|
__free_count: number;
|
|
64
|
+
__entity_to_index: Map<any, any>;
|
|
64
65
|
/**
|
|
65
66
|
* Currently allocated body count (live, regardless of awake/sleeping).
|
|
66
67
|
* @returns {number}
|
|
@@ -109,6 +110,14 @@ export class BodyStorage {
|
|
|
109
110
|
* @returns {number} entity for the body, or -1 if the slot is free.
|
|
110
111
|
*/
|
|
111
112
|
entity_at(index: number): number;
|
|
113
|
+
/**
|
|
114
|
+
* Body index for `entity`, or {@link BODY_INDEX_ABSENT} if no live body owns
|
|
115
|
+
* it. O(1) reverse of {@link entity_at} — the lookup callers use on the
|
|
116
|
+
* link / attach / joint paths instead of scanning the slot table.
|
|
117
|
+
* @param {number} entity
|
|
118
|
+
* @returns {number}
|
|
119
|
+
*/
|
|
120
|
+
index_of_entity(entity: number): number;
|
|
112
121
|
/**
|
|
113
122
|
* @param {number} index
|
|
114
123
|
* @returns {number}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BodyStorage.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/body/BodyStorage.js"],"names":[],"mappings":"AA2BA;;;;;;GAMG;AACH,oCAJW,MAAM,cACN,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,sCAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,2CAHW,MAAM,GACJ,MAAM,CAIlB;AAlCD;;;;GAIG;AACH,gCAFU,MAAM,CAEoB;AA+BpC;;;;;;;;;;;;;;;;;;GAkBG;AACH;IAEI;;;OAGG;IACH,+BAFW,MAAM,
|
|
1
|
+
{"version":3,"file":"BodyStorage.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/body/BodyStorage.js"],"names":[],"mappings":"AA2BA;;;;;;GAMG;AACH,oCAJW,MAAM,cACN,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,sCAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,2CAHW,MAAM,GACJ,MAAM,CAIlB;AAlCD;;;;GAIG;AACH,gCAFU,MAAM,CAEoB;AA+BpC;;;;;;;;;;;;;;;;;;GAkBG;AACH;IAEI;;;OAGG;IACH,+BAFW,MAAM,EAqChB;IA9BG,mBAAqB;IAGrB,gBAAgB;IAEhB,uBAAqC;IACrC,0BAAwC;IACxC,oBAAkC;IAClC,qBAAmC;IAGnC,oBAAkC;IAGlC,0BAAwC;IACxC,wBAAsC;IACtC,sBAAsB;IAGtB,yBAAuC;IACvC,qBAAqB;IAMrB,iCAAkC;IAMtC;;;OAGG;IACH,mBAEC;IAED;;;OAGG;IACH,uBAEC;IAED;;;OAGG;IACH,0BAEC;IAED;;;;OAIG;IACH,8BAEC;IAED;;;;;;;OAOG;IACH,iBAHW,MAAM,GACJ,MAAM,CA0BlB;IAED;;;;;OAKG;IACH,qBAFW,MAAM,QAwBhB;IAED;;;;OAIG;IACH,yBAHW,MAAM,GACJ,OAAO,CAWnB;IAED;;;OAGG;IACH,iBAHW,MAAM,GACJ,MAAM,CAOlB;IAED;;;;;;OAMG;IACH,wBAHW,MAAM,GACJ,MAAM,CAKlB;IAED;;;OAGG;IACH,qBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,eAHW,MAAM,GACJ,QAAQ,GAAC,MAAM,CAI3B;IAED;;;OAGG;IACH,gBAHW,MAAM,QACN,QAAQ,GAAC,MAAM,QAIzB;IAED;;;OAGG;IACH,gBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,iBAHW,MAAM,SACN,MAAM,QAIhB;IAED;;;;OAIG;IACH,yBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;;OAIG;IACH,gBAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;;OAIG;IACH,YAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,kBAFW,MAAM,QAShB;IAED;;;OAGG;IACH,qBAFW,MAAM,QAShB;IAED;;;;OAIG;IACH,0BAOC;IAED;;OAEG;IACH,eAiCC;IAID;;;OAGG;IACH,oBAgBC;IAED;;;OAGG;IACH,mBAqBC;CACJ;yBAhcwB,oBAAoB"}
|
|
@@ -106,6 +106,12 @@ export class BodyStorage {
|
|
|
106
106
|
this.__free_heap = new Uint32Array(cap);
|
|
107
107
|
this.__free_count = 0;
|
|
108
108
|
|
|
109
|
+
// Entity → body-index map (one body per entity). Keeps {@link
|
|
110
|
+
// index_of_entity} O(1) instead of an O(N) scan over the slot table on
|
|
111
|
+
// every collider attach / detach and joint link. Maintained on
|
|
112
|
+
// allocate / free; never grows with the typed arrays (a plain Map).
|
|
113
|
+
this.__entity_to_index = new Map();
|
|
114
|
+
|
|
109
115
|
// Initialise reverse map to BODY_INDEX_ABSENT.
|
|
110
116
|
this.__awake_pos.fill(BODY_INDEX_ABSENT);
|
|
111
117
|
}
|
|
@@ -167,6 +173,7 @@ export class BodyStorage {
|
|
|
167
173
|
this.__kinds[index] = BodyKind.Dynamic;
|
|
168
174
|
this.__flags[index] = 0;
|
|
169
175
|
this.__alive[index] = 1;
|
|
176
|
+
this.__entity_to_index.set(entity, index);
|
|
170
177
|
|
|
171
178
|
// Insert into awake set.
|
|
172
179
|
const awake_pos = this.__awake_count++;
|
|
@@ -196,6 +203,10 @@ export class BodyStorage {
|
|
|
196
203
|
|
|
197
204
|
this.__alive[index] = 0;
|
|
198
205
|
|
|
206
|
+
// Drop the entity → index mapping (the slot still holds the old entity
|
|
207
|
+
// value until reallocation, so delete by it now while it's valid).
|
|
208
|
+
this.__entity_to_index.delete(this.__entities[index]);
|
|
209
|
+
|
|
199
210
|
// Bump generation; wraps mod 256.
|
|
200
211
|
this.__generations[index] = (this.__generations[index] + 1) & GENERATION_MASK;
|
|
201
212
|
|
|
@@ -229,6 +240,18 @@ export class BodyStorage {
|
|
|
229
240
|
return this.__entities[index];
|
|
230
241
|
}
|
|
231
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Body index for `entity`, or {@link BODY_INDEX_ABSENT} if no live body owns
|
|
245
|
+
* it. O(1) reverse of {@link entity_at} — the lookup callers use on the
|
|
246
|
+
* link / attach / joint paths instead of scanning the slot table.
|
|
247
|
+
* @param {number} entity
|
|
248
|
+
* @returns {number}
|
|
249
|
+
*/
|
|
250
|
+
index_of_entity(entity) {
|
|
251
|
+
const idx = this.__entity_to_index.get(entity);
|
|
252
|
+
return idx === undefined ? BODY_INDEX_ABSENT : idx;
|
|
253
|
+
}
|
|
254
|
+
|
|
232
255
|
/**
|
|
233
256
|
* @param {number} index
|
|
234
257
|
* @returns {number}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate_pairs.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/broadphase/generate_pairs.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"generate_pairs.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/broadphase/generate_pairs.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,uIARW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAY;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,CAAC,CAAC,+CAElF,MAAM,OAAO,MAAM,KAAK,OAAO,QA4EhD"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assert } from "../../../core/assert.js";
|
|
1
2
|
import { bvh_query_user_data_overlaps_aabb } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js";
|
|
2
3
|
|
|
3
4
|
const scratch_aabb = new Float64Array(6);
|
|
@@ -65,6 +66,11 @@ export function generate_pairs(
|
|
|
65
66
|
dynamic_bvh,
|
|
66
67
|
scratch_aabb
|
|
67
68
|
);
|
|
69
|
+
// The BVH query writes leaves unconditionally — at capacity it both
|
|
70
|
+
// drops leaves (typed-array OOB writes no-op) AND returns a count
|
|
71
|
+
// past the buffer end, so the loop below would read `undefined`
|
|
72
|
+
// candidates and build garbage pairs. Guard the buffer size.
|
|
73
|
+
assert.lessThan(n, candidates.length, 'generate_pairs: dynamic broadphase overflowed the candidate buffer');
|
|
68
74
|
for (let c = 0; c < n; c++) {
|
|
69
75
|
const other = candidates[c];
|
|
70
76
|
if (other === my_packed) continue;
|
|
@@ -85,6 +91,7 @@ export function generate_pairs(
|
|
|
85
91
|
static_bvh,
|
|
86
92
|
scratch_aabb
|
|
87
93
|
);
|
|
94
|
+
assert.lessThan(n, candidates.length, 'generate_pairs: static broadphase overflowed the candidate buffer');
|
|
88
95
|
for (let c = 0; c < n; c++) {
|
|
89
96
|
const other = candidates[c];
|
|
90
97
|
const idA = my_packed < other ? my_packed : other;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sweep `shape` (held at the fixed `rotation`) linearly from `(fx,fy,fz)` to
|
|
3
|
+
* `(tx,ty,tz)` through both broadphase trees, ignoring colliders owned by
|
|
4
|
+
* `exclude_entity`.
|
|
5
|
+
*
|
|
6
|
+
* Returns the traversable fraction of the segment in `[0, 1]`: `1` means the
|
|
7
|
+
* path is clear; a value `< 1` means a blocker was hit at that fraction and
|
|
8
|
+
* `result` has been filled — `result.position` is the swept-shape centre at
|
|
9
|
+
* first contact, `result.normal` is the blocker's outward surface normal
|
|
10
|
+
* (B → A), `result.entity` / `result.body_id` the blocker.
|
|
11
|
+
*
|
|
12
|
+
* @param {import("../ecs/PhysicsSystem.js").PhysicsSystem} system
|
|
13
|
+
* @param {import("../../../core/geom/3d/shape/AbstractShape3D.js").AbstractShape3D} shape
|
|
14
|
+
* @param {{x:number,y:number,z:number,w:number}} rotation fixed orientation
|
|
15
|
+
* @param {number} fx
|
|
16
|
+
* @param {number} fy
|
|
17
|
+
* @param {number} fz
|
|
18
|
+
* @param {number} tx
|
|
19
|
+
* @param {number} ty
|
|
20
|
+
* @param {number} tz
|
|
21
|
+
* @param {number} exclude_entity entity to ignore (the swept body itself)
|
|
22
|
+
* @param {PhysicsSurfacePoint} result populated on hit; untouched on a clear path
|
|
23
|
+
* @returns {number} traversable fraction in [0, 1]
|
|
24
|
+
*/
|
|
25
|
+
export function ccd_sweep_segment(system: import("../ecs/PhysicsSystem.js").PhysicsSystem, shape: import("../../../core/geom/3d/shape/AbstractShape3D.js").AbstractShape3D, rotation: {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
z: number;
|
|
29
|
+
w: number;
|
|
30
|
+
}, fx: number, fy: number, fz: number, tx: number, ty: number, tz: number, exclude_entity: number, result: PhysicsSurfacePoint): number;
|
|
31
|
+
/**
|
|
32
|
+
* Post-solve continuous-collision pass over the awake set. For each awake
|
|
33
|
+
* Dynamic body flagged {@link RigidBodyFlags.CCD} that moved more than
|
|
34
|
+
* {@link CCD_MIN_SWEEP_DISTANCE} this step, sweep its primary collider along the
|
|
35
|
+
* step's net translation and stop it at the first blocker (clamp pose + remove
|
|
36
|
+
* inbound normal velocity).
|
|
37
|
+
*
|
|
38
|
+
* Start-of-step positions are captured into `system.__ccd_start_pos` (3 doubles
|
|
39
|
+
* per body index) before the substep loop; this pass reads the final pose from
|
|
40
|
+
* the live Transform. Iterates the awake list in storage order, so it is
|
|
41
|
+
* deterministic.
|
|
42
|
+
*
|
|
43
|
+
* @param {import("../ecs/PhysicsSystem.js").PhysicsSystem} system
|
|
44
|
+
*/
|
|
45
|
+
export function ccd_resolve(system: import("../ecs/PhysicsSystem.js").PhysicsSystem): void;
|
|
46
|
+
/**
|
|
47
|
+
* Continuous collision detection — linear shape-cast (the "speculative margin
|
|
48
|
+
* floor, upgraded to an opt-in per-body sweep" item from PLAN.md).
|
|
49
|
+
*
|
|
50
|
+
* The discrete pipeline detects contacts by overlap at step boundaries: a body
|
|
51
|
+
* that moves more than its own thickness in one step can start a step on one
|
|
52
|
+
* side of a thin wall and end on the other, never overlapping it on any frame
|
|
53
|
+
* the broadphase samples — it tunnels. The speculative fat-AABB margin
|
|
54
|
+
* (`compute_fat_world_aabb`) covers moderate speeds; genuinely fast movers
|
|
55
|
+
* (bullets, dropped debris, a body flung by an explosion) need a swept test.
|
|
56
|
+
*
|
|
57
|
+
* Approach (Box2D `b2_continuousPhysics`-style conservative advancement): after
|
|
58
|
+
* the substep solver has produced each body's final pose, sweep a CCD-flagged
|
|
59
|
+
* fast mover's primary collider along its NET step translation
|
|
60
|
+
* (start-of-step → final pose) using the existing {@link shape_cast} TOI engine.
|
|
61
|
+
* On the first blocker, clamp the body to the contact pose and remove the
|
|
62
|
+
* inbound normal component of its velocity (an inelastic stop) — the next
|
|
63
|
+
* discrete step then resolves the now-touching contact with the real
|
|
64
|
+
* material / restitution.
|
|
65
|
+
*
|
|
66
|
+
* Scope (v1, deliberate):
|
|
67
|
+
* - LINEAR sweep only: the orientation is held fixed through the sweep (the
|
|
68
|
+
* per-step angular motion is small at the fixed-step rate).
|
|
69
|
+
* - PRIMARY same-entity collider: a compound body sweeps its first collider;
|
|
70
|
+
* child-entity colliders (whose transform is synced outside the step) are
|
|
71
|
+
* not swept.
|
|
72
|
+
* - EXACT against static geometry (the static BVH holds tight, never-moved
|
|
73
|
+
* leaves); APPROXIMATE against other dynamic bodies (they have moved this
|
|
74
|
+
* step too, but the sweep sees their start-of-step broadphase AABBs) — the
|
|
75
|
+
* speculative-margin floor for the dynamic-vs-dynamic case.
|
|
76
|
+
* - The CCD stop itself is inelastic; the impact does not bounce. Restitution
|
|
77
|
+
* applies on the next discrete contact.
|
|
78
|
+
*/
|
|
79
|
+
/**
|
|
80
|
+
* Minimum per-step displacement (metres) before a CCD sweep is worth running.
|
|
81
|
+
* Its ONLY job is to skip a body that didn't meaningfully move — a resting /
|
|
82
|
+
* sleeping-soon body whose displacement is sub-millimetre jitter — which avoids
|
|
83
|
+
* a degenerate zero-length sweep and saves the query cost.
|
|
84
|
+
*
|
|
85
|
+
* It must NOT be tied to the body's own size. Tunnelling risk is governed by the
|
|
86
|
+
* *obstacle's* thickness, not the mover's: a 2 m sphere drifting at 0.5 m/step
|
|
87
|
+
* still passes clean through a 1 cm floor. A small absolute slop catches that;
|
|
88
|
+
* gating on a fraction of the body's extent would (wrongly) wait until the body
|
|
89
|
+
* moved more than its own radius and miss every thin-obstacle tunnel below that
|
|
90
|
+
* speed. The discrete narrowphase still owns any obstacle thicker than a body's
|
|
91
|
+
* per-step move, so CCD reliably prevents tunnelling of obstacles thicker than
|
|
92
|
+
* this slop.
|
|
93
|
+
* @type {number}
|
|
94
|
+
*/
|
|
95
|
+
export const CCD_MIN_SWEEP_DISTANCE: number;
|
|
96
|
+
import { PhysicsSurfacePoint } from "../queries/PhysicsSurfacePoint.js";
|
|
97
|
+
//# sourceMappingURL=linear_sweep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear_sweep.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ccd/linear_sweep.js"],"names":[],"mappings":"AA8FA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,0CAbW,OAAO,yBAAyB,EAAE,aAAa,SAC/C,OAAO,gDAAgD,EAAE,eAAe,YACxE;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,MACrC,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,kBACN,MAAM,UACN,mBAAmB,GACjB,MAAM,CAmClB;AAED;;;;;;;;;;;;;GAaG;AACH,oCAFW,OAAO,yBAAyB,EAAE,aAAa,QAwEzD;AAvOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,qCAFU,MAAM,CAE2B;oCArDP,mCAAmC"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Ray3 } from "../../../core/geom/3d/ray/Ray3.js";
|
|
2
|
+
import { BodyKind } from "../ecs/BodyKind.js";
|
|
3
|
+
import { RigidBodyFlags } from "../ecs/RigidBodyFlags.js";
|
|
4
|
+
import { PhysicsSurfacePoint } from "../queries/PhysicsSurfacePoint.js";
|
|
5
|
+
import { shape_cast } from "../queries/shape_cast.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Continuous collision detection — linear shape-cast (the "speculative margin
|
|
9
|
+
* floor, upgraded to an opt-in per-body sweep" item from PLAN.md).
|
|
10
|
+
*
|
|
11
|
+
* The discrete pipeline detects contacts by overlap at step boundaries: a body
|
|
12
|
+
* that moves more than its own thickness in one step can start a step on one
|
|
13
|
+
* side of a thin wall and end on the other, never overlapping it on any frame
|
|
14
|
+
* the broadphase samples — it tunnels. The speculative fat-AABB margin
|
|
15
|
+
* (`compute_fat_world_aabb`) covers moderate speeds; genuinely fast movers
|
|
16
|
+
* (bullets, dropped debris, a body flung by an explosion) need a swept test.
|
|
17
|
+
*
|
|
18
|
+
* Approach (Box2D `b2_continuousPhysics`-style conservative advancement): after
|
|
19
|
+
* the substep solver has produced each body's final pose, sweep a CCD-flagged
|
|
20
|
+
* fast mover's primary collider along its NET step translation
|
|
21
|
+
* (start-of-step → final pose) using the existing {@link shape_cast} TOI engine.
|
|
22
|
+
* On the first blocker, clamp the body to the contact pose and remove the
|
|
23
|
+
* inbound normal component of its velocity (an inelastic stop) — the next
|
|
24
|
+
* discrete step then resolves the now-touching contact with the real
|
|
25
|
+
* material / restitution.
|
|
26
|
+
*
|
|
27
|
+
* Scope (v1, deliberate):
|
|
28
|
+
* - LINEAR sweep only: the orientation is held fixed through the sweep (the
|
|
29
|
+
* per-step angular motion is small at the fixed-step rate).
|
|
30
|
+
* - PRIMARY same-entity collider: a compound body sweeps its first collider;
|
|
31
|
+
* child-entity colliders (whose transform is synced outside the step) are
|
|
32
|
+
* not swept.
|
|
33
|
+
* - EXACT against static geometry (the static BVH holds tight, never-moved
|
|
34
|
+
* leaves); APPROXIMATE against other dynamic bodies (they have moved this
|
|
35
|
+
* step too, but the sweep sees their start-of-step broadphase AABBs) — the
|
|
36
|
+
* speculative-margin floor for the dynamic-vs-dynamic case.
|
|
37
|
+
* - The CCD stop itself is inelastic; the impact does not bounce. Restitution
|
|
38
|
+
* applies on the next discrete contact.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Minimum per-step displacement (metres) before a CCD sweep is worth running.
|
|
43
|
+
* Its ONLY job is to skip a body that didn't meaningfully move — a resting /
|
|
44
|
+
* sleeping-soon body whose displacement is sub-millimetre jitter — which avoids
|
|
45
|
+
* a degenerate zero-length sweep and saves the query cost.
|
|
46
|
+
*
|
|
47
|
+
* It must NOT be tied to the body's own size. Tunnelling risk is governed by the
|
|
48
|
+
* *obstacle's* thickness, not the mover's: a 2 m sphere drifting at 0.5 m/step
|
|
49
|
+
* still passes clean through a 1 cm floor. A small absolute slop catches that;
|
|
50
|
+
* gating on a fraction of the body's extent would (wrongly) wait until the body
|
|
51
|
+
* moved more than its own radius and miss every thin-obstacle tunnel below that
|
|
52
|
+
* speed. The discrete narrowphase still owns any obstacle thicker than a body's
|
|
53
|
+
* per-step move, so CCD reliably prevents tunnelling of obstacles thicker than
|
|
54
|
+
* this slop.
|
|
55
|
+
* @type {number}
|
|
56
|
+
*/
|
|
57
|
+
export const CCD_MIN_SWEEP_DISTANCE = 1e-3;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Impact distances at or below this (metres) mean the body was already
|
|
61
|
+
* overlapping the target at the start of the step — i.e. a resting / sliding
|
|
62
|
+
* contact the discrete solver owns, not a tunnel. The sweep ignores these so
|
|
63
|
+
* CCD never clamps a body to a surface it is merely sitting or sliding on.
|
|
64
|
+
* @type {number}
|
|
65
|
+
*/
|
|
66
|
+
const CCD_INITIAL_OVERLAP_EPS = 1e-6;
|
|
67
|
+
|
|
68
|
+
// ── Module scratch ──────────────────────────────────────────────────────────
|
|
69
|
+
// CCD runs inside the step loop; reuse buffers so the pass allocates nothing.
|
|
70
|
+
|
|
71
|
+
/** Swept ray, re-seeded per sweep (origin, unit direction, tMax). */
|
|
72
|
+
const _ray = new Ray3();
|
|
73
|
+
|
|
74
|
+
/** Sweep result reused across bodies inside {@link ccd_resolve}. */
|
|
75
|
+
const _hit = new PhysicsSurfacePoint();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Entity whose colliders the current sweep must ignore (the moving body's own).
|
|
79
|
+
* Module-scoped so {@link _exclude_self} stays a stable, non-allocating
|
|
80
|
+
* reference handed to {@link shape_cast}.
|
|
81
|
+
* @type {number}
|
|
82
|
+
*/
|
|
83
|
+
let _exclude_entity = -1;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Self-exclusion filter for the sweep: drop candidates owned by the moving
|
|
87
|
+
* body's own entity (otherwise the body would "hit itself" at `t = 0`).
|
|
88
|
+
* @param {number} entity
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
function _exclude_self(entity) {
|
|
92
|
+
return entity !== _exclude_entity;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Sweep `shape` (held at the fixed `rotation`) linearly from `(fx,fy,fz)` to
|
|
97
|
+
* `(tx,ty,tz)` through both broadphase trees, ignoring colliders owned by
|
|
98
|
+
* `exclude_entity`.
|
|
99
|
+
*
|
|
100
|
+
* Returns the traversable fraction of the segment in `[0, 1]`: `1` means the
|
|
101
|
+
* path is clear; a value `< 1` means a blocker was hit at that fraction and
|
|
102
|
+
* `result` has been filled — `result.position` is the swept-shape centre at
|
|
103
|
+
* first contact, `result.normal` is the blocker's outward surface normal
|
|
104
|
+
* (B → A), `result.entity` / `result.body_id` the blocker.
|
|
105
|
+
*
|
|
106
|
+
* @param {import("../ecs/PhysicsSystem.js").PhysicsSystem} system
|
|
107
|
+
* @param {import("../../../core/geom/3d/shape/AbstractShape3D.js").AbstractShape3D} shape
|
|
108
|
+
* @param {{x:number,y:number,z:number,w:number}} rotation fixed orientation
|
|
109
|
+
* @param {number} fx
|
|
110
|
+
* @param {number} fy
|
|
111
|
+
* @param {number} fz
|
|
112
|
+
* @param {number} tx
|
|
113
|
+
* @param {number} ty
|
|
114
|
+
* @param {number} tz
|
|
115
|
+
* @param {number} exclude_entity entity to ignore (the swept body itself)
|
|
116
|
+
* @param {PhysicsSurfacePoint} result populated on hit; untouched on a clear path
|
|
117
|
+
* @returns {number} traversable fraction in [0, 1]
|
|
118
|
+
*/
|
|
119
|
+
export function ccd_sweep_segment(
|
|
120
|
+
system, shape, rotation,
|
|
121
|
+
fx, fy, fz, tx, ty, tz,
|
|
122
|
+
exclude_entity, result,
|
|
123
|
+
) {
|
|
124
|
+
const dx = tx - fx;
|
|
125
|
+
const dy = ty - fy;
|
|
126
|
+
const dz = tz - fz;
|
|
127
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
128
|
+
if (len === 0) return 1;
|
|
129
|
+
|
|
130
|
+
const inv = 1 / len;
|
|
131
|
+
_ray[0] = fx; _ray[1] = fy; _ray[2] = fz;
|
|
132
|
+
_ray[3] = dx * inv; _ray[4] = dy * inv; _ray[5] = dz * inv;
|
|
133
|
+
_ray[6] = len; // tMax in metres — direction is unit length
|
|
134
|
+
|
|
135
|
+
_exclude_entity = exclude_entity;
|
|
136
|
+
if (!shape_cast(system, _ray, shape, rotation, result, _exclude_self)) {
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// An impact at t ≈ 0 means the swept shape was already overlapping the
|
|
141
|
+
// target at the segment start — a resting / sliding contact, not a tunnel.
|
|
142
|
+
// The discrete solver owns it; clamping here would freeze a body onto a
|
|
143
|
+
// surface it is sitting or sliding on. Treat as a clear path.
|
|
144
|
+
if (result.t <= CCD_INITIAL_OVERLAP_EPS) return 1;
|
|
145
|
+
|
|
146
|
+
// result.t is the impact distance along the unit direction; normalise to a
|
|
147
|
+
// segment fraction. shape_cast reports the just-SEPARATING side of its
|
|
148
|
+
// bisection, so the swept shape at `result.t` is provably not overlapping —
|
|
149
|
+
// safe to place the body there.
|
|
150
|
+
const frac = result.t * inv;
|
|
151
|
+
return frac < 1 ? frac : 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Post-solve continuous-collision pass over the awake set. For each awake
|
|
156
|
+
* Dynamic body flagged {@link RigidBodyFlags.CCD} that moved more than
|
|
157
|
+
* {@link CCD_MIN_SWEEP_DISTANCE} this step, sweep its primary collider along the
|
|
158
|
+
* step's net translation and stop it at the first blocker (clamp pose + remove
|
|
159
|
+
* inbound normal velocity).
|
|
160
|
+
*
|
|
161
|
+
* Start-of-step positions are captured into `system.__ccd_start_pos` (3 doubles
|
|
162
|
+
* per body index) before the substep loop; this pass reads the final pose from
|
|
163
|
+
* the live Transform. Iterates the awake list in storage order, so it is
|
|
164
|
+
* deterministic.
|
|
165
|
+
*
|
|
166
|
+
* @param {import("../ecs/PhysicsSystem.js").PhysicsSystem} system
|
|
167
|
+
*/
|
|
168
|
+
export function ccd_resolve(system) {
|
|
169
|
+
const storage = system.storage;
|
|
170
|
+
const count = storage.awake_count;
|
|
171
|
+
const lists = system.__body_collider_lists;
|
|
172
|
+
const start = system.__ccd_start_pos;
|
|
173
|
+
const CCD = RigidBodyFlags.CCD;
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < count; i++) {
|
|
176
|
+
const idx = storage.awake_at(i);
|
|
177
|
+
|
|
178
|
+
const rb = system.__bodies[idx];
|
|
179
|
+
if (rb === undefined) continue;
|
|
180
|
+
if ((rb.flags & CCD) === 0) continue;
|
|
181
|
+
if (rb.kind !== BodyKind.Dynamic) continue;
|
|
182
|
+
|
|
183
|
+
const list = lists[idx];
|
|
184
|
+
if (list === undefined || list.length === 0) continue;
|
|
185
|
+
const primary = list[0];
|
|
186
|
+
const tr = primary.transform;
|
|
187
|
+
const p = tr.position;
|
|
188
|
+
|
|
189
|
+
const base = idx * 3;
|
|
190
|
+
const sx = start[base];
|
|
191
|
+
const sy = start[base + 1];
|
|
192
|
+
const sz = start[base + 2];
|
|
193
|
+
const ex = p[0];
|
|
194
|
+
const ey = p[1];
|
|
195
|
+
const ez = p[2];
|
|
196
|
+
|
|
197
|
+
const dx = ex - sx;
|
|
198
|
+
const dy = ey - sy;
|
|
199
|
+
const dz = ez - sz;
|
|
200
|
+
const disp2 = dx * dx + dy * dy + dz * dz;
|
|
201
|
+
if (disp2 === 0) continue;
|
|
202
|
+
|
|
203
|
+
// Motion gate: skip a body that barely moved (resting / negligible
|
|
204
|
+
// jitter) — avoids a degenerate sweep and the query cost. NOT tied to
|
|
205
|
+
// body size: a body tunnels a thin obstacle at speeds well below its
|
|
206
|
+
// own extent, so the threshold is a small absolute slop. A resting /
|
|
207
|
+
// sliding body that does clear it is still safe — the sweep ignores its
|
|
208
|
+
// initial-overlap contact (see CCD_INITIAL_OVERLAP_EPS).
|
|
209
|
+
if (disp2 <= CCD_MIN_SWEEP_DISTANCE * CCD_MIN_SWEEP_DISTANCE) continue;
|
|
210
|
+
|
|
211
|
+
const shape = primary.collider.shape;
|
|
212
|
+
const frac = ccd_sweep_segment(
|
|
213
|
+
system, shape, tr.rotation,
|
|
214
|
+
sx, sy, sz, ex, ey, ez,
|
|
215
|
+
primary.entity, _hit,
|
|
216
|
+
);
|
|
217
|
+
if (frac >= 1) continue; // clear path — no tunnelling this step
|
|
218
|
+
|
|
219
|
+
// Clamp the body to the first-contact pose (swept-shape centre at the
|
|
220
|
+
// TOI), so it cannot end the step on the far side of the blocker.
|
|
221
|
+
const hp = _hit.position;
|
|
222
|
+
p.set(hp[0], hp[1], hp[2]);
|
|
223
|
+
|
|
224
|
+
// Remove the inbound normal component of velocity (inelastic stop).
|
|
225
|
+
// `_hit.normal` is the blocker's outward surface normal; the body is
|
|
226
|
+
// moving into it (v·n < 0). Zeroing that component leaves the tangential
|
|
227
|
+
// slide intact and never adds energy. The next discrete step applies the
|
|
228
|
+
// real restitution / friction on the established contact.
|
|
229
|
+
const n = _hit.normal;
|
|
230
|
+
const lv = rb.linearVelocity;
|
|
231
|
+
const vn = lv[0] * n[0] + lv[1] * n[1] + lv[2] * n[2];
|
|
232
|
+
if (vn < 0) {
|
|
233
|
+
lv[0] -= vn * n[0];
|
|
234
|
+
lv[1] -= vn * n[1];
|
|
235
|
+
lv[2] -= vn * n[2];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -196,6 +196,21 @@ export class PhysicsSystem extends System<any, any, any, any, any> {
|
|
|
196
196
|
* @type {Float64Array}
|
|
197
197
|
*/
|
|
198
198
|
__pseudo_velocity: Float64Array;
|
|
199
|
+
/**
|
|
200
|
+
* Master switch for the continuous-collision pass. When false the
|
|
201
|
+
* {@link RigidBodyFlags.CCD} flag is ignored and no swept queries run.
|
|
202
|
+
* @type {boolean}
|
|
203
|
+
*/
|
|
204
|
+
ccdEnabled: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* Start-of-step world positions for CCD-flagged bodies — 3 doubles per
|
|
207
|
+
* body slot index (`[x, y, z]`). Captured in Stage 1 before the substep
|
|
208
|
+
* loop integrates poses; the CCD pass ({@link ccd_resolve}) sweeps from
|
|
209
|
+
* here to the final pose. Grows to `storage.high_water_mark * 3`; only
|
|
210
|
+
* CCD-flagged slots are written each step.
|
|
211
|
+
* @type {Float64Array}
|
|
212
|
+
*/
|
|
213
|
+
__ccd_start_pos: Float64Array;
|
|
199
214
|
/**
|
|
200
215
|
* Bound reference to {@link __pair_filter} so we hand the same
|
|
201
216
|
* callable to {@link generate_pairs} each step without per-step
|
|
@@ -322,9 +337,9 @@ export class PhysicsSystem extends System<any, any, any, any, any> {
|
|
|
322
337
|
*/
|
|
323
338
|
detach_collider(body_entity: number, collider: Collider): void;
|
|
324
339
|
/**
|
|
325
|
-
*
|
|
326
|
-
* O(
|
|
327
|
-
*
|
|
340
|
+
* Resolve an entity to its body index, or -1 if no live body owns it.
|
|
341
|
+
* O(1) via {@link BodyStorage#index_of_entity}'s entity → index map — used
|
|
342
|
+
* on the collider attach / detach and joint link paths.
|
|
328
343
|
*
|
|
329
344
|
* @private
|
|
330
345
|
* @param {number} entity
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PhysicsSystem.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/PhysicsSystem.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PhysicsSystem.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/PhysicsSystem.js"],"names":[],"mappings":"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"}
|
|
@@ -21,6 +21,7 @@ import { narrowphase_step } from "../narrowphase/narrowphase_step.js";
|
|
|
21
21
|
import { overlap_shape as overlap_shape_query } from "../queries/overlap_shape.js";
|
|
22
22
|
import { raycast as raycast_query } from "../queries/raycast.js";
|
|
23
23
|
import { shape_cast as shape_cast_query } from "../queries/shape_cast.js";
|
|
24
|
+
import { ccd_resolve } from "../ccd/linear_sweep.js";
|
|
24
25
|
import { returnTrue } from "../../../core/function/returnTrue.js";
|
|
25
26
|
import {
|
|
26
27
|
prepare_contacts,
|
|
@@ -294,6 +295,23 @@ export class PhysicsSystem extends System {
|
|
|
294
295
|
*/
|
|
295
296
|
this.__pseudo_velocity = new Float64Array(0);
|
|
296
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Master switch for the continuous-collision pass. When false the
|
|
300
|
+
* {@link RigidBodyFlags.CCD} flag is ignored and no swept queries run.
|
|
301
|
+
* @type {boolean}
|
|
302
|
+
*/
|
|
303
|
+
this.ccdEnabled = true;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Start-of-step world positions for CCD-flagged bodies — 3 doubles per
|
|
307
|
+
* body slot index (`[x, y, z]`). Captured in Stage 1 before the substep
|
|
308
|
+
* loop integrates poses; the CCD pass ({@link ccd_resolve}) sweeps from
|
|
309
|
+
* here to the final pose. Grows to `storage.high_water_mark * 3`; only
|
|
310
|
+
* CCD-flagged slots are written each step.
|
|
311
|
+
* @type {Float64Array}
|
|
312
|
+
*/
|
|
313
|
+
this.__ccd_start_pos = new Float64Array(0);
|
|
314
|
+
|
|
297
315
|
/**
|
|
298
316
|
* Bound reference to {@link __pair_filter} so we hand the same
|
|
299
317
|
* callable to {@link generate_pairs} each step without per-step
|
|
@@ -586,20 +604,16 @@ export class PhysicsSystem extends System {
|
|
|
586
604
|
}
|
|
587
605
|
|
|
588
606
|
/**
|
|
589
|
-
*
|
|
590
|
-
* O(
|
|
591
|
-
*
|
|
607
|
+
* Resolve an entity to its body index, or -1 if no live body owns it.
|
|
608
|
+
* O(1) via {@link BodyStorage#index_of_entity}'s entity → index map — used
|
|
609
|
+
* on the collider attach / detach and joint link paths.
|
|
592
610
|
*
|
|
593
611
|
* @private
|
|
594
612
|
* @param {number} entity
|
|
595
613
|
* @returns {number} body index or -1
|
|
596
614
|
*/
|
|
597
615
|
__find_body_index_by_entity(entity) {
|
|
598
|
-
|
|
599
|
-
for (let i = 0; i < hwm; i++) {
|
|
600
|
-
if (this.storage.entity_at(i) === entity) return i;
|
|
601
|
-
}
|
|
602
|
-
return -1;
|
|
616
|
+
return this.storage.index_of_entity(entity);
|
|
603
617
|
}
|
|
604
618
|
|
|
605
619
|
/**
|
|
@@ -1347,6 +1361,34 @@ export class PhysicsSystem extends System {
|
|
|
1347
1361
|
const h = dt / N;
|
|
1348
1362
|
const count_after_wake = this.storage.awake_count;
|
|
1349
1363
|
|
|
1364
|
+
// CCD: capture start-of-step positions for flagged bodies over the
|
|
1365
|
+
// post-wake awake set (poses are unchanged until the substep loop below
|
|
1366
|
+
// integrates them). The CCD pass after the solver sweeps from here to
|
|
1367
|
+
// each body's final pose. Reads the primary collider's transform so the
|
|
1368
|
+
// start matches the end the resolve pass reads. Zero-cost when no body
|
|
1369
|
+
// is flagged.
|
|
1370
|
+
const ccd_on = this.ccdEnabled;
|
|
1371
|
+
if (ccd_on) {
|
|
1372
|
+
const ccd_need = this.storage.high_water_mark * 3;
|
|
1373
|
+
if (this.__ccd_start_pos.length < ccd_need) {
|
|
1374
|
+
this.__ccd_start_pos = new Float64Array(ccd_need);
|
|
1375
|
+
}
|
|
1376
|
+
const ccd_start = this.__ccd_start_pos;
|
|
1377
|
+
for (let i = 0; i < count_after_wake; i++) {
|
|
1378
|
+
const idx = this.storage.awake_at(i);
|
|
1379
|
+
const rb = this.__bodies[idx];
|
|
1380
|
+
if (rb.kind !== BodyKind.Dynamic) continue;
|
|
1381
|
+
if ((rb.flags & RigidBodyFlags.CCD) === 0) continue;
|
|
1382
|
+
const list = this.__body_collider_lists[idx];
|
|
1383
|
+
if (list === undefined || list.length === 0) continue;
|
|
1384
|
+
const cp = list[0].transform.position;
|
|
1385
|
+
const cb = idx * 3;
|
|
1386
|
+
ccd_start[cb] = cp[0];
|
|
1387
|
+
ccd_start[cb + 1] = cp[1];
|
|
1388
|
+
ccd_start[cb + 2] = cp[2];
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1350
1392
|
// Size the pseudo-velocity buffer ONCE (it may reallocate on growth),
|
|
1351
1393
|
// then capture the reference. Inside the loop we only zero its live
|
|
1352
1394
|
// region per substep — re-capturing is unnecessary since it won't
|
|
@@ -1403,6 +1445,15 @@ export class PhysicsSystem extends System {
|
|
|
1403
1445
|
// the approach velocity captured at prepare time.
|
|
1404
1446
|
apply_restitution(this.manifolds, this);
|
|
1405
1447
|
|
|
1448
|
+
// Stage 8.5: continuous collision — sweep CCD-flagged fast movers along
|
|
1449
|
+
// their net step translation and stop them at the first blocker, so they
|
|
1450
|
+
// can't tunnel through thin geometry between discrete steps. Runs on the
|
|
1451
|
+
// final post-solve poses, before the sleep test sees the clamped
|
|
1452
|
+
// velocities. No-op when no awake body is flagged.
|
|
1453
|
+
if (ccd_on) {
|
|
1454
|
+
ccd_resolve(this);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1406
1457
|
// Stage 9: sleep test.
|
|
1407
1458
|
this.__sleep_test(dt);
|
|
1408
1459
|
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* component during solve.
|
|
9
9
|
* - {@link DisableSleep }: this body never enters the sleeping set. Bodies near it
|
|
10
10
|
* may still sleep when stable.
|
|
11
|
+
* - {@link CCD }: opt-in continuous collision detection. After the solver, the
|
|
12
|
+
* body's net step translation is shape-cast against the broadphase and the
|
|
13
|
+
* body is stopped at the first blocker, so a fast mover can't tunnel through
|
|
14
|
+
* thin geometry between discrete steps. Off by default — it costs one swept
|
|
15
|
+
* query per fast-moving flagged body per step. See `ccd/linear_sweep.js`.
|
|
11
16
|
*/
|
|
12
17
|
export type RigidBodyFlags = number;
|
|
13
18
|
export namespace RigidBodyFlags {
|
|
@@ -17,5 +22,6 @@ export namespace RigidBodyFlags {
|
|
|
17
22
|
let LockRotY: number;
|
|
18
23
|
let LockRotZ: number;
|
|
19
24
|
let DisableSleep: number;
|
|
25
|
+
let CCD: number;
|
|
20
26
|
}
|
|
21
27
|
//# sourceMappingURL=RigidBodyFlags.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RigidBodyFlags.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/RigidBodyFlags.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RigidBodyFlags.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/ecs/RigidBodyFlags.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;6BAiBU,MAAM"}
|