DSHINTERACTIVE
Tutorials
RENDERINGFortgeschritten11:52Iron Sector

Licht, das die Form kennt: Normal Maps für vorgerenderte Sprites

In einem 2D-Strategiespiel ist ein Panzer ein Bild mit eingebackener Sonne. Schlägt nachts eine Granate daneben ein, weiß das Bild nicht, welche Seite der Explosion zugewandt ist. Diese Lektion zeigt das eine Stück Daten, das das ändert, die Normal Map, und wie wir sie in Iron Sector eingebaut haben.

Sprite-Sheets mit Normal Maps
135
Sprite-Sheets mit Normal Maps
Frames, jeder mit eigenen Normalen
55.204
Frames, jeder mit eigenen Normalen
Auflösung der Normal-Seiten
½
Auflösung der Normal-Seiten
pro Frame bei 50 beleuchteten Sprites
+0,5 ms
pro Frame bei 50 beleuchteten Sprites
01

Ein Sprite ist ein Bild

In einem 2D-Strategiespiel ist ein Panzer kein 3D-Modell. Er ist ein Bild, einmal aus einer festen Kamera gerendert und in einem Atlas abgelegt. Jeder Frame ist nur ein Raster aus Farben, und das Sonnenlicht wurde beim Erzeugen des Sprites in diese Farben eingebacken.

Das ist schnell und sieht richtig aus, bis etwas anderes in der Szene zu leuchten beginnt: ein Mündungsfeuer, eine Explosion, ein brennendes Haus in der Nacht. Das Bild weiß nicht, wohin seine Flächen zeigen, also kann das Spiel höchstens das ganze Sprite aufhellen. Es wirkt wie ein Aufkleber.

Mit Normalen — Mit Normalen
Mit Normalen — Ohne Normalen
Ohne NormalenMit Normalen
Zum Vergleichen ziehen
02

Was eine Normale ist

Eine Normale ist ein Pfeil der Länge eins, der senkrecht aus einer Fläche ragt und zeigt, wohin die Fläche gerichtet ist. Um eine Fläche zu beleuchten, vergleicht ein Renderer diesen Pfeil mit einem zweiten, der zur Lichtquelle zeigt.

1760 beschrieb Johann Lambert die Regel, die Spiele bis heute nutzen: Eine matte Fläche empfängt Licht proportional zum Kosinus des Winkels zwischen den beiden Pfeilen. Dieser Kosinus ist einfach ihr Skalarprodukt: x mal x, y mal y, z mal z, und addieren. Eine Fläche, die direkt zum Licht zeigt, bekommt alles; hochkant oder abgewandt bekommt sie nichts.

brightness ∝ cos θ = n · l
n · l = nx·lx + ny·ly + nz·lz
Winkel zum LichtEmpfangenes Licht
0°
1.00
45°
0.71
60°
0.50
≥ 90°
0
03

Normal Maps in Spielen

1978 zeigte James Blinn, dass man kleine Unebenheiten vortäuschen kann, indem man die Normalen einer Fläche verbiegt, ohne neue Geometrie. 2004, mit Doom 3 und Half-Life 2, war das Standard: Ein einfaches Modell trägt eine Textur aus Normalen, gebacken von einem viel detaillierteren, und Nieten und Ziegel fangen das Licht.

2D-Spiele übernahmen die Idee. Mit Sprite Lamp malt ein Artist eine Figur von mehreren Seiten beleuchtet, und das Tool leitet daraus eine Normal Map ab. Dead Cells rendert seine Figuren aus 3D-Modellen und exportiert zu jedem Frame eine Normal Map. Iron Sector liegt diesem Fall am nächsten: Unsere Sprites werden aus 3D-Modellen gerendert, also werden die Normalen nicht gemalt oder geschätzt. Sie werden gemessen.

04

Wie eine Normale in einem Bild steckt

Eine Normale hat drei Zahlen und ein Bild drei Kanäle, also speichert eine Normal Map den Pfeil als Farbe. Rot sagt, wie weit die Fläche nach rechts zeigt, Grün, wie weit nach oben, Blau, wie weit zur Kamera.

Jeder Wert liegt zwischen −1 und 1 und wird in den Farbbereich gepresst: halbieren, einen halben addieren. Deshalb sehen Normal Maps violett aus: Eine Fläche, die direkt zur Kamera zeigt, ist (128, 128, 255). Zum Zurücklesen teilt man durch 255, verdoppelt und zieht eins ab.

