Karten aus Sechsecken in der Einheit: Wasser, Reliefobjekte und Festungsmauern

Ursprünglicher Autor: Jasper Flick
  • Übersetzung
Teile 1-3: Maschen, Farben und Höhen der Zellen

Teile 4-7: Unebenheiten, Flüsse und Straßen

Teile 8-11: Wasser, Reliefgegenstände und Mauern der

Teile 12-15: Konservieren und Laden, Texturen, Entfernungen

Teile 16-19: Suche nach dem Pfad, Spielertrupps, Animationen

Teile 20-23: Nebel des Krieges, Kartenerforschung, Verfahrensgenerierung

Teile 24-27: Wasserkreislauf, Erosion, Biomes, zylindrische Karte

Teil 8: Wasser


  • Füge Wasser zu den Zellen hinzu.
  • Triangulieren Sie die Wasseroberfläche.
  • Erstellen Sie eine Brandung mit Schaum.
  • Kombinieren Sie Wasser und Flüsse.

Wir haben bereits Unterstützung für die Flüsse hinzugefügt, und in diesem Teil werden wir die Zellen vollständig in Wasser tauchen.


Wasser kommt.

Wasserstand


Die einfachste Möglichkeit, die Wasserunterstützung zu implementieren, besteht darin, sie auf das gleiche Niveau einzustellen. Alle Zellen unterhalb dieses Niveaus werden in Wasser getaucht. Eine flexiblere Möglichkeit besteht darin, Wasser in verschiedenen Höhen abzustützen. Lassen Sie uns also den Wasserstand veränderbar machen. Dazu müssen Sie HexCellIhren Wasserstand verfolgen.

publicint WaterLevel {
		get {
			return waterLevel;
		}
		set {
			if (waterLevel == value) {
				return;
			}
			waterLevel = value;
			Refresh();
		}
	}
	int waterLevel;

Auf Wunsch können Sie sicherstellen, dass bestimmte Merkmale des Reliefs unter Wasser nicht vorhanden sind. Aber im Moment werde ich das nicht tun. Dinge wie Unterwasserstraßen passen zu mir. Sie können als Gebiete betrachtet werden, die kürzlich überflutet wurden.

Überflutung von Zellen


Jetzt, da wir Wasserstände haben, ist die wichtigste Frage, ob sich die Zellen unter Wasser befinden. Die Zelle befindet sich unter Wasser, wenn ihr Wasserstand höher als ihre Höhe ist. Um diese Informationen zu erhalten, fügen wir eine Eigenschaft hinzu.

publicbool IsUnderwater {
		get {
			return waterLevel > elevation;
		}
	}

Dies bedeutet, dass sich die Zelle über dem Wasser erhebt, wenn der Wasserstand und die Höhe gleich sind. Das heißt, die reale Wasseroberfläche befindet sich unterhalb dieser Höhe. Wie bei Flussoberflächen addieren wir den gleichen Versatz - HexMetrics.riverSurfaceElevationOffset. Ändern Sie den Namen in allgemeiner.

//	public const float riverSurfaceElevationOffset = -0.5f;publicconstfloat waterElevationOffset = -0.5f;

Ändern Sie es HexCell.RiverSurfaceYso, dass es den neuen Namen verwendet. Fügen Sie dann der Wasseroberfläche der überfluteten Zelle eine ähnliche Eigenschaft hinzu.

publicfloat RiverSurfaceY {
		get {
			return
				(elevation + HexMetrics.waterElevationOffset) *
				HexMetrics.elevationStep;
		}
	}
	publicfloat WaterSurfaceY {
		get {
			return
				(waterLevel + HexMetrics.waterElevationOffset) *
				HexMetrics.elevationStep;
		}
	}

Wasseraufbereitung


Das Ändern des Wasserstands ähnelt dem Ändern der Höhe. Daher HexMapEditormuss der aktive Wasserstand überwacht werden und ob er auf Zellen angewendet werden soll.

int activeElevation;
	int activeWaterLevel;
	…
	bool applyElevation = true;
	bool applyWaterLevel = true;
	

Fügen Sie Methoden hinzu, um diese Parameter mit der Benutzeroberfläche zu verbinden.

publicvoidSetApplyWaterLevel (bool toggle) {
		applyWaterLevel = toggle;
	}
	publicvoidSetWaterLevel (float level) {
		activeWaterLevel = (int)level;
	}

Und fügen Sie den Wasserstand in EditCell.

voidEditCell (HexCell cell) {
		if (cell) {
			if (applyColor) {
				cell.Color = activeColor;
			}
			if (applyElevation) {
				cell.Elevation = activeElevation;
			}
			if (applyWaterLevel) {
				cell.WaterLevel = activeWaterLevel;
			}
			…
		}
	}

Um der Benutzeroberfläche einen Wasserstand hinzuzufügen, duplizieren Sie die Beschriftung und den Höhenregler und ändern Sie sie dann. Vergessen Sie nicht, ihre Ereignisse an die entsprechenden Methoden anzuhängen.


Wasserstandsregler.

Einheitspaket

Wassertriangulation


Um Wasser zu triangulieren, brauchen wir ein neues Netz mit neuem Material. Erstellen Sie zuerst den Water- Shader, indem Sie den River- Shader duplizieren . Ändern Sie es, um die Farbeigenschaft zu verwenden.

Shader "Custom/Water" {
	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"="Transparent""Queue"="Transparent" }
		LOD 200
		CGPROGRAM
		#pragma surface surf Standard alpha#pragma target 3.0
		sampler2D _MainTex;
		struct Input {
			float2 uv_MainTex;
		};
		half _Glossiness;
		half _Metallic;
		fixed4 _Color;
		voidsurf (Input IN, inout SurfaceOutputStandard o) {
			fixed4 c = _Color;
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}
		ENDCG
	}
	FallBack "Diffuse"
}

Erstellen Sie mit diesem Shader ein neues Material, indem Sie das Material Wasser duplizieren und durch einen Shader ersetzen. Lassen wir die Noise-Textur, weil wir sie später verwenden.


Materielles Wasser.

Fügen Sie dem Fertighaus ein neues untergeordnetes Element hinzu, indem Sie das untergeordnete Element " Rivers" duplizieren . Es werden keine UV-Koordinaten benötigt und es muss Wasser verwendet werden . Erstellen Sie dazu wie gewohnt eine Instanz des Fertighauses, ändern Sie sie und wenden Sie die Änderungen dann auf das Fertighaus an. Beseitigen Sie danach die Instanz.



Kind Wasserobjekt.

Als nächstes geben Sie das HexGridChunkWasser in das Stützgitter.

public HexMesh terrain, rivers, roads, water;
	publicvoidTriangulate () {
		terrain.Clear();
		rivers.Clear();
		roads.Clear();
		water.Clear();
		for (int i = 0; i < cells.Length; i++) {
			Triangulate(cells[i]);
		}
		terrain.Apply();
		rivers.Apply();
		roads.Apply();
		water.Apply();
	}

Und verbinden Sie es mit dem untergeordneten Objekt des Fertighauses.


Wasserobjekt ist angeschlossen.

Wasser Sechsecke


Da Wasser die zweite Schicht bildet, geben wir für jede Richtung eine eigene Triangulationsmethode an. Wir brauchen es nur zu nennen, wenn die Zelle in Wasser eingetaucht ist.

voidTriangulate (HexDirection direction, HexCell cell) {
		…
		if (cell.IsUnderwater) {
			TriangulateWater(direction, cell, center);
		}
	}
	voidTriangulateWater (
		HexDirection direction, HexCell cell, Vector3 center
	) {
	}

Wie bei Flüssen variiert die Höhe der Wasseroberfläche in Zellen mit gleichem Wasserstand nicht sehr. Daher scheinen wir keine komplexen Kanten zu benötigen. Genug wird ein einfaches Dreieck sein.

voidTriangulateWater (
		HexDirection direction, HexCell cell, Vector3 center
	) {
		center.y = cell.WaterSurfaceY;
		Vector3 c1 = center + HexMetrics.GetFirstSolidCorner(direction);
		Vector3 c2 = center + HexMetrics.GetSecondSolidCorner(direction);
		water.AddTriangle(center, c1, c2);
	}


Sechsecke Wasser.

Wasseranschlüsse


Wir können benachbarte Zellen mit einem Viereck mit Wasser verbinden.

		water.AddTriangle(center, c1, c2);
		if (direction <= HexDirection.SE) {
			HexCell neighbor = cell.GetNeighbor(direction);
			if (neighbor == null || !neighbor.IsUnderwater) {
				return;
			}
			Vector3 bridge = HexMetrics.GetBridge(direction);
			Vector3 e1 = c1 + bridge;
			Vector3 e2 = c2 + bridge;
			water.AddQuad(c1, c2, e1, e2);
		}


Die Ränder des Wassers verbinden.

Und füllen Sie die Ecken mit einem Dreieck.

if (direction <= HexDirection.SE) {
			…
			water.AddQuad(c1, c2, e1, e2);
			if (direction <= HexDirection.E) {
				HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
				if (nextNeighbor == null || !nextNeighbor.IsUnderwater) {
					return;
				}
				water.AddTriangle(
					c2, e2, c2 + HexMetrics.GetBridge(direction.Next())
				);
			}
		}


Anschlusswinkel von Wasser.

Jetzt haben wir Wasserzellen angeschlossen, wenn sie in der Nähe sind. Sie lassen eine Lücke zwischen sich und trockenen Zellen mit größerer Höhe, aber das werden wir für später belassen.

Abgestimmte Wasserstände


Wir gingen davon aus, dass die benachbarten Unterseebootzellen den gleichen Wasserstand haben. Wenn ja, dann sieht alles gut aus, aber wenn diese Annahme verletzt wird, dann treten Fehler auf.


Inkonsistente Wasserstände.

Wir können dafür sorgen, dass das Wasser auf dem gleichen Niveau bleibt. Wenn sich beispielsweise der Wasserstand einer überfluteten Zelle ändert, können wir die Änderungen auf benachbarte Zellen verteilen, um die Pegel synchron zu halten. Dieser Prozess muss jedoch fortgesetzt werden, bis er auf Zellen trifft, die nicht in Wasser eingetaucht sind. Diese Zellen legen die Grenzen des Wasserkörpers fest.

Die Gefahr dieses Ansatzes besteht darin, dass er schnell außer Kontrolle geraten kann. Wenn die Bearbeitung nicht erfolgreich ist, kann Wasser die gesamte Karte bedecken. Dann müssen alle Fragmente gleichzeitig trianguliert werden, was zu einem enormen Verspätungssprung führt.

Also lass es uns noch nicht tun. Diese Funktion kann in einem komplexeren Editor hinzugefügt werden. Solange die Konsistenz des Wasserstandes besteht, verlassen wir das Gewissen des Benutzers.

Einheitspaket

Wasser beleben


Anstelle einer einheitlichen Farbe werden wir etwas schaffen, das einer Welle ähnelt. Wie in anderen Shadern werden wir uns immer noch nicht um schöne Grafiken bemühen, wir müssen nur die Wellen bezeichnen.


Perfekt flaches Wasser.

Machen wir dasselbe wie mit den Flüssen. Lassen Sie uns das Rauschen mit der Position der Welt abtasten und der einheitlichen Farbe hinzufügen. Fügen Sie der V-Koordinate Zeit hinzu, um die Oberfläche zu animieren.

		struct Input {
			float2 uv_MainTex;
			float3 worldPos;
		};
		…
		void surf (InputIN, inout SurfaceOutputStandard o) {
			float2 uv = IN.worldPos.xz;
			uv.y += _Time.y;
			float4 noise = tex2D(_MainTex, uv * 0.025);
			float waves = noise.z;
			fixed4 c = saturate(_Color + waves);
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}


