К основному содержанию
скилл Claude Code CX Codex Cursor GitHub Copilot

threejs-animation — скилл для Claude Code: keyframe и skeletal

Скилл для работы с keyframe-анимацией, костями и морф-целями в Three.js. Помогает воспроизводить glTF-анимации, управлять костями скелета, миксировать и кроссфейдить клипы на WebGL и WebGPU.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add CloudAI-X/threejs-skills --skill threejs-animation -a claude-code
Открыть на GitHub

Что делает threejs-animation

threejs-animation нацелен на Three.js r186 и даёт вам инструменты для трёх типов анимации: keyframe (по ключевым кадрам), skeletal (костная система) и morph-target (морфирование геометрии). Внутри — AnimationClip для хранения данных ключевых кадров, AnimationMixer для воспроизведения на объекте и его потомках, AnimationAction для управления плейбеком: начало, остановка, перемотка, обратное проигрывание, изменение скорости.

Траки поддерживают разные типы данных: Number, Vector, Quaternion, Color, Boolean. Можно выбрать интерполяцию — линейная, плавная (cubic), дискретная (step) или Безье с касательными. Для переходов между анимациями есть fade и crossFade с опциональным синхронизацией скорости воспроизведения.

Примеры работают с WebGLRenderer; для WebGPU нужен WebGPURenderer. Система запускается на CPU и работает одинаково под обоими рендерерами, что значит — одна логика для всех платформ.

Какие задачи решает

  • воспроизвести keyframe-анимацию из glTF-модели
  • управлять положением и поворотом костей в скелета
  • морфировать геометрию между сохранёнными формами
  • плавно переходить между двумя анимациями
  • сохранить итоговую позу после завершения анимации

Примеры запросов агенту

  • Помоги мне создать AnimationClip с поворотом куба и воспроизвести его через mixer
  • Напиши код для crossfade между idle и walk анимациями
  • Создай morph-target трек для анимации мимики персонажа

Как установить threejs-animation

1Через Skills CLI (Vercel)

npx skills add CloudAI-X/threejs-skills --skill threejs-animation -a claude-code

2Вручную: положить файлы

~/.claude/skills/threejs-animation/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/threejs-animation/

Из того же набора2

Весь набор →

Похожие

react-modernization Seth Hobson Скилл обновляет React-код, переносит классовые компоненты на хуки и помогает внедрять возможности React 18. Подойдёт для поэтапной модернизации старого приложения. Claude Code CX Codex Cursor 12 625установок analytics-events metabase Скилл добавляет события продуктовой аналитики Snowplow во фронтенд Metabase. Нужен, когда в код встраивают трекинг кликов, создания сущностей и других действий пользователя. Claude Code CX Codex Cursor +1 2 000установок wcag-audit-patterns Seth Hobson Проверяет веб-интерфейс по WCAG 2.2, сочетая автотесты и ручную проверку. Нужен при поиске нарушений и внедрении доступных паттернов. Claude Code CX Codex Cursor 12 486установок seo-images AgriciDaniel Проверяет alt-текст, размер, формат (WebP/AVIF), адаптивность, ленивую загрузку и метаданные изображений на странице. Находит проблемы с CLS и показывает, сколько килобайт можно сэкономить. Claude Code CX Codex Cursor +1 6 700установок cmux-custom-sidebar manaflow-ai Скилл для написания кастомной боковой панели cmux на SwiftUI-подобном языке. Панель привязывается к рабочим пространствам, вкладкам, PR и часам, горячо перезагружается при сохранении и выполняет команды cmux по нажатию. Claude Code CX Codex Cursor +1 4 800установок agentation-self-driving benjitaylor Запускает автономный визуальный аудит интерфейса в открытом окне браузера и расставляет карточки замечаний через тулбар Agentation. Claude Code CX Codex Cursor +1 7 000установок frontend-design официальный anthropics Скилл задаёт осмысленное визуальное направление для новых интерфейсов и редизайна. Подойдёт, когда стандартный шаблонный UI не устраивает. Claude Code Claude.ai 1 025 790установок design-taste-frontend Leonxlnx Скилл задаёт арт-дирекцию для публичных страниц и рабочих экранов, а затем проверяет результат в браузере по скриншотам. Нужен, когда интерфейс выглядит шаблонным или недоработанным. Claude Code CX Codex Cursor +1 673 100установок

Вопросы о threejs-animation

