Die Implementierung des Aquarelleffekts in Spielen

Bild

Einführung


Als wir im Januar 2019 anfingen, über unser neues Farbspiel zu diskutieren . Wir haben sofort entschieden, dass der Aquarelleffekt das wichtigste Element ist. Inspiriert von dieser Bulgari- Werbung stellten wir fest, dass die Umsetzung der Aquarellmalerei mit der hohen Qualität der verbleibenden Ressourcen, die wir erstellen wollten, vereinbar sein sollte. Wir haben einen interessanten Artikel von Forschern von Adobe gefunden (1) . Die darin beschriebene Aquarelltechnik sah wunderbar aus und konnte aufgrund ihrer Vektor- (und nicht Pixel-) Natur auch auf schwachen Mobilgeräten funktionieren. Unsere Implementierung basiert auf dieser Studie. Wir haben Teile davon geändert und / oder vereinfacht, weil unsere Leistungsanforderungen unterschiedlich waren. Farbton .- Dies ist ein Spiel, daher mussten wir zusätzlich zur Zeichnung selbst die gesamte 3D-Umgebung rendern und die Spielelogik in einem Frame ausführen. Wir wollten auch sicherstellen, dass die Simulation in Echtzeit durchgeführt wurde und der Spieler sofort sah, was gezeichnet wurde.


Aquarellsimulation in Farbton.

In diesem Artikel werden wir die einzelnen Details der Implementierung dieser Technik in der Unity-Spiel-Engine teilen und darüber sprechen, wie wir sie so angepasst haben, dass sie nahtlos auf Low-End-Mobilgeräten funktioniert. Wir werden mehr über die Hauptphasen dieses Algorithmus sprechen, ohne jedoch den Code zu demonstrieren. Diese Implementierung wurde in Unity 2018.4.2 erstellt und später auf Version 2018.4.7 aktualisiert.

Was ist Farbton?


Farbton . - Dies ist ein Puzzlespiel, mit dem der Spieler die Level abschließen und die Farben der Aquarelle so mischen kann, dass sie den Farben des Origamis entsprechen. Das Spiel wurde im Herbst 2019 bei Apple Arcade für iOS, macOS und tvOS veröffentlicht.


Screenshot-Farbton.

Bedarf


Die in meinem Artikel beschriebene Technik kann in drei Hauptstufen unterteilt werden, die in jedem Frame ausgeführt werden:

  1. Generiere neue Spots basierend auf Spielereingaben und füge sie der Spotliste hinzu
  2. Malsimulation für alle Stellen auf der Liste
  3. Spot-Rendering

Im Folgenden werden wir detailliert darüber sprechen, wie wir die einzelnen Phasen implementiert haben.

Wir wollten 60 FPS erreichen, dh diese Stufen und die gesamte unten beschriebene Logik werden 60 Mal pro Sekunde ausgeführt.

Input bekommen


In jedem Frame transformieren wir die Eingabe des Players (abhängig von der Plattform kann es sich um eine Berührung, die Position der Maus oder den virtuellen Cursor handeln) in eine splatData- Struktur , die Position, Bewegungsvektor, Farbe und Druck enthält (2). Zuerst überprüfen wir die Schlaglänge des Spielers auf dem Bildschirm und vergleichen sie mit einem bestimmten Schwellenwert. Mit kurzen Wischbewegungen erzeugen wir einen Punkt pro Frame an der Eingabeposition. Im umgekehrten Fall füllen wir den Abstand zwischen dem Start- und dem Endpunkt des Wischens des Spielers mit neuen Punkten, die mit einer vorgegebenen Dichte erstellt wurden (dies gewährleistet eine konstante Farbdichte unabhängig von der Wischgeschwindigkeit). Die Farbe gibt die aktuell verwendete Farbe an, und die Neigung der Bewegung gibt die Richtung des Wischens an. Erstellte neue Spots werden einer Sammlung namens splatList hinzugefügt, die auch alle zuvor erstellten Spots enthält. Es wird verwendet, um Farbe in den folgenden Schritten zu simulieren und zu rendern. Jeder einzelne Punkt kennzeichnet einen „Tropfen“ Farbe, der gerendert werden muss - den Hauptbaustein der Aquarellmalerei. Die fertige Aquarellzeichnung ist das Ergebnis des Renderns von Dutzenden / Hunderten von sich überschneidenden Punkten. Außerdem wird dem neu erstellten Spot der Wert der Lebensdauer (in Frames) zugewiesen, der bestimmt, wie lange der Spot simuliert werden kann.


