Skip to content

Commit 0de7623

Browse files
authored
feat(rapier): add rapier (#54)
* feat(rapier): gen rapier * feat(rapier): add angular-three-rapier wip wip - clean up effect code - only set events if the output ref is listened wip instanced * fix(rapier): instanced * fix(core): add NON_ROOT static field for multiple scenes * fix(core): add equality fn for objects and nonObjects on localState * docs: add rapier example * fix(rapier): auto wake up when updating rigidbody translation and rotation * docs: beautiful gradient * fix(core): adjust instance store again and notify ancestors on specific type * feat(rapier): add joints * docs: more rapier examples * docs: adjust joints example * fix(rapier): colliders setting incorrect shape * fix(rapier): call inner createJoint with assertInjector * fix(rapier): update mass properties on collider properly * docs: more rapier examples (I think we're ready)
1 parent ffadfeb commit 0de7623

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3961
-13
lines changed

apps/kitchen-sink/public/bendy.glb

20.2 KB
Binary file not shown.

apps/kitchen-sink/public/suzanne.glb

13.9 KB
Binary file not shown.

apps/kitchen-sink/src/app/app.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { filter, map, tap } from 'rxjs';
1111
<option value="soba">/soba</option>
1212
<option value="cannon">/cannon</option>
1313
<option value="postprocessing">/postprocessing</option>
14+
<option value="rapier">/rapier</option>
1415
</select>
1516
`,
1617
imports: [RouterOutlet],
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
2-
import { provideRouter } from '@angular/router';
2+
import { provideRouter, withComponentInputBinding } from '@angular/router';
33
import { appRoutes } from './app.routes';
44

55
export const appConfig: ApplicationConfig = {
66
providers: [
77
provideExperimentalZonelessChangeDetection(),
88
// provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }),
9-
provideRouter(appRoutes),
9+
provideRouter(appRoutes, withComponentInputBinding()),
1010
],
1111
};

apps/kitchen-sink/src/app/app.routes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export const appRoutes: Route[] = [
1616
loadComponent: () => import('./soba/soba'),
1717
loadChildren: () => import('./soba/soba.routes'),
1818
},
19+
{
20+
path: 'rapier',
21+
loadComponent: () => import('./rapier/rapier'),
22+
loadChildren: () => import('./rapier/rapier.routes'),
23+
},
1924
{
2025
path: '',
2126
// redirectTo: 'cannon',
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';
2+
import { injectBeforeRender, NON_ROOT } from 'angular-three';
3+
import { NgtrCuboidCollider, NgtrPhysics, NgtrRigidBody } from 'angular-three-rapier';
4+
import { NgtsPerspectiveCamera } from 'angular-three-soba/cameras';
5+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
6+
7+
@Component({
8+
standalone: true,
9+
template: `
10+
<ngts-perspective-camera [options]="{ makeDefault: true, position: [5, 5, 5] }" />
11+
12+
<ngtr-physics [options]="{ debug: true }">
13+
<ngt-object3D ngtrRigidBody [options]="{ colliders: 'hull' }" [position]="[0, 5, 0]">
14+
<ngt-mesh>
15+
<ngt-torus-geometry />
16+
</ngt-mesh>
17+
</ngt-object3D>
18+
19+
@if (currentCollider() === 1) {
20+
<ngt-object3D ngtrCuboidCollider [args]="[1, 0.5, 1]" (collisionExit)="currentCollider.set(2)" />
21+
} @else if (currentCollider() === 2) {
22+
<ngt-object3D
23+
ngtrCuboidCollider
24+
[position]="[0, -1, 0]"
25+
[args]="[3, 0.5, 3]"
26+
(collisionExit)="currentCollider.set(3)"
27+
/>
28+
} @else if (currentCollider() === 3) {
29+
<ngt-object3D
30+
ngtrCuboidCollider
31+
[position]="[0, -3, 0]"
32+
[args]="[6, 0.5, 6]"
33+
(collisionExit)="currentCollider.set(4)"
34+
/>
35+
} @else {
36+
<ngt-object3D ngtrCuboidCollider [position]="[0, -6, 0]" [args]="[20, 0.5, 20]" />
37+
}
38+
</ngtr-physics>
39+
40+
<ngts-orbit-controls />
41+
`,
42+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
43+
changeDetection: ChangeDetectionStrategy.OnPush,
44+
host: { class: 'experience-basic-rapier' },
45+
imports: [NgtrPhysics, NgtrRigidBody, NgtrCuboidCollider, NgtsOrbitControls, NgtsPerspectiveCamera],
46+
})
47+
export class Basic {
48+
static [NON_ROOT] = true;
49+
50+
protected currentCollider = signal(1);
51+
52+
constructor() {
53+
injectBeforeRender(({ camera }) => {
54+
const currentCollider = this.currentCollider();
55+
if (currentCollider === 2) {
56+
camera.position.lerp({ x: 10, y: 10, z: 10 }, 0.1);
57+
} else if (currentCollider === 3) {
58+
camera.position.lerp({ x: 15, y: 15, z: 15 }, 0.1);
59+
} else if (currentCollider === 4) {
60+
camera.position.lerp({ x: 20, y: 40, z: 40 }, 0.1);
61+
}
62+
});
63+
}
64+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
CUSTOM_ELEMENTS_SCHEMA,
5+
effect,
6+
ElementRef,
7+
inject,
8+
viewChild,
9+
} from '@angular/core';
10+
import { injectBeforeRender, NgtArgs, NON_ROOT } from 'angular-three';
11+
import { NgtrInstancedRigidBodies, NgtrPhysics } from 'angular-three-rapier';
12+
import { Color, InstancedMesh, Vector3 } from 'three';
13+
14+
const BALLS = 1000;
15+
16+
@Component({
17+
standalone: true,
18+
template: `
19+
<ngt-group>
20+
<ngt-object3D [ngtrInstancedRigidBodies]="bodies" [options]="{ colliders: 'ball', linearDamping: 5 }">
21+
<ngt-instanced-mesh #instancedMesh *args="[undefined, undefined, BALLS]" [castShadow]="true">
22+
<ngt-sphere-geometry *args="[0.2]" />
23+
<ngt-mesh-physical-material [roughness]="0" [metalness]="0.5" color="yellow" />
24+
</ngt-instanced-mesh>
25+
</ngt-object3D>
26+
</ngt-group>
27+
`,
28+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
29+
changeDetection: ChangeDetectionStrategy.OnPush,
30+
host: { class: 'cluster-rapier' },
31+
imports: [NgtrInstancedRigidBodies, NgtArgs],
32+
})
33+
export class ClusterExample {
34+
static [NON_ROOT] = true;
35+
36+
protected readonly BALLS = BALLS;
37+
protected bodies = Array.from({ length: BALLS }, (_, index) => {
38+
return {
39+
key: index,
40+
position: [Math.floor(index / 30), (index % 30) * 0.5, 0] as [number, number, number],
41+
};
42+
});
43+
44+
private rigidBodiesRef = viewChild.required(NgtrInstancedRigidBodies);
45+
private instancedMeshRef = viewChild<ElementRef<InstancedMesh>>('instancedMesh');
46+
47+
private physics = inject(NgtrPhysics);
48+
49+
constructor() {
50+
injectBeforeRender(() => {
51+
const paused = this.physics.paused();
52+
if (paused) return;
53+
54+
const rigidBodies = this.rigidBodiesRef().rigidBodyRefs();
55+
rigidBodies.forEach((body) => {
56+
const rigidBody = body.rigidBody();
57+
if (rigidBody) {
58+
const { x, y, z } = rigidBody.translation();
59+
const p = new Vector3(x, y, z);
60+
p.normalize().multiplyScalar(-0.01);
61+
rigidBody.applyImpulse(p, true);
62+
}
63+
});
64+
});
65+
66+
effect(() => {
67+
const instancedMesh = this.instancedMeshRef()?.nativeElement;
68+
if (!instancedMesh) return;
69+
70+
for (let i = 0; i < BALLS; i++) {
71+
instancedMesh.setColorAt(i, new Color(Math.random() * 0xffffff));
72+
}
73+
if (instancedMesh.instanceColor) instancedMesh.instanceColor.needsUpdate = true;
74+
});
75+
}
76+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Basic } from './basic/basic';
2+
import { ClusterExample } from './cluster/cluster';
3+
import { InstancedMeshExample } from './instanced-mesh/instanced-mesh';
4+
import { JointsExample } from './joints/joints';
5+
import { PerformanceExample } from './performance/performance';
6+
import { RopeJointExample } from './rope-joint/rope-joint';
7+
import { SpringExample } from './spring/spring';
8+
9+
export const SCENES_MAP = {
10+
basic: Basic,
11+
instancedMesh: InstancedMeshExample,
12+
performance: PerformanceExample,
13+
joints: JointsExample,
14+
cluster: ClusterExample,
15+
ropeJoint: RopeJointExample,
16+
spring: SpringExample,
17+
} as const;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
CUSTOM_ELEMENTS_SCHEMA,
5+
effect,
6+
ElementRef,
7+
signal,
8+
viewChild,
9+
} from '@angular/core';
10+
import { injectStore, NgtArgs, NgtThreeEvent, NON_ROOT } from 'angular-three';
11+
import { NgtrInstancedRigidBodies, NgtrInstancedRigidBodyOptions } from 'angular-three-rapier';
12+
import { Color, InstancedMesh } from 'three';
13+
import { injectSuzanne } from '../suzanne';
14+
15+
const MAX_COUNT = 2000;
16+
17+
@Component({
18+
standalone: true,
19+
template: `
20+
<ngt-group>
21+
@if (gltf(); as gltf) {
22+
<ngt-object3D
23+
#instancedRigidBodies="instancedRigidBodies"
24+
[ngtrInstancedRigidBodies]="bodies()"
25+
[options]="{ colliders: 'hull' }"
26+
>
27+
<ngt-instanced-mesh
28+
*args="[gltf.nodes.Suzanne.geometry, undefined, MAX_COUNT]"
29+
#instancedMesh
30+
[castShadow]="true"
31+
[count]="bodies().length"
32+
(click)="onClick(instancedRigidBodies, $any($event))"
33+
>
34+
<ngt-mesh-physical-material />
35+
</ngt-instanced-mesh>
36+
</ngt-object3D>
37+
}
38+
</ngt-group>
39+
`,
40+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
41+
changeDetection: ChangeDetectionStrategy.OnPush,
42+
host: { class: 'instanced-mesh-rapier' },
43+
imports: [NgtrInstancedRigidBodies, NgtArgs],
44+
})
45+
export class InstancedMeshExample {
46+
static [NON_ROOT] = true;
47+
48+
protected readonly MAX_COUNT = MAX_COUNT;
49+
50+
private instancedMeshRef = viewChild<ElementRef<InstancedMesh>>('instancedMesh');
51+
52+
protected gltf = injectSuzanne();
53+
private store = injectStore();
54+
55+
protected bodies = signal(Array.from({ length: 100 }, () => this.createBody()));
56+
57+
constructor() {
58+
effect(() => {
59+
const instancedMesh = this.instancedMeshRef()?.nativeElement;
60+
if (!instancedMesh) return;
61+
62+
for (let i = 0; i < MAX_COUNT; i++) {
63+
instancedMesh.setColorAt(i, new Color(Math.random() * 0xffffff));
64+
}
65+
if (instancedMesh.instanceColor) {
66+
instancedMesh.instanceColor.needsUpdate = true;
67+
}
68+
});
69+
70+
effect((onCleanup) => {
71+
const sub = this.store.snapshot.pointerMissed$.subscribe(() => {
72+
this.bodies.update((prev) => [...prev, this.createBody()]);
73+
});
74+
onCleanup(() => sub.unsubscribe());
75+
});
76+
}
77+
78+
private createBody(): NgtrInstancedRigidBodyOptions {
79+
return {
80+
key: Math.random(),
81+
position: [Math.random() * 20, Math.random() * 20, Math.random() * 20],
82+
rotation: [Math.random() * Math.PI * 2, Math.random() * Math.PI * 2, Math.random() * Math.PI * 2],
83+
scale: [0.5 + Math.random(), 0.5 + Math.random(), 0.5 + Math.random()],
84+
};
85+
}
86+
87+
onClick(instancedRigidBodies: NgtrInstancedRigidBodies, event: NgtThreeEvent<MouseEvent>) {
88+
if (event.instanceId !== undefined) {
89+
instancedRigidBodies
90+
.rigidBodyRefs()
91+
.at(event.instanceId)
92+
?.rigidBody()
93+
?.applyTorqueImpulse({ x: 0, y: 50, z: 0 }, true);
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)