Skip to content

Commit eb59ea6

Browse files
committed
docs: add animation with ccdiksolver
1 parent 4e6470d commit eb59ea6

File tree

9 files changed

+301
-1
lines changed

9 files changed

+301
-1
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { NgIf } from '@angular/common';
2+
import { Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, inject, ViewChild } from '@angular/core';
3+
import {
4+
extend,
5+
injectNgtLoader,
6+
NgtArgs,
7+
NgtBeforeRenderEvent,
8+
NgtCanvas,
9+
NgtPush,
10+
NgtState,
11+
NgtStore,
12+
} from 'angular-three';
13+
import { map } from 'rxjs';
14+
import * as THREE from 'three';
15+
import { CCDIKHelper, DRACOLoader, GLTFLoader, TransformControls } from 'three-stdlib';
16+
import { DemoOrbitControls } from '../ui-orbit-controls/orbit-controls.component';
17+
import { AnimationSkinningIKStore } from './animation-skinning-ik.store';
18+
19+
extend({ TransformControls, CCDIKHelper });
20+
21+
@Component({
22+
selector: 'demo-ik-helper',
23+
standalone: true,
24+
template: `
25+
<ng-container *ngIf="(ooi$ | ngtPush).kira as kira">
26+
<ngt-cCDIK-helper *args="[kira, iks, 0.01]" (beforeRender)="onBeforeRender()" />
27+
</ng-container>
28+
`,
29+
imports: [NgtArgs, NgIf, NgtPush],
30+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
31+
})
32+
export class IKHelper {
33+
private readonly ikStore = inject(AnimationSkinningIKStore);
34+
readonly iks = this.ikStore.iks;
35+
readonly ooi$ = this.ikStore.select('OOI');
36+
37+
onBeforeRender() {
38+
this.ikStore.solver.update();
39+
}
40+
}
41+
42+
@Component({
43+
selector: 'demo-sphere-camera',
44+
standalone: true,
45+
template: `
46+
<ngt-cube-camera *args="[0.05, 50, cubeRenderTarget]" (beforeRender)="onBeforeRender($any($event))" />
47+
`,
48+
imports: [NgtArgs],
49+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
50+
})
51+
export class SphereCamera {
52+
private readonly ikStore = inject(AnimationSkinningIKStore);
53+
readonly cubeRenderTarget = this.ikStore.get('cubeRenderTarget');
54+
55+
onBeforeRender({ object, state: { gl, scene } }: NgtBeforeRenderEvent<THREE.CubeCamera>) {
56+
const sphere = this.ikStore.OOI.sphere;
57+
if (sphere) {
58+
sphere.visible = false;
59+
sphere.getWorldPosition(object.position);
60+
object.update(gl, scene);
61+
sphere.visible = true;
62+
}
63+
}
64+
}
65+
66+
@Component({
67+
selector: 'demo-kira',
68+
standalone: true,
69+
template: `
70+
<ngt-primitive
71+
*args="[model$ | ngtPush : null]"
72+
(afterAttach)="onAfterAttach()"
73+
(beforeRender)="onBeforeRender()"
74+
/>
75+
`,
76+
imports: [NgtArgs, NgtPush],
77+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
78+
})
79+
export class Kira {
80+
private readonly ikStore = inject(AnimationSkinningIKStore);
81+
readonly v0 = new THREE.Vector3();
82+
83+
readonly model$ = injectNgtLoader(
84+
() => GLTFLoader,
85+
'assets/kira.glb',
86+
(loader) => {
87+
const dracoLoader = new DRACOLoader();
88+
dracoLoader.setDecoderPath('assets/draco/');
89+
(loader as GLTFLoader).setDRACOLoader(dracoLoader);
90+
}
91+
).pipe(
92+
map((gltf) => {
93+
const ooi: Record<string, THREE.Object3D> = {};
94+
gltf.scene.traverse((n) => {
95+
if (n.name === 'head') ooi['head'] = n;
96+
if (n.name === 'lowerarm_l') ooi['lowerarm_l'] = n;
97+
if (n.name === 'Upperarm_l') ooi['Upperarm_l'] = n;
98+
if (n.name === 'hand_l') ooi['hand_l'] = n;
99+
if (n.name === 'target_hand_l') ooi['target_hand_l'] = n;
100+
if (n.name === 'boule') ooi['sphere'] = n;
101+
if (n.name === 'Kira_Shirt_left') ooi['kira'] = n;
102+
if ((n as THREE.Mesh).isMesh) n.frustumCulled = false;
103+
});
104+
this.ikStore.set({ OOI: ooi });
105+
return gltf.scene;
106+
})
107+
);
108+
109+
onAfterAttach() {
110+
this.ikStore.kiraReady();
111+
}
112+
113+
onBeforeRender() {
114+
const head = this.ikStore.OOI.head;
115+
const sphere = this.ikStore.OOI.sphere;
116+
if (head && sphere) {
117+
sphere.getWorldPosition(this.v0);
118+
head.lookAt(this.v0);
119+
head.rotation.set(head.rotation.x, head.rotation.y + Math.PI, head.rotation.z);
120+
}
121+
}
122+
}
123+
124+
@Component({
125+
standalone: true,
126+
template: `
127+
<ngt-color *args="['#dddddd']" attach="background" />
128+
<ngt-fog-exp2 *args="['#ffffff', 0.17]" attach="fog" />
129+
130+
<ngt-ambient-light [intensity]="8" color="#ffffff" />
131+
132+
<demo-kira />
133+
<demo-sphere-camera />
134+
<demo-ik-helper />
135+
136+
<demo-orbit-controls
137+
[minDistance]="0.2"
138+
[maxDistance]="1.5"
139+
(ready)="ikStore.set({ orbitControls: $any($event) })"
140+
/>
141+
142+
<ngt-transform-controls
143+
#transformControls
144+
*args="[camera, glDom]"
145+
[size]="0.75"
146+
[showX]="false"
147+
space="world"
148+
/>
149+
`,
150+
imports: [NgtArgs, DemoOrbitControls, Kira, SphereCamera, IKHelper],
151+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
152+
})
153+
export class Scene {
154+
readonly ikStore = inject(AnimationSkinningIKStore);
155+
private readonly store = inject(NgtStore);
156+
readonly camera = this.store.get('camera');
157+
readonly glDom = this.store.get('gl', 'domElement');
158+
159+
@ViewChild('transformControls') set transformControls({ nativeElement }: ElementRef<TransformControls>) {
160+
this.ikStore.set({ transformControls: nativeElement });
161+
nativeElement.addEventListener('mouseDown', () => (this.ikStore.orbitControls.enabled = false));
162+
nativeElement.addEventListener('mouseUp', () => (this.ikStore.orbitControls.enabled = true));
163+
}
164+
}
165+
166+
@Component({
167+
standalone: true,
168+
template: `
169+
<ngt-canvas
170+
[sceneGraph]="SceneGraph"
171+
[camera]="{
172+
fov: 55,
173+
near: 0.001,
174+
far: 5000,
175+
position: [0.9728517749133652, 1.1044765132727201, 0.7316689528482836]
176+
}"
177+
[gl]="{ logarithmicDepthBuffer: true }"
178+
(created)="onCreated($event)"
179+
/>
180+
`,
181+
providers: [AnimationSkinningIKStore],
182+
imports: [NgtCanvas],
183+
})
184+
export default class DemoAnimationSkinningIK {
185+
readonly SceneGraph = Scene;
186+
187+
onCreated({ scene, camera, gl }: NgtState) {
188+
camera.lookAt(scene.position);
189+
gl.physicallyCorrectLights = true;
190+
}
191+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Injectable } from '@angular/core';
2+
import { applyProps, NgtRxStore } from 'angular-three';
3+
import { filter } from 'rxjs';
4+
import * as THREE from 'three';
5+
import { CCDIKSolver, IKS, OrbitControls, TransformControls } from 'three-stdlib';
6+
7+
type OOI = 'head' | 'lowerarm_l' | 'Upperarm_l' | 'hand_l' | 'target_hand_l' | 'sphere' | 'kira';
8+
9+
interface AnimationSkinningIKState {
10+
OOI: Record<OOI, THREE.Object3D>;
11+
orbitControls: OrbitControls;
12+
transformControls: TransformControls;
13+
cubeRenderTarget: THREE.WebGLCubeRenderTarget;
14+
material: THREE.MeshBasicMaterial;
15+
solver: CCDIKSolver;
16+
}
17+
18+
@Injectable()
19+
export class AnimationSkinningIKStore extends NgtRxStore<AnimationSkinningIKState> {
20+
readonly iks = [
21+
{
22+
target: 22, // "target_hand_l"
23+
effector: 6, // "hand_l"
24+
links: [
25+
{
26+
index: 5, // "lowerarm_l"
27+
enabled: true,
28+
rotationMin: new THREE.Vector3(1.2, -1.8, -0.4),
29+
rotationMax: new THREE.Vector3(1.7, -1.1, 0.3),
30+
},
31+
{
32+
index: 4, // "Upperarm_l"
33+
enabled: true,
34+
rotationMin: new THREE.Vector3(0.1, -0.7, -1.8),
35+
rotationMax: new THREE.Vector3(1.1, 0, -1.4),
36+
},
37+
],
38+
},
39+
];
40+
41+
override initialize() {
42+
super.initialize();
43+
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(1024);
44+
this.set({
45+
OOI: {} as AnimationSkinningIKState['OOI'],
46+
cubeRenderTarget,
47+
material: new THREE.MeshBasicMaterial({ envMap: cubeRenderTarget.texture }),
48+
});
49+
}
50+
51+
kiraReady() {
52+
this.hold(this.select('OOI').pipe(filter((ooi) => !!Object.keys(ooi).length)), (ooi) => {
53+
this.orbitControls.target.copy(ooi.sphere.position);
54+
ooi.hand_l.attach(ooi.sphere);
55+
applyProps(ooi.sphere, { material: this.get('material') });
56+
57+
this.transformControls.attach(ooi.target_hand_l);
58+
ooi.kira.add((ooi.kira as THREE.SkinnedMesh).skeleton.bones[0]);
59+
this.set({ solver: new CCDIKSolver(ooi.kira as THREE.SkinnedMesh, this.iks as unknown as IKS[]) });
60+
});
61+
}
62+
63+
get OOI() {
64+
return this.get('OOI');
65+
}
66+
67+
get orbitControls() {
68+
return this.get('orbitControls');
69+
}
70+
71+
get transformControls() {
72+
return this.get('transformControls');
73+
}
74+
75+
get solver() {
76+
return this.get('solver');
77+
}
78+
}

