Ich denke schon seit einem guten halben Jahr über die richtige Art und Weise nach, wie ich dieses Thema sinnvoll und verständlich rüberbringen kann. Das Thema Grafikformate ist leider komplexer, als viele anfangs denken, und die richtigen Formate ändern sich auch im Verlauf der Produktionspipeline. Aus diesem Grund habe ich mich nun entschieden, das Thema in drei Teile aufzuteilen:
Der Grund für diese Aufteilung ist relativ einfach: Das richtige Format hängt von den jeweiligen Anforderungen ab.

Von der Texturerstellung über den Engine-Import bis zur GPU-Laufzeit
Phase 1: Arbeitsphase
Innerhalb dieser Phase - ich habe sie einfach mal Arbeitsphase genannt, da Artists hier in der Regel ihre Hauptzeit verbringen, um Assets zu erstellen - werden in der Regel auch die Texturen erstellt. Da wir hier über Texturen reden, gehen wir einfach mal davon aus, dass mögliche 3D-Modelle bereits fertig modelliert sind. Das genaue Vorgehen hängt natürlich von den individuellen Workflows der Studios bzw. der Einzelpersonen ab, aber oft sind mehrere Programme dabei involviert, sofern man nicht mit ausschließlich prozeduralen Materials arbeitet. Wir nutzen hier nun einen beispielhaften Workflow, der sich so oder so ähnlich auch in der Praxis finden lässt:
- Exportieren des fertigen High- und Low-Poly-Modells aus der DCC
- Baken und Exportieren diverser Maps (Normal, Curvature, …) in Marmoset Toolbag
- Importieren des Low-Poly-Modells und der vorher erstellten Maps in Adobe Substance 3D Painter
- Erstellen der Texturen in Adobe Substance 3D Painter
- Exportieren der finalen Texturen für die Game Engine
Wenn man während des Texturierens zwischen Programmen wechselt, zwischen 2. und 3. in diesem Beispiel, sollte man darauf achten, dass man als Austauschformat ein Grafikformat wählt, dass möglichst alle Details erhält, es gilt also der alte Spruch “Haben ist besser als brauchen”. Viele denken jetzt bestimmt: “Ich nutze eh PNG, das ist doch ein verlustfreies Format”. Das stimmt auch soweit, doch PNG unterstützt zwar 8 und 16 Bit pro Kanal, aber kein Floating Point, sodass HDR nicht möglich ist, was uns an dieser Stelle evtl. einschränken könnte. Das stimmt auch soweit, doch PNG unterstützt erst seit der neuesten veröffentlichten Spezifikation (Juni 2025) HDR-Inhalte, sodass ich zum aktuellen Zeitpunkt von einer eingeschränkten Unterstützung von HDR-Inhalten mit PNG ausgehen würde (siehe Nachtrag). Aus diesem Grund möchte ich OpenEXR (.exr) in den Raum werfen. Das ist ein Grafikformat, was im CGI- und VFX-Bereich oft verwendet wird und im Extremfall bis zu 32 Bit Float pro Farbkanal unterstützt. Mit EXR ist es möglich, viel feinere Übergänge abzubilden als mit PNG, sodass jedes noch so kleine Detail konserviert werden kann und EXR daher das ideale Austauschformat während des Texturierens ist.
Phase 2: Import in die Game Engine
Vielen ist bestimmt aufgefallen, dass bei Punkt 5 im zuvor beschriebenen Workflow auch nochmals entschieden werden muss, welches Dateiformat man für die finalen Texturen nutzt. Und genau damit wollen wir uns nun in diesem Abschnitt auseinandersetzen.
In dieser Phase sollten die Arbeiten soweit abgeschlossen sein. Wir müssen also keine Informationen für mögliche Bearbeitungen konservieren. Unser Ziel ist es nun, die finalen Texturen so zu exportieren, dass sie möglichst fehlerfrei von der Game Engine importiert und interpretiert werden können. Aus diesem Grund setzen wir zwar auf Grafikformate, die verlustfreie Kompression anbieten, aber begrenzen die Inhalte auf die Informationen, die für das Rendering auch wirklich notwendig sind, was in der Regel 8 Bit pro Kanal sind. Das Standardformat ist hier oft PNG. PNG ist ein gutes Format an dieser Stelle, aber nicht für alle Texturen unumstritten. Grund dafür ist einerseits, dass PNG auch sogenannte “Rendering Intents” enthalten kann und andererseits, dass es ein Problem mit Transparenzen geben kann. Rendering Intents / ICC-Profile können, wenn sie falsch gesetzt sind und von der Game Engine interpretiert werden, dafür sorgen, dass wir in der Game Engine die interpretierten Werte manuell nach dem Import korrigieren müssen. Möchte man diesen Problemen aus dem Weg gehen, so bietet sich TGA an. TGA speichert rohe Pixelwerte ohne Farbmanagement oder Metadaten, was es robust für Engine-Importe macht. Die Transparenz bei TGA ist über einen separaten Alpha-Kanal geregelt, sodass auch hier, im Gegensatz zu PNG, nichts fehlinterpretiert werden kann. Dabei sind diese Texturdateien dann auch nur reine Importcontainer, da die Engines intern die Texturen nach dem Import in plattformspezifischen komprimierten Formaten speichern.
Neben dem Dateiformat ist die korrekte Farbraum-Interpretation (sRGB vs. Linear) entscheidend für das spätere Rendering-Ergebnis. Die einzigen Texturen, die normalerweise als sRGB interpretiert werden sollten, sind die Albedo- / Basecolor-Texturen. Alle anderen typischen Texturen (Normal, Roughness, Metalness, AO, …) sind linear zu interpretieren, da diese nicht direkt angezeigt werden, sondern als Grundlage für Berechnungen dienen.
Phase 3: In der Engine bzw. zur Laufzeit
Bis zu diesem Zeitpunkt haben wir uns in erster Linie darauf konzentriert, dass wir Daten bzw. Details erhalten und alles möglichst gut und verlustfrei in die Game Engine importieren können. Daher war Speicherplatz bzw. Speichereffizienz erst einmal zweitrangig. Und darüber, ob es mit den bisherigen Grafikformaten auch möglich ist, nur bestimmte Teile der Texturen zu laden, haben wir uns bislang auch keine Gedanken gemacht. Aber das ändert sich nun, denn wir fangen jetzt an, unsere Texturen auf Laufzeit zu optimieren, und da gelten andere Regeln. Genauer gesagt gelten hier die Regeln der GPU 🐲
Es gibt extra auf GPUs optimierte Grafikformate, die den großen Vorteil bieten, dass sie sehr speichereffizient sind und direkt auf der GPU dekodiert werden können. Es gibt also spezielle Hardware innerhalb der GPUs, die die Dekodierung der Bilddaten vornimmt, was einerseits schneller und effizienter als Software ist und andererseits dafür sorgt, dass weniger Daten von der CPU an die GPU geschickt werden. Dazu kommt, dass diese Formate auch direkte Unterstützung für MipMaps haben. Diese Vorteile kommen aber auch mit einem Nachteil, den wir bislang vermieden haben: Diese Speichereffizienz wird durch verlustbehaftete Komprimierung erreicht. Die Grafikformate sind generell Blockkompressionsformate, d.h. das Bild wird in einzelne Blöcke (bspw. 4x4) zerlegt und jeder dieser Blöcke einzeln enkodiert. So ist es möglich, nur bestimmte Blöcke einer Textur zu laden. In der Praxis gibt es zwei Format-Familien:
Es gibt eine relativ harte Trennung zwischen PC / Konsole und Mobile. Im klassischen DirectX-PC-Ökosystem ist BCn weiterhin Standard, während ASTC primär im Mobile- und ARM-Umfeld (bspw. Apple) verbreitet ist. Der Grund dafür ist in der Historie zu finden. BCn, damals noch S3 Texture Compression (S3TC) wurde 1998 vorgestellt und ist seitdem Teil von DirectX und OpenGL (mittlerweile natürlich auch Vulkan). Daher haben die GPU-Hersteller auch früh angefangen, passende Hardware dafür in ihren GPUs zu integrieren. ASTC wurde erst 2012 vorgestellt und ist deutlich flexibler als BCn, aber zu diesem Zeitpunkt war der Einsatz von BCn schon so verbreitet, dass man im PC- und Konsolenmarkt keine Chance mit ASTC hatte, da die GPU-Hersteller (scheinbar) nicht bereit sind, ihre GPUs um passende Hardware für ASTC zu erweitern. Im Mobile-Segment sieht es deutlich anders aus. Da dieser Markt 2012 noch sehr jung war und jedes Jahr große Sprünge in der Leistungsfähigkeit der Chips zu verbuchen waren, bieten mittlerweile die meisten Anbieter von Mobile GPUs Unterstützung für ASTC.
Bevor wir nun in die Tiefen der einzelnen Codecs abtauchen, möchte ich schon mal auf eine Zusammenfassung hinweisen, die ganz ohne Mathematik auskommt und ein paar Leitlinien liefert, wann welcher Codec der richtige sein könnte.
BCn
BCn (BC1 - BC7) basieren alle auf dem gleichen Grundprinzip. Das Bild wird in 4x4-Texelblöcke aufgeteilt und jeder Block wird separat komprimiert. Dabei gilt die Annahme, dass die Varianz zwischen den einzelnen Pixeln innerhalb eines 4x4-Texelblocks relativ gering ist, sodass es möglich sein sollte, die Farben der einzelnen Pixel so zu interpolieren, dass die Kompressionsartefakte nicht weiter auffallen.
Die jeweiligen Codecs sind dabei so konstruiert, dass sie eine konstante Speichergröße pro Block erreichen. Dadurch ist ein direkter Zugriff auf einzelne Blöcke möglich, da leicht zu berechnen ist, an welcher Speicherstelle sich welcher Block befindet. Wie genau die Blöcke komprimiert werden, hängt von dem konkreten Codec ab.
In den folgenden Beispielen nutzen wir 8 Bit RGB bzw. 8 Bit RGBA als Ausgangsformat, d.h. jeder Pixel benötigt unkomprimiert 3 bzw. 4 Byte, sodass ein gesamter 4x4-Texelblock unkomprimiert bei einem Speicherverbrauch von 48 Byte (RGB) bzw. 64 Byte (RGBA) liegt.
BC1
- Blockgröße: 8 Byte
- 2 Byte:
c0als RGB565 - 2 Byte:
c1als RGB565 - 4 Byte: Indextabelle (16 * 2 Bit)
- 2 Byte:
Jeder mit BC1 komprimierte 4x4-Texelblock belegt 8 Byte. Der erste Schritt bei der Komprimierung ist es, 2 Farben zu finden, die möglichst gut das Farbspektrum des gesamten Blocks abbilden. Diese Farben, die c0 und c1 genannt werden, werden als RGB565 gespeichert, d.h. jeweils 5 Bit für den Rot- bzw. Blaukanal und 6 Bit für den Grünkanal, was dafür sorgt, dass c0 und c1 nur jeweils 2 Byte benötigen.
Anschließend werden aus c0 und c1 temporär 2 weitere Farben (c2 und c3) berechnet. Für diese Berechnung werden in der Praxis zwei verschiedene Arten genutzt:
- \(\text{c2} = \frac{2}{3} \text{c0} + \frac{1}{3} \text{c1},\; \text{c3} = \frac{1}{3} \text{c0} + \frac{2}{3} \text{c1} \)
- \(\text{c2} = \frac{5}{8} \text{c0} + \frac{3}{8} \text{c1},\; \text{c3} = \frac{3}{8} \text{c0} + \frac{5}{8} \text{c1} \)
Die meisten Implementierungen nutzen die erste Variante, wobei NVIDIA wohl die zweite Variante nutzt (Quelle). Was zur Folge hat, dass die Ergebnisse je nach Implementierung leicht variieren können. Unterschiedliche Algorithmen für die Rückkonvertierung von RGB565 auf RGB888 können für weitere Unterschiede mit unterschiedlichen GPUs sorgen.
Anschließend prüfen wir für jeden Pixel des Texelblocks, mit welcher der 4 Farben die größte Ähnlichkeit besteht und speichern den Index der Farbe mit der größten Übereinstimmung in einer Indextabelle. Das bedeutet, dass die Indextabelle 16 Einträge von jeweils 2 Bit hat, sodass die gesamte Tabelle 4 Byte belegt.
Die richtige Wahl von c0 und c1 ist dabei essentiell, wie man sich unschwer vorstellen kann. Daher gibt es auch dafür diverse Implementierungen.
Die folgende Grafik versucht den gesamten Vorgang nochmal darzustellen:

