Three.js 从入门到实战 — 一篇文章吃透 WebGL 的灵魂封装

WebGL 的门槛在矩阵里,Three.js 的门槛只在一杯咖啡里。

这是一篇长文,约 1.2 万字 + 12 个可运行 Demo + 1 个完整小项目。从原理 → API → 实战 → 性能优化全部讲透。读完你能独立做出常见 3D 场景:产品展示、数据可视化、3D 游戏原型、Web AR/VR。


目录


一、为什么需要 Three.js

1.1 WebGL 的痛

WebGL 是浏览器原生 3D API,但它只暴露最底层的 GPU 状态机。画一个三角形要写:

// 原生 WebGL 画三角形(节选,省略错误处理)
const gl = canvas.getContext('webgl');
const vsSource = `
  attribute vec4 aPosition;
  void main() { gl_Position = aPosition; }
`;
const fsSource = `
  void main() { gl_FragColor = vec4(1, 0.5, 0.2, 1); }
`;
const vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, vsSource); gl.compileShader(vs);
const fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, fsSource); gl.compileShader(fs);
const program = gl.createProgram();
gl.attachShader(program, vs); gl.attachShader(program, fs);
gl.linkProgram(program); gl.useProgram(program);

const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
   0,  0.5,  -0.5, -0.5,  0.5, -0.5
]), gl.STATIC_DRAW);
const loc = gl.getAttribLocation(program, 'aPosition');
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 3);

70 行代码画了一个三角形 —— 还没考虑相机、光照、纹理、阴影。

1.2 Three.js 的解法

import * as THREE from 'three';
const geo = new THREE.BoxGeometry();
const mat = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geo, mat);
scene.add(mesh);

4 行得到一个旋转的彩色立方体。Three.js 把 WebGL 抽象成了面向对象的 3D 引擎

抽象层 对应概念
Scene(场景图) 类似 DOM 树,物体可父子嵌套
Mesh = Geometry + Material 物体的"形状 + 材质"
Camera 决定"从哪儿看"
Light 决定"光怎么打"
Renderer 把场景画到 canvas

💡 一句话总结:Three.js 之于 WebGL,相当于 jQuery / React 之于原生 DOM

1.3 谁在用

  • Bruno Simon 个人主页(Three.js 风向标)
  • Apple AirPods Pro 产品页(半透明 3D 模型 + 滚动触发动画)
  • Figma / 鸿蒙开发者大会 / 阿里淘宝玩一玩
  • Stripe / Vercel 官网的 hero 区域
  • 特斯拉车机 / 蔚来 NOMI(车端嵌入 H5)
  • Google Earth Web / 高德 / 百度地图 3D

二、5 分钟跑起来:Hello Cube

2.1 引入方式

A. CDN(最快,适合 Demo)

<script type="importmap">
{
  "imports": {
    "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
    "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
  }
}
</script>
<script type="module">
  import * as THREE from 'three';
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  // ...
</script>

B. NPM(生产推荐)

npm i three
npm i -D @types/three   # TypeScript
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

2.2 第一个程序:旋转的立方体

<!DOCTYPE html>
<html>
<body style="margin:0;overflow:hidden">
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'three';

// 1) 场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0f1221);

// 2) 相机:透视投影,FOV 75°,宽高比,近/远裁剪面
const camera = new THREE.PerspectiveCamera(
  75, innerWidth / innerHeight, 0.1, 1000
);
camera.position.z = 3;

// 3) 渲染器
const renderer = new THREE.WebGLRenderer({
  canvas: document.getElementById('c'),
  antialias: true,
});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));

// 4) 几何体 + 材质 + 网格
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshNormalMaterial(); // 自带颜色,无需光照
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

// 5) 渲染循环
function tick() {
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
tick();

// 6) 自适应窗口
addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>

打开浏览器,能看到一个彩色立方体在转 —— 你已经入门了。


三、核心三件套:Scene / Camera / Renderer

3.1 Scene(场景)

Scene 本质是一个树形容器,所有要渲染的东西都得 add() 进去。

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222);   // 纯色背景
scene.fog = new THREE.Fog(0x222, 5, 50);     // 雾效(远处变模糊)

const group = new THREE.Group();              // 分组,可批量平移/旋转
group.add(cube1, cube2);
scene.add(group);

scene.traverse((obj) => console.log(obj.name)); // 深度优先遍历

