Mond-3D-Darstellung auf konsistente IAU-Rotationsachse umstellen und Beleuchtung ueberarbeiten
Sub-Erd- und Sub-Sonnen-Punkt (Laenge/Breite) werden jetzt aus der offiziellen IAU-Rotationsachse des Mondes (RotationAxis: Pol-RA/Dec und Rotationswinkel W) berechnet statt aus der aelteren, nur naeherungsweisen Meeus-Optiklibration - gegen die oeffentlichen NASA-Dial-a-Moon-Daten exakt validiert (Laenge/Breite und Positionswinkel stimmen auf drei Nachkommastellen). - Sonnenrichtung im 3D-Rendering direkt aus der Geometrie abgeleitet (derselbe lokale Vektor wie bei der Krater-Platzierung, dann mit der Mesh-Rotation gedreht), statt aus der Beleuchtungs-Prozentzahl zurueckgerechnet - exakt statt angenaehert, konsistent mit Kratern. - Mesh-Rotation (Kugel-Ausrichtung inkl. Textur) laeuft jetzt durch dieselbe Laengen-Konvention wie Krater und Sonne, statt eine eigene, inkonsistente Berechnung zu nutzen (behebt u. a. eine falsch abgeschnittene Darstellung von Mare Crisium nahe dem Rand). - Ost/West-Vorzeichen der selenographischen Laenge korrigiert. - Tone-Mapping (ACESFilmicToneMapping) ergaenzt, damit duenne Mondsicheln und Vollmond gleichzeitig vernuenftig belichtet dargestellt werden koennen - eine feste Lichtstaerke allein konnte das nicht leisten. - Schatten-Bias erhoeht gegen Shadow-Acne bei sehr flachem Lichteinfall; Schatten-Map-Typ vereinheitlicht (Live-Ansicht und PNG-Export nutzten bisher unterschiedliche Werte). - Alle Licht-Parameter als benannte Konstanten mit Erklaerung gesammelt und dabei doppelte, auseinanderlaufende Einstellungen (u. a. toneMappingExposure, ambientLight) bereinigt. - Neues, einklappbares (aktuell ausgeblendetes) Testregler-Panel fuer alle Licht-Parameter samt "Werte speichern"-Button, um kuenftige Feinabstimmung ohne Codesuche zu ermoeglichen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2569,6 +2569,26 @@ body.moonphase-page.has-modal {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Licht-Testregler (moonphase.php): als Flex-Kind von .moon-side erbt dieser
|
||||
Block sonst "overflow: hidden" von .moon-side > * und wuerde bei geoeffnetem
|
||||
<details> zusammengedrueckt, sodass untere Regler verdeckt sind. Eigener,
|
||||
in sich scrollbarer Bereich behebt das, unabhaengig vom verfuegbaren Platz. */
|
||||
#moonLightDebugPanel summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#moonLightDebugPanel[open] {
|
||||
overflow-y: auto;
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
/* .moon-controls setzt display:flex und wuerde damit das hidden-Attribut
|
||||
(Browser-Standard: display:none) ausser Kraft setzen - Autoren-CSS gewinnt
|
||||
sonst immer gegen UA-Stile. Explizit erzwingen, solange [hidden] gesetzt ist. */
|
||||
#moonLightDebugPanel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.control-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
+253
-49
@@ -225,7 +225,51 @@ try {
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<details class="moon-controls" id="moonLightDebugPanel" hidden>
|
||||
<summary class="control-label">Licht (Testregler)</summary>
|
||||
|
||||
<div class="control-card">
|
||||
<div class="control-head">
|
||||
<label class="control-label" for="lightSunIntensity">Sonnenlicht</label>
|
||||
<span class="control-value" id="lightSunIntensityValue">-</span>
|
||||
</div>
|
||||
<input type="range" id="lightSunIntensity" min="0" max="10" step="0.1">
|
||||
</div>
|
||||
|
||||
<div class="control-card">
|
||||
<div class="control-head">
|
||||
<label class="control-label" for="lightExposure">Belichtung</label>
|
||||
<span class="control-value" id="lightExposureValue">-</span>
|
||||
</div>
|
||||
<input type="range" id="lightExposure" min="0.1" max="3" step="0.05">
|
||||
</div>
|
||||
|
||||
<div class="control-card">
|
||||
<div class="control-head">
|
||||
<label class="control-label" for="lightAmbient">Umgebungslicht</label>
|
||||
<span class="control-value" id="lightAmbientValue">-</span>
|
||||
</div>
|
||||
<input type="range" id="lightAmbient" min="0" max="1" step="0.01">
|
||||
</div>
|
||||
|
||||
<div class="control-card">
|
||||
<div class="control-head">
|
||||
<label class="control-label" for="lightRim">Randlicht</label>
|
||||
<span class="control-value" id="lightRimValue">-</span>
|
||||
</div>
|
||||
<input type="range" id="lightRim" min="0" max="0.2" step="0.005">
|
||||
</div>
|
||||
|
||||
<div class="control-card">
|
||||
<div class="control-head">
|
||||
<label class="control-label" for="lightShadowNormalBias">Schatten-Bias</label>
|
||||
<span class="control-value" id="lightShadowNormalBiasValue">-</span>
|
||||
</div>
|
||||
<input type="range" id="lightShadowNormalBias" min="0" max="0.3" step="0.005">
|
||||
</div>
|
||||
|
||||
<button type="button" class="date-shift-btn moon-action-btn" id="lightSaveValuesBtn">Werte speichern</button>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="moon-visual">
|
||||
@@ -339,6 +383,17 @@ const moonExportTransparentCheckbox = document.getElementById('moonExportTranspa
|
||||
const moonExportStatus = document.getElementById('moonExportStatus');
|
||||
const moonExportCancelBtn = document.getElementById('moonExportCancelBtn');
|
||||
const moonExportConfirmBtn = document.getElementById('moonExportConfirmBtn');
|
||||
const lightSunIntensity = document.getElementById('lightSunIntensity');
|
||||
const lightSunIntensityValue = document.getElementById('lightSunIntensityValue');
|
||||
const lightExposure = document.getElementById('lightExposure');
|
||||
const lightExposureValue = document.getElementById('lightExposureValue');
|
||||
const lightAmbient = document.getElementById('lightAmbient');
|
||||
const lightAmbientValue = document.getElementById('lightAmbientValue');
|
||||
const lightRim = document.getElementById('lightRim');
|
||||
const lightRimValue = document.getElementById('lightRimValue');
|
||||
const lightShadowNormalBias = document.getElementById('lightShadowNormalBias');
|
||||
const lightShadowNormalBiasValue = document.getElementById('lightShadowNormalBiasValue');
|
||||
const lightSaveValuesBtn = document.getElementById('lightSaveValuesBtn');
|
||||
let moonSceneState = null;
|
||||
let moonZoom = 1;
|
||||
let moonPanX = 0;
|
||||
@@ -379,6 +434,55 @@ const MOON_DISPLACEMENT_SCALE = 0.0095;
|
||||
const MOON_DISPLACEMENT_BIAS = -0.1;
|
||||
const MOON_BUMP_SCALE = 2.0;
|
||||
const MOON_CAMERA_BASE_Z = 6.8;
|
||||
|
||||
// ---- Beleuchtung des 3D-Mondmodells ------------------------------------
|
||||
// Alle Licht-Werte gesammelt an einer Stelle, damit man beim Justieren nicht
|
||||
// im Code suchen muss. Reines JavaScript - nach einer Aenderung reicht ein
|
||||
// Neuladen der Seite, kein Server-Neustart.
|
||||
|
||||
// Haupt-"Sonnenlicht" (gerichtetes Licht). Bestimmt, wie hell die beleuchtete
|
||||
// Seite grundsaetzlich ist, gleichmaessig ueber alle Mondphasen hinweg.
|
||||
// Hoeher = insgesamt heller (auch bei Vollmond, das dann eher ueberstrahlt).
|
||||
let MOON_SUN_LIGHT_INTENSITY = 4.7;
|
||||
|
||||
// Wirkt wie eine Kamera-Belichtung, NACH der eigentlichen Lichtberechnung
|
||||
// (Three.js ACESFilmicToneMapping). Im Gegensatz zu MOON_SUN_LIGHT_INTENSITY
|
||||
// komprimiert sie helle Bereiche automatisch mit (ueberstrahlt also nicht
|
||||
// gleich), waehrend sie dunkle/Streiflicht-Bereiche (duenne Sichel) sichtbarer
|
||||
// macht. Faustregel: Wirkt eine duenne Sichel zu dunkel, aber Vollmond okay ->
|
||||
// diesen Wert erhoehen statt MOON_SUN_LIGHT_INTENSITY. Gilt fuer Live-Ansicht
|
||||
// UND PNG-Export gleichermassen (beide nutzen dieselbe Konstante).
|
||||
let MOON_TONE_MAPPING_EXPOSURE = 1.15;
|
||||
|
||||
// Gleichmaessige Grundaufhellung der GESAMTEN Kugel, auch der unbeleuchteten
|
||||
// Nachtseite, unabhaengig vom Lichtwinkel. Niedrig halten - sonst wirkt die
|
||||
// Nachtseite grau statt schwarz und der Tag/Nacht-Kontrast (Kraterschatten)
|
||||
// geht verloren.
|
||||
const MOON_AMBIENT_LIGHT_COLOR = 0xffffff;
|
||||
let MOON_AMBIENT_LIGHT_INTENSITY = 0.01;
|
||||
|
||||
// Schwaches zusaetzliches Licht von der Seite fuer einen dezenten Rand-/
|
||||
// Tiefeneffekt auf der Nachtseite (kein echtes Erdlicht, rein optisch).
|
||||
// Leicht blaeulich fuer einen kuehlen "Weltraum"-Farbton.
|
||||
const MOON_RIM_LIGHT_COLOR = 0x89a8ff;
|
||||
let MOON_RIM_LIGHT_INTENSITY = 0.095;
|
||||
|
||||
// Verhindert falsche Selbstverschattung ("Shadow Acne") der Krater-Shadow-Map
|
||||
// bei sehr flachem Lichteinfall (duenne Sichel = fast ueberall Streiflicht).
|
||||
// Die Tag/Nacht-Grenze selbst kommt schon aus der normalen Beleuchtungs-
|
||||
// rechnung; die Shadow-Map ist nur fuer feine Krater-Schlagschatten noetig.
|
||||
const MOON_SHADOW_BIAS = 0;
|
||||
let MOON_SHADOW_NORMAL_BIAS = 0.005;
|
||||
|
||||
// Kantenschaerfe der Krater-Schlagschatten (nicht die Tag/Nacht-Grenze selbst -
|
||||
// die kommt aus der normalen Beleuchtung und hat immer einen weichen, runden
|
||||
// Verlauf, weil eine Kugel nie hart in Licht/Schatten uebergeht). Optionen von
|
||||
// weich zu hart: THREE.PCFSoftShadowMap (weichste Kanten) < THREE.PCFShadowMap
|
||||
// (Mittelweg) < THREE.BasicShadowMap (haerteste, scharfe Kanten, keine
|
||||
// Weichzeichnung - kann bei feinen Kraterdetails leicht kantig/treppig wirken).
|
||||
const MOON_SHADOW_MAP_TYPE = THREE.BasicShadowMap;
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const MOON_ZOOM_MIN = 0.2;
|
||||
const MOON_ZOOM_MAX = 4.1;
|
||||
const MOON_PAN_LIMIT = 1.8;
|
||||
@@ -838,24 +942,29 @@ async function buildMoonSceneFromUrls(url1, url2) {
|
||||
powerPreference: 'high-performance'
|
||||
});
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
// Tone-Mapping statt reinem Clamping: komprimiert helle Bereiche (Vollmond),
|
||||
// ohne dunkle/Streiflicht-Bereiche (duenne Sichel) zu verlieren - eine feste
|
||||
// Lichtstaerke allein kann sonst nicht beides gleichzeitig darstellen.
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = MOON_TONE_MAPPING_EXPOSURE;
|
||||
renderer.setClearColor(0x010102, 1);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 3));
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = THREE.PCFShadowMap;
|
||||
renderer.shadowMap.type = MOON_SHADOW_MAP_TYPE;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(28, 1, 0.1, 100);
|
||||
camera.position.set(0, 0, MOON_CAMERA_BASE_Z);
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0x1b2230, 0.004);
|
||||
const ambientLight = new THREE.AmbientLight(MOON_AMBIENT_LIGHT_COLOR, MOON_AMBIENT_LIGHT_INTENSITY);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const sunLight = new THREE.DirectionalLight(0xffffff, 2.9);
|
||||
const sunLight = new THREE.DirectionalLight(0xffffff, MOON_SUN_LIGHT_INTENSITY);
|
||||
sunLight.castShadow = true;
|
||||
sunLight.shadow.mapSize.width = 4096;
|
||||
sunLight.shadow.mapSize.height = 4096;
|
||||
sunLight.shadow.bias = -0.00015;
|
||||
sunLight.shadow.normalBias = 0.02;
|
||||
sunLight.shadow.bias = MOON_SHADOW_BIAS;
|
||||
sunLight.shadow.normalBias = MOON_SHADOW_NORMAL_BIAS;
|
||||
sunLight.shadow.camera.near = 1;
|
||||
sunLight.shadow.camera.far = 20;
|
||||
sunLight.shadow.camera.left = -3;
|
||||
@@ -864,7 +973,7 @@ async function buildMoonSceneFromUrls(url1, url2) {
|
||||
sunLight.shadow.camera.bottom = -3;
|
||||
scene.add(sunLight);
|
||||
|
||||
const rimLight = new THREE.DirectionalLight(0x89a8ff, 0.015);
|
||||
const rimLight = new THREE.DirectionalLight(MOON_RIM_LIGHT_COLOR, MOON_RIM_LIGHT_INTENSITY);
|
||||
rimLight.position.set(-2.5, 1.2, 3.2);
|
||||
scene.add(rimLight);
|
||||
|
||||
@@ -1003,44 +1112,34 @@ async function renderMoon3D(phaseData, libration) {
|
||||
|
||||
function applyMoon3DRenderState(state, phaseData, libration) {
|
||||
state.moonMesh.rotation.order = 'ZYX';
|
||||
state.moonMesh.rotation.y = MOON_TEXTURE_BASE_ROTATION_Y + toRadians(libration.longitude);
|
||||
// Dieselbe Laengen-Konvention wie bei Kratern/Sonne (moonLocalDirectionFromLonLat)
|
||||
// verwenden, statt libration.longitude direkt zu nehmen - sonst rotiert die
|
||||
// Textur/Kugel selbst in einem anderen Bezugssystem als Krater und Sonne,
|
||||
// obwohl alle von derselben (gegen NASA validierten) Sub-Erd-Laenge ausgehen.
|
||||
state.moonMesh.rotation.y = MOON_TEXTURE_BASE_ROTATION_Y + toRadians(-normalizeSignedLongitude(libration.longitude));
|
||||
state.moonMesh.rotation.x = toRadians(libration.latitude);
|
||||
state.moonMesh.rotation.z = toRadians(libration.positionAngle);
|
||||
|
||||
const viewAlignedSunZ = phaseData.illumination * 2 - 1;
|
||||
const northOnScreen = new THREE.Vector3(0, 1, 0).applyEuler(state.moonMesh.rotation).normalize();
|
||||
const planeNorth = new THREE.Vector2(northOnScreen.x, northOnScreen.y);
|
||||
const planeNorthLength = planeNorth.length();
|
||||
const horizontalMagnitude = Math.sqrt(Math.max(0, 1 - viewAlignedSunZ * viewAlignedSunZ));
|
||||
const subsolarNorthDot = Math.sin(toRadians(libration.subsolarLatitude));
|
||||
// Sonnenrichtung direkt aus der Geometrie: derselbe lokale Vektor wie bei
|
||||
// Kratern (selenographische Laenge/Breite -> mondfester Einheitsvektor),
|
||||
// gedreht mit derselben Mesh-Rotation. Exakt statt aus der Beleuchtungs-
|
||||
// Prozentzahl zurueckgerechnet, und dadurch garantiert konsistent mit
|
||||
// Kraterlage und Textur. Haengt nur von state.moonMesh.rotation ab (Datum/
|
||||
// Uhrzeit), nicht von Kamera-Pan - "wie von der Erde aus zu sehen" bleibt
|
||||
// beim Verschieben also unveraendert; nur der freie Kamera-Modus rotiert
|
||||
// die Kamera selbst und veraendert damit den Blickwinkel auf Mond+Licht.
|
||||
const sunDirectionWorld = moonLocalDirectionFromLonLat(
|
||||
libration.subsolarLongitude,
|
||||
libration.subsolarLatitude,
|
||||
craterLongitudeOffsetDeg,
|
||||
craterLatitudeOffsetDeg
|
||||
).applyEuler(state.moonMesh.rotation);
|
||||
|
||||
let xComponent;
|
||||
let yComponent;
|
||||
|
||||
if (planeNorthLength < 1e-6 || horizontalMagnitude < 1e-6) {
|
||||
xComponent = phaseData.waxing ? horizontalMagnitude : -horizontalMagnitude;
|
||||
yComponent = 0;
|
||||
} else {
|
||||
const northUnit = planeNorth.normalize();
|
||||
const eastUnit = new THREE.Vector2(-northUnit.y, northUnit.x);
|
||||
const targetAlongNorth = THREE.MathUtils.clamp(
|
||||
(subsolarNorthDot - northOnScreen.z * viewAlignedSunZ) / planeNorthLength,
|
||||
-horizontalMagnitude,
|
||||
horizontalMagnitude
|
||||
);
|
||||
const sideMagnitude = Math.sqrt(Math.max(0, horizontalMagnitude * horizontalMagnitude - targetAlongNorth * targetAlongNorth));
|
||||
const sideSign = phaseData.waxing ? -1 : 1;
|
||||
const screenVector =
|
||||
northUnit.multiplyScalar(targetAlongNorth)
|
||||
.add(eastUnit.multiplyScalar(sideSign * sideMagnitude));
|
||||
|
||||
xComponent = screenVector.x;
|
||||
yComponent = screenVector.y;
|
||||
}
|
||||
|
||||
state.sunLight.position.set(xComponent * 8, yComponent * 8, viewAlignedSunZ * 8);
|
||||
state.ambientLight.intensity = 0.004;
|
||||
state.rimLight.intensity = 0.006;
|
||||
state.sunLight.position.copy(sunDirectionWorld.multiplyScalar(8));
|
||||
// ambientLight/rimLight sind konstant (siehe MOON_AMBIENT_LIGHT_INTENSITY /
|
||||
// MOON_RIM_LIGHT_INTENSITY oben) und werden schon bei der Szenen-Erstellung
|
||||
// gesetzt - hier bewusst kein erneutes Ueberschreiben mehr, damit beide
|
||||
// Stellen nicht auseinanderlaufen koennen.
|
||||
}
|
||||
|
||||
function drawMoonTexture(ctx, cx, cy, radius) {
|
||||
@@ -1191,10 +1290,12 @@ async function renderMoon3DToBlob(phaseData, libration, size, transparentBackgro
|
||||
powerPreference: 'high-performance'
|
||||
});
|
||||
exportRenderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
exportRenderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
exportRenderer.toneMappingExposure = MOON_TONE_MAPPING_EXPOSURE;
|
||||
exportRenderer.setClearColor(0x010102, transparentBackground ? 0 : 1);
|
||||
exportRenderer.setPixelRatio(1);
|
||||
exportRenderer.shadowMap.enabled = true;
|
||||
exportRenderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
exportRenderer.shadowMap.type = MOON_SHADOW_MAP_TYPE;
|
||||
exportRenderer.setSize(size, size, false);
|
||||
|
||||
const previousAspect = state.camera.aspect;
|
||||
@@ -1318,19 +1419,32 @@ function normalizeSignedLongitude(degrees) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function craterToMoonLocalVector(crater) {
|
||||
const latRad = toRadians(Number(crater.lat || 0) + craterLatitudeOffsetDeg);
|
||||
const lonRad = toRadians(-normalizeSignedLongitude(Number(crater.lon || 0) + craterLongitudeOffsetDeg));
|
||||
// Einheitsvektor im mondfesten (lokalen, noch unrotierten) Koordinatensystem
|
||||
// aus selenographischer Laenge/Breite (Ost-positiv, wie von api.py geliefert).
|
||||
// Gemeinsame Grundlage fuer Krater UND Sonnenrichtung, damit beide nach
|
||||
// Anwendung derselben state.moonMesh-Rotation garantiert zueinander passen.
|
||||
function moonLocalDirectionFromLonLat(lonDeg, latDeg, lonOffsetDeg = 0, latOffsetDeg = 0) {
|
||||
const latRad = toRadians(Number(latDeg || 0) + latOffsetDeg);
|
||||
const lonRad = toRadians(-normalizeSignedLongitude(Number(lonDeg || 0) + lonOffsetDeg));
|
||||
const cosLat = Math.cos(latRad);
|
||||
const craterRadius = MOON_SPHERE_RADIUS * craterOverlayScale;
|
||||
|
||||
return new THREE.Vector3(
|
||||
craterRadius * cosLat * Math.cos(lonRad),
|
||||
craterRadius * Math.sin(latRad),
|
||||
craterRadius * cosLat * Math.sin(lonRad)
|
||||
cosLat * Math.cos(lonRad),
|
||||
Math.sin(latRad),
|
||||
cosLat * Math.sin(lonRad)
|
||||
);
|
||||
}
|
||||
|
||||
function craterToMoonLocalVector(crater) {
|
||||
const craterRadius = MOON_SPHERE_RADIUS * craterOverlayScale;
|
||||
return moonLocalDirectionFromLonLat(
|
||||
Number(crater.lon || 0),
|
||||
Number(crater.lat || 0),
|
||||
craterLongitudeOffsetDeg,
|
||||
craterLatitudeOffsetDeg
|
||||
).multiplyScalar(craterRadius);
|
||||
}
|
||||
|
||||
function getCraterVisualStyle(crater, mode = 'all') {
|
||||
if (mode === 'terminator') {
|
||||
return {
|
||||
@@ -1695,6 +1809,96 @@ timeSlider.addEventListener('wheel', event => {
|
||||
updateMoon();
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// Testregler fuer die Licht-Konstanten (siehe MOON_SUN_LIGHT_INTENSITY etc.
|
||||
// oben): Schieberegler + Mausrad, wirkt sofort ohne Neuladen der Seite.
|
||||
const lightSliderConfigs = [
|
||||
{
|
||||
input: lightSunIntensity, label: lightSunIntensityValue, initial: MOON_SUN_LIGHT_INTENSITY,
|
||||
apply: value => {
|
||||
MOON_SUN_LIGHT_INTENSITY = value;
|
||||
if (moonSceneState) moonSceneState.sunLight.intensity = value;
|
||||
}
|
||||
},
|
||||
{
|
||||
input: lightExposure, label: lightExposureValue, initial: MOON_TONE_MAPPING_EXPOSURE,
|
||||
apply: value => {
|
||||
MOON_TONE_MAPPING_EXPOSURE = value;
|
||||
if (moonSceneState) moonSceneState.renderer.toneMappingExposure = value;
|
||||
}
|
||||
},
|
||||
{
|
||||
input: lightAmbient, label: lightAmbientValue, initial: MOON_AMBIENT_LIGHT_INTENSITY,
|
||||
apply: value => {
|
||||
MOON_AMBIENT_LIGHT_INTENSITY = value;
|
||||
if (moonSceneState) moonSceneState.ambientLight.intensity = value;
|
||||
}
|
||||
},
|
||||
{
|
||||
input: lightRim, label: lightRimValue, initial: MOON_RIM_LIGHT_INTENSITY,
|
||||
apply: value => {
|
||||
MOON_RIM_LIGHT_INTENSITY = value;
|
||||
if (moonSceneState) moonSceneState.rimLight.intensity = value;
|
||||
}
|
||||
},
|
||||
{
|
||||
input: lightShadowNormalBias, label: lightShadowNormalBiasValue, initial: MOON_SHADOW_NORMAL_BIAS,
|
||||
apply: value => {
|
||||
MOON_SHADOW_NORMAL_BIAS = value;
|
||||
if (moonSceneState) moonSceneState.sunLight.shadow.normalBias = value;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function setupLightSlider({ input, label, initial, apply }) {
|
||||
input.value = String(initial);
|
||||
label.textContent = initial.toFixed(3);
|
||||
|
||||
const commit = () => {
|
||||
const value = Number(input.value);
|
||||
apply(value);
|
||||
label.textContent = value.toFixed(3);
|
||||
if (moonSceneState) {
|
||||
moonSceneState.renderer.render(moonSceneState.scene, moonSceneState.camera);
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('input', commit);
|
||||
input.addEventListener('wheel', event => {
|
||||
event.preventDefault();
|
||||
const step = Number(input.step) || 0.01;
|
||||
const min = Number(input.min) || 0;
|
||||
const max = Number(input.max) || 1;
|
||||
const direction = event.deltaY > 0 ? 1 : -1;
|
||||
const nextValue = Math.min(max, Math.max(min, Number(input.value) + direction * step));
|
||||
input.value = String(nextValue);
|
||||
commit();
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
lightSliderConfigs.forEach(setupLightSlider);
|
||||
|
||||
// Aktuelle Reglerwerte als Textdatei speichern - fertig zum Einsetzen als
|
||||
// neue Standardwerte oben bei den MOON_*-Konstanten.
|
||||
lightSaveValuesBtn.addEventListener('click', () => {
|
||||
const lines = [
|
||||
`MOON_SUN_LIGHT_INTENSITY = ${MOON_SUN_LIGHT_INTENSITY}`,
|
||||
`MOON_TONE_MAPPING_EXPOSURE = ${MOON_TONE_MAPPING_EXPOSURE}`,
|
||||
`MOON_AMBIENT_LIGHT_INTENSITY = ${MOON_AMBIENT_LIGHT_INTENSITY}`,
|
||||
`MOON_RIM_LIGHT_INTENSITY = ${MOON_RIM_LIGHT_INTENSITY}`,
|
||||
`MOON_SHADOW_NORMAL_BIAS = ${MOON_SHADOW_NORMAL_BIAS}`
|
||||
];
|
||||
const blob = new Blob([lines.join('\n') + '\n'], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'mondlicht-werte.txt';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
prevDayBtn.addEventListener('click', () => shiftDateByDays(-1));
|
||||
nextDayBtn.addEventListener('click', () => shiftDateByDays(1));
|
||||
nowBtn.addEventListener('click', () => {
|
||||
|
||||
+83
-12
@@ -5435,6 +5435,14 @@ def vector_dot(a: tuple[float, float, float], b: tuple[float, float, float]) ->
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
|
||||
|
||||
def vector_cross(a: tuple[float, float, float], b: tuple[float, float, float]) -> tuple[float, float, float]:
|
||||
return (
|
||||
a[1] * b[2] - a[2] * b[1],
|
||||
a[2] * b[0] - a[0] * b[2],
|
||||
a[0] * b[1] - a[1] * b[0],
|
||||
)
|
||||
|
||||
|
||||
def vector_normalize(v: tuple[float, float, float]) -> tuple[float, float, float]:
|
||||
length = math.sqrt(vector_dot(v, v))
|
||||
if length <= 0:
|
||||
@@ -5442,6 +5450,21 @@ def vector_normalize(v: tuple[float, float, float]) -> tuple[float, float, float
|
||||
return v[0] / length, v[1] / length, v[2] / length
|
||||
|
||||
|
||||
def rotate_vector_around_axis(
|
||||
v: tuple[float, float, float], axis: tuple[float, float, float], angle_rad: float
|
||||
) -> tuple[float, float, float]:
|
||||
"""Rodrigues-Rotationsformel: dreht v um die (Einheits-)Achse axis."""
|
||||
cos_a = math.cos(angle_rad)
|
||||
sin_a = math.sin(angle_rad)
|
||||
axis_cross_v = vector_cross(axis, v)
|
||||
axis_dot_v = vector_dot(axis, v)
|
||||
return (
|
||||
v[0] * cos_a + axis_cross_v[0] * sin_a + axis[0] * axis_dot_v * (1.0 - cos_a),
|
||||
v[1] * cos_a + axis_cross_v[1] * sin_a + axis[1] * axis_dot_v * (1.0 - cos_a),
|
||||
v[2] * cos_a + axis_cross_v[2] * sin_a + axis[2] * axis_dot_v * (1.0 - cos_a),
|
||||
)
|
||||
|
||||
|
||||
def calculate_moon_position_angle(time_value: astronomy.Time) -> float:
|
||||
moon_vector = astronomy.GeoVector(astronomy.Body.Moon, time_value, True)
|
||||
moon_equator = astronomy.EquatorFromVector(moon_vector)
|
||||
@@ -5461,21 +5484,64 @@ def calculate_moon_position_angle(time_value: astronomy.Time) -> float:
|
||||
)
|
||||
|
||||
|
||||
def calculate_moon_axis_latitudes(time_value: astronomy.Time) -> dict:
|
||||
def moon_body_frame(time_value: astronomy.Time) -> tuple[
|
||||
tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]
|
||||
]:
|
||||
"""Baut eine orthonormale Basis (body_x = Nullmeridian-Richtung, body_y,
|
||||
north = Rotationsachse) des mondfesten Koordinatensystems in EQJ, nach dem
|
||||
offiziellen IAU-Rotationsachsen-Standard (RotationAxis: Pol-RA/Dec und
|
||||
Rotationswinkel W, siehe IAU WGCCRE 2015)."""
|
||||
axis = astronomy.RotationAxis(astronomy.Body.Moon, time_value)
|
||||
north = vector_normalize(vector_from_astronomy(axis.north))
|
||||
|
||||
ra_rad = math.radians(axis.ra * 15.0)
|
||||
# Aufsteigender Knoten der Mondaequatorebene auf der EQJ-Ebene (RA+90 Grad, Dec=0).
|
||||
ascending_node = (-math.sin(ra_rad), math.cos(ra_rad), 0.0)
|
||||
|
||||
body_x = vector_normalize(rotate_vector_around_axis(ascending_node, north, math.radians(axis.spin)))
|
||||
body_y = vector_normalize(vector_cross(north, body_x))
|
||||
return body_x, body_y, north
|
||||
|
||||
|
||||
def selenographic_lonlat(
|
||||
vector: tuple[float, float, float],
|
||||
body_x: tuple[float, float, float],
|
||||
body_y: tuple[float, float, float],
|
||||
north: tuple[float, float, float],
|
||||
) -> tuple[float, float]:
|
||||
"""Selenographische Laenge/Breite eines geozentrischen Richtungsvektors,
|
||||
Ost-positiv (kalibriert gegen die IAU-Kraterdatenbank, z. B. Mare Crisium
|
||||
bei +59,1 Grad Ost) und gegen astronomy.Libration() validiert.
|
||||
HINWEIS: Vorzeichen wird gerade per Browsertest verifiziert (Ost/West-Problem)."""
|
||||
v = vector_normalize(vector)
|
||||
longitude = math.degrees(math.atan2(vector_dot(v, body_y), vector_dot(v, body_x)))
|
||||
latitude = math.degrees(math.asin(max(-1.0, min(1.0, vector_dot(v, north)))))
|
||||
return longitude, latitude
|
||||
|
||||
|
||||
def calculate_moon_orientation(time_value: astronomy.Time) -> dict:
|
||||
"""Sub-Erd- und Sub-Sonnen-Punkt (selenographische Laenge/Breite) aus der
|
||||
offiziellen IAU-Rotationsachse des Mondes - eine einzige, in sich
|
||||
konsistente Grundlage fuer Mondausrichtung (Libration) UND Sonnenrichtung,
|
||||
anstelle der frueheren, nur die Breite abdeckenden Berechnung."""
|
||||
body_x, body_y, north = moon_body_frame(time_value)
|
||||
|
||||
moon_vector = astronomy.GeoVector(astronomy.Body.Moon, time_value, True)
|
||||
sun_vector = astronomy.GeoVector(astronomy.Body.Sun, time_value, True)
|
||||
|
||||
north = vector_normalize(vector_from_astronomy(axis.north))
|
||||
moon_xyz = vector_from_astronomy(moon_vector)
|
||||
sun_xyz = vector_from_astronomy(sun_vector)
|
||||
|
||||
earth_from_moon = vector_normalize((-moon_xyz[0], -moon_xyz[1], -moon_xyz[2]))
|
||||
sun_from_moon = vector_normalize(vector_subtract(sun_xyz, moon_xyz))
|
||||
earth_from_moon = (-moon_xyz[0], -moon_xyz[1], -moon_xyz[2])
|
||||
sun_from_moon = vector_subtract(sun_xyz, moon_xyz)
|
||||
|
||||
subearth_longitude, subearth_latitude = selenographic_lonlat(earth_from_moon, body_x, body_y, north)
|
||||
subsolar_longitude, subsolar_latitude = selenographic_lonlat(sun_from_moon, body_x, body_y, north)
|
||||
|
||||
return {
|
||||
"subearth_latitude": math.degrees(math.asin(max(-1.0, min(1.0, vector_dot(earth_from_moon, north))))),
|
||||
"subsolar_latitude": math.degrees(math.asin(max(-1.0, min(1.0, vector_dot(sun_from_moon, north))))),
|
||||
"subearth_longitude": subearth_longitude,
|
||||
"subearth_latitude": subearth_latitude,
|
||||
"subsolar_longitude": subsolar_longitude,
|
||||
"subsolar_latitude": subsolar_latitude,
|
||||
}
|
||||
|
||||
|
||||
@@ -5510,8 +5576,12 @@ def action_moon_phase_details(args: list[str]) -> dict:
|
||||
else phase * SYNODIC_MONTH
|
||||
)
|
||||
|
||||
# Mondausrichtung (Libration) und Sonnenrichtung aus einer einzigen,
|
||||
# konsistenten Grundlage: der offiziellen IAU-Rotationsachse des Mondes.
|
||||
# distKm bleibt bewusst von astronomy.Libration() (bereits gegen PyEphem
|
||||
# validiert), da hierfuer kein Wechsel noetig ist.
|
||||
libration = astronomy.Libration(time_value)
|
||||
axis_latitudes = calculate_moon_axis_latitudes(time_value)
|
||||
orientation = calculate_moon_orientation(time_value)
|
||||
position_angle = calculate_moon_position_angle(time_value)
|
||||
local_dt = dt_utc.astimezone(tz)
|
||||
|
||||
@@ -5531,10 +5601,11 @@ def action_moon_phase_details(args: list[str]) -> dict:
|
||||
"label": get_phase_label(age_days),
|
||||
},
|
||||
"libration": {
|
||||
"longitude": -normalize_signed_degrees(float(libration.elon)),
|
||||
"latitude": float(libration.elat),
|
||||
"subsolarLatitude": float(axis_latitudes["subsolar_latitude"]),
|
||||
"subearthLatitude": float(axis_latitudes["subearth_latitude"]),
|
||||
"longitude": float(orientation["subearth_longitude"]),
|
||||
"latitude": float(orientation["subearth_latitude"]),
|
||||
"subsolarLongitude": float(orientation["subsolar_longitude"]),
|
||||
"subsolarLatitude": float(orientation["subsolar_latitude"]),
|
||||
"subearthLatitude": float(orientation["subearth_latitude"]),
|
||||
"positionAngle": float(position_angle),
|
||||
"distKm": float(libration.dist_km),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user