为什么选择 Three.js?
在数据可视化项目中,传统的 2D 图表已经无法满足需求。我们需要:
- 更强的视觉冲击力 - 3D 效果更震撼
- 更丰富的交互 - 旋转、缩放、悬停
- 更好的空间感 - 展示多维数据关系
- 更高的技术含量 - 提升产品竞争力
Three.js 是最成熟的 WebGL 库,有完善的文档和社区支持。
第一步:基础场景搭建
创建一个基本的 Three.js 场景需要:场景(Scene)、摄像机(Camera)、渲染器(Renderer)。
// 初始化场景
const scene = new THREE.Scene();
// 创建摄像机
const camera = new THREE.PerspectiveCamera(
75, // 视角
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近裁剪面
1000 // 远裁剪面
);
// 创建渲染器
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('canvas'),
alpha: true, // 透明背景
antialias: true // 抗锯齿
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
第二步:粒子系统
粒子系统是 3D 可视化的核心。我们可以用它展示数据流动、网络拓扑等。
2.1 创建粒子
const particleCount = 300;
const positions = new Float32Array(particleCount * 3);
const velocities = [];
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() * -500; // z
// 速度(用于动画)
velocities.push({
x: (Math.random() - 0.5) * 0.2,
y: (Math.random() - 0.5) * 0.2,
z: Math.random() * 2 + 1
});
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.PointsMaterial({
color: 0xff3d00,
size: 0.5,
transparent: true,
opacity: 0.6
});
const particles = new THREE.Points(geometry, material);
scene.add(particles);
2.2 粒子动画
function animate() {
requestAnimationFrame(animate);
const positions = particles.geometry.attributes.position.array;
for (let i = 0; i < particleCount; i++) {
positions[i * 3] += velocities[i].x;
positions[i * 3 + 1] += velocities[i].y;
positions[i * 3 + 2] += velocities[i].z;
// 如果粒子飞过摄像机,重置到远处
if (positions[i * 3 + 2] > 50) {
positions[i * 3] = (Math.random() - 0.5) * 100;
positions[i * 3 + 1] = (Math.random() - 0.5) * 100;
positions[i * 3 + 2] = -500;
}
}
particles.geometry.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
}
animate();
💡 性能优化技巧
- 减少粒子数量 - 300-500 个粒子足够震撼
- 使用 BufferGeometry - 比 Geometry 性能提升 10 倍
- 限制更新频率 - 不需要每帧都更新所有粒子
- 使用着色器材质 - ShaderMaterial 比 PointsMaterial 更高效
第三步:几何体可视化
除了粒子,我们还可以用几何体展示数据。
3.1 动态柱状图
// 创建柱状图
const data = [10, 25, 15, 30, 20];
const bars = [];
data.forEach((value, index) => {
const geometry = new THREE.BoxGeometry(2, value, 2);
const material = new THREE.MeshBasicMaterial({
color: 0xff3d00,
transparent: true,
opacity: 0.8
});
const bar = new THREE.Mesh(geometry, material);
bar.position.x = (index - data.length / 2) * 5;
bar.position.y = value / 2;
scene.add(bar);
bars.push(bar);
});
3.2 3D 网络图
// 创建节点
const nodes = [];
const nodeCount = 20;
for (let i = 0; i < nodeCount; i++) {
const geometry = new THREE.SphereGeometry(0.5, 16, 16);
const material = new THREE.MeshBasicMaterial({ color: 0xff3d00 });
const node = new THREE.Mesh(geometry, material);
node.position.set(
(Math.random() - 0.5) * 50,
(Math.random() - 0.5) * 50,
(Math.random() - 0.5) * 50
);
scene.add(node);
nodes.push(node);
}
// 创建连线
const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff, opacity: 0.3 });
for (let i = 0; i < nodes.length; i++) {
const connectedNodes = Math.floor(Math.random() * 3) + 1;
for (let j = 0; j < connectedNodes; j++) {
const targetIndex = Math.floor(Math.random() * nodes.length);
const geometry = new THREE.BufferGeometry().setFromPoints([
nodes[i].position,
nodes[targetIndex].position
]);
const line = new THREE.Line(geometry, lineMaterial);
scene.add(line);
}
}
第四步:交互增强
4.1 鼠标悬停检测
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(nodes);
// 重置所有节点颜色
nodes.forEach(node => {
node.material.color.setHex(0xff3d00);
});
// 高亮悬停的节点
if (intersects.length > 0) {
intersects[0].object.material.color.setHex(0xffffff);
}
});
4.2 摄像机控制
// 使用 OrbitControls
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 惯性
controls.dampingFactor = 0.05;
controls.enableZoom = true;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.5;
第五步:性能优化
5.1 按需渲染
let needsUpdate = true;
function animate() {
if (needsUpdate) {
renderer.render(scene, camera);
needsUpdate = false;
}
requestAnimationFrame(animate);
}
// 只在场景变化时更新
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
needsUpdate = true;
});
5.2 对象池
class ParticlePool {
constructor(size) {
this.pool = [];
this.active = [];
for (let i = 0; i < size; i++) {
const particle = this.createParticle();
this.pool.push(particle);
}
}
get() {
if (this.pool.length > 0) {
const particle = this.pool.pop();
this.active.push(particle);
return particle;
}
return null;
}
release(particle) {
const index = this.active.indexOf(particle);
if (index > -1) {
this.active.splice(index, 1);
this.pool.push(particle);
}
}
}
实际应用案例
📊 项目数据
- 粒子数量:300 个
- 帧率:稳定 60fps
- 内存占用:< 50MB
- 加载时间:< 2s
- 浏览器兼容:Chrome、Firefox、Safari、Edge
总结:Three.js 最佳实践
- 从简单开始 - 先实现基础功能,再逐步增强
- 性能优先 - 使用 BufferGeometry、对象池等技术
- 渐进式加载 - 分批加载资源,避免阻塞
- 响应式设计 - 适配不同屏幕尺寸
- 优雅降级 - 低性能设备自动降低效果
Three.js 很强大,但也很容易滥用。记住:炫酷的 3D 效果应该服务于数据展示,而不是喧宾夺主。