Texturen – der VRAM-Fresser, der deine Grafikkarte verschlingt
Im letzten Artikel haben wir die Vertices halbiert und das Modell etwas schlanker gemacht – aber ein echter Leichtgewicht-Champion ist es noch nicht. Denn der wahre Platzfresser sitzt woanders: bei den Texturen. Bei einem PBR-Modell machen Texturen typischerweise über 80 % des Volumens aus – und genau dieser Teil bläht sich im VRAM am heftigsten auf.
Dieser Artikel widmet sich genau diesem „VRAM-Fresser“-Problem. Drei Dinge stehen auf dem Programm: Warum PNG/JPG aus GPU-Sicht eine Art „Sünde“ darstellen; wie GPUs eigene Texturformate aussehen und warum man sie nicht einfach direkt verwenden kann; und wie die Kombination aus Basis Universal + KTX2 diese drei Welten miteinander verbindet.
Kurze Wiederholung: Warum JPG den VRAM explodieren lässt
Im letzten Artikel gab es schon eine Formel:
VRAM-Verbrauch = Breite * Höhe * 4 Bytes (RGBA) * 1,333 (mit Mipmaps)
Eine 4096×4096-Textur – egal ob sie auf der Festplatte ein 1,5-MB-JPG oder ein 8-MB-PNG ist – belegt im VRAM rund 87 MB. Der Grund ist simpel: Die GPU kennt kein JPG/PNG.
Die Texture-Sampling-Einheit der GPU kann nur eines: Zu einer gegebenen UV-Koordinate die Farbe aus einem fest definierten Pixelblock auslesen. Dafür muss die Textur im VRAM als „flach ausgebreitete Rohpixel“ vorliegen. Der Browser muss also ein JPG, bevor es auf die GPU geladen wird, per CPU komplett in RGBA-Pixel dekomprimieren und dann als ganzen Block in den VRAM schieben.
Dieser Prozess hat dreifachen Ärger im Gepäck:
- VRAM-Explosion: Die dekomprimierten Rohpixel sind riesig. 87 MB sind keine Übertreibung, sondern das Ergebnis der Formel.
- Upload-Blockade: Das Verschieben großer Pixelblöcke vom CPU-RAM in den GPU-VRAM ist langsam und blockiert das Rendering des ersten Frames.
- CPU-Dekomprimierungskosten: Das Entpacken großer Bilder kostet Zeit – auf Mobilgeräten besonders spürbar.
Um beim Bild vom „Kompressionsschwamm“ aus dem letzten Artikel zu bleiben: PNG/JPG sind der zusammengedrückte Schwamm, praktisch für den Transport. Sobald er auf der GPU ankommt, saugt er sich voll und dehnt sich wieder auf Originalgröße aus. Der Download ist schneller, aber der VRAM spart nichts.
GPUs eigene Texturformate: Von Natur aus im VRAM komprimiert
Wenn die GPU also keine komprimierten PNGs akzeptiert – können wir die Texturen dann auch im VRAM komprimiert halten? Die GPU dekodiert dann beim Sampling in Echtzeit einzelne Pixelblöcke – nahezu ohne Zusatzkosten.
Genau das machen GPU-native Texturformate. Die bekanntesten Familien:
| Formatfamilie | Vollständiger Name | Hauptplattformen | Eigenschaften |
|---|---|---|---|
| BC1-7 | Block Compression | Desktop (PC, Mac) | Etabliert, komprimiert 4×4-Pixelblöcke |
| ETC1/2 | Ericsson Texture Compression | Mobil (Android/iOS ältere Geräte) | Mobil-Standard der älteren Generation |
| ASTC | Adaptive Scalable Texture Compression | Mobil/VR (neuere Geräte) | Flexibel, beste Qualität, blockweise anpassbar |
| PVRTC | PowerVR | Ältere iOS-Geräte | Wird zunehmend von ASTC abgelöst |
Gemeinsamkeit dieser Formate: Texturen werden in kleinen 4×4-Pixel-Blöcken komprimiert gespeichert. Die GPU dekodiert beim Sampling nur diesen einen Block – und das Ergebnis ist nicht ein einzelnes Pixel, sondern ein ganzer Block. Der Vorteil: Der VRAM-Verbrauch schrumpft direkt um einen festen Faktor, unabhängig vom Bildinhalt.
Der direkte Vergleich:
| PNG/JPG (traditionell) | GPU-native Formate | |
|---|---|---|
| Dateigröße | Klein (JPG besonders klein) | Mittel (Blockkomprimierung, feste Bitrate) |
| VRAM-Verbrauch | Groß (dekomprimiert zu Rohpixeln) | Klein (blockkomprimiert, dauerhaft) |
| Upload zu GPU | Langsam (CPU-Dekompression + großer Transfer) | Schnell (direkt übertragbar, keine Dekompression) |
| Sampling-Geschwindigkeit | Schnell (bereits Rohpixel) | Schnell (Hardware-Dekodierung in Echtzeit) |
GPU-Formate scheinen also die perfekte Lösung zu sein. Warum kann man sie dann nicht einfach direkt verwenden?
Das Problem: Verschiedene Geräte, verschiedene Formate
Genau hier liegt die größte Hürde bei GPU-Texturformaten – die Fragmentierung.
- Desktop-PCs verstehen BC1-7, aber kein ASTC
- Android-Smartphones verstehen ETC2/ASTC, aber meist kein BC
- iOS (ab A7) versteht ASTC, ältere Modelle nur PVRTC
- WebGPU/WebGL nutzen dieselbe Hardware-Fähigkeiten des jeweiligen Geräts
Wenn du eine Textur „auf allen Geräten im GPU-nativen Format“ haben willst, musst du für jede Plattform eine eigene Version erstellen. Ein Produkt für Desktop + Android + iOS bedeutet: Dieselbe Textur als BC-, ETC2- und ASTC-Version. Das verdreifacht die Paketgröße und den Arbeitsaufwand.
Noch schlimmer: Im Web weißt du vorher nicht, mit welchem Gerät der Nutzer deine Seite öffnet. Alle Formate vorab zu generieren ist unrealistisch, und eine Laufzeiterkennung kommt oft zu spät.
Basis Universal: Einmal kodieren, überall transkodieren
Basis Universal (kurz Basis) wurde genau für diese Fragmentierung entwickelt. Die Idee in einem Satz:
Die Textur wird zuerst in ein „Zwischenformat“ kodiert. Zur Laufzeit wird sie dann basierend auf den GPU-Fähigkeiten des aktuellen Geräts in das passende native Format transkodiert.
Der Transkodierungsprozess (schematisch):
Quelltextur (PNG/JPG)
│ Einmalige Offline-Kodierung (langsam, nur einmal)
▼
Basis-Zwischenformat (ETC1S oder UASTC)
│ Verpackt in KTX2-Container
▼
Veröffentlichung im Web ──┬── Desktop-GPU ──→ Laufzeit-Transkodierung → BC1/3/7
├── Android ───→ Laufzeit-Transkodierung → ETC2
└── iOS/VR ────→ Laufzeit-Transkodierung → ASTC
Die Kernpunkte:
- Die Offline-Kodierung passiert nur einmal und erzeugt eine kompakte Zwischenrepräsentation
- Die Laufzeit-Transkodierung ist extrem schnell (reine Berechnung, wenige Millisekunden) und arbeitet auf Blockebene – keine Pixel-für-Pixel-Dekompression nötig
- Nach der Transkodierung landet das echte GPU-native Format im VRAM – der VRAM-Verbrauch entspricht also dem der Blockkomprimierung
Basis bietet zwei Zwischenkodierungsmodi, die wir im nächsten Artikel genauer betrachten. Hier erstmal die Namen merken:
- ETC1S: Sehr hohe Kompressionsrate, ideal für Diffuse/Albedo-Farbkarten
- UASTC: Höhere Qualität, geeignet für Normal Maps und andere präzisionssensitive Texturen
KTX2: Der Standard-Container für GPU-Texturen
Damit bleibt noch ein technisches Problem: Wo kommen die kodierten Basis-Daten hin, wie werden sie markiert und wie mit glTF verknüpft? Die Antwort: KTX2.
KTX2 (Khronos Texture 2) ist kein weiteres Bildformat, sondern ein Containerformat – ähnlich wie eine .zip-Datei egal ist, ob darin Dokumente oder Bilder stecken. KTX2 verpackt GPU-Texturdaten (inklusive Basis-kodierter) in einer standardisierten Struktur und fügt Metadaten hinzu (Format, Mipmap-Ebenen, Farbraum usw.).
In glTF wird KTX2 über die Erweiterung KHR_texture_basisu integriert: Die Textur ist dann keine PNG-Datei mehr, sondern eine KTX2-Datei mit Basis-Kodierung. Beim Laden erkennt die Engine die Gerätefähigkeiten und transkodiert in das passende BC/ETC/ASTC-Format.
Die drei Begriffe sollten klar getrennt werden:
| Name | Rolle | Analogie |
|---|---|---|
| Basis Universal | Kodierungsschema (wie Texturen ins Zwischenformat kommen) | Ein „Kompressionsalgorithmus“ |
| KTX2 | Containerformat (wie die kodierten Daten verpackt werden) | Eine „Verpackungsbox“ |
| KHR_texture_basisu | glTF-Erweiterung (signalisiert der Engine: Das ist eine Basis-Textur) | Ein „Etikett“ |
Eine KTX2-Datei kann intern entweder Basis-Kodierung enthalten (plattformübergreifend) oder ein natives Format (z. B. direkt BC7). Im Web ist es in 99 % der Fälle Basis, denn wir wollen genau das Prinzip „einmal kodieren, überall transkodieren“.
VRAM-Beispiel: 4096-Textur im Vergleich
Kombinieren wir die Formel von vorhin mit den GPU-Formaten und schauen uns den realen Verbrauch einer 4096×4096-RGBA-Textur in verschiedenen Szenarien an:
| Szenario | Dateigröße | VRAM-Verbrauch (mit Mipmaps) | Upload-Geschwindigkeit | Plattformübergreifend |
|---|---|---|---|---|
| PNG | ~8 MB | ~87 MB | Langsam (Dekompression nötig) | ✅ |
| JPG | ~1,5 MB | ~87 MB | Langsam (Dekompression nötig) | ✅ |
| WebP | ~2 MB | ~87 MB | Langsam (Dekompression nötig) | ✅ |
| KTX2 (ETC1S) | ~2-3 MB | ~11-14 MB | Schnell | ✅ (Transkodierung) |
| KTX2 (UASTC) | ~6-8 MB | ~22 MB | Schnell | ✅ (Transkodierung) |
Wie entstehen die VRAM-Zahlen? GPU-Blockkomprimierung rechnet meist mit 4 bpp (Bits pro Pixel) oder 8 bpp. Eine 4096×4096-Textur ergibt bei 4 bpp etwa 8 MB, mit Mipmaps multipliziert mit 1,333 ≈ 11 MB. UASTC wird meist auf 8 bpp transkodiert, daher etwa 22 MB.
Wichtig sind nicht die exakten Zahlen einer Zeile, sondern diese beiden Punkte:
- Traditionelle Formate (PNG/JPG/WebP) haben fast identischen VRAM-Verbrauch – allesamt dekomprimierte Rohpixel, also 87 MB. Egal wie klein die Datei ist, der VRAM spart nichts.
- KTX2 reduziert den VRAM direkt auf 1/4 bis 1/8 – und die Dateigröße ist trotzdem nicht schlechter.
Genau deshalb ist KTX2 in VR- und Mobile-Web-Umgebungen quasi Pflicht – wie viele 87-MB-Texturen passen auf ein Smartphone mit 2 GB VRAM? Bei 11 MB wären es immerhin 7.
Plattform-Support-Matrix: Welche GPU kann was?
Auch wenn Basis die Details für uns verbirgt, hilft das Verständnis der zugrunde liegenden Zuordnung bei der Fehlersuche. Hier die Unterstützung nativer Formate auf aktuellen Geräten:
| Plattform / Gerät | BC1-7 | ETC2 | ASTC | PVRTC |
|---|---|---|---|---|
| Desktop-PC (D3D11/12, Vulkan, WebGPU) | ✅ | ❌ | Teilweise (neuere GPUs) | ❌ |
| macOS (Metal) | ✅ (neuere Modelle) | ❌ | ✅ | ❌ |
| Android (Mainstream) | ❌ | ✅ | ✅ | ❌ |
| iOS (A8+) | ❌ | ✅ | ✅ | ✅ (ältere Modelle) |
| WebGL 2 | Abhängig von Extensions | ✅ | Teilweise | ❌ |
| WebGPU | ✅ (Desktop) | ✅ | ✅ (je nach Gerät) | ❌ |
Basis erkennt diese Fähigkeiten zur Laufzeit und transkodiert dieselbe Zwischenkodierung in das jeweils passende Format. Genau das macht Basis im Web nahezu unersetzlich – du kannst die Geräte der Nutzer nicht vor der Veröffentlichung kennen.
Upload-Prozess im Vergleich: Traditionell vs. GPU-Format
Zum Abschluss halten wir den Unterschied in einem Flussdiagramm fest.
Traditionelles PNG/JPG:
PNG-Datei ──Download──> CPU-RAM ──CPU-Dekompression (langsam)──> RGBA-Pixelblock ──Upload (groß, langsam)──> VRAM (87 MB)
KTX2 + Basis:
KTX2-Datei ──Download──> CPU-RAM ──Laufzeit-Transkodierung (schnell)──> GPU-Blockformat ──Upload (klein, schnell)──> VRAM (11 MB)
Letzteres spart den großen Block „CPU-Pixel-für-Pixel-Dekompression“ komplett ein, und die zu übertragende Datenmenge ist um eine Größenordnung kleiner. Schnelleres Rendering des ersten Frames und weniger VRAM-Verbrauch – das ist der Kernnutzen dieser Lösung.
Nächste Schritte
Die Theorie steht. Im nächsten Artikel wird es praktisch: Wir komprimieren Texturen mit toktx und gltf-transform tatsächlich zu KTX2, laden sie in Three.js / Babylon.js und schauen uns an, wie man zwischen ETC1S und UASTC wählt und die Kompressionsparameter optimiert.