304 lines
8.5 KiB
JavaScript
304 lines
8.5 KiB
JavaScript
// earth.js - 3D Earth creation module
|
|
|
|
import * as THREE from 'three';
|
|
import { CONFIG, EARTH_CONFIG, EARTH_MATERIAL_CONFIG } from './constants.js';
|
|
import { latLonToVector3 } from './utils.js';
|
|
|
|
export let earth = null;
|
|
export let clouds = null;
|
|
export let terrain = null;
|
|
|
|
const textureLoader = new THREE.TextureLoader();
|
|
let _earthMaterial = null;
|
|
|
|
export function createEarth(scene) {
|
|
const geometry = new THREE.SphereGeometry(CONFIG.earthRadius, 128, 128);
|
|
|
|
const C = EARTH_MATERIAL_CONFIG;
|
|
|
|
const material = new THREE.MeshPhongMaterial({
|
|
color: C.color,
|
|
specular: C.specular,
|
|
shininess: C.shininess,
|
|
emissive: C.emissive,
|
|
transparent: true,
|
|
opacity: C.opacity,
|
|
side: THREE.FrontSide,
|
|
depthWrite: true,
|
|
depthTest: true,
|
|
});
|
|
_earthMaterial = material;
|
|
|
|
earth = new THREE.Mesh(geometry, material);
|
|
earth.renderOrder = 0;
|
|
earth.rotation.x = EARTH_CONFIG.tiltRad;
|
|
scene.add(earth);
|
|
|
|
// Depth-mask occluder — invisible sphere slightly inside the earth,
|
|
// writes to the depth buffer so far-side cables/satellites are occluded.
|
|
const occluderGeometry = new THREE.SphereGeometry(
|
|
CONFIG.earthRadius * C.occluderRadiusFactor,
|
|
C.occluderSegments,
|
|
C.occluderSegments,
|
|
);
|
|
const occluderMaterial = new THREE.MeshBasicMaterial({
|
|
colorWrite: false,
|
|
side: THREE.FrontSide,
|
|
});
|
|
const occluder = new THREE.Mesh(occluderGeometry, occluderMaterial);
|
|
occluder.renderOrder = -1;
|
|
earth.add(occluder);
|
|
|
|
// Shared Fresnel vertex shader for both atmosphere layers
|
|
const ATMOS_VERTEX_SHADER = `
|
|
varying vec3 vNormal;
|
|
void main() {
|
|
vNormal = normalize(normalMatrix * normal);
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`;
|
|
|
|
// Fresnel atmosphere — inner rim
|
|
const [ir, ig, ib] = C.atmosInnerColor;
|
|
const atmosInnerGeo = new THREE.SphereGeometry(
|
|
CONFIG.earthRadius * C.atmosInnerRadiusFactor,
|
|
C.atmosInnerSegments,
|
|
C.atmosInnerSegments,
|
|
);
|
|
const atmosInnerMat = new THREE.ShaderMaterial({
|
|
vertexShader: ATMOS_VERTEX_SHADER,
|
|
fragmentShader: `
|
|
varying vec3 vNormal;
|
|
void main() {
|
|
float rim = 1.0 - abs(dot(vNormal, vec3(0.0, 0.0, 1.0)));
|
|
float intensity = pow(rim, ${C.atmosInnerRimPower.toFixed(1)});
|
|
gl_FragColor = vec4(${ir.toFixed(2)}, ${ig.toFixed(2)}, ${ib.toFixed(2)}, intensity * ${C.atmosInnerIntensity.toFixed(2)});
|
|
}
|
|
`,
|
|
blending: THREE.AdditiveBlending,
|
|
side: THREE.BackSide,
|
|
transparent: true,
|
|
depthWrite: false,
|
|
});
|
|
const atmosInner = new THREE.Mesh(atmosInnerGeo, atmosInnerMat);
|
|
atmosInner.renderOrder = 1;
|
|
earth.add(atmosInner);
|
|
|
|
// Fresnel atmosphere — outer corona
|
|
const [outerR, outerG, outerB] = C.atmosOuterColor;
|
|
const atmosOuterGeo = new THREE.SphereGeometry(
|
|
CONFIG.earthRadius * C.atmosOuterRadiusFactor,
|
|
C.atmosOuterSegments,
|
|
C.atmosOuterSegments,
|
|
);
|
|
const atmosOuterMat = new THREE.ShaderMaterial({
|
|
vertexShader: ATMOS_VERTEX_SHADER,
|
|
fragmentShader: `
|
|
varying vec3 vNormal;
|
|
void main() {
|
|
float rim = 1.0 - abs(dot(vNormal, vec3(0.0, 0.0, 1.0)));
|
|
float intensity = pow(rim, ${C.atmosOuterRimPower.toFixed(1)});
|
|
gl_FragColor = vec4(${outerR.toFixed(2)}, ${outerG.toFixed(2)}, ${outerB.toFixed(2)}, intensity * ${C.atmosOuterIntensity.toFixed(2)});
|
|
}
|
|
`,
|
|
blending: THREE.AdditiveBlending,
|
|
side: THREE.BackSide,
|
|
transparent: true,
|
|
depthWrite: false,
|
|
});
|
|
const atmosOuter = new THREE.Mesh(atmosOuterGeo, atmosOuterMat);
|
|
atmosOuter.renderOrder = 1;
|
|
earth.add(atmosOuter);
|
|
|
|
// Texture is loaded separately via loadEarthTexture() for staged loading
|
|
return earth;
|
|
}
|
|
|
|
export function createClouds(scene, earthObj) {
|
|
const geometry = new THREE.SphereGeometry(CONFIG.earthRadius + 3, 64, 64);
|
|
const material = new THREE.MeshPhongMaterial({
|
|
transparent: true,
|
|
linewidth: 2,
|
|
opacity: 0.15,
|
|
depthTest: true,
|
|
depthWrite: false,
|
|
blending: THREE.AdditiveBlending,
|
|
side: THREE.DoubleSide
|
|
});
|
|
|
|
clouds = new THREE.Mesh(geometry, material);
|
|
earthObj.add(clouds);
|
|
|
|
textureLoader.load(
|
|
'./assets/earth_clouds_1024.png',
|
|
function(texture) {
|
|
material.map = texture;
|
|
material.needsUpdate = true;
|
|
},
|
|
undefined,
|
|
function(err) {
|
|
console.log('云层纹理加载失败');
|
|
}
|
|
);
|
|
|
|
return clouds;
|
|
}
|
|
|
|
export function createTerrain(scene, earthObj, simplex) {
|
|
const geometry = new THREE.SphereGeometry(CONFIG.earthRadius, 128, 128);
|
|
const positionAttribute = geometry.getAttribute('position');
|
|
|
|
for (let i = 0; i < positionAttribute.count; i++) {
|
|
const x = positionAttribute.getX(i);
|
|
const y = positionAttribute.getY(i);
|
|
const z = positionAttribute.getZ(i);
|
|
|
|
const noise = simplex(x / 20, y / 20, z / 20);
|
|
const height = 1 + noise * 0.02;
|
|
|
|
positionAttribute.setXYZ(i, x * height, y * height, z * height);
|
|
}
|
|
|
|
geometry.computeVertexNormals();
|
|
|
|
const material = new THREE.MeshPhongMaterial({
|
|
color: 0x00aa00,
|
|
flatShading: true,
|
|
transparent: true,
|
|
opacity: 0.7
|
|
});
|
|
|
|
terrain = new THREE.Mesh(geometry, material);
|
|
terrain.visible = false;
|
|
earthObj.add(terrain);
|
|
|
|
return terrain;
|
|
}
|
|
|
|
export function toggleTerrain(visible) {
|
|
if (terrain) {
|
|
terrain.visible = visible;
|
|
}
|
|
}
|
|
|
|
export function createStars(scene) {
|
|
const starGeometry = new THREE.BufferGeometry();
|
|
const starCount = 8000;
|
|
const starPositions = new Float32Array(starCount * 3);
|
|
|
|
for (let i = 0; i < starCount * 3; i += 3) {
|
|
const r = 800 + Math.random() * 200;
|
|
const theta = Math.random() * Math.PI * 2;
|
|
const phi = Math.acos(2 * Math.random() - 1);
|
|
|
|
starPositions[i] = r * Math.sin(phi) * Math.cos(theta);
|
|
starPositions[i + 1] = r * Math.sin(phi) * Math.sin(theta);
|
|
starPositions[i + 2] = r * Math.cos(phi);
|
|
}
|
|
|
|
starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
|
|
|
|
const starMaterial = new THREE.PointsMaterial({
|
|
color: 0xffffff,
|
|
size: 0.5,
|
|
transparent: true,
|
|
blending: THREE.AdditiveBlending
|
|
});
|
|
|
|
const stars = new THREE.Points(starGeometry, starMaterial);
|
|
scene.add(stars);
|
|
|
|
return stars;
|
|
}
|
|
|
|
let latitudeLines = [];
|
|
let longitudeLines = [];
|
|
|
|
export function createGridLines(scene, earthObj) {
|
|
latitudeLines.forEach(line => scene.remove(line));
|
|
longitudeLines.forEach(line => scene.remove(line));
|
|
latitudeLines = [];
|
|
longitudeLines = [];
|
|
|
|
const earthRadius = 100.1;
|
|
const gridMaterial = new THREE.LineBasicMaterial({
|
|
color: 0x44aaff,
|
|
transparent: true,
|
|
opacity: 0.2,
|
|
linewidth: 1
|
|
});
|
|
|
|
for (let lat = -75; lat <= 75; lat += 15) {
|
|
const points = [];
|
|
for (let lon = -180; lon <= 180; lon += 5) {
|
|
const point = latLonToVector3(lat, lon, earthRadius);
|
|
points.push(point);
|
|
}
|
|
|
|
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
const line = new THREE.Line(geometry, gridMaterial);
|
|
line.userData = { type: 'latitude', value: lat };
|
|
earthObj.add(line);
|
|
latitudeLines.push(line);
|
|
}
|
|
|
|
for (let lon = -180; lon <= 180; lon += 30) {
|
|
const points = [];
|
|
for (let lat = -90; lat <= 90; lat += 5) {
|
|
const point = latLonToVector3(lat, lon, earthRadius);
|
|
points.push(point);
|
|
}
|
|
|
|
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
const line = new THREE.Line(geometry, gridMaterial);
|
|
line.userData = { type: 'longitude', value: lon };
|
|
earthObj.add(line);
|
|
longitudeLines.push(line);
|
|
}
|
|
}
|
|
|
|
export function getEarth() {
|
|
return earth;
|
|
}
|
|
|
|
export function getClouds() {
|
|
return clouds;
|
|
}
|
|
|
|
export function clearEarthTexture() {
|
|
if (!_earthMaterial) return;
|
|
_earthMaterial.map = null;
|
|
_earthMaterial.needsUpdate = true;
|
|
}
|
|
|
|
export function loadEarthTexture() {
|
|
return new Promise((resolve) => {
|
|
if (!_earthMaterial) { resolve(); return; }
|
|
|
|
const urls = EARTH_MATERIAL_CONFIG.textureUrls;
|
|
const tryLoad = (index) => {
|
|
if (index >= urls.length) {
|
|
console.warn('所有地球纹理加载失败');
|
|
resolve();
|
|
return;
|
|
}
|
|
textureLoader.load(
|
|
urls[index],
|
|
(texture) => {
|
|
texture.wrapS = THREE.RepeatWrapping;
|
|
texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
texture.anisotropy = 16;
|
|
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
texture.magFilter = THREE.LinearFilter;
|
|
_earthMaterial.map = texture;
|
|
_earthMaterial.needsUpdate = true;
|
|
resolve();
|
|
},
|
|
null,
|
|
() => tryLoad(index + 1),
|
|
);
|
|
};
|
|
tryLoad(0);
|
|
});
|
|
}
|