话不多说,先看整体效果,本文主要讲解的地球实现
核心需求
- 地球半透明,可以看到背面
- 点阵式的全球地图
- 根据数据的经纬度生成对应的柱体
- 数值越大,柱体的颜色和高度就越深越长
引入Threejs和D3
<script src="https://cdn.staticfile.org/three.js/r125/three.min.js"></script>
<script src="https://d3js.org/d3.v6.js"></script>
<script src="https://unpkg.com/three@0.125.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://unpkg.com/three@0.125.0/examples/js/utils/BufferGeometryUtils.js"></script>
基本HTML结构
<div id="box" style="width: 100%; height: 100%">
<canvas id="canvas" style="width: 100%; height: 100%" />
</div>
定义必要的变量
const box = document.getElementById("box");
const canvas = document.getElementById("canvas");
let glRender;
let camera;
let earthMesh;
let scene;
let meshGroup;
let controls;
初始化相关变量
glRender = new THREE.WebGLRenderer({ canvas, alpha: true });
glRender.setSize(canvas.clientWidth, canvas.clientHeight, false);
scene = new THREE.Scene();
const fov = 45;
const aspect = canvas.clientWidth / canvas.clientHeight;
const near = 1;
const far = 4000;
camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 400;
controls = new THREE.OrbitControls(camera, canvas);
controls.target.set(0, 0, 0);
meshGroup = new THREE.Group();
scene.add(meshGroup);
创建地球
const globeRadius = 100;
const globeSegments = 64;
const geometry = new THREE.SphereGeometry(
globeRadius,
globeSegments,
globeSegments
);
const material = new THREE.MeshBasicMaterial({
transparent: true,
opacity: 0.5,
color: 0x000000,
});
earthMesh = new THREE.Mesh(geometry, material);
meshGroup.add(earthMesh);
基本的元素创建好了,但界面没有任何显示,我们还需要渲染场景
function screenRender(){
glRender.render(scene, camera);
controls.update();
requestAnimationFrame(screenRender);
}
screenRender()
这个时候你应该看到一个圆形的物体,接下来我们开始制作点阵式地图
使用绘图处理工具绘制点阵墨卡托投影的贴图
我们把地图所有点阵的坐标记录下,最终结果保存在mapPoints.js
export default {
"points": [
{
"x": 1.5,
"y": 2031.5
},
{
"x": 1.5,
"y": 2016.5
},
...
]
}
创建点阵式地图
import mapPoints from "./mapPoints.js";
function createMapPoints() {
const material = new THREE.MeshBasicMaterial({
color: "#AAA",
});
const sphere = [];
for (let point of mapPoints.points) {
const pos = convertFlatCoordsToSphereCoords(point.x, point.y);
if (pos.x && pos.y && pos.z) {
const pingGeometry = new THREE.SphereGeometry(0.4, 5, 5);
pingGeometry.translate(pos.x, pos.y, pos.z);
sphere.push(pingGeometry);
}
}
const earthMapPoints = new THREE.Mesh(
THREE.BufferGeometryUtils.mergeBufferGeometries(sphere),
material
);
meshGroup.add(earthMapPoints);
}
const globeWidth = 4098 / 2;
const globeHeight = 1968 / 2;
function convertFlatCoordsToSphereCoords(x, y) {
let latitude = ((x - globeWidth) / globeWidth) * -180;
let longitude = ((y - globeHeight) / globeHeight) * -90;
latitude = (latitude * Math.PI) / 180;
longitude = (longitude * Math.PI) / 180;
const radius = Math.cos(longitude) * globeRadius;
const x = Math.cos(latitude) * radius;
const y = Math.sin(longitude) * globeRadius;
const z = Math.sin(latitude) * radius;
return {
x,
y,
z,
};
}
在meshGroup.add(earthMesh)后面调用createMapPoints方法
...
meshGroup.add(earthMesh);
createMapPoints();
screenRender();
...
漂亮!
接下来我们生成柱体,数据采集于disease.sh,并转换成方便我们使用的结构,保存在data.js
import data from "./data.js"
const colors = ["#ffdfe0","#ffc0c0","#FF0000","#ee7070","#c80200","#900000","#510000","#290000"];
const domain = [1000,3000,10000,50000,100000,500000,1000000,1000000];
创建生成柱体方法
function createBar() {
if (!data || data.length === 0) return;
let color;
const scale = d3.scaleLinear().domain(domain).range(colors);
data.forEach(({ lat, lng, value: size }) => {
color = scale(size);
const pos = convertLatLngToSphereCoords(lat, lng, globeRadius);
if (pos.x && pos.y && pos.z) {
const geometry = new THREE.BoxGeometry(2, 2, 1);
geometry.applyMatrix4(
new THREE.Matrix4().makeTranslation(0, 0, -0.5)
);
const barMesh = new THREE.Mesh(
geometry,
new THREE.MeshBasicMaterial({
color,
})
);
barMesh.position.set(pos.x, pos.y, pos.z);
barMesh.lookAt(earthMesh.position);
barMesh.scale.z = Math.max(size/20000, 0.1);
barMesh.updateMatrix();
meshGroup.add(barMesh);
}
});
}
function convertLatLngToSphereCoords(latitude, longitude, radius) {
const phi = (latitude * Math.PI) / 180;
const theta = ((longitude - 180) * Math.PI) / 180;
const x = -(radius + -1) * Math.cos(phi) * Math.cos(theta);
const y = (radius + -1) * Math.sin(phi);
const z = (radius + -1) * Math.cos(phi) * Math.sin(theta);
return {
x,
y,
z,
};
}
在createMapPoints()后调用createBar()
...
meshGroup.add(earthMesh);
createMapPoints();
createBar()
screenRender();
...
最终效果