Simulation von 3D-Gebäuden in einem 2D-Spiel / Sudo Null IT News

Hallo alle! Wir entwickeln seit mehreren Jahren als kleines Team eine 2D-Strategie Norland ist eine Simulation eines mittelalterlichen Königreichs.

Das Spiel ist zweidimensional, entwickelt auf Game Maker Studio 2, und während der Arbeit stand ich vor vielen Aufgaben a la „Es sollte schön sein“. Irgendwo musste ich mir ein eigenes Fahrrad einfallen lassen, irgendwo hatte ich das Glück, auf eine Beschreibung zur Lösung ähnlicher Probleme zu stoßen.

Einmal hat mich ein Artikel über das Rendern in Graveyard Keeper sehr inspiriert – das ist ein sehr cooles Material für einen 2D-Spieleentwickler, es gibt ziemlich viele ähnliche im Netz. Daher hoffe ich, dass mein Artikel auch jemandem als Inspirationsquelle dient.

Endergebnis im SpielEndergebnis im Spiel

Schatten

Um “dreidimensionale” Schatten für ein 2D-Gebäude zu erstellen, müssen Sie dieses Gebäude zuerst mit einigen Grundelementen markieren – Würfel, Zylinder, Prismen usw. Aber diese Primitiven werden keinen “Schatten werfen” im üblichen Sinne des Wortes – sie werden selbst Schatten sein.

Stellen Sie sich zum Beispiel einen Würfel vor (es ist also ein Parallelepiped, aber das Wort Würfel ist kürzer). Er besteht aus acht Gipfeln. Die x-Komponente der Normalen jedes Scheitelpunkts wird mit 0,0 geschrieben, wenn es sich um die Basis handelt, und mit 1,0, wenn es sich um die Oberseite des Würfels handelt. Die Texturkoordinaten in der x-Komponente sind die Höhe der Form, die den Schatten wirft.

1 - das Oberteil bewegt sich, 0 - das Oberteil bleibt an Ort und Stelle1 – das Oberteil bewegt sich, 0 – das Oberteil bleibt an Ort und Stelle

Der Rest der Arbeit geschieht im Vertex-Shader. Die Parameter der Sonne (der Winkel über dem Horizont und die Länge der Schatten) werden darauf übertragen. Dann ist es elementar – die Scheitelpunkte, für die normal.x gleich eins ist, werden um den angegebenen Abstand auf den gewünschten Winkel verschoben, und die restlichen Scheitelpunkte bleiben an Ort und Stelle.

Alle Transformationen werden als affin betrachtet, d.h. Parallele Würfelflächen bleiben nach Transformationen parallel.

Vereinfachte Ansicht des Vertex-Shaders (GLSL):

