Three.js 飞线可视化教程:从基础到进阶

547 阅读4分钟

1. 什么是飞线图?

飞线图(Flow Map)是一种数据可视化方式,用于展示从一个点到另一个点的流动关系和强度。在三维空间中,飞线图可以更直观地展示地理数据、网络流量或粒子运动等信息。

2. Three.js 基础准备

首先需要引入 Three.js 库:

<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>

创建基本场景:

// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 添加轨道控制器,使场景可以交互
const controls = new THREE.OrbitControls(camera, renderer.domElement);
// 动画循环
function animate() {
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, camera);
}
animate();

3. 创建基本飞线

3.1 使用 Line 几何体

// 创建飞线路径点
const points = [];
points.push(new THREE.Vector3(-2, 0, 0));
points.push(new THREE.Vector3(0, 2, 0));
points.push(new THREE.Vector3(2, 0, 0));
// 创建几何体
const geometry = new THREE.BufferGeometry().setFromPoints(points);
// 创建材质
const material = new THREE.LineBasicMaterial({ color: 0x0000ff });
// 创建线对象
const line = new THREE.Line(geometry, material);
// 添加到场景
scene.add(line);

3.2 使用 BufferGeometry 优化性能

对于大量飞线,使用 BufferGeometry 更高效:

// 创建顶点数据
const vertices = new Float32Array([
    -2, 0, 0,  // 点1
    0, 2, 0,   // 点2
    2, 0, 0    // 点3
]);
// 创建几何体
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
// 创建线
const line = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0x0000ff }));
scene.add(line);

4. 实现飞线动画效果

4.1 使用纹理和着色器

