@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.
@@ -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
- readonly handleIdEntityMap: Map<number, Rapier3dRigidBodyComponent>;
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
- this._nativeWorld.timestep = delta / 1000;
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, undefined, (mask(options.collisionFilterGroups) << 16) | mask(options.collisionFilterMask));
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
- result.hitBody = this.handleIdEntityMap.get(rigidBody.handle);
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
- // export * from './components/rapier-3d-raycast-vehicle.component';
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): never;
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
- // unlike the ammo adapter, no hand-rolled push logic needed, Rapier already computes it.
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
- throw new Error('Raycast vehicle bindings for rapier3D are not implemented');
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
- bodyDesc.mass = options.mass || 1;
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: never;
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.72",
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.72",
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.72",
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
+ });