@woosh/meep-engine 2.153.0 → 2.155.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/package.json +1 -1
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts +112 -0
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/ConvexHullShape3D.js +325 -0
- package/src/core/geom/vec3/v3_array_copy.d.ts +3 -3
- package/src/core/geom/vec3/v3_array_copy.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_array_copy.js +2 -2
- package/src/core/geom/vec3/v3_cross.d.ts +17 -0
- package/src/core/geom/vec3/v3_cross.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_cross.js +20 -0
- package/src/core/geom/vec3/v3_subtract.d.ts +16 -0
- package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -0
- package/src/core/geom/vec3/v3_subtract.js +19 -0
- package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +8 -0
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +4 -0
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.js +21 -0
- package/src/engine/physics/PLAN.md +4 -4
- package/src/engine/physics/body/BodyStorage.d.ts +3 -1
- package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
- package/src/engine/physics/body/BodyStorage.js +452 -450
- package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -1
- package/src/engine/physics/body/SolverBodyState.js +6 -5
- package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
- package/src/engine/physics/broadphase/generate_pairs.js +9 -1
- package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
- package/src/engine/physics/ccd/linear_sweep.js +237 -238
- package/src/engine/physics/computeInterceptPoint.d.ts.map +1 -1
- package/src/engine/physics/computeInterceptPoint.js +8 -3
- package/src/engine/physics/contact/ManifoldStore.d.ts +0 -16
- package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
- package/src/engine/physics/contact/ManifoldStore.js +1 -38
- package/src/engine/physics/ecs/BodyKind.d.ts +3 -2
- package/src/engine/physics/ecs/BodyKind.d.ts.map +1 -1
- package/src/engine/physics/ecs/BodyKind.js +25 -24
- package/src/engine/physics/ecs/PhysicsEvents.d.ts +4 -5
- package/src/engine/physics/ecs/PhysicsEvents.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsEvents.js +15 -16
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +5 -30
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +13 -45
- package/src/engine/physics/ecs/RigidBodySerializationAdapter.d.ts.map +1 -1
- package/src/engine/physics/ecs/RigidBodySerializationAdapter.js +85 -81
- package/src/engine/physics/ecs/is_sensor.d.ts +18 -0
- package/src/engine/physics/ecs/is_sensor.d.ts.map +1 -0
- package/src/engine/physics/ecs/is_sensor.js +27 -0
- package/src/engine/physics/events/ContactEventBuffer.d.ts +2 -1
- package/src/engine/physics/events/ContactEventBuffer.d.ts.map +1 -1
- package/src/engine/physics/events/ContactEventBuffer.js +84 -83
- package/src/engine/physics/gjk/gjk.d.ts +0 -26
- package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
- package/src/engine/physics/gjk/gjk.js +3 -52
- package/src/engine/physics/gjk/gjk_epa_penetration.d.ts +20 -0
- package/src/engine/physics/gjk/gjk_epa_penetration.d.ts.map +1 -0
- package/src/engine/physics/gjk/gjk_epa_penetration.js +548 -0
- package/src/engine/physics/gjk/minkowski_support.d.ts +4 -9
- package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -1
- package/src/engine/physics/gjk/minkowski_support.js +70 -75
- package/src/engine/physics/gjk/mpr.d.ts +1 -1
- package/src/engine/physics/gjk/mpr.d.ts.map +1 -1
- package/src/engine/physics/gjk/mpr.js +362 -344
- package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
- package/src/engine/physics/island/IslandBuilder.js +431 -428
- package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/box_box_manifold.js +4 -81
- package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/box_triangle_contact.js +4 -39
- package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/capsule_contacts.js +459 -462
- package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/clip_against_axis_uv.js +4 -1
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +83 -0
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/convex_convex_manifold.js +425 -0
- package/src/engine/physics/narrowphase/convex_decomposition.d.ts +32 -0
- package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/convex_decomposition.js +293 -0
- package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts +41 -0
- package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/mesh_convex_hull.js +106 -0
- package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts +8 -0
- package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.js +117 -0
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +105 -102
- package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts +29 -0
- package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts.map +1 -0
- package/src/engine/physics/narrowphase/reduce_manifold_contacts.js +69 -0
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_concave.js +152 -145
- package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/sphere_box_contact.js +132 -123
- package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
- package/src/engine/physics/queries/overlap_shape.js +16 -17
- package/src/engine/physics/queries/raycast.d.ts +5 -0
- package/src/engine/physics/queries/raycast.d.ts.map +1 -1
- package/src/engine/physics/queries/raycast.js +16 -8
- package/src/engine/physics/queries/shape_cast.d.ts.map +1 -1
- package/src/engine/physics/queries/shape_cast.js +13 -7
- package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
- package/src/engine/physics/solver/solve_contacts.js +8 -11
- package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
- package/src/engine/physics/vehicle/RaycastVehicle.js +339 -333
- package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +0 -13
- package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +0 -1
- package/src/engine/physics/gjk/expanding_polytope_algorithm.js +0 -399
|
@@ -1,333 +1,339 @@
|
|
|
1
|
-
import { Ray3 } from "../../../core/geom/3d/ray/Ray3.js";
|
|
2
|
-
import { PhysicsSurfacePoint } from "../queries/PhysicsSurfacePoint.js";
|
|
3
|
-
import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* @
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
out[
|
|
69
|
-
out[
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* One wheel's config + per-frame runtime state. The runtime fields
|
|
76
|
-
* (`inContact`, `compression`, contact point/normal, `rotation`, …) are read by
|
|
77
|
-
* rendering / gameplay; do not write them.
|
|
78
|
-
*/
|
|
79
|
-
export class Wheel {
|
|
80
|
-
constructor() {
|
|
81
|
-
// --- config ---
|
|
82
|
-
/** Mount point on the chassis, local. @type {Float64Array} */
|
|
83
|
-
this.localPosition = new Float64Array(3);
|
|
84
|
-
/** Suspension direction (local, unit, points "down"). @type {Float64Array} */
|
|
85
|
-
this.localSuspensionDir = Float64Array.from([0, -1, 0]);
|
|
86
|
-
/** Forward / rolling direction (local, unit). @type {Float64Array} */
|
|
87
|
-
this.localForward = Float64Array.from([0, 0, 1]);
|
|
88
|
-
this.suspensionRestLength = 0.5;
|
|
89
|
-
this.suspensionStiffness = 100;
|
|
90
|
-
this.suspensionDamping = 10;
|
|
91
|
-
this.suspensionMaxForce = Infinity;
|
|
92
|
-
this.radius = 0.3;
|
|
93
|
-
this.friction = 1.2;
|
|
94
|
-
this.steered = false;
|
|
95
|
-
this.driven = false;
|
|
96
|
-
|
|
97
|
-
// --- runtime ---
|
|
98
|
-
this.inContact = false;
|
|
99
|
-
this.suspensionLength = this.suspensionRestLength;
|
|
100
|
-
this.compression = 0;
|
|
101
|
-
/** World contact point. @type {Float64Array} */
|
|
102
|
-
this.contactPoint = new Float64Array(3);
|
|
103
|
-
/** World contact normal. @type {Float64Array} */
|
|
104
|
-
this.contactNormal = Float64Array.from([0, 1, 0]);
|
|
105
|
-
/** Body id of the surface under the wheel, or −1. */
|
|
106
|
-
this.contactBodyId = -1;
|
|
107
|
-
/** Current steering angle (rad), set via {@link RaycastVehicle#setSteering}. */
|
|
108
|
-
this.steering = 0;
|
|
109
|
-
/** Accumulated spin angle (rad) for rendering. */
|
|
110
|
-
this.rotation = 0;
|
|
111
|
-
/** Forward ground speed at the contact (m/s). */
|
|
112
|
-
this.forwardSpeed = 0;
|
|
113
|
-
/** Suspension force magnitude applied this frame (N). */
|
|
114
|
-
this.suspensionForce = 0;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export class RaycastVehicle {
|
|
119
|
-
/**
|
|
120
|
-
* @param {PhysicsSystem} system
|
|
121
|
-
* @param {RigidBody} chassisBody the chassis rigid body (Dynamic)
|
|
122
|
-
* @param {Transform} chassisTransform its world transform
|
|
123
|
-
*/
|
|
124
|
-
constructor(system, chassisBody, chassisTransform) {
|
|
125
|
-
this.system = system;
|
|
126
|
-
this.chassisBody = chassisBody;
|
|
127
|
-
this.chassisTransform = chassisTransform;
|
|
128
|
-
|
|
129
|
-
/** @type {Wheel[]} */
|
|
130
|
-
this.wheels = [];
|
|
131
|
-
|
|
132
|
-
this.__driveForce = 0;
|
|
133
|
-
this.__brakeForce = 0;
|
|
134
|
-
this.__drivenCount = 0;
|
|
135
|
-
|
|
136
|
-
this.__ray = new Ray3();
|
|
137
|
-
this.__hit = new PhysicsSurfacePoint();
|
|
138
|
-
// Exclude the chassis' own colliders from the suspension raycast.
|
|
139
|
-
this.__filter = (entity, collider) => collider._bodyId !== this.chassisBody._bodyId;
|
|
140
|
-
|
|
141
|
-
// Reused force / impulse / point payloads for the public API calls.
|
|
142
|
-
this.__force = { x: 0, y: 0, z: 0 };
|
|
143
|
-
this.__point = { x: 0, y: 0, z: 0 };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Add a wheel. Returns the created {@link Wheel} (also pushed to
|
|
148
|
-
* {@link wheels}); set any further config fields on it before stepping.
|
|
149
|
-
*
|
|
150
|
-
* @param {object} opts
|
|
151
|
-
* @param {number[]} opts.localPosition mount point on the chassis, local.
|
|
152
|
-
* @param {number[]} [opts.localSuspensionDir] unit "down", default (0,−1,0).
|
|
153
|
-
* @param {number[]} [opts.localForward] unit rolling dir, default (0,0,1).
|
|
154
|
-
* @param {number} [opts.suspensionRestLength]
|
|
155
|
-
* @param {number} [opts.suspensionStiffness] N/m.
|
|
156
|
-
* @param {number} [opts.suspensionDamping] N·s/m.
|
|
157
|
-
* @param {number} [opts.suspensionMaxForce] N (clamp).
|
|
158
|
-
* @param {number} [opts.radius]
|
|
159
|
-
* @param {number} [opts.friction] tyre friction coefficient μ.
|
|
160
|
-
* @param {boolean} [opts.steered]
|
|
161
|
-
* @param {boolean} [opts.driven]
|
|
162
|
-
* @returns {Wheel}
|
|
163
|
-
*/
|
|
164
|
-
addWheel(opts) {
|
|
165
|
-
const w = new Wheel();
|
|
166
|
-
const p = opts.localPosition;
|
|
167
|
-
w.localPosition[0] = p[0]; w.localPosition[1] = p[1]; w.localPosition[2] = p[2];
|
|
168
|
-
if (opts.localSuspensionDir) {
|
|
169
|
-
const d = opts.localSuspensionDir, L =
|
|
170
|
-
w.localSuspensionDir[0] = d[0] / L; w.localSuspensionDir[1] = d[1] / L; w.localSuspensionDir[2] = d[2] / L;
|
|
171
|
-
}
|
|
172
|
-
if (opts.localForward) {
|
|
173
|
-
const f = opts.localForward, L =
|
|
174
|
-
w.localForward[0] = f[0] / L; w.localForward[1] = f[1] / L; w.localForward[2] = f[2] / L;
|
|
175
|
-
}
|
|
176
|
-
if (opts.suspensionRestLength !== undefined) w.suspensionRestLength = opts.suspensionRestLength;
|
|
177
|
-
if (opts.suspensionStiffness !== undefined) w.suspensionStiffness = opts.suspensionStiffness;
|
|
178
|
-
if (opts.suspensionDamping !== undefined) w.suspensionDamping = opts.suspensionDamping;
|
|
179
|
-
if (opts.suspensionMaxForce !== undefined) w.suspensionMaxForce = opts.suspensionMaxForce;
|
|
180
|
-
if (opts.radius !== undefined) w.radius = opts.radius;
|
|
181
|
-
if (opts.friction !== undefined) w.friction = opts.friction;
|
|
182
|
-
if (opts.steered !== undefined) w.steered = opts.steered;
|
|
183
|
-
if (opts.driven !== undefined) w.driven = opts.driven;
|
|
184
|
-
w.suspensionLength = w.suspensionRestLength;
|
|
185
|
-
this.wheels.push(w);
|
|
186
|
-
if (w.driven) this.__drivenCount++;
|
|
187
|
-
return w;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Set the steering angle (rad) on every steered wheel. */
|
|
191
|
-
setSteering(angle) {
|
|
192
|
-
for (let i = 0; i < this.wheels.length; i++) {
|
|
193
|
-
if (this.wheels[i].steered) this.wheels[i].steering = angle;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Total engine drive force (N), split evenly across the driven wheels. */
|
|
198
|
-
setDriveForce(force) { this.__driveForce = force; }
|
|
199
|
-
|
|
200
|
-
/** Brake force (N) per wheel, opposing each wheel's forward motion. */
|
|
201
|
-
setBrake(force) { this.__brakeForce = force; }
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Step the vehicle: cast suspension rays, apply suspension + tyre forces.
|
|
205
|
-
* Call once per frame **before** `PhysicsSystem.fixedUpdate(dt)` with the
|
|
206
|
-
* same `dt`.
|
|
207
|
-
* @param {number} dt
|
|
208
|
-
*/
|
|
209
|
-
update(dt) {
|
|
210
|
-
const rb = this.chassisBody;
|
|
211
|
-
if (rb.kind !== BodyKind.Dynamic || dt <= 0) return;
|
|
212
|
-
const tr = this.chassisTransform;
|
|
213
|
-
const invMass = rb.mass > 0 ? 1 / rb.mass : 0;
|
|
214
|
-
const q = tr.rotation;
|
|
215
|
-
const px = tr.position.x, py = tr.position.y, pz = tr.position.z;
|
|
216
|
-
const lv = rb.linearVelocity, av = rb.angularVelocity;
|
|
217
|
-
const drivePer = this.__drivenCount > 0 ? this.__driveForce / this.__drivenCount : 0;
|
|
218
|
-
|
|
219
|
-
for (let wi = 0; wi < this.wheels.length; wi++) {
|
|
220
|
-
const w = this.wheels[wi];
|
|
221
|
-
|
|
222
|
-
// World mount point and suspension axis.
|
|
223
|
-
v3_quat3_apply(scratch_v, 0, w.localPosition[0], w.localPosition[1], w.localPosition[2], q[0], q[1], q[2], q[3]);
|
|
224
|
-
const mx = px + scratch_v[0], my = py + scratch_v[1], mz = pz + scratch_v[2];
|
|
225
|
-
v3_quat3_apply(scratch_v, 0, w.localSuspensionDir[0], w.localSuspensionDir[1], w.localSuspensionDir[2], q[0], q[1], q[2], q[3]);
|
|
226
|
-
const sdx = scratch_v[0], sdy = scratch_v[1], sdz = scratch_v[2]; // suspension down (unit)
|
|
227
|
-
|
|
228
|
-
const maxLen = w.suspensionRestLength + w.radius;
|
|
229
|
-
this.__ray.setOrigin(mx, my, mz);
|
|
230
|
-
this.__ray.setDirection(sdx, sdy, sdz);
|
|
231
|
-
this.__ray.tMax = maxLen;
|
|
232
|
-
|
|
233
|
-
const hit = this.system.raycast(this.__ray, this.__hit, this.__filter);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
//
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
1
|
+
import { Ray3 } from "../../../core/geom/3d/ray/Ray3.js";
|
|
2
|
+
import { PhysicsSurfacePoint } from "../queries/PhysicsSurfacePoint.js";
|
|
3
|
+
import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
|
|
4
|
+
import { v3_length } from "../../../core/geom/vec3/v3_length.js";
|
|
5
|
+
import { world_inverse_inertia_apply } from "../inertia/world_inverse_inertia.js";
|
|
6
|
+
import { BodyKind } from "../ecs/BodyKind.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* # Raycast vehicle controller
|
|
10
|
+
*
|
|
11
|
+
* The vehicle model most games ship: a single rigid-body **chassis** plus a set
|
|
12
|
+
* of **raycast wheels**. Each wheel is not a body — it is a downward ray from a
|
|
13
|
+
* chassis-local mount point. Every frame, for each wheel, the controller:
|
|
14
|
+
*
|
|
15
|
+
* 1. casts the suspension ray and, on a ground hit, measures the spring
|
|
16
|
+
* compression;
|
|
17
|
+
* 2. applies a spring + damper **suspension force** along the contact normal
|
|
18
|
+
* (this is what holds the chassis up);
|
|
19
|
+
* 3. applies **tyre friction** at the contact patch — a lateral grip impulse
|
|
20
|
+
* that resists sideways slide, plus a longitudinal drive / brake impulse,
|
|
21
|
+
* the two clamped together to a friction circle `μ·N`.
|
|
22
|
+
*
|
|
23
|
+
* Steering rotates the steered wheels' forward direction; a drive force is split
|
|
24
|
+
* across the driven wheels. It is a *controller on top of the public physics
|
|
25
|
+
* API* (`raycast` + `applyForceAt` + `applyImpulseAt`), not a new constraint —
|
|
26
|
+
* call {@link update} once per frame **before** `PhysicsSystem.fixedUpdate`, so
|
|
27
|
+
* the suspension force it accumulates is integrated by that step and the
|
|
28
|
+
* friction impulses act on the current velocity.
|
|
29
|
+
*
|
|
30
|
+
* The 6-DOF joint (spring linear + motor angular) is the alternative
|
|
31
|
+
* "simulated wheel" path; this raycast controller is the lighter, more robust
|
|
32
|
+
* default. Suspension-ray accuracy follows `PhysicsSystem.raycast`, which is
|
|
33
|
+
* narrowphase-exact for sphere / box / capsule / mesh / heightmap ground.
|
|
34
|
+
*
|
|
35
|
+
* @author Alex Goldring
|
|
36
|
+
* @copyright Company Named Limited (c) 2026
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const scratch_v = new Float64Array(3);
|
|
40
|
+
const scratch_i = new Float64Array(3);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Effective mass denominator for a unit-direction velocity constraint at a
|
|
44
|
+
* body-relative point: `invM + (r×d)·Iw⁻¹·(r×d)`.
|
|
45
|
+
* @returns {number}
|
|
46
|
+
*/
|
|
47
|
+
function effective_mass(rb, transform, invM, rx, ry, rz, dx, dy, dz) {
|
|
48
|
+
let k = invM;
|
|
49
|
+
const ii = rb.inverseInertiaLocal;
|
|
50
|
+
if (ii.x !== 0 || ii.y !== 0 || ii.z !== 0) {
|
|
51
|
+
const cx = ry * dz - rz * dy;
|
|
52
|
+
const cy = rz * dx - rx * dz;
|
|
53
|
+
const cz = rx * dy - ry * dx;
|
|
54
|
+
world_inverse_inertia_apply(scratch_i, 0, ii, transform.rotation, cx, cy, cz);
|
|
55
|
+
k += cx * scratch_i[0] + cy * scratch_i[1] + cz * scratch_i[2];
|
|
56
|
+
}
|
|
57
|
+
return k;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Rotate vector `(vx,vy,vz)` about unit axis `(ux,uy,uz)` by `angle`, into `out`. */
|
|
61
|
+
function rotate_about(out, vx, vy, vz, ux, uy, uz, angle) {
|
|
62
|
+
const c = Math.cos(angle), s = Math.sin(angle);
|
|
63
|
+
const dot = ux * vx + uy * vy + uz * vz;
|
|
64
|
+
// v·c + (k×v)·s + k·(k·v)·(1−c)
|
|
65
|
+
const kxx = uy * vz - uz * vy;
|
|
66
|
+
const kxy = uz * vx - ux * vz;
|
|
67
|
+
const kxz = ux * vy - uy * vx;
|
|
68
|
+
out[0] = vx * c + kxx * s + ux * dot * (1 - c);
|
|
69
|
+
out[1] = vy * c + kxy * s + uy * dot * (1 - c);
|
|
70
|
+
out[2] = vz * c + kxz * s + uz * dot * (1 - c);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One wheel's config + per-frame runtime state. The runtime fields
|
|
76
|
+
* (`inContact`, `compression`, contact point/normal, `rotation`, …) are read by
|
|
77
|
+
* rendering / gameplay; do not write them.
|
|
78
|
+
*/
|
|
79
|
+
export class Wheel {
|
|
80
|
+
constructor() {
|
|
81
|
+
// --- config ---
|
|
82
|
+
/** Mount point on the chassis, local. @type {Float64Array} */
|
|
83
|
+
this.localPosition = new Float64Array(3);
|
|
84
|
+
/** Suspension direction (local, unit, points "down"). @type {Float64Array} */
|
|
85
|
+
this.localSuspensionDir = Float64Array.from([0, -1, 0]);
|
|
86
|
+
/** Forward / rolling direction (local, unit). @type {Float64Array} */
|
|
87
|
+
this.localForward = Float64Array.from([0, 0, 1]);
|
|
88
|
+
this.suspensionRestLength = 0.5;
|
|
89
|
+
this.suspensionStiffness = 100;
|
|
90
|
+
this.suspensionDamping = 10;
|
|
91
|
+
this.suspensionMaxForce = Infinity;
|
|
92
|
+
this.radius = 0.3;
|
|
93
|
+
this.friction = 1.2;
|
|
94
|
+
this.steered = false;
|
|
95
|
+
this.driven = false;
|
|
96
|
+
|
|
97
|
+
// --- runtime ---
|
|
98
|
+
this.inContact = false;
|
|
99
|
+
this.suspensionLength = this.suspensionRestLength;
|
|
100
|
+
this.compression = 0;
|
|
101
|
+
/** World contact point. @type {Float64Array} */
|
|
102
|
+
this.contactPoint = new Float64Array(3);
|
|
103
|
+
/** World contact normal. @type {Float64Array} */
|
|
104
|
+
this.contactNormal = Float64Array.from([0, 1, 0]);
|
|
105
|
+
/** Body id of the surface under the wheel, or −1. */
|
|
106
|
+
this.contactBodyId = -1;
|
|
107
|
+
/** Current steering angle (rad), set via {@link RaycastVehicle#setSteering}. */
|
|
108
|
+
this.steering = 0;
|
|
109
|
+
/** Accumulated spin angle (rad) for rendering. */
|
|
110
|
+
this.rotation = 0;
|
|
111
|
+
/** Forward ground speed at the contact (m/s). */
|
|
112
|
+
this.forwardSpeed = 0;
|
|
113
|
+
/** Suspension force magnitude applied this frame (N). */
|
|
114
|
+
this.suspensionForce = 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export class RaycastVehicle {
|
|
119
|
+
/**
|
|
120
|
+
* @param {PhysicsSystem} system
|
|
121
|
+
* @param {RigidBody} chassisBody the chassis rigid body (Dynamic)
|
|
122
|
+
* @param {Transform} chassisTransform its world transform
|
|
123
|
+
*/
|
|
124
|
+
constructor(system, chassisBody, chassisTransform) {
|
|
125
|
+
this.system = system;
|
|
126
|
+
this.chassisBody = chassisBody;
|
|
127
|
+
this.chassisTransform = chassisTransform;
|
|
128
|
+
|
|
129
|
+
/** @type {Wheel[]} */
|
|
130
|
+
this.wheels = [];
|
|
131
|
+
|
|
132
|
+
this.__driveForce = 0;
|
|
133
|
+
this.__brakeForce = 0;
|
|
134
|
+
this.__drivenCount = 0;
|
|
135
|
+
|
|
136
|
+
this.__ray = new Ray3();
|
|
137
|
+
this.__hit = new PhysicsSurfacePoint();
|
|
138
|
+
// Exclude the chassis' own colliders from the suspension raycast.
|
|
139
|
+
this.__filter = (entity, collider) => collider._bodyId !== this.chassisBody._bodyId;
|
|
140
|
+
|
|
141
|
+
// Reused force / impulse / point payloads for the public API calls.
|
|
142
|
+
this.__force = { x: 0, y: 0, z: 0 };
|
|
143
|
+
this.__point = { x: 0, y: 0, z: 0 };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Add a wheel. Returns the created {@link Wheel} (also pushed to
|
|
148
|
+
* {@link wheels}); set any further config fields on it before stepping.
|
|
149
|
+
*
|
|
150
|
+
* @param {object} opts
|
|
151
|
+
* @param {number[]} opts.localPosition mount point on the chassis, local.
|
|
152
|
+
* @param {number[]} [opts.localSuspensionDir] unit "down", default (0,−1,0).
|
|
153
|
+
* @param {number[]} [opts.localForward] unit rolling dir, default (0,0,1).
|
|
154
|
+
* @param {number} [opts.suspensionRestLength]
|
|
155
|
+
* @param {number} [opts.suspensionStiffness] N/m.
|
|
156
|
+
* @param {number} [opts.suspensionDamping] N·s/m.
|
|
157
|
+
* @param {number} [opts.suspensionMaxForce] N (clamp).
|
|
158
|
+
* @param {number} [opts.radius]
|
|
159
|
+
* @param {number} [opts.friction] tyre friction coefficient μ.
|
|
160
|
+
* @param {boolean} [opts.steered]
|
|
161
|
+
* @param {boolean} [opts.driven]
|
|
162
|
+
* @returns {Wheel}
|
|
163
|
+
*/
|
|
164
|
+
addWheel(opts) {
|
|
165
|
+
const w = new Wheel();
|
|
166
|
+
const p = opts.localPosition;
|
|
167
|
+
w.localPosition[0] = p[0]; w.localPosition[1] = p[1]; w.localPosition[2] = p[2];
|
|
168
|
+
if (opts.localSuspensionDir) {
|
|
169
|
+
const d = opts.localSuspensionDir, L = v3_length(d[0], d[1], d[2]) || 1;
|
|
170
|
+
w.localSuspensionDir[0] = d[0] / L; w.localSuspensionDir[1] = d[1] / L; w.localSuspensionDir[2] = d[2] / L;
|
|
171
|
+
}
|
|
172
|
+
if (opts.localForward) {
|
|
173
|
+
const f = opts.localForward, L = v3_length(f[0], f[1], f[2]) || 1;
|
|
174
|
+
w.localForward[0] = f[0] / L; w.localForward[1] = f[1] / L; w.localForward[2] = f[2] / L;
|
|
175
|
+
}
|
|
176
|
+
if (opts.suspensionRestLength !== undefined) w.suspensionRestLength = opts.suspensionRestLength;
|
|
177
|
+
if (opts.suspensionStiffness !== undefined) w.suspensionStiffness = opts.suspensionStiffness;
|
|
178
|
+
if (opts.suspensionDamping !== undefined) w.suspensionDamping = opts.suspensionDamping;
|
|
179
|
+
if (opts.suspensionMaxForce !== undefined) w.suspensionMaxForce = opts.suspensionMaxForce;
|
|
180
|
+
if (opts.radius !== undefined) w.radius = opts.radius;
|
|
181
|
+
if (opts.friction !== undefined) w.friction = opts.friction;
|
|
182
|
+
if (opts.steered !== undefined) w.steered = opts.steered;
|
|
183
|
+
if (opts.driven !== undefined) w.driven = opts.driven;
|
|
184
|
+
w.suspensionLength = w.suspensionRestLength;
|
|
185
|
+
this.wheels.push(w);
|
|
186
|
+
if (w.driven) this.__drivenCount++;
|
|
187
|
+
return w;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Set the steering angle (rad) on every steered wheel. */
|
|
191
|
+
setSteering(angle) {
|
|
192
|
+
for (let i = 0; i < this.wheels.length; i++) {
|
|
193
|
+
if (this.wheels[i].steered) this.wheels[i].steering = angle;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Total engine drive force (N), split evenly across the driven wheels. */
|
|
198
|
+
setDriveForce(force) { this.__driveForce = force; }
|
|
199
|
+
|
|
200
|
+
/** Brake force (N) per wheel, opposing each wheel's forward motion. */
|
|
201
|
+
setBrake(force) { this.__brakeForce = force; }
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Step the vehicle: cast suspension rays, apply suspension + tyre forces.
|
|
205
|
+
* Call once per frame **before** `PhysicsSystem.fixedUpdate(dt)` with the
|
|
206
|
+
* same `dt`.
|
|
207
|
+
* @param {number} dt
|
|
208
|
+
*/
|
|
209
|
+
update(dt) {
|
|
210
|
+
const rb = this.chassisBody;
|
|
211
|
+
if (rb.kind !== BodyKind.Dynamic || dt <= 0) return;
|
|
212
|
+
const tr = this.chassisTransform;
|
|
213
|
+
const invMass = rb.mass > 0 ? 1 / rb.mass : 0;
|
|
214
|
+
const q = tr.rotation;
|
|
215
|
+
const px = tr.position.x, py = tr.position.y, pz = tr.position.z;
|
|
216
|
+
const lv = rb.linearVelocity, av = rb.angularVelocity;
|
|
217
|
+
const drivePer = this.__drivenCount > 0 ? this.__driveForce / this.__drivenCount : 0;
|
|
218
|
+
|
|
219
|
+
for (let wi = 0; wi < this.wheels.length; wi++) {
|
|
220
|
+
const w = this.wheels[wi];
|
|
221
|
+
|
|
222
|
+
// World mount point and suspension axis.
|
|
223
|
+
v3_quat3_apply(scratch_v, 0, w.localPosition[0], w.localPosition[1], w.localPosition[2], q[0], q[1], q[2], q[3]);
|
|
224
|
+
const mx = px + scratch_v[0], my = py + scratch_v[1], mz = pz + scratch_v[2];
|
|
225
|
+
v3_quat3_apply(scratch_v, 0, w.localSuspensionDir[0], w.localSuspensionDir[1], w.localSuspensionDir[2], q[0], q[1], q[2], q[3]);
|
|
226
|
+
const sdx = scratch_v[0], sdy = scratch_v[1], sdz = scratch_v[2]; // suspension down (unit)
|
|
227
|
+
|
|
228
|
+
const maxLen = w.suspensionRestLength + w.radius;
|
|
229
|
+
this.__ray.setOrigin(mx, my, mz);
|
|
230
|
+
this.__ray.setDirection(sdx, sdy, sdz);
|
|
231
|
+
this.__ray.tMax = maxLen;
|
|
232
|
+
|
|
233
|
+
const hit = this.system.raycast(this.__ray, this.__hit, this.__filter);
|
|
234
|
+
// `raycast` already bounds the hit to ray.tMax (= maxLen) and only
|
|
235
|
+
// returns true for t strictly < maxLen, so `__hit.t > maxLen` was dead.
|
|
236
|
+
if (!hit) {
|
|
237
|
+
// Airborne: suspension fully extended, no force. Reset ALL the
|
|
238
|
+
// contact-frame fields — including contactBodyId and forwardSpeed,
|
|
239
|
+
// which otherwise retain stale last-grounded-frame values.
|
|
240
|
+
w.inContact = false;
|
|
241
|
+
w.suspensionForce = 0;
|
|
242
|
+
w.suspensionLength = w.suspensionRestLength;
|
|
243
|
+
w.compression = 0;
|
|
244
|
+
w.contactBodyId = -1; // documented no-contact sentinel
|
|
245
|
+
w.forwardSpeed = 0;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const t = this.__hit.t;
|
|
250
|
+
w.inContact = true;
|
|
251
|
+
w.contactBodyId = this.__hit.body_id;
|
|
252
|
+
w.suspensionLength = t - w.radius;
|
|
253
|
+
if (w.suspensionLength < 0) w.suspensionLength = 0;
|
|
254
|
+
w.compression = w.suspensionRestLength - w.suspensionLength;
|
|
255
|
+
|
|
256
|
+
// Contact point along the ray; normal from the hit.
|
|
257
|
+
const cx = mx + sdx * t, cy = my + sdy * t, cz = mz + sdz * t;
|
|
258
|
+
let nx = this.__hit.normal.x, ny = this.__hit.normal.y, nz = this.__hit.normal.z;
|
|
259
|
+
const nlen = v3_length(nx, ny, nz) || 1;
|
|
260
|
+
nx /= nlen; ny /= nlen; nz /= nlen;
|
|
261
|
+
w.contactPoint[0] = cx; w.contactPoint[1] = cy; w.contactPoint[2] = cz;
|
|
262
|
+
w.contactNormal[0] = nx; w.contactNormal[1] = ny; w.contactNormal[2] = nz;
|
|
263
|
+
|
|
264
|
+
// Velocity of the chassis at the contact point.
|
|
265
|
+
const rx = cx - px, ry = cy - py, rz = cz - pz;
|
|
266
|
+
const vptx = lv.x + av.y * rz - av.z * ry;
|
|
267
|
+
const vpty = lv.y + av.z * rx - av.x * rz;
|
|
268
|
+
const vptz = lv.z + av.x * ry - av.y * rx;
|
|
269
|
+
|
|
270
|
+
// --- Suspension: spring + damper along the contact normal ---
|
|
271
|
+
const projVel = vptx * nx + vpty * ny + vptz * nz; // closing speed along normal
|
|
272
|
+
let susForce = w.suspensionStiffness * w.compression - w.suspensionDamping * projVel;
|
|
273
|
+
if (susForce < 0) susForce = 0;
|
|
274
|
+
else if (susForce > w.suspensionMaxForce) susForce = w.suspensionMaxForce;
|
|
275
|
+
w.suspensionForce = susForce;
|
|
276
|
+
|
|
277
|
+
if (susForce !== 0) {
|
|
278
|
+
this.__force.x = nx * susForce; this.__force.y = ny * susForce; this.__force.z = nz * susForce;
|
|
279
|
+
this.__point.x = cx; this.__point.y = cy; this.__point.z = cz;
|
|
280
|
+
this.system.applyForceAt(rb, tr, this.__force, this.__point);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// --- Tyre friction: forward / side basis in the contact plane ---
|
|
284
|
+
v3_quat3_apply(scratch_v, 0, w.localForward[0], w.localForward[1], w.localForward[2], q[0], q[1], q[2], q[3]);
|
|
285
|
+
let fwx = scratch_v[0], fwy = scratch_v[1], fwz = scratch_v[2];
|
|
286
|
+
if (w.steering !== 0) {
|
|
287
|
+
// Steer about the suspension-up axis (−suspension dir).
|
|
288
|
+
rotate_about(scratch_v, fwx, fwy, fwz, -sdx, -sdy, -sdz, w.steering);
|
|
289
|
+
fwx = scratch_v[0]; fwy = scratch_v[1]; fwz = scratch_v[2];
|
|
290
|
+
}
|
|
291
|
+
// Project forward onto the contact plane, normalise.
|
|
292
|
+
let fdotn = fwx * nx + fwy * ny + fwz * nz;
|
|
293
|
+
fwx -= fdotn * nx; fwy -= fdotn * ny; fwz -= fdotn * nz;
|
|
294
|
+
let fl = v3_length(fwx, fwy, fwz);
|
|
295
|
+
if (fl < 1e-6) continue; // degenerate (forward parallel to normal)
|
|
296
|
+
fwx /= fl; fwy /= fl; fwz /= fl;
|
|
297
|
+
// Side = normal × forward (already unit & in-plane).
|
|
298
|
+
const sx = ny * fwz - nz * fwy;
|
|
299
|
+
const sy = nz * fwx - nx * fwz;
|
|
300
|
+
const sz = nx * fwy - ny * fwx;
|
|
301
|
+
|
|
302
|
+
const fwdVel = vptx * fwx + vpty * fwy + vptz * fwz;
|
|
303
|
+
const sideVel = vptx * sx + vpty * sy + vptz * sz;
|
|
304
|
+
w.forwardSpeed = fwdVel;
|
|
305
|
+
|
|
306
|
+
// Lateral grip: impulse that cancels the side velocity.
|
|
307
|
+
const denomSide = effective_mass(rb, tr, invMass, rx, ry, rz, sx, sy, sz);
|
|
308
|
+
let jSide = denomSide > 0 ? -sideVel / denomSide : 0;
|
|
309
|
+
|
|
310
|
+
// Longitudinal: engine drive minus brake (brake never reverses).
|
|
311
|
+
let jFwd = w.driven ? drivePer * dt : 0;
|
|
312
|
+
if (this.__brakeForce > 0 && fwdVel !== 0) {
|
|
313
|
+
const denomFwd = effective_mass(rb, tr, invMass, rx, ry, rz, fwx, fwy, fwz);
|
|
314
|
+
const stopImpulse = denomFwd > 0 ? Math.abs(fwdVel) / denomFwd : 0;
|
|
315
|
+
const brakeImpulse = Math.min(this.__brakeForce * dt, stopImpulse);
|
|
316
|
+
jFwd -= Math.sign(fwdVel) * brakeImpulse;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Friction circle: clamp the combined tyre impulse to μ·N·dt.
|
|
320
|
+
const budget = w.friction * susForce * dt;
|
|
321
|
+
const mag = Math.sqrt(jFwd * jFwd + jSide * jSide);
|
|
322
|
+
if (mag > budget && mag > 0) {
|
|
323
|
+
const scale = budget / mag;
|
|
324
|
+
jFwd *= scale; jSide *= scale;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (jFwd !== 0 || jSide !== 0) {
|
|
328
|
+
this.__force.x = fwx * jFwd + sx * jSide;
|
|
329
|
+
this.__force.y = fwy * jFwd + sy * jSide;
|
|
330
|
+
this.__force.z = fwz * jFwd + sz * jSide;
|
|
331
|
+
this.__point.x = cx; this.__point.y = cy; this.__point.z = cz;
|
|
332
|
+
this.system.applyImpulseAt(rb, tr, this.__force, this.__point);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Visual wheel spin from forward ground speed.
|
|
336
|
+
if (w.radius > 0) w.rotation += (fwdVel / w.radius) * dt;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|