threejs-animation
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.
1How It Works & Mechanism
The implementation principle consists of three API discipline sets plus one VRAM garbage collection discipline: frame-rate independence (multiply by dt), using setAnimationLoop instead of bare rAF, and treating the GPU resource lifecycle as a first-class citizen.
Use `clock.getDelta()` to make motion frame-rate independent (multiply rates by `dt`), and `setAnimationLoop` (works with WebXR and pauses on tab blur) instead of raw `requestAnimationFrame`.
GLTF playback relies on AnimationMixer, where the key is "updating the mixer with delta"; crossfading uses crossFadeTo (warping=false for linear weighting), and one-shot actions use LoopOnce + clampWhenFinished + finished event.
Load a model, play clips through an `AnimationMixer`, and **update the mixer with delta time** every frame.
Camera motion employs exponential decay interpolation lerp(goal, 1 - exp(-lambda*dt)) rather than a fixed coefficient, ensuring consistent damping feel across arbitrary frame rates; scroll only alters the target position smoothed by damping, and curve fly-throughs follow CatmullRomCurve3 + getPointAt.
Smooth camera follow uses exponential interpolation toward a target. Frame-rate-correct damping:
Massive object counts leverage InstancedMesh to consolidate into a single draw call, with per-instance matrices set via setMatrixAt; per-frame updates require switching to DynamicDrawUsage, and color changes use setColorAt combined with setting needsUpdate.
One draw call for thousands of identical meshes. Set per-instance matrices (and optional colors).
The R3F branch is driven by useFrame(state, delta) and caps the DPR inside <Canvas dpr={[1, 2]}>; models and animations delegate to useGLTF and useAnimations, while springs and scrolling pull in @react-spring/three and drei ScrollControls as needed.
Drive animation with `useFrame`; it receives `state` and `delta`.
VRAM disposal is the core of the documentation: explaining why standard GC cannot release GPU buffers/textures, providing criteria for "what to dispose and what not to", and supplying a full traversal routine handling material arrays and all texture slots.
GPU resources (geometries, materials, textures, render targets) live on the *GPU*, not the JS heap. The garbage collector frees the JS objects but **cannot free the WebGL buffers/textures they uploaded**.
The R3F leak boundary is clearly bifurcated: resources declaratively created via JSX are automatically disposed upon unmount, whereas imperative ones (instantiated via new in useMemo/ref/loaders) are not and must be manually disposed in useEffect cleanups; this is validated using renderer.info.memory across a 10-cycle mount/unmount baseline regression.
Verify with `renderer.info.memory` (R3F: `useThree().gl.info.memory`): mount/unmount 10x; if `geometries`/`textures` does not return to baseline, something leaked.
2Core Capabilities
3External Dependencies
| Kind | Dependency / Endpoint |
|---|---|
| cli | npx playwright screenshot(验证回路:截 file:// 页面冻结帧) |
| cli | 仓库根共享脚本 scripts/seek-shot.sh、scripts/contact-sheet.sh(不在 skill 目录内) |
| cli | ffmpeg / ffprobe(contact-sheet.sh / probe-mp4.sh) |
| package | three + three/addons(GLTFLoader 等),产物期经 importmap 从 CDN 取 |
| package | React Three Fiber 生态(@react-three/fiber、drei 的 useGLTF/ScrollControls、@react-spring/three,以及 postprocessing 的 UnrealBloom / DepthOfField) |
4Recommended Governance & Evidence
- scripts/: Executes local scripts or commands: (SKILL.md instruction) npx playwright screenshot --wait-for-timeout=600 "file://$PWD/
- 网络端点: Declares external network access: No network call code inside skill directory; runtime network surface only stems from `npx playwright` tool acquisition (npm registry + Chromium binary download)
- 本地工作目录: File read/write and artifact output: Artifact itself: single scene.html (created by agent per output contract)
5Second-Pass Verification
- [ok] 「skill 目录内无脚本、无 3D 资产」是否成立 — 文件清单实证:目录内只有 SKILL.md、README.md(0 B) 与两份 markdown reference;无 .glb/.gltf/.hdr/贴图,也无 package.json 或构建配置。引用到的 scripts/*.sh 属仓库根资产。
- [ok] 凭证/环境变量读取是否存在 — 本仓全仓正则 `process.env|API_KEY|TOKEN|SECRET|Authorization` 零命中;credential_reads 为空成立。
- [ok] 是否有更高的触发条件被漏掉(红档特征) — `sandbox|automationcontrolled|no-verify|stealth|bypass|ignoreHTTPSErrors|dangerously` 全仓零命中;无任意代码执行面(交付物在浏览器沙箱内运行用户自己的场景代码)。
- [ok] 「显存 GC 释放不了」的解释与处置是否自洽 — 文档先讲机制(GC 释放 JS 对象但释放不了上传的 WebGL buffer/texture),再给「该 dispose / 不用 dispose」清单,再给 disposeObject 遍历(含材质数组与 for…in 贴图槽),三者互证;scene.remove 的误解也单独点破:'it frees **nothing** on the GPU'。
- [ok] R3F 自动 dispose 的边界声明是否有可验证手段 — 声明式自动 dispose vs imperative 泄漏的分界写得很具体,并要求 renderer.info.memory 挂载/卸载 10 次回归;references/resource-disposal.md 另附完整泄漏案例与 <DisposeOnUnmount> 方案,属可执行而非口号。
- [ok] 冻结 harness 与 mixer 的配合是否写对(易错点) — harness 段用 `mixer.setTime(elapsed)` 并注释 'setTime is absolute, not delta',与循环里 `mixer.update(dt)` 的增量语义区分开,说明作者意识到差异,声明可信。
6Verdict & Scenarios
de5e7ad51ab285c7…