Wasserlaufzeit × 10.

Zwei Richtungen


Bisher ist dies überhaupt nicht wie Wellen. Lassen Sie uns das Bild komplizieren, indem Sie ein zweites Rauschmuster
und diesmal die U-Koordinate hinzufügen. Verwenden Sie einen anderen Rauschkanal, um zwei verschiedene Muster als Ergebnis zu erhalten. Die fertigen Wellen werden aus diesen beiden Proben zusammengesetzt.

			float2 uv1 = IN.worldPos.xz;
			uv1.y += _Time.y;
			float4 noise1 = tex2D(_MainTex, uv1 * 0.025);
			float2 uv2 = IN.worldPos.xz;
			uv2.x += _Time.y;
			float4 noise2 = tex2D(_MainTex, uv2 * 0.025);
			float waves = noise1.z + noise2.x;

Wenn wir beide Samples summieren, erhalten wir Ergebnisse im Bereich von 0 bis 2, daher müssen wir sie auf 0 bis 1 zurückskalieren. Anstatt die Wellen einfach in zwei Hälften zu teilen, können wir die Funktion verwenden smoothstep, um ein interessanteres Ergebnis zu erzielen. Wir werden ¾ - 2 auf 0–1 setzen, damit es keine sichtbaren Wellen auf der Wasseroberfläche gibt.

float waves = noise1.z + noise2.x;
			waves = smoothstep(0.75, 2, waves);


Zwei Richtungen, Zeit × 10.

Wellen des Mischens


Es ist immer noch auffällig, dass wir zwei sich bewegende Geräuschmuster haben, die sich tatsächlich nicht ändern. Es wäre plausibel, wenn die Muster geändert würden. Dies können wir erreichen, indem wir zwischen verschiedenen Kanälen von Rauschabtastwerten interpolieren. Dies kann jedoch nicht auf die gleiche Weise geschehen, da sich sonst die gesamte Wasseroberfläche gleichzeitig ändert, und dies ist sehr auffällig. Stattdessen werden wir eine Welle des Mischens erzeugen.

Wir werden eine Mischwelle mit einer Sinuswelle erzeugen, die sich diagonal über die Wasseroberfläche bewegt. Dazu addieren wir die Koordinaten der Welt X und Z und verwenden die Summe als Eingabedaten für die Funktion sin. Verkleinern, um ausreichend große Bänder zu erhalten. Und natürlich den gleichen Wert hinzufügen, um sie zu animieren.

float blendWave =
				sin((IN.worldPos.x + IN.worldPos.z) * 0.1 + _Time.y);

Die Sinuskurven liegen zwischen -1 und 1, und wir benötigen ein Intervall von 0-1. Sie können es erhalten, indem Sie die Welle quadrieren. Um das isolierte Ergebnis anzuzeigen, verwenden Sie es anstelle der geänderten Farbe als Ausgabewert.

			sin((IN.worldPos.x + IN.worldPos.z) * 0.1 + _Time.y);
			blendWave *= blendWave;
			float waves = noise1.z + noise2.x;
			waves = smoothstep(0.75, 2, waves);
			fixed4 c = blendWave; //saturate(_Color + waves);


Wellen der Verwirrung.

Fügen Sie beiden Samples etwas Rauschen hinzu, damit Mischwellen weniger auffallen.

float blendWave = sin(
				(IN.worldPos.x + IN.worldPos.z) * 0.1 +
				(noise1.y + noise2.z) + _Time.y
			);
			blendWave *= blendWave;


Verzerrte Mischwellen.

Verwenden Sie zum Schluss eine Mischwelle, um zwischen den beiden Kanälen beider Rauschproben zu interpolieren. Nehmen Sie für maximale Variabilität vier verschiedene Kanäle.

			float waves =
				lerp(noise1.z, noise1.w, blendWave) +
				lerp(noise2.x, noise2.y, blendWave);
			waves = smoothstep(0.75, 2, waves);
			fixed4 c = saturate(_Color + waves);


Wellenmischung, Zeit × 2.

Einheitspaket

Küstenlinie


Am Ende hatten wir offenes Wasser, aber jetzt müssen wir die Lücke im Wasser entlang der Küste füllen. Da wir die Landkonturen einhalten müssen, erfordert das Wasser der Küste einen anderen Ansatz. Lassen Sie uns TriangulateWaterin zwei Methoden einteilen - eine für offenes Wasser, die zweite für die Küste. Um zu verstehen, wann wir mit der Küste arbeiten, müssen wir uns die nächste Zelle ansehen. Das heißt, in werden TriangulateWaterwir einen Nachbarn empfangen. Wenn es einen Nachbarn gibt und er nicht unter Wasser ist, dann haben wir es mit der Küste zu tun.

voidTriangulateWater (
		HexDirection direction, HexCell cell, Vector3 center
	) {
		center.y = cell.WaterSurfaceY;
		HexCell neighbor = cell.GetNeighbor(direction);
		if (neighbor != null && !neighbor.IsUnderwater) {
			TriangulateWaterShore(direction, cell, neighbor, center);
		}
		else {
			TriangulateOpenWater(direction, cell, neighbor, center);
		}
	}
	voidTriangulateOpenWater (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		Vector3 c1 = center + HexMetrics.GetFirstSolidCorner(direction);
		Vector3 c2 = center + HexMetrics.GetSecondSolidCorner(direction);
		water.AddTriangle(center, c1, c2);
		if (direction <= HexDirection.SE && neighbor != null) {
//			HexCell neighbor = cell.GetNeighbor(direction);//			if (neighbor == null || !neighbor.IsUnderwater) {//				return;//			}
			Vector3 bridge = HexMetrics.GetBridge(direction);
			…
		}
	}
	voidTriangulateWaterShore (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
	}


Es gibt keine Triangulation entlang der Küste.

Da die Küste verzerrt ist, müssen wir die Dreiecke des Wassers entlang der Küste verzerren. Deshalb brauchen wir Eckpunkte von Kanten und einen Fächer von Dreiecken.

voidTriangulateWaterShore (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		EdgeVertices e1 = new EdgeVertices(
			center + HexMetrics.GetFirstSolidCorner(direction),
			center + HexMetrics.GetSecondSolidCorner(direction)
		);
		water.AddTriangle(center, e1.v1, e1.v2);
		water.AddTriangle(center, e1.v2, e1.v3);
		water.AddTriangle(center, e1.v3, e1.v4);
		water.AddTriangle(center, e1.v4, e1.v5);
	}


Fächerdreiecke entlang der Küste.

Als nächstes kommt ein Streifen Rippen, wie im üblichen Relief. Wir sind jedoch nicht verpflichtet, uns nur auf bestimmte Richtungen zu beschränken, da wir TriangulateWaterShoreerst anrufen, wenn wir uns mit der Küste treffen, für die der Streifen immer benötigt wird.

		water.AddTriangle(center, e1.v4, e1.v5);
		Vector3 bridge = HexMetrics.GetBridge(direction);
		EdgeVertices e2 = new EdgeVertices(
			e1.v1 + bridge,
			e1.v5 + bridge
		);
		water.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2);
		water.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3);
		water.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4);
		water.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5);


Streifen von Rippen entlang der Küste.

Ebenso müssen wir jedes Mal ein eckiges Dreieck hinzufügen.

		water.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5);
		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (nextNeighbor != null) {
			water.AddTriangle(
				e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next())
			);
		}


Die Winkel der Rippen entlang der Küste.

Jetzt haben wir bereit Wasserküste. Ein Teil davon befindet sich immer unter dem Reliefnetz, daher gibt es keine Löcher.

UV-Küste


Wir können alles so lassen, wie es ist, aber es wäre interessant, wenn das Küstenwasser einen eigenen Zeitplan hätte. Zum Beispiel die Wirkung von Schaum, der bei Annäherung an die Küste größer wird. Um es zu implementieren, muss der Shader wissen, wie nahe das Fragment an der Küste liegt. Wir können diese Informationen über UV-Koordinaten übertragen.

Offenes Wasser hat keine UV-Koordinaten und benötigt keinen Schaum. Es wird nur für küstennahe Gewässer benötigt. Daher sind die Anforderungen für beide Wassertypen sehr unterschiedlich. Es ist logisch, für jeden Typ ein eigenes Netz zu erstellen. Daher fügen wir HexGridChunkein weiteres Mesh-Objekt hinzu , um es zu unterstützen.

public HexMesh terrain, rivers, roads, water, waterShore;
	publicvoidTriangulate () {
		terrain.Clear();
		rivers.Clear();
		roads.Clear();
		water.Clear();
		waterShore.Clear();
		for (int i = 0; i < cells.Length; i++) {
			Triangulate(cells[i]);
		}
		terrain.Apply();
		rivers.Apply();
		roads.Apply();
		water.Apply();
		waterShore.Apply();
	}

Dieses neue Netz wird verwendet TriangulateWaterShore.

voidTriangulateWaterShore (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		…
		waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2);
		waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3);
		waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4);
		waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5);
		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (nextNeighbor != null) {
			waterShore.AddTriangle(
				e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next())
			);
		}
	}

Duplizieren Sie das Wasserobjekt, verbinden Sie es mit dem Fertighaus und konfigurieren Sie es so, dass es die UV-Koordinaten verwendet. Wir werden auch einen Shader und ein Material für Küstenwasser erstellen, wobei der vorhandene Shader und das vorhandene Wassermaterial dupliziert werden.


Wasserufergegenstand und -material mit UV.

Ändern Sie den Water Shore- Shader so, dass anstelle von Wasser die UV-Koordinaten angezeigt werden.

			fixed4 c = fixed4(IN.uv_MainTex, 1, 1);

Da die Koordinaten noch nicht angegeben sind, wird eine Volltonfarbe angezeigt. Dies macht es leicht zu erkennen, dass die Küste tatsächlich ein separates Netz mit Material verwendet.


Separates Netz für die Küste.

Lassen Sie uns Informationen über die Küste in die Koordinate V eintragen. Auf der Wasserseite weisen wir ihr den Wert 0 zu, auf der Landseite den Wert 1. Da wir nichts mehr senden müssen, sind alle U-Koordinaten einfach 0.

		waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2);
		waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3);
		waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4);
		waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5);
		waterShore.AddQuadUV(0f, 0f, 0f, 1f);
		waterShore.AddQuadUV(0f, 0f, 0f, 1f);
		waterShore.AddQuadUV(0f, 0f, 0f, 1f);
		waterShore.AddQuadUV(0f, 0f, 0f, 1f);
		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (nextNeighbor != null) {
			waterShore.AddTriangle(
				e1.v5, e2.v5, e1.v5 + HexMetrics.GetBridge(direction.Next())
			);
			waterShore.AddTriangleUV(
				new Vector2(0f, 0f),
				new Vector2(0f, 1f),
				new Vector2(0f, 0f)
			);
		}


Übergänge zu den Küsten, falsch.

Der obige Code funktioniert für Kanten, ist jedoch in einigen Winkeln fehlerhaft. Befindet sich der nächste Nachbar unter Wasser, ist dieser Ansatz korrekt. Befindet sich der nächste Nachbar jedoch nicht unter Wasser, befindet sich der dritte Scheitelpunkt des Dreiecks unter trockenem Land.

			waterShore.AddTriangleUV(
				new Vector2(0f, 0f),
				new Vector2(0f, 1f),
				new Vector2(0f, nextNeighbor.IsUnderwater ? 0f : 1f)
			);


