KAPITEL 5 VON 7

Licht und Schatten

Ohne Licht wäre jede Fläche einfarbig und die Szene flach. Es gibt zwei grundverschiedene Wege, Licht zu berechnen. Der eine ist schnell und kennt keine Schatten, der andere liefert Schatten und ist teuer.

Weg 1: Helligkeit aus dem Winkel

Leuchte mit einer Taschenlampe senkrecht auf einen Tisch: Es entsteht ein kleiner, heller Fleck. Hältst du die Lampe schräg, wird der Fleck länger und blasser. Dieselbe Lichtmenge verteilt sich auf eine größere Fläche.

Ziehe die LampeSeitenansicht einer Fläche

Zwei Richtungen bestimmen die Helligkeit: die Normale n, ein Pfeil, der senkrecht auf der Fläche steht, und die Richtung l von der Fläche zur Lichtquelle. Entscheidend ist der Winkel zwischen beiden:

Helligkeit = cos(Winkel zwischen n und l)

Bei 0° ist der Kosinus 1 (volle Helligkeit), bei 60° ist er 0,5, bei 90° ist er 0: Das Licht streift nur noch.

Am Würfel hat jede Seite eine eigene Normale und deshalb eine eigene Helligkeit. Mehr braucht es nicht, damit er räumlich wirkt:

Ziehen zum Drehen
Die Pfeile sind die Normalen. Die Prozentzahl ist die Helligkeit der Seite.

Der Computer rechnet den Kosinus aus, ohne den Winkel selbst zu kennen. Haben n und l beide die Länge 1, genügen drei Multiplikationen. Diese Rechnung heißt Skalarprodukt:

def helligkeit(n, l):
    k = n[0]*l[0] + n[1]*l[1] + n[2]*l[2]     # ergibt cos des Winkels zwischen n und l
    return max(0, k)                          # Licht von hinten: dunkel, nicht negativ

Was dabei fehlt

Die Rechnung betrachtet nur die Fläche und die Lichtquelle. Ob ein anderes Objekt dazwischen steht, kommt in ihr nicht vor. Schatten gibt es so nicht. Im folgenden Bild ist die Kugel richtig schattiert, aber der Boden unter ihr bleibt hell. Schalte um, um den Unterschied zu sehen.

Ziehe die Lichtquelle

Weg 2: Raytracing

Raytracing dreht die Fragestellung aus Kapitel 4 um. Dort hieß es: Welche Pixel bedeckt dieses Dreieck? Jetzt heißt es für jedes Pixel: Was sieht man hier?

  1. Von der Kamera aus wird ein Strahl durch das Pixel in die Szene geschickt.
  2. Der Computer sucht das erste Objekt, das der Strahl trifft. Trifft er nichts, zeigt das Pixel den Himmel.
  3. Vom Treffpunkt aus wird ein zweiter Strahl zur Lichtquelle geschickt, der Schattenstrahl.
  4. Kommt der Schattenstrahl ungehindert an, wird die Helligkeit wie bei Weg 1 berechnet. Trifft er unterwegs ein Objekt, liegt der Punkt im Schatten.
Lampe lässt sich ziehen

Das Schema zeigt eine flache Welt von der Seite. Das Bild hat nur 16 Pixel, es ist die Spalte links. Gehe mit dem Regler die Pixel einzeln durch, dann schalte auf „Alle Pixel“.

for pixel in bild:
    strahl = strahl_durch(kamera, pixel)
    treffer = erstes_objekt_auf(strahl, szene)
    if treffer is None:
        pixel.farbe = HIMMEL
    elif weg_zum_licht_frei(treffer.punkt, lichtquelle, szene):
        pixel.farbe = treffer.farbe * helligkeit(treffer.normale, richtung_zum_licht)
    else:
        pixel.farbe = SCHATTEN

Wie findet man heraus, ob ein Strahl eine Kugel trifft?

Die einfachste Methode: Man wandert in kleinen Schritten den Strahl entlang und prüft an jeder Stelle, ob man in der Kugel ist. Das ist man, wenn der Abstand zum Mittelpunkt kleiner ist als der Radius.

def trifft_kugel(start, richtung, mitte, radius):
    weg = 0
    while weg < MAXIMALE_ENTFERNUNG:
        punkt = [start[0] + weg * richtung[0],
                 start[1] + weg * richtung[1],
                 start[2] + weg * richtung[2]]
        if abstand(punkt, mitte) <= radius:
            return weg                      # Treffer nach dieser Strecke
        weg = weg + SCHRITT
    return None                             # nichts getroffen

Das funktioniert, kostet aber viele Schritte pro Strahl. Für Kugeln und Dreiecke lässt sich der Treffpunkt auch direkt ausrechnen, ohne zu probieren. Das ist schneller und genauer, und so machen es echte Programme.

Für Neugierige: der Treffpunkt als Gleichung

Jeder Punkt auf dem Strahl lässt sich schreiben als start + weg · richtung. Gesucht ist die Strecke weg, bei der dieser Punkt genau den Abstand radius vom Mittelpunkt hat. Setzt man den Punkt in die Abstandsformel ein und quadriert, bleibt eine quadratische Gleichung in weg übrig:

