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
- 135Sprite-Sheets mit Normal Maps
- Frames, jeder mit eigenen Normalen
- 55.204Frames, jeder mit eigenen Normalen
- Auflösung der Normal-Seiten
- ½Auflösung der Normal-Seiten
- pro Frame bei 50 beleuchteten Sprites
- +0,5 mspro Frame bei 50 beleuchteten Sprites
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.


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 Licht | Empfangenes Licht |
|---|---|
| 0° | 1.00 |
| 45° | 0.71 |
| 60° | 0.50 |
| ≥ 90° | 0 |
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.
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)


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

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)
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




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° )
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.







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.
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.
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.