关于 threejs-fundamentals
threejs-fundamentals 是介绍 three.js r186 核心概念与 API 的参考技能,面向开发者讲解场景、相机、对象、渲染、数学工具、资源加载与清理、性能及 WebGPU/TSL。
主要功能
- 01场景、相机与渲染器
- 02Object3D、组与网格
- 03坐标、向量与矩阵
- 04加载管理与资源释放
- 05性能与视锥剔除
- 06WebGPU 与 TSL 基础
适用场景
- 01搭建 three.js 场景与相机
- 02配置 WebGLRenderer 或 WebGPURenderer
- 03排查几何体剔除和渲染开销
- 04管理加载进度并释放资源
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cloudai-x/ threejs-skills --skill threejs-fundamentals安装后在 Claude Code 中输入/threejs-fundamentals确认已加载。
详细介绍
Three.js 基础
目标版本为 three.js r186。示例使用 WebGLRenderer;WebGPU 和 TSL 部分使用 WebGPURenderer。
快速开始
import * as THREE from "three";
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111318);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.5, 5);
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardMaterial({ color: 0x3a86ff }),
);
scene.add(cube);
const light = new THREE.DirectionalLight(0xffffff, 2);
light.position.set(3, 4, 5); // shines from its position toward the origin
scene.add(light);
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const timer = new THREE.Timer();
timer.connect(document); // freezes the delta while the tab is hidden
function animate() {
timer.update(); // advance the timer first thing each frame
cube.rotation.y += timer.getDelta(); // seconds since the previous frame
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
如果不使用打包工具,浏览器会通过导入映射将 three.js 作为 ES 模块加载;以下是上文 WebGLRenderer 示例使用的映射:
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
</script>
使用 WebGPURenderer 时,请映射全部四个键——three.tsl.js 会导入 three/webgpu,因此缺少该条目时映射会失效:
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.webgpu.js",
"three/webgpu": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.webgpu.js",
"three/tsl": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.tsl.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
</script>
场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111318); // solid color
scene.fog = new THREE.Fog(0x111318, 1, 100); // linear fog: near, far
scene.fog = new THREE.FogExp2(0x111318, 0.02); // exponential fog: density (pick one)
scene.background也接受CubeTexture,或mapping为THREE.EquirectangularReflectionMapping的Texture;未设置映射的普通纹理会绘制为铺满屏幕的平面四边形,而不是天空盒。- 两种渲染器中的
MeshStandardMaterial和MeshPhysicalMaterial,以及仅 WebGLRenderer 中的MeshLambertMaterial和MeshPhongMaterial,都会受scene.environment照明。scene.environmentIntensity可调整其强度;material.envMapIntensity仅调整material.envMap的强度。HDR 环境请参阅 threejs-loaders。
摄像机
透视摄像机
// PerspectiveCamera(fov in degrees, aspect, near, far)
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);
camera.lookAt(0, 0, 0); // rotates the camera to face the target
camera.fov = 60;
camera.updateProjectionMatrix(); // required after changing fov, aspect, near or far
正交摄像机
// OrthographicCamera(left, right, top, bottom, near, far); frustum size in world units
const aspect = window.innerWidth / window.innerHeight;
const frustumHeight = 10;
const camera = new THREE.OrthographicCamera(
(-frustumHeight * aspect) / 2,
(frustumHeight * aspect) / 2,
frustumHeight / 2,
-frustumHeight / 2,
0.1,
1000,
);
// on resize: recompute left and right from the new aspect, then call camera.updateProjectionMatrix()
数组摄像机
// Renders the scene once per sub-camera, each into its own viewport.
// Viewport units under WebGLRenderer are drawing-buffer pixels (CSS size × pixel ratio);
// WebGPURenderer multiplies CSS pixels by the pixel ratio itself.
const width = window.innerWidth;
const height = window.innerHeight;
const dpr = renderer.getPixelRatio();
const cameras = [];
for (let i = 0; i < 4; i++) {
const subCamera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100);
subCamera.viewport = new THREE.Vector4(
((i % 2) * width * dpr) / 2,
(Math.floor(i / 2) * height * dpr) / 2,
(width * dpr) / 2,
(height * dpr) / 2,
);
subCamera.position.set(-2 + (i % 2) * 4, 1.5, 6);
subCamera.lookAt(0, 0, 0);
subCamera.updateMatrixWorld(); // sub-cameras are not scene-graph children; update them when they move
cameras.push(subCamera);
}
const arrayCamera = new THREE.ArrayCamera(cameras);
arrayCamera.position.set(0, 1.5, 6); // WebGLRenderer culls every viewport with this camera's frustum (fov 50, aspect 1 by default); WebGPURenderer uses each sub-camera's frustum
renderer.render(scene, arrayCamera);
立方摄像机
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256);
const cubeCamera = new THREE.CubeCamera(0.1, 1000, cubeRenderTarget);
scene.add(cubeCamera);
const chrome = new THREE.Mesh(
new THREE.SphereGeometry(1, 32, 16),
new THREE.MeshStandardMaterial({ color: 0xffffff, metalness: 1, roughness: 0 }),
);
chrome.material.envMap = cubeRenderTarget.texture;
scene.add(chrome);
// every frame: hide the reflective mesh, or it is drawn into the map it samples
chrome.visible = false;
cubeCamera.position.copy(chrome.position);
cubeCamera.update(renderer, scene);
chrome.visible = true;
使用 WebGPURenderer 时,请通过 three/webgpu 中的 CubeRenderTarget 构建目标,而不是使用 WebGLCubeRenderTarget。
WebGLRenderer
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector("#canvas"), // reuse an existing canvas; omit to create one
antialias: true, // MSAA
alpha: true, // let the page background show through the canvas
stencil: true, // stencil is off by default; pass this for stencil techniques
powerPreference: "high-performance",
preserveDrawingBuffer: true, // keep the drawing buffer for readPixels / screenshots
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // cap the fill rate on high-DPI displays
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping; // or AgXToneMapping, NeutralToneMapping; NoToneMapping is the default
renderer.toneMappingExposure = 1.0;
// renderer.outputColorSpace is SRGBColorSpace by default; leave it alone
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFShadowMap; // the default, already soft; VSMShadowMap and BasicShadowMap are the alternatives
renderer.setClearColor(0x000000, 0); // clear alpha 0 keeps the alpha: true canvas transparent
renderer.render(scene, camera);
对象
Object3D
Mesh、Group、Light 和 Camera 的基类。
const obj = new THREE.Object3D();
const child = new THREE.Object3D();
obj.position.set(1, 2, 3); // translation
obj.rotation.set(0, Math.PI / 4, 0); // Euler angles in radians; kept in sync with quaternion
obj.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 4); // the same rotation as a quaternion
obj.scale.set(1, 1, 1);
obj.getWorldPosition(new THREE.Vector3()); // world-space queries take a target vector to write into
obj.getWorldQuaternion(new THREE.Quaternion());
obj.getWorldDirection(new THREE.Vector3()); // the +Z axis in world space; cameras return their view direction (−Z)
obj.add(child); // reparents child; also removes it from its previous parent
obj.attach(child); // reparents and keeps child's world transform; add() keeps the local one
obj.remove(child);
obj.visible = false; // hides the whole subtree
obj.layers.enable(1); // ADD layer 1; set(1) would replace the mask and leave layer 0
obj.layers.disable(1);
obj.layers.test(camera.layers); // true when the object shares a layer with the camera
obj.traverse((descendant) => {
if (descendant.isMesh) descendant.castShadow = true; // visits obj itself, then all descendants
});
// matrix updates run automatically while matrixAutoUpdate is true (the default)
obj.matrixAutoUpdate = false;
obj.updateMatrix(); // composes obj.matrix and flags the world matrix dirty
// when writing obj.matrix directly instead, flag the world matrix yourself:
obj.matrixWorldNeedsUpdate = true;
Group 和 Mesh
const group = new THREE.Group(); // an Object3D that exists to hold children
const ball = new THREE.Mesh(new THREE.SphereGeometry(0.5), new THREE.MeshStandardMaterial());
group.add(ball);
group.rotation.y = Math.PI / 4; // transforms the whole subtree
scene.add(group);
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshStandardMaterial());
scene.add(mesh);
// one material per geometry group; BoxGeometry has six (+x, −x, +y, −y, +z, −z)
// a group whose materialIndex has no material is not drawn
const faceColors = [0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff, 0x00ffff];
const dice = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
faceColors.map((color) => new THREE.MeshStandardMaterial({ color })),
);
mesh.castShadow = true; // this object casts shadows
mesh.receiveShadow = true; // this object shows shadows
mesh.frustumCulled = true; // default: skipped when its bounding sphere leaves the camera frustum
mesh.renderOrder = 10; // renders later within the same transparency bucket
坐标系
右手坐标系,+Y 向上:+X 向右,+Y 向上,+Z 朝向观察者。位于原点且没有旋转的摄像机会沿 −Z 方向观察。
const axesHelper = new THREE.AxesHelper(5); // red X, green Y, blue Z; 5 = axis length
scene.add(axesHelper);
数学工具
Vector3
const a = new THREE.Vector3(1, 0, 0);
const b = new THREE.Vector3(0, 1, 0);
const c = a.clone(); // new vector with the same values
a.copy(b); // a becomes b
a.add(b); // in place: a = a + b
a.sub(b);
a.multiplyScalar(2);
a.divideScalar(2);
a.lerp(b, 0.5); // move halfway toward b
a.normalize(); // unit length
a.negate();
a.cross(b); // cross product, written into a (right-hand rule)
const len = a.length();
const lenSq = a.lengthSq(); // compare lengths without a square root
const dist = a.distanceTo(b);
const dot = a.dot(b); // > 0 same half-space, < 0 opposite, 0 perpendicular
const angle = a.angleTo(b); // radians
a.applyQuaternion(new THREE.Quaternion());
a.applyMatrix4(new THREE.Matrix4());
a.project(camera); // world position to normalized device coordinates
a.unproject(camera); // normalized device coordinates back to world
Matrix4、Quaternion、Euler
const m = new THREE.Matrix4();
m.identity();
m.makeTranslation(1, 2, 3);
m.makeRotationY(Math.PI / 4);
m.makeScale(2, 2, 2);
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3(1, 1, 1);
m.compose(position, quaternion, scale); // build from transform components
m.decompose(position, quaternion, scale); // read them back
m.multiply(new THREE.Matrix4()); // m = m * other (other applied first)
m.premultiply(new THREE.Matrix4()); // m = other * m
m.invert();
m.lookAt(new THREE.Vector3(0, 0, 1), new THREE.Vector3(), new THREE.Vector3(0, 1, 0)); // eye, target, up
const q = new THREE.Quaternion();
q.setFromEuler(new THREE.Euler(0, Math.PI / 2, 0, "YXZ")); // Euler order matters, XYZ is the default
q.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 4);
q.setFromRotationMatrix(m);
q.slerp(new THREE.Quaternion(), 0.5); // spherical interpolation, written into q
q.invert();
Color
const color = new THREE.Color(0xff0000); // hex number
const sky = new THREE.Color("#87ceeb"); // also color names and "rgb(255, 0, 0)"
color.setHex(0x0000ff);
color.setRGB(1, 0.5, 0); // 0–1 in the linear working color space; hex and CSS strings are sRGB
color.setRGB(1, 128 / 255, 0, THREE.SRGBColorSpace); // sRGB components, the same color as 0xff8000
color.setHSL(0.5, 1, 0.5); // hue, saturation, lightness in 0–1, also linear unless a color space is passed
color.lerp(sky, 0.5); // interpolate toward the target, written into color
color.multiplyScalar(2); // scale the components
MathUtils
const clamped = THREE.MathUtils.clamp(15, 1, 10); // 10
const mixed = THREE.MathUtils.lerp(0, 10, 0.5); // 5
const mapped = THREE.MathUtils.mapLinear(0.5, 0, 1, 0, 100); // 50
const radians = THREE.MathUtils.degToRad(90);
const degrees = THREE.MathUtils.radToDeg(Math.PI);
const random = THREE.MathUtils.randFloat(1, 10);
const eased = THREE.MathUtils.smoothstep(0.5, 0, 1); // eased 0–1 curve
加载管理器
const manager = new THREE.LoadingManager();
manager.onStart = (url, loaded, total) => console.log(`started ${url}`);
manager.onProgress = (url, loaded, total) => console.log(`${loaded}/${total} ${url}`);
manager.onLoad = () => console.log("all items loaded");
manager.onError = (url) => console.error(`failed ${url}`);
const loader = new THREE.TextureLoader(manager); // any loader takes a manager
清理
scene.remove(mesh);
// dispose geometry, materials and textures only when nothing else still uses them
mesh.geometry.dispose();
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
for (const material of materials) {
for (const value of Object.values(material)) {
if (value?.isTexture) value.dispose(); // material.dispose() leaves its textures alone
}
material.dispose();
}
mesh.dispose(); // frees renderer state kept for this object only
renderer.setAnimationLoop(null); // stop the loop
renderer.dispose(); // frees the renderer's GPU resources; the WebGL context stays alive
renderer.forceContextLoss(); // also release the context when the canvas is discarded
Object3D.dispose() 会释放每个对象的渲染器状态(WebGPURenderer 绑定、WebGLRenderer 实例化网格缓冲区);它不会遍历子对象,也不会释放几何体、材质或纹理。要拆除子树,请使用 traverse 遍历并像上面那样释放每个网格。覆盖 dispose() 的自定义 Object3D 子类应调用 super.dispose()。timer.dispose() 会断开已连接的计时器。
性能
- 视锥体剔除会检测包围球,而不是包围盒:普通网格使用
geometry.boundingSphere,InstancedMesh、SkinnedMesh 和 BatchedMesh 使用对象自身的boundingSphere。两者都只会计算一次,因此编辑位置后请调用geometry.computeBoundingSphere();实例或骨骼移动到远离初始位置的地方后,请调用object.computeBoundingSphere(),否则可见对象可能会被剔除。 - 将共享同一材质的静态几何体合并为一个网格,并对同一对象的多个副本使用
InstancedMesh(参阅 threejs-geometry)。 - 使用
LOD根据距离切换网格;渲染器会在每一帧为你调用update(camera)。 - 使用 WebGLRenderer 时,
renderer.info.render.calls会统计上次render()调用的绘制调用次数(每帧有多个渲染通道时,请设置renderer.info.autoReset = false,并在每帧调用一次renderer.info.reset());renderer.info.memory则统计当前存活的几何体和纹理。合并网格或释放对象后,请检查这些值。
import * as BufferGeometryUtils from "three/addons/utils/BufferGeometryUtils.js";
// inputs must carry the same set of attributes
const merged = BufferGeometryUtils.mergeGeometries([
new THREE.BoxGeometry(1, 1, 1),
new THREE.SphereGeometry(1, 32, 16),
]);
const lod = new THREE.LOD();
lod.addLevel(new THREE.Mesh(new THREE.SphereGeometry(1, 32, 16)), 0); // distance 0: full detail
lod.addLevel(new THREE.Mesh(new THREE.SphereGeometry(1, 12, 8)), 20);
lod.addLevel(new THREE.Mesh(new THREE.SphereGeometry(1, 6, 4)), 60);
scene.add(lod);
WebGPU 和 TSL
import * as THREE from "three/webgpu";
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init(); // must complete before any other use of the renderer
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111318); // the WebGPU canvas is transparent by default
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.5, 5);
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshStandardNodeMaterial({ color: 0x3a86ff }),
);
scene.add(cube);
const light = new THREE.DirectionalLight(0xffffff, 2);
light.position.set(3, 4, 5); // shines from its position toward the origin
scene.add(light);
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const timer = new THREE.Timer();
timer.connect(document);
function animate() {
timer.update();
cube.rotation.y += timer.getDelta();
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
renderer.render()会在await renderer.init()完成前抛出异常。setAnimationLoop也会运行初始化,但应在构造完成后立即等待init(),再执行会访问渲染器的设置代码:WebGL 2 回退会在init()期间替换renderer.backend。- 如果 WebGPU 不可用,WebGPURenderer 会自动回退到 WebGL 2;
new THREE.WebGPURenderer({ forceWebGL: true })会强制使用回退方案,而await renderer.init()之后,renderer.backend.isWebGPUBackend会报告当前使用的后端。 - 节点材质和 TSL 面向 WebGPURenderer(任一后端均可)。WebGLRenderer 仅通过
WebGLNodesHandler插件(three/addons/tsl/WebGLNodesHandler.js,传入renderer.setNodesHandler())才能渲染这些材质;这是一种迁移辅助方案,不支持 VSM 阴影、MRT、透射或 WebGPU 后期处理栈。ShaderMaterial 和onBeforeCompile仅适用于 WebGLRenderer。自定义着色可从 threejs-shaders 开始。 MeshStandardMaterial等内置材质会自动转换为对应的节点材质;设置节点插槽时,请使用*NodeMaterial类。- 这里没有
renderer.capabilities对象:各向异性值通过renderer.getMaxAnisotropy()获取。 - GPU 数据读取仍是异步的:
await renderer.getArrayBufferAsync(attribute)。
常见错误
renderer.shadowMap.type = THREE.PCFSoftShadowMap→renderer.shadowMap.type = THREE.PCFShadowMap:该选项在 r182 中已弃用;现在两种渲染器都会发出警告并回退到 PCFShadowMap,而后者本身已经是软阴影。const clock = new THREE.Clock()→const timer = new THREE.Timer(); timer.connect(document);:Clock 在 r183 中已弃用,构造时会发出警告;每帧调用timer.getDelta()或timer.getElapsed()前,先调用一次timer.update()。requestAnimationFrame(animate)→renderer.setAnimationLoop(animate):两种渲染器的文档都建议使用它,而不是手动循环;WebXR 帧只能通过它运行;对于 WebGPURenderer,render()会在setAnimationLoop执行异步初始化之前抛出错误。subCamera.viewport = new THREE.Vector4(0, 0, 0.5, 0.5)→subCamera.viewport = new THREE.Vector4(x * dpr, y * dpr, width * dpr, height * dpr):在 WebGLRenderer 下,ArrayCamera 的视口单位是绘图缓冲区像素(WebGPURenderer 会自行按像素比缩放 CSS 像素),因此使用小数值时不会绘制任何内容。cubeCamera.update(renderer, scene)→mesh.visible = false; cubeCamera.update(renderer, scene); mesh.visible = true;:可见的反射网格会被渲染到其材质正在采样的同一个立方体贴图中。scene.background = texture→texture.mapping = THREE.EquirectangularReflectionMapping; scene.background = texture:如果没有等距柱状映射或立方体映射,纹理会绘制成铺满屏幕的平面四边形,而不是天空盒。import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js"→import * as BufferGeometryUtils from "three/addons/utils/BufferGeometryUtils.js":标准导入映射不会解析three/examples/jsm/说明符,而是映射three/addons/。<script src="three.min.js"></script>→<script type="module">import * as THREE from "three";</script>:build/three.js和build/three.min.js已在 r161 中移除,因此在浏览器中,three.js 会通过导入映射以 ES 模块形式加载。mesh.layers.set(1)→mesh.layers.enable(1):set会替换图层掩码,并将网格从默认摄像机和灯光所在的第 0 层移除。renderer.outputEncoding = THREE.sRGBEncoding→renderer.outputColorSpace = THREE.SRGBColorSpace:编码 API 已不存在,而且颜色空间属性默认值就是SRGBColorSpace,因此通常不需要赋值。renderer.physicallyCorrectLights = true→const bulb = new THREE.PointLight(0xffffff, 100):基于物理的灯光单位始终启用,physicallyCorrectLights和useLegacyLights开关也已不存在,因此应删除该行,并根据距离为点光源和聚光灯设置合适的坎德拉强度(threejs-lighting)。material.envMapIntensity = 0.5→scene.environmentIntensity = 0.5:envMapIntensity只会缩放材质自身的envMap,不会缩放scene.environment。mesh.dispose()→mesh.dispose(); mesh.geometry.dispose(); for (const value of Object.values(mesh.material)) if (value?.isTexture) value.dispose(); mesh.material.dispose();:对象级别的 dispose 只会释放每个对象的渲染器状态,不会释放几何体、材质或纹理;而material.dispose()也不会释放纹理。import { WebGLCubeRenderTarget } from "three/webgpu"→import { CubeRenderTarget } from "three/webgpu":three/webgpu不会导出 WebGLCubeRenderTarget,且它与 WebGPURenderer 不兼容;对 WebGLRenderer 仍应使用它。await renderer.renderAsync(scene, camera)→await renderer.init(); renderer.render(scene, camera):异步渲染方法在 r181 中已弃用;初始化一次,然后在循环中同步渲染。
另请参阅
threejs-geometry— 创建和编辑 BufferGeometry、实例化、合并、边界体积。threejs-materials— 选择和配置材质及其纹理槽。threejs-textures— 加载纹理、色彩空间、UV 通道。threejs-lighting— 光源类型、单位和阴影配置。threejs-animation— 用于关键帧动画的 AnimationMixer 和剪辑系统。threejs-interaction— 射线投射、指针事件和相机控件。threejs-loaders— glTF 和其他文件加载器、异步加载模式。threejs-shaders— WebGLRenderer 上的 ShaderMaterial 和 GLSL,以及 WebGPURenderer 上的 TSL 节点材质。threejs-postprocessing— EffectComposer 通道和 WebGPU 渲染管线。threejs-performance— 衡量帧开销并减少绘制调用、内存占用和填充率。threejs-react-three-fiber— 将同一场景图编写为 React 组件。