release: bump version to 0.29.0
This commit is contained in:
487
frontend/public/earth/js/celestial.js
Normal file
487
frontend/public/earth/js/celestial.js
Normal file
@@ -0,0 +1,487 @@
|
||||
import * as THREE from "three";
|
||||
import * as Astronomy from "astronomy-engine";
|
||||
|
||||
import { CELESTIAL_CONFIG, EARTH_CONFIG } from "./constants.js";
|
||||
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
const defaultSunDirection = new THREE.Vector3(1, 0.2, 0.4).normalize();
|
||||
const defaultMoonDirection = new THREE.Vector3(-0.6, 0.45, -0.2).normalize();
|
||||
|
||||
let celestialRoot = null;
|
||||
let skySphere = null;
|
||||
let brightStarsGroup = null;
|
||||
let sunSprite = null;
|
||||
let moonSprite = null;
|
||||
let sunHaloSprite = null;
|
||||
let moonHaloSprite = null;
|
||||
let brightStarTexture = null;
|
||||
let sunDirection = defaultSunDirection.clone();
|
||||
let moonDirection = defaultMoonDirection.clone();
|
||||
let lastUpdatedAt = 0;
|
||||
let linkedSunLight = null;
|
||||
let linkedBackLight = null;
|
||||
let linkedEarth = null;
|
||||
let brightStarSprites = [];
|
||||
let celestialRotationQuaternion = new THREE.Quaternion();
|
||||
let celestialViewQuaternion = new THREE.Quaternion();
|
||||
let runtimeOrientationEuler = {
|
||||
...CELESTIAL_CONFIG.orientationEulerRad,
|
||||
};
|
||||
let runtimeFollowConfig = {
|
||||
...CELESTIAL_CONFIG.followEarthRotation,
|
||||
};
|
||||
const scratchEuler = new THREE.Euler(0, 0, 0, "YXZ");
|
||||
|
||||
function getCelestialEuler() {
|
||||
const { x, y, z } = runtimeOrientationEuler;
|
||||
return new THREE.Euler(x, y, z, "YXZ");
|
||||
}
|
||||
|
||||
function refreshCelestialOrientation() {
|
||||
celestialRotationQuaternion.setFromEuler(getCelestialEuler());
|
||||
refreshCelestialView();
|
||||
}
|
||||
|
||||
function refreshCelestialView() {
|
||||
if (linkedEarth && runtimeFollowConfig.enabled) {
|
||||
const followX = runtimeFollowConfig.x
|
||||
? (linkedEarth.rotation.x - EARTH_CONFIG.tiltRad) * (runtimeFollowConfig.invertX ? -1 : 1)
|
||||
: 0;
|
||||
const followY = runtimeFollowConfig.y
|
||||
? linkedEarth.rotation.y * (runtimeFollowConfig.invertY ? -1 : 1)
|
||||
: 0;
|
||||
const followZ = runtimeFollowConfig.z
|
||||
? linkedEarth.rotation.z * (runtimeFollowConfig.invertZ ? -1 : 1)
|
||||
: 0;
|
||||
|
||||
scratchEuler.set(
|
||||
followX,
|
||||
followY,
|
||||
followZ,
|
||||
"YXZ",
|
||||
);
|
||||
celestialViewQuaternion.setFromEuler(scratchEuler);
|
||||
} else {
|
||||
celestialViewQuaternion.identity();
|
||||
}
|
||||
|
||||
if (celestialRoot) {
|
||||
celestialRoot.quaternion
|
||||
.copy(celestialRotationQuaternion)
|
||||
.multiply(celestialViewQuaternion);
|
||||
}
|
||||
}
|
||||
|
||||
function applyCelestialOrientation(direction) {
|
||||
return direction
|
||||
.clone()
|
||||
.applyQuaternion(celestialRotationQuaternion)
|
||||
.applyQuaternion(celestialViewQuaternion)
|
||||
.normalize();
|
||||
}
|
||||
|
||||
function configureTextureEncoding(texture) {
|
||||
if (!texture) return;
|
||||
if ("colorSpace" in texture && THREE.SRGBColorSpace) {
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
} else if ("encoding" in texture && THREE.sRGBEncoding) {
|
||||
texture.encoding = THREE.sRGBEncoding;
|
||||
}
|
||||
}
|
||||
|
||||
function createDiscTexture(stops, size = 256) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const gradient = ctx.createRadialGradient(
|
||||
size / 2,
|
||||
size / 2,
|
||||
0,
|
||||
size / 2,
|
||||
size / 2,
|
||||
size / 2,
|
||||
);
|
||||
|
||||
stops.forEach(([offset, color]) => gradient.addColorStop(offset, color));
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
configureTextureEncoding(texture);
|
||||
texture.needsUpdate = true;
|
||||
return texture;
|
||||
}
|
||||
|
||||
function createSprite({ texture, scale, opacity = 1, name, color = 0xffffff }) {
|
||||
const material = new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
opacity,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
toneMapped: false,
|
||||
color,
|
||||
});
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.name = name;
|
||||
sprite.scale.setScalar(scale);
|
||||
sprite.renderOrder = 100;
|
||||
return sprite;
|
||||
}
|
||||
|
||||
function createSkySphere() {
|
||||
const geometry = new THREE.SphereGeometry(
|
||||
CELESTIAL_CONFIG.skyRadius,
|
||||
64,
|
||||
64,
|
||||
);
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
color: 0xffffff,
|
||||
side: THREE.BackSide,
|
||||
transparent: CELESTIAL_CONFIG.skyOpacity < 1,
|
||||
opacity: CELESTIAL_CONFIG.skyOpacity,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
fog: false,
|
||||
});
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.name = "celestial-sky-sphere";
|
||||
mesh.renderOrder = -1000;
|
||||
mesh.raycast = () => {};
|
||||
|
||||
textureLoader.load(
|
||||
CELESTIAL_CONFIG.starMapUrl,
|
||||
(texture) => {
|
||||
configureTextureEncoding(texture);
|
||||
material.map = texture;
|
||||
material.needsUpdate = true;
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
console.warn("Failed to load celestial star map texture");
|
||||
},
|
||||
);
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
function geoVectorToWorldDirection(vector) {
|
||||
return new THREE.Vector3(vector.x, vector.z, vector.y).normalize();
|
||||
}
|
||||
|
||||
function raDecToWorldDirection(raDeg, decDeg) {
|
||||
const raRad = THREE.MathUtils.degToRad(raDeg);
|
||||
const decRad = THREE.MathUtils.degToRad(decDeg);
|
||||
|
||||
const x = Math.cos(decRad) * Math.cos(raRad);
|
||||
const y = Math.sin(decRad);
|
||||
const z = Math.cos(decRad) * Math.sin(raRad);
|
||||
|
||||
return new THREE.Vector3(x, z, y).normalize();
|
||||
}
|
||||
|
||||
function colorFromBvIndex(colorIndex) {
|
||||
if (colorIndex <= -0.2) return 0xa8c9ff;
|
||||
if (colorIndex <= 0.0) return 0xc8dcff;
|
||||
if (colorIndex <= 0.3) return 0xf4f7ff;
|
||||
if (colorIndex <= 0.7) return 0xfff4dc;
|
||||
if (colorIndex <= 1.1) return 0xffdfb0;
|
||||
if (colorIndex <= 1.5) return 0xffc47f;
|
||||
return 0xffa35e;
|
||||
}
|
||||
|
||||
function scaleFromMagnitude(mag) {
|
||||
const brightness = THREE.MathUtils.clamp(
|
||||
1 - (mag - (-1.5)) / (CELESTIAL_CONFIG.brightStarMinMag - (-1.5)),
|
||||
0,
|
||||
1,
|
||||
);
|
||||
return THREE.MathUtils.lerp(
|
||||
CELESTIAL_CONFIG.brightStarMinScale,
|
||||
CELESTIAL_CONFIG.brightStarMaxScale,
|
||||
Math.pow(brightness, 0.72),
|
||||
);
|
||||
}
|
||||
|
||||
function createBrightStarTexture() {
|
||||
return createDiscTexture([
|
||||
[0, "rgba(255,255,255,1)"],
|
||||
[0.18, "rgba(255,255,255,0.98)"],
|
||||
[0.46, "rgba(215,230,255,0.42)"],
|
||||
[1, "rgba(128,168,255,0)"],
|
||||
], 128);
|
||||
}
|
||||
|
||||
function createBrightStarsGroup() {
|
||||
const group = new THREE.Group();
|
||||
group.name = "bright-stars-group";
|
||||
group.visible = true;
|
||||
return group;
|
||||
}
|
||||
|
||||
async function loadBrightStars() {
|
||||
try {
|
||||
const response = await fetch(CELESTIAL_CONFIG.brightStarsUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const stars = await response.json();
|
||||
const filteredStars = stars
|
||||
.filter((star) => Number.isFinite(star?.raDeg) && Number.isFinite(star?.decDeg))
|
||||
.filter((star) => star.mag <= CELESTIAL_CONFIG.brightStarMinMag)
|
||||
.sort((a, b) => a.mag - b.mag)
|
||||
.slice(0, CELESTIAL_CONFIG.brightStarMaxCount);
|
||||
|
||||
brightStarTexture = createBrightStarTexture();
|
||||
brightStarSprites = filteredStars.map((star) => {
|
||||
const sprite = createSprite({
|
||||
texture: brightStarTexture,
|
||||
scale: scaleFromMagnitude(star.mag),
|
||||
opacity: CELESTIAL_CONFIG.brightStarOpacity,
|
||||
name: `bright-star-${star.name}`,
|
||||
color: colorFromBvIndex(star.colorIndex ?? 0.4),
|
||||
});
|
||||
sprite.position
|
||||
.copy(raDecToWorldDirection(star.raDeg, star.decDeg))
|
||||
.multiplyScalar(CELESTIAL_CONFIG.brightStarDistance);
|
||||
sprite.userData.star = star;
|
||||
return sprite;
|
||||
});
|
||||
|
||||
brightStarSprites.forEach((sprite) => brightStarsGroup?.add(sprite));
|
||||
} catch (error) {
|
||||
console.warn("Failed to load bright star layer", error);
|
||||
}
|
||||
}
|
||||
|
||||
function computeBodyDirection(body, date) {
|
||||
const vector = Astronomy.GeoVector(body, date, false);
|
||||
return geoVectorToWorldDirection(vector);
|
||||
}
|
||||
|
||||
function updateSpritePositions() {
|
||||
if (sunSprite) {
|
||||
sunSprite.position.copy(sunDirection).multiplyScalar(CELESTIAL_CONFIG.sunDistance);
|
||||
}
|
||||
if (sunHaloSprite) {
|
||||
sunHaloSprite.position.copy(sunDirection).multiplyScalar(CELESTIAL_CONFIG.sunDistance);
|
||||
}
|
||||
|
||||
if (moonSprite) {
|
||||
moonSprite.position.copy(moonDirection).multiplyScalar(CELESTIAL_CONFIG.moonDistance);
|
||||
}
|
||||
if (moonHaloSprite) {
|
||||
moonHaloSprite.position.copy(moonDirection).multiplyScalar(CELESTIAL_CONFIG.moonDistance);
|
||||
}
|
||||
}
|
||||
|
||||
function updateLighting() {
|
||||
const calibratedSunDirection = applyCelestialOrientation(sunDirection);
|
||||
|
||||
if (linkedSunLight) {
|
||||
linkedSunLight.color.setHex(CELESTIAL_CONFIG.sunLightColor);
|
||||
linkedSunLight.intensity = CELESTIAL_CONFIG.sunLightIntensity;
|
||||
linkedSunLight.position
|
||||
.copy(calibratedSunDirection)
|
||||
.multiplyScalar(CELESTIAL_CONFIG.sunLightDistance);
|
||||
}
|
||||
|
||||
if (linkedBackLight) {
|
||||
linkedBackLight.color.setHex(CELESTIAL_CONFIG.backLightColor);
|
||||
linkedBackLight.intensity = CELESTIAL_CONFIG.backLightIntensity;
|
||||
linkedBackLight.position
|
||||
.copy(calibratedSunDirection)
|
||||
.multiplyScalar(-CELESTIAL_CONFIG.sunLightDistance * 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
function computeCelestialState(date = new Date()) {
|
||||
sunDirection = computeBodyDirection(Astronomy.Body.Sun, date);
|
||||
moonDirection = computeBodyDirection(Astronomy.Body.Moon, date);
|
||||
updateSpritePositions();
|
||||
updateLighting();
|
||||
lastUpdatedAt = date.getTime();
|
||||
}
|
||||
|
||||
export function initCelestialLayer(
|
||||
scene,
|
||||
{ camera = null, sunLight = null, backLight = null, earth = null } = {},
|
||||
) {
|
||||
if (!scene || !CELESTIAL_CONFIG.enabled) return null;
|
||||
|
||||
disposeCelestialLayer();
|
||||
|
||||
linkedSunLight = sunLight;
|
||||
linkedBackLight = backLight;
|
||||
linkedEarth = earth;
|
||||
|
||||
celestialRoot = new THREE.Group();
|
||||
celestialRoot.name = "celestial-root";
|
||||
celestialRoot.position.set(0, 0, 0);
|
||||
|
||||
refreshCelestialOrientation();
|
||||
|
||||
skySphere = createSkySphere();
|
||||
brightStarsGroup = createBrightStarsGroup();
|
||||
|
||||
const sunTexture = createDiscTexture([
|
||||
[0, "rgba(255,255,255,1)"],
|
||||
[0.08, "rgba(255,251,242,1)"],
|
||||
[0.22, "rgba(255,242,205,0.98)"],
|
||||
[0.52, "rgba(255,211,117,0.9)"],
|
||||
[0.82, "rgba(255,162,54,0.18)"],
|
||||
[1, "rgba(255,120,32,0)"],
|
||||
]);
|
||||
const sunHaloTexture = createDiscTexture([
|
||||
[0, "rgba(255,245,214,0.9)"],
|
||||
[0.2, "rgba(255,220,154,0.54)"],
|
||||
[0.52, "rgba(255,160,72,0.16)"],
|
||||
[1, "rgba(255,120,32,0)"],
|
||||
], 512);
|
||||
const moonTexture = createDiscTexture([
|
||||
[0, "rgba(255,255,255,0.98)"],
|
||||
[0.42, "rgba(229,236,248,0.92)"],
|
||||
[0.76, "rgba(164,178,202,0.34)"],
|
||||
[1, "rgba(80,92,118,0)"],
|
||||
]);
|
||||
const moonHaloTexture = createDiscTexture([
|
||||
[0, "rgba(226,235,250,0.42)"],
|
||||
[0.38, "rgba(188,203,230,0.16)"],
|
||||
[1, "rgba(120,136,170,0)"],
|
||||
], 384);
|
||||
|
||||
sunSprite = createSprite({
|
||||
texture: sunTexture,
|
||||
scale: CELESTIAL_CONFIG.sunScale,
|
||||
name: "sun-sprite",
|
||||
});
|
||||
sunHaloSprite = createSprite({
|
||||
texture: sunHaloTexture,
|
||||
scale: CELESTIAL_CONFIG.sunHaloScale,
|
||||
opacity: 0.78,
|
||||
name: "sun-halo-sprite",
|
||||
});
|
||||
moonSprite = createSprite({
|
||||
texture: moonTexture,
|
||||
scale: CELESTIAL_CONFIG.moonScale,
|
||||
opacity: 0.98,
|
||||
name: "moon-sprite",
|
||||
});
|
||||
moonHaloSprite = createSprite({
|
||||
texture: moonHaloTexture,
|
||||
scale: CELESTIAL_CONFIG.moonHaloScale,
|
||||
opacity: 0.52,
|
||||
name: "moon-halo-sprite",
|
||||
});
|
||||
|
||||
sunHaloSprite.renderOrder = 98;
|
||||
moonHaloSprite.renderOrder = 98;
|
||||
|
||||
celestialRoot.add(skySphere);
|
||||
celestialRoot.add(brightStarsGroup);
|
||||
celestialRoot.add(sunHaloSprite);
|
||||
celestialRoot.add(moonHaloSprite);
|
||||
celestialRoot.add(sunSprite);
|
||||
celestialRoot.add(moonSprite);
|
||||
scene.add(celestialRoot);
|
||||
|
||||
computeCelestialState(new Date());
|
||||
loadBrightStars();
|
||||
|
||||
return {
|
||||
root: celestialRoot,
|
||||
getSunDirection: () => sunDirection.clone(),
|
||||
getMoonDirection: () => moonDirection.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
export function updateCelestialLayer(date = new Date(), camera = null) {
|
||||
if (!celestialRoot) return;
|
||||
|
||||
refreshCelestialView();
|
||||
|
||||
const now = date.getTime();
|
||||
if (!lastUpdatedAt || now - lastUpdatedAt >= CELESTIAL_CONFIG.updateIntervalMs) {
|
||||
computeCelestialState(date);
|
||||
} else {
|
||||
updateLighting();
|
||||
updateSpritePositions();
|
||||
}
|
||||
}
|
||||
|
||||
export function getSunDirection() {
|
||||
return applyCelestialOrientation(sunDirection);
|
||||
}
|
||||
|
||||
export function getMoonDirection() {
|
||||
return applyCelestialOrientation(moonDirection);
|
||||
}
|
||||
|
||||
export function getCelestialDebugState() {
|
||||
return {
|
||||
orientationEulerRad: { ...runtimeOrientationEuler },
|
||||
followEarthRotation: { ...runtimeFollowConfig },
|
||||
};
|
||||
}
|
||||
|
||||
export function setCelestialOrientation(nextEuler = {}) {
|
||||
runtimeOrientationEuler = {
|
||||
...runtimeOrientationEuler,
|
||||
...nextEuler,
|
||||
};
|
||||
refreshCelestialOrientation();
|
||||
updateLighting();
|
||||
return getCelestialDebugState();
|
||||
}
|
||||
|
||||
export function setCelestialFollow(nextFollow = {}) {
|
||||
runtimeFollowConfig = {
|
||||
...runtimeFollowConfig,
|
||||
...nextFollow,
|
||||
};
|
||||
refreshCelestialView();
|
||||
updateLighting();
|
||||
return getCelestialDebugState();
|
||||
}
|
||||
|
||||
export function disposeCelestialLayer() {
|
||||
if (celestialRoot?.parent) {
|
||||
celestialRoot.parent.remove(celestialRoot);
|
||||
}
|
||||
|
||||
[skySphere, sunSprite, moonSprite, sunHaloSprite, moonHaloSprite, ...brightStarSprites].forEach((object) => {
|
||||
if (!object) return;
|
||||
if (object.geometry) object.geometry.dispose();
|
||||
if (object.material) {
|
||||
if (object.material.map) object.material.map.dispose?.();
|
||||
object.material.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
skySphere = null;
|
||||
brightStarsGroup = null;
|
||||
sunSprite = null;
|
||||
moonSprite = null;
|
||||
sunHaloSprite = null;
|
||||
moonHaloSprite = null;
|
||||
celestialRoot = null;
|
||||
brightStarSprites = [];
|
||||
brightStarTexture?.dispose?.();
|
||||
brightStarTexture = null;
|
||||
|
||||
lastUpdatedAt = 0;
|
||||
linkedSunLight = null;
|
||||
linkedBackLight = null;
|
||||
linkedEarth = null;
|
||||
sunDirection.copy(defaultSunDirection);
|
||||
moonDirection.copy(defaultMoonDirection);
|
||||
runtimeOrientationEuler = {
|
||||
...CELESTIAL_CONFIG.orientationEulerRad,
|
||||
};
|
||||
runtimeFollowConfig = {
|
||||
...CELESTIAL_CONFIG.followEarthRotation,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user