Übergänge zu den Küsten, richtig.

Schaum an der Küste


Nachdem die Übergänge zur Küste korrekt implementiert wurden, können Sie sie verwenden, um einen Schaumeffekt zu erzeugen. Der einfachste Weg, einer einheitlichen Farbe den Wert der Küste zu verleihen.

void surf (InputIN, inout SurfaceOutputStandard o) {
			float shore = IN.uv_MainTex.y;
			float foam = shore;
			fixed4 c = saturate(_Color + foam);
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}


Linearer Schaum.

Um den Schaum interessanter zu machen, multiplizieren Sie ihn mit dem Quadrat der Sinuswelle.

float foam = sin(shore * 10);
			foam *= foam * shore;


Verblassender quadratischer Sinusschaum.

Lassen Sie uns die Schaumfront vergrößern, wenn wir uns dem Ufer nähern. Dies kann erreicht werden, indem die Quadratwurzel gezogen wird, bevor der Wert der Küste verwendet wird.

float shore = IN.uv_MainTex.y;
			shore = sqrt(shore);


In Ufernähe wird der Schaum dicker.

Fügen Sie Verzerrungen hinzu, damit es natürlicher aussieht. Machen wir es so, dass die Verzerrung schwächer wird, wenn wir uns dem Ufer nähern. So wird es besser an die Küste passen.

			float2 noiseUV = IN.worldPos.xz;
			float4 noise = tex2D(_MainTex, noiseUV * 0.015);
			float distortion = noise.x * (1 - shore);
			float foam = sin((shore + distortion) * 10);
			foam *= foam * shore;


Verzerrter Schaum.

Und das alles ist natürlich animiert: sowohl eine Sinuswelle als auch eine Verzerrung.

			float2 noiseUV = IN.worldPos.xz + _Time.y * 0.25;
			float4 noise = tex2D(_MainTex, noiseUV * 0.015);
			float distortion = noise.x * (1 - shore);
			float foam = sin((shore + distortion) * 10 - _Time.y);
			foam *= foam * shore;


Animierter Schaum.

Zusätzlich zum ankommenden Schaum gibt es einen Rückzug. Fügen wir für die Simulation eine zweite Sinuskurve hinzu, die sich in die entgegengesetzte Richtung bewegt. Machen Sie es schwächer und fügen Sie eine Zeitverschiebung hinzu. Der fertige Schaum wird das Maximum dieser beiden Sinuskurven sein.

float distortion1 = noise.x * (1 - shore);
			float foam1 = sin((shore + distortion1) * 10 - _Time.y);
			foam1 *= foam1;
			float distortion2 = noise.y * (1 - shore);
			float foam2 = sin((shore + distortion2) * 10 + _Time.y + 2);
			foam2 *= foam2 * 0.7;
			float foam = max(foam1, foam2) * shore;


Eingehender und zurückgehender Schaum.

Wellen und Schaum mischen


Es gibt einen scharfen Übergang zwischen offenen Gewässern und Küstengewässern, da offene Wasserwellen nicht in den Küstengewässern enthalten sind. Um dies zu beheben, müssen wir diese Wellen in den Water Shore- Shader aufnehmen .

Anstatt den Code der Wellen zu kopieren, fügen wir ihn in die Include- Datei Water.cginc ein . Tatsächlich fügen wir Code für Schaum und für Wellen ein, jeweils als separate Funktion.

Wie funktionieren Shader-Include-Dateien?
Создание собственных include-файлов шейдеров рассматривается в туториале Rendering 5, Multiple Lights.

float Foam (float shore, float2 worldXZ, sampler2D noiseTex) {
//	float shore = IN.uv_MainTex.y;
	shore = sqrt(shore);
	float2 noiseUV = worldXZ + _Time.y * 0.25;
	float4 noise = tex2D(noiseTex, noiseUV * 0.015);
	float distortion1 = noise.x * (1 - shore);
	float foam1 = sin((shore + distortion1) * 10 - _Time.y);
	foam1 *= foam1;
	float distortion2 = noise.y * (1 - shore);
	float foam2 = sin((shore + distortion2) * 10 + _Time.y + 2);
	foam2 *= foam2 * 0.7;
	return max(foam1, foam2) * shore;
}
float Waves (float2 worldXZ, sampler2D noiseTex) {
	float2 uv1 = worldXZ;
	uv1.y += _Time.y;
	float4 noise1 = tex2D(noiseTex, uv1 * 0.025);
	float2 uv2 = worldXZ;
	uv2.x += _Time.y;
	float4 noise2 = tex2D(noiseTex, uv2 * 0.025);
	float blendWave = sin(
		(worldXZ.x + worldXZ.y) * 0.1 +
		(noise1.y + noise2.z) + _Time.y
	);
	blendWave *= blendWave;
	float waves =
		lerp(noise1.z, noise1.w, blendWave) +
		lerp(noise2.x, noise2.y, blendWave);
	return smoothstep(0.75, 2, waves);
}

Ändern Sie den Water- Shader , um die neue Include-Datei zu verwenden.

		#include "Water.cginc"
		sampler2D _MainTex;
		…
		void surf (InputIN, inout SurfaceOutputStandard o) {
			float waves = Waves(IN.worldPos.xz, _MainTex);
			fixed4 c = saturate(_Color + waves);
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}

Der Water Shore Shader berechnet Werte für Schaum und Wellen. Dann dämpfen wir die Wellen, während wir uns dem Ufer nähern. Das Endergebnis wird ein Maximum an Schaum und Wellen sein.

		#include "Water.cginc"
		sampler2D _MainTex;
		…
		void surf (InputIN, inout SurfaceOutputStandard o) {
			float shore = IN.uv_MainTex.y;
			float foam = Foam(shore, IN.worldPos.xz, _MainTex);
			float waves = Waves(IN.worldPos.xz, _MainTex);
			waves *= 1 - shore;
			fixed4 c = saturate(_Color + max(foam, waves));
			o.Albedo = c.rgb;
			o.Metallic = _Metallic;
			o.Smoothness = _Glossiness;
			o.Alpha = c.a;
		}


Schaum und Wellen mischen.

Einheitspaket

Wieder über Küstenwasser


Ein Teil des Küstennetzes ist unter dem Netz des Reliefs verborgen. Das ist normal, aber nur ein kleiner Teil ist verborgen. Leider verbergen steile Klippen den größten Teil des Küstenwassers und schäumen daher.


Fast verstecktes Küstenwasser.

Wir können damit umgehen, indem wir die Küste vergrößern. Dies kann erreicht werden, indem der Radius der Sechsecke des Wassers verringert wird. Hierfür benötigen wir neben dem Integritätskoeffizienten HexMetricsein Wasserverhältnis sowie Methoden zur Ermittlung von Wasserwinkeln.

Der Integritätskoeffizient beträgt 0,8. Um die Größe der Wasserverbindungen zu verdoppeln, müssen wir dem Wasserkoeffizienten einen Wert von 0,6 zuweisen.

publicconstfloat waterFactor = 0.6f;
	publicstatic Vector3 GetFirstWaterCorner (HexDirection direction) {
		return corners[(int)direction] * waterFactor;
	}
	publicstatic Vector3 GetSecondWaterCorner (HexDirection direction) {
		return corners[(int)direction + 1] * waterFactor;
	}

Mit diesen neuen Methoden ermitteln HexGridChunkwir die Winkel des Wassers.

voidTriangulateOpenWater (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		Vector3 c1 = center + HexMetrics.GetFirstWaterCorner(direction);
		Vector3 c2 = center + HexMetrics.GetSecondWaterCorner(direction);
		…
	}
	voidTriangulateWaterShore (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		EdgeVertices e1 = new EdgeVertices(
			center + HexMetrics.GetFirstWaterCorner(direction),
			center + HexMetrics.GetSecondWaterCorner(direction)
		);
		…
	}


Mit Wasserwinkeln.

Der Abstand zwischen den Sechsecken des Wassers hat sich in der Tat verdoppelt. Jetzt HexMetricssollte auch eine Methode zur Schaffung von Brücken im Wasser haben.

publicconstfloat waterBlendFactor = 1f - waterFactor;
	publicstatic Vector3 GetWaterBridge (HexDirection direction) {
		return (corners[(int)direction] + corners[(int)direction + 1]) *
			waterBlendFactor;
	}

Ändern wir es HexGridChunkso, dass es die neue Methode verwendet.

voidTriangulateOpenWater (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		…
		if (direction <= HexDirection.SE && neighbor != null) {
			Vector3 bridge = HexMetrics.GetWaterBridge(direction);
			…
			if (direction <= HexDirection.E) {
				…
				water.AddTriangle(
					c2, e2, c2 + HexMetrics.GetWaterBridge(direction.Next())
				);
			}
		}
	}
	voidTriangulateWaterShore (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		…
		Vector3 bridge = HexMetrics.GetWaterBridge(direction);
		…
		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (nextNeighbor != null) {
			waterShore.AddTriangle(
				e1.v5, e2.v5, e1.v5 +
					HexMetrics.GetWaterBridge(direction.Next())
			);
			…
		}
	}


Lange Brücken im Wasser.

Zwischen den Rippen von Wasser und Land


Das gibt uns zwar mehr Platz für den Schaum, verbirgt aber jetzt noch mehr unter dem Relief. Im Idealfall können wir den Wasserrand von der Wasserseite und den Landrand von der Landseite verwenden.

Wir können keine einfache Brücke benutzen, um den gegenüberliegenden Rand des Landes zu finden, wenn wir von den Ecken des Wassers ausgehen. Stattdessen können wir vom Zentrum des Nachbarn aus in die entgegengesetzte Richtung gehen. Ändern Sie TriangulateWaterShore, um diesen neuen Ansatz zu verwenden.

//		Vector3 bridge = HexMetrics.GetWaterBridge(direction);
		Vector3 center2 = neighbor.Position;
		center2.y = center.y;
		EdgeVertices e2 = new EdgeVertices(
			center2 + HexMetrics.GetSecondSolidCorner(direction.Opposite()),
			center2 + HexMetrics.GetFirstSolidCorner(direction.Opposite())
		);
		…
		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (nextNeighbor != null) {
			Vector3 center3 = nextNeighbor.Position;
			center3.y = center.y;
			waterShore.AddTriangle(
				e1.v5, e2.v5, center3 +
					HexMetrics.GetFirstSolidCorner(direction.Previous())
			);
			…
		}


Falsche Ecken der Kanten.

Es hat funktioniert, aber jetzt müssen wir wieder zwei Fälle für Eckdreiecke betrachten.

		HexCell nextNeighbor = cell.GetNeighbor(direction.Next());
		if (nextNeighbor != null) {
//			Vector3 center3 = nextNeighbor.Position;//			center3.y = center.y;
			Vector3 v3 = nextNeighbor.Position + (nextNeighbor.IsUnderwater ?
				HexMetrics.GetFirstWaterCorner(direction.Previous()) :
				HexMetrics.GetFirstSolidCorner(direction.Previous()));
			v3.y = center.y;
			waterShore.AddTriangle(e1.v5, e2.v5, v3);
			waterShore.AddTriangleUV(
				new Vector2(0f, 0f),
				new Vector2(0f, 1f),
				new Vector2(0f, nextNeighbor.IsUnderwater ? 0f : 1f)
			);
		}


Die richtigen Winkel der Kanten.

