1实现原理 · 为什么它能做到
实现原理是三组 API 纪律加一条显存回收纪律:帧率无关(乘 dt)、用 setAnimationLoop 而非裸 rAF、以及把 GPU 资源生命周期当作一等公民。
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 播放走 AnimationMixer,关键是「用 delta 更新 mixer」;交叉淡化用 crossFadeTo(warping=false 为线性权重),单次动作用 LoopOnce + clampWhenFinished + finished 事件。
Load a model, play clips through an `AnimationMixer`, and **update the mixer with delta time** every frame.
相机运动用指数阻尼插值 lerp(goal, 1 - exp(-lambda*dt)) 而不是固定系数,从而任意帧率下阻尼手感一致;滚动只改目标位,由阻尼平滑,曲线飞行走 CatmullRomCurve3 + getPointAt。
Smooth camera follow uses exponential interpolation toward a target. Frame-rate-correct damping:
大量对象用 InstancedMesh 换一次 draw call,逐实例矩阵 setMatrixAt;每帧更新要切 DynamicDrawUsage,颜色走 setColorAt 并置 needsUpdate。
One draw call for thousands of identical meshes. Set per-instance matrices (and optional colors).
R3F 分支以 useFrame(state, delta) 驱动,并把 dpr 上限写进 <Canvas dpr={[1, 2]}>;模型/动画交回 useGLTF、useAnimations,弹簧与滚动按需引 @react-spring/three、drei ScrollControls。
Drive animation with `useFrame`; it receives `state` and `delta`.
显存回收是文档核心:解释 GC 释放不了 GPU buffer/texture 的机制,给「该 dispose 什么、什么不用」的判据,再给含材质数组与所有贴图槽的整套遍历。
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**.
R3F 的泄漏边界被切开:JSX 声明式创建的资源卸载时自动 dispose,imperative(useMemo/ref/loader 里 new 出来)的不会,必须在 useEffect 清理里手动 dispose;并用 renderer.info.memory 做 10 次挂载/卸载的基线回归来证实。
Verify with `renderer.info.memory` (R3F: `useThree().gl.info.memory`): mount/unmount 10x; if `geometries`/`textures` does not return to baseline, something leaked.
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| 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) |
4风险提醒 风险提醒:蓝色 · 知晓即可
- 验证回路依赖 npx 远程获取 Playwright 与 Chromium — 内联 `npx playwright screenshot` 与仓库根 seek-shot.sh(`npx -y playwright screenshot`):首次运行安装远程包并下载浏览器二进制,版本不锁定。
- 无头软件渲染下 GPU 相关结论不可直接推广 — swiftshader 可以出图,但帧时间、显存占用与泄漏表现与真机 GPU 差异明显;renderer.info.memory 回归应在目标设备复跑,否则可能得出错误结论。
- R3F/后处理分支引入较重的第三方依赖面 — @react-three/fiber、drei、@react-spring/three、postprocessing 由用户项目安装;版本漂移会改变 useFrame/dpr 行为与 dispose 语义,建议锁版(本 skill 不含 lockfile)。
- 产物期从 CDN 取 three 与远端 GLTF/贴图,版本与来源均未锁定 — 输出契约要求 importmap 指 CDN;模型/贴图若来自第三方 URL,长期在线发布需自托管、固定版本并确认许可。
- 仓库根脚本不在 skill 目录内 — 只安装 skills/threejs-animation 时 scripts/seek-shot.sh 引用悬空;需自行改用等价 playwright/ffmpeg 命令。
5第二遍独立确认
- [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)` 的增量语义区分开,说明作者意识到差异,声明可信。
6结论
de5e7ad51ab285c7…50697d659f