Как установить threejs-animation в Claude Code?

Выполни npx skills add CloudAI-X/threejs-skills --skill threejs-animation -a claude-code, затем посмотри инструкции в ~/.claude/skills/threejs-animation/SKILL.md.

Что делает threejs-animation?

Даёт API для создания и воспроизведения keyframe-анимаций, управления костями скелета, морфирования геометрии и плавных переходов между клипами в Three.js.

Работает ли скилл с WebGPU?

Да, примеры используют WebGLRenderer по умолчанию, но система поддерживает WebGPURenderer с тем же кодом логики.

Исходный текст

SKILL.md · MIT
---
name: threejs-animation
description: "Keyframe, skeletal and morph-target animation, procedural motion and clip blending, on WebGL or with WebGPU and TSL. Use when animating objects, playing glTF animations, posing bones, driving morph targets, or mixing and crossfading clips."
---

# Three.js Animation

Targets three.js r186. Examples use WebGLRenderer; the WebGPU and TSL section uses WebGPURenderer.

## Quick Start

```javascript
import * as THREE from "three";

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0.5, 3);

const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshNormalMaterial());
scene.add(mesh);

const timer = new THREE.Timer();
timer.connect(document); // also keeps deltas small after the tab was hidden

function animate() {
  timer.update(); // first thing each frame: fixes getDelta()/getElapsed() for this frame
  const delta = timer.getDelta();
  const elapsed = timer.getElapsed();

  mesh.rotation.y += delta; // frame-rate-independent spin
  mesh.position.y = Math.sin(elapsed * 2) * 0.25; // bob on absolute time

  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);
```

## How the System Fits Together

- **AnimationClip**: keyframe data — tracks of times and values, no playback state.
- **AnimationMixer**: plays clips on one root object and all its descendants.
- **AnimationAction**: playback control for one clip on one mixer.

Each frame, `mixer.update(delta)` samples the active clips and writes the values into ordinary object properties. The system runs on the CPU and never touches the renderer, so it drives models the same way under either renderer.

## AnimationClip and Keyframe Tracks

```javascript
// One track per animated property. values.length must be times.length * valueSize.
const times = [0, 1, 2]; // keyframe times in seconds
const values = [0, 1, 0]; // one scalar per keyframe for a Number track

const track = new THREE.NumberKeyframeTrack(".position[y]", times, values); // the root's position.y

const clip = new THREE.AnimationClip("bounce", 2, [track]); // duration in seconds
// pass -1 as the duration to derive it from the tracks
```

Track names follow `nodeName.objectName[objectIndex].propertyName[propertyIndex]`; all parts except the property are optional. `.position` binds the root's position, `.material.opacity` a material property, `.position[y]` one component, `.morphTargetInfluences[smile]` one morph target, `Hips.position` a named descendant — that is how bones are addressed in a skinned model. Object names may be `material`, `materials`, `bones` or `map`.

```javascript
// Number track (value size 1); create the material with transparent: true, or opacity is ignored
new THREE.NumberKeyframeTrack(".material.opacity", [0, 1], [1, 0]);

// Vector track (value size 3: x, y, z per keyframe)
new THREE.VectorKeyframeTrack(".position", [0, 1, 2], [0, 0, 0, 1, 2, 0, 0, 0, 0]);

// Rotation track (value size 4): quaternion x, y, z, w per keyframe, not Euler angles
const q2 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
new THREE.QuaternionKeyframeTrack(
  ".quaternion",
  [0, 1],
  [0, 0, 0, 1, q2.x, q2.y, q2.z, q2.w], // identity, then q2
);

// Color track (value size 3)
new THREE.ColorKeyframeTrack(".material.color", [0, 1], [1, 0, 0, 0, 1, 0]);

// Boolean track (always discrete)
new THREE.BooleanKeyframeTrack(".visible", [0, 1], [true, false]);
```

### Interpolation

```javascript
const track = new THREE.VectorKeyframeTrack(
  ".position",
  [0, 1, 2],
  [0, 0, 0, 1, 2, 0, 0, 0, 0],
);

track.setInterpolation(THREE.InterpolateLinear); // default
track.setInterpolation(THREE.InterpolateSmooth); // cubic through the keyframes
track.setInterpolation(THREE.InterpolateDiscrete); // step, no in-between values

// Bezier takes absolute (time, value) control points, two numbers per keyframe per
// component: a stride-1 two-keyframe track needs four entries per array
const morphTrack = new THREE.NumberKeyframeTrack(
  ".morphTargetInfluences[smile]",
  [0, 1],
  [0, 1],
);
morphTrack.setInterpolation(THREE.InterpolateBezier);
morphTrack.settings = { inTangents: [0, 0, 1, 1], outTangents: [0, 1, 1, 0] };
```

