Release 0.58.0 includes the Earth high-precision boundary PMTiles/MVT pipeline, standardized Earth boundary source collectors, China POV boundary configuration templates, and removal of the legacy low-precision GeoJSON fallback. It also adds Earth news target-location queueing/archive support, fixes datasource task status visibility, documents the Earth surface depth-spacing rules that prevent far-zoom z-fighting snow/black blocks, and updates bilingual operations/developer docs.
624 lines
20 KiB
JavaScript
624 lines
20 KiB
JavaScript
// earth.js - 3D Earth creation module
|
|
|
|
import * as THREE from 'three';
|
|
import {
|
|
CLOUD_LAYER_CONFIG,
|
|
CONFIG,
|
|
EARTH_CONFIG,
|
|
EARTH_MATERIAL_CONFIG,
|
|
GRID_CONFIG,
|
|
STARFIELD_CONFIG,
|
|
TERRAIN_CONFIG,
|
|
} from './constants.js';
|
|
import { latLonToVector3 } from './utils.js';
|
|
|
|
export let earth = null;
|
|
export let clouds = null;
|
|
export let terrain = null;
|
|
let showGridLines = false;
|
|
let showClouds = true;
|
|
|
|
const textureLoader = new THREE.TextureLoader();
|
|
let _earthMaterial = null;
|
|
let _earthTextureOverlay = null;
|
|
let _earthTextureOverlayMaterial = null;
|
|
let _earthShaders = [];
|
|
let _dayNightEnabled = true;
|
|
let _loadedTexture = null;
|
|
let _textureLoadPromise = null;
|
|
let _textureVisible = true;
|
|
let _earthRimGlow = null;
|
|
let _cloudTexture = null;
|
|
let _cloudTextureLoadPromise = null;
|
|
const _earthSunDirection = new THREE.Vector3(
|
|
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.x,
|
|
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.y,
|
|
EARTH_MATERIAL_CONFIG.dayNight.sunDirection.z,
|
|
).normalize();
|
|
|
|
function applyEarthDayNightShader(material, options = {}) {
|
|
if (!material || !EARTH_MATERIAL_CONFIG.dayNight.enabled) return;
|
|
|
|
const twilightColor = new THREE.Color(EARTH_MATERIAL_CONFIG.dayNight.twilightColor);
|
|
const nightTintColor = new THREE.Color(EARTH_MATERIAL_CONFIG.dayNight.nightTintColor);
|
|
const {
|
|
nightFloor = EARTH_MATERIAL_CONFIG.dayNight.nightFloor,
|
|
} = options;
|
|
material.onBeforeCompile = (shader) => {
|
|
_earthShaders.push(shader);
|
|
shader.uniforms.uSunDirectionWorld = { value: _earthSunDirection.clone() };
|
|
shader.uniforms.uNightFloor = { value: nightFloor };
|
|
shader.uniforms.uDayBoost = { value: EARTH_MATERIAL_CONFIG.dayNight.dayBoost };
|
|
shader.uniforms.uFeatherScale = { value: EARTH_MATERIAL_CONFIG.dayNight.featherScale };
|
|
shader.uniforms.uTwilightWidth = { value: EARTH_MATERIAL_CONFIG.dayNight.twilightWidth };
|
|
shader.uniforms.uTwilightFeatherScale = {
|
|
value: EARTH_MATERIAL_CONFIG.dayNight.twilightFeatherScale,
|
|
};
|
|
shader.uniforms.uTwilightIntensity = { value: EARTH_MATERIAL_CONFIG.dayNight.twilightIntensity };
|
|
shader.uniforms.uTwilightColor = { value: twilightColor };
|
|
shader.uniforms.uNightTintColor = { value: nightTintColor };
|
|
shader.uniforms.uNightTintIntensity = { value: EARTH_MATERIAL_CONFIG.dayNight.nightTintIntensity };
|
|
shader.uniforms.uDayNightEnabled = { value: _dayNightEnabled ? 1.0 : 0.0 };
|
|
|
|
shader.vertexShader = shader.vertexShader.replace(
|
|
"#include <common>",
|
|
`#include <common>
|
|
varying vec3 vWorldNormal;`,
|
|
).replace(
|
|
"#include <begin_vertex>",
|
|
`#include <begin_vertex>
|
|
vWorldNormal = normalize(mat3(modelMatrix) * normal);`,
|
|
);
|
|
|
|
shader.fragmentShader = shader.fragmentShader.replace(
|
|
"#include <common>",
|
|
`#include <common>
|
|
varying vec3 vWorldNormal;
|
|
uniform vec3 uSunDirectionWorld;
|
|
uniform float uNightFloor;
|
|
uniform float uDayBoost;
|
|
uniform float uFeatherScale;
|
|
uniform float uTwilightWidth;
|
|
uniform float uTwilightFeatherScale;
|
|
uniform float uTwilightIntensity;
|
|
uniform vec3 uTwilightColor;
|
|
uniform vec3 uNightTintColor;
|
|
uniform float uNightTintIntensity;
|
|
uniform float uDayNightEnabled;`,
|
|
).replace(
|
|
"#include <output_fragment>",
|
|
`
|
|
vec3 worldNormal = normalize(vWorldNormal);
|
|
vec3 sunDir = normalize(uSunDirectionWorld);
|
|
float sunFacing = dot(worldNormal, sunDir);
|
|
float edgeFeather = max(fwidth(sunFacing) * uFeatherScale, uTwilightWidth);
|
|
float daylight = smoothstep(-edgeFeather, edgeFeather, sunFacing);
|
|
float twilight = 1.0 - smoothstep(0.0, edgeFeather * uTwilightFeatherScale, abs(sunFacing));
|
|
|
|
// Camera-facing diffuse: vNormal and vViewPosition are both in view space.
|
|
// N·V gives 1.0 at center-facing, 0 at limb — creates depth cue regardless of earth rotation.
|
|
float nDotV = max(0.0, dot(normalize(vNormal), normalize(vViewPosition)));
|
|
float cameraBoost = mix(0.62, 1.08, nDotV);
|
|
|
|
float dn = uDayNightEnabled;
|
|
vec3 dnLight = outgoingLight;
|
|
dnLight *= mix(uNightFloor, uDayBoost, daylight);
|
|
dnLight += uTwilightColor * twilight * uTwilightIntensity;
|
|
dnLight += uNightTintColor * (1.0 - daylight) * uNightTintIntensity;
|
|
dnLight = dnLight / (vec3(1.0) + max(dnLight - vec3(0.68), vec3(0.0)) * 0.86);
|
|
|
|
// dn=0: emissive base (from material, set in JS) * camera-facing boost → always readable
|
|
// dn=1: full day/night solar lighting
|
|
outgoingLight = mix(outgoingLight * cameraBoost, dnLight, dn);
|
|
|
|
#include <output_fragment>
|
|
`,
|
|
);
|
|
};
|
|
|
|
material.customProgramCacheKey = () => "earth-day-night-v5";
|
|
material.needsUpdate = true;
|
|
}
|
|
|
|
export function createEarth(scene) {
|
|
_earthShaders = [];
|
|
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: C.opacity < 1,
|
|
opacity: C.opacity,
|
|
side: THREE.FrontSide,
|
|
depthWrite: true,
|
|
depthTest: true,
|
|
});
|
|
applyEarthDayNightShader(material);
|
|
_earthMaterial = material;
|
|
|
|
earth = new THREE.Mesh(geometry, material);
|
|
earth.renderOrder = 0;
|
|
earth.rotation.x = EARTH_CONFIG.tiltRad;
|
|
scene.add(earth);
|
|
|
|
const textureOverlayGeometry = new THREE.SphereGeometry(
|
|
CONFIG.earthRadius + C.textureOverlayAltitudeOffset,
|
|
128,
|
|
128,
|
|
);
|
|
_earthTextureOverlayMaterial = new THREE.MeshPhongMaterial({
|
|
color: 0xffffff,
|
|
specular: C.textureOverlaySpecular,
|
|
shininess: C.textureOverlayShininess,
|
|
transparent: true,
|
|
opacity: C.textureOverlayOpacity,
|
|
side: THREE.FrontSide,
|
|
depthWrite: false,
|
|
depthTest: true,
|
|
});
|
|
applyEarthDayNightShader(_earthTextureOverlayMaterial);
|
|
_earthTextureOverlay = new THREE.Mesh(
|
|
textureOverlayGeometry,
|
|
_earthTextureOverlayMaterial,
|
|
);
|
|
_earthTextureOverlay.name = "earth-high-res-texture-overlay";
|
|
_earthTextureOverlay.renderOrder = C.textureOverlayRenderOrder;
|
|
_earthTextureOverlay.visible = false;
|
|
earth.add(_earthTextureOverlay);
|
|
|
|
// 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,
|
|
depthTest: false,
|
|
depthWrite: true,
|
|
side: THREE.FrontSide,
|
|
});
|
|
const occluder = new THREE.Mesh(occluderGeometry, occluderMaterial);
|
|
occluder.renderOrder = 0.5;
|
|
earth.add(occluder);
|
|
|
|
// Keep the original atmosphere shells on the legacy camera-facing shader so
|
|
// they stay as a soft edge cue instead of becoming a visible transparent hull
|
|
// at close zoom levels.
|
|
const ATMOS_VERTEX_SHADER = `
|
|
varying vec3 vNormal;
|
|
void main() {
|
|
vNormal = normalize(normalMatrix * normal);
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`;
|
|
|
|
const RIM_VERTEX_SHADER = `
|
|
varying vec3 vNormal;
|
|
varying vec3 vViewDirection;
|
|
void main() {
|
|
vNormal = normalize(normalMatrix * normal);
|
|
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
vViewDirection = normalize(-mvPosition.xyz);
|
|
gl_Position = projectionMatrix * mvPosition;
|
|
}
|
|
`;
|
|
|
|
// 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);
|
|
|
|
// Fresnel rim cue: an outer shell keeps the edge tied to the globe while bypassing
|
|
// the darker fill layers that can otherwise hide a same-radius glow. Unlike the
|
|
// legacy atmosphere shells, this one uses the real view direction so its highlight
|
|
// stays attached to the visible globe edge while zooming.
|
|
const [rr, rg, rb] = C.rimGlowColor;
|
|
const rimGlowGeo = new THREE.SphereGeometry(
|
|
CONFIG.earthRadius * C.rimGlowRadiusFactor,
|
|
C.rimGlowSegments,
|
|
C.rimGlowSegments,
|
|
);
|
|
const rimGlowMat = new THREE.ShaderMaterial({
|
|
vertexShader: RIM_VERTEX_SHADER,
|
|
fragmentShader: `
|
|
varying vec3 vNormal;
|
|
varying vec3 vViewDirection;
|
|
void main() {
|
|
float viewFacing = max(dot(normalize(vNormal), normalize(vViewDirection)), 0.0);
|
|
float rim = 1.0 - viewFacing;
|
|
float alpha = pow(rim, ${C.rimGlowPower.toFixed(1)}) * ${C.rimGlowIntensity.toFixed(2)};
|
|
gl_FragColor = vec4(${rr.toFixed(2)}, ${rg.toFixed(2)}, ${rb.toFixed(2)}, alpha);
|
|
}
|
|
`,
|
|
blending: THREE.AdditiveBlending,
|
|
side: THREE.FrontSide,
|
|
transparent: true,
|
|
depthTest: false,
|
|
depthWrite: false,
|
|
});
|
|
_earthRimGlow = new THREE.Mesh(rimGlowGeo, rimGlowMat);
|
|
_earthRimGlow.name = "earth-rim-glow";
|
|
_earthRimGlow.renderOrder = C.rimGlowRenderOrder;
|
|
earth.add(_earthRimGlow);
|
|
|
|
// Texture is loaded separately via loadEarthTexture() for staged loading
|
|
return earth;
|
|
}
|
|
|
|
export function createClouds(scene, earthObj) {
|
|
const geometry = new THREE.SphereGeometry(
|
|
CONFIG.earthRadius + CLOUD_LAYER_CONFIG.radiusOffset,
|
|
CLOUD_LAYER_CONFIG.widthSegments,
|
|
CLOUD_LAYER_CONFIG.heightSegments,
|
|
);
|
|
const material = new THREE.MeshPhongMaterial({
|
|
transparent: true,
|
|
opacity: CLOUD_LAYER_CONFIG.opacity,
|
|
depthTest: true,
|
|
depthWrite: false,
|
|
blending: THREE.AdditiveBlending,
|
|
side: THREE.DoubleSide
|
|
});
|
|
|
|
clouds = new THREE.Mesh(geometry, material);
|
|
clouds.name = "earth-atmosphere-clouds";
|
|
clouds.visible = false;
|
|
earthObj.add(clouds);
|
|
|
|
return clouds;
|
|
}
|
|
|
|
export function toggleClouds(visible) {
|
|
showClouds = Boolean(visible);
|
|
if (clouds) {
|
|
clouds.visible = showClouds && Boolean(clouds.material?.map);
|
|
}
|
|
}
|
|
|
|
export function getShowClouds() {
|
|
return showClouds;
|
|
}
|
|
|
|
export function loadCloudTexture() {
|
|
if (_cloudTexture) return Promise.resolve(_cloudTexture);
|
|
if (_cloudTextureLoadPromise) return _cloudTextureLoadPromise;
|
|
|
|
_cloudTextureLoadPromise = new Promise((resolve, reject) => {
|
|
if (!clouds?.material) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
|
|
textureLoader.load(
|
|
CLOUD_LAYER_CONFIG.textureUrl,
|
|
(texture) => {
|
|
_cloudTexture = texture;
|
|
clouds.material.map = texture;
|
|
clouds.material.needsUpdate = true;
|
|
clouds.visible = showClouds;
|
|
resolve(texture);
|
|
},
|
|
undefined,
|
|
(error) => {
|
|
console.warn("云层纹理加载失败");
|
|
reject(error);
|
|
},
|
|
);
|
|
});
|
|
|
|
_cloudTextureLoadPromise.finally(() => {
|
|
_cloudTextureLoadPromise = null;
|
|
});
|
|
return _cloudTextureLoadPromise;
|
|
}
|
|
|
|
export function createTerrain(earthObj) {
|
|
const geometry = new THREE.SphereGeometry(
|
|
CONFIG.earthRadius + TERRAIN_CONFIG.baseRadiusOffset,
|
|
TERRAIN_CONFIG.geometryWidthSegments,
|
|
TERRAIN_CONFIG.geometryHeightSegments,
|
|
);
|
|
const material = new THREE.MeshPhongMaterial({
|
|
color: TERRAIN_CONFIG.color,
|
|
emissive: TERRAIN_CONFIG.emissive,
|
|
specular: TERRAIN_CONFIG.specular,
|
|
shininess: TERRAIN_CONFIG.shininess,
|
|
vertexColors: true,
|
|
transparent: true,
|
|
opacity: TERRAIN_CONFIG.opacity,
|
|
flatShading: false,
|
|
depthWrite: false,
|
|
depthTest: true,
|
|
polygonOffset: true,
|
|
polygonOffsetFactor: -1,
|
|
polygonOffsetUnits: -1,
|
|
});
|
|
|
|
terrain = new THREE.Mesh(geometry, material);
|
|
terrain.name = "earth-real-terrain";
|
|
terrain.visible = false;
|
|
terrain.renderOrder = 1.2;
|
|
terrain.raycast = () => {};
|
|
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 = STARFIELD_CONFIG.count;
|
|
const starPositions = new Float32Array(starCount * 3);
|
|
|
|
for (let i = 0; i < starCount * 3; i += 3) {
|
|
const r = STARFIELD_CONFIG.minRadius + Math.random() * STARFIELD_CONFIG.radiusJitter;
|
|
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: STARFIELD_CONFIG.color,
|
|
size: STARFIELD_CONFIG.size,
|
|
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 = CONFIG.earthRadius + GRID_CONFIG.radiusOffset;
|
|
const gridMaterial = new THREE.LineBasicMaterial({
|
|
color: GRID_CONFIG.color,
|
|
transparent: true,
|
|
opacity: GRID_CONFIG.opacity,
|
|
linewidth: GRID_CONFIG.lineWidth,
|
|
depthTest: true,
|
|
depthWrite: false,
|
|
});
|
|
|
|
for (let lat = -75; lat <= 75; lat += GRID_CONFIG.latitudeStep) {
|
|
const points = [];
|
|
for (let lon = -180; lon <= 180; lon += GRID_CONFIG.segmentStep) {
|
|
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 };
|
|
line.renderOrder = GRID_CONFIG.renderOrder;
|
|
line.visible = showGridLines;
|
|
earthObj.add(line);
|
|
latitudeLines.push(line);
|
|
}
|
|
|
|
for (let lon = -180; lon <= 180; lon += GRID_CONFIG.longitudeStep) {
|
|
const points = [];
|
|
for (let lat = -90; lat <= 90; lat += GRID_CONFIG.segmentStep) {
|
|
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 };
|
|
line.renderOrder = GRID_CONFIG.renderOrder;
|
|
line.visible = showGridLines;
|
|
earthObj.add(line);
|
|
longitudeLines.push(line);
|
|
}
|
|
}
|
|
|
|
export function toggleGridLines(visible) {
|
|
showGridLines = visible;
|
|
latitudeLines.forEach((line) => {
|
|
line.visible = visible;
|
|
});
|
|
longitudeLines.forEach((line) => {
|
|
line.visible = visible;
|
|
});
|
|
}
|
|
|
|
export function getShowGridLines() {
|
|
return showGridLines;
|
|
}
|
|
|
|
export function getEarth() {
|
|
return earth;
|
|
}
|
|
|
|
export function getEarthSurfacePickTarget() {
|
|
return _earthTextureOverlay?.visible ? _earthTextureOverlay : earth;
|
|
}
|
|
|
|
export function getClouds() {
|
|
return clouds;
|
|
}
|
|
|
|
export function clearEarthTexture() {
|
|
_loadedTexture = null;
|
|
_textureLoadPromise = null;
|
|
if (_earthTextureOverlayMaterial) {
|
|
_earthTextureOverlayMaterial.map = null;
|
|
_earthTextureOverlayMaterial.needsUpdate = true;
|
|
}
|
|
if (_earthTextureOverlay) {
|
|
_earthTextureOverlay.visible = false;
|
|
}
|
|
if (_earthRimGlow) {
|
|
_earthRimGlow.visible = true;
|
|
}
|
|
}
|
|
|
|
export function setEarthSunDirection(direction) {
|
|
if (!direction) return;
|
|
_earthSunDirection.copy(direction).normalize();
|
|
_earthShaders.forEach((shader) => {
|
|
shader?.uniforms?.uSunDirectionWorld?.value?.copy(_earthSunDirection);
|
|
});
|
|
}
|
|
|
|
export function setDayNightEnabled(enabled) {
|
|
_dayNightEnabled = enabled;
|
|
_earthShaders.forEach((shader) => {
|
|
if (shader?.uniforms?.uDayNightEnabled) {
|
|
shader.uniforms.uDayNightEnabled.value = enabled ? 1.0 : 0.0;
|
|
}
|
|
});
|
|
if (_earthMaterial) {
|
|
if (enabled) {
|
|
// Restore normal Phong lighting + custom day/night shader
|
|
_earthMaterial.color.setHex(EARTH_MATERIAL_CONFIG.color);
|
|
_earthMaterial.emissive.setHex(EARTH_MATERIAL_CONFIG.emissive);
|
|
_earthMaterial.emissiveMap = null;
|
|
} else {
|
|
// Full bright: zero diffuse so directional light has no effect;
|
|
_earthMaterial.color.setRGB(0, 0, 0);
|
|
_earthMaterial.emissive.setHex(EARTH_MATERIAL_CONFIG.color);
|
|
_earthMaterial.emissiveMap = null;
|
|
}
|
|
_earthMaterial.needsUpdate = true;
|
|
}
|
|
}
|
|
|
|
export function loadEarthTexture() {
|
|
if (_loadedTexture) return Promise.resolve(_loadedTexture);
|
|
if (_textureLoadPromise) return _textureLoadPromise;
|
|
|
|
_textureLoadPromise = new Promise((resolve) => {
|
|
if (!_earthTextureOverlayMaterial) { 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;
|
|
_loadedTexture = texture;
|
|
_earthTextureOverlayMaterial.map = texture;
|
|
_earthTextureOverlayMaterial.needsUpdate = true;
|
|
if (_earthTextureOverlay) {
|
|
_earthTextureOverlay.visible = _textureVisible;
|
|
}
|
|
if (_earthRimGlow) {
|
|
_earthRimGlow.visible = !_textureVisible;
|
|
}
|
|
resolve(texture);
|
|
},
|
|
null,
|
|
() => tryLoad(index + 1),
|
|
);
|
|
};
|
|
tryLoad(0);
|
|
});
|
|
|
|
_textureLoadPromise.finally(() => {
|
|
_textureLoadPromise = null;
|
|
});
|
|
return _textureLoadPromise;
|
|
}
|
|
|
|
export function setEarthTextureVisible(visible) {
|
|
_textureVisible = Boolean(visible);
|
|
const textureShowing = _textureVisible && Boolean(_loadedTexture);
|
|
if (_earthTextureOverlay) {
|
|
_earthTextureOverlay.visible = textureShowing;
|
|
}
|
|
if (_earthTextureOverlayMaterial) {
|
|
_earthTextureOverlayMaterial.map = _loadedTexture || null;
|
|
_earthTextureOverlayMaterial.needsUpdate = true;
|
|
}
|
|
if (_earthRimGlow) {
|
|
_earthRimGlow.visible = !textureShowing;
|
|
}
|
|
}
|
|
|
|
export function getEarthTextureVisible() {
|
|
return _textureVisible;
|
|
}
|