So verwendet man LUTs (Lookup-Tabellen) in Videospielen
(blog.frost.kiwi)- LUTs (Lookup-Tabellen) sind eine Methode, vorab berechnete Werte tabellarisch nachzuschlagen, und werden breit eingesetzt – von der Einfärbung von Schwarzweiß-Wärmebildern bis zu Objektvariationen in Spielen und Color Grading von Videos
- Das WebGL-Beispiel zeigt einen Ablauf, bei dem Videoframes als 2D-Textur auf die GPU geladen und dann im Fragment Shader, der die Farbe pro Pixel bestimmt, Tinting, 1D-LUTs und 3D-LUTs angewendet werden
- Eine 1D-LUT verwendet den Helligkeitswert
[0.0 - 1.0]als Koordinate in einem Farbarray, um Schwarzweiß-Eingaben in RGB umzuwandeln; durch bilineare Filterung der GPU können selbst kleine LUTs Zwischenwerte natürlich interpolieren - Wie im Fall von Valves Left 4 Dead werden Tinting und LUTs genutzt, um innerhalb begrenzten Texturspeichers Farbvarianten für Autos, Zombiehaut und Kleidung zu erzeugen
- LUTs verursachen zusätzliche Kosten durch weitere Texturzugriffe, sind aber ein praktisches Werkzeug, um teure Berechnungen wie Gammakorrektur vorab zu speichern oder externe Color-Grading-Workflows in Echtzeitgrafik zu übernehmen
Was LUTs in der Grafikprogrammierung leisten
- Eine Lookup-Tabelle (LUT) ist eine Methode, Werte im Voraus zu berechnen, in Zeilen oder Tabellen zu speichern und bei Bedarf nachzuschlagen
- In der Grafikprogrammierung führt diese einfache Struktur zu verschiedenen visuellen Effekten
- Schwarzweißbilder werden eingefärbt
- Farbkorrekturen und Stilisierung werden angewendet
- Objektvariationen in Videospielen werden effizient erzeugt
- Das Beispiel verwendet WebGL, aber dieselbe Technik lässt sich auch in DirectX, OpenGL, Vulkan, Unity und in der wissenschaftlichen Datenvisualisierung einsetzen
- Das Demovideo stammt von einer Panasonic GH6 und einer TESTO-890-Wärmebildkamera; die Verarbeitung verlässt das Gerät nicht und erfolgt lokal auf der GPU
WebGL-Pipeline und Tinting
- Die Ausgabe der Wärmebildkamera ist ein Schwarzweißvideo, und jeder Frame wird über WebGL als 2D-Textur an die GPU übergeben
- Der Fragment Shader ist der Code, der für jedes Fragment der Ausgabe die endgültige Farbe bestimmt
- Im Beispiel wird mit
texture2D(video, tex).rgbdie Videofarbe an der aktuellen Position gelesen - Die endgültige Ausgabe in WebGL 1.0 wird als RGBA in
gl_FragColorgeschrieben
- Im Beispiel wird mit
- Tinting ist eine Methode, bei der die Texturfarbe mit einer bestimmten Farbe multipliziert wird, um einen Farbstich aufzutragen
- Im Beispiel wird mit
videoColor * vec3(1.0, 0.5, 0.0)ein Orangeton multipliziert - Weiß wird dadurch orange, Schwarz bleibt schwarz, da es mit 0 multipliziert wird
- Im Beispiel wird mit
- In diesem Kontext können die Multiplikationskosten von Tinting schwer messbar sein
- Der Aufwand, eine Textur aus dem Speicher zu laden, ist deutlich größer als der einer Multiplikation
- Während viele Threads parallel laufen, kann während des Wartens auf einen Texturzugriff für ein Pixel bereits die Multiplikation für andere Pixel ausgeführt werden
- Das erklärt den Unterschied, den Tinting verursacht, bedeutet aber nicht, dass die gesamte Performance-Optimierung damit abgeschlossen wäre
Wie Valve Tinting einsetzt
- Valve Software nutzte in Left 4 Dead Tinting, um Variationen von Autos zu erzeugen
- Für jede Autovariante und Farbe eine eigene Textur vorzuhalten, könnte das Budget für Texturspeicher überschreiten
- Valve verwendet eine zusätzliche Maskentextur, um die lackierten Bereiche eines Autos pro Instanz mit unterschiedlichen Farben zu tinten
- Eine Farbtextur wird von drei Automodellen gemeinsam genutzt
- Mit den Kosten von zwei Texturen lassen sich vier Automodelle und praktisch unbegrenzt viele Farbvarianten erzeugen
- In der Source Engine lässt sich nicht nur bei Autos, sondern im Grunde bei fast allem Tinting einsetzen
Schwarzweißwerte mit einer 1D-LUT in Farben umwandeln
- Eine 1D-LUT ist ein Zahlenarray; bei einem RGB-Bild also ein eindimensionales Array aus Farben
- Der Helligkeitswert eines Schwarzweißbilds wird als X-Koordinate in der LUT verwendet, um eine Farbe abzurufen
- Der Helligkeitswert des Videos liegt im Bereich
[0.0 - 1.0] 0.0für Schwarz wird auf die linke Farbe der LUT abgebildet,1.0für Weiß auf die rechte- Zwischenwerte werden auf die entsprechende Position dazwischen abgebildet
- Der Helligkeitswert des Videos liegt im Bereich
- Da WebGL 1.0 keine 1D-Texturen hat, wird eine 2D-Textur mit 1 Pixel Höhe verwendet
- Der Beispielcode hat die Form
texture2D(lut, vec2(videoColor, 0.5)) - Da der Eingang ein 1D-Wert und der Ausgang RGB ist, ergibt sich eine Struktur „1D vector in, 3D vector out“
- Der Beispielcode hat die Form
- Die bilineare Filterung der GPU interpoliert Zwischenwerte beim Texturlesen automatisch
- Ein 8-Bit-Schwarzweißbild hat 256 Helligkeitsstufen
- Selbst wenn die 1D-LUT nur 32 Pixel breit ist, werden Zugriffe zwischen den Pixeln linear interpoliert
- Schon mit Informationen aus 8 Pixeln lässt sich ein Ergebnis erzeugen, das einem farbigen Verlauf mit 256 Pixel Breite nahekommt
Auswahl von Colormaps und wahrnehmungsmäßige Gleichmäßigkeit
- Das Beispiel enthält eine Liste von Colormaps, die von matplotlib unterstützt und als 1D-LUT exportiert werden können
- Wenn die Daten keine besondere Struktur haben, ist es sinnvoll, wahrnehmungsmäßig gleichmäßige Colormaps zu verwenden oder die Farben darauf aufzubauen
- Die viridis-Familie ist ein typisches Beispiel
- Wahrnehmungsmäßig gleichmäßige Colormaps behalten diese Eigenschaft auch in Graustufen bei: kalte Bereiche bleiben dunkel, heiße Bereiche hell
- Colormaps wie jet, die vor allem den Farbton (hue) ändern und die wahrgenommene Helligkeit ignorieren, garantieren diese Eigenschaft nicht
- Ein wichtiges Kriterium bei der Wahl einer Colormap ist außerdem, dass auch Menschen mit Farbsehschwäche die Daten korrekt interpretieren können
Performance von 1D-LUTs und Polynomapproximation
- Die Kosten einer kleinen 1D-LUT können meist gering sein, aber es gibt mehr zu beachten als bei Tinting
- Die wichtigste Performance-Sorge ist ein abhängiger Texturzugriff
- Das Ergebnis des ersten Texturzugriffs bestimmt die Koordinate des zweiten
- In der Grafikprogrammierung ist das ein Muster, das bestimmte optimierbare Pfade ausschließen kann
- GPUs besitzen Textur-Caches, und kleine LUTs passen oft hinein, wodurch die Kosten des LUT-Zugriffs sehr niedrig sein können
- Für eine genaue Beurteilung der Performance sind plattformspezifische Analysetools nötig
- Nvidia NSight bietet Performance-Analyse pro Shader-Stufe, unterstützt aber kein OpenGL
- Eine Colormap lässt sich auch als Polynomapproximation implementieren, ohne eine LUT-Textur zu verwenden
- Matt Zuckers ShaderToy-Approximation von viridis ist in Form der Horner-Methode geschrieben
- Die Farbe wird mit Multiply-Add-Operationen der Form
c0+t*(c1+t*(c2+t*(c3+t*(c4+t*(c5+t*c6)))))berechnet - Ob das tatsächlich schneller ist als eine LUT, lässt sich ohne plattformspezifische Analyse nur schwer beurteilen
Zombie-Variationen in Left 4 Dead 2
- Left 4 Dead 2 verwendet LUTs für Farbvariationen von Haut und Kleidung, bei denen einfaches Tinting nicht ausreicht
- Reines Tinting bietet möglicherweise nicht genug Vielfalt in der Luminanz der Farben
- Für Haut und Kleidung werden einige LUTs zufällig ausgewählt, um unterschiedliche Farbvarianten zu erzeugen
- Artists wählen geeignete Color Ramps aus, die dann ins finale Spiel übernommen werden
- So lassen sich aus einer einzigen Textur sowohl helle als auch dunkle Anzugvarianten erzeugen
- Die vollständige Präsentation dazu ist auf der GDC unter Shading a Bigger Better Sequel zu finden
- „Exclusive Masking“ ist eine Methode, bei der zwei Texturbereiche in einem Kanal untergebracht werden
- Texture 1 belegt den Bereich 0–128
- Texture 2 belegt den Bereich 128–256
- Der Preis dafür ist verringerte Farbpräzision
1D-LUTs zum Vorabspeichern von Berechnungen
- 1D-LUTs können in der Grafikprogrammierung auch als Cache für teure Berechnungen dienen
- Ein typisches Beispiel ist die Gammakorrektur
- Wenn man nicht approximiert, wird die Funktion
pow()benötigt - Besonders auf älteren GPUs kann
pow()eine teure Instruktion sein - Verwendet man die standardmäßige stückweise sRGB-Kurve, kommen zusätzlich noch Verzweigungen hinzu
- Wenn man nicht approximiert, wird die Funktion
- Wenn die Gamma-Transformation vorab in einer 1D-LUT berechnet wird, lassen sich Berechnungen pro Pixel überspringen
- In diesem Fall ergibt sich eine Struktur „1D vector in, 1D vector out“
- Eine 1D-LUT kann bis zu vier Farbkanäle ausgeben
- Redshift ist ein Softwarebeispiel, das den Monitor nachts orange tintet, um die Augen zu schonen
- Es verändert die Gamma Ramp der Rot-, Grün- und Blaukanäle des Monitors
- Kelvin-Wärme → RGB und zusätzliche Gamma-Berechnungen werden im Voraus in drei 1D-LUTs berechnet
- Die zugehörige Implementierung steht im Redshift-Quellcode
- Bei diesem Ansatz erfolgt das Remapping im Monitor statt in der Grafikkarte, sodass es keinen Performance-Einfluss gibt; die Unterstützung auf Hardware-Schnittstellen ist inzwischen jedoch oft instabil oder defekt
- Beim Grafik-Stack des Raspberry Pi ist diese Unterstützung in einem neueren Update verloren gegangen
- Microsoft warnt vor der Verwendung von SetDeviceGammaRamp
3D-LUTs: den gesamten RGB-Raum neu abbilden
- Eine 3D-LUT stellt den gesamten RGB-Raum als einen einzigen Würfel dar und bildet jede mögliche Farbe neu ab
- Ein eingehender dreidimensionaler RGB-Vektor wird in einen ausgehenden dreidimensionalen RGB-Vektor umgewandelt, also „3D vector in, 3D vector out“
- Wenn man nur je eine 1D-LUT auf Rot, Grün und Blau anwendet, lässt sich zwar die Farbbalance verändern
- Für Transformationen, die Kombinationen von Eingangs-RGB-Werten benötigen, ist jedoch eine 3D-LUT nötig
- Sättigungsänderungen
- Farbtonänderungen
- Korrektur bestimmter Farben
- Farbtrennung
- 3D-LUTs haben üblicherweise Würfelform; in Spielen werden sie oft als Strip oder Quadrat gespeichert, in Videoschnitt-Tools als „Iridas/Adobe“-
.cube-Datei
3D-LUTs in WebGL 1.0 implementieren
- Das Beispielvideo wurde mit einer Panasonic GH6 im Panasonic V-Log-Farbprofil aufgenommen
- V-Log ist ein Log-Profil mit klar definierter Gamut- und Gamma-Charakteristik, das mehr Dynamikumfang erhält
- Da WebGL 1.0 keine 3D-Texturen unterstützt, wird ein 3D-Texturzugriff über zwei 2D-Texturzugriffe plus Interpolation implementiert
- Dieser Ansatz basiert auf Gregg Tavares’ Google-I/O-2011-Vortrag und der Erklärung auf webglfundamentals.org
- Im bestehenden Beispielcode gab es einen Fehler bei der Berechnung der Z-Achse des Würfels, der zu einem Blaustich führte; das wurde 2019 korrigiert
- Wer Kompatibilität mit WebGL 1.0, OpenGLES 2 und OpenGL 2.1 ohne
OES_texture_3Derhalten will, sollte aktuellen Code für 3D-Textur-Sampling verwenden
Color-Grading-Workflows mit 3D-LUTs übernehmen
- Korrekturen, die auf eine 3D-LUT angewendet werden, wirken genauso auf Videos oder Grafikszene, die durch diese LUT laufen
- Im Beispiel werden das V-Log-Video und die LUT in DaVinci Resolve importiert und dort bearbeitet
- Es wird Panasonics V-Log to V-709 3D-LUT angewendet
- Kontrast und Weißpunkt werden so angepasst, dass Weiß volle Helligkeit erreicht
- Anschließend wird die korrigierte LUT wieder exportiert
- Das eigentliche Video bleibt unverändert; das Color Grading wird in WebGL in Echtzeit über die LUT angewendet
- Die hochgeladene LUT muss dieselbe Größe beibehalten
- Die Einschränkung im Beispiel ist 1024px × 32px
- Die Form eines 32³px-Würfels muss erhalten bleiben
Wie die Spielebranche 3D-LUTs verwendet
- Farben eines Spiels mit 3D-LUTs in externen Tools zu stilisieren, ist in der Videospielbranche ein gut etablierter Standard-Workflow
- Das typische Verfahren sieht so aus
- Einen Screenshot der Szene erstellen, die farbkorrigiert werden soll
- Screenshot und initialisierte 3D-LUT gemeinsam in einem Tool wie Photoshop öffnen
- Auf Screenshot und LUT dieselbe Farbkorrektur anwenden
- Den 3D-LUT-Teil ausschneiden und exportieren
- Auch Left 4 Dead verwendet diese Methode
- Die Farbkorrektur-Werkzeuge in Photoshop lassen sich frei einsetzen, Filter, die Beziehungen zwischen mehreren Pixeln verändern, jedoch nicht
- Convolution-basierte Filter wie blur, sharpen oder emboss verwischen neu zugeordnete Farben und können unerwartete Ergebnisse erzeugen
Komplexes Color Grading in eine einzige LUT backen
- Color-Grading-Tools wie DaVinci Resolve können komplexe Farbtransformationen und Grading-Schritte erzeugen und als 3D-LUT exportieren
- Das Beispiel verwendet eine Kodak-2383-LUT, um einen Film-Print-Look zu erzielen
- V-Log-Gamma-Material wird in Cineon Film Log umgewandelt
- Der Farbraum wird in Rec.709 umgewandelt
- Eine Filmemulation wird angewendet, und das Gamma wird zurück nach Rec.709 konvertiert
- Zum Schluss wird der Weißpunkt angepasst
- So lassen sich komplexe Detailtransformationen in eine einzige LUT backen und in eine Grafik-Anwendung übernehmen, ohne sie direkt implementieren zu müssen
- Das Beispielvideo liegt aus Kompatibilitätsgründen im stark komprimierten 8-Bit-Format vor, typischerweise arbeitet man hier aber mit 10-Bit-Footage
- LUTs werden oft kleiner eingesetzt, als man vermuten würde
- Panasonics offizielle V-Log-to-V-709-3D-LUT hat eine Größe von 33³px
- Panasonics kamerainterne Monitoring-LUT verwendet 17³px
- Selbst in Panasonics Cinema-Kameras im fünfstelligen Dollarbereich werden 17³px verwendet
LUT-Beispiele außerhalb der Grafik
- LUTs kommen nicht nur in der Grafikprogrammierung zum Einsatz
- Auch beim Port von Tomb Raider auf den Game Boy Advance wurden LUTs verwendet
- Der Open-Source-Port der OpenLara-Engine verarbeitet vieles in Software, weil der Game Boy Advance keine 3D-Funktionen besitzt
- Eines davon ist eine LUT zur Implementierung ganzzahliger Division
- Diese LUT wurde am Anfang des ROM-Adressraums platziert, um eine load instruction zu überspringen
1 Kommentare
Meinungen auf Hacker News
Ohne Lookup-Tabellen hätte man diesen Effekt wohl gar nicht umsetzen können: https://twitter.com/zeta0134/status/1756988843851383181
Der Kernpunkt ist, dass zwei Lookup-Tabellen verwendet werden. Die große Tabelle speichert kreisförmige Beleuchtungsinformationen innerhalb eines konfigurierbaren Radius um den Spieler, und für jeden Radius gibt es eine komplette eigene Tabelle. Die zweite Tabelle enthält eine pseudozufällige Reihenfolge der Hintergrundzeilen. Bei jedem Aufruf der Fackel-Beleuchtungsroutine bleibt nur Zeit, 1/20 des Bildschirms zu aktualisieren; wenn man die Reihenfolge aber etwas randomisiert, wirken die Ränder weicher und der sonst sichtbare Raster-Scan wird verborgen. Diese Reihenfolge ist ein Grab Bag, damit keine Zeile bei den Updates verhungert; Echtzeitberechnung ist zu langsam, daher nutzt man eine Tabelle.
Der erste Lookup-Tabellen-Effekt, der mich wirklich beeindruckt hat, war die Methode zur Erzeugung eines Texture Tunnels
Man hat für jedes Pixel auf dem Bildschirm eine Lookup-Tabelle mit Winkel und Entfernung vom Bildschirmzentrum und wählt damit aus, welches Texel an welcher Pixelposition platziert wird. Es sieht aus, als bewege man sich durch einen Tunnel aus 3D-Geometrie, ist aber sehr billig und daher sogar auf pico möglich: https://www.lexaloffle.com/bbs/?pid=63818
Anfangs dachte ich, Stardust hätte diesen Effekt verwendet, aber gerade nachgeschaut: Tatsächlich wurde im Hintergrund nur eine sich wiederholende 6-Frame-Animation abgespielt: https://codetapper.com/amiga/sprite-tricks/stardust/
https://web.engr.oregonstate.edu/~mjb/cs557/Projects/Papers/Parallax_Occlusion_Mapping.pdf
Es gibt ein Video darüber, wie Wind Waker mehrere LUTs verwendet, um seinen unverwechselbaren Look zu erzeugen. BoTW und ToTK nutzen dieselbe Technik.
https://www.youtube.com/watch?v=mnxs6CR6Zrk
Ich bin der Autor des Artikels. Wenn es Fragen gibt, kann ich sie beantworten.
Allerdings war ich etwas überrascht von der Art, wie Farbkorrektur per LUT vorgestellt wird. Im Artikel wirkt es wie eine clevere Nischenlösung, die L4D2 genutzt hat, tatsächlich ist es aber seit langer Zeit Industriestandard und wurde in jedem Spiel verwendet, an dem ich gearbeitet habe – von AAA-Spielen wie NFS (2015) bis zu großen Indie-Titeln wie Lost in Random.
[1] https://www.copetti.org/writings/consoles/game-boy-advance/#operating-system
„Here is every single colormap that matlibplot supports,...“
Selbst im langweiligen Bereich der Business Processes ist das überraschend nützlich.
Oft lässt sich eine Codebasis voller Bedingungslogik durch saubere Lookup-Tabellen vereinfachen. Selbst scheinbar einfache Dinge können zu Lookup-Tabellen mit 50.000 Zeilen werden, weshalb man nicht immer sofort daran denkt. Für Endnutzer kann das viel wirken, aber Computer kümmern sich kaum darum. Außerdem sind Lookup-Tabellen repetitiv und daher meist gut zu pflegen; und selbst wenn nicht, sind sie immer noch wertvoll. Sie können auch von Endnutzern konfiguriert werden, ohne den Code zu ändern. Insgesamt ist es ein besonders nützliches Konzept für viele Szenarien, die beim Coden von Business-Logik ständig auftauchen.
Retro-Spiele haben wirklich sehr viele Tabellen verwendet. Damals war Speicherzugriff sehr schnell und Prozessoren waren langsam, daher war es sinnvoll, möglichst viele Berechnungen in Tabellen auszulagern. Je cleverer man dabei war, desto spektakulärere Spiele konnte man bauen.
Ich mag den Kanal. Damit seine Mods schneller laufen, hat er den gesamten Mario-64-Code überarbeitet und auf stabile 60 FPS gebracht [0]
[0] https://www.youtube.com/watch?v=t_rzYnXEQlE
Wenn man viel mit LUTs arbeitet: Ich entwickle eine Mac-App für anspruchsvolle Color Science und Transformations-Workflows: https://videovillage.com/lattice
Ich habe mich früher ein wenig mit der NES-Homebrew-Szene beschäftigt und in einem Spiel namens Bobl erstaunliche Physikeffekte gesehen, die weit über dem lagen, was das System eigentlich berechnen konnte. Es stellte sich heraus, dass es eine Lookup-Tabelle war, und mir wurde klar, dass man mit einem einfachen Werkzeug etwas wie einen sehr komplexen Prozess aussehen lassen kann.
https://morphcatgames.itch.io/bobl
A Podcast Of Unnecessary Detail hat eine Folge über die SNES-Doom-Portierung und darüber gemacht, wie LUTs für trigonometrische Funktionen verwendet wurden. Der Kontext war, dass das SNES keinen Grafikprozessor hatte.
https://festivalofthespokennerd.com/podcast/series-3-episode-4/
https://github.com/RandalLinden/DOOM-FX
Sie konnte auch Hintergründe rotieren und skalieren. Das ist Mode 7 — man denke an Mario Kart — und sie unterstützte auch Hardware-Transparenz, wie man sie in vielen Spielen sieht. Außerdem hatten Cartridges wie Doom und Starfox den Super FX-Chip, der 3D-Berechnungen übernahm. Bei diesen speziellen Titeln hatte man es also faktisch mit zwei Grafikprozessoren zu tun.
Es gibt eine hervorragend und schmerzhaft detaillierte Serie von Retro Game Mechanics Explained zur SNES-Hardware:
https://www.youtube.com/watch?v=57ibhDU2SAI&list=PLHQ0utQyFw5KCcj1ljIhExH_lvGwfn6GV&index=1
LUT-Einsätze, die mir aus meiner eigenen Arbeit einfallen, waren atmosphärische Streuung, Einfärben von Sprites, Nachtsicht-Zielfernrohre, FLIR-Zielfernrohre, Schwarzweiß-„Videofeed“-Effekte, Glitch-Effekte, Heightmap-Shading, Alpha-Punkt-Faktoren für Abgasfahnen von Raumschiffen, Heatmaps zur Maus-Verweildauer von Website-Besuchern, kristalline Effekte und schließlich Post-Processing-Kolorierung im nativen Farbraum.
Eine LUT ist die Visualisierung eines Arrays bereits bekannter Werte und erstaunlich nützlich.
http://www.effectgames.com/demos/canvascycle/
Die ursprüngliche Xbox konnte das nicht besonders gut, und Photoshop unterstützte es ebenfalls nicht, weshalb es an Beliebtheit verlor. Moderne Hardware kann von AESprite erzeugte Palette Swaps aber problemlos verarbeiten.