@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/src/vehicle.ts
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { createRayHit } from './query.ts';
|
|
2
|
+
import type { QueryFilter, RayHit } from './query.ts';
|
|
3
|
+
import type { PhysicsWorld } from './world.ts';
|
|
4
|
+
import type { GroundProbe, GroundProbeHit } from './controller.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A raycast vehicle: a chassis body, and wheels that are rays rather than bodies.
|
|
8
|
+
*
|
|
9
|
+
* **Rigid wheels on joints are the obvious alternative and are unstable in practice.** They need
|
|
10
|
+
* joint stiffness and substep counts that nobody wants to pay, which is why Bullet, Godot and Unity
|
|
11
|
+
* all raycast. *What that costs, said where the decision is:* a raycast wheel cannot wedge in
|
|
12
|
+
* geometry, cannot collide with another wheel, and a vehicle on its side behaves as a box rather
|
|
13
|
+
* than as a car. *What would reverse it* is a game about vehicles interlocking with terrain.
|
|
14
|
+
*
|
|
15
|
+
* **The tyre curve is a table the caller supplies.** Pacejka's magic formula is `sin(atan(…))` and
|
|
16
|
+
* both are banned from the tick, so grip is sampled from a curve and linearly interpolated. This is
|
|
17
|
+
* the one place in Track B where the arithmetic contract produced a *better* API than the closed
|
|
18
|
+
* form it replaced: a curve is edited by moving a point, and a magic formula is edited by guessing
|
|
19
|
+
* at a coefficient nobody can picture.
|
|
20
|
+
*
|
|
21
|
+
* Everything here is applied as an impulse at a point on the chassis, so the vehicle's roll, pitch
|
|
22
|
+
* and weight transfer fall out of the solver rather than being modelled separately.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Grip against slip, as a lookup table.
|
|
27
|
+
*
|
|
28
|
+
* `force[i]` is the fraction of the load available as lateral or longitudinal force at
|
|
29
|
+
* `slip[i]`. Both arrays are the same length and `slip` must ascend. Beyond the last entry the
|
|
30
|
+
* final value is held, which is what a tyre past its peak does.
|
|
31
|
+
*/
|
|
32
|
+
export interface TyreCurve {
|
|
33
|
+
readonly slip: Float32Array;
|
|
34
|
+
readonly force: Float32Array;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A curve that rises to a peak and falls away, which is what a tyre does. */
|
|
38
|
+
export function defaultTyreCurve(peak = 1): TyreCurve {
|
|
39
|
+
return {
|
|
40
|
+
slip: new Float32Array([0, 0.1, 0.2, 0.4, 0.8, 1.5]),
|
|
41
|
+
force: new Float32Array([0, peak * 0.75, peak, peak * 0.9, peak * 0.75, peak * 0.7]),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface WheelOptions {
|
|
46
|
+
/** Where the suspension is anchored, in the chassis's local frame. */
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
z: number;
|
|
50
|
+
radius?: number;
|
|
51
|
+
/** How far the wheel may travel below its anchor. */
|
|
52
|
+
suspensionTravel?: number;
|
|
53
|
+
/** Spring rate, in newtons per metre of compression. */
|
|
54
|
+
stiffness?: number;
|
|
55
|
+
/** Damping, in newtons per metre per second. */
|
|
56
|
+
damping?: number;
|
|
57
|
+
/** Whether steering input turns this wheel. */
|
|
58
|
+
steers?: boolean;
|
|
59
|
+
/** Whether engine and brake force reach it. */
|
|
60
|
+
driven?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface VehicleOptions {
|
|
64
|
+
wheels: readonly WheelOptions[];
|
|
65
|
+
/** Maximum steering angle, as the tangent of the angle rather than the angle. */
|
|
66
|
+
maxSteerTangent?: number;
|
|
67
|
+
/** Newtons at full throttle, shared across driven wheels. */
|
|
68
|
+
engineForce?: number;
|
|
69
|
+
/** Newtons at full brake. */
|
|
70
|
+
brakeForce?: number;
|
|
71
|
+
longitudinal?: TyreCurve;
|
|
72
|
+
lateral?: TyreCurve;
|
|
73
|
+
filter?: QueryFilter;
|
|
74
|
+
/**
|
|
75
|
+
* A surface the wheels can rest on that is not a body.
|
|
76
|
+
*
|
|
77
|
+
* The same seam `CharacterController` takes, and here for the same reason plus one: a consumer
|
|
78
|
+
* whose ground is a height function had a character that stood on it and a car that fell through
|
|
79
|
+
* it, so the world's floor was two things that had to be kept in step by hand. It was reported
|
|
80
|
+
* that way — a hundred lines of ground query written consumer-side, and then written again for
|
|
81
|
+
* everything else that stands on it.
|
|
82
|
+
*
|
|
83
|
+
* Consulted only where the wheel's ray found no body, on the same ruling the controller carries:
|
|
84
|
+
* a body is a thing that is there and a surface is a description of where the ground is.
|
|
85
|
+
*/
|
|
86
|
+
ground?: GroundProbe;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface VehicleInput {
|
|
90
|
+
/** −1 to 1. */
|
|
91
|
+
throttle: number;
|
|
92
|
+
/** 0 to 1. */
|
|
93
|
+
brake: number;
|
|
94
|
+
/** −1 to 1, scaled by `maxSteerTangent`. */
|
|
95
|
+
steer: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class Vehicle {
|
|
99
|
+
readonly chassis: number;
|
|
100
|
+
readonly wheelCount: number;
|
|
101
|
+
/** Whether each wheel found ground on the last update. */
|
|
102
|
+
readonly grounded: Uint8Array;
|
|
103
|
+
/** How far each wheel's suspension is compressed, in metres. */
|
|
104
|
+
readonly compression: Float32Array;
|
|
105
|
+
/** Where each wheel's contact is, xyz-packed. Meaningful where `grounded`. */
|
|
106
|
+
readonly contact: Float32Array;
|
|
107
|
+
|
|
108
|
+
private readonly local: Float32Array;
|
|
109
|
+
private readonly radius: Float32Array;
|
|
110
|
+
private readonly travel: Float32Array;
|
|
111
|
+
private readonly stiffness: Float32Array;
|
|
112
|
+
private readonly damping: Float32Array;
|
|
113
|
+
private readonly steers: Uint8Array;
|
|
114
|
+
private readonly driven: Uint8Array;
|
|
115
|
+
private readonly maxSteerTangent: number;
|
|
116
|
+
private readonly engineForce: number;
|
|
117
|
+
private readonly brakeForce: number;
|
|
118
|
+
private readonly longitudinal: TyreCurve;
|
|
119
|
+
private readonly lateral: TyreCurve;
|
|
120
|
+
/**
|
|
121
|
+
* The suspension ray must ignore the car it holds up.
|
|
122
|
+
*
|
|
123
|
+
* A wheel anchor sits *inside* the chassis by construction — that is what a suspension mount is —
|
|
124
|
+
* and a ray starting inside a body reports that body at fraction zero, which is the right answer
|
|
125
|
+
* to the question a line-of-sight test asks and the wrong one here. Left unfiltered, every wheel
|
|
126
|
+
* found the chassis at zero distance, read the suspension as fully compressed, and pushed the car
|
|
127
|
+
* against itself: it sank onto its own chassis and sat there.
|
|
128
|
+
*/
|
|
129
|
+
private readonly filter: QueryFilter;
|
|
130
|
+
private readonly hit: RayHit = createRayHit();
|
|
131
|
+
private readonly probe: GroundProbe | null;
|
|
132
|
+
/** Scratch for the probe, claimed once: every wheel asks it every tick. */
|
|
133
|
+
private readonly probed: GroundProbeHit = { y: 0, normalX: 0, normalY: 1, normalZ: 0 };
|
|
134
|
+
/**
|
|
135
|
+
* Every wheel's impulse, gathered before any is applied: `rx ry rz px py pz` per wheel.
|
|
136
|
+
*
|
|
137
|
+
* **Applying them one at a time makes the car curve.** Each wheel then sees a velocity the
|
|
138
|
+
* previous wheel has already changed, so the left front's lateral impulse biases what the right
|
|
139
|
+
* front computes, and the asymmetry accumulates into a yaw rather than cancelling. Measured on a
|
|
140
|
+
* symmetric car under straight throttle: 0.011 metres of drift after 2 metres, 0.64 after 39, and
|
|
141
|
+
* **5.49 after 155** — a curve, not a wobble. Forces at an instant are simultaneous, and gathering
|
|
142
|
+
* them is both the correct physics and the cheaper fix.
|
|
143
|
+
*/
|
|
144
|
+
private readonly pending: Float64Array;
|
|
145
|
+
|
|
146
|
+
constructor(chassis: number, options: VehicleOptions) {
|
|
147
|
+
this.chassis = chassis;
|
|
148
|
+
this.wheelCount = options.wheels.length;
|
|
149
|
+
const n = this.wheelCount;
|
|
150
|
+
this.local = new Float32Array(n * 3);
|
|
151
|
+
this.radius = new Float32Array(n);
|
|
152
|
+
this.travel = new Float32Array(n);
|
|
153
|
+
this.stiffness = new Float32Array(n);
|
|
154
|
+
this.damping = new Float32Array(n);
|
|
155
|
+
this.steers = new Uint8Array(n);
|
|
156
|
+
this.driven = new Uint8Array(n);
|
|
157
|
+
this.grounded = new Uint8Array(n);
|
|
158
|
+
this.compression = new Float32Array(n);
|
|
159
|
+
this.contact = new Float32Array(n * 3);
|
|
160
|
+
options.wheels.forEach((w, i) => {
|
|
161
|
+
this.local[i * 3] = w.x;
|
|
162
|
+
this.local[i * 3 + 1] = w.y;
|
|
163
|
+
this.local[i * 3 + 2] = w.z;
|
|
164
|
+
this.radius[i] = w.radius ?? 0.35;
|
|
165
|
+
this.travel[i] = w.suspensionTravel ?? 0.3;
|
|
166
|
+
this.stiffness[i] = w.stiffness ?? 40000;
|
|
167
|
+
this.damping[i] = w.damping ?? 4000;
|
|
168
|
+
this.steers[i] = w.steers ? 1 : 0;
|
|
169
|
+
this.driven[i] = w.driven ? 1 : 0;
|
|
170
|
+
});
|
|
171
|
+
this.maxSteerTangent = options.maxSteerTangent ?? 0.6;
|
|
172
|
+
this.engineForce = options.engineForce ?? 6000;
|
|
173
|
+
this.brakeForce = options.brakeForce ?? 9000;
|
|
174
|
+
this.longitudinal = options.longitudinal ?? defaultTyreCurve(1.2);
|
|
175
|
+
this.lateral = options.lateral ?? defaultTyreCurve(1.4);
|
|
176
|
+
this.filter = { ...options.filter, ignore: chassis };
|
|
177
|
+
this.probe = options.ground ?? null;
|
|
178
|
+
this.pending = new Float64Array(n * 6);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Advance the vehicle one fixed tick. Call before `world.step`. */
|
|
182
|
+
update(world: PhysicsWorld, dt: number, input: VehicleInput): void {
|
|
183
|
+
const body = this.chassis;
|
|
184
|
+
const bodies = world.bodies;
|
|
185
|
+
this.pending.fill(0);
|
|
186
|
+
let drivenCount = 0;
|
|
187
|
+
for (let i = 0; i < this.wheelCount; i++) if (this.driven[i]) drivenCount++;
|
|
188
|
+
const perWheelDrive = drivenCount > 0 ? (input.throttle * this.engineForce) / drivenCount : 0;
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < this.wheelCount; i++) {
|
|
191
|
+
rotate(bodies, body, this.local, i * 3, ANCHOR);
|
|
192
|
+
const ax = (bodies.posX[body] ?? 0) + ANCHOR[0];
|
|
193
|
+
const ay = (bodies.posY[body] ?? 0) + ANCHOR[1];
|
|
194
|
+
const az = (bodies.posZ[body] ?? 0) + ANCHOR[2];
|
|
195
|
+
|
|
196
|
+
// The suspension looks along the chassis's own down, not world down.
|
|
197
|
+
axis(bodies, body, 0, -1, 0, DOWN);
|
|
198
|
+
const reach = (this.travel[i] ?? 0) + (this.radius[i] ?? 0);
|
|
199
|
+
if (
|
|
200
|
+
!world.raycast(ax, ay, az, DOWN[0], DOWN[1], DOWN[2], reach, this.hit, this.filter) &&
|
|
201
|
+
!this.probeUnder(ax, ay, az, reach)
|
|
202
|
+
) {
|
|
203
|
+
this.grounded[i] = 0;
|
|
204
|
+
this.compression[i] = 0;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
this.grounded[i] = 1;
|
|
208
|
+
this.contact[i * 3] = this.hit.x;
|
|
209
|
+
this.contact[i * 3 + 1] = this.hit.y;
|
|
210
|
+
this.contact[i * 3 + 2] = this.hit.z;
|
|
211
|
+
|
|
212
|
+
const distance = this.hit.fraction * reach;
|
|
213
|
+
/*
|
|
214
|
+
* Compression is capped at the travel. Past that the wheel is bottomed out and the *chassis*
|
|
215
|
+
* takes the load through its own collider, which is what a bump stop is.
|
|
216
|
+
*
|
|
217
|
+
* **No test here reaches it, and that is worth saying rather than leaving somebody to look.**
|
|
218
|
+
* With wheels mounted inside the chassis — the ordinary arrangement — the chassis collider
|
|
219
|
+
* meets the ground before the suspension can bottom out, so removing this line changes a drop
|
|
220
|
+
* from six metres by a centimetre and nothing else. It stays because a vehicle with outboard
|
|
221
|
+
* mounts *can* reach it, and an uncapped spring then answers a hard landing with several
|
|
222
|
+
* times the car's weight.
|
|
223
|
+
*/
|
|
224
|
+
let compression = reach - distance;
|
|
225
|
+
const bumpStop = this.travel[i] ?? 0;
|
|
226
|
+
if (compression > bumpStop) compression = bumpStop;
|
|
227
|
+
this.compression[i] = compression;
|
|
228
|
+
|
|
229
|
+
// The anchor's velocity, which is what the damper answers to.
|
|
230
|
+
pointVelocity(bodies, body, ANCHOR, VEL);
|
|
231
|
+
const alongDown = VEL[0] * DOWN[0] + VEL[1] * DOWN[1] + VEL[2] * DOWN[2];
|
|
232
|
+
/*
|
|
233
|
+
* **The damper adds to the load while the spring is compressing.** `alongDown` is positive
|
|
234
|
+
* when the anchor is moving toward the ground, which is exactly when compression is
|
|
235
|
+
* increasing, so a damper opposing that motion pushes *up*. Subtracting it instead — which is
|
|
236
|
+
* what `k·x − c·v` reads like if `v` is taken as the wrong sign — leaves the spring undamped
|
|
237
|
+
* and clamps the load to zero on the way down: the car fell, bounced on a pure spring, and
|
|
238
|
+
* launched itself upside down.
|
|
239
|
+
*/
|
|
240
|
+
let load = (this.stiffness[i] ?? 0) * compression + (this.damping[i] ?? 0) * alongDown;
|
|
241
|
+
if (load < 0) load = 0;
|
|
242
|
+
// Along the surface normal rather than along the ray, so a slope pushes sideways too.
|
|
243
|
+
this.gather(
|
|
244
|
+
i,
|
|
245
|
+
ANCHOR,
|
|
246
|
+
this.hit.nx * load * dt,
|
|
247
|
+
this.hit.ny * load * dt,
|
|
248
|
+
this.hit.nz * load * dt,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// The wheel's own frame: forward turned by steering, and lateral across it.
|
|
252
|
+
const steer = this.steers[i] ? input.steer * this.maxSteerTangent : 0;
|
|
253
|
+
axis(bodies, body, steer, 0, 1, FORWARD);
|
|
254
|
+
normalise(FORWARD);
|
|
255
|
+
axis(bodies, body, 1, 0, -steer, SIDE);
|
|
256
|
+
normalise(SIDE);
|
|
257
|
+
// Flattened onto the surface, so grip acts along the ground rather than into it.
|
|
258
|
+
project(FORWARD, this.hit.nx, this.hit.ny, this.hit.nz);
|
|
259
|
+
project(SIDE, this.hit.nx, this.hit.ny, this.hit.nz);
|
|
260
|
+
normalise(FORWARD);
|
|
261
|
+
normalise(SIDE);
|
|
262
|
+
|
|
263
|
+
pointVelocity(bodies, body, ANCHOR, VEL);
|
|
264
|
+
const vForward = VEL[0] * FORWARD[0] + VEL[1] * FORWARD[1] + VEL[2] * FORWARD[2];
|
|
265
|
+
const vSide = VEL[0] * SIDE[0] + VEL[1] * SIDE[1] + VEL[2] * SIDE[2];
|
|
266
|
+
const speed = Math.sqrt(vForward * vForward + vSide * vSide);
|
|
267
|
+
|
|
268
|
+
/*
|
|
269
|
+
* Slip is the sideways or braking velocity against the speed the wheel is going, which is what
|
|
270
|
+
* a tyre curve is indexed by. At a standstill the ratio is undefined, so the slip is taken as
|
|
271
|
+
* the velocity itself — a stationary car with a sideways nudge should still resist it.
|
|
272
|
+
*/
|
|
273
|
+
const denom = speed > 1 ? speed : 1;
|
|
274
|
+
const lateralGrip = sampleTyreCurve(this.lateral, Math.abs(vSide) / denom) * load;
|
|
275
|
+
const sideImpulse = clampMagnitude(-vSide * massAt(bodies, body), lateralGrip * dt);
|
|
276
|
+
this.gather(i, ANCHOR, SIDE[0] * sideImpulse, SIDE[1] * sideImpulse, SIDE[2] * sideImpulse);
|
|
277
|
+
|
|
278
|
+
let forwardForce = this.driven[i] ? perWheelDrive : 0;
|
|
279
|
+
if (input.brake > 0) {
|
|
280
|
+
const brake = input.brake * this.brakeForce;
|
|
281
|
+
forwardForce += vForward > 0 ? -brake : brake;
|
|
282
|
+
}
|
|
283
|
+
const traction = sampleTyreCurve(this.longitudinal, Math.abs(vForward) / denom) * load;
|
|
284
|
+
const limit = traction > 0 ? traction : load;
|
|
285
|
+
const drive = clampMagnitude(forwardForce, limit);
|
|
286
|
+
this.gather(
|
|
287
|
+
i,
|
|
288
|
+
ANCHOR,
|
|
289
|
+
FORWARD[0] * drive * dt,
|
|
290
|
+
FORWARD[1] * drive * dt,
|
|
291
|
+
FORWARD[2] * drive * dt,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Now apply, all from the state every wheel was measured against.
|
|
296
|
+
for (let i = 0; i < this.wheelCount; i++) {
|
|
297
|
+
const at = i * 6;
|
|
298
|
+
ANCHOR[0] = this.pending[at];
|
|
299
|
+
ANCHOR[1] = this.pending[at + 1];
|
|
300
|
+
ANCHOR[2] = this.pending[at + 2];
|
|
301
|
+
applyImpulse(
|
|
302
|
+
bodies,
|
|
303
|
+
body,
|
|
304
|
+
ANCHOR,
|
|
305
|
+
this.pending[at + 3],
|
|
306
|
+
this.pending[at + 4],
|
|
307
|
+
this.pending[at + 5],
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
world.wakeIsland(body);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Add an impulse at a wheel's anchor to what will be applied at the end of the pass. */
|
|
314
|
+
/**
|
|
315
|
+
* Rest a wheel on the analytic surface, filling `hit` as a ray against a body would have.
|
|
316
|
+
*
|
|
317
|
+
* Everything downstream — the spring, the damper, the tyre frame flattened onto the ground —
|
|
318
|
+
* reads `hit`, so filling it is the whole of the wiring and no grip code learns that a surface
|
|
319
|
+
* exists.
|
|
320
|
+
*
|
|
321
|
+
* **The column is the anchor's own and the drop is measured along the chassis's down**, which is
|
|
322
|
+
* exact while the chassis is upright and off by its tilt when it is not: the true contact sits a
|
|
323
|
+
* little to the side of the column that was sampled, and on a slope that column is a little
|
|
324
|
+
* higher or lower than the contact. At the lean angles a car spends its life at the error is
|
|
325
|
+
* millimetres, and the alternative is solving a ray against an arbitrary height function, which
|
|
326
|
+
* is an iteration this package does not allow itself. A vehicle on its roof finds no ground here
|
|
327
|
+
* at all, because there is no downward intersection to find, which is the honest answer.
|
|
328
|
+
*/
|
|
329
|
+
private probeUnder(ax: number, ay: number, az: number, reach: number): boolean {
|
|
330
|
+
if (this.probe === null || DOWN[1] >= 0) return false;
|
|
331
|
+
if (!this.probe.sample(ax, az, this.probed, ay)) return false;
|
|
332
|
+
const distance = (this.probed.y - ay) / DOWN[1];
|
|
333
|
+
if (!(distance >= 0) || distance > reach) return false;
|
|
334
|
+
this.hit.body = -1;
|
|
335
|
+
this.hit.fraction = distance / reach;
|
|
336
|
+
this.hit.x = ax + DOWN[0] * distance;
|
|
337
|
+
this.hit.y = ay + DOWN[1] * distance;
|
|
338
|
+
this.hit.z = az + DOWN[2] * distance;
|
|
339
|
+
this.hit.nx = this.probed.normalX;
|
|
340
|
+
this.hit.ny = this.probed.normalY;
|
|
341
|
+
this.hit.nz = this.probed.normalZ;
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private gather(i: number, r: Float64Array, px: number, py: number, pz: number): void {
|
|
346
|
+
const at = i * 6;
|
|
347
|
+
this.pending[at] = r[0];
|
|
348
|
+
this.pending[at + 1] = r[1];
|
|
349
|
+
this.pending[at + 2] = r[2];
|
|
350
|
+
this.pending[at + 3] += px;
|
|
351
|
+
this.pending[at + 4] += py;
|
|
352
|
+
this.pending[at + 5] += pz;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Sample a curve at `at`, holding the last value beyond its end. */
|
|
357
|
+
export function sampleTyreCurve(curve: TyreCurve, at: number): number {
|
|
358
|
+
const n = curve.slip.length;
|
|
359
|
+
if (n === 0) return 0;
|
|
360
|
+
if (at <= (curve.slip[0] ?? 0)) return curve.force[0] ?? 0;
|
|
361
|
+
for (let i = 1; i < n; i++) {
|
|
362
|
+
const s1 = curve.slip[i] ?? 0;
|
|
363
|
+
if (at <= s1) {
|
|
364
|
+
const s0 = curve.slip[i - 1] ?? 0;
|
|
365
|
+
const span = s1 - s0;
|
|
366
|
+
const t = span > 0 ? (at - s0) / span : 0;
|
|
367
|
+
const f0 = curve.force[i - 1] ?? 0;
|
|
368
|
+
return f0 + ((curve.force[i] ?? 0) - f0) * t;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return curve.force[n - 1] ?? 0;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const ANCHOR = new Float64Array(3);
|
|
375
|
+
const DOWN = new Float64Array(3);
|
|
376
|
+
const FORWARD = new Float64Array(3);
|
|
377
|
+
const SIDE = new Float64Array(3);
|
|
378
|
+
const VEL = new Float64Array(3);
|
|
379
|
+
const INV_I = new Float32Array(6);
|
|
380
|
+
|
|
381
|
+
function clampMagnitude(v: number, limit: number): number {
|
|
382
|
+
if (v > limit) return limit;
|
|
383
|
+
if (v < -limit) return -limit;
|
|
384
|
+
return v;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** A representative mass at the anchor, for turning a wanted velocity change into an impulse. */
|
|
388
|
+
function massAt(bodies: PhysicsWorld['bodies'], body: number): number {
|
|
389
|
+
const im = bodies.invMass[body] ?? 0;
|
|
390
|
+
return im > 0 ? 1 / im : 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function normalise(v: Float64Array): void {
|
|
394
|
+
const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
395
|
+
if (len === 0) return;
|
|
396
|
+
v[0] /= len;
|
|
397
|
+
v[1] /= len;
|
|
398
|
+
v[2] /= len;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Remove the component of `v` along a normal, leaving it in the surface. */
|
|
402
|
+
function project(v: Float64Array, nx: number, ny: number, nz: number): void {
|
|
403
|
+
const d = v[0] * nx + v[1] * ny + v[2] * nz;
|
|
404
|
+
v[0] -= nx * d;
|
|
405
|
+
v[1] -= ny * d;
|
|
406
|
+
v[2] -= nz * d;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function rotate(
|
|
410
|
+
bodies: PhysicsWorld['bodies'],
|
|
411
|
+
i: number,
|
|
412
|
+
src: Float32Array,
|
|
413
|
+
at: number,
|
|
414
|
+
out: Float64Array,
|
|
415
|
+
): void {
|
|
416
|
+
const x = src[at] ?? 0;
|
|
417
|
+
const y = src[at + 1] ?? 0;
|
|
418
|
+
const z = src[at + 2] ?? 0;
|
|
419
|
+
const qx = bodies.rotX[i] ?? 0;
|
|
420
|
+
const qy = bodies.rotY[i] ?? 0;
|
|
421
|
+
const qz = bodies.rotZ[i] ?? 0;
|
|
422
|
+
const qw = bodies.rotW[i] ?? 1;
|
|
423
|
+
const tx = 2 * (qy * z - qz * y);
|
|
424
|
+
const ty = 2 * (qz * x - qx * z);
|
|
425
|
+
const tz = 2 * (qx * y - qy * x);
|
|
426
|
+
out[0] = x + qw * tx + (qy * tz - qz * ty);
|
|
427
|
+
out[1] = y + qw * ty + (qz * tx - qx * tz);
|
|
428
|
+
out[2] = z + qw * tz + (qx * ty - qy * tx);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** A direction in the body's frame, rotated into world. */
|
|
432
|
+
function axis(
|
|
433
|
+
bodies: PhysicsWorld['bodies'],
|
|
434
|
+
i: number,
|
|
435
|
+
x: number,
|
|
436
|
+
y: number,
|
|
437
|
+
z: number,
|
|
438
|
+
out: Float64Array,
|
|
439
|
+
): void {
|
|
440
|
+
const qx = bodies.rotX[i] ?? 0;
|
|
441
|
+
const qy = bodies.rotY[i] ?? 0;
|
|
442
|
+
const qz = bodies.rotZ[i] ?? 0;
|
|
443
|
+
const qw = bodies.rotW[i] ?? 1;
|
|
444
|
+
const tx = 2 * (qy * z - qz * y);
|
|
445
|
+
const ty = 2 * (qz * x - qx * z);
|
|
446
|
+
const tz = 2 * (qx * y - qy * x);
|
|
447
|
+
out[0] = x + qw * tx + (qy * tz - qz * ty);
|
|
448
|
+
out[1] = y + qw * ty + (qz * tx - qx * tz);
|
|
449
|
+
out[2] = z + qw * tz + (qx * ty - qy * tx);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function pointVelocity(
|
|
453
|
+
bodies: PhysicsWorld['bodies'],
|
|
454
|
+
i: number,
|
|
455
|
+
r: Float64Array,
|
|
456
|
+
out: Float64Array,
|
|
457
|
+
): void {
|
|
458
|
+
const wx = bodies.angX[i] ?? 0;
|
|
459
|
+
const wy = bodies.angY[i] ?? 0;
|
|
460
|
+
const wz = bodies.angZ[i] ?? 0;
|
|
461
|
+
out[0] = (bodies.velX[i] ?? 0) + (wy * r[2] - wz * r[1]);
|
|
462
|
+
out[1] = (bodies.velY[i] ?? 0) + (wz * r[0] - wx * r[2]);
|
|
463
|
+
out[2] = (bodies.velZ[i] ?? 0) + (wx * r[1] - wy * r[0]);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function applyImpulse(
|
|
467
|
+
bodies: PhysicsWorld['bodies'],
|
|
468
|
+
i: number,
|
|
469
|
+
r: Float64Array,
|
|
470
|
+
px: number,
|
|
471
|
+
py: number,
|
|
472
|
+
pz: number,
|
|
473
|
+
): void {
|
|
474
|
+
const im = bodies.invMass[i] ?? 0;
|
|
475
|
+
if (im === 0) return;
|
|
476
|
+
bodies.velX[i] = (bodies.velX[i] ?? 0) + px * im;
|
|
477
|
+
bodies.velY[i] = (bodies.velY[i] ?? 0) + py * im;
|
|
478
|
+
bodies.velZ[i] = (bodies.velZ[i] ?? 0) + pz * im;
|
|
479
|
+
bodies.worldInverseInertia(i, INV_I);
|
|
480
|
+
const tx = r[1] * pz - r[2] * py;
|
|
481
|
+
const ty = r[2] * px - r[0] * pz;
|
|
482
|
+
const tz = r[0] * py - r[1] * px;
|
|
483
|
+
bodies.angX[i] =
|
|
484
|
+
(bodies.angX[i] ?? 0) + (INV_I[0] ?? 0) * tx + (INV_I[3] ?? 0) * ty + (INV_I[4] ?? 0) * tz;
|
|
485
|
+
bodies.angY[i] =
|
|
486
|
+
(bodies.angY[i] ?? 0) + (INV_I[3] ?? 0) * tx + (INV_I[1] ?? 0) * ty + (INV_I[5] ?? 0) * tz;
|
|
487
|
+
bodies.angZ[i] =
|
|
488
|
+
(bodies.angZ[i] ?? 0) + (INV_I[4] ?? 0) * tx + (INV_I[5] ?? 0) * ty + (INV_I[2] ?? 0) * tz;
|
|
489
|
+
}
|