weg² + 2 · b · weg + c = 0

Dabei fasst b zusammen, wie stark der Strahl auf den Mittelpunkt zu- oder von ihm wegläuft, und c, wie weit der Startpunkt von der Kugeloberfläche entfernt ist. Die Lösungsformel liefert weg = −b ± √(b² − c). Steht unter der Wurzel etwas Negatives, verfehlt der Strahl die Kugel. Sonst gibt es zwei Lösungen: den Eintritts- und den Austrittspunkt.

Aufwand

Für jeden Strahl muss das erste getroffene Objekt gefunden werden. Die naheliegende Lösung testet den Strahl gegen jedes Objekt der Szene. Bei 2 Millionen Pixeln und 1 Million Dreiecken wären das 2 Billionen Tests pro Bild, und die Schattenstrahlen kommen noch dazu. Das ist aussichtslos.

Die Lösung: Hüllkörper in Hüllkörpern

Die Idee kennst du aus Kapitel 3: Man packt Objekte in einfache Hüllen. Hier geht man einen Schritt weiter. Alle Objekte kommen in einen großen Quader. Dieser wird in zwei Hälften geteilt, jede Hälfte bekommt ihren eigenen Quader, und so weiter, bis in jedem Quader nur noch ein oder zwei Objekte liegen. Es entsteht ein Baum aus Hüllkörpern.

Für einen Strahl heißt das: Verfehlt er einen Quader, kann er nichts treffen, was darin liegt. Alle Objekte in diesem Quader entfallen mit einem einzigen Test.

Ziehe das Ende des Strahls. Dunkel markiert sind die Objekte, die wirklich getestet werden. Die Rahmen sind die Quader, die der Strahl durchquert. Erhöhe die Zahl der Objekte und beobachte, wie wenig die Zahl der Tests mit dem Baum wächst.

Mit jeder Ebene des Baums halbiert sich die Zahl der Objekte, die noch in Frage kommen. Bei n Objekten braucht ein Strahl deshalb nur etwa log₂ n Schritte bis zum Ziel, statt n. Bei einer Million Dreiecken sind das rund 20 Schritte.

VerfahrenAufwand pro Bild wächst mitSchatten und Spiegelungen
Rasterisierung (Kapitel 4)Zahl der Dreieckenur über Umwege
Raytracing ohne BaumPixel · Dreieckeergeben sich direkt
Raytracing mit BaumPixel · log₂(Dreiecke)ergeben sich direkt

Und die Rasterisierung?

Auch sie kann Schatten erzeugen, aber nur mit einem Umweg: Die Szene wird ein zweites Mal gezeichnet, diesmal aus Sicht der Lichtquelle. Der Tiefenpuffer dieses Bildes sagt, welche Punkte das Licht „sieht“. Alle anderen liegen im Schatten.

Heutige Spiele kombinieren beide Wege. Das Grundbild entsteht durch Rasterisierung, weil sie schneller ist. Raytracing kommt gezielt dort hinzu, wo es am meisten bringt: bei weichen Schatten und Spiegelungen.

Merke
Weg 1: Die Helligkeit einer Fläche ist der Kosinus des Winkels zwischen ihrer Normale und der Richtung zum Licht. Das ist schnell, kennt aber keine Schatten. Weg 2, Raytracing: Pro Pixel wird ein Strahl von der Kamera verfolgt und vom Treffpunkt ein Schattenstrahl zum Licht. Bezahlbar wird das durch einen Baum aus Hüllkörpern, der pro Strahl nur etwa log₂ n Tests braucht.

Aufgabe: Eine Szene hat 8 Millionen Dreiecke. Wie viele Schritte braucht ein Strahl ungefähr im Baum aus Hüllkörpern, wenn jede Ebene die Zahl der Dreiecke halbiert?

Lösung anzeigen

log₂(8 000 000) ≈ 23, denn 2²³ = 8 388 608. Statt 8 Millionen Tests genügen etwa 23 Schritte bis zu einem einzelnen Dreieck.

Aufgabe: Ein Bild hat 1920 × 1080 Pixel. Wie viele Strahlen braucht Raytracing mindestens, wenn jedes Pixel einen Kamerastrahl und einen Schattenstrahl bekommt? Wie viele sind es pro Sekunde bei 60 Bildern?

Lösung anzeigen

1920 · 1080 ≈ 2,07 Millionen Pixel, mit Schattenstrahl also rund 4,1 Millionen Strahlen pro Bild und knapp 250 Millionen pro Sekunde.

Aufgabe: Warum genügt dem Schattenstrahl die Antwort „irgendein Hindernis“, während der Kamerastrahl das erste getroffene Objekt braucht?

Lösung anzeigen

Für den Schatten zählt nur, ob etwas im Weg steht. Die Suche kann beim ersten gefundenen Hindernis abbrechen. Der Kamerastrahl muss wissen, welches Objekt vorn liegt und damit sichtbar ist.