为什么选择 Three.js?

在数据可视化项目中,传统的 2D 图表已经无法满足需求。我们需要:

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 最佳实践

  1. 从简单开始 - 先实现基础功能,再逐步增强
  2. 性能优先 - 使用 BufferGeometry、对象池等技术
  3. 渐进式加载 - 分批加载资源,避免阻塞
  4. 响应式设计 - 适配不同屏幕尺寸
  5. 优雅降级 - 低性能设备自动降低效果

Three.js 很强大,但也很容易滥用。记住:炫酷的 3D 效果应该服务于数据展示,而不是喧宾夺主。