In diesem Tutorial erfahren Sie, wie Sie einen geometrischen Shader schreiben, um Grashalme aus den Spitzen des eingehenden Netzes zu generieren und die Dichte des Grases mithilfe von Tessellation zu steuern.
Der Artikel beschreibt den schrittweisen Prozess des Schreibens eines Grasshaders in Unity. Der Shader empfängt das eingehende Netz und generiert aus jedem Scheitelpunkt des Netzes mithilfe des
geometrischen Shaders einen Grashalm. Aus Gründen des Interesses und des Realismus haben die Grashalme eine
zufällige Größe und
Rotation und werden auch vom
Wind beeinflusst . Um die Dichte des Grases zu kontrollieren, verwenden wir
Tessellation , um das eingehende Netz zu trennen. Das Gras kann Schatten
werfen und
empfangen .
Das fertige Projekt wird am Ende des Artikels veröffentlicht. Die generierte Shader-Datei enthält eine große Anzahl von Kommentaren, die das Verständnis erleichtern.
Anforderungen
Um dieses Tutorial abzuschließen, benötigen Sie praktische Kenntnisse über die Unity-Engine und ein erstes Verständnis der Syntax und Funktionalität von Shadern.
Laden Sie den Entwurf des Projekts herunter (.zip) .
An die Arbeit gehen
Laden Sie den Entwurf des Projekts herunter und öffnen Sie ihn im Unity-Editor. Öffnen Sie die Hauptszene und dann den
Grass
Shader in Ihrem Code-Editor.
Diese Datei enthält einen Shader, der weiße Farbe erzeugt, sowie einige Funktionen, die wir in diesem Tutorial verwenden werden. Sie werden feststellen, dass diese Funktionen zusammen mit dem Vertex-Shader im
CGINCLUDE
Block
außerhalb von SubShader
. Der in diesem Block platzierte Code wird
automatisch in alle Durchgänge im Shader aufgenommen. Dies wird später nützlich sein, da unser Shader mehrere Durchgänge hat.
Wir beginnen mit dem Schreiben eines
geometrischen Shaders , der aus jedem Scheitelpunkt auf der Oberfläche unseres Netzes Dreiecke generiert.
1. Geometrische Shader
Geometrische Shader sind ein optionaler Bestandteil der Rendering-Pipeline. Sie werden
nach dem Vertex-Shader (oder Tessellation-Shader, wenn Tessellation verwendet wird) und vor der Verarbeitung der Vertices für den Fragment-Shader ausgeführt.
Direct3D Graphics Pipeline 11. Beachten Sie, dass in diesem Diagramm der Fragment-Shader als Pixel-Shader bezeichnet wird .Geometrische Shader erhalten am Eingang ein einzelnes
Grundelement und können null, ein oder mehrere Grundelemente erzeugen. Wir beginnen mit dem Schreiben eines geometrischen Shaders, der einen
Scheitelpunkt (oder
Punkt ) an der Eingabe empfängt und
ein Dreieck füttert, das
einen Grashalm darstellt.
Der obige Code deklariert einen geometrischen Shader namens
geo
mit zwei Parametern. Das erste
triangle float4 IN[3]
, dass ein Dreieck (bestehend aus drei Punkten) als Eingabe verwendet wird. Der zweite, z. B.
TriangleStream
, richtet einen Shader für die Ausgabe eines Dreiecksstroms ein, sodass jeder Scheitelpunkt die
geometryOutput
Struktur zum Übertragen seiner Daten verwendet.
Wir haben oben gesagt, dass der Shader einen Scheitelpunkt erhält und einen Grashalm ausgibt. Warum bekommen wir dann ein Dreieck?Es ist weniger kostspielig, einen
als Eingabe zu nehmen. Dies kann wie folgt erfolgen.
void geo(point vertexOutput IN[1], inout TriangleStream<geometryOutput> triStream)
Da unser eingehendes Netz (in diesem Fall
GrassPlane10x10
im Ordner "Netz") eine
Dreieckstopologie aufweist , führt dies zu einer Nichtübereinstimmung zwischen der eingehenden Netz-Topologie und dem erforderlichen Eingabeprimitiv. Obwohl dies in DirectX HLSL
zulässig ist, ist
es in OpenGL nicht
zulässig , sodass ein Fehler angezeigt wird.
Zusätzlich fügen wir den letzten Parameter in eckigen Klammern über der Funktionsdeklaration hinzu:
[maxvertexcount(3)]
. Er teilt der GPU mit, dass wir
nicht mehr als 3 Eckpunkte ausgeben werden (aber nicht müssen). Wir lassen
SubShader
auch einen geometrischen Shader verwenden, indem wir ihn in
Pass
deklarieren.
Unser geometrischer Shader macht noch nichts; Fügen Sie zum Zeichnen eines Dreiecks den folgenden Code in den geometrischen Shader ein.
geometryOutput o; o.pos = float4(0.5, 0, 0, 1); triStream.Append(o); o.pos = float4(-0.5, 0, 0, 1); triStream.Append(o); o.pos = float4(0, 1, 0, 1); triStream.Append(o);
Dies ergab sehr seltsame Ergebnisse. Wenn Sie die Kamera bewegen, wird deutlich, dass das Dreieck im
Bildschirmbereich gerendert
wird . Dies ist logisch: Da der geometrische Shader unmittelbar vor der Verarbeitung der Scheitelpunkte ausgeführt wird, entzieht er dem Scheitelpunkt-Shader die Verantwortung dafür, dass die Scheitelpunkte im
Kürzungsbereich angezeigt
werden . Wir werden unseren Code ändern, um dies widerzuspiegeln.
Jetzt ist unser Dreieck in der Welt richtig gerendert. Es scheint jedoch, dass nur eine erstellt wird. Tatsächlich wird für jeden Scheitelpunkt unseres Netzes ein Dreieck
gezeichnet , aber die den Scheitelpunkten des Dreiecks zugewiesenen Positionen sind
konstant - sie ändern sich nicht für jeden eingehenden Scheitelpunkt. Daher befinden sich alle Dreiecke übereinander.
Wir werden dies beheben, indem wir die Positionen der ausgehenden Scheitelpunkte relativ zum eingehenden Punkt versetzen.
Warum bilden einige Eckpunkte kein Dreieck?Obwohl wir festgestellt haben, dass das eingehende Grundelement ein
Dreieck ist , wird ein Grashalm nur von
einem der Punkte des Dreiecks übertragen, wobei die beiden anderen verworfen werden. Natürlich können wir einen Grashalm von allen drei ankommenden Punkten übertragen, aber dies führt dazu, dass benachbarte Dreiecke übermäßig Grashalme übereinander bilden.
Sie können dieses Problem auch lösen, indem Sie Netze mit dem Typ Topologiepunkte als eingehende Netze des geometrischen Shaders verwenden.
Dreiecke werden jetzt korrekt gezeichnet und ihre Basis befindet sich an der Spitze, die sie aussendet. Bevor Sie
GrassPlane
, machen Sie das
GrassPlane
Objekt in der Szene
inaktiv und
GrassBall
das
GrassBall
Objekt. Wir möchten, dass das Gras auf verschiedenen Oberflächentypen korrekt erzeugt wird. Daher ist es wichtig, es an Maschen unterschiedlicher Form zu testen.
Bisher werden alle Dreiecke in einer Richtung und nicht von der Oberfläche der Kugel nach außen emittiert. Um dieses Problem zu lösen, werden wir Grashalme in einem
tangentialen Raum erzeugen.
2. Tangentenraum
Im Idealfall möchten wir Grashalme erstellen, indem wir eine andere Breite, Höhe, Krümmung und Drehung einstellen, ohne den Winkel der Oberfläche zu berücksichtigen, von der der Grashalm emittiert wird. Einfach ausgedrückt, definieren wir einen Grashalm in einem Raum
lokal zum Scheitelpunkt, der ihn aussendet , und transformieren ihn dann so, dass er
lokal zum Netz ist . Dieser Raum wird
Tangentenraum genannt .
Im Tangentenraum werden die X- , Y- und Z- Achsen relativ zur Normalen und zur Position der Oberfläche (in unserem Fall den Eckpunkten) definiert.Wie in jedem anderen Raum können wir den Tangentenraum eines Scheitelpunkts mit drei Vektoren definieren:
rechts ,
vorwärts und
aufwärts . Mit diesen Vektoren können wir eine Matrix erstellen, um den Grashalm von der Tangente in den lokalen Raum zu drehen.
Sie können
direkt und nach
oben auf die Vektoren zugreifen, indem Sie neue Eingabescheitelpunktdaten hinzufügen.
Der dritte Vektor kann berechnet werden, indem das
Vektorprodukt zwischen zwei anderen genommen wird. Ein Vektorprodukt gibt einen Vektor
senkrecht zu zwei eingehenden Vektoren zurück.
Warum wird das Ergebnis des Vektorprodukts mit der Koordinate der Tangente w multipliziert?Beim Exportieren eines Netzes aus einem 3D-Editor sind in der Regel bereits Binormale (auch
Tangenten an zwei Punkte genannt ) in den Netzdaten gespeichert. Anstatt diese Binormale zu importieren, nimmt Unity einfach die Richtung jeder Binormale und weist sie der Koordinate der Tangente
w zu . Auf diese Weise können Sie Speicherplatz sparen und gleichzeitig das richtige Binormal wiederherstellen. Eine ausführliche Diskussion zu diesem Thema finden Sie
hier .
Mit allen drei Vektoren können wir eine Matrix für die Transformation zwischen tangentialen und lokalen Räumen erstellen. Wir werden jeden Scheitelpunkt des Grashalms mit dieser Matrix
UnityObjectToClipPos
, bevor
UnityObjectToClipPos
ihn an
UnityObjectToClipPos
, das einen Scheitelpunkt im lokalen Raum erwartet.
Bevor wir die Matrix verwenden, übertragen wir den Vertex-Ausgabecode an die Funktion, um nicht immer wieder dieselben Codezeilen zu schreiben. Dies wird als
DRY-Prinzip bezeichnet .
Wiederholen Sie sich nicht .
Schließlich multiplizieren wir die Ausgabescheitelpunkte mit der
tangentToLocal
Matrix und
tangentToLocal
sie korrekt an der Normalen ihres Eingabepunkts aus.
triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0.5, 0, 0)))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(-0.5, 0, 0)))); triStream.Append(VertexOutput(pos + mul(tangentToLocal, float3(0, 1, 0))));
Dies ist eher das, was wir brauchen, aber nicht ganz richtig. Das Problem hierbei ist, dass wir anfangs die Richtung „up“ (up) der
Y- Achse zugewiesen haben; Im Tangentenraum befindet sich die Aufwärtsrichtung jedoch normalerweise entlang der
Z- Achse. Jetzt werden wir diese Änderungen vornehmen.
3. Aussehen von Gras
Damit die Dreiecke eher wie Grashalme aussehen, müssen Sie Farben und Variationen hinzufügen. Wir beginnen mit dem Hinzufügen eines
Gefälles , das von der Spitze des Grashalms nach unten verläuft.
3.1 Farbverlauf
Unser Ziel ist es, dem Künstler zu ermöglichen, zwei Farben einzustellen - oben und unten, und zwischen diesen beiden Farben zu interpolieren, die er auf die Basis des Grashalms kippt. Diese Farben sind in der Shader-Datei bereits als
_TopColor
und
_BottomColor
. Für die ordnungsgemäße Abtastung müssen Sie die
UV-Koordinaten an den Fragment-Shader übergeben.
Wir haben UV-Koordinaten für einen Grashalm in Form eines Dreiecks erstellt, dessen zwei Eckpunkte sich unten links und rechts befinden und dessen Spitzenscheitelpunkt in der Mitte oben liegt.
UV-Koordinaten der drei Eckpunkte der Grashalme. Obwohl wir die Grashalme mit einem einfachen Farbverlauf bemalen, können Sie mit einer ähnlichen Anordnung von Texturen Texturen überlagern.Jetzt können wir die oberen und unteren Farben im Fragment-Shader mit UV
lerp
und sie dann mit
lerp
interpolieren. Wir müssen auch die Parameter des Fragment-
float4
ändern und
geometryOutput
als Eingabe und nicht nur die Position von
float4
.
3.2 Zufällige Blattrichtung
Um Variabilität zu erzeugen und dem Gras ein natürlicheres Aussehen zu verleihen, lassen wir jeden Grashalm in eine zufällige Richtung schauen. Dazu müssen wir eine Rotationsmatrix erstellen, die den Grashalm zufällig um seine
obere Achse dreht.
Die Shader-Datei enthält zwei Funktionen, die uns dabei helfen:
rand
, das aus dreidimensionalen Eingaben eine Zufallszahl generiert, und
AngleAxis3x3
, das den Winkel (im
Bogenmaß ) empfängt und eine Matrix zurückgibt, die diesen Wert um die angegebene Achse dreht. Die letztere Funktion funktioniert genauso wie die C #
Quaternion.AngleAxis- Funktion (nur
AngleAxis3x3
gibt eine Matrix zurück, keine Quaternion).
Die
rand
Funktion gibt eine Zahl im Bereich 0 ... 1 zurück. Wir multiplizieren es mit
2 Pi , um den gesamten Bereich der Winkelwerte zu erhalten.
Wir verwenden die eingehende Positionsposition als Startwert für eine zufällige Rotation. Aus diesem Grund hat jeder Grashalm seine eigene Rotation, die in jedem Rahmen konstant ist.
Die Drehung kann auf den Grashalm angewendet werden, indem er mit der erstellten
tangentToLocal
Matrix
tangentToLocal
wird. Beachten Sie, dass die Matrixmultiplikation
nicht kommutativ ist . Die Reihenfolge der Operanden ist
wichtig .
3.3 Zufälliges Vorwärtsbiegen
Wenn alle Grashalme perfekt ausgerichtet sind, sehen sie gleich aus. Dies mag für gepflegtes Gras geeignet sein, zum Beispiel auf einem gepflegten Rasen, aber in der Natur wächst das Gras nicht so. Wir werden eine neue Matrix erstellen, um das Gras entlang der
X- Achse zu drehen, sowie eine Eigenschaft, um diese Drehung zu steuern.
Wieder verwenden wir die Position des Grashalms als zufälligen Samen, diesmal indem wir ihn
fegen , um einen einzigartigen Samen zu erzeugen. Wir werden auch
UNITY_PI
mit
0,5 multiplizieren; Dies gibt uns ein zufälliges Intervall von 0 ... 90 Grad.
Wir wenden diese Matrix erneut durch Rotation an und multiplizieren alles in der richtigen Reihenfolge.
3.4 Breite und Höhe
Während die Größe des Grashalms auf eine Breite von 1 Einheit und eine Höhe von 1 Einheit begrenzt ist. Wir werden Eigenschaften hinzufügen, um die Größe zu steuern, sowie Eigenschaften, um zufällige Variationen hinzuzufügen.
Dreiecke sind jetzt viel mehr wie Grashalme, aber auch zu wenig. Es gibt einfach nicht genug Spitzen im eingehenden Netz, um den Eindruck eines dicht bewachsenen Feldes zu erwecken.
Eine Lösung besteht darin, ein neues, dichteres Netz zu erstellen, entweder mit C # oder in einem 3D-Editor. Dies wird funktionieren, aber es wird uns nicht ermöglichen, die Dichte des Grases dynamisch zu steuern. Stattdessen teilen wir das eingehende Netz mithilfe der
Tessellation auf .
4. Tessellation
Die Tessellation ist eine optionale Phase der Rendering-Pipeline, die nach dem Vertex-Shader und vor dem geometrischen Shader (falls vorhanden) ausgeführt wird. Seine Aufgabe ist es, eine eingehende Oberfläche in viele Grundelemente zu unterteilen. Die Tessellation wird in zwei programmierbaren Schritten implementiert:
Hull- und
Domain- Shader.
Für Oberflächen-Shader verfügt Unity über eine
integrierte Tessellierungsimplementierung . Da wir
jedoch keine Oberflächen-Shader verwenden, müssen wir unsere eigenen Shell- und Domain-Shader implementieren. In diesem Artikel werde ich die Implementierung von Tessellation nicht im Detail diskutieren, und wir verwenden einfach die vorhandene Datei
CustomTessellation.cginc
. Diese Datei wurde aus dem
Artikel Catlike Coding übernommen , der eine hervorragende Informationsquelle zur Implementierung von Tessellation in Unity darstellt.
Wenn wir das
TessellationExample
Objekt in die Szene aufnehmen, werden wir feststellen, dass es bereits Material enthält, das die Tessellation implementiert. Das Ändern der Eigenschaft
Tessellation Uniform zeigt den Unterteilungseffekt.
Wir implementieren eine Tessellation im Grasshader, um die Dichte des Flugzeugs und damit die Anzahl der erzeugten Grashalme zu steuern. Zuerst müssen Sie die Datei
CustomTessellation.cginc
hinzufügen. Wir werden es durch seinen
relativen Pfad zum Shader bezeichnen.
Wenn Sie
CustomTessellation.cginc
öffnen, werden Sie feststellen, dass darin bereits
vertexInput
und
vertexOutput
sowie Vertex-Shader definiert sind. Sie müssen sie in unserem Grasshader nicht neu definieren. Sie können gelöscht werden.
Beachten Sie, dass der
vert
Vertex-Shader in
CustomTessellation.cginc
die Eingabe einfach direkt an die Tessellation-Stufe übergibt. Die im Domain-Shader
vertexOutput
Funktion
vertexOutput
übernimmt die Erstellung der
vertexOutput
Struktur.
Jetzt können wir dem Grassader Shader- und
Domain- Shader hinzufügen. Wir werden auch eine neue
_TessellationUniform
Eigenschaft hinzufügen, um die Einheitengröße zu steuern. Die dieser Eigenschaft entsprechende Variable wurde bereits in
CustomTessellation.cginc
.
Durch Ändern der Eigenschaft
Tessellation Uniform können wir nun die Dichte des Grases steuern. Ich fand heraus, dass mit einem Wert von
5 gute Ergebnisse erzielt werden.
5. Der Wind
Wir implementieren den Wind, indem wir die
Verzerrungstextur abtasten. Diese Textur sieht aus wie
eine normale Karte , nur gibt es nur zwei statt drei Kanäle. Wir werden diese beiden Kanäle als Windrichtungen entlang
X und
Y verwenden.Bevor wir die Windtextur abtasten, müssen wir eine UV-Koordinate erstellen. Anstatt die dem Netz zugewiesenen Texturkoordinaten zu verwenden, wenden wir die Position des eingehenden Punkts an. Wenn es auf der Welt mehrere Grasmaschen gibt, entsteht die Illusion, dass sie alle Teil desselben Windsystems sind. Wir verwenden auch die
_Time
Variable _Time
Shader, um die
_Time
entlang der Grasoberfläche zu scrollen.
Wir wenden die Skalierung und den Versatz von
_WindDistortionMap
auf die Position an und verschieben sie dann weiter auf
_Time.y
, skaliert auf
_WindFrequency
. Jetzt werden wir diese UVs verwenden, um die Textur abzutasten und eine Eigenschaft zu erstellen, um die Stärke des Windes zu steuern.
Beachten Sie, dass wir den abgetasteten Wert von der Textur vom Intervall 0 ... 1 bis zum Intervall -1 ... 1 skalieren. Als nächstes können wir einen normalisierten Vektor erzeugen, der die Windrichtung angibt.
Jetzt können wir eine Matrix erstellen, um diesen Vektor zu drehen und mit unserer
transformationMatrix
multiplizieren.
Schließlich übertragen wir die Windtextur (an der Wurzel des Projekts) in das Feld
Wind Distortion Map des Grasmaterials im Unity-Editor. Wir setzen auch den
Tiling- Parameter der Textur auf
0.01, 0.01
.
Wenn das Gras im Szenenfenster nicht animiert wird, klicken Sie auf die
Schaltfläche Skybox, Nebel und verschiedene andere Effekte umschalten, um animierte Materialien zu aktivieren.
Aus der Ferne sieht das Gras richtig aus, aber wenn wir uns den Grashalm genau ansehen, stellen wir fest, dass sich der gesamte Grashalm dreht, weshalb die Basis nicht mehr am Boden befestigt ist.Die Basis des Grashalms ist nicht mehr am Boden befestigt, sondern schneidet ihn ( rot dargestellt ) und hängt über der Bodenebene (angezeigt durch die grüne Linie).Wir werden dies beheben, indem wir eine zweite Transformationsmatrix definieren, die nur für zwei Eckpunkte der Basis gilt. In dieser Matrix nicht werden enthalten Matrix windRotation
und bendRotationMatrix
Dank, auf dem die Basis auf die Grasfläche angebracht ist.
6. Krümmung der Grashalme
Jetzt werden einzelne Grashalme durch ein Dreieck definiert. In großen Entfernungen ist dies kein Problem, aber in der Nähe des Grashalms sehen sie eher sehr steif und geometrisch aus als organisch und lebhaft. Wir werden dies beheben, indem wir Grashalme aus mehreren Dreiecken bauen und sie entlang der Kurve biegen .Jeder Grashalm wird in mehrere Segmente unterteilt . Jedes Segment hat eine rechteckige Form und besteht aus zwei Dreiecken, mit Ausnahme des oberen Segments - es ist ein Dreieck, das die Spitze des Grashalms bezeichnet.Bisher haben wir nur drei Eckpunkte gezeichnet und so ein einziges Dreieck erstellt. Woher weiß der geometrische Shader dann, wenn es mehr Eckpunkte gibt, welche zu verbinden und Dreiecke zu bilden sind? Die Antwort liegt in der DatenstrukturDreiecksstreifen . Die ersten drei Scheitelpunkte verbinden sich und bilden ein Dreieck, und jeder neue Scheitelpunkt bildet mit den beiden vorherigen ein Dreieck.Ein unterteilter Grashalm, der als Dreiecksstreifen dargestellt wird und jeweils einen Scheitelpunkt erstellt. Nach den ersten drei Scheitelpunkten bildet jeder neue Scheitelpunkt mit den beiden vorherigen Scheitelpunkten ein neues Dreieck.Dies ist nicht nur effizienter in Bezug auf die Speichernutzung, sondern ermöglicht es Ihnen auch, einfach und schnell Dreieckssequenzen in Ihrem Code zu erstellen. Wenn wir mehrere Dreiecksstreifen erstellen möchten , können wir RestartStrip für die TriangleStream
Funktion aufrufen . Bevor wir beginnen, mehr Scheitelpunkte aus dem geometrischen Shader zu zeichnen, müssen wir ihn vergrößern . Wir werden das Design verwenden , damit der Shader-Autor die Anzahl der Segmente steuern und die Anzahl der angezeigten Scheitelpunkte daraus berechnen kann.maxvertexcount
#define
Zunächst setzen wir die Anzahl der Segmente auf 3 und aktualisieren maxvertexcount
, um die Anzahl der Scheitelpunkte basierend auf der Anzahl der Segmente zu berechnen.Um einen segmentierten Grashalm zu erstellen, verwenden wir einen Zyklus for
. Jede Iteration der Schleife fügt zwei Eckpunkte hinzu : links und rechts . Nach Abschluss der Spitze fügen wir den letzten Scheitelpunkt an der Spitze des Grashalms hinzu.Bevor wir dies tun, ist es nützlich, einen Teil der Rechenposition der Eckpunkte der Grashalme des Codes in die Funktion zu verschieben, da wir diesen Code innerhalb und außerhalb der Schleife mehrmals verwenden. Fügen Sie dem Block CGINCLUDE
Folgendes hinzu: geometryOutput GenerateGrassVertex(float3 vertexPosition, float width, float height, float2 uv, float3x3 transformMatrix) { float3 tangentPoint = float3(width, 0, height); float3 localPosition = vertexPosition + mul(transformMatrix, tangentPoint); return VertexOutput(localPosition, uv); }
Diese Funktion führt dieselben Aufgaben aus, da sie die Argumente übergibt, die wir zuvor übergeben haben VertexOutput
, um die Eckpunkte des Grashalms zu generieren. Wenn Sie eine Position, Höhe und Breite erhalten, transformiert es den Scheitelpunkt mithilfe der übertragenen Matrix korrekt und weist ihm eine UV-Koordinate zu. Wir werden den vorhandenen Code aktualisieren, damit die Funktion ordnungsgemäß funktioniert.
Die Funktion hat ordnungsgemäß funktioniert und wir sind bereit, den Vertex-Generierungscode in die Schleife zu verschieben for
. Fügen Sie unter der Zeile float width
Folgendes hinzu : for (int i = 0; i < BLADE_SEGMENTS; i++) { float t = i / (float)BLADE_SEGMENTS; }
Wir kündigen einen Zyklus an, der für jeden Grashalmsegment einmal ausgeführt wird. Fügen Sie innerhalb der Schleife eine Variable hinzu t
. Diese Variable speichert einen Wert im Bereich von 0 bis 1, der angibt, wie weit wir uns entlang des Grashalms bewegt haben. Wir verwenden diesen Wert, um die Breite und Höhe des Segments in jeder Iteration der Schleife zu berechnen.
Wenn Sie einen Grashalm nach oben bewegen, nimmt die Höhe zu und die Breite ab. Jetzt können wir der Schleife Aufrufe GenerateGrassVertex
hinzufügen, um dem Dreiecksstrom Scheitelpunkte hinzuzufügen. Wir werden auch einen Aufruf GenerateGrassVertex
außerhalb der Schleife hinzufügen , um die Spitze des Grashalms zu erzeugen.
Schauen Sie sich die Zeile mit der Deklaration an float3x3 transformMatrix
- hier wählen wir eine von zwei Transformationsmatrizen aus: Wir nehmen transformationMatrixFacing
für die Eckpunkte der Basis und transformationMatrix
für alle anderen.Grashalme sind jetzt in viele Segmente unterteilt, aber die Blattoberfläche ist immer noch flach - neue Dreiecke sind noch nicht beteiligt. Wir werden ein Grashalm Krümmung hinzufügen, um die Position des Scheitels der Verschiebung des Y . Zuerst müssen wir die Funktion GenerateGrassVertex
so ändern , dass sie einen Offset in Y erhält , den wir aufrufen werden forward
.
Um die Verschiebung jedes Scheitelpunkts zu berechnen, setzen wir einen pow
Wert in die Funktion ein t
. Nach dem Anheben t
auf eine Kraft ist ihre Auswirkung auf die Vorwärtsverschiebung nichtlinear und verwandelt den Grashalm in eine Kurve.
Dies ist ein ziemlich großer Code, aber alle Arbeiten werden ähnlich ausgeführt wie für die Breite und Höhe des Grashalms. Bei niedrigeren Werten _BladeForward
, und _BladeCurve
bekommen wir einen geordnete, gepflegten Rasen, und größere Werte der entgegengesetzte Wirkung.7. Licht und Schatten
Als letzten Schritt zur Vervollständigung des Shaders werden wir die Möglichkeit hinzufügen , Schatten zu werfen und zu empfangen . Wir werden auch eine einfache Beleuchtung von der Hauptlichtquelle hinzufügen.7.1 Schatten werfen
Um Schatten in Unity zu werfen, müssen Sie dem Shader einen zweiten Durchgang hinzufügen. Diese Passage wird von den schattenerzeugenden Lichtquellen in der Szene verwendet, um die Tiefe des Grases in ihre Schattenkarte zu rendern . Dies bedeutet, dass der geometrische Shader im Schattengang gestartet werden muss, damit die Grashalme Schatten werfen können.Da der geometrische Shader in Blöcken geschrieben ist CGINCLUDE
, können wir ihn in beliebigen Durchläufen der Datei verwenden. Erstellen Sie einen zweiten Durchgang, der dieselben Shader wie der erste verwendet, mit Ausnahme des Fragment-Shaders. Wir definieren einen neuen, in den wir ein Makro schreiben, das die Ausgabe verarbeitet.
Neben der Erstellung eines neuen Fragment-Shaders gibt es in dieser Passage einige wichtige Unterschiede. Die Beschriftung ist nicht LightMode
wichtig - dies sagt Unity, dass diese Passage verwendet werden sollte, um das Objekt in Schattenkarten zu rendern. Hier gibt es auch eine Präprozessor-Direktive . Es stellt sicher, dass der Shader alle erforderlichen Optionen kompiliert, um Schatten zu werfen. Lassen Sie uns ein Spiel Objekt machen , ist aktiv in der Szene; So erhalten wir eine Oberfläche, auf die die Grashalme einen Schatten werfen können.ShadowCaster
ForwardBase
multi_compile_shadowcaster
Fence
7.2 Schatten bekommen
Nachdem Unity die Schattenkarte aus der Sicht der Lichtquelle gerendert hat, die den Schatten erzeugt, wird eine Passage gestartet, die die Schatten in der Textur des Bildschirmbereichs "sammelt" . Um diese Textur abzutasten, müssen wir die Positionen der Scheitelpunkte im Bildschirmbereich berechnen und an den Fragment-Shader übertragen.
Im Fragment-Shader der Passage können ForwardBase
wir ein Makro verwenden, um einen Wert zu erhalten, der float
angibt, ob sich die Oberfläche im Schatten befindet oder nicht. Dieser Wert liegt im Bereich von 0 bis 1, wobei 0 für vollständige Schattierung und 1 für vollständige Beleuchtung steht.Warum heißt die UV-Koordinate des Bildschirmbereichs _ShadowCoord? Dies entspricht nicht den vorherigen Namenskonventionen.Unity ( ).
SHADOW_ATTENUATION
.
Autolight.cginc
, , .
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
- , .
Schließlich müssen wir den Shader richtig konfigurieren, um Schatten zu empfangen. Zu diesem Zweck fügen wir dem Pass eine ForwardBase
Präprozessor-Direktive hinzu, damit alle erforderlichen Shader-Optionen kompiliert werden.
Nachdem wir die Kamera näher gebracht haben, können wir Artefakte auf der Oberfläche der Grashalme feststellen. Sie werden durch die Tatsache verursacht, dass einzelne Grashalme Schatten auf sich selbst werfen. Wir können dies beheben, indem wir eine lineare Verschiebung anwenden oder die Positionen der Scheitelpunkte im Kürzungsraum etwas vom Bildschirm weg verschieben. Wir werden das Unity-Makro dafür verwenden und es in das Design #if
einbeziehen, sodass die Operation nur im Schattenpfad ausgeführt wird.
Nach dem Anwenden der linearen Schattenverschiebung verschwinden Schattenartefakte in Form von Streifen von der Oberfläche der Dreiecke.Warum gibt es Artefakte an den Rändern der schattierten Grashalme?(multisample anti-aliasing
MSAA ) Unity
, . , .
— , ,
Unity . ( );
Unity .
7.3 Beleuchtung
Wir werden die Beleuchtung mit einem sehr einfachen und gebräuchlichen Berechnungsalgorithmus für diffuses Licht implementieren.... wobei N die Normale zur Oberfläche ist, L die normalisierte Richtung der Hauptlichtquelle ist und I die berechnete Beleuchtung ist. In diesem Tutorial wird keine indirekte Beleuchtung implementiert.Im Moment sind den Spitzen der Grashalme keine Normalen zugeordnet. Wie bei Scheitelpunktpositionen berechnen wir zuerst die Normalen im Tangentenraum und konvertieren sie dann in lokale.Wenn Schaufelkrümmung Betrag wird auf 1 , die alle das Gras im Tangentenraum in der gleichen Richtung: das Gegenteil von der Achse Y. Als ersten Durchgang unserer Lösung berechnen wir die Normalen unter der Annahme, dass keine Krümmung vorliegt.
tangentNormal
, definiert als direkt gegenüber der Y- Achse , wird durch dieselbe Matrix transformiert, mit der wir die Tangentenpunkte in den lokalen Raum konvertiert haben. Jetzt können wir es an eine Funktion VertexOutput
und dann an eine Struktur übergeben geometryOutput
.
Beachten Sie, dass wir vor dem Abschluss das Normale in den Weltraum verwandeln . Die Einheit vermittelt den Shadern die Richtung der Hauptquelle des gerichteten Lichts im Weltraum, daher ist diese Transformation notwendig.Jetzt können wir die Normalen im Shader-Fragment visualisieren ForwardBase
, um das Ergebnis unserer Arbeit zu überprüfen.
Da in unserem Shader ein Cull
Wert zugewiesen ist Off
, werden beide Seiten des Grashalms gerendert. Damit die Normalen in die richtige Richtung gerichtet werden, verwenden wir einen Hilfsparameter VFACE
, den wir dem Fragment-Shader hinzugefügt haben.Das Argument fixed facing
gibt eine positive Zahl zurück, wenn wir die Vorderseite der Oberfläche anzeigen, und eine negative Zahl, wenn es das Gegenteil ist. Wir verwenden dies im obigen Code, um bei Bedarf das Normale umzudrehen.Wenn der Blattkrümmungsbetrag größer als 1 ist, wird die tangentiale Z- Position jedes Scheitelpunkts um den forward
an die Funktion übergebenen Betrag verschoben GenerateGrassVertex
. Wir werden diesen Wert verwenden, um die Z- Achse der Normalen proportional zu skalieren .
Fügen Sie abschließend den Code zum Fragment-Shader hinzu, um die Schatten, die gerichtete Beleuchtung und die Umgebungsbeleuchtung zu kombinieren. Ich empfehle, in meinem Tutorial zu Toon-Shadern detailliertere Informationen zur Implementierung der benutzerdefinierten Beleuchtung in Shadern zu lesen .
Fazit
In diesem Tutorial bedeckt Gras eine kleine Fläche von 10 x 10 Einheiten. Damit der Shader große Freiflächen abdecken und gleichzeitig eine hohe Leistung erzielen kann, müssen Optimierungen eingeführt werden. Sie können die Tessellierung basierend auf der Entfernung anwenden, damit weniger Grashalme von der Kamera entfernt werden. Darüber hinaus können über große Entfernungen anstelle einzelner Grashalme Gruppen von Grashalmen mit einem einzigen Viereck mit einer überlagerten Textur gezeichnet werden.Die Grasstruktur ist im Standard Assets- Paket der Unity Engine enthalten . Viele Grashalme werden auf ein Viereck gezeichnet, wodurch die Anzahl der Dreiecke in der Szene verringert wird.Obwohl wir von Natur aus keine geometrischen Shader mit Oberflächen-Shadern verwenden können, um die Funktionalität von Beleuchtung und Schattierung zu verbessern oder zu erweitern, können Sie dieses GitHub-Repository studieren , das die Lösung des Problems durch verzögertes Rendern und manuelles Füllen von G-Puffern demonstriert.Shader-Quellcode im GitHub-RepositoryErgänzung: Zusammenarbeit
Ohne Interoperabilität können Grafikeffekte für Spieler statisch oder leblos erscheinen. Dieses Tutorial ist bereits sehr lang, daher habe ich keinen Abschnitt über die Interaktion von Weltobjekten mit Gras hinzugefügt.Eine naive Implementierung interaktiver Kräuter würde zwei Komponenten enthalten: etwas in der Spielwelt, das Daten an den Shader übertragen kann, um ihm mitzuteilen, mit welchem Teil des Grases interagiert wird, und Code im Shader, um diese Daten zu interpretieren .Ein Beispiel, wie dies mit Wasser umgesetzt werden kann, wird hier gezeigt . Es kann angepasst werden, um mit Gras zu arbeiten; Anstatt an der Stelle, an der sich der Charakter befindet, Wellen zu zeichnen, können Sie den Grashalm nach unten drehen, um die Auswirkungen von Schritten zu simulieren.