three.js创建3D地球
1 three-globe
three-globe是一个基于Three.js的开源项目,用于创建三维地球数据可视化。它通过WebGL技术,使用户能够在浏览器中展示地球上的各种数据,如国家边界、城市位置、温度分布等。使用three-globe开源项目可以快速的构建一个3D地球
2 使用示例
1 使用以下版本
"three": "^0.171.0",
"three-globe": "^2.35.3",
npm install three@^0.171.0 three-globe@^2.35.3
2 index.vue
<template>
<div id="canvasEarth"></div>
<div id="readerEarth"></div>
</template>
<script setup lang="js">
import * as THREE from "three"
import ThreeGlobe from 'three-globe'
//卫星模型
import {createSatellite} from '@/views/earth/satellite.ts';
//3D地球
import {createGlobe} from '@/views/earth/globe.ts';
//棱锥
import {createCone} from '@/views/earth/cone.ts';
//中国描边
import {createMapStroke} from '@/views/earth/cityLine.ts';
//3D地球模型对象
const Globe = new ThreeGlobe();
//场景对象
const scene = new THREE.Scene();
//设置地球材质透明度
var material=Globe.globeMaterial();
material.transparent = true; // 启用透明度
material.opacity = 0.8; // 设置透明度为 0.8
// 设置双面渲染,以避免穿透效果
material.side = THREE.DoubleSide; // 双面渲染
// 设置 alpha 测试,这样可以避免透明度小于某个值时,材质仍然被渲染
material.alphaTest = 0.1; // 透明度低于 0.1 时,材质不被渲染
scene.add(Globe);
//渲染地球
createGlobe(Globe,scene);
//渲染卫星
createSatellite( Globe);
//渲染棱锥
createCone( Globe);
//中国描边
createMapStroke( Globe)
</script>
<style>
canvas { display: block; }
#canvasEarth {
position: relative;
width: 100%;
height: 100%;
top: -10%;
}
#readerEarth {
position: absolute;
top: 0;
left: 0;
pointer-events: none; /* Prevent it from blocking interactions */
}
.label-country{
position: relative;
left: 30px;
}
</style>
3 satellite.ts 需要一个卫星的3d建模
import { ColladaLoader } from 'three/addons/loaders/ColladaLoader.js';
import * as THREE from "three";
function createSatellite(Globe: any){
const loader = new ColladaLoader();
loader.load('/3D/model.dae', function (collada) {
const satellite = collada.scene;
satellite.scale.set(0.02,0.02,0.02);
satellite.position.set(-70,70,70);
Globe.add(satellite)
var angle = 0;
const radius = 130; // 轨道半径
const path = new THREE.CurvePath();
const curve = new THREE.EllipseCurve(0, 0, radius, radius, 0, Math.PI * 2);
path.add(curve);
(function animate() {
if (satellite) {
angle += 0.001; // 每一帧增加角度
const position = path.getPointAt((angle % 1)); // 获取路径上的点
satellite.position.set(position.x, position.y, 0); // 更新卫星位置
}
requestAnimationFrame(animate);
})();
});
}
export {createSatellite};
4 globe.ts
import marble from "@/assets/img/earth-night.jpg";
import * as THREE from "three";
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
import {CSS2DRenderer} from 'three/addons/renderers/CSS2DRenderer.js';
import {setWordData} from './data.ts';
function createGlobe(Globe: any, scene: THREE.Scene) {
//设置3D地球 线,光点,光圈,文字
setWordData(Globe);
Globe.globeImageUrl(marble)
.showAtmosphere(true) // 是否显示围绕地球的明亮光晕,代表大气层
.atmosphereColor('rgba(94,165,196,0.24)') // 大气颜色
.atmosphereAltitude(0.4) // 表示大气最大高
.arcAltitudeAutoScale(0.6)
.arcCurveResolution(128)
.arcColor('color') // 线条颜色的 Arc 对象访问器函数或属性
.arcDashLength('dashLength') // 用于表示圆弧中虚线段的长度
.arcDashGap('dashGap') // 用于表示短划线段之间的间隙长度
.arcDashInitialGap('dashInitialGap') // 初始间隙长度
.arcStroke('stroke')
.arcDashAnimateTime('dashAnimateTime') // 用于对整行长度从起点到终点置的运动进行动画处理。
.arcsTransitionDuration(0); // 过渡持续时间
// 环境光
Globe.add(new THREE.AmbientLight(0xffffff, 10));
// 设置相机
const camera = new THREE.PerspectiveCamera();
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
camera.position.z = 450;
//渲染器
const renderer = new THREE.WebGLRenderer({
alpha: true, antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
const canvasContainer = document.getElementById('canvasEarth') as HTMLElement;
canvasContainer.appendChild(renderer.domElement);
var css2DRenderer = new CSS2DRenderer();
css2DRenderer.setSize(window.innerWidth, window.innerHeight);
css2DRenderer.domElement.style.position = 'absolute'; // 保持绝对定位
css2DRenderer.domElement.style.top = '0px';
css2DRenderer.domElement.style.zIndex = '1'; // 确保它不遮挡 WebGL 渲染器
css2DRenderer.domElement.style.pointerEvents = 'none'; // 禁止捕获鼠标事件
canvasContainer.appendChild(css2DRenderer.domElement);
// 轨道偏移器
const tbControls = new OrbitControls(camera, renderer.domElement);
tbControls.enableZoom = false;
tbControls.update();
(function animate() {
requestAnimationFrame(animate);
Globe.rotateY(0.001);
tbControls.update();
renderer.render(scene, camera);
css2DRenderer.render(scene, camera);
})();
// 监听窗口尺寸变化
window.addEventListener('resize', () => {
// 更新渲染器大小
renderer.setSize(window.innerWidth, window.innerHeight);
css2DRenderer.setSize(window.innerWidth, window.innerHeight);
// 更新摄像机的纵横比
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix(); // 更新投影矩阵
});
}
export {createGlobe}
5 data.ts
import {getRandomItems} from "@/views/util/randomItems.ts";
import mygeo from "@/assets/json/mygeo.json";
import * as THREE from "three";
import aperture from "@/assets/img/aperture.png";
import pointJPG from "@/assets/img/point.png";
import {createTag} from "@/views/earth/tag.ts";
const apertureGroup = new THREE.Group();
const pointGroup = new THREE.Group()
let labelObject = new THREE.Group()
let arcsData: any[] = [];
let arcsDataCopy: any[] = [];
const mashNormal = new THREE.Vector3(0, 0, 1);
function setWordData(Globe:any){
setInterval(async () => {
arcsData = [];
arcsDataCopy = [];
pointGroup.clear();
apertureGroup.clear();
labelObject.clear();
// const elements = document.querySelectorAll(".label-country");
// elements.forEach(element => {
// element.remove()
// });
const coordItem = getRandomItems(mygeo, 10);
coordItem.forEach((arc: any) => {
arcsData.push({
startLat: arc.coordinate[1],
startLng: arc.coordinate[0],
endLat: 28.45892581576906,
endLng: 116.539649563166506,
size: Math.random() * 1,
dashLength: 1,
stroke: 0.5,
dashAnimateTime: 1,
dashInitialGap: 1,
dashGap: 0,
r: 1,
country: arc.country,
op: "-",
city: arc.city,
color: 'rgba(16,220,228,0.5)'
})
})
arcsData.push({
startLat: 28.45892581576906,
startLng: 116.539649563166506,
endLat: 28.45892581576906,
endLng: 116.539649563166506,
size: Math.random() * 1,
dashLength: 1,
stroke: 0.5,
dashAnimateTime: 1,
dashInitialGap: 1,
dashGap: 0,
r: 1.15,
country: '中国',
op: "-",
city: '南昌',
color: '#5fd0da'
})
arcsDataCopy = arcsData
arcsData.forEach(arc => {
arcsDataCopy.push({
startLat: arc.startLat,
startLng: arc.startLng,
endLat: 28.45892581576906,
endLng: 116.539649563166506,
size: Math.random() * 1,
dashLength: 0.02,
stroke: 1.5,
dashAnimateTime: Math.floor(Math.random() * (2500 - 1500 + 1)) + 1500,
dashInitialGap: 1.3,
dashGap: 1,
color: 'rgba(243,158,0,0.8)'
})
// 添加光圈
const geometry = new THREE.PlaneGeometry(12, 12);
const texture = new THREE.TextureLoader().load(aperture);
const apertureMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
map: texture,
transparent: true,
aoMapIntensity: 0,
side: THREE.DoubleSide
})
const apertureMesh = new THREE.Mesh(geometry, apertureMaterial)
const apertureCoords = Globe.getCoords(arc.startLat, arc.startLng);
const apertureVector3 = new THREE.Vector3(apertureCoords.x, apertureCoords.y, apertureCoords.z).normalize()
apertureMesh.position.set(apertureCoords.x * arc.r, apertureCoords.y * arc.r, apertureCoords.z * arc.r);
apertureMesh.quaternion.setFromUnitVectors(mashNormal, apertureVector3)
apertureGroup.add(apertureMesh);
//添加光点
const pointGeometry = new THREE.PlaneGeometry(4, 4);
const pointTexture = new THREE.TextureLoader().load(pointJPG);
const pointMaterial = new THREE.MeshBasicMaterial({
color: 0xd2cccc,
map: pointTexture,
transparent: true
})
const pointMesh = new THREE.Mesh(pointGeometry, pointMaterial)
pointMesh.position.set(apertureCoords.x * 1.01, apertureCoords.y * 1.01, apertureCoords.z * 1.01);
pointMesh.quaternion.setFromUnitVectors(mashNormal, apertureVector3)
pointGroup.add(pointMesh);
var _s = Math.random();
(function aperturAnimate() {
_s += 0.01;
apertureMesh.scale.set(_s, _s, _s)
if (_s <= 1.7) {
apertureMesh.material.opacity = (_s - 1.0) / (1.7 - 1.0)
} else if (_s > 1.7 && _s <= 2.5) {
apertureMesh.material.opacity = 1 - (_s - 1.7) / (2.5 - 1.7)
} else {
_s = 1.0
}
requestAnimationFrame(aperturAnimate);
})();
})
labelObject = createTag(Globe, arcsData);
Globe.add(labelObject)
Globe.add(apertureGroup)
Globe.add(pointGroup)
Globe.arcsData(arcsDataCopy) // 弧映射图层中列表
}, 8000)
}
export {setWordData}
6 tag.ts
import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
import * as THREE from "three";
function createTag(Globe: any,arcsData:any){
const labelGroup = new THREE.Group()
arcsData.forEach((country: any)=>{
const Gt = Globe.getCoords(country.startLat, country.startLng);
var label = document.createElement('div');
label.className = 'label-country';
label.textContent = country.country;
label.style.color = 'rgb(237,236,236)';
label.style.fontSize = '0.8vw';
label.style.opacity = '0.7';
label.style.backgroundColor = 'rgba(42,142,196,0.53)';
label.style.padding = '0.3vw 0.6vw';
label.style.borderRadius = '0.2vw';
label.style.pointerEvents ='none';//避免HTML标签遮挡三维场景的鼠标事件//·设置HTML元素标签在three.js世界坐标中位置
var labelObject = new CSS2DObject(label);
labelObject.position.set(Gt.x,Gt.y,Gt.z);
labelGroup.add(labelObject)
})
return labelGroup;
}
export {createTag}
7 cone.ts
import * as THREE from "three"
function createCone(Globe: any){
const Gt = Globe.getCoords(28.45892581576906, 116.539649563166506);
const Vector3 = new THREE.Vector3(Gt.x, Gt.y, Gt.z).normalize()
const mashNormal = new THREE.Vector3(0, 0, 1);
// 设置棱锥
const height = 15
const coneGeometry = new THREE.ConeGeometry(4, height, 4);
coneGeometry.rotateX(-Math.PI / 2)
coneGeometry.translate(0, 0, height / 2)
const coneMaterial = new THREE.MeshLambertMaterial({color: 0x10dce4});
const coneMesh = new THREE.Mesh(coneGeometry, coneMaterial);
const coneMesh2 = coneMesh.clone()
coneMesh2.scale.z = 0.5
coneMesh2.position.z = height * (1 + coneMesh2.scale.z)
coneMesh2.rotateX(Math.PI)
coneMesh.add(coneMesh2)
coneMesh.position.set(Gt.x, Gt.y, Gt.z);
coneMesh.quaternion.setFromUnitVectors(mashNormal, Vector3)
Globe.add(coneMesh);
var _r = 1.06;
(function animate() {
_r -= 0.001;
if (_r <= 1.0) {
_r = 1.06;
}
coneMesh.rotateZ(0.05);
coneMesh.position.set(Gt.x * _r, Gt.y * _r, Gt.z * _r);
requestAnimationFrame(animate);
})();
}
export {createCone}
8 cityLine.ts
import * as THREE from "three"
import chinaInfoJson from '../china/json/china.json'
export const createMapStroke = (Globe:any) => {
const cityStroke = new THREE.Object3D();
cityStroke.name = "cityStroke";
const lineMaterial = new THREE.LineBasicMaterial({
color: 0xb9995b,
opacity: 0.5, // 初始透明度
transparent: true, // 透明度控制
side: THREE.DoubleSide
});
chinaInfoJson.features.forEach((elem: any) => {
const provinceLine = new THREE.Group();
provinceLine.name = elem.properties.name;
const coordinates = elem.geometry.coordinates;
coordinates.forEach((multiPolygon: any) => {
multiPolygon.forEach((polygon: any) => {
const line = createCityLine(polygon, lineMaterial,Globe);
provinceLine.add(line);
});
});
cityStroke.add(provinceLine);
})
Globe.add(cityStroke);
}
/**
* 球面画线
* @param polygon
* @param lineMaterial
*/
export const createCityLine = (polygon: any, lineMaterial: THREE.LineBasicMaterial,Globe:any) => {
const positions = [];
const linGeometry = new THREE.BufferGeometry();
for (let i = 0; i < polygon.length; i++) {
let pos = Globe.getCoords( polygon[i][1], polygon[i][0]);
positions.push(pos.x, pos.y, pos.z);
}
linGeometry.setAttribute(
"position",
new THREE.Float32BufferAttribute(positions, 3)
);
return new THREE.Line(linGeometry, lineMaterial)
}
9 randomItems.ts 随机获取 number 条数据
function getRandomItems(arr:any, count:number) {
const shuffled = arr.slice(); // 复制数组
let i = arr.length, randomIndex, temp;
// 洗牌算法(Fisher-Yates shuffle)
while (i--) {
randomIndex = Math.floor(Math.random() * (i + 1)); // 生成随机索引
temp = shuffled[i];
shuffled[i] = shuffled[randomIndex];
shuffled[randomIndex] = temp;
}
// 返回前 count 条数据
return shuffled.slice(0, count);
}
export {getRandomItems}
静态资源
GeoJSON 格式的全球,全国地理数据,这个可以去网上找
地球贴图
圆环
点
mygeo.json
[
{
"country": "科特迪瓦",
"city": "阿比让",
"coordinate": [
4.016666666666667,
5.316666666666666
]
},
...... //这里填写更多的地区坐标信息
{
"country": "瑞士",
"city": "苏黎世",
"coordinate": [
8.533333333333333,
47.36666666666667
]
}
]