336 lines
8.5 KiB
Vue
336 lines
8.5 KiB
Vue
<template>
|
|
<div ref="earthContainer" class="earth-container"></div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
|
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
|
// 地球纹理URL
|
|
import earthBlueMarble from '../../assets/images/earth-blue-marble.jpg';
|
|
|
|
const earthContainer = ref<HTMLDivElement | null>(null);
|
|
|
|
let scene: THREE.Scene | null = null;
|
|
let camera: THREE.PerspectiveCamera | null = null;
|
|
let renderer: THREE.WebGLRenderer | null = null;
|
|
let controls: OrbitControls | null = null;
|
|
let earth: THREE.Mesh | null = null;
|
|
let markers: THREE.Mesh[] = [];
|
|
let animationId: number | null = null;
|
|
|
|
// 城市数据 - 红色主要城市
|
|
const primaryCities = [
|
|
{ name: '广州', lat: 23.1291, lng: 113.2644, color: 0xFA2D30 },
|
|
];
|
|
|
|
// 城市数据 - 黄色次要城市
|
|
const secondaryCities = [
|
|
{ name: '北京', lat: 39.9042, lng: 116.4074, color: 0xFFF963 },
|
|
{ name: '上海', lat: 31.2304, lng: 121.4737, color: 0xFFF963 },
|
|
{ name: '香港', lat: 22.3964, lng: 114.1095, color: 0xFFF963 },
|
|
{ name: '新加坡', lat: 1.3521, lng: 103.8198, color: 0xFFF963 },
|
|
{ name: '东京', lat: 35.6824, lng: 139.759, color: 0xFFF963 },
|
|
{ name: '伦敦', lat: 51.5074, lng: -0.1278, color: 0xFFF963 },
|
|
{ name: '迪拜', lat: 25.276987, lng: 55.296249, color: 0xFFF963 },
|
|
{ name: '纽约', lat: 40.748817, lng: -73.985428, color: 0xFFF963 },
|
|
{ name: '旧金山', lat: 37.7749, lng: -122.4194, color: 0xFFF963 },
|
|
{ name: '惉尼', lat: -33.8688, lng: 151.2093, color: 0xFFF963 },
|
|
{ name: '巴黎', lat: 48.8566, lng: 2.3522, color: 0xFFF963 },
|
|
];
|
|
|
|
const EARTH_RADIUS = 1.0;
|
|
|
|
const initScene = () => {
|
|
if (!earthContainer.value) return;
|
|
|
|
const width = earthContainer.value.clientWidth;
|
|
const height = earthContainer.value.clientHeight;
|
|
|
|
// 创建场景
|
|
scene = new THREE.Scene();
|
|
|
|
// 创建相机
|
|
camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100);
|
|
camera.position.set(0.0, 1.5, 3.0);
|
|
|
|
// 创建渲染器
|
|
renderer = new THREE.WebGLRenderer({
|
|
antialias: true,
|
|
alpha: true,
|
|
});
|
|
renderer.setSize(width, height);
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
earthContainer.value.appendChild(renderer.domElement);
|
|
|
|
// 创建 OrbitControls
|
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.autoRotate = true;
|
|
controls.autoRotateSpeed = -1.0;
|
|
controls.enablePan = false;
|
|
controls.enableZoom = false;
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
|
|
// 添加光源
|
|
addLights();
|
|
|
|
// 创建地球
|
|
createEarth();
|
|
|
|
// 创建大气层
|
|
createAtmosphere();
|
|
};
|
|
|
|
const addLights = () => {
|
|
if (!scene) return;
|
|
|
|
// 环境光
|
|
const ambient = new THREE.AmbientLight(0xffffff, 1);
|
|
scene.add(ambient);
|
|
|
|
// 方向光
|
|
const directional = new THREE.DirectionalLight(0xffffff, 0.5);
|
|
directional.position.set(5.0, 2.0, 5.0).normalize();
|
|
scene.add(directional);
|
|
};
|
|
|
|
const createEarth = () => {
|
|
if (!scene) return;
|
|
|
|
const loader = new THREE.TextureLoader();
|
|
|
|
loader.load(earthBlueMarble, (texture) => {
|
|
if (!scene) return;
|
|
|
|
// 设置各向异性过滤
|
|
if (renderer) {
|
|
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
|
}
|
|
|
|
const geometry = new THREE.SphereGeometry(EARTH_RADIUS, 64, 48);
|
|
const material = new THREE.MeshStandardMaterial({
|
|
color: 0xFFFFFF,
|
|
map: texture,
|
|
roughness: 0.6,
|
|
metalness: 0.1,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
|
|
earth = new THREE.Mesh(geometry, material);
|
|
scene.add(earth);
|
|
|
|
// 地球加载完成后添加标记点
|
|
createMarkers();
|
|
});
|
|
};
|
|
|
|
const createAtmosphere = () => {
|
|
if (!scene) return;
|
|
|
|
const atmosphereGeometry = new THREE.SphereGeometry(1.05, 64, 48);
|
|
const atmosphereMaterial = new THREE.ShaderMaterial({
|
|
vertexShader: `
|
|
varying vec3 vertexNormal;
|
|
void main() {
|
|
vertexNormal = normalize(normalMatrix * normal);
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`,
|
|
fragmentShader: `
|
|
varying vec3 vertexNormal;
|
|
void main() {
|
|
float intensity = pow(0.6 - dot(vertexNormal, vec3(0, 0, 1.0)), 2.0);
|
|
gl_FragColor = vec4(0.3, 0.6, 1.0, 0.8) * intensity;
|
|
}
|
|
`,
|
|
blending: THREE.AdditiveBlending,
|
|
side: THREE.BackSide,
|
|
transparent: true,
|
|
});
|
|
|
|
const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
|
|
atmosphere.scale.set(1.1, 1.1, 1.1);
|
|
atmosphere.position.x = -0.02;
|
|
atmosphere.position.y = 0.02;
|
|
scene.add(atmosphere);
|
|
};
|
|
|
|
// 经纬度转换为3D坐标
|
|
const latLngToVector3 = (lat: number, lng: number, radius: number): THREE.Vector3 => {
|
|
const phi = (90 - lat) * (Math.PI / 180);
|
|
const theta = (lng + 180) * (Math.PI / 180);
|
|
|
|
const x = -radius * Math.sin(phi) * Math.cos(theta);
|
|
const y = radius * Math.cos(phi);
|
|
const z = radius * Math.sin(phi) * Math.sin(theta);
|
|
|
|
return new THREE.Vector3(x, y, z);
|
|
};
|
|
|
|
// 创建标记点
|
|
const createMarkers = () => {
|
|
if (!scene) return;
|
|
|
|
const markerRadius = 1.06;
|
|
|
|
// 创建主要城市标记(红色,较大)
|
|
primaryCities.forEach((city) => {
|
|
const marker = createCityMarker(city.lat, city.lng, markerRadius, city.color, 0.02, true);
|
|
if (marker) {
|
|
scene!.add(marker);
|
|
markers.push(marker);
|
|
}
|
|
});
|
|
|
|
// 创建次要城市标记(黄色,较小)
|
|
secondaryCities.forEach((city) => {
|
|
const marker = createCityMarker(city.lat, city.lng, markerRadius, city.color, 0.01, false);
|
|
if (marker) {
|
|
scene!.add(marker);
|
|
markers.push(marker);
|
|
}
|
|
});
|
|
};
|
|
|
|
// 创建城市标记
|
|
const createCityMarker = (
|
|
lat: number,
|
|
lng: number,
|
|
radius: number,
|
|
color: number,
|
|
size: number,
|
|
hasRing: boolean
|
|
): THREE.Mesh | null => {
|
|
// 创建圆形标记
|
|
const geometry = new THREE.CircleGeometry(size, 32);
|
|
const material = new THREE.MeshBasicMaterial({
|
|
color: color,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
const marker = new THREE.Mesh(geometry, material);
|
|
|
|
// 添加外环
|
|
if (hasRing) {
|
|
const ringGeometry = new THREE.RingGeometry(size * 1.2, size * 1.7, 32);
|
|
const ringMaterial = new THREE.MeshBasicMaterial({
|
|
color: color,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
|
|
marker.add(ring);
|
|
} else {
|
|
// 次要城市添加较小的外环
|
|
const ringGeometry = new THREE.RingGeometry(size * 1.5, size * 1.8, 32);
|
|
const ringMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0x898A51,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
|
|
marker.add(ring);
|
|
}
|
|
|
|
// 计算位置
|
|
const position = latLngToVector3(lat, lng, radius);
|
|
marker.position.copy(position);
|
|
|
|
// 让标记朝向球心外侧
|
|
marker.lookAt(new THREE.Vector3(0, 0, 0));
|
|
marker.rotateX(Math.PI);
|
|
|
|
return marker;
|
|
};
|
|
|
|
const animate = () => {
|
|
if (!scene || !camera || !renderer) return;
|
|
|
|
animationId = requestAnimationFrame(animate);
|
|
|
|
// 更新控制器
|
|
if (controls) {
|
|
controls.update();
|
|
}
|
|
|
|
// 让标记点始终面向相机
|
|
markers.forEach((marker) => {
|
|
marker.quaternion.copy(camera!.quaternion);
|
|
});
|
|
|
|
renderer.render(scene, camera);
|
|
};
|
|
|
|
const onResize = () => {
|
|
if (!earthContainer.value || !camera || !renderer) return;
|
|
|
|
const width = earthContainer.value.clientWidth;
|
|
const height = earthContainer.value.clientHeight;
|
|
|
|
camera.aspect = width / height;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(width, height);
|
|
};
|
|
|
|
const dispose = () => {
|
|
if (animationId) {
|
|
cancelAnimationFrame(animationId);
|
|
}
|
|
|
|
if (controls) {
|
|
controls.dispose();
|
|
}
|
|
|
|
// 清理标记
|
|
markers.forEach((marker) => {
|
|
marker.geometry?.dispose();
|
|
if (marker.material instanceof THREE.Material) {
|
|
marker.material.dispose();
|
|
}
|
|
});
|
|
markers = [];
|
|
|
|
// 清理地球
|
|
if (earth) {
|
|
earth.geometry?.dispose();
|
|
if (earth.material instanceof THREE.Material) {
|
|
earth.material.dispose();
|
|
}
|
|
}
|
|
|
|
if (renderer) {
|
|
renderer.dispose();
|
|
if (earthContainer.value && renderer.domElement) {
|
|
earthContainer.value.removeChild(renderer.domElement);
|
|
}
|
|
}
|
|
|
|
scene = null;
|
|
camera = null;
|
|
renderer = null;
|
|
controls = null;
|
|
earth = null;
|
|
};
|
|
|
|
onMounted(async () => {
|
|
await nextTick();
|
|
initScene();
|
|
animate();
|
|
window.addEventListener('resize', onResize);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
dispose();
|
|
window.removeEventListener('resize', onResize);
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.earth-container {
|
|
width: 100%;
|
|
height: 100%;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
z-index: 50;
|
|
}
|
|
</style>
|