BC1 Beispiel
Der Unterschied zwischen Source und Decompressed mag auf den ersten Blick groß sein, erst recht, weil ich c0 und c1 einfach aus dem Bauch heraus gewählt habe. Aber schaut euch auch mal den Vergleich in Originalgröße an:

Generell kann man in der Grafik 4 Vergleiche erkennen. Dabei ist links immer der Originalblock und rechts daneben der BC1-Block:
- 4x4-Texelblock in Originalgröße
- 4x4-Texelblock in Originalgröße, aber sowohl vertikal als auch horizontal mit 4 Wiederholungen
- 4x4-Texelblock in vierfacher Auflösung
- 4x4-Texelblock in vierfacher Auflösung, aber sowohl vertikal als auch horizontal mit 4 Wiederholungen
In der Theorie gibt es für BC1 auch einen Modus, der 1 Bit als Transparenz pro Pixel speichern kann, d.h. Pixel können entweder transparent oder opak sein. In der Praxis sollte man aber auf einen der anderen Codecs setzen, wenn der Alphakanal benötigt wird.
BC2 / BC3
- Blockgröße: 16 Byte
- RGB: 8 Byte (siehe BC1)
- Alpha: 8 Byte
BC2 und BC3 sind sich sehr ähnlich. Beide erweitern BC1 um die Unterstützung von Alpha.
Bei BC2 werden dafür für jeden der expliziten Pixel 4 Bit gespeichert.
Bei BC3 wird Alpha mit einer eigenen Interpolationstabelle gespeichert, was deutlich bessere Übergänge ermöglicht. Diese Interpolationstabelle funktioniert ähnlich wie die Farb-Indextabelle bei BC1. Anstatt Farbwerte in RGB565 werden pro Texelblock 2 Alphawerte mit jeweils 1 Byte gespeichert. Die verbleibenden 6 Byte werden für die die Interpolationstabelle verwendet, was \(6\, \text{Byte} / 16 = 3\, \text{Bit}\) pro Tabelleneintrag bedeutet. Somit werden bis zu 8 verschiedene Alphawerte pro Texelblock unterstützt.
BC4
- Blockgröße: 8 Byte
Die Implementierung von BC4 ist identisch zur Verfahrensweise mit dem Alphakanal von BC3. Es gibt also ebenfalls 2 gespeicherte Farben / Graustufen (jeweils 1 Byte) und eine Indextabelle mit jeweils 3 Bit pro Pixel, sodass 8 verschiedene Werte pro 4x4-Texelblock unterstützt werden. Somit ist BC4 perfekt für 1-Kanal-Texturen (Graustufen) wie bspw. Height Maps.
BC5
- Blockgröße: 16 Byte
BC5 besteht einfach aus zwei BC4-Blöcken, die hintereinander gespeichert werden. Daher bietet sich BC5 für Texturen mit zwei unabhängigen Kanälen an, was es zum idealen Format für Normal Maps macht, da der dritte Kanal (B bzw. Z) im Shader (\(z = \pm \sqrt{1 - x^2 - y^2}\)) rekonstruiert werden kann.
BC6H & BC7
Die Faktoren, die das Ergebnis von BC1 am meisten limitieren, sind:
- Geringe Bittiefe von RGB565, insbesondere auch die ungleiche Verteilung der Bits pro Kanal, sorgt vor allem bei Graustufen für Farbverschiebungen
- Es werden nur 4 mögliche Farben pro Texelblock unterstützt
- Alle Farben liegen auf einer Linie im RGB-Spektrum, da
c2undc3ausc0undc1interpoliert werden
Diese möglichen Probleme sollen mit BC6H und BC7 gelöst werden, die mit DirectX 11 eingeführt wurden. Dafür werden über den Codec vorgegebene Modi benutzt, mit denen eine flexiblere Nutzung des Speichers möglich ist. Es findet ein, je nach Modus unterschiedlicher, Trade-off zwischen der Präzision der gespeicherten Farben und der Anzahl an möglichen Farbzwischenwerten statt. Das hat zur Folge, dass dadurch die optimale Komprimierung deutlich aufwendiger wird, dafür aber auch die Kompressionsartefakte minimiert werden.
BC6H
- Blockgröße: 16 Byte
BC6H ist als HDR-Format gedacht und speichert die RGB-Werte als FP16 ab. Es stehen insgesamt 14 unterschiedliche Modi bereit, die bspw. hier nachgelesen werden können.
BC7
- Blockgröße: 16 Byte
BC7 ist der aktuell qualitativ hochwertigste Codec von BCn für RGBA. Es werden 8 verschiedene Modi unterstützt, die den 4x4-Texelbock in bis zu 3 Teilregionen (Subsets) unterteilen, wobei für jede Teilregion jeweils 2 Farbwerte gespeichert werden, zwischen denen interpoliert wird. Die Behandlung von Alpha hängt ebenfalls vom gewählten Modus ab. Eine Übersicht über alle verfügbaren Modi findet man bspw. hier.
ASTC
Adaptive Scalable Texture Compression (ASTC) war eine gemeinsame Entwicklung von ARM und AMD, die 2012 vorgestellt wurde. Es werden LDR und HDR jeweils als 2D- oder 3D-Texturen unterstützt. Das generelle Verfahren ist dabei auf den ersten Blick sehr ähnlich zu BC6H / BC7, d.h. es gibt vordefinierte Modi, die unterschiedliche Partitionen vorgeben, aber der Unterschied liegt im Detail. ASTC kann mit unterschiedlichen Blöckgrößen von 4x4 bis 12x12 (2D) bzw. von 3x3x3 bis 6x6x6 operieren.
Ein weiterer Unterschied ist, dass ASTC Bounded Integer Sequence Encoding (BISE) verwendet. Das ist ein spezielles Kodierungsverfahren, um Integerzahlen eines festen Zahlenbereichs effizient zu kodieren. Das Problem bei Binärzahlen ist, dass diese in ihrer Binärdarstellung dann am effizientesten zu speichern sind, wenn der zu kodierende Zahlenbereich eine Zweierpotenz ist, also \(\log_2\) ein glattes Ergebnis liefert.
Wenn man es nun aber bspw. mit dem Zahlenbereich \(0 - 9\), also \(10\) Werten, zu tun hat, benötigt man idealerweise \(\log_2(10) = 3.32\) Bits, um alle Werte möglichst kompakt speichern zu können. Aber \(3.32\) Bits lassen sich so natürlich nicht abbilden. d.h. man muss mindestens 4 Bits nutzen. Wenn man nun 3 Zahlen im Zahlenbereich \(0 - 9\) speichern möchte, würde man normalerweise \(3 * 4\,\text{Bits} = 12\,\text{Bits}\) benötigen. Kombiniert man sie aber mit BISE reichen \(3 * \log_2(10) = 9.97 \), also 10 Bits aus.
Empfehlungen
Im PC- und Konsolenbereich hat man beim Grafikformat eigentlich keine große Wahl, da man um BCn nicht herumkommt, wenn man Hardwareunterstützung haben möchte. Umso wichtiger ist die Wahl des richtigen Codecs:
| Format | Kanäle | Bytes/Block | Besonderheit |
|---|---|---|---|
| BC1 | RGB(+1 bit alpha) | 8 | Grundformat, simpel |
| BC2 | RGBA | 16 | BC1 + explizite Alphawerte |
| BC3 | RGBA | 16 | BC1 + interpolierte Alphawerte |
| BC4 | 1 Kanal | 8 | Monochrom/ Graustufen |
| BC5 | 2 Kanäle | 16 | Normalmaps / Masken |
| BC6H | HDR RGB | 16 | FP16 HDR |
| BC7 | RGBA | 16 | Höchste Qualität |
Im Mobile-Bereich sieht das anders aus. Es gibt ältere Format-Familien wie ETC und PVRTC, die teilweise noch eine breitere Hardwareunterstützung haben oder aber natürlich ASTC, was aktuell das flexibelste und leistungsfähigste Format ist.
Fazit & Ausblick
Ich hoffe, jedem von euch ist beim Lesen des Artikels bewusst geworden, dass das richtige Grafikformat immer vom konkreten Anwendungsfall abhängt:
- In Phase 1 versucht man, möglichst viele Details beim Wechseln zwischen Programmen zu erhalten
- In Phase 2 versucht man, den gewünschten Detailgrad möglichst gut in die Game Engine zu überführen
- In Phase 3 versucht man, den Detailgrad möglichst laufzeiteffizient zu erhalten und geht daher einige Kompromisse ein
Gerade für Phase 2 wird es in Zukunft mit AVIF einen sehr interessanten Kandidaten geben, der aktuell noch etwas Zeit benötigt, um eine breitere Tool- und Engine-Unterstützung zu bekommen.
In Phase 3 existiert mit ASTC ein sehr leistungsfähiges Format, dem aktuell leider die Hardwareunterstützung im PC- und Konsolenbereich fehlt. Es bleibt zu hoffen, dass die GPU-Hersteller sich mal wieder auf ihre Herkunft besinnen und die HW-Unterstützung für ASTC in den nächsten Jahren endlich (wieder) einführen.
Sollte Interesse bestehen, kann ich auch gerne in einem weiteren Artikel erklären, wie man die passenden Einstellungen in den jeweiligen Game Engines konfiguriert. Lasst mich einfach nur wissen, ob euch das interessieren würde.

