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. 常见问题与解决方案
- 性能问题:当飞线数量超过 1000 条时,考虑使用 InstancedMesh 或自定义着色器
- 渲染顺序:使用 material.depthTest = false 可以让飞线始终显示在其他物体上方
- 飞线相交问题:在复杂场景中,飞线可能会相互遮挡,可以通过调整材质的透明度和深度测试来解决
- 移动设备兼容性:在移动设备上,复杂的飞线动画可能会导致性能下降,建议简化着色器或减少飞线数量
通过以上方法,你可以在 Three.js 中实现各种复杂的飞线可视化效果,从简单的数据展示到复杂的地理信息系统都可以轻松应对。不断练习和尝试不同的参数设置,你将能够创建出令人印象深刻的三维数据可视化作品。