colour = n × 0.5 + 0.5
n = colour × 2 − 1
(218, 83, 205) → (0.71, −0.35, 0.61)
Farb-Frame und Normal-Frame des Tigers, im selben Durchgang gerendert.
Farb-Frame und Normal-Frame des Tigers, im selben Durchgang gerendert.
Ein Pixel des Turms: gespeichert (218, 83, 205) → ÷255 → ×2 − 1 → der Pfeil (0,71; −0,35; 0,61).
Ein Pixel des Turms: gespeichert (218, 83, 205) → ÷255 → ×2 − 1 → der Pfeil (0,71; −0,35; 0,61).
05

Woher unsere Normalen kommen

Die Modelle in Iron Sector sind als Code geschrieben und werden in Voxel verwandelt, Würfel von etwa zwei Zentimetern. Ein Voxel an der Oberfläche hat mindestens einen leeren Nachbarn. Für seine Normale betrachtet der Generator alle 26 Nachbarn: Jeder gefüllte schiebt den Pfeil von sich weg, die diagonalen etwas schwächer. Die Summe, auf Länge eins gebracht, zeigt aus dem Material heraus.

Auf einem flachen Dach ist unten alles gefüllt und oben alles leer, der Pfeil zeigt genau nach oben; auf einem runden Turm fächern die Pfeile weich auf. Derselbe Durchgang, der die Farbe eines Pixels schreibt, schreibt auch seine Normale in denselben Pixel, daher passt die Normal Map exakt zum Sprite. Für das Spiel waren das 135 Sprite-Sheets und 55.204 Frames, gebaut neben den vorhandenen Atlanten.

n = normalize( −Σ filled(k) · dₖ / |dₖ| )   over 26 neighbours
Dasselbe Modell aus mehreren Richtungen: Farbe und Normalen, Frame für Frame.
Dasselbe Modell aus mehreren Richtungen: Farbe und Normalen, Frame für Frame.
06

Die Sonne, die schon da ist

Jedes Sprite trägt bereits ein Licht: eine Sonne von links oben, beim Rendern eingebacken, 30 % Umgebungslicht und 70 % Sonne nach demselben Skalarprodukt.

Zuerst versuchten wir den Lehrbuchweg: Farben ohne Licht speichern und alles zur Laufzeit neu beleuchten. Der neu beleuchtete Panzer wich um bis zu 32 von 255 Stufen ab, unser Limit war 3; der weiche Bodenschatten und Details unterhalb eines Pixels lassen sich aus den Maps nicht zurückgewinnen. Also bleibt der abgenommene Tageslook genau so, und neues Licht wird obendrauf addiert. Genau das macht auch ein echter Blitz.

intensity = 0.30 + 0.70 · max(0, n · L)
L = (−0.55, 0.30, 0.78)
07

Die Gleichung eines Lichts

Die hinzugefügte Farbe ist die Eigenfarbe des Sprites mal einer Stärke k, mal der Lichtfarbe, mal einem Richtungsterm, mal einem Entfernungsterm. Der Richtungsterm ist das Skalarprodukt mit Wrap Lighting: 0,2 vor dem Teilen addiert lässt das Licht etwas um die Kanten greifen, wie auf staubigem lackiertem Stahl. Der Entfernungsterm fällt weich von eins auf null am Radius des Lichts.

Nimm einen Turmpixel bei Nacht und einen Blitz auf halbem Radius. Von rechts ist n · l = 0,71, nach Wrap 0,76; der Abfall 0,56; mit k = 2,2 wird der Pixel um 93 % heller. Von links ist n · l negativ, und der Pixel bekommt nichts. Ohne Normalen bekommen beide Seiten dieselben 124 %, und der Panzer wirkt flach.

added = colour × k × light × wrap(n · l) × falloff(d / r)
wrap(x) = max(0, (x + 0.2) / 1.2)
falloff(q) = (1 − q²)²
k: day 0.35 · dawn 1.2 · night 2.2
Links schlägt eine Granate ein. Mit Normalen leuchten nur die ihr zugewandten Flächen auf. — Mit Normalen
Links schlägt eine Granate ein. Mit Normalen leuchten nur die ihr zugewandten Flächen auf. — Ohne Normalen
Ohne NormalenMit Normalen
Zum Vergleichen ziehen
Links schlägt eine Granate ein. Mit Normalen leuchten nur die ihr zugewandten Flächen auf.
Dieselbe Granate rechts: Das Licht wechselt die Seite. — Mit Normalen
Dieselbe Granate rechts: Das Licht wechselt die Seite. — Ohne Normalen
Ohne NormalenMit Normalen
Zum Vergleichen ziehen
Dieselbe Granate rechts: Das Licht wechselt die Seite.
08

Zwei Räume, eine Kamera

Die Normalen liegen im Bildschirmraum, so wie die Kamera sie sieht. Die Lichter leben in der Welt, als Punkt am Boden und Höhe darüber. Unsere Kamera ist für immer fest, 45° herum und 30° nach unten, also ist die Umrechnung eine einzige Drehung um 30°.