void main () {float move_amount = u_vSun.x; float shadow_length = u_vSun.y * 1,3; Floathöhe = in_TextureCoord0.x; vec4 object_space_pos = vec4(0.0); if (in_Normal.x > 0.5) { object_space_pos = vec4( in_Position.x + sin(move_amount) * (shadow_length * height), in_Position.y + cos(move_amount) * (shadow_length * height), in_Position.z, 1.0 ); } else { object_space_pos = vec4( in_Position.x, in_Position.y, in_Position.z, 1.0 ); } gl_Position = gm_Matrizen[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos; }Tatsächlich sieht der Würfel wie auf Bild 2 aus. D.h.  und Spitzen "eines" und Spitzen "0" liegen paarweise an denselben Koordinaten.  Abbildung 3 zeigt, wie die Eckpunkte verschoben werden "eines" relativ zur Lichtquelle.  Abbildung 4 zeigt das endgültige Aussehen des Schattens nach dem Ausführen des Fragment-Shaders, der einfach alles mit Schwarz füllt.Tatsächlich sieht der Würfel wie auf Bild 2 aus. D.h. und Scheitelpunkte “1” und Scheitelpunkte “0” liegen paarweise in denselben Koordinaten. Abbildung 3 zeigt, wie sich die “1”-Eckpunkte relativ zur Lichtquelle bewegen. Abbildung 4 zeigt das endgültige Aussehen des Schattens nach dem Ausführen des Fragment-Shaders, der einfach alles mit Schwarz füllt.

Andere Primitive werden auf ähnliche Weise erstellt – ein Zylinder und Prismen für Dächer zweier Arten (horizontal und vertikal). Ergebnis:

Primitive, von denen Schatten für alle Gebäude gesammelt werdenPrimitive, von denen Schatten für alle Gebäude gesammelt werden

Als Ergebnis besteht der Würfel aus 8 Ecken und 10 Dreiecken (2 Dreiecke von der Basis des Würfels können weggeworfen werden – sie beeinflussen nichts, da sie nach dem Gießen immer noch nicht sichtbar sind).

Damit die Schatten am Ende durchscheinend sind, aber gleichzeitig an den Stellen, an denen sich die Schatten berühren, keine Schichtung entsteht, müssen Sie alle Schatten mit Alpha gleich eins auf die Oberfläche zeichnen (auftauchen). Und schon zeichnest du die Fläche mit dem gewünschten Alpha. Außerdem kann die Oberfläche mit einem Gaußschen Weichzeichnungs-Shader weichgezeichnet werden, um die Unvollkommenheit von Low-Poly-Schatten auszugleichen.

Z-Sortierung und Heightmap

Unser Spiel ist 2D (mit Ausnahme der Windmühlenflügel – die Verwendung von Sprite-Animationen in diesem Fall wäre aufgrund der großen Größe jedes Frames des Animationsstreifens extrem verschwenderisch, daher mussten wir ein 3D-Modell verwenden), aber die Charaktere sollten es tun sowohl vor als auch hinter Gebäuden erscheinen können . In einfachen Fällen wird das irgendwie so gelöst:

Tiefe = -y; /* Standardkonstrukt im Game Maker für automatische Tiefensortierung. Seit einigen Jahren gilt es aufgrund der Einführung eines Schichtsystems in die Engine als obsolet. Aber es funktioniert immer noch. */

Wir brauchen den Charakter aber auch, damit er innerhalb des Gebäudes korrekt dargestellt wird – und es können viele Objekte darin sein.

Dies hilft uns beim automatischen Sortieren auf der GPU mit Z-Buffer.

Für diejenigen, die es nicht wissen – Z-Buffer ist eine Datenstruktur, die die Tiefe jedes Pixels als eine Bruchzahl von 0 bis 1 beschreibt. Wenn Sie Farbe anstelle von Zahlen ersetzen, erhalten Sie ein Schwarzweißbild.

Bild aus WikipediaBild aus Wikipedia

In GMS 2 kann der Tiefenpuffer mit einigen Einschränkungen bearbeitet werden – er kann nicht als Textur abgerufen werden, er kann nicht in Shadern gelesen und geändert werden (standardmäßig ja, aber es gibt Problemumgehungen). Aber Sie können immer noch einfach Z-sortieren! Und das brauchen wir.

Mit Z-Sortieren können Sie Pixel mit größerer Tiefe verwerfen (Dies ist das Standardverhalten, kann aber geändert werden)als die bereits auf dem Bildschirm gezeichneten (bzw. im Z-Puffer).

Diese. Wenn Sie ein Gebäude Stück für Stück zeichnen, seine Tiefe für jedes Stück angeben und dann ein Zeichen zeichnen (mit einer Tiefe gleich seinem -y), dann werden die Pixel des Zeichens, die sich in der Bedingungstabelle befinden, die Z-Sortierung verwenden einfach nicht gezeichnet und es stellt sich heraus, dass das Zeichen korrekt von dieser Tabelle überdeckt wird.

Aber dafür müssen Sie das Gebäude markieren. Auf dem Sprite platziere ich Quads, die als vertikale Wand konfiguriert werden können. (in diesem Fall haben die oberen Scheitelpunkte des Quads die gleiche Tiefe wie die unteren Scheitelpunkte) oder horizontale Fläche (Die oberen und unteren Scheitelpunkte des Quads haben unterschiedliche Tiefen, abhängig von ihrer Position auf der y-Achse).

Im Screenshot erscheinen Wände als gefüllte orangefarbene Rechtecke und Oberflächen als leere weiße Rechtecke. Bei einem Quad kann man auch die Höhe über dem Boden angeben, aber das sind schon Nuancen.

Markierte Quads zur Bestimmung der Z-Tiefe verschiedener Teile des Sprites am Beispiel des Inneren des TempelsMarkierte Quads zur Bestimmung der Z-Tiefe verschiedener Teile des Sprites am Beispiel des Inneren des Tempels

Diese beschrifteten Quads werden dann in Polygone umgewandelt und in den Vertexpuffer des Objekts geschrieben. Zusätzlich zur Z-Ebene tragen die Vertices Informationen über die UV-Koordinaten der Gebäudetextur. (sowie UV-Texturkarten von nachts leuchtenden Fenstern, Normalen und andere Hilfsparameter für beliebige Effekte).

Der Nachteil dieses Ansatzes ist für diejenigen offensichtlich, die bereits mit Z-Buffer gearbeitet haben. Schließlich unterstützt es keine Transluzenz. Diese. Wenn zuerst ein durchscheinendes Glas in den Puffer gezogen wurde, werden wir später, wenn wir versuchen, etwas hinter dieses Glas zu zeichnen, scheitern, weil das Glas näher ist als die neuen Pixel, die gezeichnet werden. Um diesen Punkt zu umgehen, müssen Sie zuerst die gezeichneten Assets auf der CPU von weit nach nah sortieren und sie erst dann auf den Bildschirm (und in den Puffer) ziehen. Aber wir sind das einfacher angegangen – es gibt keine lichtdurchlässigen Elemente in unseren Gebäuden 🙂

Jenseits der Z-Quads (also habe ich diese Polygone benannt, um die Z-Ebene anzuzeigen)ein wichtiger Bestandteil des Asset-Markups, sind H-Quads (Höhenquadrate). Diese Quads werden benötigt, um Gebäude mit Höhenunterschieden zu versehen (z. B. Stufen und eine Kanzel in einem Tempel). Hier ist alles einfach – das ist ein Rechteck mit Höhenangabe. Dann finden wir zur Laufzeit den Schnittpunkt des unteren Punkts des Zeichens mit dem H-Quad darunter und verschieben das Zeichen nach oben oder unten auf die angegebene Höhe.

Z-Quads und H-Quads im EinsatzZ-Quads und H-Quads im Einsatz

Normale und Fensterkarten

Es gibt eine solche Technologie Normal Mapping. Vereinfacht ausgedrückt ist dies eine Textur, bei der die x-, y-, z-Komponenten des Normalenvektors für dieses Pixel in die r-, g-, b-Kanäle jedes Pixels geschrieben werden. Diese. “wohin gerichtet ist” dieses Pixel relativ zum Weltkoordinatensystem.

Aufgrund der Besonderheiten beim Schreiben eines Vektors im RGB-Äquivalent ist die Ausgabe normalerweise eine lila-rot-grüne Karte. Und wenn Sie es auf einen einfachen Shader übertragen, können Sie durch Bewegen einer virtuellen Glühbirne mit Licht das Schattenspiel auf einem 2D-Sprite darstellen. Und das brauchen wir.

Es bleibt zu verstehen, wo man diese normale Karte bekommt. Mit einem 3D-Modell ist alles einfach – mit modernen 3D-Modellierungspaketen können Sie mit vier Klicks eine normale Karte erstellen, da das Modell alle erforderlichen Informationen dafür enthält.

Bei einem 2D-Sprite ist es anders – es ist nur ein Bild. Und nur die Person, die sie ansieht (und wahrscheinlich moderne neuronale Netze) feststellen, dass dies die Dachschräge ist, dies aber die Fensterbank ist.

Aber es gibt einen Ausweg. Für manuell oder halbautomatisch (mit erwartetem durchschnittlich-schlechtem Ergebnis) Um normale Karten aus 2D-Bildern zu erstellen, gibt es eine gewisse Menge an Software. Zum Beispiel SpriteLamp, SpriteIlluminator, Laigter, Gimp-Plugins usw. Sie können sogar Ihre eigene kleine Software mit einem Pinselwerkzeug und einem Trackball schreiben, um den erforderlichen Winkel der gezeichneten Normalen festzulegen.

Ein einfaches Gebäude in einem solchen Programm zu skizzieren, erfordert nicht viel Aufwand. Hier ist die senkrechte Wand, hier das Dach und hier der hervorstehende Teil, der stärker hervorgehoben werden kann. Mehr als eine Farbe ist oft genug. Sie können in dieser Angelegenheit nicht super genau sein, weil. Wir speichern die Normal Map mit der 0,25-fachen Größe des Gebäude-Sprites, sodass Unvollkommenheiten durch Interpolation abgedeckt werden.

Vereinfachter GLSL-Vertex-Shader:

void main () { vec4 normal_raw = texture2D (gm_BaseTexture, v_vNormalUV); vec3 normal = normalisieren (normal_raw.rgb * 2,0 – 1,0); vec3 light_dir = normalize(u_vLightDir); // Grundfarbe vec4 color = v_vColour * texture2D(gm_BaseTexture, v_vTexcoord); vec3 Lichtfarbe = vec3 (1,0, 1,0, 0,98); vec3 shadow_color = vec3 (0,76, 0,76, 1,0); float light_factor = smoothstep(0.2, 0.8, dot(normal, light_dir)); // Licht und Schatten vec3 lighting_color = mix(shadow_color * 0.5, 1.1 * light_color, light_factor); // Grundfarbe mit Licht und Schatten vec4 result_color = mix(color, vec4(color.rgb * lighting_color, color.a), normal_raw.a * u_fLightStrength); if (result_color.a < u_fAlphaDiscardValue) verwerfen; gl_FragColor = result_color; }Teilweise Gebäude-Rendering-PipelineTeilweise Gebäude-Rendering-Pipeline

Neben der normalen Karte wird auch eine Fensterkarte verwendet. Bei Dämmerung sollten die Pixel des zu zeichnenden Gebäudes, die den weißen Pixeln der Fensterkarte entsprechen, auf Orange gesetzt werden.

Natürlich sind dies nicht alle Effekte, die wir in unserem Rendering verwenden. Auf dem Gif vom Anfang des Artikels gibt es noch Licht um die Fenster, LUT-Farbkorrektur, um den Tageszeitwechsel zu simulieren (um so etwas umzusetzen, kann ich diesen Artikel empfehlen), Rauchpartikel von Rohren usw . Und wenn Sie die Kamera näher an das Gebäude heranführen, wird das Dach mit dem Auflösungseffekt entfernt. Aber das ist eine ganz andere Geschichte.

Aufmerksamen Lesern ist vielleicht aufgefallen, dass der Schatten nicht ganz (oder besser gesagt überhaupt nicht) mit der Beleuchtung des Gebäudes aus der normalen Karte übereinstimmt. So kommt der Schatten von unten, das Licht fällt also auf die hintere, für den Spieler nicht sichtbare Wand des Gebäudes. Gleichzeitig ist die Fassade aber weiterhin beleuchtet.

Wenn der Schatten des Gebäudes oben wäre (also die Sonne die Fassade beleuchtete), dann hätte dieser Vorfall vermieden werden können, aber ich habe den Schatten bewusst genau von unten gesetzt, weil es ist einfach viel schöner. Vor allem mittags.

Ich hoffe, der Artikel kann jemandem helfen, denn 2D-Spiele werden noch nicht sterben und jeder liebt Schönheit.

Similar Posts

Leave a Reply

Your email address will not be published.