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:
2026-09-13 13:08:54 +02:00
co-authored by Claude Sonnet 5
parent 2396cac6db
commit efb2d8ed04
3 changed files with 356 additions and 61 deletions
+83 -12
View File
@@ -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),
},