Im September dieses Jahres sollte das Titan World-Handyspiel von Unstoppable, dem Minsker Büro von Glu Mobile, veröffentlicht werden. Das Projekt wurde kurz vor der Weltveröffentlichung abgesagt. Aber die Erfolge blieben und die interessantesten von ihnen, mit freundlicher Genehmigung der Leiter des Studios Dennis Zdonov und Alex Paley, möchte ich mit der Öffentlichkeit teilen.Im März 2018 hielten der Teamleiter und ich ein Treffen ab, bei dem wir besprachen, was als nächstes zu tun ist: Der Rendercode wurde fertiggestellt, und die Pläne enthielten keine neuen Funktionen und Spezialeffekte. Es schien eine logische Entscheidung zu sein, das Partikelsystem von Grund auf neu zu schreiben - nach allen Tests führte es zu den größten Produktivitätseinbußen und machte die Designer mit seiner Benutzeroberfläche (Textkonfigurationsdatei) und den äußerst mageren Funktionen verrückt.
Es sollte beachtet werden, dass das Team die meiste Zeit im "Morgen Release" -Modus an dem Spiel gearbeitet hat, also habe ich alle Subsysteme geschrieben, erstens, um nicht zu brechen, was bereits funktioniert, und zweitens mit einem kurzen Entwicklungszyklus. Insbesondere wurden die meisten Effekte, zu denen das reguläre System nicht in der Lage war, im Fragment-Shader ausgeführt, ohne den Hauptcode zu beeinflussen.
Die Beschränkung der Anzahl der Partikel (Transformationsmatrizen für jedes Partikel wurden auf CPU gebildet, die Schlussfolgerung wurde durch den Installateur des gl-erweiterbaren ios gezogen), zum Beispiel war es notwendig, einen Shader zu schreiben, der eine große Anzahl von Partikeln basierend auf der analytischen Darstellung der Form von Objekten "emuliert" und mit dem Raum zusammengesetzt gefälschte Daten in den Tiefenpuffer legen.
Die z-Koordinate des Fragments wurde für ein ebenes Teilchen berechnet, als ob wir eine Kugel zeichnen würden, und der Radius dieser Kugel wurde durch den Sinus des Perlin-Rauschens unter Berücksichtigung der Zeit moduliert:
r=.5+.5*sin(perlin(specialUV)+time)
Eine vollständige Beschreibung der Rekonstruktion der Tiefe der Kugel finden Sie in
Íñigo Quílez , aber ich habe einen vereinfachten, schnelleren Code verwendet. Natürlich war er eine grobe Annäherung, aber bei komplexen geometrischen Formen (Rauch, Explosionen) gab er ein ziemlich anständiges Bild.
Gameplay-Screenshot. Der Rauchrock wurde in einem kleinen Teil hergestellt, mehrere weitere blieben am Hauptteil der Explosion zurück. Natürlich sah es „vom Boden aus“ am spektakulärsten aus, als Gebäude und Einheiten von Rauch sanft umhüllt wurden. Vorschläge, die Position der Kamera während der Explosion zu ändern, wurden jedoch nicht produziert.Erklärung des Problems
Was wolltest du auf dem Weg nach draußen machen? Wir gingen vielmehr von den Einschränkungen aus, mit denen wir beim vorherigen Partikelsystem gequält wurden. Die Situation wurde durch die Tatsache verschlechtert, dass das Frame-Budget fast erschöpft war und auf schwachen Geräten (wie iPad Air) sowohl Pixel- als auch Vertex-Pipelines voll geladen waren. Daher wollte ich als Ergebnis das produktivste System erhalten, auch wenn ich die Funktionalität etwas einschränkte.
Designer haben eine Liste von Funktionen zusammengestellt und eine Skizze der Benutzeroberfläche erstellt, die auf ihren eigenen Erfahrungen und Praktiken mit Einheit, Unwirklichkeit und Nachwirkungen basiert.
Verfügbare Technologie
Aufgrund des Erbes und der Einschränkungen, die von der Zentrale auferlegt wurden, waren wir auf OpenGlES 2 beschränkt. Daher waren Technologien wie das in modernen Partikelsystemen verwendete Transformationsfeedback nicht verfügbar.
Was war noch übrig? Verwenden Sie das Abrufen und Speichern von Vertex-Texturen in Texturen? Eine funktionierende Option, aber der Speicher ist auch fast vorbei, die Leistung einer solchen Lösung ist nicht die optimalste und das Ergebnis unterscheidet sich nicht in der architektonischen Schönheit.
Zu diesem Zeitpunkt hatte ich viele Artikel über die Implementierung von Partikelsystemen auf GPU gelesen. Die überwiegende Mehrheit enthielt einen hellen Titel ("Millionen von Partikeln auf mobiler GPU, mit Vorlieben und Dichtern"). Die Implementierung bestand jedoch aus Beispielen für einfache, wenn auch amüsant aussehende Emitter / Attraktoren und war im Allgemeinen für den realen Einsatz im Spiel fast nutzlos.
Dieser Artikel brachte maximalen Nutzen: Der Autor löste das eigentliche Problem und machte keine „kugelförmigen Partikel im Vakuum“. Die Benchmark-Zahlen aus diesem Artikel und die Profilerstellungsergebnisse haben in der Entwurfsphase viel Zeit gespart.
Suche nach Ansätzen
Ich begann damit, die vom Partikelsystem gelösten Probleme zu klassifizieren und nach bestimmten Fällen zu suchen. Es stellte sich ungefähr Folgendes heraus (ein Teil der tatsächlichen Docks des Konzepts aus der Korrespondenz mit dem Teamleiter):
“- Partikel / Mesh-Arrays mit zyklischer Bewegung. Keine Verarbeitungsposition, alles durch die Bewegungsgleichung. Anwendungen - Rauch aus Rohren, Dampf über Wasser, Schnee / Regen, volumetrischer Nebel, schwankende Bäume, teilweise Verwendung bei nichtzyklischen Auswirkungen von Explosionen.
- Bänder. Bildung von vb durch Ereignis, Verarbeitung nur auf der GPU (Aufnahmen durch Strahlen, Flüge entlang einer festen (?) Flugbahn mit einer Spur). Vielleicht hebt die Variante mit der Übertragung der Start-Ziel-Koordinaten auf die Uniformen und der Konstruktion des Bandes durch vertexID ab. mit t.z. Kreuz mit Fresnel wie bei Direktlichtern + UVScroll rendern.
- Partikelerzeugung und Geschwindigkeitsverarbeitung. Die vielseitigste und schwierigste / langsamste Option finden Sie unter Tech Motion Processing. “
Kurz gesagt: Es gibt verschiedene Partikeleffekte, von denen einige einfacher als andere implementiert werden können.
Wir haben beschlossen, die Aufgabe in mehrere Iterationen aufzuteilen - von einfach bis komplex. Das Prototyping wurde auf meiner Engine / meinem Editor unter Windows / DirectX11 durchgeführt, da die Geschwindigkeit einer solchen Entwicklung um mehrere Größenordnungen höher war. Das Projekt wurde in wenigen Sekunden kompiliert, und die Shader wurden im laufenden Betrieb bearbeitet und im Hintergrund kompiliert. Das Ergebnis wurde in Echtzeit angezeigt, ohne dass zusätzliche Gesten wie das Drücken von Tasten erforderlich waren. Jeder, der große Projekte mit einer Menge MacBook / Xcode erstellt hat, wird die Gründe für diese Entscheidung verstehen.
Alle Codebeispiele stammen aus dem Windows-Prototyp.
Entwicklungsumgebung für Windows.Implementierung
Die erste Stufe ist die statische Ausgabe einer Anordnung von Partikeln. Nichts kompliziertes: Starten Sie den Vertex-Bufffer, füllen Sie ihn mit Quads (schreiben Sie die richtige UV für jedes Quad) und nähen Sie die Vertex-ID in die "zusätzliche" UV. Danach bilden wir im Shader anhand der Scheitelpunkt-ID basierend auf den Emittereinstellungen die Positionen der Partikel und stellen mithilfe von UV die Bildschirmkoordinaten wieder her.
Wenn vertex_id nativ verfügbar ist, können Sie vollständig auf einen Puffer und ohne UV verzichten, um die Bildschirmkoordinaten wiederherzustellen (was in der Windows-Version der Fall war).
Shader:
struct VS_INPUT { … uint v_id:SV_VertexID; … } //float index = input.uv2.x/6.0;// vertex_id index = floor(input.v_id/6.0);// vertex_id float2 map[6]={0,0,1,0,1,1,0,0,1,1,0,1}; float2 quaduv=map[frac(input.v_id/6.0)*6];
Danach können Sie einfache Szenarien mit sehr wenig Code implementieren. Beispielsweise ist eine zyklische Bewegung mit kleinen Abweichungen für den Schneeeffekt geeignet. Unser Ziel war es jedoch, den Künstlern die Kontrolle über das Verhalten der Partikel zu geben, und sie wissen, wie Sie wissen, selten, wie man Shader erstellt. Die Option mit Verhaltensvoreinstellungen und Bearbeitungsparametern über die Schieberegler war ebenfalls nicht attraktiv - Shader wechseln oder nach innen verzweigen, voreingestellte Optionen multiplizieren, mangelnde vollständige Kontrolle.
Die nächste Aufgabe bestand darin, das Ein- und Ausblenden für ein solches System zu implementieren. Partikel sollten nicht aus dem Nichts erscheinen und im Nirgendwo verschwinden. Bei der klassischen Implementierung eines Partikelsystems verarbeiten wir den Puffer programmgesteuert mit CPU, erstellen neue Partikel und entfernen alte. Um eine gute Leistung zu erzielen, müssen Sie einen intelligenten Speichermanager schreiben. Aber was passiert, wenn Sie die "toten" Partikel nicht zeichnen?
Angenommen, (für den Anfang) das Zeitintervall der Partikelemission und die Lebensdauer eines Partikels ist eine Konstante innerhalb eines einzelnen Emitters.