Es hat gut funktioniert, aber jetzt, da der größte Teil des Schaums sichtbar ist, ist er ziemlich ausgeprägt. Um dies zu kompensieren, werden wir den Effekt etwas schwächer machen und den Maßstab der Küste im Shader verringern.

	shore = sqrt(shore) * 0.9;


Fertigschaum.

Einheitspaket

U-Boot-Flüsse


Zumindest dort, wo Flüsse nicht hineinfließen, haben wir Wasser. Da sich Wasser und Flüsse noch nicht bemerken, fließen Flüsse durch und unter das Wasser.


Flüsse fließen in Wasser.

Die Reihenfolge, in der durchscheinende Objekte gerendert werden, hängt von ihrem Abstand zur Kamera ab. Die nächstgelegenen Objekte werden zuletzt gerendert, sodass sie sich oben befinden. Wenn Sie die Kamera bewegen, treten manchmal Flüsse und manchmal Wasser übereinander auf. Beginnen wir damit, die Renderreihenfolge konstant zu halten. Flüsse müssen über dem Wasser gezogen werden, damit die Wasserfälle richtig angezeigt werden. Dies können wir erreichen, indem wir die Warteschlange des River Shader ändern .

		Tags { "RenderType"="Transparent""Queue"="Transparent+1" }


Zeichne den Fluss zuletzt.

Unterwasserflüsse verstecken


Obwohl das Flussbett unter Wasser sein kann und tatsächlich Wasser durch es fließen kann, sollten wir dieses Wasser nicht sehen. Und noch mehr sollte es nicht auf der realen Wasseroberfläche gerendert werden. Wir können das Wasser von U-Boot-Flüssen loswerden, indem wir Flusssegmente nur hinzufügen, wenn sich die aktuelle Zelle nicht unter Wasser befindet.

voidTriangulateWithRiverBeginOrEnd (
		HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e
	) {
		…
		if (!cell.IsUnderwater) {
			bool reversed = cell.HasIncomingRiver;
			…
		}
	}
	voidTriangulateWithRiver (
		HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e
	) {
		…
		if (!cell.IsUnderwater) {
			bool reversed = cell.IncomingRiver == direction;
			…
		}
	}

Zu TriangulateConnectionBeginn werden wir einen Abschnitt des Flusses hinzufügen, wenn weder die Strömung noch die benachbarte Zelle unter Wasser sind.

if (cell.HasRiverThroughEdge(direction)) {
			e2.v3.y = neighbor.StreamBedY;
			if (!cell.IsUnderwater && !neighbor.IsUnderwater) {
				TriangulateRiverQuad(
					e1.v2, e1.v4, e2.v2, e2.v4,
					cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f,
					cell.HasIncomingRiver && cell.IncomingRiver == direction
				);
			}
		}


Keine Unterwasserflüsse mehr.

Wasserfälle


Es gibt keine Unterwasserflüsse mehr, aber jetzt haben wir Löcher in den Teilen der Flüsse, in denen sie auf die Wasseroberfläche treffen. Flüsse, die mit Wasser gespült werden, verursachen kleine Löcher oder Überlappungen. Am auffälligsten sind jedoch die fehlenden Wasserfälle für Flüsse aus größerer Höhe. Lass es uns zuerst tun.

Das Flusssegment mit einem Wasserfall führte durch die Wasseroberfläche. Infolgedessen war es teilweise über und teilweise unter Wasser. Wir müssen einen Teil über dem Wasserspiegel halten und alles andere wegwerfen. Wir müssen hart dafür arbeiten, also werden wir eine separate Methode erstellen.

Die neue Methode erfordert vier Gipfel, zwei Ebenen von Flüssen und Wasserstand. Wir werden es so aufstellen, dass wir in die Richtung der Strömung schauen, den Wasserfall hinunter. Daher befinden sich die ersten beiden Eckpunkte sowie die linke und die rechte Seite oben, gefolgt von den unteren.

voidTriangulateWaterfallInWater (
		Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4,
		float y1, float y2, float waterY
	) {
		v1.y = v2.y = y1;
		v3.y = v4.y = y2;
		rivers.AddQuad(v1, v2, v3, v4);
		rivers.AddQuadUV(0f, 1f, 0.8f, 1f);
	}

Nennen wir diese Methode TriangulateConnection, wenn ein Nachbar unter Wasser ist und wir einen Wasserfall erstellen.

if (!cell.IsUnderwater) {
				if (!neighbor.IsUnderwater) {
					TriangulateRiverQuad(
						e1.v2, e1.v4, e2.v2, e2.v4,
						cell.RiverSurfaceY, neighbor.RiverSurfaceY, 0.8f,
						cell.HasIncomingRiver && cell.IncomingRiver == direction
					);
				}
				elseif (cell.Elevation > neighbor.WaterLevel) {
					TriangulateWaterfallInWater(
						e1.v2, e1.v4, e2.v2, e2.v4,
						cell.RiverSurfaceY, neighbor.RiverSurfaceY,
						neighbor.WaterSurfaceY
					);
				}
			}

Wir müssen auch Wasserfälle in die entgegengesetzte Richtung verarbeiten, wenn sich die aktuelle Zelle unter Wasser befindet und die nächste Zelle nicht.

if (!cell.IsUnderwater) {
				…
			}
			elseif (
				!neighbor.IsUnderwater &&
				neighbor.Elevation > cell.WaterLevel
			) {
				TriangulateWaterfallInWater(
					e2.v4, e2.v2, e1.v4, e1.v2,
					neighbor.RiverSurfaceY, cell.RiverSurfaceY,
					cell.WaterSurfaceY
				);
			}

Also bekommen wir wieder das Quad des Quellflusses. Als nächstes müssen wir ändern, TriangulateWaterfallInWaterso dass es die unteren Spitzen auf den Wasserspiegel anhebt. Leider reicht es nicht aus, nur die Y-Koordinaten zu ändern. Dadurch kann der Wasserfall von der Klippe wegbewegt werden, wodurch sich Löcher bilden können. Stattdessen müssen Sie die unteren Eckpunkte mithilfe der Interpolation nach oben verschieben.


Wir interpolieren.

Um die unteren Gipfel nach oben zu bewegen, teilen Sie deren Abstand unter der Wasseroberfläche durch die Höhe des Wasserfalls. Dies gibt uns den Wert des Interpolators.

		v1.y = v2.y = y1;
		v3.y = v4.y = y2;
		float t = (waterY - y2) / (y1 - y2);
		v3 = Vector3.Lerp(v3, v1, t);
		v4 = Vector3.Lerp(v4, v2, t);
		rivers.AddQuad(v1, v2, v3, v4);
		rivers.AddQuadUV(0f, 1f, 0.8f, 1f);

Als Ergebnis erhalten wir einen verkürzten Wasserfall mit der gleichen Ausrichtung. Da sich jedoch die Positionen der unteren Scheitelpunkte geändert haben, werden sie nicht wie die ursprünglichen Scheitelpunkte verzerrt. Dies bedeutet, dass das Endergebnis immer noch nicht mit dem ursprünglichen Wasserfall übereinstimmt. Um dieses Problem zu lösen, müssen wir die Eckpunkte vor der Interpolation manuell verzerren und dann ein unverzerrtes Quad hinzufügen.

		v1.y = v2.y = y1;
		v3.y = v4.y = y2;
		v1 = HexMetrics.Perturb(v1);
		v2 = HexMetrics.Perturb(v2);
		v3 = HexMetrics.Perturb(v3);
		v4 = HexMetrics.Perturb(v4);
		float t = (waterY - y2) / (y1 - y2);
		v3 = Vector3.Lerp(v3, v1, t);
		v4 = Vector3.Lerp(v4, v2, t);
		rivers.AddQuadUnperturbed(v1, v2, v3, v4);
		rivers.AddQuadUV(0f, 1f, 0.8f, 1f);

Da wir bereits eine Methode zum Hinzufügen unverzerrter Dreiecke haben, müssen wir sie nicht unbedingt für Quad-S erstellen. Daher fügen wir die erforderliche Methode hinzu HexMesh.AddQuadUnperturbed.

publicvoidAddQuadUnperturbed (
		Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4
	) {
		int vertexIndex = vertices.Count;
		vertices.Add(v1);
		vertices.Add(v2);
		vertices.Add(v3);
		vertices.Add(v4);
		triangles.Add(vertexIndex);
		triangles.Add(vertexIndex + 2);
		triangles.Add(vertexIndex + 1);
		triangles.Add(vertexIndex + 1);
		triangles.Add(vertexIndex + 2);
		triangles.Add(vertexIndex + 3);
	}


Wasserfälle enden an der Wasseroberfläche.

Einheitspaket

Mündung


Wenn Flüsse auf der gleichen Höhe wie die Wasseroberfläche fließen, berührt die Masche des Flusses die Masche der Küste. Wenn es ein Fluss wäre, der ins Meer oder in den Ozean fließt, dann gäbe es eine Strömung des Flusses mit einer Brandung. Daher werden wir diese Bereiche Münder nennen.


Der Fluss trifft auf die Küste, ohne die Gipfel zu verzerren.

Jetzt haben wir zwei Probleme mit dem Mund. Erstens verbinden sich Quad-Flüsse mit der zweiten und vierten Spitze der Rippen und passieren die dritte. Da die Küste des Wassers den dritten Peak nicht nutzt, kann es zu einem Loch oder einer Überlappung kommen. Wir können dieses Problem lösen, indem wir die Mundgeometrie ändern.

Das zweite Problem ist, dass es einen scharfen Übergang zwischen Schaum und Flussmaterial gibt. Um das Problem zu lösen, benötigen wir ein anderes Material, das die Auswirkungen des Flusses und des Wassers mischt.

Dies bedeutet, dass die Münder einen speziellen Ansatz erfordern. Lassen Sie uns daher eine separate Methode für sie erstellen. Es sollte aufgerufen werden, TriangulateWaterShorewenn sich ein Fluss in die aktuelle Richtung bewegt.

voidTriangulateWaterShore (
		HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center
	) {
		…
		if (cell.HasRiverThroughEdge(direction)) {
			TriangulateEstuary(e1, e2);
		}
		else {
			waterShore.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2);
			waterShore.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3);
			waterShore.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4);
			waterShore.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5);
			waterShore.AddQuadUV(0f, 0f, 0f, 1f);
			waterShore.AddQuadUV(0f, 0f, 0f, 1f);
			waterShore.AddQuadUV(0f, 0f, 0f, 1f);
			waterShore.AddQuadUV(0f, 0f, 0f, 1f);
		}
		…
	}
	voidTriangulateEstuary (EdgeVertices e1, EdgeVertices e2) {
	}

Eine Region, in der beide Effekte gemischt werden, muss nicht das gesamte Band ausfüllen. Die Form eines Trapezes reicht uns. Daher können wir zwei Küstendreiecke an den Seiten verwenden.

voidTriangulateEstuary (EdgeVertices e1, EdgeVertices e2) {
		waterShore.AddTriangle(e2.v1, e1.v2, e1.v1);
		waterShore.AddTriangle(e2.v5, e1.v5, e1.v4);
		waterShore.AddTriangleUV(
			new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f)
		);
		waterShore.AddTriangleUV(
			new Vector2(0f, 1f), new Vector2(0f, 0f), new Vector2(0f, 0f)
		);
	}


Trapezloch für den Mischbereich.

UV2-Koordinaten


Um den Effekt des Flusses zu erzeugen, benötigen wir UV-Koordinaten. Aber um einen Schaumeffekt zu erzeugen, brauchen wir auch UV-Koordinaten. Das heißt, wenn wir sie mischen, benötigen wir zwei Sätze von UV-Koordinaten. Glücklicherweise können Unity-Engine-Maschen bis zu vier UV-Sätze unterstützen. Wir müssen nur HexMeshden zweiten Satz unterstützen.

