Appearance
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);性能优化
- 对象池:重复使用对象,避免频繁创建销毁
- 实例化渲染:使用 InstancedMesh 渲染大量相同物体
- LOD(细节层次):根据距离切换不同精度的模型
- 纹理压缩:使用 KTX2、DDS 等压缩格式
- 按需加载:使用代码分割和懒加载
- 渲染裁剪:设置合理的裁剪平面
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();