proejct commit
This commit is contained in:
+475
@@ -0,0 +1,475 @@
|
||||
# Physik der Sandbox
|
||||
|
||||
Dieses Dokument erklärt, *was* die Physik-Engine berechnet und *warum* sie es so tut. Alle
|
||||
Formeln finden sich im Code wieder, meist mit derselben Bezeichnung. Die Zahlenwerte stehen
|
||||
in [`src/sandbox/config.py`](../src/sandbox/config.py) (`PhysicsConfig`).
|
||||
|
||||
Die Physik-Schicht (`src/sandbox/physics/`) verwendet ausschließlich das Standardmodul
|
||||
`math` sowie `typing`/`dataclasses` für Typangaben. Ein Architektur-Test stellt sicher,
|
||||
dass dort nie pygame importiert wird.
|
||||
|
||||
## Inhalt
|
||||
|
||||
1. [Einheiten und Koordinaten](#1-einheiten-und-koordinaten)
|
||||
2. [Körper: Masse und Trägheit](#2-körper-masse-und-trägheit)
|
||||
3. [Integration mit festem Zeitschritt](#3-integration-mit-festem-zeitschritt)
|
||||
4. [Ablauf eines Simulationsschritts](#4-ablauf-eines-simulationsschritts)
|
||||
5. [Kollisionserkennung](#5-kollisionserkennung)
|
||||
6. [Impulsbasierte Kollisionsauflösung](#6-impulsbasierte-kollisionsauflösung)
|
||||
7. [Restitution (Elastizität)](#7-restitution-elastizität)
|
||||
8. [Reibung und Rollen](#8-reibung-und-rollen)
|
||||
9. [Rollwiderstand](#9-rollwiderstand)
|
||||
10. [Positionskorrektur](#10-positionskorrektur)
|
||||
11. [Ruhe ohne Zittern: Schlafmodus](#11-ruhe-ohne-zittern-schlafmodus)
|
||||
12. [Tunneln verhindern](#12-tunneln-verhindern)
|
||||
13. [Ziehen und Werfen](#13-ziehen-und-werfen)
|
||||
14. [Parameterübersicht](#14-parameterübersicht)
|
||||
|
||||
---
|
||||
|
||||
## 1. Einheiten und Koordinaten
|
||||
|
||||
| Größe | Einheit |
|
||||
|---|---|
|
||||
| Länge | Pixel (px) |
|
||||
| Zeit | Sekunde (s) |
|
||||
| Geschwindigkeit | px/s |
|
||||
| Masse | Fläche × Dichte (px² · Dichte; nur Verhältnisse zählen) |
|
||||
|
||||
Der Ursprung liegt oben links in der Welt, **x zeigt nach rechts, y nach unten** – wie auf
|
||||
dem Bildschirm. Die Schwerkraft ist deshalb `g = (0, +980) px/s²`; bei einem Maßstab von
|
||||
100 px pro Meter entspricht das 9,8 m/s².
|
||||
|
||||
Weil y nach unten zeigt, bedeutet ein positiver Drehwinkel eine Drehung **im
|
||||
Uhrzeigersinn** auf dem Bildschirm. Die Formeln sind davon unabhängig, solange man
|
||||
konsequent `a × b = a.x·b.y − a.y·b.x` verwendet.
|
||||
|
||||
## 2. Körper: Masse und Trägheit
|
||||
|
||||
Jeder Körper (`Body`) hat eine Form, eine Position (Schwerpunkt), eine Geschwindigkeit und
|
||||
bei Kreisen zusätzlich Winkel und Winkelgeschwindigkeit.
|
||||
|
||||
**Masse = Fläche × Dichte.** Damit sind größere Körper bei gleicher Dichte schwerer:
|
||||
|
||||
| Form | Fläche | Trägheitsmoment |
|
||||
|---|---|---|
|
||||
| Kreis (Radius r) | `π·r²` | `I = ½·m·r²` (homogene Scheibe) |
|
||||
| Rechteck (halbe Kanten hw, hh) | `4·hw·hh` | unendlich (dreht nie) |
|
||||
|
||||
Herleitung des Scheiben-Trägheitsmoments: Die Scheibe zerfällt in dünne Ringe mit Radius s
|
||||
und Masse `dm = m/(π·r²) · 2π·s·ds`. Jeder Ring trägt `s²·dm` bei:
|
||||
|
||||
```text
|
||||
I = (2m / r²) · ∫₀ʳ s³ ds = (2m / r²) · r⁴/4 = ½·m·r²
|
||||
```
|
||||
|
||||
Rechtecke sind laut Aufgabenstellung **achsenparallel**. Ein unendliches Trägheitsmoment
|
||||
(also `1/I = 0`) garantiert, dass kein Drehimpuls sie jemals drehen kann.
|
||||
|
||||
**Inverse Massen.** Gespeichert werden zusätzlich `1/m` und `1/I`. Wände haben `1/m = 0`
|
||||
(unendliche Masse). So passen bewegliche und unbewegliche Körper ohne Sonderfälle in
|
||||
dieselben Formeln: Ein Impuls P ändert die Geschwindigkeit um `P/m` – bei einer Wand um 0.
|
||||
|
||||
## 3. Integration mit festem Zeitschritt
|
||||
|
||||
### Semi-impliziter Euler
|
||||
|
||||
Pro Schritt `dt = 1/120 s`:
|
||||
|
||||
```text
|
||||
v ← v + g·dt (zuerst die Geschwindigkeit …)
|
||||
x ← x + v·dt (… dann die Position mit der NEUEN Geschwindigkeit)
|
||||
θ ← θ + ω·dt
|
||||
```
|
||||
|
||||
Der explizite Euler (Position mit der *alten* Geschwindigkeit) gewinnt bei Schwingungen und
|
||||
Umlaufbahnen systematisch Energie und wird instabil. Der semi-implizite (symplektische)
|
||||
Euler kostet genauso wenig, bleibt aber energetisch stabil.
|
||||
|
||||
Für den freien Fall ergibt sich nach n Schritten exakt `v = n·g·dt` und
|
||||
`s = g·dt²·n(n+1)/2`. Das weicht nur um den Faktor `(1 + 1/n)` vom analytischen
|
||||
`s = ½·g·t²` ab; nach 0,5 s sind das 1,7 %. Genau diese Beziehung prüft ein Test.
|
||||
|
||||
### Fester Zeitschritt, entkoppelt von der Bildrate
|
||||
|
||||
Die Stabilität hängt von `dt` ab. Mit der schwankenden Frame-Dauer als `dt` würde sich die
|
||||
Simulation auf jedem Rechner anders verhalten. Ein Ruckler von 200 ms ließe Körper außerdem
|
||||
durch andere hindurchspringen. Deshalb sammelt ein Akkumulator (`FixedTimestep`) die real
|
||||
vergangene Zeit und gibt sie in festen Portionen weiter:
|
||||
|
||||
```text
|
||||
akkumulator += frame_zeit
|
||||
while akkumulator ≥ dt: world.step(dt); akkumulator −= dt
|
||||
alpha = akkumulator / dt (Rest für die Interpolation)
|
||||
```
|
||||
|
||||
- **Obergrenze** `max_steps_per_frame`: Schafft der Rechner die Physik nicht in Echtzeit,
|
||||
wird der Rückstand verworfen, statt ihn endlos mitzuschleppen ("Spiral of Death").
|
||||
- **Interpolation:** Gezeichnet wird `lerp(vorige_position, position, alpha)`. Die Physik
|
||||
läuft mit 120 Hz, der Bildschirm z. B. mit 60 Hz – ohne Interpolation ruckelt die
|
||||
Bewegung sichtbar.
|
||||
|
||||
## 4. Ablauf eines Simulationsschritts
|
||||
|
||||
`World.step(dt)` führt nacheinander aus:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Zustand merken<br/>für Interpolation] --> B[Geschwindigkeiten integrieren<br/>v += g·dt]
|
||||
B --> C[Broadphase<br/>Sweep-and-Prune]
|
||||
C --> D[Narrowphase<br/>Kontakte Körper–Körper]
|
||||
D --> E[Berührte Schläfer wecken]
|
||||
E --> F[Wandkontakte]
|
||||
F --> G[Impulse lösen<br/>Normale · Reibung · Rollwiderstand]
|
||||
G --> H[Geschwindigkeit begrenzen]
|
||||
H --> I[Positionen integrieren<br/>x += v·dt]
|
||||
I --> J[Positionskorrektur]
|
||||
J --> K[Notbremse an den Wänden]
|
||||
K --> L[Schlafmodus aktualisieren]
|
||||
```
|
||||
|
||||
Vor jedem Schritt setzt die Anwendung die Geschwindigkeit eines gezogenen Körpers
|
||||
(siehe [Abschnitt 13](#13-ziehen-und-werfen)).
|
||||
|
||||
## 5. Kollisionserkennung
|
||||
|
||||
Jede Prüfung liefert einen **Kontakt** mit
|
||||
|
||||
- **Normale n** – Einheitsvektor von Körper A zu Körper B,
|
||||
- **Eindringtiefe** – Überlappung entlang n (≥ 0),
|
||||
- **Kontaktpunkt** – Angriffspunkt der Kräfte.
|
||||
|
||||
### Broadphase: Sweep-and-Prune
|
||||
|
||||
Alle Paare genau zu prüfen kostet n·(n−1)/2 Tests – bei 150 Körpern über 11 000 pro
|
||||
Schritt. Deshalb werden die umschließenden Boxen nach ihrer linken Kante sortiert. Für jede
|
||||
Box müssen nur die Nachfolger geprüft werden, die links von ihrer rechten Kante beginnen.
|
||||
Der Aufwand sinkt auf etwa O(n·log n + k) für k benachbarte Paare. Paare aus zwei
|
||||
schlafenden Körpern werden übersprungen.
|
||||
|
||||
### Kreis–Kreis
|
||||
|
||||
```text
|
||||
d = |p_B − p_A|, Kontakt, wenn d ≤ r_A + r_B
|
||||
n = (p_B − p_A) / d, Tiefe = r_A + r_B − d
|
||||
```
|
||||
|
||||
Der Kontaktpunkt liegt in der Mitte des Überlappungsbereichs. Liegen die Mittelpunkte exakt
|
||||
aufeinander, ist die Richtung unbestimmt. Dann wird die feste Ersatznormale (0, −1)
|
||||
verwendet, statt durch 0 zu teilen.
|
||||
|
||||
### Kreis–Rechteck
|
||||
|
||||
Den Kreismittelpunkt (relativ zum Rechteck) komponentenweise auf das Rechteck klemmen. Das
|
||||
liefert den nächstgelegenen Punkt q des Rechtecks:
|
||||
|
||||
```text
|
||||
q = (clamp(dx, −hw, hw), clamp(dy, −hh, hh))
|
||||
Kontakt, wenn |Mittelpunkt − q| ≤ r
|
||||
```
|
||||
|
||||
Sonderfall: Liegt der Kreismittelpunkt *im* Rechteck, ist q der Mittelpunkt selbst und
|
||||
liefert keine Richtung. Dann wird der Kreis über die nächstgelegene Kante hinausgeschoben;
|
||||
die Tiefe ist Radius + Abstand zu dieser Kante.
|
||||
|
||||
### Rechteck–Rechteck
|
||||
|
||||
Zwei achsenparallele Rechtecke überlappen genau dann, wenn sich ihre Projektionen auf
|
||||
**beide** Achsen überlappen (Trennachsensatz):
|
||||
|
||||
```text
|
||||
overlap_x = hw_A + hw_B − |dx|, overlap_y = hh_A + hh_B − |dy|
|
||||
```
|
||||
|
||||
Getrennt wird entlang der Achse mit der kleineren Überlappung – das ist der kürzeste Weg
|
||||
aus der Durchdringung. Bei Gleichstand gewinnt die y-Achse, weil aufeinanderliegende Körper
|
||||
der häufigste Fall sind.
|
||||
|
||||
### Wände als Halbebenen
|
||||
|
||||
Boden, Decke und Seiten sind Halbebenen `n·p ≥ offset` (die Normale zeigt in die Welt).
|
||||
Die Reichweite eines Rechtecks entlang n ist `|n.x|·hw + |n.y|·hh`, die eines Kreises r.
|
||||
Weil eine Halbebene unendlich dick ist, wird ein Körper **auch dann erkannt, wenn er in
|
||||
einem Schritt komplett hinter die Wandlinie springt** – er kann nicht hindurchtunneln.
|
||||
|
||||
## 6. Impulsbasierte Kollisionsauflösung
|
||||
|
||||
### Herleitung des Kollisionsimpulses
|
||||
|
||||
Relativgeschwindigkeit des Kontaktpunkts (B relativ zu A), mit den Hebelarmen r vom
|
||||
Schwerpunkt zum Kontaktpunkt und `ω × r = ω·(−r.y, r.x)`:
|
||||
|
||||
```text
|
||||
v_rel = (v_B + ω_B × r_B) − (v_A + ω_A × r_A), v_n = v_rel · n
|
||||
```
|
||||
|
||||
`v_n < 0` bedeutet: Die Körper nähern sich an. Ein Impuls `P = λ·n` wirkt auf B positiv und
|
||||
auf A negativ (actio = reactio):
|
||||
|
||||
```text
|
||||
v_A' = v_A − λ·n/m_A ω_A' = ω_A − (r_A × λn)/I_A
|
||||
v_B' = v_B + λ·n/m_B ω_B' = ω_B + (r_B × λn)/I_B
|
||||
```
|
||||
|
||||
Eingesetzt ändert sich die Normalgeschwindigkeit linear in λ:
|
||||
|
||||
```text
|
||||
v_n' = v_n + λ·K, K = 1/m_A + 1/m_B + (r_A × n)²/I_A + (r_B × n)²/I_B
|
||||
```
|
||||
|
||||
Aus Newtons Stoßgesetz `v_n' = −e·v_n` folgt:
|
||||
|
||||
```text
|
||||
λ = −(1 + e) · v_n / K
|
||||
```
|
||||
|
||||
`1/K` ist die **effektive Masse** des Kontakts – die träge Masse, die der Stoß entlang n
|
||||
"spürt". Für einen einzelnen zentralen Stoß erhält diese Formel den Gesamtimpuls exakt und
|
||||
liefert die Lehrbuchergebnisse. Beim elastischen Stoß zweier Kreise mit Massenverhältnis
|
||||
1 : 3 und Geschwindigkeiten 200 bzw. −100 px/s kommen zum Beispiel exakt −250 und 50 px/s
|
||||
heraus. Ein Test prüft das.
|
||||
|
||||
**Besonderheit dieser Engine:** Bei Kreisen geht die Normale durch den Mittelpunkt
|
||||
(r ∥ n, also r × n = 0), und Rechtecke drehen sich nie (1/I = 0). Die Drehterme von K
|
||||
verschwinden daher für die Normale – **Normalimpulse erzeugen kein Drehmoment**. Nur die
|
||||
Reibung bringt Kreise ins Rollen.
|
||||
|
||||
### Viele Kontakte: Sequential Impulses
|
||||
|
||||
Liegt ein Körper auf mehreren anderen, beeinflussen sich die Kontakte gegenseitig. Statt
|
||||
ein Gleichungssystem zu lösen, werden die Kontakte reihum mehrfach aufgelöst
|
||||
(Gauß-Seidel, `velocity_iterations = 8`). Zwei Details machen das Verfahren korrekt und
|
||||
stabil:
|
||||
|
||||
1. **Akkumulierte Impulse klemmen.** Geklemmt wird der über alle Durchläufe *summierte*
|
||||
Impuls (`P_n ≥ 0`), nicht der Teilimpuls eines Durchlaufs. Ein späterer Durchlauf darf
|
||||
einen zu großen früheren Impuls also wieder zurücknehmen. Das Verfahren konvergiert so
|
||||
gegen die physikalische Lösung, in der Kontakte nur drücken und nie ziehen.
|
||||
2. **Warm-Starting.** Die akkumulierten Impulse des vorigen Schritts werden zu Beginn erneut
|
||||
angewendet. Ruhende Kontakte ändern sich kaum, der Startwert ist also schon fast richtig –
|
||||
Stapel stehen dadurch nach wenigen Durchläufen stabil. Hat sich die Normale stark
|
||||
gedreht (Skalarprodukt < 0,95), wird der alte Impuls verworfen.
|
||||
|
||||
Reihenfolge je Kontakt und Durchlauf: **Normale → Reibung → Rollwiderstand.** Reibung und
|
||||
Rollwiderstand sind durch den Normalimpuls begrenzt. Werden sie danach gelöst, gelten ihre
|
||||
Grenzen nach jedem Durchlauf exakt für den aktuellen Normalimpuls.
|
||||
|
||||
## 7. Restitution (Elastizität)
|
||||
|
||||
Die Stoßzahl `e ∈ [0, 1]` ist das Verhältnis von Trenn- zu Aufprallgeschwindigkeit:
|
||||
`e = 0` völlig unelastisch, `e = 1` vollkommen elastisch.
|
||||
|
||||
- **Energieverlust:** Beim zentralen Stoß geht `ΔE = ½·μ·(1 − e²)·v_rel²` verloren
|
||||
(μ = m_A·m_B/(m_A + m_B), die reduzierte Masse). Ein Test prüft genau diesen Wert.
|
||||
- **Sprunghöhe:** Ein fallender Ball erreicht nach dem Aufprall `e²` seiner Fallhöhe
|
||||
(Energie ∝ v²).
|
||||
- **Mischregel:** `e = max(e_A, e_B)` – ein Gummiball springt auch von einem Holzklotz
|
||||
ab. Wände haben `e = 0`, sodass allein das Material des Körpers zählt.
|
||||
- **Schwellwert (`restitution_threshold` = 50 px/s):** Unterhalb dieser
|
||||
Aufprallgeschwindigkeit wird nicht abgeprallt. Sonst würden aufliegende Körper durch
|
||||
winzige Dauer-Hüpfer zittern.
|
||||
- **Aufprallgeschwindigkeit vom Schrittbeginn:** `v_n` wird aus den Geschwindigkeiten *vor*
|
||||
Schwerkraft und Warm-Starting bestimmt (`Body.previous_velocity`). Nähme man die
|
||||
Geschwindigkeit nach der Schwerkraft, prallte ein Ball mit `e·(v + g·dt)` statt `e·v` ab.
|
||||
Ein vollkommen elastischer Ball (e = 1) gewönne dann mit jedem Aufprall Höhe – Energie
|
||||
aus dem Nichts. So springt er dagegen immer wieder genau auf seine Fallhöhe; ein Test
|
||||
prüft das über mehrere Sprünge.
|
||||
- **Gehaltene Körper prallen nicht ab:** Die Hand fängt den Stoß ab. Andernfalls würde ein
|
||||
gegen ein Hindernis gedrückter Körper in jedem Schritt zurückprallen, sofort wieder
|
||||
herangezogen werden und dabei sichtbar zittern.
|
||||
|
||||
## 8. Reibung und Rollen
|
||||
|
||||
**Coulomb-Reibung:** Entlang der Tangente `t = (−n.y, n.x)` soll die Gleitgeschwindigkeit
|
||||
verschwinden. Der Reibungsimpuls ist aber begrenzt:
|
||||
|
||||
```text
|
||||
|P_t| ≤ μ · P_n
|
||||
```
|
||||
|
||||
Reicht die Grenze, haften die Körper (Haftreibung). Sonst rutschen sie mit konstanter
|
||||
Verzögerung `μ·g` (Gleitreibung): Eine Box mit 300 px/s kommt nach `v²/(2μg)` zum Stehen.
|
||||
Gemischt wird mit dem geometrischen Mittel `μ = √(μ_A·μ_B)` – ein glatter Partner macht
|
||||
den Kontakt glatt.
|
||||
|
||||
**Rollen:** Der Reibungsimpuls greift am Kontaktpunkt an, nicht im Schwerpunkt. Bei Kreisen
|
||||
erzeugt er daher ein Drehmoment `r × P_t`, die effektive Masse enthält den Term
|
||||
`(r × t)²/I`. Eine rutschende Scheibe wird gebremst und zugleich in Drehung versetzt, bis
|
||||
die Rollbedingung `v = ω·r` gilt. Drehimpulserhaltung um den Kontaktpunkt liefert die
|
||||
Endgeschwindigkeit – unabhängig vom Reibwert:
|
||||
|
||||
```text
|
||||
v_end = v₀ / (1 + I/(m·r²)) = v₀ / 1,5 = ⅔ · v₀
|
||||
```
|
||||
|
||||
Auch das prüft ein Test. Damit man die Drehung sieht, zeichnet der Renderer in jeden Kreis
|
||||
einen Radius-Strich.
|
||||
|
||||
## 9. Rollwiderstand
|
||||
|
||||
Reines Rollen verbraucht keine Reibungsenergie – ein rollender Kreis würde ewig rollen. Ein
|
||||
Rechteck auf einem kleinen Kreis führe darauf wie auf einer Rolle davon. Real verformen sich
|
||||
Kreis und Unterlage ein wenig, was ein bremsendes Moment erzeugt:
|
||||
|
||||
```text
|
||||
M ≤ μ_r · N · r (μ_r = rolling_resistance = 0,1)
|
||||
```
|
||||
|
||||
Pro Schritt entspricht das einem Drehimpuls `≤ μ_r · P_n · r`. Umgesetzt ist das wie die
|
||||
Reibung: als begrenzter Impuls, hier aber auf die relative Winkelgeschwindigkeit
|
||||
`ω_B − ω_A`, mit der effektiven Masse `1/(1/I_A + 1/I_B)`. Weil die Grenze mit der
|
||||
Normalkraft wächst, kommen schwer belastete "Rollen" schnell zur Ruhe, während ein frei
|
||||
rollender Ball weit rollt.
|
||||
|
||||
Für einen rollenden Ball ergibt sich eine konstante Verzögerung:
|
||||
|
||||
```text
|
||||
a = μ_r·g / (1 + I/(m·r²)) = μ_r·g / 1,5 ≈ 65 px/s²
|
||||
```
|
||||
|
||||
Ein Ball mit 200 px/s rollt so etwa 300 px weit. Der Test misst genau diese Verzögerung.
|
||||
|
||||
## 10. Positionskorrektur
|
||||
|
||||
Impulse korrigieren nur Geschwindigkeiten. Durch den diskreten Zeitschritt überlappen
|
||||
Körper trotzdem ein wenig: Ein fallender Körper "landet" meist schon etwas in der
|
||||
Unterlage. Ohne Korrektur würden Stapel langsam ineinander einsinken.
|
||||
|
||||
Die Korrektur verschiebt die **Positionen** direkt ("Projektion"), im Verhältnis der
|
||||
inversen Massen:
|
||||
|
||||
```text
|
||||
C = factor · max(Tiefe − slop, 0) (höchstens max_position_correction)
|
||||
p_A −= n · C · (1/m_A) / (1/m_A + 1/m_B)
|
||||
p_B += n · C · (1/m_B) / (1/m_A + 1/m_B)
|
||||
```
|
||||
|
||||
- **Warum Positionen statt Zusatzgeschwindigkeit (Baumgarte)?** Eine Geschwindigkeit, die
|
||||
Körper auseinandertreibt, bleibt nach der Trennung erhalten und schleudert sie davon –
|
||||
besonders bei großen Überlappungen, etwa wenn ein Objekt mitten in einen Haufen gesetzt
|
||||
wird. Die Projektion erzeugt keine Bewegungsenergie.
|
||||
- **Slop (0,5 px):** Eine kleine Resttiefe bleibt stehen. So wird der Kontakt im nächsten
|
||||
Schritt sicher wieder erkannt, statt ständig zwischen "Kontakt" und "kein Kontakt" zu
|
||||
wechseln – auch das wäre sichtbares Zittern.
|
||||
- **Aktuelle Tiefe ohne erneute Kollisionsprüfung:** Tiefe bei der Erkennung minus die
|
||||
seither erfolgte Relativverschiebung entlang n. Das ist exakt für Wände und Rechtecke und
|
||||
eine sehr gute Näherung für Kreise.
|
||||
- **Notbremse:** Kein Körper darf mehr als `max_wall_penetration` (12 px) in eine Wand
|
||||
ragen. Der Wert liegt bewusst über der größten Eindringtiefe, die ein frei fliegender
|
||||
Körper in einem Schritt erreichen kann (`MAX_SPEED · dt ≈ 10,8 px`). Normale Aufpralle
|
||||
löst deshalb immer der Solver; die Bremse greift nur in Extremfällen, etwa wenn ein
|
||||
gezogenes Objekt einen Körper gegen die Wand quetscht. Würde sie schon bei normalen
|
||||
Aufprallen greifen, verschöbe sie Positionen und fügte dabei Lageenergie hinzu.
|
||||
|
||||
## 11. Ruhe ohne Zittern: Schlafmodus
|
||||
|
||||
Ein Solver mit endlich vielen Iterationen hinterlässt immer winzige Restbewegungen. Ein
|
||||
Stapel "atmet" dadurch sichtbar oder kriecht. Deshalb werden ruhende Körper eingefroren:
|
||||
|
||||
1. Jeder Körper zählt, wie lange er ununterbrochen fast stillsteht
|
||||
(`|v| ≤ 4 px/s` und `|ω| ≤ 0,1 rad/s`).
|
||||
2. Körper, die sich direkt oder über andere berühren, bilden eine **Insel** (Union-Find über
|
||||
die Kontakte). Wände verbinden keine Inseln, sonst wäre alles am Boden eine einzige Insel.
|
||||
3. Eine Insel schläft ein, wenn **alle** ihre Körper seit `time_to_sleep` (0,5 s) ruhen.
|
||||
Jeder Körper merkt sich dabei seine Insel (`Body.sleep_island`).
|
||||
4. Schlafende Körper werden weder integriert noch aufgelöst – das spart auch Rechenzeit.
|
||||
5. **Aufwecken – immer die ganze Insel:** bei Berührung durch einen wachen, beweglichen
|
||||
Körper, beim Greifen, beim Umschalten der Schwerkraft und wenn ein Körper der Insel
|
||||
gelöscht wird.
|
||||
|
||||
Warum Inseln statt einzelner Körper? Sonst könnte ein schlafender Körper unter einem wachen
|
||||
liegen. Der wache weckt ihn, beide ruhen, einer schläft früher ein, der andere weckt ihn
|
||||
wieder – ein endloses Hin und Her.
|
||||
|
||||
Warum wacht die ganze Insel auf? Zwei schlafende Körper werden nicht auf Kontakte
|
||||
untereinander geprüft. Wird die unterste Kiste eines schlafenden Stapels geweckt und im
|
||||
selben Schritt weggestoßen – oder im Pausenmodus weggezogen –, bemerkte sonst niemand, dass
|
||||
die Kisten darüber ihre Stütze verloren haben. Sie schliefen in der Luft weiter. Weil alle
|
||||
Körper, die einander berühren, gemeinsam eingeschlafen sind, genügt es, ihre Insel
|
||||
gemeinsam zu wecken.
|
||||
|
||||
Zusammen mit Schwellwert (Abschnitt 7), Slop (Abschnitt 10), Warm-Starting (Abschnitt 6)
|
||||
und Rollwiderstand (Abschnitt 9) kommen Stapel und Haufen vollständig zur Ruhe. Ein Test
|
||||
lässt 60 zufällige Körper fallen und verlangt, dass nach 8 s alle schlafen.
|
||||
|
||||
**Grenze des Verfahrens:** Wie jeder Schlafmodus mit Geschwindigkeitsschwellen kann er
|
||||
eine Lage einfrieren, die kein echtes Gleichgewicht ist. Ein Beispiel: Ein Kreis liegt
|
||||
wenige Pixel außermittig auf einem anderen Kreis. Ist die Kontaktnormale steiler geneigt,
|
||||
als der Rollwiderstand hält (tan α > μ_r), rollt er ab. Anfangs so langsam, dass er
|
||||
0,5 s lang unter 4 px/s bleibt und einschläft. Er bleibt dann liegen, bis ihn etwas
|
||||
berührt, und rollt erst danach weiter. Die Auswirkung beschränkt sich auf wenige Pixel.
|
||||
Strengere Schwellen würden solche Fälle seltener machen, ließen dafür aber auch ruhende
|
||||
Haufen später einschlafen.
|
||||
|
||||
## 12. Tunneln verhindern
|
||||
|
||||
Die Kollisionserkennung ist diskret: Sie sieht nur die Positionen am Ende jedes Schritts.
|
||||
|
||||
- **Wände** sind Halbebenen und damit ohnehin tunnelsicher (Abschnitt 5).
|
||||
- **Körper untereinander:** Fliegen zwei der kleinsten Körper (Radius `MIN_BODY_SIZE` =
|
||||
12 px) frontal aufeinander zu, darf ihre Relativbewegung pro Schritt die Summe ihrer
|
||||
Radien nicht übersteigen. Sonst springen die Mittelpunkte aneinander vorbei:
|
||||
|
||||
```text
|
||||
2·v·dt < 2·MIN_BODY_SIZE ⇒ v < MIN_BODY_SIZE / dt = 1440 px/s
|
||||
```
|
||||
|
||||
Mit Sicherheitsfaktor 0,9 ergibt das `MAX_SPEED ≈ 1296 px/s`. Jede Geschwindigkeit wird
|
||||
nach dem Lösen der Impulse darauf begrenzt, ebenso die Wurfgeschwindigkeit. Aus demselben
|
||||
Grund ist bei Rechtecken keine *halbe* Kantenlänge kleiner als `MIN_BODY_SIZE`, jede
|
||||
Seite ist also mindestens 24 px lang.
|
||||
- Auch die Winkelgeschwindigkeit ist begrenzt (`MAX_SPEED / MIN_BODY_SIZE`).
|
||||
|
||||
## 13. Ziehen und Werfen
|
||||
|
||||
Ein gezogener Körper wird **nicht** an die Mausposition teleportiert. Dann könnte er durch
|
||||
andere Körper und Wände hindurchgeschoben werden. Stattdessen setzt `DragController` vor
|
||||
jedem Physikschritt die Geschwindigkeit in Richtung Ziel:
|
||||
|
||||
```text
|
||||
v = (Ziel − Position) · rate (rate = 60/s; rate·dt ≤ 1 verhindert Überschwingen)
|
||||
```
|
||||
|
||||
Danach löst der Solver die Kontakte ganz normal. Das gezogene Objekt **kollidiert also
|
||||
weiterhin**: Leichtere Körper werden weggeschoben, Wände und schwere Stapel bremsen es. Das
|
||||
Ziel wird so geklemmt, dass der Körper ganz in die Welt passt. Gehaltene Körper unterliegen
|
||||
keiner Schwerkraft ("die Hand trägt sie"), prallen nicht ab ("die Hand fängt den Stoß ab",
|
||||
siehe [Abschnitt 7](#7-restitution-elastizität)) und schlafen nie ein. Beim Greifen wird
|
||||
die Schlaf-Insel des Körpers geweckt.
|
||||
|
||||
Folge der fehlenden Restitution: Schlägt man mit einem gehaltenen Objekt einen Ball, stößt
|
||||
es ihn wie ein unelastischer Schläger. Der Ball fliegt deshalb etwas langsamer davon als
|
||||
bei einem freien, elastischen Stoß (in einer Messung des Reviews um etwa 13 %). Dafür
|
||||
steht ein gegen ein Hindernis gedrücktes Objekt ruhig.
|
||||
|
||||
**Werfen:** Beim Loslassen erhält der Körper die Mausgeschwindigkeit der letzten Frames.
|
||||
`VelocityTracker` glättet sie in zwei Stufen:
|
||||
|
||||
1. **Zeitfenster:** mittlere Geschwindigkeit über die letzten 0,1 s (Weg / Zeit zwischen
|
||||
ältester und neuester Probe) – unabhängig von der Bildrate.
|
||||
2. **Exponentielle Glättung:** `v ← v + (v_roh − v)·(1 − e^(−Δt/τ))` mit τ = 0,05 s. Der
|
||||
Exponentialterm macht die Glättung ebenfalls bildratenunabhängig.
|
||||
|
||||
Hält man die Maus vor dem Loslassen still, klingt die Geschwindigkeit schnell ab, und das
|
||||
Objekt fällt einfach herunter. Im Pausenmodus wird direkt verschoben und nicht geworfen.
|
||||
|
||||
## 14. Parameterübersicht
|
||||
|
||||
Alle Werte stehen in [`config.py`](../src/sandbox/config.py) und lassen sich dort anpassen.
|
||||
|
||||
| Parameter | Wert | Bedeutung |
|
||||
|---|---|---|
|
||||
| `gravity` | (0, 980) px/s² | Schwerkraft |
|
||||
| `time_step` | 1/120 s | fester Zeitschritt |
|
||||
| `velocity_iterations` | 8 | Solver-Durchläufe pro Schritt |
|
||||
| `position_iterations` | 2 | Durchläufe der Positionskorrektur |
|
||||
| `restitution_threshold` | 50 px/s | darunter kein Abprallen |
|
||||
| `penetration_slop` | 0,5 px | geduldete Resttiefe |
|
||||
| `position_correction_factor` | 0,4 | Anteil der Korrektur pro Durchlauf |
|
||||
| `max_position_correction` | 6 px | Obergrenze der Korrektur pro Durchlauf |
|
||||
| `max_speed` | ≈ 1296 px/s | Tunnel-Schutz |
|
||||
| `rolling_resistance` | 0,1 | Rollwiderstandsbeiwert μ_r |
|
||||
| `sleep_linear_speed` / `sleep_angular_speed` | 4 px/s / 0,1 rad/s | Ruheschwellen |
|
||||
| `time_to_sleep` | 0,5 s | Ruhezeit bis zum Einschlafen |
|
||||
| `wall_friction` / `wall_restitution` | 0,6 / 0 | Wandmaterial |
|
||||
| `max_wall_penetration` | 12 px | Notbremse an den Wänden (> `MAX_SPEED · dt`) |
|
||||
Reference in New Issue
Block a user