Texturen für 64k Intro: wie es heute gemacht wird

Dieser Artikel ist Teil zwei unserer H - Immersion - Reihe . Der erste Teil kann hier gelesen werden: Eintauchen in Eintauchen .

Wenn Sie eine Animation mit nur 64 KB erstellen, ist es schwierig, vorgefertigte Bilder zu verwenden. Wir können sie nicht auf herkömmliche Weise speichern, da sie nicht effizient genug sind, selbst wenn Sie eine Komprimierung anwenden, z. B. JPEG. Eine alternative Lösung ist die prozedurale Generierung, dh das Schreiben von Code, der die Erstellung von Bildern während der Programmausführung beschreibt. Unsere Implementierung dieser Lösung war ein Texturgenerator - ein grundlegender Bestandteil unserer Toolchain. In diesem Beitrag werden wir erklären, wie wir es in H - Immersion entwickelt und verwendet haben.


U-Boot-Scheinwerfer beleuchten die Details des Meeresbodens.

Frühe Version


Die Texturgenerierung war eines der allerersten Elemente unserer Codebasis: Prozedurale Texturen wurden bereits in unserem ersten B - Incubation - Intro verwendet. Der Code bestand aus einer Reihe von Funktionen zum Füllen, Filtern, Transformieren und Kombinieren von Texturen sowie einer großen Schleife, die alle Texturen umgeht. Diese Funktionen wurden in reinem C ++ geschrieben, aber die C-API-Interaktion wurde später hinzugefügt, damit sie vom C PicoC- Interpreter ausgewertet werden können . Zu dieser Zeit haben wir PicoC verwendet, um die Zeit zu reduzieren, die für jede Iteration benötigt wird. Auf diese Weise konnten wir Texturen während der Programmausführung ändern und neu laden. Der Wechsel zur C-Teilmenge war ein kleines Opfer im Vergleich zu der Tatsache, dass wir jetzt den Code ändern und das Ergebnis sofort sehen konnten, ohne die gesamte Demo schließen, neu kompilieren und neu laden zu müssen.


Mit einem einfachen Muster, ein wenig Rauschen und Verformung können wir eine stilisierte Holzstruktur erhalten.


In dieser Szene aus F - Felix 'Werkstatt wurden verschiedene Holzstrukturen verwendet.

Einige Zeit haben wir die Funktionen dieses Generators untersucht und ihn daher mit einem kleinen PHP-Skript und einer einfachen Weboberfläche auf einem Webserver veröffentlicht. Wir konnten den Texturcode in ein Textfeld schreiben, und das Skript übergab ihn an den Generator, der das Ergebnis dann als PNG-Datei ausgab, um es auf der Seite anzuzeigen. Sehr bald begannen wir während der Mittagspause direkt bei der Arbeit zu skizzieren und unsere kleinen Meisterwerke mit anderen Mitgliedern der Gruppe zu teilen. Diese Interaktion motivierte uns zum kreativen Prozess.


Webgalerie unseres alten Texturgenerators. Alle Texturen können im Browser bearbeitet werden.

Vollständige Neugestaltung


Der Texturgenerator blieb lange Zeit nahezu unverändert; Wir fanden es gut und unsere Effektivität nahm nicht mehr zu. Aber als wir herausfanden, dass es in den Internetforen viele Künstler gibt, die ihre vollständig prozedural erzeugten Texturen demonstrieren und Herausforderungen zu verschiedenen Themen arrangieren. Der prozedurale Inhalt war einst ein Merkmal der Demoszene , wurde jedoch von Allegorithmic , ShaderToy und ähnlichen Tools der Öffentlichkeit zugänglich gemacht. Wir haben nicht darauf geachtet, und sie fingen an, uns leicht auf die Schulterblätter zu setzen. Inakzeptabel!


Stoffcouch . Eine vollständig prozedurale Stoffstruktur, die in Substance Designer erstellt wurde. Gepostet von: Imanol Delgado. www.artstation.com/imanoldelgado

Bild

Waldboden . Vollständig prozedurale Waldbodentextur, erstellt von Substance Designer. Gepostet von Daniel Thiger. www.artstation.com/dete