## AnimationMixer

```javascript
const track = new THREE.NumberKeyframeTrack(".rotation[y]", [0, 2], [0, Math.PI * 2]);
const clip = new THREE.AnimationClip("spin", 2, [track]);

// One mixer per animated root; it reaches the root and all descendants
const mixer = new THREE.AnimationMixer(mesh);
const action = mixer.clipAction(clip); // cached: calling it again for this clip and root returns the same action
action.play();

const timer = new THREE.Timer();
timer.connect(document);

function animate() {
  timer.update();
  mixer.update(timer.getDelta()); // required: advances every active action
  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);

// Events fire on the mixer while it updates; "finished" needs LoopOnce or finite repetitions
mixer.addEventListener("finished", (e) => console.log("finished:", e.action.getClip().name));
mixer.addEventListener("loop", (e) => console.log("looped:", e.action.getClip().name));
```

## AnimationAction

```javascript
const mixer = new THREE.AnimationMixer(mesh);
const clip = new THREE.AnimationClip("spin", 2, [
  new THREE.NumberKeyframeTrack(".rotation[y]", [0, 2], [0, Math.PI * 2]),
]);
const action = mixer.clipAction(clip);

action.play(); // schedule and start
action.reset(); // rewind to time 0 and cancel fades
action.stop(); // unschedule; properties no other scheduled action animates return to their original values
action.halt(0.3); // decelerate to a stop within 0.3 s

action.time = 0.5; // scrub within the clip
action.timeScale = -1; // negative plays backward
action.paused = true; // freeze in place

action.setLoop(THREE.LoopOnce, 1); // stop (or hold) after one pass
action.setLoop(THREE.LoopRepeat, Infinity); // the default
action.setLoop(THREE.LoopPingPong, 4); // alternate direction each pass
action.clampWhenFinished = true; // hold the final pose instead of resetting
```

### Fades and Crossfades

```javascript
const mixer = new THREE.AnimationMixer(mesh);
const idleClip = new THREE.AnimationClip("idle", 2, [
  new THREE.NumberKeyframeTrack(".position[y]", [0, 1, 2], [0, 0.05, 0]),
]);
const walkClip = new THREE.AnimationClip("walk", 1, [
  new THREE.NumberKeyframeTrack(".rotation[y]", [0, 1], [0, Math.PI]),
]);

const idle = mixer.clipAction(idleClip).play();
const walk = mixer.clipAction(walkClip).play();

// Hand over from idle to walk over 0.4 s; true as the third argument also warps
// both time scales so their cycle lengths match during the fade
idle.crossFadeTo(walk, 0.4, false);

// The same hand-over from the one-sided calls (use one form, not both)
walk.reset().fadeIn(0.4).play(); // ramp weight up from 0
idle.fadeOut(0.4); // ramp weight down to 0
```

## Playing glTF Animations

```javascript
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils.js";

const gltf = await new GLTFLoader().loadAsync("model.glb");
const model = gltf.scene;
scene.add(model);

// GLTFLoader's default material (MeshStandardMaterial) is lit: without lights the model renders black
const sun = new THREE.DirectionalLight(0xffffff, 2);
sun.position.set(3, 5, 2);
scene.add(sun, new THREE.HemisphereLight(0xffffff, 0x444444, 1.5));

// gltf.animations is an array of parsed AnimationClips
const clips = gltf.animations;

const mixer = new THREE.AnimationMixer(model);
const walkClip = THREE.AnimationClip.findByName(clips, "Walk");
if (walkClip) mixer.clipAction(walkClip).play();

// A second character: clone the skinned model, give the copy its own mixer
const model2 = SkeletonUtils.clone(model);
model2.position.x = 1.5; // beside the first one
scene.add(model2);
const mixer2 = new THREE.AnimationMixer(model2);
if (walkClip) mixer2.clipAction(walkClip).play(); // same clip, two mixers

const timer = new THREE.Timer();
timer.connect(document);

function animate() {
  timer.update();
  const delta = timer.getDelta();
  mixer.update(delta);
  mixer2.update(delta);
  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);
```