publicbool useCollider, useColors, useUVCoordinates, useUV2Coordinates;
	[NonSerialized] List<Vector2> uvs, uv2s;
	publicvoidClear () {
		…
		if (useUVCoordinates) {
			uvs = ListPool<Vector2>.Get();
		}
		if (useUV2Coordinates) {
			uv2s = ListPool<Vector2>.Get();
		}
		triangles = ListPool<int>.Get();
	}
	publicvoidApply () {
		…
		if (useUVCoordinates) {
			hexMesh.SetUVs(0, uvs);
			ListPool<Vector2>.Add(uvs);
		}
		if (useUV2Coordinates) {
			hexMesh.SetUVs(1, uv2s);
			ListPool<Vector2>.Add(uv2s);
		}
		…
	}

Um einen zweiten UV-Satz hinzuzufügen, duplizieren wir die Methoden zum Arbeiten mit UV und ändern die Art und Weise, die wir benötigen.

publicvoidAddTriangleUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3) {
		uv2s.Add(uv1);
		uv2s.Add(uv2);
		uv2s.Add(uv3);
	}
	publicvoidAddQuadUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3, Vector3 uv4) {
		uv2s.Add(uv1);
		uv2s.Add(uv2);
		uv2s.Add(uv3);
		uv2s.Add(uv4);
	}
	publicvoidAddQuadUV2 (float uMin, float uMax, float vMin, float vMax) {
		uv2s.Add(new Vector2(uMin, vMin));
		uv2s.Add(new Vector2(uMax, vMin));
		uv2s.Add(new Vector2(uMin, vMax));
		uv2s.Add(new Vector2(uMax, vMax));
	}

River Shader-Funktion


Da wir den River-Effekt in zwei Shadern verwenden werden, verschieben Sie den Code aus dem River- Shader in die neue Funktion der Include-Datei Water .

float River (float2 riverUV, sampler2D noiseTex) {
	float2 uv = riverUV;
	uv.x = uv.x * 0.0625 + _Time.y * 0.005;
	uv.y -= _Time.y * 0.25;
	float4 noise = tex2D(noiseTex, uv);
	float2 uv2 = riverUV;
	uv2.x = uv2.x * 0.0625 - _Time.y * 0.0052;
	uv2.y -= _Time.y * 0.23;
	float4 noise2 = tex2D(noiseTex, uv2);
	return noise.x * noise2.w;
}

Ändern Sie den River Shader , um diese neue Funktion zu verwenden.

		#include "Water.cginc"
		sampler2D _MainTex;
		…
		void surf (InputIN, inout SurfaceOutputStandard o) {
			float river = River(IN.uv_MainTex, _MainTex);
			fixed4 c = saturate(_Color + river);
			…
		}

Objekte Mund


Zur HexGridChunkUnterstützung des Mundobjektnetzes hinzufügen.

public HexMesh terrain, rivers, roads, water, waterShore, estuaries;
	publicvoidTriangulate () {
		terrain.Clear();
		rivers.Clear();
		roads.Clear();
		water.Clear();
		waterShore.Clear();
		estuaries.Clear();
		for (int i = 0; i < cells.Length; i++) {
			Triangulate(cells[i]);
		}
		terrain.Apply();
		rivers.Apply();
		roads.Apply();
		water.Apply();
		waterShore.Apply();
		estuaries.Apply();
	}

Erstellen Sie einen Shader-, Material- und Objektmund, duplizieren Sie die Küste und ändern Sie sie. Verbinden Sie es mit dem Fragment und verwenden Sie die UV- und UV2-Koordinaten.


Estuarties Objekt.

Mündungs-Triangulation


Wir können das Problem eines Lochs oder einer Überlagerung lösen, indem wir ein Dreieck zwischen dem Ende des Flusses und dem mittleren Rand des Wassers platzieren. Da unser Mund-Shader ein Duplikat des Küsten-Shaders ist, stellen wir die UV-Koordinaten ein, die dem Schaum-Effekt entsprechen.

voidTriangulateEstuary (EdgeVertices e1, EdgeVertices e2) {
		…
		estuaries.AddTriangle(e1.v3, e2.v2, e2.v4);
		estuaries.AddTriangleUV(
			new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 1f)
		);
	}


Mittleres Dreieck.

Wir können das gesamte Trapez füllen, indem wir auf beiden Seiten des mittleren Dreiecks ein Viereck einfügen.

		estuaries.AddQuad(e1.v2, e1.v3, e2.v1, e2.v2);
		estuaries.AddTriangle(e1.v3, e2.v2, e2.v4);
		estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5);
		estuaries.AddQuadUV(0f, 0f, 0f, 1f);
		estuaries.AddTriangleUV(
			new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, 1f)
		);
		estuaries.AddQuadUV(0f, 0f, 0f, 1f);


Bereit Trapez.

Drehen wir die Ausrichtung des Vierecks nach links, so dass es eine verkürzte diagonale Verbindung hat und als Ergebnis eine symmetrische Geometrie erhalten.

		estuaries.AddQuad(e2.v1, e1.v2, e2.v2, e1.v3);
		estuaries.AddTriangle(e1.v3, e2.v2, e2.v4);
		estuaries.AddQuad(e1.v3, e1.v4, e2.v4, e2.v5);
		estuaries.AddQuadUV(
			new Vector2(0f, 1f), new Vector2(0f, 0f),
			new Vector2(0f, 1f), new Vector2(0f, 0f)
		);
//		estuaries.AddQuadUV(0f, 0f, 0f, 1f);


Gedrehte vierfache, symmetrische Geometrie

Fluss fließen


Um die Wirkung des Flusses zu unterstützen, müssen wir UV2-Koordinaten hinzufügen. Der Boden des mittleren Dreiecks befindet sich in der Mitte des Flusses, daher sollte seine U-Koordinate gleich 0,5 sein. Wenn der Fluss in Richtung Wasser fließt, erhält der linke Punkt die U-Koordinate gleich 1 und der rechte die U-Koordinate mit dem Wert 0. Die Y-Koordinaten seien 0 und 1, entsprechend der Flussrichtung.

		estuaries.AddTriangleUV2(
			new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f)
		);

Die Vierecke auf beiden Seiten des Dreiecks müssen mit dieser Ausrichtung übereinstimmen. Wir behalten die gleichen U-Koordinaten für Punkte bei, die die Breite des Flusses überschreiten.

		estuaries.AddQuadUV2(
			new Vector2(1f, 0f), new Vector2(1f, 1f),
			new Vector2(1f, 0f), new Vector2(0.5f, 1f)
		);
		estuaries.AddTriangleUV2(
			new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f)
		);
		estuaries.AddQuadUV2(
			new Vector2(0.5f, 1f), new Vector2(0f, 1f),
			new Vector2(0f, 0f), new Vector2(0f, 0f)
		);


UV2-Trapez.

Um sicherzustellen, dass die UV2-Koordinaten korrekt eingestellt sind, rendern sie mit dem Shader " Mündung" . Wir können auf diese Koordinaten zugreifen, indem wir sie zur Eingabestruktur hinzufügen float2 uv2_MainTex.

		struct Input {
			float2 uv_MainTex;
			float2 uv2_MainTex;
			float3 worldPos;
		};
		…
		void surf (InputIN, inout SurfaceOutputStandard o) {
			float shore = IN.uv_MainTex.y;
			float foam = Foam(shore, IN.worldPos.xz, _MainTex);
			float waves = Waves(IN.worldPos.xz, _MainTex);
			waves *= 1 - shore;
			fixed4 c = fixed4(IN.uv2_MainTex, 1, 1);
			…
		}


UV2-Koordinaten.

Alles sieht gut aus, Sie können einen Shader verwenden, um einen Flusseffekt zu erzeugen.

void surf (InputIN, inout SurfaceOutputStandard o) {
			…
			float river = River(IN.uv2_MainTex, _MainTex);
			fixed4 c = saturate(_Color + river);
			…
		}


Verwenden Sie UV2, um einen Flusseffekt zu erzielen.

Wir haben Flüsse so angelegt, dass sich beim Triangulieren der Verbindungen zwischen den Zellen die V-Koordinaten des Flusses von 0,8 auf 1 ändern. Daher sollten wir hier auch dieses Intervall und nicht Werte von 0 auf 1 verwenden. Die Küstenverbindung ist jedoch 50% größer als normale Zellenverbindungen . Für eine optimale Anpassung an den Flussverlauf müssen wir daher die Werte von 0,8 auf 1,1 ändern.

		estuaries.AddQuadUV2(
			new Vector2(1f, 0.8f), new Vector2(1f, 1.1f),
			new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f)
		);
		estuaries.AddTriangleUV2(
			new Vector2(0.5f, 1.1f),
			new Vector2(1f, 0.8f),
			new Vector2(0f, 0.8f)
		);
		estuaries.AddQuadUV2(
			new Vector2(0.5f, 1.1f), new Vector2(0f, 1.1f),
			new Vector2(0f, 0.8f), new Vector2(0f, 0.8f)
		);



Synchronisierter Fluss und Mündung.

Flusskontrolle


Während sich die Strömung des Flusses in einer geraden Linie bewegt. Aber wenn Wasser in ein größeres Gebiet fließt, dehnt es sich aus. Die Strömung wird gebogen. Wir können dies simulieren, indem wir die UV2-Koordinaten falten.

Anstatt die oberen Koordinaten U über die Flussbreite hinaus konstant zu halten, verschieben wir sie um 0,5. Der Punkt ganz links ist auf 1,5 eingestellt, der Punkt ganz rechts ist -0,5.

Gleichzeitig erweitern wir den Fluss, indem wir die Koordinaten U des linken und rechten unteren Punkts verschieben. Ändern Sie die linke von 1 auf 0,7 und die rechte von 0 auf 0,3.

		estuaries.AddQuadUV2(
			new Vector2(1.5f, 0.8f), new Vector2(0.7f, 1.1f),
			new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f)
		);
		…
		estuaries.AddQuadUV2(
			new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.1f),
			new Vector2(0f, 0.8f), new Vector2(-0.5f, 0.8f)
		);



Die Ausdehnung des Flusses.

Ändern Sie die V-Koordinaten derselben vier Punkte, um den Krümmungseffekt zu vervollständigen. Wenn das Wasser vom Ende des Flusses wegfließt, erhöhen wir die V-Koordinaten der oberen Punkte auf 1. Um eine bessere Kurve zu erzielen, erhöhen wir die V-Koordinaten der beiden unteren Punkte auf 1,15.

		estuaries.AddQuadUV2(
			new Vector2(1.5f, 1f), new Vector2(0.7f, 1.15f),
			new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f)
		);
		estuaries.AddTriangleUV2(
			new Vector2(0.5f, 1.1f),
			new Vector2(1f, 0.8f),
			new Vector2(0f, 0.8f)
		);
		estuaries.AddQuadUV2(
			new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.15f),
			new Vector2(0f, 0.8f), new Vector2(-0.5f, 1f)
		);



Gebogener Flussfluss.

Mischen von Fluss und Küste


Wir haben nur noch die Auswirkungen der Küste und des Flusses zu mischen. Dazu verwenden wir die lineare Interpolation, wobei der Wert der Küste als Interpolator verwendet wird.

			float shoreWater = max(foam, waves);
			float river = River(IN.uv2_MainTex, _MainTex);
			float water = lerp(shoreWater, river, IN.uv_MainTex.x);
			fixed4 c = saturate(_Color + water);