Für jeden Pixel nimmt der Shader den Vektor zum Licht, neigt ihn um den Kamerawinkel und bildet erst dann das Skalarprodukt. Ein Feuer 10 Einheiten hoch und 10 Einheiten vor dem Panzer wird zu einem Pfeil, der vor allem in den Bildschirm und etwas von oben zeigt: Es beleuchtet die uns zugewandten Flächen und ein wenig das Dach.

v = ( dx,  dz·cos 30° − dy·sin 30°,  dy·cos 30° + dz·sin 30° )
09

Im Spiel

Nachts feuert ein Tiger seine Hauptkanone. Der Blitz dauert wenige Frames, aber sein Licht reicht sechsmal weiter als sein sichtbarer Schein, denn im Dunkeln bemerkt man das Licht. Ohne Normalen malt der Blitz einen hellen Kreis gleichermaßen auf Boden und Panzer. Mit Normalen leuchten Frontplatte und Blende auf, das Heck bleibt dunkel. Bei Tag ist derselbe Blitz kaum zu sehen, wie es sein soll.

Bei Explosionen zahlen sich Normalen aus. Eine Granate links beleuchtet die linke Seite, eine rechts lässt das Licht wandern. Im Sperrfeuer nimmt jedes Sprite die acht stärksten Lichter und addiert sie, sodass ein Panzer gleichzeitig von zwei, drei Seiten beleuchtet sein kann, jede in ihrer eigenen Farbe.

TagTag
DämmerungDämmerung
NachtNacht
Derselbe Schuss bei Tag, in der Dämmerung und bei Nacht. Die Stärke k wächst, je dunkler die Szene ist.
Mit Normalen — Mit Normalen
Mit Normalen — Ohne Normalen
Ohne NormalenMit Normalen
Zum Vergleichen ziehen
Ein brennendes Wrack direkt neben dem Panzer beleuchtet ihn fast gleichmäßig, mit Absicht. — Mit Normalen
Ein brennendes Wrack direkt neben dem Panzer beleuchtet ihn fast gleichmäßig, mit Absicht. — Ohne Normalen
Ohne NormalenMit Normalen
Zum Vergleichen ziehen
Ein brennendes Wrack direkt neben dem Panzer beleuchtet ihn fast gleichmäßig, mit Absicht.
10

Licht, das an der Wand endet

Zu entscheiden, wo das Licht gezeichnet wird, war die halbe Arbeit. Über allen Sprites gezeichnet, schien ein Panzer hinter einem Haus durch die Wand. Unter dem Nachtfilter gezeichnet, kappte das dunkle Blau das Rot auf ein Drittel, und Feuer wurde braun.

Die Lösung war ein eigenes Lichtbild. Der Renderer zeichnet die Szene ein zweites Mal in derselben Reihenfolge in eine separate Textur: beleuchtete Sprites als reines Licht, alle anderen als schwarze Silhouette, sodass ein Haus vorn den Panzer dahinter verdeckt. Dann wird dieses Bild über die Nachtfarben addiert, und Feuer bleibt warm. Eine ehrliche Grenze bleibt: Das Licht wirft noch keine Schatten.

11

Was es kostet

Eine Normal Map ist so groß wie das Bild, zu dem sie gehört, aber Form ändert sich langsamer als Farbe, daher liegen die Normalen in halber Auflösung vor. Hochskaliert weichen sie um etwa eine Stufe von 255 ab. Die Szene braucht ein Zehntel bis ein Viertel mehr Speicher, und auf einer schnellen GPU kosten fünfzig beleuchtete Sprites etwa eine halbe Millisekunde pro Frame.

Das ganze System ist optional. Mit ausgeschalteter Beleuchtung ändert sich kein einziger Pixel, und keine Normal Map wird geladen. „Mittel“ beleuchtet 50 Sprites mit je 3 Lichtern, „Hoch“ 150 Sprites mit je 8 Lichtern.

12

Zusammenfassung

Drei Zahlen in einem Pixel, ein Skalarprodukt pro Licht und eine sorgfältige Entscheidung, wo es gezeichnet wird. Die Sprites sind immer noch Bilder, einmal aus einer festen Kamera gerendert, aber jetzt weiß jeder Pixel, wohin er zeigt. Und wenn im Dunkeln etwas brennt, dreht der Panzer seine Panzerung ins Licht.

Neue Lektionen

Neue Tutorials per E-Mail

Eine E-Mail, wenn eine neue Lektion erscheint, mit Video und Text.

Wir nutzen deine Adresse nur für diese Updates. Kein Spam.

Neue Tutorials auf YouTube.

Devlogs, Analysen und Schritt-für-Schritt-Lektionen vom Team von DSH Interactive.

Auf YouTube abonnieren