🎬 快速定位 / 入口 >>>
three.js 最常见的任务不是背 API,而是快速搭起“场景 -> 相机 -> 光照 -> 模型 -> 交互”的工作流。
Scene + Camera + RendererMesh(geometry, material)MeshStandardMaterial + LightGLTFLoaderRaycasterEffectComposer WebGPU:/
🚀 起手式:先把场景跑起来 >>>
第一阶段只解决“能看到东西”,不要一上来就堆模型、后处理和复杂材质。
new THREE.Scene():创建场景new THREE.PerspectiveCamera():最常用相机new THREE.WebGLRenderer({ antialias: true }):默认起点renderer.setAnimationLoop(render):统一渲染循环renderer.setSize(...):同步视口大小
import * as THREE from "three";
const scene = new THREE.Scene();
scene.background = new THREE.Color("#0f172a");
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 1.5, 4);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
renderer.setAnimationLoop(() => {
renderer.render(scene, camera);
});
🧱 Recipe:先放一个能打光的物体 >>>
调材质之前先把光照补齐,否则你会误以为材质或贴图有问题。
BoxGeometry SphereGeometry:/MeshStandardMaterialDirectionalLightAmbientLightcastShadow / receiveShadow
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: "#38bdf8", roughness: 0.4 });
const mesh = new THREE.Mesh(geometry, material);
const ambient = new THREE.AmbientLight("#ffffff", 0.35);
const sun = new THREE.DirectionalLight("#ffffff", 2);
sun.position.set(5, 8, 6);
scene.add(mesh, ambient, sun);
📐 Recipe:处理 resize,避免视图拉伸 >>>
相机和 renderer 必须一起改,漏掉 `updateProjectionMatrix()` 就会出问题。
camera.aspectcamera.updateProjectionMatrix()renderer.setSize(...)2
function resize() {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
}
addEventListener("resize", resize);
📦 Recipe:加载 glTF / GLB 模型 >>>
three.js 实战里大多数模型流程都围绕 `GLTFLoader` 展开。
GLTFLoader:加载 glTF / GLBscene.add(gltf.scene):挂到主场景AnimationMixer:播放模型动画DRACOLoader:启用 Draco 压缩支持KTX2Loader:压缩纹理更省显存
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
const loader = new GLTFLoader();
const gltf = await loader.loadAsync("/models/robot.glb");
gltf.scene.position.y = -1;
scene.add(gltf.scene);
🖱️ Recipe:做鼠标拾取和高亮 >>>
交互核心是“屏幕坐标 -> NDC -> 射线 -> 求交对象”。
Vector2:存 NDC 鼠标坐标Raycaster:射线检测raycaster.setFromCamera(pointer, camera):从相机出发intersectObjects(list, true):检测命中
const pointer = new THREE.Vector2();
const raycaster = new THREE.Raycaster();
function onPointerMove(event) {
pointer.x = (event.clientX / innerWidth) * 2 - 1;
pointer.y = -(event.clientY / innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const hits = raycaster.intersectObjects(scene.children, true);
console.log(hits[0]?.object?.name);
}
✨ Recipe:接动画和后处理,但别拆多套循环 >>>
动画、控制器、后处理都应该进同一条渲染循环,不要各跑各的。
Clock delta:算AnimationMixer:更新模型动画EffectComposer:组织后处理RenderPass:基础 passUnrealBloomPass:Bloom
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
const delta = clock.getDelta();
mixer?.update(delta);
composer ? composer.render() : renderer.render(scene, camera);
});
🧪 Recipe:想试 WebGPU / TSL 时怎么渐进迁移 >>>
WebGPU 更适合按实验模块逐步替换,不适合全项目一次切换。
- 先保留 WebGL 主路径
WebGPURenderernavigator.gpu- 新材质/节点逻辑逐步迁到 TSL
- 保持 fallback,别让整个应用只剩实验渲染后端
import { WebGPURenderer } from "three/webgpu";
if ("gpu" in navigator) {
const renderer = new WebGPURenderer({ antialias: true });
await renderer.init();
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
}
⚠️ 常见坑 / 决策规则 >>>
three.js 卡顿和画面异常,通常不是单一 API 用错,而是 workflow 失序。
- 看不到物体时先查相机、灯光、near/far、材质类型
- 模型大时先压缩网格和纹理,再谈代码优化
dispose()setPixelRatio(devicePixelRatio) 2- 不要同时混用多套 render loop