Files
geg-gas-web/src/views/secondDev/Earth3D.vue
2025-12-29 16:51:37 +08:00

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>