Geocron 3D verbessert

This commit is contained in:
2026-04-03 09:24:48 +02:00
parent eb35fd6abd
commit f4bc622c47
3 changed files with 297 additions and 40 deletions
View File
View File
+297 -40
View File
@@ -25,6 +25,14 @@ require __DIR__ . '/header.php';
<input type="checkbox" id="geocron3dLabelsToggle">
<span>Satelliten-Beschriftungen</span>
</label>
<div class="geocron3d-time-controls" aria-label="Zeitsteuerung">
<button type="button" class="geocron3d-time-button is-active" data-time-scale="1">Live</button>
<button type="button" class="geocron3d-time-button" data-time-scale="25">25x</button>
<button type="button" class="geocron3d-time-button" data-time-scale="60">60x</button>
<button type="button" class="geocron3d-time-button" data-time-scale="120">120x</button>
<button type="button" class="geocron3d-time-button" data-time-scale="0">Pause</button>
</div>
<div class="geocron3d-time-readout" id="geocron3dTimeReadout">Simulationszeit: --</div>
<div class="geocron3d-status" id="geocron3dStatus">Lade 3D-Szene…</div>
</div>
<div class="geocron3d-view" id="geocron3dView">
@@ -113,12 +121,44 @@ require __DIR__ . '/header.php';
font-size: 0.92rem;
}
.geocron3d-time-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 0 0 12px;
}
.geocron3d-time-button {
padding: 8px 12px;
border: 1px solid rgba(108, 145, 220, 0.28);
border-radius: 999px;
background: rgba(10, 16, 32, 0.72);
color: rgba(220, 232, 255, 0.92);
font: inherit;
cursor: pointer;
transition: background-color 0.16s ease, border-color 0.16s ease, transform 0.16s ease;
}
.geocron3d-time-button:hover {
border-color: rgba(125, 170, 255, 0.55);
background: rgba(18, 28, 52, 0.92);
}
.geocron3d-time-button.is-active {
border-color: rgba(150, 190, 255, 0.78);
background: rgba(38, 66, 122, 0.92);
}
.geocron3d-time-readout {
margin: 0 0 12px;
color: rgba(192, 214, 255, 0.88);
font-size: 0.9rem;
line-height: 1.5;
}
.geocron3d-view {
position: relative;
background:
radial-gradient(circle at 50% 36%, rgba(70, 110, 190, 0.12), transparent 30%),
radial-gradient(circle at 50% 68%, rgba(8, 12, 20, 0.1), transparent 44%),
#02050b;
background: #000;
}
.geocron3d-view canvas {
@@ -178,6 +218,7 @@ require __DIR__ . '/header.php';
}
</style>
<script src="./js/astronomy.browser.min.js"></script>
<script type="importmap">
{
"imports": {
@@ -190,6 +231,8 @@ require __DIR__ . '/header.php';
import * as THREE from 'three';
import { GLTFLoader } from './js/GLTFLoader.js';
const AstronomyEngine = window.Astronomy;
const EARTH_RADIUS = 6;
const EARTH_REAL_RADIUS_KM = 6371;
const ORBIT_WINDOW_MINUTES = 90;
@@ -197,14 +240,24 @@ const ORBIT_STEP_SECONDS = 10;
const ORBIT_REFRESH_MS = 30_000;
const MONTH_INDEX = new Date().getMonth() + 1;
const EARTH_LONGITUDE_OFFSET_DEG = 0;
const EARTH_AXIAL_TILT_DEG = 23.439281;
const canvas = document.getElementById('geocron3dCanvas');
const host = document.getElementById('geocron3dView');
const statusEl = document.getElementById('geocron3dStatus');
const labelsToggle = document.getElementById('geocron3dLabelsToggle');
const labelsLayer = document.getElementById('geocron3dLabels');
const timeReadoutEl = document.getElementById('geocron3dTimeReadout');
const timeButtons = Array.from(document.querySelectorAll('[data-time-scale]'));
let labelsEnabled = false;
let issModelTemplatePromise = null;
let simulatedTime = new Date();
let lastFrameTimeMs = performance.now();
let timeScale = 1;
if (!AstronomyEngine) {
throw new Error('Astronomy Engine konnte nicht geladen werden.');
}
function setStatus(text, isError = false) {
statusEl.textContent = text;
@@ -252,6 +305,64 @@ function wrapLongitude(lon) {
return ((lon + 180) % 360 + 360) % 360 - 180;
}
function getJulianDate(date) {
return date.getTime() / 86400000 + 2440587.5;
}
function getGreenwichMeanSiderealTime(date) {
const julianDate = getJulianDate(date);
const t = (julianDate - 2451545.0) / 36525;
let gmstDeg = 280.46061837
+ 360.98564736629 * (julianDate - 2451545.0)
+ 0.000387933 * t * t
- (t * t * t) / 38710000;
gmstDeg = ((gmstDeg % 360) + 360) % 360;
return THREE.MathUtils.degToRad(gmstDeg);
}
function getSunSubsolarPoint(date) {
const sunVector = AstronomyEngine.GeoVector(AstronomyEngine.Body.Sun, date, true);
const radius = Math.hypot(sunVector.x, sunVector.y, sunVector.z);
const declinationRad = Math.asin(sunVector.z / radius);
const rightAscensionRad = Math.atan2(sunVector.y, sunVector.x);
const gmstRad = getGreenwichMeanSiderealTime(date);
const subsolarLongitudeRad = THREE.MathUtils.euclideanModulo(rightAscensionRad - gmstRad + Math.PI, Math.PI * 2) - Math.PI;
return {
latDeg: THREE.MathUtils.radToDeg(declinationRad),
lonDeg: THREE.MathUtils.radToDeg(subsolarLongitudeRad),
};
}
function formatSimulationTime(date) {
return new Intl.DateTimeFormat('de-DE', {
dateStyle: 'medium',
timeStyle: 'medium',
timeZone: 'UTC',
}).format(date) + ' UTC';
}
function updateTimeReadout() {
const modeLabel = timeScale === 0 ? 'Pausiert' : (timeScale === 1 ? 'Live' : `${timeScale}x`);
timeReadoutEl.textContent = `Simulationszeit: ${formatSimulationTime(simulatedTime)} (${modeLabel})`;
}
function setTimeScale(nextScale) {
timeScale = nextScale;
if (nextScale === 1) {
simulatedTime = new Date();
lastFrameTimeMs = performance.now();
if (satelliteLib && satellites.length > 0) {
refreshOrbitLines(satellites, satelliteLib, simulatedTime);
nextOrbitRefreshAt = performance.now() + ORBIT_REFRESH_MS;
}
}
for (const button of timeButtons) {
button.classList.toggle('is-active', Number(button.dataset.timeScale) === nextScale);
}
updateTimeReadout();
}
function createOrbitGeometry(samples) {
const positions = [];
for (const sample of samples) {
@@ -272,8 +383,7 @@ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x02050b);
scene.fog = new THREE.FogExp2(0x02050b, 0.04);
scene.background = new THREE.Color(0x000000);
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 500);
camera.position.set(0, 4.8, 18);
@@ -281,6 +391,13 @@ camera.position.set(0, 4.8, 18);
const root = new THREE.Group();
scene.add(root);
const earthTiltGroup = new THREE.Group();
earthTiltGroup.rotation.z = THREE.MathUtils.degToRad(-EARTH_AXIAL_TILT_DEG);
root.add(earthTiltGroup);
const earthRotationGroup = new THREE.Group();
earthTiltGroup.add(earthRotationGroup);
const starsGeometry = new THREE.BufferGeometry();
const starPositions = [];
for (let i = 0; i < 1500; i++) {
@@ -353,7 +470,7 @@ const earthMesh = new THREE.Mesh(
new THREE.SphereGeometry(EARTH_RADIUS, 96, 96),
earthMaterial
);
root.add(earthMesh);
earthRotationGroup.add(earthMesh);
const atmosphereMesh = new THREE.Mesh(
new THREE.SphereGeometry(EARTH_RADIUS * 1.035, 64, 64),
@@ -364,23 +481,12 @@ const atmosphereMesh = new THREE.Mesh(
side: THREE.BackSide,
})
);
root.add(atmosphereMesh);
const earthGlow = new THREE.Sprite(
new THREE.SpriteMaterial({
color: 0x7aa7ff,
transparent: true,
opacity: 0.16,
depthWrite: false,
})
);
earthGlow.scale.setScalar(17);
root.add(earthGlow);
earthRotationGroup.add(atmosphereMesh);
const satelliteGroup = new THREE.Group();
const orbitGroup = new THREE.Group();
root.add(orbitGroup);
root.add(satelliteGroup);
earthRotationGroup.add(orbitGroup);
earthRotationGroup.add(satelliteGroup);
const dragState = {
active: false,
@@ -503,34 +609,158 @@ function loadIssModelTemplate() {
return issModelTemplatePromise;
}
function createSatelliteFallback(color, isIss = false) {
if (!isIss) {
return new THREE.Mesh(
new THREE.SphereGeometry(0.11, 16, 16),
new THREE.MeshBasicMaterial({ color })
);
}
function createSatelliteFallback(color, isIss = false, variantSeed = 0) {
const group = new THREE.Group();
const bodyMaterial = new THREE.MeshStandardMaterial({
color: 0xd7dde8,
color: isIss ? 0xd7dde8 : new THREE.Color(color).lerp(new THREE.Color(0xd9e1ef), 0.45),
roughness: 0.65,
metalness: 0.35,
});
const panelMaterial = new THREE.MeshStandardMaterial({
color: 0x587fc8,
color: isIss ? 0x587fc8 : new THREE.Color(color).offsetHSL(0.02, -0.05, -0.12),
roughness: 0.55,
metalness: 0.2,
emissive: 0x112244,
emissiveIntensity: 0.18,
});
const trussMaterial = new THREE.MeshStandardMaterial({
color: 0xb8c3d9,
color: isIss ? 0xb8c3d9 : 0xb9c4d8,
roughness: 0.7,
metalness: 0.28,
});
if (!isIss) {
const variant = Math.abs(variantSeed) % 4;
if (variant === 0) {
const bus = new THREE.Mesh(
new THREE.BoxGeometry(0.18, 0.14, 0.14),
bodyMaterial
);
group.add(bus);
const mast = new THREE.Mesh(
new THREE.CylinderGeometry(0.012, 0.012, 0.62, 10),
trussMaterial
);
mast.rotation.z = Math.PI / 2;
group.add(mast);
const panelGeometry = new THREE.BoxGeometry(0.24, 0.012, 0.14);
const leftPanel = new THREE.Mesh(panelGeometry, panelMaterial);
leftPanel.position.x = -0.43;
group.add(leftPanel);
const rightPanel = new THREE.Mesh(panelGeometry, panelMaterial);
rightPanel.position.x = 0.43;
group.add(rightPanel);
const antenna = new THREE.Mesh(
new THREE.CylinderGeometry(0.008, 0.008, 0.16, 10),
trussMaterial
);
antenna.position.y = 0.14;
group.add(antenna);
} else if (variant === 1) {
const body = new THREE.Mesh(
new THREE.CylinderGeometry(0.09, 0.09, 0.22, 12),
bodyMaterial
);
body.rotation.z = Math.PI / 2;
group.add(body);
const dish = new THREE.Mesh(
new THREE.SphereGeometry(0.085, 16, 12, 0, Math.PI * 2, 0, Math.PI / 2),
trussMaterial
);
dish.rotation.z = -Math.PI / 2;
dish.position.x = 0.18;
group.add(dish);
const panelGeometry = new THREE.BoxGeometry(0.18, 0.012, 0.11);
const topPanel = new THREE.Mesh(panelGeometry, panelMaterial);
topPanel.position.set(-0.05, 0.2, 0);
topPanel.rotation.z = Math.PI / 2;
group.add(topPanel);
const bottomPanel = new THREE.Mesh(panelGeometry, panelMaterial);
bottomPanel.position.set(-0.05, -0.2, 0);
bottomPanel.rotation.z = Math.PI / 2;
group.add(bottomPanel);
} else if (variant === 2) {
const core = new THREE.Mesh(
new THREE.BoxGeometry(0.16, 0.16, 0.2),
bodyMaterial
);
group.add(core);
const armX = new THREE.Mesh(
new THREE.BoxGeometry(0.58, 0.018, 0.018),
trussMaterial
);
group.add(armX);
const armY = new THREE.Mesh(
new THREE.BoxGeometry(0.018, 0.58, 0.018),
trussMaterial
);
group.add(armY);
const panelGeometry = new THREE.BoxGeometry(0.14, 0.012, 0.12);
const panelOffsets = [
[0.34, 0, 0],
[-0.34, 0, 0],
[0, 0.34, 0],
[0, -0.34, 0],
];
for (const [x, y, z] of panelOffsets) {
const panel = new THREE.Mesh(panelGeometry, panelMaterial);
panel.position.set(x, y, z);
panel.rotation.z = Math.abs(y) > 0 ? Math.PI / 2 : 0;
group.add(panel);
}
} else {
const bus = new THREE.Mesh(
new THREE.BoxGeometry(0.18, 0.11, 0.24),
bodyMaterial
);
group.add(bus);
const leftWing = new THREE.Mesh(
new THREE.BoxGeometry(0.32, 0.012, 0.12),
panelMaterial
);
leftWing.position.set(-0.28, 0.03, 0);
leftWing.rotation.z = 0.16;
group.add(leftWing);
const rightWing = new THREE.Mesh(
new THREE.BoxGeometry(0.32, 0.012, 0.12),
panelMaterial
);
rightWing.position.set(0.28, -0.03, 0);
rightWing.rotation.z = -0.16;
group.add(rightWing);
const boom = new THREE.Mesh(
new THREE.CylinderGeometry(0.01, 0.01, 0.28, 10),
trussMaterial
);
boom.position.y = -0.18;
group.add(boom);
const sensor = new THREE.Mesh(
new THREE.BoxGeometry(0.05, 0.05, 0.09),
trussMaterial
);
sensor.position.y = -0.33;
group.add(sensor);
}
return group;
}
const core = new THREE.Mesh(
new THREE.BoxGeometry(0.24, 0.14, 0.14),
bodyMaterial
@@ -612,7 +842,7 @@ async function buildSatellites(rows, satelliteLib) {
const isIss = row.norad_cat_id === 25544;
const mesh = isIss && issTemplate
? issTemplate.clone(true)
: createSatelliteFallback(color, isIss);
: createSatelliteFallback(color, isIss, row.norad_cat_id + index);
mesh.scale.setScalar(isIss && issTemplate ? 0.0035 : (isIss ? 1.35 : 1));
satelliteGroup.add(mesh);
@@ -621,7 +851,7 @@ async function buildSatellites(rows, satelliteLib) {
labelEl.textContent = row.object_name ?? String(row.norad_cat_id);
labelsLayer.appendChild(labelEl);
const orbitSamples = buildOrbitSamples(satrec, satelliteLib, new Date());
const orbitSamples = buildOrbitSamples(satrec, satelliteLib, simulatedTime);
let orbitLine = null;
if (orbitSamples.length > 1) {
orbitLine = new THREE.Line(
@@ -698,8 +928,8 @@ function updateSatellitePositions(items, satelliteLib, now) {
continue;
}
const labelPos = pos.clone().multiplyScalar(1.035);
const ndc = labelPos.project(camera);
const labelPos = item.mesh.getWorldPosition(new THREE.Vector3()).multiplyScalar(1.035);
const ndc = labelPos.clone().project(camera);
const isOffscreen = ndc.z < -1 || ndc.z > 1 || ndc.x < -1 || ndc.x > 1 || ndc.y < -1 || ndc.y > 1;
if (isOffscreen) {
@@ -708,7 +938,7 @@ function updateSatellitePositions(items, satelliteLib, now) {
}
const viewDir = camera.position.clone().normalize();
const satNormal = pos.clone().normalize();
const satNormal = labelPos.clone().normalize();
if (satNormal.dot(viewDir) <= 0) {
item.labelEl.classList.remove('is-visible');
continue;
@@ -722,6 +952,15 @@ function updateSatellitePositions(items, satelliteLib, now) {
}
}
function updateEarthOrientation(now) {
const gmst = getGreenwichMeanSiderealTime(now);
earthRotationGroup.rotation.y = gmst + THREE.MathUtils.degToRad(EARTH_LONGITUDE_OFFSET_DEG);
const subsolarPoint = getSunSubsolarPoint(now);
const sunDirectionLocal = lonLatAltToVector3(subsolarPoint.latDeg, subsolarPoint.lonDeg, 0).normalize().multiplyScalar(28);
sunLight.position.copy(earthRotationGroup.localToWorld(sunDirectionLocal));
}
let satellites = [];
let satelliteLib = null;
let nextOrbitRefreshAt = 0;
@@ -736,8 +975,16 @@ labelsToggle.addEventListener('change', () => {
}
});
for (const button of timeButtons) {
button.addEventListener('click', () => {
setTimeScale(Number(button.dataset.timeScale));
});
}
async function init() {
try {
simulatedTime = new Date();
updateTimeReadout();
satelliteLib = await loadSatelliteJs();
setStatus('Lade TLE-Daten und ISS-Modell…');
@@ -766,12 +1013,22 @@ async function init() {
function animate() {
requestAnimationFrame(animate);
const frameNowMs = performance.now();
const deltaMs = Math.min(frameNowMs - lastFrameTimeMs, 1000);
lastFrameTimeMs = frameNowMs;
if (timeScale !== 0) {
simulatedTime = new Date(simulatedTime.getTime() + deltaMs * timeScale);
}
updateTimeReadout();
updateEarthOrientation(simulatedTime);
if (satelliteLib && satellites.length > 0) {
const now = new Date();
updateSatellitePositions(satellites, satelliteLib, now);
updateSatellitePositions(satellites, satelliteLib, simulatedTime);
if (performance.now() >= nextOrbitRefreshAt) {
refreshOrbitLines(satellites, satelliteLib, now);
refreshOrbitLines(satellites, satelliteLib, simulatedTime);
nextOrbitRefreshAt = performance.now() + ORBIT_REFRESH_MS;
}
}