--- name: threejs-skills description: Complete Three.js toolkit for AI agents covering 3D scene setup, cameras, geometries, materials, lighting, and animation mixer/clip workflows. license: MIT metadata: author: pinkforest / cloudai-x repo: cloudai-x/threejs-skills version: "1.0" ---
Three.js Skills
A curated, comprehensive collection of Three.js skill files providing foundational 3D knowledge, accurate API references, constructor signatures, working code examples, performance optimization tips, and animation systems.
Subgroups Included
1. Three.js Fundamentals — Scene setup, cameras, renderer, Object3D hierarchy, coordinate systems, geometries, materials, and lighting 2. Three.js Animation Systems — AnimationMixer, AnimationClip, AnimationAction, keyframes, transitions, morph targets, and performance
Subgroup 1: Three.js Fundamentals
--- name: threejs-fundamentals description: Three.js scene setup, cameras, renderer, Object3D hierarchy, coordinate systems. Use when setting up 3D scenes, creating cameras, configuring renderers, managing object hierarchies, or working with transforms. ---
Three.js Fundamentals
Quick Start
import * as THREE from "three";// Create scene, camera, renderer const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000, ); const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement);
// Add a mesh const geometry = new THREE.BoxGeometry(1, 1, 1); const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 }); const cube = new THREE.Mesh(geometry, material); scene.add(cube);
// Add light scene.add(new THREE.AmbientLight(0xffffff, 0.5)); const dirLight = new THREE.DirectionalLight(0xffffff, 1); dirLight.position.set(5, 5, 5); scene.add(dirLight);
camera.position.z = 5;
// Animation loop function animate() { requestAnimationFrame(animate); cube.rotation.x += 0.01; cube.rotation.y += 0.01; renderer.render(scene, camera); } animate();
// Handle resize window.addEventListener("resize", () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); ```
Core Classes
Scene
Container for all 3D objects, lights, and cameras.
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000); // Solid color
scene.background = texture; // Skybox texture
scene.background = cubeTexture; // Cubemap
scene.environment = envMap; // Environment map for PBR
scene.fog = new THREE.Fog(0xffffff, 1, 100); // Linear fog
scene.fog = new THREE.FogExp2(0xffffff, 0.02); // Exponential fogCameras
PerspectiveCamera - Most common, simulates human eye.
// PerspectiveCamera(fov, aspect, near, far)
const camera = new THREE.PerspectiveCamera(
75, // Field of view (degrees)
window.innerWidth / window.innerHeight, // Aspect ratio
0.1, // Near clipping plane
1000, // Far clipping plane
);camera.position.set(0, 5, 10); camera.lookAt(0, 0, 0); camera.updateProjectionMatrix(); // Call after changing fov, aspect, near, far ```
OrthographicCamera - No perspective distortion, good for 2D/isometric.
// OrthographicCamera(left, right, top, bottom, near, far)
const aspect = window.innerWidth / window.innerHeight;
const frustumSize = 10;
const camera = new THREE.OrthographicCamera(
(frustumSize * aspect) / -2,
(frustumSize * aspect) / 2,
frustumSize / 2,
frustumSize / -2,
0.1,
1000,
);ArrayCamera - Multiple viewports with sub-cameras.
const cameras = [];
for (let i = 0; i < 4; i++) {
const subcamera = new THREE.PerspectiveCamera(40, 1, 0.1, 100);
subcamera.viewport = new THREE.Vector4(
Math.floor(i % 2) * 0.5,
Math.floor(i / 2) * 0.5,
0.5,
0.5,
);
cameras.push(subcamera);
}
const arrayCamera = new THREE.ArrayCamera(cameras);CubeCamera - Renders environment maps for reflections.
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256);
const cubeCamera = new THREE.CubeCamera(0.1, 1000, cubeRenderTarget);
scene.add(cubeCamera);// Use for reflections material.envMap = cubeRenderTarget.texture;
// Update each frame (expensive!) cubeCamera.position.copy(reflectiveMesh.position); cubeCamera.update(renderer, scene); ```
WebGLRenderer
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector("#canvas"), // Optional existing canvas
antialias: true, // Smooth edges
alpha: true, // Transparent background
powerPreference: "high-performance", // GPU hint
preserveDrawingBuffer: true, // For screenshots
});renderer.setSize(width, height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Tone mapping renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.0;
// Color space (Three.js r152+) renderer.outputColorSpace = THREE.SRGBColorSpace;
// Shadows renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Clear color renderer.setClearColor(0x000000, 1);
// Render renderer.render(scene, camera); ```
Object3D
Base class for all 3D objects. Mesh, Group, Light, Camera all extend Object3D.
const obj = new THREE.Object3D();// Transform obj.position.set(x, y, z); obj.rotation.set(x, y, z); // Euler angles (radians) obj.quaternion.set(x, y, z, w); // Quaternion rotation obj.scale.set(x, y, z);
// Local vs World transforms obj.getWorldPosition(targetVector); obj.getWorldQuaternion(targetQuaternion); obj.getWorldDirection(targetVector);
// Hierarchy obj.add(child); obj.remove(child); obj.parent; obj.children;
// Visibility obj.visible = false;
// Layers (for selective rendering/raycasting) obj.layers.set(1); obj.layers.enable(2); obj.layers.disable(0);
// Traverse hierarchy obj.traverse((child) => { if (child.isMesh) child.material.color.set(0xff0000); });
// Matrix updates obj.matrixAutoUpdate = true; // Default: auto-update matrices obj.updateMatrix(); // Manual matrix update obj.updateMatrixWorld(true); // Update world matrix recursively ```
Group
Empty container for organizing objects.
const group = new THREE.Group();
group.add(mesh1);
group.add(mesh2);
scene.add(group);// Transform entire group group.position.x = 5; group.rotation.y = Math.PI / 4; ```
Mesh
Combines geometry and material.
const mesh = new THREE.Mesh(geometry, material);// Multiple materials (one per geometry group) const mesh = new THREE.Mesh(geometry, [material1, material2]);
// Useful properties mesh.geometry; mesh.material; mesh.castShadow = true; mesh.receiveShadow = true;
// Frustum culling mesh.frustumCulled = true; // Default: skip if outside camera view
// Render order mesh.renderOrder = 10; // Higher = rendered later ```
Coordinate System
Three.js uses a right-handed coordinate system:
- +X points right
- +Y points up
- +Z points toward viewer (out of screen)
// Axes helper
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper); // Red=X, Green=Y, Blue=ZMath Utilities
Vector3
const v = new THREE.Vector3(x, y, z);
v.set(x, y, z);
v.copy(otherVector);
v.clone();// Operations (modify in place) v.add(v2); v.sub(v2); v.multiply(v2); v.multiplyScalar(2); v.divideScalar(2); v.normalize(); v.negate(); v.clamp(min, max); v.lerp(target, alpha);
// Calculations (return new value) v.length(); v.lengthSq(); // Faster than length() v.distanceTo(v2); v.dot(v2); v.cross(v2); // Modifies v v.angleTo(v2);
// Transform v.applyMatrix4(matrix); v.applyQuaternion(q); v.project(camera); // World to NDC v.unproject(camera); // NDC to world ```
Matrix4
const m = new THREE.Matrix4();
m.identity();
m.copy(other);
m.clone();// Build transforms m.makeTranslation(x, y, z); m.makeRotationX(theta); m.makeRotationY(theta); m.makeRotationZ(theta); m.makeRotationFromQuaternion(q); m.makeScale(x, y, z);
// Compose/decompose m.compose(position, quaternion, scale); m.decompose(position, quaternion, scale);
// Operations m.multiply(m2); // m = m * m2 m.premultiply(m2); // m = m2 * m m.invert(); m.transpose();
// Camera matrices m.makePerspective(left, right, top, bottom, near, far); m.makeOrthographic(left, right, top, bottom, near, far); m.lookAt(eye, target, up); ```
Quaternion
const q = new THREE.Quaternion();
q.setFromEuler(euler);
q.setFromAxisAngle(axis, angle);
q.setFromRotationMatrix(matrix);q.multiply(q2); q.slerp(target, t); // Spherical interpolation q.normalize(); q.invert(); ```
Euler
const euler = new THREE.Euler(x, y, z, "XYZ"); // Order matters!
euler.setFromQuaternion(q);
euler.setFromRotationMatrix(m);// Rotation orders: 'XYZ', 'YXZ', 'ZXY', 'XZY', 'YZX', 'ZYX' ```
Color
const color = new THREE.Color(0xff0000);
const color = new THREE.Color("red");
const color = new THREE.Color("rgb(255, 0, 0)");
const color = new THREE.Color("#ff0000");color.setHex(0x00ff00); color.setRGB(r, g, b); // 0-1 range color.setHSL(h, s, l); // 0-1 range
color.lerp(otherColor, alpha); color.multiply(otherColor); color.multiplyScalar(2); ```
MathUtils
THREE.MathUtils.clamp(value, min, max);
THREE.MathUtils.lerp(start, end, alpha);
THREE.MathUtils.mapLinear(value, inMin, inMax, outMin, outMax);
THREE.MathUtils.degToRad(degrees);
THREE.MathUtils.radToDeg(radians);
THREE.MathUtils.randFloat(min, max);
THREE.MathUtils.randInt(min, max);
THREE.MathUtils.smoothstep(x, min, max);
THREE.MathUtils.smootherstep(x, min, max);Common Patterns
Proper Cleanup
function dispose() {
// Dispose geometries
mesh.geometry.dispose();// Dispose materials if (Array.isArray(mesh.material)) { mesh.material.forEach((m) => m.dispose()); } else { mesh.material.dispose(); }
// Dispose textures texture.dispose();
// Remove from scene scene.remove(mesh);
// Dispose renderer renderer.dispose(); } ```
Clock for Animation
const clock = new THREE.Clock();function animate() { const delta = clock.getDelta(); // Time since last frame (seconds) const elapsed = clock.getElapsedTime(); // Total time (seconds)
mesh.rotation.y += delta * 0.5; // Consistent speed regardless of framerate
requestAnimationFrame(animate); renderer.render(scene, camera); } ```
Responsive Canvas
function onWindowResize() {
const width = window.innerWidth;
const height = window.innerHeight;camera.aspect = width / height; camera.updateProjectionMatrix();
renderer.setSize(width, height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); } window.addEventListener("resize", onWindowResize); ```
Loading Manager
const manager = new THREE.LoadingManager();manager.onStart = (url, loaded, total) => console.log("Started loading");
manager.onLoad = () => console.log("All loaded");
manager.onProgress = (url, loaded, total) => console.log(${loaded}/${total});
manager.onError = (url) => console.error(Error loading ${url});
const textureLoader = new THREE.TextureLoader(manager); const gltfLoader = new GLTFLoader(manager); ```
Performance Tips
- Limit draw calls: Merge geometries, use instancing, atlas textures
- Frustum culling: Enabled by default, ensure bounding boxes are correct
- LOD (Level of Detail): Use
THREE.LODfor distance-based mesh switching - Object pooling: Reuse objects instead of creating/destroying
- Avoid `getWorldPosition` in loops: Cache results
// Merge static geometries
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
const merged = mergeGeometries([geo1, geo2, geo3]);// LOD const lod = new THREE.LOD(); lod.addLevel(highDetailMesh, 0); lod.addLevel(medDetailMesh, 50); lod.addLevel(lowDetailMesh, 100); scene.add(lod); ```
See Also
threejs-geometry- Geometry creation and manipulationthreejs-materials- Material types and propertiesthreejs-lighting- Light types and shadows
Subgroup 2: Three.js Animation Systems
--- name: threejs-animation 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. ---
Three.js Animation
Quick Start
import * as THREE from "three";// Simple procedural animation 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(); ```
Animation System Overview
Three.js animation system has three main components:
- AnimationClip - Container for keyframe data
- AnimationMixer - Plays animations on a root object
- AnimationAction - Controls playback of a clip
AnimationClip
Stores keyframe animation data.
// Create animation clip
const times = [0, 1, 2]; // Keyframe times (seconds)
const values = [0, 1, 0]; // Values at each keyframeconst track = new THREE.NumberKeyframeTrack( ".position[y]", // Property path times, values, );
const clip = new THREE.AnimationClip("bounce", 2, [track]); ```
KeyframeTrack Types
// Number track (single value)
new THREE.NumberKeyframeTrack(".opacity", times, [1, 0]);
new THREE.NumberKeyframeTrack(".material.opacity", times, [1, 0]);// Vector track (position, scale) new THREE.VectorKeyframeTrack(".position", times, [ 0, 0, 0, // t=0 1, 2, 0, // t=1 0, 0, 0, // t=2 ]);
// Quaternion track (rotation) 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], );
// Color track new THREE.ColorKeyframeTrack(".material.color", times, [ 1, 0, 0, // red 0, 1, 0, // green 0, 0, 1, // blue ]);
// Boolean track new THREE.BooleanKeyframeTrack(".visible", [0, 0.5, 1], [true, false, true]);
// String track (for morph targets) new THREE.StringKeyframeTrack( ".morphTargetInfluences[smile]", [0, 1], ["0", "1"], ); ```
Interpolation Modes
const track = new THREE.VectorKeyframeTrack(".position", times, values);// Interpolation track.setInterpolation(THREE.InterpolateLinear); // Default track.setInterpolation(THREE.InterpolateSmooth); // Cubic spline track.setInterpolation(THREE.InterpolateDiscrete); // Step function ```
AnimationMixer
Plays animations on an object and its descendants.
const mixer = new THREE.AnimationMixer(model);// Create action from clip const action = mixer.clipAction(clip); action.play();
// Update in animation loop function animate() { const delta = clock.getDelta(); mixer.update(delta); // Required!
requestAnimationFrame(animate); renderer.render(scene, camera); } ```
Mixer Events
mixer.addEventListener("finished", (e) => {
console.log("Animation finished:", e.action.getClip().name);
});mixer.addEventListener("loop", (e) => { console.log("Animation looped:", e.action.getClip().name); }); ```
AnimationAction
Controls playback of an animation clip.
const action = mixer.clipAction(clip);// Playback control action.play(); action.stop(); action.reset(); action.halt(fadeOutDuration);
// Playback state action.isRunning(); action.isScheduled();
// Time control action.time = 0.5; // Current time action.timeScale = 1; // Playback speed (negative = reverse) action.paused = false;
// Weight (for blending) action.weight = 1; // 0-1, contribution to final pose action.setEffectiveWeight(1);
// Loop modes action.loop = THREE.LoopRepeat; // Default: loop forever action.loop = THREE.LoopOnce; // Play once and stop action.loop = THREE.LoopPingPong; // Alternate forward/backward action.repetitions = 3; // Number of loops (Infinity default)
// Clamping action.clampWhenFinished = true; // Hold last frame when done
// Blending 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 between animations const action1 = mixer.clipAction(clip1); const action2 = mixer.clipAction(clip2);
action1.play();
// Later, crossfade to action2 action1.crossFadeTo(action2, 0.5, true); action2.play(); ```
Loading GLTF Animations
Most common source of skeletal animations.
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);
// Create mixer const mixer = new THREE.AnimationMixer(model);
// Get all clips const clips = gltf.animations; console.log( "Available animations:", clips.map((c) => c.name), );
// Play first animation if (clips.length > 0) { const action = mixer.clipAction(clips[0]); action.play(); }
// Play specific animation by name const walkClip = THREE.AnimationClip.findByName(clips, "Walk"); if (walkClip) { mixer.clipAction(walkClip).play(); }
// Store mixer for update loop window.mixer = mixer; });
// Animation loop function animate() { const delta = clock.getDelta(); if (window.mixer) window.mixer.update(delta);
requestAnimationFrame(animate); renderer.render(scene, camera); } ```
Skeletal Animation
Skeleton and Bones
// Access skeleton from skinned mesh
const skinnedMesh = model.getObjectByProperty("type", "SkinnedMesh");
const skeleton = skinnedMesh.skeleton;// Access bones skeleton.bones.forEach((bone) => { console.log(bone.name, bone.position, bone.rotation); });
// Find specific bone by name const headBone = skeleton.bones.find((b) => b.name === "Head"); if (headBone) headBone.rotation.y = Math.PI / 4; // Turn head
// Skeleton helper const helper = new THREE.SkeletonHelper(model); scene.add(helper); ```
Programmatic Bone Animation
function animate() {
const time = clock.getElapsedTime();// Animate bone const headBone = skeleton.bones.find((b) => b.name === "Head"); if (headBone) { headBone.rotation.y = Math.sin(time) * 0.3; }
// Update mixer if also playing clips mixer.update(clock.getDelta()); } ```
Bone Attachments
// Attach object to bone
const weapon = new THREE.Mesh(weaponGeometry, weaponMaterial);
const handBone = skeleton.bones.find((b) => b.name === "RightHand");
if (handBone) handBone.add(weapon);// Offset attachment weapon.position.set(0, 0, 0.5); weapon.rotation.set(0, Math.PI / 2, 0); ```
Morph Targets
Blend between different mesh shapes.
// Morph targets are stored in geometry
const geometry = mesh.geometry;
console.log("Morph attributes:", Object.keys(geometry.morphAttributes));// Access morph target influences mesh.morphTargetInfluences; // Array of weights mesh.morphTargetDictionary; // Name -> index mapping
// Set morph target by index mesh.morphTargetInfluences[0] = 0.5;
// Set by name const smileIndex = mesh.morphTargetDictionary["smile"]; mesh.morphTargetInfluences[smileIndex] = 1; ```
Animating Morph Targets
// Procedural
function animate() {
const t = clock.getElapsedTime();
mesh.morphTargetInfluences[0] = (Math.sin(t) + 1) / 2;
}// With keyframe animation 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(); ```
Animation Blending
Mix multiple animations together.
// Setup actions
const idleAction = mixer.clipAction(idleClip);
const walkAction = mixer.clipAction(walkClip);
const runAction = mixer.clipAction(runClip);// Play all with different weights idleAction.play(); walkAction.play(); runAction.play();
// Set initial weights idleAction.setEffectiveWeight(1); walkAction.setEffectiveWeight(0); runAction.setEffectiveWeight(0);
// Blend based on speed 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); } } ```
Additive Blending
// Base pose
const baseAction = mixer.clipAction(baseClip);
baseAction.play();// Additive layer (e.g., breathing) const additiveAction = mixer.clipAction(additiveClip); additiveAction.blendMode = THREE.AdditiveAnimationBlendMode; additiveAction.play();
// Convert clip to additive THREE.AnimationUtils.makeClipAdditive(additiveClip); ```
Animation Utilities
import * as THREE from "three";// Find clip by name const clip = THREE.AnimationClip.findByName(clips, "Walk");
// Create subclip const subclip = THREE.AnimationUtils.subclip(clip, "subclip", 0, 30, 30);
// Convert to additive THREE.AnimationUtils.makeClipAdditive(clip); THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip);
// Clone clip const clone = clip.clone();
// Get clip duration clip.duration;
// Optimize clip (remove redundant keyframes) clip.optimize();
// Reset clip to first frame clip.resetDuration(); ```
Procedural Animation Patterns
Smooth Damping
// Smooth follow/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); } ```
Spring Physics
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); } ```
Oscillation
function animate() {
const t = clock.getElapsedTime();// Sine wave mesh.position.y = Math.sin(t * 2) * 0.5;
// Bouncing mesh.position.y = Math.abs(Math.sin(t * 3)) * 2;
// Circular motion mesh.position.x = Math.cos(t) * 2; mesh.position.z = Math.sin(t) * 2;
// Figure 8 mesh.position.x = Math.sin(t) * 2; mesh.position.z = Math.sin(t * 2) * 1; } ```
Performance Tips
- Share clips: Same AnimationClip can be used on multiple mixers
- Optimize clips: Call
clip.optimize()to remove redundant keyframes - Disable when off-screen: Stop mixer updates for invisible objects
- Use LOD for animations: Simpler rigs for distant characters
- Limit active mixers: Each mixer.update() has a cost
// Pause animation when not visible
mesh.onBeforeRender = () => {
action.paused = false;
};mesh.onAfterRender = () => { // Check if will be visible next frame if (!isInFrustum(mesh)) { action.paused = true; } };
// Cache clips const clipCache = new Map(); function getClip(name) { if (!clipCache.has(name)) { clipCache.set(name, loadClip(name)); } return clipCache.get(name); } ```
See Also
threejs-loaders- Loading animated GLTF modelsthreejs-fundamentals- Clock and animation loopthreejs-shaders- Vertex animation in shaders