## Skeletal Animation

```javascript
// mesh is the loaded model root; find its first SkinnedMesh
const skinnedMesh = mesh.getObjectByProperty("type", "SkinnedMesh");
const skeleton = skinnedMesh.skeleton;
console.log(skeleton.bones.map((b) => b.name));

// Grab a bone to pose or attach to
const handBone = skeleton.bones.find((b) => b.name === "RightHand");

// Visualize the rig
const helper = new THREE.SkeletonHelper(mesh);
scene.add(helper);
```

Procedural bone control runs **after** `mixer.update()`, or a clip that animates the same bone overwrites it:

```javascript
const skinnedMesh = mesh.getObjectByProperty("type", "SkinnedMesh");
const headBone = skinnedMesh.skeleton.bones.find((b) => b.name === "Head");
const mixer = new THREE.AnimationMixer(mesh); // play the model's clips on it (see Playing glTF Animations)

const timer = new THREE.Timer();
timer.connect(document);

function animate() {
  timer.update();
  mixer.update(timer.getDelta()); // clips write their bone pose here

  // Override afterwards: update() overwrites every property the playing clips animate
  if (headBone) headBone.rotation.y = Math.sin(timer.getElapsed()) * 0.3;
  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);
```

Bones are Object3Ds, so attaching means parenting:

```javascript
const skinnedMesh = mesh.getObjectByProperty("type", "SkinnedMesh");
const handBone = skinnedMesh.skeleton.bones.find((b) => b.name === "RightHand");

const weapon = new THREE.Mesh(
  new THREE.BoxGeometry(0.08, 0.08, 0.5),
  new THREE.MeshNormalMaterial(),
);
if (handBone) {
  handBone.add(weapon); // weapon now follows the hand's bone transform
  weapon.position.set(0, 0, 0.25); // offset within the bone's space
  weapon.rotation.set(0, Math.PI / 2, 0);
}
```

## Morph Targets

```javascript
// mesh carries morph targets (loaded, or added by hand below)
const names = Object.keys(mesh.morphTargetDictionary); // name -> index
console.log(names, mesh.morphTargetInfluences); // one weight per target

// Blend a target by index or by name; 0 = base shape, 1 = full target
mesh.morphTargetInfluences[0] = 0.5;
const smileIndex = mesh.morphTargetDictionary["Mouth_Smile"];
if (smileIndex !== undefined) mesh.morphTargetInfluences[smileIndex] = 1;
```

Adding a morph target by hand:

```javascript
const geometry = mesh.geometry;

// A morph target is a full-size position (or normal) attribute beside the base one
const raised = geometry.attributes.position.array.slice(); // same vertex count
for (let i = 1; i < raised.length; i += 3) {
  raised[i] += 0.5; // y is every third component: lift every vertex
}

const target = new THREE.Float32BufferAttribute(raised, 3);
target.name = "raised"; // named targets stay addressable from tracks

geometry.morphAttributes.position = [target];
mesh.updateMorphTargets(); // rebuilds morphTargetInfluences and the dictionary
```

Driving morph targets:

```javascript
// Procedural, inside the render loop after timer.update():
// mesh.morphTargetInfluences[0] = (Math.sin(timer.getElapsed() * 2) + 1) / 2;

// Keyframed: a Number track drives one target by name — never a String track
const track = new THREE.NumberKeyframeTrack(
  ".morphTargetInfluences[raised]",
  [0, 0.5, 1],
  [0, 1, 0],
);
const clip = new THREE.AnimationClip("pulse", 1, [track]);
const mixer = new THREE.AnimationMixer(mesh);
mixer.clipAction(clip).play();
```

## Blending Clips by Weight

```javascript
const mixer = new THREE.AnimationMixer(mesh);
const idleClip = new THREE.AnimationClip("idle", 2, [
  new THREE.NumberKeyframeTrack(".position[y]", [0, 1, 2], [0, 0.05, 0]),
]);
const runClip = new THREE.AnimationClip("run", 1, [
  new THREE.NumberKeyframeTrack(".position[y]", [0, 0.5, 1], [0, 0.3, 0]),
]);

const idle = mixer.clipAction(idleClip).play();
const run = mixer.clipAction(runClip).play();

// Both sample every frame; the weights mix their poses
function setBlend(speed) {
  const t = Math.min(speed / 5, 1); // 0 = idle, 5 and up = full run
  idle.setEffectiveWeight(1 - t);
  run.setEffectiveWeight(t);
}

setBlend(2.5); // half idle, half run
```