Ein Beispiel für die Interpolation langer Wischpunkte. Hohlkreise kennzeichnen Punkte, die in regelmäßigen Abständen erzeugt werden.

Segeltuch


Wie echte Farbe brauchen wir eine Leinwand. Um dies zu implementieren, haben wir einen begrenzten Bereich im 3D-Raum erstellt, der wie ein Blatt Papier aussieht. Die Eingabekoordinaten des Players und alle anderen Vorgänge, z. B. das Rendern eines Netzes, werden im Canvas-Bereich aufgezeichnet. In ähnlicher Weise hängt die Größe eines Puffers, der zum Simulieren des Zeichnens verwendet wird, in Pixel von der Größe der Leinwand ab. Der in diesem Artikel verwendete Begriff "Canvas" ist in keiner Weise mit der Canvas-Klasse von Unity UI verknüpft.


Das grüne Rechteck zeigt den Leinwandbereich im Spiel

Stelle


Optisch wird der Punkt durch ein rundes Netz dargestellt, dessen Rand aus 25 Eckpunkten besteht. Sie können es als „Tropfen“ wahrnehmen, den ein nasser Pinsel auf einem Stück Papier hinterlässt, wenn Sie es für einen sehr kurzen Moment berühren. Wir fügen der Position jedes Scheitelpunkts einen kleinen zufälligen Versatz hinzu, der die Ungleichmäßigkeit der Kanten der Farbflecken sicherstellt.


Beispiele für Maschennetze.

Für jeden Scheitelpunkt speichern wir auch den Geschwindigkeitsvektor nach außen, der dann in der Simulationsphase verwendet wird. Wir generieren mehrere solcher Netze mit kleinen Abweichungen zwischen den Formularen und speichern ihre Daten in einem skriptuemy-Objekt ( einem skriptfähigen Objekt ). Jedes Mal, wenn ein Spieler in Echtzeit einen Punkt zeichnet, weisen wir ihm ein zufällig aus diesem Satz ausgewähltes Netz zu. Es ist erwähnenswert, dass die Leinwand bei unterschiedlichen Bildschirmauflösungen eine unterschiedliche Größe in Pixel hat. Damit auf allen Geräten der Koeffizient der Größe der Spots gleich ist, ändern wir zu Beginn des Spiels die Skalierung entsprechend der Größe der Leinwand.


Ein Beispiel für Punktvektoren, die mit neuen Punktdaten gespeichert wurden.

Wenn ein Punktnetz erzeugt wird, speichern wir auch seinen „Benetzungsbereich“, der eine Reihe von Pixeln definiert, die sich innerhalb der ursprünglichen Punktgrenzen befinden. Der Benetzungsbereich wird verwendet, um die Advektion zu simulieren . Während der Ausführung der Anwendung zum Zeitpunkt der Erstellung jedes neuen Spots markieren wir die Leinwand darunter als nass. Wenn wir die Bewegung von Farbe simulieren, lassen wir sie sich über die Bereiche der Leinwand „ausbreiten“, die bereits nass geworden sind. Wir speichern den Feuchtigkeitsgehalt der Leinwand im globalen Wetmap- Puffer , der aktualisiert wird, wenn jeder neue Punkt hinzugefügt wird. Neben der Teilnahme am Mischen zweier Farben spielt die Advektion eine wichtige Rolle für das endgültige Erscheinungsbild des Malstrichs.


Wetmap- Füllung , Pixel innerhalb der Punktform (grüner Kreis) markieren den Wetmap- Puffer (Raster) als nass (grün). Der Wetmap-Puffer selbst hat eine viel höhere Auflösung.

Zusätzlich enthält jeder Punkt einen Opazitätswert , der eine Funktion seiner Fläche ist. es repräsentiert den Effekt der Speicherung von Pigment (eine konstante Menge an Pigment im Fleck). Wenn die Größe eines Spots während der Simulation zunimmt, nimmt seine Deckkraft ab und umgekehrt.


Ein Beispiel für Farbe ohne Advektion (links) und damit (rechts).


Beispiele für Farbvorschub.

Simulationszyklus


Nachdem die Eingabe des Players im aktuellen Frame empfangen und in neue Spots konvertiert wurde, besteht der nächste Schritt darin, die Spots zu simulieren, um die Verbreitung von Aquarellen zu simulieren. Zu Beginn dieser Simulation haben wir eine Liste der Punkte, die aktualisiert werden müssen, und eine aktualisierte Wetmap .