Obwohl dies funktionieren sollte, wird möglicherweise ein Kompilierungsfehler angezeigt. Der Compiler beschwert sich über das Überschreiben _MainTex_ST. Der Grund ist ein Fehler im Unity Surface Shader-Compiler, der durch die Verwendung von uv_MainTexund gleichzeitig verursacht wurde uv2_MainTex. Wir müssen einen Workaround finden.

Anstatt zu verwenden, müssen uv2_MainTexwir die sekundären UV-Koordinaten manuell übertragen. Um dies zu tun, umbenennen uv2_MainTexin riverUV. Dann fügen wir dem Shader eine Vertex-Funktion hinzu, die ihm Koordinaten zuweist.

		#pragma surface surf Standard alpha vertex:vert
		…
		struct Input {
			float2 uv_MainTex;
			float2 riverUV;
			float3 worldPos;
		};
		…
		void vert (inout appdata_full v, outInput o) {
			UNITY_INITIALIZE_OUTPUT(Input, o);
			o.riverUV = v.texcoord1.xy;
		}
		void surf (InputIN, inout SurfaceOutputStandard o) {
			…
			float river = River(IN.riverUV, _MainTex);
			…
		}


Interpolation basierend auf dem Wert der Küste.

Die Interpolation funktioniert mit Ausnahme des linken und rechten Scheitelpunkts oben. An diesen Punkten sollte der Fluss verschwinden. Daher können wir den Wert der Küste nicht nutzen. Wir müssen einen anderen Wert verwenden, der in diesen beiden Eckpunkten 0 ist. Glücklicherweise haben wir immer noch die U-Koordinate des ersten Satzes von UVs, sodass wir diesen Wert dort speichern können.

		estuaries.AddQuadUV(
			new Vector2(0f, 1f), new Vector2(0f, 0f),
			new Vector2(1f, 1f), new Vector2(0f, 0f)
		);
		estuaries.AddTriangleUV(
			new Vector2(0f, 0f), new Vector2(1f, 1f), new Vector2(1f, 1f)
		);
		estuaries.AddQuadUV(
			new Vector2(0f, 0f), new Vector2(0f, 0f),
			new Vector2(1f, 1f), new Vector2(0f, 1f)
		);
//		estuaries.AddQuadUV(0f, 0f, 0f, 1f);


Richtiges Mischen.

Jetzt haben die Mündungen eine gute Mischung zwischen dem sich ausdehnenden Fluss, dem Küstenwasser und dem Schaum. Obwohl es keine exakte Übereinstimmung mit Wasserfällen schafft, sieht dieser Effekt bei Wasserfällen gut aus.


Mund in Aktion

Einheitspaket

Flüsse fließen aus Stauseen


Wir haben bereits Flüsse, die in Stauseen fließen, aber es gibt keine Unterstützung für Flüsse, die in eine andere Richtung fließen. Es gibt Seen, aus denen Flüsse fließen, also müssen wir sie auch hinzufügen.

Wenn ein Fluss aus einem Stausee fließt, fließt er tatsächlich in eine größere Höhe. Dies ist derzeit nicht möglich. Wir müssen eine Ausnahme machen und diese Situation zulassen, wenn der Wasserstand der Höhe des Zielpunkts entspricht. Fügen wir der HexCellprivaten Methode hinzu und prüfen anhand unseres neuen Kriteriums, ob der Nachbar der richtige Zielpunkt für den abfließenden Fluss ist.

boolIsValidRiverDestination (HexCell neighbor) {
		return neighbor && (
			elevation >= neighbor.elevation || waterLevel == neighbor.elevation
		);
	}

Wir werden unsere neue Methode anwenden, um festzustellen, ob es möglich ist, einen abgehenden Fluss zu erzeugen.

publicvoidSetOutgoingRiver (HexDirection direction) {
		if (hasOutgoingRiver && outgoingRiver == direction) {
			return;
		}
		HexCell neighbor = GetNeighbor(direction);
//		if (!neighbor || elevation < neighbor.elevation) {if (!IsValidRiverDestination(neighbor)) {
			return;
		}
		RemoveOutgoingRiver();
		…
	}

Auch dort müssen Sie den Fluss überprüfen, wenn sich die Höhe der Zelle oder der Wasserstand ändert. Erstellen Sie eine private Methode, die diese Aufgabe übernimmt.

voidValidateRivers () {
		if (
			hasOutgoingRiver &&
			!IsValidRiverDestination(GetNeighbor(outgoingRiver))
		) {
			RemoveOutgoingRiver();
		}
		if (
			hasIncomingRiver &&
			!GetNeighbor(incomingRiver).IsValidRiverDestination(this)
		) {
			RemoveIncomingRiver();
		}
	}

Wir verwenden diese neue Methode in den Eigenschaften Elevationund WaterLevel.

publicint Elevation {
		…
		set {
			…
//			if (//				hasOutgoingRiver &&//				elevation < GetNeighbor(outgoingRiver).elevation//			) {//				RemoveOutgoingRiver();//			}//			if (//				hasIncomingRiver &&//				elevation > GetNeighbor(incomingRiver).elevation//			) {//				RemoveIncomingRiver();//			}
			ValidateRivers();
			…
		}
	}
	publicint WaterLevel {
		…
		set {
			if (waterLevel == value) {
				return;
			}
			waterLevel = value;
			ValidateRivers();
			Refresh();
		}
	}


Ausgehend und betretend die Seen des Flusses.

Flow entfalten


Wir HexGridChunk.TriangulateEstuarygingen davon aus, dass Flüsse nur in Stauseen fließen können. Daher bewegt sich der Fluss eines Flusses immer in eine Richtung. Wir müssen den Fluss umkehren, wenn wir es mit einem Fluss zu tun haben, der aus einem Stausee fließt. Dazu muss man TriangulateEstuarydie Strömungsrichtung kennen. Daher geben wir ihm einen booleschen Parameter, der festlegt, ob es sich um einen eingehenden Fluss handelt.

voidTriangulateEstuary (
		EdgeVertices e1, EdgeVertices e2, bool incomingRiver
	) {
	…
}

Wir werden diese Information übermitteln, wenn wir diese Methode von aufrufen TriangulateWaterShore.

if (cell.HasRiverThroughEdge(direction)) {
			TriangulateEstuary(e1, e2, cell.IncomingRiver == direction);
		}

Jetzt müssen wir den Fluss des Flusses drehen und die Koordinaten von UV2 ändern. Die Koordinaten von U für abgehende Flüsse müssen gespiegelt werden: -0,5 wird zu 1,5, 0 wird zu 1, 1 wird zu 0 und 1,5 wird zu -0,5.

Bei V-Koordinaten sind die Dinge etwas komplizierter. Wenn Sie sich ansehen, wie wir mit invertierten Verbindungen von Flüssen gearbeitet haben, sollte 0.8 0 und 1 −0.2 sein. Dies bedeutet, dass 1.1 zu -0.3 und 1.15 zu -0.35 wird.

Da die Koordinaten von UV2 in jedem Fall sehr unterschiedlich sind, schreiben wir einen separaten Code für sie.

voidTriangulateEstuary (
		EdgeVertices e1, EdgeVertices e2, bool incomingRiver
	) {
		…
		if (incomingRiver) {
			estuaries.AddQuadUV2(
				new Vector2(1.5f, 1f), new Vector2(0.7f, 1.15f),
				new Vector2(1f, 0.8f), new Vector2(0.5f, 1.1f)
			);
			estuaries.AddTriangleUV2(
				new Vector2(0.5f, 1.1f),
				new Vector2(1f, 0.8f),
				new Vector2(0f, 0.8f)
			);
			estuaries.AddQuadUV2(
				new Vector2(0.5f, 1.1f), new Vector2(0.3f, 1.15f),
				new Vector2(0f, 0.8f), new Vector2(-0.5f, 1f)
			);
		}
		else {
			estuaries.AddQuadUV2(
				new Vector2(-0.5f, -0.2f), new Vector2(0.3f, -0.35f),
				new Vector2(0f, 0f), new Vector2(0.5f, -0.3f)
			);
			estuaries.AddTriangleUV2(
				new Vector2(0.5f, -0.3f),
				new Vector2(0f, 0f),
				new Vector2(1f, 0f)
			);
			estuaries.AddQuadUV2(
				new Vector2(0.5f, -0.3f), new Vector2(0.7f, -0.35f),
				new Vector2(1f, 0f), new Vector2(1.5f, -0.2f)
			);
		}
	}


Der richtige Fluss der Flüsse.

Einheitspaket

Teil 9: Reliefobjekte


  • Fügen Sie dem Relief Objekte hinzu.
  • Erstellen Sie Unterstützung für Dichtestufen von Objekten.
  • Wir benutzen verschiedene Objekte im Level.
  • Wir mischen drei verschiedene Arten von Objekten.

In diesem Teil werden wir über das Hinzufügen von Objekten zum Relief sprechen. Wir werden Objekte wie Gebäude und Bäume erstellen.


Konflikt zwischen Wäldern, Ackerland und Urbanisierung.

Unterstützung für Objekte hinzufügen


Obwohl die Form des Reliefs Variationen aufweist, passiert nichts darauf. Es ist ein lebloses Land. Um ihm Leben einzuhauchen, müssen Sie solche Objekte hinzufügen. wie Bäume und Häuser. Diese Objekte sind nicht Teil des Reliefnetzes, sondern separate Objekte. Dies hindert uns jedoch nicht daran, sie beim Triangulieren des Geländes hinzuzufügen.

HexGridChunkEs ist mir egal, wie das Netz funktioniert. Er befiehlt einfach einem seiner Kinder, HexMeshein Dreieck oder ein Viereck hinzuzufügen. Ebenso kann es ein untergeordnetes Element haben, das Objekte darauf platziert.

Objekt-Manager


Lassen Sie uns eine Komponente erstellen HexFeatureManager, die Objekte innerhalb eines Fragments behandelt. Wir verwenden das gleiche Schema , das und in HexMesh- ihm Methoden geben Clear, Applyund AddFeature. Da das Objekt irgendwo platziert werden muss, AddFeatureerhält die Methode den Positionsparameter.

Wir werden mit der Implementierungsbeschaffung beginnen, die bisher nichts bewirken wird.

using UnityEngine;
publicclassHexFeatureManager : MonoBehaviour {
	publicvoidClear () {}
	publicvoidApply () {}
	publicvoidAddFeature (Vector3 position) {}
}

Jetzt können wir einen Link zu einer solchen Komponente in hinzufügen HexGridChunk. Dann können Sie es sowie alle untergeordneten Elemente in den Triangulationsprozess einbeziehenHexMesh .

public HexFeatureManager features;
	publicvoidTriangulate () {
		terrain.Clear();
		rivers.Clear();
		roads.Clear();
		water.Clear();
		waterShore.Clear();
		estuaries.Clear();
		features.Clear();
		for (int i = 0; i < cells.Length; i++) {
			Triangulate(cells[i]);
		}
		terrain.Apply();
		rivers.Apply();
		roads.Apply();
		water.Apply();
		waterShore.Apply();
		estuaries.Apply();
		features.Apply();
	}

Beginnen wir mit dem Platzieren eines Objekts in der Mitte jeder Zelle.

voidTriangulate (HexCell cell) {
		for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) {
			Triangulate(d, cell);
		}
		features.AddFeature(cell.Position);
	}