Dann können wir unseren Puffer (der nur die Scheitelpunkt-ID enthält) spekulativ als kreisförmig darstellen und seine maximale Größe wie folgt bestimmen:
pCount = round (prtPerSec * LifeTime / 60.0); pCountT = floor (prtPerSec * EmissionEndTime / 60.0); pCount=min (pCount, pCountT);
Berechnen Sie im Shader die Zeit basierend auf Index und Zeit (seit dem Beginn des Effekts verstrichene Zeit).
pTime=time-index/prtPerSec;
Befindet sich der Emitter in einer zyklischen Phase (alle Partikel werden emittiert und sterben jetzt ab und werden synchron geboren), machen wir aus der Zeit des Partikels Frac und erhalten so eine Schleife.
Wir müssen keine Partikel mit einer pTime von weniger als Null zeichnen - sie sind noch nicht geboren. Gleiches gilt für Partikel, bei denen die Summe aus Lebensdauer und Stromzeit die Emissionsendezeit überschreitet. In beiden Fällen zeichnen wir nichts, indem wir die Partikelgröße aufheben und / oder hinter den Bildschirm fallen lassen. Dieser Ansatz führt zu einem geringen Overhead in den Fadein / Fadeout-Phasen, während die maximale Leistung in der Sustain-Phase beibehalten wird.
Der Algorithmus kann leicht verbessert werden, indem nur der Teil des Scheitelpunktpuffers gesendet wird, der lebende Partikel zum Rendern enthält. Aufgrund der Tatsache, dass die Emission nacheinander auftritt, werden lebende Partikel höchstens einmal segmentiert, d.h. Es sind zwei Drawcalls erforderlich.
Wenn Sie nun die aktuelle Zeit jedes Partikels kennen, können Sie die Geschwindigkeit, Beschleunigung (und im Allgemeinen alle anderen Parameter) einstellen, um die Bewegungsgleichung zu schreiben, die zu den Koordinaten im Weltraum führt.
Wenn Sie restauriert von vertex_id uv verwenden, erhalten Sie bereits vier Punkte (genauer gesagt, wir verschieben jeden der Quad-Punkte in die gewünschte Richtung), auf denen der Vertex-Shader nach Abschluss der Projektion seine Arbeit abschließt.
p.xy+=(quaduv-.5);
Mit dem kostenlosen Bonus hatten wir die Möglichkeit, nicht nur den Emitter anzuhalten, sondern auch die Zeit mit der Genauigkeit des Rahmens zurückzuspulen. Diese Funktion hat sich bei der Gestaltung komplexer Effekte als sehr nützlich erwiesen.
Wir erhöhen die Funktionalität
Die nächste Iteration in der Entwicklung war die Lösung des Problems eines sich bewegenden Emitters. Unser spezielles System wusste nichts über seine Position, und als sich der Emitter bewegte, bewegte sich der gesamte Effekt synchron dahinter. Für Rauch aus dem Auspuffrohr und ähnliche Effekte sah es mehr als seltsam aus.
Die Idee war, die Emitterposition in einem Scheitelpunktpuffer aufzuzeichnen, wenn ein neues Teilchen geboren wurde. Da die Anzahl solcher Partikel gering ist, sollte der Overhead minimal sein.
Ein Kollege schlug vor, dass er bei der Entwicklung seiner eigenen Benutzeroberfläche nur einen Teil des Scheitelpunktpuffers zuordnen / nicht zuordnen und mit der Leistung dieser Lösung sehr zufrieden war. Ich habe Tests durchgeführt und es stellte sich heraus, dass dieser Ansatz sowohl auf Desktop- als auch auf mobilen Plattformen wirklich gut funktioniert.
Die Schwierigkeit trat bei der Synchronisation der Zeit auf CPU und GPU auf. Es musste sichergestellt werden, dass die Pufferaktualisierung genau dann durchgeführt wurde, wenn sich das „neue“, geschlungene Partikel in seiner Startposition befand. Das heißt, in Bezug auf den Ringpuffer ist es notwendig, die Grenzen des Aktualisierungsbereichs mit der Betriebszeit des Emitters zu synchronisieren.
Ich habe den hlsl-Code nach C ++ übertragen, für den Test habe ich den Emitter geschrieben, der sich in Lissajous bewegt, und das alles hat plötzlich funktioniert. Von Zeit zu Zeit „spuckte“ das System jedoch auf ein oder mehrere Partikel, feuerte sie in eine beliebige Richtung, entfernte sie nicht rechtzeitig oder erzeugte neue an beliebigen Orten.
Das Problem wurde gelöst, indem die Genauigkeit der Berechnung der Zeit im Motor überprüft und gleichzeitig das Zeitdelta bei der Aufzeichnung der neuen Emitterposition überprüft wurde, sodass der gesamte Pufferabschnitt, der von der vorherigen Iteration nicht betroffen war, aktualisiert wurde. Es war auch notwendig, dass das System unter den Bedingungen einer erzwungenen Desynchronisation arbeitete - ein plötzlicher Rückgang der fps sollte den Effekt nicht beeinträchtigen, zumal unser Spiel für verschiedene Geräte je nach Leistung unterschiedliche fps aufzeichnete - 60/30/20.
Der Methodencode ist ziemlich gewachsen (der Ringpuffer ist schwer elegant zu verarbeiten), aber unter Berücksichtigung aller Bedingungen funktionierte das System korrekt und stabil.
Um diese Zeit hatte der Partner bereits den „Fisch“ des Editors erstellt, der zum Testen des Systems ausreichte, und die / api-Vorlagen für die Integration des Systems in unsere Engine geschrieben.
Ich habe den gesamten Code auf ios / opengl portiert, integriert und schließlich echte Effekttests auf einem echten Gerät durchgeführt. Es wurde deutlich, dass das System nicht nur funktioniert, sondern auch für die Produktion geeignet ist. Es blieb noch Zeit, den UI-Editor fertigzustellen und den Code auf den Status "Es ist nicht beängstigend, ihn morgen zu veröffentlichen" zu bringen.
Wir haben uns sogar schon darauf vorbereitet, einen Speichermanager zu schreiben, um keinen Puffer (der schließlich vertex_id, uv, position und den anfänglichen Partikelvektor speicherte) für jeden neuen Effekt mit einem dynamischen Emitter zuzuweisen / zu zerstören, als mir eine andere Idee einfiel.
Die Tatsache, dass der Scheitelpunktpuffer in diesem System vorhanden ist, hat mich verfolgt. Er sah deutlich in ihrem Archaismus, "dem Erbe des dunklen Zeitalters des festen Förderers". Bei Testeffekten an einem Windows-Prototyp dachte ich, dass die Bewegung des Emitters immer glatt und immer viel langsamer ist als die Bewegung des Partikels. Darüber hinaus führt die Aktualisierung der Position bei einer großen Anzahl von Partikeln dazu, dass Hunderte von Partikeln dieselben Daten aufzeichnen. Die Lösung erwies sich als einfach: Wir führen ein festes Array ein, in das die durch die Lebensdauer des Partikels normalisierte „Historie“ der Position des Emitters fällt. Und auf GPU werden wir die Daten interpolieren. Danach verschwand der Bedarf an dynamischen Puffern in der Version ios / gles2 (nur die allgemeine Statik blieb für die Implementierung von vertex_id übrig), und in Windows / dx11-Versionen verschwanden die Puffer aufgrund der nativen vertex_id und der Fähigkeit von d3d api, null zu akzeptieren, anstatt mit dem Vertex-Puffer zu verknüpfen.
Daher verbraucht die Win-Version des Systems nach modernen Maßstäben überhaupt keinen Speicher, egal wie viele Partikel wir anzeigen möchten. Nur ein kleiner konstanter Puffer mit Parametern, ein Puffer von Positionen / Basen (60 Vektorpaare erwiesen sich als ausreichend, mit einem Rand für jeden Fall) und, falls erforderlich, Textur. Leistungsmessungen zeigen eine Geschwindigkeit nahe an synthetischen Tests.
Darüber hinaus sah der „Schwanz“ in Effekten wie Funken viel natürlicher aus, da durch Interpolation die Abtastung durch Frames entfernt werden konnte und der Emitter seine Position reibungslos änderte, als ob Zeichnungsaufrufe mit einer Frequenz von Hunderten von Hertz ausgeführt würden.
Eigenschaften
Zusätzlich zur Grundfunktionalität des Partikelfluges (Geschwindigkeit, Beschleunigung, Schwerkraft, Widerstand des Mediums) benötigten wir eine bestimmte Menge an funktionellem „Fett“.
Infolgedessen wurden Bewegungsunschärfe (Partikel, die sich entlang eines Bewegungsvektors dehnen), Partikelorientierung über den Bewegungsvektor (dies ermöglicht beispielsweise das Erstellen einer Partikelkugel), die Änderung der Partikelgröße entsprechend der aktuellen Lebenszeit und Dutzende anderer kleiner Dinge implementiert.
Komplexität entstand mit Vektorfeldern: Da das System seinen Zustand (Position, Beschleunigung usw.) nicht für jedes Teilchen speichert, sondern jedes Mal durch die Bewegungsgleichung berechnet, waren eine Reihe von Effekten (wie die Bewegung des Schaums beim Rühren von Kaffee) im Prinzip unmöglich. Eine einfache Modulation von Geschwindigkeit und Beschleunigung durch das Geräusch von Perlin ergab jedoch Ergebnisse, die recht modern aussehen. Die Echtzeit-Rauschberechnung für so viele Partikel erwies sich als zu teuer (selbst bei einer Begrenzung auf fünf Oktaven), sodass eine Textur generiert wurde, aus der der Vertex-Shader dann eine Stichprobe erstellen würde. Um den Effekt eines gefälschten Vektorfeldes zu verstärken, wurde eine kleine Verschiebung der Abtastkoordinaten in Abhängigkeit von der aktuellen Zeit des Emitters hinzugefügt.
Der Zigarettenrauchtest verteilt die Anfangsgeschwindigkeit und die Beschleunigung auf das Perlingeräusch.Pixelförderer
Zunächst wollten wir nur die Farbe / Transparenz des Partikels je nach Zeit ändern. Ich habe dem Pixel-Shader mehrere Algorithmen hinzugefügt.
Textur Farbrotation - vereinfacht, Sünde (Farbe + Zeit). Ermöglicht bis zu einem gewissen Grad die Nachahmung des Permutationseffekts von AfterEffects.
Gefälschte Beleuchtung - Modulation der Farbe eines Partikels durch einen Gradienten in Weltkoordinaten, unabhängig vom Drehwinkel des Partikels.
Grenzentwicklung - Wenn sich ein Partikel im Raum bewegt, werden seine Grenzen (Alpha-Kanal) durch eine Kombination aus Scheinwerfer- und Perlin-Rauschen moduliert, wodurch sich eine Strömungsdynamik ergibt, die Wolken, Rauch und anderen Flüssigkeitseffekten sehr ähnlich ist.
Shader Pseudo Code:
b=perlin(uv)
In einer etwas komplizierten Version konnte dieser Shader Grenzen mit beliebiger Weichheit und mit einem Konturhighlight zeichnen, was dem Realismus „explosive“ Effekte hinzufügte.
Die ersten Experimente mit der Entwicklung von Grenzen.Was weiter?
Obwohl der Editor bereits einsatzbereit und in die Engine integriert war, hatten die Designer keine Zeit, einen einzigen Effekt darauf zu erzielen - das Projekt wurde geschlossen. Es gibt jedoch keine Hindernisse für die anderweitige Verwendung dieser Praktiken - beispielsweise für die Arbeit an der Demo-Revision.
Aus technologischer Sicht gibt es auch Bewegungsspielraum - jetzt sind beispielsweise mehrere Auswirkungen der Zerstörung von Drahtgitterobjekten in Betrieb:

Die Frage nach dem Sortieren von Partikeln für das Alpha-Blending bleibt bislang offen: Da im Shader alles analytisch betrachtet wird, gibt es tatsächlich keine Eingabedaten zum Sortieren. Aber es gibt ein großes Feld zum Experimentieren!
Während der Entwicklung von Titan World wurden im grafischen Teil des Spiels viele Tricks angewendet, aber beim nächsten Mal mehr darüber.
PS Hier können Sie in die Quell-Alpha-Engine
eintauchen . Beispiele befinden sich im Ordner release / samples, die Hauptsteuertasten sind Leerzeichen, Alt | Strg + Maus. Shader liegen direkt in FXP-Dateien, ihr Code ist über das Editorfenster verfügbar.