Vue3实现地球上加载柱体
最终效果为上图。
实现该技术,需要一些技术,我分别罗列一下:
- canvas:需要使用canvas根据json来绘制地球,不懂的可以看这篇canvas绘制地球
- threejs:需要会使用threejs,这里并没有使用shader,不需要制作复杂的东西。
- Vue3:这个可选。不会也能实现。
需要使用的插件:
-
@surbowl/world-geo-json-zh :这个第三方包是简体中文 Geo JSON 世界地图,带有国家(地区)的 ISO 3166 代码、中文简称与全称。含中国南海海域十段线。
-
three :这个我就不用说了。
然后下面是具体实现的代码:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import * as TWEEN from 'three/examples/jsm/libs/tween.module.js';
import worldJSON from '@surbowl/world-geo-json-zh'
import earthquakeJSON from '../assets/json/earthquake.json'
export default (domId) => {
/* ------------------------------初始化三件套--------------------------------- */
const dom = document.getElementById(domId);
const { innerHeight, innerWidth } = window
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 1, 2000);
camera.position.set(0, 0, 10);
camera.lookAt(scene.position);
const renderer = new THREE.WebGLRenderer({
antialias: true,// 抗锯齿
alpha: false,// 透明度
powerPreference: 'high-performance',// 性能
logarithmicDepthBuffer: true,// 深度缓冲
})
// renderer.setClearColor(0x000000, 0);// 设置背景色
// renderer.clear();// 清除渲染器
renderer.shadowMap.enabled = true;// 开启阴影
renderer.shadowMap.type = THREE.PCFSoftShadowMap;// 阴影类型
renderer.outputEncoding = THREE.sRGBEncoding;// 输出编码
renderer.toneMapping = THREE.ACESFilmicToneMapping;// 色调映射
renderer.toneMappingExposure = 1;// 色调映射曝光
renderer.physicallyCorrectLights = true;// 物理正确灯光
renderer.setPixelRatio(devicePixelRatio);// 设置像素比
renderer.setSize(innerWidth, innerHeight);// 设置渲染器大小
dom.appendChild(renderer.domElement);
// 重置大小
window.addEventListener('resize', () => {
const { innerHeight, innerWidth } = window
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
})
/* ------------------------------初始化工具--------------------------------- */
const controls = new OrbitControls(camera, renderer.domElement) // 相机轨道控制器
controls.enableDamping = true // 是否开启阻尼
controls.dampingFactor = 0.05// 阻尼系数
controls.panSpeed = -1// 平移速度
// const axesHelper = new THREE.AxesHelper(10);
// scene.add(axesHelper);
/* ------------------------------正题--------------------------------- */
// 地图配置
const mapOptions = {
sphere: null, // 球体
bg: 'rgb(10 ,20 ,28)',// 背景色
borderColor: 'rgb(10 ,20 ,28)',// 边框颜色
blurColor: '#000000',// 模糊颜色
borderWidth: 1,// 边框宽度
blurWidth: 5,// 模糊宽度
fillColor: 'rgb(26, 35, 44)',// 填充颜色
barHueStart: 0.7,// 柱体颜色起始值
barHueEnd: 0.2,// 柱体颜色结束值
barLightStart: 0.1,// 柱体亮度起始值
barLightEnd: 1.0// 柱体亮度结束值
}
// 相机位置
const cameraPos = {
x: 0.27000767404584447,
y: 1.0782003329514755,
z: 3.8134631736522793
}
// 相机控制器位置
const controlPos = {
x: 0,
y: 0,
z: 0
}
// 柱状图信息
const barInfo = {
barMin: 0.01,
barMax: 0.5,
currentBarH: 0.01,// 柱体高度
min: Number.MAX_SAFE_INTEGER,
max: Number.MIN_SAFE_INTEGER,
range: 0,
mesh: null,// 柱体
lonHelper: null, // 经度辅助线
latHelper: null, // 纬度辅助线
positionHelper: null,
originHelper: null,
}
// 用于绑定整个地球的容器
const objGroup = new THREE.Group();
scene.add(objGroup);
// 绘制地图
const drawRegion = (ctx, c, geoInfo) => {
ctx.beginPath();
c.forEach((item, i) => {
let pos = [(item[0] + 180) * 10, (-item[1] + 90) * 10];
if (i == 0) {
ctx.moveTo(pos[0], pos[1]);
} else {
ctx.lineTo(pos[0], pos[1]);
}
});
ctx.closePath();
ctx.fill();
ctx.stroke();
}
// 创建地球
const createMap = () => {
const canvas = document.createElement('canvas');
canvas.width = 3600;
canvas.height = 1800;
const ctx = canvas.getContext('2d');
ctx.fillStyle = mapOptions.bg;
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.fill();
// 设置地图样式
ctx.strokeStyle = mapOptions.borderColor;
ctx.lineWidth = mapOptions.borderWidth;
ctx.fillStyle = mapOptions.fillColor;
if (mapOptions.blurWidth) {
ctx.shadowColor = mapOptions.blurColor;
ctx.shadowBlur = mapOptions.blurWidth;
}
// 遍历数据
worldJSON.features.forEach(c1 => {
// 判断是否为多边形
if (c1.geometry.type == 'MultiPolygon') {
c1.geometry.coordinates.forEach(c2 => {
c2.forEach(c3 => {
drawRegion(ctx, c3)
})
})
}
else {
c1.geometry.coordinates.forEach(c2 => {
drawRegion(ctx, c2)
})
}
})
const map = new THREE.CanvasTexture(canvas);// 创建纹理贴图
map.wrapS = THREE.RepeatWrapping;// 水平方向重复
map.wrapT = THREE.RepeatWrapping;// 垂直方向重复
const geometry = new THREE.SphereGeometry(1, 32, 32);// 创建球体几何体
const material = new THREE.MeshBasicMaterial({
map: map,// 纹理贴图
transparent: true// 透明
});
mapOptions.sphere = new THREE.Mesh(geometry, material);
mapOptions.sphere.visible = false;// 隐藏地球
objGroup.add(mapOptions.sphere);// 添加到场景中
}
// 创建柱体
const createBar = (info, index) => {
const amount = (info.mag - barInfo.min) / barInfo.range;// 根据值计算比例
const hue = THREE.MathUtils.lerp(mapOptions.barHueStart, mapOptions.barHueEnd, amount);// 根据值计算颜色
const saturation = 1;// 饱和度
const lightness = THREE.MathUtils.lerp(mapOptions.barLightStart, mapOptions.barLightEnd, amount);// 根据值计算亮度
const color = new THREE.Color();
color.setHSL(hue, saturation, lightness);// 设置颜色
barInfo.mesh.setColorAt(index, color);// 设置颜色
barInfo.lonHelper.rotation.y = THREE.MathUtils.degToRad(info.lon) + Math.PI * 0.5;
barInfo.latHelper.rotation.x = THREE.MathUtils.degToRad(-info.lat);
barInfo.positionHelper.updateWorldMatrix(true, false);
let h = THREE.MathUtils.lerp(0.01, 0.5, amount);
barInfo.positionHelper.scale.set(0.01, 0.01, h <= barInfo.currentBarH ? h : barInfo.currentBarH);
barInfo.originHelper.updateWorldMatrix(true, false);
barInfo.mesh.setMatrixAt(index, barInfo.originHelper.matrixWorld);
}
// 创建柱体群
const createBars = (list) => {
list.forEach((info, index) => {
createBar(info, index)
})
barInfo.mesh.instanceColor.needsUpdate = true;
barInfo.mesh.instanceMatrix.needsUpdate = true;
}
// 创建全部柱体
const createAllBars = () => {
// 辅助对象
barInfo.lonHelper = new THREE.Object3D();// 经度辅助对象
scene.add(barInfo.lonHelper);
barInfo.latHelper = new THREE.Object3D();// 纬度辅助对象
barInfo.lonHelper.add(barInfo.latHelper);
barInfo.positionHelper = new THREE.Object3D();// 位置辅助对象
barInfo.positionHelper.position.z = 1;
barInfo.latHelper.add(barInfo.positionHelper);
barInfo.originHelper = new THREE.Object3D();// 原点
barInfo.originHelper.position.z = 0.5;
barInfo.positionHelper.add(barInfo.originHelper);
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
const boxMaterial = new THREE.MeshBasicMaterial({ color: '#FFFFFF' });
earthquakeJSON.forEach(c1 => {
if (barInfo.min > c1.mag) {
barInfo.min = c1.mag;
}
if (barInfo.max < c1.mag) {
barInfo.max = c1.mag;
}
})
barInfo.range = barInfo.max - barInfo.min;
barInfo.mesh = new THREE.InstancedMesh(boxGeometry, boxMaterial, earthquakeJSON.length);
objGroup.add(barInfo.mesh);
createBars(earthquakeJSON);
objGroup.scale.set(0.1, 0.1, 0.1)
mapOptions.sphere.visible = true;// 显示地球
}
// 播放动画
const play = () => {
const orgCamera = camera.position;
const orgControl = controls.target;
const tween = new TWEEN.Tween({
scale: 0.1,
rotate: 0,
cameraX: orgCamera.x,
cameraY: orgCamera.y,
cameraZ: orgCamera.z,
controlsX: orgControl.x,
controlsY: orgControl.y,
controlsZ: orgControl.z
})
.to({
scale: 1,
rotate: Math.PI,
cameraX: cameraPos.x,
cameraY: cameraPos.y,
cameraZ: cameraPos.z,
controlsX: controlPos.x,
controlsY: controlPos.y,
controlsZ: controlPos.z
}, 2000)
.easing(TWEEN.Easing.Quadratic.Out)
.onUpdate((obj) => {
objGroup.scale.set(obj.scale, obj.scale, obj.scale);
objGroup.rotation.y = obj.rotate;
camera.position.set(obj.cameraX, obj.cameraY, obj.cameraZ);
controls.target.set(obj.controlsX, obj.controlsY, obj.controlsZ);
})
.chain(
new TWEEN.Tween({ h: barInfo.barMin })
.to({ h: barInfo.barMax }, 2000)
.easing(TWEEN.Easing.Quadratic.Out)
.onUpdate((obj) => {
barInfo.currentBarH = obj.h;
createBars(earthquakeJSON);
})
)
.start();
TWEEN.add(tween);
}
// 初始化
const init = () => {
camera.near = 0.1;// 相机最近距离
camera.updateProjectionMatrix();// 更新相机投影矩阵
createMap();
createAllBars();
play();
}
init();
/* ------------------------------动画函数--------------------------------- */
const animation = () => {
TWEEN.update();
renderer.render(scene, camera);
controls.update();
requestAnimationFrame(animation);
}
animation();
}
在引入的地方有使用到一个文件 earthquake.json我也一块放在资源里面了。
最后在vue的onMounted生命周期里面调用就好了。