Communitygithub.com

aelyraAI/threejs-animation

Animação Three.js - animação por keyframe, animação esquelética, morph targets, mixagem de animações. Use ao animar objetos, reproduzir animações GLTF, criar movimento procedural ou misturar animações.

O que é threejs-animation?

Esta skill fornece um guia abrangente sobre o sistema de animação do Three.js, cobrindo desde conceitos básicos até técnicas avançadas de animação procedural. Ela explica os três componentes principais do sistema (AnimationClip, AnimationMixer e AnimationAction) e demonstra como criar e controlar animações por keyframe para diferentes tipos de propriedades, como posição, rotação, cor e morph targets. A skill também aborda a importação e reprodução de animações esqueléticas de arquivos GLTF, que é o caso de uso mais comum em jogos e aplicações 3D. Para desenvolvedores que precisam de movimentos mais naturais, são apresentados padrões de animação procedural como damping suave, física de molas e oscilações, além de técnicas de blend entre múltiplas animações (incluindo blend aditivo) e dicas de performance para otimização em tempo real.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/aelyraAI/aelyra-skills/tree/HEAD/threejs-animation

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Animação Three.js

Início Rápido

import * as THREE from "three";

// Animação procedural simples
const clock = new THREE.Clock();

function animate() {
  const delta = clock.getDelta();
  const elapsed = clock.getElapsedTime();

  mesh.rotation.y += delta;
  mesh.position.y = Math.sin(elapsed) * 0.5;

  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();

Visão Geral do Sistema de Animação

O sistema de animação do Three.js possui três componentes principais:

  1. AnimationClip - Contêiner para dados de keyframe
  2. AnimationMixer - Reproduz animações em um objeto raiz
  3. AnimationAction - Controla a reprodução de um clip

AnimationClip

Armazena dados de animação por keyframe.

// Criar clip de animação
const times = [0, 1, 2]; // Tempos dos keyframes (segundos)
const values = [0, 1, 0]; // Valores em cada keyframe

const track = new THREE.NumberKeyframeTrack(
  ".position[y]", // Caminho da propriedade
  times,
  values,
);

const clip = new THREE.AnimationClip("bounce", 2, [track]);

Tipos de KeyframeTrack

// Track numérico (valor único)
new THREE.NumberKeyframeTrack(".opacity", times, [1, 0]);
new THREE.NumberKeyframeTrack(".material.opacity", times, [1, 0]);

// Track de vetor (posição, escala)
new THREE.VectorKeyframeTrack(".position", times, [
  0,
  0,
  0, // t=0
  1,
  2,
  0, // t=1
  0,
  0,
  0, // t=2
]);

// Track de quaternion (rotação)
const q1 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0));
const q2 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0));
new THREE.QuaternionKeyframeTrack(
  ".quaternion",
  [0, 1],
  [q1.x, q1.y, q1.z, q1.w, q2.x, q2.y, q2.z, q2.w],
);

// Track de cor
new THREE.ColorKeyframeTrack(".material.color", times, [
  1, 0, 0, // vermelho
  0, 1, 0, // verde
  0, 0, 1, // azul
]);

// Track booleano
new THREE.BooleanKeyframeTrack(".visible", [0, 0.5, 1], [true, false, true]);

// Track de string (para morph targets)
new THREE.StringKeyframeTrack(
  ".morphTargetInfluences[smile]",
  [0, 1],
  ["0", "1"],
);

Modos de Interpolação

const track = new THREE.VectorKeyframeTrack(".position", times, values);

// Interpolação
track.setInterpolation(THREE.InterpolateLinear); // Padrão
track.setInterpolation(THREE.InterpolateSmooth); // Spline cúbica
track.setInterpolation(THREE.InterpolateDiscrete); // Função degrau

AnimationMixer

Reproduz animações em um objeto e seus descendentes.

const mixer = new THREE.AnimationMixer(model);

// Criar ação a partir do clip
const action = mixer.clipAction(clip);
action.play();