// 创建材质
const material = new THREE.ShaderMaterial({
    uniforms: {
        u_time: { value: 0.0 },
        u_speed: { value: 2.0 },
        u_width: { value: 0.05 },
        u_color: { value: new THREE.Color(0x00ffff) }
    },
    vertexShader: `
        varying vec2 vUv;
        void main() {
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        uniform float u_time;
        uniform float u_speed;
        uniform float u_width;
        uniform vec3 u_color;
        varying vec2 vUv;
        
        void main() {
            // 创建流动效果
            float alpha = smoothstep(u_width, 0.0, abs(vUv.y - 0.5));
            float offset = fract(vUv.x - u_time * u_speed);
            alpha *= smoothstep(0.2, 0.0, offset) * smoothstep(0.0, 0.2, offset + 0.3);
            
            gl_FragColor = vec4(u_color, alpha);
        }
    `,
    side: THREE.DoubleSide,
    transparent: true
});
// 创建带宽度的线几何体
const geometry = new THREE.PlaneGeometry(1, 0.1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 在动画循环中更新时间
function animate() {
    requestAnimationFrame(animate);
    material.uniforms.u_time.value += 0.01;
    renderer.render(scene, camera);
}
animate();

4.2 使用 TubeGeometry 创建管状飞线

// 创建路径曲线
const curve = new THREE.CatmullRomCurve3([
    new THREE.Vector3(-2, 0, 0),
    new THREE.Vector3(0, 2, 0),
    new THREE.Vector3(2, 0, 0),
    new THREE.Vector3(0, -2, 0)
]);
curve.curveType = 'catmullrom';
curve.tension = 0.5;
// 创建管道几何体
const geometry = new THREE.TubeGeometry(curve, 64, 0.05, 8, false);
// 创建材质
const material = new THREE.MeshBasicMaterial({ color: 0x00ffff, wireframe: false });
// 创建网格
const tube = new THREE.Mesh(geometry, material);
scene.add(tube);

5. 飞线数据可视化应用

5.1 地理数据飞线

// 假设我们有一组地理坐标点
const geoPoints = [
    { lng: -74.0060, lat: 40.7128 }, // 纽约
    { lng: 2.3522, lat: 48.8566 },   // 巴黎
    { lng: 116.4074, lat: 39.9042 }  // 北京
];
// 将地理坐标转换为三维坐标
function geoTo3D(lng, lat, radius = 3) {
    const phi = (90 - lat) * Math.PI / 180;
    const theta = (lng + 180) * Math.PI / 180;
    
    return new THREE.Vector3(
        -(radius * Math.sin(phi) * Math.cos(theta)),
        radius * Math.cos(phi),
        radius * Math.sin(phi) * Math.sin(theta)
    );
}
// 创建地球模型
const earthGeometry = new THREE.SphereGeometry(3, 64, 64);
const earthMaterial = new THREE.MeshBasicMaterial({ color: 0x336699 });
const earth = new THREE.Mesh(earthGeometry, earthMaterial);
scene.add(earth);
// 创建飞线
for (let i = 0; i < geoPoints.length; i++) {
    for (let j = i + 1; j < geoPoints.length; j++) {
        const p1 = geoTo3D(geoPoints[i].lng, geoPoints[i].lat);
        const p2 = geoTo3D(geoPoints[j].lng, geoPoints[j].lat);
        
        // 创建曲线
        const curve = new THREE.QuadraticBezierCurve3(
            p1,
            new THREE.Vector3(0, 1, 0), // 控制点
            p2
        );
        
        // 创建飞线
        const points = curve.getPoints(50);
        const geometry = new THREE.BufferGeometry().setFromPoints(points);
        const material = new THREE.LineBasicMaterial({ color: 0x00ffff });
        const line = new THREE.Line(geometry, material);
        scene.add(line);
    }
}

6. 性能优化与高级技巧

6.1 使用 Points 代替 Line

// 创建大量点
const count = 1000;
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
// 初始化点位置和颜色
for (let i = 0; i < count; i++) {
    const i3 = i * 3;
    positions[i3] = (Math.random() - 0.5) * 10;
    positions[i3 + 1] = (Math.random() - 0.5) * 10;
    positions[i3 + 2] = (Math.random() - 0.5) * 10;
    
    colors[i3] = Math.random();
    colors[i3 + 1] = Math.random();
    colors[i3 + 2] = Math.random();
}
// 创建几何体
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
// 创建材质
const material = new THREE.PointsMaterial({
    size: 0.1,
    vertexColors: true
});
// 创建点云
const points = new THREE.Points(geometry, material);
scene.add(points);

6.2 使用 InstancedMesh 优化大量飞线

// 创建单个飞线几何体
const lineGeometry = new THREE.BoxGeometry(1, 0.05, 0.05);
// 创建材质
const material = new THREE.MeshBasicMaterial({ color: 0x00ffff });
// 创建实例化网格 (1000个实例)
const instances = 1000;
const instancedMesh = new THREE.InstancedMesh(lineGeometry, material, instances);
scene.add(instancedMesh);
// 设置每个实例的位置和旋转
const matrix = new THREE.Matrix4();
const position = new THREE.Vector3();
const rotation = new THREE.Euler();
const scale = new THREE.Vector3(1, 1, 1);
for (let i = 0; i < instances; i++) {
    // 随机位置和方向
    position.set(
        (Math.random() - 0.5) * 10,
        (Math.random() - 0.5) * 10,
        (Math.random() - 0.5) * 10
    );
    
    rotation.set(
        Math.random() * Math.PI,
        Math.random() * Math.PI,
        Math.random() * Math.PI
    );
    
    matrix.compose(position, new THREE.Quaternion().setFromEuler(rotation), scale);
    instancedMesh.setMatrixAt(i, matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;

7. 常见问题与解决方案

  1. 性能问题:当飞线数量超过 1000 条时,考虑使用 InstancedMesh 或自定义着色器
  1. 渲染顺序:使用 material.depthTest = false 可以让飞线始终显示在其他物体上方
  1. 飞线相交问题:在复杂场景中,飞线可能会相互遮挡,可以通过调整材质的透明度和深度测试来解决
  1. 移动设备兼容性:在移动设备上,复杂的飞线动画可能会导致性能下降,建议简化着色器或减少飞线数量

通过以上方法,你可以在 Three.js 中实现各种复杂的飞线可视化效果,从简单的数据展示到复杂的地理信息系统都可以轻松应对。不断练习和尝试不同的参数设置,你将能够创建出令人印象深刻的三维数据可视化作品。