threejs-animation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseThree.js Motion (3D / WebGL)
Three.js 动效(3D / WebGL)
Build real-time 3D motion for the web: animated scenes, camera moves, GLTF playback, instancing, scroll-linked 3D, and React Three Fiber. Optimize for smooth 60fps and clean disposal.
为网页构建实时3D动效:动画场景、相机移动、GLTF播放、实例化、滚动关联3D以及React Three Fiber。优化以实现流畅的60fps帧率和干净的资源释放。
When to use
适用场景
- 3D hero sections, product viewers, interactive/scroll-linked 3D backgrounds.
- Playing and blending GLTF animation clips.
- Rendering many objects efficiently (particle fields, tiles, forests).
- Camera fly-throughs and scroll-driven camera moves.
- 3D首屏区域、产品查看器、交互式/滚动关联3D背景。
- 播放与混合GLTF动画片段。
- 高效渲染大量对象(粒子场、瓦片、森林)。
- 相机飞行动画和滚动驱动的相机移动。
Minimal scene + render loop
最小场景 + 渲染循环
js
import * 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);
});Use to make motion frame-rate independent (multiply rates by ), and (works with WebXR and pauses on tab blur) instead of raw .
clock.getDelta()dtsetAnimationLooprequestAnimationFramejs
import * 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)); // 限制设备像素比 —— 对视网膜屏性能至关重要
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; // 帧率独立的动效
renderer.render(scene, camera);
});
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});使用使动效独立于帧率(将速率乘以),并使用(支持WebXR,在标签页失焦时暂停)替代原生。
clock.getDelta()dtsetAnimationLooprequestAnimationFrameGLTF animation clips
GLTF动画片段
Load a model, play clips through an , and update the mixer with delta time every frame.
AnimationMixerjs
import { 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);Crossfade between two clips smoothly:
js
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
}For one-shot clips (e.g. a jump), set and listen for .
action.setLoop(THREE.LoopOnce); action.clampWhenFinished = true;mixer.addEventListener('finished', ...)加载模型,通过播放片段,并每帧用增量时间更新mixer。
AnimationMixerjs
import { 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();
});
// 在循环中:if (mixer) mixer.update(dt);在两个片段间平滑交叉淡入淡出:
js
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:线性权重过渡
}对于一次性片段(如跳跃),设置并监听。
action.setLoop(THREE.LoopOnce); action.clampWhenFinished = true;mixer.addEventListener('finished', ...)Camera lerp and scroll-linked motion
相机插值与滚动关联动效
Smooth camera follow uses exponential interpolation toward a target. Frame-rate-correct damping:
js
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);Scroll-linked camera: map (0→1) to camera position along a path. Update the goal on scroll, let damping smooth it.
scrollY / maxScrolljs
addEventListener('scroll', () => {
const p = scrollY / (document.body.scrollHeight - innerHeight);
target.set(0, p * -10, 5 - p * 3); // move down and in
});Use and for curved fly-throughs.
THREE.CatmullRomCurve3curve.getPointAt(p)平滑相机跟随使用指数插值趋近目标。帧率修正的阻尼效果:
js
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
}
// 循环中:dampVec(camera.position, target, 5, dt); camera.lookAt(0, 0, 0);滚动关联相机:将(0→1)映射到相机沿路径的位置。在滚动时更新目标,让阻尼效果平滑过渡。
scrollY / maxScrolljs
addEventListener('scroll', () => {
const p = scrollY / (document.body.scrollHeight - innerHeight);
target.set(0, p * -10, 5 - p * 3); // 向下并向内移动
});使用和实现曲线飞行动画。
THREE.CatmullRomCurve3curve.getPointAt(p)Instancing for many objects
大量对象的实例化
One draw call for thousands of identical meshes. Set per-instance matrices (and optional colors).
js
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.Set if updating every frame. Per-instance color: then .
inst.instanceMatrix.setUsage(THREE.DynamicDrawUsage)inst.setColorAt(i, color)inst.instanceColor.needsUpdate = true一次绘制调用渲染数千个相同网格。设置每个实例的矩阵(可选颜色)。
js
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);
// 动画:更新每个实例的dummy,调用setMatrixAt,然后每帧设置needsUpdate = true。如果每帧更新,设置。每个实例的颜色:然后。
inst.instanceMatrix.setUsage(THREE.DynamicDrawUsage)inst.setColorAt(i, color)inst.instanceColor.needsUpdate = trueReact Three Fiber (R3F)
React Three Fiber (R3F)
Drive animation with ; it receives and .
useFramestatedeltajsx
import { 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/threeScrollControlsuseScroll使用驱动动画;它接收和参数。
useFramestatedeltajsx
import { 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/threeScrollControlsuseScrollPerformance and cleanup (critical)
性能与清理(至关重要)
- Cap pixel ratio: . This is the single biggest win on retina/mobile.
Math.min(devicePixelRatio, 2) - Reuse geometries and materials; merge static geometry with .
BufferGeometryUtils.mergeGeometries - Prefer matcaps or baked lighting over many real-time lights; each shadow-casting light is expensive.
- Material/emissive motion (pulsing emissive, displacement) often reads better and is cheaper than moving lots of geometry.
- 限制设备像素比:。这是提升视网膜屏/移动端性能最有效的手段。
Math.min(devicePixelRatio, 2) - 复用几何体和材质;使用合并静态几何体。
BufferGeometryUtils.mergeGeometries - 优先使用材质捕获(matcaps)或烘焙光照,而非大量实时光源;每个投射阴影的光源都很耗费性能。
- 材质/自发光动效(脉冲自发光、位移)通常视觉效果更好,且比移动大量几何体更节省性能。
Cleanup & GPU memory (disposal)
清理与GPU内存(资源释放)
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. Without explicit , VRAM grows on every mount until the context is lost (black canvas, ). This is why keeps rising across mount/unmount cycles.
.dispose()WebGL: CONTEXT_LOST_WEBGLrenderer.info.memoryDispose what allocates GPU memory; ignore what does not:
- Dispose: (VBOs),
geometry.dispose()(shader program — but NOT its textures),material.dispose()(per map slot:texture.dispose(),map,normalMap,roughnessMap, …),envMap, and on final teardownrenderTarget.dispose()+renderer.dispose().renderer.forceContextLoss() - No disposal needed: /
Mesh/Group(GC reclaims after removal),Scene/Vector3/Matrix4(plain JS), lights/cameras (except shadow map render targets).Color - only detaches from the graph — it frees nothing on the GPU. Geometry/material/textures stay resident until explicitly disposed.
scene.remove(mesh)
Teardown traversal — disposing a loaded model or whole scene must handle material arrays and every texture map per material:
js
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?.();R3F unmount cleanup — resources created declaratively in JSX are auto-disposed on unmount (R3F walks attached objects and calls ). Leaks happen with imperative resources ( in hooks/refs/loaders) that R3F never attached. Create them with and dispose in a matching cleanup:
.dispose()new THREE.X()useMemouseEffectjsx
const geometry = useMemo(() => new THREE.BufferGeometry(/* … */), [count]);
useEffect(() => () => geometry.dispose(), [geometry]); // disposes on unmount AND dep changeFor shared resources reused across mounts, set on the JSX object and dispose only at app shutdown. Verify with (R3F: ): mount/unmount 10x; if / does not return to baseline, something leaked.
dispose={null}renderer.info.memoryuseThree().gl.info.memorygeometriestexturesSee for the full disposal utility (all map slots, shader uniforms, render targets, skinned meshes), EffectComposer/render-target disposal, a leak-test harness, R3F cleanup recipes (refs, textures, GLTF cache clearing, ), and HMR safety.
references/resource-disposal.mdrenderer.info<DisposeOnUnmount>GPU资源(几何体、材质、纹理、渲染目标)存储在GPU中,而非JS堆内存。垃圾回收器会释放JS对象,但无法释放它们上传的WebGL缓冲区/纹理。如果不显式调用,VRAM会在每次挂载时增长,直到上下文丢失(画布变黑,出现错误)。这就是为什么在挂载/卸载循环中持续上升的原因。
.dispose()WebGL: CONTEXT_LOST_WEBGLrenderer.info.memory释放占用GPU内存的资源;忽略不占用的资源:
- 需要释放:(顶点缓冲区对象VBO)、
geometry.dispose()(着色器程序——但不包括其纹理)、material.dispose()(每个贴图插槽:texture.dispose()、map、normalMap、roughnessMap等)、envMap,以及在最终销毁时调用renderTarget.dispose()+renderer.dispose()。renderer.forceContextLoss() - 无需释放:/
Mesh/Group(移除后由垃圾回收器回收)、Scene/Vector3/Matrix4(纯JS对象)、光源/相机(阴影贴图渲染目标除外)。Color - 仅将对象从场景图中分离——不会释放任何GPU资源。几何体/材质/纹理会一直驻留,直到显式释放。
scene.remove(mesh)
遍历销毁——释放加载的模型或整个场景时,必须处理材质数组以及每个材质的所有纹理贴图:
js
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) { // 释放任何纹理属性
const v = m[key];
if (v && v.isTexture) v.dispose();
}
m.dispose();
}
}
});
root.parent?.remove(root);
}
// SPA路由销毁:renderer.setAnimationLoop(null); disposeObject(scene);
// renderer.dispose(); renderer.forceContextLoss?.();R3F卸载清理——在JSX中声明式创建的资源会在卸载时自动释放(R3F会遍历关联对象并调用)。内存泄漏发生在命令式创建的资源(在钩子/引用/加载器中使用),这些资源从未被R3F关联。使用创建它们,并在对应的清理函数中释放:
.dispose()new THREE.X()useMemouseEffectjsx
const geometry = useMemo(() => new THREE.BufferGeometry(/* … */), [count]);
useEffect(() => () => geometry.dispose(), [geometry]); // 在卸载和依赖项变化时释放对于跨挂载复用的共享资源,在JSX对象上设置,仅在应用关闭时释放。使用(R3F中为)验证:挂载/卸载10次;如果/未回到初始值,则存在内存泄漏。
dispose={null}renderer.info.memoryuseThree().gl.info.memorygeometriestextures查看获取完整的资源释放工具(所有贴图插槽、着色器 uniforms、渲染目标、蒙皮网格)、EffectComposer/渲染目标销毁、完整渲染器销毁、R3F清理方案(引用、纹理、GLTF缓存清理、)以及HMR安全策略。
references/resource-disposal.md<DisposeOnUnmount>Deliver & verify (standalone HTML)
交付与验证(独立HTML)
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
For a self-contained 3D scene (hero, WebGL background, micro-scene) the deliverable is one HTML file that opens directly in a browser — Three.js pulled from a CDN via an importmap, one render loop, no build step. A single file is the right tier for a scene; don't reach for a bundler when one file does the job.
Output contract:
- One : importmap pins
.html+threeto a CDN; your scene and the render loop in one inlinethree/addons.<script type="module"> - Drive ALL motion from one source of truth — (and
clock.getElapsedTime()/camera read from it). Nomixer, no per-object wall-clock.Date.now() - Seed any randomness (instance placement, jitter) from a fixed seed so frames reproduce.
Seek/freeze harness — render ONE frame at a fixed time for screenshots. forces elapsed time to seconds, advances the scene to that instant, renders once, and stops the loop — a deterministic still.
?t=NNhtml
<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>Verify loop — render → freeze → screenshot → check: open the file at three instants — start, mid, end (, , for an animated clip/loop) — screenshot each, and check both fidelity (matches the brief) and artifacts: a black canvas = parse/init error (check the console), clipped/off-frame geometry, NaN positions (objects vanish), missing model/texture (CDN 404). WebGL needs a GPU context; Playwright/Chromium supplies one (swiftshader) headless.
?t=0?t=<mid>?t=<end>bash
npx playwright screenshot --wait-for-timeout=600 "file://$PWD/scene.html?t=1.5" frame-mid.pngBefore you finish:
- Canvas actually renders — not blank, no console/WebGL errors, no CDN 404s.
- freezes a reproducible frame (same N → same pixels; randomness seeded).
?t=N - Screenshotted at start / mid / end — matches the brief, no clipping/NaN/black.
- Disposed and leak-free if embedded in an SPA (+ dispose; see disposal section).
setAnimationLoop(null) - honored — slow/halt rotation or auto-play where relevant.
prefers-reduced-motion
打包工具(目录):scripts/会冻结scripts/seek-shot.sh anim.html 0 1.5 3工具并截取每个时刻的截图;?t=N将截图拼接成一张预览图。详见scripts/contact-sheet.sh sheet.png frame-*.png。scripts/README.md
对于独立的3D场景(首屏、WebGL背景、微型场景),交付物应为可直接在浏览器中打开的单个HTML文件——Three.js通过importmap从CDN引入,单个渲染循环,无需构建步骤。单个文件是场景的最佳交付形式;当单个文件就能满足需求时,无需使用打包工具。
输出规范:
- 单个文件:importmap将
.html+three固定到CDN;场景和渲染循环写在单个内联three/addons中。<script type="module"> - 所有动效由单一数据源驱动——(
clock.getElapsedTime()/相机从中读取)。禁止使用mixer,禁止每个对象使用独立的壁钟时间。Date.now() - 任何随机性(实例位置、抖动)使用固定种子,确保帧可复现。
定位/冻结工具——在固定时间渲染单帧用于截图。将流逝时间强制设置为秒,将场景推进到该时刻,渲染一次后停止循环——生成确定性的静态画面。
?t=NNhtml
<script type="module">
// ... 构建场景、相机、渲染器、时钟、mixer ...
const t = new URLSearchParams(location.search).get("t");
function frame(elapsed) { // 纯函数:场景状态是流逝时间的函数
mesh.rotation.y = elapsed * 0.5;
if (mixer) { mixer.setTime(elapsed); } // setTime是绝对时间,而非增量时间
renderer.render(scene, camera);
}
if (t !== null) {
frame(parseFloat(t)); // 单帧固定画面,无循环
window.__ready = true;
} else {
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => frame(clock.getElapsedTime()));
}
</script>验证循环——渲染→冻结→截图→检查: 在三个时刻打开文件——开始、中间、结束(动画片段/循环使用、、)——分别截图,检查保真度(符合需求)和瑕疵:黑色画布=解析/初始化错误(检查控制台)、几何体被裁剪/超出画面、NaN位置(对象消失)、模型/纹理缺失(CDN 404)。WebGL需要GPU上下文;Playwright/Chromium提供无头模式的swiftshader GPU上下文。
?t=0?t=<mid>?t=<end>bash
npx playwright screenshot --wait-for-timeout=600 "file://$PWD/scene.html?t=1.5" frame-mid.png完成前检查:
- 画布实际渲染——无空白、无控制台/WebGL错误、无CDN 404。
- 可冻结可复现的帧(相同N→相同像素;随机性已播种)。
?t=N - 在开始/中间/结束时刻截图——符合需求,无裁剪/NaN/黑色画面。
- 嵌入SPA时可释放且无内存泄漏(+ 释放;见资源释放章节)。
setAnimationLoop(null) - 遵循——在相关位置减慢/停止旋转或自动播放。
prefers-reduced-motion
Quick reference
快速参考
| 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 | |
| 目标 | API |
|---|---|
| 帧率独立动效 | 乘以 |
| 播放GLTF片段 | |
| 混合片段 | |
| 平滑相机 | |
| 大量对象 | |
| 滚动关联3D | 将滚动值0→1映射到相机目标 |
| 限制设备像素比 | |
Reference files
参考文件
- — Full AnimationMixer crossfade and one-shot handling, useFrame patterns (clock, pointer, lerp), drei ScrollControls scroll-linked camera, InstancedMesh per-frame animation with color, pixel-ratio and adaptive resolution, complete dispose-on-unmount routine, and postprocessing (UnrealBloom, DepthOfField/bokeh) for both vanilla Three and R3F.
references/r3f-and-perf.md - — Complete GPU resource disposal: full disposal utility (all map slots, shader uniforms, render targets, env maps, skinned meshes), EffectComposer/render-target teardown, full renderer teardown, R3F leak cases and the useMemo+cleanup pattern, ref/texture/GLTF cleanup recipes,
references/resource-disposal.mdhelper, shared-resource and InstancedMesh strategies,<DisposeOnUnmount>leak-test harness, and HMR safety.renderer.info
- ——完整的AnimationMixer交叉淡入淡出和一次性片段处理、useFrame模式(时钟、指针、插值)、drei ScrollControls滚动关联相机、带颜色的InstancedMesh逐帧动画、像素比和自适应分辨率、完整的卸载释放流程,以及原生Three和R3F的后期处理(UnrealBloom、DepthOfField/散景)。
references/r3f-and-perf.md - ——完整的GPU资源释放:全功能释放工具(所有贴图插槽、着色器uniforms、渲染目标、环境贴图、蒙皮网格)、EffectComposer/渲染目标销毁、完整渲染器销毁、R3F内存泄漏场景和useMemo+清理模式、引用/纹理/GLTF清理方案、
references/resource-disposal.md工具、共享资源和InstancedMesh策略、<DisposeOnUnmount>内存泄漏测试工具,以及HMR安全策略。renderer.info