@driftengine/physics 3.61.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +503 -0
- package/dist/bodies.d.ts +121 -0
- package/dist/bodies.js +298 -0
- package/dist/buoyancy.d.ts +46 -0
- package/dist/buoyancy.js +45 -0
- package/dist/cloth.d.ts +226 -0
- package/dist/cloth.js +587 -0
- package/dist/collide/aabb.d.ts +10 -0
- package/dist/collide/aabb.js +11 -0
- package/dist/collide/body.d.ts +202 -0
- package/dist/collide/body.js +142 -0
- package/dist/collide/eject.d.ts +39 -0
- package/dist/collide/eject.js +142 -0
- package/dist/collide/index.d.ts +18 -0
- package/dist/collide/index.js +5 -0
- package/dist/collide/scratch.d.ts +8 -0
- package/dist/collide/scratch.js +9 -0
- package/dist/collide/segment.d.ts +12 -0
- package/dist/collide/segment.js +109 -0
- package/dist/collide/sweep.d.ts +3 -0
- package/dist/collide/sweep.js +1036 -0
- package/dist/colliderSet.d.ts +239 -0
- package/dist/colliderSet.js +546 -0
- package/dist/compoundContact.d.ts +70 -0
- package/dist/compoundContact.js +221 -0
- package/dist/controller.d.ts +351 -0
- package/dist/controller.js +641 -0
- package/dist/cylinder.d.ts +78 -0
- package/dist/cylinder.js +294 -0
- package/dist/cylinderContact.d.ts +7 -0
- package/dist/cylinderContact.js +704 -0
- package/dist/decompose.d.ts +105 -0
- package/dist/decompose.js +979 -0
- package/dist/events.d.ts +43 -0
- package/dist/events.js +95 -0
- package/dist/executor.d.ts +147 -0
- package/dist/executor.js +173 -0
- package/dist/faces.d.ts +26 -0
- package/dist/faces.js +34 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.js +139 -0
- package/dist/groundMotor.d.ts +34 -0
- package/dist/groundMotor.js +41 -0
- package/dist/heightfieldShape.d.ts +128 -0
- package/dist/heightfieldShape.js +328 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +47 -0
- package/dist/island.d.ts +55 -0
- package/dist/island.js +227 -0
- package/dist/islandSolve.d.ts +70 -0
- package/dist/islandSolve.js +171 -0
- package/dist/islandWorker.d.ts +18 -0
- package/dist/islandWorker.js +125 -0
- package/dist/joints.d.ts +180 -0
- package/dist/joints.js +1094 -0
- package/dist/manifold.d.ts +89 -0
- package/dist/manifold.js +608 -0
- package/dist/mass.d.ts +53 -0
- package/dist/mass.js +353 -0
- package/dist/meshContact.d.ts +57 -0
- package/dist/meshContact.js +393 -0
- package/dist/meshShape.d.ts +76 -0
- package/dist/meshShape.js +175 -0
- package/dist/pairs.d.ts +62 -0
- package/dist/pairs.js +154 -0
- package/dist/query.d.ts +71 -0
- package/dist/query.js +683 -0
- package/dist/ragdoll.d.ts +135 -0
- package/dist/ragdoll.js +489 -0
- package/dist/shape.d.ts +177 -0
- package/dist/shape.js +533 -0
- package/dist/solver.d.ts +136 -0
- package/dist/solver.js +601 -0
- package/dist/stage.d.ts +167 -0
- package/dist/stage.js +350 -0
- package/dist/stallEscape.d.ts +141 -0
- package/dist/stallEscape.js +254 -0
- package/dist/sweptStep.d.ts +173 -0
- package/dist/sweptStep.js +472 -0
- package/dist/tree.d.ts +61 -0
- package/dist/tree.js +348 -0
- package/dist/vehicle.d.ts +154 -0
- package/dist/vehicle.js +343 -0
- package/dist/workerPool.d.ts +107 -0
- package/dist/workerPool.js +239 -0
- package/dist/workers.d.ts +37 -0
- package/dist/workers.js +35 -0
- package/dist/world.d.ts +298 -0
- package/dist/world.js +712 -0
- package/package.json +55 -0
- package/src/bodies.ts +369 -0
- package/src/buoyancy.ts +78 -0
- package/src/cloth.ts +734 -0
- package/src/collide/aabb.ts +28 -0
- package/src/collide/body.ts +336 -0
- package/src/collide/eject.ts +156 -0
- package/src/collide/index.ts +26 -0
- package/src/collide/scratch.ts +10 -0
- package/src/collide/segment.ts +127 -0
- package/src/collide/sweep.ts +1132 -0
- package/src/colliderSet.ts +637 -0
- package/src/compoundContact.ts +284 -0
- package/src/controller.ts +817 -0
- package/src/cylinder.ts +340 -0
- package/src/cylinderContact.ts +825 -0
- package/src/decompose.ts +1198 -0
- package/src/events.ts +108 -0
- package/src/executor.ts +224 -0
- package/src/faces.ts +37 -0
- package/src/fingerprint.ts +146 -0
- package/src/groundMotor.ts +53 -0
- package/src/heightfieldShape.ts +395 -0
- package/src/index.ts +164 -0
- package/src/island.ts +219 -0
- package/src/islandSolve.ts +218 -0
- package/src/islandWorker.ts +141 -0
- package/src/joints.ts +1367 -0
- package/src/manifold.ts +798 -0
- package/src/mass.ts +424 -0
- package/src/meshContact.ts +443 -0
- package/src/meshShape.ts +261 -0
- package/src/pairs.ts +169 -0
- package/src/query.ts +844 -0
- package/src/ragdoll.ts +662 -0
- package/src/shape.ts +656 -0
- package/src/solver.ts +873 -0
- package/src/stage.ts +406 -0
- package/src/stallEscape.ts +362 -0
- package/src/sweptStep.ts +609 -0
- package/src/tree.ts +366 -0
- package/src/vehicle.ts +489 -0
- package/src/workerPool.ts +283 -0
- package/src/workers.ts +38 -0
- package/src/world.ts +961 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ConvexShape } from './shape.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Contact manifolds: which way two shapes separate, by how much, and where they touch.
|
|
4
|
+
*
|
|
5
|
+
* **Three cases, not one, and the split is what makes each of them exact.** A `ConvexShape` is a
|
|
6
|
+
* point cloud plus a rounding radius, so a shape with faces is a polytope and one with a point or
|
|
7
|
+
* two is a sphere or a capsule. Separating-axis tests are exact for two polytopes and *not* exact
|
|
8
|
+
* for a rounded shape against one, because the Minkowski sum of a polytope and a ball is curved
|
|
9
|
+
* wherever the polytope has an edge or a vertex, and no enumerated axis points along that curve.
|
|
10
|
+
* So:
|
|
11
|
+
*
|
|
12
|
+
* - **polytope against polytope** is SAT over the axes the shape already enumerates, then
|
|
13
|
+
* Sutherland-Hodgman clipping of the incident face against the reference face's side planes;
|
|
14
|
+
* - **sphere or capsule against polytope** is the closest point on the polytope to each of the one
|
|
15
|
+
* or two points, which the face loops make exact and which needs no axis list at all;
|
|
16
|
+
* - **sphere or capsule against either** is the closest points of two segments.
|
|
17
|
+
*
|
|
18
|
+
* **A fourth case joined them on 2026-08-27 and lives next door**: a cylinder has a curved side, so
|
|
19
|
+
* no list of axes read off the shape can be exact for it, and `cylinderContact.ts` builds one
|
|
20
|
+
* against the *other* shape instead. It is dispatched first below, before anything reads a face
|
|
21
|
+
* count, because a cylinder has no faces and would otherwise be mistaken for a capsule.
|
|
22
|
+
*
|
|
23
|
+
* A capsule against a face produces two contacts, one per endpoint, which is what stops a capsule
|
|
24
|
+
* lying on the ground from rocking. **What that gives up** is a capsule crossing an edge at an
|
|
25
|
+
* angle, where two endpoint contacts approximate a contact that is really along the edge. **What
|
|
26
|
+
* would make it wrong** is a consumer rolling capsules over rough terrain and seeing them catch;
|
|
27
|
+
* the answer then is a segment-against-polytope clip rather than two point queries.
|
|
28
|
+
*
|
|
29
|
+
* **Every contact point carries a feature id**, and that is the load-bearing part. It is how a
|
|
30
|
+
* manifold is matched to the previous tick's so accumulated impulses warm-start the next solve.
|
|
31
|
+
* Matching by proximity within a tolerance is the usual alternative and is both order-dependent
|
|
32
|
+
* and tolerance-dependent, which the determinism contract forbids twice over.
|
|
33
|
+
*/
|
|
34
|
+
/** Where a shape sits: a position and a unit quaternion. */
|
|
35
|
+
export interface ShapePose {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
z: number;
|
|
39
|
+
qx: number;
|
|
40
|
+
qy: number;
|
|
41
|
+
qz: number;
|
|
42
|
+
qw: number;
|
|
43
|
+
}
|
|
44
|
+
/** At most four points survive reduction, which is enough to hold a face flat. */
|
|
45
|
+
export declare const MAX_CONTACTS = 4;
|
|
46
|
+
export interface Manifold {
|
|
47
|
+
/** Unit, pointing from A to B. */
|
|
48
|
+
nx: number;
|
|
49
|
+
ny: number;
|
|
50
|
+
nz: number;
|
|
51
|
+
count: number;
|
|
52
|
+
/** World contact points, xyz-packed, `count` of them. */
|
|
53
|
+
points: Float32Array;
|
|
54
|
+
/** Signed gap per point. Negative is overlap. */
|
|
55
|
+
separations: Float32Array;
|
|
56
|
+
/** Stable per contact across ticks, which is what warm starting needs. */
|
|
57
|
+
featureIds: Int32Array;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the contact rides a **curved** part of A's surface, and the same for B.
|
|
60
|
+
*
|
|
61
|
+
* The solver stores each anchor in its body's own frame and rotates it forward through the
|
|
62
|
+
* substeps, which is exact for a face or a corner and wrong for a ball: the point where a rolling
|
|
63
|
+
* sphere touches the floor stays at the bottom, and an anchor that turns with the body climbs the
|
|
64
|
+
* side instead. **Measured before it was fixed**: a sphere of radius 1 rolling at 6 rad/s sank
|
|
65
|
+
* 28 mm into the floor and stayed there, because every step's anchor drift read as a gap the
|
|
66
|
+
* solver then let close. A cylinder did the same, and a box did not.
|
|
67
|
+
*
|
|
68
|
+
* So a curved anchor is held in **world** frame instead, where a constant offset from the centre
|
|
69
|
+
* is the right answer for the whole tick. **What that gives up** is the tangential correction for
|
|
70
|
+
* a body that genuinely turns under the contact within one step — second order over a sixtieth of
|
|
71
|
+
* a second, against a first-order error the other way. **What would make it wrong** is a
|
|
72
|
+
* consumer stepping at a rate slow enough for a body to turn appreciably inside one step, where
|
|
73
|
+
* the answer is a narrowphase per substep and a much larger bill.
|
|
74
|
+
*
|
|
75
|
+
* A cylinder sets these per *feature*: its cap is flat and turns with the body, so a cap contact
|
|
76
|
+
* is not curved, while its side and its rim are.
|
|
77
|
+
*/
|
|
78
|
+
curvedA: boolean;
|
|
79
|
+
curvedB: boolean;
|
|
80
|
+
}
|
|
81
|
+
export declare function createManifold(): Manifold;
|
|
82
|
+
/**
|
|
83
|
+
* Fill `out` and return whether the shapes are within `margin` of touching.
|
|
84
|
+
*
|
|
85
|
+
* A positive separation inside the margin is a **speculative** contact: the constraint will permit
|
|
86
|
+
* approach up to exactly that gap and no further, which is what gives continuous collision without
|
|
87
|
+
* a separate time-of-impact pass.
|
|
88
|
+
*/
|
|
89
|
+
export declare function collideShapes(shapeA: ConvexShape, poseA: ShapePose, shapeB: ConvexShape, poseB: ShapePose, margin: number, out: Manifold): boolean;
|
package/dist/manifold.js
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { isCylinder } from './cylinder.js';
|
|
2
|
+
import { collideCylinder } from './cylinderContact.js';
|
|
3
|
+
import { faceCount, faceVertices } from './faces.js';
|
|
4
|
+
/** At most four points survive reduction, which is enough to hold a face flat. */
|
|
5
|
+
export const MAX_CONTACTS = 4;
|
|
6
|
+
export function createManifold() {
|
|
7
|
+
return {
|
|
8
|
+
nx: 0,
|
|
9
|
+
ny: 1,
|
|
10
|
+
nz: 0,
|
|
11
|
+
count: 0,
|
|
12
|
+
points: new Float32Array(MAX_CONTACTS * 3),
|
|
13
|
+
separations: new Float32Array(MAX_CONTACTS),
|
|
14
|
+
featureIds: new Int32Array(MAX_CONTACTS),
|
|
15
|
+
curvedA: false,
|
|
16
|
+
curvedB: false,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Module scratch. Nothing here allocates per call. */
|
|
20
|
+
const MAX_POINTS = 64;
|
|
21
|
+
const worldA = new Float32Array(MAX_POINTS * 3);
|
|
22
|
+
const worldB = new Float32Array(MAX_POINTS * 3);
|
|
23
|
+
const planesA = new Float32Array(MAX_POINTS * 4);
|
|
24
|
+
const planesB = new Float32Array(MAX_POINTS * 4);
|
|
25
|
+
const loop = new Uint16Array(MAX_POINTS);
|
|
26
|
+
const clipIn = new Float32Array(MAX_POINTS * 3);
|
|
27
|
+
const clipOut = new Float32Array(MAX_POINTS * 3);
|
|
28
|
+
const clipInId = new Int32Array(MAX_POINTS);
|
|
29
|
+
const clipOutId = new Int32Array(MAX_POINTS);
|
|
30
|
+
const rot = new Float32Array(9);
|
|
31
|
+
/**
|
|
32
|
+
* Fill `out` and return whether the shapes are within `margin` of touching.
|
|
33
|
+
*
|
|
34
|
+
* A positive separation inside the margin is a **speculative** contact: the constraint will permit
|
|
35
|
+
* approach up to exactly that gap and no further, which is what gives continuous collision without
|
|
36
|
+
* a separate time-of-impact pass.
|
|
37
|
+
*/
|
|
38
|
+
export function collideShapes(shapeA, poseA, shapeB, poseB, margin, out) {
|
|
39
|
+
out.count = 0;
|
|
40
|
+
if (isCylinder(shapeA) || isCylinder(shapeB)) {
|
|
41
|
+
return collideCylinder(shapeA, poseA, shapeB, poseB, margin, out);
|
|
42
|
+
}
|
|
43
|
+
const facesA = faceCount(shapeA);
|
|
44
|
+
const facesB = faceCount(shapeB);
|
|
45
|
+
// A faceless shape is a ball or a capsule, and every point of it that can touch anything is
|
|
46
|
+
// curved. A polytope's contacts are on faces, edges and corners, all of which turn with it.
|
|
47
|
+
out.curvedA = facesA === 0;
|
|
48
|
+
out.curvedB = facesB === 0;
|
|
49
|
+
toWorld(shapeA, poseA, worldA);
|
|
50
|
+
toWorld(shapeB, poseB, worldB);
|
|
51
|
+
if (facesA === 0 && facesB === 0)
|
|
52
|
+
return roundRound(shapeA, shapeB, margin, out);
|
|
53
|
+
if (facesA === 0)
|
|
54
|
+
return roundPolytope(shapeA, shapeB, poseB, margin, out, false);
|
|
55
|
+
if (facesB === 0)
|
|
56
|
+
return roundPolytope(shapeB, shapeA, poseA, margin, out, true);
|
|
57
|
+
planesToWorld(shapeA, poseA, worldA, planesA);
|
|
58
|
+
planesToWorld(shapeB, poseB, worldB, planesB);
|
|
59
|
+
return polytopePolytope(shapeA, shapeB, facesA, facesB, margin, out);
|
|
60
|
+
}
|
|
61
|
+
/* ---------- sphere or capsule against sphere or capsule ---------- */
|
|
62
|
+
function roundRound(a, b, margin, out) {
|
|
63
|
+
const na = a.vertices.length / 3;
|
|
64
|
+
const nb = b.vertices.length / 3;
|
|
65
|
+
const ax = worldA[0] ?? 0;
|
|
66
|
+
const ay = worldA[1] ?? 0;
|
|
67
|
+
const az = worldA[2] ?? 0;
|
|
68
|
+
const bx = na > 1 ? (worldA[3] ?? 0) : ax;
|
|
69
|
+
const by = na > 1 ? (worldA[4] ?? 0) : ay;
|
|
70
|
+
const bz = na > 1 ? (worldA[5] ?? 0) : az;
|
|
71
|
+
const cx = worldB[0] ?? 0;
|
|
72
|
+
const cy = worldB[1] ?? 0;
|
|
73
|
+
const cz = worldB[2] ?? 0;
|
|
74
|
+
const dx = nb > 1 ? (worldB[3] ?? 0) : cx;
|
|
75
|
+
const dy = nb > 1 ? (worldB[4] ?? 0) : cy;
|
|
76
|
+
const dz = nb > 1 ? (worldB[5] ?? 0) : cz;
|
|
77
|
+
segmentClosest(ax, ay, az, bx, by, bz, cx, cy, cz, dx, dy, dz);
|
|
78
|
+
let ux = CLOSEST[3] - CLOSEST[0];
|
|
79
|
+
let uy = CLOSEST[4] - CLOSEST[1];
|
|
80
|
+
let uz = CLOSEST[5] - CLOSEST[2];
|
|
81
|
+
let d = Math.sqrt(ux * ux + uy * uy + uz * uz);
|
|
82
|
+
if (d === 0) {
|
|
83
|
+
/*
|
|
84
|
+
* The axes intersect, so there is no direction *between* them. Any unit vector is as good as
|
|
85
|
+
* another and world up is the stable choice; the distance stays **zero**, which is what it is.
|
|
86
|
+
*
|
|
87
|
+
* Setting it to one instead — as this did — fabricates a separation of a whole metre and makes
|
|
88
|
+
* two fully overlapped capsules report no contact at all. Found by a test whose own geometry
|
|
89
|
+
* was wrong, which is the only reason the case was reached.
|
|
90
|
+
*/
|
|
91
|
+
ux = 0;
|
|
92
|
+
uy = 1;
|
|
93
|
+
uz = 0;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
ux /= d;
|
|
97
|
+
uy /= d;
|
|
98
|
+
uz /= d;
|
|
99
|
+
}
|
|
100
|
+
const separation = d - a.radius - b.radius;
|
|
101
|
+
if (separation > margin)
|
|
102
|
+
return false;
|
|
103
|
+
out.nx = ux;
|
|
104
|
+
out.ny = uy;
|
|
105
|
+
out.nz = uz;
|
|
106
|
+
out.count = 1;
|
|
107
|
+
// Midway between the two surfaces, which is where the constraint should act.
|
|
108
|
+
out.points[0] = CLOSEST[0] + ux * (a.radius + separation / 2);
|
|
109
|
+
out.points[1] = CLOSEST[1] + uy * (a.radius + separation / 2);
|
|
110
|
+
out.points[2] = CLOSEST[2] + uz * (a.radius + separation / 2);
|
|
111
|
+
out.separations[0] = separation;
|
|
112
|
+
out.featureIds[0] = 0;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
/* ---------- sphere or capsule against a polytope ---------- */
|
|
116
|
+
function roundPolytope(round, poly, polyPose, margin, out, flipped) {
|
|
117
|
+
// `worldA` holds whichever shape was passed first, so read the round one from the right buffer.
|
|
118
|
+
const roundWorld = flipped ? worldB : worldA;
|
|
119
|
+
const polyWorld = flipped ? worldA : worldB;
|
|
120
|
+
planesToWorld(poly, polyPose, polyWorld, planesB);
|
|
121
|
+
const points = round.vertices.length / 3;
|
|
122
|
+
const faces = faceCount(poly);
|
|
123
|
+
let count = 0;
|
|
124
|
+
let bestSep = Infinity;
|
|
125
|
+
let nx = 0;
|
|
126
|
+
let ny = 1;
|
|
127
|
+
let nz = 0;
|
|
128
|
+
for (let p = 0; p < points; p++) {
|
|
129
|
+
const px = roundWorld[p * 3] ?? 0;
|
|
130
|
+
const py = roundWorld[p * 3 + 1] ?? 0;
|
|
131
|
+
const pz = roundWorld[p * 3 + 2] ?? 0;
|
|
132
|
+
closestOnPolytope(poly, polyWorld, planesB, faces, px, py, pz);
|
|
133
|
+
let ux = px - CLOSEST[0];
|
|
134
|
+
let uy = py - CLOSEST[1];
|
|
135
|
+
let uz = pz - CLOSEST[2];
|
|
136
|
+
let d = Math.sqrt(ux * ux + uy * uy + uz * uz);
|
|
137
|
+
if (CLOSEST[6] < 0) {
|
|
138
|
+
// Inside: the closest face's outward normal is the direction, and depth is its plane distance.
|
|
139
|
+
ux = CLOSEST[3];
|
|
140
|
+
uy = CLOSEST[4];
|
|
141
|
+
uz = CLOSEST[5];
|
|
142
|
+
d = CLOSEST[6];
|
|
143
|
+
}
|
|
144
|
+
else if (d === 0) {
|
|
145
|
+
ux = CLOSEST[3];
|
|
146
|
+
uy = CLOSEST[4];
|
|
147
|
+
uz = CLOSEST[5];
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
ux /= d;
|
|
151
|
+
uy /= d;
|
|
152
|
+
uz /= d;
|
|
153
|
+
}
|
|
154
|
+
const separation = d - round.radius - poly.radius;
|
|
155
|
+
if (separation > margin)
|
|
156
|
+
continue;
|
|
157
|
+
if (count >= MAX_CONTACTS)
|
|
158
|
+
break;
|
|
159
|
+
// Normal points from the round shape toward the polytope, then flipped if A was the polytope.
|
|
160
|
+
const sx = flipped ? ux : -ux;
|
|
161
|
+
const sy = flipped ? uy : -uy;
|
|
162
|
+
const sz = flipped ? uz : -uz;
|
|
163
|
+
if (separation < bestSep) {
|
|
164
|
+
bestSep = separation;
|
|
165
|
+
nx = sx;
|
|
166
|
+
ny = sy;
|
|
167
|
+
nz = sz;
|
|
168
|
+
}
|
|
169
|
+
const at = count * 3;
|
|
170
|
+
out.points[at] = CLOSEST[0];
|
|
171
|
+
out.points[at + 1] = CLOSEST[1];
|
|
172
|
+
out.points[at + 2] = CLOSEST[2];
|
|
173
|
+
out.separations[count] = separation;
|
|
174
|
+
// The endpoint that produced it, which is stable while the capsule keeps its orientation.
|
|
175
|
+
out.featureIds[count] = 0x40000000 | p;
|
|
176
|
+
count++;
|
|
177
|
+
}
|
|
178
|
+
if (count === 0)
|
|
179
|
+
return false;
|
|
180
|
+
out.nx = nx;
|
|
181
|
+
out.ny = ny;
|
|
182
|
+
out.nz = nz;
|
|
183
|
+
out.count = count;
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
/* ---------- polytope against polytope ---------- */
|
|
187
|
+
function polytopePolytope(a, b, facesA, facesB, margin, out) {
|
|
188
|
+
const na = a.vertices.length / 3;
|
|
189
|
+
const nb = b.vertices.length / 3;
|
|
190
|
+
const radii = a.radius + b.radius;
|
|
191
|
+
let best = -Infinity;
|
|
192
|
+
let bestFace = -1;
|
|
193
|
+
let bestFromA = true;
|
|
194
|
+
for (let f = 0; f < facesA; f++) {
|
|
195
|
+
const s = separationAlong(planesA[f * 4] ?? 0, planesA[f * 4 + 1] ?? 0, planesA[f * 4 + 2] ?? 0, na, nb);
|
|
196
|
+
if (s > best) {
|
|
197
|
+
best = s;
|
|
198
|
+
bestFace = f;
|
|
199
|
+
bestFromA = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (let f = 0; f < facesB; f++) {
|
|
203
|
+
// B's outward normal points from B to A, so negate it to keep every axis A-to-B.
|
|
204
|
+
const s = separationAlong(-(planesB[f * 4] ?? 0), -(planesB[f * 4 + 1] ?? 0), -(planesB[f * 4 + 2] ?? 0), na, nb);
|
|
205
|
+
if (s > best) {
|
|
206
|
+
best = s;
|
|
207
|
+
bestFace = f;
|
|
208
|
+
bestFromA = false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (best - radii > margin)
|
|
212
|
+
return false;
|
|
213
|
+
const sign = bestFromA ? 1 : -1;
|
|
214
|
+
const rp = bestFromA ? planesA : planesB;
|
|
215
|
+
const nx = sign * (rp[bestFace * 4] ?? 0);
|
|
216
|
+
const ny = sign * (rp[bestFace * 4 + 1] ?? 0);
|
|
217
|
+
const nz = sign * (rp[bestFace * 4 + 2] ?? 0);
|
|
218
|
+
const refShape = bestFromA ? a : b;
|
|
219
|
+
const incShape = bestFromA ? b : a;
|
|
220
|
+
const refWorld = bestFromA ? worldA : worldB;
|
|
221
|
+
const incWorld = bestFromA ? worldB : worldA;
|
|
222
|
+
const refPlanes = bestFromA ? planesA : planesB;
|
|
223
|
+
const incPlanes = bestFromA ? planesB : planesA;
|
|
224
|
+
const incFaces = bestFromA ? facesB : facesA;
|
|
225
|
+
// The incident face is the one most anti-parallel to the reference normal.
|
|
226
|
+
let incFace = 0;
|
|
227
|
+
let mostOpposed = Infinity;
|
|
228
|
+
for (let f = 0; f < incFaces; f++) {
|
|
229
|
+
const d = (incPlanes[f * 4] ?? 0) * nx +
|
|
230
|
+
(incPlanes[f * 4 + 1] ?? 0) * ny +
|
|
231
|
+
(incPlanes[f * 4 + 2] ?? 0) * nz;
|
|
232
|
+
const signed = bestFromA ? d : -d;
|
|
233
|
+
if (signed < mostOpposed) {
|
|
234
|
+
mostOpposed = signed;
|
|
235
|
+
incFace = f;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Clip the incident loop against every side plane of the reference face.
|
|
239
|
+
let count = faceVertices(incShape, incFace, loop);
|
|
240
|
+
for (let i = 0; i < count; i++) {
|
|
241
|
+
const v = (loop[i] ?? 0) * 3;
|
|
242
|
+
clipIn[i * 3] = incWorld[v] ?? 0;
|
|
243
|
+
clipIn[i * 3 + 1] = incWorld[v + 1] ?? 0;
|
|
244
|
+
clipIn[i * 3 + 2] = incWorld[v + 2] ?? 0;
|
|
245
|
+
clipInId[i] = featureId(0, bestFace, incFace, loop[i] ?? 0);
|
|
246
|
+
}
|
|
247
|
+
const refCount = faceVertices(refShape, bestFace, loop);
|
|
248
|
+
const rnx = refPlanes[bestFace * 4] ?? 0;
|
|
249
|
+
const rny = refPlanes[bestFace * 4 + 1] ?? 0;
|
|
250
|
+
const rnz = refPlanes[bestFace * 4 + 2] ?? 0;
|
|
251
|
+
const rd = refPlanes[bestFace * 4 + 3] ?? 0;
|
|
252
|
+
for (let e = 0; e < refCount && count > 0; e++) {
|
|
253
|
+
const v0 = (loop[e] ?? 0) * 3;
|
|
254
|
+
const v1 = (loop[(e + 1) % refCount] ?? 0) * 3;
|
|
255
|
+
const ex = (refWorld[v1] ?? 0) - (refWorld[v0] ?? 0);
|
|
256
|
+
const ey = (refWorld[v1 + 1] ?? 0) - (refWorld[v0 + 1] ?? 0);
|
|
257
|
+
const ez = (refWorld[v1 + 2] ?? 0) - (refWorld[v0 + 2] ?? 0);
|
|
258
|
+
// Outward side plane: the face normal crossed with the edge, pointing away from the polygon.
|
|
259
|
+
let sx = ey * rnz - ez * rny;
|
|
260
|
+
let sy = ez * rnx - ex * rnz;
|
|
261
|
+
let sz = ex * rny - ey * rnx;
|
|
262
|
+
const len = Math.sqrt(sx * sx + sy * sy + sz * sz);
|
|
263
|
+
if (len === 0)
|
|
264
|
+
continue;
|
|
265
|
+
sx /= len;
|
|
266
|
+
sy /= len;
|
|
267
|
+
sz /= len;
|
|
268
|
+
const sd = sx * (refWorld[v0] ?? 0) + sy * (refWorld[v0 + 1] ?? 0) + sz * (refWorld[v0 + 2] ?? 0);
|
|
269
|
+
count = clipAgainst(sx, sy, sz, sd, count, e, incFace);
|
|
270
|
+
}
|
|
271
|
+
// Keep what is close enough to the reference plane, at most four, deepest first.
|
|
272
|
+
let kept = 0;
|
|
273
|
+
for (let i = 0; i < count; i++) {
|
|
274
|
+
const px = clipIn[i * 3] ?? 0;
|
|
275
|
+
const py = clipIn[i * 3 + 1] ?? 0;
|
|
276
|
+
const pz = clipIn[i * 3 + 2] ?? 0;
|
|
277
|
+
const separation = px * rnx + py * rny + pz * rnz - rd - radii;
|
|
278
|
+
if (separation > margin)
|
|
279
|
+
continue;
|
|
280
|
+
if (kept < MAX_CONTACTS) {
|
|
281
|
+
const at = kept * 3;
|
|
282
|
+
out.points[at] = px;
|
|
283
|
+
out.points[at + 1] = py;
|
|
284
|
+
out.points[at + 2] = pz;
|
|
285
|
+
out.separations[kept] = separation;
|
|
286
|
+
out.featureIds[kept] = clipInId[i] ?? 0;
|
|
287
|
+
kept++;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// Replace the shallowest, so four points describe the deepest corners of the overlap.
|
|
291
|
+
let worst = 0;
|
|
292
|
+
for (let k = 1; k < MAX_CONTACTS; k++) {
|
|
293
|
+
if ((out.separations[k] ?? 0) > (out.separations[worst] ?? 0))
|
|
294
|
+
worst = k;
|
|
295
|
+
}
|
|
296
|
+
if (separation < (out.separations[worst] ?? 0)) {
|
|
297
|
+
const at = worst * 3;
|
|
298
|
+
out.points[at] = px;
|
|
299
|
+
out.points[at + 1] = py;
|
|
300
|
+
out.points[at + 2] = pz;
|
|
301
|
+
out.separations[worst] = separation;
|
|
302
|
+
out.featureIds[worst] = clipInId[i] ?? 0;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (kept === 0)
|
|
307
|
+
return false;
|
|
308
|
+
out.nx = nx;
|
|
309
|
+
out.ny = ny;
|
|
310
|
+
out.nz = nz;
|
|
311
|
+
out.count = kept;
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Pack a contact's identity: which kind of feature it is, and which two features made it.
|
|
316
|
+
*
|
|
317
|
+
* **A kept incident vertex and a point cut from the edge leaving it are different contacts**, and
|
|
318
|
+
* naming them both after that vertex is how two of four points ended up sharing an id — which
|
|
319
|
+
* silently gives one contact another's accumulated impulse the next tick. A cut is named by the
|
|
320
|
+
* *reference edge* that cut it and the incident edge it sat on; a kept vertex by the reference face
|
|
321
|
+
* and its own index.
|
|
322
|
+
*/
|
|
323
|
+
function featureId(kind, refIndex, incFace, incVertex) {
|
|
324
|
+
return (kind << 30) | ((refIndex & 0xff) << 16) | ((incFace & 0xff) << 8) | (incVertex & 0xff);
|
|
325
|
+
}
|
|
326
|
+
/** Sutherland-Hodgman against one plane, in place. Returns the surviving count. */
|
|
327
|
+
function clipAgainst(nx, ny, nz, d, count, refEdge, incFace) {
|
|
328
|
+
let out = 0;
|
|
329
|
+
for (let i = 0; i < count; i++) {
|
|
330
|
+
const j = (i + 1) % count;
|
|
331
|
+
const ax = clipIn[i * 3] ?? 0;
|
|
332
|
+
const ay = clipIn[i * 3 + 1] ?? 0;
|
|
333
|
+
const az = clipIn[i * 3 + 2] ?? 0;
|
|
334
|
+
const bx = clipIn[j * 3] ?? 0;
|
|
335
|
+
const by = clipIn[j * 3 + 1] ?? 0;
|
|
336
|
+
const bz = clipIn[j * 3 + 2] ?? 0;
|
|
337
|
+
const da = ax * nx + ay * ny + az * nz - d;
|
|
338
|
+
const db = bx * nx + by * ny + bz * nz - d;
|
|
339
|
+
if (da <= 0) {
|
|
340
|
+
clipOut[out * 3] = ax;
|
|
341
|
+
clipOut[out * 3 + 1] = ay;
|
|
342
|
+
clipOut[out * 3 + 2] = az;
|
|
343
|
+
clipOutId[out] = clipInId[i] ?? 0;
|
|
344
|
+
out++;
|
|
345
|
+
}
|
|
346
|
+
if (da * db < 0) {
|
|
347
|
+
const t = da / (da - db);
|
|
348
|
+
clipOut[out * 3] = ax + (bx - ax) * t;
|
|
349
|
+
clipOut[out * 3 + 1] = ay + (by - ay) * t;
|
|
350
|
+
clipOut[out * 3 + 2] = az + (bz - az) * t;
|
|
351
|
+
// Named by the reference edge that cut it and the incident edge it sat on, not by a vertex.
|
|
352
|
+
clipOutId[out] = featureId(1, refEdge, incFace, (clipInId[i] ?? 0) & 0xff);
|
|
353
|
+
out++;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
for (let i = 0; i < out * 3; i++)
|
|
357
|
+
clipIn[i] = clipOut[i] ?? 0;
|
|
358
|
+
for (let i = 0; i < out; i++)
|
|
359
|
+
clipInId[i] = clipOutId[i] ?? 0;
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
/** Support separation of B beyond A along one axis, both shapes already in world. */
|
|
363
|
+
function separationAlong(nx, ny, nz, na, nb) {
|
|
364
|
+
let maxA = -Infinity;
|
|
365
|
+
for (let i = 0; i < na; i++) {
|
|
366
|
+
const d = (worldA[i * 3] ?? 0) * nx + (worldA[i * 3 + 1] ?? 0) * ny + (worldA[i * 3 + 2] ?? 0) * nz;
|
|
367
|
+
if (d > maxA)
|
|
368
|
+
maxA = d;
|
|
369
|
+
}
|
|
370
|
+
let minB = Infinity;
|
|
371
|
+
for (let i = 0; i < nb; i++) {
|
|
372
|
+
const d = (worldB[i * 3] ?? 0) * nx + (worldB[i * 3 + 1] ?? 0) * ny + (worldB[i * 3 + 2] ?? 0) * nz;
|
|
373
|
+
if (d < minB)
|
|
374
|
+
minB = d;
|
|
375
|
+
}
|
|
376
|
+
return minB - maxA;
|
|
377
|
+
}
|
|
378
|
+
/* ---------- geometry helpers, all of them allocation-free ---------- */
|
|
379
|
+
/** `[cx, cy, cz, nx, ny, nz, signedDistance]`, or the two closest points for a segment pair. */
|
|
380
|
+
const CLOSEST = new Float64Array(7);
|
|
381
|
+
/**
|
|
382
|
+
* The closest point on a convex polytope to `p`, exactly.
|
|
383
|
+
*
|
|
384
|
+
* Inside every face plane means the point is inside the solid, and the answer is then the least
|
|
385
|
+
* penetrating face rather than a surface point: `CLOSEST[6]` comes back negative to say so.
|
|
386
|
+
*/
|
|
387
|
+
function closestOnPolytope(shape, world, planes, faces, px, py, pz) {
|
|
388
|
+
let inside = true;
|
|
389
|
+
let bestPlane = 0;
|
|
390
|
+
let bestPlaneDist = -Infinity;
|
|
391
|
+
for (let f = 0; f < faces; f++) {
|
|
392
|
+
const d = px * (planes[f * 4] ?? 0) +
|
|
393
|
+
py * (planes[f * 4 + 1] ?? 0) +
|
|
394
|
+
pz * (planes[f * 4 + 2] ?? 0) -
|
|
395
|
+
(planes[f * 4 + 3] ?? 0);
|
|
396
|
+
if (d > 0)
|
|
397
|
+
inside = false;
|
|
398
|
+
if (d > bestPlaneDist) {
|
|
399
|
+
bestPlaneDist = d;
|
|
400
|
+
bestPlane = f;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (inside) {
|
|
404
|
+
const nx = planes[bestPlane * 4] ?? 0;
|
|
405
|
+
const ny = planes[bestPlane * 4 + 1] ?? 0;
|
|
406
|
+
const nz = planes[bestPlane * 4 + 2] ?? 0;
|
|
407
|
+
CLOSEST[0] = px - nx * bestPlaneDist;
|
|
408
|
+
CLOSEST[1] = py - ny * bestPlaneDist;
|
|
409
|
+
CLOSEST[2] = pz - nz * bestPlaneDist;
|
|
410
|
+
CLOSEST[3] = nx;
|
|
411
|
+
CLOSEST[4] = ny;
|
|
412
|
+
CLOSEST[5] = nz;
|
|
413
|
+
CLOSEST[6] = bestPlaneDist;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
let bestD2 = Infinity;
|
|
417
|
+
for (let f = 0; f < faces; f++) {
|
|
418
|
+
const n = faceVertices(shape, f, loop);
|
|
419
|
+
const a = (loop[0] ?? 0) * 3;
|
|
420
|
+
for (let i = 1; i + 1 < n; i++) {
|
|
421
|
+
const b = (loop[i] ?? 0) * 3;
|
|
422
|
+
const c = (loop[i + 1] ?? 0) * 3;
|
|
423
|
+
closestOnTriangle(px, py, pz, world[a] ?? 0, world[a + 1] ?? 0, world[a + 2] ?? 0, world[b] ?? 0, world[b + 1] ?? 0, world[b + 2] ?? 0, world[c] ?? 0, world[c + 1] ?? 0, world[c + 2] ?? 0);
|
|
424
|
+
const dx = px - TRI[0];
|
|
425
|
+
const dy = py - TRI[1];
|
|
426
|
+
const dz = pz - TRI[2];
|
|
427
|
+
const d2 = dx * dx + dy * dy + dz * dz;
|
|
428
|
+
if (d2 < bestD2) {
|
|
429
|
+
bestD2 = d2;
|
|
430
|
+
CLOSEST[0] = TRI[0];
|
|
431
|
+
CLOSEST[1] = TRI[1];
|
|
432
|
+
CLOSEST[2] = TRI[2];
|
|
433
|
+
CLOSEST[3] = planes[f * 4] ?? 0;
|
|
434
|
+
CLOSEST[4] = planes[f * 4 + 1] ?? 0;
|
|
435
|
+
CLOSEST[5] = planes[f * 4 + 2] ?? 0;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
CLOSEST[6] = Math.sqrt(bestD2);
|
|
440
|
+
}
|
|
441
|
+
const TRI = new Float64Array(3);
|
|
442
|
+
/** Closest point on a triangle to `p`, by the standard barycentric region test. */
|
|
443
|
+
function closestOnTriangle(px, py, pz, ax, ay, az, bx, by, bz, cx, cy, cz) {
|
|
444
|
+
const abx = bx - ax;
|
|
445
|
+
const aby = by - ay;
|
|
446
|
+
const abz = bz - az;
|
|
447
|
+
const acx = cx - ax;
|
|
448
|
+
const acy = cy - ay;
|
|
449
|
+
const acz = cz - az;
|
|
450
|
+
const apx = px - ax;
|
|
451
|
+
const apy = py - ay;
|
|
452
|
+
const apz = pz - az;
|
|
453
|
+
const d1 = abx * apx + aby * apy + abz * apz;
|
|
454
|
+
const d2 = acx * apx + acy * apy + acz * apz;
|
|
455
|
+
if (d1 <= 0 && d2 <= 0) {
|
|
456
|
+
TRI[0] = ax;
|
|
457
|
+
TRI[1] = ay;
|
|
458
|
+
TRI[2] = az;
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const bpx = px - bx;
|
|
462
|
+
const bpy = py - by;
|
|
463
|
+
const bpz = pz - bz;
|
|
464
|
+
const d3 = abx * bpx + aby * bpy + abz * bpz;
|
|
465
|
+
const d4 = acx * bpx + acy * bpy + acz * bpz;
|
|
466
|
+
if (d3 >= 0 && d4 <= d3) {
|
|
467
|
+
TRI[0] = bx;
|
|
468
|
+
TRI[1] = by;
|
|
469
|
+
TRI[2] = bz;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const cpx = px - cx;
|
|
473
|
+
const cpy = py - cy;
|
|
474
|
+
const cpz = pz - cz;
|
|
475
|
+
const d5 = abx * cpx + aby * cpy + abz * cpz;
|
|
476
|
+
const d6 = acx * cpx + acy * cpy + acz * cpz;
|
|
477
|
+
if (d6 >= 0 && d5 <= d6) {
|
|
478
|
+
TRI[0] = cx;
|
|
479
|
+
TRI[1] = cy;
|
|
480
|
+
TRI[2] = cz;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const vc = d1 * d4 - d3 * d2;
|
|
484
|
+
if (vc <= 0 && d1 >= 0 && d3 <= 0) {
|
|
485
|
+
const t = d1 / (d1 - d3);
|
|
486
|
+
TRI[0] = ax + abx * t;
|
|
487
|
+
TRI[1] = ay + aby * t;
|
|
488
|
+
TRI[2] = az + abz * t;
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const vb = d5 * d2 - d1 * d6;
|
|
492
|
+
if (vb <= 0 && d2 >= 0 && d6 <= 0) {
|
|
493
|
+
const t = d2 / (d2 - d6);
|
|
494
|
+
TRI[0] = ax + acx * t;
|
|
495
|
+
TRI[1] = ay + acy * t;
|
|
496
|
+
TRI[2] = az + acz * t;
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const va = d3 * d6 - d5 * d4;
|
|
500
|
+
if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) {
|
|
501
|
+
const t = (d4 - d3) / (d4 - d3 + (d5 - d6));
|
|
502
|
+
TRI[0] = bx + (cx - bx) * t;
|
|
503
|
+
TRI[1] = by + (cy - by) * t;
|
|
504
|
+
TRI[2] = bz + (cz - bz) * t;
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const denom = 1 / (va + vb + vc);
|
|
508
|
+
const v = vb * denom;
|
|
509
|
+
const w = vc * denom;
|
|
510
|
+
TRI[0] = ax + abx * v + acx * w;
|
|
511
|
+
TRI[1] = ay + aby * v + acy * w;
|
|
512
|
+
TRI[2] = az + abz * v + acz * w;
|
|
513
|
+
}
|
|
514
|
+
/** Closest points of two segments, written into `CLOSEST[0..5]`. */
|
|
515
|
+
function segmentClosest(p0x, p0y, p0z, p1x, p1y, p1z, q0x, q0y, q0z, q1x, q1y, q1z) {
|
|
516
|
+
const dx = p1x - p0x;
|
|
517
|
+
const dy = p1y - p0y;
|
|
518
|
+
const dz = p1z - p0z;
|
|
519
|
+
const ex = q1x - q0x;
|
|
520
|
+
const ey = q1y - q0y;
|
|
521
|
+
const ez = q1z - q0z;
|
|
522
|
+
const rx = p0x - q0x;
|
|
523
|
+
const ry = p0y - q0y;
|
|
524
|
+
const rz = p0z - q0z;
|
|
525
|
+
const a = dx * dx + dy * dy + dz * dz;
|
|
526
|
+
const e = ex * ex + ey * ey + ez * ez;
|
|
527
|
+
const f = ex * rx + ey * ry + ez * rz;
|
|
528
|
+
let s = 0;
|
|
529
|
+
let t = 0;
|
|
530
|
+
if (a <= 1e-12 && e <= 1e-12) {
|
|
531
|
+
// Two points.
|
|
532
|
+
}
|
|
533
|
+
else if (a <= 1e-12) {
|
|
534
|
+
t = clamp01(f / e);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
const c = dx * rx + dy * ry + dz * rz;
|
|
538
|
+
if (e <= 1e-12) {
|
|
539
|
+
s = clamp01(-c / a);
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
const b = dx * ex + dy * ey + dz * ez;
|
|
543
|
+
const denom = a * e - b * b;
|
|
544
|
+
s = denom !== 0 ? clamp01((b * f - c * e) / denom) : 0;
|
|
545
|
+
t = (b * s + f) / e;
|
|
546
|
+
if (t < 0) {
|
|
547
|
+
t = 0;
|
|
548
|
+
s = clamp01(-c / a);
|
|
549
|
+
}
|
|
550
|
+
else if (t > 1) {
|
|
551
|
+
t = 1;
|
|
552
|
+
s = clamp01((b - c) / a);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
CLOSEST[0] = p0x + dx * s;
|
|
557
|
+
CLOSEST[1] = p0y + dy * s;
|
|
558
|
+
CLOSEST[2] = p0z + dz * s;
|
|
559
|
+
CLOSEST[3] = q0x + ex * t;
|
|
560
|
+
CLOSEST[4] = q0y + ey * t;
|
|
561
|
+
CLOSEST[5] = q0z + ez * t;
|
|
562
|
+
}
|
|
563
|
+
function clamp01(v) {
|
|
564
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
565
|
+
}
|
|
566
|
+
/** Write a pose's rotation matrix into `rot`, row-major. */
|
|
567
|
+
function poseRotation(p) {
|
|
568
|
+
const { qx: x, qy: y, qz: z, qw: w } = p;
|
|
569
|
+
rot[0] = 1 - 2 * (y * y + z * z);
|
|
570
|
+
rot[1] = 2 * (x * y - z * w);
|
|
571
|
+
rot[2] = 2 * (x * z + y * w);
|
|
572
|
+
rot[3] = 2 * (x * y + z * w);
|
|
573
|
+
rot[4] = 1 - 2 * (x * x + z * z);
|
|
574
|
+
rot[5] = 2 * (y * z - x * w);
|
|
575
|
+
rot[6] = 2 * (x * z - y * w);
|
|
576
|
+
rot[7] = 2 * (y * z + x * w);
|
|
577
|
+
rot[8] = 1 - 2 * (x * x + y * y);
|
|
578
|
+
}
|
|
579
|
+
function toWorld(shape, pose, out) {
|
|
580
|
+
poseRotation(pose);
|
|
581
|
+
const n = shape.vertices.length / 3;
|
|
582
|
+
for (let i = 0; i < n; i++) {
|
|
583
|
+
const x = shape.vertices[i * 3] ?? 0;
|
|
584
|
+
const y = shape.vertices[i * 3 + 1] ?? 0;
|
|
585
|
+
const z = shape.vertices[i * 3 + 2] ?? 0;
|
|
586
|
+
out[i * 3] = (rot[0] ?? 0) * x + (rot[1] ?? 0) * y + (rot[2] ?? 0) * z + pose.x;
|
|
587
|
+
out[i * 3 + 1] = (rot[3] ?? 0) * x + (rot[4] ?? 0) * y + (rot[5] ?? 0) * z + pose.y;
|
|
588
|
+
out[i * 3 + 2] = (rot[6] ?? 0) * x + (rot[7] ?? 0) * y + (rot[8] ?? 0) * z + pose.z;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/** Rotate every face plane and re-anchor its offset on a world vertex of that face. */
|
|
592
|
+
function planesToWorld(shape, pose, world, out) {
|
|
593
|
+
poseRotation(pose);
|
|
594
|
+
const faces = faceCount(shape);
|
|
595
|
+
for (let f = 0; f < faces; f++) {
|
|
596
|
+
const x = shape.facePlanes[f * 4] ?? 0;
|
|
597
|
+
const y = shape.facePlanes[f * 4 + 1] ?? 0;
|
|
598
|
+
const z = shape.facePlanes[f * 4 + 2] ?? 0;
|
|
599
|
+
const nx = (rot[0] ?? 0) * x + (rot[1] ?? 0) * y + (rot[2] ?? 0) * z;
|
|
600
|
+
const ny = (rot[3] ?? 0) * x + (rot[4] ?? 0) * y + (rot[5] ?? 0) * z;
|
|
601
|
+
const nz = (rot[6] ?? 0) * x + (rot[7] ?? 0) * y + (rot[8] ?? 0) * z;
|
|
602
|
+
const v = (shape.faceVertexIndices[shape.faceVertexStart[f] ?? 0] ?? 0) * 3;
|
|
603
|
+
out[f * 4] = nx;
|
|
604
|
+
out[f * 4 + 1] = ny;
|
|
605
|
+
out[f * 4 + 2] = nz;
|
|
606
|
+
out[f * 4 + 3] = nx * (world[v] ?? 0) + ny * (world[v + 1] ?? 0) + nz * (world[v + 2] ?? 0);
|
|
607
|
+
}
|
|
608
|
+
}
|