create-threejs-game 1.0.0
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.
- package/README.md +97 -0
- package/bin/cli.js +370 -0
- package/package.json +29 -0
- package/template/.claude/skills/threejs-animation/SKILL.md +552 -0
- package/template/.claude/skills/threejs-fundamentals/SKILL.md +488 -0
- package/template/.claude/skills/threejs-geometry/SKILL.md +548 -0
- package/template/.claude/skills/threejs-interaction/SKILL.md +660 -0
- package/template/.claude/skills/threejs-lighting/SKILL.md +481 -0
- package/template/.claude/skills/threejs-loaders/SKILL.md +623 -0
- package/template/.claude/skills/threejs-materials/SKILL.md +520 -0
- package/template/.claude/skills/threejs-postprocessing/SKILL.md +602 -0
- package/template/.claude/skills/threejs-shaders/SKILL.md +642 -0
- package/template/.claude/skills/threejs-textures/SKILL.md +628 -0
- package/template/.codex/skills/threejs-animation/SKILL.md +552 -0
- package/template/.codex/skills/threejs-fundamentals/SKILL.md +488 -0
- package/template/.codex/skills/threejs-geometry/SKILL.md +548 -0
- package/template/.codex/skills/threejs-interaction/SKILL.md +660 -0
- package/template/.codex/skills/threejs-lighting/SKILL.md +481 -0
- package/template/.codex/skills/threejs-loaders/SKILL.md +623 -0
- package/template/.codex/skills/threejs-materials/SKILL.md +520 -0
- package/template/.codex/skills/threejs-postprocessing/SKILL.md +602 -0
- package/template/.codex/skills/threejs-shaders/SKILL.md +642 -0
- package/template/.codex/skills/threejs-textures/SKILL.md +628 -0
- package/template/README.md +352 -0
- package/template/docs/.gitkeep +0 -0
- package/template/plans/.gitkeep +0 -0
- package/template/prompts/01-mockup-generation.md +44 -0
- package/template/prompts/02-prd-generation.md +119 -0
- package/template/prompts/03-tdd-generation.md +127 -0
- package/template/prompts/04-execution-plan.md +89 -0
- package/template/prompts/05-implementation.md +61 -0
- package/template/public/assets/.gitkeep +0 -0
- package/template/scripts/config.example.json +12 -0
- package/template/scripts/generate-assets-json.js +149 -0
- package/template/scripts/generate-mockup.js +197 -0
- package/template/scripts/generate-plan.js +295 -0
- package/template/scripts/generate-prd.js +297 -0
- package/template/scripts/generate-tdd.js +283 -0
- package/template/scripts/pipeline.js +229 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: threejs-animation
|
|
3
|
+
description: Three.js animation - keyframe animation, skeletal animation, morph targets, animation mixing. Use when animating objects, playing GLTF animations, creating procedural motion, or blending animations.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Three.js Animation
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
```javascript
|
|
11
|
+
import * as THREE from "three";
|
|
12
|
+
|
|
13
|
+
// Simple procedural animation
|
|
14
|
+
const clock = new THREE.Clock();
|
|
15
|
+
|
|
16
|
+
function animate() {
|
|
17
|
+
const delta = clock.getDelta();
|
|
18
|
+
const elapsed = clock.getElapsedTime();
|
|
19
|
+
|
|
20
|
+
mesh.rotation.y += delta;
|
|
21
|
+
mesh.position.y = Math.sin(elapsed) * 0.5;
|
|
22
|
+
|
|
23
|
+
requestAnimationFrame(animate);
|
|
24
|
+
renderer.render(scene, camera);
|
|
25
|
+
}
|
|
26
|
+
animate();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Animation System Overview
|
|
30
|
+
|
|
31
|
+
Three.js animation system has three main components:
|
|
32
|
+
|
|
33
|
+
1. **AnimationClip** - Container for keyframe data
|
|
34
|
+
2. **AnimationMixer** - Plays animations on a root object
|
|
35
|
+
3. **AnimationAction** - Controls playback of a clip
|
|
36
|
+
|
|
37
|
+
## AnimationClip
|
|
38
|
+
|
|
39
|
+
Stores keyframe animation data.
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// Create animation clip
|
|
43
|
+
const times = [0, 1, 2]; // Keyframe times (seconds)
|
|
44
|
+
const values = [0, 1, 0]; // Values at each keyframe
|
|
45
|
+
|
|
46
|
+
const track = new THREE.NumberKeyframeTrack(
|
|
47
|
+
".position[y]", // Property path
|
|
48
|
+
times,
|
|
49
|
+
values,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const clip = new THREE.AnimationClip("bounce", 2, [track]);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### KeyframeTrack Types
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
// Number track (single value)
|
|
59
|
+
new THREE.NumberKeyframeTrack(".opacity", times, [1, 0]);
|
|
60
|
+
new THREE.NumberKeyframeTrack(".material.opacity", times, [1, 0]);
|
|
61
|
+
|
|
62
|
+
// Vector track (position, scale)
|
|
63
|
+
new THREE.VectorKeyframeTrack(".position", times, [
|
|
64
|
+
0,
|
|
65
|
+
0,
|
|
66
|
+
0, // t=0
|
|
67
|
+
1,
|
|
68
|
+
2,
|
|
69
|
+
0, // t=1
|
|
70
|
+
0,
|
|
71
|
+
0,
|
|
72
|
+
0, // t=2
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// Quaternion track (rotation)
|
|
76
|
+
const q1 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0));
|
|
77
|
+
const q2 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0));
|
|
78
|
+
new THREE.QuaternionKeyframeTrack(
|
|
79
|
+
".quaternion",
|
|
80
|
+
[0, 1],
|
|
81
|
+
[q1.x, q1.y, q1.z, q1.w, q2.x, q2.y, q2.z, q2.w],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Color track
|
|
85
|
+
new THREE.ColorKeyframeTrack(".material.color", times, [
|
|
86
|
+
1,
|
|
87
|
+
0,
|
|
88
|
+
0, // red
|
|
89
|
+
0,
|
|
90
|
+
1,
|
|
91
|
+
0, // green
|
|
92
|
+
0,
|
|
93
|
+
0,
|
|
94
|
+
1, // blue
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
// Boolean track
|
|
98
|
+
new THREE.BooleanKeyframeTrack(".visible", [0, 0.5, 1], [true, false, true]);
|
|
99
|
+
|
|
100
|
+
// String track (for morph targets)
|
|
101
|
+
new THREE.StringKeyframeTrack(
|
|
102
|
+
".morphTargetInfluences[smile]",
|
|
103
|
+
[0, 1],
|
|
104
|
+
["0", "1"],
|
|
105
|
+
);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Interpolation Modes
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const track = new THREE.VectorKeyframeTrack(".position", times, values);
|
|
112
|
+
|
|
113
|
+
// Interpolation
|
|
114
|
+
track.setInterpolation(THREE.InterpolateLinear); // Default
|
|
115
|
+
track.setInterpolation(THREE.InterpolateSmooth); // Cubic spline
|
|
116
|
+
track.setInterpolation(THREE.InterpolateDiscrete); // Step function
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## AnimationMixer
|
|
120
|
+
|
|
121
|
+
Plays animations on an object and its descendants.
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
const mixer = new THREE.AnimationMixer(model);
|
|
125
|
+
|
|
126
|
+
// Create action from clip
|
|
127
|
+
const action = mixer.clipAction(clip);
|
|
128
|
+
action.play();
|
|
129
|
+
|
|
130
|
+
// Update in animation loop
|
|
131
|
+
function animate() {
|
|
132
|
+
const delta = clock.getDelta();
|
|
133
|
+
mixer.update(delta); // Required!
|
|
134
|
+
|
|
135
|
+
requestAnimationFrame(animate);
|
|
136
|
+
renderer.render(scene, camera);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Mixer Events
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
mixer.addEventListener("finished", (e) => {
|
|
144
|
+
console.log("Animation finished:", e.action.getClip().name);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
mixer.addEventListener("loop", (e) => {
|
|
148
|
+
console.log("Animation looped:", e.action.getClip().name);
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## AnimationAction
|
|
153
|
+
|
|
154
|
+
Controls playback of an animation clip.
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
const action = mixer.clipAction(clip);
|
|
158
|
+
|
|
159
|
+
// Playback control
|
|
160
|
+
action.play();
|
|
161
|
+
action.stop();
|
|
162
|
+
action.reset();
|
|
163
|
+
action.halt(fadeOutDuration);
|
|
164
|
+
|
|
165
|
+
// Playback state
|
|
166
|
+
action.isRunning();
|
|
167
|
+
action.isScheduled();
|
|
168
|
+
|
|
169
|
+
// Time control
|
|
170
|
+
action.time = 0.5; // Current time
|
|
171
|
+
action.timeScale = 1; // Playback speed (negative = reverse)
|
|
172
|
+
action.paused = false;
|
|
173
|
+
|
|
174
|
+
// Weight (for blending)
|
|
175
|
+
action.weight = 1; // 0-1, contribution to final pose
|
|
176
|
+
action.setEffectiveWeight(1);
|
|
177
|
+
|
|
178
|
+
// Loop modes
|
|
179
|
+
action.loop = THREE.LoopRepeat; // Default: loop forever
|
|
180
|
+
action.loop = THREE.LoopOnce; // Play once and stop
|
|
181
|
+
action.loop = THREE.LoopPingPong; // Alternate forward/backward
|
|
182
|
+
action.repetitions = 3; // Number of loops (Infinity default)
|
|
183
|
+
|
|
184
|
+
// Clamping
|
|
185
|
+
action.clampWhenFinished = true; // Hold last frame when done
|
|
186
|
+
|
|
187
|
+
// Blending
|
|
188
|
+
action.blendMode = THREE.NormalAnimationBlendMode;
|
|
189
|
+
action.blendMode = THREE.AdditiveAnimationBlendMode;
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Fade In/Out
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
// Fade in
|
|
196
|
+
action.reset().fadeIn(0.5).play();
|
|
197
|
+
|
|
198
|
+
// Fade out
|
|
199
|
+
action.fadeOut(0.5);
|
|
200
|
+
|
|
201
|
+
// Crossfade between animations
|
|
202
|
+
const action1 = mixer.clipAction(clip1);
|
|
203
|
+
const action2 = mixer.clipAction(clip2);
|
|
204
|
+
|
|
205
|
+
action1.play();
|
|
206
|
+
|
|
207
|
+
// Later, crossfade to action2
|
|
208
|
+
action1.crossFadeTo(action2, 0.5, true);
|
|
209
|
+
action2.play();
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Loading GLTF Animations
|
|
213
|
+
|
|
214
|
+
Most common source of skeletal animations.
|
|
215
|
+
|
|
216
|
+
```javascript
|
|
217
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
218
|
+
|
|
219
|
+
const loader = new GLTFLoader();
|
|
220
|
+
loader.load("model.glb", (gltf) => {
|
|
221
|
+
const model = gltf.scene;
|
|
222
|
+
scene.add(model);
|
|
223
|
+
|
|
224
|
+
// Create mixer
|
|
225
|
+
const mixer = new THREE.AnimationMixer(model);
|
|
226
|
+
|
|
227
|
+
// Get all clips
|
|
228
|
+
const clips = gltf.animations;
|
|
229
|
+
console.log(
|
|
230
|
+
"Available animations:",
|
|
231
|
+
clips.map((c) => c.name),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Play first animation
|
|
235
|
+
if (clips.length > 0) {
|
|
236
|
+
const action = mixer.clipAction(clips[0]);
|
|
237
|
+
action.play();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Play specific animation by name
|
|
241
|
+
const walkClip = THREE.AnimationClip.findByName(clips, "Walk");
|
|
242
|
+
if (walkClip) {
|
|
243
|
+
mixer.clipAction(walkClip).play();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Store mixer for update loop
|
|
247
|
+
window.mixer = mixer;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Animation loop
|
|
251
|
+
function animate() {
|
|
252
|
+
const delta = clock.getDelta();
|
|
253
|
+
if (window.mixer) window.mixer.update(delta);
|
|
254
|
+
|
|
255
|
+
requestAnimationFrame(animate);
|
|
256
|
+
renderer.render(scene, camera);
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Skeletal Animation
|
|
261
|
+
|
|
262
|
+
### Skeleton and Bones
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
// Access skeleton from skinned mesh
|
|
266
|
+
const skinnedMesh = model.getObjectByProperty("type", "SkinnedMesh");
|
|
267
|
+
const skeleton = skinnedMesh.skeleton;
|
|
268
|
+
|
|
269
|
+
// Access bones
|
|
270
|
+
skeleton.bones.forEach((bone) => {
|
|
271
|
+
console.log(bone.name, bone.position, bone.rotation);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Find specific bone by name
|
|
275
|
+
const headBone = skeleton.bones.find((b) => b.name === "Head");
|
|
276
|
+
if (headBone) headBone.rotation.y = Math.PI / 4; // Turn head
|
|
277
|
+
|
|
278
|
+
// Skeleton helper
|
|
279
|
+
const helper = new THREE.SkeletonHelper(model);
|
|
280
|
+
scene.add(helper);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Programmatic Bone Animation
|
|
284
|
+
|
|
285
|
+
```javascript
|
|
286
|
+
function animate() {
|
|
287
|
+
const time = clock.getElapsedTime();
|
|
288
|
+
|
|
289
|
+
// Animate bone
|
|
290
|
+
const headBone = skeleton.bones.find((b) => b.name === "Head");
|
|
291
|
+
if (headBone) {
|
|
292
|
+
headBone.rotation.y = Math.sin(time) * 0.3;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Update mixer if also playing clips
|
|
296
|
+
mixer.update(clock.getDelta());
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Bone Attachments
|
|
301
|
+
|
|
302
|
+
```javascript
|
|
303
|
+
// Attach object to bone
|
|
304
|
+
const weapon = new THREE.Mesh(weaponGeometry, weaponMaterial);
|
|
305
|
+
const handBone = skeleton.bones.find((b) => b.name === "RightHand");
|
|
306
|
+
if (handBone) handBone.add(weapon);
|
|
307
|
+
|
|
308
|
+
// Offset attachment
|
|
309
|
+
weapon.position.set(0, 0, 0.5);
|
|
310
|
+
weapon.rotation.set(0, Math.PI / 2, 0);
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Morph Targets
|
|
314
|
+
|
|
315
|
+
Blend between different mesh shapes.
|
|
316
|
+
|
|
317
|
+
```javascript
|
|
318
|
+
// Morph targets are stored in geometry
|
|
319
|
+
const geometry = mesh.geometry;
|
|
320
|
+
console.log("Morph attributes:", Object.keys(geometry.morphAttributes));
|
|
321
|
+
|
|
322
|
+
// Access morph target influences
|
|
323
|
+
mesh.morphTargetInfluences; // Array of weights
|
|
324
|
+
mesh.morphTargetDictionary; // Name -> index mapping
|
|
325
|
+
|
|
326
|
+
// Set morph target by index
|
|
327
|
+
mesh.morphTargetInfluences[0] = 0.5;
|
|
328
|
+
|
|
329
|
+
// Set by name
|
|
330
|
+
const smileIndex = mesh.morphTargetDictionary["smile"];
|
|
331
|
+
mesh.morphTargetInfluences[smileIndex] = 1;
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Animating Morph Targets
|
|
335
|
+
|
|
336
|
+
```javascript
|
|
337
|
+
// Procedural
|
|
338
|
+
function animate() {
|
|
339
|
+
const t = clock.getElapsedTime();
|
|
340
|
+
mesh.morphTargetInfluences[0] = (Math.sin(t) + 1) / 2;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// With keyframe animation
|
|
344
|
+
const track = new THREE.NumberKeyframeTrack(
|
|
345
|
+
".morphTargetInfluences[smile]",
|
|
346
|
+
[0, 0.5, 1],
|
|
347
|
+
[0, 1, 0],
|
|
348
|
+
);
|
|
349
|
+
const clip = new THREE.AnimationClip("smile", 1, [track]);
|
|
350
|
+
mixer.clipAction(clip).play();
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Animation Blending
|
|
354
|
+
|
|
355
|
+
Mix multiple animations together.
|
|
356
|
+
|
|
357
|
+
```javascript
|
|
358
|
+
// Setup actions
|
|
359
|
+
const idleAction = mixer.clipAction(idleClip);
|
|
360
|
+
const walkAction = mixer.clipAction(walkClip);
|
|
361
|
+
const runAction = mixer.clipAction(runClip);
|
|
362
|
+
|
|
363
|
+
// Play all with different weights
|
|
364
|
+
idleAction.play();
|
|
365
|
+
walkAction.play();
|
|
366
|
+
runAction.play();
|
|
367
|
+
|
|
368
|
+
// Set initial weights
|
|
369
|
+
idleAction.setEffectiveWeight(1);
|
|
370
|
+
walkAction.setEffectiveWeight(0);
|
|
371
|
+
runAction.setEffectiveWeight(0);
|
|
372
|
+
|
|
373
|
+
// Blend based on speed
|
|
374
|
+
function updateAnimations(speed) {
|
|
375
|
+
if (speed < 0.1) {
|
|
376
|
+
idleAction.setEffectiveWeight(1);
|
|
377
|
+
walkAction.setEffectiveWeight(0);
|
|
378
|
+
runAction.setEffectiveWeight(0);
|
|
379
|
+
} else if (speed < 5) {
|
|
380
|
+
const t = speed / 5;
|
|
381
|
+
idleAction.setEffectiveWeight(1 - t);
|
|
382
|
+
walkAction.setEffectiveWeight(t);
|
|
383
|
+
runAction.setEffectiveWeight(0);
|
|
384
|
+
} else {
|
|
385
|
+
const t = Math.min((speed - 5) / 5, 1);
|
|
386
|
+
idleAction.setEffectiveWeight(0);
|
|
387
|
+
walkAction.setEffectiveWeight(1 - t);
|
|
388
|
+
runAction.setEffectiveWeight(t);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Additive Blending
|
|
394
|
+
|
|
395
|
+
```javascript
|
|
396
|
+
// Base pose
|
|
397
|
+
const baseAction = mixer.clipAction(baseClip);
|
|
398
|
+
baseAction.play();
|
|
399
|
+
|
|
400
|
+
// Additive layer (e.g., breathing)
|
|
401
|
+
const additiveAction = mixer.clipAction(additiveClip);
|
|
402
|
+
additiveAction.blendMode = THREE.AdditiveAnimationBlendMode;
|
|
403
|
+
additiveAction.play();
|
|
404
|
+
|
|
405
|
+
// Convert clip to additive
|
|
406
|
+
THREE.AnimationUtils.makeClipAdditive(additiveClip);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Animation Utilities
|
|
410
|
+
|
|
411
|
+
```javascript
|
|
412
|
+
import * as THREE from "three";
|
|
413
|
+
|
|
414
|
+
// Find clip by name
|
|
415
|
+
const clip = THREE.AnimationClip.findByName(clips, "Walk");
|
|
416
|
+
|
|
417
|
+
// Create subclip
|
|
418
|
+
const subclip = THREE.AnimationUtils.subclip(clip, "subclip", 0, 30, 30);
|
|
419
|
+
|
|
420
|
+
// Convert to additive
|
|
421
|
+
THREE.AnimationUtils.makeClipAdditive(clip);
|
|
422
|
+
THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip);
|
|
423
|
+
|
|
424
|
+
// Clone clip
|
|
425
|
+
const clone = clip.clone();
|
|
426
|
+
|
|
427
|
+
// Get clip duration
|
|
428
|
+
clip.duration;
|
|
429
|
+
|
|
430
|
+
// Optimize clip (remove redundant keyframes)
|
|
431
|
+
clip.optimize();
|
|
432
|
+
|
|
433
|
+
// Reset clip to first frame
|
|
434
|
+
clip.resetDuration();
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Procedural Animation Patterns
|
|
438
|
+
|
|
439
|
+
### Smooth Damping
|
|
440
|
+
|
|
441
|
+
```javascript
|
|
442
|
+
// Smooth follow/lerp
|
|
443
|
+
const target = new THREE.Vector3();
|
|
444
|
+
const current = new THREE.Vector3();
|
|
445
|
+
const velocity = new THREE.Vector3();
|
|
446
|
+
|
|
447
|
+
function smoothDamp(current, target, velocity, smoothTime, deltaTime) {
|
|
448
|
+
const omega = 2 / smoothTime;
|
|
449
|
+
const x = omega * deltaTime;
|
|
450
|
+
const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);
|
|
451
|
+
const change = current.clone().sub(target);
|
|
452
|
+
const temp = velocity
|
|
453
|
+
.clone()
|
|
454
|
+
.add(change.clone().multiplyScalar(omega))
|
|
455
|
+
.multiplyScalar(deltaTime);
|
|
456
|
+
velocity.sub(temp.clone().multiplyScalar(omega)).multiplyScalar(exp);
|
|
457
|
+
return target.clone().add(change.add(temp).multiplyScalar(exp));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function animate() {
|
|
461
|
+
current.copy(smoothDamp(current, target, velocity, 0.3, delta));
|
|
462
|
+
mesh.position.copy(current);
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Spring Physics
|
|
467
|
+
|
|
468
|
+
```javascript
|
|
469
|
+
class Spring {
|
|
470
|
+
constructor(stiffness = 100, damping = 10) {
|
|
471
|
+
this.stiffness = stiffness;
|
|
472
|
+
this.damping = damping;
|
|
473
|
+
this.position = 0;
|
|
474
|
+
this.velocity = 0;
|
|
475
|
+
this.target = 0;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
update(dt) {
|
|
479
|
+
const force = -this.stiffness * (this.position - this.target);
|
|
480
|
+
const dampingForce = -this.damping * this.velocity;
|
|
481
|
+
this.velocity += (force + dampingForce) * dt;
|
|
482
|
+
this.position += this.velocity * dt;
|
|
483
|
+
return this.position;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const spring = new Spring(100, 10);
|
|
488
|
+
spring.target = 1;
|
|
489
|
+
|
|
490
|
+
function animate() {
|
|
491
|
+
mesh.position.y = spring.update(delta);
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Oscillation
|
|
496
|
+
|
|
497
|
+
```javascript
|
|
498
|
+
function animate() {
|
|
499
|
+
const t = clock.getElapsedTime();
|
|
500
|
+
|
|
501
|
+
// Sine wave
|
|
502
|
+
mesh.position.y = Math.sin(t * 2) * 0.5;
|
|
503
|
+
|
|
504
|
+
// Bouncing
|
|
505
|
+
mesh.position.y = Math.abs(Math.sin(t * 3)) * 2;
|
|
506
|
+
|
|
507
|
+
// Circular motion
|
|
508
|
+
mesh.position.x = Math.cos(t) * 2;
|
|
509
|
+
mesh.position.z = Math.sin(t) * 2;
|
|
510
|
+
|
|
511
|
+
// Figure 8
|
|
512
|
+
mesh.position.x = Math.sin(t) * 2;
|
|
513
|
+
mesh.position.z = Math.sin(t * 2) * 1;
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
## Performance Tips
|
|
518
|
+
|
|
519
|
+
1. **Share clips**: Same AnimationClip can be used on multiple mixers
|
|
520
|
+
2. **Optimize clips**: Call `clip.optimize()` to remove redundant keyframes
|
|
521
|
+
3. **Disable when off-screen**: Stop mixer updates for invisible objects
|
|
522
|
+
4. **Use LOD for animations**: Simpler rigs for distant characters
|
|
523
|
+
5. **Limit active mixers**: Each mixer.update() has a cost
|
|
524
|
+
|
|
525
|
+
```javascript
|
|
526
|
+
// Pause animation when not visible
|
|
527
|
+
mesh.onBeforeRender = () => {
|
|
528
|
+
action.paused = false;
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
mesh.onAfterRender = () => {
|
|
532
|
+
// Check if will be visible next frame
|
|
533
|
+
if (!isInFrustum(mesh)) {
|
|
534
|
+
action.paused = true;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// Cache clips
|
|
539
|
+
const clipCache = new Map();
|
|
540
|
+
function getClip(name) {
|
|
541
|
+
if (!clipCache.has(name)) {
|
|
542
|
+
clipCache.set(name, loadClip(name));
|
|
543
|
+
}
|
|
544
|
+
return clipCache.get(name);
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
## See Also
|
|
549
|
+
|
|
550
|
+
- `threejs-loaders` - Loading animated GLTF models
|
|
551
|
+
- `threejs-fundamentals` - Clock and animation loop
|
|
552
|
+
- `threejs-shaders` - Vertex animation in shaders
|