Ich habe kürzlich an der Demo-Szene Revision 2019 in der Kategorie PC 4k-Intro teilgenommen und mein Intro hat den ersten Platz gewonnen. Ich habe Codierung und Grafik gemacht und Dixan hat Musik komponiert. Die Grundregel des Wettbewerbs besteht darin, eine ausführbare Datei oder Website mit einer Größe von nur 4096 Byte zu erstellen. Dies bedeutet, dass alles mithilfe von Mathematik und Algorithmen generiert werden muss. Auf keine andere Weise kann ich Bilder, Video und Audio in so wenig Speicher komprimieren. In diesem Artikel werde ich über die Rendering-Pipeline meines Newton-Intro-Intro sprechen. Unten können Sie das fertige Ergebnis sehen oder
hier klicken, um zu sehen, wie es bei Revision live aussah, oder
gehen Sie zu pouet , um das Intro, das am Wettbewerb
teilgenommen hat, zu kommentieren und herunterzuladen. Über die Arbeit und Korrekturen der Wettbewerber können Sie hier
lesen .
Die Ray-Marschdistanz-Feldtechnik ist in der 4k-Intro-Disziplin sehr beliebt, da Sie damit komplexe Formen in nur wenigen Codezeilen angeben können. Der Nachteil dieses Ansatzes ist jedoch die Ausführungsgeschwindigkeit. Um die Szene zu rendern, müssen Sie den Schnittpunkt der Strahlen mit der Szene finden, zuerst bestimmen, was Sie sehen, beispielsweise einen Strahl von der Kamera, und dann nachfolgende Strahlen vom Objekt zu den Lichtquellen, um die Beleuchtung zu berechnen. Wenn Sie mit Ray Marching arbeiten, können diese Schnittpunkte nicht in einem Schritt gefunden werden. Sie müssen viele kleine Schritte entlang des Strahls ausführen und alle Objekte an jedem Punkt bewerten. Wenn Sie Raytracing verwenden, können Sie den genauen Schnittpunkt finden, indem Sie jedes Objekt nur einmal überprüfen. Die Menge der Formen, die verwendet werden können, ist jedoch sehr begrenzt: Sie benötigen für jeden Typ eine Formel, um den Schnittpunkt mit dem Strahl zu berechnen.
In diesem Intro wollte ich eine sehr genaue Beleuchtung simulieren. Da es notwendig war, Millionen von Strahlen in der Szene zu reflektieren, schien die Strahlverfolgung eine logische Wahl zu sein, um diesen Effekt zu erzielen. Ich habe mich auf eine einzelne Figur beschränkt - eine Kugel, weil der Schnittpunkt eines Strahls und einer Kugel ganz einfach berechnet wird. Sogar die Wände im Intro sind eigentlich sehr große Kugeln. Darüber hinaus vereinfachte es die Simulation der Physik; es genügte, nur Konflikte zwischen den Sphären zu berücksichtigen.
Um die Menge an Code zu veranschaulichen, die in 4096 Bytes passt, habe ich unten den vollständigen Quellcode des fertigen Intro vorgestellt. Alle Teile außer dem HTML-Code am Ende werden als PNG-Bild codiert, um sie auf eine kleinere Größe zu komprimieren. Ohne diese Komprimierung hätte der Code fast 8900 Bytes benötigt. Der Teil namens Synth ist eine abgespeckte Version von
SoundBox . Um den Code in diesem minimierten Format zu verpacken, habe ich den
Google Closure Compiler und den
Shader Minifier verwendet . Am Ende wird fast alles mit
JsExe in PNG
komprimiert . Die vollständige Kompilierungspipeline ist im Quellcode meines vorherigen 4k-Intro
Core Critical zu sehen , da sie vollständig mit der hier vorgestellten übereinstimmt.
Musik und Synthesizer sind vollständig in Javascript implementiert. Der Teil in WebGL ist in zwei Teile unterteilt (im Code grün hervorgehoben). Sie richtet die Render-Pipeline ein. Die Elemente Physik und Raytracer sind GLSL-Shader. Der Rest des Codes wird in einem PNG-Bild codiert und HTML wird unverändert am Ende des resultierenden Bildes hinzugefügt. Der Browser ignoriert Bilddaten und führt nur HTML-Code aus, der PNG wieder in Javascript dekodiert und ausführt.Pipeline rendern
Das Bild unten zeigt die Rendering-Pipeline. Es besteht aus zwei Teilen. Der erste Teil der Pipeline ist ein Physiksimulator. Die Intro-Szene enthält 50 Kugeln, die im Raum miteinander kollidieren. Der Raum selbst besteht aus sechs Kugeln, von denen einige kleiner als andere sind, um mehr gekrümmte Wände zu schaffen. Zwei vertikale Beleuchtungsquellen in den Ecken sind ebenfalls Kugeln, dh insgesamt 58 Kugeln in der Szene. Der zweite Teil der Pipeline ist der Ray Tracer, der die Szene rendert. Das folgende Diagramm zeigt das Rendern eines Frames zum Zeitpunkt t. Die Physiksimulation nimmt den vorherigen Frame (t-1) und simuliert den aktuellen Zustand. Der Raytracer nimmt die aktuellen Positionen und Positionen des vorherigen Frames (für den Geschwindigkeitskanal) auf und rendert die Szene. Anschließend kombiniert die Nachbearbeitung die vorherigen 5 Frames und den aktuellen Frame, um Verzerrungen und Rauschen zu reduzieren, und erstellt dann ein fertiges Ergebnis.

