Teile 1-3: Netz, Farben und ZellenhöhenTeile 4-7: Unebenheiten, Flüsse und StraßenTeile 8-11: Wasser, Landformen und WälleTeile 12-15: Speichern und Laden, Texturen, EntfernungenTeile 16-19: Weg finden, Spielerkader, AnimationenTeile 20-23: Nebel des Krieges, Kartenforschung, VerfahrensgenerierungTeile 24-27: Wasserkreislauf, Erosion, Biomes, zylindrische KarteTeil 4: Rauheiten
Inhaltsverzeichnis
- Probieren Sie die Rauschstruktur aus.
- Verschieben Sie die Eckpunkte.
- Wir bewahren die Flachheit der Zellen.
- Unterteilen Sie die Kanten der Zellen.
Während unser Gitter ein strenges Muster von Waben war. In diesem Teil werden wir Unebenheiten hinzufügen, damit die Karte natürlicher aussieht.
Keine gleichmäßigen Sechsecke mehr.Der Lärm
Um Unebenheiten hinzuzufügen, benötigen wir eine Randomisierung, aber keine echte Zufälligkeit. Wir möchten, dass beim Ändern der Karte alles konsistent ist. Andernfalls springen die Objekte, wenn Sie Änderungen vornehmen. Das heißt, wir brauchen irgendeine Form von reproduzierbarem Pseudozufallsrauschen.
Ein guter Kandidat ist Perlins Lärm. Es ist überall reproduzierbar. Wenn mehrere Frequenzen kombiniert werden, entsteht auch ein Rauschen, das über große Entfernungen stark variieren kann, auf kurzen Entfernungen jedoch nahezu gleich bleibt. Dadurch können relativ glatte Verzerrungen erzeugt werden. Aneinander benachbarte Punkte bleiben normalerweise in der Nähe und sind nicht in entgegengesetzte Richtungen verstreut.
Wir können Perlin-Rauschen programmgesteuert erzeugen. Im
Noise- Tutorial erkläre ich, wie das geht. Wir können aber auch von einer vorgenerierten Rauschtextur abtasten. Der Vorteil der Verwendung von Texturen besteht darin, dass sie einfacher und schneller sind als die Berechnung des Mehrfrequenzrauschens von Perlin. Der Nachteil ist, dass die Textur mehr Speicherplatz beansprucht und nur einen kleinen Rauschbereich abdeckt. Daher sollte es nahtlos verbunden und groß genug sein, damit die Wiederholung nicht auffällt.
Rauschstruktur
Wir werden die Textur verwenden, daher ist das
Noise- Tutorial optional. Wir brauchen also eine Textur. Da ist sie:
Perlin Noise Textur nahtlos verbinden.Die oben gezeigte Textur enthält Perlins nahtlos gekoppeltes Mehrfrequenzrauschen. Dies ist ein Graustufenbild. Sein Durchschnittswert beträgt 0,5 und die Extremwerte tendieren zu 0 und 1.
Aber warten Sie, es gibt nur einen Wert für jeden Punkt. Wenn wir eine 3D-Verzerrung benötigen, benötigen wir mindestens drei Pseudozufallsstichproben! Daher benötigen wir zwei weitere Texturen mit unterschiedlichem Rauschen.
Wir können sie erstellen oder unterschiedliche Rauschwerte in jedem der Farbkanäle speichern. Auf diese Weise können wir bis zu vier Rauschmuster in einer Textur speichern. Hier ist diese Textur.
Vier in einem.Wie erstelle ich eine solche Textur?Ich habe
NumberFlow verwendet . Dies ist der prozedurale Textureditor, den ich für Unity erstellt habe.
Laden Sie diese Textur herunter und importieren Sie sie in Ihr Unity-Projekt. Da wir die Textur durch Code abtasten werden, sollte sie lesbar sein. Schalten Sie den
Texturtyp auf
Erweitert und aktivieren Sie
Lesen / Schreiben aktiviert . Dadurch werden die Texturdaten im Speicher gespeichert und können über C # -Code aufgerufen werden. Stellen Sie
Format auf
Automatic Truecolor ein , sonst funktioniert nichts. Wir möchten nicht, dass die Texturkomprimierung unser Rauschmuster zerstört.
Sie können
Mip Maps generieren deaktivieren, da wir sie nicht benötigen. Aktivieren Sie auch
Bypass sRGB Sampling . Wir werden das nicht brauchen, aber es wird so sein. Dieser Parameter gibt an, dass die Textur keine Farbdaten im Gammaraum enthält.
Importierte Rauschtextur.
Wann ist sRGB-Sampling wichtig?Wenn wir eine Textur in einem Shader verwenden möchten, würde dies einen Unterschied machen. Wenn Sie den linearen Rendering-Modus verwenden, konvertiert das Abtasten der Textur automatisch die Farbdaten aus dem Farbumfang in einen linearen Farbraum. Im Fall unserer Rauschstruktur führt dies zu falschen Ergebnissen, sodass wir dies nicht benötigen.
Warum sehen meine Einstellungen für den Texturimport anders aus?Sie wurden geändert, nachdem dieses Tutorial geschrieben wurde. Sie müssen die Standard-2D-Textureinstellungen verwenden, sRGB (Color Texture) sollte deaktiviert und Compression auf None gesetzt sein .
Rauschabtastung
HexMetrics
wir
HexMetrics
Rauschabtastfunktionen
HexMetrics
damit Sie es überall verwenden können. Dies bedeutet, dass
HexMetrics
einen Verweis auf die Rauschtextur enthalten muss.
public static Texture2D noiseSource;
Da dies keine Komponente ist, können wir ihr über den Editor keine Textur zuweisen. Daher verwenden wir als Vermittler
HexGrid
. Da
HexGrid
zuerst wirkt, ist es in Ordnung, wenn wir die Textur zu Beginn der
Awake
Methode übergeben.
public Texture2D noiseSource; void Awake () { HexMetrics.noiseSource = noiseSource; … }
Dieser Ansatz überlebt jedoch die Neukompilierung im Wiedergabemodus nicht. Statische Variablen werden von der Unity-Engine nicht serialisiert. Um dieses Problem zu lösen,
OnEnable
die Textur auch in der
OnEnable
Ereignismethode neu zu. Diese Methode wird nach der Neukompilierung aufgerufen.
void OnEnable () { HexMetrics.noiseSource = noiseSource; }
Weisen Sie eine Rauschstruktur zu.HexMetrics
nun Zugriff auf die Textur hat, fügen wir eine praktische Rauschabtastmethode hinzu. Diese Methode nimmt eine Position in der Welt ein und erzeugt einen 4D-Vektor, der vier Rauschproben enthält.
public static Vector4 SampleNoise (Vector3 position) { }
Proben wurden durch Abtasten der Textur unter Verwendung einer bilinearen Filterung erstellt, bei der die Koordinaten der Welt X und Z als UV-Koordinaten verwendet wurden. Da unsere Rauschquelle zweidimensional ist, ignorieren wir die dritte Koordinate der Welt. Wenn die Rauschquelle dreidimensional wäre, würden wir auch die Y-Koordinate verwenden.
Als Ergebnis erhalten wir eine Farbe, die in einen 4D-Vektor konvertiert werden kann. Eine solche Reduzierung kann indirekt sein,
(Vector4)
wir können die Farbe direkt zurückgeben, ohne explizit
(Vector4)
.
public static Vector4 SampleNoise (Vector3 position) { return noiseSource.GetPixelBilinear(position.x, position.z); }
EinheitspaketScheitelpunktbewegung
Wir werden unser glattes Gitter aus Waben verzerren und jeden der Eckpunkte einzeln bewegen.
HexMesh
Sie dazu die
HexMesh
Methode zu
Perturb
. Es nimmt einen unbeweglichen Punkt und gibt den bewegten zurück. Zu diesem Zweck verwendet er beim Abtasten von Rauschen einen nicht verschobenen Punkt.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); }
Fügen wir einfach die Rauschabtastwerte X, Y und Z direkt zu den entsprechenden Punktkoordinaten hinzu und verwenden Sie diese als Ergebnis.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += sample.x; position.y += sample.y; position.z += sample.z; return position; }
Wie können wir
HexMesh
schnell ändern, um alle Scheitelpunkte zu verschieben?
AddTriangle
Ändern jedes Scheitelpunkts beim Hinzufügen von Scheitelpunkten zur Liste in den
AddQuad
AddTriangle
und
AddQuad
. Lass es uns tun.
void AddTriangle (Vector3 v1, Vector3 v2, Vector3 v3) { int vertexIndex = vertices.Count; vertices.Add(Perturb(v1)); vertices.Add(Perturb(v2)); vertices.Add(Perturb(v3)); … } void AddQuad (Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4) { int vertexIndex = vertices.Count; vertices.Add(Perturb(v1)); vertices.Add(Perturb(v2)); vertices.Add(Perturb(v3)); vertices.Add(Perturb(v4)); … }
Bleiben Vierecke nach dem Verschieben ihrer Eckpunkte flach?Höchstwahrscheinlich nicht. Sie bestehen aus zwei Dreiecken, die nicht mehr in derselben Ebene liegen. Da diese Dreiecke jedoch zwei gemeinsame Scheitelpunkte haben, werden die Normalen dieser Scheitelpunkte geglättet. Dies bedeutet, dass wir keine scharfen Übergänge zwischen zwei Dreiecken haben werden. Wenn die Verzerrung nicht zu groß ist, werden wir die Vierecke immer noch als flach wahrnehmen.
Die Eckpunkte werden entweder verschoben oder nicht.Während die Änderungen nicht sehr auffällig sind, sind nur die Zellbezeichnungen verschwunden. Dies geschah, weil wir den Punkten Rauschproben hinzugefügt haben, die immer positiv sind. Infolgedessen stiegen alle Dreiecke über ihre Markierungen und schlossen sie. Wir müssen die Änderungen so zentrieren, dass sie in beide Richtungen auftreten. Ändern Sie das Intervall der Rauschprobe von 0–1 auf –1–1.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += sample.x * 2f - 1f; position.y += sample.y * 2f - 1f; position.z += sample.z * 2f - 1f; return position; }
Zentrierte Verschiebung.Die Größe (Kraft) der Verschiebung
Jetzt ist es offensichtlich, dass wir das Raster verzerrt haben, aber der Effekt ist kaum spürbar. Die Änderung in jeder Dimension beträgt nicht mehr als 1 Einheit. Das heißt, die theoretische maximale Verschiebung beträgt √3 ≈ 1,73 Einheiten, was, wenn überhaupt, äußerst selten vorkommt. Da der Außenradius der Zellen 10 Einheiten beträgt, sind die Verschiebungen relativ klein.
Die Lösung besteht darin,
HexMetrics
einen
HexMetrics
damit Sie die Bewegungen skalieren können. Versuchen wir, Kraft 5 anzuwenden. In diesem Fall beträgt die theoretische maximale Verschiebung √75 ≈ 8,66 Einheiten, was viel deutlicher ist.
public const float cellPerturbStrength = 5f;
Wir wenden Kraft an, indem wir sie mit Stichproben in
HexMesh.Perturb
.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength; position.y += (sample.y * 2f - 1f) * HexMetrics.cellPerturbStrength; position.z += (sample.z * 2f - 1f) * HexMetrics.cellPerturbStrength; return position; }
Erhöhte Kraft.Geräuschskala
Obwohl das Raster vor der Änderung gut aussieht, kann nach dem Erscheinen der Leisten alles schief gehen. Ihre Spitzen können in unvorhersehbar unterschiedliche Richtungen verzerrt sein, was zu Chaos führt. Bei Verwendung von Perlin-Rauschen sollte dies nicht passieren.
Das Problem entsteht, weil wir die Koordinaten der Welt direkt verwenden, um das Rauschen abzutasten. Aus diesem Grund ist die Textur in jeder Einheit verborgen und die Zellen sind viel größer als dieser Wert. Tatsächlich wird die Textur an beliebigen Punkten abgetastet, wodurch ihre vorhandene Integrität zerstört wird.
Zeilen mit einem Raster von 10 x 10 überlappen Zellen.Wir müssen die Rauschabtastung so skalieren, dass die Textur einen viel größeren Bereich abdeckt.
HexMetrics
wir diese Skala zu
HexMetrics
weisen ihr einen Wert von 0,003 zu und skalieren dann die Koordinaten der Stichproben um diesen Faktor.
public const float noiseScale = 0.003f; public static Vector4 SampleNoise (Vector3 position) { return noiseSource.GetPixelBilinear( position.x * noiseScale, position.z * noiseScale ); }
Es stellt sich plötzlich heraus, dass unsere Textur 333 & frac13; quadratische Einheiten, und seine lokale Integrität wird offensichtlich.
Skaliertes Rauschen.Zusätzlich vergrößert eine neue Skala den Abstand zwischen den Geräuschfugen. Da die Zellen einen Innendurchmesser von 10 √ 3 Einheiten haben, werden sie in der X-Dimension niemals exakt gekachelt. Aufgrund der lokalen Integrität des Rauschens können wir jedoch in größerem Maßstab immer noch sich wiederholende Muster erkennen, etwa alle 20 Zellen. auch wenn die Details nicht übereinstimmen. Sie sind jedoch nur auf der Karte ohne andere charakteristische Merkmale erkennbar.
EinheitspaketZellzentren ausrichten
Wenn Sie alle Scheitelpunkte verschieben, sieht die Karte natürlicher aus, es gibt jedoch mehrere Probleme. Da die Zellen jetzt gezackt sind, schneiden sich ihre Beschriftungen mit dem Netz. Und in den Fugen der Felsvorsprünge mit Klippen entstehen Risse. Wir werden die Risse für später verlassen, aber jetzt werden wir uns auf die Oberflächen der Zellen konzentrieren.
Die Karte wurde weniger streng, aber es traten mehr Probleme auf.Der einfachste Weg, das Schnittpunktproblem zu lösen, besteht darin, die Zentren der Zellen flach zu machen. Lassen Sie uns die Y-Koordinate in
HexMesh.Perturb
einfach nicht ändern.
Vector3 Perturb (Vector3 position) { Vector4 sample = HexMetrics.SampleNoise(position); position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength;
Ausgerichtete Zellen.Mit dieser Änderung bleiben alle vertikalen Positionen unverändert, sowohl in der Mitte der Zellen als auch an den Stufen der Leisten. Es ist zu beachten, dass dies die maximale Verschiebung nur in der XZ-Ebene auf √50 ≈ 7.07 reduziert.
Dies ist eine gute Änderung, da dadurch die Identifizierung einzelner Zellen vereinfacht wird und die Leisten nicht zu chaotisch werden. Aber es wäre trotzdem schön, eine kleine vertikale Bewegung hinzuzufügen.
Zellenhöhe verschieben
Anstatt auf jeden Scheitelpunkt eine vertikale Bewegung anzuwenden, können wir sie auf eine Zelle anwenden. In diesem Fall bleibt jede Zelle flach, aber die Variabilität zwischen den Zellen bleibt bestehen. Es wäre auch logisch, eine andere Skala zu verwenden, um die Höhe zu
HexMetrics
.
HexMetrics
Sie sie daher zu
HexMetrics
. Eine Kraft von 1,5 Einheiten erzeugt eine geringfügige Abweichung, die ungefähr der Höhe einer Stufe der Kante entspricht.
public const float elevationPerturbStrength = 1.5f;
Ändern Sie die
HexCell.Elevation
Eigenschaft so, dass diese Verschiebung auf die vertikale Position der Zelle
HexCell.Elevation
wird.
public int Elevation { get { return elevation; } set { elevation = value; Vector3 position = transform.localPosition; position.y = value * HexMetrics.elevationStep; position.y += (HexMetrics.SampleNoise(position).y * 2f - 1f) * HexMetrics.elevationPerturbStrength; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = -position.y; uiRect.localPosition = uiPosition; } }
Damit die Verschiebung sofort angewendet werden kann, müssen Sie die Höhe jeder Zelle in
HexGrid.CreateCell
explizit
HexGrid.CreateCell
. Andernfalls ist das Raster zunächst flach. Lassen Sie es uns am Ende tun, nachdem Sie die Benutzeroberfläche erstellt haben.
void CreateCell (int x, int z, int i) { … cell.Elevation = 0; }
Verschobene Höhen mit Rissen.Mit den gleichen Höhen
Im Netz sind viele Risse aufgetreten, da wir beim Triangulieren des Netzes nicht die gleichen Zellenhöhen verwenden.
HexCell
wir
HexCell
eine Eigenschaft
HexCell
, um ihre Position zu ermitteln, sodass Sie sie überall verwenden können.
public Vector3 Position { get { return transform.localPosition; } }
Jetzt können wir diese Eigenschaft in
HexMesh.Triangulate
, um die Mitte der Zelle zu bestimmen.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; … }
Und wir können es in
TriangulateConnection
wenn wir die vertikalen Positionen benachbarter Zellen definieren.
void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 v2 ) { … Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Position.y; … HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = v2 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Position.y; … } }
Konsequente Verwendung der Zellenhöhe.EinheitspaketZellenkanteneinheit
Obwohl die Zellen eine schöne Variation haben, sehen sie immer noch wie offensichtliche Sechsecke aus. Dies ist an sich kein Problem, aber wir können ihr Aussehen verbessern.
Deutlich sichtbare hexagonale Zellen.Wenn wir mehr Eckpunkte hätten, gäbe es eine größere lokale Variabilität. Teilen wir also jede Kante der Zelle in zwei Teile, indem wir die Oberseite der Kante in der Mitte zwischen jedem Eckenpaar hinzufügen. Dies bedeutet, dass
HexMesh.Triangulate
nicht ein, sondern zwei Dreiecke hinzufügen sollte.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; Vector3 v1 = center + HexMetrics.GetFirstSolidCorner(direction); Vector3 v2 = center + HexMetrics.GetSecondSolidCorner(direction); Vector3 e1 = Vector3.Lerp(v1, v2, 0.5f); AddTriangle(center, v1, e1); AddTriangleColor(cell.color); AddTriangle(center, e1, v2); AddTriangleColor(cell.color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, v2); } }
Zwölf statt sechs Seiten.Durch das Verdoppeln von Scheitelpunkten und Dreiecken werden die Kanten der Zelle variabler. Machen wir sie noch ungleichmäßiger, indem wir die Anzahl der Eckpunkte verdreifachen.
Vector3 e1 = Vector3.Lerp(v1, v2, 1f / 3f); Vector3 e2 = Vector3.Lerp(v1, v2, 2f / 3f); AddTriangle(center, v1, e1); AddTriangleColor(cell.color); AddTriangle(center, e1, e2); AddTriangleColor(cell.color); AddTriangle(center, e2, v2); AddTriangleColor(cell.color);
18 Seiten.Rib Joint Division
Natürlich müssen wir auch die Kantenfugen unterteilen. Daher werden wir die neuen Scheitelpunktkanten an
TriangulateConnection
.
if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, v1, e1, e2, v2); }
Fügen Sie der
TriangulateConnection
die entsprechenden Parameter hinzu, damit sie mit zusätzlichen Scheitelpunkten arbeiten kann.
void TriangulateConnection ( HexDirection direction, HexCell cell, Vector3 v1, Vector3 e1, Vector3 e2, Vector3 v2 ) { … }
Wir müssen auch die zusätzlichen Kanten der Kanten für benachbarte Zellen berechnen. Wir können sie berechnen, nachdem wir die Brücke mit der anderen Seite verbunden haben.
Vector3 bridge = HexMetrics.GetBridge(direction); Vector3 v3 = v1 + bridge; Vector3 v4 = v2 + bridge; v3.y = v4.y = neighbor.Position.y; Vector3 e3 = Vector3.Lerp(v3, v4, 1f / 3f); Vector3 e4 = Vector3.Lerp(v3, v4, 2f / 3f);
Als nächstes müssen wir die Triangulation der Rippe ändern. Bis wir die Hänge mit den Leisten ignorieren, fügen Sie einfach drei statt eines Quad hinzu.
if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(v1, v2, cell, v3, v4, neighbor); } else { AddQuad(v1, e1, v3, e3); AddQuadColor(cell.color, neighbor.color); AddQuad(e1, e2, e3, e4); AddQuadColor(cell.color, neighbor.color); AddQuad(e2, v2, e4, v4); AddQuadColor(cell.color, neighbor.color); }
Unterteilte Verbindungen.Die Vereinigung der Kanten der Kanten
Da wir zur Beschreibung der Kanten jetzt vier Eckpunkte benötigen, wäre es logisch, sie zu einer Menge zu kombinieren. Dies ist bequemer als das Arbeiten mit vier unabhängigen Scheitelpunkten. Erstellen Sie hierfür eine einfache
EdgeVertices
Struktur. Es sollte vier Eckpunkte enthalten, die im Uhrzeigersinn entlang der Zellenkante verlaufen.
using UnityEngine; public struct EdgeVertices { public Vector3 v1, v2, v3, v4; }
Sollten sie nicht serialisierbar sein?Wir werden diese Struktur nur zur Triangulation verwenden. Zu diesem Zeitpunkt müssen die Eckpunkte der Kanten nicht gespeichert werden, sodass sie nicht serialisierbar sein müssen.
Fügen Sie eine praktische Konstruktormethode hinzu, die sich mit der Berechnung der Zwischenpunkte der Kante befasst.
public EdgeVertices (Vector3 corner1, Vector3 corner2) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, 1f / 3f); v3 = Vector3.Lerp(corner1, corner2, 2f / 3f); v4 = corner2; }
Jetzt können wir
HexMesh
eine separate Triangulationsmethode
HexMesh
, um einen Fächer aus Dreiecken zwischen der Mitte der Zelle und einer ihrer Kanten zu erstellen.
void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { AddTriangle(center, edge.v1, edge.v2); AddTriangleColor(color); AddTriangle(center, edge.v2, edge.v3); AddTriangleColor(color); AddTriangle(center, edge.v3, edge.v4); AddTriangleColor(color); }
Und ein Verfahren zum Triangulieren eines Viereckstreifens zwischen zwei Kanten.
void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2 ) { AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); AddQuadColor(c1, c2); AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); AddQuadColor(c1, c2); AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); AddQuadColor(c1, c2); }
Dadurch können wir die
Triangulate
vereinfachen.
void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); TriangulateEdgeFan(center, e, cell.color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } }
Fahren wir mit
TriangulateConnection
. Jetzt können wir
TriangulateEdgeStrip
, aber es müssen andere Ersetzungen vorgenommen werden. Wo wir früher
v1
, müssen wir
e1.v1
. In ähnlicher Weise wird
v2
zu
e1.v4
,
v3
zu
e2.v1
und
v4
zu
e2.v4
.
void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); bridge.y = neighbor.Position.y - cell.Position.y; EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v4 + bridge ); if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1.v1, e1.v4, cell, e2.v1, e2.v4, neighbor); } else { TriangulateEdgeStrip(e1, cell.color, e2, neighbor.color); } HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { Vector3 v5 = e1.v4 + HexMetrics.GetBridge(direction.Next()); v5.y = nextNeighbor.Position.y; if (cell.Elevation <= neighbor.Elevation) { if (cell.Elevation <= nextNeighbor.Elevation) { TriangulateCorner( e1.v4, cell, e2.v4, neighbor, v5, nextNeighbor ); } else { TriangulateCorner( v5, nextNeighbor, e1.v4, cell, e2.v4, neighbor ); } } else if (neighbor.Elevation <= nextNeighbor.Elevation) { TriangulateCorner( e2.v4, neighbor, v5, nextNeighbor, e1.v4, cell ); } else { TriangulateCorner( v5, nextNeighbor, e1.v4, cell, e2.v4, neighbor ); } }
Ledge Division
Wir müssen die Leisten teilen. Daher übergeben wir die Kanten an
TriangulateEdgeTerraces
.
if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor); }
Jetzt müssen wir
TriangulateEdgeTerraces
so ändern, dass es zwischen Kanten und nicht zwischen Scheitelpunktpaaren interpoliert. Nehmen wir an, dass
EdgeVertices
eine bequeme statische Methode dafür hat. Auf diese Weise können wir
TriangulateEdgeTerraces
vereinfachen, anstatt es zu komplizieren.
void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, 1); TriangulateEdgeStrip(begin, beginCell.color, e2, c2); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(beginCell.color, endCell.color, i); TriangulateEdgeStrip(e1, c1, e2, c2); } TriangulateEdgeStrip(e2, c2, end, endCell.color); }
Die
EdgeVertices.TerraceLerp
Methode interpoliert einfach die Leisten zwischen allen vier Scheitelpunktpaaren zweier Kanten.
public static EdgeVertices TerraceLerp ( EdgeVertices a, EdgeVertices b, int step) { EdgeVertices result; result.v1 = HexMetrics.TerraceLerp(a.v1, b.v1, step); result.v2 = HexMetrics.TerraceLerp(a.v2, b.v2, step); result.v3 = HexMetrics.TerraceLerp(a.v3, b.v3, step); result.v4 = HexMetrics.TerraceLerp(a.v4, b.v4, step); return result; }
Unterteilte Leisten.EinheitspaketVerbinden Sie Klippen und Felsvorsprünge wieder
Bisher haben wir Risse in der Kreuzung von Klippen und Felsvorsprüngen ignoriert. Es ist Zeit, dieses Problem zu lösen. Betrachten wir zunächst die Fälle Cliff-Slope-Slope (OSS) und Slope-Cliff-Slope (SOS).
Maschenlöcher.Das Problem entsteht, weil sich die Grenzen verschoben haben. Dies bedeutet, dass sie jetzt nicht genau auf der Seite der Klippe liegen, was zu einem Riss führt. Manchmal sind diese Löcher unsichtbar und manchmal auffällig.
Die Lösung besteht darin, den oberen Rand nicht zu verschieben. Dies bedeutet, dass wir steuern müssen, ob der Punkt verschoben wird. Am einfachsten wäre es, eine
AddTriangle
Alternative zu erstellen, die Scheitelpunkte überhaupt nicht verschiebt.
void AddTriangleUnperturbed (Vector3 v1, Vector3 v2, Vector3 v3) { int vertexIndex = vertices.Count; vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); triangles.Add(vertexIndex); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2); }
Ändern Sie das
TriangulateBoundaryTriangle
so, dass es diese Methode verwendet. Dies bedeutet, dass er alle Scheitelpunkte mit Ausnahme der Grenzscheitelpunkte explizit verschieben muss.
void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.TerraceLerp(begin, left, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangleUnperturbed(Perturb(begin), Perturb(v2), boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.TerraceLerp(begin, left, i); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangleUnperturbed(Perturb(v1), Perturb(v2), boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangleUnperturbed(Perturb(v2), Perturb(left), boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
Folgendes ist zu beachten: Da wir
v2
nicht verwenden, um einen anderen Punkt zu erhalten, können wir ihn sofort verschieben. Dies ist eine einfache Optimierung, die die Codemenge reduziert. Lassen Sie uns sie einführen.
void TriangulateBoundaryTriangle ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, 1); AddTriangleUnperturbed(Perturb(begin), v2, boundary); AddTriangleColor(beginCell.color, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginCell.color, leftCell.color, i); AddTriangleUnperturbed(v1, v2, boundary); AddTriangleColor(c1, c2, boundaryColor); } AddTriangleUnperturbed(v2, Perturb(left), boundary); AddTriangleColor(c2, leftCell.color, boundaryColor); }
Unbewegte Grenzen.Es sieht besser aus, aber wir sind noch nicht fertig. Innerhalb der
TriangulateCornerTerracesCliff
Methode wird der Grenzpunkt zwischen dem linken und dem rechten Punkt interpoliert. Diese Punkte wurden jedoch noch nicht verschoben. Damit der Grenzpunkt der resultierenden Klippe entspricht, müssen wir zwischen den bewegten Punkten interpolieren.
Vector3 boundary = Vector3.Lerp(Perturb(begin), Perturb(right), b);
Gleiches gilt für die
TriangulateCornerCliffTerraces
Methode.
Vector3 boundary = Vector3.Lerp(Perturb(begin), Perturb(left), b);
Die Löcher sind weg.Doppelklippen und Hang
In allen verbleibenden problematischen Fällen sind zwei Klippen und ein Hang vorhanden.
Großes Loch wegen eines einzelnen Dreiecks.Dieses Problem wird behoben, indem ein einzelnes Dreieck im
else
Block am Ende von
TriangulateCornerTerracesCliff
manuell
TriangulateCornerTerracesCliff
.
else { AddTriangleUnperturbed(Perturb(left), Perturb(right), boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); }
Gleiches gilt für
TriangulateCornerCliffTerraces
.
else { AddTriangleUnperturbed(Perturb(left), Perturb(right), boundary); AddTriangleColor(leftCell.color, rightCell.color, boundaryColor); }
Befreien Sie sich von den neuesten Rissen.EinheitspaketFertigstellung
Jetzt haben wir ein völlig korrektes verzerrtes Netz. Sein Aussehen hängt vom spezifischen Geräusch, seiner Größe und den Verzerrungskräften ab. In unserem Fall scheint die Verzerrung zu stark zu sein. Obwohl diese Ungleichmäßigkeit schön aussieht, möchten wir nicht, dass die Zellen zu stark vom geraden Gitter abweichen. Am Ende verwenden wir es immer noch, um die Zelle zu definieren, deren Größe geändert werden soll. Und wenn die Größe der Zellen zu stark variiert, wird es für uns schwieriger sein, den Inhalt darin zu platzieren.Unverzerrte und verzerrte Netze.Es scheint, dass die Kraft 5 zum Verzerren der Zellen zu groß ist.Die Verzerrung der Zellen liegt zwischen 0 und 5.Reduzieren wir sie auf 4, um den Komfort des Rasters zu erhöhen, ohne es zu korrekt zu machen. Dies stellt sicher, dass der maximale XZ-Versatz √32 ≈ 5,66 Einheiten beträgt. public const float cellPerturbStrength = 4f;
Zellverzerrungsstärke 4.Ein weiterer Wert, der geändert werden kann, ist der Integritätskoeffizient. Wenn wir es erhöhen, werden die flachen Zentren der Zellen größer, dh es wird mehr Platz für zukünftige Inhalte geben. Dabei werden sie natürlich sechseckiger.Integritätskoeffizient von 0,75 bis 0,95.Eine leichte Erhöhung des Integritätskoeffizienten auf 0,8 wird unser zukünftiges Leben leicht vereinfachen. public const float solidFactor = 0.8f;
Integritätskoeffizient 0.8.Schließlich stellen Sie möglicherweise fest, dass die Unterschiede zwischen den Höhenstufen zu stark sind. Dies ist praktisch, wenn Sie sicherstellen müssen, dass das Netz korrekt generiert wird. Damit sind wir jedoch bereits fertig. Reduzieren wir es auf 1 Einheit pro Schritt, dh auf 3. public const float elevationStep = 3f;
Die Tonhöhe wird auf 3 reduziert.Wir können auch die Stärke der Tonhöhenverzerrung ändern. Aber jetzt hat es einen Wert von 1,5, was einem halben Höhenschritt entspricht, was zu uns passt.Kleine Höhenstufen ermöglichen eine logischere Verwendung aller sieben Höhenstufen. Dies erhöht die Variabilität der Karte.Wir verwenden sieben Höhenstufen.EinheitspaketTeil 5: größere Karten
- Wir teilen das Gitter in Fragmente.
- Wir steuern die Kamera.
- Färben Sie die Farben und Höhen separat aus.
- Verwenden Sie den vergrößerten Pinsel der Zellen.
Bisher haben wir mit einer sehr kleinen Karte gearbeitet. Es ist Zeit, es zu erhöhen.Es ist Zeit zu zoomen.Netzfragmente
Wir können das Gitter nicht zu groß machen, weil wir an die Grenzen dessen stoßen, was in ein Netz passen kann. Wie kann man dieses Problem lösen? Verwenden Sie mehrere Netze. Dazu müssen wir unser Gitter in mehrere Fragmente aufteilen. Wir verwenden rechteckige Fragmente konstanter Größe.Teilen Sie das Gitter in 3 mal 3 Segmente.Verwenden Sie 5 mal 5 Blöcke, dh 25 Zellen pro Fragment. Definieren Sie sie in HexMetrics
. public const int chunkSizeX = 5, chunkSizeZ = 5;
Welche Fragmentgröße kann als geeignet angesehen werden?. , . . , (frustum culling), . .
Jetzt können wir keine Größe für das Netz verwenden, es muss ein Vielfaches der Fragmentgröße sein. Ändern wir es HexGrid
daher so, dass es seine Größe nicht in separaten Zellen, sondern in Fragmenten festlegt. Stellen Sie die Standardgröße auf 4 x 3 Fragmente ein, dh nur 12 Fragmente oder 300 Zellen. So bekommen wir eine bequeme Testkarte. public int chunkCountX = 4, chunkCountZ = 3;
Wir benutzen immer noch width
und height
, aber jetzt sollten sie privat werden. Und benenne sie in cellCountX
und um cellCountZ
. Verwenden Sie den Editor, um alle Vorkommen dieser Variablen gleichzeitig umzubenennen. Jetzt wird klar, wann es sich um die Anzahl der Fragmente oder Zellen handelt.
Geben Sie die Größe in Fragmenten an.Ändern Sie dies Awake
so, dass bei Bedarf die Anzahl der Zellen aus der Anzahl der Fragmente berechnet wird. Wir heben die Erstellung von Zellen in einer separaten Methode hervor, um nicht zu verstopfen Awake
. void Awake () { HexMetrics.noiseSource = noiseSource; gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cellCountX = chunkCountX * HexMetrics.chunkSizeX; cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; CreateCells(); } void CreateCells () { cells = new HexCell[cellCountZ * cellCountX]; for (int z = 0, i = 0; z < cellCountZ; z++) { for (int x = 0; x < cellCountX; x++) { CreateCell(x, z, i++); } } }
Fragment Fertighaus
Um die Netzfragmente zu beschreiben, benötigen wir einen neuen Komponententyp. using UnityEngine; using UnityEngine.UI; public class HexGridChunk : MonoBehaviour { }
Als nächstes erstellen wir ein vorgefertigtes Fragment. Dazu duplizieren wir das Hex Grid- Objekt und benennen es in Hex Grid Chunk um . Entfernen Sie die Komponente HexGrid
und fügen Sie stattdessen eine Komponente hinzu HexGridChunk
. Verwandeln Sie es dann in ein Fertighaus und entfernen Sie das Objekt aus der Szene.Ein Fragment vorgefertigt mit eigener Leinwand und Netz.Da er Instanzen dieser Fragmente erstellen HexGrid
wird, geben wir ihm einen Link zum Fertighaus des Fragments. public HexGridChunk chunkPrefab;
Jetzt mit Fragmenten.Das Erstellen von Instanzen von Fragmenten ähnelt dem Erstellen von Instanzen von Zellen. Wir werden sie mit Hilfe eines Arrays verfolgen und mit einer Doppelschleife füllen. HexGridChunk[] chunks; void Awake () { … CreateChunks(); CreateCells(); } void CreateChunks () { chunks = new HexGridChunk[chunkCountX * chunkCountZ]; for (int z = 0, i = 0; z < chunkCountZ; z++) { for (int x = 0; x < chunkCountX; x++) { HexGridChunk chunk = chunks[i++] = Instantiate(chunkPrefab); chunk.transform.SetParent(transform); } } }
Das Initialisieren eines Fragments ähnelt dem Initialisieren eines Sechseckgitters. Sie setzt alles ein Awake
und führt eine Triangulation durch Start
. Es erfordert einen Verweis auf die Zeichenfläche und das Netz sowie ein Array für die Zellen. Das Fragment erstellt diese Zellen jedoch nicht. Das Raster wird dies weiterhin tun. public class HexGridChunk : MonoBehaviour { HexCell[] cells; HexMesh hexMesh; Canvas gridCanvas; void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; } void Start () { hexMesh.Triangulate(cells); } }
Zuweisen von Zellen zu Fragmenten
HexGrid
erstellt immer noch alle Zellen. Dies ist normal, aber jetzt müssen wir jede Zelle einem geeigneten Fragment hinzufügen und dürfen sie nicht mit unserem eigenen Netz und unserer eigenen Leinwand festlegen. void CreateCell (int x, int z, int i) { … HexCell cell = cells[i] = Instantiate<HexCell>(cellPrefab);
Wir können das richtige Fragment mithilfe der Ganzzahldivision x
und z
der Fragmentgröße finden. void AddCellToChunk (int x, int z, HexCell cell) { int chunkX = x / HexMetrics.chunkSizeX; int chunkZ = z / HexMetrics.chunkSizeZ; HexGridChunk chunk = chunks[chunkX + chunkZ * chunkCountX]; }
Mithilfe von Zwischenergebnissen können wir auch den lokalen Index der Zelle in diesem Fragment bestimmen. Danach können Sie dem Fragment eine Zelle hinzufügen. void AddCellToChunk (int x, int z, HexCell cell) { int chunkX = x / HexMetrics.chunkSizeX; int chunkZ = z / HexMetrics.chunkSizeZ; HexGridChunk chunk = chunks[chunkX + chunkZ * chunkCountX]; int localX = x - chunkX * HexMetrics.chunkSizeX; int localZ = z - chunkZ * HexMetrics.chunkSizeZ; chunk.AddCell(localX + localZ * HexMetrics.chunkSizeX, cell); }
Anschließend wird HexGridChunk.AddCell
die Zelle in einem eigenen Array platziert und die übergeordneten Elemente für die Zelle und ihre Benutzeroberfläche festgelegt. public void AddCell (int index, HexCell cell) { cells[index] = cell; cell.transform.SetParent(transform, false); cell.uiRect.SetParent(gridCanvas.transform, false); }
Fegen
Zu diesem Zeitpunkt kann er HexGrid
die Leinwand und das Sechsecknetz seiner Kinder sowie den Code entfernen.
Da wir es losgeworden sind Refresh
, sollten wir es HexMapEditor
nicht mehr benutzen. void EditCell (HexCell cell) { cell.color = activeColor; cell.Elevation = activeElevation;
Das gereinigte Sechseckgitter.Nach dem Starten des Wiedergabemodus sieht die Karte immer noch gleich aus. Die Hierarchie der Objekte wird jedoch unterschiedlich sein. Hex Grid erstellt jetzt untergeordnete Fragmentobjekte, die Zellen sowie deren Netz und Zeichenfläche enthalten.Untergeordnete Fragmente im Wiedergabemodus.Vielleicht haben wir einige Probleme mit Zelletiketten. Zunächst haben wir die Beschriftungsbreite auf 5 gesetzt. Dies war ausreichend, um die beiden Zeichen, die für uns ausreichten, auf einer kleinen Karte anzuzeigen. Aber jetzt können wir Koordinaten wie −10 haben, in denen es drei Zeichen gibt. Sie passen nicht und werden zugeschnitten. Um dies zu beheben, erhöhen Sie die Breite der Zellenbezeichnung auf 10 oder noch mehr.Erweiterte Zellbezeichnungen.Jetzt können wir viel größere Karten erstellen! Da wir beim Start das gesamte Raster generieren, kann es lange dauern, große Karten zu erstellen. Aber nach der Fertigstellung werden wir einen riesigen Raum zum Experimentieren haben.Korrektur der Zellbearbeitung
Die Bearbeitung scheint derzeit nicht zu funktionieren, da das Raster nicht mehr aktualisiert wird. Wir Fragmente fügen Sie so ein Verfahren aktualisiert werden müssen , Refresh
um HexGridChunk
. public void Refresh () { hexMesh.Triangulate(cells); }
Wann sollten wir diese Methode aufrufen? Wir haben das gesamte Raster jedes Mal aktualisiert, da wir nur ein Netz hatten. Aber jetzt haben wir viele Fragmente. Anstatt sie alle jedes Mal zu aktualisieren, ist es viel effizienter, die geänderten Fragmente zu aktualisieren. Andernfalls wird das Wechseln großer Karten zu einem sehr langsamen Vorgang.Aber woher wissen wir, welches Fragment aktualisiert werden soll? Am einfachsten ist es, jeder Zelle mitzuteilen, zu welchem Fragment sie gehört. Dann kann die Zelle ihr Fragment aktualisieren, wenn diese Zelle geändert wird. Geben wir also einen HexCell
Link zu seinem Fragment. public HexGridChunk chunk;
HexGridChunk
kann sich beim Hinzufügen selbst zur Zelle hinzufügen. public void AddCell (int index, HexCell cell) { cells[index] = cell; cell.chunk = this; cell.transform.SetParent(transform, false); cell.uiRect.SetParent(gridCanvas.transform, false); }
Indem wir sie verbinden, ergänzen wir die HexCell
Methode Refresh
. Jedes Mal, wenn eine Zelle aktualisiert wird, wird einfach ihr Fragment aktualisiert. void Refresh () { chunk.Refresh(); }
Wir müssen es nicht HexCell.Refresh
allgemein machen, da die Zelle selbst besser weiß, wann sie geändert wurde. Zum Beispiel, nachdem seine Höhe geändert wurde. public int Elevation { get { return elevation; } set { … Refresh(); } }
Tatsächlich müssen wir es nur aktualisieren, wenn sich seine Höhe auf einen anderen Wert geändert hat. Sie muss nicht einmal etwas neu berechnen, wenn wir ihr dieselbe Größe wie zuvor zuweisen. Daher können wir den Anfang des Setters verlassen. public int Elevation { get { return elevation; } set { if (elevation == value) { return; } … } }
Wir werden jedoch auch Berechnungen zum ersten Mal überspringen, wenn die Höhe auf 0 gesetzt ist, da dies der Standardwert für die Maschenhöhe ist. Um dies zu vermeiden, werden wir den Anfangswert so festlegen, wie wir ihn niemals verwenden. int elevation = int.MinValue;
Was ist int.MinValue?, integer. C# integer —
32- , 2 32 integer, , . .
— −2 31 = −2 147 483 648. !
2 31 − 1 = 2 147 483 647. 2 31 - .
Um die Farbänderung der Zelle zu erkennen, müssen wir sie auch in eine Eigenschaft umwandeln. Benennen Sie es in Color
Großbuchstaben um und verwandeln Sie es dann in eine Eigenschaft mit einer privaten Variablen color
. Der Standardfarbwert ist transparentes Schwarz, was zu uns passt. public Color Color { get { return color; } set { if (color == value) { return; } color = value; Refresh(); } } Color color;
Wenn wir jetzt den Wiedergabemodus starten, erhalten wir Nullreferenzausnahmen. Dies geschieht, weil wir die Farbe und Höhe auf ihre Standardwerte setzen, bevor wir eine Zelle ihrem Fragment zuweisen. Es ist normal, dass wir die Fragmente zu diesem Zeitpunkt nicht aktualisieren, da wir sie triangulieren, nachdem die Initialisierung abgeschlossen ist. Mit anderen Worten, wir aktualisieren ein Fragment nur, wenn es zugewiesen ist. void Refresh () { if (chunk) { chunk.Refresh(); } }
Wir können endlich wieder die Zellen wechseln! Es tritt jedoch ein Problem auf. Beim Zeichnen entlang der Ränder von Fragmenten erscheinen Nähte.Fehler an den Grenzen von Fragmenten.Dies ist logisch, denn wenn sich eine einzelne Zelle ändert, ändern sich auch alle Verbindungen zu ihren Nachbarn. Und diese Nachbarn können in anderen Fragmenten sein. Die einfachste Lösung besteht darin, alle benachbarten Zellen zu aktualisieren, wenn sie unterschiedlich sind. void Refresh () { if (chunk) { chunk.Refresh(); for (int i = 0; i < neighbors.Length; i++) { HexCell neighbor = neighbors[i]; if (neighbor != null && neighbor.chunk != chunk) { neighbor.chunk.Refresh(); } } } }
Obwohl dies funktioniert, kann es sich herausstellen, dass wir ein Fragment mehrmals aktualisieren. Und wenn wir anfangen, mehrere Zellen gleichzeitig zu färben, wird alles schlimmer.Wir müssen jedoch nicht sofort nach der Aktualisierung des Fragments triangulieren. Stattdessen schreiben wir einfach, dass ein Update erforderlich ist, und triangulieren, nachdem die Änderung abgeschlossen ist.Da es HexGridChunk
nichts anderes tut, können wir seinen aktivierten Status verwenden, um die Notwendigkeit von Updates zu signalisieren. Bei der Aktualisierung wird die Komponente berücksichtigt. Durch mehrmaliges Einschalten ändert sich nichts. Die Komponente wird später aktualisiert. Wir werden an dieser Stelle triangulieren und die Komponente wieder deaktivieren.Wir verwenden LateUpdate
stattdessenUpdate
um sicherzustellen, dass die Triangulation erfolgt, nachdem die Änderung für den aktuellen Frame abgeschlossen ist. public void Refresh () {
Was ist der Unterschied zwischen Update und LateUpdate?Update
- . LateUpdate
. , .
Da unsere Komponente standardmäßig aktiviert ist, müssen wir nicht mehr explizit triangulieren Start
. Daher kann diese Methode entfernt werden.
Fragmente von 20 mal 20 mit 10.000 Zellen.Verallgemeinerte Listen
Obwohl wir die Art und Weise, wie das Gitter trianguliert wird, erheblich geändert haben, HexMesh
bleibt es gleich. Alles, was er zum Arbeiten braucht, ist eine Reihe von Zellen. Es ist ihm egal, ob es ein oder mehrere Sechsecke gibt. Wir haben jedoch noch nicht in Betracht gezogen, mehrere Netze zu verwenden. Vielleicht kann hier etwas verbessert werden?Die verwendeten HexMesh
Listen sind im Wesentlichen temporäre Puffer. Sie werden nur zur Triangulation verwendet. Und die Fragmente werden einzeln trianguliert. Daher benötigen wir in der Tat nur einen Satz von Listen und nicht einen Satz für jedes Sechsecknetzobjekt. Dies kann erreicht werden, indem die Listen statisch gemacht werden. static List<Vector3> vertices = new List<Vector3>(); static List<Color> colors = new List<Color>(); static List<int> triangles = new List<int>(); void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); meshCollider = gameObject.AddComponent<MeshCollider>(); hexMesh.name = "Hex Mesh";
Sind statische Listen wirklich so wichtig?. , , .
, . 20 20 100.
EinheitspaketKamerasteuerung
Die große Kamera ist wunderbar, aber sie ist nutzlos, wenn wir sie nicht sehen können. Um die gesamte Karte zu inspizieren, müssen wir die Kamera bewegen. Zoomen ist ebenfalls nützlich. Erstellen wir daher eine Kamera, um diese Aktionen auszuführen.Erstellen Sie ein Dummy-Objekt und nennen Sie es Hex Map Camera . Legen Sie die Transformationskomponente so ab, dass sie sich zum Ursprung bewegt, ohne ihre Drehung und Skalierung zu ändern. In dem es ein Kind - Objekt namens Schwenker , und er wird ein Child - Objekt hinzufügen Stock . Machen Sie die Hauptkamera zu einem Kind des Sticks und setzen Sie die Transformationskomponente zurück.Die Hierarchie der Kamera.Das Ziel des Kamerascharniers (Swivel) besteht darin, den Winkel zu steuern, in dem die Kamera auf die Karte schaut. Lassen Sie es uns drehen (45, 0, 0). Der Griff (Stick) steuert die Entfernung, in der sich die Kameras befinden. Setzen wir ihr eine Position (0, 0, -45).Jetzt brauchen wir eine Komponente, um dieses System zu steuern. Weisen Sie diese Komponente dem Stamm der Kamerahierarchie zu. Geben Sie ihm eine Verbindung zum Scharnier und Griff, um sie hineinzuholen Awake
. using UnityEngine; public class HexMapCamera : MonoBehaviour { Transform swivel, stick; void Awake () { swivel = transform.GetChild(0); stick = swivel.GetChild(0); } }
Hexagon Kartenkamera.Zoom
Die erste Funktion, die wir erstellen, ist das Zoomen (Zoomen). Wir können die aktuelle Zoomstufe mit der Float-Variablen steuern. Ein Wert von 0 bedeutet, dass wir vollständig entfernt sind, und ein Wert von 1 bedeutet, dass wir vollständig entfernt sind. Beginnen wir mit dem maximalen Zoom. float zoom = 1f;
Das Zoomen erfolgt normalerweise mit dem Mausrad oder der analogen Steuerung. Wir können es mit der Standard- Maus-ScrollWheel- Eingabeachse implementieren . Fügen Sie eine Methode hinzu Update
, die das Vorhandensein eines Eingabedeltas überprüft. Wenn eines vorhanden ist, wird die Zoomänderungsmethode aufgerufen. void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } } void AdjustZoom (float delta) { }
Um die Zoomstufe zu ändern, fügen wir einfach ein Delta hinzu und begrenzen dann den Wert (Klemme), um im Bereich von 0 bis 1 zu bleiben. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); }
Beim Vergrößern und Verkleinern sollte sich der Abstand zur Kamera entsprechend ändern. Dies kann durch Ändern der Position des Griffs in Z erfolgen. Fügen Sie zwei allgemeine Float-Variablen hinzu, um die Position des Griffs bei minimalem und maximalem Zoom anzupassen. Da wir eine relativ kleine Karte entwickeln, setzen Sie die Werte auf -250 und -45. public float stickMinZoom, stickMaxZoom;
Nach dem Ändern des Zooms führen wir eine lineare Interpolation zwischen diesen beiden Werten basierend auf dem neuen Zoomwert durch. Aktualisieren Sie dann die Position des Griffs. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); float distance = Mathf.Lerp(stickMinZoom, stickMaxZoom, zoom); stick.localPosition = new Vector3(0f, 0f, distance); }
Minimale und maximale Stick-Werte.Jetzt funktioniert der Zoom, ist aber bisher nicht sehr nützlich. Wenn der Zoom weiter entfernt ist, zeigt die Kamera normalerweise eine Draufsicht. Wir können dies durch Drehen des Scharniers realisieren. Daher addieren wir die Variablen min und max für das Scharnier. Stellen wir ihnen die Werte 90 und 45 ein. public float swivelMinZoom, swivelMaxZoom;
Wie bei der Griffposition interpolieren wir, um einen geeigneten Zoomwinkel zu finden. Dann stellen wir die Drehung des Scharniers ein. void AdjustZoom (float delta) { zoom = Mathf.Clamp01(zoom + delta); float distance = Mathf.Lerp(stickMinZoom, stickMaxZoom, zoom); stick.localPosition = new Vector3(0f, 0f, distance); float angle = Mathf.Lerp(swivelMinZoom, swivelMaxZoom, zoom); swivel.localRotation = Quaternion.Euler(angle, 0f, 0f); }
Der minimale und maximale Wert von Swivel.Die Änderungsrate des Zooms kann durch Ändern der Empfindlichkeit der Eingabeparameter des Mausrads angepasst werden. Sie finden sie unter Bearbeiten / Projekteinstellungen / Eingabe . Wenn Sie sie beispielsweise von 0,1 auf 0,025 ändern, wird der Zoom langsamer und gleichmäßiger geändert.Mausrad-Eingabemöglichkeiten.Umzug
Fahren wir nun mit dem Bewegen der Kamera fort. Die Bewegung in Richtung X und Z müssen wir Update
wie beim Zoom implementieren . Hierfür können wir horizontale und vertikale Eingabeachsen verwenden . Dadurch können wir die Kamera mit den Pfeilen und den WASD-Tasten bewegen. void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } float xDelta = Input.GetAxis("Horizontal"); float zDelta = Input.GetAxis("Vertical"); if (xDelta != 0f || zDelta != 0f) { AdjustPosition(xDelta, zDelta); } } void AdjustPosition (float xDelta, float zDelta) { }
Der einfachste Ansatz besteht darin, die aktuelle Position des Kamerasystems zu ermitteln, die Deltas X und Z hinzuzufügen und das Ergebnis der Position des Systems zuzuweisen. void AdjustPosition (float xDelta, float zDelta) { Vector3 position = transform.localPosition; position += new Vector3(xDelta, 0f, zDelta); transform.localPosition = position; }
Aus diesem Grund bewegt sich die Kamera, während Sie die Pfeile oder WASD gedrückt halten, jedoch nicht mit konstanter Geschwindigkeit. Dies hängt von der Bildrate ab. Um die Entfernung zu bestimmen, die Sie zurücklegen müssen, verwenden wir das Zeitdelta sowie die erforderliche Geschwindigkeit. Daher fügen wir eine gemeinsame Variable hinzu, moveSpeed
setzen sie auf 100 und multiplizieren sie dann mit dem Zeitdelta, um das Positionsdelta zu erhalten. public float moveSpeed; void AdjustPosition (float xDelta, float zDelta) { float distance = moveSpeed * Time.deltaTime; Vector3 position = transform.localPosition; position += new Vector3(xDelta, 0f, zDelta) * distance; transform.localPosition = position; }
Bewegungsgeschwindigkeit.Jetzt können wir uns mit konstanter Geschwindigkeit entlang der X- oder Z-Achse bewegen. Wenn wir uns jedoch gleichzeitig (diagonal) entlang beider Achsen bewegen, ist die Bewegung schneller. Um dies zu beheben, müssen wir den Delta-Vektor normalisieren. Auf diese Weise können Sie es als Ziel verwenden. void AdjustPosition (float xDelta, float zDelta) { Vector3 direction = new Vector3(xDelta, 0f, zDelta).normalized; float distance = moveSpeed * Time.deltaTime; Vector3 position = transform.localPosition; position += direction * distance; transform.localPosition = position; }
Die diagonale Bewegung ist jetzt korrekt implementiert, aber plötzlich stellt sich heraus, dass sich die Kamera auch nach dem Loslassen aller Tasten noch ziemlich lange weiterbewegt. Dies liegt daran, dass die Eingangsachsen unmittelbar nach dem Drücken der Tasten nicht sofort zu den Grenzwerten springen. Sie brauchen etwas Zeit dafür. Gleiches gilt für das Loslassen von Schlüsseln. Es braucht Zeit, um zu den Nullachsenwerten zurückzukehren. Da wir jedoch die Eingabewerte normalisiert haben, wird die maximale Geschwindigkeit ständig beibehalten.Wir können die Eingabeparameter anpassen, um die Verzögerungen zu beseitigen, aber sie vermitteln ein Gefühl der Glätte, das es wert ist, gespeichert zu werden. Wir können den extremsten Wert der Achsen als Dämpfungsbewegungskoeffizienten anwenden. Vector3 direction = new Vector3(xDelta, 0f, zDelta).normalized; float damping = Mathf.Max(Mathf.Abs(xDelta), Mathf.Abs(zDelta)); float distance = moveSpeed * damping * Time.deltaTime;
Bewegung mit Dämpfung.Jetzt funktioniert die Bewegung gut, zumindest mit zunehmendem Zoom. Aber in einiger Entfernung stellt sich heraus, dass es zu langsam ist. Mit reduziertem Zoom müssen wir beschleunigen. Dies kann durch Ersetzen einer Variablen moveSpeed
durch zwei für minimalen und maximalen Zoom und anschließendes Interpolieren erfolgen. Weisen Sie ihnen Werte von 400 und 100 zu.
Die Bewegungsgeschwindigkeit variiert mit der Zoomstufe.Jetzt können wir uns schnell auf der Karte bewegen! Tatsächlich können wir uns weit über die Karte hinausbewegen, aber dies ist unerwünscht. Die Kamera sollte in der Karte bleiben. Um dies sicherzustellen, müssen wir die Grenzen der Karte kennen, daher ist eine Verknüpfung mit dem Raster erforderlich. Fügen Sie es hinzu und verbinden Sie es. public HexGrid grid;
Sie müssen die Rastergröße anfordern.Nachdem wir an eine neue Position gewechselt sind, werden wir sie mit der neuen Methode begrenzen. void AdjustPosition (float xDelta, float zDelta) { … transform.localPosition = ClampPosition(position); } Vector3 ClampPosition (Vector3 position) { return position; }
Position X hat einen Minimalwert von 0 und das Maximum wird durch die Größe der Karte bestimmt. Vector3 ClampPosition (Vector3 position) { float xMax = grid.chunkCountX * HexMetrics.chunkSizeX * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); return position; }
Gleiches gilt für Position Z. Vector3 ClampPosition (Vector3 position) { float xMax = grid.chunkCountX * HexMetrics.chunkSizeX * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); float zMax = grid.chunkCountZ * HexMetrics.chunkSizeZ * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax); return position; }
In der Tat ist dies ein wenig ungenau. Der Startpunkt befindet sich in der Mitte der Zelle, nicht links. Daher soll die Kamera in der Mitte der Zellen ganz rechts anhalten. Subtrahieren Sie dazu die Hälfte der Zelle vom Maximum von X. float xMax = (grid.chunkCountX * HexMetrics.chunkSizeX - 0.5f) * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax);
Aus dem gleichen Grund müssen wir das Maximum Z reduzieren. Da sich die Metriken geringfügig unterscheiden, müssen wir die gesamte Zelle subtrahieren. float zMax = (grid.chunkCountZ * HexMetrics.chunkSizeZ - 1) * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax);
Mit der Bewegung, die wir gemacht haben, bleibt nur ein kleines Detail übrig. Manchmal reagiert die Benutzeroberfläche auf die Pfeiltasten, und dies führt dazu, dass sich der Schieberegler bewegt, wenn Sie die Kamera bewegen. Dies geschieht, wenn sich die Benutzeroberfläche als aktiv betrachtet, nachdem Sie darauf geklickt haben und sich der Cursor weiterhin darüber befindet.Sie können verhindern, dass die Benutzeroberfläche Tastatureingaben abhört. Dies kann erreicht werden, indem das EventSystem- Objekt angewiesen wird , Send Navigation Events nicht auszuführen .Keine Navigationsereignisse mehr.Drehen Sie sich
Möchten Sie sehen, was sich hinter der Klippe befindet? Es wäre bequem, die Kamera drehen zu können! Fügen wir diese Funktion hinzu.Die Zoomstufe ist für die Drehung nicht wichtig, nur die Geschwindigkeit reicht aus. Fügen Sie eine gemeinsame Variable hinzu rotationSpeed
und setzen Sie sie auf 180 Grad. Überprüfen Sie das Rotationsdelta, Update
indem Sie die Rotationsachse abtasten und gegebenenfalls die Rotation ändern. public float rotationSpeed; void Update () { float zoomDelta = Input.GetAxis("Mouse ScrollWheel"); if (zoomDelta != 0f) { AdjustZoom(zoomDelta); } float rotationDelta = Input.GetAxis("Rotation"); if (rotationDelta != 0f) { AdjustRotation(rotationDelta); } float xDelta = Input.GetAxis("Horizontal"); float zDelta = Input.GetAxis("Vertical"); if (xDelta != 0f || zDelta != 0f) { AdjustPosition(xDelta, zDelta); } } void AdjustRotation (float delta) { }
Drehgeschwindigkeit.Tatsächlich ist die Rotationsachse nicht standardmäßig. Wir müssen es selbst schaffen. Gehen Sie zu den Eingabeparametern und duplizieren Sie den obersten Eintrag Vertikal . Ändern Sie den Namen des Duplikats in Rotation und ändern Sie die Tasten in QE und ein Komma (,) mit einem Punkt (.).Eingangsachse drehen.Ich habe das Unity-Paket heruntergeladen. Warum habe ich diese Eingabe nicht?. Unity. , . , , .
Der Drehwinkel, den wir verfolgen und ändern werden AdjustRotation
. Danach drehen wir das gesamte Kamerasystem. float rotationAngle; void AdjustRotation (float delta) { rotationAngle += delta * rotationSpeed * Time.deltaTime; transform.localRotation = Quaternion.Euler(0f, rotationAngle, 0f); }
Da der Vollkreis 360 Grad beträgt, rollen wir den Drehwinkel so, dass er im Bereich von 0 bis 360 liegt. void AdjustRotation (float delta) { rotationAngle += delta * rotationSpeed * Time.deltaTime; if (rotationAngle < 0f) { rotationAngle += 360f; } else if (rotationAngle >= 360f) { rotationAngle -= 360f; } transform.localRotation = Quaternion.Euler(0f, rotationAngle, 0f); }
In Aktion schalten.Jetzt funktioniert die Rotation. Wenn Sie es überprüfen, können Sie sehen, dass die Bewegung absolut ist. Daher ist die Bewegung nach dem Drehen um 180 Grad das Gegenteil von dem, was erwartet wurde. Für den Benutzer wäre es viel bequemer, wenn die Bewegung relativ zum Betrachtungswinkel der Kamera ausgeführt wird. Wir können dies tun, indem wir die aktuelle Drehung mit der Bewegungsrichtung multiplizieren. void AdjustPosition (float xDelta, float zDelta) { Vector3 direction = transform.localRotation * new Vector3(xDelta, 0f, zDelta).normalized; … }
Relative Verschiebung.EinheitspaketErweiterte Bearbeitung
Jetzt, da wir eine größere Karte haben, können Sie die Kartenbearbeitungswerkzeuge verbessern. Das Ändern einer Zelle zu einem Zeitpunkt ist zu lang, daher wäre es schön, einen größeren Pinsel zu erstellen. Es ist auch praktisch, wenn Sie malen oder die Höhe ändern können, wobei alles andere unverändert bleibt.Optionale Farbe und Höhe
Wir können Farben optional machen, indem wir der Umschaltgruppe eine leere Auswahloption hinzufügen. Duplizieren Sie einen der Farbschalter und ersetzen Sie das Etikett durch --- oder ähnliches, um anzuzeigen, dass es sich nicht um eine Farbe handelt. Ändern Sie dann das Argument des Ereignisses On Value Changed in −1.Ungültiger Farbindex.Natürlich ist dieser Index für ein Array von Farben nicht gültig. Wir können damit bestimmen, ob Farbe auf Zellen angewendet werden soll. bool applyColor; public void SelectColor (int index) { applyColor = index >= 0; if (applyColor) { activeColor = colors[index]; } } void EditCell (HexCell cell) { if (applyColor) { cell.Color = activeColor; } cell.Elevation = activeElevation; }
Die Höhe wird durch einen Schieberegler gesteuert, daher können wir keinen Schalter hinzufügen. Stattdessen können wir einen separaten Schalter verwenden, um die Höhenbearbeitung zu aktivieren oder zu deaktivieren. Standardmäßig ist es aktiviert. bool applyElevation = true; void EditCell (HexCell cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } }
Fügen Sie der Benutzeroberfläche einen neuen Höhenschalter hinzu. Ich werde auch alles auf ein neues Bedienfeld setzen und den Höhenregler horizontal machen, damit die Benutzeroberfläche schöner wird.Optionale Farbe und Höhe.Um die Höhe zu aktivieren, benötigen wir eine neue Methode, die wir mit der Benutzeroberfläche verbinden. public void SetApplyElevation (bool toggle) { applyElevation = toggle; }
Stellen Sie durch Anschließen an den Höhenschalter sicher, dass die dynamische Bool-Methode oben in der Liste der Methoden verwendet wird. Die richtigen Versionen zeigen im Inspektor kein Häkchen an.Wir übermitteln den Status des Höhenschalters.Jetzt können wir nur noch die Färbung mit Blumen oder nur die Höhe wählen. Oder beides wie immer. Wir können uns sogar dafür entscheiden, weder das eine noch das andere zu ändern, aber bisher ist es für uns nicht besonders nützlich.Wechseln Sie zwischen Farbe und Höhe.Warum schaltet sich die Höhe bei der Auswahl einer Farbe aus?, toggle group. , , toggle group.
Pinselgröße
Fügen Sie zur Unterstützung der Größe des Pinsels eine ganzzahlige Variable brushSize
und eine Methode zum Festlegen über die Benutzeroberfläche hinzu. Wir werden den Schieberegler verwenden, also müssen wir den Wert erneut von float in int konvertieren. int brushSize; public void SetBrushSize (float size) { brushSize = (int)size; }
Schieberegler für Pinselgröße.Sie können einen neuen Schieberegler erstellen, indem Sie den Höhenschieberegler duplizieren. Ändern Sie den Maximalwert auf 4 und hängen Sie ihn an die entsprechende Methode an. Ich habe ihm auch einen Tag hinzugefügt.Einstellungen für den Pinselgrößen-Schieberegler.Jetzt, da wir mehrere Zellen gleichzeitig bearbeiten können, müssen wir die Methode verwenden EditCells
. Diese Methode ruft EditCell
alle beteiligten Zellen auf. Die ursprünglich ausgewählte Zelle wird als Mitte des Pinsels betrachtet. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCells(hexGrid.GetCell(hit.point)); } } void EditCells (HexCell center) { } void EditCell (HexCell cell) { … }
Die Größe des Pinsels bestimmt den Radius der Bearbeitung. Bei einem Radius von 0 ist dies nur eine zentrale Zelle. Mit einem Radius von 1 ist dies das Zentrum und seine Nachbarn. In einem Radius von 2 werden die Nachbarn des Zentrums und ihre unmittelbaren Nachbarn eingeschaltet. Usw.
Bis zu Radius 3.Um Zellen zu bearbeiten, müssen Sie sie in einer Schleife umgehen. Zuerst brauchen wir die X- und Z-Koordinaten des Zentrums. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; }
Wir finden die minimale Z-Koordinate durch Subtrahieren des Radius. Also definieren wir die Nulllinie. Ab dieser Linie durchlaufen wir die Linie, bis wir die Linie in der Mitte abdecken. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { } }
Die erste Zelle in der unteren Reihe hat dieselbe X-Koordinate wie die mittlere Zelle. Diese Koordinate nimmt mit zunehmender Zeilennummer ab.Die letzte Zelle hat immer eine X-Koordinate, die der Mittelkoordinate plus Radius entspricht.Jetzt können wir jede Zeile durchlaufen und Zellen anhand ihrer Koordinaten abrufen. for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { for (int x = centerX - r; x <= centerX + brushSize; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } }
Wir haben noch keine Methode HexGrid.GetCell
mit einem Koordinatenparameter, also erstellen Sie sie. Konvertieren Sie in die Koordinaten der Verschiebungen und erhalten Sie die Zelle. public HexCell GetCell (HexCoordinates coordinates) { int z = coordinates.Z; int x = coordinates.X + z / 2; return cells[x + z * cellCountX]; }
Der untere Teil des Pinsels, Größe 2.Wir bedecken den Rest des Pinsels und führen einen Zyklus von oben nach unten zur Mitte durch. In diesem Fall wird die Logik gespiegelt und die zentrale Zeile muss ausgeschlossen werden. void EditCells (HexCell center) { int centerX = center.coordinates.X; int centerZ = center.coordinates.Z; for (int r = 0, z = centerZ - brushSize; z <= centerZ; z++, r++) { for (int x = centerX - r; x <= centerX + brushSize; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } } for (int r = 0, z = centerZ + brushSize; z > centerZ; z--, r++) { for (int x = centerX - brushSize; x <= centerX + r; x++) { EditCell(hexGrid.GetCell(new HexCoordinates(x, z))); } } }
Der gesamte Pinsel, Größe 2.Dies funktioniert, es sei denn, unser Pinsel geht über die Ränder des Rasters hinaus. In diesem Fall erhalten wir eine Ausnahme für einen Index außerhalb des Bereichs. Um dies zu vermeiden, überprüfen Sie die Grenzen HexGrid.GetCell
und kehren Sie zurück, null
wenn eine nicht vorhandene Zelle angefordert wird. public HexCell GetCell (HexCoordinates coordinates) { int z = coordinates.Z; if (z < 0 || z >= cellCountZ) { return null; } int x = coordinates.X + z / 2; if (x < 0 || x >= cellCountX) { return null; } return cells[x + z * cellCountX]; }
Um eine Nullreferenzausnahme zu vermeiden, HexMapEditor
muss vor dem Bearbeiten geprüft werden, ob die Zelle tatsächlich vorhanden ist. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } } }
Mehrere Pinselgrößen verwenden.Schalten Sie die Sichtbarkeit der Zellenbeschriftung um
Meistens müssen wir keine Zelletiketten sehen. Machen wir sie also optional. Da jedes Stück seine eigene Leinwand schafft, fügen Sie ein Verfahren ShowUI
zu HexGridChunk
. Wenn die Benutzeroberfläche sichtbar sein soll, aktivieren wir die Zeichenfläche. Andernfalls deaktivieren Sie es. public void ShowUI (bool visible) { gridCanvas.gameObject.SetActive(visible); }
Lassen Sie uns die Benutzeroberfläche standardmäßig ausblenden. void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); hexMesh = GetComponentInChildren<HexMesh>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ]; ShowUI(false); }
Da das Auftreten von UI - Schalter für die gesamte Karte, fügen Sie eine Methode ShowUI
in HexGrid
. Es leitet die Anfrage nur an seine Fragmente weiter. public void ShowUI (bool visible) { for (int i = 0; i < chunks.Length; i++) { chunks[i].ShowUI(visible); } }
HexMapEditor
erhält die gleiche Methode und leitet die Anforderung an das Raster weiter. public void ShowUI (bool visible) { hexGrid.ShowUI(visible); }
Schließlich können wir der Benutzeroberfläche einen Schalter hinzufügen und diese verbinden.Tag-Sichtbarkeitsschalter.EinheitspaketTeil 6: Flüsse
- Hinzufügen von Flüssen zu Zellen.
- Drag & Drop-Unterstützung zum Zeichnen von Flüssen.
- Flussbetten schaffen.
- Verwenden mehrerer Netze pro Fragment.
- Erstellen Sie einen gemeinsamen Listenpool.
- Triangulation und Animation von fließendem Wasser.
Im vorherigen Teil haben wir über die Unterstützung großer Karten gesprochen. Jetzt können wir zu größeren Reliefelementen übergehen. Dieses Mal werden wir über die Flüsse sprechen.Flüsse fließen aus den Bergen.Flusszellen
Es gibt drei Möglichkeiten, einem Sechseckgitter Flüsse hinzuzufügen. Der erste Weg ist, sie von Zelle zu Zelle fließen zu lassen. So wird es in Endless Legend implementiert. Die zweite Möglichkeit besteht darin, sie zwischen den Zellen von Kante zu Kante fließen zu lassen. So wird es in Civilization 5 implementiert. Der dritte Weg besteht darin, überhaupt keine speziellen Flussstrukturen zu schaffen, sondern Wasserzellen zu verwenden, um sie vorzuschlagen. So werden die Flüsse in Age of Wonders 3 implementiert.In unserem Fall sind die Ränder der Zellen bereits von Hängen und Klippen besetzt. Dies lässt wenig Raum für Flüsse. Deshalb werden wir sie von Zelle zu Zelle fließen lassen. Dies bedeutet, dass in jeder Zelle entweder kein Fluss vorhanden ist oder ein Fluss entlang des Flusses fließt oder dass sich ein Anfang oder ein Ende des Flusses darin befindet. In den Zellen, entlang derer der Fluss fließt, kann er gerade fließen und eine oder zwei Schritte drehen.Fünf mögliche Flusskonfigurationen.Wir werden das Verzweigen oder Zusammenführen von Flüssen nicht unterstützen. Dies wird die Dinge noch komplizierter machen, insbesondere den Wasserfluss. Wir werden auch nicht von großen Wassermengen verwirrt sein. Wir werden sie in einem anderen Tutorial betrachten.Flussverfolgung
Die Zelle, entlang der der Fluss fließt, kann gleichzeitig als ein ein- und ausgehender Fluss betrachtet werden. Wenn es den Anfang eines Flusses enthält, hat es nur einen abgehenden Fluss. Und wenn es das Ende des Flusses enthält, dann hat es nur einen ankommenden Fluss. Wir können diese Informationen unter HexCell
Verwendung von zwei Booleschen Werten speichern . bool hasIncomingRiver, hasOutgoingRiver;
Das reicht aber nicht. Wir müssen auch die Richtung dieser Flüsse kennen. Im Fall eines abgehenden Flusses zeigt es an, wohin er sich bewegt. Bei einem ankommenden Fluss wird angezeigt, woher er stammt. bool hasIncomingRiver, hasOutgoingRiver; HexDirection incomingRiver, outgoingRiver;
Diese Informationen werden beim Triangulieren von Zellen benötigt, daher werden Eigenschaften hinzugefügt, um Zugriff darauf zu erhalten. Wir werden es nicht unterstützen, sie direkt zuzuweisen. Dazu fügen wir eine separate Methode hinzu. public bool HasIncomingRiver { get { return hasIncomingRiver; } } public bool HasOutgoingRiver { get { return hasOutgoingRiver; } } public HexDirection IncomingRiver { get { return incomingRiver; } } public HexDirection OutgoingRiver { get { return outgoingRiver; } }
Eine wichtige Frage ist, ob sich in der Zelle ein Fluss befindet, unabhängig von den Details. Fügen wir daher auch hierfür eine Eigenschaft hinzu. public bool HasRiver { get { return hasIncomingRiver || hasOutgoingRiver; } }
Eine andere logische Frage: ist der Anfang oder das Ende des Flusses in der Zelle. Wenn der Zustand des ein- und ausgehenden Flusses unterschiedlich ist, ist dies nur der Fall. Daher werden wir dies zu einer weiteren Eigenschaft machen. public bool HasRiverBeginOrEnd { get { return hasIncomingRiver != hasOutgoingRiver; } }
Und schließlich ist es hilfreich zu wissen, ob der Fluss durch einen bestimmten Kamm fließt, ob er ein- oder ausgeht. public bool HasRiverThroughEdge (HexDirection direction) { return hasIncomingRiver && incomingRiver == direction || hasOutgoingRiver && outgoingRiver == direction; }
Flussentfernung
Bevor wir einer Zelle einen Fluss hinzufügen, implementieren wir zunächst die Unterstützung für die Flussentfernung. Zunächst werden wir eine Methode schreiben, um nur den abgehenden Teil des Flusses zu entfernen.Wenn sich in der Zelle kein abfließender Fluss befindet, muss nichts unternommen werden. Andernfalls schalten Sie es aus und führen Sie das Update durch. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; Refresh(); }
Das ist aber noch nicht alles. Der abgehende Fluss muss irgendwo weitergehen. Daher muss es einen Nachbarn mit dem ankommenden Fluss geben. Wir müssen sie auch loswerden. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; Refresh(); HexCell neighbor = GetNeighbor(outgoingRiver); neighbor.hasIncomingRiver = false; neighbor.Refresh(); }
Kann ein Fluss nicht aus einer Karte fließen?, . , .
Das Entfernen eines Flusses aus einer Zelle ändert nur das Erscheinungsbild dieser Zelle. Im Gegensatz zur Bearbeitungshöhe oder -farbe hat dies keine Auswirkungen auf die Nachbarn. Daher müssen wir nur die Zelle selbst aktualisieren, nicht aber ihre Nachbarn. public void RemoveOutgoingRiver () { if (!hasOutgoingRiver) { return; } hasOutgoingRiver = false; RefreshSelfOnly(); HexCell neighbor = GetNeighbor(outgoingRiver); neighbor.hasIncomingRiver = false; neighbor.RefreshSelfOnly(); }
Diese Methode RefreshSelfOnly
aktualisiert einfach das Fragment, zu dem die Zelle gehört. Da wir den Fluss während der Netzinitialisierung nicht ändern, müssen wir uns keine Sorgen machen, wenn bereits ein Fragment zugewiesen wurde. void RefreshSelfOnly () { chunk.Refresh(); }
Das Entfernen eingehender Flüsse funktioniert genauso. public void RemoveIncomingRiver () { if (!hasIncomingRiver) { return; } hasIncomingRiver = false; RefreshSelfOnly(); HexCell neighbor = GetNeighbor(incomingRiver); neighbor.hasOutgoingRiver = false; neighbor.RefreshSelfOnly(); }
Und die Entfernung des gesamten Flusses bedeutet einfach die Entfernung sowohl der ankommenden als auch der abgehenden Teile des Flusses. public void RemoveRiver () { RemoveOutgoingRiver(); RemoveIncomingRiver(); }
Flüsse hinzufügen
Um die Entstehung von Flüssen zu unterstützen, benötigen wir eine Methode zur Spezifizierung des abgehenden Flusses der Zelle. Er muss alle vorherigen ausgehenden Flüsse neu definieren und den entsprechenden ankommenden Fluss festlegen.Zunächst müssen wir nichts tun, wenn der Fluss bereits existiert. public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } }
Als nächstes müssen wir sicherstellen, dass es einen Nachbarn in der richtigen Richtung gibt. Außerdem können Flüsse nicht fließen. Daher müssen wir die Operation abschließen, wenn der Nachbar höher ist. HexCell neighbor = GetNeighbor(direction); if (!neighbor || elevation < neighbor.elevation) { return; }
Als nächstes müssen wir den vorherigen abgehenden Fluss räumen. Und wir müssen auch den ankommenden Fluss entfernen, wenn er einem neuen abgehenden Fluss überlagert ist. RemoveOutgoingRiver(); if (hasIncomingRiver && incomingRiver == direction) { RemoveIncomingRiver(); }
Jetzt können wir mit dem Aufbau des ausgehenden Flusses fortfahren. hasOutgoingRiver = true; outgoingRiver = direction; RefreshSelfOnly();
Und vergessen Sie nicht, den eingehenden Fluss für eine andere Zelle festzulegen, nachdem Sie den aktuell eingehenden Fluss entfernt haben, falls vorhanden. neighbor.RemoveIncomingRiver(); neighbor.hasIncomingRiver = true; neighbor.incomingRiver = direction.Opposite(); neighbor.RefreshSelfOnly();
Flüsse losfließen lassen
Nachdem wir es nun möglich gemacht haben, nur die richtigen Flüsse hinzuzufügen, können andere Aktionen immer noch die falschen erzeugen. Wenn wir die Höhe der Zelle ändern, müssen wir erneut mit Nachdruck sicherstellen, dass die Flüsse nur nach unten fließen können. Alle unregelmäßigen Flüsse müssen entfernt werden. public int Elevation { get { return elevation; } set { … if ( hasOutgoingRiver && elevation < GetNeighbor(outgoingRiver).elevation ) { RemoveOutgoingRiver(); } if ( hasIncomingRiver && elevation > GetNeighbor(incomingRiver).elevation ) { RemoveIncomingRiver(); } Refresh(); } }
EinheitspaketFlüsse wechseln
Um die Flussbearbeitung zu unterstützen, müssen wir der Benutzeroberfläche einen Flussschalter hinzufügen. Tatsächlich. Wir brauchen Unterstützung für drei Bearbeitungsmodi. Wir müssen entweder die Flüsse ignorieren oder sie hinzufügen oder löschen. Wir können eine einfache Helfer-Aufzählung von Schaltern verwenden, um den Status zu verfolgen. Da wir es nur im Editor verwenden, können wir es innerhalb der Klasse HexMapEditor
zusammen mit dem Feld für den Flussmodus definieren. enum OptionalToggle { Ignore, Yes, No } OptionalToggle riverMode;
Und wir brauchen eine Methode, um das Flussregime über die Benutzeroberfläche zu ändern. public void SetRiverMode (int mode) { riverMode = (OptionalToggle)mode; }
Um das Flussregime zu steuern, fügen Sie der Benutzeroberfläche drei Schalter hinzu und verbinden Sie sie mit der neuen Umschaltgruppe, wie wir es mit den Farben getan haben. Ich habe die Schalter so konfiguriert, dass sich ihre Beschriftungen unter den Kontrollkästchen befinden. Aus diesem Grund bleiben sie dünn genug, um alle drei Optionen in einer Zeile unterzubringen.UI FlüsseWarum nicht eine Dropdown-Liste verwenden?, . dropdown list Unity Play. , .
Drag & Drop-Erkennung
Um einen Fluss zu schaffen, brauchen wir sowohl eine Zelle als auch eine Richtung. Im Moment HexMapEditor
liefert uns diese Information nicht. Daher müssen wir Drag & Drop-Unterstützung von einer Zelle zur anderen hinzufügen.Wir müssen wissen, ob dieser Widerstand korrekt ist, und auch seine Richtung bestimmen. Und um das Ziehen und Ablegen zu erkennen, müssen wir uns an die vorherige Zelle erinnern. bool isDrag; HexDirection dragDirection; HexCell previousCell;
Wenn das Ziehen nicht ausgeführt wird, ist dies in der vorherigen Zelle zunächst nicht der Fall. Das heißt, wenn keine Eingabe erfolgt oder wir nicht mit der Karte interagieren, müssen Sie ihr einen Wert zuweisen null
. void Update () { if ( Input.GetMouseButton(0) && !EventSystem.current.IsPointerOverGameObject() ) { HandleInput(); } else { previousCell = null; } } void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { EditCells(hexGrid.GetCell(hit.point)); } else { previousCell = null; } }
Die aktuelle Zelle ist diejenige, die wir gefunden haben, indem wir den Strahl mit dem Netz gekreuzt haben. Nach dem Bearbeiten der Zellen wird es aktualisiert und wird zur vorherigen Zelle für ein neues Update. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); EditCells(currentCell); previousCell = currentCell; } else { previousCell = null; } }
Nachdem wir die aktuelle Zelle ermittelt haben, können wir sie gegebenenfalls mit der vorherigen Zelle vergleichen. Wenn wir zwei verschiedene Zellen erhalten, haben wir möglicherweise das richtige Drag & Drop und müssen dies überprüfen. Ansonsten ist dies definitiv kein Drag & Drop. if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } EditCells(currentCell); previousCell = currentCell; isDrag = true; }
Wie überprüfen wir das Drag & Drop? Überprüfen, ob die aktuelle Zelle ein Nachbar der vorherigen ist. Wir überprüfen dies, indem wir seine Nachbarn in einem Zyklus umgehen. Wenn wir eine Übereinstimmung finden, erkennen wir auch sofort die Richtung des Ziehens. void ValidateDrag (HexCell currentCell) { for ( dragDirection = HexDirection.NE; dragDirection <= HexDirection.NW; dragDirection++ ) { if (previousCell.GetNeighbor(dragDirection) == currentCell) { isDrag = true; return; } } isDrag = false; }
Werden wir ruckartige Drags erzeugen?, . «» , .
, . .
Zellen ändern
Jetzt, da wir Drag & Drop erkennen können, können wir ausgehende Flüsse definieren. Wir können auch Flüsse entfernen, dafür ist keine Drag & Drop-Unterstützung erforderlich. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } else if (isDrag && riverMode == OptionalToggle.Yes) { previousCell.SetOutgoingRiver(dragDirection); } } }
Dieser Code zeichnet den Fluss von der vorherigen Zelle in die aktuelle. Aber er ignoriert die Größe des Pinsels. Das ist ziemlich logisch, aber zeichnen wir die Flüsse für alle Zellen, die von der Bürste geschlossen werden. Dies kann durch Ausführen von Operationen an der bearbeiteten Zelle erfolgen. In unserem Fall müssen wir sicherstellen, dass tatsächlich eine andere Zelle existiert. else if (isDrag && riverMode == OptionalToggle.Yes) { HexCell otherCell = cell.GetNeighbor(dragDirection.Opposite()); if (otherCell) { otherCell.SetOutgoingRiver(dragDirection); } }
Jetzt können wir die Flüsse bearbeiten, aber noch nicht sehen. Wir können überprüfen, ob dies funktioniert, indem wir die geänderten Zellen im Debug-Inspektor untersuchen.Eine Zelle mit einem Fluss im Debug-Inspektor.Was ist ein Debug-Inspektor?. . , .
EinheitspaketFlussbetten zwischen Zellen
Bei der Triangulation eines Flusses müssen zwei Teile berücksichtigt werden: die Lage des Flussbettes und das durch ihn fließende Wasser. Zuerst werden wir einen Kanal erstellen und das Wasser für später verlassen.Der einfachste Teil des Flusses ist dort, wo er in Verbindungsstellen zwischen Zellen fließt. Während wir diesen Bereich mit einem Streifen von drei Quad triangulieren. Wir können ein Flussbett hinzufügen, indem wir das mittlere Quad absenken und zwei Kanalwände hinzufügen.Hinzufügen eines Flusses zu einem Rippenstreifen.Dafür werden im Fall des Flusses zwei zusätzliche Quads benötigt und ein Kanal mit zwei vertikalen Wänden erstellt. Ein alternativer Ansatz ist die Verwendung von vier Quad. Dann senken wir den mittleren Gipfel, um ein Bett mit schrägen Wänden zu schaffen.Immer vier Quad.Die ständige Verwendung der gleichen Anzahl von Vierecken ist praktisch. Wählen Sie diese Option.Edge Tops hinzufügen
Der Übergang von drei zu vier pro Kante erfordert die Erstellung eines zusätzlichen Scheitelpunkts der Kante. Rewrite EdgeVertices
, erste Umbenennung v4
in v5
, und dann umbenannt v3
zu v4
. Aktionen in dieser Reihenfolge stellen sicher, dass der gesamte Code weiterhin auf die richtigen Scheitelpunkte verweist. Verwenden Sie dazu die Umbenennungs- oder Refactor-Option Ihres Editors, damit die Änderungen überall angewendet werden. Andernfalls müssen Sie den gesamten Code manuell überprüfen und Änderungen vornehmen. public Vector3 v1, v2, v4, v5;
Fügen Sie nach dem Umbenennen alles hinzu v3
. public Vector3 v1, v2, v3, v4, v5;
Fügen Sie dem Konstruktor einen neuen Scheitelpunkt hinzu. Es befindet sich in der Mitte zwischen den Eckgipfeln. Außerdem sollten andere Eckpunkte jetzt in ½ und ¾ und nicht in & frac13; und & frac23; public EdgeVertices (Vector3 corner1, Vector3 corner2) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, 0.25f); v3 = Vector3.Lerp(corner1, corner2, 0.5f); v4 = Vector3.Lerp(corner1, corner2, 0.75f); v5 = corner2; }
Hinzufügen v3
und hinzufügen TerraceLerp
. public static EdgeVertices TerraceLerp ( EdgeVertices a, EdgeVertices b, int step) { EdgeVertices result; result.v1 = HexMetrics.TerraceLerp(a.v1, b.v1, step); result.v2 = HexMetrics.TerraceLerp(a.v2, b.v2, step); result.v3 = HexMetrics.TerraceLerp(a.v3, b.v3, step); result.v4 = HexMetrics.TerraceLerp(a.v4, b.v4, step); result.v5 = HexMetrics.TerraceLerp(a.v5, b.v5, step); return result; }
Jetzt HexMesh
muss ich einen zusätzlichen Scheitelpunkt in die Fächerdreiecke der Rippe aufnehmen. void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { AddTriangle(center, edge.v1, edge.v2); AddTriangleColor(color); AddTriangle(center, edge.v2, edge.v3); AddTriangleColor(color); AddTriangle(center, edge.v3, edge.v4); AddTriangleColor(color); AddTriangle(center, edge.v4, edge.v5); AddTriangleColor(color); }
Und auch in seinen Streifen von Vierecken. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2 ) { AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); AddQuadColor(c1, c2); AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); AddQuadColor(c1, c2); AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); AddQuadColor(c1, c2); AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); AddQuadColor(c1, c2); }
Vergleich von vier und fünf Eckpunkten pro Kante.Die Höhe des Flussbettes
Wir haben den Kanal erstellt, indem wir die untere Oberseite der Rippe abgesenkt haben. Es bestimmt die vertikale Position des Flussbettes. Obwohl die genaue vertikale Position jeder Zelle verzerrt ist, müssen wir in Zellen mit derselben Höhe die gleiche Höhe des Flussbettes beibehalten. Dank dieses Wassers muss es nicht stromaufwärts fließen. Darüber hinaus sollte das Bett niedrig genug sein, um auch bei den am stärksten abgelenkten vertikalen Zellen darunter zu bleiben, während gleichzeitig genügend Platz für Wasser bleibt.Stellen Sie diesen Versatz auf ein HexMetrics
und drücken Sie ihn als Höhe aus. Offsets einer Ebene reichen aus. public const float streamBedElevationOffset = -1f;
Wir können diese Metrik verwenden, um Eigenschaften hinzuzufügen HexCell
, um die vertikale Position des Zellflussbettes zu erhalten. public float StreamBedY { get { return (elevation + HexMetrics.streamBedElevationOffset) * HexMetrics.elevationStep; } }
Kanal erstellen
Wenn HexMesh
einer der sechs dreieckigen Teile einer Zelle trianguliert ist, können wir bestimmen, ob ein Fluss entlang seines Randes fließt. Wenn ja, können wir den mittleren Gipfel der Rippe auf die Höhe des Flussbettes absenken. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; } TriangulateEdgeFan(center, e, cell.Color); if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } }
Ändern Sie den mittleren Scheitelpunkt der Rippe.Wir können sehen, wie die ersten Anzeichen des Flusses erscheinen, aber im Relief entstehen Löcher. Um sie zu schließen, müssen wir eine andere Kante ändern und dann die Verbindung triangulieren. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { HexCell neighbor = cell.GetNeighbor(direction); if (neighbor == null) { return; } Vector3 bridge = HexMetrics.GetBridge(direction); bridge.y = neighbor.Position.y - cell.Position.y; EdgeVertices e2 = new EdgeVertices( e1.v1 + bridge, e1.v5 + bridge ); if (cell.HasRiverThroughEdge(direction)) { e2.v3.y = neighbor.StreamBedY; } … }
Abgeschlossene Kanäle der Rippengelenke.EinheitspaketFlussbetten durch eine Zelle
Jetzt haben wir die richtigen Flussbetten zwischen den Zellen. Wenn der Fluss durch die Zelle fließt, enden die Kanäle immer in ihrer Mitte. Um dieses Problem zu lösen, muss es funktionieren. Beginnen wir mit dem Fall, dass ein Fluss direkt von einer Kante zur anderen durch eine Zelle fließt.Wenn es keinen Fluss gibt, kann jeder Teil der Zelle ein einfacher Fan von Dreiecken sein. Wenn der Fluss jedoch direkt fließt, muss ein Kanal eingefügt werden. Tatsächlich müssen wir den zentralen Scheitelpunkt in eine Linie strecken und so die beiden mittleren Dreiecke in Vierecke verwandeln. Dann verwandelt sich der Fächer der Dreiecke in ein Trapez.Wir fügen den Kanal in das Dreieck ein.Solche Kanäle sind viel länger als diejenigen, die durch die Verbindung von Zellen verlaufen. Dies wird deutlich, wenn die Scheitelpunktpositionen verzerrt sind. Teilen wir daher das Trapez in zwei Segmente, indem wir einen weiteren Satz von Scheitelpunktkanten in die Mitte zwischen der Mitte und der Kante einfügen.Kanal-Triangulation.Da sich die Triangulation mit einem Fluss stark von der Triangulation ohne Fluss unterscheidet, erstellen wir eine separate Methode dafür. Wenn wir einen Fluss haben, verwenden wir diese Methode, andernfalls hinterlassen wir einen Fan von Dreiecken. void Triangulate (HexDirection direction, HexCell cell) { Vector3 center = cell.Position; EdgeVertices e = new EdgeVertices( center + HexMetrics.GetFirstSolidCorner(direction), center + HexMetrics.GetSecondSolidCorner(direction) ); if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; TriangulateWithRiver(direction, cell, center, e); } } else { TriangulateEdgeFan(center, e, cell.Color); } if (direction <= HexDirection.SE) { TriangulateConnection(direction, cell, e); } } void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { }
Löcher, in denen es Flüsse geben sollte.Deaktivieren Sie vorübergehend die Zellverzerrung, um besser zu sehen, was passiert. public const float cellPerturbStrength = 0f;
Unverzerrte Spitzen.Triangulation direkt durch die Zelle
Um einen Kanal direkt durch einen Teil der Zelle zu erstellen, müssen wir die Mitte in eine Linie strecken. Diese Linie sollte die gleiche Breite wie der Kanal haben. Wir können den linken Scheitelpunkt finden, indem wir ¼ der Entfernung von der Mitte zur ersten Ecke des vorherigen Teils verschieben. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { Vector3 centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; }
Ähnliches gilt für den rechten Scheitelpunkt. In diesem Fall benötigen wir die zweite Ecke des nächsten Teils. Vector3 centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; Vector3 centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f;
Die Mittellinie kann durch Erstellen von Scheitelpunktkanten zwischen der Mitte und der Kante gefunden werden. EdgeVertices m = new EdgeVertices( Vector3.Lerp(centerL, e.v1, 0.5f), Vector3.Lerp(centerR, e.v5, 0.5f) );
Ändern Sie als Nächstes den mittleren Scheitelpunkt der Mittelrippe sowie die Mitte, da diese zu den unteren Punkten des Kanals werden. m.v3.y = center.y = e.v3.y;
Jetzt können wir TriangulateEdgeStrip
den Raum zwischen der Mittellinie und der Kantenlinie füllen. TriangulateEdgeStrip(m, cell.Color, e, cell.Color);
Komprimierte Kanäle.Leider sehen die Kanäle komprimiert aus. Dies geschieht, weil die mittleren Eckpunkte der Rippe zu nahe beieinander liegen. Warum ist das passiert?Wenn wir annehmen, dass die Länge der Außenkante 1 ist, beträgt die Länge der Mittellinie ½. Da sich die mittlere Kante in der Mitte zwischen ihnen befindet, sollte ihre Länge gleich ¾ sein.Die Kanalbreite beträgt ½ und sollte konstant bleiben. Da die Länge der Mittelkante ¾ beträgt, bleibt laut & frac18; nur ¼ übrig. auf beiden Seiten des Kanals.Relative Längen.Da die Länge der Mittelkante ¾ beträgt, ist & frac18; wird relativ zur Länge der Mittelrippe gleich & frac16; Dies bedeutet, dass der zweite und vierte Eckpunkt mit Sechsteln und nicht mit Vierteln interpoliert werden sollten.Wir können eine solche alternative Interpolation unterstützen, indem wir sie einem EdgeVertices
anderen Konstruktor hinzufügen . Anstelle fester Interpolationen für v2
und verwenden v4
wir einen Parameter. public EdgeVertices (Vector3 corner1, Vector3 corner2, float outerStep) { v1 = corner1; v2 = Vector3.Lerp(corner1, corner2, outerStep); v3 = Vector3.Lerp(corner1, corner2, 0.5f); v4 = Vector3.Lerp(corner1, corner2, 1f - outerStep); v5 = corner2; }
Jetzt können wir es mit & frac16; c HexMesh.TriangulateWithRiver
. EdgeVertices m = new EdgeVertices( Vector3.Lerp(centerL, e.v1, 0.5f), Vector3.Lerp(centerR, e.v5, 0.5f), 1f / 6f );
Direkte Kanäle.Nachdem wir den Kanal gerade gemacht haben, können wir zum zweiten Teil des Trapezes gehen. In diesem Fall können wir den Rippenstreifen nicht verwenden, daher müssen wir dies manuell tun. Lassen Sie uns zuerst Dreiecke an den Seiten erstellen. AddTriangle(centerL, m.v1, m.v2); AddTriangleColor(cell.Color); AddTriangle(centerR, m.v4, m.v5); AddTriangleColor(cell.Color);
Seitendreiecke.Es sieht gut aus, also füllen wir den verbleibenden Raum mit zwei Vierecken und erstellen den letzten Teil des Kanals. AddTriangle(centerL, m.v1, m.v2); AddTriangleColor(cell.Color); AddQuad(centerL, center, m.v2, m.v3); AddQuadColor(cell.Color); AddQuad(center, centerR, m.v3, m.v4); AddQuadColor(cell.Color); AddTriangle(centerR, m.v4, m.v5); AddTriangleColor(cell.Color);
Tatsächlich haben wir keine Alternative, AddQuadColor
die nur einen Parameter erfordert. Wir haben es zwar nicht gebraucht. Also lasst es uns schaffen. void AddQuadColor (Color color) { colors.Add(color); colors.Add(color); colors.Add(color); colors.Add(color); }
Gerade Kanäle abgeschlossen.Triangulation starten und beenden
Die Triangulation eines Teils, der nur den Anfang oder das Ende eines Flusses hat, ist ganz anders und erfordert daher eine eigene Methode. Daher werden wir dies einchecken Triangulate
und die entsprechende Methode aufrufen. if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; if (cell.HasRiverBeginOrEnd) { TriangulateWithRiverBeginOrEnd(direction, cell, center, e); } else { TriangulateWithRiver(direction, cell, center, e); } } }
In diesem Fall möchten wir den Kanal in der Mitte vervollständigen, verwenden dafür jedoch immer noch zwei Stufen. Daher erstellen wir erneut die mittlere Kante zwischen der Mitte oder der Kante. Da wir den Kanal vervollständigen möchten, freuen wir uns sehr, dass er komprimiert wird. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) ); }
Damit der Kanal nicht zu schnell flach wird, ordnen wir die Höhe des Flussbettes dem mittleren Gipfel zu. Das Zentrum muss jedoch nicht geändert werden. m.v3.y = e.v3.y;
Wir können mit einem Rippenstreifen und einem Fächer triangulieren. TriangulateEdgeStrip(m, cell.Color, e, cell.Color); TriangulateEdgeFan(center, m, cell.Color);
Start- und Endpunkte.One-Step-Turns
Betrachten Sie als nächstes die scharfen Kurven im Zickzack zwischen benachbarten Zellen. Wir werden auch damit umgehen TriangulateWithRiver
. Daher müssen wir bestimmen, mit welcher Art von Fluss wir arbeiten.Zick-Zack-Fluss.Wenn in der Zelle ein Fluss fließt, der sowohl in die entgegengesetzte Richtung als auch in die Richtung fließt, mit der wir arbeiten, sollte dies ein gerader Fluss sein. In diesem Fall können wir die bereits berechnete Mittellinie speichern. Andernfalls kehrt es zu einem Punkt zurück und faltet die Mittellinie. Vector3 centerL, centerR; if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else { centerL = centerR = center; }
Gekräuselte Zickzacke.Wir können scharfe Kurven erkennen, indem wir prüfen, ob in der Zelle ein Fluss durch den nächsten oder vorherigen Teil der Zelle fließt. Wenn ja, müssen wir die Mittellinie an der Kante zwischen diesem und dem benachbarten Teil ausrichten. Wir können dies tun, indem wir die entsprechende Seite der Linie in der Mitte zwischen der Mitte und dem gemeinsamen Winkel platzieren. Die andere Seite der Linie wird dann zur Mitte. if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 0.5f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 0.5f); centerR = center; } else { centerL = centerR = center; }
Nachdem wir entschieden haben, wo sich der linke und der rechte Punkt befinden, können wir das resultierende Zentrum bestimmen, indem wir sie mitteln. if (cell.HasRiverThroughEdge(direction.Opposite())) { … } center = Vector3.Lerp(centerL, centerR, 0.5f);
Versetzte Mittelrippe.Obwohl der Kanal auf beiden Seiten die gleiche Breite hat, sieht er ziemlich komprimiert aus. Dies wird durch Drehen der Mittellinie um 60 ° verursacht. Sie können diesen Effekt glätten, indem Sie die Breite der Mittellinie leicht erhöhen. Anstatt mit ½ zu interpolieren, verwenden wir & frac23; else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 2f / 3f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 2f / 3f); centerR = center; }
Zickzack ohne Kompression.Zweistufige Kurven
Die restlichen Fälle liegen zwischen Zickzack und geraden Flüssen. Dies sind zweistufige Kurven, die sanft geschwungene Flüsse erzeugen.Der sich schlängelnde Fluss.Um zwischen zwei möglichen Orientierungen zu unterscheiden, müssen wir verwenden direction.Next().Next()
. Aber lassen Sie uns machen es bequemer durch Hinzufügen von HexDirection
Erweiterungsmethoden Next2
und Previous2
. public static HexDirection Previous2 (this HexDirection direction) { direction -= 2; return direction >= HexDirection.NE ? direction : (direction + 6); } public static HexDirection Next2 (this HexDirection direction) { direction += 2; return direction <= HexDirection.NW ? direction : (direction - 6); }
Zurück zu HexMesh.TriangulateWithRiver
. Jetzt können wir die Richtung unseres mäandrierenden Flusses mit erkennen direction.Next2()
. if (cell.HasRiverThroughEdge(direction.Opposite())) { centerL = center + HexMetrics.GetFirstSolidCorner(direction.Previous()) * 0.25f; centerR = center + HexMetrics.GetSecondSolidCorner(direction.Next()) * 0.25f; } else if (cell.HasRiverThroughEdge(direction.Next())) { centerL = center; centerR = Vector3.Lerp(center, e.v5, 2f / 3f); } else if (cell.HasRiverThroughEdge(direction.Previous())) { centerL = Vector3.Lerp(center, e.v1, 2f / 3f); centerR = center; } else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = centerR = center; } else { centerL = centerR = center; }
In diesen beiden letzten Fällen müssen wir die Mittellinie auf den Teil der Zelle verschieben, der sich auf der Innenseite der Kurve befindet. Wenn wir einen Vektor in der Mitte einer festen Kante hätten, könnten wir ihn verwenden, um den Endpunkt zu positionieren. Stellen wir uns vor, wir haben eine Methode dafür. else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = center; centerR = center + HexMetrics.GetSolidEdgeMiddle(direction.Next()) * 0.5f; } else { centerL = center + HexMetrics.GetSolidEdgeMiddle(direction.Previous()) * 0.5f; centerR = center; }
Natürlich müssen wir jetzt eine solche Methode hinzufügen HexMetrics
. Er muss nur zwei Vektoren benachbarter Winkel mitteln und den Integritätskoeffizienten anwenden. public static Vector3 GetSolidEdgeMiddle (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * (0.5f * solidFactor); }
Leicht komprimierte Kurven.Unsere Mittellinien sind jetzt korrekt um 30 ° gedreht. Sie sind aber nicht lang genug, weshalb die Kanäle etwas komprimiert sind. Dies geschieht, weil der Mittelpunkt der Rippe näher an der Mitte liegt als der Winkel der Rippe. Sein Abstand entspricht dem inneren Radius, nicht dem äußeren. Das heißt, wir arbeiten im falschen Maßstab.Wir konvertieren bereits von extern zu intern Radius bei HexMetrics
. Wir müssen den umgekehrten Vorgang ausführen. Lassen Sie uns also beide Umrechnungsfaktoren über verfügbar machen HexMetrics
. public const float outerToInner = 0.866025404f; public const float innerToOuter = 1f / outerToInner; public const float outerRadius = 10f; public const float innerRadius = outerRadius * outerToInner;
Jetzt können wir zur richtigen Skala übergehen HexMesh.TriangulateWithRiver
. Die Kanäle bleiben aufgrund ihrer Drehung immer noch etwas zusammengedrückt, dies ist jedoch viel weniger ausgeprägt als im Fall von Zickzacklinien. Daher müssen wir dies nicht kompensieren. else if (cell.HasRiverThroughEdge(direction.Next2())) { centerL = center; centerR = center + HexMetrics.GetSolidEdgeMiddle(direction.Next()) * (0.5f * HexMetrics.innerToOuter); } else { centerL = center + HexMetrics.GetSolidEdgeMiddle(direction.Previous()) * (0.5f * HexMetrics.innerToOuter); centerR = center; }
Glatte Kurven.EinheitspaketTriangulation in der Nähe von Flüssen
Unsere Flüsse sind bereit. Aber wir haben noch keine anderen Teile der Zellen, die die Flüsse enthalten, trianguliert. Jetzt werden wir diese Löcher schließen.Löcher in der Nähe der Kanäle.Wenn die Zelle einen Fluss hat, dieser aber nicht in die aktuelle Richtung fließt, Triangulate
rufen wir eine neue Methode in auf. if (cell.HasRiver) { if (cell.HasRiverThroughEdge(direction)) { e.v3.y = cell.StreamBedY; if (cell.HasRiverBeginOrEnd) { TriangulateWithRiverBeginOrEnd(direction, cell, center, e); } else { TriangulateWithRiver(direction, cell, center, e); } } else { TriangulateAdjacentToRiver(direction, cell, center, e); } } else { TriangulateEdgeFan(center, e, cell.Color); }
Bei dieser Methode füllen wir das Zellendreieck mit einem Streifen und einem Fächer. Nur ein Ventilator wird uns nicht ausreichen, da die Gipfel dem mittleren Rand der Teile entsprechen sollten, die den Fluss enthalten. void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) ); TriangulateEdgeStrip(m, cell.Color, e, cell.Color); TriangulateEdgeFan(center, m, cell.Color); }
Überlagerung in Kurven und geraden Flüssen.Passen Sie den Kanal an
Natürlich müssen wir das Zentrum, das wir verwenden, an den zentralen Teil anpassen, der von den Flussteilen verwendet wird. Mit Zickzack ist alles in Ordnung und Kurven und gerade Flüsse erfordern Aufmerksamkeit. Daher müssen wir sowohl den Flusstyp als auch seine relative Ausrichtung bestimmen.Beginnen wir mit der Überprüfung, ob wir uns innerhalb der Kurve befinden. In diesem Fall enthalten sowohl die vorherige als auch die nächste Richtung den Fluss. Wenn ja, müssen wir die Mitte an den Rand verschieben. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } } EdgeVertices m = new EdgeVertices( Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f) );
Es wurde ein Fall behoben, in dem der Fluss von beiden Seiten fließt.Wenn wir einen Fluss in eine andere Richtung haben, aber nicht in die vorherige, prüfen wir, ob er gerade ist. Wenn ja, verschieben Sie die Mitte in die erste Ecke. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } else if ( cell.HasRiverThroughEdge(direction.Previous2()) ) { center += HexMetrics.GetFirstSolidCorner(direction) * 0.25f; } }
Feste halbe Überlagerung mit einem geraden Fluss.Also haben wir das Problem mit der Hälfte der Teile gelöst, die an die geraden Flüsse angrenzen. Der letzte Fall - wir haben einen Fluss in der vorherigen Richtung und er ist gerade. In diesem Fall müssen Sie die Mitte in die nächste Ecke verschieben. if (cell.HasRiverThroughEdge(direction.Next())) { if (cell.HasRiverThroughEdge(direction.Previous())) { center += HexMetrics.GetSolidEdgeMiddle(direction) * (HexMetrics.innerToOuter * 0.5f); } else if ( cell.HasRiverThroughEdge(direction.Previous2()) ) { center += HexMetrics.GetFirstSolidCorner(direction) * 0.25f; } } else if ( cell.HasRiverThroughEdge(direction.Previous()) && cell.HasRiverThroughEdge(direction.Next2()) ) { center += HexMetrics.GetSecondSolidCorner(direction) * 0.25f; }
Keine Überlagerungen mehr.EinheitspaketHexMesh-Generalisierung
Wir haben die Triangulation der Kanäle abgeschlossen. Jetzt können wir sie mit Wasser füllen. Da sich Wasser von Land unterscheidet, müssen wir ein anderes Netz mit unterschiedlichen Scheitelpunktdaten und unterschiedlichem Material verwenden. Es wäre sehr praktisch, wenn wir HexMesh
sowohl Sushi als auch Wasser verwenden könnten . Verallgemeinern wir es also, HexMesh
indem wir es in eine Klasse verwandeln, die sich mit diesen Netzen befasst, unabhängig davon, wofür es verwendet wird. Wir werden die Aufgabe der Triangulation seiner Zellen weitergeben HexGridChunk
.Verschieben der Störungsmethode
Da die Methode Perturb
ziemlich verallgemeinert ist und an verschiedenen Stellen verwendet wird, verschieben wir sie auf HexMetrics
. Benennen Sie es zunächst in um HexMetrics.Perturb
. Dies ist ein falscher Methodenname, der jedoch den gesamten Code für die ordnungsgemäße Verwendung umgestaltet. Wenn Ihr Code-Editor über spezielle Funktionen zum Verschieben von Methoden verfügt, verwenden Sie diese.Wenn Sie die Methode nach innen verschieben HexMetrics
, machen Sie sie allgemein und statisch und korrigieren Sie dann ihren Namen. public static Vector3 Perturb (Vector3 position) { Vector4 sample = SampleNoise(position); position.x += (sample.x * 2f - 1f) * cellPerturbStrength; position.z += (sample.z * 2f - 1f) * cellPerturbStrength; return position; }
Triangulationsmethoden verschieben
Bei HexGridChunk
der Änderung von Variablen hexMesh
in den gemeinsamen Variablen terrain
. public HexMesh terrain;
Als nächstes überarbeiten wir alle Methoden Add…
aus HexMesh
c terrain.Add…
. Verschieben Sie dann alle Methoden Triangulate…
nach HexGridChunk
. Danach können Sie die Namen der Methoden Add…
in korrigieren HexMesh
und allgemein machen. Infolgedessen werden alle komplexen Triangulationsmethoden gefunden HexGridChunk
, und einfache Methoden zum Hinzufügen von Daten zum Netz bleiben erhalten HexMesh
.Wir sind noch nicht fertig. Jetzt HexGridChunk.LateUpdate
sollte es seine eigene Methode aufrufen Triangulate
. Außerdem sollte es keine Zellen mehr als Argument übergeben. Daher kann es Triangulate
seinen Parameter verlieren. Und er muss die Reinigung und Anwendung der Netzdaten delegieren HexMesh
. void LateUpdate () { Triangulate();
Fügen Sie die erforderlichen Methoden Clear
und Apply
in HexMesh
. public void Clear () { hexMesh.Clear(); vertices.Clear(); colors.Clear(); triangles.Clear(); } public void Apply () { hexMesh.SetVertices(vertices); hexMesh.SetColors(colors); hexMesh.SetTriangles(triangles, 0); hexMesh.RecalculateNormals(); meshCollider.sharedMesh = hexMesh; }
Was ist mit SetVertices, SetColors und SetTriangles?Mesh
. . , .
SetTriangles
integer, . , .
Zum Schluss hängen Sie das untergeordnete Element des Netzes manuell an das Fragment-Fertighaus an. Wir können dies nicht mehr automatisch tun, da wir dem Netz bald ein zweites Kind hinzufügen werden. Benennen Sie es in Terrain um, um seinen Zweck anzugeben.Weisen Sie eine Erleichterung zu.Das Umbenennen eines Fertighauses funktioniert nicht?. , . , Apply , . .
Listenpools erstellen
Obwohl wir ziemlich viel Code verschoben haben, sollte unsere Karte immer noch genauso funktionieren wie zuvor. Das Hinzufügen eines weiteren Netzes zum Fragment ändert dies nicht. Wenn wir dies jedoch mit der Gegenwart tun HexMesh
, können Fehler auftreten.Das Problem ist, dass wir davon ausgegangen sind, dass wir jeweils nur mit einem Netz arbeiten würden. Dies ermöglichte es uns, statische Listen zum Speichern temporärer Netzdaten zu verwenden. Nach dem Hinzufügen von Wasser arbeiten wir jedoch gleichzeitig mit zwei Maschen, sodass wir keine statischen Listen mehr verwenden können.Wir werden jedoch nicht für jede Instanz zu den Listensätzen zurückkehren HexMesh
. Stattdessen verwenden wir einen statischen Listenpool. Standardmäßig ist dieses Pooling nicht vorhanden. Beginnen wir also damit, selbst eine gemeinsame Listenpoolklasse zu erstellen. public static class ListPool<T> { }
Wie funktioniert ListPool <T>?, List<int>
. <T>
ListPool
, , . , T
( template).
Um eine Sammlung von Listen in einem Pool zu speichern, können wir den Stapel verwenden. Normalerweise verwende ich keine Listen, da Unity sie nicht serialisiert, aber in diesem Fall spielt es keine Rolle. using System.Collections.Generic; public static class ListPool<T> { static Stack<List<T>> stack = new Stack<List<T>>(); }
Was bedeutet Stapel <Liste <t>>?. , . .
Fügen Sie eine allgemeine statische Methode hinzu, um die Liste aus dem Pool abzurufen. Wenn der Stapel nicht leer ist, extrahieren wir die Top-Liste und geben diese zurück. Andernfalls erstellen wir eine neue Liste. public static List<T> Get () { if (stack.Count > 0) { return stack.Pop(); } return new List<T>(); }
Um Listen wiederzuverwenden, müssen Sie sie dem Pool hinzufügen, nachdem Sie mit ihnen gearbeitet haben. ListPool
löscht die Liste und schiebt sie auf den Stapel. public static void Add (List<T> list) { list.Clear(); stack.Push(list); }
Jetzt können wir die Pools in nutzen HexMesh
. Ersetzen Sie statische Listen durch nicht statische private Links. Markieren wir sie NonSerialized
so, dass Unity sie beim Neukompilieren nicht beibehält. Oder schreiben System.NonSerialized
oder using System;
am Anfang des Skripts hinzufügen . [NonSerialized] List<Vector3> vertices; [NonSerialized] List<Color> colors; [NonSerialized] List<int> triangles;
Da das Netz unmittelbar vor dem Hinzufügen neuer Daten bereinigt wird, müssen Sie hier Listen aus Pools abrufen. public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); colors = ListPool<Color>.Get(); triangles = ListPool<int>.Get(); }
Nachdem wir diese Netze angewendet haben, brauchen wir sie nicht mehr, sodass wir sie hier zu den Pools hinzufügen können. public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); hexMesh.SetColors(colors); ListPool<Color>.Add(colors); hexMesh.SetTriangles(triangles, 0); ListPool<int>.Add(triangles); hexMesh.RecalculateNormals(); meshCollider.sharedMesh = hexMesh; }
Daher haben wir Listen mehrfach verwendet, unabhängig davon, wie viele Netze wir gleichzeitig füllen.Optionaler Collider
Obwohl unser Gelände einen Collider benötigt, wird er für Flüsse nicht wirklich benötigt. Die Strahlen passieren einfach das Wasser und schneiden sich mit dem darunter liegenden Kanal. Machen wir es so, dass wir das Vorhandensein eines Colliders für konfigurieren können HexMesh
. Wir erkennen dies, indem wir ein gemeinsames Feld hinzufügen bool useCollider
. Für das Gelände schalten wir es ein. public bool useCollider;
Verwenden eines Mesh-Colliders.Der Collider muss erst erstellt und zugewiesen werden, wenn er aktiviert ist. void Awake () { GetComponent<MeshFilter>().mesh = hexMesh = new Mesh(); if (useCollider) { meshCollider = gameObject.AddComponent<MeshCollider>(); } hexMesh.name = "Hex Mesh"; } public void Apply () { … if (useCollider) { meshCollider.sharedMesh = hexMesh; } … }
Optionale Farben
Scheitelpunktfarben können auch optional sein. Wir brauchen sie, um verschiedene Arten von Reliefs zu demonstrieren, aber Wasser ändert seine Farbe nicht. Wir können sie optional machen, genauso wie wir den Collider optional gemacht haben. public bool useCollider, useColors; public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useColors) { colors = ListPool<Color>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useColors) { hexMesh.SetColors(colors); ListPool<Color>.Add(colors); } … }
Natürlich sollte das Gelände die Farben der Eckpunkte verwenden, also schalten Sie sie ein.Verwendung von Farben.Optionales UV
Währenddessen können wir auch Unterstützung für optionale UV-Koordinaten hinzufügen. Obwohl das Relief sie nicht benutzt, werden wir sie für Wasser brauchen. public bool useCollider, useColors, useUVCoordinates; [NonSerialized] List<Vector2> uvs; public void Clear () { hexMesh.Clear(); vertices = ListPool<Vector3>.Get(); if (useColors) { colors = ListPool<Color>.Get(); } if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { hexMesh.SetVertices(vertices); ListPool<Vector3>.Add(vertices); if (useColors) { hexMesh.SetColors(colors); ListPool<Color>.Add(colors); } if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } … }
Wir verwenden keine UV-Koordinaten.Um diese Funktion zu verwenden, erstellen Sie Methoden zum Hinzufügen von UV-Koordinaten zu Dreiecken und Vierecken. public void AddTriangleUV (Vector2 uv1, Vector2 uv2, Vector3 uv3) { uvs.Add(uv1); uvs.Add(uv2); uvs.Add(uv3); } public void AddQuadUV (Vector2 uv1, Vector2 uv2, Vector3 uv3, Vector3 uv4) { uvs.Add(uv1); uvs.Add(uv2); uvs.Add(uv3); uvs.Add(uv4); }
Fügen wir eine zusätzliche Methode hinzu, AddQuadUV
um bequem einen rechteckigen UV-Bereich hinzuzufügen. Dies ist der Standardfall, wenn das Quad und seine Textur gleich sind. Wir werden es für das Wasser des Flusses verwenden. public void AddQuadUV (float uMin, float uMax, float vMin, float vMax) { uvs.Add(new Vector2(uMin, vMin)); uvs.Add(new Vector2(uMax, vMin)); uvs.Add(new Vector2(uMin, vMax)); uvs.Add(new Vector2(uMax, vMax)); }
EinheitspaketAktuelle Flüsse
Endlich ist es Zeit, Wasser zu schaffen! Wir werden dies mit einem Quad tun, das die Wasseroberfläche anzeigt. Und da wir mit Flüssen arbeiten, muss Wasser fließen. Dazu verwenden wir UV-Koordinaten, die die Ausrichtung des Flusses angeben. Um dies zu visualisieren, benötigen wir einen neuen Shader. Erstellen Sie daher einen neuen Standard-Shader und nennen Sie ihn River . Ändern Sie es so, dass die UV-Koordinaten in den grünen und roten Albedokanälen aufgezeichnet werden. Shader "Custom/River" { … void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb * IN.color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; o.Albedo.rg = IN.uv_MainTex; } ENDCG } FallBack "Diffuse" }
Zum HexGridChunk
allgemeinen Feld hinzufügen HexMesh rivers
. Wir reinigen es und wenden es auf die gleiche Weise an wie im Falle einer Erleichterung. public HexMesh terrain, rivers; public void Triangulate () { terrain.Clear(); rivers.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); }
Werden wir zusätzliche Draw Calls haben, auch wenn wir keine Flüsse haben?Unity , . , - .
Ändern Sie das Fertighaus (über die Instanz), duplizieren Sie das Geländeobjekt, benennen Sie es in Flüsse um und verbinden Sie es.Fertighausfragment mit Flüssen.Erstellen Sie das River- Material mit unserem neuen Shader und lassen Sie das Rivers- Objekt es verwenden . Wir haben auch die Sechsecknetzkomponente des Objekts so eingerichtet, dass UV-Koordinaten verwendet werden, jedoch keine Scheitelpunktfarben oder der Kollider.Unterobjekt Flüsse.Wasser triangulieren
Bevor wir Wasser triangulieren können, müssen wir den Pegel seiner Oberfläche bestimmen. Machen HexMetrics
wir eine Höhenverschiebung , wie wir es mit dem Flussbett gemacht haben. Da die vertikale Verzerrung der Zelle gleich der Hälfte der Höhenverschiebung ist, verwenden wir sie, um die Oberfläche des Flusses zu verschieben. Wir garantieren also, dass sich das Wasser niemals über der Topographie der Zelle befindet. public const float riverSurfaceElevationOffset = -0.5f;
Warum nicht etwas niedriger machen?, . , .
Fügen Sie eine HexCell
Eigenschaft hinzu, um die vertikale Position der Oberfläche des Flusses zu ermitteln. public float RiverSurfaceY { get { return (elevation + HexMetrics.riverSurfaceElevationOffset) * HexMetrics.elevationStep; } }
Jetzt können wir arbeiten HexGridChunk
! Da wir viele Flussvierecke erstellen werden, fügen wir hierfür eine separate Methode hinzu. Geben wir ihm vier Eckpunkte und eine Höhe als Parameter. Auf diese Weise können wir bequem die vertikale Position aller vier Scheitelpunkte gleichzeitig einstellen, bevor wir Quad hinzufügen. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y ) { v1.y = v2.y = v3.y = v4.y = y; rivers.AddQuad(v1, v2, v3, v4); }
Wir werden hier die UV-Koordinaten des Vierecks hinzufügen. Gehen Sie einfach von links nach rechts und von unten nach oben. rivers.AddQuad(v1, v2, v3, v4); rivers.AddQuadUV(0f, 1f, 0f, 1f);
TriangulateWithRiver
- Dies ist die erste Methode, zu der wir die Vierecke der Flüsse hinzufügen. Das erste Quad befindet sich zwischen der Mitte und der Mitte. Der zweite ist zwischen der Mitte und der Rippe. Wir verwenden nur die Eckpunkte, die wir bereits haben. Da diese Spitzen unterschätzt werden, befindet sich das Wasser infolgedessen teilweise unter den geneigten Wänden des Kanals. Daher müssen wir uns nicht um die genaue Position des Wasserrands kümmern. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateRiverQuad(centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY); TriangulateRiverQuad(m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY); }
Die ersten Anzeichen von Wasser.Warum ändert sich die Breite des Wassers?, , — . . .
Mit dem Fluss bewegen
Derzeit stimmen die UV-Koordinaten nicht mit der Flussrichtung überein. Wir müssen hier die Konsistenz wahren. Angenommen, die U-Koordinate ist 0 auf der linken Seite des Flusses und 1 auf der rechten Seite, wenn Sie stromabwärts schauen. Und die V-Koordinate sollte in Flussrichtung von 0 bis 1 variieren.Unter Verwendung dieser Spezifikation sind UVs korrekt, wenn der ausgehende Fluss trianguliert wird, aber sie stellen sich als falsch heraus und müssen umgedreht werden, wenn der eingehende Fluss trianguliert wird. Fügen Sie den TriangulateRiverQuad
Parameter hinzu, um die Arbeit zu vereinfachen bool reversed
. Verwenden Sie es, um UV bei Bedarf umzudrehen. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, bool reversed ) { v1.y = v2.y = v3.y = v4.y = y; rivers.AddQuad(v1, v2, v3, v4); if (reversed) { rivers.AddQuadUV(1f, 0f, 1f, 0f); } else { rivers.AddQuadUV(0f, 1f, 0f, 1f); } }
Wie TriangulateWithRiver
wir wissen , dass wir die Richtung drehen müssen, wenn sie mit eingehendem Fluss geht. bool reversed = cell.IncomingRiver == direction; TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, reversed ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, reversed );
Die vereinbarte Richtung der Flüsse.Der Anfang und das Ende des Flusses
Im Inneren müssen TriangulateWithRiverBeginOrEnd
wir nur prüfen, ob wir einen ankommenden Fluss haben, um die Richtung des Flusses zu bestimmen. Dann können wir einen weiteren Quad River zwischen der Mitte und der Rippe einfügen. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … bool reversed = cell.HasIncomingRiver; TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, reversed ); }
Der Teil zwischen der Mitte und der Mitte ist ein Dreieck, daher können wir ihn nicht verwenden TriangulateRiverQuad
. Der einzige signifikante Unterschied besteht darin, dass sich der zentrale Gipfel in der Mitte des Flusses befindet. Daher ist seine Koordinate U immer gleich ½. center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); if (reversed) { rivers.AddTriangleUV( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); } else { rivers.AddTriangleUV( new Vector2(0.5f, 0f), new Vector2(0f, 1f), new Vector2(1f, 1f) ); }
Wasser am Anfang und am Ende.Fehlen an den Enden Teile des Wassers?, quad , . . .
, . , . .
Fluss zwischen Zellen
Beim Hinzufügen von Wasser zwischen den Zellen müssen wir auf den Höhenunterschied achten. Damit Wasser Hänge und Klippen hinunterfließen kann, TriangulateRiverQuad
muss es zwei Höhenparameter unterstützen. Fügen wir also einen zweiten hinzu. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, bool reversed ) { v1.y = v2.y = y1; v3.y = v4.y = y2; rivers.AddQuad(v1, v2, v3, v4); if (reversed) { rivers.AddQuadUV(1f, 0f, 1f, 0f); } else { rivers.AddQuadUV(0f, 1f, 0f, 1f); } }
Fügen Sie der Einfachheit halber eine Option hinzu, die eine Höhe erhält. Es wird nur eine andere Methode aufgerufen. void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, bool reversed ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, reversed); }
Jetzt können wir Quad River und In hinzufügen TriangulateConnection
. Da wir uns zwischen den Zellen befinden, können wir nicht sofort herausfinden, um welche Art von Fluss es sich handelt. Um festzustellen, ob eine Abbiegung erforderlich ist, müssen wir prüfen, ob ein Fluss ankommt und ob er sich in unsere Richtung bewegt. if (cell.HasRiverThroughEdge(direction)) { e2.v3.y = neighbor.StreamBedY; TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, cell.HasIncomingRiver && cell.IncomingRiver == direction ); }
Der fertiggestellte Fluss.Dehnung der V-Koordinate
Bisher haben wir in jedem Flusssegment V-Koordinaten von 0 bis 1. Das heißt, es gibt nur vier davon in der Zelle. Fünf, wenn wir auch Verbindungen zwischen Zellen hinzufügen. Was auch immer wir verwenden, um den Fluss zu texturieren, es muss genauso oft wiederholt werden.Wir können die Anzahl der Wiederholungen reduzieren, indem wir die V-Koordinaten so strecken, dass sie in der gesamten Zelle plus einer Verbindung von 0 auf 1 gehen. Dies kann durch Erhöhen der V-Koordinate in jedem Segment um 0,2 erfolgen. Wenn wir 0,4 in die Mitte setzen, wird es in der Mitte 0,6 und am Rand 0,8. In der Zellenverbindung ist der Wert dann 1.Wenn der Fluss in die entgegengesetzte Richtung fließt, können wir immer noch 0,4 in die Mitte setzen, aber in der Mitte wird es 0,2 und am Rand - 0. Wenn wir so weitermachen, bis sich die Zelle verbindet, ist das Ergebnis -0,2. Dies ist normal, da es für eine Textur mit Wiederholungsfilterungsmodus ähnlich wie 0,8 ist, genauso wie 0 gleich 1 ist.Änderung der Koordinaten V.Um dies zu unterstützen, müssen wir TriangulateRiverQuad
einen weiteren Parameter hinzufügen . void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y, float v, bool reversed ) { TriangulateRiverQuad(v1, v2, v3, v4, y, y, v, reversed); } void TriangulateRiverQuad ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float y1, float y2, float v, bool reversed ) { … }
Wenn die Richtung nicht umgekehrt wird, verwenden wir einfach die übertragene Koordinate am unteren Rand des Vierecks und addieren am oberen Rand 0,2. else { rivers.AddQuadUV(0f, 1f, v, v + 0.2f); }
Wir können mit einer umgekehrten Richtung arbeiten, indem wir die Koordinaten von 0,8 und 0,6 subtrahieren. if (reversed) { rivers.AddQuadUV(1f, 0f, 0.8f - v, 0.6f - v); }
Jetzt müssen wir die richtigen Koordinaten übertragen, als hätten wir es mit einem abgehenden Fluss zu tun. Beginnen wir mit TriangulateWithRiver
. TriangulateRiverQuad( centerL, centerR, m.v2, m.v4, cell.RiverSurfaceY, 0.4f, reversed ); TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed );
Dann TriangulateConnection
ändert sich wie folgt. TriangulateRiverQuad( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f, cell.HasIncomingRiver && cell.IncomingRiver == direction );
Und schließlich TriangulateWithRiverBeginOrEnd
. TriangulateRiverQuad( m.v2, m.v4, e.v2, e.v4, cell.RiverSurfaceY, 0.6f, reversed ); center.y = m.v2.y = m.v4.y = cell.RiverSurfaceY; rivers.AddTriangle(center, m.v2, m.v4); if (reversed) { rivers.AddTriangleUV( new Vector2(0.5f, 0.4f), new Vector2(1f, 0.2f), new Vector2(0f, 0.2f) ); } else { rivers.AddTriangleUV( new Vector2(0.5f, 0.4f), new Vector2(0f, 0.6f), new Vector2(1f, 0.6f) ); }
Gestreckte V-KoordinatenUm die Faltung der V-Koordinaten korrekt anzuzeigen, stellen Sie sicher, dass sie im River Shader positiv bleiben. if (IN.uv_MainTex.y < 0) { IN.uv_MainTex.y += 1; } o.Albedo.rg = IN.uv_MainTex;
Reduzierte Koordinaten V.EinheitspaketFlussanimation
Nachdem wir mit den UV-Koordinaten fertig sind, können wir mit der Animation der Flüsse fortfahren. Der River Shader wird dies tun, damit wir das Netz nicht ständig aktualisieren müssen.Wir werden in diesem Tutorial keinen komplexen River Shader erstellen, aber später. Im Moment erstellen wir einen einfachen Effekt, der ein Verständnis der Funktionsweise von Animationen vermittelt.Die Animation wird durch Verschieben der V-Koordinaten basierend auf der Spielzeit erstellt. Mit Unity können Sie den Wert mithilfe einer Variablen ermitteln _Time
. Seine Komponente Y enthält die unveränderte Zeit, die wir verwenden. Andere Komponenten enthalten andere Zeitskalen.Wir werden die Faltung entlang V loswerden, weil wir sie nicht mehr brauchen. Stattdessen subtrahieren wir die aktuelle Zeit von der V-Koordinate. Dadurch wird die Koordinate nach unten verschoben, wodurch die Illusion entsteht, dass der Strom stromabwärts des Flusses fließt. // if (IN.uv_MainTex.y < 0) { // IN.uv_MainTex.y += 1; // } IN.uv_MainTex.y -= _Time.y; o.Albedo.rg = IN.uv_MainTex;
In einer Sekunde wird die V-Koordinate an allen Punkten kleiner als Null, sodass wir den Unterschied nicht mehr sehen. Dies ist wiederum normal, wenn die Filterung im Texturwiederholungsmodus verwendet wird. Aber um zu sehen, was passiert, können wir den Bruchteil der V-Koordinate nehmen. IN.uv_MainTex.y -= _Time.y; IN.uv_MainTex.y = frac(IN.uv_MainTex.y); o.Albedo.rg = IN.uv_MainTex;
Animierte V-Koordinaten.Lärmgebrauch
Jetzt ist unser Fluss animiert, aber in Richtung und Geschwindigkeit gibt es scharfe Übergänge. Unser UV-Muster macht sie ziemlich offensichtlich, aber es ist schwieriger zu erkennen, wenn Sie ein wasserähnlicheres Muster verwenden. Anstatt rohes UV-Licht anzuzeigen, probieren wir die Textur aus. Wir können unsere vorhandene Rauschstruktur verwenden. Wir probieren es aus und multiplizieren die Farbe des Materials mit dem ersten Rauschkanal. void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = IN.uv_MainTex; uv.y -= _Time.y; float4 noise = tex2D(_MainTex, uv); fixed4 c = _Color * noise.r; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Weisen Sie dem Flussmaterial die Geräuschstruktur zu und stellen Sie sicher, dass es weiß ist.Rauschtextur verwenden.Da die V-Koordinaten sehr gestreckt sind, erstreckt sich die Rauschstruktur auch entlang des Flusses. Leider ist der Kurs nicht sehr schön. Lassen Sie uns versuchen, es auf eine andere Weise zu dehnen - die Skalierung der Koordinaten von U stark zu reduzieren. Ein Sechzehntel wird ausreichen. Dies bedeutet, dass wir nur ein schmales Band von Rauschtexturen abtasten. float2 uv = IN.uv_MainTex; uv.x *= 0.0625; uv.y -= _Time.y;
Dehnen der U-Koordinate.Lassen Sie uns auch auf ein Viertel pro Sekunde verlangsamen, damit der Abschluss des Texturzyklus vier Sekunden dauert. uv.y -= _Time.y * 0.25;
Das aktuelle Rauschen.Rauschmischung
Alles sieht schon viel besser aus, aber das Muster bleibt immer das gleiche. Wasser verhält sich nicht so.Da wir nur ein kleines Rauschband verwenden, können wir das Muster variieren, indem wir dieses Band entlang der Textur verschieben. Dies erfolgt durch Hinzufügen von Zeit zur U-Koordinate. Wir müssen es langsam machen, sonst scheint der Fluss seitwärts zu fließen. Versuchen wir den Koeffizienten von 0,005. Dies bedeutet, dass es 200 Sekunden dauert, um das Muster fertigzustellen. uv.x = uv.x * 0.0625 + _Time.y * 0.005;
Bewegliches Geräusch.Das sieht leider nicht sehr schön aus. Wasser scheint immer noch statisch und die Verschiebung ist deutlich spürbar, obwohl es sehr langsam ist. Wir können die Verschiebung verbergen, indem wir zwei Rauschproben kombinieren und sie in entgegengesetzte Richtungen verschieben. Wenn wir zum Verschieben des zweiten Samples leicht unterschiedliche Werte verwenden, erstellen wir eine leichte Animation der Änderung.Damit wir niemals dasselbe Rauschmuster überlappen, verwenden wir für das zweite Sample einen anderen Kanal. float2 uv = IN.uv_MainTex; uv.x = uv.x * 0.0625 + _Time.y * 0.005; uv.y -= _Time.y * 0.25; float4 noise = tex2D(_MainTex, uv); float2 uv2 = IN.uv_MainTex; uv2.x = uv2.x * 0.0625 - _Time.y * 0.0052; uv2.y -= _Time.y * 0.23; float4 noise2 = tex2D(_MainTex, uv2); fixed4 c = _Color * (noise.r * noise2.a);
Eine Kombination aus zwei sich verschiebenden Geräuschmustern.Durchscheinendes Wasser
Unser Muster sieht sehr dynamisch aus. Der nächste Schritt besteht darin, es durchscheinend zu machen.Stellen Sie zunächst sicher, dass das Wasser keine Schatten wirft. Sie können sie über die Renderer-Komponente des Rivers- Objekts im Fertighaus deaktivieren.Schattenwurf ist deaktiviert.Schalten Sie nun den Shader in den transparenten Modus. Verwenden Sie dazu Shader-Tags. Fügen Sie dann das #pragma surface
Schlüsselwort zur Zeile hinzu alpha
. Während wir hier sind, können Sie das Schlüsselwort entfernen fullforwardshadows
, da wir immer noch keine Schatten werfen. Tags { "RenderType"="Transparent" "Queue"="Transparent" } LOD 200 CGPROGRAM #pragma surface surf Standard alpha // fullforwardshadows #pragma target 3.0
Jetzt werden wir die Art und Weise ändern, wie wir die Farbe des Flusses einstellen. Anstatt Rauschen mit Farbe zu multiplizieren, fügen wir Rauschen hinzu. Dann verwenden wir die Funktion saturate
, um das Ergebnis so zu begrenzen, dass es 1 nicht überschreitet. fixed4 c = saturate(_Color + noise.r * noise2.a);
Dadurch können wir die Materialfarbe als Grundfarbe verwenden. Rauschen erhöht die Helligkeit und Deckkraft. Versuchen wir, eine blaue Farbe mit einer relativ geringen Deckkraft zu verwenden. Als Ergebnis erhalten wir blaues durchscheinendes Wasser mit weißen Spritzern.Farbiges durchscheinendes Wasser.EinheitspaketFertigstellung
Jetzt, da alles zu funktionieren scheint, ist es Zeit, die Spitzen wieder zu verzerren. Dies verformt nicht nur die Ränder der Zellen, sondern macht unsere Flüsse auch uneben. public const float cellPerturbStrength = 4f;
Verzerrte und verzerrte Spitzen.Untersuchen wir das Gelände auf Probleme, die durch Verzerrungen entstanden sind. Sieht aus wie sie sind! Schauen wir uns die hohen Wasserfälle an.Wasser von Klippen abgeschnitten.Wasser, das von einem hohen Wasserfall fällt, verschwindet hinter einer Klippe. Wenn dies passiert, ist es sehr auffällig, also müssen wir etwas dagegen tun.Viel weniger offensichtlich ist, dass die Wasserfälle abfallen können, anstatt direkt abzusteigen. Obwohl Wasser in Wirklichkeit nicht so fließt, fällt es nicht besonders auf. Unser Gehirn wird es so interpretieren, dass es uns normal erscheint. Also ignoriere es einfach.Der einfachste Weg, um Wasserverluste zu vermeiden, ist die Vertiefung der Flussbetten. So schaffen wir mehr Platz zwischen der Wasseroberfläche und dem Flussbett. Dadurch werden auch die Wände des Kanals vertikaler, gehen Sie also nicht zu tief. Fragen wirHexMetrics.streamBedElevationOffset
Wert -1,75. Dies wird den Großteil der Probleme lösen und das Bett wird nicht zu tief. Ein Teil des Wassers wird immer noch abgeschnitten, aber nicht die gesamten Wasserfälle. public const float streamBedElevationOffset = -1.75f;
Detaillierte Kanäle.EinheitspaketTeil 7: Straßen
- Straßenunterstützung hinzufügen.
- Triangulieren Sie die Straße.
- Wir kombinieren Straßen und Flüsse.
- Verbesserung des Erscheinungsbildes von Straßen.
Die ersten Anzeichen der Zivilisation.Zellen mit Straßen
Wie Flüsse verlaufen Straßen von Zelle zu Zelle durch die Mitte der Zellenränder. Der große Unterschied besteht darin, dass auf den Straßen kein Wasser fließt und sie daher bidirektional sind. Darüber hinaus sind Kreuzungen für ein funktionierendes Straßennetz erforderlich, sodass mehr als zwei Straßen pro Zelle unterstützt werden müssen.Wenn Sie zulassen, dass die Straßen in alle sechs Richtungen verlaufen, kann die Zelle null bis sechs Straßen enthalten. Das sind insgesamt vierzehn mögliche Straßenkonfigurationen. Dies sind viel mehr als fünf mögliche Flusskonfigurationen. Um dies zu handhaben, müssen wir einen allgemeineren Ansatz verwenden, der alle Konfigurationen handhaben kann.14 mögliche Straßenkonfigurationen.Straßenverfolgung
Der einfachste Weg, Straßen in einer Zelle zu verfolgen, besteht darin, ein Array von Booleschen Werten zu verwenden. Fügen Sie das private Feld des Arrays hinzu HexCell
und machen Sie es serialisierbar, damit Sie es im Inspektor sehen können. Stellen Sie die Größe des Arrays über das Zellen-Fertighaus so ein, dass es sechs Straßen unterstützt. [SerializeField] bool[] roads;
Fertighaus mit sechs Straßen.Fügen Sie eine Methode hinzu, um zu überprüfen, ob die Zelle einen Pfad in eine bestimmte Richtung hat. public bool HasRoadThroughEdge (HexDirection direction) { return roads[(int)direction]; }
Es ist auch praktisch zu wissen, ob sich mindestens eine Straße in der Zelle befindet, daher fügen wir hierfür eine Eigenschaft hinzu. Gehen Sie einfach um das Array in der Schleife herum und kehren Sie zurück true
, sobald wir den Weg gefunden haben. Wenn es keine Straßen gibt, kehren Sie zurück false
. public bool HasRoads { get { for (int i = 0; i < roads.Length; i++) { if (roads[i]) { return true; } } return false; } }
Straßenentfernung
Wie bei Flüssen werden wir eine Methode hinzufügen, um alle Straßen aus der Zelle zu entfernen. Dies kann mit einer Schleife erfolgen, die jede zuvor aktivierte Straße trennt. public void RemoveRoads () { for (int i = 0; i < neighbors.Length; i++) { if (roads[i]) { roads[i] = false; } } }
Natürlich müssen wir auch die entsprechenden teuren Zellen in den Nachbarn deaktivieren. if (roads[i]) { roads[i] = false; neighbors[i].roads[(int)((HexDirection)i).Opposite()] = false; }
Danach müssen wir jede der Zellen aktualisieren. Da die Straßen lokal für die Zellen sind, müssen wir nur die Zellen selbst ohne ihre Nachbarn aktualisieren. if (roads[i]) { roads[i] = false; neighbors[i].roads[(int)((HexDirection)i).Opposite()] = false; neighbors[i].RefreshSelfOnly(); RefreshSelfOnly(); }
Straßen hinzufügen
Das Hinzufügen von Straßen ähnelt dem Entfernen von Straßen. Der einzige Unterschied besteht darin, dass wir Boolean einen Wert zuweisen true
, nicht false
. Wir können eine private Methode erstellen, die beide Operationen ausführen kann. Dann wird es möglich sein, die Straße sowohl hinzuzufügen als auch zu entfernen. public void AddRoad (HexDirection direction) { if (!roads[(int)direction]) { SetRoad((int)direction, true); } } public void RemoveRoads () { for (int i = 0; i < neighbors.Length; i++) { if (roads[i]) { SetRoad(i, false); } } } void SetRoad (int index, bool state) { roads[index] = state; neighbors[index].roads[(int)((HexDirection)index).Opposite()] = state; neighbors[index].RefreshSelfOnly(); RefreshSelfOnly(); }
Wir können nicht gleichzeitig einen Fluss und eine Straße in die gleiche Richtung führen. Daher werden wir vor dem Hinzufügen der Straße prüfen, ob ein Platz dafür vorhanden ist. public void AddRoad (HexDirection direction) { if (!roads[(int)direction] && !HasRiverThroughEdge(direction)) { SetRoad((int)direction, true); } }
Außerdem können Straßen nicht mit Klippen kombiniert werden, da sie zu scharf sind. Oder lohnt es sich vielleicht, den Weg durch eine niedrige Klippe zu ebnen, aber nicht durch eine hohe? Um dies festzustellen, müssen wir eine Methode erstellen, die den Höhenunterschied in eine bestimmte Richtung angibt. public int GetElevationDifference (HexDirection direction) { int difference = elevation - GetNeighbor(direction).elevation; return difference >= 0 ? difference : -difference; }
Jetzt können wir die Straßen in einem ausreichend kleinen Höhenunterschied hinzufügen. Ich beschränke mich nur auf Pisten, also maximal 1 Einheit. public void AddRoad (HexDirection direction) { if ( !roads[(int)direction] && !HasRiverThroughEdge(direction) && GetElevationDifference(direction) <= 1 ) { SetRoad((int)direction, true); } }
Die falschen Straßen entfernen
Wir haben Straßen nur hinzugefügt, wenn dies zulässig ist. Jetzt müssen wir sicherstellen, dass sie entfernt werden, wenn sie später falsch werden, beispielsweise beim Hinzufügen eines Flusses. Wir können die Platzierung von Flüssen auf Straßen verbieten, aber Flüsse werden nicht durch Straßen unterbrochen. Lassen Sie sie die Straße aus dem Weg waschen.Es wird für uns ausreichen, nach der Straße zu fragen false
, unabhängig davon, ob die Straße war. Es wird immer beide Zellen aktualisiert werden, so dass wir nicht mehr benötigen , um explizit nennt RefreshSelfOnly
in SetOutgoingRiver
. public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } HexCell neighbor = GetNeighbor(direction); if (!neighbor || elevation < neighbor.elevation) { return; } RemoveOutgoingRiver(); if (hasIncomingRiver && incomingRiver == direction) { RemoveIncomingRiver(); } hasOutgoingRiver = true; outgoingRiver = direction;
Eine andere Operation, die die Straße falsch machen kann, ist eine Änderung der Höhe. In diesem Fall müssen wir nach Straßen in alle Richtungen suchen. Wenn der Höhenunterschied zu groß ist, muss die vorhandene Straße gelöscht werden. public int Elevation { get { return elevation; } set { … for (int i = 0; i < roads.Length; i++) { if (roads[i] && GetElevationDifference((HexDirection)i) > 1) { SetRoad(i, false); } } Refresh(); } }
EinheitspaketStraßenbearbeitung
Das Bearbeiten von Straßen funktioniert genauso wie das Bearbeiten von Flüssen. Daher HexMapEditor
ist ein weiterer Schalter sowie eine Methode zum Festlegen des Status erforderlich. OptionalToggle riverMode, roadMode; public void SetRiverMode (int mode) { riverMode = (OptionalToggle)mode; } public void SetRoadMode (int mode) { roadMode = (OptionalToggle)mode; }
Die Methode EditCell
sollte nun das Entfernen durch Hinzufügen von Straßen unterstützen. Dies bedeutet, dass er beim Ziehen und Ablegen eine von zwei möglichen Aktionen ausführen kann. Wir strukturieren den Code ein wenig um, damit bei korrektem Drag & Drop die Zustände beider Schalter überprüft werden. void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } if (roadMode == OptionalToggle.No) { cell.RemoveRoads(); } if (isDrag) { HexCell otherCell = cell.GetNeighbor(dragDirection.Opposite()); if (otherCell) { if (riverMode == OptionalToggle.Yes) { otherCell.SetOutgoingRiver(dragDirection); } if (roadMode == OptionalToggle.Yes) { otherCell.AddRoad(dragDirection); } } } } }
Wir können der Benutzeroberfläche schnell eine Straßenleiste hinzufügen, indem wir die Flussleiste kopieren und die von den Schaltern aufgerufene Methode ändern.Als Ergebnis erhalten wir eine ziemlich hohe Benutzeroberfläche. Um dies zu beheben, habe ich das Layout der Farbfelder geändert, um es an die kompakteren Straßen- und Flussfelder anzupassen.Benutzeroberfläche mit Straßen.Da ich jetzt zwei Zeilen mit drei Farboptionen verwende, ist Platz für eine andere Farbe. Also habe ich einen Artikel für Orange hinzugefügt.Fünf Farben: Gelb, Grün, Blau, Orange und Weiß.Jetzt können wir die Straßen bearbeiten, aber bisher sind sie nicht sichtbar. Mit dem Inspektor können Sie sicherstellen, dass alles funktioniert.Zelle mit Straßen im Inspektor.EinheitspaketStraßentriangulation
Um Straßen anzuzeigen, müssen Sie sie triangulieren. Dies ähnelt der Erstellung eines Netzes für Flüsse, nur das Flussbett wird nicht im Relief angezeigt.Erstellen Sie zunächst einen neuen Standard-Shader, der erneut UV-Koordinaten verwendet, um die Straßenoberfläche zu malen. Shader "Custom/Road" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = fixed4(IN.uv_MainTex, 1, 1); o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" }
Erstellen Sie mit diesem Shader ein Straßenmaterial.Materielle Straße.Stellen Sie das Fertighaus des Fragments so ein, dass es ein weiteres untergeordnetes Sechsecknetz für die Straßen erhält. Dieses Netz sollte keine Schatten werfen und darf nur UV-Koordinaten verwenden. Der schnellste Weg, dies zu tun, ist eine vorgefertigte Instanz - duplizieren Sie das Rivers- Objekt und ersetzen Sie sein Material.Kinderobjektstraßen.Fügen Sie danach das HexGridChunk
allgemeine Feld hinzu HexMesh roads
und fügen Sie es in ein Triangulate
. Verbinden Sie es im Inspektor mit dem Roads- Objekt . public HexMesh terrain, rivers, roads; public void Triangulate () { terrain.Clear(); rivers.Clear(); roads.Clear(); for (int i = 0; i < cells.Length; i++) { Triangulate(cells[i]); } terrain.Apply(); rivers.Apply(); roads.Apply(); }
Das Roads-Objekt ist verbunden.Straßen zwischen Zellen
Schauen wir uns zunächst die Straßensegmente zwischen den Zellen an. Wie Flüsse sind Straßen durch zwei mittlere Quad gesperrt. Wir decken diese Verbindungsvierecke vollständig mit den Straßenvierecken ab, damit die Positionen der gleichen sechs Spitzen verwendet werden können. Fügen Sie dazu der HexGridChunk
Methode hinzu TriangulateRoadSegment
. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6 ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); }
Da wir uns nicht mehr um den Wasserfluss kümmern müssen, ist die V-Koordinate nicht erforderlich, daher weisen wir ihr überall den Wert 0 zu. Mit der U-Koordinate können wir angeben, ob wir uns mitten auf der Straße oder auf der Seite befinden. Lassen Sie es in der Mitte gleich 1 und auf beiden Seiten gleich 0 sein. void TriangulateRoadSegment ( Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector3 v5, Vector3 v6 ) { roads.AddQuad(v1, v2, v4, v5); roads.AddQuad(v2, v3, v5, v6); roads.AddQuadUV(0f, 1f, 0f, 0f); roads.AddQuadUV(1f, 0f, 0f, 0f); }
Ein Straßenabschnitt zwischen Zellen.Es wäre logisch, diese Methode aufzurufen TriangulateEdgeStrip
, aber nur, wenn es wirklich eine Straße gibt. Fügen Sie der Methode einen booleschen Parameter hinzu, um diese Informationen zu übergeben. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad ) { … }
Natürlich werden wir jetzt Compilerfehler erhalten, da diese Informationen bisher noch nicht übertragen wurden. In allen Fällen kann der Aufruf TriangulateEdgeStrip
als letztes Argument hinzugefügt werden false
. Wir können jedoch auch deklarieren, dass der Standardwert dieses Parameters gleich ist false
. Aus diesem Grund wird der Parameter optional und Kompilierungsfehler verschwinden. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad = false ) { … }
Wie funktionieren optionale Parameter?, . ,
int MyMethod (int x = 1, int y = 2) { return x + y; }
int MyMethod (int x, int y) { return x + y; } int MyMethod (int x) { return MyMethod(x, 2); } int MyMethod () { return MyMethod(1, 2}; }
. . . .
Um die Straße zu triangulieren, rufen Sie TriangulateRoadSegment
bei Bedarf einfach mit den mittleren sechs Gipfeln an. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, EdgeVertices e2, Color c2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuadColor(c1, c2); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); terrain.AddQuadColor(c1, c2); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } }
So gehen wir mit Flachzellenverbindungen um. Um die Straßen auf den Felsvorsprüngen zu unterstützen, müssen wir auch angeben, TriangulateEdgeTerraces
wo die Straße hinzugefügt werden soll. Er kann diese Informationen einfach übermitteln TriangulateEdgeStrip
. void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(beginCell.Color, endCell.Color, 1); TriangulateEdgeStrip(begin, beginCell.Color, e2, c2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(beginCell.Color, endCell.Color, i); TriangulateEdgeStrip(e1, c1, e2, c2, hasRoad); } TriangulateEdgeStrip(e2, c2, end, endCell.Color, hasRoad); }
TriangulateEdgeTerraces
drinnen gerufen TriangulateConnection
. Hier können wir feststellen, ob tatsächlich eine Straße in die aktuelle Richtung führt, sowohl beim Triangulieren einer Kante als auch beim Triangulieren von Leisten. if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces( e1, cell, e2, neighbor, cell.HasRoadThroughEdge(direction) ); } else { TriangulateEdgeStrip( e1, cell.Color, e2, neighbor.Color, cell.HasRoadThroughEdge(direction) ); }
Straßensegmente zwischen Zellen.Zelle über Rendering
Beim Zeichnen von Straßen sehen Sie, dass Straßensegmente zwischen Zellen angezeigt werden. Die Mitte dieser Segmente ist lila mit einem Übergang zu blau an den Rändern.Wenn Sie die Kamera bewegen, können die Segmente jedoch flackern und manchmal vollständig verschwinden. Dies liegt daran, dass die Dreiecke der Straßen genau die Geländedreiecke überlappen. Dreiecke zum Rendern werden zufällig ausgewählt. Dieses Problem kann in zwei Schritten behoben werden.Zunächst möchten wir die Straßen zeichnen, nachdem das Relief gezeichnet wurde. Dies kann erreicht werden, indem sie nach dem Rendern der üblichen Geometrie gerendert werden, dh indem sie in eine spätere Renderwarteschlange gestellt werden. Tags { "RenderType"="Opaque" "Queue" = "Geometry+1" }
Zweitens müssen wir sicherstellen, dass Straßen an derselben Position über Geländedreiecke gezogen werden. Dies kann durch Hinzufügen des Tiefentestversatzes erfolgen. Dadurch kann die GPU davon ausgehen, dass sich die Dreiecke näher an der Kamera befinden als sie tatsächlich sind. Tags { "RenderType"="Opaque" "Queue" = "Geometry+1" } LOD 200 Offset -1, -1
Straßen durch Zellen
Beim Triangulieren von Flüssen mussten wir uns mit nicht mehr als zwei Flussrichtungen pro Zelle befassen. Wir könnten fünf mögliche Optionen identifizieren und sie unterschiedlich triangulieren, um die richtig aussehenden Flüsse zu erzeugen. Bei Straßen gibt es jedoch vierzehn mögliche Optionen. Wir werden nicht für jede dieser Optionen separate Ansätze verwenden. Stattdessen werden wir jede der sechs Zellenrichtungen unabhängig von der spezifischen Straßenkonfiguration auf dieselbe Weise verarbeiten.Wenn eine Straße entlang eines Teils der Zelle verläuft, zeichnen wir sie direkt in die Mitte der Zelle, ohne die Dreieckszone zu verlassen. Wir werden einen Straßenabschnitt vom Rand bis zur Hälfte in Richtung des Zentrums zeichnen. Dann verwenden wir zwei Dreiecke, um den Rest zur Mitte zu schließen.Triangulation eines Teils der Straße.Um dieses Schema zu triangulieren, müssen wir die Mitte der Zelle, die linken und rechten mittleren Eckpunkte und die Eckpunkte der Kante kennen. Fügen Sie eine Methode TriangulateRoad
mit den entsprechenden Parametern hinzu. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e ) { }
Um ein Straßensegment zu bauen, benötigen wir einen zusätzlichen Gipfel. Es befindet sich zwischen dem linken und rechten mittleren Gipfel. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e ) { Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); }
Jetzt können wir auch die verbleibenden zwei Dreiecke hinzufügen. TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR);
Wir müssen auch die UV-Koordinaten der Dreiecke hinzufügen. Zwei ihrer Gipfel befinden sich in der Mitte der Straße, der Rest befindet sich am Rande. roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) );
Beschränken wir uns vorerst auf Zellen, in denen es keine Flüsse gibt. In diesen Fällen wird Triangulate
einfach ein Fan von Dreiecken erzeugt. Verschieben Sie diesen Code in eine separate Methode. Dann fügen wir einen Anruf hinzu, TriangulateRoad
wenn die Straße tatsächlich ist. Der linke und der rechte mittlere Scheitelpunkt können durch Interpolation zwischen dem mittleren und zwei Eckscheitelpunkten gefunden werden. void Triangulate (HexDirection direction, HexCell cell) { … if (cell.HasRiver) { … } else { TriangulateWithoutRiver(direction, cell, center, e); } … } void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoadThroughEdge(direction)) { TriangulateRoad( center, Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f), e ); } }
Straßen, die durch die Zellen führen.Straßenrippen
Jetzt können wir die Straßen sehen, aber näher an der Mitte der Zellen verengen sie sich. Da wir nicht prüfen, um welche der vierzehn Optionen es sich handelt, können wir die Mitte der Straße nicht verschieben, um schönere Formen zu erstellen. Stattdessen können wir zusätzliche Straßenkanten in anderen Teilen der Zelle hinzufügen.Wenn Straßen durch die Zelle verlaufen, jedoch nicht in der aktuellen Richtung, fügen wir den Straßenrändern ein Dreieck hinzu. Dieses Dreieck wird durch den mittleren, linken und rechten mittleren Eckpunkt definiert. In diesem Fall liegt nur der zentrale Gipfel in der Mitte der Straße. Die anderen beiden liegen auf ihrer Rippe. void TriangulateRoadEdge (Vector3 center, Vector3 mL, Vector3 mR) { roads.AddTriangle(center, mL, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(0f, 0f) ); }
Ein Teil des Straßenrandes.Wenn wir eine volle Straße oder nur eine Kante triangulieren müssen, müssen wir sie verlassen TriangulateRoad
. Zu diesem Zweck muss diese Methode wissen, ob die Straße durch die Richtung der aktuellen Zellenkante verläuft. Deshalb fügen wir hierfür einen Parameter hinzu. void TriangulateRoad ( Vector3 center, Vector3 mL, Vector3 mR, EdgeVertices e, bool hasRoadThroughCellEdge ) { if (hasRoadThroughCellEdge) { Vector3 mC = Vector3.Lerp(mL, mR, 0.5f); TriangulateRoadSegment(mL, mC, mR, e.v2, e.v3, e.v4); roads.AddTriangle(center, mL, mC); roads.AddTriangle(center, mC, mR); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(0f, 0f), new Vector2(1f, 0f) ); roads.AddTriangleUV( new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(0f, 0f) ); } else { TriangulateRoadEdge(center, mL, mR); } }
Jetzt muss TriangulateWithoutRiver
es anrufen, TriangulateRoad
wenn Straßen durch die Zelle führen. Und er muss Informationen darüber übermitteln, ob die Straße durch die aktuelle Kante führt. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoads) { TriangulateRoad( center, Vector3.Lerp(center, e.v1, 0.5f), Vector3.Lerp(center, e.v5, 0.5f), e, cell.HasRoadThroughEdge(direction) ); } }
Straßen mit fertigen Rippen.Straßenglättung
Die Straßen sind jetzt fertig. Leider erzeugt dieser Ansatz Ausbuchtungen in der Mitte der Zellen. Das Platzieren der linken und rechten Spitze in der Mitte zwischen der Mitte und den Ecken passt zu uns, wenn sich neben ihnen eine Straße befindet. Wenn dies nicht der Fall ist, liegt eine Ausbuchtung vor. Um dies zu vermeiden, können wir in solchen Fällen die Eckpunkte näher an der Mitte platzieren. Genauer gesagt, dann interpolieren mit ¼, nicht mit ½.Erstellen wir eine separate Methode, um herauszufinden, welche Interpolatoren verwendet werden sollen. Da es zwei davon gibt, können wir das Ergebnis eingeben Vector2
. Seine Komponente X ist der Interpolator des linken Punktes und seine Komponente Y ist der Interpolator des rechten Punktes. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; return interpolators; }
Wenn eine Straße in die aktuelle Richtung führt, können wir die Punkte in der Mitte platzieren. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; if (cell.HasRoadThroughEdge(direction)) { interpolators.x = interpolators.y = 0.5f; } return interpolators; }
Andernfalls können die Optionen unterschiedlich sein. Für den linken Punkt können wir ½ verwenden, wenn eine Straße in die vorherige Richtung führt. Wenn dies nicht der Fall ist, müssen wir ¼ verwenden. Gleiches gilt für den richtigen Punkt, jedoch unter Berücksichtigung der folgenden Richtung. Vector2 GetRoadInterpolators (HexDirection direction, HexCell cell) { Vector2 interpolators; if (cell.HasRoadThroughEdge(direction)) { interpolators.x = interpolators.y = 0.5f; } else { interpolators.x = cell.HasRoadThroughEdge(direction.Previous()) ? 0.5f : 0.25f; interpolators.y = cell.HasRoadThroughEdge(direction.Next()) ? 0.5f : 0.25f; } return interpolators; }
Mit dieser neuen Methode können Sie jetzt bestimmen, welche Interpolatoren verwendet werden. Dank dessen werden die Straßen geglättet. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, cell.Color); if (cell.HasRoads) { Vector2 interpolators = GetRoadInterpolators(direction, cell); TriangulateRoad( center, Vector3.Lerp(center, e.v1, interpolators.x), Vector3.Lerp(center, e.v5, interpolators.y), e, cell.HasRoadThroughEdge(direction) ); } }
Glatte Straßen.EinheitspaketDie Kombination von Flüssen und Straßen
Gegenwärtig haben wir funktionierende Straßen, aber nur, wenn es keine Flüsse gibt. Wenn sich in der Zelle ein Fluss befindet, werden die Straßen nicht trianguliert.In der Nähe der Flüsse gibt es keine Straßen.Lassen Sie uns eine Methode erstellen, TriangulateRoadAdjacentToRiver
um mit dieser Situation umzugehen. Wir stellen es auf die üblichen Parameter ein. Wir werden es am Anfang der Methode aufrufen TriangulateAdjacentToRiver
. void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { if (cell.HasRoads) { TriangulateRoadAdjacentToRiver(direction, cell, center, e); } … } void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { }
Zunächst werden wir das Gleiche tun wie für Straßen ohne Flüsse. Wir werden prüfen, ob die Straße durch die aktuelle Kante führt, Interpolatoren erhalten, Mittelspitzen erstellen und anrufen TriangulateRoad
. Da jedoch Flüsse auf dem Weg erscheinen, müssen wir die Straßen von ihnen wegbewegen. Infolgedessen befindet sich die Mitte der Straße in einer anderen Position. Wir verwenden eine Variable, um diese neue Position zu speichern roadCenter
. Anfangs ist es gleich der Mitte der Zelle. void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); }
Also werden wir Teilstraßen in Zellen mit Flüssen erstellen. Die Richtungen, durch die die Flüsse fließen, werden die Lücken in den Straßen durchschneiden.Straßen mit Räumen.Anfang oder Ende des Flusses
Schauen wir uns zunächst Zellen an, die entweder den Anfang oder das Ende eines Flusses enthalten. Damit sich die Straßen nicht mit Wasser überschneiden, verschieben wir die Straßenmitte vom Fluss. Fügen Sie die HexCell
Eigenschaft hinzu, um die Richtung des ein- oder ausgehenden Flusses zu ermitteln . public HexDirection RiverBeginOrEndDirection { get { return hasIncomingRiver ? incomingRiver : outgoingRiver; } }
Jetzt können wir diese Eigenschaft verwenden HexGridChunk.TriangulateRoadAdjacentToRiver
, um die Straßenmitte in die entgegengesetzte Richtung zu verschieben. Es reicht aus, ein Drittel in diese Richtung zur Mittelrippe zu bewegen. bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge);
Geänderte Straßen.Als nächstes müssen wir die Lücken schließen. Wir werden dies tun, indem wir zusätzliche Dreiecke an den Straßenrändern hinzufügen, wenn wir uns in der Nähe des Flusses befinden. Wenn es in der vorherigen Richtung einen Fluss gibt, fügen wir ein Dreieck zwischen der Mitte der Straße, der Mitte der Zelle und dem mittleren linken Punkt hinzu. Und wenn der Fluss in die nächste Richtung geht, fügen wir ein Dreieck zwischen der Mitte der Straße, dem mittleren rechten Punkt und der Mitte der Zelle hinzu.Wir werden dies unabhängig von der Konfiguration des Flusses tun, also setzen Sie diesen Code am Ende der Methode. Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); if (cell.HasRiverThroughEdge(direction.Previous())) { TriangulateRoadEdge(roadCenter, center, mL); } if (cell.HasRiverThroughEdge(direction.Next())) { TriangulateRoadEdge(roadCenter, mR, center); }
Können Sie die else-Anweisung nicht verwenden?. , .
Bereit Straßen.Gerade Flüsse
Zellen mit geraden Flüssen sind besonders schwierig, da sie das Zentrum der Zelle im Wesentlichen in zwei Teile teilen. Wir fügen bereits zusätzliche Dreiecke hinzu, um die Lücken zwischen den Flüssen zu füllen, aber wir müssen auch die Straßen auf gegenüberliegenden Seiten des Flusses trennen.Straßen, die einen geraden Fluss überlappen.Wenn die Zelle nicht den Anfang oder das Ende des Flusses hat, können wir prüfen, ob die ein- und ausgehenden Flüsse in entgegengesetzte Richtungen fließen. Wenn ja, dann haben wir einen direkten Fluss. if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { }
Um festzustellen, wo sich der Fluss relativ zur aktuellen Richtung befindet, müssen wir die benachbarten Richtungen überprüfen. Der Fluss ist entweder links oder rechts. Da wir dies am Ende der Methode tun, werden diese Anforderungen in booleschen Variablen zwischengespeichert. Dies vereinfacht auch das Lesen unseres Codes. bool hasRoadThroughEdge = cell.HasRoadThroughEdge(direction); bool previousHasRiver = cell.HasRiverThroughEdge(direction.Previous()); bool nextHasRiver = cell.HasRiverThroughEdge(direction.Next()); Vector2 interpolators = GetRoadInterpolators(direction, cell); Vector3 roadCenter = center; if (cell.HasRiverBeginOrEnd) { roadCenter += HexMetrics.GetSolidEdgeMiddle( cell.RiverBeginOrEndDirection.Opposite() ) * (1f / 3f); } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { if (previousHasRiver) { } else { } } Vector3 mL = Vector3.Lerp(roadCenter, e.v1, interpolators.x); Vector3 mR = Vector3.Lerp(roadCenter, e.v5, interpolators.y); TriangulateRoad(roadCenter, mL, mR, e, hasRoadThroughEdge); if (previousHasRiver) { TriangulateRoadEdge(roadCenter, center, mL); } if (nextHasRiver) { TriangulateRoadEdge(roadCenter, mR, center); }
Wir müssen die Mitte der Straße auf einen Winkelvektor verschieben, der in die entgegengesetzte Richtung vom Fluss zeigt. Wenn der Fluss durch die vorherige Richtung fließt, ist dies der zweite Raumwinkel. Ansonsten ist dies der erste Raumwinkel. else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { Vector3 corner; if (previousHasRiver) { corner = HexMetrics.GetSecondSolidCorner(direction); } else { corner = HexMetrics.GetFirstSolidCorner(direction); } }
Um die Straße so zu bewegen, dass sie an den Fluss angrenzt, müssen wir die Mitte der Straße um den halben Abstand zu dieser Ecke verschieben. Dann müssen wir auch die Mitte der Zelle um ein Viertel der Entfernung in diese Richtung bewegen. else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { Vector3 corner; if (previousHasRiver) { corner = HexMetrics.GetSecondSolidCorner(direction); } else { corner = HexMetrics.GetFirstSolidCorner(direction); } roadCenter += corner * 0.5f; center += corner * 0.25f; }
Geteilte Straßen.Wir haben ein Straßennetz in dieser Zelle geteilt. Dies ist normal, wenn sich die Straßen auf beiden Seiten des Flusses befinden. Aber wenn es auf einer Seite keine Straße gibt, haben wir ein kleines Stück einer abgelegenen Straße. Das ist unlogisch, also lasst uns solche Teile loswerden.Stellen Sie sicher, dass eine Straße in die aktuelle Richtung führt. Ist dies nicht der Fall, überprüfen Sie die andere Richtung derselben Flussseite auf das Vorhandensein der Straße. Wenn es dort oder dort keine vorbeifahrende Straße gibt, verlassen wir die Methode vor dem Triangulieren. if (previousHasRiver) { if ( !hasRoadThroughEdge && !cell.HasRoadThroughEdge(direction.Next()) ) { return; } corner = HexMetrics.GetSecondSolidCorner(direction); } else { if ( !hasRoadThroughEdge && !cell.HasRoadThroughEdge(direction.Previous()) ) { return; } corner = HexMetrics.GetFirstSolidCorner(direction); }
Verkürzte Straßen.Zick-Zack-Flüsse
Die nächste Art von Fluss ist Zickzack. Solche Flüsse teilen sich nicht das Straßennetz, daher müssen wir nur die Mitte der Straße verschieben.Zickzack durch die Straßen.Der einfachste Weg, um nach Zickzack zu suchen, besteht darin, die Richtungen der ein- und ausgehenden Flüsse zu vergleichen. Wenn sie benachbart sind, haben wir einen Zickzack. Dies führt je nach Strömungsrichtung zu zwei möglichen Optionen. if (cell.HasRiverBeginOrEnd) { … } else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { … } else if (cell.IncomingRiver == cell.OutgoingRiver.Previous()) { } else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { }
Wir können die Mitte der Straße mit einer der Ecken der Richtung des ankommenden Flusses verschieben. Der von Ihnen gewählte Winkel hängt von der Strömungsrichtung ab. Bewegen Sie die Straßenmitte aus diesem Winkel um den Faktor 0,2. else if (cell.IncomingRiver == cell.OutgoingRiver.Previous()) { roadCenter -= HexMetrics.GetSecondCorner(cell.IncomingRiver) * 0.2f; } else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { roadCenter -= HexMetrics.GetFirstCorner(cell.IncomingRiver) * 0.2f; }
Die Straße schob sich von den Zickzacklinien weg.In den krummen Flüssen
Die letzte Flusskonfiguration ist eine glatte Kurve. Wie beim direkten Fluss kann auch dieser Straßen trennen. In diesem Fall sind die Parteien jedoch unterschiedlich. Zuerst müssen wir mit der Innenseite der Kurve arbeiten.Ein geschwungener Fluss mit asphaltierten Straßen.Wenn wir auf beiden Seiten der aktuellen Richtung einen Fluss haben, befinden wir uns innerhalb der Kurve. else if (cell.IncomingRiver == cell.OutgoingRiver.Next()) { … } else if (previousHasRiver && nextHasRiver) { }
Wir müssen die Straßenmitte zum aktuellen Rand der Zelle bewegen und die Straße etwas verkürzen. Ein Koeffizient von 0,7 reicht aus. Das Zellzentrum sollte sich ebenfalls mit einem Koeffizienten von 0,5 verschieben. else if (previousHasRiver && nextHasRiver) { Vector3 offset = HexMetrics.GetSolidEdgeMiddle(direction) * HexMetrics.innerToOuter; roadCenter += offset * 0.7f; center += offset * 0.5f; }
Verkürzte Straßen.Wie bei geraden Flüssen müssen wir die isolierten Teile der Straßen abschneiden. In diesem Fall reicht es aus, nur die aktuelle Richtung zu überprüfen. else if (previousHasRiver && nextHasRiver) { if (!hasRoadThroughEdge) { return; } Vector3 offset = HexMetrics.GetSolidEdgeMiddle(direction) * HexMetrics.innerToOuter; roadCenter += offset * 0.7f; center += offset * 0.5f; }
Straßen abschneiden.Außerhalb der krummen Flüsse
Nach Überprüfung aller vorherigen Fälle war die einzige verbleibende Option der äußere Teil des gekrümmten Flusses. Draußen gibt es drei Teile der Zelle. Wir müssen die mittlere Richtung finden. Nachdem wir es erhalten haben, können wir die Mitte der Straße um den Faktor 0,25 in Richtung dieser Rippe bewegen. else if (previousHasRiver && nextHasRiver) { … } else { HexDirection middle; if (previousHasRiver) { middle = direction.Next(); } else if (nextHasRiver) { middle = direction.Previous(); } else { middle = direction; } roadCenter += HexMetrics.GetSolidEdgeMiddle(middle) * 0.25f; }
Die Außenseite der Straße wurde geändert.Als letzten Schritt müssen wir die Straßen auf dieser Seite des Flusses abschneiden. Am einfachsten ist es, alle drei Straßenrichtungen relativ zur Mitte zu überprüfen. Wenn es keine Straßen gibt, hören wir auf zu arbeiten. else { HexDirection middle; if (previousHasRiver) { middle = direction.Next(); } else if (nextHasRiver) { middle = direction.Previous(); } else { middle = direction; } if ( !cell.HasRoadThroughEdge(middle) && !cell.HasRoadThroughEdge(middle.Previous()) && !cell.HasRoadThroughEdge(middle.Next()) ) { return; } roadCenter += HexMetrics.GetSolidEdgeMiddle(middle) * 0.25f; }
Straßen vor und nach dem Abschneiden.Nachdem alle Flussoptionen verarbeitet wurden, können unsere Flüsse und Straßen nebeneinander existieren. Flüsse ignorieren Straßen und Straßen passen sich an Flüsse an.Die Kombination von Flüssen und Straßen.EinheitspaketDas Aussehen der Straßen
Bis zu diesem Moment haben wir ihre UV-Koordinaten als Straßenfarben verwendet. Da sich nur die U-Koordinate geändert hat, haben wir tatsächlich den Übergang zwischen der Mitte und dem Straßenrand angezeigt.Anzeige von UV-Koordinaten.Jetzt, da die Straßen genau richtig trianguliert sind, können wir den Road Shader so ändern, dass er eher Straßen ähnelt. Wie bei Flüssen ist dies eine einfache Visualisierung ohne Schnickschnack.Wir beginnen mit der Verwendung von Volltonfarben für Straßen. Verwenden Sie einfach die Farbe des Materials. Ich habe es rot gemacht. void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Rote Straßen.Und es sieht schon viel besser aus! Aber lassen Sie uns fortfahren und die Straße mit dem Gelände mischen, wobei wir die U-Koordinate als Mischfaktor verwenden. void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = _Color; float blend = IN.uv_MainTex.x; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = blend; }
Es scheint, dass dies nichts geändert hat. Es ist passiert, weil unser Shader undurchsichtig ist. Jetzt braucht er Alpha-Blending. Insbesondere benötigen wir einen Shader für eine passende Aufkleberoberfläche. Wir können den erforderlichen Shader erhalten, indem wir der Direktive eine #pragma surface
Zeile hinzufügen decal:blend
. #pragma surface surf Standard fullforwardshadows decal:blend
Die Mischung der Straßen.Also haben wir eine glatte lineare Mischung von der Mitte bis zur Kante erstellt, die nicht sehr hübsch aussieht. Damit es wie eine Straße aussieht, benötigen wir einen festen Bereich, gefolgt von einem schnellen Übergang zu einem undurchsichtigen Bereich. Sie können die Funktion dafür verwenden smoothstep
. Es wandelt einen linearen Verlauf von 0 nach 1 in eine S-förmige Kurve um.Lineare Progression und sanfter Schritt.Die Funktion smoothstep
verfügt über einen minimalen und einen maximalen Parameter, um die Kurve in einem beliebigen Intervall anzupassen. Eingabewerte außerhalb des Bereichs sind begrenzt, um die Kurve flach zu halten. Verwenden wir 0,4 am Anfang der Kurve und 0,7 am Ende. Dies bedeutet, dass die U-Koordinate von 0 bis 0,4 vollständig transparent ist. Und U-Koordinaten von 0,7 bis 1 sind vollständig undurchsichtig. Der Übergang erfolgt zwischen 0,4 und 0,7. float blend = IN.uv_MainTex.x; blend = smoothstep(0.4, 0.7, blend);
Schneller Übergang zwischen undurchsichtigen und transparenten Bereichen.Straße mit Lärm
Da das Straßennetz verzerrt wird, haben die Straßen unterschiedliche Breiten. Daher ist auch die Breite des Übergangs an den Kanten variabel. Manchmal ist es verschwommen, manchmal hart. Eine solche Variabilität passt zu uns, wenn wir die Straßen als sandig oder erdig wahrnehmen.Machen wir den nächsten Schritt und fügen Sie den Straßenrändern Lärm hinzu. Dadurch werden sie ungleichmäßiger und weniger polygonal. Wir können dies tun, indem wir die Rauschtextur abtasten. Für die Abtastung können Sie die Koordinaten der XZ-Welt verwenden, genau wie beim Verzerren der Eckpunkte der Zellen.Fügen Sie der Eingabestruktur hinzu, um Zugriff auf die Position der Welt im Surface Shader zu erhalten float3 worldPos
. struct Input { float2 uv_MainTex; float3 worldPos; };
Jetzt können wir diese Position verwenden surf
, um die Haupttextur abzutasten. Verkleinern Sie auch, sonst wiederholt sich die Textur zu oft. float4 noise = tex2D(_MainTex, IN.worldPos.xz * 0.025); fixed4 c = _Color; float blend = IN.uv_MainTex.x;
Wir verzerren den Übergang, indem wir die Koordinate U mit multiplizieren noise.x
. Da die Geräuschwerte jedoch durchschnittlich 0,5 betragen, verschwinden die meisten Straßen. Um dies zu vermeiden, addieren Sie vor der Multiplikation 0,5 zum Rauschen. float blend = IN.uv_MainTex.x; blend *= noise.x + 0.5; blend = smoothstep(0.4, 0.7, blend);
Verzerrte Straßenränder.Um dies zu beenden, werden wir auch die Farbe der Straßen verzerren. Dies gibt den Straßen ein Gefühl von Schmutz, der unscharfen Kanten entspricht.Multiplizieren Sie die Farbe mit einem anderen Rauschkanal, z noise.y
. B. mit . Wir erhalten also durchschnittlich die Hälfte des Farbwerts. Da dies zu viel ist, werden wir die Rauschskala leicht reduzieren und eine Konstante hinzufügen, damit die Summe 1 erreichen kann. fixed4 c = _Color * (noise.y * 0.75 + 0.25);
Raue Straßen.Einheitspaket