Skip to content

Commit 0bedb23

Browse files
committed
fix(rapier): make sure args and shape colliders are in sync
1 parent e95ce5d commit 0bedb23

File tree

8 files changed

+513
-16
lines changed

8 files changed

+513
-16
lines changed
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import { ChangeDetectionStrategy, Component, computed, CUSTOM_ELEMENTS_SCHEMA, input } from '@angular/core';
2+
import { NgtArgs, NgtThreeElements, NgtVector3 } from 'angular-three';
3+
import {
4+
NgtrBallCollider,
5+
NgtrCapsuleCollider,
6+
NgtrConeCollider,
7+
NgtrCuboidCollider,
8+
NgtrCylinderCollider,
9+
NgtrHeightfieldCollider,
10+
NgtrRigidBody,
11+
NgtrRoundConeCollider,
12+
NgtrRoundCuboidCollider,
13+
NgtrRoundCylinderCollider,
14+
} from 'angular-three-rapier';
15+
import { NgtsHTML } from 'angular-three-soba/misc';
16+
import * as THREE from 'three';
17+
import { RoundedBoxGeometry } from 'three-stdlib';
18+
import { injectSuzanne } from '../suzanne';
19+
20+
const heightFieldHeight = 10;
21+
const heightFieldWidth = 10;
22+
const heightField = Array.from({ length: heightFieldHeight * heightFieldWidth }, () => Math.random());
23+
24+
const heightFieldGeometry = new THREE.PlaneGeometry(
25+
heightFieldWidth,
26+
heightFieldHeight,
27+
heightFieldWidth - 1,
28+
heightFieldHeight - 1,
29+
);
30+
31+
heightField.forEach((v, index) => {
32+
heightFieldGeometry.attributes['position'].array[index * 3 + 2] = v;
33+
});
34+
heightFieldGeometry.scale(1, -1, 1);
35+
heightFieldGeometry.rotateX(-Math.PI / 2);
36+
heightFieldGeometry.rotateY(-Math.PI / 2);
37+
heightFieldGeometry.computeVertexNormals();
38+
39+
const roundBoxGeometry = new RoundedBoxGeometry(1.4, 1.4, 1.4, 8, 0.2);
40+
41+
@Component({
42+
selector: 'app-cute-box',
43+
template: `
44+
<ngt-mesh castShadow receiveShadow [parameters]="options()">
45+
<ngt-box-geometry />
46+
<ngt-mesh-physical-material color="orange" />
47+
</ngt-mesh>
48+
`,
49+
changeDetection: ChangeDetectionStrategy.OnPush,
50+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
51+
})
52+
export class CuteBox {
53+
options = input({} as Partial<NgtThreeElements['ngt-mesh']>);
54+
}
55+
56+
@Component({
57+
selector: 'app-rigid-body-box',
58+
template: `
59+
<ngt-object3D rigidBody [position]="position()">
60+
<ngt-mesh castShadow receiveShadow>
61+
<ngt-box-geometry />
62+
<ngt-mesh-physical-material color="orange" />
63+
</ngt-mesh>
64+
</ngt-object3D>
65+
`,
66+
changeDetection: ChangeDetectionStrategy.OnPush,
67+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
68+
imports: [NgtrRigidBody],
69+
})
70+
export class RigidBodyBox {
71+
position = input<NgtVector3>([0, 0, 0]);
72+
}
73+
74+
@Component({
75+
selector: 'app-suzanne',
76+
template: `
77+
<ngt-primitive *args="[scene()]" [parameters]="{ castShadow: true, receiveShadow: true, visible: visible() }" />
78+
`,
79+
imports: [NgtArgs],
80+
changeDetection: ChangeDetectionStrategy.OnPush,
81+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
82+
})
83+
export class Suzanne {
84+
visible = input(true);
85+
86+
private suzanne = injectSuzanne();
87+
protected scene = computed(() => {
88+
const suzanne = this.suzanne();
89+
if (!suzanne) return null;
90+
return suzanne.nodes.Suzanne.clone();
91+
});
92+
}
93+
94+
@Component({
95+
selector: 'app-all-colliders-rapier',
96+
template: `
97+
<ngt-group>
98+
<ngt-object3D rigidBody [options]="{ colliders: false }">
99+
<app-cute-box />
100+
<ngt-object3D [cuboidCollider]="[0.5, 0.5, 0.5]" />
101+
<ngts-html>
102+
<div htmlContent>CuboidCollider</div>
103+
</ngts-html>
104+
</ngt-object3D>
105+
106+
<ngt-object3D rigidBody [position]="[2, 0, 0]" [options]="{ colliders: false }">
107+
<ngt-mesh [geometry]="roundBoxGeometry" castShadow receiveShadow>
108+
<ngt-mesh-physical-material color="orange" />
109+
</ngt-mesh>
110+
<ngt-object3D [roundCuboidCollider]="[0.5, 0.5, 0.5, 0.2]" />
111+
<ngts-html>
112+
<div htmlContent>RoundCuboidCollider</div>
113+
</ngts-html>
114+
</ngt-object3D>
115+
116+
<ngt-object3D rigidBody [position]="[4, 0, 0]" [options]="{ colliders: false }">
117+
<ngt-mesh castShadow receiveShadow [scale]="0.5">
118+
<ngt-sphere-geometry />
119+
<ngt-mesh-physical-material color="orange" />
120+
</ngt-mesh>
121+
<ngt-object3D [ballCollider]="[0.5]" />
122+
<ngts-html>
123+
<div htmlContent>BallCollider</div>
124+
</ngts-html>
125+
</ngt-object3D>
126+
127+
<ngt-object3D rigidBody [position]="[6, 0, 0]" [options]="{ colliders: false }">
128+
<ngt-mesh castShadow receiveShadow [geometry]="capsuleGeometry">
129+
<!-- TODO: rigidBody does not work with *args
130+
<ngt-capsule-geometry *args="[0.5, 1, 4, 8]" />
131+
-->
132+
<ngt-mesh-physical-material color="orange" />
133+
</ngt-mesh>
134+
<ngt-object3D [capsuleCollider]="[0.5, 0.5]" />
135+
<ngts-html>
136+
<div htmlContent>CapsuleCollider</div>
137+
</ngts-html>
138+
</ngt-object3D>
139+
140+
<ngt-object3D rigidBody [position]="[15, 0, 0]" [options]="{ colliders: false }">
141+
<ngt-mesh castShadow receiveShadow [geometry]="cylinderGeometry">
142+
<!-- TODO: rigidBody does not work with *args
143+
<ngt-cylinder-geometry *args="[0.5, 0.5, 2]" />
144+
-->
145+
<ngt-mesh-physical-material color="orange" />
146+
</ngt-mesh>
147+
<ngt-object3D [cylinderCollider]="[1, 0.5]" />
148+
<ngts-html>
149+
<div htmlContent>CylinderCollider</div>
150+
</ngts-html>
151+
</ngt-object3D>
152+
153+
<ngt-object3D rigidBody [position]="[8, 0, 0]" [options]="{ colliders: 'trimesh' }">
154+
<app-suzanne />
155+
<ngts-html>
156+
<div htmlContent>TrimeshCollider</div>
157+
</ngts-html>
158+
</ngt-object3D>
159+
160+
<ngt-object3D rigidBody [position]="[11, 0, 0]" [options]="{ colliders: 'hull' }">
161+
<app-suzanne />
162+
<ngts-html>
163+
<div htmlContent>HullCollider</div>
164+
</ngts-html>
165+
</ngt-object3D>
166+
167+
<ngt-object3D rigidBody [position]="[-5, 0, 0]" [options]="{ colliders: 'hull', includeInvisible: true }">
168+
<ngt-object3D>
169+
<app-suzanne [visible]="false" />
170+
</ngt-object3D>
171+
172+
<ngts-html>
173+
<div htmlContent>Invisible Collider</div>
174+
</ngts-html>
175+
</ngt-object3D>
176+
177+
<ngt-object3D rigidBody [options]="{ colliders: false }">
178+
<ngt-mesh castShadow receiveShadow [geometry]="coneGeometry">
179+
<!-- TODO: rigidBody does not work with *args
180+
<ngt-cone-geometry *args="[0.5, 2]" />
181+
-->
182+
<ngt-mesh-physical-material color="orange" />
183+
</ngt-mesh>
184+
<ngt-object3D [coneCollider]="[1, 0.5]" />
185+
<ngts-html>
186+
<div htmlContent>ConeCollider</div>
187+
</ngts-html>
188+
</ngt-object3D>
189+
190+
<ngt-object3D rigidBody [position]="[0, 3, 0]" [options]="{ colliders: false }">
191+
<ngt-mesh castShadow receiveShadow [geometry]="coneGeometry">
192+
<!-- TODO: rigidBody does not work with *args
193+
<ngt-cone-geometry *args="[0.5, 2]" />
194+
-->
195+
<ngt-mesh-physical-material color="orange" />
196+
</ngt-mesh>
197+
<ngt-object3D [roundConeCollider]="[1, 0.5, 0.1]" />
198+
<ngts-html>
199+
<div htmlContent>RoundConeCollider</div>
200+
</ngts-html>
201+
</ngt-object3D>
202+
203+
<ngt-object3D rigidBody [position]="[3, 3, 0]" [options]="{ colliders: false }">
204+
<ngt-mesh castShadow receiveShadow [geometry]="cylinderGeometry">
205+
<!-- TODO: rigidBody does not work with *args
206+
<ngt-cylinder-geometry *args="[0.5, 0.5, 2]" />
207+
-->
208+
<ngt-mesh-physical-material color="orange" />
209+
</ngt-mesh>
210+
<ngt-object3D [roundCylinderCollider]="[1, 0.4, 0.1]" />
211+
<ngts-html>
212+
<div htmlContent>RoundCylinderCollider</div>
213+
</ngts-html>
214+
</ngt-object3D>
215+
216+
<ngt-object3D rigidBody [position]="[0, -8, 0]" [options]="{ colliders: false }">
217+
<ngt-mesh castShadow receiveShadow [geometry]="heightFieldGeometry">
218+
<ngt-mesh-physical-material color="orange" [side]="DoubleSide" />
219+
</ngt-mesh>
220+
<ngt-object3D
221+
[heightfieldCollider]="[
222+
heightFieldWidth - 1,
223+
heightFieldHeight - 1,
224+
heightField,
225+
{ x: heightFieldWidth, y: 1, z: heightFieldHeight },
226+
]"
227+
/>
228+
<ngts-html>
229+
<div htmlContent>HeightfieldCollider</div>
230+
</ngts-html>
231+
</ngt-object3D>
232+
233+
<app-rigid-body-box [position]="[4, 10, 2]" />
234+
</ngt-group>
235+
`,
236+
imports: [
237+
NgtrRigidBody,
238+
RigidBodyBox,
239+
CuteBox,
240+
Suzanne,
241+
NgtrCuboidCollider,
242+
NgtrRoundCuboidCollider,
243+
NgtrBallCollider,
244+
NgtrCapsuleCollider,
245+
NgtrCylinderCollider,
246+
NgtrConeCollider,
247+
NgtrRoundConeCollider,
248+
NgtrRoundCylinderCollider,
249+
NgtrHeightfieldCollider,
250+
NgtsHTML,
251+
],
252+
changeDetection: ChangeDetectionStrategy.OnPush,
253+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
254+
})
255+
export default class AllCollidersExample {
256+
protected readonly DoubleSide = THREE.DoubleSide;
257+
protected readonly roundBoxGeometry = roundBoxGeometry;
258+
protected readonly heightFieldGeometry = heightFieldGeometry;
259+
protected readonly heightFieldWidth = heightFieldWidth;
260+
protected readonly heightFieldHeight = heightFieldHeight;
261+
protected readonly heightField = heightField;
262+
263+
protected capsuleGeometry = new THREE.CapsuleGeometry(0.5, 1, 4, 8);
264+
protected cylinderGeometry = new THREE.CylinderGeometry(0.5, 0.5, 2);
265+
protected coneGeometry = new THREE.ConeGeometry(0.5, 2);
266+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, output, signal, viewChild } from '@angular/core';
2+
import { NgtrContactForcePayload, NgtrRigidBody } from 'angular-three-rapier';
3+
import * as THREE from 'three';
4+
5+
// magic number: this is the start force for where the ball drops from
6+
// and is used to calculate the color change
7+
const startForce = 6500;
8+
const startColor = new THREE.Color(0xffffff);
9+
const floorColor = signal<THREE.ColorRepresentation>(startColor.clone().multiplyScalar(1).getHex());
10+
11+
@Component({
12+
selector: 'app-ball',
13+
template: `
14+
<ngt-object3D
15+
rigidBody
16+
[position]="[2, 15, 0]"
17+
[options]="{ colliders: 'ball', restitution: 1.5 }"
18+
(contactForce)="onContactForce($event)"
19+
(collisionEnter)="onCollisionEnter()"
20+
>
21+
<ngt-mesh castShadow receiveShadow>
22+
<ngt-sphere-geometry />
23+
<ngt-mesh-physical-material color="red" />
24+
</ngt-mesh>
25+
</ngt-object3D>
26+
`,
27+
imports: [NgtrRigidBody],
28+
changeDetection: ChangeDetectionStrategy.OnPush,
29+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
30+
})
31+
export class Ball {
32+
contactForce = output<number>();
33+
34+
private rigidBodyRef = viewChild.required(NgtrRigidBody);
35+
36+
protected onContactForce(event: NgtrContactForcePayload) {
37+
const rigidBody = this.rigidBodyRef().rigidBody();
38+
39+
const { totalForceMagnitude } = event;
40+
41+
if (totalForceMagnitude < 300) {
42+
rigidBody?.applyImpulse({ x: 0, y: 65, z: 0 }, true);
43+
}
44+
45+
this.contactForce.emit(totalForceMagnitude);
46+
console.log('contact force', event);
47+
}
48+
49+
protected onCollisionEnter() {
50+
console.log('collision enter');
51+
}
52+
}
53+
54+
@Component({
55+
selector: 'app-floor',
56+
template: `
57+
<ngt-object3D rigidBody="fixed" [options]="{ colliders: 'cuboid' }">
58+
<ngt-mesh [geometry]="boxGeometry">
59+
<!-- TODO: rigidBody does not work with *args
60+
<ngt-box-geometry *args="[10, 1, 10]" />
61+
-->
62+
<ngt-mesh-physical-material [color]="floorColor()" />
63+
</ngt-mesh>
64+
</ngt-object3D>
65+
`,
66+
imports: [NgtrRigidBody],
67+
changeDetection: ChangeDetectionStrategy.OnPush,
68+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
69+
})
70+
export class Floor {
71+
protected readonly floorColor = floorColor;
72+
protected boxGeometry = new THREE.BoxGeometry(10, 1, 10);
73+
}
74+
75+
@Component({
76+
selector: 'app-rapier-contact-force-events',
77+
template: `
78+
<ngt-group [position]="[0, -10, -10]">
79+
<app-ball (contactForce)="onContactForce($event)" />
80+
<app-floor />
81+
</ngt-group>
82+
`,
83+
imports: [Ball, Floor],
84+
changeDetection: ChangeDetectionStrategy.OnPush,
85+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
86+
})
87+
export default class ContactForceEventsExample {
88+
protected readonly floorColor = floorColor;
89+
90+
protected onContactForce(totalForceMagnitude: number) {
91+
const color = startColor.clone().multiplyScalar(1 - totalForceMagnitude / startForce);
92+
floorColor.set(color.getHex());
93+
}
94+
}

apps/examples/src/app/rapier/rapier.routes.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ const routes: Routes = [
3333
path: 'performance',
3434
loadComponent: () => import('./performance/performance'),
3535
},
36+
{
37+
path: 'all-colliders',
38+
loadComponent: () => import('./all-colliders/all-colliders'),
39+
},
40+
{
41+
path: 'sensors',
42+
loadComponent: () => import('./sensors/sensors'),
43+
},
44+
{
45+
path: 'contact-force-events',
46+
loadComponent: () => import('./contact-force-events/contact-force-events'),
47+
},
3648
{
3749
path: '',
3850
redirectTo: 'basic',

apps/examples/src/app/rapier/rapier.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,16 @@ extend(THREE);
3232
host: { class: 'rapier' },
3333
})
3434
export default class Rapier {
35-
protected examples = ['basic', 'rope-joint', 'spring', 'cluster', 'instanced-mesh', 'joints', 'performance'];
35+
protected examples = [
36+
'basic',
37+
'rope-joint',
38+
'spring',
39+
'cluster',
40+
'instanced-mesh',
41+
'joints',
42+
'performance',
43+
'all-colliders',
44+
'sensors',
45+
'contact-force-events',
46+
];
3647
}

0 commit comments

Comments
 (0)