Geocron 3D verbessert
This commit is contained in:
+297
-40
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user