// Atualizar no loop de animação
function animate() {
  const delta = clock.getDelta();
  mixer.update(delta); // Obrigatório!

  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

Eventos do Mixer

mixer.addEventListener("finished", (e) => {
  console.log("Animação concluída:", e.action.getClip().name);
});

mixer.addEventListener("loop", (e) => {
  console.log("Animação em loop:", e.action.getClip().name);
});

AnimationAction

Controla a reprodução de um clip de animação.

const action = mixer.clipAction(clip);

// Controle de reprodução
action.play();
action.stop();
action.reset();
action.halt(fadeOutDuration);

// Estado de reprodução
action.isRunning();
action.isScheduled();

// Controle de tempo
action.time = 0.5; // Tempo atual
action.timeScale = 1; // Velocidade de reprodução (negativo = reverso)
action.paused = false;

// Peso (para blend)
action.weight = 1; // 0-1, contribuição para a pose final
action.setEffectiveWeight(1);

// Modos de loop
action.loop = THREE.LoopRepeat; // Padrão: loop infinito
action.loop = THREE.LoopOnce; // Tocar uma vez e parar
action.loop = THREE.LoopPingPong; // Alternar para frente/trás
action.repetitions = 3; // Número de loops (Infinity padrão)

// Fixação
action.clampWhenFinished = true; // Manter último quadro ao terminar

// Blend
action.blendMode = THREE.NormalAnimationBlendMode;
action.blendMode = THREE.AdditiveAnimationBlendMode;

Fade In/Out

// Fade in
action.reset().fadeIn(0.5).play();

// Fade out
action.fadeOut(0.5);

// Crossfade entre animações
const action1 = mixer.clipAction(clip1);
const action2 = mixer.clipAction(clip2);

action1.play();

// Depois, crossfade para action2
action1.crossFadeTo(action2, 0.5, true);
action2.play();

Carregando Animações GLTF

Fonte mais comum de animações esqueléticas.

import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";

const loader = new GLTFLoader();
loader.load("model.glb", (gltf) => {
  const model = gltf.scene;
  scene.add(model);

  // Criar mixer
  const mixer = new THREE.AnimationMixer(model);

  // Obter todos os clips
  const clips = gltf.animations;
  console.log(
    "Animações disponíveis:",
    clips.map((c) => c.name),
  );

  // Reproduzir primeira animação
  if (clips.length > 0) {
    const action = mixer.clipAction(clips[0]);
    action.play();
  }

  // Reproduzir animação específica por nome
  const walkClip = THREE.AnimationClip.findByName(clips, "Walk");
  if (walkClip) {
    mixer.clipAction(walkClip).play();
  }

  // Armazenar mixer para o loop de atualização
  window.mixer = mixer;
});

// Loop de animação
function animate() {
  const delta = clock.getDelta();
  if (window.mixer) window.mixer.update(delta);

  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

Animação Esquelética

Esqueleto e Ossos

// Acessar esqueleto a partir de malha com skin
const skinnedMesh = model.getObjectByProperty("type", "SkinnedMesh");
const skeleton = skinnedMesh.skeleton;

// Acessar ossos
skeleton.bones.forEach((bone) => {
  console.log(bone.name, bone.position, bone.rotation);
});

// Encontrar osso específico por nome
const headBone = skeleton.bones.find((b) => b.name === "Head");
if (headBone) headBone.rotation.y = Math.PI / 4; // Virar cabeça

// Auxiliar de esqueleto
const helper = new THREE.SkeletonHelper(model);
scene.add(helper);

Animação Programática de Ossos

function animate() {
  const time = clock.getElapsedTime();

  // Animar osso
  const headBone = skeleton.bones.find((b) => b.name === "Head");
  if (headBone) {
    headBone.rotation.y = Math.sin(time) * 0.3;
  }

  // Atualizar mixer se também estiver reproduzindo clips
  mixer.update(clock.getDelta());
}

Anexos a Ossos

// Anexar objeto a um osso
const weapon = new THREE.Mesh(weaponGeometry, weaponMaterial);
const handBone = skeleton.bones.find((b) => b.name === "RightHand");
if (handBone) handBone.add(weapon);

// Deslocamento do anexo
weapon.position.set(0, 0, 0.5);
weapon.rotation.set(0, Math.PI / 2, 0);

Morph Targets

Misturar entre diferentes formas de malha.

// Morph targets são armazenados na geometria
const geometry = mesh.geometry;
console.log("Atributos morph:", Object.keys(geometry.morphAttributes));

// Acessar influências de morph target
mesh.morphTargetInfluences; // Array de pesos
mesh.morphTargetDictionary; // Mapeamento nome -> índice

// Definir morph target por índice
mesh.morphTargetInfluences[0] = 0.5;

// Definir por nome
const smileIndex = mesh.morphTargetDictionary["smile"];
mesh.morphTargetInfluences[smileIndex] = 1;

Animando Morph Targets

// Procedural
function animate() {
  const t = clock.getElapsedTime();
  mesh.morphTargetInfluences[0] = (Math.sin(t) + 1) / 2;
}

// Com animação por keyframe
const track = new THREE.NumberKeyframeTrack(
  ".morphTargetInfluences[smile]",
  [0, 0.5, 1],
  [0, 1, 0],
);
const clip = new THREE.AnimationClip("smile", 1, [track]);
mixer.clipAction(clip).play();

Blend de Animações

Misturar múltiplas animações.

// Configurar ações
const idleAction = mixer.clipAction(idleClip);
const walkAction = mixer.clipAction(walkClip);
const runAction = mixer.clipAction(runClip);

// Reproduzir todas com pesos diferentes
idleAction.play();
walkAction.play();
runAction.play();

// Definir pesos iniciais
idleAction.setEffectiveWeight(1);
walkAction.setEffectiveWeight(0);
runAction.setEffectiveWeight(0);

// Blend baseado na velocidade
function updateAnimations(speed) {
  if (speed < 0.1) {
    idleAction.setEffectiveWeight(1);
    walkAction.setEffectiveWeight(0);
    runAction.setEffectiveWeight(0);
  } else if (speed < 5) {
    const t = speed / 5;
    idleAction.setEffectiveWeight(1 - t);
    walkAction.setEffectiveWeight(t);
    runAction.setEffectiveWeight(0);
  } else {
    const t = Math.min((speed - 5) / 5, 1);
    idleAction.setEffectiveWeight(0);
    walkAction.setEffectiveWeight(1 - t);
    runAction.setEffectiveWeight(t);
  }
}

Blend Aditivo

// Pose base
const baseAction = mixer.clipAction(baseClip);
baseAction.play();

// Camada aditiva (ex.: respiração)
const additiveAction = mixer.clipAction(additiveClip);
additiveAction.blendMode = THREE.AdditiveAnimationBlendMode;
additiveAction.play();

// Converter clip para aditivo
THREE.AnimationUtils.makeClipAdditive(additiveClip);

Utilitários de Animação

import * as THREE from "three";

// Encontrar clip por nome
const clip = THREE.AnimationClip.findByName(clips, "Walk");

// Criar subclip
const subclip = THREE.AnimationUtils.subclip(clip, "subclip", 0, 30, 30);

// Converter para aditivo
THREE.AnimationUtils.makeClipAdditive(clip);
THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip);

// Clonar clip
const clone = clip.clone();

// Obter duração do clip
clip.duration;

// Otimizar clip (remover keyframes redundantes)
clip.optimize();

// Redefinir duração do clip para o primeiro quadro
clip.resetDuration();

Padrões de Animação Procedural

Damping Suave

// Seguir suave/lerp
const target = new THREE.Vector3();
const current = new THREE.Vector3();
const velocity = new THREE.Vector3();

function smoothDamp(current, target, velocity, smoothTime, deltaTime) {
  const omega = 2 / smoothTime;
  const x = omega * deltaTime;
  const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);
  const change = current.clone().sub(target);
  const temp = velocity
    .clone()
    .add(change.clone().multiplyScalar(omega))
    .multiplyScalar(deltaTime);
  velocity.sub(temp.clone().multiplyScalar(omega)).multiplyScalar(exp);
  return target.clone().add(change.add(temp).multiplyScalar(exp));
}