Wir mussten unsere Werkzeuge lange überdenken. Glücklicherweise konnten wir durch jahrelange Arbeit mit demselben Texturgenerator seine Mängel erkennen. Darüber hinaus hat uns unser Generator für entstehende Netze mitgeteilt, wie die Pipeline für prozedurale Inhalte aussehen soll.

Der wichtigste architektonische Fehler war die Implementierung der Generierung als eine Reihe von Operationen mit Texturobjekten. Aus Sicht einer übergeordneten Perspektive mag dies der richtige Ansatz sein, aber aus Sicht der Implementierung haben Funktionen wie Textur.DoSomething () oder Combine (TexturA, TexturB) schwerwiegende Nachteile.

Erstens müssen Sie diese Funktionen für den OOP-Stil als Teil der API deklarieren, egal wie einfach sie sind. Dies ist ein ernstes Problem, da es nicht gut skaliert werden kann und vor allem unnötige Reibung im kreativen Prozess verursacht. Wir wollten die API nicht jedes Mal ändern, wenn wir etwas Neues ausprobieren mussten. Dies erschwert das Experimentieren und schränkt die kreative Freiheit ein.

Zweitens erfordert dieser Ansatz in Bezug auf die Leistung, dass Sie Texturdaten in Zyklen so oft verarbeiten, wie es Operationen gibt. Dies wäre nicht besonders wichtig, wenn diese Vorgänge im Hinblick auf die Kosten für den Zugriff auf große Speicherfragmente kostspielig wären, dies ist jedoch normalerweise nicht der Fall. Mit Ausnahme eines sehr kleinen Teils der Operationen, die beispielsweise Perlin-Rauschen erzeugen oder füllen , sind sie grundsätzlich sehr einfach und erfordern nur wenige Anweisungen zum Texturpunkt. Das heißt, wir haben die Texturdaten umgangen, um triviale Operationen auszuführen, was unter dem Gesichtspunkt des Caching äußerst ineffizient ist.

Die neue Struktur löst diese Probleme durch die Reorganisation der Logik. Die meisten Funktionen in der Praxis führen unabhängig voneinander dieselbe Operation für jedes Texturelement aus. Daher können wir anstelle einer textur.DoSomething () -Funktion, die alle Elemente umgeht, die textur.ApplyFunction (f) schreiben, wobei f (element) nur für ein einzelnes Texturelement funktioniert. Dann kann f (Element) gemäß einer bestimmten Textur geschrieben werden.

Dies scheint eine geringfügige Änderung zu sein. Diese Struktur vereinfacht jedoch die API, macht den Generierungscode flexibler und ausdrucksvoller, cachefreundlicher und ermöglicht eine einfache parallele Verarbeitung. Viele der Leser haben bereits erkannt, dass dies im Wesentlichen ein Shader ist. Die eigentliche Implementierung bleibt jedoch der auf dem Prozessor ausgeführte C ++ - Code. Wir behalten uns weiterhin die Möglichkeit vor, Operationen außerhalb der Schleife auszuführen, verwenden diese Option jedoch nur, wenn dies beispielsweise durch Faltung erforderlich ist.

Es war:


//     . // API . //    -  API. //      . class ProceduralTexture { void DoSomething(parameters) { for (int i = 0; i < size; ++i) { //   . (*this)[i] = … } } void PerlinNoise(parameters) { … } void Voronoi(parameters) { … } void Filter(parameters) { … } void GenerateNormalMap() { … } }; void GenerateSomeTexture(texture t) { t.PerlinNoise(someParameter); t.Filter(someOtherParameter); … //  .. t.GenerateNormalMap(); } 

Es wurde:


 //       . // API . //     . //      . class ProceduralTexture { void ApplyFunction(functionPointer f) { for (int i = 0; i < size; ++i) { //    . (*this)[i] = f((*this)[i]); } } }; void GenerateNormalMap(ProceduralTexture t) { … } void SomeTextureGenerationPass(void* out, PixelInfo in) { result = PerlinNoise(in); result = Filter(result); … //  .. *out = result; } void GenerateSomeTexture(texture t) { t.ApplyFunction(SomeTextureGenerationPass); GenerateNormalMap(t); } 

Parallelisierung


