01
文章背景
在我们负责的某个项目中有一个需求,其展馆的可视化大屏首屏需要 3D 地球作为载体,来展示各国贸易额的信息,因此催生了 3D 地球的需求。需求的侧重点更多在于美观,动态方面,对于数据展示层面的需求反而并不多。
基于这个3D地球的实现有了这篇文章。

最终呈现效果展示
02
实现方式
一、方案
通过 Three.js 画出球体
将准备好的地球纹理材质包导入并覆盖在球体上形成地球
通过设置光源形成光暗变化
通过旋转球体使其运动
飞线实现
二、实现细则
//Step 1 创建地球
var geometry = new THREE.SphereGeometry(radius,radius,radius); // 创建球体模型
var loader = new THREE.TextureLoader(); // 创建资源加载器
loader.load('./map.png', function(texture){ // 加载地球材质图片 参数为回调函数
var material = new THREE.MeshLambertMaterial( { map: texture } ); // 创建面材质并加入到屏中
global = new THREE.Mesh(geometry, material)
scene.add( global )
})
//Step 2 设置光源
var point = new THREE.PointLight(0xffffff);
point.position.set(400, 200, 300);
scene.add(point);
var ambient = new THREE.AmbientLight(0x444444);
scene.add(ambient);
point为点光源,特点是从一点开始往不同方向发散且不产生阴影。一般用来作为主光源。ambient为散射光,即笼罩在环境的光,一般用来作为主色调。
详情可参考:初识ThreeJS中常见的光源
(https://zhuanlan.zhihu.com/p/25262894)
//Step 3 动画实现
function animate() {
requestAnimationFrame(animate);
if (global) {
global.rotation.y += 0.005;
}
renderer.render(scene, camera);
}
通过不断的增加球体的 y 轴使其不停的逆时针旋转
//Step 4 飞线实现
飞线的实现较为复杂,且方案较多,本文提供的方案并不一定是最优解,仅作为一种思路参考
1、数据准备
//mock数据
function randomLine(lineNum) {
const mockData = [];
Array.from({ length: lineNum }, (v, k) => {
mockData.push({
line: [randomPos(), randomPos()],
});
});
return mockData;
}
function randomPos() {
return [Math.random() * 360 - 180, Math.random() * 180 - 90];
}
randomPos 可以随机生成球体上的经纬度点,两点则生成一条线的基本轨迹。
function createPosition(lnglat) {
const spherical = new THREE.Spherical();
spherical.radius = 60;
const lng = lnglat[0];
const lat = lnglat[1];
const theta = (lng + 90) * (Math.PI / 180);
const phi = (90 - lat) * (Math.PI / 180);
spherical.phi = phi;
spherical.theta = theta;
const position = new THREE.Vector3();
position.setFromSpherical(spherical);
return position;
}
此方法将经纬度转换为屏幕中的空间坐标,实现方式是通过 Three.js 提供的setFromSpherical方法,提供球体半径以及经纬度则可以计算出空间坐标。
2、弧线计算
对于一条线来分析,我们得到了线的起点跟终点的坐标还不够。我们需要得到的是从起点到终点的绕着地球表面的弧线。要保证不与地球相交穿模且弧线优美。弧线的生成方法是通过 Three.js 提供的CatmullRomCurve3这个生成曲线来实现的,参数是多个表示顶点坐标的 Vector3 对象组成的数组 Array,我们至少要提供 8 个顶点坐标才能保证线条走向与我们的期待值一致。
这里使用的是二分法:
function getSplinePoints(positions) {
const [start, end] = positions;
const points = [];
points[0] = new THREE.Vector3(start.x, start.y, start.z);
points[8] = new THREE.Vector3(end.x, end.y, end.z);
const getPoint = (point1, point2) => {
const tempMiddle = new THREE.Vector3(
0.5 * (point1.x + point2.x),
0.5 * (point1.y + point2.y),
0.5 * (point1.z + point2.z)
);
const tempMiddleLength = Math.sqrt(
tempMiddle.x ** 2 + tempMiddle.y ** 2 + tempMiddle.z ** 2
);
const rate = (radius + 10) / tempMiddleLength;
const middle = new THREE.Vector3(
rate * tempMiddle.x,
rate * tempMiddle.y,
rate * tempMiddle.z
);
return middle;
};
points[4] = getPoint(points[0], points[8]);
points[2] = getPoint(points[0], points[4]);
points[1] = getPoint(points[0], points[2]);
points[3] = getPoint(points[2], points[4]);
points[6] = getPoint(points[4], points[8]);
points[5] = getPoint(points[4], points[6]);
points[7] = getPoint(points[6], points[8]);
return points;
}
点的计算方式是先计算出两点的中点位置,再通过勾股定理计算出球体中心点到中点的斜边长,然后通过球体半径再加上我们设置的飞线距离球体的高度等比延伸计算出我们需要的位置。通过传入的起点和终点先确定第 4 个点的位置,然后通过起点和第四个点确定第二个点位置,依此类推,计算出 8 个点的坐标。然后调用CatmullRomCurve3即可得到弧线。
3、弧线生成
const lineBallList = new THREE.Object3D();
const lineCurve = new THREE.CatmullRomCurve3(
getSplinePoints(positions),
false
);
const lineGeometry = new THREE.BufferGeometry();
const points = lineCurve.getPoints(50);
lineGeometry.setFromPoints(points);
const matericalLine = new THREE.LineBasicMaterial({ color });
const line = new THREE.Line(lineGeometry, matericalLine);
for (let ballIndex = 0; ballIndex < 100; ballIndex += 1) {
const ballGeomtry = new THREE.SphereGeometry(0.3);
const materialBall = new THREE.MeshBasicMaterial({
color,
transparent: true,
opacity: 1 - ballIndex / (100 + 10),
});
const lineBall = new THREE.Mesh(ballGeomtry, materialBall);
lineBall.originOpacity = 1 - ballIndex / (100 + 10);
lineBallList.add(lineBall);
}
lineCurve.getPoints(50)这个方法指定了一条弧线上具体生成多少个点,此方法会返回生成的点集。50 这个数字是需要通过具体需求调试的,对性能以及效果的影响很大。代码中的 color 是一个变量,这样可以实现不同线条颜色不同。最后我们通过循环点,给每一个点加上透明度渐变的材质包,就得到了一段慢慢变淡的拖着长尾巴的彗星状弧线。
4、动起来
function animate() {
requestAnimationFrame(animate);
const lineBalls = globalLineBalls.children;
lineBalls.forEach((balls) => {
const ballCurve = balls.curve;
const ballList = balls.children;
const ballPos = balls.pos;
if (ballPos < 1.2) {
ballList.forEach((ball, index) => {
const newPos =
ballPos - index * 0.002 > 0 ? ballPos - index * 0.002 : 0;
if (newPos >= 1) {
ball.opacity = 0;
} else {
ball.opacity = ball.originOpacity;
const ballPosition = ballCurve.getPointAt(newPos);
ball.position.x = ballPosition.x;
ball.position.y = ballPosition.y;
ball.position.z = ballPosition.z;
}
ball.neepsUpdate = true;
});
balls.pos = ballPos + 0.008;
} else {
balls.pos = 0;
}
});
renderer.render(scene, camera);
}
这里的思路是设置了隐性的参数值pos,通过维护pos的数值大小来决定展示哪些点。
03
总结
通过上述案例,相信大家已经对数据可视化的思路和实现方式有了初步的了解。然而,实际的实现过程往往是复杂的,因为数据可视化涉及到许多数学问题。在解决这些问题时,我们可以尝试先建立模型,建立坐标系来分析具体问题,从而找到解决方案。数据可视化是一个既有挑战性又有创造性的领域,希望通过不断学习和实践,我们能够更好地应对和解决数据可视化中的难题。
“
Hello~
这里是神州数码云基地
编程大法,技术前沿,尽在其中
超多原创技术干货持续输出ing~
想要第一时间获取
超硬技术干货
快快点击关注+设为星标★
拜托拜托啦
这对“我们”都很重要哦~
- END -
往期精选
了解云基地,就现在!

