Normal Mapping via Surfaces und Shader (2D)

    • Studio

    Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

    • Normal Mapping via Surfaces und Shader (2D)

      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:

      GML-Quellcode

      1. surf_objects = surface_create(view_wview,view_hview)


      „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:

      GML-Quellcode

      1. if not surface_exists(surf_objects)
      2. {
      3. surf_objects = surface_create(view_wview,view_hview)
      4. }


      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):

      GML-Quellcode

      1. if keyboard_check(vk_up)
      2. view_yview -= 16
      3. if keyboard_check(vk_down)
      4. view_yview += 16


      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

      1. surface_set_target(surf_objects)
      2. draw_clear_alpha(c_black,0)
      3. with obj_rock
      4. {
      5. draw_sprite(sprite_index,0,x-view_xview,y-view_yview)
      6. }
      7. surface_reset_target()
      8. draw_surface(surf_objects,view_xview,view_yview)
      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:

      GML-Quellcode

      1. nm_index = spr_rock_nm


      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

      1. attribute vec3 in_Position; // (x,y,z)
      2. attribute vec4 in_Colour; // (r,g,b,a)
      3. attribute vec2 in_TextureCoord; // (u,v)
      4. varying vec2 v_texcoord;
      5. varying vec4 v_color;
      6. uniform sampler2D s_multitex;
      7. void main()
      8. {
      9. vec4 object_space_pos = vec4( in_Position.x, in_Position.y, in_Position.z, 1.0);
      10. gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
      11. v_color = in_Colour;
      12. v_texcoord = in_TextureCoord;
      13. }
      Alles anzeigen


      Auch im „Fragment“-Tab löschen wir alles, und ersetzen es mit:

      Quellcode

      1. varying vec2 v_texcoord;
      2. varying vec4 v_color;
      3. uniform sampler2D s_multitex;
      4. //values used for shading algorithm...
      5. uniform vec2 Resolution; //resolution of screen
      6. uniform vec3 LightPos; //light position, normalized
      7. uniform vec4 LightColor; //light RGBA -- alpha is intensity
      8. uniform vec4 AmbientColor; //ambient RGBA -- alpha is intensity
      9. uniform vec3 Falloff; //attenuation coefficients
      10. void main()
      11. {
      12. //RGBA of our diffuse color
      13. vec4 DiffuseColor = texture2D(gm_BaseTexture, v_texcoord);
      14. //RGB of our normal map
      15. vec3 NormalMap = texture2D(s_multitex, v_texcoord).rgb;
      16. //The delta position of light
      17. vec3 LightDir = vec3(LightPos.xy - (v_texcoord.xy / Resolution.xy), LightPos.z);
      18. //Correct for aspect ratio
      19. LightDir.x *= Resolution.x / Resolution.y;
      20. //Determine distance (used for attenuation) BEFORE we normalize our LightDir
      21. float D = length(LightDir);
      22. //normalize our vectors
      23. vec3 N = normalize(NormalMap * 2.0 - 1.0);
      24. vec3 L = normalize(LightDir);
      25. //Pre-multiply light color with intensity
      26. //Then perform "N dot L" to determine our diffuse term
      27. vec3 Diffuse = (LightColor.rgb * LightColor.a) * max(dot(N, L), 0.0);
      28. //pre-multiply ambient color with intensity
      29. vec3 Ambient = AmbientColor.rgb * AmbientColor.a;
      30. //calculate attenuation
      31. float Attenuation = 1.0 / ( Falloff.x + (Falloff.y*D) + (Falloff.z*D*D) );
      32. //the calculation which brings it all together
      33. vec3 Intensity = Ambient + Diffuse * Attenuation;
      34. vec3 FinalColor = DiffuseColor.rgb * Intensity;
      35. gl_FragColor = v_color * vec4(FinalColor, DiffuseColor.a);
      36. }
      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:

      GML-Quellcode

      1. surf_main = surface_create(view_wview,view_hview)
      2. surf_objects = surface_create(view_wview,view_hview)
      3. surf_nm = surface_create(view_wview,view_hview)
      4. //nm variables
      5. s_multitex = shader_get_sampler_index(sha_nm, "s_multitex")
      6. bg_multitex = surface_get_texture(surf_nm)
      7. Resolution = shader_get_uniform(sha_nm,"Resolution")
      8. LightPos = shader_get_uniform(sha_nm,"LightPos")
      9. LightColor = shader_get_uniform(sha_nm,"LightColor")
      10. AmbientColor = shader_get_uniform(sha_nm,"AmbientColor")
      11. Falloff = shader_get_uniform(sha_nm,"Falloff")
      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:

      GML-Quellcode

      1. if not surface_exists(surf_objects)
      2. {
      3. surf_objects = surface_create(view_wview,view_hview)
      4. }
      5. if not surface_exists(surf_nm)
      6. {
      7. surf_nm = surface_create(view_wview,view_hview)
      8. }
      9. if not surface_exists(surf_main)
      10. {
      11. surf_main = surface_create(view_wview,view_hview)
      12. }
      13. if keyboard_check(vk_up)
      14. view_yview -= 16
      15. if keyboard_check(vk_down)
      16. view_yview += 16
      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:

      GML-Quellcode

      1. surface_set_target(surf_nm)
      2. draw_clear_alpha(c_black,0)
      3. with obj_rock
      4. {
      5. draw_sprite(nm_index,0,x-view_xview,y-view_yview)
      6. }
      7. surface_reset_target()
      8. surface_set_target(surf_objects)
      9. draw_clear_alpha(c_black,0)
      10. with obj_rock
      11. {
      12. draw_sprite(sprite_index,0,x-view_xview,y-view_yview)
      13. }
      14. surface_reset_target()
      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:

      GML-Quellcode

      1. surface_set_target(surf_main)
      2. draw_clear_alpha(c_black,0)
      3. //set normal map shader
      4. shader_set(sha_nm)
      5. shader_set_uniform_f(Resolution,1,1)
      6. shader_set_uniform_f(LightPos,(mouse_x-view_xview)/view_wview,(mouse_y-view_yview)/view_hview,0.1)
      7. shader_set_uniform_f(LightColor,1,0.7,0.3,6.0) //R,G,B, Strength
      8. shader_set_uniform_f(AmbientColor,1,1,1,1) //R,G,B, Strength
      9. shader_set_uniform_f(Falloff,0.4,2.0,10.0)
      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:

      GML-Quellcode

      1. //drawen von objektsurface auf main surface, und texture_set_stage
      2. texture_set_stage(s_multitex,bg_multitex)
      3. draw_surface(surf_objects,0,0)
      4. surface_reset_target()
      5. shader_reset()

      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:

      GML-Quellcode

      1. draw_surface(surf_main,view_xview,view_yview)

      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)

    • Hmmmmm....also
      ich habs zwar hinbekommen aber verstanden hab ichs noch nich.

      Ab Punkt 6 hörts bei mir auf. Du schmeißt uns irgendwelche fertigen Codeschnipsel an den Kopf und ich habs imernoch nicht verstanden. Ich könnte jetzt nicht einfach einen anderen Shader implementieren wenn ich wollte.
      Z.b. gehst du gar nicht wirklich darauf ein wozu
      der ganze Quatsch da ist und so:

      GML-Quellcode

      1. Resolution = shader_get_uniform(sha_nm,"Resolution")
      2. LightPos = shader_get_uniform(sha_nm,"LightPos")
      3. LightColor = shader_get_uniform(sha_nm,"LightColor")
      4. AmbientColor = shader_get_uniform(sha_nm,"AmbientColor")
      5. Falloff = shader_get_uniform(sha_nm,"Falloff")

      oder was "shader_set_uniform_f" ist

      Ich bin leider nicht wirklich schlau geworden aus diesem Tutorial. Vielleicht wäre es besser gewesen wenn du ein "Tutorial" zu einem Blur-Shader machen würdest.
      z.b. um so ein Unschärfe/Gaussian Blur Effekt hinzubekommen wie er in dem Menü im Screenshot welchen du im Screenshot-Samstag-Thread gepostet hast zu sehen ist.
      Und dann weniger auf die alt bekannten Sachen wie Surfaces, Erstellen von Objekten oder Views eingehen, sondern mehr auf die konkreten Shader-GML-Codes.
      So dass man dann letztendlich surfaces oder den ganzen Bildschirm mit beliebigen Effekten versehen kann. Egal obs nun irgendwie einfärben ist oder blurren oder verzerren.
      Sowas wär halt ganz cool.
      Vielleicht schaff ichs ja irgendwann mir das beizubringen, dann würde ich dazu ein Tutorial machen, aber aktuell kann ich das leider noch nicht. (Mach bitte lieber du :D)
      Trotzdem vielen Dank fürs Tutorial. Und ein wenig hab ich ja doch gelernt ;)

      -Pac
      Sorm ist Schuld

      Edit: Doch ist er

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von Pacmangamer ()

    • Pacmangamer schrieb:

      Ich könnte jetzt nicht einfach einen anderen Shader implementieren wenn ich wollte.

      Ich wollte in dem Tutorial im Grunde nur klarmachen, wie man das aufbaut damit man einen Shadereffekt über den ganzen Bildschirm bekommt.
      Da ist das mehr oder weniger unabhängig davon welchen Shader man benutzt. Trotzdem gibt es da einige Unterschiede, manche sind ziemlich einfach einzubauen,
      andere benötigen mehrere Variablen oder folgen speziellen Regeln.

      Pacmangamer schrieb:

      oder was "shader_set_uniform_f" ist

      Um das zu verstehen müsste man eben Shader verstehen, und wie schon gesagt: Auf das Schreiben eines Shaders habe ich hier kein Gewicht gelegt, weil ich bis dato auch ohne dem erweiterten Fachwissen
      ausgekommen bin. In der GMC werden die Shader auch von den Experten erstellt, und die Community baut sie dann einfach ein, und natürlich hast du recht wenn du sagst, es ist besser wenn man sich alles selbst zusammenbaut, damit kann ich aber derzeit nicht dienen.

      Pacmangamer schrieb:

      Vielleicht wäre es besser gewesen wenn du ein "Tutorial" zu einem Blur-Shader machen würdest.
      z.b. um so ein Unschärfe/Gaussian Blur Effekt hinzubekommen wie er in dem Menü im Screenshot welchen du im Screenshot-Samstag-Thread gepostet hast zu sehen ist.
      Und dann weniger auf die alt bekannten Sachen wie Surfaces, Erstellen von Objekten oder Views eingehen, sondern mehr auf die konkreten Shader-GML-Codes.
      So dass man dann letztendlich surfaces oder den ganzen Bildschirm mit beliebigen Effekten versehen kann. Egal obs nun irgendwie einfärben ist oder blurren oder verzerren.

      Du hast wahrscheinlich recht, ich bin etwas zu sehr ins Detail gegangen bei Sachen die die meisten hier wohl anwenden können.
      Allerdings ist der Blur-Shader nicht wesentlich anders einzubauen. Da braucht man eben nicht diese Variablen die du gepostet hast, sondern andere.
      Es variiert da auch, manche Blur-Shader benötigen nur eine Surface, andere wiederum zwei (um horizontalen Blur und vertikalen Blur zusammenzurechnen)
      Ich weiß nicht ob es viel Sinn hat, dafür ein Tutorial zu erstellen, da es hier auch ein Beispiel dazu gibt, und es hätte wohl mehr Sinn, wenn es jemand macht der ihn auch gleichzeitig schreiben kann, damit wir alle etwas davon haben und nicht nur etwas abkupfern.

      Interessanterweise merke ich jetzt, dass dieses Tutorial aufgrund eines Missverständnisses entstanden ist, weil du ja dachtest ich würde einige Shader auch selbst schreiben (ich dachte ich hätte das gut genug erklärt, dass das nicht so ist, aber dem ist offenbar nicht so). Dementsprechend hoch waren deine Erwartungen, und da tut es mir Leid, dass das jetzt nicht hingehauen hat. Also hätte ich das Tutorial vielleicht gar nicht schreiben sollen, mich nicht verleiten lassen sollen.

      Danke für deine ehrliche Kritik jedenfalls, nächstes mal sollte ich mich wohl auf etwas konzentrieren was ich auch hundertprozentig beherrsche.
    • Ich verweise mal auf mein Tutorial -> Studio 3D Tilt Shift Blur (Tiefenschärfe) . Dort werden die Shader selbst aber nicht erklärt, weswegen ich ein Tutorial erstelle wo ich auf diese eingehe. Dies wird dann eine Vortsetzung von LEWA's Tutorial. Dort kann ich dann auf all den Kram eingehen den man braucht. Im Grunde genommen sind Shader zu programmieren sehr einfach und das tolle daran ist, dass man für jede X-Beliebige Engine oder Sprache selbst Shader schreiben kann. Ich werde in diesem Tutorial auch auf die 3d Schatten eingehen, die ich mal erwähnt habe. Wann ich dann mit dem Tutorial fertig bin, kann ich jetzt leider noch nicht sagen.

      Und zu der vorherigen Kritik gibt es nicht viel hinzuzufügen. Im großen und ganzem hast du gut erklärt wie man die Shader benutzt. Auch wie man die Normalmaps erstellt und wie sie funktionieren hast du gut erklärt. Für alle die aber kein Photoshop haben, gibt es auch in Gimp eine Funktion die Bilder in Normalmaps verwandelt. Diese findet man unter Filter->Abbilden->Normalmap... .