Rendern eines Frames zum Zeitpunkt t.Der physikalische Teil ist recht einfach. Im Internet finden Sie viele Tutorials zum Erstellen primitiver Simulationen für Kugeln. Position, Radius, Geschwindigkeit und Masse werden in zwei Texturen mit einer Auflösung von 1 x 58 gespeichert. Ich habe die Webgl 2-Funktion verwendet, mit der mehrere Renderziele gerendert werden können, sodass die Daten zweier Texturen gleichzeitig aufgezeichnet werden. Die gleiche Funktionalität wird vom Raytracer verwendet, um drei Texturen zu erstellen. Webgl bietet keinen Zugriff auf die Raytracing-APIs NVidia RTX oder DirectX Raytracing (DXR), sodass alles von Grund auf neu erfolgt.
Ray Tracer
Raytracing selbst ist eine ziemlich primitive Technik. Wir geben einen Strahl in die Szene ab, er wird viermal reflektiert, und wenn er in die Lichtquelle gelangt, sammelt sich die Farbe der Reflexionen an. sonst werden wir schwarz. In 4096 Bytes (einschließlich Musik, Synthesizer, Physik und Rendering) ist kein Platz für die Erstellung komplexer beschleunigender Raytracing-Strukturen. Daher verwenden wir die Brute-Force-Methode, dh wir überprüfen alle 57 Kugeln (die Vorderwand ist ausgeschlossen) für jeden Strahl, ohne Optimierungen vorzunehmen, um einen Teil der Kugeln auszuschließen. Dies bedeutet, dass Sie für 60 Bilder pro Sekunde in einer Auflösung von 1080p nur 2-6 Strahlen oder Samples pro Pixel emittieren können. Dies ist nah genug, um eine gleichmäßige Beleuchtung zu erzeugen.
1 Abtastung pro Pixel.6 Samples pro Pixel.Wie gehe ich damit um? Zuerst habe ich den Raytracing-Algorithmus untersucht, aber er wurde bereits auf den Punkt gebracht. Ich habe es geschafft, die Leistung leicht zu steigern, indem ich Fälle eliminierte, in denen der Strahl innerhalb der Kugel beginnt, da solche Fälle nur bei Vorhandensein von Transparenzeffekten anwendbar sind und nur undurchsichtige Objekte in unserer Szene vorhanden waren. Danach habe ich jede if-Bedingung in einer separaten Anweisung zusammengefasst, um unnötige Verzweigungen zu vermeiden: Trotz der „redundanten“ Berechnungen ist dieser Ansatz immer noch schneller als eine Reihe von bedingten Anweisungen. Es war auch möglich, das Abtastmuster zu verbessern: Anstatt Strahlen zufällig zu emittieren, konnten wir sie in einem gleichmäßigeren Muster über die Szene verteilen. Leider hat dies nicht geholfen und zu welligen Artefakten in jedem Algorithmus geführt, den ich ausprobiert habe. Dieser Ansatz führte jedoch zu guten Ergebnissen für Standbilder. Infolgedessen kehrte ich zu einer völlig zufälligen Verteilung zurück.
Benachbarte Pixel sollten eine sehr ähnliche Beleuchtung haben. Warum also nicht bei der Berechnung der Beleuchtung eines einzelnen Pixels verwenden? Wir möchten keine Texturen verwischen, sondern nur die Beleuchtung. Daher müssen wir sie in separaten Kanälen rendern. Außerdem möchten wir keine Objekte verwischen. Daher müssen wir die Kennungen von Objekten berücksichtigen, um zu wissen, welche Pixel leicht verwischt werden können. Da wir lichtreflektierende Objekte haben und klare Reflexionen benötigen, reicht es nicht aus, nur die ID des ersten Objekts herauszufinden, mit dem der Strahl kollidiert. Ich habe einen Sonderfall für reine reflektierende Materialien verwendet, um auch die IDs des ersten und zweiten Objekts, die in Reflexionen sichtbar sind, in den Objektidentifizierungskanal aufzunehmen. In diesem Fall kann durch Unschärfe die Beleuchtung von Objekten in Reflexionen geglättet werden, während gleichzeitig die Grenzen von Objekten beibehalten werden.