每个 Object3D 都有:

  • position / rotation / scale:本地变换(Vector3 / Euler)
  • parent / children:父子关系
  • visible:是否渲染
  • userData:自定义数据挂载点

3.2 Camera(相机)

类型 用途 关键参数
PerspectiveCamera 透视(最常用,模拟人眼,近大远小) fov, aspect, near, far
OrthographicCamera 正交(无近大远小,CAD/2.5D 用) left, right, top, bottom, near, far
ArrayCamera 多视口(VR)
CubeCamera 立方体环境贴图采集
// 透视相机:90% 场景用它
const camera = new THREE.PerspectiveCamera(
  45,                       // FOV,越小越"长焦"
  window.innerWidth / window.innerHeight,
  0.1,                      // near:比这近的物体不显示
  1000                      // far:比这远的物体不显示
);
camera.position.set(5, 5, 10);
camera.lookAt(0, 0, 0);     // 朝原点看

// 正交相机:俯视图、UI、棋盘类游戏
const aspect = innerWidth / innerHeight;
const d = 5;
const orthoCam = new THREE.OrthographicCamera(
  -d * aspect, d * aspect, d, -d, 0.1, 100
);

⚠️ :改完 aspect 一定要 camera.updateProjectionMatrix(),否则不生效。

3.3 Renderer(渲染器)

const renderer = new THREE.WebGLRenderer({
  canvas,                    // 指定 canvas,不传会自动创建
  antialias: true,           // 抗锯齿(开启会有性能开销)
  alpha: true,               // 透明背景
  powerPreference: 'high-performance', // 强制独显
  preserveDrawingBuffer: true,         // 截图必备
});
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); // ⚠️ 限到 2
renderer.outputColorSpace = THREE.SRGBColorSpace;      // r152+ 必填
renderer.toneMapping = THREE.ACESFilmicToneMapping;    // 电影级色调
renderer.shadowMap.enabled = true;                     // 开阴影
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

WebGPU 渲染器(实验性):

import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
const renderer = new WebGPURenderer();
await renderer.init();

WebGPU 是 WebGL 的下一代,支持 Compute Shader,未来 3 年会逐步替代 WebGL。


四、坐标系与变换:搞懂这一节就成功一半

4.1 右手坐标系

Three.js 用 右手坐标系

       +Y (上)
        │
        │
        └─────── +X (右)
       ╱
      ╱
    +Z (朝向你)

记忆方法:右手伸出,拇指 X,食指 Y,中指 Z(指向自己)。

4.2 三种空间

空间 含义 例子
本地(local) 相对父对象 子物体 position(1,0,0) 表示在父物体右边 1 米
世界(world) 全局坐标 mesh.getWorldPosition(v)
屏幕(screen) 像素坐标 鼠标事件 event.clientX
// 本地 → 世界
const v = new THREE.Vector3();
mesh.getWorldPosition(v);

// 世界 → 屏幕
const s = mesh.position.clone().project(camera);
const x = (s.x * 0.5 + 0.5) * width;
const y = (-s.y * 0.5 + 0.5) * height;

// 屏幕 → 世界(反向)
const ndc = new THREE.Vector3(
  (mouseX / width) * 2 - 1,
  -(mouseY / height) * 2 + 1,
  0
);
ndc.unproject(camera);

4.3 旋转的三种姿势

// A. Euler 欧拉角(直观但有"万向锁")
mesh.rotation.x = Math.PI / 4;  // 弧度,PI = 180°
mesh.rotation.set(0, Math.PI, 0);

// B. Quaternion 四元数(无万向锁,骨骼动画用它)
const q = new THREE.Quaternion();
q.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
mesh.quaternion.copy(q);

// C. lookAt(让物体面朝某点)
mesh.lookAt(0, 0, 0);

4.4 矩阵复合

每个 Object3D 内部维护两个矩阵:

mesh.matrix         // 本地变换矩阵(由 position/rotation/scale 自动生成)
mesh.matrixWorld    // 世界变换矩阵 = parent.matrixWorld × matrix

修改完 position 后,Three.js 会在每帧渲染前自动重算。如果你手动改 matrix,要:

mesh.matrixAutoUpdate = false;
mesh.matrix.copy(yourMatrix);
mesh.matrixWorldNeedsUpdate = true;

五、几何体(Geometry)— 物体的"骨架"

5.1 内置几何体