function animate() {
  current.copy(smoothDamp(current, target, velocity, 0.3, delta));
  mesh.position.copy(current);
}

Física de Mola

class Spring {
  constructor(stiffness = 100, damping = 10) {
    this.stiffness = stiffness;
    this.damping = damping;
    this.position = 0;
    this.velocity = 0;
    this.target = 0;
  }

  update(dt) {
    const force = -this.stiffness * (this.position - this.target);
    const dampingForce = -this.damping * this.velocity;
    this.velocity += (force + dampingForce) * dt;
    this.position += this.velocity * dt;
    return this.position;
  }
}

const spring = new Spring(100, 10);
spring.target = 1;

function animate() {
  mesh.position.y = spring.update(delta);
}

Oscilação

function animate() {
  const t = clock.getElapsedTime();

  // Onda senoidal
  mesh.position.y = Math.sin(t * 2) * 0.5;

  // Quicando
  mesh.position.y = Math.abs(Math.sin(t * 3)) * 2;

  // Movimento circular
  mesh.position.x = Math.cos(t) * 2;
  mesh.position.z = Math.sin(t) * 2;

  // Figura 8
  mesh.position.x = Math.sin(t) * 2;
  mesh.position.z = Math.sin(t * 2) * 1;
}

Dicas de Performance

  1. Compartilhar clips: O mesmo AnimationClip pode ser usado em múltiplos mixers
  2. Otimizar clips: Chame clip.optimize() para remover keyframes redundantes
  3. Desativar quando fora da tela: Pare atualizações do mixer para objetos invisíveis
  4. Usar LOD para animações: Rigs mais simples para personagens distantes
  5. Limitar mixers ativos: Cada mixer.update() tem um custo
// Pausar animação quando não visível
mesh.onBeforeRender = () => {
  action.paused = false;
};

mesh.onAfterRender = () => {
  // Verificar se será visível no próximo quadro
  if (!isInFrustum(mesh)) {
    action.paused = true;
  }
};

// Cache de clips
const clipCache = new Map();
function getClip(name) {
  if (!clipCache.has(name)) {
    clipCache.set(name, loadClip(name));
  }
  return clipCache.get(name);
}

Veja Também

  • threejs-loaders - Carregar modelos GLTF animados
  • threejs-fundamentals - Clock e loop de animação
  • threejs-shaders - Animação de vértices em shaders

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Habilidades Relacionadas