@gg-web-engine/rapier3d 0.0.72 → 0.0.73
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/dist/components/rapier-3d-character-controller.component.d.ts +13 -8
- package/dist/components/rapier-3d-character-controller.component.js +49 -8
- package/dist/components/rapier-3d-raycast-vehicle.component.d.ts +47 -2
- package/dist/components/rapier-3d-raycast-vehicle.component.js +153 -45
- package/dist/components/rapier-3d-rigid-body.component.d.ts +20 -1
- package/dist/components/rapier-3d-rigid-body.component.js +73 -2
- package/dist/components/rapier-3d-trigger.component.d.ts +7 -6
- package/dist/components/rapier-3d-trigger.component.js +6 -0
- package/dist/components/rapier-3d-world.component.d.ts +35 -1
- package/dist/components/rapier-3d-world.component.js +59 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/rapier-3d-factory.d.ts +3 -2
- package/dist/rapier-3d-factory.js +24 -5
- package/dist/types.d.ts +2 -1
- package/package.json +3 -3
- package/test/components/rapier-3d-player-character-controller-integration.spec.ts +3 -0
- package/test/components/rapier-3d-raycast-vehicle.component.spec.ts +160 -0
- package/test/components/rapier-3d-rigid-body.component.spec.ts +68 -0
- package/test/components/rapier-3d-trigger-character-controller-integration.spec.ts +85 -0
|
@@ -54,6 +54,12 @@ export class Rapier3dTriggerComponent extends Rapier3dRigidBodyComponent {
|
|
|
54
54
|
* component(s) care.
|
|
55
55
|
*/
|
|
56
56
|
notifyOverlap(otherBody, started) {
|
|
57
|
+
// `otherBody` can be a `Rapier3dCharacterControllerComponent` (a player walking through this
|
|
58
|
+
// trigger) as well as an ordinary rigid body - `onEnter$`/`onLeft$` (and `ITrigger3dComponent`'s
|
|
59
|
+
// own `onEntityEntered`/`onEntityLeft`) are typed as this same union rather than narrowed to a
|
|
60
|
+
// rigid body, since a character controller only satisfies `IBodyComponent`, not
|
|
61
|
+
// `IRigidBodyComponent` (no `linearVelocity`/`resetMotion()`/collision events). This mirrors the
|
|
62
|
+
// ammo adapter's own `AmmoTriggerComponent`, which resolves the same overlap case the same way.
|
|
57
63
|
if (started) {
|
|
58
64
|
this.overlaps.add(otherBody);
|
|
59
65
|
this.onEnter$.next(otherBody);
|
|
@@ -2,6 +2,7 @@ import { CollisionGroup, IPhysicsWorld3dComponent, Point3, RaycastOptions, Rayca
|
|
|
2
2
|
import { Collider, EventQueue, World } from '@dimforge/rapier3d-compat';
|
|
3
3
|
import { Rapier3dRigidBodyComponent } from './rapier-3d-rigid-body.component';
|
|
4
4
|
import { Rapier3dCharacterControllerComponent } from './rapier-3d-character-controller.component';
|
|
5
|
+
import { Rapier3dRaycastVehicleComponent } from './rapier-3d-raycast-vehicle.component';
|
|
5
6
|
import { Rapier3dFactory } from '../rapier-3d-factory';
|
|
6
7
|
import { Rapier3dLoader } from '../rapier-3d-loader';
|
|
7
8
|
import { Rapier3dPhysicsTypeDocRepo } from '../types';
|
|
@@ -23,7 +24,28 @@ export declare class Rapier3dWorldComponent implements IPhysicsWorld3dComponent<
|
|
|
23
24
|
get nativeWorld(): World;
|
|
24
25
|
private _eventQueue;
|
|
25
26
|
get eventQueue(): EventQueue;
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Keyed by rigid-body handle. Includes `Rapier3dCharacterControllerComponent`s alongside ordinary
|
|
29
|
+
* `Rapier3dRigidBodyComponent`s (triggers included, since `Rapier3dTriggerComponent extends
|
|
30
|
+
* Rapier3dRigidBodyComponent`) - a character controller's kinematic body still gets a real Rapier
|
|
31
|
+
* rigid-body handle on `addToWorld` (see that class), so it registers here the same way, letting
|
|
32
|
+
* `dispatchCollisionEvents` resolve sensor-overlap pairs against it (so a `Trigger` fires for a
|
|
33
|
+
* player walking through it, not just for ordinary rigid bodies/vehicle chassis) and letting
|
|
34
|
+
* `raycast()` resolve a hit against it too. `dispatchCollisionEvents` still narrows to
|
|
35
|
+
* `Rapier3dRigidBodyComponent` before treating a pair as a real (non-sensor) contact, since a
|
|
36
|
+
* character controller has no `notifyCollisionStart`/`notifyCollisionEnd` to call - its physical
|
|
37
|
+
* response comes from its own sweep-based `move()`, not Rapier's contact solver.
|
|
38
|
+
*/
|
|
39
|
+
readonly handleIdEntityMap: Map<number, Rapier3dRigidBodyComponent | Rapier3dCharacterControllerComponent>;
|
|
40
|
+
/**
|
|
41
|
+
* Every `Rapier3dRaycastVehicleComponent` currently in this world - unlike an ordinary rigid body
|
|
42
|
+
* or `Rapier3dCharacterControllerComponent`, a vehicle needs an explicit per-tick
|
|
43
|
+
* `updateVehicle()` call (see that class's own doc for why: nothing steps Rapier's vehicle
|
|
44
|
+
* controller automatically as part of `World.step()`). `simulate()` drives every registered
|
|
45
|
+
* vehicle from this set immediately before stepping the world, so the forces it just wrote into
|
|
46
|
+
* the chassis's velocity get integrated by that same step.
|
|
47
|
+
*/
|
|
48
|
+
readonly raycastVehicles: Set<Rapier3dRaycastVehicleComponent>;
|
|
27
49
|
constructor();
|
|
28
50
|
init(): Promise<void>;
|
|
29
51
|
simulate(delta: number): void;
|
|
@@ -39,6 +61,12 @@ export declare class Rapier3dWorldComponent implements IPhysicsWorld3dComponent<
|
|
|
39
61
|
* `handleIdEntityMap` is keyed by rigid-body handle (see `addToWorld`), so each handle is resolved
|
|
40
62
|
* via `World.getCollider(handle)` (returns `null` for a since-removed collider, not a throw - safe
|
|
41
63
|
* to just skip) then `Collider.parent()` to reach the owning `RigidBody` before the map lookup.
|
|
64
|
+
*
|
|
65
|
+
* `handleIdEntityMap` also holds `Rapier3dCharacterControllerComponent`s (see its own doc), so
|
|
66
|
+
* `comp1`/`comp2` below can each be a character controller as well as a rigid body/trigger - the
|
|
67
|
+
* sensor branch handles that directly (`notifyOverlap` accepts either), while the real-contact
|
|
68
|
+
* branch narrows to `Rapier3dRigidBodyComponent` first, since a character controller has no
|
|
69
|
+
* collision-event API to call into.
|
|
42
70
|
*/
|
|
43
71
|
protected dispatchCollisionEvents(): void;
|
|
44
72
|
/**
|
|
@@ -70,6 +98,12 @@ export declare class Rapier3dWorldComponent implements IPhysicsWorld3dComponent<
|
|
|
70
98
|
protected lockedCollisionGroups: number[];
|
|
71
99
|
registerCollisionGroup(): CollisionGroup;
|
|
72
100
|
deregisterCollisionGroup(group: CollisionGroup): void;
|
|
101
|
+
/**
|
|
102
|
+
* `castRay`'s own default (no `filterFlags`) treats a sensor collider as a solid obstacle, exactly
|
|
103
|
+
* like any real one - `QueryFilterFlags.EXCLUDE_SENSORS` is required so a raycast never reports a
|
|
104
|
+
* hit against a `Trigger`'s own collider, matching what "trigger" means everywhere else in this
|
|
105
|
+
* engine (a sensor with no collision response, see `ITrigger3dComponent`).
|
|
106
|
+
*/
|
|
73
107
|
raycast(options: RaycastOptions<Point3>): RaycastResult<Point3, Rapier3dRigidBodyComponent>;
|
|
74
108
|
dispose(): void;
|
|
75
109
|
}
|
|
@@ -8,7 +8,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { BitMask, Pnt3, } from '@gg-web-engine/core';
|
|
11
|
-
import { EventQueue, init, Vector3, World } from '@dimforge/rapier3d-compat';
|
|
11
|
+
import { EventQueue, init, QueryFilterFlags, Vector3, World } from '@dimforge/rapier3d-compat';
|
|
12
|
+
import { Rapier3dRigidBodyComponent } from './rapier-3d-rigid-body.component';
|
|
12
13
|
import { Rapier3dTriggerComponent } from './rapier-3d-trigger.component';
|
|
13
14
|
import { Rapier3dFactory } from '../rapier-3d-factory';
|
|
14
15
|
import { Rapier3dLoader } from '../rapier-3d-loader';
|
|
@@ -59,7 +60,28 @@ export class Rapier3dWorldComponent {
|
|
|
59
60
|
this.mainCollisionGroup = 0;
|
|
60
61
|
this._nativeWorld = null;
|
|
61
62
|
this._eventQueue = null;
|
|
63
|
+
/**
|
|
64
|
+
* Keyed by rigid-body handle. Includes `Rapier3dCharacterControllerComponent`s alongside ordinary
|
|
65
|
+
* `Rapier3dRigidBodyComponent`s (triggers included, since `Rapier3dTriggerComponent extends
|
|
66
|
+
* Rapier3dRigidBodyComponent`) - a character controller's kinematic body still gets a real Rapier
|
|
67
|
+
* rigid-body handle on `addToWorld` (see that class), so it registers here the same way, letting
|
|
68
|
+
* `dispatchCollisionEvents` resolve sensor-overlap pairs against it (so a `Trigger` fires for a
|
|
69
|
+
* player walking through it, not just for ordinary rigid bodies/vehicle chassis) and letting
|
|
70
|
+
* `raycast()` resolve a hit against it too. `dispatchCollisionEvents` still narrows to
|
|
71
|
+
* `Rapier3dRigidBodyComponent` before treating a pair as a real (non-sensor) contact, since a
|
|
72
|
+
* character controller has no `notifyCollisionStart`/`notifyCollisionEnd` to call - its physical
|
|
73
|
+
* response comes from its own sweep-based `move()`, not Rapier's contact solver.
|
|
74
|
+
*/
|
|
62
75
|
this.handleIdEntityMap = new Map();
|
|
76
|
+
/**
|
|
77
|
+
* Every `Rapier3dRaycastVehicleComponent` currently in this world - unlike an ordinary rigid body
|
|
78
|
+
* or `Rapier3dCharacterControllerComponent`, a vehicle needs an explicit per-tick
|
|
79
|
+
* `updateVehicle()` call (see that class's own doc for why: nothing steps Rapier's vehicle
|
|
80
|
+
* controller automatically as part of `World.step()`). `simulate()` drives every registered
|
|
81
|
+
* vehicle from this set immediately before stepping the world, so the forces it just wrote into
|
|
82
|
+
* the chassis's velocity get integrated by that same step.
|
|
83
|
+
*/
|
|
84
|
+
this.raycastVehicles = new Set();
|
|
63
85
|
this.lockedCollisionGroups = [];
|
|
64
86
|
this.added$.subscribe(c => this.children.push(c));
|
|
65
87
|
this.removed$.subscribe(c => this.children.splice(this.children.indexOf(c), 1));
|
|
@@ -75,7 +97,14 @@ export class Rapier3dWorldComponent {
|
|
|
75
97
|
}
|
|
76
98
|
simulate(delta) {
|
|
77
99
|
var _a;
|
|
78
|
-
|
|
100
|
+
const dt = delta / 1000;
|
|
101
|
+
// must run *before* `World.step()` - `updateVehicle` directly writes each vehicle's chassis
|
|
102
|
+
// velocity from this tick's suspension/engine/brake forces, which `step()` then integrates like
|
|
103
|
+
// any other dynamic body's velocity (see `Rapier3dRaycastVehicleComponent`'s own doc).
|
|
104
|
+
for (const vehicle of this.raycastVehicles) {
|
|
105
|
+
vehicle.stepVehicleController(dt);
|
|
106
|
+
}
|
|
107
|
+
this._nativeWorld.timestep = dt;
|
|
79
108
|
(_a = this._nativeWorld) === null || _a === void 0 ? void 0 : _a.step(this.eventQueue);
|
|
80
109
|
this.dispatchCollisionEvents();
|
|
81
110
|
}
|
|
@@ -91,6 +120,12 @@ export class Rapier3dWorldComponent {
|
|
|
91
120
|
* `handleIdEntityMap` is keyed by rigid-body handle (see `addToWorld`), so each handle is resolved
|
|
92
121
|
* via `World.getCollider(handle)` (returns `null` for a since-removed collider, not a throw - safe
|
|
93
122
|
* to just skip) then `Collider.parent()` to reach the owning `RigidBody` before the map lookup.
|
|
123
|
+
*
|
|
124
|
+
* `handleIdEntityMap` also holds `Rapier3dCharacterControllerComponent`s (see its own doc), so
|
|
125
|
+
* `comp1`/`comp2` below can each be a character controller as well as a rigid body/trigger - the
|
|
126
|
+
* sensor branch handles that directly (`notifyOverlap` accepts either), while the real-contact
|
|
127
|
+
* branch narrows to `Rapier3dRigidBodyComponent` first, since a character controller has no
|
|
128
|
+
* collision-event API to call into.
|
|
94
129
|
*/
|
|
95
130
|
dispatchCollisionEvents() {
|
|
96
131
|
const nativeWorld = this._nativeWorld;
|
|
@@ -122,6 +157,13 @@ export class Rapier3dWorldComponent {
|
|
|
122
157
|
}
|
|
123
158
|
return;
|
|
124
159
|
}
|
|
160
|
+
// A character controller has no real-contact API (`notifyCollisionStart`/`notifyCollisionEnd`)
|
|
161
|
+
// to call into - its own sweep-based `move()` already handles physical response, so a non-sensor
|
|
162
|
+
// pair involving one is simply not reported as a collision event. (Sensor pairs above are
|
|
163
|
+
// unaffected by this - `notifyOverlap` accepts either component type.)
|
|
164
|
+
if (!(comp1 instanceof Rapier3dRigidBodyComponent) || !(comp2 instanceof Rapier3dRigidBodyComponent)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
125
167
|
if (started) {
|
|
126
168
|
const geometry = this.computeContactGeometry(collider1, collider2);
|
|
127
169
|
if (!geometry) {
|
|
@@ -219,6 +261,12 @@ export class Rapier3dWorldComponent {
|
|
|
219
261
|
deregisterCollisionGroup(group) {
|
|
220
262
|
this.lockedCollisionGroups = this.lockedCollisionGroups.filter(x => x !== group);
|
|
221
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* `castRay`'s own default (no `filterFlags`) treats a sensor collider as a solid obstacle, exactly
|
|
266
|
+
* like any real one - `QueryFilterFlags.EXCLUDE_SENSORS` is required so a raycast never reports a
|
|
267
|
+
* hit against a `Trigger`'s own collider, matching what "trigger" means everywhere else in this
|
|
268
|
+
* engine (a sensor with no collision response, see `ITrigger3dComponent`).
|
|
269
|
+
*/
|
|
222
270
|
raycast(options) {
|
|
223
271
|
if (!this._nativeWorld) {
|
|
224
272
|
return { hasHit: false };
|
|
@@ -237,7 +285,7 @@ export class Rapier3dWorldComponent {
|
|
|
237
285
|
const mask = (groups) => {
|
|
238
286
|
return groups ? BitMask.pack(groups, 16) : BitMask.full(16);
|
|
239
287
|
};
|
|
240
|
-
const hit = this._nativeWorld.castRay(ray, rayLength, true,
|
|
288
|
+
const hit = this._nativeWorld.castRay(ray, rayLength, true, QueryFilterFlags.EXCLUDE_SENSORS, (mask(options.collisionFilterGroups) << 16) | mask(options.collisionFilterMask));
|
|
241
289
|
if (!hit) {
|
|
242
290
|
return { hasHit: false };
|
|
243
291
|
}
|
|
@@ -250,7 +298,14 @@ export class Rapier3dWorldComponent {
|
|
|
250
298
|
if (!rigidBody) {
|
|
251
299
|
return { hasHit: false };
|
|
252
300
|
}
|
|
253
|
-
|
|
301
|
+
// `handleIdEntityMap` also holds character controllers now (see its own doc) - raycast's
|
|
302
|
+
// contract only promises `PTypeDoc['rigidBody'] | PTypeDoc['trigger']`, so a hit against one is
|
|
303
|
+
// filtered out here rather than surfaced as `hitBody`, unlike a trigger's sensor-overlap
|
|
304
|
+
// resolution which does report it. `ObjectGrabController`'s character-controller self-hit skip
|
|
305
|
+
// (see `gg-engine-core-development`) is unaffected either way - it never relied on `hitBody`
|
|
306
|
+
// resolving at all.
|
|
307
|
+
const resolvedHitBody = this.handleIdEntityMap.get(rigidBody.handle);
|
|
308
|
+
result.hitBody = resolvedHitBody instanceof Rapier3dRigidBodyComponent ? resolvedHitBody : undefined;
|
|
254
309
|
result.hitDistance = hit.timeOfImpact;
|
|
255
310
|
result.hitPoint = Pnt3.add(origin, Pnt3.scalarMult(direction, hit.timeOfImpact));
|
|
256
311
|
const rayIntersection = hit.collider.castRayAndGetNormal(ray, hit.timeOfImpact, true);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './components/rapier-3d-rigid-body.component';
|
|
2
2
|
export * from './components/rapier-3d-trigger.component';
|
|
3
|
+
export * from './components/rapier-3d-raycast-vehicle.component';
|
|
3
4
|
export * from './components/rapier-3d-character-controller.component';
|
|
4
5
|
export * from './components/rapier-3d-world.component';
|
|
5
6
|
export * from './rapier-3d-factory';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from './components/rapier-3d-rigid-body.component';
|
|
2
2
|
export * from './components/rapier-3d-trigger.component';
|
|
3
|
-
|
|
3
|
+
export * from './components/rapier-3d-raycast-vehicle.component';
|
|
4
4
|
export * from './components/rapier-3d-character-controller.component';
|
|
5
5
|
export * from './components/rapier-3d-world.component';
|
|
6
6
|
export * from './rapier-3d-factory';
|
|
@@ -2,6 +2,7 @@ import { Body3DOptions, BodyShape3DDescriptor, CharacterController3dOptions, IPh
|
|
|
2
2
|
import { ColliderDesc, RigidBodyDesc } from '@dimforge/rapier3d-compat';
|
|
3
3
|
import { Rapier3dRigidBodyComponent } from './components/rapier-3d-rigid-body.component';
|
|
4
4
|
import { Rapier3dTriggerComponent } from './components/rapier-3d-trigger.component';
|
|
5
|
+
import { Rapier3dRaycastVehicleComponent } from './components/rapier-3d-raycast-vehicle.component';
|
|
5
6
|
import { Rapier3dCharacterControllerComponent } from './components/rapier-3d-character-controller.component';
|
|
6
7
|
import { Rapier3dWorldComponent } from './components/rapier-3d-world.component';
|
|
7
8
|
import { Rapier3dPhysicsTypeDocRepo } from './types';
|
|
@@ -16,7 +17,7 @@ export declare class Rapier3dFactory implements IPhysicsBody3dComponentFactory<R
|
|
|
16
17
|
position?: Point3;
|
|
17
18
|
rotation?: Point4;
|
|
18
19
|
}): Rapier3dTriggerComponent;
|
|
19
|
-
createRaycastVehicle(chassis: Rapier3dRigidBodyComponent):
|
|
20
|
+
createRaycastVehicle(chassis: Rapier3dRigidBodyComponent): Rapier3dRaycastVehicleComponent;
|
|
20
21
|
createCharacterController(options: CharacterController3dOptions, transform?: {
|
|
21
22
|
position?: Point3;
|
|
22
23
|
rotation?: Point4;
|
|
@@ -25,5 +26,5 @@ export declare class Rapier3dFactory implements IPhysicsBody3dComponentFactory<R
|
|
|
25
26
|
createRigidBodyDescr(options: Partial<Body3DOptions>, transform?: {
|
|
26
27
|
position?: Point3;
|
|
27
28
|
rotation?: Point4;
|
|
28
|
-
}): RigidBodyDesc;
|
|
29
|
+
}, colliderDescr?: ColliderDesc[]): RigidBodyDesc;
|
|
29
30
|
}
|
|
@@ -2,6 +2,7 @@ import { getCylinderRadii, Pnt3, Qtrn, } from '@gg-web-engine/core';
|
|
|
2
2
|
import { ActiveEvents, ColliderDesc, Quaternion, RigidBodyDesc } from '@dimforge/rapier3d-compat';
|
|
3
3
|
import { Rapier3dRigidBodyComponent } from './components/rapier-3d-rigid-body.component';
|
|
4
4
|
import { Rapier3dTriggerComponent } from './components/rapier-3d-trigger.component';
|
|
5
|
+
import { Rapier3dRaycastVehicleComponent } from './components/rapier-3d-raycast-vehicle.component';
|
|
5
6
|
import { Rapier3dCharacterControllerComponent } from './components/rapier-3d-character-controller.component';
|
|
6
7
|
const DEFAULT_CHARACTER_CONTROLLER_OPTIONS = {
|
|
7
8
|
offset: 0.01,
|
|
@@ -14,7 +15,7 @@ const DEFAULT_CHARACTER_CONTROLLER_OPTIONS = {
|
|
|
14
15
|
interactWithCollisionGroups: 'all',
|
|
15
16
|
// `Rapier3dCharacterControllerComponent.addToWorld` wires this straight into Rapier's own
|
|
16
17
|
// `KinematicCharacterController.setCharacterMass` + `setApplyImpulsesToDynamicBodies(true)` -
|
|
17
|
-
//
|
|
18
|
+
// no hand-rolled push logic needed, Rapier already computes it.
|
|
18
19
|
pushMass: 80,
|
|
19
20
|
};
|
|
20
21
|
export class Rapier3dFactory {
|
|
@@ -29,7 +30,7 @@ export class Rapier3dFactory {
|
|
|
29
30
|
// ordinary rigid-body-vs-rigid-body contact needs it here too, since neither side had it before).
|
|
30
31
|
const colliderDescr = this.createColliderDescr(descriptor.shape);
|
|
31
32
|
colliderDescr.forEach(c => c.setActiveEvents(ActiveEvents.COLLISION_EVENTS));
|
|
32
|
-
return new Rapier3dRigidBodyComponent(this.world, colliderDescr, descriptor.shape, this.createRigidBodyDescr(descriptor.body, transform), Object.assign({ friction: 0.5, restitution: 0.1, ownCollisionGroups: [this.world.mainCollisionGroup], interactWithCollisionGroups: [this.world.mainCollisionGroup], ccd: false }, descriptor.body));
|
|
33
|
+
return new Rapier3dRigidBodyComponent(this.world, colliderDescr, descriptor.shape, this.createRigidBodyDescr(descriptor.body, transform, colliderDescr), Object.assign({ friction: 0.5, restitution: 0.1, ownCollisionGroups: [this.world.mainCollisionGroup], interactWithCollisionGroups: [this.world.mainCollisionGroup], ccd: false }, descriptor.body));
|
|
33
34
|
}
|
|
34
35
|
createTrigger(descriptor, transform) {
|
|
35
36
|
const colliderDescr = this.createColliderDescr(descriptor);
|
|
@@ -40,7 +41,7 @@ export class Rapier3dFactory {
|
|
|
40
41
|
return new Rapier3dTriggerComponent(this.world, colliderDescr, descriptor, this.createRigidBodyDescr({ bodyType: 'static' }, transform));
|
|
41
42
|
}
|
|
42
43
|
createRaycastVehicle(chassis) {
|
|
43
|
-
|
|
44
|
+
return new Rapier3dRaycastVehicleComponent(this.world, chassis);
|
|
44
45
|
}
|
|
45
46
|
createCharacterController(options, transform) {
|
|
46
47
|
const resolvedOptions = Object.assign(Object.assign(Object.assign({}, DEFAULT_CHARACTER_CONTROLLER_OPTIONS), { ownCollisionGroups: [this.world.mainCollisionGroup] }), options);
|
|
@@ -127,7 +128,7 @@ export class Rapier3dFactory {
|
|
|
127
128
|
}
|
|
128
129
|
throw new Error(`Shape "${descriptor.shape}" not implemented for Rapier 3D`);
|
|
129
130
|
}
|
|
130
|
-
createRigidBodyDescr(options, transform) {
|
|
131
|
+
createRigidBodyDescr(options, transform, colliderDescr = []) {
|
|
131
132
|
const pos = (transform === null || transform === void 0 ? void 0 : transform.position) || Pnt3.O;
|
|
132
133
|
const rot = (transform === null || transform === void 0 ? void 0 : transform.rotation) || Qtrn.O;
|
|
133
134
|
let bodyDesc;
|
|
@@ -143,7 +144,25 @@ export class Rapier3dFactory {
|
|
|
143
144
|
}
|
|
144
145
|
else {
|
|
145
146
|
bodyDesc = RigidBodyDesc.dynamic();
|
|
146
|
-
|
|
147
|
+
// Mass is set on the collider(s) (`ColliderDesc.setMass`), not as `RigidBodyDesc.mass` -
|
|
148
|
+
// that field is "additional" mass layered on top of whatever the colliders themselves
|
|
149
|
+
// contribute (see its own doc: "the total mass of the rigid-body is equal to the sum of this
|
|
150
|
+
// additional mass and the mass computed from the colliders"), and critically, additional mass
|
|
151
|
+
// contributes no extra rotational inertia of its own - it behaves like a point mass exactly at
|
|
152
|
+
// the body's center of mass. Setting it there instead of on the collider(s) used to leave every
|
|
153
|
+
// dynamic body's rotational inertia derived from the collider's un-scaled default density (1),
|
|
154
|
+
// regardless of how large `options.mass` actually was - correct enough for the many demos that
|
|
155
|
+
// never depend on rotational dynamics, but confirmed to visibly break `Rapier3dRaycastVehicleComponent`:
|
|
156
|
+
// a heavy (mass: 800) chassis with a barely-there (~4kg, density-1-derived) rotational inertia
|
|
157
|
+
// spun wildly on any steering input (huge angular acceleration for a given torque, since inertia
|
|
158
|
+
// is what resists it) while still translating at a realistic, correctly-scaled 800kg pace.
|
|
159
|
+
// `setMass` instead asks Rapier to auto-derive inertia from the shape *scaled to that mass*, the
|
|
160
|
+
// same computation an un-overridden default density would produce, just correctly sized. Split
|
|
161
|
+
// evenly across every sub-collider of a `COMPOUND` shape (no per-sub-shape volume query is
|
|
162
|
+
// exposed to weight this by volume instead) so the sum still equals the requested total mass.
|
|
163
|
+
const mass = options.mass || 1;
|
|
164
|
+
const perColliderMass = mass / (colliderDescr.length || 1);
|
|
165
|
+
colliderDescr.forEach(c => c.setMass(perColliderMass));
|
|
147
166
|
// Only a dynamic body can tunnel through geometry it crosses within a single step - a fixed
|
|
148
167
|
// or kinematic body is never the one doing the moving-too-fast-to-detect part of that, so CCD
|
|
149
168
|
// is meaningless for either (see `BodyOptions.ccd`'s own doc).
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Rapier3dFactory } from './rapier-3d-factory';
|
|
|
2
2
|
import { Rapier3dLoader } from './rapier-3d-loader';
|
|
3
3
|
import { Rapier3dRigidBodyComponent } from './components/rapier-3d-rigid-body.component';
|
|
4
4
|
import { Rapier3dTriggerComponent } from './components/rapier-3d-trigger.component';
|
|
5
|
+
import { Rapier3dRaycastVehicleComponent } from './components/rapier-3d-raycast-vehicle.component';
|
|
5
6
|
import { Rapier3dCharacterControllerComponent } from './components/rapier-3d-character-controller.component';
|
|
6
7
|
import { Gg3dWorld, Gg3dWorldSceneTypeDocPPatch, Gg3dWorldTypeDocPPatch } from '@gg-web-engine/core';
|
|
7
8
|
import { Rapier3dWorldComponent } from './components/rapier-3d-world.component';
|
|
@@ -10,7 +11,7 @@ export type Rapier3dPhysicsTypeDocRepo = {
|
|
|
10
11
|
loader: Rapier3dLoader;
|
|
11
12
|
rigidBody: Rapier3dRigidBodyComponent;
|
|
12
13
|
trigger: Rapier3dTriggerComponent;
|
|
13
|
-
raycastVehicle:
|
|
14
|
+
raycastVehicle: Rapier3dRaycastVehicleComponent;
|
|
14
15
|
characterController: Rapier3dCharacterControllerComponent;
|
|
15
16
|
};
|
|
16
17
|
export type Rapier3dTypeDoc = Gg3dWorldTypeDocPPatch<Rapier3dPhysicsTypeDocRepo>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gg-web-engine/rapier3d",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.73",
|
|
4
4
|
"description": "An attempt to create open source game engine for browser",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"homepage": "https://github.com/AndyGura/gg-web-engine#readme",
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@dimforge/rapier3d-compat": "0.20.0",
|
|
31
|
-
"@gg-web-engine/core": "0.0.
|
|
31
|
+
"@gg-web-engine/core": "0.0.73",
|
|
32
32
|
"@types/jest": "^30.0.0",
|
|
33
33
|
"@types/node": "^26.4.0",
|
|
34
34
|
"jest": "^30.5.0",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@dimforge/rapier3d-compat": "0.20.0",
|
|
43
|
-
"@gg-web-engine/core": "0.0.
|
|
43
|
+
"@gg-web-engine/core": "0.0.73",
|
|
44
44
|
"rxjs": "7.8.2"
|
|
45
45
|
},
|
|
46
46
|
"jest": {
|
|
@@ -14,6 +14,9 @@ const fakeCamera = () =>
|
|
|
14
14
|
enableRenderLayer: () => {},
|
|
15
15
|
disableRenderLayer: () => {},
|
|
16
16
|
isRenderLayerEnabled: () => true,
|
|
17
|
+
// Read once at construction time (`baseFov`) and written back on entering third-person - a
|
|
18
|
+
// plain field is enough here, this suite doesn't assert on fov itself.
|
|
19
|
+
camera: { fov: 75 },
|
|
17
20
|
}) as any;
|
|
18
21
|
|
|
19
22
|
const setup = async () => {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { SuspensionOptions, WheelOptions } from '@gg-web-engine/core';
|
|
2
|
+
import { Rapier3dFactory, Rapier3dRaycastVehicleComponent, Rapier3dRigidBodyComponent, Rapier3dWorldComponent } from '../../src';
|
|
3
|
+
|
|
4
|
+
// Shared suspension/wheel setup mirroring examples/ammo-car-three-rapier3d - see that example
|
|
5
|
+
// for the full car setup.
|
|
6
|
+
const suspension: SuspensionOptions = {
|
|
7
|
+
compression: 4.4,
|
|
8
|
+
damping: 2.3,
|
|
9
|
+
restLength: 0.6,
|
|
10
|
+
stiffness: 20,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const wheelOptions = (isFront: boolean, isLeft: boolean): WheelOptions => ({
|
|
14
|
+
isFront,
|
|
15
|
+
isLeft,
|
|
16
|
+
tyreRadius: isFront ? 0.35 : 0.4,
|
|
17
|
+
tyreWidth: isFront ? 0.2 : 0.3,
|
|
18
|
+
frictionSlip: 1000,
|
|
19
|
+
rollInfluence: 0.2,
|
|
20
|
+
maxTravel: 5,
|
|
21
|
+
position: {
|
|
22
|
+
x: isLeft ? 1 : -1,
|
|
23
|
+
y: isFront ? 1.7 : -1,
|
|
24
|
+
z: 0.3,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const addWheels = (vehicle: Rapier3dRaycastVehicleComponent) => {
|
|
29
|
+
for (const isFront of [true, false]) {
|
|
30
|
+
for (const isLeft of [true, false]) {
|
|
31
|
+
vehicle.addWheel(wheelOptions(isFront, isLeft), suspension);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const createFloor = (factory: Rapier3dFactory, world: Rapier3dWorldComponent, topZ: number): Rapier3dRigidBodyComponent => {
|
|
37
|
+
const floor = factory.createRigidBody(
|
|
38
|
+
{ shape: { shape: 'BOX', dimensions: { x: 75, y: 75, z: 1 } }, body: { bodyType: 'static', mass: 0 } },
|
|
39
|
+
{ position: { x: 0, y: 0, z: topZ - 0.5 } },
|
|
40
|
+
);
|
|
41
|
+
floor.addToWorld({ physicsWorld: world } as any);
|
|
42
|
+
return floor;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const createVehicle = (
|
|
46
|
+
world: Rapier3dWorldComponent,
|
|
47
|
+
factory: Rapier3dFactory,
|
|
48
|
+
position: { x: number; y: number; z: number },
|
|
49
|
+
): Rapier3dRaycastVehicleComponent => {
|
|
50
|
+
const chassis = factory.createRigidBody(
|
|
51
|
+
{ shape: { shape: 'BOX', dimensions: { x: 1.8, y: 4, z: 0.6 } }, body: { bodyType: 'dynamic', mass: 800 } },
|
|
52
|
+
{ position },
|
|
53
|
+
);
|
|
54
|
+
const vehicle = factory.createRaycastVehicle(chassis);
|
|
55
|
+
addWheels(vehicle);
|
|
56
|
+
vehicle.addToWorld({ physicsWorld: world } as any);
|
|
57
|
+
return vehicle;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Small steps, matching this package's other integration specs (see
|
|
61
|
+
// `rapier-3d-rigid-body-collision.spec.ts`) rather than one huge `simulate()` call.
|
|
62
|
+
const advance = (world: Rapier3dWorldComponent, totalMs: number, stepMs = 16) => {
|
|
63
|
+
for (let elapsed = 0; elapsed < totalMs; elapsed += stepMs) {
|
|
64
|
+
world.simulate(stepMs);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
describe('Rapier3dRaycastVehicleComponent', () => {
|
|
69
|
+
let world: Rapier3dWorldComponent;
|
|
70
|
+
let factory: Rapier3dFactory;
|
|
71
|
+
|
|
72
|
+
beforeEach(async () => {
|
|
73
|
+
if (world) {
|
|
74
|
+
world.dispose();
|
|
75
|
+
}
|
|
76
|
+
world = new Rapier3dWorldComponent();
|
|
77
|
+
await world.init();
|
|
78
|
+
factory = world.factory;
|
|
79
|
+
// default earth-like gravity, so vehicles actually fall onto the floors below
|
|
80
|
+
world.gravity = { x: 0, y: 0, z: -9.82 };
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterAll(() => {
|
|
84
|
+
world.dispose();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should fall under gravity and come to rest on a floor below it', () => {
|
|
88
|
+
createFloor(factory, world, 0);
|
|
89
|
+
const vehicle = createVehicle(world, factory, { x: 0, y: 0, z: 4 });
|
|
90
|
+
|
|
91
|
+
advance(world, 6000);
|
|
92
|
+
|
|
93
|
+
// chassis should have settled just above the floor surface (z = 0), not still falling
|
|
94
|
+
// and not sunk through it
|
|
95
|
+
expect(vehicle.position.z).toBeGreaterThan(-1);
|
|
96
|
+
expect(vehicle.position.z).toBeLessThan(3);
|
|
97
|
+
expect(vehicle.isWheelTouchesGround(0)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it(
|
|
101
|
+
'should let each vehicle fall through a floor with a different collision group and rest ' +
|
|
102
|
+
'only on the floor sharing its own collision group',
|
|
103
|
+
() => {
|
|
104
|
+
const groupA = world.registerCollisionGroup();
|
|
105
|
+
const groupB = world.registerCollisionGroup();
|
|
106
|
+
|
|
107
|
+
const floorA = createFloor(factory, world, 0);
|
|
108
|
+
floorA.ownCollisionGroups = floorA.interactWithCollisionGroups = [groupA];
|
|
109
|
+
|
|
110
|
+
const floorB = createFloor(factory, world, -10);
|
|
111
|
+
floorB.ownCollisionGroups = floorB.interactWithCollisionGroups = [groupB];
|
|
112
|
+
|
|
113
|
+
const vehicleA = createVehicle(world, factory, { x: 0, y: 0, z: 4 });
|
|
114
|
+
vehicleA.ownCollisionGroups = vehicleA.interactWithCollisionGroups = [groupA];
|
|
115
|
+
|
|
116
|
+
const vehicleB = createVehicle(world, factory, { x: 20, y: 0, z: 4 });
|
|
117
|
+
vehicleB.ownCollisionGroups = vehicleB.interactWithCollisionGroups = [groupB];
|
|
118
|
+
|
|
119
|
+
advance(world, 6000);
|
|
120
|
+
|
|
121
|
+
// vehicleA shares its collision group with floorA (z=0) - it should have landed there, both
|
|
122
|
+
// via chassis-body collision and via the raycast vehicle's own suspension raycasts.
|
|
123
|
+
expect(vehicleA.position.z).toBeGreaterThan(-1);
|
|
124
|
+
expect(vehicleA.position.z).toBeLessThan(3);
|
|
125
|
+
expect(vehicleA.isWheelTouchesGround(0)).toBe(true);
|
|
126
|
+
|
|
127
|
+
// vehicleB does not share a collision group with floorA, so it must fall straight through it
|
|
128
|
+
// (this is what threading `filterGroups` into `updateVehicle()` fixes - see
|
|
129
|
+
// `Rapier3dRaycastVehicleComponent.stepVehicleController`'s doc - without it the wheels' own
|
|
130
|
+
// suspension raycasts would ignore collision groups entirely and detect floorA as ground,
|
|
131
|
+
// holding the vehicle up even though its chassis passes through). It should come to rest on
|
|
132
|
+
// floorB (z=-10) instead.
|
|
133
|
+
expect(vehicleB.position.z).toBeGreaterThan(-11);
|
|
134
|
+
expect(vehicleB.position.z).toBeLessThan(-7);
|
|
135
|
+
expect(vehicleB.isWheelTouchesGround(0)).toBe(true);
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
it('should drive forward under engine force and report a matching positive wheelSpeed', () => {
|
|
140
|
+
createFloor(factory, world, 0);
|
|
141
|
+
const vehicle = createVehicle(world, factory, { x: 0, y: 0, z: 2 });
|
|
142
|
+
|
|
143
|
+
// let it settle onto the floor first
|
|
144
|
+
advance(world, 3000);
|
|
145
|
+
const settledY = vehicle.position.y;
|
|
146
|
+
|
|
147
|
+
// apply engine force to the rear wheels (indices 2, 3 per `addWheels`' iteration order)
|
|
148
|
+
vehicle.applyEngineForce(2, 4000);
|
|
149
|
+
vehicle.applyEngineForce(3, 4000);
|
|
150
|
+
advance(world, 3000);
|
|
151
|
+
|
|
152
|
+
// the vehicle should have moved forward (+y, this engine's forward axis) and report a
|
|
153
|
+
// meaningfully positive speed while doing so - not still ~0 and not wildly larger than a sane
|
|
154
|
+
// few-second driving distance (which would indicate a unit-conversion bug, e.g. a stray x3.6/
|
|
155
|
+
// /3.6 - see `wheelSpeed`'s own doc).
|
|
156
|
+
expect(vehicle.position.y).toBeGreaterThan(settledY + 0.5);
|
|
157
|
+
expect(vehicle.wheelSpeed).toBeGreaterThan(0.1);
|
|
158
|
+
expect(vehicle.wheelSpeed).toBeLessThan(50);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Pnt3 } from '@gg-web-engine/core';
|
|
2
|
+
import { Rapier3dFactory, Rapier3dWorldComponent } from '../../src';
|
|
3
|
+
|
|
4
|
+
describe('Rapier3dRigidBodyComponent.bodyOptions', () => {
|
|
5
|
+
let world: Rapier3dWorldComponent;
|
|
6
|
+
let factory: Rapier3dFactory;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
if (world) {
|
|
10
|
+
world.dispose();
|
|
11
|
+
}
|
|
12
|
+
world = new Rapier3dWorldComponent();
|
|
13
|
+
factory = new Rapier3dFactory(world);
|
|
14
|
+
await world.init();
|
|
15
|
+
world.gravity = Pnt3.O;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
world.dispose();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("reads back a dynamic body's mass/friction/restitution/ccd exactly as created", () => {
|
|
23
|
+
const body = factory.createRigidBody(
|
|
24
|
+
{
|
|
25
|
+
shape: { shape: 'SPHERE', radius: 1 },
|
|
26
|
+
body: { bodyType: 'dynamic', mass: 5, friction: 0.3, restitution: 0.7, ccd: true },
|
|
27
|
+
},
|
|
28
|
+
{ position: { x: 0, y: 0, z: 0 } },
|
|
29
|
+
);
|
|
30
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
31
|
+
|
|
32
|
+
expect(body.bodyOptions).toMatchObject({
|
|
33
|
+
bodyType: 'dynamic',
|
|
34
|
+
mass: 5,
|
|
35
|
+
friction: 0.3,
|
|
36
|
+
restitution: 0.7,
|
|
37
|
+
ccd: true,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it.each(['static', 'kinematic_pos', 'kinematic_vel'] as const)(
|
|
42
|
+
'reports bodyType "%s" correctly, with ccd always false for a non-dynamic body',
|
|
43
|
+
bodyType => {
|
|
44
|
+
const body = factory.createRigidBody(
|
|
45
|
+
{ shape: { shape: 'SPHERE', radius: 1 }, body: { bodyType, mass: 1, ccd: true } },
|
|
46
|
+
{ position: { x: 0, y: 0, z: 0 } },
|
|
47
|
+
);
|
|
48
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
49
|
+
|
|
50
|
+
expect(body.bodyOptions.bodyType).toBe(bodyType);
|
|
51
|
+
expect(body.bodyOptions.ccd).toBe(false);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
it('reflects a mutated ownCollisionGroups/interactWithCollisionGroups live', () => {
|
|
56
|
+
const body = factory.createRigidBody(
|
|
57
|
+
{ shape: { shape: 'SPHERE', radius: 1 }, body: { bodyType: 'static', mass: 0 } },
|
|
58
|
+
{ position: { x: 0, y: 0, z: 0 } },
|
|
59
|
+
);
|
|
60
|
+
body.addToWorld({ physicsWorld: world } as any);
|
|
61
|
+
|
|
62
|
+
body.ownCollisionGroups = [3];
|
|
63
|
+
body.interactWithCollisionGroups = [5, 6];
|
|
64
|
+
|
|
65
|
+
expect(body.bodyOptions.ownCollisionGroups).toEqual([3]);
|
|
66
|
+
expect(body.bodyOptions.interactWithCollisionGroups).toEqual([5, 6]);
|
|
67
|
+
});
|
|
68
|
+
});
|