Skip to content

Commit 8ba12fe

Browse files
committed
feat(rapier): add interaction groups and physics step support
1 parent 0f3eb98 commit 8ba12fe

File tree

5 files changed

+131
-3
lines changed

5 files changed

+131
-3
lines changed

libs/rapier/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
export * from './lib/colliders';
22
export * from './lib/instanced-rigid-bodies';
3+
export * from './lib/interaction-groups';
34
export * from './lib/joints';
45
export * from './lib/mesh-collider';
56
export * from './lib/physics';
7+
export * from './lib/physics-step-callback';
68
export * from './lib/rigid-body';
79

810
export type * from './lib/types';
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { computed, Directive, effect, inject, InjectionToken, input } from '@angular/core';
2+
import { InteractionGroups } from '@dimforge/rapier3d-compat';
3+
4+
/**
5+
* Calculates an InteractionGroup bitmask for use in the `collisionGroups` or `solverGroups`
6+
* properties of RigidBody or Collider components. The first argument represents a list of
7+
* groups the entity is in (expressed as numbers from 0 to 15). The second argument is a list
8+
* of groups that will be filtered against. When it is omitted, all groups are filtered against.
9+
*
10+
* @example
11+
* A RigidBody that is member of group 0 and will collide with everything from groups 0 and 1:
12+
*
13+
* ```tsx
14+
* <RigidBody collisionGroups={interactionGroups([0], [0, 1])} />
15+
* ```
16+
*
17+
* A RigidBody that is member of groups 0 and 1 and will collide with everything else:
18+
*
19+
* ```tsx
20+
* <RigidBody collisionGroups={interactionGroups([0, 1])} />
21+
* ```
22+
*
23+
* A RigidBody that is member of groups 0 and 1 and will not collide with anything:
24+
*
25+
* ```tsx
26+
* <RigidBody collisionGroups={interactionGroups([0, 1], [])} />
27+
* ```
28+
*
29+
* Please note that Rapier needs interaction filters to evaluate to true between _both_ colliding
30+
* entities for collision events to trigger.
31+
*
32+
* @param memberships Groups the collider is a member of. (Values can range from 0 to 15.)
33+
* @param filters Groups the interaction group should filter against. (Values can range from 0 to 15.)
34+
* @returns An InteractionGroup bitmask.
35+
*/
36+
export function interactionGroups(memberships: number | number[], filters?: number | number[]): InteractionGroups {
37+
return (bitmask(memberships) << 16) + (filters !== undefined ? bitmask(filters) : 0b1111_1111_1111_1111);
38+
}
39+
40+
function bitmask(groups: number | number[]): InteractionGroups {
41+
return [groups].flat().reduce((acc, layer) => acc | (1 << layer), 0);
42+
}
43+
44+
export const COLLISION_GROUPS_HANDLER = new InjectionToken<
45+
() => undefined | ((interactionGroups: InteractionGroups) => void)
46+
>('COLLISION_GROUPS_HANDLER');
47+
48+
@Directive({ selector: 'ngt-object3D[interactionGroups]' })
49+
export class NgtrInteractionGroups {
50+
inputs = input.required<[number | number[], (number | number[])?]>({ alias: 'interactionGroups' });
51+
interactionGroups = computed(() => {
52+
const [memberships, filters] = this.inputs();
53+
return interactionGroups(memberships, filters);
54+
});
55+
56+
constructor() {
57+
const collisionGroupsHandlerFn = inject(COLLISION_GROUPS_HANDLER, { host: true, optional: true });
58+
59+
effect(() => {
60+
if (!collisionGroupsHandlerFn) return;
61+
const handler = collisionGroupsHandlerFn();
62+
if (!handler) return;
63+
handler(this.interactionGroups());
64+
});
65+
}
66+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { DestroyRef, inject, Injector } from '@angular/core';
2+
import { assertInjector } from 'ngxtension/assert-injector';
3+
import { NgtrPhysics } from './physics';
4+
import type { NgtrWorldStepCallback } from './types';
5+
6+
export function injectBeforePhysicsStep(callback: NgtrWorldStepCallback, injector?: Injector) {
7+
return assertInjector(injectBeforePhysicsStep, injector, () => {
8+
const physics = inject(NgtrPhysics);
9+
10+
physics.beforeStepCallbacks.add(callback);
11+
12+
inject(DestroyRef).onDestroy(() => {
13+
physics.beforeStepCallbacks.delete(callback);
14+
});
15+
});
16+
}
17+
18+
export function injectAfterPhysicsStep(callback: NgtrWorldStepCallback, injector?: Injector) {
19+
return assertInjector(injectAfterPhysicsStep, injector, () => {
20+
const physics = inject(NgtrPhysics);
21+
22+
physics.afterStepCallbacks.add(callback);
23+
24+
inject(DestroyRef).onDestroy(() => {
25+
physics.afterStepCallbacks.delete(callback);
26+
});
27+
});
28+
}

libs/rapier/src/lib/physics.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ export class NgtrPhysics {
127127
colliderStates: NgtrColliderStateMap = new Map();
128128
rigidBodyEvents: NgtrEventMap = new Map();
129129
colliderEvents: NgtrEventMap = new Map();
130-
private beforeStepCallbacks: NgtrWorldStepCallbackSet = new Set();
131-
private afterStepCallbacks: NgtrWorldStepCallbackSet = new Set();
130+
beforeStepCallbacks: NgtrWorldStepCallbackSet = new Set();
131+
afterStepCallbacks: NgtrWorldStepCallbackSet = new Set();
132132

133133
private eventQueue = computed(() => {
134134
const rapier = this.rapier();

libs/rapier/src/lib/rigid-body.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,16 @@ import {
1111
model,
1212
output,
1313
untracked,
14+
viewChildren,
1415
} from '@angular/core';
15-
import { ActiveEvents, Collider, ColliderDesc, RigidBody, RigidBodyDesc } from '@dimforge/rapier3d-compat';
16+
import {
17+
ActiveEvents,
18+
Collider,
19+
ColliderDesc,
20+
InteractionGroups,
21+
RigidBody,
22+
RigidBodyDesc,
23+
} from '@dimforge/rapier3d-compat';
1624
import {
1725
applyProps,
1826
extend,
@@ -29,6 +37,7 @@ import {
2937
import { mergeInputs } from 'ngxtension/inject-inputs';
3038
import * as THREE from 'three';
3139
import { Object3D } from 'three';
40+
import { COLLISION_GROUPS_HANDLER } from './interaction-groups';
3241
import { NgtrPhysics } from './physics';
3342
import { _matrix4, _position, _rotation, _scale, _vector3 } from './shared';
3443
import type {
@@ -439,6 +448,27 @@ export const rigidBodyDefaultOptions: NgtrRigidBodyOptions = {
439448
schemas: [CUSTOM_ELEMENTS_SCHEMA],
440449
changeDetection: ChangeDetectionStrategy.OnPush,
441450
imports: [NgtrAnyCollider],
451+
providers: [
452+
{
453+
provide: COLLISION_GROUPS_HANDLER,
454+
useFactory: (rigidBody: NgtrRigidBody) => {
455+
return () => {
456+
const anyColliders = rigidBody.anyColliders();
457+
if (!anyColliders.length) return;
458+
459+
const colliders = anyColliders.map((anyCollider) => anyCollider['collider']);
460+
return (interactionGroups: InteractionGroups) => {
461+
for (const colliderFn of colliders) {
462+
const collider = colliderFn();
463+
if (!collider) continue;
464+
collider.setCollisionGroups(interactionGroups);
465+
}
466+
};
467+
};
468+
},
469+
deps: [NgtrRigidBody],
470+
},
471+
],
442472
})
443473
export class NgtrRigidBody {
444474
type = input.required({
@@ -455,6 +485,8 @@ export class NgtrRigidBody {
455485
userData = input<NgtThreeElements['ngt-object3D']['userData']>();
456486
options = input(rigidBodyDefaultOptions, { transform: mergeInputs(rigidBodyDefaultOptions) });
457487

488+
anyColliders = viewChildren(NgtrAnyCollider);
489+
458490
private object3DParameters = computed(() => {
459491
const [position, rotation, scale, quaternion, userData] = [
460492
this.position(),

0 commit comments

Comments
 (0)