apps/demo/src/app/app.routes.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,13 @@ export const routes: Routes = [
3939
asset: 'assets/demo/animation-keyframes',
4040
},
4141
},
42+
{
43+
path: 'animation-skinning-ik',
44+
loadComponent: () => import('./animation-skinning-ik/animation-skinning-ik.component'),
45+
data: {
46+
description: 'Three.js animation with CCDIKSolver',
47+
link: '/animation-skinning-ik',
48+
asset: 'assets/demo/animation-skinning-ik',
49+
},
50+
},
4251
];

apps/demo/src/app/ui-orbit-controls/orbit-controls.component.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
1-
import { Component, CUSTOM_ELEMENTS_SCHEMA, inject, Input } from '@angular/core';
1+
import {
2+
Component,
3+
CUSTOM_ELEMENTS_SCHEMA,
4+
ElementRef,
5+
EventEmitter,
6+
inject,
7+
Input,
8+
Output,
9+
ViewChild,
10+
} from '@angular/core';
211
import { NgtArgs, NgtStore } from 'angular-three';
12+
import { OrbitControls } from 'three-stdlib';
313

414
@Component({
515
selector: 'demo-orbit-controls',
616
standalone: true,
717
template: `
818
<ngt-orbit-controls
919
*args="[camera, glDom]"
20+
#orbitControls
1021
[enableDamping]="true"
1122
[enablePan]="enablePan"
1223
[autoRotate]="autoRotate"
1324
[target]="target"
25+
[minDistance]="minDistance"
26+
[maxDistance]="maxDistance"
1427
(beforeRender)="$any($event).object.update()"
1528
/>
1629
`,
@@ -25,4 +38,12 @@ export class DemoOrbitControls {
2538
@Input() enablePan = true;
2639
@Input() autoRotate = false;
2740
@Input() target = [0, 0, 0];
41+
@Input() minDistance = 0;
42+
@Input() maxDistance = Infinity;
43+
44+
@Output() ready = new EventEmitter<OrbitControls>();
45+
46+
@ViewChild('orbitControls') set orbitControls({ nativeElement }: ElementRef<OrbitControls>) {
47+
this.ready.emit(nativeElement);
48+
}
2849
}
Loading
Binary file not shown.
Binary file not shown.

apps/demo/src/assets/kira.glb

11.6 MB
Binary file not shown.

libs/angular-three/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './lib/portal';
1111
export * from './lib/stores/rx-store';
1212
export * from './lib/stores/store';
1313
export * from './lib/types';
14+
export * from './lib/utils/apply-props';
1415
export { createAttachFunction } from './lib/utils/attach';
1516
export * from './lib/utils/instance';
1617
export * from './lib/utils/is';

0 commit comments

Comments
 (0)