Texturkanal, wir müssen ihn nicht verwischen.Hier im roten Kanal befindet sich die ID des ersten Objekts, in grün - das zweite und in blau - das dritte. In der Praxis werden sie alle in einen einzigen Gleitkommawert codiert, in dem der ganzzahlige Teil die Kennungen von Objekten speichert und der gebrochene Teil die Rauheit anzeigt: 332211.RR.Da es in der Szene Objekte mit unterschiedlicher Rauheit gibt (einige Bereiche sind rau, das Licht wird auf andere gestreut, im dritten gibt es eine Spiegelreflexion), speichere ich die Rauheit, um den Unschärferadius zu steuern. Da die Szene keine kleinen Details enthält, habe ich einen großen 50 x 50-Kern mit den Gewichten in Form von inversen Quadraten zum Verwischen verwendet. Der Weltraum wird nicht berücksichtigt (dies könnte realisiert werden, um genauere Ergebnisse zu erzielen), da auf Oberflächen, die in einigen Richtungen in einem Winkel angeordnet sind, ein größerer Bereich erodiert wird. Eine solche Unschärfe erzeugt ein ziemlich glattes Bild, aber Artefakte sind insbesondere in Bewegung deutlich sichtbar.
Beleuchtungskanal mit Unschärfe und immer noch auffälligen Artefakten. In diesem Bild sind verschwommene Punkte an der Rückwand sichtbar, die durch einen kleinen Fehler mit den Kennungen des zweiten reflektierten Objekts verursacht werden (die Strahlen verlassen die Szene). Auf dem fertigen Bild ist dies nicht sehr auffällig, da klare Reflexionen vom Texturkanal aufgenommen werden. Lichtquellen werden auch verschwommen, aber ich mochte diesen Effekt und habe ihn verlassen. Falls gewünscht, kann dies verhindert werden, indem die Kennungen von Objekten je nach Material geändert werden.Wenn sich Objekte in der Szene befinden und die Kamera die Szene langsam aufnimmt, sollte die Beleuchtung in jedem Bild konstant bleiben. Daher können wir nicht nur die XY-Koordinaten des Bildschirms verwischen. wir können in der Zeit verschwimmen. Wenn wir davon ausgehen, dass sich die Beleuchtung in 100 ms nicht zu stark ändert, können wir sie für 6 Bilder mitteln. Während dieses Zeitfensters bewegen sich die Objekte und die Kamera jedoch noch ein Stück weit, sodass eine einfache Berechnung des Durchschnitts für 6 Bilder ein sehr verschwommenes Bild ergibt. Wir wissen jedoch, wo sich alle Objekte und die Kamera in der vorherigen Karte befanden, sodass wir die Geschwindigkeitsvektoren im Bildschirmbereich berechnen können. Dies wird als vorübergehende Projektion bezeichnet. Wenn ich zum Zeitpunkt t ein Pixel habe, kann ich die Geschwindigkeit dieses Pixels nehmen und berechnen, wo es zum Zeitpunkt t-1 war, und dann berechnen, wo sich das Pixel zum Zeitpunkt t-1 zum Zeitpunkt t-2 befindet, und so weiter. 5 Frames zurück. Im Gegensatz zur Unschärfe im Bildschirmbereich habe ich für jedes Bild das gleiche Gewicht verwendet, d. H. hat gerade die Farbe zwischen allen Frames gemittelt, um eine vorübergehende „Unschärfe“ zu erzielen.