new THREE.BoxGeometry(width, height, depth);
new THREE.SphereGeometry(radius, widthSeg, heightSeg);
new THREE.PlaneGeometry(width, height);
new THREE.CylinderGeometry(radiusTop, radiusBottom, height, segs);
new THREE.ConeGeometry(radius, height, segs);
new THREE.TorusGeometry(radius, tubeRadius, radialSeg, tubularSeg);
new THREE.TorusKnotGeometry(...);
new THREE.IcosahedronGeometry(radius, detail);   // 二十面体(细分多了变球)
new THREE.TetrahedronGeometry();                 // 四面体
new THREE.RingGeometry(innerR, outerR, segs);
new THREE.TextGeometry(text, { font, size });    // 3D 文字(需加载字体)

5.2 自定义几何体(BufferGeometry)

所有内置几何体最终都是 BufferGeometry。手写一个三角形:

const geo = new THREE.BufferGeometry();

// 顶点位置(每 3 个数为一个顶点)
const vertices = new Float32Array([
   0,  1, 0,
  -1, -1, 0,
   1, -1, 0,
]);
geo.setAttribute('position', new THREE.BufferAttribute(vertices, 3));

// 顶点法线(用于光照计算)
geo.computeVertexNormals();

// 索引(让顶点复用,省内存)
geo.setIndex([0, 1, 2]);

// UV(贴图坐标)
const uvs = new Float32Array([0.5, 1, 0, 0, 1, 0]);
geo.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));

5.3 修改几何体顶点

const pos = geo.attributes.position;
for (let i = 0; i < pos.count; i++) {
  const y = pos.getY(i);
  pos.setY(i, y + Math.sin(Date.now() * 0.001 + i) * 0.1);
}
pos.needsUpdate = true;     // ⚠️ 必加,否则 GPU 不更新

5.4 几何体合并(性能技巧)

import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';

const merged = mergeGeometries([geoA, geoB, geoC]);
const mesh = new THREE.Mesh(merged, material);
// 1 个 mesh = 1 次 draw call,比 3 个 mesh 快得多

六、材质(Material)— 物体的"皮肤"

6.1 选材质的决策树

需要响应光照吗?
├── 否 → MeshBasicMaterial(最便宜)
└── 是
    ├── 卡通/不在乎物理真实 → MeshLambertMaterial / MeshPhongMaterial
    └── 要照片级真实 → MeshStandardMaterial(PBR)
                       └── 要更高精度 → MeshPhysicalMaterial(车漆/玻璃)

6.2 常用材质

// 1. Basic:纯色 / 贴图,不受光照影响
new THREE.MeshBasicMaterial({ color: 0xff6688, wireframe: true });

// 2. Normal:根据法线显示颜色(debug 神器)
new THREE.MeshNormalMaterial();

// 3. Lambert:漫反射(哑光,磨砂)
new THREE.MeshLambertMaterial({ color: 0x8888ff });

// 4. Phong:带高光(塑料、瓷器)
new THREE.MeshPhongMaterial({
  color: 0xff8888,
  shininess: 100,
  specular: 0xffffff,
});

// 5. Standard(PBR):现代游戏标准
new THREE.MeshStandardMaterial({
  color: 0xffffff,
  roughness: 0.5,    // 0=镜面,1=完全粗糙
  metalness: 0.8,    // 0=塑料,1=金属
  map: colorTex,
  normalMap: normalTex,
  roughnessMap: roughTex,
  metalnessMap: metalTex,
  aoMap: aoTex,
});

// 6. Physical:Standard 增强(清漆 / 透射 / 虹彩)
new THREE.MeshPhysicalMaterial({
  clearcoat: 1.0,
  transmission: 0.9,    // 透明(水晶/玻璃)
  ior: 1.5,             // 折射率
  thickness: 0.5,
});

// 7. Shader 自定义
new THREE.ShaderMaterial({
  uniforms: { uTime: { value: 0 } },
  vertexShader: `...`,
  fragmentShader: `...`,
});

6.3 通用属性

material.transparent = true;
material.opacity = 0.5;
material.side = THREE.DoubleSide;   // 双面渲染(默认只渲染正面)
material.depthWrite = false;        // 透明物体常配合此项
material.blending = THREE.AdditiveBlending;  // 加法混合(火焰/光晕)
material.wireframe = true;          // 线框模式
material.flatShading = true;        // 低多边形风格

⚠️ 半透明排序坑:Three.js 的透明物体按 z-depth 排序,复杂场景下可能错乱。解决方案:① material.depthWrite = false ② 用 renderOrder 手动指定 ③ 改用 OIT(Order-Independent Transparency)。


七、光照(Light)— 让世界亮起来

⚠️ 用了 MeshBasicMaterial 不需要光照;用 Lambert / Phong / Standard 必须加灯,否则黑屏。

7.1 6 种光源

// 1. 环境光:均匀照亮全场,无方向、无阴影
scene.add(new THREE.AmbientLight(0xffffff, 0.3));

// 2. 平行光:模拟太阳,方向恒定(可投阴影)
const sun = new THREE.DirectionalLight(0xffffff, 1);
sun.position.set(5, 10, 7);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
scene.add(sun);

// 3. 点光源:模拟灯泡,向四周发光(可投阴影)
const bulb = new THREE.PointLight(0xff8844, 1, 20);  // 颜色, 强度, 距离
bulb.position.set(0, 5, 0);
scene.add(bulb);

// 4. 聚光灯:手电筒,圆锥范围(可投阴影)
const spot = new THREE.SpotLight(0xffffff, 1, 30, Math.PI / 6, 0.2);
scene.add(spot);

// 5. 半球光:天空 / 地面双色照明(户外场景神器)
scene.add(new THREE.HemisphereLight(0x87ceeb, 0x654321, 0.6));

// 6. 矩形面光:模拟柔光板(IES 物理灯)
import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
RectAreaLightUniformsLib.init();
const rect = new THREE.RectAreaLight(0xffffff, 5, 4, 2);
scene.add(rect);

7.2 阴影怎么开

阴影开销大,要配合三步:

// ① 渲染器开
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;  // 软阴影

// ② 光源开
sun.castShadow = true;
sun.shadow.camera.left = -10;     // 阴影相机视锥(影响阴影范围)
sun.shadow.camera.right = 10;
sun.shadow.camera.top = 10;
sun.shadow.camera.bottom = -10;
sun.shadow.bias = -0.0005;        // 解决"阴影痤疮"

// ③ 物体开
mesh.castShadow = true;
ground.receiveShadow = true;

7.3 调试光照

const helper = new THREE.DirectionalLightHelper(sun, 1);
scene.add(helper);

const shadowHelper = new THREE.CameraHelper(sun.shadow.camera);
scene.add(shadowHelper);

八、纹理与贴图(Texture)

8.1 加载基础纹理

const loader = new THREE.TextureLoader();
const tex = loader.load(
  '/wood.jpg',
  (t) => console.log('loaded'),
  undefined,
  (e) => console.error(e),
);
tex.colorSpace = THREE.SRGBColorSpace;   // ⚠️ 颜色贴图必加(r152+)
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(4, 4);                    // 平铺 4×4
tex.anisotropy = 16;                     // 各向异性过滤(远处不糊)

const mat = new THREE.MeshStandardMaterial({ map: tex });

8.2 PBR 全套贴图

现代 PBR 流程通常需要 5–7 张贴图:

贴图 作用 颜色空间
albedo / map 颜色 sRGB
normalMap 法线(凹凸细节) Linear
roughnessMap 粗糙度 Linear
metalnessMap 金属度 Linear
aoMap 环境光遮蔽 Linear
displacementMap 位移(真实位移顶点) Linear
emissiveMap 自发光 sRGB
const mat = new THREE.MeshStandardMaterial({
  map: load('color.jpg'),
  normalMap: load('normal.jpg'),
  roughnessMap: load('rough.jpg'),
  metalnessMap: load('metal.jpg'),
  aoMap: load('ao.jpg'),
  envMap: envTex,           // 环境贴图(决定金属表面反射什么)
});

function load(url) {
  const t = loader.load(url);
  if (url.includes('color') || url.includes('emissive')) {
    t.colorSpace = THREE.SRGBColorSpace;
  }
  return t;
}

8.3 环境贴图(HDRI)— 真实感关键

import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';

new RGBELoader().load('/sky.hdr', (hdr) => {
  hdr.mapping = THREE.EquirectangularReflectionMapping;
  scene.background = hdr;
  scene.environment = hdr;   // 所有 PBR 材质自动反射这张图
});

免费 HDRI 资源:polyhaven.com

8.4 视频纹理

const video = document.createElement('video');
video.src = '/loop.mp4';
video.loop = true;
video.muted = true;
video.play();

const videoTex = new THREE.VideoTexture(video);
const screen = new THREE.Mesh(
  new THREE.PlaneGeometry(16, 9),
  new THREE.MeshBasicMaterial({ map: videoTex })
);

九、相机控制器(OrbitControls 等)

9.1 OrbitControls — 最常用

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;       // 阻尼,丝滑
controls.dampingFactor = 0.05;
controls.minDistance = 2;
controls.maxDistance = 20;
controls.maxPolarAngle = Math.PI / 2; // 不让相机翻到地下
controls.autoRotate = true;
controls.autoRotateSpeed = 0.5;

// 渲染循环里调用
function tick() {
  controls.update();   // ⚠️ 开启 damping 后必须每帧 update
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}

9.2 其他控制器

import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js';
import { FlyControls }         from 'three/addons/controls/FlyControls.js';
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; // FPS
import { TransformControls }   from 'three/addons/controls/TransformControls.js';   // 类 Blender
import { DragControls }        from 'three/addons/controls/DragControls.js';        // 拖拽
import { ArcballControls }     from 'three/addons/controls/ArcballControls.js';     // 自由轨迹球

十、加载 3D 模型(GLTF / FBX / OBJ)

10.1 选哪个格式

格式 大小 动画 推荐
glTF / glb 小(二进制) ✅ 骨骼/Morph/PBR ⭐⭐⭐⭐⭐ Web 标准
FBX 老项目过渡用
OBJ 静态模型
STL 3D 打印
USDZ iOS AR Quick Look

业内共识:优先 glTF,只有 glTF 是 Khronos 官方制定的"3D 界 JPEG"。

10.2 加载 glTF(含 Draco / KTX2 压缩)

import { GLTFLoader }  from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { KTX2Loader }  from 'three/addons/loaders/KTX2Loader.js';

const draco = new DRACOLoader().setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
const ktx2  = new KTX2Loader().setTranscoderPath('https://unpkg.com/three@0.160.0/examples/jsm/libs/basis/');
ktx2.detectSupport(renderer);

const loader = new GLTFLoader().setDRACOLoader(draco).setKTX2Loader(ktx2);

loader.load(
  '/models/duck.glb',
  (gltf) => {
    scene.add(gltf.scene);
    console.log('动画', gltf.animations);
    console.log('相机', gltf.cameras);
  },
  (xhr) => console.log((xhr.loaded / xhr.total * 100) + '%'),
  (err) => console.error(err),
);

10.3 模型编辑常用操作

gltf.scene.traverse((child) => {
  if (child.isMesh) {
    child.castShadow = true;
    child.receiveShadow = true;
    // 替换材质
    if (child.name === 'Body') child.material.color.set(0xff0000);
  }
});

// 居中并缩放到合适大小
const box = new THREE.Box3().setFromObject(gltf.scene);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
gltf.scene.position.sub(center);
gltf.scene.scale.setScalar(2 / Math.max(size.x, size.y, size.z));

十一、动画:Tween / Clock / Mixer

11.1 三种动画方式

A. 手写帧动画(最简单)

function tick() {
  cube.rotation.y += 0.01;
  cube.position.x = Math.sin(Date.now() * 0.001) * 3;
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}

B. Tween.js(补间动画)

import TWEEN from 'three/addons/libs/tween.module.js';

new TWEEN.Tween(cube.position)
  .to({ x: 5, y: 2, z: 0 }, 1500)
  .easing(TWEEN.Easing.Cubic.InOut)
  .onUpdate(() => console.log('animating'))
  .start();

// 渲染循环里
TWEEN.update();

C. AnimationMixer(骨骼/Morph 动画,glTF 必用)

const mixer = new THREE.AnimationMixer(gltf.scene);
const action = mixer.clipAction(gltf.animations[0]);
action.play();
action.setLoop(THREE.LoopRepeat);
action.setEffectiveTimeScale(1);
action.crossFadeTo(otherAction, 0.5);   // 平滑切换动作

const clock = new THREE.Clock();
function tick() {
  const dt = clock.getDelta();
  mixer.update(dt);
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}

11.2 Clock — 帧率无关的时间

const clock = new THREE.Clock();
clock.getDelta();      // 上一帧到这一帧的秒数
clock.getElapsedTime(); // 累计运行秒数

// 用 dt 让动画在不同帧率设备上保持一致速度
mesh.rotation.y += 1.0 * dt;   // 每秒旋转 1 弧度,60fps/120fps 都一样

十二、射线检测(Raycaster)— 鼠标拾取

12.1 鼠标点击物体

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

renderer.domElement.addEventListener('click', (e) => {
  // 屏幕坐标 → NDC(归一化设备坐标 -1~1)
  mouse.x = (e.clientX / innerWidth) * 2 - 1;
  mouse.y = -(e.clientY / innerHeight) * 2 + 1;

  raycaster.setFromCamera(mouse, camera);

  // 检测哪些物体被命中(按距离排序)
  const hits = raycaster.intersectObjects(scene.children, true);
  if (hits.length > 0) {
    const obj = hits[0].object;
    console.log('点中:', obj.name, '距离:', hits[0].distance);
    obj.material.color.set(Math.random() * 0xffffff);
  }
});

12.2 FPS 射击:以相机正前方发射射线

// 用于 PointerLockControls 模式(FPS 游戏)
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
const hits = raycaster.intersectObjects(enemies);
if (hits.length > 0) {
  hits[0].object.userData.hp -= 25;
}

12.3 性能优化

// ① 限定层级(不遍历全场景)
const targets = [enemy1, enemy2, ground];
raycaster.intersectObjects(targets, false);

// ② 设置最大距离
raycaster.far = 50;

// ③ Layer 分层
mesh.layers.set(1);
raycaster.layers.set(1);  // 只测试 layer 1 的物体

十三、后处理(EffectComposer)

辉光 / 景深 / 抗锯齿 / 描边 / 像素化等"电影感"特效需要后处理。

import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass }     from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass }from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass }     from 'three/addons/postprocessing/OutputPass.js';

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new UnrealBloomPass(
  new THREE.Vector2(innerWidth, innerHeight),
  1.5,    // strength
  0.4,    // radius
  0.85,   // threshold
));
composer.addPass(new OutputPass());

// 渲染循环里改用 composer
function tick() {
  composer.render();
  requestAnimationFrame(tick);
}

常用 Pass:

Pass 作用
RenderPass 必备,输出场景
UnrealBloomPass 辉光(霓虹/魔法效果)
BokehPass 景深(专业级)
OutlinePass 物体描边(编辑器选中效果)
SMAAPass / FXAAPass 后处理抗锯齿
GlitchPass 故障扰动
FilmPass 老电影颗粒
ShaderPass 自定义 GLSL 滤镜

十四、性能优化清单(生产必备)

14.1 监控工具

import Stats from 'three/addons/libs/stats.module.js';
const stats = new Stats();
document.body.appendChild(stats.dom);

function tick() {
  stats.begin();
  renderer.render(scene, camera);
  stats.end();
  requestAnimationFrame(tick);
}

// 关键指标
console.log(renderer.info);
// {
//   render:   { calls: 23, triangles: 12450, points: 0, lines: 0 },
//   memory:   { geometries: 5, textures: 8 },
//   programs: [...]
// }

生产经验:手机端 draw calls < 100三角面 < 100K 才能稳 60fps。

14.2 13 条优化清单

  1. 合并几何体mergeGeometries,10 个 cube → 1 个 mesh
  2. InstancedMesh:100 万个相同物体也只 1 次 draw call
    const instanced = new THREE.InstancedMesh(geo, mat, 10000);
    const m = new THREE.Matrix4();
    for (let i = 0; i < 10000; i++) {
      m.setPosition(Math.random() * 100, 0, Math.random() * 100);
      instanced.setMatrixAt(i, m);
    }
    
  3. LOD(Level of Detail):远的用简模
    const lod = new THREE.LOD();
    lod.addLevel(highPolyMesh, 0);
    lod.addLevel(midPolyMesh, 50);
    lod.addLevel(lowPolyMesh, 200);
    
  4. frustumCulled:默认开启,相机外的物体不渲染
  5. 冻结静态物体mesh.matrixAutoUpdate = false
  6. 像素比限制renderer.setPixelRatio(Math.min(devicePixelRatio, 2))
  7. 纹理压缩:用 KTX2 / Basis Universal,比 PNG 小 6–10 倍
  8. Draco 压缩 glTF:模型体积减 80%
  9. 关闭不必要的 antialias:用 FXAA/SMAA 后处理代替
  10. 阴影分辨率收敛shadow.mapSize 1024×1024 通常够用
  11. 共享材质:相同材质只创建一次
  12. OffscreenCanvas + Worker:把渲染挪到 Worker(Chrome 已支持)
  13. 节流 resizeresize 事件用 debounce

14.3 内存泄漏防护

function dispose(obj) {
  obj.traverse((c) => {
    if (c.geometry) c.geometry.dispose();
    if (c.material) {
      if (Array.isArray(c.material)) c.material.forEach((m) => m.dispose());
      else c.material.dispose();
    }
    if (c.texture) c.texture.dispose();
  });
}
scene.remove(model);
dispose(model);
renderer.renderLists.dispose();

十五、完整实战:旋转地球 + 星空 + 鼠标交互

来一个能直接上线的小项目:地球 + 5000 颗星星 + 自动旋转 + 点击爆炸特效

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Three.js Earth Demo</title>
<style>
  body { margin: 0; overflow: hidden; background: #000; }
  canvas { display: block; }
  #info {
    position: fixed; top: 16px; left: 16px;
    color: rgba(255,255,255,.7); font: 12px Menlo, monospace;
    pointer-events: none;
  }
</style>
</head>
<body>
<div id="info">🖱 拖拽旋转 · 滚轮缩放 · 点击地球爆炸</div>
<script type="importmap">
{
  "imports": {
    "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
    "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
  }
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

/* ========== 基础三件套 ========== */
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(0, 0, 6);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);

/* ========== 控制器 ========== */
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.minDistance = 3;
controls.maxDistance = 15;

/* ========== 光照 ========== */
scene.add(new THREE.AmbientLight(0xffffff, 0.15));
const sun = new THREE.DirectionalLight(0xffffff, 1.5);
sun.position.set(5, 3, 5);
scene.add(sun);

/* ========== 地球 ========== */
const earthGroup = new THREE.Group();
scene.add(earthGroup);

const loader = new THREE.TextureLoader();
const earthTex = loader.load(
  'https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg'
);
earthTex.colorSpace = THREE.SRGBColorSpace;

const specTex = loader.load(
  'https://threejs.org/examples/textures/planets/earth_specular_2048.jpg'
);

const earth = new THREE.Mesh(
  new THREE.SphereGeometry(1, 64, 64),
  new THREE.MeshPhongMaterial({
    map: earthTex,
    specularMap: specTex,
    shininess: 30,
  })
);
earthGroup.add(earth);

/* ========== 大气层(半透明外壳,制造光晕) ========== */
const atmosphere = new THREE.Mesh(
  new THREE.SphereGeometry(1.05, 64, 64),
  new THREE.ShaderMaterial({
    transparent: true,
    side: THREE.BackSide,
    blending: THREE.AdditiveBlending,
    vertexShader: `
      varying vec3 vNormal;
      void main() {
        vNormal = normalize(normalMatrix * normal);
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      varying vec3 vNormal;
      void main() {
        float intensity = pow(0.65 - dot(vNormal, vec3(0,0,1)), 2.0);
        gl_FragColor = vec4(0.3, 0.6, 1.0, 1.0) * intensity;
      }
    `,
  })
);
earthGroup.add(atmosphere);

/* ========== 星空(5000 个粒子) ========== */
const starGeo = new THREE.BufferGeometry();
const starCount = 5000;
const positions = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
  // 球面均匀分布
  const r = 80 + Math.random() * 20;
  const theta = Math.random() * Math.PI * 2;
  const phi = Math.acos(2 * Math.random() - 1);
  positions[i * 3]     = r * Math.sin(phi) * Math.cos(theta);
  positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
  positions[i * 3 + 2] = r * Math.cos(phi);
}
starGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));

const stars = new THREE.Points(
  starGeo,
  new THREE.PointsMaterial({
    color: 0xffffff,
    size: 0.4,
    sizeAttenuation: true,
    transparent: true,
    opacity: 0.9,
  })
);
scene.add(stars);

/* ========== 点击爆炸特效 ========== */
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const explosions = [];

