Am Boden reicht eine Zahl: ein Kurs von 0 bis 359 Grad. Im Raum gibt es keine ausgezeichnete Ebene, an der man diese Zahl aufhängen könnte — dort braucht es zwei getrennte Größen:
| Größe | Frage | Darstellung |
|---|---|---|
| Richtung | Wohin fliegt das Fahrzeug? | Einheitsvektor Vec3 im Bezugsrahmen |
| Lage (Attitude) | Wie ist es dabei gedreht? | Quaternion Quat |
Beides ist verschieden: Ein Raumfahrzeug kann rückwärts fliegen und dabei vorwärts blicken. Wer beides in einer Größe führt, verliert genau diese Unterscheidung.
→ std.astro (vorgesehen: Bahnmechanik, Lage, Guidance) · std.quat · std.matrix · Mathematik-Units · Aerospace & Safety
Ein Vektor ohne Bezugsrahmen ist bedeutungslos. Üblich sind:
| Rahmen | Ursprung | Achsen | Wofür |
|---|---|---|---|
| ECI (inertial) | Erdmittelpunkt | x zum Frühlingspunkt, z zum Nordpol | Bahnmechanik — dreht sich nicht mit der Erde |
| ECEF | Erdmittelpunkt | mitrotierend | Positionen über Grund, GNSS |
| LVLH | Fahrzeug | z nach unten (Nadir), x in Flugrichtung | Lageregelung im Orbit |
| Body | Fahrzeug | fest am Rumpf | Sensoren, Triebwerke |
Der Rahmen gehört in den Namen, nicht in einen Kommentar: pos_eci, nase_body. Eine Verwechslung zwischen ECI und ECEF ist kein Rundungsfehler, sondern ein Kurs, der sich pro Stunde um 15 Grad verdreht.
Der Compiler kann die Trennung heute nicht erzwingen — die Namensgebung trägt die Last allein.
*dim/utypeprüfen die Dimension (Länge gegen Zeit), nicht den Rahmen: ECI und ECEF sind beide Längen.
* Ein eigener Struct je Rahmen hilft ebenfalls nicht: Struct-Typen werden bei Zuweisung und Argumentübergabe gar nicht geprüft (#1886).var p: PosEci := vel;übersetzt anstandslos, obwohlVelEcietwas anderes bedeutet — und selbstnimmA(c)mit einem völlig anders geformten Struct läuft durch, mit dem Zeiger als Zahl gelesen. Nachgemessen mit 1.1.14A.
Solange das so ist: Rahmen in den Namen (pos_eci,nase_body), Umrechnungen in eine Funktion je Rahmenpaar bündeln, und diese Funktionen prüfen (Testfall mit bekanntem Ergebnis). Sobald #1886 behoben ist, wird der eigene Struct je Rahmen zur tragenden Lösung.
Die naheliegende Idee — drei Winkel für Roll, Nick und Gier, jeder als wraps 0..359 — bricht an einer Stelle zusammen, die im Raum regelmäßig vorkommt: bei ±90 Grad Nickwinkel fallen zwei Drehachsen zusammen (Gimbal Lock). Gemessen mit lyxc 1.1.14A gegen std.quat:
import std.quat;
import std.io;
fn zeige(name: pchar, q: Quat): void {
var g: f64 := 180.0 / 3.14159265358979;
Print(name); Print(" roll="); Print(FloatToStr(QuatRoll(q)*g, 2));
Print(" pitch="); Print(FloatToStr(QuatPitch(q)*g, 2));
Print(" yaw="); PrintLn(FloatToStr(QuatYaw(q)*g, 2));
}
fn main(): int64 {
var r: f64 := 3.14159265358979 / 180.0;
zeige("(30,45,60) ->", QuatFromEuler(30.0*r, 45.0*r, 60.0*r));
zeige("(30,89,0) ->", QuatFromEuler(30.0*r, 89.0*r, 0.0*r));
zeige("(30,90,0) ->", QuatFromEuler(30.0*r, 90.0*r, 0.0*r));
return 0;
}
(30,45,60) -> roll=30.00 pitch=45.00 yaw=60.00
(30,89,0) -> roll=30.00 pitch=89.00 yaw=0.00
(30,90,0) -> roll=31.14 pitch=90.00 yaw=2.05
Bis 89 Grad stimmt die Rückrechnung auf die Nachkommastelle. Bei genau 90 Grad kommen aus (30, 90, 0) plötzlich (31.14, 90, 2.05) heraus — nicht weil die Bibliothek rechnet, sondern weil die Zerlegung dort nicht mehr eindeutig ist: Roll und Gier drehen um dieselbe Achse, unendlich viele Winkelpaare beschreiben dieselbe Lage.
Für ein Flugzeug ist das eine Randlage. Für ein Raumfahrzeug ist „Nase senkrecht zur Bahnebene„ ein Betriebszustand.
Euler-Winkel deshalb nur an der Oberfläche — zum Anzeigen, im Protokoll, als Eingabe. Gerechnet wird mit Quaternionen.
import std.quat;
import std.matrix;
import std.math;
import std.io;
// Bezugsrahmen: ECI, x = Fruehlingspunkt, z = Nordpol, rechtshaendig.
fn richtung(q: Quat): Vec3 {
// Wohin zeigt die Nase? Koerpereigene x-Achse in den Bezugsrahmen gedreht.
var r: Quat := QuatRotateVec3(q, 1.0, 0.0, 0.0);
return Vec3New(r.x, r.y, r.z);
}
fn winkelZwischen(a: Vec3, b: Vec3): f64 {
var c: f64 := Vec3Dot(Vec3Normalize(a), Vec3Normalize(b));
if (c > 1.0) { c := 1.0; } // Rundung abfangen: acos(1.0000001) ist NaN
if (c < -1.0) { c := 0.0 - 1.0; }
return AcosF64(c) * 180.0 / 3.14159265358979;
}
fn zeige(name: pchar, v: Vec3): void {
Print(name); Print(" ("); Print(FloatToStr(v.x, 3));
Print(", "); Print(FloatToStr(v.y, 3));
Print(", "); Print(FloatToStr(v.z, 3)); PrintLn(")");
}
fn main(): int64 {
var lage: Quat := QuatIdentity();
zeige("Ist-Richtung :", richtung(lage));
var soll: Vec3 := Vec3Normalize(Vec3New(0.866, 0.5, 0.174));
zeige("Soll-Richtung:", soll);
Print("Winkelabstand: ");
PrintLn(FloatToStr(winkelZwischen(richtung(lage), soll), 2));
// Kuerzeste Drehung dorthin, in fuenf Schritten hinuebergedreht
var ziel: Quat := QuatFromTo(1.0, 0.0, 0.0, soll.x, soll.y, soll.z);
var i: int64 := 1;
while (i <= 5) {
var t: f64 := (i as f64) / 5.0;
var zwischen: Quat := QuatSlerp(lage, ziel, t);
Print(" Schritt "); Print(IntToStr(i)); Print(": Restwinkel ");
PrintLn(FloatToStr(winkelZwischen(richtung(zwischen), soll), 2));
i := i + 1;
}
return 0;
}
Ist-Richtung : (1.000, 0.000, 0.000)
Soll-Richtung: (0.853, 0.493, 0.171)
Winkelabstand: 31.44
Schritt 1: Restwinkel 25.15
Schritt 2: Restwinkel 18.86
Schritt 3: Restwinkel 12.58
Schritt 4: Restwinkel 6.29
Schritt 5: Restwinkel 0.00
Der Restwinkel fällt gleichmäßig — das ist die Eigenschaft, für die es QuatSlerp gibt: die Drehung läuft auf dem kürzesten Bogen und mit gleichbleibender Winkelgeschwindigkeit. QuatNlerp ist billiger, aber ungleichmäßig; für eine Lageregelung ist der Unterschied sichtbar.
| Aufgabe | Funktion |
|---|---|
| Lage aus Achse und Winkel | QuatFromAxisAngle(ax, ay, az, winkel) |
| Kürzeste Drehung von einer Richtung zur anderen | QuatFromTo(ax,ay,az, bx,by,bz) |
| Vektor mit der Lage drehen | QuatRotateVec3(q, x, y, z) |
| Zwei Lagen gleichmäßig überblenden | QuatSlerp(a, b, t) |
Lagen verketten (erst a, dann b) | QuatMul(b, a) — Reihenfolge beachten |
| Drehung rückgängig | QuatConj(q) bei Einheitsquaternion, sonst QuatInverse(q) |
| Nur zum Anzeigen | QuatRoll, QuatPitch, QuatYaw |
| Sind zwei Lagen dieselbe Drehung? | QuatSameRotation(a, b, eps) |
| Richtung, Abstand, Kreuzprodukt | Vec3Normalize, Vec3Dot, Vec3Cross aus std.matrix |
qund−qbeschreiben dieselbe Drehung. Ein Vergleich mitQuatApproxEqmeldet sie als verschieden; dafür gibt esQuatSameRotation. Das ist keine Eigenart der Bibliothek, sondern die doppelte Überdeckung der Drehgruppe — dieselbe Lage hat zwei Zahlendarstellungen.
Richtung und Lage einzeln herumzureichen ist fehleranfällig — beide gehören zusammen und zu einem Zeitpunkt. Ein Zustandsvektor bündelt das:
import std.quat;
import std.matrix;
import std.io;
// Bezugsrahmen im Namen, nicht im Kommentar (siehe Abschnitt 1)
type PosEci = struct { v: Vec3; }
type VelEci = struct { v: Vec3; }
type Zustand = struct {
pos: PosEci; // Meter, ECI
vel: VelEci; // Meter je Sekunde, ECI
lage: Quat; // Body -> ECI, Einheitsquaternion
t_ns: int64; // Gueltigkeitszeitpunkt, monoton
// Wohin zeigt die Nase? Koerpereigene x-Achse in den Rahmen gedreht.
fn Richtung(): Vec3 {
var r: Quat := QuatRotateVec3(self.lage, 1.0, 0.0, 0.0);
return Vec3New(r.x, r.y, r.z);
}
// Selbstprüfung: eine Lage, die keine Einheitslaenge hat, ist keine Drehung.
fn LageGueltig(): bool { return QuatIsUnit(self.lage, 0.000001); }
}
fn main(): int64 {
var z: Zustand;
z.pos.v := Vec3New(7000000.0, 0.0, 0.0);
z.vel.v := Vec3New(0.0, 7500.0, 0.0);
z.lage := QuatIdentity();
z.t_ns := 0;
var r: Vec3 := z.Richtung();
Print("Richtung x="); PrintLn(FloatToStr(r.x, 3));
Print("Lage gueltig="); PrintLn(IntToStr(BoolI(z.LageGueltig())));
return 0;
}
fn BoolI(b: bool): int64 { if (b) { return 1; } return 0; }
Richtung x=1.000
Lage gueltig=1
Vier Entwurfsentscheidungen dahinter:
LageGueltig() ist die eine Stelle, an der die Einheitslänge geprüft wird.dispose-Pflicht, Kopie beim Zuweisen — passend für einen Wert, der pro Regelzyklus entsteht.Struct-Typen werden inzwischen geprüft (#1886, nachgemessen mit lyxc 1.2.2B):var b: Q := a;mit zwei gleich aufgebauten, verschieden benannten Structs bricht mitsema error: Initialisierung: anderer benannter Typab, und dasselbe gilt beim Übergeben als Argument (Argument: anderer benannter Typ).
Für Bezugsrahmen hilft das trotzdem nur bedingt: werpos_eciundpos_ecefals denselben Typ führt (etwa beide alsAVec3), bekommt weiterhin keine Meldung. Die Trennung entsteht erst, wenn man je Rahmen einen eigenen benannten Typ anlegt — dann trägt sie der Compiler mit.
QuatNormalize nach jeder Verkettung, QuatIsUnit(q, eps) als Prüfung im Selbsttest.acos gegen Rundung absichern. Vec3Dot zweier Einheitsvektoren kann 1.0000000002 ergeben — AcosF64 liefert dann NaN, und NaN pflanzt sich stumm durch die ganze Regelung fort. Das Klemmen auf [−1, 1] im Beispiel oben ist Pflicht, keine Vorsicht.range 0..359 oder range -180..180; damit prüft der Compiler die Grenzen und der Laufzeit-Wächter bricht bei einem falschen Wert ab, statt ihn weiterzureichen.@redundant schützt einzelne Werte; vier Komponenten dreifach zu führen ergibt keine gültige Drehung, wenn eine Kopie kippt. Sinnvoller ist die Prüfung QuatIsUnit plus eine zweite, unabhängig gerechnete Lagequelle (Sternsensor gegen Kreisel) — Redundanz auf Systemebene, nicht auf Bitebene.Letzte Aktualisierung: 2026-09-05 (#1886, nachgemessen mit lyxc 1.2.2B) — Kasten gezogen: Struct-Typen werden geprüft; die Rahmenverwechslung bleibt nur ungeprüft, solange beide denselben Typ tragen.
Vorherige letzte Aktualisierung: 2026-08-30 — Abschnitt „Den Zustand als eigenen Datentyp“ ergänzt; dabei gemessen, dass Struct-Typen nicht geprüft werden (#1886) — der Rat „eigener Struct je Bezugsrahmen„ ist entsprechend richtiggestellt.
Zuvor 2026-08-30 — Seite neu angelegt; alle Beispiele gegen lyxc 1.1.14A gebaut und ausgeführt, die Gimbal-Lock-Zahlen gemessen (nicht gerechnet).