Ein Pixelgeschwindigkeitskanal, der basierend auf der Bewegung des Objekts und der Kamera meldet, wo sich das Pixel im letzten Frame befand.Um ein gemeinsames Verwischen von Objekten zu vermeiden, verwenden wir erneut den Kanal der Objektkennungen. In diesem Fall betrachten wir nur das erste Objekt, mit dem der Strahl kollidierte. Dies stellt ein Anti-Aliasing innerhalb des Objekts bereit, d.h. in Reflexionen.Natürlich war das Pixel im vorherigen Frame möglicherweise nicht sichtbar. Es kann von einem anderen Objekt ausgeblendet oder außerhalb des Sichtfelds der Kamera liegen. In solchen Fällen können wir die vorherigen Informationen nicht verwenden. Diese Prüfung wird für jeden Frame separat durchgeführt, sodass wir 1 bis 6 Samples oder Frames pro Pixel erhalten und die möglichen verwenden. Die folgende Abbildung zeigt, dass dies für langsame Objekte kein sehr ernstes Problem ist.
Wenn Objekte sich bewegen und neue Teile der Szene öffnen, verfügen wir nicht über 6 Informationsrahmen, um diese für diese Teile zu mitteln. Dieses Bild zeigt Bereiche mit 6 Rahmen (weiß) sowie Bereiche ohne diese (allmählich dunkler werdende Schattierungen). Das Auftreten der Konturen wird durch die Randomisierung der Abtastorte für das Pixel in jedem Rahmen und die Tatsache verursacht, dass wir die Kennung des Objekts aus der ersten Stichprobe entnehmen.Verschwommenes Licht wird über sechs Bilder gemittelt. Artefakte sind nahezu unsichtbar und das Ergebnis ist über die Zeit stabil, da in jedem Bild nur ein Bild von sechs Änderungen, bei denen die Beleuchtung berücksichtigt wird, berücksichtigt wird.Wenn wir all dies kombinieren, erhalten wir ein fertiges Bild. Die Beleuchtung wird auf benachbarte Pixel verwischt, während Texturen und Reflexionen klar bleiben. All dies wird dann zwischen sechs Bildern gemittelt, um im Laufe der Zeit ein noch glatteres und stabileres Bild zu erzeugen.
Das fertige Bild.Dämpfungsartefakte sind immer noch erkennbar, da ich mehrere Abtastwerte pro Pixel gemittelt habe, obwohl ich den Kanal der Objektkennung und die Geschwindigkeit für die erste Kreuzung verwendet habe. Sie können versuchen, dies zu beheben und die Reflexionen zu glätten, indem Sie die Proben verwerfen, wenn sie nicht mit der ersten übereinstimmen oder zumindest wenn die erste Kollision nicht in der richtigen Reihenfolge übereinstimmt. In der Praxis sind die Spuren fast unsichtbar, so dass ich mich nicht darum gekümmert habe, sie zu beseitigen. Die Grenzen von Objekten sind ebenfalls verzerrt, da die Kanäle für Geschwindigkeit und Objektkennungen nicht geglättet werden können. Ich habe über die Möglichkeit nachgedacht, das gesamte Bild mit 2160p mit einer weiteren Reduzierung des Maßstabs auf 1080p zu rendern, aber meine NVidia GTX 980ti ist nicht in der Lage, solche Auflösungen mit 60fps zu verarbeiten, und habe mich daher entschlossen, diese Idee aufzugeben.
Im Allgemeinen bin ich sehr zufrieden mit dem Ergebnis des Intro. Ich habe es geschafft, alles, was ich vorhatte, hineinzuquetschen, und trotz kleinerer Fehler war das Endergebnis von sehr hoher Qualität. In Zukunft können Sie versuchen, Fehler zu beheben und das Anti-Aliasing zu verbessern. Es lohnt sich auch, mit Funktionen wie Transparenz, Bewegungsunschärfe, verschiedenen Formen und Objekttransformationen zu experimentieren.