@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
package/README.md
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
# @driftengine/physics
|
|
2
|
+
|
|
3
|
+
Convex collision shapes, the spatial hash over them, ray and segment queries, and the swept
|
|
4
|
+
kinematic sweep that resolves a moving body against a static world one axis at a time.
|
|
5
|
+
|
|
6
|
+
**It imports no other engine package.** Together with `@driftengine/entities` that makes a
|
|
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.0 KB gzipped** standalone, measured
|
|
9
|
+
by `scripts/size-gate.test.mjs` against `scripts/fixtures/size/physics-only.ts`.
|
|
10
|
+
|
|
11
|
+
**`@driftengine/core` re-exports every name here**, so reaching for collision through the core
|
|
12
|
+
barrel works exactly as it did before these files lived in a package of their own. Core _depends on_
|
|
13
|
+
this package rather than the other way round, which is the opposite direction from every other
|
|
14
|
+
optional package and is not a preference: the cinematic and third-person cameras, the contact probe
|
|
15
|
+
and the ribbon builder all need the sweep.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import {
|
|
19
|
+
AXIS_X,
|
|
20
|
+
AXIS_Y,
|
|
21
|
+
ColliderSet,
|
|
22
|
+
boxCollider,
|
|
23
|
+
hullShape,
|
|
24
|
+
moveAxis,
|
|
25
|
+
} from '@driftengine/physics';
|
|
26
|
+
import type { Body } from '@driftengine/physics';
|
|
27
|
+
|
|
28
|
+
const world = new ColliderSet([boxCollider(0, -0.5, 0, 20, 0.5, 20)]);
|
|
29
|
+
const body: Body = { x: 0, y: 2, z: 0, hx: 0.35, hy: 0.9, hz: 0.35 };
|
|
30
|
+
|
|
31
|
+
moveAxis(body, world, AXIS_X, 0.05); // returns how far it actually travelled
|
|
32
|
+
moveAxis(body, world, AXIS_Y, -0.02);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Rigid bodies
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { BODY_DYNAMIC, BODY_STATIC, PhysicsWorld, boxShape } from '@driftengine/physics';
|
|
39
|
+
|
|
40
|
+
const world = new PhysicsWorld({ substeps: 4 });
|
|
41
|
+
world.addBody({ type: BODY_STATIC, shape: boxShape(50, 1, 50), y: -1, friction: 0.8 });
|
|
42
|
+
const crate = world.addBody({ type: BODY_DYNAMIC, shape: boxShape(0.5, 0.5, 0.5), y: 4 });
|
|
43
|
+
|
|
44
|
+
world.step(1 / 60); // fixed clock, always
|
|
45
|
+
world.bodies.posY[crate];
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Constraints are **soft**: each carries a stiffness in hertz and a damping ratio rather than a
|
|
49
|
+
fraction of penetration per step. That is what makes `substeps` a quality dial — raise it and a
|
|
50
|
+
stack rests at the same height and a ball bounces to the same height, so the option buys accuracy
|
|
51
|
+
without changing behaviour.
|
|
52
|
+
|
|
53
|
+
Contacts persist across ticks by **feature id**, never by proximity within a tolerance, which is
|
|
54
|
+
what makes warm starting correct rather than order-dependent.
|
|
55
|
+
|
|
56
|
+
Islands are connected components over **dynamic bodies only**, so two towers standing on one floor
|
|
57
|
+
are two islands rather than one. Sleeping is island-gated and counted in ticks. An executor seam
|
|
58
|
+
runs the islands, and a gate asserts a serial runner and a deliberately reordered one produce
|
|
59
|
+
identical state — no two islands share a body, so order cannot matter, and that is asserted rather
|
|
60
|
+
than argued.
|
|
61
|
+
|
|
62
|
+
## Joints, queries and sensors
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { JOINT_REVOLUTE, createRayHit } from '@driftengine/physics';
|
|
66
|
+
|
|
67
|
+
world.addJoint({
|
|
68
|
+
type: JOINT_REVOLUTE,
|
|
69
|
+
bodyA: post,
|
|
70
|
+
bodyB: door,
|
|
71
|
+
anchorAX: 0.1,
|
|
72
|
+
anchorBX: -0.8,
|
|
73
|
+
axisY: 1,
|
|
74
|
+
lower: -0.5,
|
|
75
|
+
upper: 0.5,
|
|
76
|
+
motorSpeed: 2,
|
|
77
|
+
motorMaxForce: 400,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const hit = createRayHit();
|
|
81
|
+
if (world.raycast(0, 2, 0, 0, -1, 0, 50, hit, { ignore: player })) hit.body;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Seven types — distance, spherical, revolute, prismatic, fixed, cone-twist and a configurable
|
|
85
|
+
six-DOF — with motors, **one-sided** limits and a breaking threshold. `addJoint` captures the
|
|
86
|
+
relative pose the bodies are in now, so a rig can be posed and then jointed without anything
|
|
87
|
+
snapping on the first tick. No angle appears anywhere in the solve: limits compare cosines and
|
|
88
|
+
quaternion components, which is what a clamp wants and what the determinism gate permits.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
world.addJoint({
|
|
92
|
+
type: JOINT_SIX_DOF,
|
|
93
|
+
bodyA: hips,
|
|
94
|
+
bodyB: thigh,
|
|
95
|
+
/* linear x, y, z then angular x, y, z, in the hips' own frame */
|
|
96
|
+
dof: [DOF_LOCKED, DOF_LOCKED, DOF_LOCKED, DOF_LIMITED, DOF_FREE, DOF_LOCKED],
|
|
97
|
+
dofLower: [0, 0, 0, -Math.sin(Math.PI / 8), 0, 0],
|
|
98
|
+
dofUpper: [0, 0, 0, Math.sin(Math.PI / 8), 0, 0],
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Three things to know before reaching for it. **Nothing said is everything locked**, so a joint you
|
|
103
|
+
forgot to describe is rigid rather than one whose bodies fly apart. **Angular bounds are sines of
|
|
104
|
+
half angles**, the same unit `swingCos` and `twistSin` use, because no angle appears in this solver
|
|
105
|
+
— a quarter turn is `Math.sin(Math.PI / 4)`. And **an all-locked six-DOF joint is softer than
|
|
106
|
+
`JOINT_FIXED`**: it solves six independent scalar rows where the fixed joint solves two coupled 3x3
|
|
107
|
+
blocks, so use the fixed one when you want all six. It has no motor, and says so on the console
|
|
108
|
+
rather than accepting one and never driving.
|
|
109
|
+
|
|
110
|
+
**A cylinder is a shape here**, and the number that put it there is worth knowing
|
|
111
|
+
because it is what a prism costs you. `cylinderShape(radius, halfHeight)` stands along y and turns
|
|
112
|
+
with its body, so a wheel is a cylinder with a quarter turn on it. The workaround it replaces was an
|
|
113
|
+
n-gon prism through `hullShape`: a hull takes at most 64 points, a prism spends two a side, so 32
|
|
114
|
+
sides is the wall, and a 32-sided wheel of radius 1 rolling at 6 m/s **bobs 16 mm** with no sides
|
|
115
|
+
left to add. The same wheel as a cylinder bobs **under 2 mm**, which is inside the solver's own
|
|
116
|
+
5 mm slop. `prismRoll.test.ts` measures both, side by side.
|
|
117
|
+
|
|
118
|
+
The difference is a second kind of rounding rather than a second kind of shape: `sideRadius` grows a
|
|
119
|
+
shape by a _disc_ perpendicular to the segment through its two points, where `radius` grows it by a
|
|
120
|
+
ball. Two flat caps, a curved side, a sharp rim between them. Everything reads both — contacts,
|
|
121
|
+
inertia, bounds, rays and cloth — and a cylinder's support across its axis is exactly its radius at
|
|
122
|
+
every angle, which is the property a prism cannot have and the reason a wheel stops bobbing.
|
|
123
|
+
|
|
124
|
+
**Friction is a box by default and an ellipse if you ask for one.** Each tangent row is clamped to
|
|
125
|
+
`mu * N` on its own, so a slide along the diagonal between the two axes meets `sqrt(2)` times the
|
|
126
|
+
grip it meets along an axis — measured, 0.737 m of travel against 1.064 m from the same 4 m/s.
|
|
127
|
+
`frictionModel: 'elliptical'` couples the rows and the distance is 1.06 m whatever the heading,
|
|
128
|
+
which is what friction physically is. It is not the default: every stacking result moves under it,
|
|
129
|
+
and a golden fingerprint or a stored replay is a thing consumers keep.
|
|
130
|
+
|
|
131
|
+
Queries fill caller-owned buffers and allocate nothing. A ray starting inside a body reports that
|
|
132
|
+
body at fraction zero, because a line-of-sight test that says a wall it started inside is not there
|
|
133
|
+
is worse than useless.
|
|
134
|
+
|
|
135
|
+
A **sensor** reports overlap and resolves none of it. Events drain into `world.events` after the
|
|
136
|
+
tick, ordered by pair key, rather than firing callbacks mid-solve — a callback that removes a body
|
|
137
|
+
during the solve invalidates every index the solver is holding.
|
|
138
|
+
|
|
139
|
+
## Characters and ragdolls
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { CharacterController, ragdollFromBones } from '@driftengine/physics';
|
|
143
|
+
|
|
144
|
+
const player = new CharacterController({ coyoteTicks: 6, jumpBufferTicks: 6 });
|
|
145
|
+
player.teleport(0, 2, 0);
|
|
146
|
+
player.move(world, 1 / 60, { moveX: 4, moveZ: 0, jump: held });
|
|
147
|
+
|
|
148
|
+
// A skeleton already has both of these; nothing is imported in either direction.
|
|
149
|
+
const doll = ragdollFromBones(
|
|
150
|
+
world,
|
|
151
|
+
skeleton.joints.map((j) => j.parent),
|
|
152
|
+
skeleton.world,
|
|
153
|
+
);
|
|
154
|
+
doll.drive(world, animatedPose, 0.6); // a hit reaction, not a body gone limp
|
|
155
|
+
doll.writePose(currentPose);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The controller is **kinematic**, not a dynamic body: a dynamic character falls over and turns every
|
|
159
|
+
question about movement into a question about torque. Its feel parameters live on it — acceleration,
|
|
160
|
+
air control, variable jump height, slide boost — and **coyote time and jump buffering are counted in
|
|
161
|
+
ticks**, never accumulated in a float of seconds, so a replay matches after an hour of uptime.
|
|
162
|
+
|
|
163
|
+
The ragdoll takes a parent array and a flat array of joint world matrices, and writes back through a
|
|
164
|
+
structural target that an animation `Pose` satisfies. Neither package names the other. **`sync` puts
|
|
165
|
+
every body back on its bones and clears its velocities**, which is what a hit reaction needs and
|
|
166
|
+
what nothing had: a ragdoll is built once, so between reactions its bodies sit where the last one
|
|
167
|
+
left them while the character keeps moving — and blending toward _that_ snaps the character to a
|
|
168
|
+
stale shape somewhere else, carrying the speed of however long the bodies had been falling.
|
|
169
|
+
|
|
170
|
+
## When the geometry has stopped cooperating
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { ESCAPE_SEALED, StallEscape, applyBuoyancy, createBuoyancy } from '@driftengine/physics';
|
|
174
|
+
|
|
175
|
+
const escape = new StallEscape({ stalledTicks: 30, budgetM: 4 });
|
|
176
|
+
// Last in the tick, after the move has resolved: it reads where the body ended up.
|
|
177
|
+
const out = escape.step(body, colliders, { trying, refused, grounded, supported }, veto);
|
|
178
|
+
if (out === ESCAPE_SEALED) respawn(); // your decision, not the engine's
|
|
179
|
+
|
|
180
|
+
const sea = createBuoyancy({ level: 0, drag: 3, sinkSpeed: -1.2 });
|
|
181
|
+
const submerged = applyBuoyancy(sea, body.y, velocity, 1 / 60);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**A controller resolves a move against geometry; it has no opinion about a body that has been asking
|
|
185
|
+
to move for half a second and going nowhere.** That gap is where players get stuck, and the rules
|
|
186
|
+
that close it only show up in a shipped game:
|
|
187
|
+
|
|
188
|
+
- **Progress is net travel over a window**, never this tick's displacement — a body jammed under a
|
|
189
|
+
low ceiling _jitters_ by thirty times any per-tick threshold while sitting at the same
|
|
190
|
+
coordinates for as long as the key is held.
|
|
191
|
+
- **Grounded, supported and headroom are three separate facts.** A body with all three can jump out
|
|
192
|
+
and turn and walk away, and is not stuck however blocked it is — being blocked is what a wall is
|
|
193
|
+
_for_. Without that gate this arms on somebody merely leaning on scenery and carries them sideways
|
|
194
|
+
with the controls doing nothing.
|
|
195
|
+
- **A direction the world would refuse is not an escape route.** `EscapeVeto` is taken structurally,
|
|
196
|
+
the same way `GroundProbe` is on the controller: a deck edge carrying no collider at all reports
|
|
197
|
+
clear air to a probe that asks only the colliders.
|
|
198
|
+
- **Sealed is reported, never acted on.** The engine says there is nowhere to put the body; what
|
|
199
|
+
that costs a player belongs to you. Pressing into a wall is allowed to achieve nothing, and is not
|
|
200
|
+
allowed to be fatal.
|
|
201
|
+
|
|
202
|
+
`applyBuoyancy` is the other half of a body meeting the world: drag on **every** axis, because
|
|
203
|
+
killing horizontal momentum is what makes falling in a setback rather than a shortcut, and a
|
|
204
|
+
terminal sink speed so a drop settles instead of accelerating into the seabed. `accelerateAlong` is
|
|
205
|
+
Quake's accelerate with its trap named — the amount added is clamped against the shortfall _along
|
|
206
|
+
the wish direction_, which is also what buys air-strafing.
|
|
207
|
+
|
|
208
|
+
## Vehicles and cloth
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { ClothBody, Vehicle, makeClothGrid } from '@driftengine/physics';
|
|
212
|
+
|
|
213
|
+
const car = new Vehicle(chassis, { wheels: [...], lateral: myTyreCurve });
|
|
214
|
+
car.update(world, 1 / 60, { throttle: 1, brake: 0, steer });
|
|
215
|
+
|
|
216
|
+
const grid = makeClothGrid(20, 20, 0.1);
|
|
217
|
+
const cape = new ClothBody(grid.positions, grid.links, grid.bendLinks);
|
|
218
|
+
cape.step(world, 1 / 60); // after world.step
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The vehicle is a **raycast** vehicle: wheels are rays, not bodies. A raycast wheel cannot wedge in
|
|
222
|
+
geometry or collide with another wheel, and in exchange it is stable at ordinary substep counts,
|
|
223
|
+
which rigid wheels on joints are not. Grip comes from a **table you supply** rather than a magic
|
|
224
|
+
formula, because the tick may not use `sin` or `atan` — a curve is edited by moving a point.
|
|
225
|
+
|
|
226
|
+
**Both the controller and the vehicle take a `ground` probe**, so a world whose floor is analytic
|
|
227
|
+
has one floor rather than one per thing that stands on it. Anything answering "how high is the
|
|
228
|
+
ground at this column, and which way does it face" satisfies it, and `@driftengine/core` has three
|
|
229
|
+
that do: `RibbonSurface` for a route, `BoxSurface` for pads, and `heightSurface(heightAt)` around a
|
|
230
|
+
function of your own. A wheel consults it only where its ray found no body, and the character does
|
|
231
|
+
the same: a body is a thing that is there, and a surface is a description of where the ground is.
|
|
232
|
+
|
|
233
|
+
**A road can pass under a road**, and this is worth saying plainly because it reads like something a
|
|
234
|
+
height field cannot do. `heightSurface` takes a **list** of fields as well as one:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
const ground = heightSurface([
|
|
238
|
+
(x, z) => terrainAt(x, z), // the carriageway, everywhere
|
|
239
|
+
(x, z) => deckAt(x, z), // the flyover, NaN off the structure
|
|
240
|
+
]);
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**A layer answers `NaN` where it is not there**, so a deck is a floor exactly over its span and
|
|
244
|
+
nothing at all beside it. Which floor a query gets is decided by the height the asker is at, and
|
|
245
|
+
every caller already passes it — the character controller, the vehicle, the third-person camera and
|
|
246
|
+
the rain field all do. A caller with no position, a generator placing a lamp, gets the highest,
|
|
247
|
+
which is what it wants.
|
|
248
|
+
|
|
249
|
+
Order in the list carries no meaning: the rule is nearness to the asker, with a tie broken toward
|
|
250
|
+
the floor below, so a body on the lower road stays on it directly beneath a span.
|
|
251
|
+
|
|
252
|
+
**And a collider can be the ground, so a deck can be a slab instead of a height.** A collider set
|
|
253
|
+
was solid to walk _into_ and not to stand _on_: the sweep resolves a body against it laterally, and
|
|
254
|
+
the character controller senses ground through bodies and through a `GroundSurface`, which a
|
|
255
|
+
collider set was neither. Measured: a character dropped over a `ColliderSet` slab whose lid is at
|
|
256
|
+
5.5 m fell to −84 m in three seconds, which is what it does with no slab at all.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { colliderSurface } from '@driftengine/core';
|
|
260
|
+
|
|
261
|
+
const ground = heightSurface([...]); // or any other surface
|
|
262
|
+
const decks = colliderSurface(set, { minY: -20, maxY: 60 });
|
|
263
|
+
const world = new CompositeSurface([ground, decks]);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`minY` and `maxY` are the vertical extent the column search walks and they are **required**: the
|
|
267
|
+
spatial hash visits every cell in that column, so an unbounded one is half a million cells and a
|
|
268
|
+
frozen frame. You know how tall your world is; the engine does not.
|
|
269
|
+
|
|
270
|
+
**A sloped hull holds you up at the slope, not at its bounding box.** The lid is solved against the
|
|
271
|
+
solid's own face planes, so a ramp supports a body along its face and hands back a normal that tilts
|
|
272
|
+
with it — treating the broad-phase box as the answer would stand a body on thin air over the low
|
|
273
|
+
half of every ramp.
|
|
274
|
+
|
|
275
|
+
**Drawing cloth needs `updateMesh` in `@driftengine/core`**, which is what a deforming surface has
|
|
276
|
+
instead of a model matrix — a mesh is otherwise immutable, and recreating one per frame is a GPU
|
|
277
|
+
buffer allocation per frame. Create it with `{ dynamic: true }` and hand it positions and normals
|
|
278
|
+
each frame.
|
|
279
|
+
|
|
280
|
+
Cloth is XPBD. Bending resists **bowing** rather than orientation, so it will not hold a cantilever
|
|
281
|
+
out — that is a property of a skip-one distance constraint, and the alternative is noted where the
|
|
282
|
+
links are built.
|
|
283
|
+
|
|
284
|
+
**A sheet pushes what it lands on, if you ask it to.** `coupling` is 0 to 1 and 0 is the default; at
|
|
285
|
+
anything above, a particle pushed out of a body hands the body back the momentum it lost, at the
|
|
286
|
+
contact point, so a sheet landing on one end of a plank tips it. `particleMass` is what that
|
|
287
|
+
momentum is measured in, in kilograms, and is deliberately not `invMass` — that array is a relative
|
|
288
|
+
weight the compliance is tuned against. The exchange is explicit, so the body answers on the next
|
|
289
|
+
tick: a very heavy sheet on a very light body rings, and turning `coupling` down is the answer.
|
|
290
|
+
Merging cloth particles into the rigid solver's islands would converge in one solve and is still
|
|
291
|
+
refused, because merged islands are what the parallel executor is made of.
|
|
292
|
+
|
|
293
|
+
**A sheet stops passing through itself, if you ask it to.** `selfDistance` is in metres and 0 is the
|
|
294
|
+
default, because its tuning is the half that decides whether it looks right: a little under the grid
|
|
295
|
+
spacing is the honest starting point. Particles a link joins are exempt — a stretch link's rest
|
|
296
|
+
length _is_ the spacing, so anything else would have every neighbour fighting its own link and the
|
|
297
|
+
sheet would inflate rather than drape.
|
|
298
|
+
|
|
299
|
+
## A level is a mesh, and everything in it is a hull
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import { meshShape } from '@driftengine/physics';
|
|
303
|
+
|
|
304
|
+
world.addBody({ type: BODY_STATIC, shape: meshShape(positions, indices) });
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`meshShape` takes triangles and builds their planes, an edge classification and a tree over them.
|
|
308
|
+
**Static only, and it is enforced**: `addBody` refuses one on a moving body and `colliderFromShape`
|
|
309
|
+
refuses one outright. A moving concave thing wants convex decomposition into hulls, which `decomposeConvex` produces and `BodyDesc.shapes` accepts; before 2026-09-03 that sentence named something the dynamics could not do, which the
|
|
310
|
+
runtime already accepts.
|
|
311
|
+
|
|
312
|
+
Every triangle goes through the same narrow phase everything else does, so a box, a sphere, a
|
|
313
|
+
capsule, a cylinder and a hull are all exact against a mesh with no second implementation of any of
|
|
314
|
+
them. A mesh pair produces **several** contact planes rather than one, because a body in a corner
|
|
315
|
+
has two or three and reducing them to one picks a wall and lets the body through the others.
|
|
316
|
+
|
|
317
|
+
**Contacts are one-sided and rays are two-sided.** A level mesh is a surface: a two-sided contact
|
|
318
|
+
fires anything that has sunk below it back out the wrong way, while a sight line that saw through a
|
|
319
|
+
wall from behind is a worse answer than one that did not.
|
|
320
|
+
|
|
321
|
+
**Interior edges are filtered**, which is the part that decides whether a mesh floor feels like a
|
|
322
|
+
floor. Without it a box sliding across two coplanar triangles catches on their seam — a character
|
|
323
|
+
stumbling on a flat floor once a metre. Measured: a box slid at 6 m/s across sixteen quads keeps
|
|
324
|
+
**80% of its speed with the filter and 1% without it**.
|
|
325
|
+
|
|
326
|
+
## A world whose colliders stream
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import { ColliderSet } from '@driftengine/physics';
|
|
330
|
+
|
|
331
|
+
const world = new ColliderSet([]); // empty, because a streamer builds by group
|
|
332
|
+
const groups = new Map<number, number>(); // region -> handle
|
|
333
|
+
|
|
334
|
+
function crossInto(region: number): void {
|
|
335
|
+
for (const r of ring(region)) {
|
|
336
|
+
if (!groups.has(r)) groups.set(r, world.add(collidersFor(r)));
|
|
337
|
+
}
|
|
338
|
+
for (const [r, group] of [...groups]) {
|
|
339
|
+
if (inRing(r, region)) continue;
|
|
340
|
+
world.remove(group); // the grid is updated for this group and no other
|
|
341
|
+
groups.delete(r);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**One set follows the player, and an index is an index.** That is the whole point of the shape:
|
|
347
|
+
with a set per region, a query result means nothing without the set that answered it, so every
|
|
348
|
+
consumer of a hit carries a pair where it once carry a number.
|
|
349
|
+
|
|
350
|
+
Measured by `npm run check:streaming`, driving a body across region boundaries at five frame rates:
|
|
351
|
+
**a crossing costs 0.090 ms** where rebuilding a set of the same size costs about 41 ms, and
|
|
352
|
+
**queries on the streamed set run at 1.25 to 1.31 times** a freshly built one's. The alternative of
|
|
353
|
+
keeping one set per region and asking the nine around the player measured a **7.5x** multiplier on
|
|
354
|
+
every collision query in the game.
|
|
355
|
+
|
|
356
|
+
`world.bytes()` answers what the resident ring costs, which is the question that decides a region
|
|
357
|
+
size: `slots` and `shapes` are exact, and `index` carries an estimate of per-object overhead a
|
|
358
|
+
JavaScript engine does not expose. `world.absorb(other)` folds one set into another by copying its
|
|
359
|
+
packed bounds and offsetting its buckets, with nothing rehashed.
|
|
360
|
+
|
|
361
|
+
**Three things worth knowing before you build on it.**
|
|
362
|
+
|
|
363
|
+
`count` is the live count and `capacity` is the slot count. They are equal until the first
|
|
364
|
+
`remove`; after that the live colliders are sparse, so a walk over `data` runs to `capacity` and
|
|
365
|
+
skips `!liveAt(i)`. Nothing that never removes has to change.
|
|
366
|
+
|
|
367
|
+
**A set built by the constructor cannot be streamed.** Those colliders belong to a reserved group
|
|
368
|
+
`remove` refuses, so build empty and put everything in through `add`.
|
|
369
|
+
|
|
370
|
+
**`fingerprintColliders` answers a narrower question on a set that has been mutated**: "the same set
|
|
371
|
+
with the same history", not "the same world". Replay is unaffected — the same inputs drive the same
|
|
372
|
+
path and so the same slots — but two sessions that reached the same world by different routes will
|
|
373
|
+
not agree.
|
|
374
|
+
|
|
375
|
+
## Two kinds of rounding, and what they can and cannot be
|
|
376
|
+
|
|
377
|
+
A `ConvexShape` is **a point cloud plus two kinds of rounding**, with its separating features
|
|
378
|
+
enumerated once at build time. A box is its eight corners; a sphere is one point and its `radius`; a
|
|
379
|
+
capsule is two points and its `radius`; a cylinder is two points and its `sideRadius`. The face
|
|
380
|
+
normals and edge directions are the separating axes SAT tests, and a fixed, data-driven axis list
|
|
381
|
+
per shape pair is what lets two machines replay the same run bit for bit.
|
|
382
|
+
|
|
383
|
+
**A cylinder is the one shape a single radius cannot express**, which is why there are two: a
|
|
384
|
+
uniform radius grows around every feature and would round the rim along with the side, and the rim
|
|
385
|
+
is what a wheel rolls on. `sideRadius` grows the shape by a disc perpendicular to its axis instead.
|
|
386
|
+
Its curved side has no separating axis to enumerate, so a pair involving one builds its axis list
|
|
387
|
+
against the _other_ shape — `cylinderContact.ts` — and answers every axis exactly from the support
|
|
388
|
+
function. That costs about 5 KB gzipped and it is most of what the section header once called the
|
|
389
|
+
one representation.
|
|
390
|
+
|
|
391
|
+
`hullShape` is O(n⁴) in the point count and caps at 64 points, deliberately. Shapes come from the
|
|
392
|
+
few points that _define_ an element, never from a render mesh.
|
|
393
|
+
|
|
394
|
+
## Islands on other threads, if your site can pay for it
|
|
395
|
+
|
|
396
|
+
No two islands share a body, so solving them in any order or all at once gives the same bits. The
|
|
397
|
+
`Executor` seam has been in this package since islands landed and `ShuffledExecutor` has been
|
|
398
|
+
asserting that property since. What kept a pool out was never the engine.
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import { PhysicsWorld } from '@driftengine/physics';
|
|
402
|
+
import { createIslandPool } from '@driftengine/physics/src/workers.ts';
|
|
403
|
+
|
|
404
|
+
const world = new PhysicsWorld({ workers: 4, pool: createIslandPool });
|
|
405
|
+
world.parallelism; // { requested: 4, running: 4, reason: '' }
|
|
406
|
+
world.dispose(); // terminates them; a world with a pool must be disposed
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**`pool` is passed in, and it lives behind its own specifier, and both are measurements.** A world
|
|
410
|
+
that reached the pool on its own put 2,629 bytes gzipped into every bundle, including everyone who
|
|
411
|
+
never asks for a worker. That was half of it. The other half cost 4.7 times as much: a bundler
|
|
412
|
+
rewrites `new Worker(new URL(...))` at transform time, before tree-shaking decides anything is
|
|
413
|
+
unreachable, so a barrel that merely re-exported the factory still wrote **12,259 gzipped bytes** of
|
|
414
|
+
worker to disk in builds where nothing could fetch it. Reported by one consumer and reproduced on a
|
|
415
|
+
second the same day.
|
|
416
|
+
|
|
417
|
+
So the factory is at `@driftengine/physics/src/workers.ts`. A caller already had to name it, so the
|
|
418
|
+
cost is the specifier and nothing else, and importing `PhysicsWorld` now names no worker at all.
|
|
419
|
+
|
|
420
|
+
**Read `parallelism` instead of assuming the option took.** Every path that does not start a pool
|
|
421
|
+
sets `reason` to a whole sentence: the page is not cross-origin isolated, the bundler could not build
|
|
422
|
+
the worker, no pool was passed. `running: 0` with correct serial physics is this design working, and
|
|
423
|
+
a silent fallback is the one outcome it refuses.
|
|
424
|
+
|
|
425
|
+
### The headers, which are the actual price
|
|
426
|
+
|
|
427
|
+
`SharedArrayBuffer` does not exist on a page that is not cross-origin isolated. Two header pairs get
|
|
428
|
+
you there, and they cost very different things:
|
|
429
|
+
|
|
430
|
+
| | |
|
|
431
|
+
| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
|
|
432
|
+
| `Cross-Origin-Opener-Policy: same-origin` + `Cross-Origin-Embedder-Policy: require-corp` | Every cross-origin resource that does not send CORP stops loading. Fonts, CDN images, embedded video, analytics. |
|
|
433
|
+
| `Cross-Origin-Opener-Policy: same-origin` + `Cross-Origin-Embedder-Policy: credentialless` | Cross-origin resources still load, sent without credentials. |
|
|
434
|
+
|
|
435
|
+
**Both were measured in Chrome 151, not remembered**: `npm run check:pool` grants isolation under
|
|
436
|
+
either and runs the pool identically. `credentialless` is the one to reach for first. Safari has not
|
|
437
|
+
been tested here, and this sentence is the whole of what is known about it.
|
|
438
|
+
|
|
439
|
+
The headers go on the document. In Vite, `server.headers`; on Cloudflare Pages, a `_headers` file;
|
|
440
|
+
in nginx, `add_header`. `demo/dev/vite.isolated.config.ts` is a working example of the first.
|
|
441
|
+
|
|
442
|
+
### What it is worth, and the two bounds it is held between
|
|
443
|
+
|
|
444
|
+
Measured on a 24-core desktop, 256 islands of ten boxes, by `npm run bench:islands`:
|
|
445
|
+
|
|
446
|
+
| workers | 1 | 2 | 4 | 8 |
|
|
447
|
+
| ------- | ----- | ----- | ----- | ----- |
|
|
448
|
+
| speedup | 1.05x | 1.22x | 1.26x | 1.39x |
|
|
449
|
+
|
|
450
|
+
**Neither bound is a promise and both belong to your scene.** Amdahl's is `1 / (1 - solve share)`,
|
|
451
|
+
and the bench prints that share. The other is `total island cost / largest island cost`: the tick
|
|
452
|
+
cannot end before its slowest island does, so a hundred jointed boxes are one island and gain
|
|
453
|
+
nothing at any worker count. The figures above are well under the first bound, and the staging copy
|
|
454
|
+
accounts for 1.7% of the gap and no more; what accounts for the rest has not been measured, so this
|
|
455
|
+
does not guess.
|
|
456
|
+
|
|
457
|
+
### When your bundler cannot build the worker
|
|
458
|
+
|
|
459
|
+
The default is `new Worker(new URL('./islandWorker.ts', import.meta.url), { type: 'module' })`, which
|
|
460
|
+
Vite, webpack 5 and Parcel resolve. If yours does not, `parallelism.reason` says so and you pass your
|
|
461
|
+
own:
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
// islandWorker.ts, in your app
|
|
465
|
+
import '@driftengine/physics/src/islandWorker.ts';
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
new PhysicsWorld({
|
|
470
|
+
workers: 4,
|
|
471
|
+
pool: createIslandPool,
|
|
472
|
+
spawn: () => new Worker(new URL('./islandWorker.ts', import.meta.url), { type: 'module' }),
|
|
473
|
+
});
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### What makes any of this safe
|
|
477
|
+
|
|
478
|
+
`DisjointExecutor` snapshots the body lanes before each island and fails when two islands write one
|
|
479
|
+
of them. It needs no threads and no timing, so it cannot be flaky in either direction, and it is the
|
|
480
|
+
one test in this package that would notice the day the disjointness assumption stopped holding.
|
|
481
|
+
Every write inside the solve is guarded by `invMass > 0` or walks `bodyOrder`, which holds only
|
|
482
|
+
dynamic bodies, so a static floor under twenty towers is read by all of them and written by none.
|
|
483
|
+
|
|
484
|
+
## Determinism is a construction here, not an aspiration
|
|
485
|
+
|
|
486
|
+
Everything on the path that feeds a tick uses `+ - * /` and `Math.sqrt` and nothing else. Those are
|
|
487
|
+
what ECMAScript specifies as IEEE-754 operations, and IEEE-754 requires `sqrt` correctly rounded.
|
|
488
|
+
The functions ECMAScript declines to specify — `hypot`, `pow`, `sin`, `atan2` and the rest — are
|
|
489
|
+
banned, and `scripts/determinism.test.mjs` fails on any of them.
|
|
490
|
+
|
|
491
|
+
That gate exists because the claim came first and was false. `hullShape`'s header promised the same
|
|
492
|
+
features in the same order on every machine while ten `Math.hypot` calls sat under it, two of them
|
|
493
|
+
deciding whether a plane becomes a face and how many separating axes exist. Nothing in the suite
|
|
494
|
+
could see it.
|
|
495
|
+
|
|
496
|
+
`fingerprintColliders` hashes the geometry a recording was made against, so a consumer can refuse a
|
|
497
|
+
replay rather than run it against a world that has since changed.
|
|
498
|
+
|
|
499
|
+
## Documentation
|
|
500
|
+
|
|
501
|
+
This file covers the dynamics landing on top of the collision core: the solver, joints,
|
|
502
|
+
queries, a character controller, ragdolls, vehicles and cloth — and what each of them
|
|
503
|
+
refuses, with what would reverse it.
|
package/dist/bodies.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ConvexShape } from './shape.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Rigid body state, structure-of-arrays, indexed by a dense body index.
|
|
4
|
+
*
|
|
5
|
+
* **`Float32Array` for state and doubles for every intermediate.** A store rounds through
|
|
6
|
+
* `Math.fround`, which ECMAScript specifies exactly, while the arithmetic between stores stays in
|
|
7
|
+
* doubles — so the solver computes at higher precision than it keeps and halves its memory
|
|
8
|
+
* bandwidth. Both halves of that are deliberate, and JavaScript has no SIMD to spend the bandwidth
|
|
9
|
+
* on instead.
|
|
10
|
+
*
|
|
11
|
+
* **Indices are dense and removal swaps the last body in**, so a caller holding an index must be
|
|
12
|
+
* told when one moves. `remove` returns the index that moved, or −1, and every structure keyed by
|
|
13
|
+
* body index has to act on it.
|
|
14
|
+
*/
|
|
15
|
+
export declare const BODY_STATIC = 0;
|
|
16
|
+
export declare const BODY_KINEMATIC = 1;
|
|
17
|
+
export declare const BODY_DYNAMIC = 2;
|
|
18
|
+
/** Static never moves, kinematic moves but is not pushed, dynamic is solved. */
|
|
19
|
+
export type BodyType = 0 | 1 | 2;
|
|
20
|
+
/** Set on a body whose island is asleep. Woken bodies clear it together. */
|
|
21
|
+
export declare const BODY_SLEEPING = 1;
|
|
22
|
+
/**
|
|
23
|
+
* A sensor reports overlap and resolves none of it.
|
|
24
|
+
*
|
|
25
|
+
* Narrowphase still runs — an overlap that is never computed cannot be reported — and the solver is
|
|
26
|
+
* what skips it. **What this costs** is a sensor being as dear as a solid body in the broadphase and
|
|
27
|
+
* narrowphase, which is where most of the cost is anyway.
|
|
28
|
+
*/
|
|
29
|
+
export declare const BODY_SENSOR = 2;
|
|
30
|
+
export interface BodyDesc {
|
|
31
|
+
readonly type: BodyType;
|
|
32
|
+
/**
|
|
33
|
+
* The body's shape, or absent where it is made of several. Exactly one of these two.
|
|
34
|
+
*
|
|
35
|
+
* A body carrying both is refused at `addBody`, because there is no sensible reading of it: the
|
|
36
|
+
* narrow phase would have to decide which one it meant and either choice is a body that collides
|
|
37
|
+
* as something the caller did not describe.
|
|
38
|
+
*/
|
|
39
|
+
readonly shape?: ConvexShape;
|
|
40
|
+
/**
|
|
41
|
+
* Several shapes, as one rigid body. What `decomposeConvex` produces, and where it goes.
|
|
42
|
+
*
|
|
43
|
+
* **No offset and no rotation per part**, because a `ConvexShape` is already a point cloud in the
|
|
44
|
+
* body's frame — see `vertices` above. A decomposition's parts are where they belong the moment
|
|
45
|
+
* they exist, and a field that would always be zero is a field somebody eventually sets wrong. The
|
|
46
|
+
* kinematic `Body.parts` carries offsets because it wraps shapes built at the origin and placed
|
|
47
|
+
* afterwards; nothing here does that.
|
|
48
|
+
*
|
|
49
|
+
* Mass is `combineMassProperties`: volumes added, the centre weighted, each tensor moved to the
|
|
50
|
+
* body's centre by the parallel axis theorem. Contacts are one manifold per touching part, with
|
|
51
|
+
* the part pair folded into every feature id so warm starting cannot confuse two of them.
|
|
52
|
+
*
|
|
53
|
+
* Refused above `MAX_BODY_PARTS`, at the call rather than in the tick.
|
|
54
|
+
*/
|
|
55
|
+
readonly shapes?: readonly ConvexShape[];
|
|
56
|
+
readonly density?: number;
|
|
57
|
+
readonly x?: number;
|
|
58
|
+
readonly y?: number;
|
|
59
|
+
readonly z?: number;
|
|
60
|
+
/** Unit quaternion. Defaults to identity. */
|
|
61
|
+
readonly qx?: number;
|
|
62
|
+
readonly qy?: number;
|
|
63
|
+
readonly qz?: number;
|
|
64
|
+
readonly qw?: number;
|
|
65
|
+
readonly friction?: number;
|
|
66
|
+
readonly restitution?: number;
|
|
67
|
+
/** Which layers this body is on, and which it layersInteract with. 32 bits each. */
|
|
68
|
+
readonly layer?: number;
|
|
69
|
+
readonly mask?: number;
|
|
70
|
+
/** A sensor reports overlap and is never pushed out of it. */
|
|
71
|
+
readonly sensor?: boolean;
|
|
72
|
+
}
|
|
73
|
+
export declare class BodySet {
|
|
74
|
+
count: number;
|
|
75
|
+
posX: Float32Array;
|
|
76
|
+
posY: Float32Array;
|
|
77
|
+
posZ: Float32Array;
|
|
78
|
+
rotX: Float32Array;
|
|
79
|
+
rotY: Float32Array;
|
|
80
|
+
rotZ: Float32Array;
|
|
81
|
+
rotW: Float32Array;
|
|
82
|
+
velX: Float32Array;
|
|
83
|
+
velY: Float32Array;
|
|
84
|
+
velZ: Float32Array;
|
|
85
|
+
angX: Float32Array;
|
|
86
|
+
angY: Float32Array;
|
|
87
|
+
angZ: Float32Array;
|
|
88
|
+
invMass: Float32Array;
|
|
89
|
+
/** Body-frame inverse inertia, six symmetric terms per body: xx, yy, zz, xy, xz, yz. */
|
|
90
|
+
invInertia: Float32Array;
|
|
91
|
+
/** Centre of mass in body frame, which a shape built off-centre has. */
|
|
92
|
+
comX: Float32Array;
|
|
93
|
+
comY: Float32Array;
|
|
94
|
+
comZ: Float32Array;
|
|
95
|
+
friction: Float32Array;
|
|
96
|
+
restitution: Float32Array;
|
|
97
|
+
type: Uint8Array;
|
|
98
|
+
flags: Uint8Array;
|
|
99
|
+
sleepTicks: Uint16Array;
|
|
100
|
+
layer: Uint32Array;
|
|
101
|
+
mask: Uint32Array;
|
|
102
|
+
shape: (ConvexShape | undefined)[];
|
|
103
|
+
/** The parts of a compound body, or undefined where it has a single `shape`. */
|
|
104
|
+
shapes: (readonly ConvexShape[] | undefined)[];
|
|
105
|
+
private capacity;
|
|
106
|
+
constructor(capacity?: number);
|
|
107
|
+
add(desc: BodyDesc): number;
|
|
108
|
+
/** Swap-remove. Returns the index that moved into `index`, or −1 if it was the last. */
|
|
109
|
+
remove(index: number): number;
|
|
110
|
+
/**
|
|
111
|
+
* Fill `out` with the world-frame inverse inertia, six symmetric terms.
|
|
112
|
+
*
|
|
113
|
+
* `R · I⁻¹ · Rᵀ`, a congruence, rather than a diagonalisation. Storing three principal moments
|
|
114
|
+
* would need an eigen decomposition, and Jacobi is iterative — a fixed sweep count would be
|
|
115
|
+
* deterministic but is one more thing to get exactly right for no gain. **What this gives up** is
|
|
116
|
+
* six floats per body instead of three and a congruence per substep. **What would make it wrong**
|
|
117
|
+
* is a measurement showing that congruence dominates the tick.
|
|
118
|
+
*/
|
|
119
|
+
worldInverseInertia(index: number, out: Float32Array, at?: number): void;
|
|
120
|
+
private grow;
|
|
121
|
+
}
|