Jetzt brauchen wir einen echten Objektmanager. Fügen Sie dem Hex Grid Chunk- Fertighaus ein weiteres Kind hinzu und geben Sie ihm eine Komponente HexFeatureManager. Dann können Sie ein Fragment daran anschließen.




Objektmanager zu Fragment-Fertighaus hinzugefügt.

Fertige Objekte


Welches Geländeobjekt werden wir erstellen? Für den ersten Test ist der Würfel durchaus geeignet. Erstellen Sie beispielsweise einen ausreichend großen Würfel mit einer Skala (3, 3, 3) und verwandeln Sie ihn in einen Fertigbau. Erstellen Sie auch ein Material dafür. Ich habe das Standardmaterial mit roter Farbe verwendet. Entfernen Sie es aus dem Collider, da wir es nicht brauchen.


Fertighauswürfel.

Objektmanager benötigen einen Link zu diesem Fertighaus. Fügen Sie diesen hinzu HexFeatureManagerund verbinden Sie sie anschließend. Da das Platzieren eines Objekts den Zugriff auf die Transformationskomponente erfordert, verwenden wir es als Referenztyp.

public Transform featurePrefab;


Objektmanager mit Fertigteil.

Instanzen von Objekten erstellen


Die Struktur ist fertig und wir können beginnen, Reliefobjekte hinzuzufügen! Erstellen Sie einfach eine Instanz des Fertighauses HexFeatureManager.AddFeatureund legen Sie dessen Position fest.

publicvoidAddFeature (Vector3 position) {
		Transform instance = Instantiate(featurePrefab);
		instance.localPosition = position;
	}


Instanzen von Reliefobjekten.

Ab diesem Zeitpunkt wird das Relief mit Würfeln gefüllt. Zumindest die oberen Hälften der Würfel, da der lokale Ursprungspunkt für das Würfelnetz in Unity in der Mitte des Würfels liegt und der untere Teil unter der Oberfläche des Reliefs liegt. Um die Würfel auf der Topographie zu platzieren, müssen wir sie auf die Hälfte ihrer Höhe bewegen.

publicvoidAddFeature (Vector3 position) {
		Transform instance = Instantiate(featurePrefab);
		position.y += instance.localScale.y * 0.5f;
		instance.localPosition = position;
	}


Würfel auf der Oberfläche des Reliefs.

Was ist, wenn wir ein anderes Netz verwenden?
Такой подход применим только для стандартного куба. Если мы используем собственные меши, то лучше будет создавать их так, чтобы их локальная точка начала координат находилась внизу. Тогда не придётся вообще изменять их позицию.

Natürlich sind unsere Zellen verzerrt, also müssen wir die Position von Objekten verzerren. So werden wir die perfekte Wiederholbarkeit des Gitters los.

		instance.localPosition = HexMetrics.Perturb(position);


Verzerrte Positionen von Objekten.

Zerstörung von Reliefobjekten


Mit jeder Aktualisierung des Fragments erstellen wir neue Reliefobjekte. Dies bedeutet, dass wir immer mehr Objekte an denselben Positionen erstellen. Um Duplikate zu vermeiden, müssen wir beim Aufräumen eines Fragments alte Objekte entfernen.

Der schnellste Weg, dies zu tun, besteht darin, ein Containerspielobjekt zu erstellen und alle Reliefobjekte in seine untergeordneten Objekte umzuwandeln. Wenn Clearwir dann anrufen, werden wir diesen Container zerstören und einen neuen erstellen. Der Container selbst wird ein Kind seines Managers sein.

	Transform container;
	publicvoidClear () {
		if (container) {
			Destroy(container.gameObject);
		}
		container = new GameObject("Features Container").transform;
		container.SetParent(transform, false);
	}
	…
	publicvoidAddFeature (Vector3 position) {
		Transform instance = Instantiate(featurePrefab);
		position.y += instance.localScale.y * 0.5f;
		instance.localPosition = HexMetrics.Perturb(position);
		instance.SetParent(container, false);
	}

Wahrscheinlich jedes Mal ineffizient, um Reliefobjekte zu erschaffen und zu zerstören.
Да, кажется, что это так. Но пока нас это волновать не должно. Сначала нам нужно правильно разместить объекты. Разобравшись с этим, мы увидим, что такие действия являются узким местом, поэтому подумаем об эффективности. Именно тогда мы можем прийти и к использованию метода HexFeatureManager.Apply. Но оставим это для будущего туториала. К счастью, всё не так плохо, потому что мы разделили рельеф на фрагменты.

Einheitspaket

Platzierung von Reliefobjekten


Im Moment platzieren wir Objekte in der Mitte jeder Zelle. Für leere Zellen sieht das normal aus, aber auf Zellen mit Flüssen und Straßen sowie mit Wasser überflutet scheint es seltsam.


Objekte befinden sich überall.

Überprüfen Sie daher vor dem Platzieren des Objekts, HexGridChunk.Triangulateob die Zelle leer ist.

if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) {
			features.AddFeature(cell.Position);
		}


Begrenzte Unterkunft.

Ein Objekt pro Richtung


Nur ein Objekt pro Zelle ist nicht zu viel. Es gibt viel Platz für jede Menge Gegenstände. Daher fügen wir ein zusätzliches Objekt in die Mitte jedes der sechs Dreiecke der Zelle ein, dh eines pro Richtung.

Wir werden dies in einer anderen Methode tun, Triangulatewenn wir wissen, dass sich kein Fluss in der Zelle befindet. Wir müssen noch prüfen, ob wir unter Wasser sind und ob es eine Straße in der Zelle gibt. In diesem Fall interessieren uns jedoch nur die Straßen, die in die aktuelle Richtung führen.

voidTriangulate (HexDirection direction, HexCell cell) {
		…
		if (cell.HasRiver) {
			…
		}
		else {
			TriangulateWithoutRiver(direction, cell, center, e);
			if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) {
				features.AddFeature((center + e.v1 + e.v5) * (1f / 3f));
			}
		}
		…
	}


Viele Einrichtungen, aber nicht in der Nähe der Flüsse.

Dies schafft viel mehr Objekte! Sie erscheinen neben den Straßen, aber sie meiden immer noch die Flüsse. Um Objekte entlang der Flüsse zu platzieren, können wir sie auch hinzufügen TriangulateAdjacentToRiver. Aber auch hier nur, wenn das Dreieck nicht unter Wasser ist und keine Straße auf ihm ist.

voidTriangulateAdjacentToRiver (
		HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e
	) {
		…
		if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) {
			features.AddFeature((center + e.v1 + e.v5) * (1f / 3f));
		}
	}


Gegenstände tauchten neben den Flüssen auf.

Können so viele Objekte gerendert werden?
Большое количество объектов создаёт множество вызовов отрисовки, но здесь помогает dynamic batching движка Unity. Так как объекты малы, их меши должны иметь всего несколько вершин. Это позволяет объединить многие из них в один batch. Но если это окажется «узким местом», то придётся поработать с ними в будущем. Также можно использовать instancing, который при работе со множеством мелких мешей сравним с dynamic batching.

Einheitspaket

Vielzahl von Objekten


Alle unsere Reliefobjekte haben die gleiche Ausrichtung, was völlig unnatürlich aussieht. Lassen Sie uns jedem von ihnen eine zufällige Wendung geben.

publicvoidAddFeature (Vector3 position) {
		Transform instance = Instantiate(featurePrefab);
		position.y += instance.localScale.y * 0.5f;
		instance.localPosition = HexMetrics.Perturb(position);
		instance.localRotation = Quaternion.Euler(0f, 360f * Random.value, 0f);
		instance.SetParent(container, false);
	}


Zufällige Runden.

Das Ergebnis wird also viel vielfältiger. Leider erhalten Objekte bei jeder Aktualisierung eines Fragments eine neue zufällige Rotation. Das Bearbeiten von Zellen sollte die Objekte in der Nachbarschaft nicht verändern, daher benötigen wir einen anderen Ansatz.

Wir haben eine Geräuschstruktur, die immer gleich ist. Diese Textur enthält jedoch Perlins Gradientenrauschen und ist lokal abgestimmt. Das ist es, was wir brauchen, wenn wir die Positionen der Eckpunkte in den Zellen verzerren. Die Kurven müssen aber nicht koordiniert werden. Alle Windungen sollten gleich wahrscheinlich und gemischt sein. Aus diesem Grund benötigen wir eine Textur mit Zufallswerten, die keine Gradienten aufweisen und ohne bilineare Filterung abgetastet werden können. Im Wesentlichen handelt es sich hierbei um ein Hash-Gitter, das die Grundlage für Gradientenrauschen bildet.

Erstellen einer Hash-Tabelle


Wir können eine Hash-Tabelle aus einem Array von Float-Werten erstellen und diese einmal mit zufälligen Werten füllen. Aus diesem Grund brauchen wir überhaupt keine Textur. Fügen wir es hinzu HexMetrics. Eine Größe von 256 mal 256 reicht für eine ausreichende Variabilität aus.

publicconstint hashGridSize = 256;
	staticfloat[] hashGrid;
	publicstaticvoidInitializeHashGrid () {
		hashGrid = newfloat[hashGridSize * hashGridSize];
		for (int i = 0; i < hashGrid.Length; i++) {
			hashGrid[i] = Random.value;
		}
	}

Zufällige Werte werden durch eine mathematische Formel generiert, die immer die gleichen Ergebnisse liefert. Die resultierende Sequenz hängt von der Anzahl der Startwerte ab, die standardmäßig dem aktuellen Zeitwert entspricht. Deshalb erhalten wir in jeder Spielsitzung unterschiedliche Ergebnisse.

Um die Neuerstellung immer identischer Objekte sicherzustellen, müssen wir der Initialisierungsmethode den Seed-Parameter hinzufügen.

publicstaticvoidInitializeHashGrid (int seed) {
		hashGrid = newfloat[hashGridSize * hashGridSize];
		Random.InitState(seed);
		for (int i = 0; i < hashGrid.Length; i++) {
			hashGrid[i] = Random.value;
		}
	}

Nachdem wir den Zufallszahlenstrom initialisiert haben, erhalten wir immer die gleiche Reihenfolge. Daher sind scheinbar zufällige Ereignisse, die nach der Kartengenerierung auftreten, auch immer dieselben. Wir können dies vermeiden, indem wir den Zustand des Zufallszahlengenerators beibehalten, bevor er initialisiert wird. Nach Abschluss der Arbeiten können wir ihn nach dem alten Zustand fragen.

		Random.State currentState = Random.state;
		Random.InitState(seed);
		for (int i = 0; i < hashGrid.Length; i++) {
			hashGrid[i] = Random.value;
		}
		Random.state = currentState;

Die Hash-Tabelle wird HexGridgleichzeitig mit der Zuweisung einer Rauschtextur initialisiert . Das heißt, in den Methoden HexGrid.Startund HexGrid.Awake. Lassen Sie es uns so machen, dass Werte nicht öfter als nötig generiert werden.

publicint seed;
	voidAwake () {
		HexMetrics.noiseSource = noiseSource;
		HexMetrics.InitializeHashGrid(seed);
		…
	}
	voidOnEnable () {
		if (!HexMetrics.noiseSource) {
			HexMetrics.noiseSource = noiseSource;
			HexMetrics.InitializeHashGrid(seed);
		}
	}

Die gemeinsame Startvariable ermöglicht es uns, den Startwert für die Karte zu wählen. Jeder Wert wird ausreichen. Ich habe 1234 gewählt.


Auswahl des Samens.

Hash-Tabelle verwenden