### Additive Animation

```javascript
const mixer = new THREE.AnimationMixer(mesh);
const walkClip = new THREE.AnimationClip("walk", 1, [
  new THREE.NumberKeyframeTrack(".rotation[y]", [0, 1], [0, Math.PI]),
]);
const breathClip = new THREE.AnimationClip("breath", 2, [
  new THREE.NumberKeyframeTrack(".position[y]", [0, 1, 2], [0, 0.02, 0]),
]);

// Re-bases every track on its frame 0 and marks the clip additive
THREE.AnimationUtils.makeClipAdditive(breathClip);

mixer.clipAction(walkClip).play(); // normal base clip
mixer.clipAction(breathClip).play(); // adds its offsets on top of the base pose
```

## Clip Utilities

```javascript
const clip = new THREE.AnimationClip("walk", 2, [
  new THREE.NumberKeyframeTrack(".rotation[y]", [0, 1, 2], [0, Math.PI, Math.PI * 2]),
]);

const walk = THREE.AnimationClip.findByName([clip], "walk"); // name lookup
// Keeps the existing keyframes in frames [0, 31) at 30 fps (times 0 and 1); it never resamples
const cut = THREE.AnimationUtils.subclip(clip, "half", 0, 31, 30);
const copy = clip.clone(); // independent deep copy

clip.optimize(); // drops duplicate-time keys, and keys equal to both neighbours on any non-smooth track (Bezier tangents are not compacted)
clip.resetDuration(); // recomputes duration from the longest track — it does not seek
console.log(clip.validate()); // false for a fractional values-to-times ratio, an empty track, unsorted times or NaN
```

## Procedural Motion

```javascript
const timer = new THREE.Timer();
timer.connect(document);

// Exponential approach; a higher lambda snaps faster. Frame-rate independent.
// The same shape drives positions, rotations or any scalar each frame.
let scale = 0;

function animate() {
  timer.update();
  scale = THREE.MathUtils.damp(scale, 1, 4, timer.getDelta());
  mesh.scale.setScalar(0.5 + scale * 0.5);
  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);
```

## Performance

- The same AnimationClip can feed any number of mixers — share one clip set across characters instead of copying it.
- Call `mixer.update()` only for characters that are visible or about to be; an un-updated mixer simply leaves the last pose in place.
- A SkinnedMesh computes its bounding sphere once, the first time culling or raycasting needs it, so a clip that moves it far from that pose can get it culled at the screen edge: after `mixer.update()` call `mixer.getRoot().updateMatrixWorld()` then `skinnedMesh.computeBoundingSphere()` (a CPU pass over every vertex) or set `skinnedMesh.frustumCulled = false`.

```javascript
// Free a mixer and its cached bindings when its model leaves for good
const mixer = new THREE.AnimationMixer(mesh);
mixer.stopAllAction();
mixer.uncacheRoot(mesh); // releases the property bindings created for the model
```

## WebGPU and TSL

Everything above — mixer, actions, bone transforms, morph weights — is computed on the CPU and drives a model under WebGPURenderer unchanged. TSL adds animation on the GPU: `time` (elapsed seconds) and `deltaTime` (seconds since the previous frame) are float uniforms the renderer refreshes every frame, and the oscillators `oscSine`, `oscSquare`, `oscTriangle` and `oscSawtooth` return 0-to-1 waves with a one-second period from a time node, defaulting to `time` (`oscSine(time.mul(0.5))` takes two seconds). Per-vertex motion needs a node material and a subdivided mesh.

```javascript
import * as THREE from "three/webgpu";
import { positionLocal, time, vec3 } from "three/tsl";

const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init(); // before any other use of the renderer
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111418); // the WebGPU canvas is transparent by default

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 2, 3);
camera.lookAt(0, 0, 0);

// Ripple a subdivided plane along its normal (local z) on the GPU; time drives it, no per-frame JS
const material = new THREE.MeshBasicNodeMaterial({ wireframe: true });
material.positionNode = positionLocal.add(
  vec3(0, 0, positionLocal.x.mul(2).sin().mul(time.mul(2).cos()).mul(0.2)),
);
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(3, 3, 32, 32), material);
mesh.rotation.x = -Math.PI / 2;
scene.add(mesh);

function animate() {
  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);
```