In jedem Frame gehen wir die Liste der Punkte um und ändern die Positionen aller Eckpunkte der Punkte unter Verwendung der folgenden Gleichung:


Dabei gilt: m ist der neue Bewegungsvektor, a ist der konstante Korrekturparameter (0,33), b ist der Bewegungssteigungsvektor = normalisierte Richtung des Schlagens des Spielers multipliziert mit 0,3, cr ist der Skalarwert der Leinwandrauheit = Random.Range (1,1 + r), r ist der globale Rauheitsparameter, für Standardfarbe setzen wir ihn auf 0,4, v ist der Geschwindigkeitsvektor, der im Voraus mit dem Punktnetz erstellt wurde , vm ist der Geschwindigkeitsfaktor, der Skalarwert, den wir in einigen Situationen lokal verwenden, um die Advektion zu beschleunigen, x (t +) 1) - mögliche neue Scheitelpunktposition, x (t) - aktuelle Scheitelpunktposition, brIst der Verzweigungsrauheitsvektor = (Random.Range (-r, r), Random.Range (-r, r)), ist w (x) der Benetzungswert im Wetmap-Puffer.

Das Ergebnis solcher Gleichungen wird als voreingenommener Zufallslauf bezeichnet . Es ahmt das Verhalten von Partikeln in echten Aquarellfarben nach. Wir versuchen, jeden Scheitelpunkt des Punkts von seiner Mitte ( v ) nach außen zu bewegen , um Zufälligkeit hinzuzufügen. Dann ändert sich die Bewegungsrichtung geringfügig mit der Richtung des Strichs ( b ) und wird erneut durch eine andere Rauheitskomponente ( br ) randomisiert . Dann wird diese neue Scheitelpunktposition mit einer Wetmap verglichen . Wenn die Leinwand an der neuen Position bereits nass war (Wert im Wetmap- Puffergrößer als 0), dann geben wir dem Scheitelpunkt eine neue Position x (t + 1) , andernfalls ändern wir seine Position nicht. Infolgedessen verteilt sich die Farbe nur in den Bereichen der Leinwand, die bereits nass waren. In der letzten Phase berechnen wir den Spotbereich neu, der im Renderzyklus verwendet wird, um seine Deckkraft zu ändern.


Mikroskaliges Beispiel einer Advektionssimulation zwischen zwei aktiven Farbflecken.

Renderzyklus - Nasspuffer


Nachdem Sie die Spots nachgezählt haben, können Sie mit dem Rendern beginnen. Am Ausgang nach der Emulationsphase stellt sich heraus, dass das Netz der Punkte häufig deformiert ist (es treten beispielsweise Schnittpunkte auf). Daher verwenden wir für die korrekte Wiedergabe ohne zusätzliche Kosten für die wiederholte Triangulation eine Lösung mit Schablonenpuffer mit zwei Durchgängen. Die Unity Graphics- Zeichenoberfläche wird zum Rendern von Spots verwendet , und der Renderzyklus wird innerhalb der Unity OnPostRender- Methode ausgeführt . Punktnetze werden gerendert, um die Textur ( WetBuffer ) mit einer separaten Kamera zu rendern . Zu Beginn des Zyklus wird wetBuffer gelöscht und mithilfe von Graphics.SetRenderTarget (wetBuffer) als Renderziel festgelegt . Weiter für jeden aktiven Spot aus splatList Wir führen die im folgenden Diagramm gezeigte Sequenz aus:


Renderzyklusdiagramm.

Wir beginnen mit der Reinigung des Schablonenpuffers vor jedem Punkt, damit der Zustand des Schablonenpuffers des vorherigen Punkts den neuen Punkt nicht beeinflusst. Dann wählen wir das Material aus, mit dem der Punkt gezeichnet wird. Dieses Material ist für die Farbe des Spots verantwortlich und wir wählen es basierend auf dem Farbindex aus, der in splatData gespeichert wurde, als der Spieler den Spot gezeichnet hat . Dann ändern wir die Farbopazität (Alphakanal) basierend auf der im vorherigen Schritt berechneten Fläche des Punktnetzes. Das Rendern selbst wird mit einem Schablonenpuffer-Shader mit zwei Durchgängen durchgeführt. Im ersten Durchgang (Material.SetPass (0)) übergeben wir das ursprüngliche Punktnetz, um die Koordinaten aufzuzeichnen, in die das Netz gefüllt ist. Mit diesem Pass ColorMaskhat den Wert 0 zugewiesen, sodass das Netz selbst nicht gerendert wird. Im zweiten Durchgang (Material.SetPass (1)) verwenden wir das um das Punktnetz beschriebene Viereck. Wir überprüfen den Wert im Schablonenpuffer für jedes Pixel des Vierecks; Wenn der Wert eins ist, wird das Pixel gerendert, andernfalls wird es übersprungen. Als Ergebnis dieser Operation rendern wir dieselbe Form wie das Punktnetz, aber es enthält sicherlich keine unerwünschten Artefakte, z. B. Selbstüberschneidungen.


Das Verfahren zur Durchführung der Doppelschablonenpuffertechnik (von links nach rechts). Beachten Sie, dass dieser Schablonenpuffer eine viel höhere Auflösung als gezeigt hat, sodass er seine ursprüngliche Form mit großer Genauigkeit beibehalten kann.


Ein Beispiel für drei sich überschneidende Punkte, die auf herkömmliche Weise gerendert wurden, was zum Auftreten von Artefakten führte (links) und die Verwendung der Zwei-Pass-Schablonenpuffertechnik unter Eliminierung aller Artefakte (rechts).

Nachdem alle Spots in WetBuffer gerendert wurden, wird es in der Spielszene angezeigt. Unsere Leinwand verwendet einen provisorischen Shader, der einen WetBuffer , eine diffuse Papierkarte und eine normale Papierkarte kombiniert .


Canvas-Shader: nur WetBuffer (links), hinzugefügte Papierstruktur (Mitte), normale Karte hinzugefügt (rechts).

Das Spiel unterstützt einen Modus für Menschen mit Farbenblindheit, in dem separate Muster über die Farbe gelegt werden. Um dies zu erreichen, haben wir das Material der Flecken geändert, indem wir die Textur des Musters mit Kacheln hinzugefügt haben. Muster folgen den Regeln zum Mischen der Farben des Spiels, z. B. Blau (Balken) + Gelb (Kreise) ergeben Grün (Kreise in den Balken) an der Kreuzung. Um Muster nahtlos zu mischen, müssen sie im selben UV-Raum gerendert werden. Wir passen die UV-Koordinaten des im zweiten Durchgang des Schablonenpuffers verwendeten Vierecks an und teilen die x- und y-Positionen (die im Canvas-Bereich angegeben sind) durch die Breite und Höhe der Canvas. Als Ergebnis erhalten wir die korrekten Werte von u, v im Raum von 0 bis 1.


Ein Beispiel für Farbenblindheitsmuster.

Optimierung - Puffer für getrocknete Stellen


Wie oben erwähnt, bestand eine unserer Aufgaben darin, mobile Geräte mit geringem Stromverbrauch zu unterstützen. Spot-Rendering stellte sich als Engpass unseres Spiels heraus. Jeder Punkt erfordert drei Zeichenaufrufe (zwei Durchgänge aufrufen + Schablonenpuffer löschen). Da die Mallinie zehn oder Hunderte von Punkten enthält, steigt die Anzahl der Zeichenaufrufe schnell an und führt zu einem Rückgang der Bildrate. Um dies zu bewältigen, haben wir zwei Optimierungstechniken angewendet: erstens das gleichzeitige Zeichnen aller „getrockneten“ Punkte in dryBuffer und zweitens die lokale Beschleunigung des Trocknens der Punkte nach Erreichen einer bestimmten Anzahl aktiver Punkte.

dryBufferWird dem Renderzyklus eine zusätzliche Rendertextur hinzugefügt. Wie bereits erwähnt, hat jeder Spot eine Lebensdauer (in Frames), die mit jedem Frame abnimmt. Nachdem die Lebensdauer 0 erreicht hat, gilt der Fleck als „ausgetrocknet“. Trockene Stellen werden nicht mehr simuliert, ihre Form ändert sich nicht und daher müssen sie nicht in jedem Frame erneut gerendert werden.


DryBuffer in Aktion; Die grauen Flecken zeigen die in dryBuffer kopierten Flecken.

Jeder Punkt , deren Lebensdauer erreicht 0 von dem entfernt splatList und „kopiert“ zu dryBuffer . Während des Kopiervorgangs wird der Renderzyklus wiederverwendet, und diesmal wird dryBuffer als Ziel- Rendertextur festgelegt .

Das richtige Mischen zwischen WetBuffer und DryBuffer kann nicht durch einfaches Überlappen der Puffer im Canvas-Shader erreicht werden, da die Rendertextur des WetBuffer- Puffersenthält Spots, die bereits mit dem Alpha-Wert gerendert wurden (was vormultipliziertem Alpha entspricht). Wir haben dieses Problem umgangen, indem wir dem Start des Renderzyklus einen Schritt hinzugefügt haben, bevor wir die Spots iterativ durchlaufen haben. Zu diesem Zeitpunkt rendern wir ein Viereck von der Größe einer Kamera- Trimmpyramide , die dryBuffer anzeigt . Dank dessen wird jeder Fleck, der in wetBuffer gerendert wird, bereits mit trockenen, zuvor gestrichenen Flecken gemischt.


Eine Mischung aus nassen und getrockneten Stellen.

Der dryBuffer- Puffer sammelt alle "getrockneten" Stellen und wird zwischen den Frames nicht gelöscht. Daher kann der gesamte Speicher, der abgelaufenen Flecken zugeordnet ist, gelöscht werden, nachdem sie in den Puffer „kopiert“ wurden.


Dank der Optimierung mit dryBuffer sind die Farbmengen , die ein Spieler auf die Leinwand auftragen kann , nicht mehr begrenzt.

Wenn Sie die dryBuffer- Technik separat verwenden, kann der Spieler mit nahezu unendlich viel Farbe zeichnen, garantiert jedoch keine gleichbleibende Leistung. Wie oben erwähnt, hat der Malstrich eine konstante Dicke , die durch Zeichnen unter Interpolation vieler Punkte zwischen dem Start- und dem Endpunkt des Wischens erreicht wird. Bei vielen schnellen und langen Wischen kann der Spieler eine große Anzahl von aktiven Punkten erzeugen. Diese Spots werden simuliert und über die durch ihre Lebensdauer festgelegte Anzahl von Frames gerendert, was letztendlich zu niedrigeren Frameraten führt.

Um eine stabile Bildrate zu gewährleisten, haben wir den Algorithmus so geändert, dass die Anzahl der aktiven Spots durch einen konstanten Wert von maxActiveSplats begrenzt wurde . Alle Flecken, die diesen Wert überschreiten, „trocknen“ sofort aus. Dies wird erreicht, indem die Lebensdauer der ältesten aktiven Spots auf 0 reduziert wird, weshalb sie früher in den Puffer für getrocknete Spots kopiert werden. Da wir, wenn wir die Lebensdauer verkürzen, einen Punkt im unvollständigen Zustand der Simulation erhalten (was sehr interessant aussehen wird), erhöhen wir gleichzeitig die Ausbreitungsgeschwindigkeit der Farbe. Aufgrund der Geschwindigkeitssteigerung erreicht der Spot fast die gleiche Größe wie bei normaler Geschwindigkeit mit einer normalen Lebensdauer.


Demonstration von maximal 40 (oben) und 80 (unten) aktiven Spots. In dryBuffer kopierte getrocknete Stellen werden grau angezeigt. Der Wert gibt die „Menge“ an Farbe an, die gleichzeitig simuliert werden kann.

Der Wert von maxActiveSplats ist der wichtigste Leistungsparameter. Er ermöglicht es uns, die Anzahl der Zeichenaufrufe , die wir dem Aquarell-Rendering zuweisen können, genau zu steuern. Wir legen es beim Start fest, basierend auf der Plattform- und Geräteleistung. Sie können diesen Wert auch während der Anwendungsausführung ändern, wenn eine Verringerung der Bildrate festgestellt wird.

Fazit


Die Implementierung dieses Algorithmus ist zu einer interessanten und herausfordernden Aufgabe geworden. Wir hoffen, dass die Leser den Artikel genossen haben. Sie können Fragen in den Kommentaren zum Original stellen . Wenn Sie unser Aquarell in Aktion schätzen möchten, versuchen Sie, Tönung zu spielen. auf der Apple Arcade .


Screenshot eines Spiels, das auf Apple TV läuft

(1) S. DiVerdi, A. Krishnaswamy, R. MÄch und D. Ito, "Malen mit Polygonen: Eine prozedurale Aquarellmaschine", in IEEE Transactions on Visualization and Computer Graphics, vol. 19, nein. 5, pp. 723–735, Mai 2013. doi: 10.1109 / TVCG.2012.295

(2) Der Druck wird nur beim Zeichnen des Apple Pencil auf einem iPad berücksichtigt.

All Articles