Fügen Sie zur Verwendung der Hash-Tabelle die HexMetricsStichprobenmethode hinzu. Wie es SampleNoiseverwendet es die xz-Position, um den Wert zu erhalten. Der Hash-Index wird gefunden, indem die Koordinaten auf ganzzahlige Werte begrenzt werden und dann der Rest der ganzzahligen Division durch die Größe der Tabelle erhalten wird.

publicstaticfloatSampleHashGrid (Vector3 position) {
		int x = (int)position.x % hashGridSize;
		int z = (int)position.z % hashGridSize;
		return hashGrid[x + z * hashGridSize];
	}

Was macht %?
Это оператор модуля, он вычисляет остаток от деления, в нашем случае — целочисленного деления. Например, ряд −4, −3, −2, −1, 0, 1, 2, 3, 4 modulo 3 превращается в −1, 0, −2, −1, 0, 1, 2, 0, 1.

Dies funktioniert für positive Koordinaten, aber nicht für negative, da für solche Zahlen der Rest negativ ist. Wir können dies beheben, indem wir negative Ergebnisse durch Hinzufügen einer Tabellengröße korrigieren.

int x = (int)position.x % hashGridSize;
		if (x < 0) {
			x += hashGridSize;
		}
		int z = (int)position.z % hashGridSize;
		if (z < 0) {
			z += hashGridSize;
		}

Jetzt schaffen wir für jede quadratische Einheit unseren eigenen Wert. In der Realität brauchen wir jedoch keine solche Dichtetabelle. Objekte sind weiter voneinander entfernt. Wir können die Tabelle dehnen, indem wir die Skalierung der Position reduzieren, bevor wir den Index berechnen. Wir benötigen nur einen eindeutigen Wert für ein 4 x 4-Quadrat.

publicconstfloat hashGridScale = 0.25f;
	publicstaticfloatSampleHashGrid (Vector3 position) {
		int x = (int)(position.x * hashGridScale) % hashGridSize;
		if (x < 0) {
			x += hashGridSize;
		}
		int z = (int)(position.z * hashGridScale) % hashGridSize;
		if (z < 0) {
			z += hashGridSize;
		}
		return hashGrid[x + z * hashGridSize];
	}

Kehren wir zu HexFeatureManager.AddFeatureunserer neuen Hash-Tabelle zurück, um den Wert zu ermitteln. Nachdem wir es angewendet haben, um die Drehung festzulegen, bleiben die Objekte beim Bearbeiten des Reliefs fixiert.

publicvoidAddFeature (Vector3 position) {
		float hash = HexMetrics.SampleHashGrid(position);
		Transform instance = Instantiate(featurePrefab);
		position.y += instance.localScale.y * 0.5f;
		instance.localPosition = HexMetrics.Perturb(position);
		instance.localRotation = Quaternion.Euler(0f, 360f * hash, 0f);
		instance.SetParent(container, false);
	}

Unterbringungsschwelle


Obwohl Objekte eine andere Drehung haben, ist das Muster in ihrer Platzierung immer noch erkennbar. In jeder Zelle befinden sich sieben Objekte. Wir können diesem Schema Chaos hinzufügen, indem wir einige der Objekte willkürlich weglassen. Wie entscheiden wir, ob wir ein Objekt hinzufügen oder nicht? Natürlich testen wir einen anderen Zufallswert!

Das heißt, jetzt brauchen wir statt eines einzelnen Hash-Werts zwei. Ihre Unterstützung kann durch die Verwendung einer Hash-Tabelle anstelle einer floatVariablen als Array-Typ hinzugefügt werden Vector2. Vektoroperationen sind jedoch für Hash-Werte nicht sinnvoll. Erstellen wir daher eine spezielle Struktur für diesen Zweck. Sie wird nur zwei Float-Werte benötigen. Fügen wir eine statische Methode hinzu, um ein Paar zufälliger Werte zu erstellen.

using UnityEngine;
publicstruct HexHash {
	publicfloat a, b;
	publicstatic HexHash Create () {
		HexHash hash;
		hash.a = Random.value;
		hash.b = Random.value;
		return hash;
	}
}

Sollte es nicht serialisiert werden?
Мы храним эти структуры только в таблице хешей, которая статична, поэтому при рекомпиляции не будет сериализоваться движком Unity. Следовательно, она не обязана быть сериализуемой.

Ändern Sie es HexMetricsso, dass es die neue Struktur verwendet.

static HexHash[] hashGrid;
	publicstaticvoidInitializeHashGrid (int seed) {
		hashGrid = new HexHash[hashGridSize * hashGridSize];
		Random.State currentState = Random.state;
		Random.InitState(seed);
		for (int i = 0; i < hashGrid.Length; i++) {
			hashGrid[i] = HexHash.Create();
		}
		Random.state = currentState;
	}
	publicstatic HexHash SampleHashGrid (Vector3 position) {
		…
	}

Hat HexFeatureManager.AddFeaturenun Zugriff auf zwei Hashwerte. Lassen Sie uns zunächst entscheiden, ob ein Objekt hinzugefügt oder übersprungen werden soll. Wenn der Wert gleich oder größer als 0,5 ist, überspringen wir ihn. In diesem Fall werden wir ungefähr die Hälfte der Objekte los. Der zweite Wert wird normalerweise zur Bestimmung der Kurve verwendet.

publicvoidAddFeature (Vector3 position) {
		HexHash hash = HexMetrics.SampleHashGrid(position);
		if (hash.a >= 0.5f) {
			return;
		}
		Transform instance = Instantiate(featurePrefab);
		position.y += instance.localScale.y * 0.5f;
		instance.localPosition = HexMetrics.Perturb(position);
		instance.localRotation = Quaternion.Euler(0f, 360f * hash.b, 0f);
		instance.SetParent(container, false);
	}


Die Dichte der Objekte wird um 50% reduziert.

Einheitspaket

Objekte zeichnen


Anstatt Objekte überall zu platzieren, lassen Sie sie sich bearbeiten. Wir werden jedoch keine separaten Objekte zeichnen, sondern jeder Zelle die Ebene der Objekte hinzufügen. Diese Ebene steuert die Wahrscheinlichkeit von Objekten in der Zelle. Standardmäßig ist der Wert Null, dh die Objekte fehlen.

Da rote Würfel auf unserem Relief nicht wie natürliche Objekte aussehen, nennen wir sie Gebäude. Sie werden die Urbanisierung repräsentieren. Lassen Sie uns den HexCellGrad der Verstädterung erhöhen.

publicint UrbanLevel {
		get {
			return urbanLevel;
		}
		set {
			if (urbanLevel != value) {
				urbanLevel = value;
				RefreshSelfOnly();
			}
		}
	}
	int urbanLevel;

Wir können es so machen, dass der Urbanisierungsgrad für die Unterwasserzelle Null ist, aber dies ist nicht notwendig, wir überspringen bereits die Erstellung von Unterwasserobjekten. Und vielleicht werden wir irgendwann Wasserobjekte der Urbanisierung wie Docks und Unterwasserstrukturen hinzufügen.

Dichteregler


Um den Verstädterungsgrad zu ändern, werden wir HexMapEditoreinen weiteren Schieberegler zur Unterstützung hinzufügen .

int activeUrbanLevel;
	…
	bool applyUrbanLevel;
	…
	publicvoidSetApplyUrbanLevel (bool toggle) {
		applyUrbanLevel = toggle;
	}
	publicvoidSetUrbanLevel (float level) {
		activeUrbanLevel = (int)level;
	}
	voidEditCell (HexCell cell) {
		if (cell) {
			…
			if (applyWaterLevel) {
				cell.WaterLevel = activeWaterLevel;
			}
			if (applyUrbanLevel) {
				cell.UrbanLevel = activeUrbanLevel;
			}
			if (riverMode == OptionalToggle.No) {
				cell.RemoveRiver();
			}
			…
		}
	}

Fügen Sie der Benutzeroberfläche einen weiteren Schieberegler hinzu und verbinden Sie ihn mit den entsprechenden Methoden. Ich werde ein neues Bedienfeld auf der rechten Seite des Bildschirms platzieren, um ein Überlaufen des linken Bedienfelds zu vermeiden.

Wie viele Level werden wir brauchen? Schauen wir uns vier an, die Null, Niedrig, Mittel und Hohe Dichte bezeichnen.



Schieberegler für die Urbanisierung.

Schwellenwertänderung


Jetzt, da wir eine Urbanisierungsebene haben, müssen wir diese verwenden, um zu bestimmen, ob Objekte platziert werden sollen. Dazu müssen wir den Urbanisierungsgrad als zusätzlichen Parameter in hinzufügen HexFeatureManager.AddFeature. Lassen Sie uns noch einen Schritt weitergehen und die Zelle selbst passieren. In Zukunft werden wir uns wohler fühlen.

Am schnellsten können Sie den Urbanisierungsgrad ausnutzen, indem Sie ihn mit 0,25 multiplizieren und den Wert als neuen Schwellenwert für das Überspringen von Objekten verwenden. Aus diesem Grund steigt die Wahrscheinlichkeit eines Objekts mit jedem Level um 25%.

publicvoidAddFeature (HexCell cell, Vector3 position) {
		HexHash hash = HexMetrics.SampleHashGrid(position);
		if (hash.a >= cell.UrbanLevel * 0.25f) {
			return;
		}
		…
	}

Übergeben Sie die Zellen an, damit es funktioniert HexGridChunk.

voidTriangulate (HexCell cell) {
		…
		if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) {
			features.AddFeature(cell,  cell.Position);
		}
	}
	voidTriangulate (HexDirection direction, HexCell cell) {
		…
			if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) {
				features.AddFeature(cell, (center + e.v1 + e.v5) * (1f / 3f));
			}
		…
	}
	…
	voidTriangulateAdjacentToRiver (
		HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e
	) {
		…
		if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) {
			features.AddFeature(cell, (center + e.v1 + e.v5) * (1f / 3f));
		}
	}


Zeichnungsdichte der Urbanisierung.

Einheitspaket

Mehrere Fertighäuser mit Geländeobjekten


Die Unterschiede in der Wahrscheinlichkeit des Auftretens von Objekten reichen nicht aus, um eine klare Trennung zwischen niedrigem und hohem Verstädterungsgrad zu erreichen. In einigen Zellen wird es einfach mehr oder weniger als die erwartete Anzahl von Gebäuden geben. Wir können den Unterschied deutlicher machen, indem wir für jedes Level ein eigenes Fertighaus verwenden.

Lassen Sie uns das Feld featurePrefabin loswerden HexFeatureManagerund es durch ein Array für die Fertighäuser der Urbanisierung ersetzen. Um das entsprechende Fertighaus zu erhalten, subtrahieren wir eins vom Urbanisierungsgrad und verwenden den Wert als Index.

<del>//	public Transform featurePrefab;</del>public Transform[] urbanPrefabs;
	publicvoidAddFeature (HexCell cell, Vector3 position) {
		…
		Transform instance = Instantiate(urbanPrefabs[cell.UrbanLevel - 1]);
		…
	}

Wir erstellen zwei Duplikate des Fertigteils des Objekts, benennen sie um und ändern sie so, dass sie drei verschiedene Urbanisierungsebenen kennzeichnen. Stufe 1 ist eine niedrige Dichte, daher verwenden wir einen Würfel mit einer Kantenlänge, die eine Hütte bezeichnet. Ich werde das Fertigteil-Level 2 auf (1,5, 2, 1,5) skalieren, damit es wie ein zweistöckiges Gebäude aussieht. Für hohe Gebäude der Stufe 3 habe ich die Skala (2, 5, 2) verw

Jetzt auch beliebt: