Skip to content

three.js

Three.js 是一个跨浏览器 JavaScript 库和应用程序接口(API),用于在 Web 浏览器中创建和显示动画 3D 计算机图形。

官方网站

简介

Three.js 封装了 WebGL 的底层复杂性,提供了简洁直观的 API,使开发者能够轻松创建 3D 场景。它广泛应用于以下领域:

  • 网页游戏:2D/3D 网页游戏开发
  • 数据可视化:3D 数据展示和交互
  • 虚拟现实:VR/AR 应用开发
  • 产品展示:3D 产品展示和配置器
  • 建筑可视化:建筑和室内设计展示
  • 教育应用:交互式 3D 教学演示

核心概念

1. Scene(场景)

场景是所有 3D 对象的容器,类似于虚拟世界。

javascript
import * as THREE from 'three';

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb); // 设置天空蓝背景
scene.fog = new THREE.Fog(0x87ceeb, 1, 1000); // 添加雾效果

2. Camera(相机)

相机决定观察 3D 场景的视角。

透视相机(PerspectiveCamera)

javascript
const camera = new THREE.PerspectiveCamera(
  75, // 视野角度(FOV)
  window.innerWidth / window.innerHeight, // 宽高比
  0.1, // 近裁剪面
  1000 // 远裁剪面
);
camera.position.set(0, 5, 10); // 设置相机位置
camera.lookAt(0, 0, 0); // 相机看向原点

正交相机(OrthographicCamera)

javascript
const frustumSize = 10;
const aspect = window.innerWidth / window.innerHeight;
const camera = new THREE.OrthographicCamera(
  frustumSize * aspect / -2,
  frustumSize * aspect / 2,
  frustumSize / 2,
  frustumSize / -2,
  1,
  1000
);

3. Renderer(渲染器)

渲染器负责将场景渲染到 HTML 元素中。

javascript
const renderer = new THREE.WebGLRenderer({
  antialias: true, // 开启抗锯齿
  alpha: true // 允许透明背景
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio); // 支持高DPI屏幕
renderer.shadowMap.enabled = true; // 开启阴影
document.body.appendChild(renderer.domElement);

基本几何体

1. 基础几何体

javascript
// 立方体
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
const boxMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const box = new THREE.Mesh(boxGeometry, boxMaterial);

// 球体
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
const sphereMaterial = new THREE.MeshStandardMaterial({
  color: 0xff0000,
  roughness: 0.5,
  metalness: 0.5
});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);

// 平面
const planeGeometry = new THREE.PlaneGeometry(10, 10);
const planeMaterial = new THREE.MeshStandardMaterial({
  color: 0x808080,
  side: THREE.DoubleSide
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2; // 旋转水平

// 圆环
const torusGeometry = new THREE.TorusGeometry(1, 0.3, 16, 100);
const torusMaterial = new THREE.MeshPhongMaterial({ color: 0x0000ff });
const torus = new THREE.Mesh(torusGeometry, torusMaterial);

// 圆锥体
const coneGeometry = new THREE.ConeGeometry(1, 2, 32);
const coneMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 });
const cone = new THREE.Mesh(coneGeometry, coneMaterial);

// 圆柱体
const cylinderGeometry = new THREE.CylinderGeometry(1, 1, 2, 32);
const cylinderMaterial = new THREE.MeshToonMaterial({ color: 0xff00ff });
const cylinder = new THREE.Mesh(cylinderGeometry, cylinderMaterial);

2. 几何体操作

javascript
// 获取几何体尺寸
const box = new THREE.Mesh(
  new THREE.BoxGeometry(2, 3, 4),
  new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
console.log(box.geometry.parameters); // { width: 2, height: 3, depth: 4 }

// 合并几何体
import { BufferGeometryUtils } from 'three/examples/jsm/utils/BufferGeometryUtils.js';

const geometries = [];
geometries.push(new THREE.BoxGeometry(1, 1, 1).translate(-1, 0, 0));
geometries.push(new THREE.BoxGeometry(1, 1, 1).translate(1, 0, 0));
const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries);
const mergedMesh = new THREE.Mesh(mergedGeometry, new THREE.MeshBasicMaterial({ color: 0x00ff00 }));

材质类型

javascript
// 基础材质 - 不受光照影响
const basicMaterial = new THREE.MeshBasicMaterial({
  color: 0xff0000,
  wireframe: false,
  transparent: true,
  opacity: 0.8
});

// 标准材质 - 基于物理的渲染
const standardMaterial = new THREE.MeshStandardMaterial({
  color: 0x00ff00,
  roughness: 0.2, // 粗糙度
  metalness: 0.8, // 金属度
  map: textureLoader.load('texture.jpg'), // 纹理贴图
  normalMap: textureLoader.load('normal.jpg'), // 法线贴图
  displacementMap: textureLoader.load('height.jpg') // 置换贴图
});

// Phong 材质 - 镜面高光
const phongMaterial = new THREE.MeshPhongMaterial({
  color: 0x0000ff,
  emissive: 0x111111, // 自发光
  specular: 0xffffff, // 高光颜色
  shininess: 30 // 高光强度
});

// Lambert 材质 - 简单光照模型
const lambertMaterial = new THREE.MeshLambertMaterial({
  color: 0xffff00,
  emissive: 0x000000
});

// Toon 材质 - 卡通风格
const toonMaterial = new THREE.MeshToonMaterial({
  color: 0xff6600,
  gradientMap: gradientTexture
});

光照系统

1. 环境光

javascript
const ambientLight = new THREE.AmbientLight(0x404040, 0.5); // 颜色, 强度
scene.add(ambientLight);

2. 平行光

javascript
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 7);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);

3. 点光源

javascript
const pointLight = new THREE.PointLight(0xff0000, 1, 100);
pointLight.position.set(0, 5, 0);
pointLight.castShadow = true;
scene.add(pointLight);

// 可视化点光源位置
const pointLightHelper = new THREE.PointLightHelper(pointLight, 0.5);
scene.add(pointLightHelper);

4. 聚光灯

javascript
const spotLight = new THREE.SpotLight(0xffffff, 1);
spotLight.position.set(0, 10, 0);
spotLight.angle = Math.PI / 6; // 聚光灯角度
spotLight.penumbra = 0.3; // 边缘柔化
spotLight.decay = 2; // 衰减
spotLight.distance = 50; // 距离
spotLight.castShadow = true;
scene.add(spotLight);

5. 半球光

javascript
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.5);
hemisphereLight.position.set(0, 20, 0);
scene.add(hemisphereLight);

纹理贴图

javascript
const textureLoader = new THREE.TextureLoader();

// 加载单个纹理
const texture = textureLoader.load('path/to/texture.jpg');
texture.wrapS = THREE.RepeatWrapping; // 水平重复
texture.wrapT = THREE.RepeatWrapping; // 垂直重复
texture.repeat.set(2, 2); // 重复次数
texture.offset.set(0, 0); // 偏移
texture.rotation = Math.PI / 4; // 旋转

// 加载立方体贴图(天空盒)
const cubeTextureLoader = new THREE.CubeTextureLoader();
const cubeTexture = cubeTextureLoader.load([
  'px.jpg', 'nx.jpg',
  'py.jpg', 'ny.jpg',
  'pz.jpg', 'nz.jpg'
]);
scene.background = cubeTexture;

// 数据纹理
const data = new Uint8Array([
  255, 0, 0, 255,
  0, 255, 0, 255,
  0, 0, 255, 255,
  255, 255, 0, 255
]);
const dataTexture = new THREE.DataTexture(data, 2, 2, THREE.RGBAFormat);

动画与交互

1. 动画循环

javascript
function animate() {
  requestAnimationFrame(animate);

  // 旋转立方体
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;

  // 移动球体
  sphere.position.y = Math.sin(Date.now() * 0.001) * 2;

  renderer.render(scene, camera);
}
animate();

2. 鼠标交互

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

function onMouseMove(event) {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}

function onClick(event) {
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(scene.children);

  intersects.forEach((intersect) => {
    intersect.object.material.color.set(0xff0000);
  });
}

window.addEventListener('mousemove', onMouseMove);
window.addEventListener('click', onClick);

3. 轨道控制器

javascript
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 启用阻尼效果
controls.dampingFactor = 0.05;
controls.enableZoom = true; // 启用缩放
controls.enablePan = true; // 启用平移
controls.autoRotate = true; // 自动旋转
controls.autoRotateSpeed = 2.0;
controls.minDistance = 5; // 最小距离
controls.maxDistance = 50; // 最大距离

4. 补间动画

javascript
import TWEEN from '@tweenjs/tween.js';

const position = { x: 0 };
new TWEEN.Tween(position)
  .to({ x: 10 }, 1000)
  .easing(TWEEN.Easing.Quadratic.Out)
  .onUpdate(() => {
    cube.position.x = position.x;
  })
  .start();

function animateWithTween() {
  requestAnimationFrame(animateWithTween);
  TWEEN.update();
  renderer.render(scene, camera);
}

粒子系统

1. 基础粒子

javascript
const particleCount = 1000;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);

for (let i = 0; i < particleCount; i++) {
  positions[i * 3] = (Math.random() - 0.5) * 100; // x
  positions[i * 3 + 1] = (Math.random() - 0.5) * 100; // y
  positions[i * 3 + 2] = (Math.random() - 0.5) * 100; // z

  colors[i * 3] = Math.random(); // r
  colors[i * 3 + 1] = Math.random(); // g
  colors[i * 3 + 2] = Math.random(); // b
}

geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

const particles = new THREE.Points(
  geometry,
  new THREE.PointsMaterial({
    size: 0.5,
    vertexColors: true,
    transparent: true,
    opacity: 0.8
  })
);
scene.add(particles);

2. 精灵贴图

javascript
import { Sprite } from 'three';
import { SpriteMaterial } from 'three';

const spriteTexture = textureLoader.load('particle.png');
const spriteMaterial = new THREE.SpriteMaterial({ map: spriteTexture });
const sprite = new THREE.Sprite(spriteMaterial);
sprite.scale.set(2, 2, 1);
sprite.position.set(0, 5, 0);
scene.add(sprite);

后期处理

javascript
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';

const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

// 辉光效果
const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  1.5, // 强度
  0.4, // 半径
  0.85 // 阈值
);
composer.addPass(bloomPass);

// 在动画循环中使用
function animate() {
  requestAnimationFrame(animate);
  composer.render();
}

加载器

javascript
// GLTF 加载器
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

const gltfLoader = new GLTFLoader();
gltfLoader.load(
  'model.gltf',
  (gltf) => {
    const model = gltf.scene;
    model.scale.set(1, 1, 1);
    scene.add(model);

    // 动画混合器
    const mixer = new THREE.AnimationMixer(model);
    gltf.animations.forEach((clip) => {
      mixer.clipAction(clip).play();
    });
  },
  (xhr) => {
    console.log((xhr.loaded / xhr.total * 100) + '% loaded');
  },
  (error) => {
    console.error('An error happened:', error);
  }
);

// OBJ 加载器
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';

const objLoader = new OBJLoader();
objLoader.load(
  'model.obj',
  (object) => {
    scene.add(object);
  }
);

// 材质加载器
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js';

const mtlLoader = new MTLLoader();
mtlLoader.load('materials.mtl', (materials) => {
  materials.preload();
  objLoader.setMaterials(materials);
  objLoader.load('model.obj', (object) => {
    scene.add(object);
  });
});

窗口自适应

javascript
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  composer.setSize(window.innerWidth, window.innerHeight);
}

window.addEventListener('resize', onWindowResize);

性能优化

  1. 对象池:重复使用对象,避免频繁创建销毁
  2. 实例化渲染:使用 InstancedMesh 渲染大量相同物体
  3. LOD(细节层次):根据距离切换不同精度的模型
  4. 纹理压缩:使用 KTX2、DDS 等压缩格式
  5. 按需加载:使用代码分割和懒加载
  6. 渲染裁剪:设置合理的裁剪平面
javascript
// 实例化渲染示例
const count = 1000;
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);

const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
  dummy.position.set(
    (Math.random() - 0.5) * 100,
    (Math.random() - 0.5) * 100,
    (Math.random() - 0.5) * 100
  );
  dummy.rotation.set(
    Math.random() * Math.PI,
    Math.random() * Math.PI,
    Math.random() * Math.PI
  );
  dummy.scale.setScalar(Math.random() * 2 + 0.5);
  dummy.updateMatrix();
  instancedMesh.setMatrixAt(i, dummy.matrix);
}

scene.add(instancedMesh);

常用辅助工具

javascript
import {
  AxesHelper,
  GridHelper,
  BoxHelper,
  CameraHelper,
  DirectionalLightHelper,
  HemisphereLightHelper,
  SkeletonHelper,
  VertexNormalsHelper,
  FaceNormalsHelper
} from 'three';

// 坐标轴辅助
const axesHelper = new AxesHelper(5);
scene.add(axesHelper);

// 网格辅助
const gridHelper = new GridHelper(20, 20, 0x888888, 0x444444);
scene.add(gridHelper);

// 包围盒辅助
const boxHelper = new THREE.BoxHelper(cube, 0xffff00);
scene.add(boxHelper);

// 法线辅助
const normalsHelper = new VertexNormalsHelper(cube, 0.5, 0xff0000);
scene.add(normalsHelper);

完整示例

javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

function init() {
  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x1a1a2e);

  const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  );
  camera.position.set(3, 3, 5);

  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.shadowMap.enabled = true;
  document.body.appendChild(renderer.domElement);

  const controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;

  const ambientLight = new THREE.AmbientLight(0x404040);
  scene.add(ambientLight);

  const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
  directionalLight.position.set(5, 10, 7);
  directionalLight.castShadow = true;
  scene.add(directionalLight);

  const cube = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshStandardMaterial({ color: 0x00ff88 })
  );
  cube.castShadow = true;
  scene.add(cube);

  const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(10, 10),
    new THREE.MeshStandardMaterial({ color: 0x333333 })
  );
  plane.rotation.x = -Math.PI / 2;
  plane.receiveShadow = true;
  scene.add(plane);

  const gridHelper = new THREE.GridHelper(10, 10);
  scene.add(gridHelper);

  function animate() {
    requestAnimationFrame(animate);

    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

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

  animate();

  window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  });
}

init();