On a skinned mesh, build displacement on `positionLocal`: skinning, morph targets, batching and instancing are applied to it before `positionNode` runs, so the mixer-driven pose survives. `positionGeometry` is the raw rest-pose attribute — never the base for displacement on an animated mesh.

```javascript
import * as THREE from "three/webgpu";
import { color, mix, oscSine, positionLocal, vec3 } from "three/tsl";

// The pulse rides on top of the skinned pose: positionLocal already carries it
const material = new THREE.MeshBasicNodeMaterial();
material.positionNode = positionLocal.add(vec3(0, oscSine().mul(0.05), 0));

// Oscillators animate any node slot, not just positions
material.colorNode = mix(color(0x202030), color(0xffd24a), oscSine());

const skinnedMesh = scene.getObjectByProperty("type", "SkinnedMesh"); // a loaded animated model
if (skinnedMesh) skinnedMesh.material = material;
```

## Common Mistakes

- `const clock = new THREE.Clock();` → `const timer = new THREE.Timer(); timer.connect(document);`: the Clock class is deprecated in r183 and Timer is its replacement.
- `const elapsed = clock.getElapsedTime(); const delta = clock.getDelta();` → `timer.update(); const delta = timer.getDelta(); const elapsed = timer.getElapsed();`: getElapsedTime() calls getDelta() internally, so the following delta read is about zero and the mixer stalls.
- `requestAnimationFrame(animate);` → `renderer.setAnimationLoop(animate);`: both renderers' docs advise it over a manual loop, and it hands frames to the WebXR session when one starts.
- `import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";` → `import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";`: the examples/jsm specifier does not resolve under the standard import map.
- `new THREE.NumberKeyframeTrack(".material.opacity", [0, 1, 2], [1, 0])` → `new THREE.NumberKeyframeTrack(".material.opacity", [0, 1], [1, 0])`: values must hold times.length × valueSize entries; with a fractional ratio play() never returns, and any other mismatch samples wrong values.
- `new THREE.StringKeyframeTrack(".morphTargetInfluences[smile]", [0, 1], ["0", "1"])` → `new THREE.NumberKeyframeTrack(".morphTargetInfluences[smile]", [0, 1], [0, 1])`: morph influences are numbers, and a string track writes "0"/"1" strings into the array.
- `headBone.rotation.y = Math.sin(t) * 0.3; mixer.update(delta);` → `mixer.update(delta); headBone.rotation.y = Math.sin(t) * 0.3;`: update() writes the clip's pose over any value set before it.
- `clip.resetDuration()` → `action.reset()`: resetDuration() only recomputes the clip's duration from its longest track, while reset() rewinds the action to time 0 and re-enables it after a one-shot finished.
- `material.positionNode = positionGeometry.add(offset)` → `material.positionNode = positionLocal.add(offset)`: positionGeometry is the raw attribute, so building on it drops skinning, morph targets, batching and instancing.
- `const uTime = uniform(0); uTime.value = timer.getElapsed(); const wave = uTime.mul(2).sin();` → `const wave = time.mul(2).sin();`: the renderer already advances the TSL time node every frame, so a Timer-fed uniform is redundant per-frame JavaScript.
- `const model2 = model.clone();` → `const model2 = SkeletonUtils.clone(model);`: cloning a skinned model shares its skeleton between the copies; each copy needs its own.
- `import { clone } from "three/addons/utils/SkeletonUtils.js";` → `import * as SkeletonUtils from "three/addons/utils/SkeletonUtils.js";`: the namespace keeps the call as SkeletonUtils.clone(model), which cannot be mistaken for the skeleton-sharing model.clone().
- `new Promise((resolve, reject) => loader.load(url, resolve, undefined, reject))` → `await loader.loadAsync(url)`: loaders already expose the promise form, and loadAsync takes the progress callback as its second argument.

## See Also

- `threejs-fundamentals` — render loop, Timer, renderer and scene setup
- `threejs-loaders` — loading glTF models and their animation clips
- `threejs-shaders` — GPU vertex displacement with TSL or ShaderMaterial

© CloudAI-X · MIT · CloudAI-X/threejs-skills

Смотрите также

Спонсоры проекта

Компании, которые помогают держать аналитику открытой