Normal Mapping via Surfaces und Shader (2D) – Step by Step
Abb. 1
Vorwort
Dieses (mein erstes) Tutorial soll dazu dienen, ein Grundverständnis von Surfaces, Normal Maps und dem Implementieren von Shadern zu vermitteln, und denjenigen die nach schönen Shader-Effekten lechzen einen leichten Weg zu zeigen, um bestehende Shader (die GM-Community ist großartig und stellt viele frei zur Verfügung) einzubauen.
Im Verlauf dieses Tutorials nehmen wir eine kachelbare Textur, von der ausgehend wir dann eine Normal Map erstellen (hierfür gibt es verschiedene Filter und Plugins, im Tutorial greifen wir auf den nVidia Normal Map Filter zurück). Durch einen Shader von xygthop3 wird die Diffuse-Textur dann mit der Normal Map kombiniert und der Effekt wird mit Surfaces über den Viewbereich sichtbar gemacht.
Achtung: Das Tutorial ist ziemlich lang geraten, was auch daran liegt, dass es eine Schritt-für-Schritt-Anleitung (mit vielen Schritten) ist. Dafür habe ich mich sehr bemüht, es auch für Einsteiger möglichst verständlich zu machen.
Verlauf des Tutorials (Hauptteil):
1. Vorbereitung
2. Aufsetzen des Projektes
3. Theorie über Normal Maps und Bumpmaps
4. Die Erstellung der Normal Map mit Photoshop
5. Aufbau der Objekt-Surface und View-Bewegung
6. Einbau des Shaders und der Normal-Map-Textur
7. Kombination von drei Surfaces mit einem Shader: Finaler Effekt
Wichtig: Hier wird nicht beschrieben wie man einen Normal-Map-Shader schreibt, bzw. welche Techniken dahinterstecken, da mein Wissensstand dazu zum jetzigen Zeitpunkt noch lange nicht ausreicht und es sowieso den Rahmen dieser Anleitung sprengen würde. Wer tiefer in die Materie gehen möchte, dem empfehle ich die folgenden Links.
Weiterführendes
Shader Basics, von LEWA: Klick
Linksammlung von gm-d.de: Klick
Shadersammlung von xygthop3 in der GMC (inkludiert auch für jeden Shader eine Beispieldatei): Klick
(beim Verwenden nicht vergessen den Ersteller in den Credits zu erwähnen, er bietet seine Shader immerhin gratis an!)
Bumpmap-Artikel auf Wikipedia: Klick
Normal-Map-Artikel auf Wikipedia: Klick
Voraussetzungen
> Installierte Version von Gamemaker: Studio
> Grundlegende GML-Kenntnisse
> Kenntnisse über die Funktionsweise von Draw-Events
> Installiertes Bildbearbeitungsprogramm
> Kenntnis vom GM-Ressourcenbaum und generell vom UI des Gamemakers
> Aufmerksamkeitsspanne von über 3 Minuten
1. Vorbereitung
Als ersten Schritt installieren wir ein Normal-Map-Plugin. Dafür sind hier Links für Photoshop und GIMP aufgelistet. Falls du ein anderes Bildbearbeitungsprogramm verwendest wirst du sicher auch selbst fündig über eine Suche mit deiner bevorzugten Suchmaschine.
Download von der nVidia-Seite für Photoshop: Klick
Nach dem Download starten wir mit einem Doppelklick die Installation vom Downloadmanager aus, geben die gewünschte Zieldestination an (normalerweise sollte die Standardeinstellung passen) und los geht’s. Nach der Installation muss Photoshop neugestartet werden, falls bereits eine Instanz läuft.
Download vom ähnlich aufgebautem Plugin für GIMP: Klick
Jetzt wo wir das haben, und das Bildbearbeitungsprogramm auch bereit für den Einsatz ist, brauchen wir noch die Textur, welche wir später auch als Ausgangsmaterial für die Normal-Map benutzen.
Diese kannst du dir hier runterladen: Klick
… oder wenn du ganz ungeduldig bist, gibt es hier ein vorbereitetes Projekt-ZIP: Klick
… womit du das anschließende Kapitel „Aufsetzen des Projektes“ auch überspringen kannst.
Ich empfehle aber, es sich zumindest durchzulesen.
2. Aufsetzen des Projektes
Nun gut, lasst uns beginnen indem wir ein neues Projekt starten und benennen, zum Beispiel: „normalmap_tutorial“ (keine Leerzeichen verwenden, da es ansonsten zu Problemen kommt).
Am Anfang gehen wir in die Global Game Settings unter Windows > Graphics und aktivieren „Use synchronisation to avoid tearing“. Das macht die Darstellung unserer Testanwendung erst erträglich bzw. verhindert ein "Ruckeln" das ansonsten auftreten würde.
Als nächstes erstellen wir ein Sprite und laden mit „Load Sprite“ die Datei „spr_rock.png“ in den Ressourcenbaum. Wir benennen das Sprite „spr_rock“. Alle restlichen Parameter lassen wir unberührt.
Abb. 2
Im nächsten Schritt erstellen wir zwei Objekte: Ein „obj_controller“ und ein „obj_rock“.
Dem „obj_rock“ weisen wir das zuvor erstellte Sprite „spr_rock“ zu. Beide Objekte bleiben „Visible“, auch das Controllerobjekt, andernfalls kann man in dem Objekt keine Draw-Events ausführen, und genau das wollen wir später.
Genauer gesagt wollen wir im „obj_controller“ das Zeichnen aller Objekte bzw. Instanzen übernehmen (in unserem Fall ist das nur ein Objekt – „obj_rock“ – aber man kann diese Methode auch für mehrere Objekte, beispielsweise in Kombination mit Parents verwenden indem man nur den Parent zum Drawen angibt).
Hierbei ist es aber wichtig zu wissen, dass sich die Objekte zusätzlich noch selbst zeichnen, was komplett überflüssig ist in unserem Fall. Um das zu verhindern können wir einfach im Draw-Event von „obj_rock“ einen Kommentar hinzufügen, z.B: //Um das Drawen kümmert sich der obj_controller, diese Zeile nicht entfernen! – damit man sich später noch auskennt und die Zeile nicht versehentlich löscht in einem Akt der Unachtsamkeit.
Wichtig: Den „obj_rock“ auf invisible zu stellen würde nichts bringen, da es nicht nur das eigene Draw-Event „verbietet“ sondern auch das externe Drawen vom Objekt im Controllerobjekt.
Abb. 3
Anschließend befassen wir uns mit dem Room Editor.
In den Settings geben wir dem Room nun einen Namen, z.B. „rm_main“, und schreiben bei „Width“ 1280 und bei „Height“ 2560 hinein. Bei „Speed“ tragen wir 60 ein, womit wir für diese Anwendung 60 Bilder pro Sekunde bekommen.
Abb. 4
Danach lasst uns in den Reiter „Views“ wechseln. Aktiviere das Häkchen bei „Enable the use of Views“ und bei „Visible when room starts“. Sowohl im Abschnitt „View in room“ als auch in „Port on screen“ geben wir bei W (Width) 1280 ein, und bei H (Height) 720. Diese Werte gelten nun als Spielauflösung.
Nachdem wir in unserem Testlevel/Room (wie auch immer) noch keine Objekte haben, müssen wir diese noch setzen. Dazu gehen wir in den Reiter „objects“. Am wichtigsten ist erstmal, dass der „obj_controller“ in unserem Room einen Platz findet. Solange er innerhalb der Roombegrenzungen platziert wird ist es egal wo.
Dann fehlen noch die Objekte. Damit die Texturen auch akkurat aneinander liegen empfiehlt sich die „Snap X“- und „Snap Y“-Einstellung auf 128 oder 256 Pixel zu setzen, da das Sprite selbst ein 256 Pixel Quadrat ist. Jetzt noch nach Lust und Laune platzieren und fertig.
Abb. 5
Damit hätten wir das Aufsetzen des Projektes abgeschlossen und können zum interessanten Teil übergehen.
Wichtig: Beim Testen des Projektes im momentanen Zustand sehen wir nur einen schwarzen Bildschirm. Lass dich davon nicht abschrecken, du hast nichts falsch gemacht (wenn du nichts ausgelassen hast). Das liegt daran, dass wir das Draw-Event vom obj_rock deaktiviert haben. Wenn du das bisherige Resultat trotzdem begutachten willst, dann entferne einfach vorübergehend die Auskommentierung, siehe Abb. 3.
3. Theorie über Normal Maps und Bumpmaps
Bevor wir unsere erste eigene Normal Map erstellen, befassen wir uns einmal damit was das überhaupt ist.
Oftmals herrscht Verwirrung ob Normal Maps und Bumpmaps das Gleiche sind, oder beide Begriffe werden durcheinandergebracht (ich habe auch erst im Zuge der Recherche die Unterschiede herausgefunden).
Eine Bumpmap ist eine Möglichkeit Geometriedetails von einer Textur zu simulieren, ohne den tatsächlichen Polygon-Count (Anzahl der Polygone) zu erhöhen. Dafür wird eine Graustufen-Textur verwendet, auf der Schwarz die tiefliegendste Stelle repräsentiert, und je heller (näher zu Weiß) die Pixel sind, desto erhabener (höher) scheinen sie.
Abb. 6, 7, 8
Diese Methode kann aber zu Problemen führen. Da Bilder mit Graustufen (üblicherweise 8 Bit) nur 256 Abstufungen besitzen, kann bei näherer Betrachtung Treppenbildung entstehen. Außerdem muss man darauf achten, dass die Helligkeitsübergänge möglichst weich gezeichnet sind – schärfere Kanten sehen damit ziemlich „choppy“ aus, wobei das natürlich auch von der Auflösung abhängt.
Normal Maps funktionieren da etwas anders. Anstatt einem Greyscale-Bild wird ein Farbbild verwendet, welches verschiedene Farben für verschiedene Normalen verwendet.
Hier gibt es online einen Normal-Map-Generator mit dem man sich ein gutes Bild von der Funktionsweise machen kann (allerdings muss man darauf achten, dass auch wirklich nur Normal-Maps aktiviert sind): Klick
Vereinfacht gesagt überwiegen die Vorteile von Normal-Maps gegenüber Bumpmaps, da Normal-Maps über die Farbdarstellung mehr Informationen über die Normalen speichern im Vergleich zu einer Bumpmap.
In unserem Beispiel erstellen wir die Normal-Map aus der Ursprungstextur heraus, was dann in etwa so aussehen kann:
Ursprungs- (Diffuse-) Textur:
Abb. 9
Generierte Normal-Map:
Abb. 10
4. Die Erstellung der Normal-Map mit Photoshop
Kommen wir nun zum Photoshop-Teil (oder GIMP-Teil, aber ich werde hier auf Photoshop eingehen).
Zuerst speichern wir eine Kopie von der Diffuse-Textur „spr_rock.png“, und nennen sie „spr_rock_nm.png“ damit wir die Datei später mit einem Klick überschreiben können. Dann suchen wir uns das zauberhafte Plugin aus, welches wir zuvor heruntergeladen haben. Es sollte zu finden sein unter Filter > NVIDIA Tools > NormalMapFilter.
Ein Fenster sollte sich sogleich öffnen welches unterteilt ist in ein Vorschaufenster, und in die Bereiche „Height Generation“, „3D-View Options“, „Height Source“, „Alternate Conversions“, und „Alpha Field“.
Im Feld „Height Generation“ haben wir links Optionen um die Raumkoordinaten zu invertieren und rechts können wir den Filter Type auswählen, welcher im Grunde die Genauigkeit der Normal-Map bestimmt. Wir lassen alle Checkboxes unchecked, bis auf „4 Sample“ welche wir aktivieren.
Unten bei „Min Z“ und „Scale“ können wir die Höhe der Normal-Map bestimmen.
Jede Änderung wird automatisch im Vorschaufenster angezeigt. Mit dem Button „3D Preview“ bekommt man eine Darstellung der Textur im Raum, wofür man die Parameter im Abschnitt „3D View Options“ anpassen kann. Die 3D-Vorschau bietet meiner Meinung nach aber nicht wirklich einen guten Anhaltspunkt um zu sehen ob alles so funktioniert wie gewünscht, also es ersetzt den Praxistest im GM nicht. Falls du also glaubst, es sieht alles ein bisschen komisch aus, warte erst ab, wie das Resultat im Gamemaker ist. Außerdem kann man sich, wenn die Textur einmal generiert ist, denke ich schon ein gutes Bild davon machen.
Im Feld „Height Source“ wählen wir nun „Average RGB“ falls noch nicht ausgewählt, und den Rest lassen wir wie er ist und klicken auf „OK“.
Abb. 11
Das Ergebnis ist eine wunderschöne bunte Normal-Map, allerdings benutzen wir hier ja eine Felstextur, also wird die Depth (Tiefe) wahrscheinlich nicht ausreichend sein.
In Photoshop können wir uns hier aber mit einem Trick helfen, indem wir die Ebene duplizieren (Tastenkürzel: Ctrl + J), mit einem Gauss-Blur (Filter > Weichzeichnungsfilter > Gaußscher Weichzeichner) von, sagen wir mal 1 oder 2 versehen, dann den Blend-Modus „Ineinanderkopieren“ verwenden, dann wieder duplizieren, einen höheren Gauss-Blur verwenden, und das Spielchen weitermachen bis wir ungefähr das hier erreichen:
Abb. 12
Weil wir die Textur nun ziemlich stark bearbeitet haben würde sie in diesem Zustand inkompatibel sein und Probleme verursachen. Um das zu verhindern rechnen wir erstmal alle Ebenen zusammen (Ebene > Auf Hintergrundebene reduzieren), rufen den Filter von nVidia nochmals auf und wählen bei „Alternate Conversions“ die Checkbox „Normalize Only“ aus. Damit wird eine bestehende Normal-Map sozusagen wieder den Regeln angepasst. Wir klicken wieder auf „OK“ und sehen, dass die Textur wieder etwas an Kontrast verliert (soll so sein).
Abb. 13
Jetzt überschreiben wir die Datei und damit ist unsere Normal-Map fertig.
5. Aufbau der Objekt-Surface und View-Bewegung
Zunächst einmal eine kurze Beschreibung von Surfaces, falls unklar ist was das eigentlich ist: Surfaces sind reservierter Speicher vom VRAM (Video-RAM) auf der Grafikkarte. Sie dienen sozusagen als Zwischenspeicher um grafischen Inhalt weiterzuverarbeiten. Standardmäßig wird im Gamemaker eine einzige Surface verwendet, die „application_surface“ (das ist zumindest die einzige von außen hin ersichtliche). Man kann aber selbst noch viele weitere hinzufügen um spezielle Effekte zu erschaffen, Texturen weiterzuverarbeiten, etc. Die Möglichkeiten sind vielfältig, allerdings sollte man es nicht übertreiben, da diese wie gesagt einen Teil des VRAMS verbrauchen, und ältere Rechner schnell Probleme mit nur ein paar wenigen Surfaces bekommen können.
Aber nun lasset uns wieder zum Projekt zurückkehren, welches wir jetzt erweitern.
Als erstes fügen wir im „obj_controller“ ein Create-Event hinzu, in welchem wir mit einem „Piece of Code“ diese Zeile schreiben:
„surf_objects“ speichert nun die ID der Surface, auf welche wir später referenzieren, und als Breite und Höhe haben wir hier einfach die Breite und Höhe der View genommen die wir erstellt haben (also in dem Fall 1280 und 720).
Weiter geht’s zum Step-Event, in welchem wir schreiben:
Dieser Code ist wichtig um zu verhindern, dass uns die Surface abhanden kommt, wenn wir vom Vollbildmodus in den Fenstermodus und vice versa wechseln, der Bildschirmschoner angeht, oder weitere Unterbrechungen passieren. Das Abhandenkommen liegt daran, dass Surfaces „flüchtig“ sind, bzw. nicht nur von einem Programm reserviert werden können, sondern von allen.
Weiters fügen wir diese Zeilen hinzu (im gleichen Codestück):
Ich schätze die Funktionsweise sollte klar sein: Wenn wir „Pfeil nach oben“ drücken bewegt sich die View nach oben, wenn wir „Pfeil nach unten“ drücken dann nach unten.
Wechseln wir nun ins Draw-Event:
Alles anzeigen
Der Befehl in Zeile 1 lässt alles fortan Gezeichnete in die Surface „surf_objects“ drawen, anstelle des Bildschirms.
Mit draw_clear_alpha(c_black,0) stellen wir sicher, dass die Surface sauber ist bevor wir sie benutzen, andernfalls können Artefakte von anderen Anwendungen o.ä. auftauchen.
Mit einer with-Schleife gehen wir nun durch alle Instanzen des Objektes „obj_rock“ durch und zeichnen den entsprechenden Sprite-Index.
Wichtig: Als Koordinaten verwenden wir hier x-view_xview und y-view_yview, weil für Surfaces eigene Koordinaten gelten. Deswegen müssen wir die Viewkoordinaten von den Positionskoordinaten des Objektes abziehen. Wir könnten auch eine Surface in der Größe des Rooms verwenden, dann bräuchten wir diese Adjustierung nicht, aber das wäre Verschwendung von VRAM.
Als nächstes setzen wir mit surface_reset_target() das Ziel des Drawens wieder zum Bildschirm zurück, andernfalls würde alles weiterhin auf dieser Surface gezeichnet werden, was wir nicht wollen.
In der letzten Zeile müssen wir noch dafür sorgen, dass die Surface selbst gezeichnet wird (ansonsten wäre alles umsonst und wir bekämen nur einen schwarzen Bildschirm). Hier verwenden wir wieder die normalen Viewkoordinaten, da wir wieder vom ganzen Room ausgehen, und nicht mehr IN die Surface zeichnen.
Damit wäre der erste Schritt der Implementierung geschafft. Wir können uns das Ergebnis ansehen, mit den Pfeiltasten navigieren und sehen wieder alle Instanzen.
6. Einbau des Shaders und der Normal-Map-Textur
Jetzt laden wir unser Normal-Map-Sprite in das Spiel hinein. Dazu gehen wir einfach auf „Create Sprite“ und wählen die entsprechende Textur aus. Die Parameter lassen wir dabei unverändert, und nennen das Sprite „spr_rock_nm“.
Im „obj_rock“ erstellen wir ein Create-Event und schreiben:
Damit haben wir neben dem „sprite_index“ einen zweiten Index für das Normal-Map-Sprite erstellt, auf welchen wir später zurückgreifen können.
Als nächstes bauen wir den Shader ein. Dazu rechtsklicken wir in der Ordnerstruktur auf „Shaders“ und gehen auf „Create Shader“.
Der Code den wir nun sehen, ist der des Standard-Shaders und ist aufgeteilt in Vertex Shader und Fragment Shader. Im Tab „Vertex“ löschen wir den kompletten Inhalt und fügen das hier ein:
Alles anzeigen
Auch im „Fragment“-Tab löschen wir alles, und ersetzen es mit:
Alles anzeigen
Den Shader findet man übrigens neben vielen anderen Shadern hier: Klick
bzw. genauer gesagt hier: Download
Wichtig: Diese Shader funktionieren mit Windows, aber bekommen erst in Zukunft auch Support für andere Plattformen. Bitte auch nicht vergessen, den Ersteller in den Credits zu erwähnen. Außerdem unterstützt dieser Shader nur eine Lichtquelle, es gibt aber auch andere Varianten zu finden die mehr Lichtquellen unterstützen, oder man verwendet mehrere Surfaces und rechnet diese zusammen.
7. Kombination von drei Surfaces mit einem Shader: Finaler Effekt
Jetzt wird es langsam spannend. Wir müssen jetzt nur noch die drei Events vom Controllerobjekt modifizieren, und dann sind wir auch schon fertig.
Um den finalen Effekt zu erzielen, reicht uns die eine Surface die wir bisher haben nicht aus. Wir brauchen drei Stück von den Dingern. So kann man sich das Ganze dann im Endeffekt vorstellen (natürlich liegen die Surfaces direkt übereinander und nicht, wie hier dargestellt, versetzt):
Abb. 14
Legende:
A – surf_nm (Normal-Map-Surface)
B – surf_objects (Objektesurface)
C – surf_main (finale Surface in der „surf_nm“ und „surf_objects“ miteinander verrechnet werden)
D – Lichtquelle (hierfür verwenden wir die Mauskoordinaten, aber man kann diese natürlich beliebig zuweisen)
Mit dem Wissen bewaffnet schreiten wir nun zum Create-Event und fügen ein paar Zeilen hinzu, bis wir das hier haben:
Alles anzeigen
Zu Beginn erstellen wir also die drei Surfaces, welche alle die gleichen Dimensionen aufweisen. Darunter befinden sich einige Variablen die der Shader braucht.
Auf zum Step-Event, welcher jetzt so aussehen soll:
Alles anzeigen
Wir sehen, dass wir hier die Abfrage, ob die jeweilige Surface existiert + Neuerstellung einfach noch für die zwei neuerstellten Surfaces schreiben müssen.
Wenn wir das haben, kommen wir zum Grand Finale, dem Draw-Event. Zunächst schreiben wir das hier:
Alles anzeigen
Das dürfte jetzt verständlich sein: In der ersten Surface „surf_nm“ lassen wir von allen Instanzen des Objektes „obj_rock“ den „nm_index“ zeichnen, welchen wir im Create-Event definiert haben. In der zweiten Surface dann einfach den normalen „sprite_index“.
Weiter geht’s, wir fügen diese Zeilen hinzu:
Alles anzeigen
Nachdem wir hier die Surface „gesäubert“ haben mit draw_clear_alpha(c_black,0) aktivieren wir den Shader mit den darauffolgenden Zeilen, weiters fügen wir das hier hinzu:
Mit texture_set_stage(s_multitex,bg_multitex) kombinieren wir den Shader mit der Surface „surf_nm“, hier zwischengespeichert als „bg_multitex“, und Drawen anschließend die Objektsurface darauf. Nun sind alle Surface- und Shaderaufgaben abgeschlossen, daher das Resetten am Ende.
Zum Schluss (diesmal aber wirklich) schreiben wir noch:
Damit Drawen wir die End-Surface (in welcher wir die beiden anderen Surfaces mit dem Shader kombiniert haben) auf den Bildschirm.
Wenn wir das Projekt jetzt testen, sollte es so aussehen:
Abb. 15
Wenn wir mit den Pfeiltasten navigieren, sehen wir, dass die Surfaces, obwohl sie immer nur so groß sind wie der Viewbereich automatisch „mitgehen“ ohne, dass wir es merken (bzw. eigentlich die Instanzen/Objekte, die Surfaces bleiben immer wo sie sind), die Größe der Surfaces ist damit nicht von der Größe des Rooms abhängig, und der Shader funktioniert einwandfrei (wenn nicht, dann ist irgendetwas schiefgelaufen).
Zum Abschluss gibt es hier noch die fertige Projektdatei zum Download und Vergleich: Klick
Herzlichen Glückwunsch, du hast es nun geschafft, und danke fürs Durchhalten durch dieses doch ziemlich langgestreckte Tutorial!
Wenn du jetzt auf den Geschmack gekommen bist, findest du weiterführende Links ganz am Anfang dieses Tutorials.
Conclusio
Ich hoffe mit diesem Tutorial einige Fragen bezüglich Shaderimplementierung und Surfaces beantwortet zu haben. Mir ist klar, dass viele den kompletten Inhalt schon kennen und verstehen, es ist auch wie gesagt mehr für die GM-Studio-Anfänger geeignet, und ich hoffe dass ich denjenigen helfen konnte, die da bisher noch im Dunkeln getappt sind.
Falls noch Unklarheiten bestehen bitte nicht zögern und einfach eine Frage hier stellen, bzw. wenn es Kritik gibt (ist schließlich mein erstes Tutorial) dann lasst eurer Sprache einfach freien Lauf, dann versuche ich die genannten Mängel zu verbessern und Verbesserungsvorschläge einzubauen. Wenn ihr irgendwann einmal bemerkt, dass ein Link tot sein sollte, dann lasst es mich auch wissen.
Mit der Zeit werden wahrscheinlich kleine Updates und weitere Verlinkungen folgen.
Bei shaderspezifischen Fragen, gibt es hier auf gm-d.de auch Experten, die da weiterhelfen können, denn wie gesagt: Von Shadern selbst habe ich kaum Ahnung.
Abb. 1
Vorwort
Dieses (mein erstes) Tutorial soll dazu dienen, ein Grundverständnis von Surfaces, Normal Maps und dem Implementieren von Shadern zu vermitteln, und denjenigen die nach schönen Shader-Effekten lechzen einen leichten Weg zu zeigen, um bestehende Shader (die GM-Community ist großartig und stellt viele frei zur Verfügung) einzubauen.
Im Verlauf dieses Tutorials nehmen wir eine kachelbare Textur, von der ausgehend wir dann eine Normal Map erstellen (hierfür gibt es verschiedene Filter und Plugins, im Tutorial greifen wir auf den nVidia Normal Map Filter zurück). Durch einen Shader von xygthop3 wird die Diffuse-Textur dann mit der Normal Map kombiniert und der Effekt wird mit Surfaces über den Viewbereich sichtbar gemacht.
Achtung: Das Tutorial ist ziemlich lang geraten, was auch daran liegt, dass es eine Schritt-für-Schritt-Anleitung (mit vielen Schritten) ist. Dafür habe ich mich sehr bemüht, es auch für Einsteiger möglichst verständlich zu machen.
Verlauf des Tutorials (Hauptteil):
1. Vorbereitung
2. Aufsetzen des Projektes
3. Theorie über Normal Maps und Bumpmaps
4. Die Erstellung der Normal Map mit Photoshop
5. Aufbau der Objekt-Surface und View-Bewegung
6. Einbau des Shaders und der Normal-Map-Textur
7. Kombination von drei Surfaces mit einem Shader: Finaler Effekt
Wichtig: Hier wird nicht beschrieben wie man einen Normal-Map-Shader schreibt, bzw. welche Techniken dahinterstecken, da mein Wissensstand dazu zum jetzigen Zeitpunkt noch lange nicht ausreicht und es sowieso den Rahmen dieser Anleitung sprengen würde. Wer tiefer in die Materie gehen möchte, dem empfehle ich die folgenden Links.
Weiterführendes
Shader Basics, von LEWA: Klick
Linksammlung von gm-d.de: Klick
Shadersammlung von xygthop3 in der GMC (inkludiert auch für jeden Shader eine Beispieldatei): Klick
(beim Verwenden nicht vergessen den Ersteller in den Credits zu erwähnen, er bietet seine Shader immerhin gratis an!)
Bumpmap-Artikel auf Wikipedia: Klick
Normal-Map-Artikel auf Wikipedia: Klick
Voraussetzungen
> Installierte Version von Gamemaker: Studio
> Grundlegende GML-Kenntnisse
> Kenntnisse über die Funktionsweise von Draw-Events
> Installiertes Bildbearbeitungsprogramm
> Kenntnis vom GM-Ressourcenbaum und generell vom UI des Gamemakers
> Aufmerksamkeitsspanne von über 3 Minuten
1. Vorbereitung
Als ersten Schritt installieren wir ein Normal-Map-Plugin. Dafür sind hier Links für Photoshop und GIMP aufgelistet. Falls du ein anderes Bildbearbeitungsprogramm verwendest wirst du sicher auch selbst fündig über eine Suche mit deiner bevorzugten Suchmaschine.
Download von der nVidia-Seite für Photoshop: Klick
Nach dem Download starten wir mit einem Doppelklick die Installation vom Downloadmanager aus, geben die gewünschte Zieldestination an (normalerweise sollte die Standardeinstellung passen) und los geht’s. Nach der Installation muss Photoshop neugestartet werden, falls bereits eine Instanz läuft.
Download vom ähnlich aufgebautem Plugin für GIMP: Klick
Jetzt wo wir das haben, und das Bildbearbeitungsprogramm auch bereit für den Einsatz ist, brauchen wir noch die Textur, welche wir später auch als Ausgangsmaterial für die Normal-Map benutzen.
Diese kannst du dir hier runterladen: Klick
… oder wenn du ganz ungeduldig bist, gibt es hier ein vorbereitetes Projekt-ZIP: Klick
… womit du das anschließende Kapitel „Aufsetzen des Projektes“ auch überspringen kannst.
Ich empfehle aber, es sich zumindest durchzulesen.
2. Aufsetzen des Projektes
Nun gut, lasst uns beginnen indem wir ein neues Projekt starten und benennen, zum Beispiel: „normalmap_tutorial“ (keine Leerzeichen verwenden, da es ansonsten zu Problemen kommt).
Am Anfang gehen wir in die Global Game Settings unter Windows > Graphics und aktivieren „Use synchronisation to avoid tearing“. Das macht die Darstellung unserer Testanwendung erst erträglich bzw. verhindert ein "Ruckeln" das ansonsten auftreten würde.
Als nächstes erstellen wir ein Sprite und laden mit „Load Sprite“ die Datei „spr_rock.png“ in den Ressourcenbaum. Wir benennen das Sprite „spr_rock“. Alle restlichen Parameter lassen wir unberührt.
Abb. 2
Im nächsten Schritt erstellen wir zwei Objekte: Ein „obj_controller“ und ein „obj_rock“.
Dem „obj_rock“ weisen wir das zuvor erstellte Sprite „spr_rock“ zu. Beide Objekte bleiben „Visible“, auch das Controllerobjekt, andernfalls kann man in dem Objekt keine Draw-Events ausführen, und genau das wollen wir später.
Genauer gesagt wollen wir im „obj_controller“ das Zeichnen aller Objekte bzw. Instanzen übernehmen (in unserem Fall ist das nur ein Objekt – „obj_rock“ – aber man kann diese Methode auch für mehrere Objekte, beispielsweise in Kombination mit Parents verwenden indem man nur den Parent zum Drawen angibt).
Hierbei ist es aber wichtig zu wissen, dass sich die Objekte zusätzlich noch selbst zeichnen, was komplett überflüssig ist in unserem Fall. Um das zu verhindern können wir einfach im Draw-Event von „obj_rock“ einen Kommentar hinzufügen, z.B: //Um das Drawen kümmert sich der obj_controller, diese Zeile nicht entfernen! – damit man sich später noch auskennt und die Zeile nicht versehentlich löscht in einem Akt der Unachtsamkeit.
Wichtig: Den „obj_rock“ auf invisible zu stellen würde nichts bringen, da es nicht nur das eigene Draw-Event „verbietet“ sondern auch das externe Drawen vom Objekt im Controllerobjekt.
Abb. 3
Anschließend befassen wir uns mit dem Room Editor.
In den Settings geben wir dem Room nun einen Namen, z.B. „rm_main“, und schreiben bei „Width“ 1280 und bei „Height“ 2560 hinein. Bei „Speed“ tragen wir 60 ein, womit wir für diese Anwendung 60 Bilder pro Sekunde bekommen.
Abb. 4
Danach lasst uns in den Reiter „Views“ wechseln. Aktiviere das Häkchen bei „Enable the use of Views“ und bei „Visible when room starts“. Sowohl im Abschnitt „View in room“ als auch in „Port on screen“ geben wir bei W (Width) 1280 ein, und bei H (Height) 720. Diese Werte gelten nun als Spielauflösung.
Nachdem wir in unserem Testlevel/Room (wie auch immer) noch keine Objekte haben, müssen wir diese noch setzen. Dazu gehen wir in den Reiter „objects“. Am wichtigsten ist erstmal, dass der „obj_controller“ in unserem Room einen Platz findet. Solange er innerhalb der Roombegrenzungen platziert wird ist es egal wo.
Dann fehlen noch die Objekte. Damit die Texturen auch akkurat aneinander liegen empfiehlt sich die „Snap X“- und „Snap Y“-Einstellung auf 128 oder 256 Pixel zu setzen, da das Sprite selbst ein 256 Pixel Quadrat ist. Jetzt noch nach Lust und Laune platzieren und fertig.
Abb. 5
Damit hätten wir das Aufsetzen des Projektes abgeschlossen und können zum interessanten Teil übergehen.
Wichtig: Beim Testen des Projektes im momentanen Zustand sehen wir nur einen schwarzen Bildschirm. Lass dich davon nicht abschrecken, du hast nichts falsch gemacht (wenn du nichts ausgelassen hast). Das liegt daran, dass wir das Draw-Event vom obj_rock deaktiviert haben. Wenn du das bisherige Resultat trotzdem begutachten willst, dann entferne einfach vorübergehend die Auskommentierung, siehe Abb. 3.
3. Theorie über Normal Maps und Bumpmaps
Bevor wir unsere erste eigene Normal Map erstellen, befassen wir uns einmal damit was das überhaupt ist.
Oftmals herrscht Verwirrung ob Normal Maps und Bumpmaps das Gleiche sind, oder beide Begriffe werden durcheinandergebracht (ich habe auch erst im Zuge der Recherche die Unterschiede herausgefunden).
Eine Bumpmap ist eine Möglichkeit Geometriedetails von einer Textur zu simulieren, ohne den tatsächlichen Polygon-Count (Anzahl der Polygone) zu erhöhen. Dafür wird eine Graustufen-Textur verwendet, auf der Schwarz die tiefliegendste Stelle repräsentiert, und je heller (näher zu Weiß) die Pixel sind, desto erhabener (höher) scheinen sie.
Abb. 6, 7, 8
Diese Methode kann aber zu Problemen führen. Da Bilder mit Graustufen (üblicherweise 8 Bit) nur 256 Abstufungen besitzen, kann bei näherer Betrachtung Treppenbildung entstehen. Außerdem muss man darauf achten, dass die Helligkeitsübergänge möglichst weich gezeichnet sind – schärfere Kanten sehen damit ziemlich „choppy“ aus, wobei das natürlich auch von der Auflösung abhängt.
Normal Maps funktionieren da etwas anders. Anstatt einem Greyscale-Bild wird ein Farbbild verwendet, welches verschiedene Farben für verschiedene Normalen verwendet.
Hier gibt es online einen Normal-Map-Generator mit dem man sich ein gutes Bild von der Funktionsweise machen kann (allerdings muss man darauf achten, dass auch wirklich nur Normal-Maps aktiviert sind): Klick
Vereinfacht gesagt überwiegen die Vorteile von Normal-Maps gegenüber Bumpmaps, da Normal-Maps über die Farbdarstellung mehr Informationen über die Normalen speichern im Vergleich zu einer Bumpmap.
In unserem Beispiel erstellen wir die Normal-Map aus der Ursprungstextur heraus, was dann in etwa so aussehen kann:
Ursprungs- (Diffuse-) Textur:
Abb. 9
Generierte Normal-Map:
Abb. 10
4. Die Erstellung der Normal-Map mit Photoshop
Kommen wir nun zum Photoshop-Teil (oder GIMP-Teil, aber ich werde hier auf Photoshop eingehen).
Zuerst speichern wir eine Kopie von der Diffuse-Textur „spr_rock.png“, und nennen sie „spr_rock_nm.png“ damit wir die Datei später mit einem Klick überschreiben können. Dann suchen wir uns das zauberhafte Plugin aus, welches wir zuvor heruntergeladen haben. Es sollte zu finden sein unter Filter > NVIDIA Tools > NormalMapFilter.
Ein Fenster sollte sich sogleich öffnen welches unterteilt ist in ein Vorschaufenster, und in die Bereiche „Height Generation“, „3D-View Options“, „Height Source“, „Alternate Conversions“, und „Alpha Field“.
Im Feld „Height Generation“ haben wir links Optionen um die Raumkoordinaten zu invertieren und rechts können wir den Filter Type auswählen, welcher im Grunde die Genauigkeit der Normal-Map bestimmt. Wir lassen alle Checkboxes unchecked, bis auf „4 Sample“ welche wir aktivieren.
Unten bei „Min Z“ und „Scale“ können wir die Höhe der Normal-Map bestimmen.
Jede Änderung wird automatisch im Vorschaufenster angezeigt. Mit dem Button „3D Preview“ bekommt man eine Darstellung der Textur im Raum, wofür man die Parameter im Abschnitt „3D View Options“ anpassen kann. Die 3D-Vorschau bietet meiner Meinung nach aber nicht wirklich einen guten Anhaltspunkt um zu sehen ob alles so funktioniert wie gewünscht, also es ersetzt den Praxistest im GM nicht. Falls du also glaubst, es sieht alles ein bisschen komisch aus, warte erst ab, wie das Resultat im Gamemaker ist. Außerdem kann man sich, wenn die Textur einmal generiert ist, denke ich schon ein gutes Bild davon machen.
Im Feld „Height Source“ wählen wir nun „Average RGB“ falls noch nicht ausgewählt, und den Rest lassen wir wie er ist und klicken auf „OK“.
Abb. 11
Das Ergebnis ist eine wunderschöne bunte Normal-Map, allerdings benutzen wir hier ja eine Felstextur, also wird die Depth (Tiefe) wahrscheinlich nicht ausreichend sein.
In Photoshop können wir uns hier aber mit einem Trick helfen, indem wir die Ebene duplizieren (Tastenkürzel: Ctrl + J), mit einem Gauss-Blur (Filter > Weichzeichnungsfilter > Gaußscher Weichzeichner) von, sagen wir mal 1 oder 2 versehen, dann den Blend-Modus „Ineinanderkopieren“ verwenden, dann wieder duplizieren, einen höheren Gauss-Blur verwenden, und das Spielchen weitermachen bis wir ungefähr das hier erreichen:
Abb. 12
Weil wir die Textur nun ziemlich stark bearbeitet haben würde sie in diesem Zustand inkompatibel sein und Probleme verursachen. Um das zu verhindern rechnen wir erstmal alle Ebenen zusammen (Ebene > Auf Hintergrundebene reduzieren), rufen den Filter von nVidia nochmals auf und wählen bei „Alternate Conversions“ die Checkbox „Normalize Only“ aus. Damit wird eine bestehende Normal-Map sozusagen wieder den Regeln angepasst. Wir klicken wieder auf „OK“ und sehen, dass die Textur wieder etwas an Kontrast verliert (soll so sein).
Abb. 13
Jetzt überschreiben wir die Datei und damit ist unsere Normal-Map fertig.
5. Aufbau der Objekt-Surface und View-Bewegung
Zunächst einmal eine kurze Beschreibung von Surfaces, falls unklar ist was das eigentlich ist: Surfaces sind reservierter Speicher vom VRAM (Video-RAM) auf der Grafikkarte. Sie dienen sozusagen als Zwischenspeicher um grafischen Inhalt weiterzuverarbeiten. Standardmäßig wird im Gamemaker eine einzige Surface verwendet, die „application_surface“ (das ist zumindest die einzige von außen hin ersichtliche). Man kann aber selbst noch viele weitere hinzufügen um spezielle Effekte zu erschaffen, Texturen weiterzuverarbeiten, etc. Die Möglichkeiten sind vielfältig, allerdings sollte man es nicht übertreiben, da diese wie gesagt einen Teil des VRAMS verbrauchen, und ältere Rechner schnell Probleme mit nur ein paar wenigen Surfaces bekommen können.
Aber nun lasset uns wieder zum Projekt zurückkehren, welches wir jetzt erweitern.
Als erstes fügen wir im „obj_controller“ ein Create-Event hinzu, in welchem wir mit einem „Piece of Code“ diese Zeile schreiben:
„surf_objects“ speichert nun die ID der Surface, auf welche wir später referenzieren, und als Breite und Höhe haben wir hier einfach die Breite und Höhe der View genommen die wir erstellt haben (also in dem Fall 1280 und 720).
Weiter geht’s zum Step-Event, in welchem wir schreiben:
Dieser Code ist wichtig um zu verhindern, dass uns die Surface abhanden kommt, wenn wir vom Vollbildmodus in den Fenstermodus und vice versa wechseln, der Bildschirmschoner angeht, oder weitere Unterbrechungen passieren. Das Abhandenkommen liegt daran, dass Surfaces „flüchtig“ sind, bzw. nicht nur von einem Programm reserviert werden können, sondern von allen.
Weiters fügen wir diese Zeilen hinzu (im gleichen Codestück):
Ich schätze die Funktionsweise sollte klar sein: Wenn wir „Pfeil nach oben“ drücken bewegt sich die View nach oben, wenn wir „Pfeil nach unten“ drücken dann nach unten.
Wechseln wir nun ins Draw-Event:
GML-Quellcode
Der Befehl in Zeile 1 lässt alles fortan Gezeichnete in die Surface „surf_objects“ drawen, anstelle des Bildschirms.
Mit draw_clear_alpha(c_black,0) stellen wir sicher, dass die Surface sauber ist bevor wir sie benutzen, andernfalls können Artefakte von anderen Anwendungen o.ä. auftauchen.
Mit einer with-Schleife gehen wir nun durch alle Instanzen des Objektes „obj_rock“ durch und zeichnen den entsprechenden Sprite-Index.
Wichtig: Als Koordinaten verwenden wir hier x-view_xview und y-view_yview, weil für Surfaces eigene Koordinaten gelten. Deswegen müssen wir die Viewkoordinaten von den Positionskoordinaten des Objektes abziehen. Wir könnten auch eine Surface in der Größe des Rooms verwenden, dann bräuchten wir diese Adjustierung nicht, aber das wäre Verschwendung von VRAM.
Als nächstes setzen wir mit surface_reset_target() das Ziel des Drawens wieder zum Bildschirm zurück, andernfalls würde alles weiterhin auf dieser Surface gezeichnet werden, was wir nicht wollen.
In der letzten Zeile müssen wir noch dafür sorgen, dass die Surface selbst gezeichnet wird (ansonsten wäre alles umsonst und wir bekämen nur einen schwarzen Bildschirm). Hier verwenden wir wieder die normalen Viewkoordinaten, da wir wieder vom ganzen Room ausgehen, und nicht mehr IN die Surface zeichnen.
Damit wäre der erste Schritt der Implementierung geschafft. Wir können uns das Ergebnis ansehen, mit den Pfeiltasten navigieren und sehen wieder alle Instanzen.
6. Einbau des Shaders und der Normal-Map-Textur
Jetzt laden wir unser Normal-Map-Sprite in das Spiel hinein. Dazu gehen wir einfach auf „Create Sprite“ und wählen die entsprechende Textur aus. Die Parameter lassen wir dabei unverändert, und nennen das Sprite „spr_rock_nm“.
Im „obj_rock“ erstellen wir ein Create-Event und schreiben:
Damit haben wir neben dem „sprite_index“ einen zweiten Index für das Normal-Map-Sprite erstellt, auf welchen wir später zurückgreifen können.
Als nächstes bauen wir den Shader ein. Dazu rechtsklicken wir in der Ordnerstruktur auf „Shaders“ und gehen auf „Create Shader“.
Der Code den wir nun sehen, ist der des Standard-Shaders und ist aufgeteilt in Vertex Shader und Fragment Shader. Im Tab „Vertex“ löschen wir den kompletten Inhalt und fügen das hier ein:
Quellcode
- attribute vec3 in_Position; // (x,y,z)
- attribute vec4 in_Colour; // (r,g,b,a)
- attribute vec2 in_TextureCoord; // (u,v)
- varying vec2 v_texcoord;
- varying vec4 v_color;
- uniform sampler2D s_multitex;
- void main()
- {
- vec4 object_space_pos = vec4( in_Position.x, in_Position.y, in_Position.z, 1.0);
- gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
- v_color = in_Colour;
- v_texcoord = in_TextureCoord;
- }
Auch im „Fragment“-Tab löschen wir alles, und ersetzen es mit:
Quellcode
- varying vec2 v_texcoord;
- varying vec4 v_color;
- uniform sampler2D s_multitex;
- //values used for shading algorithm...
- uniform vec2 Resolution; //resolution of screen
- uniform vec3 LightPos; //light position, normalized
- uniform vec4 LightColor; //light RGBA -- alpha is intensity
- uniform vec4 AmbientColor; //ambient RGBA -- alpha is intensity
- uniform vec3 Falloff; //attenuation coefficients
- void main()
- {
- //RGBA of our diffuse color
- vec4 DiffuseColor = texture2D(gm_BaseTexture, v_texcoord);
- //RGB of our normal map
- vec3 NormalMap = texture2D(s_multitex, v_texcoord).rgb;
- //The delta position of light
- vec3 LightDir = vec3(LightPos.xy - (v_texcoord.xy / Resolution.xy), LightPos.z);
- //Correct for aspect ratio
- LightDir.x *= Resolution.x / Resolution.y;
- //Determine distance (used for attenuation) BEFORE we normalize our LightDir
- float D = length(LightDir);
- //normalize our vectors
- vec3 N = normalize(NormalMap * 2.0 - 1.0);
- vec3 L = normalize(LightDir);
- //Pre-multiply light color with intensity
- //Then perform "N dot L" to determine our diffuse term
- vec3 Diffuse = (LightColor.rgb * LightColor.a) * max(dot(N, L), 0.0);
- //pre-multiply ambient color with intensity
- vec3 Ambient = AmbientColor.rgb * AmbientColor.a;
- //calculate attenuation
- float Attenuation = 1.0 / ( Falloff.x + (Falloff.y*D) + (Falloff.z*D*D) );
- //the calculation which brings it all together
- vec3 Intensity = Ambient + Diffuse * Attenuation;
- vec3 FinalColor = DiffuseColor.rgb * Intensity;
- gl_FragColor = v_color * vec4(FinalColor, DiffuseColor.a);
- }
Den Shader findet man übrigens neben vielen anderen Shadern hier: Klick
bzw. genauer gesagt hier: Download
Wichtig: Diese Shader funktionieren mit Windows, aber bekommen erst in Zukunft auch Support für andere Plattformen. Bitte auch nicht vergessen, den Ersteller in den Credits zu erwähnen. Außerdem unterstützt dieser Shader nur eine Lichtquelle, es gibt aber auch andere Varianten zu finden die mehr Lichtquellen unterstützen, oder man verwendet mehrere Surfaces und rechnet diese zusammen.
7. Kombination von drei Surfaces mit einem Shader: Finaler Effekt
Jetzt wird es langsam spannend. Wir müssen jetzt nur noch die drei Events vom Controllerobjekt modifizieren, und dann sind wir auch schon fertig.
Um den finalen Effekt zu erzielen, reicht uns die eine Surface die wir bisher haben nicht aus. Wir brauchen drei Stück von den Dingern. So kann man sich das Ganze dann im Endeffekt vorstellen (natürlich liegen die Surfaces direkt übereinander und nicht, wie hier dargestellt, versetzt):
Abb. 14
Legende:
A – surf_nm (Normal-Map-Surface)
B – surf_objects (Objektesurface)
C – surf_main (finale Surface in der „surf_nm“ und „surf_objects“ miteinander verrechnet werden)
D – Lichtquelle (hierfür verwenden wir die Mauskoordinaten, aber man kann diese natürlich beliebig zuweisen)
Mit dem Wissen bewaffnet schreiten wir nun zum Create-Event und fügen ein paar Zeilen hinzu, bis wir das hier haben:
GML-Quellcode
- surf_main = surface_create(view_wview,view_hview)
- surf_objects = surface_create(view_wview,view_hview)
- surf_nm = surface_create(view_wview,view_hview)
- //nm variables
- s_multitex = shader_get_sampler_index(sha_nm, "s_multitex")
- bg_multitex = surface_get_texture(surf_nm)
- Resolution = shader_get_uniform(sha_nm,"Resolution")
- LightPos = shader_get_uniform(sha_nm,"LightPos")
- LightColor = shader_get_uniform(sha_nm,"LightColor")
- AmbientColor = shader_get_uniform(sha_nm,"AmbientColor")
- Falloff = shader_get_uniform(sha_nm,"Falloff")
Zu Beginn erstellen wir also die drei Surfaces, welche alle die gleichen Dimensionen aufweisen. Darunter befinden sich einige Variablen die der Shader braucht.
Auf zum Step-Event, welcher jetzt so aussehen soll:
GML-Quellcode
- if not surface_exists(surf_objects)
- {
- surf_objects = surface_create(view_wview,view_hview)
- }
- if not surface_exists(surf_nm)
- {
- surf_nm = surface_create(view_wview,view_hview)
- }
- if not surface_exists(surf_main)
- {
- surf_main = surface_create(view_wview,view_hview)
- }
- if keyboard_check(vk_up)
- view_yview -= 16
- if keyboard_check(vk_down)
- view_yview += 16
Wir sehen, dass wir hier die Abfrage, ob die jeweilige Surface existiert + Neuerstellung einfach noch für die zwei neuerstellten Surfaces schreiben müssen.
Wenn wir das haben, kommen wir zum Grand Finale, dem Draw-Event. Zunächst schreiben wir das hier:
GML-Quellcode
- surface_set_target(surf_nm)
- draw_clear_alpha(c_black,0)
- with obj_rock
- {
- draw_sprite(nm_index,0,x-view_xview,y-view_yview)
- }
- surface_reset_target()
- surface_set_target(surf_objects)
- draw_clear_alpha(c_black,0)
- with obj_rock
- {
- draw_sprite(sprite_index,0,x-view_xview,y-view_yview)
- }
- surface_reset_target()
Das dürfte jetzt verständlich sein: In der ersten Surface „surf_nm“ lassen wir von allen Instanzen des Objektes „obj_rock“ den „nm_index“ zeichnen, welchen wir im Create-Event definiert haben. In der zweiten Surface dann einfach den normalen „sprite_index“.
Weiter geht’s, wir fügen diese Zeilen hinzu:
GML-Quellcode
- surface_set_target(surf_main)
- draw_clear_alpha(c_black,0)
- //set normal map shader
- shader_set(sha_nm)
- shader_set_uniform_f(Resolution,1,1)
- shader_set_uniform_f(LightPos,(mouse_x-view_xview)/view_wview,(mouse_y-view_yview)/view_hview,0.1)
- shader_set_uniform_f(LightColor,1,0.7,0.3,6.0) //R,G,B, Strength
- shader_set_uniform_f(AmbientColor,1,1,1,1) //R,G,B, Strength
- shader_set_uniform_f(Falloff,0.4,2.0,10.0)
Nachdem wir hier die Surface „gesäubert“ haben mit draw_clear_alpha(c_black,0) aktivieren wir den Shader mit den darauffolgenden Zeilen, weiters fügen wir das hier hinzu:
Mit texture_set_stage(s_multitex,bg_multitex) kombinieren wir den Shader mit der Surface „surf_nm“, hier zwischengespeichert als „bg_multitex“, und Drawen anschließend die Objektsurface darauf. Nun sind alle Surface- und Shaderaufgaben abgeschlossen, daher das Resetten am Ende.
Zum Schluss (diesmal aber wirklich) schreiben wir noch:
Damit Drawen wir die End-Surface (in welcher wir die beiden anderen Surfaces mit dem Shader kombiniert haben) auf den Bildschirm.
Wenn wir das Projekt jetzt testen, sollte es so aussehen:
Abb. 15
Wenn wir mit den Pfeiltasten navigieren, sehen wir, dass die Surfaces, obwohl sie immer nur so groß sind wie der Viewbereich automatisch „mitgehen“ ohne, dass wir es merken (bzw. eigentlich die Instanzen/Objekte, die Surfaces bleiben immer wo sie sind), die Größe der Surfaces ist damit nicht von der Größe des Rooms abhängig, und der Shader funktioniert einwandfrei (wenn nicht, dann ist irgendetwas schiefgelaufen).
Zum Abschluss gibt es hier noch die fertige Projektdatei zum Download und Vergleich: Klick
Herzlichen Glückwunsch, du hast es nun geschafft, und danke fürs Durchhalten durch dieses doch ziemlich langgestreckte Tutorial!
Wenn du jetzt auf den Geschmack gekommen bist, findest du weiterführende Links ganz am Anfang dieses Tutorials.
Conclusio
Ich hoffe mit diesem Tutorial einige Fragen bezüglich Shaderimplementierung und Surfaces beantwortet zu haben. Mir ist klar, dass viele den kompletten Inhalt schon kennen und verstehen, es ist auch wie gesagt mehr für die GM-Studio-Anfänger geeignet, und ich hoffe dass ich denjenigen helfen konnte, die da bisher noch im Dunkeln getappt sind.
Falls noch Unklarheiten bestehen bitte nicht zögern und einfach eine Frage hier stellen, bzw. wenn es Kritik gibt (ist schließlich mein erstes Tutorial) dann lasst eurer Sprache einfach freien Lauf, dann versuche ich die genannten Mängel zu verbessern und Verbesserungsvorschläge einzubauen. Wenn ihr irgendwann einmal bemerkt, dass ein Link tot sein sollte, dann lasst es mich auch wissen.
Mit der Zeit werden wahrscheinlich kleine Updates und weitere Verlinkungen folgen.
Bei shaderspezifischen Fragen, gibt es hier auf gm-d.de auch Experten, die da weiterhelfen können, denn wie gesagt: Von Shadern selbst habe ich kaum Ahnung.
Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von RLP () aus folgendem Grund: Existenzabfragen von Surfaces entfernt im Draw-Event (da sie unnötig sind und nur im Step-Event benötigt werden)