Die Erzeugung von Texturen braucht Zeit, und ein offensichtlicher Kandidat für die Reduzierung dieser Zeit ist die parallele Codeausführung. Zumindest können Sie lernen, wie Sie mehrere Texturen gleichzeitig generieren. Genau das haben wir für die Werkstatt von F - Felix getan, was die Ladezeit erheblich verkürzt hat.

Dies spart jedoch keine Zeit dort, wo sie am meisten benötigt wird. Es dauert immer noch viel Zeit, um eine Textur zu erzeugen. Dies gilt für die Änderung, da wir die Textur vor jeder Änderung immer wieder neu laden. Stattdessen ist es besser, den Code zur internen Texturgenerierung zu parallelisieren. Da der Code jetzt im Wesentlichen aus einer großen Funktion besteht, die in einer Schleife auf jedes Texel angewendet wird, wird die Parallelisierung einfach und effizient. Reduziert die Kosten für Experimente, Tuning und Entwürfe, was sich direkt auf den kreativen Prozess auswirkt.




Illustration einer Idee, die wir für H - Immersion erforscht und verworfen haben: eine Mosaikdekoration mit Orichalconfutter. Hier wird es in unserem interaktiven Bearbeitungswerkzeug angezeigt.

GPU-seitige Generierung


Wenn dies immer noch nicht offensichtlich ist, werde ich sagen, dass die Texturgenerierung vollständig in der CPU durchgeführt wird. Vielleicht lesen einige von Ihnen diese Zeilen jetzt und sind ratlos "aber warum ?!". Es scheint, dass der offensichtliche Schritt die Texturgenerierung im Videoprozessor ist. Zunächst wird die Erzeugungsrate um eine Größenordnung erhöht. Warum benutzen wir es nicht?

Der Hauptgrund ist, dass das Ziel unseres kleinen Redesigns darin bestand, auf der CPU zu bleiben. Der Wechsel zu einer GPU würde viel mehr Arbeit bedeuten. Wir müssten zusätzliche Probleme lösen, für die wir noch nicht genug Erfahrung haben. Wenn wir mit der CPU arbeiten, haben wir ein klares Verständnis dafür, was wir wollen, und wir wissen, wie wir frühere Fehler beheben können.

Die gute Nachricht ist jedoch, dass das Experimentieren mit der GPU dank der neuen Struktur jetzt ziemlich trivial erscheint. Das Testen von Kombinationen beider Prozessortypen wird für die Zukunft ein interessantes Experiment sein.

Texturgenerierung und physikalisch genaue Schattierung


Eine weitere Einschränkung des alten Designs bestand darin, dass die Textur nur als RGB-Bild betrachtet wurde. Wenn wir mehr Informationen generieren mussten, sagen wir die diffuse Textur und die Textur von Normalen für dieselbe Oberfläche, dann hinderte uns nichts daran, aber die API half nicht viel. Dies ist im Zusammenhang mit Physically Based Shading (PBR) besonders wichtig geworden.

In einer herkömmlichen Pipeline ohne PBR werden normalerweise Farbtexturen verwendet, in denen viele Informationen gebacken werden. Solche Texturen stellen oft das endgültige Erscheinungsbild der Oberfläche dar: Sie haben bereits ein bestimmtes Volumen, Risse sind abgedunkelt und es kann sogar zu Reflexionen kommen. Wenn mehrere Texturen gleichzeitig verwendet werden, werden große und kleine Details normalerweise kombiniert, um normale Karten oder Oberflächenreflexionsvermögen hinzuzufügen.

Oberflächen-PBR-Förderer verwenden typischerweise mehrere Texturensätze, die physikalische Werte darstellen und nicht das gewünschte künstlerische Ergebnis. Die diffuse Farbtextur, die der häufig als „Farbe“ der Oberfläche bezeichneten Farbe am nächsten kommt, ist normalerweise flach und uninteressant. Der Farbspiegel wird durch den Brechungsindex der Oberfläche bestimmt. Die meisten Details und Variabilitäten stammen aus den Texturen von Normalen und Rauheit (Rauheit) (die jemand als gleich betrachten kann, aber mit zwei verschiedenen Skalen). Das wahrgenommene Reflexionsvermögen einer Oberfläche wird eine Folge ihres Rauheitsgrades. In diesem Stadium wird es logischer sein, nicht in Materialien, sondern in Materialien zu denken.










Die neue Struktur ermöglicht es uns, beliebige Pixelformate für Texturen zu deklarieren. Nachdem wir es zu einem Teil der API gemacht haben, erlauben wir ihm, mit dem gesamten Boilerplate-Code umzugehen. Nachdem wir das Pixelformat deklariert haben, können wir uns auf den Creative-Code konzentrieren, ohne zu viel Aufwand für die Verarbeitung dieser Daten aufzuwenden. Zur Laufzeit werden mehrere Texturen generiert und transparent auf die GPU übertragen.

In einigen PBR-Pipelines werden diffuse und spiegelnde Farben nicht direkt übertragen. Stattdessen werden die Parameter "Grundfarbe" und "Metallizität" verwendet, was seine Vor- und Nachteile hat. In H - Immersion verwenden wir das diffuse + spiegelnde Modell, und das Material besteht normalerweise aus fünf Schichten:

  1. Diffuse Farbe (RGB; 0: Vantablack ; 1: Neuschnee ).
  2. Spiegelfarbe (RGB: Anteil des bei 90 ° reflektierten Lichts, auch als F0 oder R0 bekannt ).
  3. Rauheit (A; 0: perfekt glatt; 1: gummiartig).
  4. Normal (XYZ; Einheitsvektor).
  5. Geländehöhe (A; wird für die Parallaxenokklusionskartierung verwendet).

Bei Verwendung wurden Lichtemissionsinformationen direkt zum Shader hinzugefügt. Wir fanden es nicht notwendig, Umgebungsokklusion zu haben, da in den meisten Szenen überhaupt keine Umgebungsbeleuchtung vorhanden ist. Ich bin jedoch nicht überrascht, dass wir zusätzliche Ebenen oder andere Arten von Informationen haben werden, zum Beispiel Anisotropie oder Opazität.



Die obigen Bilder zeigen ein kürzlich durchgeführtes Experiment zur Erzeugung einer lokalen Umgebungsokklusion basierend auf der Höhe. Für jede Richtung gehen wir eine vorgegebene Entfernung durch und behalten die größte Steigung bei (Höhenunterschied geteilt durch Entfernung). Dann berechnen wir die Okklusion aus der durchschnittlichen Steigung.

Einschränkungen und zukünftige Arbeit


Wie Sie sehen, hat sich die neue Struktur gegenüber der alten Struktur erheblich verbessert. Darüber hinaus fördert sie den kreativen Ausdruck. Sie hat jedoch immer noch Einschränkungen, die wir in Zukunft beseitigen wollen.

Obwohl es in diesem Intro keine Probleme gab, haben wir festgestellt, dass die Speicherzuweisung ein Hindernis sein kann. Beim Generieren von Texturen wird ein Array von Float-Werten verwendet. Bei großen Texturen mit vielen Ebenen können Sie schnell auf ein Problem mit der Speicherzuweisung stoßen. Es gibt verschiedene Möglichkeiten, dies zu lösen, aber alle haben ihre Nachteile. Zum Beispiel können wir Texturen Kachel für Kachel erzeugen, während die Skalierbarkeit besser ist. Die Implementierung einiger Operationen, wie z. B. der Faltung, wird jedoch weniger offensichtlich.

Darüber hinaus haben wir in diesem Artikel trotz des verwendeten Wortes „Materialien“ nur über Texturen gesprochen, nicht aber über Shader. Die Verwendung von Materialien sollte jedoch auch zu Shadern führen. Dieser Widerspruch spiegelt die Einschränkungen der vorhandenen Struktur wider: Texturgenerierung und Schattierung sind zwei separate Teile, die durch eine Brücke getrennt sind. Wir haben versucht, das Überqueren dieser Brücke zu vereinfachen, aber wir möchten, dass diese Teile eins werden. Wenn ein Material beispielsweise sowohl statische als auch dynamische Parameter hat, möchten wir diese an einer Stelle beschreiben. Dies ist ein komplexes Thema, und wir wissen noch nicht, ob es eine gute Lösung gibt, aber lassen Sie uns nicht weiterkommen.

Bild

Ein Experiment zur Erzeugung einer Stoffstruktur ähnlich der oben gezeigten Arbeit von Imadol Delgado.

Source: https://habr.com/ru/post/de419007/


All Articles