reze-engine 0.35.0 → 0.36.1
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/README.md +4 -3
- package/dist/engine.js +2 -2
- package/dist/model.d.ts +0 -4
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +0 -16
- package/dist/physics/contact.d.ts +12 -3
- package/dist/physics/contact.d.ts.map +1 -1
- package/dist/physics/contact.js +5 -2
- package/dist/physics/manifold.d.ts +32 -11
- package/dist/physics/manifold.d.ts.map +1 -1
- package/dist/physics/manifold.js +137 -57
- package/dist/physics/solver.d.ts +7 -1
- package/dist/physics/solver.d.ts.map +1 -1
- package/dist/physics/solver.js +536 -182
- package/dist/physics/world.d.ts +2 -0
- package/dist/physics/world.d.ts.map +1 -1
- package/dist/physics/world.js +16 -43
- package/package.json +1 -1
- package/src/engine.ts +2 -2
- package/src/model.ts +0 -20
- package/src/physics/contact.ts +17 -7
- package/src/physics/manifold.ts +151 -0
- package/src/physics/solver.ts +520 -184
- package/src/physics/world.ts +17 -43
package/dist/physics/world.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface WindOptions {
|
|
|
29
29
|
export declare class World {
|
|
30
30
|
readonly gravity: Vec3;
|
|
31
31
|
solverIterations: number;
|
|
32
|
+
/** Per-pair contact impulse history behind warm starting. */
|
|
33
|
+
private manifolds;
|
|
32
34
|
private dampCacheDt;
|
|
33
35
|
private linDampFactor;
|
|
34
36
|
private angDampFactor;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"world.d.ts","sourceRoot":"","sources":["../../src/physics/world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAC9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAE5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAC1D,OAAO,
|
|
1
|
+
{"version":3,"file":"world.d.ts","sourceRoot":"","sources":["../../src/physics/world.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAC9B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAE5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAC1D,OAAO,EAAgE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAEzG,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAO1D;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B,iFAAiF;IACjF,SAAS,EAAE,IAAI,CAAA;IACf,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAA;IAChB;;;kEAG8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,wBAAwB;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,qBAAa,KAAK;IAChB,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAA;IACtB,gBAAgB,SAAK;IAErB,6DAA6D;IAC7D,OAAO,CAAC,SAAS,CAAsB;IAIvC,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,aAAa,CAA4B;IACjD,OAAO,CAAC,aAAa,CAA4B;IAIjD,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,aAAa,CAAO;IAC5B;gDAC4C;IAC5C,OAAO,CAAC,SAAS,CAAI;gBAET,OAAO,EAAE,IAAI;IAIzB,UAAU,CAAC,CAAC,EAAE,IAAI,GAAG,IAAI;IAMzB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI;IAoBvC,OAAO,IAAI,WAAW,GAAG,IAAI;IAW7B,IAAI,CACF,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,sBAAsB,EAAE,EACrC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,WAAW,EACrB,EAAE,EAAE,MAAM,GACT,IAAI;CA4IR"}
|
package/dist/physics/world.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Vec3 } from "../math";
|
|
2
2
|
import { RigidbodyType } from "./types";
|
|
3
|
-
import { solveConstraints } from "./solver";
|
|
3
|
+
import { solveConstraints, saveContactImpulses, applySplitImpulsePush } from "./solver";
|
|
4
|
+
import { ManifoldCache } from "./manifold";
|
|
4
5
|
import { findContacts } from "./contact";
|
|
5
6
|
export class World {
|
|
6
7
|
constructor(gravity) {
|
|
7
8
|
this.solverIterations = 10;
|
|
9
|
+
/** Per-pair contact impulse history behind warm starting. */
|
|
10
|
+
this.manifolds = new ManifoldCache();
|
|
8
11
|
// Per-body damping factors pow(1−damping, dt), cached because damping and
|
|
9
12
|
// the fixed dt never change — two Math.pow per body per substep otherwise.
|
|
10
13
|
this.dampCacheDt = -1;
|
|
@@ -121,50 +124,20 @@ export class World {
|
|
|
121
124
|
findContacts(store, contacts);
|
|
122
125
|
// 3. Solve joint + contact constraints (velocity-only).
|
|
123
126
|
if (constraints.length > 0 || contacts.count > 0) {
|
|
124
|
-
solveConstraints(store, constraints, cache, contacts, dt, this.solverIterations);
|
|
127
|
+
solveConstraints(store, constraints, cache, contacts, dt, this.solverIterations, this.manifolds);
|
|
128
|
+
applySplitImpulsePush(store, dt);
|
|
129
|
+
saveContactImpulses(store, contacts, this.manifolds);
|
|
125
130
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// it because it doesn't go through the velocity channel. Inverse-mass
|
|
129
|
-
// weighted so a kinematic body stays put and only the dynamic one
|
|
130
|
-
// translates.
|
|
131
|
-
// Softer and later than a rigid-body engine would use, deliberately. MMD
|
|
132
|
-
// cloth is a dense chain of small bodies resting on the character, and
|
|
133
|
-
// aggressive penetration recovery feeds those contacts energy that the
|
|
134
|
-
// joint springs hand straight back — the tips never settle. Measured on a
|
|
135
|
-
// 688-body model: resting peak velocity 1.79 → 1.10, and 12.9 → 6.6 under
|
|
136
|
-
// an idle animation, for a slop of two hundredths of a unit (under 2 mm at
|
|
137
|
-
// MMD scale) that no eye finds.
|
|
138
|
-
const POS_CORRECTION_FACTOR = 0.15;
|
|
139
|
-
const POS_SLOP = 0.02;
|
|
140
|
-
for (let ci = 0; ci < contacts.count; ci++) {
|
|
141
|
-
const c = contacts.get(ci);
|
|
142
|
-
if (c.depth <= POS_SLOP)
|
|
143
|
-
continue;
|
|
144
|
-
const imA = invMass[c.bodyA];
|
|
145
|
-
const imB = invMass[c.bodyB];
|
|
146
|
-
const total = imA + imB;
|
|
147
|
-
if (total <= 0)
|
|
148
|
-
continue;
|
|
149
|
-
const correction = (c.depth - POS_SLOP) * POS_CORRECTION_FACTOR;
|
|
150
|
-
const dx = correction * c.nx;
|
|
151
|
-
const dy = correction * c.ny;
|
|
152
|
-
const dz = correction * c.nz;
|
|
153
|
-
const ai = c.bodyA * 3;
|
|
154
|
-
const bi = c.bodyB * 3;
|
|
155
|
-
if (imA > 0) {
|
|
156
|
-
const fA = imA / total;
|
|
157
|
-
pos[ai + 0] -= dx * fA;
|
|
158
|
-
pos[ai + 1] -= dy * fA;
|
|
159
|
-
pos[ai + 2] -= dz * fA;
|
|
160
|
-
}
|
|
161
|
-
if (imB > 0) {
|
|
162
|
-
const fB = imB / total;
|
|
163
|
-
pos[bi + 0] += dx * fB;
|
|
164
|
-
pos[bi + 1] += dy * fB;
|
|
165
|
-
pos[bi + 2] += dz * fB;
|
|
166
|
-
}
|
|
131
|
+
else {
|
|
132
|
+
this.manifolds.clear();
|
|
167
133
|
}
|
|
134
|
+
// 4. Penetration recovery happens in the contact velocity row (Baumgarte,
|
|
135
|
+
// CONTACT_ERP), not here. Bullet 2.75 — the build MMD's physics runs —
|
|
136
|
+
// defaults m_splitImpulse to false and folds positionalError straight
|
|
137
|
+
// into m_rhs, so a separate position-translation pass is a divergence
|
|
138
|
+
// from what PMX rigs are authored against. It also could not be made to
|
|
139
|
+
// behave: every contact translated the body by its own full share, so a
|
|
140
|
+
// panel carrying dozens of rows was shoved out dozens of times.
|
|
168
141
|
// 5. Integrate. Cap angular velocity at π/2 per step — a high-impulse
|
|
169
142
|
// contact spike on a low-inertia body would otherwise spin past π
|
|
170
143
|
// in one step and trash the quaternion integration. Linear velocity
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -3024,7 +3024,7 @@ export class Engine {
|
|
|
3024
3024
|
|
|
3025
3025
|
dispose() {
|
|
3026
3026
|
this.stopRenderLoop()
|
|
3027
|
-
this.forEachInstance((inst) => inst.model.
|
|
3027
|
+
this.forEachInstance((inst) => inst.model.stop())
|
|
3028
3028
|
if (Engine.instance === this) Engine.instance = null
|
|
3029
3029
|
if (this.camera) this.camera.detachControl()
|
|
3030
3030
|
|
|
@@ -3100,7 +3100,7 @@ export class Engine {
|
|
|
3100
3100
|
removeModel(name: string): void {
|
|
3101
3101
|
const inst = this.modelInstances.get(name)
|
|
3102
3102
|
if (!inst) return
|
|
3103
|
-
inst.model.
|
|
3103
|
+
inst.model.stop()
|
|
3104
3104
|
for (const path of inst.textureCacheKeys) {
|
|
3105
3105
|
const tex = this.textureCache.get(path)
|
|
3106
3106
|
if (!tex) continue
|
package/src/model.ts
CHANGED
|
@@ -1485,20 +1485,10 @@ export class Model {
|
|
|
1485
1485
|
return true
|
|
1486
1486
|
}
|
|
1487
1487
|
|
|
1488
|
-
// @deprecated Use model.play()
|
|
1489
|
-
playAnimation(): void {
|
|
1490
|
-
this.animationState.play()
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
1488
|
pause(): void {
|
|
1494
1489
|
this.animationState.pause()
|
|
1495
1490
|
}
|
|
1496
1491
|
|
|
1497
|
-
// @deprecated Use model.pause()
|
|
1498
|
-
pauseAnimation(): void {
|
|
1499
|
-
this.animationState.pause()
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
1492
|
stop(): void {
|
|
1503
1493
|
this.blendEntries = null
|
|
1504
1494
|
this.crossfade = null
|
|
@@ -1506,11 +1496,6 @@ export class Model {
|
|
|
1506
1496
|
this.animationState.stop()
|
|
1507
1497
|
}
|
|
1508
1498
|
|
|
1509
|
-
// @deprecated Use model.stop()
|
|
1510
|
-
stopAnimation(): void {
|
|
1511
|
-
this.animationState.stop()
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
1499
|
/** Deactivate the current clip entirely (stop + forget). Unlike stop(), the
|
|
1515
1500
|
* pose is no longer re-applied each frame afterwards — follow with
|
|
1516
1501
|
* resetAllBones()/resetAllMorphs() to return to the bind pose. */
|
|
@@ -1528,11 +1513,6 @@ export class Model {
|
|
|
1528
1513
|
this.animationState.seek(seconds)
|
|
1529
1514
|
}
|
|
1530
1515
|
|
|
1531
|
-
// @deprecated Use model.seek()
|
|
1532
|
-
seekAnimation(seconds: number): void {
|
|
1533
|
-
this.animationState.seek(seconds)
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
1516
|
getAnimationProgress(): AnimationProgress {
|
|
1537
1517
|
const p = this.animationState.getProgress()
|
|
1538
1518
|
return {
|
package/src/physics/contact.ts
CHANGED
|
@@ -51,11 +51,18 @@ export interface Contact {
|
|
|
51
51
|
cBxN: number; cByN: number; cBzN: number // rB × n
|
|
52
52
|
jacInvN: number
|
|
53
53
|
bounceVel: number // restitution reference, captured at setup from initial relVelN
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
/** Substeps this contact point has persisted (from the manifold cache).
|
|
55
|
+
* Bullet kills restitution past m_restingContactRestitutionThreshold = 2. */
|
|
56
|
+
age: number
|
|
57
|
+
/** Penetration term routed to the SPLIT (pseudo-velocity) channel instead of
|
|
58
|
+
* the real one, when the contact is deeper than the split threshold. */
|
|
59
|
+
rhsPenetration: number
|
|
60
|
+
/** Accumulated impulse on the split channel — Bullet's m_appliedPushImpulse. */
|
|
61
|
+
appliedPushImpulse: number
|
|
62
|
+
/** Baumgarte bias, depth·ERP/dt — Bullet 2.75's positionalError. Positive
|
|
63
|
+
* when penetrating (pushes apart), negative when separated (allows the
|
|
64
|
+
* approach that closes the gap). See CONTACT_ERP. */
|
|
65
|
+
biasVel: number
|
|
59
66
|
// Friction tangent 1:
|
|
60
67
|
t1x: number; t1y: number; t1z: number
|
|
61
68
|
cAxT1: number; cAyT1: number; cAzT1: number
|
|
@@ -84,8 +91,10 @@ function makeContact(): Contact {
|
|
|
84
91
|
cBxN: 0, cByN: 0, cBzN: 0,
|
|
85
92
|
jacInvN: 0,
|
|
86
93
|
bounceVel: 0,
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
age: 0,
|
|
95
|
+
rhsPenetration: 0,
|
|
96
|
+
appliedPushImpulse: 0,
|
|
97
|
+
biasVel: 0,
|
|
89
98
|
t1x: 0, t1y: 0, t1z: 0,
|
|
90
99
|
cAxT1: 0, cAyT1: 0, cAzT1: 0,
|
|
91
100
|
cBxT1: 0, cByT1: 0, cBzT1: 0,
|
|
@@ -108,6 +117,7 @@ export class ContactPool {
|
|
|
108
117
|
c.appliedNormalImpulse = 0
|
|
109
118
|
c.appliedFrictionImpulse1 = 0
|
|
110
119
|
c.appliedFrictionImpulse2 = 0
|
|
120
|
+
c.appliedPushImpulse = 0
|
|
111
121
|
this.count++
|
|
112
122
|
return c
|
|
113
123
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Persistent contact manifolds — the cache that makes warm starting possible.
|
|
2
|
+
//
|
|
3
|
+
// Bullet 2.75 keeps a btPersistentManifold per body pair holding up to 4 points,
|
|
4
|
+
// each carrying the impulse it converged to last step. At setup the solver seeds
|
|
5
|
+
// each row with `cp.m_appliedImpulse * m_warmstartingFactor` (0.85) and applies
|
|
6
|
+
// it immediately, so a resting stack starts the substep already holding roughly
|
|
7
|
+
// the load it needs instead of rediscovering it from zero every time.
|
|
8
|
+
//
|
|
9
|
+
// That is not a nicety here: penetration recovery rides in the contact velocity
|
|
10
|
+
// row as a Baumgarte term, and a row rebuilt from zero each substep overshoots
|
|
11
|
+
// it. Warm starting on its own was measured WORSE on this engine — but that was
|
|
12
|
+
// against a solver with no bias term and a position-correction pass, which is a
|
|
13
|
+
// different system. The two are one design in Bullet and are ported as one.
|
|
14
|
+
//
|
|
15
|
+
// Points are matched by proximity in each body's own local frame, which is what
|
|
16
|
+
// btPersistentManifold does — a world-space match would drift with the body.
|
|
17
|
+
|
|
18
|
+
const MAX_POINTS = 4
|
|
19
|
+
/** Match radius, in model units. Bullet's gContactBreakingThreshold is 0.02;
|
|
20
|
+
* ours is the contact margin, so a point that merely slid along a face is
|
|
21
|
+
* still recognised as the same point rather than dropped and rebuilt. */
|
|
22
|
+
const MATCH_DIST_SQ = 0.04 * 0.04
|
|
23
|
+
|
|
24
|
+
interface Point {
|
|
25
|
+
/** Contact point in each body's local frame (lever arms are world-space and
|
|
26
|
+
* rotate with the body, so they cannot be compared across substeps). */
|
|
27
|
+
lax: number; lay: number; laz: number
|
|
28
|
+
lbx: number; lby: number; lbz: number
|
|
29
|
+
normalImpulse: number
|
|
30
|
+
frictionImpulse1: number
|
|
31
|
+
frictionImpulse2: number
|
|
32
|
+
/** Substeps this point has survived — Bullet disables restitution past
|
|
33
|
+
* m_restingContactRestitutionThreshold (2) so resting contacts stop bouncing. */
|
|
34
|
+
age: number
|
|
35
|
+
/** Marks points seen this substep; unseen ones are dropped. */
|
|
36
|
+
seen: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class ManifoldCache {
|
|
40
|
+
private pairs = new Map<number, Point[]>()
|
|
41
|
+
private touched = new Set<number>()
|
|
42
|
+
|
|
43
|
+
private static key(a: number, b: number): number {
|
|
44
|
+
return a < b ? a * 65536 + b : b * 65536 + a
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Look up what this contact point converged to last substep. Returns null
|
|
48
|
+
* when it is new. */
|
|
49
|
+
find(a: number, b: number, lax: number, lay: number, laz: number): Point | null {
|
|
50
|
+
const list = this.pairs.get(ManifoldCache.key(a, b))
|
|
51
|
+
if (list === undefined) return null
|
|
52
|
+
let best: Point | null = null
|
|
53
|
+
let bestD = MATCH_DIST_SQ
|
|
54
|
+
for (let i = 0; i < list.length; i++) {
|
|
55
|
+
const p = list[i]
|
|
56
|
+
const dx = p.lax - lax, dy = p.lay - lay, dz = p.laz - laz
|
|
57
|
+
const d = dx * dx + dy * dy + dz * dz
|
|
58
|
+
if (d < bestD) { bestD = d; best = p }
|
|
59
|
+
}
|
|
60
|
+
return best
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Record what this point converged to, for the next substep to start from. */
|
|
64
|
+
store(
|
|
65
|
+
a: number, b: number,
|
|
66
|
+
lax: number, lay: number, laz: number,
|
|
67
|
+
lbx: number, lby: number, lbz: number,
|
|
68
|
+
normalImpulse: number, frictionImpulse1: number, frictionImpulse2: number,
|
|
69
|
+
age: number,
|
|
70
|
+
): void {
|
|
71
|
+
const k = ManifoldCache.key(a, b)
|
|
72
|
+
this.touched.add(k)
|
|
73
|
+
let list = this.pairs.get(k)
|
|
74
|
+
if (list === undefined) { list = []; this.pairs.set(k, list) }
|
|
75
|
+
// Replace the nearest existing point, else append; past MAX_POINTS drop the
|
|
76
|
+
// shallowest-held one so the manifold keeps the load-bearing corners.
|
|
77
|
+
let best = -1
|
|
78
|
+
let bestD = MATCH_DIST_SQ
|
|
79
|
+
for (let i = 0; i < list.length; i++) {
|
|
80
|
+
const p = list[i]
|
|
81
|
+
const dx = p.lax - lax, dy = p.lay - lay, dz = p.laz - laz
|
|
82
|
+
const d = dx * dx + dy * dy + dz * dz
|
|
83
|
+
if (d < bestD) { bestD = d; best = i }
|
|
84
|
+
}
|
|
85
|
+
if (best < 0) {
|
|
86
|
+
if (list.length < MAX_POINTS) {
|
|
87
|
+
list.push({ lax, lay, laz, lbx, lby, lbz, normalImpulse, frictionImpulse1, frictionImpulse2, age, seen: true })
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
// btPersistentManifold::sortCachedPoints — when a 5th point arrives, drop
|
|
91
|
+
// whichever of the 5 leaves the largest quadrilateral. Area is what keeps
|
|
92
|
+
// a resting box from pivoting; dropping the shallowest instead can leave
|
|
93
|
+
// four nearly-collinear points that pin position but not orientation.
|
|
94
|
+
best = worstAreaIndex(list, lax, lay, laz)
|
|
95
|
+
}
|
|
96
|
+
const p = list[best]
|
|
97
|
+
p.lax = lax; p.lay = lay; p.laz = laz
|
|
98
|
+
p.lbx = lbx; p.lby = lby; p.lbz = lbz
|
|
99
|
+
p.normalImpulse = normalImpulse
|
|
100
|
+
p.frictionImpulse1 = frictionImpulse1
|
|
101
|
+
p.frictionImpulse2 = frictionImpulse2
|
|
102
|
+
p.age = age
|
|
103
|
+
p.seen = true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Drop every point not re-seen this substep, and every pair left empty.
|
|
107
|
+
* Without this a separated pair keeps handing back a stale impulse. */
|
|
108
|
+
endStep(): void {
|
|
109
|
+
for (const [k, list] of this.pairs) {
|
|
110
|
+
if (!this.touched.has(k)) { this.pairs.delete(k); continue }
|
|
111
|
+
let w = 0
|
|
112
|
+
for (let i = 0; i < list.length; i++) {
|
|
113
|
+
const p = list[i]
|
|
114
|
+
if (!p.seen) continue
|
|
115
|
+
p.seen = false
|
|
116
|
+
list[w++] = p
|
|
117
|
+
}
|
|
118
|
+
list.length = w
|
|
119
|
+
if (w === 0) this.pairs.delete(k)
|
|
120
|
+
}
|
|
121
|
+
this.touched.clear()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
clear(): void {
|
|
125
|
+
this.pairs.clear()
|
|
126
|
+
this.touched.clear()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
/** Which of the 4 cached points to replace so the surviving quad keeps the most
|
|
132
|
+
* area once the new point joins it. */
|
|
133
|
+
function worstAreaIndex(list: Point[], nx: number, ny: number, nz: number): number {
|
|
134
|
+
let bestIdx = 0
|
|
135
|
+
let bestArea = -1
|
|
136
|
+
for (let drop = 0; drop < list.length; drop++) {
|
|
137
|
+
// The quad is: the new point plus the three survivors.
|
|
138
|
+
const pts: number[][] = [[nx, ny, nz]]
|
|
139
|
+
for (let i = 0; i < list.length; i++) if (i !== drop) pts.push([list[i].lax, list[i].lay, list[i].laz])
|
|
140
|
+
if (pts.length < 4) continue
|
|
141
|
+
// |d0 × d1| over the diagonals — Bullet's area proxy.
|
|
142
|
+
const d0x = pts[0][0] - pts[2][0], d0y = pts[0][1] - pts[2][1], d0z = pts[0][2] - pts[2][2]
|
|
143
|
+
const d1x = pts[1][0] - pts[3][0], d1y = pts[1][1] - pts[3][1], d1z = pts[1][2] - pts[3][2]
|
|
144
|
+
const cx = d0y * d1z - d0z * d1y
|
|
145
|
+
const cy = d0z * d1x - d0x * d1z
|
|
146
|
+
const cz = d0x * d1y - d0y * d1x
|
|
147
|
+
const area = cx * cx + cy * cy + cz * cz
|
|
148
|
+
if (area > bestArea) { bestArea = area; bestIdx = drop }
|
|
149
|
+
}
|
|
150
|
+
return bestIdx
|
|
151
|
+
}
|