renderer.domElement.addEventListener('click', (e) => {
  mouse.x = (e.clientX / innerWidth) * 2 - 1;
  mouse.y = -(e.clientY / innerHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);
  const hits = raycaster.intersectObject(earth);
  if (hits.length === 0) return;

  const point = hits[0].point;
  const count = 80;
  const pos = new Float32Array(count * 3);
  const vel = [];
  for (let i = 0; i < count; i++) {
    pos[i * 3] = point.x;
    pos[i * 3 + 1] = point.y;
    pos[i * 3 + 2] = point.z;
    const dir = point.clone().normalize().add(
      new THREE.Vector3(
        (Math.random() - 0.5) * 1.5,
        (Math.random() - 0.5) * 1.5,
        (Math.random() - 0.5) * 1.5
      )
    ).normalize();
    vel.push(dir.multiplyScalar(0.02 + Math.random() * 0.04));
  }
  const g = new THREE.BufferGeometry();
  g.setAttribute('position', new THREE.BufferAttribute(pos, 3));
  const m = new THREE.PointsMaterial({
    color: 0xffaa33,
    size: 0.08,
    transparent: true,
  });
  const p = new THREE.Points(g, m);
  scene.add(p);
  explosions.push({ p, vel, life: 0 });
});

/* ========== 渲染循环 ========== */
const clock = new THREE.Clock();

function tick() {
  const dt = clock.getDelta();

  earthGroup.rotation.y += 0.05 * dt;       // 地球自转
  stars.rotation.y      -= 0.005 * dt;       // 星空反向缓慢转

  // 更新爆炸粒子
  for (let i = explosions.length - 1; i >= 0; i--) {
    const ex = explosions[i];
    ex.life += dt;
    const arr = ex.p.geometry.attributes.position.array;
    for (let j = 0; j < ex.vel.length; j++) {
      arr[j * 3]     += ex.vel[j].x;
      arr[j * 3 + 1] += ex.vel[j].y;
      arr[j * 3 + 2] += ex.vel[j].z;
    }
    ex.p.geometry.attributes.position.needsUpdate = true;
    ex.p.material.opacity = Math.max(0, 1 - ex.life / 1.5);
    if (ex.life > 1.5) {
      scene.remove(ex.p);
      ex.p.geometry.dispose();
      ex.p.material.dispose();
      explosions.splice(i, 1);
    }
  }

  controls.update();
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
tick();

/* ========== 自适应 ========== */
addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>

把它保存为 earth.html 双击打开 —— 你已经有一个能展示给老板看的 3D Demo 了 🌍。


十六、生态与下一步学习路线

16.1 上层封装框架

框架 特色 推荐人群
React Three Fiber (R3F) 用 JSX 写 3D React 用户首选
TresJS Vue 版 R3F Vue 用户
Drei R3F 工具库(控制器、相机、辅助) 配合 R3F 用
Theatre.js 时间线动画编辑器 做交互动画
Babylon.js 微软出品,更"游戏引擎"化 做 Web 游戏
A-Frame HTML 标签写 VR WebXR 入门
PlayCanvas 商业级 Web 引擎 大型在线游戏

16.2 R3F 一瞥

import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';

function Box() {
  return (
    <mesh rotation={[0.4, 0.2, 0]}>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color="hotpink" />
    </mesh>
  );
}

export default function App() {
  return (
    <Canvas camera={{ position: [3, 3, 3] }}>
      <ambientLight intensity={0.3} />
      <directionalLight position={[5, 5, 5]} />
      <Box />
      <OrbitControls />
    </Canvas>
  );
}

声明式写法 + 组件化 —— 这是未来。

16.3 学习资源

  • 📖 官网threejs.org — 必看 Examples(500+ 案例源码)
  • 🎓 Three.js Journey(Bruno Simon)— 业界公认最好的付费课程
  • 📺 YouTube:Bruno Simon / Wawa Sensei
  • 📚 《WebGL 编程指南》 — 想搞懂底层必读
  • 🎨 shadertoy.com — Shader 灵感库
  • 🎁 polyhaven.com — 免费高质量 HDRI / 模型 / 贴图
  • 🎬 sketchfab.com — 模型素材市场

16.4 进阶路线

入门 ✅(你在这)
  ↓
熟悉 r3f / drei → 用 React 工作流做交互
  ↓
学 GLSL Shader → 写自定义视觉效果
  ↓
学 Blender / 模型优化 → 自己产出资产
  ↓
WebXR / WebGPU → 下一代 Web 3D

写在最后

Three.js 的强大不在于"能做什么",而在于它把图形学的入门门槛降到了 4 行代码

但同时,它也是一座冰山 —— 90% 的功能藏在 Examples 文件夹里。遇到问题第一件事不是 Google,而是去 threejs.org/examples 翻 5 分钟,多半能找到答案。

「别担心做不出像 Bruno Simon 那样的作品,每个 Three.js 大佬都是从一个旋转的立方体开始的。」

愿你也能在 Web 里造出自己的星辰大海 🌌。


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