Loading...
Loading...
This skill should be used when the user asks to "build a 3D hero section or WebGL background", "animate a Three.js scene", "play or crossfade GLTF animation clips", "move/lerp the camera or link it to scroll", "render thousands of objects with InstancedMesh", "set up React Three Fiber (R3F) motion", "add bloom/DOF post-processing", or "keep a Three.js/R3F animation smooth and leak-free (dispose geometries/materials/textures, free GPU memory on unmount)". Covers scenes, render loops, camera moves, GLTF AnimationMixer, instancing, scroll-linked 3D, R3F, and animation performance/disposal.
npx skill4agent add iart-ai/webgl-animation-skills threejs-animationimport * as THREE from 'three';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 0, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); // cap DPR — critical for retina perf
document.body.appendChild(renderer.domElement);
const mesh = new THREE.Mesh(
new THREE.IcosahedronGeometry(1, 0),
new THREE.MeshStandardMaterial({ color: 0x44aaff, flatShading: true })
);
scene.add(mesh, new THREE.DirectionalLight(0xffffff, 2).translateZ(5),
new THREE.AmbientLight(0xffffff, 0.4));
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
const dt = clock.getDelta();
mesh.rotation.y += dt * 0.5; // frame-rate independent
renderer.render(scene, camera);
});
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});clock.getDelta()dtsetAnimationLooprequestAnimationFrameAnimationMixerimport { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
let mixer, actions = {};
new GLTFLoader().load('model.glb', (gltf) => {
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
gltf.animations.forEach((clip) => { actions[clip.name] = mixer.clipAction(clip); });
actions['Idle']?.play();
});
// In the loop: if (mixer) mixer.update(dt);function crossfade(fromName, toName, dur = 0.4) {
const from = actions[fromName], to = actions[toName];
to.reset().setEffectiveWeight(1).play();
from.crossFadeTo(to, dur, false); // warping=false: linear weight ramp
}action.setLoop(THREE.LoopOnce); action.clampWhenFinished = true;mixer.addEventListener('finished', ...)const target = new THREE.Vector3(0, 0, 5);
function dampVec(current, goal, lambda, dt) {
current.lerp(goal, 1 - Math.exp(-lambda * dt)); // lambda ~ 3..8
}
// loop: dampVec(camera.position, target, 5, dt); camera.lookAt(0, 0, 0);scrollY / maxScrolladdEventListener('scroll', () => {
const p = scrollY / (document.body.scrollHeight - innerHeight);
target.set(0, p * -10, 5 - p * 3); // move down and in
});THREE.CatmullRomCurve3curve.getPointAt(p)const count = 5000;
const geo = new THREE.BoxGeometry(0.1, 0.1, 0.1);
const mat = new THREE.MeshStandardMaterial();
const inst = new THREE.InstancedMesh(geo, mat, count);
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
dummy.position.set((Math.random()-0.5)*20, (Math.random()-0.5)*20, (Math.random()-0.5)*20);
dummy.updateMatrix();
inst.setMatrixAt(i, dummy.matrix);
}
inst.instanceMatrix.needsUpdate = true;
scene.add(inst);
// Animate: update dummy per instance, setMatrixAt, then needsUpdate = true each frame.inst.instanceMatrix.setUsage(THREE.DynamicDrawUsage)inst.setColorAt(i, color)inst.instanceColor.needsUpdate = trueuseFramestatedeltaimport { Canvas, useFrame } from '@react-three/fiber';
import { useRef } from 'react';
function Spinner() {
const ref = useRef();
useFrame((state, delta) => {
ref.current.rotation.y += delta * 0.5;
ref.current.position.y = Math.sin(state.clock.elapsedTime) * 0.3;
});
return <mesh ref={ref}><icosahedronGeometry /><meshStandardMaterial /></mesh>;
}
export default () => (
<Canvas dpr={[1, 2]} camera={{ position: [0, 0, 5] }}>
<ambientLight intensity={0.4} /><directionalLight position={[5, 5, 5]} />
<Spinner />
</Canvas>
);dpr={[1, 2]}useGLTFuseAnimations@react-spring/threeScrollControlsuseScrollMath.min(devicePixelRatio, 2)BufferGeometryUtils.mergeGeometries.dispose()WebGL: CONTEXT_LOST_WEBGLrenderer.info.memorygeometry.dispose()material.dispose()texture.dispose()mapnormalMaproughnessMapenvMaprenderTarget.dispose()renderer.dispose()renderer.forceContextLoss()MeshGroupSceneVector3Matrix4Colorscene.remove(mesh)function disposeObject(root) {
root.traverse((obj) => {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) {
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
for (const m of mats) {
for (const key in m) { // dispose any texture property
const v = m[key];
if (v && v.isTexture) v.dispose();
}
m.dispose();
}
}
});
root.parent?.remove(root);
}
// SPA route teardown: renderer.setAnimationLoop(null); disposeObject(scene);
// renderer.dispose(); renderer.forceContextLoss?.();.dispose()new THREE.X()useMemouseEffectconst geometry = useMemo(() => new THREE.BufferGeometry(/* … */), [count]);
useEffect(() => () => geometry.dispose(), [geometry]); // disposes on unmount AND dep changedispose={null}renderer.info.memoryuseThree().gl.info.memorygeometriestexturesreferences/resource-disposal.mdrenderer.info<DisposeOnUnmount>Packaged helper ():scripts/freezes thescripts/seek-shot.sh anim.html 0 1.5 3harness and screenshots each moment;?t=Ntiles them for one-glance review. Seescripts/contact-sheet.sh sheet.png frame-*.png.scripts/README.md
.htmlthreethree/addons<script type="module">clock.getElapsedTime()mixerDate.now()?t=NN<script type="module">
// ... build scene, camera, renderer, clock, mixer ...
const t = new URLSearchParams(location.search).get("t");
function frame(elapsed) { // pure: scene state is a function of elapsed
mesh.rotation.y = elapsed * 0.5;
if (mixer) { mixer.setTime(elapsed); } // setTime is absolute, not delta
renderer.render(scene, camera);
}
if (t !== null) {
frame(parseFloat(t)); // one fixed frame, no loop
window.__ready = true;
} else {
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => frame(clock.getElapsedTime()));
}
</script>?t=0?t=<mid>?t=<end>npx playwright screenshot --wait-for-timeout=600 "file://$PWD/scene.html?t=1.5" frame-mid.png?t=NsetAnimationLoop(null)prefers-reduced-motion| Goal | API |
|---|---|
| Frame-independent motion | multiply by |
| Play GLTF clip | |
| Blend clips | |
| Smooth camera | |
| Many objects | |
| Scroll 3D | map scroll 0→1 to camera target |
| Cap DPR | |
references/r3f-and-perf.mdreferences/resource-disposal.md<DisposeOnUnmount>renderer.info