reze-engine 0.34.0 → 0.36.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/README.md +6 -5
- 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 +3 -0
- package/dist/physics/contact.d.ts.map +1 -1
- package/dist/physics/contact.js +386 -19
- package/dist/physics/manifold.d.ts +13 -0
- package/dist/physics/manifold.d.ts.map +1 -0
- package/dist/physics/manifold.js +68 -0
- package/dist/physics/solver.d.ts.map +1 -1
- package/dist/physics/solver.js +64 -4
- package/package.json +1 -1
- package/src/engine.ts +2 -2
- package/src/model.ts +0 -20
- package/src/physics/contact.ts +410 -15
- package/src/physics/solver.ts +64 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifold.d.ts","sourceRoot":"","sources":["../../src/physics/manifold.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAK5C,qBAAa,YAAY;IACvB,OAAO,CAAC,CAAC,CAAkC;IAC3C,OAAO,CAAC,CAAC,CAAkC;IAE3C;mFAC+E;IAC/E,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAmB7B;;yDAEqD;IACrD,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;CAgBnC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Contact impulse history — the cache that makes warm starting possible.
|
|
2
|
+
//
|
|
3
|
+
// Bullet keeps a btPersistentManifold per body pair and carries each point's
|
|
4
|
+
// m_appliedImpulse into the next substep at m_warmstartingFactor (0.85), so a
|
|
5
|
+
// resting contact starts from the answer it converged to last time instead of
|
|
6
|
+
// from zero. Without it a sustained contact is rediscovered every substep:
|
|
7
|
+
// it overshoots, releases, re-forms — which is chatter. Measured here, contacts
|
|
8
|
+
// account for ALL of the visible shake under animation (25 shaking bodies with
|
|
9
|
+
// contacts on, 0 with them off), so this is the mechanism that matters.
|
|
10
|
+
//
|
|
11
|
+
// Identity is by ORDINAL within the pair, not by proximity. Our narrowphase is
|
|
12
|
+
// deterministic — the same pair walks the same code path and emits its contacts
|
|
13
|
+
// in the same order every substep — so "the third contact of this pair" is a
|
|
14
|
+
// stabler key than "the cached point nearest this one", which needs a distance
|
|
15
|
+
// threshold and mis-pairs whenever two contacts fall inside it.
|
|
16
|
+
const MAX_PER_PAIR = 8;
|
|
17
|
+
const STRIDE = 3; // normal, friction1, friction2
|
|
18
|
+
export class ImpulseCache {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.a = new Map();
|
|
21
|
+
this.b = new Map();
|
|
22
|
+
}
|
|
23
|
+
/** Seed this substep's contacts with what the same rows converged to last
|
|
24
|
+
* substep. Contacts with no history start at zero, as new contacts should. */
|
|
25
|
+
seed(pool) {
|
|
26
|
+
const ord = new Map();
|
|
27
|
+
for (let i = 0; i < pool.count; i++) {
|
|
28
|
+
const c = pool.get(i);
|
|
29
|
+
const key = c.bodyA * 65536 + c.bodyB;
|
|
30
|
+
const n = ord.get(key) ?? 0;
|
|
31
|
+
ord.set(key, n + 1);
|
|
32
|
+
c.manifoldKey = key;
|
|
33
|
+
c.manifoldSlot = n;
|
|
34
|
+
if (n >= MAX_PER_PAIR)
|
|
35
|
+
continue;
|
|
36
|
+
const prev = this.a.get(key);
|
|
37
|
+
if (!prev)
|
|
38
|
+
continue;
|
|
39
|
+
const o = n * STRIDE;
|
|
40
|
+
c.appliedNormalImpulse = prev[o];
|
|
41
|
+
c.appliedFrictionImpulse1 = prev[o + 1];
|
|
42
|
+
c.appliedFrictionImpulse2 = prev[o + 2];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Store the converged impulses, then swap: this substep's results become the
|
|
46
|
+
* next one's history. Double-buffered so a pair that stops touching simply
|
|
47
|
+
* falls out rather than needing an expiry sweep. */
|
|
48
|
+
writeback(pool) {
|
|
49
|
+
this.b.clear();
|
|
50
|
+
for (let i = 0; i < pool.count; i++) {
|
|
51
|
+
const c = pool.get(i);
|
|
52
|
+
if (c.manifoldSlot >= MAX_PER_PAIR)
|
|
53
|
+
continue;
|
|
54
|
+
let arr = this.b.get(c.manifoldKey);
|
|
55
|
+
if (!arr) {
|
|
56
|
+
arr = new Float32Array(MAX_PER_PAIR * STRIDE);
|
|
57
|
+
this.b.set(c.manifoldKey, arr);
|
|
58
|
+
}
|
|
59
|
+
const o = c.manifoldSlot * STRIDE;
|
|
60
|
+
arr[o] = c.appliedNormalImpulse;
|
|
61
|
+
arr[o + 1] = c.appliedFrictionImpulse1;
|
|
62
|
+
arr[o + 2] = c.appliedFrictionImpulse2;
|
|
63
|
+
}
|
|
64
|
+
const t = this.a;
|
|
65
|
+
this.a = this.b;
|
|
66
|
+
this.b = t;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"solver.d.ts","sourceRoot":"","sources":["../../src/physics/solver.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAC5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAE1D,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"solver.d.ts","sourceRoot":"","sources":["../../src/physics/solver.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAC5C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAE1D,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,WAAW,CAAA;AAgFrD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,WAAW,EAAE,sBAAsB,EAAE,EACrC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,WAAW,EACrB,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,GACjB,IAAI,CA2CN;AAyCD,qBAAa,WAAW;IACtB,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAA;IACxB,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAA;IACtB;sEACkE;IAClE,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAA;gBACZ,WAAW,EAAE,sBAAsB,EAAE;CAUlD"}
|
package/dist/physics/solver.js
CHANGED
|
@@ -13,6 +13,29 @@
|
|
|
13
13
|
import { Mat4 } from "../math";
|
|
14
14
|
import { STOP_ERP } from "./constraint";
|
|
15
15
|
const BOUNCE_THRESHOLD = 2.0;
|
|
16
|
+
// Successive over-relaxation factor on CONTACT rows only (joints untouched).
|
|
17
|
+
// Each contact row independently drives the relative velocity at its own point
|
|
18
|
+
// to zero; when a dress panel carries up to 43 of them at once, they all
|
|
19
|
+
// correct the same motion and the body is over-braked, differently every
|
|
20
|
+
// substep. Scaling each row's step damps that without changing the fixed
|
|
21
|
+
// point — the accumulated impulse still converges to the same answer, just
|
|
22
|
+
// approached rather than overshot.
|
|
23
|
+
//
|
|
24
|
+
// The gain is PER CONTACT, scaled by how contended its two bodies are, not a
|
|
25
|
+
// flat constant. A flat factor was measured first and is wrong: it also slows
|
|
26
|
+
// the well-conditioned rows, and a body resting on a SINGLE contact can then
|
|
27
|
+
// no longer cancel its approach velocity within the iteration budget, so it
|
|
28
|
+
// creeps forever instead of settling (托特 peak speed at 15 s: 0.11 at gain
|
|
29
|
+
// 1.0, 0.46 at 0.5, 0.72 at 0.3 — a permanent limit cycle on a one-contact
|
|
30
|
+
// body). Over-constraint is a local property, so the remedy has to be local.
|
|
31
|
+
//
|
|
32
|
+
// gain = 1 / max(rows on A, rows on B), floored. One contact → 1.0, exact and
|
|
33
|
+
// settling preserved; a dress panel sharing 20 rows → 0.05, damped. This is
|
|
34
|
+
// the standard mass-splitting blend toward Jacobi in the contended cluster.
|
|
35
|
+
const CONTACT_SOR_MIN = 0.12;
|
|
36
|
+
// Per-body contact-row counts for the gain above; grown on demand, refilled
|
|
37
|
+
// each substep. Module-level so the solve allocates nothing.
|
|
38
|
+
let _rowCount = new Int32Array(0);
|
|
16
39
|
// Ceilings on limit-correction velocity. In normal operation limit errors are
|
|
17
40
|
// tiny; a large error only appears after a discontinuity (teleport, stall,
|
|
18
41
|
// deep penetration), and feeding err·ERP/dt to the solver unclamped then
|
|
@@ -74,8 +97,27 @@ export function solveConstraints(store, constraints, cache, contacts, dt, iterat
|
|
|
74
97
|
for (let c = 0; c < constraints.length; c++) {
|
|
75
98
|
setupConstraint(constraints[c], c, cache, store, dt, invDt);
|
|
76
99
|
}
|
|
100
|
+
// How many contact rows each body carries this substep — the per-contact
|
|
101
|
+
// relaxation gain below is a function of the more contended of its two
|
|
102
|
+
// bodies. Counting is O(contacts), done once, before any setup.
|
|
103
|
+
if (_rowCount.length < store.count)
|
|
104
|
+
_rowCount = new Int32Array(store.count);
|
|
105
|
+
else
|
|
106
|
+
_rowCount.fill(0, 0, store.count);
|
|
107
|
+
// Only DYNAMIC bodies are counted. A static body — above all the ground —
|
|
108
|
+
// collects a row from every body resting on it, and letting that inflate the
|
|
109
|
+
// count crushes the gain of each of those contacts to the floor, so nothing
|
|
110
|
+
// resting on the ground can build enough impulse and it creeps instead of
|
|
111
|
+
// settling. A body with invMass 0 cannot be over-braked in the first place.
|
|
112
|
+
for (let ci = 0; ci < contacts.count; ci++) {
|
|
113
|
+
const c = contacts.get(ci);
|
|
114
|
+
if (invMass[c.bodyA] > 0)
|
|
115
|
+
_rowCount[c.bodyA]++;
|
|
116
|
+
if (invMass[c.bodyB] > 0)
|
|
117
|
+
_rowCount[c.bodyB]++;
|
|
118
|
+
}
|
|
77
119
|
for (let ci = 0; ci < contacts.count; ci++) {
|
|
78
|
-
setupContactRow(contacts.get(ci), lv, av, invMass, W);
|
|
120
|
+
setupContactRow(contacts.get(ci), lv, av, invMass, W, invDt);
|
|
79
121
|
}
|
|
80
122
|
for (let iter = 0; iter < iterations; iter++) {
|
|
81
123
|
for (let c = 0; c < constraints.length; c++) {
|
|
@@ -692,7 +734,7 @@ function iterateConstraint(ci, cache, lv, av, invMass) {
|
|
|
692
734
|
// SETUP: pre-compute Jacobians, friction basis, and the bounce reference
|
|
693
735
|
// from the *initial* closing velocity (Bullet's pattern — captures restitution
|
|
694
736
|
// before iter 1 zeroes out the approach).
|
|
695
|
-
function setupContactRow(c, lv, av, invMass, W) {
|
|
737
|
+
function setupContactRow(c, lv, av, invMass, W, invDt) {
|
|
696
738
|
const ai = c.bodyA * 3;
|
|
697
739
|
const bi = c.bodyB * 3;
|
|
698
740
|
const a9 = c.bodyA * 9;
|
|
@@ -736,6 +778,24 @@ function setupContactRow(c, lv, av, invMass, W) {
|
|
|
736
778
|
c.bounceVel = c.restitution > 0 && relVelN0 < -BOUNCE_THRESHOLD
|
|
737
779
|
? -c.restitution * relVelN0
|
|
738
780
|
: 0;
|
|
781
|
+
// Speculative rows (depth < 0 — the shapes are inside the margin band but
|
|
782
|
+
// NOT touching) must not brake a body that hasn't arrived yet. Their whole
|
|
783
|
+
// job is to stop it crossing the surface within this substep, so the
|
|
784
|
+
// approach speed they leave alone is exactly the one that closes the
|
|
785
|
+
// remaining gap in dt; only the excess above that is cancelled.
|
|
786
|
+
//
|
|
787
|
+
// Without this the row targets relVelN = 0 like a touching contact and
|
|
788
|
+
// stops approaching bodies dead up to CONTACT_MARGIN away from anything.
|
|
789
|
+
// The push-only clamp does NOT prevent that — it only forbids a negative
|
|
790
|
+
// (pulling) impulse, not a large positive one on a body in mid-air. On a
|
|
791
|
+
// dress rig half of all contact rows are speculative and 88% of them fire,
|
|
792
|
+
// which is the field of invisible brakes the cloth was shaking against.
|
|
793
|
+
c.allowedApproachVel = c.depth < 0 ? -c.depth * invDt : 0;
|
|
794
|
+
// Relaxation gain, from the more contended of the two bodies (see
|
|
795
|
+
// CONTACT_SOR_MIN). A lone contact keeps gain 1.0 and stays exact.
|
|
796
|
+
const contended = _rowCount[c.bodyA] > _rowCount[c.bodyB] ? _rowCount[c.bodyA] : _rowCount[c.bodyB];
|
|
797
|
+
const gain = contended > 1 ? 1 / contended : 1;
|
|
798
|
+
c.sorGain = gain < CONTACT_SOR_MIN ? CONTACT_SOR_MIN : gain;
|
|
739
799
|
// Friction tangent basis. Pick the axis least aligned with n.
|
|
740
800
|
let t1x, t1y, t1z;
|
|
741
801
|
if (Math.abs(nx) < 0.7071) {
|
|
@@ -840,7 +900,7 @@ function iterateContactRow(c, lv, av, invMass) {
|
|
|
840
900
|
if (jacInvN > 0) {
|
|
841
901
|
const nx = c.nx, ny = c.ny, nz = c.nz;
|
|
842
902
|
const relVelN = dvx * nx + dvy * ny + dvz * nz;
|
|
843
|
-
let dImpN = (c.bounceVel - relVelN) * jacInvN;
|
|
903
|
+
let dImpN = (c.bounceVel - c.allowedApproachVel - relVelN) * jacInvN * c.sorGain;
|
|
844
904
|
const oldN = c.appliedNormalImpulse;
|
|
845
905
|
let newN = oldN + dImpN;
|
|
846
906
|
if (newN < 0) {
|
|
@@ -890,7 +950,7 @@ function applyFrictionTangent(c, ai, bi, dvx, dvy, dvz, tx, ty, tz, cAx, cAy, cA
|
|
|
890
950
|
if (jacInv <= 0)
|
|
891
951
|
return;
|
|
892
952
|
const relVel = dvx * tx + dvy * ty + dvz * tz;
|
|
893
|
-
let dImp = -relVel * jacInv;
|
|
953
|
+
let dImp = -relVel * jacInv * c.sorGain;
|
|
894
954
|
const old = slot === 1 ? c.appliedFrictionImpulse1 : c.appliedFrictionImpulse2;
|
|
895
955
|
let next = old + dImp;
|
|
896
956
|
if (next < -muNormal) {
|
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 {
|