github仓库地址:https://github.com/RainManGO/3d-earth
npm:https://www.npmjs.com/package/3d-earth
支持vue/react/html 嵌入简单。
实现步骤分解:
接上篇从第七步骤说起
每个城市都是通过坐标准确的添加到地图,那么就涉及到经纬度转球面xyz坐标。
其实就是经纬度转xyz坐标系,这张图可以看明白。
工具函数代码:
export const lon2xyz = (
radius: number,
longitude: number,
latitude: number
) => {
var lon = (longitude * Math.PI) / 180; //转弧度值
var lat = (latitude * Math.PI) / 180; //转弧度值
lon = -lon; // three.js坐标系z坐标轴对应经度-90度,而不是90度
// 经纬度坐标转球面坐标计算公式
var x = radius * Math.cos(lat) * Math.cos(lon);
var y = radius * Math.sin(lat);
var z = radius * Math.cos(lat) * Math.sin(lon);
// 返回球面坐标
return {
x: x,
y: y,
z: z,
};
};
这里城市位置是两个长方形几何体加到地球上,需要调整下姿势。
一个贴图是是涟漪底图可以更改颜色:
var cityGeometry = new PlaneBufferGeometry(1, 1); //默认在XOY平面上
var textureLoader = new TextureLoader(); // TextureLoader创建一个纹理加载器对象
var texture = textureLoader.load(wavePng);
// 如果不同mesh材质的透明度、颜色等属性同一时刻不同,材质不能共享
var cityWaveMaterial = new MeshBasicMaterial({
color: 0x22ffcc,
map: texture,
transparent: true, //使用背景透明的png贴图,注意开启透明计算
opacity: 1.0,
side: DoubleSide, //双面可见
depthWrite: false, //禁止写入深度缓冲区数据
});
//城市点添加
var pointTexture = textureLoader.load(pointPng);
var cityPointMaterial = new MeshBasicMaterial({
color:0xffc300,
map: pointTexture,
transparent: true, //使用背景透明的png贴图,注意开启透明计算
depthWrite:false,//禁止写入深度缓冲区数据
});
var cityWaveMesh = new Mesh(cityGeometry, cityWaveMaterial);
下一步需要调整到地球姿势,贴合球体:
var size = earthRadius * 0.12; //矩形平面Mesh的尺寸
(cityWaveMesh as any).size = size; //自顶一个属性,表示mesh静态大小
cityWaveMesh.scale.set(size, size, size); //设置mesh大小
(cityWaveMesh as any)._s = Math.random() * 1.0 + 1.0; //自定义属性._s表示mesh在原始大小基础上放大倍数 光圈在原来mesh.size基础上1~2倍之间变化
cityWaveMesh.position.set(cityXyz.x, cityXyz.y, cityXyz.z);
cityMesh.position.set(cityXyz.x, cityXyz.y, cityXyz.z)
// mesh姿态设置
// mesh在球面上的法线方向(球心和球面坐标构成的方向向量)
var coordVec3 = new Vector3(cityXyz.x, cityXyz.y, cityXyz.z).normalize();
// mesh默认在XOY平面上,法线方向沿着z轴new THREE.Vector3(0, 0, 1)
var meshNormal = new Vector3(0, 0, 1);
// 四元数属性.quaternion表示mesh的角度状态
//.setFromUnitVectors();计算两个向量之间构成的四元数值
cityWaveMesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);
cityMesh.quaternion.setFromUnitVectors(meshNormal, coordVec3);
这个动画其实就是将几何体大小进行缩放和透明度变化,具体算法代码如下:
export const cityWaveAnimate = (WaveMeshArr: Mesh[]) => {
// 所有波动光圈都有自己的透明度和大小状态
// 一个波动光圈透明度变化过程是:0~1~0反复循环
WaveMeshArr.forEach(function (mesh:any) {
mesh._s += 0.007;
mesh.scale.set(
mesh.size * mesh._s,
mesh.size * mesh._s,
mesh.size * mesh._s
);
if (mesh._s <= 1.5) {
mesh.material.opacity = (mesh._s - 1) * 2; //2等于1/(1.5-1.0),保证透明度在0~1之间变化
} else if (mesh._s > 1.5 && mesh._s <= 2) {
mesh.material.opacity = 1 - (mesh._s - 1.5) * 2; //2等于1/(2.0-1.5) mesh缩放2倍对应0 缩放1.5被对应1
} else {
mesh._s = 1.0;
}
});
};
飞线主要有三种方式
都试了试发现B样条比较好看,使用了这个其他曲线后期会分解
主要思路:
代码如下:
import { FlyData, City } from "../types/index";
import { InitFlyLine } from "../tools/flyLine";
import { lon2xyz } from "../tools/index";
import { earthRadius } from "../config/index";
import { Vector3, CatmullRomCurve3, Object3D } from "three";
import pointPng from "../img/point.png";
export const earthAddFlyLine = (
earth: Object3D,
flyLineData: FlyData[],
cityList: Record<string, City>
) => {
let flyManager: InitFlyLine = null;
if (flyManager == null) {
flyManager = new InitFlyLine({
texture: pointPng,
});
}
for (var i = 0; i < flyLineData.length; i++) {
var flyLine = flyLineData[i];
for (var j = 0; j < flyLine.to.length; j++) {
randomAddFlyLine(
earth,
flyManager,
cityList[flyLine.from],
cityList[flyLine.to[j]],
flyLine.color
);
}
}
return flyManager;
};
// 随机个时间间隔后,再添加连线(以免同时添加连线,显示效果死板)
const randomAddFlyLine = (
earth: Object3D,
flyManager: InitFlyLine,
fromCity: City,
toCity: City,
color: string
) => {
setTimeout(function () {
addFlyLine(earth, flyManager,fromCity, toCity, color);
}, Math.ceil(Math.random() * 15000));
};
// 增加城市之间飞线
const addFlyLine = (
earth: Object3D,
flyManager: InitFlyLine,
fromCity: City,
toCity: City,
color: string
) => {
var coefficient = 1;
var curvePoints = new Array();
var fromXyz = lon2xyz(earthRadius, fromCity.longitude, fromCity.latitude);
var toXyz = lon2xyz(earthRadius, toCity.longitude, toCity.latitude);
curvePoints.push(new Vector3(fromXyz.x, fromXyz.y, fromXyz.z));
//根据城市之间距离远近,取不同个数个点
var distanceDivRadius =
Math.sqrt(
(fromXyz.x - toXyz.x) * (fromXyz.x - toXyz.x) +
(fromXyz.y - toXyz.y) * (fromXyz.y - toXyz.y) +
(fromXyz.z - toXyz.z) * (fromXyz.z - toXyz.z)
) / earthRadius;
var partCount = 3 + Math.ceil(distanceDivRadius * 3);
for (var i = 0; i < partCount; i++) {
var partCoefficient =
coefficient + (partCount - Math.abs((partCount - 1) / 2 - i)) * 0.01;
var partTopXyz = getPartTopPoint(
{
x:
(fromXyz.x * (partCount - i)) / partCount +
(toXyz.x * (i + 1)) / partCount,
y:
(fromXyz.y * (partCount - i)) / partCount +
(toXyz.y * (i + 1)) / partCount,
z:
(fromXyz.z * (partCount - i)) / partCount +
(toXyz.z * (i + 1)) / partCount,
},
earthRadius,
partCoefficient
);
curvePoints.push(new Vector3(partTopXyz.x, partTopXyz.y, partTopXyz.z));
}
curvePoints.push(new Vector3(toXyz.x, toXyz.y, toXyz.z));
//使用B样条,将这些点拟合成一条曲线(这里没有使用贝赛尔曲线,因为拟合出来的点要在地球周围,不能穿过地球)
var curve = new CatmullRomCurve3(curvePoints, false);
//从B样条里获取点
var pointCount = Math.ceil(500 * partCount);
var allPoints = curve.getPoints(pointCount);
//制作飞线动画
// @ts-ignore
var flyMesh = flyManager.addFly({
curve: allPoints, //飞线飞线其实是N个点构成的
color: color, //点的颜色
width: 0.3, //点的半径
length: Math.ceil((allPoints.length * 3) / 5), //飞线的长度(点的个数)
speed: partCount + 10, //飞线的速度
repeat: Infinity, //循环次数
});
earth.add(flyMesh);
};
const getPartTopPoint = (
innerPoint: { x: number; y: number; z: number },
earthRadius: number,
partCoefficient: number
) => {
var fromPartLen = Math.sqrt(
innerPoint.x * innerPoint.x +
innerPoint.y * innerPoint.y +
innerPoint.z * innerPoint.z
);
return {
x: (innerPoint.x * partCoefficient * earthRadius) / fromPartLen,
y: (innerPoint.y * partCoefficient * earthRadius) / fromPartLen,
z: (innerPoint.z * partCoefficient * earthRadius) / fromPartLen,
};
};
旋转动画的原理主要是利用tween 动画,然后更新地球位置和轨道控制器的zoom 。
具体代码如下:
//旋转地球动画
var rotateEarthStep = new TWEEN.Tween({
rotateY: startRotateY,
zoom: startZoom,
})
.to({ rotateY: endRotateY, zoom: endZoom }, 36000) //.to({rotateY: endRotateY, zoom: endZoom}, 10000)
.easing(TWEEN.Easing.Quadratic.Out)
.onUpdate(function (object: any) {
if (that.earth3dObj) {
that.earth3dObj.rotation.set(0, object.rotateY, 0);
}
(that.orbitControl as any).zoom0 = object.zoom < 1 ? 1 : object.zoom;
that.orbitControl.reset();
});
var rotateEarthStepBack = new TWEEN.Tween({
rotateY: endRotateY,
zoom: endZoom,
})
.to({ rotateY: 3.15 * Math.PI * 2, zoom: startZoom }, 36000) //.to({rotateY: endRotateY, zoom: endZoom}, 10000)
.easing(TWEEN.Easing.Quadratic.Out)
.onUpdate(function (object: any) {
if (that.earth3dObj) {
that.earth3dObj.rotation.set(0, object.rotateY, 0);
}
(that.orbitControl as any).zoom0 = object.zoom < 1 ? 1 : object.zoom;
that.orbitControl.reset();
});
rotateEarthStep.chain(rotateEarthStepBack);
rotateEarthStepBack.chain(rotateEarthStep);
rotateEarthStep.start();
}
这样就完成了一个漂亮的地球了,花了半个月的时间的研究成果。开心,希望一直保持coding,热爱分享。