@driftengine/physics 3.62.0 → 4.0.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/NOTICE +20 -0
- package/README.md +1 -1
- package/dist/query.js +66 -4
- package/dist/workerPool.js +13 -6
- package/package.json +1 -1
- package/src/query.ts +65 -4
- package/src/workerPool.ts +9 -4
package/NOTICE
CHANGED
|
@@ -7,3 +7,23 @@ This product includes software developed at Drift Technologies
|
|
|
7
7
|
Licensed under the Apache License, Version 2.0. Section 4(d) of that licence
|
|
8
8
|
requires this NOTICE to be reproduced in any derivative work you distribute,
|
|
9
9
|
in the source, the documentation, or a display generated by the work.
|
|
10
|
+
|
|
11
|
+
Models run by DriftCapture
|
|
12
|
+
--------------------------
|
|
13
|
+
DriftCapture runs the models below. Their weights are not in this repository
|
|
14
|
+
and are not distributed with it: tools/capture-weights/ fetches each at the
|
|
15
|
+
revision its manifest pins, verifies it, and converts it, and a game that
|
|
16
|
+
ships one ships it under that model's own licence. Each model's definition in
|
|
17
|
+
this engine is written from its project's published code.
|
|
18
|
+
|
|
19
|
+
- Depth Anything 3 Small and Depth Anything 3 Base. Apache License 2.0.
|
|
20
|
+
Copyright (c) 2025 ByteDance Ltd. and/or its affiliates.
|
|
21
|
+
- Depth Anything V2 Small. Apache License 2.0. The University of Hong Kong
|
|
22
|
+
and TikTok; converted to the Transformers layout by The HuggingFace Team.
|
|
23
|
+
- MobileSAM. Apache License 2.0. Its image encoder is TinyViT, Copyright (c)
|
|
24
|
+
2022 Microsoft; its prompt encoder and mask decoder are Segment Anything's,
|
|
25
|
+
Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
26
|
+
- SAM 2.1 tiny. Apache License 2.0. Copyright (c) Meta Platforms, Inc. and
|
|
27
|
+
affiliates.
|
|
28
|
+
- OWLv2 base patch16. Apache License 2.0. Copyright 2023 Google AI and The
|
|
29
|
+
HuggingFace Team.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ kinematic sweep that resolves a moving body against a static world one axis at a
|
|
|
5
5
|
|
|
6
6
|
**It imports no other engine package.** Together with `@driftengine/entities` that makes a
|
|
7
7
|
deterministic simulation you can run with no renderer in its module graph — in Node, in a worker,
|
|
8
|
-
or on a server, which is what an authoritative host needs. **45.
|
|
8
|
+
or on a server, which is what an authoritative host needs. **45.1 KB gzipped** standalone, measured
|
|
9
9
|
by `scripts/size-gate.test.mjs` against `scripts/fixtures/size/physics-only.ts`.
|
|
10
10
|
|
|
11
11
|
**`@driftengine/core` re-exports every name here**, so reaching for collision through the core
|
package/dist/query.js
CHANGED
|
@@ -21,6 +21,36 @@ const manifold = createManifold();
|
|
|
21
21
|
* The full set is `PhysicsWorld`'s business, where every plane becomes a constraint.
|
|
22
22
|
*/
|
|
23
23
|
const MESH_ONE = [createManifold()];
|
|
24
|
+
/**
|
|
25
|
+
* Manifolds a sweep against a mesh gathers before it decides how far it may advance.
|
|
26
|
+
*
|
|
27
|
+
* **Which triangle stopped a sweep is not a question one manifold can answer.** `collideMesh` fills
|
|
28
|
+
* its output in the order the tree hands triangles over, and the sweep read one of them — so near a
|
|
29
|
+
* crease, where a wall meets a floor, a downward sweep was answered with the wall's horizontal
|
|
30
|
+
* normal and nothing stopped the fall. Found by the first test of `CharacterController` against a
|
|
31
|
+
* triangle mesh: a capsule on a captured room's floor sank straight through it, while the same
|
|
32
|
+
* floor **on its own** held it, because adding the walls changed which triangle came back first.
|
|
33
|
+
* Sixteen is far more than a sweep meets at once, and the one facing back along the travel is the
|
|
34
|
+
* one in the way.
|
|
35
|
+
*/
|
|
36
|
+
const MESH_SET = [
|
|
37
|
+
createManifold(),
|
|
38
|
+
createManifold(),
|
|
39
|
+
createManifold(),
|
|
40
|
+
createManifold(),
|
|
41
|
+
createManifold(),
|
|
42
|
+
createManifold(),
|
|
43
|
+
createManifold(),
|
|
44
|
+
createManifold(),
|
|
45
|
+
createManifold(),
|
|
46
|
+
createManifold(),
|
|
47
|
+
createManifold(),
|
|
48
|
+
createManifold(),
|
|
49
|
+
createManifold(),
|
|
50
|
+
createManifold(),
|
|
51
|
+
createManifold(),
|
|
52
|
+
createManifold(),
|
|
53
|
+
];
|
|
24
54
|
function copyManifold(from, to) {
|
|
25
55
|
to.nx = from.nx;
|
|
26
56
|
to.ny = from.ny;
|
|
@@ -626,12 +656,44 @@ export function shapecastWorld(bodies, tree, shape, pose, dx, dy, dz, out, filte
|
|
|
626
656
|
* since a step short of the true one converges rather than missing.
|
|
627
657
|
*/
|
|
628
658
|
if (isMesh(shape) || isMesh(other)) {
|
|
629
|
-
|
|
659
|
+
/*
|
|
660
|
+
* **Asked for within the travel that is left, not within everything.** The margin inflates
|
|
661
|
+
* the tree query's box, so a margin of a billion metres makes every triangle of the mesh a
|
|
662
|
+
* candidate — which is both the whole mesh's cost per step and the reason the set that came
|
|
663
|
+
* back was arbitrary. Nothing further away than the sweep can reach can stop it.
|
|
664
|
+
*/
|
|
665
|
+
const reach = travel * (1 - t) + 1e-4;
|
|
666
|
+
const met = collideMesh(shape, poseA, other, poseB, reach, MESH_SET);
|
|
667
|
+
if (met === 0)
|
|
630
668
|
break;
|
|
631
|
-
|
|
632
|
-
|
|
669
|
+
/*
|
|
670
|
+
* **Two different questions, and one manifold cannot answer both.** How far the sweep may
|
|
671
|
+
* advance is bounded by the *nearest* surface, whichever way it faces. Which surface
|
|
672
|
+
* *stopped* it is the nearest one facing back along the travel — and near a crease, where a
|
|
673
|
+
* wall meets a floor, those are not the same triangle. Answering the first for both is what
|
|
674
|
+
* made a capsule walking at a captured step sink through the floor: the wall beside it was
|
|
675
|
+
* always the nearer contact, so every downward sweep reported a horizontal normal and
|
|
676
|
+
* nothing ever stopped the fall. On box bodies, which have one manifold, the same walk is
|
|
677
|
+
* fine — which is why this only ever showed on a mesh.
|
|
678
|
+
*/
|
|
679
|
+
let blocking = -1;
|
|
680
|
+
let into = 1e-9;
|
|
681
|
+
for (let m = 0; m < met; m++) {
|
|
682
|
+
const held = MESH_SET[m];
|
|
683
|
+
if (held === undefined)
|
|
684
|
+
continue;
|
|
685
|
+
if ((held.separations[0] ?? 0) >= 1e-4)
|
|
686
|
+
continue;
|
|
687
|
+
const opposes = dx * held.nx + dy * held.ny + dz * held.nz;
|
|
688
|
+
if (opposes > into) {
|
|
689
|
+
into = opposes;
|
|
690
|
+
blocking = m;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const chosen = MESH_SET[blocking >= 0 ? blocking : 0];
|
|
694
|
+
if (chosen === undefined)
|
|
633
695
|
break;
|
|
634
|
-
copyManifold(
|
|
696
|
+
copyManifold(chosen, manifold);
|
|
635
697
|
}
|
|
636
698
|
else if (!collideShapes(shape, poseA, other, poseB, 1e9, manifold))
|
|
637
699
|
break;
|
package/dist/workerPool.js
CHANGED
|
@@ -107,9 +107,13 @@ export function createIslandPool(request) {
|
|
|
107
107
|
* A build that does not throws, which becomes a reason and serial physics — and `spawn` is the way
|
|
108
108
|
* out, which is why this is a default and not the only path. Null where there is no `Worker` at all.
|
|
109
109
|
*/
|
|
110
|
-
const defaultSpawn =
|
|
110
|
+
const defaultSpawn =
|
|
111
|
+
// platform: feature probe — no `Worker` means serial physics, with a reason
|
|
112
|
+
typeof Worker === 'undefined'
|
|
111
113
|
? null
|
|
112
|
-
: () =>
|
|
114
|
+
: () =>
|
|
115
|
+
// platform: browser default — `spawn` is the seam a native host supplies
|
|
116
|
+
new Worker(new URL('./islandWorker.js', import.meta.url), { type: 'module' });
|
|
113
117
|
/**
|
|
114
118
|
* The pool itself: `StagedExecutor` with the serial solve replaced by a fork and a join.
|
|
115
119
|
*
|
|
@@ -184,12 +188,15 @@ export class WorkerPoolExecutor extends StagedExecutor {
|
|
|
184
188
|
join() {
|
|
185
189
|
const ctl = this.control;
|
|
186
190
|
const wanted = this.workers.length;
|
|
187
|
-
|
|
191
|
+
/* Monotonic, because this is a spin against a deadline and the wall clock moves. A backward
|
|
192
|
+
step means the deadline never arrives and the join spins until the tab is closed — which is
|
|
193
|
+
the exact failure the deadline above was added to prevent. */
|
|
194
|
+
const deadline = performance.now() + JOIN_DEADLINE_MS;
|
|
188
195
|
for (;;) {
|
|
189
196
|
const done = Atomics.load(ctl, CTL_DONE);
|
|
190
197
|
if (done >= wanted)
|
|
191
198
|
return true;
|
|
192
|
-
if (
|
|
199
|
+
if (performance.now() > deadline) {
|
|
193
200
|
this.failure =
|
|
194
201
|
'a worker stopped answering, so island solving went back to this thread. The world is ' +
|
|
195
202
|
'correct and serial from here.';
|
|
@@ -223,9 +230,9 @@ export class WorkerPoolExecutor extends StagedExecutor {
|
|
|
223
230
|
plan: stage.plan,
|
|
224
231
|
});
|
|
225
232
|
}
|
|
226
|
-
const deadline =
|
|
233
|
+
const deadline = performance.now() + JOIN_DEADLINE_MS;
|
|
227
234
|
while (Atomics.load(ctl, CTL_ACK) < this.workers.length) {
|
|
228
|
-
if (
|
|
235
|
+
if (performance.now() > deadline) {
|
|
229
236
|
this.failure =
|
|
230
237
|
'the workers did not take the shared buffer, so island solving stayed on this thread.';
|
|
231
238
|
return false;
|
package/package.json
CHANGED
package/src/query.ts
CHANGED
|
@@ -71,6 +71,37 @@ const manifold = createManifold();
|
|
|
71
71
|
*/
|
|
72
72
|
const MESH_ONE = [createManifold()] as const;
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Manifolds a sweep against a mesh gathers before it decides how far it may advance.
|
|
76
|
+
*
|
|
77
|
+
* **Which triangle stopped a sweep is not a question one manifold can answer.** `collideMesh` fills
|
|
78
|
+
* its output in the order the tree hands triangles over, and the sweep read one of them — so near a
|
|
79
|
+
* crease, where a wall meets a floor, a downward sweep was answered with the wall's horizontal
|
|
80
|
+
* normal and nothing stopped the fall. Found by the first test of `CharacterController` against a
|
|
81
|
+
* triangle mesh: a capsule on a captured room's floor sank straight through it, while the same
|
|
82
|
+
* floor **on its own** held it, because adding the walls changed which triangle came back first.
|
|
83
|
+
* Sixteen is far more than a sweep meets at once, and the one facing back along the travel is the
|
|
84
|
+
* one in the way.
|
|
85
|
+
*/
|
|
86
|
+
const MESH_SET = [
|
|
87
|
+
createManifold(),
|
|
88
|
+
createManifold(),
|
|
89
|
+
createManifold(),
|
|
90
|
+
createManifold(),
|
|
91
|
+
createManifold(),
|
|
92
|
+
createManifold(),
|
|
93
|
+
createManifold(),
|
|
94
|
+
createManifold(),
|
|
95
|
+
createManifold(),
|
|
96
|
+
createManifold(),
|
|
97
|
+
createManifold(),
|
|
98
|
+
createManifold(),
|
|
99
|
+
createManifold(),
|
|
100
|
+
createManifold(),
|
|
101
|
+
createManifold(),
|
|
102
|
+
createManifold(),
|
|
103
|
+
] as const;
|
|
104
|
+
|
|
74
105
|
function copyManifold(from: typeof manifold, to: typeof manifold): void {
|
|
75
106
|
to.nx = from.nx;
|
|
76
107
|
to.ny = from.ny;
|
|
@@ -792,10 +823,40 @@ export function shapecastWorld(
|
|
|
792
823
|
* since a step short of the true one converges rather than missing.
|
|
793
824
|
*/
|
|
794
825
|
if (isMesh(shape) || isMesh(other)) {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
826
|
+
/*
|
|
827
|
+
* **Asked for within the travel that is left, not within everything.** The margin inflates
|
|
828
|
+
* the tree query's box, so a margin of a billion metres makes every triangle of the mesh a
|
|
829
|
+
* candidate — which is both the whole mesh's cost per step and the reason the set that came
|
|
830
|
+
* back was arbitrary. Nothing further away than the sweep can reach can stop it.
|
|
831
|
+
*/
|
|
832
|
+
const reach = travel * (1 - t) + 1e-4;
|
|
833
|
+
const met = collideMesh(shape, poseA, other, poseB, reach, MESH_SET);
|
|
834
|
+
if (met === 0) break;
|
|
835
|
+
/*
|
|
836
|
+
* **Two different questions, and one manifold cannot answer both.** How far the sweep may
|
|
837
|
+
* advance is bounded by the *nearest* surface, whichever way it faces. Which surface
|
|
838
|
+
* *stopped* it is the nearest one facing back along the travel — and near a crease, where a
|
|
839
|
+
* wall meets a floor, those are not the same triangle. Answering the first for both is what
|
|
840
|
+
* made a capsule walking at a captured step sink through the floor: the wall beside it was
|
|
841
|
+
* always the nearer contact, so every downward sweep reported a horizontal normal and
|
|
842
|
+
* nothing ever stopped the fall. On box bodies, which have one manifold, the same walk is
|
|
843
|
+
* fine — which is why this only ever showed on a mesh.
|
|
844
|
+
*/
|
|
845
|
+
let blocking = -1;
|
|
846
|
+
let into = 1e-9;
|
|
847
|
+
for (let m = 0; m < met; m++) {
|
|
848
|
+
const held = MESH_SET[m];
|
|
849
|
+
if (held === undefined) continue;
|
|
850
|
+
if ((held.separations[0] ?? 0) >= 1e-4) continue;
|
|
851
|
+
const opposes = dx * held.nx + dy * held.ny + dz * held.nz;
|
|
852
|
+
if (opposes > into) {
|
|
853
|
+
into = opposes;
|
|
854
|
+
blocking = m;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
const chosen = MESH_SET[blocking >= 0 ? blocking : 0];
|
|
858
|
+
if (chosen === undefined) break;
|
|
859
|
+
copyManifold(chosen, manifold);
|
|
799
860
|
} else if (!collideShapes(shape, poseA, other, poseB, 1e9, manifold)) break;
|
|
800
861
|
const gap = manifold.separations[0] ?? 0;
|
|
801
862
|
if (gap < 1e-4) {
|
package/src/workerPool.ts
CHANGED
|
@@ -141,9 +141,11 @@ export function createIslandPool(request: PoolRequest): PoolOutcome {
|
|
|
141
141
|
* out, which is why this is a default and not the only path. Null where there is no `Worker` at all.
|
|
142
142
|
*/
|
|
143
143
|
const defaultSpawn: (() => PoolWorker) | null =
|
|
144
|
+
// platform: feature probe — no `Worker` means serial physics, with a reason
|
|
144
145
|
typeof Worker === 'undefined'
|
|
145
146
|
? null
|
|
146
147
|
: (): PoolWorker =>
|
|
148
|
+
// platform: browser default — `spawn` is the seam a native host supplies
|
|
147
149
|
new Worker(new URL('./islandWorker.ts', import.meta.url), { type: 'module' }) as PoolWorker;
|
|
148
150
|
|
|
149
151
|
/**
|
|
@@ -227,11 +229,14 @@ export class WorkerPoolExecutor extends StagedExecutor implements Executor {
|
|
|
227
229
|
private join(): boolean {
|
|
228
230
|
const ctl = this.control;
|
|
229
231
|
const wanted = this.workers.length;
|
|
230
|
-
|
|
232
|
+
/* Monotonic, because this is a spin against a deadline and the wall clock moves. A backward
|
|
233
|
+
step means the deadline never arrives and the join spins until the tab is closed — which is
|
|
234
|
+
the exact failure the deadline above was added to prevent. */
|
|
235
|
+
const deadline = performance.now() + JOIN_DEADLINE_MS;
|
|
231
236
|
for (;;) {
|
|
232
237
|
const done = Atomics.load(ctl, CTL_DONE);
|
|
233
238
|
if (done >= wanted) return true;
|
|
234
|
-
if (
|
|
239
|
+
if (performance.now() > deadline) {
|
|
235
240
|
this.failure =
|
|
236
241
|
'a worker stopped answering, so island solving went back to this thread. The world is ' +
|
|
237
242
|
'correct and serial from here.';
|
|
@@ -267,9 +272,9 @@ export class WorkerPoolExecutor extends StagedExecutor implements Executor {
|
|
|
267
272
|
});
|
|
268
273
|
}
|
|
269
274
|
|
|
270
|
-
const deadline =
|
|
275
|
+
const deadline = performance.now() + JOIN_DEADLINE_MS;
|
|
271
276
|
while (Atomics.load(ctl, CTL_ACK) < this.workers.length) {
|
|
272
|
-
if (
|
|
277
|
+
if (performance.now() > deadline) {
|
|
273
278
|
this.failure =
|
|
274
279
|
'the workers did not take the shared buffer, so island solving stayed on this thread.';
|
|
275
280
|
return false;
|