reinhard@finalmedia.de cafeface
Fri Jul 10 05:10:55 PM CEST 2026
Public Domain
fsplat liest auf stdin .fsplat files sowie auf descriptor 4 steuerungsbefehle (z.B. kamerapositionsbefehle) und gibt auf stdout fdraw befehle aus.
fsplat ist ein minimalistischer multi-threaded Software-Rasterizer, der mehrere .fsplat-Dateien über die Standardeingabe streamt und parallel auf CPU-Kernen verarbeitet.
Er benötigt somit keine GPU, sondern arbeitet komplett auf CPU, ist ohne externe Abhängigkeiten in C geschrieben und lässt sich gegen die musl libc statisch gelinkt als 76kb große Binary bauen. Ein ideales Werkzeug zum Experimentieren mit Gaussian Splats unter Beibehaltung der Unix-Philosophie.
Das Tool liest dabei von stdin die Szenendaten im fsplat Format ein. Auf stdin kommen also bei Bedarf mehrere konkatentierte und vorsortierte fsplat Szenen zum Einsatz, man kann also eine sehr große Szenerie einfach nacheinander streamen und zusammensetzen. Die Reihenfolge ist dabei relevant!
Auf Descriptor 4 werden die Kameraposition, Winkel, Zoom etc. als reine ASCII Befehlszeilen übertragen. Das können statische Zeilen sein, dynamisch erzeugte, von Mausposition abhängig oder als Animation.
export SCREEN_WIDTH=120 SCREEN_HEIGHT=80 zcat szene1.fsplat.gz szene2.fsplat.gz | ./fsplat 4< <(./kameraflug.awk) | fdrawterm
Jeder Thread innerhalb von fsplat führt dann die rechenintensive SH-Evaluierung, das Frustum Culling und den schnellen Bit-Radix-Sort vollkommen unabhängig auf seinen eigenen CPU-Kernen aus.
Jede Szene bringt dabei ihr eigenes Codebook mit, mit jeweils bis zu 256 Formeln für die SH Pixelfarbe. (oder auch weniger bei Bedarf) Wir benötigen also auch nur 30 bytes pro Splat statt 32 bytes, da die Farbinformationen nur im SH Codebook referenziert vorliegt und dabei nur ein Byte Speicherplatz pro Splat benötigt. Details dazu in den FAQ auf dieser Seite.
Konzept: Der Einlese-Algorithmus befüllt nacheinander die isolierten Slots in scenes. Sobald die Kamera-Steuerung über Descriptor 4 rendert, werden alle Worker parallel angestoßen und anschließend sequentiell in den Puffer geblendet. Der Performance-Vorteile dieser Architektur ist klar: Es gibt kein Thread-Overhead beim Blending: Da wir im Hauptthread sequentiell blenden, entfallen komplexe Locking-Mechanismen (pthread_mutex_t), Atomics oder Race Conditions auf dem globalen Framebuffer komplett. Zudem haben wir ein optimiertes Speicher-Layout. Da sh_idx im RAM ein schlankes 1-Byte-Feld bleibt, profitiert das s-Array von maximaler Cache-Dichte während der Transformation im Thread.
Steuerungbefehle werden auf Descriptor 4 zeilenweise übergeben. Man kann P für die Kamerapositionen übergeben (nach jedem Absetzen eines P Befehls, wird auch das Frame neu gerendert) oder man übergibt einen O Befehl mit Offset-Koordinaten. Dann werden alle, ab diesem Zeitpunkt auf stdin einströmenden Daten mit diesem Offset kalkuliert.
Die Ausgabe erfolgt auf stdout als fdraw Befehle. Du kannst das also in beliebigen Tools weiterverarbeiten, zwischenspeichern oder analysieren.
Zur Ansicht kannst du z.B. fdraw, fdrawterm oder fbdraw nutzen. In den Demos empfehle ich fdrawterm, da es dir auf dem Terminal eine schöne und schnelle Vorschau gibt, die du auch remote via ssh nutzen kannst! Du findest es hier
export SCREEN_WIDTH=120 SCREEN_HEIGHT=80 zcat szene.fsplat.gz | ./fsplat 4< <(./kamera_animation.sh) | fdrawterm
und wild gemische Szenen nur zu Demostrationszwecken
export SCREEN_WIDTH=120 SCREEN_HEIGHT=80 zcat szene.fsplat.gz train.fsplat.gz | ./fsplat 4< <(./kamera_animation.sh) | fdrawterm
export SCREEN_WIDTH=640 SCREEN_HEIGHT=480
zcat original.splat.gz | ./splat2fsplat | ./fsplat 4< <(./kamera_animation.sh) | fdraw | ffplay -loglevel 8 -f rawvideo -pixel_format rgba -video_size ${SCREEN_WIDTH}x${SCREEN_HEIGHT} -framerate 30 -i -
Hier noch meine Beispielszene glas.fsplat als Rendering mit fsplat, fdraw und ffmpeg
export SCREEN_WIDTH=640 SCREEN_HEIGHT=480
zcat glas.fsplat.gz | ./fsplat 4< <(echo P 4 -4 -2 180 35 80) | fdraw | ffmpeg -loglevel 8 -f rawvideo -pixel_format rgba -video_size ${SCREEN_WIDTH}x${SCREEN_HEIGHT} -framerate 30 -i - glas%d.png
Und diese Szene kombiniert mit dem bereits passend skalierten chair.fsplat.gz als Miniaturobjekt. (Dank an Alfredo M - quinszouls für das Modell)
export SCREEN_WIDTH=120 SCREEN_HEIGHT=80
zcat chair.fsplat.gz glas.fsplat.gz | ./fsplat 4< glasline | fdrawterm
export SCREEN_WIDTH=640 SCREEN_HEIGHT=480
zcat chair.fsplat.gz glas.fsplat.gz | ./fsplat 4< <(echo P 4 -4 -2 180 35 80) | fdraw | ffmpeg -loglevel 8 -f rawvideo -pixel_format rgba -video_size ${SCREEN_WIDTH}x${SCREEN_HEIGHT} -framerate 30 -i - combined%d.png
Tipps: Wenn nach dem Laden von chair.fsplat im descriptor 4 ein passenden O Befehl mit Offset angegeben werden wird, bevor die glas.fsplat Szene geladen wird, kann der Stuhl im Raum, bzw. der Raum zum Stuhl, verschoben werden.
Wird die Reihenfolge chair.fsplat.gz und glas.fsplat.gz vertauscht, ist der Stuhl nicht mehr sichtbar, da er überzeichnet wird.
Zu beachten ist natürlich, dass jede .fsplat datei eine eigene SH-Tabelle aufweist.
Hier noch als Video gerechnet mit kleiner Kameraanimation
export SCREEN_WIDTH=320 SCREEN_HEIGHT=240
zcat chair.fsplat.gz glas.fsplat.gz | ./fsplat 4< glasline | fdraw | ffmpeg -loglevel 8 -f rawvideo -pixel_format rgba -video_size ${SCREEN_WIDTH}x${SCREEN_HEIGHT} -framerate 300 -i - -y glas_chair_combined.mp4
export SCREEN_WIDTH=640 SCREEN_HEIGHT=480
test ! -e /dev/shm/controlpipe && mkfifo /dev/shm/controlpipe
zcat szene.fsplat.gz | ./fsplat 4< /dev/shm/controlpipe | fdraw | ffplay -loglevel 8 -f rawvideo -pixel_format rgba -video_size ${SCREEN_WIDTH}x${SCREEN_HEIGHT} -framerate 30 -i - &
./flycontrol.sh > /dev/shm/controlpipe
export SCREEN_WIDTH=80 SCREEN_HEIGHT=50 curl -Ls https://finalmedia.de/code/fsplat/szene.fsplat.gz | zcat | ./fsplat 4< <(curl -Ls https://finalmedia.de/code/fsplat/camlines) | fdrawterm
# Empfehlungen zum Erkunden einer Szene export SCREEN_WIDTH=120 SCREEN_HEIGHT=80 zcat szene.fsplat.gz | ./fsplat 4< <(./kameraflug.awk | pv -qlL 25) | fdrawterm zcat bicycle.fsplat.gz | ./fsplat 4< <(./kameraflug.awk) | fdrawterm
Test auf einem sehr langsamen Celeron CPU J1900 bei 2GhZ. Kein AVX, InOrder. Ganz simple embedded CPU. Ich messe dazu mittels tai64n letztlich die vergangengen Nanosekunden zwischen den erzeuten frames. bei jedem neuen Frame geben wir ja einen . in stderr aus. Damit kann man schon den Verlauf und das Delta erkennen. Ich messe die vollständige Kette mit niedriger und höherer Auflösung.
export SCREEN_WIDTH=120 SCREEN_HEIGHT=80; zcat szene.fsplat.gz | ./fsplat 4< <(./kamera_animation.sh) 2>&1 >/dev/null | tai64n | tai64nlocal 2026-07-02 18:37:13.755091500 SCREEN: 120x80 2026-07-02 18:37:14.442751500 1065994 Splats geladen. 2026-07-02 18:37:14.624989500 . 2026-07-02 18:37:14.800078500 . 2026-07-02 18:37:14.971646500 . 2026-07-02 18:37:15.145416500 . 2026-07-02 18:37:15.314967500 . 2026-07-02 18:37:15.486919500 . 2026-07-02 18:37:15.659503500 . 2026-07-02 18:37:15.832940500 . 2026-07-02 18:37:16.006537500 . 2026-07-02 18:37:16.181396500 . 2026-07-02 18:37:16.357425500 . 2026-07-02 18:37:16.534471500 . 2026-07-02 18:37:16.713569500 . 2026-07-02 18:37:16.895258500 . 2026-07-02 18:37:17.077460500 . 2026-07-02 18:37:17.259299500 . 2026-07-02 18:37:17.442589500 . 2026-07-02 18:37:17.624338500 . ^C export SCREEN_WIDTH=640 SCREEN_HEIGHT=480; zcat szene.fsplat.gz | ./fsplat 4< <(./kamera_animation.sh) 2>&1 >/dev/null | tai64n | tai64nlocal 2026-07-02 18:37:27.041403500 SCREEN: 640x480 2026-07-02 18:37:27.757438500 1065994 Splats geladen. 2026-07-02 18:37:30.051540500 . 2026-07-02 18:37:32.338945500 . 2026-07-02 18:37:34.637367500 . 2026-07-02 18:37:36.940418500 . 2026-07-02 18:37:39.270503500 . 2026-07-02 18:37:41.602458500 . 2026-07-02 18:37:43.929949500 .
Bei VGA (640x480) rendert das Programm auf dem Testsystem konstant in rund 2,29 Sekunden pro Frame (30.051 -> 32.338 -> 34.637 -> 36.940).
Bei der Vorschau (120x80) liegt der Durchlauf auf dem Testsystem konstant bei rund 172 Millisekunden pro Frame (14.624 -> 14.800 -> 14.971 -> 15.145). Das System läuft also mit 5,8 FPS.
Auf aktuellen modernen CPUs ist da natürlich sehr viel mehr drin. Es zeigt nur, wie gut das Verfahren auch auf einer so schwachbrüstigen CPU und ohne jede GPU Unterstützung noch performed!
Wenn du das pv tool installiert hast, kannst du auch die FPS messen:
export SCREEN_WIDTH=100 SCREEN_HEIGHT=50; zcat szene.fsplat.gz | ./fsplat 4< <(./kamera_animation.sh) 2>&1 >/dev/null | pv -rlX
Entwicklung im djb stil. kein libc, kein printf etc. minimale abhängigkeiten
Statt qsort der Originalimplementierung ein radixsort.
Kein malloc, kein calloc, also keine dynamische Speicher-Allokation, damit auch kein Heap-Overhead.
Die Allokation beim Systemstart ist somit vollständig statisch. Es gibt keine Speicherfragmentierung und keine Betriebssystem-Aufrufe während des Renderings.
Keine Speicher-Leaks: Da kein Puffer dynamisch erzeugt wird, kann zur Laufzeit physisch kein Memoryleak entstehen und der Stack bleibt sauber.
Optimierte Cache-Lokalität: Die CPU verarbeitet die Arrays im BSS-Segment in exakt der Reihenfolge, in der sie im RAM liegen. Der Hardware-Prefetcher des hiesigen Demo Celerons kann die Datenströme ohne jede Latenz in die Ausführungseinheiten füttern.
Wir legen Puffer für jede maximal mögliche Szene fest (z.B. ein Limit von 1.500.000 Splats pro Szene und eine maximale Bildschirmauflösung von 1920x1080). Diese Limits sind im Quellcode einfach anpassbar!
Im Beispiel mit der musl-libc gebaut, statisch gelinked.
Beim Compilieren ist zwingend -mcmodel=large zu verwenden, da wir für sehr große Szenen auch 128GB RAM etc. mehr verwenden werden.
Erstellen einer netpbm Grafik aus den fdraw Befehlen. Mit Standardtools.
(echo P3 640 480 255; export SCREEN_WIDTH=640 SCREEN_HEIGHT=480; zcat szene.fsplat.gz | ./fsplat 4< <(head -n 1 camlines) | grep p | sort -V -k2,3 | cut -d " " -f4- | tr -dc "0-9 \n" | tr "\n" " ") > demo.ppm
Hier ein CPU Performancetest für deine Steamdeck. Es werden keine GPU Beschleunigung verwendet, wir nutzen nur die ZEN2 CPU und die Grafikausgabe auf dem Terminal:
Öffne auf der SteamDeck im Desktop Modus mittels STRG+ALT+T ein Terminal und vergößere es auf Vollbild mittels der Taste F11. Dann:
curl -s https://finalmedia.de/code/fsplat/steamdeck.txt | sh
Vermutlich hast du den Descriptor 4 nicht übergeben. Dann beendet sich das tool, da es ja keine Kameraposition bekommt, die es rendern soll.
Was du zum schnellen Testen tun kannst, ist ein
... | fsplat 4< /proc/self/status | ...zu verwenden.
Der Trick funktioniert, da grep ^P /proc/self/status immer ein Pid und PPid liefert, wenngleich nicht mit gültigen Kamerapositionen, aber es erzwingt ein Framerendering.
Wenn du partout keine named pipe nutzen willst, wie in den Beispielen vorgesehen, kannst du auch einen ewiger hook an descriptor 4 binden, in den du dann später on demand Daten injectest. Konkret
... | fsplat 4< <(tail -f /dev/null) | ...und dann in einem separaten Terminal in den descriptor 4 die Kamerapositionen injecten, nachdem du die PID des Prozesses herausgefunden hast. Konkret also:
curl -Ls finalmedia.de/code/fsplat/camlines > /proc/$(pgrep fsplat)/fd/4
Mach dir welche. Du findest online diverse weitere Demo-Szenen hiltree, etc. Und du kannst mit den gängigen Tools auch Szenen exportieren. Oft auch mit Apps deines Smartphones etc. Oben findest du meine Konvertierungs-Tools, um aus einer .ply oder .splat Datei eine .fsplat Datei zu erzeugen: ply2fsplat und splat2fsplat.
Diese arbeiten als Filter und man kann damit einfach bestehende splat oder ply Szenen nach fsplat umwandeln. Da das original splat ohnehin kein SH hat, gibt das kein Mehrgewinn. Aber ply hat SH Support und da bietet sich dann das fsplat Format an. Beispielnutzung:
zcat erbsen.ply.gz | ply2fsplat | gzip -9 erbsen.fsplat.gz zcat linsen.splat.gz | splat2fsplat | gzip -9 linsen.fsplat.gz splat2fsplat < bohnen.splat | gzip -9 bohnen.fsplat.gz splat2fsplat < import.splat > export.fsplat
Und weil es filter sind, muss man auch keine Dateien zwischenspeichern. Man kann direkt
zcat linsen.splat.gz | splat2fsplat | fsplat 4< ....als Kette nutzen
Beachte: Im Quellcode sind Limits definiert für max Splat sizes etc. Die musst du ggf. anheben. Die Limits sind sehr konservativ gesetzt. Zudem habe ich in allen Tools bewusst auf malloc und calloc etc. verzichtet, damit Verifizierbarkeit und Vorhersagbarkeit der Speicherbelastung bei der Anwendung erhalten bleiben.
Anstatt eine 3D-Szene mühsam aus Millionen von Dreiecken (Polygonen) oder starren Blöcken (Voxeln) aufzubauen, repräsentiert Gaussian Splatting die Welt durch eine riesige Wolke aus dreidimensionalen, weichen "Gauß-Glocken" (3D Gaussians).
Jeder einzelne Punkt (Splat) besitzt:Beim Rendern wird jede dieser weichen Kugeln flach auf den 2D-Bildschirm projiziert (das namensgebende Splatting, ähnlich wie ein Schneeball, der an einer Wand zerplatzt) und erzeugt einen sanften Farbverlauf von innen nach außen.
Mit einer klassischen Kamera fertigst du z.B. tausende Fotos von Objekten an. Diese werden analysiert daraus die Punktwolken gerechnet. Manchmal nimmt man auch lidar zur Hilfe, rechnet aus Videos und Bewegung und zieht auch KI hinzu. Letztlich werden daraus nun die Splats gerechnet, also durch Pruning all das entfernt, was übereinander liegt und man eh nicht sehen kann. Die Splats werden so im Raum angeordnet, gedreht, überlappt etc, dass eine fotorealisistische Szene entsteht, die sich aus jedem Blickwinkel betrachten lässt. Also so wird die Szenerie Schritt für Schritt aufgeräumt und das Ergebnis dann sortiert. Zudem wird erkannt, ob es spiegelnde, transparente oder opake Oberflächen gibt, irisierende Farben wie bei Schmetterlingsflügel, Seifenblasen, Ölfilm oder Prachtkäfern, queteleschen Ringen. Man kann also je nach Bedarf mehr oder weniger nötige auch Information über Sphärische Harmonische (SH) sichern. Und das bringt besondere Realitätsnähe. In diesem Fall blickwinkelabhängige Farbeffekte (Anisotropie), Inferenzen, Beugung, Lichtstreckung. Gerade SH dritter Ordnung bringen diese Möglichkeiten der Darstellung:
Die 0. Ordnung (ein einzelner Koeffizient) speichert nur eine feste, richtungsunabhängige Grundfarbe (diffuse Reflexion). Erst die 3. Ordnung liefert insgesamt 16 Koeffizienten pro Farbkanal (insgesamt 48 Parameter). Diese mathematischen Schwingungen auf der Kugeloberfläche erlauben es dem Algorithmus, komplexere hochfrequente Helligkeits- und Farbänderungen exakt abzubilden, wenn sich der Blickwinkel der Kamera verändert. Sphärische Harmonische (SH) funktionieren wie eine Fourier-Transformation, jedoch nicht auf einer flachen Linie, sondern auf der Oberfläche einer Kugel.
Ja. Die Wahl der SH-Ordnung ist der wichtigste Hebel für die finale Dateigröße eines Gaussian Splatting Modells. Da eine Szene oft aus Millionen von einzelnen Gaussians besteht, multipliziert sich jeder einzelne Parameter millionenfach.
Ein einzelner 3D-Gaussian Splat benötigt für seine geometrischen Eigenschaften (Position, Skalierung, Rotation, Opazität) standardmäßig 14 Parameter. Schauen wir uns an, wie die SH-Koeffizienten die Gesamtgröße pro Gaussian verändern (unter Annahme von Standard-32-Bit-Fließkommazahlen / 4 Byte pro Parameter):
SH-Ordnung SH-Parameter (RGB) Geometrie-Parameter Gesamtparameter Bytes pro Gaussian Splat Ordnung 0 3 14 17 68 Bytes Ordnung 1 3 + 9 = 12 14 26 104 Bytes Ordnung 2 12 + 15 = 27 14 41 164 Bytes Ordnung 3 27 + 21 = 48 14 62 248 Bytes
Das Praxisbeispiel (Szene mit 5 Millionen Gaussians): Mit Ordnung 0 (keine winkelabhängige Farbe) wiegt die unkomprimierte Datei ca. 340 MB. Mit Ordnung 3 (für Irisieren und Glanz) wiegt dieselbe Szene stolze 1,24 GB.
Der Sprung zur dritten Ordnung verdreifacht also die benötigten Farbparameter im Vergleich zur 2. Ordnung fast. Deshalb nutzen viele Echtzeit-Anwendungen in der Praxis Kompressionsverfahren (wie Quantisierung) oder stoppen standardmäßig bei Ordnung 1 oder 2, außer die Szene lebt explizit von komplexen Lichteffekten.
KISS Prinzip und weil es hier um die Darstellung geht. Eine .splat Datei, also das von Kevoin Kwok entwickelte Original-Format, ist bereits ein sehr brauchbares Format. Es enthält keine Header und keine Metadaten. Die Datei besteht ausschließlich aus einer fortlaufenden Kette von 32 Byte Blöcken (ein Block pro Gauß Splat). Vorsortiert und Optimiert. Sie ist also sehr gut und technisch einfach zu verarbeiten.
Die Daten selbst sind verlustbehaftet gespeichert. quantisiert. Die Datei hat exakt die Struktur, die auch eine GPU erwartet. Es gibt kein Entpacken und kein Parsen. Die Bytes können direkt im Grafikspeicher landen. Eine .splat Datei ist im Grund also ein reines, lineares Binär-Array. Die Daten sind exakt so angeordnet, wie man sie braucht: Für das korrekte Rendering von transparenten Objekten müssen Splats von hinten nach vorne (bezogen auf die Kamera) gezeichnet werden. Die Daten in .splat files sind daher oft vorsortiert nach Cluster-Logik oder Wichtigkeit, damit bereits die wichtigsten Strukturen angezeigt werden können, während der Rest noch läd.
Anstattt 32bit floats für jeden Wert zu nutzen, werden die Daten komprimiert. Spherical Harmonics werden dabei komplett entfernt oder auf die Basis-Farbe nullter Ordnung reduziert. Die verbleibende RGB-Farbe wird in ein kompaktes 8-Bit Format (uint8) umgewandelt. Rotation und Skalierung werden ebenfalls in kleinere Ganzzahlformate (8bit oder 16bit) gequetscht, da das mesnchliche Auge minimale Abweichungen bei der Ellipsoid Form gar nicht wahrnimmt.
Eine .splat file ist im Grunde also das, was C-Entwickler unter einem Raw Binary Dump verstehen. Keine komplexen Header, keine Kompression. Rein Wiederholung desselben Datenblocks. Die Komprimierung der gesamten Datei macht man vorher, also gzip der gesamten Datei, .splat.gz und kann sie daher auch separat prozessieren. Praktisch ist das ebenso ideal beim Ausliefern statischer Files, da ja browser und webserver .gz files im Hintergrund transparent verwenden können.
keiner
Offset Datentyp Beschreibung 0-11 float[3] Position (x,y,z) je 4 Bytes standard IEEE 754 float 12-23 float[3] Skalierung sx,sy,sz des Ellipsoids je 4 bytes 24-27 uint8_t[4] Farbe und Opazität rgba 8bit Farbwerte 0-255 28-31 uint8_t[4] Rotation Quaternion qx,qy,qz,qw
Das einzige Manko einer original splat file ist also: Es fehlen die SH. Nur die Grundfarbe des Objekst ist als RGB Wert gespeichert, daher kann keine blickwinkelabhängige Farbe beim Rendering berücksichtig werden... und gerade diese blickwinkelabhängigkeit schafft ja normalerweise bei Gaussian Splatting erst die Realitätsnähe (Spiegelungen im Glas, irisierende Effekte etc).
Generell bietet das originale splat Format jedoch bereits perfekte Streaming-Eigenschaften und Unix-Pipeline-Tauglichkeit. Da die Punkte unstrukturiert sind - sprich, kein Punkt muss wissen, wer sein Nachbar ist (im Gegensatz zu klassischer 3D Polygon-Grafik aus Dreiecken) - kann man die Datei als reinen Datenstrom betrachten. Man kann sie von vorne nach hinten lesen, mitten im Stream abbrechen oder beliebig umsortieren, ohne dass die Pipeline kollabiert. Das macht es ideal für den Streaming-Ansatz in reinem C.
Aus diesem Grund setzt meine Eigenentwicklung fsplat als Dateiformat auch auf dem splat Format auf. Die 3 Farb-Byte RGB pro Splat habe ich dabei jedoch komplett entfernt und durch ein einziges Byte (sh_idx) ersetzt. Die echten 48 SH Werte für die Farben liegen jetzt ausschließlich ganz am Anfang im Codebook im Dateiheader. Wenn wir nun eine .splat Datei nur die Grundfarbe der 0. Ordnung hat) nach .fsplat konvertieren, setzen wir im Codebook die restlichen 45 Koeffizienten (die Ordnungen 1,2 und 3) auf exakt 0.0f.
Der eigentliche Datenteil ist damit nun schlanker, und die Datei wird kleiner, erhält aber weiterhin die relevante Information. Damit die 0.0f Bytes aus der Datei exakt an die richtige Stelle im RAM kommen, muss das Struct im C-Renderer zwingend ein festes Array aus 48 Floats (192 Bytes) sein.
Ich habe die Datenstruktur also einfach ergänzt um einen 1 Byte Index für einen SH-Codebook-Lookup (Vektorquantisierung) und der Datei einen header mit dem codebook zugeteilt. Die fsplat Datei sieht also im Gegensatz zu einer splat Datei nun so aus und ist auch deutlich kompakter, dennoch bleibt die Komplexität überschaubar:
Offset Datentyp Beschreibung 0-3 uint8 Anzahl Muster N: Gibt an, wieviele Codebook Einträge folgen (standard 256) 4-49155 float[256][48] SH-Codebook 256x48 IEEE 754 Floats (insgesamt 49.152 Bytes). Enthält je 16 Koeffizienten für den r, g und b Kanal. Bei Konvertierung aus .splat sind die Koeffizienten 1-15 pro Kanel mit 0.0f Nullbytes gefüllt. Was sich auch sehr gut komprimieren lässt. 49156 - 49159 Anzahl Splats M: Gibt an, wieviele FSplat Datenblöcke im Anschluss folgen
0-11 float[3] Position (x,y,z) je 4 Bytes standard IEEE 754 float 12-23 float[3] Skalierung sx,sy,sz des Ellipsoids je 4 bytes 24 uint8 Transparenz/Alpha: Der Dichtewert des Splat 0 bis 255 25-28 uint8_t[4] Rotation Quaternion qx,qy,qz,qw 29 uint8 Muster-Index sh_idx der in das FSplat SH-Wörterbuch 8bit (0-255) auf das zugehörige Lichtmuster im Header-Codebook. (statt direkte Angabe von RGB-Bytes)
Nun, die Idee ist Folgende: Bei einer Szene gibt es für unterschiedliche Splats häufig ähnlich Änderungen von Farbwechseln aus unterschiedlichem Blickwinkel. Man kann also im Vorfeld bereits die komplette Szene auf deren Vorkommen analysieren und diese Vorkommen in einer Formel abbilden. Dann sammelt man nun diese Formeln in einem Codebook, nicht mehr die einzelnen Farben. In unserem Fall hat das Codebook 255 mögliche Slots für unterschiedliche Formeln. Und statt der vollständigen SH Informationen speichern wir nun zu jedem Splat nicht mehr die Farbe, sondern nur dieses eine Byte, dass auf die entsprechende Formel im Codebook verweist. Dann kann der Renderer diese Formeln beim Start passend zur Szenerie bereits vorberechnen und hat dann zur Laufzeit eine sehr schnelle Lookup-Table, um die passende Farbe zu finden. Wiederum je nach Blickwinkel. Für die meisten Szenen funktioniert das hinreichend. Eine Szene müsste extrem komplexe Unterschiede aufweisen, damit wir mehr als 255 Formeln bräuchten. Man muss also pro Splat nur 48 Einzelwerte zu je 3 Byte speichern, sondern man braucht nur insgesamt ein Byte mehr pro Splat.
Das Tool fsplat braucht also als Eingabeformat auch fsplat files.
Daher gibt es ja auch meine zwei Converter Tools, die die Farbvorkommen in der Szenerie auch analysieren.
Generell wäre es auch möglich, wiederum weitere Codebooks zu hinterlegen, dann brauchen wir aber pro splat ein weiteres byte, um auch das codebook zu referenzieren. Das ist aber nicht nötig. Es ist dann der elegantere Ansatz, mehrere szenenfiles zu erzeugen und im renderer pro szenenfile ein neues codebook und auch andere speicherbereiche anzusprechen, wenn man szenen über threads verteilt. Das ist hier so realisiert. Mehrere oder größere Codebooks müssen also nicht Bestandteil des .fsplat files sein. Grund: Im header der splatfiles habe ich auch die Anzahl der in der Szene enthaltenen splats definiert. Das hat den Vorteil, dass man mehrere Szenenfiles nun konkatenieren kann und damit ja auch der Beginn einer neuen Szene und neue Codebücher erkennbar bleibt, weil wir wissen, wann eine Szene "endet". Mehrere Szenenfiles werden also sequentiell gelesen und jede bringt bei Bedarf wieder eigene codebücher mit. Und alle wirken zusammen in einem gemeinsamen virtuellen 3D Raum den der Renderer dafür aufbaut. Soweit das Konzept. Man könnte Szenen also einfach nach belieben aufteilen, ohne Chunks o.ä, durch geschicktes vorsortieren der eigentlichen szenen files, vor dem konkatenieren. Dennoch bleibt alles streambar und extern komprimierbar.
Desweiteren ist 256 ja nur das obere Ende. Du kannst auch einige kleinere Szenen erstellen, die jeweils nur 64 Slots belegen etc. Es wirkt ja alles auf den gemeinsamen virtuellen 3D Raum
Damit verlagere ich auch sinnvolle Aufgaben zum Preprocessing. Mittel separater Skripte und Tools kann man also im Vorfeld Szenen erst einmal alle analysieren, aufteilen, zerteilen, die jeweils sinnvollste SH Tabellen erkennen und in separaten .fsplat-files hinterlegen. Da es einzelne Files sind, kann man sie auch einzeln prüfen, betrachten etc, oder eben in Summe konkateniert und als stream auswerten, erst on demand laden etc.
Mir ist kein Format bekannt, dass die gleiche Einfachheit wie splat beibehält, dennoch als Ganzes mit beliebigen Komprimierungstools packbar bleibt und dennoch SH abbilden kann. Daher das fsplat Format.
Folgende Formate sind geläufig:
.ply ist eigentlich ein klassisches Datenformat für 3D Punktwohlen. Im Kontext von Gaussian Splatting speichert es jeden Splat als einen Punkt mit einer Vielzahl von Attributen: Position (x,y,z) also das Zentrum des Ellipsois im 3D Raum. Die Skalierung (Sx,Sy,Sz), also die Ausdehnung des Splats in drei Richtungen und meist als Log-Skala gespeichert. Die Rotation (qw,qx,qz,qz), also ein Quaternion, das die Ausrichtung des Ellipsoids im Raum beschreibt, die Opazität alpha, was als Dichte oder Durchsichtigkeit des Splats vor der Aktivierungsfunktion zu verstehen ist, sowie die Spherical Harmonics (SH-Koeffinzienten), ein Satz von meist 3 bis zu 45 Werten pro RGB Farbkanel. Diese mathematischen Koeffizienten sorgen dafür, dass sich ie Farbe des Splats je nach Blickwinkel des Betrachters ändert. Für photorealisistische Spiegelungen, Lichteffekte und ist das essentiell. Vorteil des Formats ist, dass es keine Informationsverluste gibt. Um die Originalszene also zu speichern ist das ein gutes Format. Die Datein sind dann aber riesig und für ein Streaming ungeeignet.
.pop (Popcorn Splats) Progressive Octree Points ist ein Ansatz hierarchischer Speicherung. Die Splats werden in einer Baumstruktur (Octree) organisiert. Vorteil ist, dass es für sehr große Szenen gut geeinet iest. Wenn die Kamera weit weg ist, werden nur wenige Splats gerendert, Kommt man näher, werden die restlichen für diesen spezifischen Bereich geladen. Das spart ernom Grafikspeicher. Die Daten sind nicht linear gespeichert, sondern in einer Baumstruktur. Der Datei-Header enthält Zeiger oder Indizes auf verschienende Knoten im 3D Raum. Jeder Knoten enthält eine Teilmenge der Splats für ein bestimmtes Detaillevel (LOD). Das reine Einlesen der Datei in dern Speicher ist zwar mit wenigen fread Befehlen getan, aber die Verarbeitung ist komplex. Die Baumstruktur muss im Speicher verwaltet werden, Pointer Arithmetik für die Child-Nodes und zur Laufzeit muss algorithmisch entschieden werden, welche Äste des Baums gerade geladen oder gezeichnet werden müssen. Komplexer Code. Komplexität ist der Feind.
.gltg mit .glb ist ein Standard der Khronos Group. Aber er ist komplex. gltf basiert auf JSON für die Szenenstruktur, und binäre Puffer für die eigentlichen Geometriedaten. Man benötigt einen JSON Partner, muss verschachtelte Logik von Szenen, Nodes, Meshes etc. auflösen, um an die eigentlichen Splat Rohdaten zu kommen. Das bringt bloat und Abhängigkeit - und Komplexität ist der Freind.
.spz ist SplatZip von Niantic Scaniverse und sehr komplex aber auch stark komprimiert. Im Gegensatz zu .splat files liegen die Daten nicht als Kette von Blöcken vor, stattdessen nutzt es ein planare, kanalorientiertes layout. Alle X Koordinaten liegen hintereinander, alle Rotationen hintereinander, alle Farben hintereinander. Positionen werden auf 24 bit Ganzzahlen heruntergefbrochem Rotationen in ein normalisiertes Quaternion gequetscht und SH werden komprimiert. Die neuste Version des Formats (spz4) teilt die Kanäle auch in sechs unabhängige Streams auf, die parallel mit zstd kompromiert sind. Vorab liefert ein kleiner Datei-Header (als Table of Contents) die Offsets für diese Streams. Man braucht also zwingend einen ZSTD Dekomprimierungsroutine um die Daten im RAM zu entpacken und kann dafür nicht einfach ein bestehendes Entpacker-Programm in der pipeline vorwegschalten. Mittels Bit-Shifting werden die quantisierten Bit-Weerte (die 24 Bit Ganzzahlen) wieder in Standard 32 Bit Floats umgerechnet, damit die GPU was damit anfangen kann.
.ksplat ist darauf optimiert, dass Szenen bereits während des Downloads progressiv gerendert werden können. Es kommt ein JSON artiger oder binärer header zum Einsatz, der die Metadaten zur Szene enthält. Dann werden die Splats in räumliche Cluster (blöcke) aufgeteilt und anstatt absoluter 3D Koordinaten, speichert der Block Header eine Basis-Position im Raum. Die einzelnen Splats innerhalb des Blocks speichern nur noch ein kleineres komprimiertes Vektor-Offset, also realtiv zur Basis-Position. Für die Rotationen wird ein cleverer Trick genutzt, man muss die größte Komponenten nicht speichern, da sie berechnet werden kann. Es werden nur die 3 kleinsten Komponenten in einem extrem dichten Bitfeld (10+10+10 Bits+2 Bits für den Index) abgelegt. Das Format ist zwar stark komprimiert und platzsparende, aber alles in allem verlangt das eine Menge algorithmischer Logik auf der CPU. Header parsen, Chunks durchlaufen, relative Positionierung mathematisch auflösen, das Entpacken der Rotationen erfordert komplexe Bit Maskierungen und das ales macht es fehleranfällig und komplex. Und Komplexität ist der Feind.
.gsplat nutzt moderne Quantisierung und Clustering. Es behält reduzierte Spherical Harmonics (Vektortabellen) bei, berechnet statt Rotation/Skalierung oft direkt die 3D-Kovarianzmatrizen im Voraus und besitzt einen dedizierten Header zur Konfiguration der Streams. Es kommt dem Ansatz schon sehr nahe, definiert aber 2 bytes für einen SH-Index. Das gsplat-Format verlagert also diese Berechnung vom Shader in den Export-Prozess. Es speichert die resultierende Matrix direkt ab und muss nur 6 statt 9 Kovarianzwerte im Datenstrom unterbringen. Problem dabei: Durch die Vorausberechnung ist auch das nachträgliche verändern der Splats erschwert, wenn nicht gleich unmöglich. Beim Rendering kommt die Zwicker Projektion zum Einsatz. Zudem ist der Header deutlich aufwendiger konstruiert.
.sog (Spatially Ordered Gaussians) ist ein hochentwickeltes, extrem komprimiertes Open-Source-Format, das ursprünglich auf einer Forschungsarbeit des Fraunhofer HHI (ECCV 2024) basiert und von PlayCanvas für das Web standardisiert wurde. Es transformiert 3D-Geometriedaten in 2D-Bildtexturen (meistens WebP). Dadurch können moderne Bildkompressions-Algorithmen genutzt werden, was zu einer 95-prozentigen Reduktion der Dateigröße (bis um den Faktor 20 kleiner als ply) führt. sog sortiert alle Gaussians im Raum nach einer raumfüllenden Kurve (Morton-Code / Z-Kurve) oder einem 2D-Gitter, sodass 3D-Nachbarn auch in der Datei nebeneinander liegen. Diese sortierten Werte werden dann pixelweise in 2D-Bildkanäle codiert. Jedes Pixel (x, y) in den Texturen beschreibt einen Gaussian Splat. Eine .sog-Datei ist im Wesentlichen ein komprimiertes ZIP-Archiv, bestehend aus einer meta.json, means_l.webp, means_u.webp, quats.webp, scales.webp, sh0.webp, etc. also mehrerer aufeinander abgestimmten verlustfreien WebP-Bildern besteht. Die Struktur ist somit viel zu aufwendig und nichts für einen einfachen Parser.
Mein Ansatz ist daher die Datenhaltung in .ply Files, das vorherige Konvertieren in .fsplat Files zur eigentlichen Nutzung. fsplat ist dabei als guter Kompromiss gewählt, um SH zu erhalten und dennoch keine komplexe Parser zu benötigen.
Gaussian Splats ist eine zukunftsweisende und vielversprechende Strategie.
Derzeitige Entwicklung geht auch in Richtung 4DGS, also auch Spatial Animations, Bewegungsinformationen und Video in der fotorealisitischen 3D-Szenerie. Ganz ohne Polygone. Durch die Techniken WebGPU wird es einfach im Browser nutzbar.
radsplat ist zudem performancetechnisch beeindruckend.