Parties 1-3: maillage, couleurs et hauteurs de celluleParties 4-7: bosses, rivières et routesParties 8-11: eau, reliefs et rempartsParties 12-15: sauvegarde et chargement, textures, distancesParties 16-19: trouver le chemin, équipes de joueurs, animationsParties 20-23: Brouillard de guerre, recherche cartographique, génération procéduraleParties 24-27: cycle de l'eau, érosion, biomes, carte cylindriquePartie 8: l'eau
- Ajoutez de l'eau aux cellules.
- Triangule la surface de l'eau.
- Créez un surf avec de la mousse.
- Combinez l'eau et les rivières.
Nous avons déjà ajouté le support de la rivière, et dans cette partie, nous allons plonger complètement les cellules dans l'eau.
L'eau arrive.Niveau d'eau
Le moyen le plus simple est de mettre en œuvre le soutien de l'eau en le plaçant au même niveau. Toutes les cellules dont la hauteur est inférieure à ce niveau sont immergées dans l'eau. Mais une manière plus flexible serait de maintenir l'eau à différentes hauteurs, alors rendons le niveau d'eau modifiable. Pour cela,
HexCell
doit surveiller son niveau d'eau.
public int WaterLevel { get { return waterLevel; } set { if (waterLevel == value) { return; } waterLevel = value; Refresh(); } } int waterLevel;
Si vous le souhaitez, vous pouvez vous assurer que certaines caractéristiques du relief n'existaient pas sous l'eau. Mais pour l'instant je ne ferai pas ça. Des choses comme les routes sous-marines me conviennent. Ils peuvent être considérés comme des zones récemment inondées.
Cellules d'inondation
Maintenant que nous avons des niveaux d'eau, la question la plus importante est de savoir si les cellules sont sous l'eau. Une cellule est sous l'eau si son niveau d'eau est supérieur à sa hauteur. Pour obtenir ces informations, nous allons ajouter une propriété.
public bool IsUnderwater { get { return waterLevel > elevation; } }
Cela signifie que lorsque le niveau et la hauteur de l'eau sont égaux, la cellule s'élève au-dessus de l'eau. Autrement dit, la surface réelle de l'eau est inférieure à cette hauteur. Comme pour les surfaces fluviales, ajoutons le même décalage -
HexMetrics.riverSurfaceElevationOffset
. Changez son nom pour un nom plus général.
Modifiez
HexCell.RiverSurfaceY
afin qu'il utilise le nouveau nom. Ensuite, nous ajoutons une propriété similaire à la surface de l'eau de la cellule inondée.
public float RiverSurfaceY { get { return (elevation + HexMetrics.waterElevationOffset) * HexMetrics.elevationStep; } } public float WaterSurfaceY { get { return (waterLevel + HexMetrics.waterElevationOffset) * HexMetrics.elevationStep; } }
Édition de l'eau
La modification du niveau d'eau est similaire à la modification de la hauteur. Par conséquent,
HexMapEditor
doit surveiller le niveau d'eau actif et s'il doit être appliqué aux cellules.
int activeElevation; int activeWaterLevel; … bool applyElevation = true; bool applyWaterLevel = true;
Ajoutez des méthodes pour connecter ces paramètres à l'interface utilisateur.
public void SetApplyWaterLevel (bool toggle) { applyWaterLevel = toggle; } public void SetWaterLevel (float level) { activeWaterLevel = (int)level; }
Et ajoutez le niveau d'eau à
EditCell
.
void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (applyWaterLevel) { cell.WaterLevel = activeWaterLevel; } … } }
Pour ajouter un niveau d'eau à l'interface utilisateur, dupliquez l'étiquette et le curseur de hauteur, puis modifiez-les. N'oubliez pas d'attacher leurs événements aux méthodes appropriées.
Curseur de niveau d'eau.paquet d'unitéTriangulation de l'eau
Pour trianguler l'eau, nous avons besoin d'un nouveau maillage avec un nouveau matériau. Tout d'abord, créez un shader
Water , en dupliquant le shader
River . Modifiez-le pour qu'il utilise la propriété color.
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; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; } ENDCG } FallBack "Diffuse" }
Créez un nouveau matériau avec ce shader en dupliquant le matériau
Water et en le remplaçant par un shader. Laissez la texture du bruit, car nous l'utiliserons plus tard.
Matière eau.Ajoutez un nouvel enfant au préfabriqué en dupliquant l'enfant
Rivers . Il n'a pas besoin de coordonnées UV et il doit utiliser de l'
eau . Comme d'habitude, nous le ferons en créant une instance du préfabriqué, en le modifiant, puis en appliquant les modifications au préfabriqué. Après cela, débarrassez-vous de l'instance.
Enfant objet eau.Ensuite, ajoutez un support de maillage d'eau à
HexGridChunk
.
public HexMesh terrain, rivers, roads, water; public void Triangulate () { 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(); }
Et connectez-le à l'enfant préfabriqué.
L'objet Eau est connecté.Hexagones d'eau
Puisque l'eau forme une deuxième couche, donnons-lui notre propre méthode de triangulation pour chacune des directions. Nous devons l'appeler uniquement lorsque la cellule est immergée dans l'eau.
void Triangulate (HexDirection direction, HexCell cell) { … if (cell.IsUnderwater) { TriangulateWater(direction, cell, center); } } void TriangulateWater ( HexDirection direction, HexCell cell, Vector3 center ) { }
Comme pour les rivières, la hauteur de la surface de l'eau ne varie pas beaucoup dans les cellules avec le même niveau d'eau. Par conséquent, nous ne semblons pas avoir besoin de côtes complexes. Un simple triangle suffira.
void TriangulateWater ( 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); }
Hexagones d'eau.Composés de l'eau
Nous pouvons connecter des cellules voisines avec de l'eau avec un quadrilatère.
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); }
Connexions des bords de l'eau.Et remplissez les coins avec un triangle.
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()) ); } }
Joints de coins d'eau.Maintenant, nous avons des cellules d'eau connectées lorsqu'elles sont à proximité. Ils laissent un espace entre eux et les cellules sèches de hauteur plus élevée, mais nous le laisserons pour plus tard.
Niveaux d'eau harmonisés
Nous avons émis l'hypothèse que les cellules sous-marines voisines ont le même niveau d'eau. Si tel est le cas, alors tout semble bien, mais si cette hypothèse est violée, des erreurs se produisent.
Niveaux d'eau incohérents.Nous pouvons faire en sorte que l'eau reste au même niveau. Par exemple, lorsque le niveau d'eau d'une cellule inondée change, nous pouvons propager les changements aux cellules voisines afin de maintenir les niveaux synchronisés. Cependant, ce processus devrait se poursuivre jusqu'à ce qu'il rencontre des cellules qui ne sont pas immergées dans l'eau. Ces cellules définissent les limites de la masse d'eau.
Le danger de cette approche est qu'elle peut rapidement devenir incontrôlable. Si la modification échoue, l'eau peut couvrir toute la carte. Ensuite, tous les fragments devront être triangulés simultanément, ce qui entraînera un énorme saut dans les retards.
Alors ne le faisons pas encore. Cette fonctionnalité peut être ajoutée dans un éditeur plus complexe. Tout en cohérence des niveaux d'eau, nous laissons la conscience de l'utilisateur.
paquet d'unitéAnimation sur l'eau
Au lieu d'une couleur uniforme, nous créerons quelque chose qui ressemble à des vagues. Comme dans les autres shaders, pour l'instant nous ne nous efforcerons pas d'avoir un beau graphisme, il nous suffit de désigner les vagues.
Une eau parfaitement plate.Faisons ce que nous avons fait avec les rivières. Nous échantillonnons le bruit avec la position du monde et l'ajoutons à une couleur uniforme. Pour animer la surface, ajoutez du temps à la coordonnée V.
struct Input { float2 uv_MainTex; float3 worldPos; }; … void surf (Input IN, 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 = ca; }
Défilement de l'eau, temps × 10.Deux directions
Jusqu'à présent, ce n'est pas du tout comme des vagues. Compliquons l'image en ajoutant un deuxième échantillon de bruit
et cette fois en ajoutant la coordonnée U. Nous utilisons un canal de bruit différent pour obtenir deux modèles différents en conséquence. Les vagues finies seront ces deux échantillons empilés ensemble.
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;
Lors de la sommation des deux échantillons, nous obtenons des résultats dans l'intervalle 0–2, nous devons donc le redimensionner à 0–1. Au lieu de diviser simplement les vagues en deux, nous pouvons utiliser la fonction
smoothstep
pour créer un résultat plus intéressant. Nous mettons ¾ - 2 sur 0–1 pour qu'il n'y ait pas d'ondes visibles à la surface de l'eau.
float waves = noise1.z + noise2.x; waves = smoothstep(0.75, 2, waves);
Deux directions, temps × 10.Des vagues de mixage
Il est toujours notable que nous avons deux modèles de bruit en mouvement qui ne changent pas réellement. Il serait plus plausible que les modèles changent. Nous pouvons le réaliser en interpolant entre différents canaux d'échantillons de bruit. Mais cela ne peut pas être fait de la même manière, sinon toute la surface de l'eau changera simultanément, et cela est très visible. Au lieu de cela, nous créerons une vague de confusion.
Nous allons créer une onde de mélange à l'aide d'une sinusoïde, qui se déplace en diagonale le long de la surface de l'eau. Nous le ferons en ajoutant les coordonnées mondiales X et Z et en utilisant la somme comme entrée de la fonction
sin
. Effectuez un zoom arrière pour obtenir des bandes suffisamment grandes. Et bien sûr, ajoutons la même valeur pour les animer.
float blendWave = sin((IN.worldPos.x + IN.worldPos.z) * 0.1 + _Time.y);
Les ondes sinusoïdales varient entre -1 et 1, et nous avons besoin d'un intervalle de 0 à 1. Vous pouvez l'obtenir en quadrillant la vague. Pour voir un résultat isolé, utilisez-le à la place de la couleur modifiée comme valeur de sortie.
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);
Des vagues de mixage.Pour rendre les ondes de mélange moins visibles, ajoutez-y du bruit provenant des deux échantillons.
float blendWave = sin( (IN.worldPos.x + IN.worldPos.z) * 0.1 + (noise1.y + noise2.z) + _Time.y ); blendWave *= blendWave;
Vagues de mélange déformées.Enfin, nous utilisons une onde de mélange pour interpoler entre les deux canaux des deux échantillons de bruit. Pour une variation maximale, prenez quatre canaux différents.
float waves = lerp(noise1.z, noise1.w, blendWave) + lerp(noise2.x, noise2.y, blendWave)
Mélange des vagues, temps × 2.paquet d'unitéLa côte
Nous en avons terminé avec les eaux libres, mais maintenant nous devons combler le vide dans l'eau le long de la côte. Comme nous devons nous conformer aux contours des terres, les eaux côtières nécessitent une approche différente. Divisons
TriangulateWater
en deux méthodes - une pour les eaux libres et une pour la côte. Pour comprendre quand nous travaillons avec la côte, nous devons regarder la cellule voisine. Autrement dit, dans
TriangulateWater
nous aurons un voisin. S'il y a un voisin et qu'il n'est pas sous l'eau, alors nous avons affaire à la côte.
void TriangulateWater ( 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); } } void TriangulateOpenWater ( 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) {
Il n'y a pas de triangulation le long de la côte.Puisque la côte est déformée, nous devons déformer les triangles d'eau le long de la côte. Par conséquent, nous avons besoin des sommets des bords et de l'éventail des triangles.
void TriangulateWaterShore ( 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); }
Des fans de triangles le long de la côte.Vient ensuite une bande de côtes, comme dans un relief normal. Cependant, nous ne sommes pas obligés de nous limiter à certaines zones, car nous n'appelons
TriangulateWaterShore
lorsque nous rencontrons la côte, pour laquelle la bande est toujours nécessaire.
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);
Rayures de côtes le long de la côte.De même, nous devons également ajouter un triangle angulaire à chaque fois.
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()) ); }
Les coins des côtes le long de la côte.Maintenant, nous avons de l'eau prête pour la côte. Une partie est toujours en dessous du maillage en relief, il n'y a donc pas de trous.
Côte UV
Nous pouvons tout laisser tel quel, mais il serait intéressant que les eaux côtières aient leur propre horaire. Par exemple, l'effet de la mousse, qui devient plus importante à l'approche de la côte. Pour le mettre en œuvre, le shader doit savoir à quel point le fragment est proche de la côte. Nous pouvons transmettre ces informations via les coordonnées UV.
L'eau libre n'a pas de coordonnées UV et n'a pas besoin de mousse. Il n'est nécessaire que pour l'eau près de la côte. Par conséquent, les exigences pour les deux types d'eau sont très différentes. Il serait logique de créer votre propre maillage pour chaque type. Par conséquent, nous ajoutons la prise en charge d'un autre objet maillé à HexGridChunk.
public HexMesh terrain, rivers, roads, water, waterShore; public void Triangulate () { 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(); }
Ce nouveau maillage utilisera
TriangulateWaterShore
.
void TriangulateWaterShore ( 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()) ); } }
Dupliquez l'objet eau, connectez-le au préfabriqué et réglez-le de sorte qu'il utilise les coordonnées UV. Nous créons également un shader et un matériau pour les eaux côtières, reproduisant le shader et le matériau d'eau existants.
Installation de rivage d'eau et matériau UV.Modifiez le shader
Water Shore de sorte qu'au lieu de l'eau, il affiche les coordonnées UV.
fixed4 c = fixed4(IN.uv_MainTex, 1, 1)
Comme aucune coordonnée n'a encore été définie, il affichera une couleur unie. Grâce à cela, il est facile de voir que la côte utilise en fait un maillage séparé avec du matériau.
Maille séparée pour la côte.Mettons les informations sur la côte dans la coordonnée V. Côté eau, affectez-lui une valeur de 0, côté terre - valeur 1. Comme nous n'avons pas besoin de transmettre autre chose, toutes les coordonnées U seront simplement 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) ); }
Les transitions vers les côtes sont fausses.Le code ci-dessus fonctionne pour les bords, mais est faux à certains égards. Si le prochain voisin est sous l'eau, cette approche sera correcte. Mais lorsque le prochain voisin n'est pas sous l'eau, le troisième sommet du triangle sera sous terre.
waterShore.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, nextNeighbor.IsUnderwater ? 0f : 1f) );
Les transitions vers les côtes sont correctes.Mousse sur la côte
Maintenant que les transitions vers la côte sont correctement mises en œuvre, vous pouvez les utiliser pour créer un effet mousse. Le moyen le plus simple consiste à ajouter la valeur de la côte à une couleur uniforme.
void surf (Input IN, 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 = ca; }
Mousse linéaire.Pour rendre la mousse plus intéressante, multipliez-la par le carré de la sinusoïde.
float foam = sin(shore * 10); foam *= foam * shore;
Décoloration de la mousse carrée sinusoïdale.Agrandissons le front de mousse à l'approche du rivage. Cela peut être fait en prenant sa racine carrée avant d'utiliser la valeur de la côte.
float shore = IN.uv_MainTex.y; shore = sqrt(shore);
La mousse devient plus épaisse près du rivage.Ajoutez de la distorsion pour la rendre plus naturelle. Rendons la distorsion plus faible à l'approche de la côte. Il vaudra donc mieux border la côte.
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;
Mousse avec distorsion.Et, bien sûr, nous animons tout cela: à la fois une sinusoïde et des distorsions.
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;
Mousse animée.En plus de la mousse entrante, il y en a également une qui recule. Ajoutons une deuxième sinusoïde, qui se déplace dans la direction opposée, pour la simuler. Rendez-le plus faible et ajoutez un décalage temporel. La mousse finie sera le maximum de ces deux sinusoïdes.
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;
Mousse entrante et en retrait.Mélange de vagues et de mousse
Il y a une transition abrupte entre les eaux libres et côtières car les vagues d'eau libre ne sont pas incluses dans les eaux côtières. Pour résoudre ce problème, nous devons inclure ces vagues dans le shader
Water Shore .
Au lieu de copier le code d'onde,
collons- le dans le fichier d'inclusion
Water.cginc . En fait, nous y insérons du code pour la mousse et les vagues, chacun en tant que fonction distincte.
Comment fonctionnent les fichiers d'inclusion de shader?La création de vos propres fichiers shader d'inclusion est traitée dans le didacticiel
Rendu 5, Lumières multiples .
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); }
Modifiez le shader
Water pour qu'il utilise le nouveau fichier include.
#include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, 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 = ca; }
Dans le shader
Water Shore , les valeurs sont calculées pour la mousse et les vagues. Puis nous assourdissons les vagues à l'approche du rivage. Le résultat final sera un maximum de mousse et de vagues.
#include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, 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 = ca; }
Un mélange de mousse et de vagues.paquet d'unitéEncore une fois sur les eaux côtières
Une partie du maillage de la côte est cachée sous le maillage en relief. C'est normal, mais seule une petite partie est cachée. Malheureusement, les falaises abruptes cachent la plupart des eaux côtières, et donc de la mousse.
Eau côtière presque cachée.Nous pouvons gérer cela en augmentant la taille de la bande de la côte. Cela peut être fait en réduisant le rayon des hexagones d'eau. Pour cela, en plus du coefficient d'intégrité, nous avons besoin d'un coefficient d'eau
HexMetrics
, ainsi que de méthodes pour obtenir des angles d'eau.
Le coefficient d'intégrité est de 0,8. Pour doubler la taille des composés de l'eau, nous devons définir le coefficient d'eau à 0,6.
public const float waterFactor = 0.6f; public static Vector3 GetFirstWaterCorner (HexDirection direction) { return corners[(int)direction] * waterFactor; } public static Vector3 GetSecondWaterCorner (HexDirection direction) { return corners[(int)direction + 1] * waterFactor; }
Nous utiliserons ces nouvelles méthodes HexGridChunk
pour trouver les angles de l'eau. void TriangulateOpenWater ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { Vector3 c1 = center + HexMetrics.GetFirstWaterCorner(direction); Vector3 c2 = center + HexMetrics.GetSecondWaterCorner(direction); … } void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { EdgeVertices e1 = new EdgeVertices( center + HexMetrics.GetFirstWaterCorner(direction), center + HexMetrics.GetSecondWaterCorner(direction) ); … }
Utiliser des coins d'eau.La distance entre les hexagones de l'eau a en fait doublé. Maintenant, HexMetrics
il devrait également avoir une méthode pour créer des ponts dans l'eau. public const float waterBlendFactor = 1f - waterFactor; public static Vector3 GetWaterBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * waterBlendFactor; }
Changez HexGridChunk
pour qu'il utilise la nouvelle méthode. void TriangulateOpenWater ( 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()) ); } } } void TriangulateWaterShore ( 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()) ); … } }
De longs ponts dans l'eau.Entre les côtes de l'eau et de la terre
Bien que cela nous donne plus d'espace pour la mousse, maintenant encore plus est caché sous le relief. Idéalement, nous pourrons utiliser une côte d'eau côté eau et une côte terre côté terre.Nous ne pouvons pas utiliser un simple pont pour trouver le bord opposé de la terre, si nous partons des coins de l'eau. Au lieu de cela, nous pouvons aller dans la direction opposée, depuis le centre du voisin. Changez TriangulateWaterShore
pour utiliser cette nouvelle approche.
Mauvais coins des bords.Cela a fonctionné, seulement maintenant, nous devons à nouveau considérer deux cas pour les triangles angulaires. HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) {
Les coins corrects des bords.Cela a bien fonctionné, mais maintenant que la majeure partie de la mousse est visible, elle devient assez prononcée. Pour compenser cela, nous allons rendre l'effet un peu plus faible en réduisant l'échelle de la valeur de la côte dans le shader. shore = sqrt(shore) * 0.9
Mousse prête.paquet d'unitéRivières sous-marines
Nous nous sommes retrouvés avec de l'eau, du moins dans les endroits où aucune rivière ne s'y jette. Puisque l'eau et les rivières ne se remarquent pas encore, les rivières couleront à travers et sous l'eau.Rivières coulant dans l'eau.L'ordre dans lequel les objets translucides sont rendus dépend de leur distance de la caméra. Les objets les plus proches sont rendus en dernier, ils sont donc en haut. Lorsque vous déplacez la caméra, cela signifie que parfois des rivières et parfois de l'eau apparaîtront les unes sur les autres. Commençons par rendre constante l'ordre de rendu. Les rivières doivent être dessinées au-dessus de l'eau afin que les cascades soient affichées correctement. Nous pouvons implémenter cela en modifiant la file d'attente du shader River . Tags { "RenderType"="Transparent" "Queue"="Transparent+1" }
Nous dessinons les rivières en dernier.Cacher la rivière sous-marine
Bien que le lit de la rivière puisse très bien être sous l'eau, et que l'eau puisse en fait couler, nous ne devrions pas voir cette eau. Et encore plus, il ne faut pas le restituer au-dessus d'une véritable surface d'eau. Nous pouvons nous débarrasser de l'eau des rivières sous-marines en ajoutant des segments de rivière uniquement lorsque la cellule actuelle n'est pas sous l'eau. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.HasIncomingRiver; … } } void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater) { bool reversed = cell.IncomingRiver == direction; … } }
Pour TriangulateConnection
commencer, nous ajouterons un segment de rivière lorsque ni le courant ni la cellule voisine ne sont sous l'eau. 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 ); } }
Plus de rivières sous-marines.Cascades
Il n'y a plus de rivières sous-marines, mais maintenant nous avons des trous à ces endroits des rivières où ils rencontrent la surface de l'eau. Les rivières au même niveau que l'eau créent de petits trous ou superpositions. Mais les plus remarquables sont les chutes d'eau manquantes pour les rivières coulant d'une plus grande hauteur. Occupons-nous d'abord d'eux.Un segment de rivière avec une cascade passait à travers la surface de l'eau. En conséquence, il s'est retrouvé partiellement au-dessus et partiellement sous l'eau. Nous devons garder une partie au-dessus du niveau de l'eau, en rejetant tout le reste. Vous devrez travailler dur pour cela, alors créez une méthode distincte.La nouvelle méthode nécessite quatre pics, deux niveaux de rivière et un niveau d'eau. Nous allons le régler de façon à regarder dans le sens du courant, en bas de la cascade. Par conséquent, les deux premiers pics et les côtés gauche et droit seront au sommet, et les plus bas suivront. void TriangulateWaterfallInWater ( 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); }
Nous appellerons cette méthode TriangulateConnection
lorsqu'un voisin est sous l'eau et nous créons une cascade. 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 ); } else if (cell.Elevation > neighbor.WaterLevel) { TriangulateWaterfallInWater( e1.v2, e1.v4, e2.v2, e2.v4, cell.RiverSurfaceY, neighbor.RiverSurfaceY, neighbor.WaterSurfaceY ); } }
Nous devons également traiter les chutes d'eau dans la direction opposée, lorsque la cellule actuelle est sous l'eau, et la suivante ne l'est pas. if (!cell.IsUnderwater) { … } else if ( !neighbor.IsUnderwater && neighbor.Elevation > cell.WaterLevel ) { TriangulateWaterfallInWater( e2.v4, e2.v2, e1.v4, e1.v2, neighbor.RiverSurfaceY, cell.RiverSurfaceY, cell.WaterSurfaceY ); }
Encore une fois, nous obtenons le quad de la rivière d'origine. Ensuite, nous devons changer TriangulateWaterfallInWater
pour qu'il élève les pics inférieurs au niveau de l'eau. Malheureusement, changer uniquement les coordonnées Y ne sera pas suffisant. Cela peut pousser la cascade de la falaise, qui peut former des trous. Au lieu de cela, vous devez déplacer les sommets inférieurs vers les sommets supérieurs à l'aide de l'interpolation.Interpoler.Pour déplacer les pics inférieurs vers le haut, divisez leur distance sous la surface de l'eau par la hauteur de la cascade. Cela nous donnera une valeur d'interpolateur. 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);
En conséquence, nous obtenons une cascade raccourcie qui a la même orientation. Cependant, étant donné que les positions des sommets inférieurs ont changé, elles ne seront pas déformées comme les sommets d'origine. Cela signifie que le résultat final ne coïncidera toujours pas avec la cascade d'origine. Pour résoudre ce problème, nous devons déformer manuellement les sommets avant d'interpoler, puis ajouter le quadruple non déformé. 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);
Comme nous avons déjà une méthode pour ajouter des triangles non déformés, nous n'avons vraiment pas besoin d'en créer un pour les quads. Par conséquent, nous ajoutons la méthode nécessaire HexMesh.AddQuadUnperturbed
. public void AddQuadUnperturbed ( 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); }
Les chutes d'eau se terminent à la surface de l'eau.paquet d'unitéEstuaires
Lorsque les rivières coulent à la même hauteur que la surface de l'eau, le maillage du fleuve touche le maillage côtier. S'il s'agissait d'une rivière se jetant dans la mer ou dans l'océan, il y aurait alors un ruisseau de la rivière rencontré par les vagues. Par conséquent, nous appellerons ces zones des estuaires.La rivière rencontre la côte sans déformer les sommets.Maintenant, nous avons deux problèmes de bouche. Premièrement, les rivières quadruples relient les deuxième et quatrième sommets des côtes, en sautant le troisième. Puisque la côte de l'eau n'utilise pas le troisième pic, elle peut créer un trou ou se chevaucher. Nous pouvons résoudre ce problème en modifiant la géométrie des bouches.Le deuxième problème est qu'il y a une transition abrupte entre la mousse et les matériaux fluviaux. Pour le résoudre, nous avons besoin d'un autre matériau qui réalise le mélange des effets d'une rivière et de l'eau.Cela signifie que les bouches nécessitent une approche spéciale, alors créons une méthode distincte pour elles. Il faut l'appeler TriangulateWaterShore
lorsqu'il y a une rivière qui se déplace dans la direction actuelle. void TriangulateWaterShore ( 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); } … } void TriangulateEstuary (EdgeVertices e1, EdgeVertices e2) { }
Une région mélangeant les deux effets n'est pas nécessaire pour remplir la bande entière. La forme trapézoïdale nous suffira. Par conséquent, nous pouvons utiliser deux triangles côtiers sur les côtés. void TriangulateEstuary (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) ); }
Trou trapézoïdal pour la zone de mélange.Coordonnées UV2
Pour créer un effet de rivière, nous avons besoin de coordonnées UV. Mais pour créer un effet mousse, vous avez également besoin de coordonnées UV. Autrement dit, lors de leur mélange, nous avons besoin de deux ensembles de coordonnées UV. Heureusement, les maillages du moteur Unity peuvent prendre en charge jusqu'à quatre ensembles UV. Nous avons juste besoin d'ajouter à l' HexMesh
appui du deuxième set. public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; [NonSerialized] List<Vector2> uvs, uv2s; public void Clear () { … if (useUVCoordinates) { uvs = ListPool<Vector2>.Get(); } if (useUV2Coordinates) { uv2s = ListPool<Vector2>.Get(); } triangles = ListPool<int>.Get(); } public void Apply () { … if (useUVCoordinates) { hexMesh.SetUVs(0, uvs); ListPool<Vector2>.Add(uvs); } if (useUV2Coordinates) { hexMesh.SetUVs(1, uv2s); ListPool<Vector2>.Add(uv2s); } … }
Pour ajouter un deuxième ensemble d'UV, nous dupliquons les méthodes de travail avec les UV et changeons la façon dont nous avons besoin. public void AddTriangleUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3) { uv2s.Add(uv1); uv2s.Add(uv2); uv2s.Add(uv3); } public void AddQuadUV2 (Vector2 uv1, Vector2 uv2, Vector3 uv3, Vector3 uv4) { uv2s.Add(uv1); uv2s.Add(uv2); uv2s.Add(uv3); uv2s.Add(uv4); } public void AddQuadUV2 (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)); }
Fonction River Shader
Comme nous utiliserons l'effet rivière dans deux shaders, nous déplacerons le code du River shader vers la nouvelle fonction de fichier Water include . 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; }
Modifiez le shader River pour utiliser cette nouvelle fonctionnalité. #include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, inout SurfaceOutputStandard o) { float river = River(IN.uv_MainTex, _MainTex); fixed4 c = saturate(_Color + river); … }
Objets de la bouche
Ajoutez une HexGridChunk
bouche pour soutenir l'objet maillé. public HexMesh terrain, rivers, roads, water, waterShore, estuaries; public void Triangulate () { 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(); }
Créez un shader, un matériau et un objet de la bouche, en dupliquant la côte et en la changeant. Connectez-le au fragment et faites-le utiliser les coordonnées UV et UV2.Estuarties d'objets.Triangulation de la bouche
Nous pouvons résoudre le problème du trou ou du chevauchement en plaçant un triangle entre l'extrémité de la rivière et le milieu du bord de l'eau. Puisque notre shader de bouche est un double du shader de côte, nous avons défini les coordonnées UV pour correspondre à l'effet de mousse. void TriangulateEstuary (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) ); }
Triangle central.Nous pouvons remplir tout le trapèze en ajoutant un quadruple des deux côtés du triangle central. 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);
Trapèze prêt.Tournons l'orientation quad vers la gauche afin qu'elle ait une connexion diagonale raccourcie, et par conséquent nous obtenons une géométrie symétrique. 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) );
Quad tourné, géométrie symétriqueDébit de la rivière
Pour soutenir l'effet rivière, nous devons ajouter des coordonnées UV2. Le bas du triangle central est au milieu de la rivière, donc sa coordonnée U doit être égale à 0,5. Puisque la rivière coule vers l'eau, le point gauche reçoit la coordonnée U égale à 1, et la droite reçoit la coordonnée U avec une valeur de 0. Nous fixons les coordonnées Y à 0 et 1, correspondant à la direction du courant. estuaries.AddTriangleUV2( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) );
Les quadrangles des deux côtés du triangle doivent coïncider avec cette orientation. Nous conservons les mêmes coordonnées U pour les points dépassant la largeur de la rivière. 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) );
Trapèze UV2.Pour vous assurer que nous avons correctement défini les coordonnées UV2, effectuez le rendu du nuanceur Estuaire . Nous pouvons accéder à ces coordonnées en ajoutant à la structure d'entrée float2 uv2_MainTex
. struct Input { float2 uv_MainTex; float2 uv2_MainTex; float3 worldPos; }; … void surf (Input IN, 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); … }
Coordonnées UV2.Tout a l'air bien, vous pouvez utiliser un shader pour créer un effet rivière. void surf (Input IN, inout SurfaceOutputStandard o) { … float river = River(IN.uv2_MainTex, _MainTex); fixed4 c = saturate(_Color + river); … }
Utilisez UV2 pour créer un effet de rivière.Nous avons créé les rivières de telle manière que lors de la triangulation des connexions entre les cellules, les coordonnées de la rivière V changent de 0,8 à 1. Par conséquent, ici, nous devrions également utiliser cet intervalle, et non de 0 à 1. Cependant, la connexion côtière est 50% de plus que les connexions cellulaires ordinaires . Par conséquent, pour s'adapter au mieux au cours de la rivière, nous devons changer les valeurs de 0,8 à 1,1. 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) );
Débit synchronisé de la rivière et de l'estuaire.Réglage du débit
Pendant que la rivière se déplace en ligne droite. Mais lorsque l'eau s'écoule dans une zone plus grande, elle se dilate. Le courant se courbe. Nous pouvons simuler cela en repliant les coordonnées UV2.Au lieu de garder les coordonnées U supérieures constantes en dehors de la largeur de la rivière, déplacez-les de 0,5. Le point le plus à gauche est 1,5, le plus à droite est -0,5.En même temps, nous élargissons le flux en déplaçant les coordonnées U des points inférieurs gauche et droit. Changez celui de gauche de 1 à 0,7 et celui de droite de 0 à 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) );
Expansion de la rivière.Pour terminer l'effet de courbure, modifiez les coordonnées V des quatre mêmes points. Puisque l'eau s'écoule loin de l'extrémité de la rivière, nous augmenterons les coordonnées des points supérieurs V à 1. Et pour créer une meilleure courbe, nous augmenterons les coordonnées V des deux points inférieurs à 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) );
Le cours incurvé de la rivière.Mélange rivière et côte
Il ne nous reste plus qu'à mélanger les effets de la côte et du fleuve. Pour ce faire, nous utilisons une interpolation linéaire, en prenant la valeur de la côte comme interpolateur. 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);
Bien que cela devrait fonctionner, vous pouvez obtenir une erreur de compilation. Le compilateur se plaint de la redéfinition _MainTex_ST
. La raison en est une erreur dans le compilateur de shaders de surface Unity causée par l'utilisation simultanée de uv_MainTex
et uv2_MainTex
. Nous devons trouver une solution de contournement.Au lieu de l'utiliser uv2_MainTex
, nous devrons transférer manuellement les coordonnées UV secondaires. Pour ce faire, renommez- uv2_MainTex
le riverUV
. Ajoutez ensuite une fonction de sommet au shader, qui lui assigne des coordonnées. #pragma surface surf Standard alpha vertex:vert … struct Input { float2 uv_MainTex; float2 riverUV; float3 worldPos; }; … void vert (inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); o.riverUV = v.texcoord1.xy; } void surf (Input IN, inout SurfaceOutputStandard o) { … float river = River(IN.riverUV, _MainTex); … }
Interpolation basée sur la valeur de la côte.L'interpolation fonctionne, à l'exception des sommets gauche et droit en haut. À ces points, la rivière devrait disparaître. Par conséquent, nous ne pouvons pas utiliser la valeur de la côte. Nous devrons utiliser une valeur différente, qui à ces deux sommets est 0. Heureusement, nous avons toujours la coordonnée U du premier ensemble UV, afin que nous puissions y stocker cette valeur. 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) );
Le bon mélange.Maintenant, les bouches ont un bon mélange entre la rivière en expansion, l'eau côtière et la mousse. Bien que cela ne crée pas une correspondance exacte avec les cascades, cet effet semble également bon avec les cascades.Pack d'unités Estuaires en actionRivières coulant des plans d'eau
Nous avons déjà des rivières qui se jettent dans des plans d'eau, mais il n'y a aucun soutien pour les rivières coulant dans une direction différente. Il y a des lacs d'où coulent les rivières, nous devons donc les ajouter également.Lorsqu'une rivière s'écoule d'une étendue d'eau, elle s'écoule en fait vers une altitude plus élevée. Ce n'est actuellement pas possible. Nous devons faire une exception et autoriser cette situation si le niveau d'eau correspond à la hauteur du point cible. Ajoutons à une HexCell
méthode privée qui vérifie selon notre nouveau critère si le voisin est le bon point cible pour la rivière sortante. bool IsValidRiverDestination (HexCell neighbor) { return neighbor && ( elevation >= neighbor.elevation || waterLevel == neighbor.elevation ); }
Nous utiliserons notre nouvelle méthode pour déterminer s'il est possible de créer une rivière sortante. public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } HexCell neighbor = GetNeighbor(direction);
De plus, vous devez vérifier la rivière lors du changement de la hauteur de la cellule ou du niveau de l'eau. Créons une méthode privée qui fera cette tâche. void ValidateRivers () { if ( hasOutgoingRiver && !IsValidRiverDestination(GetNeighbor(outgoingRiver)) ) { RemoveOutgoingRiver(); } if ( hasIncomingRiver && !GetNeighbor(incomingRiver).IsValidRiverDestination(this) ) { RemoveIncomingRiver(); } }
Nous utiliserons cette nouvelle méthode dans les propriétés Elevation
et WaterLevel
. public int Elevation { … set { …
Sortant et entrant dans les lacs fluviaux.Inverser le courant
Nous avons créé HexGridChunk.TriangulateEstuary
, suggérant que les rivières ne peuvent s'écouler que dans les plans d'eau. Par conséquent, par conséquent, le cours de la rivière se déplace toujours dans une direction. Nous devons inverser le débit lorsqu'il s'agit d'une rivière s'écoulant d'une étendue d'eau. Pour ce faire, vous devez TriangulateEstuary
connaître la direction du flux. Par conséquent, nous lui donnons un paramètre booléen qui détermine si nous avons affaire à une rivière entrante. void TriangulateEstuary ( EdgeVertices e1, EdgeVertices e2, bool incomingRiver ) { … }
Nous transmettrons ces informations lors de l'appel de cette méthode à partir de TriangulateWaterShore
. if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary(e1, e2, cell.IncomingRiver == direction); }
Maintenant, nous devons augmenter le débit de la rivière en changeant les coordonnées de UV2. Les coordonnées U des rivières sortantes doivent être reflétées: −0,5 devient 1,5, 0 devient 1, 1 devient 0 et 1,5 devient −0,5.Avec les coordonnées V, les choses sont un peu plus compliquées. Si vous regardez comment nous avons travaillé avec les connexions fluviales inversées, 0,8 devrait être 0 et 1 devrait être -0,2. Cela signifie que 1,1 devient −0,3 et 1,15 devient −0,35.Comme dans chaque cas, les coordonnées UV2 sont très différentes, écrivons-leur un code séparé. void TriangulateEstuary ( 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) ); } }
Le cours correct des rivières.paquet d'unitéPartie 9: caractéristiques de relief
- Ajoutez des objets au relief.
- Nous créons un support pour les niveaux de densité des objets.
- Nous utilisons divers objets dans le niveau.
- Mélangez trois types d'objets différents.
Dans cette partie, nous parlerons de l'ajout d'objets au terrain. Nous allons créer des objets tels que des bâtiments et des arbres.Conflit entre forêts, terres agricoles et urbanisation.Ajouter la prise en charge des objets
Bien que la forme du relief ait des variations, jusqu'à présent rien ne se passe dessus. C'est une terre sans vie. Pour lui donner vie, vous devez ajouter de tels objets. comme des arbres et des maisons. Ces objets ne font pas partie du maillage en relief, mais seront des objets séparés. Mais cela ne nous empêche pas de les ajouter lors de la triangulation du terrain.HexGridChunk
Peu importe comment fonctionne le maillage. Il ordonne simplement à un de ses enfants d' HexMesh
ajouter un triangle ou un quad. De même, il peut avoir un élément enfant qui traite du placement des objets sur eux.Gestionnaire d'objets
Créons un composant HexFeatureManager
qui prend soin des objets dans un seul fragment. Nous utilisons le même schéma que dans HexMesh
- lui donner des méthodes Clear
, Apply
et AddFeature
. Étant donné que l'objet doit être placé quelque part, la méthode AddFeature
reçoit le paramètre position.Nous allons commencer avec une implémentation vierge qui ne fera rien pour l'instant. using UnityEngine; public class HexFeatureManager : MonoBehaviour { public void Clear () {} public void Apply () {} public void AddFeature (Vector3 position) {} }
Nous pouvons maintenant ajouter un lien vers un tel composant dans HexGridChunk
. Vous pouvez ensuite l'inclure dans le processus de triangulation, comme tous les éléments enfants HexMesh
. public HexFeatureManager features; public void Triangulate () { 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(); }
Commençons par placer un objet au centre de chaque cellule void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } features.AddFeature(cell.Position); }
Maintenant, nous avons besoin d'un vrai gestionnaire d'objets. Ajoutez un autre enfant au préfabriqué Hex Grid Chunk et donnez-lui un composant HexFeatureManager
. Ensuite, vous pouvez y connecter un fragment.Un gestionnaire d'objets ajouté au fragment préfabriqué.Objets préfabriqués
Quel objet terrain créerons-nous? Pour le premier test, un cube est tout à fait adapté. Créons un cube assez grand, par exemple, avec une échelle de (3, 3, 3) et transformons-le en préfabriqué. Créez également du matériel pour lui. J'ai utilisé le matériau par défaut avec du rouge. Supprimons son collisionneur, car nous n'en avons pas besoin.Cube préfabriqué.Les gestionnaires d'objets auront besoin d'un lien vers ce préfabriqué, alors ajoutez-le à HexFeatureManager
, puis connectez-les. Étant donné que l'accès au composant de transformation est requis pour placer l'objet, nous l'utilisons comme type de lien. public Transform featurePrefab;
Gestionnaire d'objets avec préfabriqué.Création d'instances d'objets
La structure est prête et nous pouvons commencer à ajouter des caractéristiques de terrain! Créez simplement une instance du préfabriqué HexFeatureManager.AddFeature
et définissez sa position. public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); instance.localPosition = position; }
Instances de caractéristiques du terrain.Désormais, le terrain sera rempli de cubes. Au moins les moitiés supérieures des cubes, car l'origine locale du maillage du cube dans Unity est au centre du cube et le bas est en dessous de la surface du relief. Pour placer des cubes sur la topographie, nous devons les déplacer jusqu'à la moitié de leur hauteur. public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = position; }
Cubes à la surface du relief.Et si nous utilisons un autre maillage?. , , . .
Bien sûr, nos cellules sont déformées, nous devons donc déformer la position des objets. On se débarrasse donc d'une parfaite répétabilité du maillage. instance.localPosition = HexMetrics.Perturb(position);
Positions déformées des objets.Destruction d'objets de secours
Chaque fois qu'un fragment est mis à jour, nous créons de nouveaux objets en relief. Cela signifie que pendant que nous créons de plus en plus d'objets dans les mêmes positions. Pour éviter les doublons, nous devons nous débarrasser des anciens objets lors du nettoyage d'un fragment.La façon la plus rapide de le faire est de créer un objet conteneur de jeu et de transformer tous les objets en relief en ses enfants. Ensuite, une fois appelé, Clear
nous détruirons ce conteneur et en créerons un nouveau. Le conteneur lui-même sera un enfant de son manager. Transform container; public void Clear () { if (container) { Destroy(container.gameObject); } container = new GameObject("Features Container").transform; container.SetParent(transform, false); } … public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.SetParent(container, false); }
Il est probablement inefficace de créer et de détruire à chaque fois des objets en relief., , . . . , , , . HexFeatureManager.Apply
. . , , .
paquet d'unitéPlacement d'objets en relief
Pendant que nous plaçons des objets au centre de chaque cellule. Pour les cellules vides, cela semble normal, mais sur les cellules contenant des rivières et des routes, ainsi que inondées d'eau, cela semble étrange.Les objets sont partout.Par conséquent, vérifions avant de placer l'objet HexGridChunk.Triangulate
si la cellule est vide. if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell.Position); }
Hébergement limité.Un objet par direction
Un seul objet par cellule n'est pas trop. Il y a encore beaucoup de place pour un tas d'objets. Par conséquent, nous ajoutons un objet supplémentaire au centre de chacun des six triangles de la cellule, c'est-à-dire un par direction.Nous le ferons dans une autre méthode Triangulate
, quand nous saurons qu'il n'y a pas de rivière dans la cellule. Nous devons encore vérifier si nous sommes sous l'eau et s'il y a une route dans la cellule. Mais dans ce cas, nous ne nous intéressons qu'aux routes allant dans la direction actuelle. void Triangulate (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)); } } … }
De nombreuses installations, mais pas à proximité des rivières.Cela crée beaucoup plus d'objets! Ils apparaissent près des routes, mais évitent toujours les rivières. Pour placer des objets le long des rivières, nous pouvons également les ajouter à l'intérieur TriangulateAdjacentToRiver
. Mais encore une fois seulement lorsque le triangle n'est pas sous l'eau et qu'il n'y a pas de route dessus. void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature((center + e.v1 + e.v5) * (1f / 3f)); } }
Des objets sont apparus à côté des rivières.Est-il possible de rendre autant d'objets?, dynamic batching Unity. , . batch. « », . instancing, dynamic batching.
paquet d'unitéVariété d'objets
Tous nos objets en relief ont la même orientation, ce qui n'a rien de naturel. Donnons à chacun une touche aléatoire. public void AddFeature (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); }
Tours aléatoires.Le résultat devient donc beaucoup plus diversifié. Malheureusement, chaque fois qu'un fragment est mis à jour, les objets reçoivent une nouvelle rotation aléatoire. La modification des cellules ne doit pas modifier les objets dans le voisinage, nous avons donc besoin d'une approche différente.Nous avons une texture de bruit qui est toujours la même. Cependant, cette texture contient du bruit de gradient Perlin et elle est localement cohérente. C'est exactement ce dont nous avons besoin pour déformer les positions des sommets dans les cellules. Mais les virages n'ont pas à être cohérents. Tous les virages doivent être également probables et mixtes. Par conséquent, nous avons besoin d'une texture avec des valeurs aléatoires non dégradées, qui peuvent être échantillonnées sans filtrage bilinéaire. Il s'agit essentiellement d'une grille de hachage qui constitue la base du bruit de gradient.Création d'une table de hachage
Nous pouvons créer une table de hachage à partir d'un tableau de valeurs flottantes et la remplir une fois avec des valeurs aléatoires. Grâce à cela, nous n'avons pas du tout besoin d'une texture. Ajoutons-le à HexMetrics
. Une taille de 256 par 256 suffit pour une variation suffisante. public const int hashGridSize = 256; static float[] hashGrid; public static void InitializeHashGrid () { hashGrid = new float[hashGridSize * hashGridSize]; for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = Random.value; } }
Les valeurs aléatoires sont générées par une formule mathématique qui donne toujours les mêmes résultats. La séquence résultante dépend du nombre de graines, qui par défaut est égal à la valeur actuelle du temps. C'est pourquoi à chaque session de jeu, nous obtiendrons des résultats différents.Pour garantir la recréation d'objets toujours identiques, nous devons ajouter le paramètre de départ à la méthode d'initialisation. public static void InitializeHashGrid (int seed) { hashGrid = new float[hashGridSize * hashGridSize]; Random.InitState(seed); for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = Random.value; } }
Maintenant que nous avons initialisé le flux de nombres aléatoires, nous en obtiendrons toujours la même séquence. Par conséquent, les événements apparemment aléatoires survenant après la génération de la carte seront également toujours les mêmes. Nous pouvons éviter cela en stockant l'état du générateur de nombres aléatoires avant de l'initialiser. Après avoir terminé le travail, nous pouvons lui demander l'ancien état. Random.State currentState = Random.state; Random.InitState(seed); for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = Random.value; } Random.state = currentState;
La table de hachage est initialisée HexGrid
en même temps qu'elle affecte la texture de bruit. Autrement dit, dans les méthodes HexGrid.Start
et HexGrid.Awake
. Nous faisons en sorte que les valeurs ne soient pas générées plus souvent que nécessaire. public int seed; void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); … } void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); } }
La variable de départ générique nous permet de sélectionner la valeur de départ pour la carte. N'importe quelle valeur fera l'affaire. J'ai choisi 1234.Le choix de la graine.Utilisation d'une table de hachage
Pour utiliser la table de hachage, ajoutez à la HexMetrics
méthode d'échantillonnage. Comme SampleNoise
, il utilise les coordonnées de la position XZ pour obtenir la valeur. L'index de hachage est trouvé en restreignant les coordonnées aux valeurs entières, puis en obtenant le reste de la division entière par la taille de la table. public static float SampleHashGrid (Vector3 position) { int x = (int)position.x % hashGridSize; int z = (int)position.z % hashGridSize; return hashGrid[x + z * hashGridSize]; }
Que fait%?, , — . , −4, −3, −2, −1, 0, 1, 2, 3, 4 modulo 3 −1, 0, −2, −1, 0, 1, 2, 0, 1.
Cela fonctionne pour les coordonnées positives, mais pas pour les coordonnées négatives, car pour de tels nombres, le reste sera négatif. Nous pouvons résoudre ce problème en ajoutant la taille du tableau aux résultats négatifs. int x = (int)position.x % hashGridSize; if (x < 0) { x += hashGridSize; } int z = (int)position.z % hashGridSize; if (z < 0) { z += hashGridSize; }
Maintenant, pour chaque unité carrée, nous créons notre propre valeur. Cependant, en fait, nous n'avons pas besoin d'une telle densité de table. Les objets sont espacés les uns des autres. Nous pouvons étirer le tableau en réduisant l'échelle de position avant de calculer l'indice. Une valeur unique pour un carré de 4 x 4 nous suffira. public const float hashGridScale = 0.25f; public static float SampleHashGrid (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]; }
Revenons à HexFeatureManager.AddFeature
et utilisons notre nouvelle table de hachage pour obtenir la valeur. Après l'avoir appliqué pour spécifier la rotation, les objets resteront immobiles lors de la modification du terrain. public void AddFeature (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); }
Seuil de placement
Bien que les objets aient des rotations différentes, un motif est toujours perceptible dans leur placement. Chaque cellule a sept objets. Nous pouvons ajouter du chaos à ce schéma, en sautant arbitrairement certains des objets. Comment décidons-nous d'ajouter ou non un objet? Bien sûr, vérifiez une autre valeur aléatoire!Autrement dit, au lieu d'une valeur de hachage, nous en avons besoin de deux. Leur prise en charge peut être ajoutée en utilisant des hachages au lieu d'une float
variable comme type de tableau de table Vector2
. Mais les opérations vectorielles n'ont pas de sens pour les valeurs de hachage, alors créons une structure spéciale à cet effet. Elle n'aura besoin que de deux valeurs flottantes. Et ajoutons une méthode statique pour créer une paire de valeurs aléatoires. using UnityEngine; public struct HexHash { public float a, b; public static HexHash Create () { HexHash hash; hash.a = Random.value; hash.b = Random.value; return hash; } }
Ne doit-il pas être sérialisé?, , Unity. , .
Modifiez-le HexMetrics
pour qu'il utilise la nouvelle structure. static HexHash[] hashGrid; public static void InitializeHashGrid (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; } public static HexHash SampleHashGrid (Vector3 position) { … }
A désormais HexFeatureManager.AddFeature
accès à deux valeurs de hachage. Utilisons le premier pour décider d'ajouter un objet ou de le sauter. Si la valeur est égale ou supérieure à 0,5, sautez. Ce faisant, nous nous débarrasserons d'environ la moitié des objets. La deuxième valeur sera utilisée comme d'habitude pour déterminer la rotation. public void AddFeature (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); }
La densité des objets est réduite de 50%.paquet d'unitéDessiner des objets
Au lieu de placer des objets partout, rendons-les modifiables. Mais nous ne dessinerons pas d'objets séparés, mais ajouterons le niveau des objets à chaque cellule. Ce niveau contrôlera la probabilité d'apparition d'objets dans la cellule. Par défaut, la valeur est zéro, c'est-à-dire que les objets seront absents.Puisque les cubes rouges sur notre terrain ne ressemblent pas à des objets naturels, appelons-les bâtiments. Ils représenteront l'urbanisation. Ajoutons au HexCell
niveau d'urbanisation. public int UrbanLevel { get { return urbanLevel; } set { if (urbanLevel != value) { urbanLevel = value; RefreshSelfOnly(); } } } int urbanLevel;
Nous pouvons rendre le niveau d'urbanisation d'une cellule sous-marine égal à zéro, mais ce n'est pas nécessaire, nous sautons de toute façon la création d'objets sous-marins. Et peut-être qu'à un moment donné, nous ajouterons des plans d'eau de l'urbanisation, tels que des quais et des structures sous-marines.Curseur de densité
Pour changer le niveau d'urbanisation, nous ajoutons HexMapEditor
un curseur supplémentaire à l' appui. int activeUrbanLevel; … bool applyUrbanLevel; … public void SetApplyUrbanLevel (bool toggle) { applyUrbanLevel = toggle; } public void SetUrbanLevel (float level) { activeUrbanLevel = (int)level; } void EditCell (HexCell cell) { if (cell) { … if (applyWaterLevel) { cell.WaterLevel = activeWaterLevel; } if (applyUrbanLevel) { cell.UrbanLevel = activeUrbanLevel; } if (riverMode == OptionalToggle.No) { cell.RemoveRiver(); } … } }
Ajoutez un autre curseur à l'interface utilisateur et combinez-le avec les méthodes appropriées. Je placerai un nouveau panneau sur le côté droit de l'écran pour éviter de déborder du panneau de gauche.De combien de niveaux avons-nous besoin? Arrêtons-nous sur quatre, dénotant une densité nulle, faible, moyenne et élevée.Curseur d'urbanisation.Changement de seuil
Maintenant que nous avons le niveau d'urbanisation, nous devons l'utiliser pour déterminer s'il faut placer des objets. Pour ce faire, nous devons ajouter le niveau d'urbanisation comme paramètre supplémentaire à HexFeatureManager.AddFeature
. Faisons un pas de plus et transférons simplement la cellule elle-même. À l'avenir, ce sera plus pratique pour nous.Le moyen le plus rapide d'utiliser le niveau d'urbanisation consiste à le multiplier par 0,25 et à utiliser la valeur comme nouveau seuil pour sauter des objets. De ce fait, la probabilité d'apparition de l'objet augmentera de 25% à chaque niveau. public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); if (hash.a >= cell.UrbanLevel * 0.25f) { return; } … }
Pour que cela fonctionne, passons les cellules à HexGridChunk
. void Triangulate (HexCell cell) { … if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } } void Triangulate (HexDirection direction, HexCell cell) { … if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature(cell, (center + e.v1 + e.v5) * (1f / 3f)); } … } … void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … if (!cell.IsUnderwater && !cell.HasRoadThroughEdge(direction)) { features.AddFeature(cell, (center + e.v1 + e.v5) * (1f / 3f)); } }
Dessin des niveaux de densité d'urbanisation.paquet d'unitéPlusieurs préfabriqués d'objets en relief
Les différences de probabilité d'apparition d'objets ne suffisent pas à créer une séparation claire entre les niveaux d'urbanisation faibles et élevés. Dans certaines cellules, il y aura simplement plus ou moins que le nombre de bâtiments prévu. Nous pouvons rendre la différence plus claire en utilisant notre propre préfabriqué pour chaque niveau.Nous débarrasser des champs featurePrefab
dans HexFeatureManager
et le remplacer par un tableau d'urbanisation de maisons préfabriquées. Pour obtenir le préfabriqué approprié, nous allons soustraire un du niveau d'urbanisation et utiliser la valeur comme indice. <del>
Créez deux doublons du préfabriqué de l'objet, renommez-les et modifiez-les afin qu'ils indiquent trois niveaux d'urbanisation différents. Le niveau 1 est une faible densité, nous utilisons donc un cube avec une longueur unitaire d'un bord, désignant une cabane. J'élargirai le préfabriqué de niveau 2 à (1,5, 2, 1,5) pour qu'il ressemble à un bâtiment à deux étages. Pour les bâtiments de haut niveau 3, j'ai utilisé l'échelle (2, 5, 2).Utilisation de préfabriqués différents pour chaque niveau d'urbanisation.Mélange préfabriqué
Nous ne sommes pas tenus de nous limiter à une séparation stricte des types de bâtiments. Vous pouvez les mélanger un peu, comme cela se produit dans le monde réel. Au lieu d'un seuil par niveau, utilisons-en trois, un pour chaque type de bâtiment.Au niveau 1, nous utilisons le placement de cabanes dans 40% des cas. Il n'y aura aucun autre bâtiment ici. Pour le niveau, nous utilisons les trois valeurs (0,4, 0, 0).Au niveau 2, remplacez les cabanes par des bâtiments plus grands et ajoutez 20% de chances pour des cabanes supplémentaires. Nous ne ferons pas de grands immeubles. Autrement dit, nous utilisons le seuil de trois valeurs (0,2, 0,4, 0).Au niveau 3, nous remplaçons les bâtiments moyens par de grands, remplaçons à nouveau les cabanes et ajoutons 20% de chances supplémentaires aux cabanes. Les valeurs de seuil seront égales à (0,2, 0,2, 0,4).Autrement dit, l'idée est qu'avec l'augmentation du niveau d'urbanisation, nous moderniserons les bâtiments existants et en ajouterons de nouveaux dans des espaces vides. Pour supprimer un bâtiment existant, nous devons utiliser les mêmes intervalles de valeurs de hachage. Si les hachages entre 0 et 0,4 au niveau 1 étaient des cabanes, alors au niveau 3, le même intervalle créera de hauts bâtiments. Au niveau 3, les bâtiments de grande hauteur devraient être créés avec des hachages dans la plage 0–0,4, des bâtiments à deux étages dans la plage 0,4–0,6 et des cabanes dans la plage 0,6–0,8. Si vous les vérifiez du plus grand au plus petit, cela peut être fait en utilisant le triple des seuils (0,4, 0,6, 0,8). Les seuils de niveau 2 deviendront alors (0, 0,4, 0,6) et les seuils de niveau 1 deviendront (0, 0, 0,4).Enregistrons ces seuils dansHexMetrics
comme une collection de tableaux avec une méthode qui vous permet d'obtenir des seuils pour un certain niveau. Comme nous ne nous intéressons qu'aux niveaux avec des objets, nous ignorons le niveau 0. static float[][] featureThresholds = { new float[] {0.0f, 0.0f, 0.4f}, new float[] {0.0f, 0.4f, 0.6f}, new float[] {0.4f, 0.6f, 0.8f} }; public static float[] GetFeatureThresholds (int level) { return featureThresholds[level]; }
Ensuite, ajoutez à la HexFeatureManager
méthode qui utilise le niveau et la valeur de hachage pour sélectionner le préfabriqué. Si le niveau est supérieur à zéro, alors nous obtenons des seuils en utilisant un niveau réduit de un. Ensuite, nous parcourons les seuils jusqu'à ce que l'un d'eux dépasse la valeur de hachage. Cela signifie que nous avons trouvé un préfabriqué. Si nous ne trouvons pas, retournez null. Transform PickPrefab (int level, float hash) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return urbanPrefabs[i]; } } } return null; }
Cette approche nécessite de réorganiser les liens vers les préfabriqués afin qu'ils passent de haute à basse densité.Ordre préfabriqué inversé.Nous allons utiliser notre nouvelle méthode dans AddFeature
pour sélectionner un préfabriqué. Si nous ne le recevons pas, nous sautons l'objet. Sinon, créez-en une instance et continuez comme précédemment. public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position);
Mélangez les préfabriqués.Variations de niveau
Nous avons maintenant des bâtiments bien mixtes, mais jusqu'à présent, il n'y en a que trois. Nous pouvons encore augmenter la variabilité en reliant une collection de préfabriqués à chaque niveau de densité d'urbanisation. Après cela, il sera possible d'en choisir un au hasard. Cela nécessitera une nouvelle valeur aléatoire, alors ajoutez un troisième c HexHash
. public float a, b, c; public static HexHash Create () { HexHash hash; hash.a = Random.value; hash.b = Random.value; hash.c = Random.value; return hash; }
Transformons-le HexFeatureManager.urbanPrefabs
en un tableau de tableaux et ajoutons un PickPrefab
paramètre à la méthode choice
. Nous l'utilisons pour sélectionner l'index du tableau intégré, en le multipliant par la longueur de ce tableau et en le convertissant en entier. public Transform[][] urbanPrefabs; … Transform PickPrefab (int level, float hash, float choice) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return urbanPrefabs[i][(int)(choice * urbanPrefabs[i].Length)]; } } } return null; }
Justifions notre choix sur la valeur du second hachage (B). Ensuite, vous devez passer de B à C. public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); Transform prefab = PickPrefab(cell.UrbanLevel, hash.a, hash.b); if (!prefab) { return; } Transform instance = Instantiate(prefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = HexMetrics.Perturb(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash.c, 0f); instance.SetParent(container, false); }
Avant de continuer, nous devons considérer ce qui Random.value
pourrait renvoyer une valeur de 1. Pour cette raison, l'index du tableau peut aller au-delà. Pour éviter que cela ne se produise, modifions légèrement les valeurs de hachage. Nous les mettons simplement à l'échelle afin de ne pas nous soucier des spécificités que nous utilisons. public static HexHash Create () { HexHash hash; hash.a = Random.value * 0.999f; hash.b = Random.value * 0.999f; hash.c = Random.value * 0.999f; return hash; }
Malheureusement, l'inspecteur n'affiche pas les tableaux de tableaux. Par conséquent, nous ne pouvons pas les configurer. Pour contourner cette limitation, créez une structure sérialisable dans laquelle encapsuler le tableau intégré. Donnons-lui une méthode qui convertit de choix en index de tableau et retourne un préfabriqué. using UnityEngine; [System.Serializable] public struct HexFeatureCollection { public Transform[] prefabs; public Transform Pick (float choice) { return prefabs[(int)(choice * prefabs.Length)]; } }
Nous utilisons à la HexFeatureManager
place des tableaux intégrés un tableau de ces collections.
Nous pouvons maintenant attribuer plusieurs bâtiments à chaque niveau de densité. Puisqu'ils sont indépendants, nous n'avons pas à utiliser la même quantité par niveau. J'ai juste utilisé deux options par niveau, ajoutant une option inférieure plus longue à chacune. J'ai choisi les échelles pour eux (3,5, 3, 2), (2,75, 1,5, 1,5) et (1,75, 1, 1).Deux types de bâtiments par niveau de densité.paquet d'unitéPlusieurs types d'objets
Dans le schéma existant, nous pouvons créer des structures urbaines tout à fait dignes. Mais le relief peut contenir non seulement des bâtiments. Que diriez-vous des fermes ou des usines? Ajoutons aux HexCell
niveaux et pour eux. Ils ne s'excluent pas mutuellement et peuvent se mélanger. public int FarmLevel { get { return farmLevel; } set { if (farmLevel != value) { farmLevel = value; RefreshSelfOnly(); } } } public int PlantLevel { get { return plantLevel; } set { if (plantLevel != value) { plantLevel = value; RefreshSelfOnly(); } } } int urbanLevel, farmLevel, plantLevel;
Bien sûr, cela nécessite un support dans HexMapEditor
deux curseurs supplémentaires. int activeUrbanLevel, activeFarmLevel, activePlantLevel; bool applyUrbanLevel, applyFarmLevel, applyPlantLevel; … public void SetApplyFarmLevel (bool toggle) { applyFarmLevel = toggle; } public void SetFarmLevel (float level) { activeFarmLevel = (int)level; } public void SetApplyPlantLevel (bool toggle) { applyPlantLevel = toggle; } public void SetPlantLevel (float level) { activePlantLevel = (int)level; } … void EditCell (HexCell cell) { if (cell) { … if (applyUrbanLevel) { cell.UrbanLevel = activeUrbanLevel; } if (applyFarmLevel) { cell.FarmLevel = activeFarmLevel; } if (applyPlantLevel) { cell.PlantLevel = activePlantLevel; } … } }
Ajoutez-les à l'interface utilisateur.Trois curseurs.De plus, des collections supplémentaires seront nécessaires HexFeatureManager
. public HexFeatureCollection[] urbanCollections, farmCollections, plantCollections;
Trois collections d'objets en relief.J'ai créé pour les fermes et les usines deux préfabriqués par niveau de densité, ainsi que pour les collections de bâtiments. Pour chacun d'eux, j'ai utilisé des cubes. Les fermes ont un matériau vert clair, les plantes ont un matériau vert foncé.J'ai fait des cubes de ferme d'une hauteur de 0,1 unité pour indiquer des allotissements carrés de terres agricoles. En tant qu'échelles haute densité, j'ai choisi (2,5, 0,1, 2,5) et (3,5, 0,1, 2). En moyenne, les sites ont une superficie de 1,75 et une taille de 2,5 par 1,25. Un faible niveau de densité a été obtenu dans la zone 1 et une taille de 1,5 sur 0,75.Les plantes préfabriquées désignent de grands arbres et de grands arbustes. Les préfabriqués à haute densité sont les plus grands (1,25, 4,5, 1,25) et (1,5, 3, 1,5). Les échelles moyennes sont (0,75, 3, 0,75) et (1, 1,5, 1). Les plus petites plantes ont des tailles (0,5, 1,5, 0,5) et (0,75, 1, 0,75).Sélection de fonctions en relief
Chaque type d'objet doit recevoir sa propre valeur de hachage afin qu'ils aient des modèles de création différents et que vous puissiez les mélanger. Ajoutez HexHash
deux valeurs supplémentaires. public float a, b, c, d, e; public static HexHash Create () { HexHash hash; hash.a = Random.value * 0.999f; hash.b = Random.value * 0.999f; hash.c = Random.value * 0.999f; hash.d = Random.value * 0.999f; hash.e = Random.value * 0.999f; return hash; }
Vous devez maintenant HexFeatureManager.PickPrefab
travailler avec différentes collections. Ajoutez un paramètre pour simplifier le processus. Modifiez également le hachage utilisé par la variante du préfabriqué sélectionné en D et le hachage pour la rotation en E. Transform PickPrefab ( HexFeatureCollection[] collection, int level, float hash, float choice ) { if (level > 0) { float[] thresholds = HexMetrics.GetFeatureThresholds(level - 1); for (int i = 0; i < thresholds.Length; i++) { if (hash < thresholds[i]) { return collection[i].Pick(choice); } } } return null; } public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); Transform prefab = PickPrefab( urbanCollections, cell.UrbanLevel, hash.a, hash.d ); … instance.localRotation = Quaternion.Euler(0f, 360f * hash.e, 0f); instance.SetParent(container, false); }
AddFeature
Sélectionne actuellement l' urbanisation préfabriquée. C'est normal, nous avons besoin de plus d'options. Par conséquent, nous ajoutons un autre préfabriqué des fermes. Comme valeur de hachage, utilisez B. Le choix de l'option sera à nouveau D. Transform prefab = PickPrefab( urbanCollections, cell.UrbanLevel, hash.a, hash.d ); Transform otherPrefab = PickPrefab( farmCollections, cell.FarmLevel, hash.b, hash.d ); if (!prefab) { return; }
Quel type d'instance préfabriquée allons-nous créer en conséquence? Si l'un d'eux s'avère nul, le choix est évident. Cependant, si les deux existent, nous devons prendre une décision. Ajoutons simplement le préfabriqué avec la valeur de hachage la plus faible. Transform otherPrefab = PickPrefab( farmCollections, cell.FarmLevel, hash.b, hash.d ); if (prefab) { if (otherPrefab && hash.b < hash.a) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } else { return; }
Un mélange d'objets urbains et ruraux.Ensuite, faites de même avec les plantes en utilisant la valeur du hachage C. if (prefab) { if (otherPrefab && hash.b < hash.a) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } otherPrefab = PickPrefab( plantCollections, cell.PlantLevel, hash.c, hash.d ); if (prefab) { if (otherPrefab && hash.c < hash.a) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } else { return; }
Cependant, nous ne pouvons pas simplement copier le code. Lorsque nous choisissons l'objet rural au lieu de l'objet urbain, nous devons comparer le hachage des plantes avec le hachage des fermes, et non avec celui de la ville. Par conséquent, nous devons suivre le hachage que nous avons décidé de choisir et le comparer. float usedHash = hash.a; if (prefab) { if (otherPrefab && hash.b < hash.a) { prefab = otherPrefab; usedHash = hash.b; } } else if (otherPrefab) { prefab = otherPrefab; usedHash = hash.b; } otherPrefab = PickPrefab( plantCollections, cell.PlantLevel, hash.c, hash.d ); if (prefab) { if (otherPrefab && hash.c < usedHash) { prefab = otherPrefab; } } else if (otherPrefab) { prefab = otherPrefab; } else { return; }
Un mélange d'objets urbains, ruraux et végétaux.paquet d'unitéPartie 10: murs
- Nous enfermons les cellules.
- Nous construisons des murs le long des bords des cellules.
- Parcourons les rivières et les routes.
- Évitez l'eau et connectez-vous avec les falaises.
Dans cette partie, nous ajouterons entre les cellules du mur.Il n'y a rien de plus invitant qu'un haut mur.Modification des murs
Pour soutenir les murs, nous devons savoir où les placer. Nous les placerons entre les cellules le long des bords les reliant. Étant donné que les objets déjà existants sont situés dans la partie centrale des cellules, nous n'avons pas à craindre que les murs les traversent.Murs le long des bords.Les murs sont des objets de terrain, bien que grands. Comme les autres objets, nous ne les éditerons pas directement. Au lieu de cela, nous allons changer les cellules. Nous n'aurons pas de segments séparés des murs, mais nous nous engagerons à enfermer les cellules dans leur ensemble.Propriété fortifiée
Pour prendre en charge les cellules clôturées, ajoutez à la HexCell
propriété Walled
. Il s'agit d'un simple interrupteur. Étant donné que les murs sont situés entre les cellules, nous devons mettre à jour les cellules modifiées et leurs voisins. public bool Walled { get { return walled; } set { if (walled != value) { walled = value; Refresh(); } } } bool walled;
Commutateur de l'éditeur
Pour commuter l'état «clôturé» des cellules, nous devons ajouter la HexMapEditor
prise en charge du commutateur. Par conséquent, nous ajoutons un autre champ OptionalToggle
et une méthode pour le définir. OptionalToggle riverMode, roadMode, walledMode; … public void SetWalledMode (int mode) { walledMode = (OptionalToggle)mode; }
Contrairement aux rivières et aux routes, les murs ne vont pas de cellule en cellule, mais sont entre eux. Par conséquent, nous n'avons pas besoin de penser au glisser-déposer. Lorsque l'interrupteur mural est actif, nous définissons simplement l'état clôturé de la cellule actuelle en fonction de l'état de cet interrupteur. void EditCell (HexCell cell) { if (cell) { … if (roadMode == OptionalToggle.No) { cell.RemoveRoads(); } if (walledMode != OptionalToggle.Ignore) { cell.Walled = walledMode == OptionalToggle.Yes; } if (isDrag) { … } } }
Nous dupliquons l'un des éléments précédents des commutateurs d'interface utilisateur et les modifions pour qu'ils contrôlent l'état de "clôture". Je les mettrai dans le panneau d'interface utilisateur avec d'autres objets.L'interrupteur "escrime".paquet d'unitéCréation de murs
Étant donné que les murs suivent les contours des cellules, ils ne devraient pas avoir une forme constante. Par conséquent, nous ne pouvons pas simplement utiliser un préfabriqué pour eux, comme nous l'avons fait avec d'autres caractéristiques du terrain. Au lieu de cela, nous devons construire un maillage, comme nous l'avons fait avec le relief. Cela signifie que notre fragment préfabriqué a besoin d'un autre élément enfant HexMesh
. Dupliquez l'un des autres maillages enfants et créez des ombres sur les nouveaux objets Murs . Ils n'ont besoin de rien sauf des sommets et des triangles, donc toutes les options HexMesh
doivent être désactivées.Murs préfabriqués subsidiaires.Il sera logique que les murs soient un objet urbain, donc pour eux j'ai utilisé le matériau rouge des bâtiments.Gestion des murs
Les murs étant des objets de relief, ils doivent y faire face HexFeatureManager
. Par conséquent, nous donnerons au gestionnaire des objets en relief un lien vers l'objet Walls , et lui ferons appeler les méthodes Clear
et Apply
. public HexMesh walls; … public void Clear () { … walls.Clear(); } public void Apply () { walls.Apply(); }
Murs connectés au gestionnaire de topographie.Les murs ne devraient-ils pas être un enfant de fonctionnalités?, . , Walls Hex Grid Chunk .
Maintenant, nous devons ajouter une méthode au gestionnaire qui nous permet d'y ajouter des murs. Étant donné que les murs sont le long des bords entre les cellules, il a besoin de connaître les sommets correspondants des bords et des cellules. HexGridChunk
le fera traverser TriangulateConnection
, lors de la triangulation de la cellule et de l'un de ses voisins. De ce point de vue, la cellule actuelle est du côté le plus proche du mur et l'autre du côté le plus éloigné. public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { }
Nous appellerons cette nouvelle méthode HexGridChunk.TriangulateConnection
après la fin de tous les autres travaux de connexion et immédiatement avant la transition vers le triangle angulaire. Nous laisserons le gestionnaire des objets en relief décider lui-même où le mur doit être situé. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { … } else { … } features.AddWall(e1, cell, e2, neighbor); HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (direction <= HexDirection.E && nextNeighbor != null) { … } }
Construire un segment de mur
Le mur entier serpentera à travers plusieurs bords des cellules. Chaque bord ne contient qu'un seul élément de mur. Du point de vue de la cellule proche, le segment commence sur le côté gauche de la côte et se termine sur la droite. Ajoutons à une HexFeatureManager
méthode distincte qui utilise quatre sommets aux coins d'une arête. void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { }
Côtés proches et lointains.AddWall
peut appeler cette méthode avec les premier et dernier bords des bords. Mais les murs ne devraient être ajoutés que lorsque nous avons une connexion entre une cellule clôturée et une cellule non clôturée. Peu importe laquelle des cellules est à l'intérieur et laquelle est à l'extérieur, seule la différence dans leurs états est prise en compte. public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v5, far.v5); } }
Le segment le plus simple du mur est un quadruple, se tenant au milieu de la nervure. Nous trouverons ses pics inférieurs, interpolant vers le milieu du plus proche au plus éloigné. void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f); Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f); }
Quelle doit être la hauteur du mur? Fixons sa hauteur à HexMetrics
. Je leur ai fait la taille d'un niveau de hauteur de cellule. public const float wallHeight = 3f;
HexFeatureManager.AddWallSegment
peut utiliser cette hauteur pour positionner les troisième et quatrième sommets du quad, et également l'ajouter au maillage walls
. Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f); Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f); Vector3 v1, v2, v3, v4; v1 = v3 = left; v2 = v4 = right; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v1, v2, v3, v4);
Nous pouvons maintenant modifier les murs et ils seront affichés sous forme de bandes quadruples. Cependant, nous ne verrons pas de mur continu. Chaque quad est visible d'un seul côté. Son visage est dirigé vers la cellule à partir de laquelle il a été ajouté.Quad-parois unilatérales.Nous pouvons résoudre rapidement ce problème en ajoutant un deuxième quad tourné dans l'autre sens. walls.AddQuad(v1, v2, v3, v4); walls.AddQuad(v2, v1, v4, v3);
Murs bilatéraux.Maintenant, tous les murs sont visibles dans leur intégralité, mais il y a encore des trous dans les coins des cellules où les trois cellules se rencontrent. Nous les remplirons plus tard.Murs épais
Bien que les murs soient déjà visibles des deux côtés, ils n'ont pas d'épaisseur. En fait, les murs sont minces, comme du papier, et presque invisibles à un certain angle. Faisons-les donc ensemble en ajoutant de l'épaisseur. Réglez leur épaisseur HexMetrics
. J'ai choisi une valeur de 0,75 unité, cela me semblait convenir. public const float wallThickness = 0.75f;
Pour rendre deux murs épais, vous devez séparer deux quads sur les côtés. Ils doivent se déplacer dans des directions opposées. Un côté doit se déplacer vers le bord proche, l'autre vers le bord éloigné. Le vecteur de décalage pour cela est égal far - near
, mais pour laisser le haut du mur plat, nous devons définir son composant Y sur 0.Comme cela doit être fait pour les côtés gauche et droit du segment de mur, ajoutons un HexMetrics
vecteur de décalage à la méthode pour le calculer. public static Vector3 WallThicknessOffset (Vector3 near, Vector3 far) { Vector3 offset; offset.x = far.x - near.x; offset.y = 0f; offset.z = far.z - near.z; return offset; }
Pour que le mur reste au centre de la nervure, la distance réelle de mouvement le long de ce vecteur doit être égale à la moitié de l'épaisseur de chaque côté. Et pour nous assurer que nous avons vraiment bougé la bonne distance, nous normalisons le vecteur de déplacement avant de le mettre à l'échelle. return offset.normalized * (wallThickness * 0.5f);
Nous utilisons cette méthode HexFeatureManager.AddWallSegment
pour changer la position des quads. Comme le vecteur de déplacement va de la cellule la plus proche à la cellule éloignée, soustrayez-le du quadrilatère proche et ajoutez-le à la cellule éloignée. Vector3 left = Vector3.Lerp(nearLeft, farLeft, 0.5f); Vector3 right = Vector3.Lerp(nearRight, farRight, 0.5f); Vector3 leftThicknessOffset = HexMetrics.WallThicknessOffset(nearLeft, farLeft); Vector3 rightThicknessOffset = HexMetrics.WallThicknessOffset(nearRight, farRight); Vector3 v1, v2, v3, v4; v1 = v3 = left - leftThicknessOffset; v2 = v4 = right - rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v1, v2, v3, v4); v1 = v3 = left + leftThicknessOffset; v2 = v4 = right + rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v2, v1, v4, v3);
Murs avec décalages.Les quads sont désormais biaisés, bien que cela ne soit pas entièrement visible.Les épaisseurs de paroi sont-elles les mêmes?, «-» . , . . , . , . , - , . .
Hauts des murs
Pour rendre l'épaisseur du mur visible par le haut, nous devons ajouter un quadruple en haut du mur. La façon la plus simple de le faire est de se souvenir des deux sommets supérieurs du premier quad et de les connecter aux deux sommets supérieurs du deuxième quad. Vector3 v1, v2, v3, v4; v1 = v3 = left - leftThicknessOffset; v2 = v4 = right - rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v1, v2, v3, v4); Vector3 t1 = v3, t2 = v4; v1 = v3 = left + leftThicknessOffset; v2 = v4 = right + rightThicknessOffset; v3.y = v4.y = left.y + HexMetrics.wallHeight; walls.AddQuad(v2, v1, v4, v3); walls.AddQuad(t1, t2, v3, v4);
Murs avec dessus.Virage
Nous avons encore des trous aux coins des cellules. Pour les remplir, nous devons ajouter un segment à la zone triangulaire entre les cellules. Chaque coin relie trois cellules. Chaque cellule peut avoir ou non un mur. Autrement dit, huit configurations sont possibles.Configurations angulaires.Nous plaçons des murs uniquement entre des cellules avec différents états clôturés. Cela réduit le nombre de configurations à six. Dans chacun d'eux, une des cellules se trouve à l'intérieur de la courbe des murs. Considérons cette cellule comme un point de référence autour duquel le mur est courbé. Du point de vue de cette cellule, le mur commence par un bord commun à la cellule de gauche et se termine par un bord commun à la cellule de droite.Rôles cellulaires.Autrement dit, nous devons créer une méthode AddWallSegment
dont les paramètres sont trois sommets du coin. Bien que nous puissions écrire du code pour trianguler ce segment, il s'agit en fait d'un cas particulier de la méthode AddWallSegment
. Un point d'ancrage joue le rôle des deux sommets proches. void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { AddWallSegment(pivot, left, pivot, right); }
Ensuite, créez une variante de la méthode AddWall
pour les trois sommets de l'angle et leurs cellules. L'objectif de cette méthode est de déterminer l'angle, qui est le point de référence, s'il existe. Par conséquent, il doit considérer les huit configurations possibles et en appeler AddWallSegment
six. public void AddWall ( Vector3 c1, HexCell cell1, Vector3 c2, HexCell cell2, Vector3 c3, HexCell cell3 ) { if (cell1.Walled) { if (cell2.Walled) { if (!cell3.Walled) { AddWallSegment(c3, cell3, c1, cell1, c2, cell2); } } else if (cell3.Walled) { AddWallSegment(c2, cell2, c3, cell3, c1, cell1); } else { AddWallSegment(c1, cell1, c2, cell2, c3, cell3); } } else if (cell2.Walled) { if (cell3.Walled) { AddWallSegment(c1, cell1, c2, cell2, c3, cell3); } else { AddWallSegment(c2, cell2, c3, cell3, c1, cell1); } } else if (cell3.Walled) { AddWallSegment(c3, cell3, c1, cell1, c2, cell2); } }
Pour ajouter des segments d'angle, appelez cette méthode à la fin HexGridChunk.TriangulateCorner
. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); }
Des murs avec des coins, mais il y a encore des trous.Fermez les trous
Il y a encore des trous dans les murs car la hauteur des segments de mur est variable. Alors que les segments le long des bords sont de hauteur constante, les segments d'angle sont entre deux bords différents. Étant donné que chaque bord peut avoir sa propre hauteur, des trous apparaissent aux coins.Pour résoudre ce problème, modifiez- AddWallSegment
le afin qu'il stocke séparément les coordonnées Y des sommets supérieurs gauche et droit. float leftTop = left.y + HexMetrics.wallHeight; float rightTop = right.y + HexMetrics.wallHeight; Vector3 v1, v2, v3, v4; v1 = v3 = left - leftThicknessOffset; v2 = v4 = right - rightThicknessOffset; v3.y = leftTop; v4.y = rightTop; walls.AddQuad(v1, v2, v3, v4); Vector3 t1 = v3, t2 = v4; v1 = v3 = left + leftThicknessOffset; v2 = v4 = right + rightThicknessOffset; v3.y = leftTop; v4.y = rightTop; walls.AddQuad(v2, v1, v4, v3);
Murs fermés.Les murs sont maintenant fermés, mais vous voyez probablement encore des trous dans les ombres du mur. Cela est dû au paramètre Normal Bias du paramètre d' ombre directionnelle. Lorsqu'il est supérieur à zéro, les triangles des objets projetant une ombre se déplacent le long de la normale à la surface. Cela évite l'ombrage automatique, mais crée en même temps des trous dans les cas où les triangles regardent dans des directions différentes. Dans ce cas, des trous peuvent être créés dans l'ombre d'une géométrie fine, par exemple, comme nos murs.Vous pouvez vous débarrasser de ces artefacts d'ombre en réduisant le biais normal à zéro. Ou modifiez le mode de mur de rendu de maillage Cast Shadows sur Deux faces . Cela rendra l'objet de projection d'ombre rendu des deux côtés de chaque triangle de mur pour le rendu, ce qui fermera tous les trous.Il n'y a plus de trous dans les ombres.paquet d'unitéMur de rebord
Jusqu'à présent, nos murs sont assez droits. Pour un terrain plat, ce n'est pas mal du tout, mais cela semble étrange lorsque les murs coïncident avec les rebords. Cela se produit lorsqu'il existe une différence d'un niveau de hauteur entre les cellules des côtés opposés du mur.Murs droits sur les rebords.Suivez le bord
Au lieu de créer un segment pour le bord entier, nous en créerons un pour chaque partie de la bande de bord. Nous pouvons le faire en appelant quatre fois AddWallSegment
dans la version AddWall
Edge. public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v2, far.v2); AddWallSegment(near.v2, far.v2, near.v3, far.v3); AddWallSegment(near.v3, far.v3, near.v4, far.v4); AddWallSegment(near.v4, far.v4, near.v5, far.v5); } }
Murs courbes.Les murs reprennent désormais la forme des bords déformés. En combinaison avec les rebords, il semble beaucoup mieux. De plus, il crée des murs plus intéressants sur un relief plat.Placer des murs au sol
En regardant les murs sur les rebords, vous pouvez trouver un problème. Les murs pendent au sol! Cela est vrai pour les bords plats inclinés, mais généralement pas si perceptibles.Des murs suspendus en l'air.Pour résoudre le problème, nous devons abaisser les murs. Le moyen le plus simple consiste à abaisser tout le mur pour que son sommet reste plat. En même temps, une partie du mur sur le côté supérieur s'abaissera légèrement dans le relief, mais cela nous conviendra.Pour abaisser le mur, nous devons déterminer quel côté est le plus bas - près ou loin. Nous pouvons simplement utiliser la hauteur du côté le plus bas, mais nous n'avons pas besoin d'aller si bas. Vous pouvez interpoler la coordonnée Y de bas en haut avec un décalage d'un peu moins de 0,5. Étant donné que les murs ne deviennent occasionnellement plus hauts que la marche inférieure du rebord, nous pouvons utiliser la marche verticale du rebord comme décalage. Une épaisseur de paroi différente de la configuration de rebord peut nécessiter un décalage différent.Le mur abaissé.Ajoutons à la HexMetrics
méthode WallLerp
qui traite de cette interpolation, en plus de faire la moyenne des coordonnées X et Z des sommets proches et lointains. Il est basé sur une méthode TerraceLerp
. public const float wallElevationOffset = verticalTerraceStepSize; … public static Vector3 WallLerp (Vector3 near, Vector3 far) { near.x += (far.x - near.x) * 0.5f; near.z += (far.z - near.z) * 0.5f; float v = near.y < far.y ? wallElevationOffset : (1f - wallElevationOffset); near.y += (far.y - near.y) * v; return near; }
Forcez HexFeatureManager
cette méthode pour déterminer les sommets gauche et droit. void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { Vector3 left = HexMetrics.WallLerp(nearLeft, farLeft); Vector3 right = HexMetrics.WallLerp(nearRight, farRight); … }
Murs debout sur le sol.Changement de la distorsion du mur
Maintenant, nos murs sont en bon accord avec les différences de hauteur. Mais ils ne correspondent toujours pas entièrement aux bords déformés, bien qu'ils soient proches d'eux. Cela s'est produit parce que nous déterminons d'abord le sommet des murs, puis les déformons. Étant donné que ces sommets se situent quelque part entre les sommets des bords proches et éloignés, leur distorsion sera légèrement différente.Le fait que les murs suivent inexactement les nervures n'est pas un problème. Cependant, la distorsion des sommets de la paroi change d'épaisseur par ailleurs relativement uniforme. Si nous organisons les murs en fonction de sommets déformés, puis ajoutons des quadrilatères non déformés, leur épaisseur ne devrait pas varier beaucoup. void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { nearLeft = HexMetrics.Perturb(nearLeft); farLeft = HexMetrics.Perturb(farLeft); nearRight = HexMetrics.Perturb(nearRight); farRight = HexMetrics.Perturb(farRight); … walls.AddQuadUnperturbed(v1, v2, v3, v4); … walls.AddQuadUnperturbed(v2, v1, v4, v3); walls.AddQuadUnperturbed(t1, t2, v3, v4); }
Les sommets non déformés des murs.Grâce à cette approche, les murs ne suivront plus les bords aussi précisément qu'auparavant. Mais en retour, ils deviendront moins cassés et auront une épaisseur plus constante.Épaisseur de paroi plus uniforme.paquet d'unitéTrous dans les murs
Jusqu'à présent, nous avons ignoré la possibilité d'une rivière ou d'une route traversant le mur. Lorsque cela se produit, nous devons faire un trou dans le mur à travers lequel une rivière ou une route peut passer.Pour ce faire, ajoutez AddWall
deux paramètres booléens pour indiquer si une rivière ou une route passe par un bord. Bien que nous puissions les gérer différemment, supprimons simplement les deux segments intermédiaires dans les deux cas. public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell, bool hasRiver, bool hasRoad ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v2, far.v2); if (hasRiver || hasRoad) {
Maintenant, il HexGridChunk.TriangulateConnection
devrait fournir les données nécessaires. Comme il avait déjà besoin des mêmes informations, mettons-les en cache dans des variables booléennes et enregistrons les appels aux méthodes correspondantes une seule fois. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … bool hasRiver = cell.HasRiverThroughEdge(direction); bool hasRoad = cell.HasRoadThroughEdge(direction); if (hasRiver) { … } if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { TriangulateEdgeStrip(e1, cell.Color, e2, neighbor.Color, hasRoad); } features.AddWall(e1, cell, e2, neighbor, hasRiver, hasRoad); … }
Les trous dans les murs pour le passage des rivières et des routes.Nous couvrons les murs
Ces nouvelles ouvertures créent des endroits pour compléter les murs. Nous devons fermer ces points d'extrémité avec des quads afin de ne pas pouvoir regarder à travers les côtés des murs. Créons une HexFeatureManager
méthode à cet effet AddWallCap
. Cela fonctionne comme AddWallSegment
, mais il n'a besoin que d'une paire de pics proches. Faites-lui ajouter un quad, allant du côté le plus éloigné du mur. void AddWallCap (Vector3 near, Vector3 far) { near = HexMetrics.Perturb(near); far = HexMetrics.Perturb(far); Vector3 center = HexMetrics.WallLerp(near, far); Vector3 thickness = HexMetrics.WallThicknessOffset(near, far); Vector3 v1, v2, v3, v4; v1 = v3 = center - thickness; v2 = v4 = center + thickness; v3.y = v4.y = center.y + HexMetrics.wallHeight; walls.AddQuadUnperturbed(v1, v2, v3, v4); }
Quand il AddWall
constate que nous avons besoin d'un trou, nous ajoutons un couvercle entre les deuxième et quatrième paires de bords des bords. Pour la quatrième paire de sommets, vous devez changer l'orientation, sinon la face quadruple regardera vers l'intérieur. public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell, bool hasRiver, bool hasRoad ) { if (nearCell.Walled != farCell.Walled) { AddWallSegment(near.v1, far.v1, near.v2, far.v2); if (hasRiver || hasRoad) { AddWallCap(near.v2, far.v2); AddWallCap(far.v4, near.v4); } … } }
Trous fermés dans les murs.Qu'en est-il des trous sur les bords de la carte?, . . , .
paquet d'unitéÉviter les falaises et l'eau
Enfin, regardons les bords contenant des falaises ou de l'eau. Étant donné que les falaises sont essentiellement de grands murs, il serait illogique d'y placer un mur supplémentaire. De plus, cela aura l'air mauvais. Les murs sous-marins sont également complètement illogiques, tout comme la restriction par les murs de la côte.Murs sur les falaises et dans l'eau.Nous pouvons supprimer les murs de ces bords inutiles avec des contrôles supplémentaires AddWall
. Un mur ne peut pas être sous l'eau et une côte commune avec lui ne peut pas être une falaise. public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell, bool hasRiver, bool hasRoad ) { if ( nearCell.Walled != farCell.Walled && !nearCell.IsUnderwater && !farCell.IsUnderwater && nearCell.GetEdgeType(farCell) != HexEdgeType.Cliff ) { … } }
Les murs d'obstruction le long des nervures ont été enlevés, mais les coins sont restés en place.Suppression des coins des murs
La suppression des segments d'angle inutiles nécessitera un peu plus d'efforts. Le cas le plus simple est lorsque la cellule de support est sous l'eau. Cela garantit qu'aucun segment de mur à proximité ne peut être connecté. void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { if (pivotCell.IsUnderwater) { return; } AddWallSegment(pivot, left, pivot, right); }
Il n'y a plus de cellules de support sous-marines.Maintenant, nous devons examiner deux autres cellules. Si l'un d'eux est sous l'eau ou connecté à la cellule de support par une rupture, il n'y a pas de paroi le long de cette nervure. Si cela est vrai pour au moins un côté, il ne doit pas y avoir de segment de mur dans ce coin.Nous déterminons individuellement s'il y a un mur gauche ou droit. Nous mettons les résultats dans des variables booléennes pour faciliter le travail avec. if (pivotCell.IsUnderwater) { return; } bool hasLeftWall = !leftCell.IsUnderwater && pivotCell.GetEdgeType(leftCell) != HexEdgeType.Cliff; bool hasRighWall = !rightCell.IsUnderwater && pivotCell.GetEdgeType(rightCell) != HexEdgeType.Cliff; if (hasLeftWall && hasRighWall) { AddWallSegment(pivot, left, pivot, right); }
Suppression de tous les angles gênants.Fermez les coins
Lorsqu'il n'y a pas de mur sur le bord gauche ou droit, le travail est terminé. Mais si le mur est dans une seule direction, cela signifie qu'il y a un autre trou dans le mur. Par conséquent, vous devez le fermer. if (hasLeftWall) { if (hasRighWall) { AddWallSegment(pivot, left, pivot, right); } else { AddWallCap(pivot, left); } } else if (hasRighWall) { AddWallCap(right, pivot); }
Nous fermons les murs.Connexion des murs aux falaises
Dans une situation, les murs semblent imparfaits. Lorsque le mur atteint le bas de la falaise, il se termine. Mais comme les falaises ne sont pas complètement verticales, un trou étroit est créé entre le mur et le bord de la falaise. Au sommet de la falaise, un tel problème ne se pose pas.Trous entre murs et faces de falaises.Ce serait beaucoup mieux si le mur continuait jusqu'au bord même de la falaise. Nous pouvons le faire en ajoutant un autre segment de mur entre l'extrémité actuelle du mur et le coin supérieur de la falaise. Étant donné que la majeure partie de ce segment sera cachée à l'intérieur de la falaise, nous pouvons faire sans réduire l'épaisseur de la paroi à l'intérieur de la falaise à zéro. Ainsi, il nous suffit de créer un coin: deux quadruples allant jusqu'au point et un triangle au-dessus d'eux. Créons une méthode à cet effet AddWallWedge
. Cela peut être fait en copiant AddWallCap
et en ajoutant un point de calage. void AddWallWedge (Vector3 near, Vector3 far, Vector3 point) { near = HexMetrics.Perturb(near); far = HexMetrics.Perturb(far); point = HexMetrics.Perturb(point); Vector3 center = HexMetrics.WallLerp(near, far); Vector3 thickness = HexMetrics.WallThicknessOffset(near, far); Vector3 v1, v2, v3, v4; Vector3 pointTop = point; point.y = center.y; v1 = v3 = center - thickness; v2 = v4 = center + thickness; v3.y = v4.y = pointTop.y = center.y + HexMetrics.wallHeight;
Dans les AddWallSegment
coins, nous appellerons cette méthode lorsque le mur ne va que dans une seule direction et que ce mur est à une hauteur inférieure à l'autre côté. C'est dans ces conditions que nous rencontrons le bord d'une falaise. if (hasLeftWall) { if (hasRighWall) { AddWallSegment(pivot, left, pivot, right); } else if (leftCell.Elevation < rightCell.Elevation) { AddWallWedge(pivot, left, right); } else { AddWallCap(pivot, left); } } else if (hasRighWall) { if (rightCell.Elevation < leftCell.Elevation) { AddWallWedge(right, pivot, left); } else { AddWallCap(right, pivot); } }
, .unitypackage11:
.Dans la partie précédente, nous avons ajouté un support mural. Ce sont de simples segments de murs droits sans différences apparentes. Maintenant, nous allons rendre les murs plus intéressants en y ajoutant des tours.Les segments de mur doivent être créés de manière procédurale pour correspondre au relief. Ce n'est pas obligatoire pour les tours, on peut utiliser le préfabriqué habituel.Nous pouvons créer une simple tour de deux cubes avec un matériau rouge. La base de la tour a une taille de 2 par 2 unités et une hauteur de 4 unités, c'est-à-dire qu'elle est plus épaisse et plus haute que le mur. Au-dessus de ce cube, nous placerons un cube unitaire désignant le sommet de la tour. Comme tous les autres préfabriqués, ces cubes ne nécessitent pas de collisionneurs.Le modèle de tour étant composé de plusieurs objets, nous en faisons des enfants de l'objet racine. Placez-les de sorte que l'origine locale de la racine soit à la base de la tour. Grâce à cela, nous pouvons placer les tours sans se soucier de leur hauteur.Tour préfabriquée.Ajoutez un lien vers ce préfabriqué HexFeatureManager
et connectez-le. public Transform wallTower;
Lien vers la tour préfabriquée.Construire des tours
Commençons par placer des tours au milieu de chaque segment de mur. Pour ce faire, nous allons créer une tour à la fin de la méthode AddWallSegment
. Sa position sera la moyenne des points gauche et droit du segment. void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { … Transform towerInstance = Instantiate(wallTower); towerInstance.transform.localPosition = (left + right) * 0.5f; towerInstance.SetParent(container, false); }
Une tour par segment de mur.Nous avons de nombreuses tours le long du mur, mais leur orientation ne change pas. Nous devons changer leur rotation pour qu'ils s'alignent avec le mur. Puisque nous avons les points droit et gauche du mur, nous savons quelle direction est droite. Nous pouvons utiliser ces connaissances pour déterminer l'orientation du segment de mur, et donc de la tour.Au lieu de calculer la rotation nous-mêmes, nous attribuons simplement un Transform.right
vecteur à la propriété . Le code Unity changera la rotation de l'objet pour que sa direction locale à droite corresponde au vecteur transmis. Transform towerInstance = Instantiate(wallTower); towerInstance.transform.localPosition = (left + right) * 0.5f; Vector3 rightDirection = right - left; rightDirection.y = 0f; towerInstance.transform.right = rightDirection; towerInstance.SetParent(container, false);
Les tours sont alignées avec le mur.Comment fonctionne l'affectation Transform.right?Quaternion.FromToRotation
. .
public Vector3 right { get { return rotation * Vector3.right; } set { rotation = Quaternion.FromToRotation(Vector3.right, value); } }
Réduisez le nombre de tours
Une tour par segment de mur, c'est trop. Rendons l'ajout de la tour facultatif en ajoutant un AddWallSegment
paramètre au booléen. Réglez-le sur la valeur par défaut false
. Dans ce cas, toutes les tours disparaîtront. void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight, bool addTower = false ) { … if (addTower) { Transform towerInstance = Instantiate(wallTower); towerInstance.transform.localPosition = (left + right) * 0.5f; Vector3 rightDirection = right - left; rightDirection.y = 0f; towerInstance.transform.right = rightDirection; towerInstance.SetParent(container, false); } }
Plaçons les tours uniquement dans les coins des cellules. En conséquence, nous obtenons moins de tours avec des distances assez constantes entre elles. void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … AddWallSegment(pivot, left, pivot, right, true); … }
Les tours ne sont que dans les coins.Il semble assez bon, mais nous aurons peut-être besoin d'un placement moins périodique des tours. Comme pour les autres caractéristiques du terrain, nous pouvons utiliser la table de hachage pour décider de placer la tour dans un coin. Pour ce faire, nous utilisons le centre du coin pour échantillonner le tableau, puis nous comparerons l'une des valeurs de hachage avec la valeur de seuil des tours. HexHash hash = HexMetrics.SampleHashGrid( (pivot + left + right) * (1f / 3f) ); bool hasTower = hash.e < HexMetrics.wallTowerThreshold; AddWallSegment(pivot, left, pivot, right, hasTower);
La valeur seuil fait référence à HexMetrics
. Avec une valeur de 0,5, des tours seront créées dans la moitié des cas, mais nous pouvons créer des murs avec de nombreuses tours ou sans elles. public const float wallTowerThreshold = 0.5f;
Tours aléatoires.Nous enlevons les tours des pistes
Maintenant, nous plaçons des tours quelle que soit la forme du terrain. Cependant, sur les pentes de la tour, cela semble illogique. Ici, les murs s'inclinent et peuvent traverser le haut de la tour.Tours sur les pistes.Pour éviter les pentes, nous vérifierons si les cellules des coins droit et gauche sont à la même hauteur. Ce n'est que dans ce cas qu'il est possible de placer une tour. bool hasTower = false; if (leftCell.Elevation == rightCell.Elevation) { HexHash hash = HexMetrics.SampleHashGrid( (pivot + left + right) * (1f / 3f) ); hasTower = hash.e < HexMetrics.wallTowerThreshold; } AddWallSegment(pivot, left, pivot, right, hasTower);
Il n'y a plus de tours sur les murs des pistes.Nous avons mis les murs et les tours au sol
Bien que nous évitions les murs sur les pentes, le relief des deux côtés du mur peut toujours avoir des hauteurs différentes. Les murs peuvent courir le long des rebords et les cellules de même hauteur peuvent avoir différentes positions verticales. Pour cette raison, la base de la tour peut être en l'air.Tours en l'air.En fait, les murs des pentes peuvent également pendre en l'air, mais ce n'est pas aussi visible que pour les tours.Les murs sont dans l'air.Cela peut être corrigé en étirant la base des murs et des tours au sol. Pour ce faire, ajoutez le décalage Y pour les murs HexMetrics
. Une unité en bas suffira. Augmentez la hauteur des tours du même montant. public const float wallHeight = 4f; public const float wallYOffset = -1f;
Nous la modifions HexMetrics.WallLerp
pour que lors de la détermination de la coordonnée Y, elle tienne compte du nouveau décalage. public static Vector3 WallLerp (Vector3 near, Vector3 far) { near.x += (far.x - near.x) * 0.5f; near.z += (far.z - near.z) * 0.5f; float v = near.y < far.y ? wallElevationOffset : (1f - wallElevationOffset); near.y += (far.y - near.y) * v + wallYOffset; return near; }
Nous devons également changer le préfabriqué de la tour, car la base sera désormais à une unité sous le sol. Par conséquent, nous augmentons la hauteur du cube de base d'une unité et modifions en conséquence la position locale des cubes.Murs et tours au sol.paquet d'unitéPonts
À ce stade, nous avons des rivières et des routes, mais les routes ne peuvent en aucun cas traverser des rivières. C'est le bon moment pour ajouter des ponts.Commençons par un simple cube à l'échelle qui jouera le rôle d'un pont préfabriqué. La largeur des rivières varie, mais il y a environ sept unités de distance entre les centres routiers des deux côtés. Par conséquent, nous lui donnons une échelle approximative (3, 1, 7). Ajoutez du matériau urbain rouge préfabriqué et débarrassez-vous de son collisionneur. Comme pour les tours, placez le cube à l'intérieur de l'objet racine avec la même échelle. Pour cette raison, la géométrie du pont lui-même ne sera pas importante.Ajoutez un lien vers le préfabriqué du pont HexFeatureManager
et attribuez-lui un préfabriqué. public Transform wallTower, bridge;
Pont préfabriqué attribué.Placement des ponts
Pour placer le pont, nous avons besoin d'une méthode HexFeatureManager.AddBridge
. Le pont doit être situé entre le centre de la rivière et l'un des côtés de la rivière. public void AddBridge (Vector3 roadCenter1, Vector3 roadCenter2) { Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.SetParent(container, false); }
Nous transmettrons les centres routiers non déformés, nous devrons donc les déformer avant de placer le pont. roadCenter1 = HexMetrics.Perturb(roadCenter1); roadCenter2 = HexMetrics.Perturb(roadCenter2); Transform instance = Instantiate(bridge);
Pour aligner correctement le pont, nous pouvons utiliser la même approche que lors de la rotation des tours. Dans ce cas, les centres routiers définissent le vecteur avant du pont. Puisque nous restons dans la même cellule, ce vecteur sera certainement horizontal, nous n'avons donc pas besoin de mettre à zéro sa composante Y. Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.forward = roadCenter2 - roadCenter1; instance.SetParent(container, false);
Nous construisons des ponts sur des rivières droites
Les seules configurations de rivière qui nécessitent des ponts sont droites et courbes. Les routes peuvent passer par des points d'extrémité et, en zigzags, les routes ne peuvent être qu'à proximité.Pour commencer, imaginons des rivières droites. A l'intérieur, le HexGridChunk.TriangulateRoadAdjacentToRiver
premier opérateur else if
aménage des routes à proximité de ces rivières. Par conséquent, nous allons ajouter ici des ponts.Nous sommes d'un côté de la rivière. Le centre de la route se déplace de la rivière, puis le centre de la cellule se déplace également. Pour trouver le centre de la route du côté opposé, nous devons déplacer la direction opposée du même montant. Cela doit être fait avant de changer le centre lui-même. void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … else if (cell.IncomingRiver == cell.OutgoingRiver.Opposite()) { … roadCenter += corner * 0.5f; features.AddBridge(roadCenter, center - corner * 0.5f); center += corner * 0.25f; } … }
Ponts sur les rivières droites.Des ponts sont apparus! Mais maintenant, nous avons une instance de ponts pour chaque direction à travers laquelle la rivière ne coule pas. Nous devons nous assurer qu'une seule instance du pont est générée dans la cellule. Cela peut être fait en choisissant une direction par rapport à la rivière et sur sa base pour générer un pont. Vous pouvez choisir n'importe quelle direction. roadCenter += corner * 0.5f; if (cell.IncomingRiver == direction.Next()) { features.AddBridge(roadCenter, center - corner * 0.5f); } center += corner * 0.25f;
De plus, nous devons ajouter un pont uniquement lorsqu'il y a une route des deux côtés de la rivière. Pour le moment, nous sommes déjà certains qu'il y a une route du côté actuel. Par conséquent, vous devez vérifier s'il y a une route de l'autre côté de la rivière. if (cell.IncomingRiver == direction.Next() && ( cell.HasRoadThroughEdge(direction.Next2()) || cell.HasRoadThroughEdge(direction.Opposite()) )) { features.AddBridge(roadCenter, center - corner * 0.5f); }
Ponts entre les routes des deux côtés.Ponts sur les rivières courbes
Les ponts sur les rivières courbes fonctionnent de la même façon, mais leur topologie est légèrement différente. Nous ajouterons un pont lorsque nous serons à l'extérieur de la courbe. Cela se produit dans le dernier bloc else
. Il utilise la direction médiane pour décaler le centre de la route. Nous devrons utiliser ce décalage deux fois avec différentes échelles, alors enregistrez-le dans une variable. void TriangulateRoadAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … 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; } Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; } … }
L'échelle de déplacement à l'extérieur de la courbe est de 0,25 et à l'intérieur HexMetrics.innerToOuter * 0.7f
. Nous l'utilisons pour placer le pont. Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) );
Ponts sur les rivières courbes.Là encore, nous devons éviter les ponts en double. Nous pouvons le faire en ajoutant des ponts uniquement à partir du milieu. Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; if (direction == middle) { features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); }
Et encore une fois, vous devez vous assurer que la route est du côté opposé. if ( direction == middle && cell.HasRoadThroughEdge(direction.Opposite()) ) { features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); }
Ponts entre les routes des deux côtés.Mise à l'échelle du pont
Puisque nous déformons le terrain, la distance entre les centres des routes et les côtés opposés de la rivière varie. Parfois les ponts sont trop courts, parfois trop longs.Distances variables mais longueurs de pont constantes.Bien que nous ayons créé un pont d'une longueur de sept unités, vous pouvez le mettre à l'échelle pour qu'il corresponde à la vraie distance entre les centres des routes. Cela signifie que le modèle de pont est déformé. Comme les distances ne varient pas beaucoup, la déformation peut être plus acceptable que les ponts qui ne conviennent pas à la longueur.Pour effectuer une mise à l'échelle appropriée, nous devons connaître la longueur initiale du pont préfabriqué. Nous conserverons cette longueur dans HexMetrics
. public const float bridgeDesignLength = 7f;
Nous pouvons maintenant attribuer l'échelle le long de l'instance Z du pont à la distance entre les centres des routes, divisée par la longueur d'origine. Étant donné que la racine du préfabriqué du pont a la même échelle, le pont s'étirera correctement. public void AddBridge (Vector3 roadCenter1, Vector3 roadCenter2) { roadCenter1 = HexMetrics.Perturb(roadCenter1); roadCenter2 = HexMetrics.Perturb(roadCenter2); Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.forward = roadCenter2 - roadCenter1; float length = Vector3.Distance(roadCenter1, roadCenter2); instance.localScale = new Vector3( 1f, 1f, length * (1f / HexMetrics.bridgeDesignLength) ); instance.SetParent(container, false); }
La longueur changeante des ponts.Construction de ponts
Au lieu d'un simple cube, nous pouvons utiliser un modèle de pont plus intéressant. Par exemple, vous pouvez créer un pont arqué grossier de trois cubes mis à l'échelle et tournés. Bien sûr, vous pouvez créer des modèles 3D beaucoup plus complexes, y compris des parties de la route. Mais notez que l'objet entier sera légèrement compressé et étiré.Ponts arqués de différentes longueurs.paquet d'unitéObjets spéciaux
Jusqu'à présent, nos cellules peuvent contenir des objets urbains, ruraux et végétaux. Même si chacun d'eux a trois niveaux, tous les objets sont assez petits par rapport à la taille de la cellule. Et si nous avons besoin d'un grand bâtiment, comme un château?Ajoutons un type spécial d'objet au terrain. Ces objets sont si gros qu'ils occupent toute la cellule. Chacun de ces objets est unique et a besoin de son propre préfabriqué. Par exemple, un château simple peut être créé à partir d'un cube central et de quatre tours d'angle. L'échelle (6, 4, 6) du cube central créera une serrure suffisamment grande, qui s'insère néanmoins même dans une cellule fortement déformée.Préfabriqué du château.Un autre objet spécial peut être une ziggourat, par exemple, constituée de trois cubes placés les uns sur les autres. Pour le cube inférieur, l'échelle (8, 2,5, 8) convient.Ziggourat préfabriqué.Les objets spéciaux peuvent être quelconques, pas nécessairement architecturaux. Par exemple, un groupe d'arbres massifs jusqu'à dix unités de haut peut indiquer une cellule remplie de mégaflore.Mégaflore préfabriquée.Ajoutez au HexFeatureManager
tableau pour suivre ces préfabriqués. public Transform[] special;
Tout d'abord, ajoutez un château au tableau, puis la ziggourat, puis la mégaflore.Personnalisation d'objets spéciaux.Rendre les cellules spéciales
Maintenant, HexCell
un index des objets spéciaux est requis, qui détermine le type d'un objet spécial, s'il est là. int specialIndex;
Comme d'autres objets en relief, donnons-lui la possibilité de recevoir et de définir cette valeur. public int SpecialIndex { get { return specialIndex; } set { if (specialIndex != value) { specialIndex = value; RefreshSelfOnly(); } } }
Par défaut, la cellule ne contient pas d'objet spécial. Nous désignons cela par l'index 0. Ajoutez une propriété qui utilise cette approche pour déterminer si une cellule est spéciale. public bool IsSpecial { get { return specialIndex > 0; } }
Pour modifier des cellules, ajoutez la prise en charge de l'index des objets spéciaux dans HexMapEditor
. Il fonctionne de manière similaire aux niveaux des installations urbaines, rurales et végétales. int activeUrbanLevel, activeFarmLevel, activePlantLevel, activeSpecialIndex; … bool applyUrbanLevel, applyFarmLevel, applyPlantLevel, applySpecialIndex; … public void SetApplySpecialIndex (bool toggle) { applySpecialIndex = toggle; } public void SetSpecialIndex (float index) { activeSpecialIndex = (int)index; } … void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (applyWaterLevel) { cell.WaterLevel = activeWaterLevel; } if (applySpecialIndex) { cell.SpecialIndex = activeSpecialIndex; } if (applyUrbanLevel) { cell.UrbanLevel = activeUrbanLevel; } … } }
Ajoutez un curseur à l'interface utilisateur pour contrôler l'objet spécial. Comme nous avons trois objets, nous utilisons l'intervalle 0–3 dans le curseur. Zéro signifiera l'absence d'un objet, un - un château, deux - ziggourat, trois - mégaflore.Curseur pour objets spéciaux.Ajout d'objets spéciaux
Nous pouvons maintenant attribuer des objets spéciaux aux cellules. Pour qu'ils apparaissent, nous devons ajouter à HexFeatureManager
une autre méthode. Il crée simplement une instance de l'objet spécial souhaité et le place dans la position souhaitée. Puisque zéro indique l'absence d'un objet, nous devons soustraire l'unité de l'index des objets spéciaux de la cellule avant d'avoir accès au tableau de préfabriqués. public void AddSpecialFeature (HexCell cell, Vector3 position) { Transform instance = Instantiate(special[cell.SpecialIndex - 1]); instance.localPosition = HexMetrics.Perturb(position); instance.SetParent(container, false); }
Donnons à l'objet une rotation arbitraire en utilisant la table de hachage. public void AddSpecialFeature (HexCell cell, Vector3 position) { Transform instance = Instantiate(special[cell.SpecialIndex - 1]); instance.localPosition = HexMetrics.Perturb(position); HexHash hash = HexMetrics.SampleHashGrid(position); instance.localRotation = Quaternion.Euler(0f, 360f * hash.e, 0f); instance.SetParent(container, false); }
Lors de la triangulation d'une cellule, nous HexGridChunk.Triangulate
vérifierons si la cellule contient un objet spécial. Si oui, alors nous appelons notre nouvelle méthode, tout comme AddFeature
. void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } if (cell.IsSpecial) { features.AddSpecialFeature(cell, cell.Position); } }
Objets spéciaux. Ils sont beaucoup plus gros que d'habitude.Évitez les rivières
Étant donné que les objets spéciaux sont situés au centre des cellules, ils ne se combinent pas avec les rivières, car ils pendent au-dessus d'eux.Objets sur les rivières.Pour éviter la création d'objets spéciaux au-dessus des rivières, nous modifions la propriété HexCell.SpecialIndex
. Nous ne changerons l'indice que s'il n'y a pas de cours d'eau dans la cellule. public int SpecialIndex { … set { if (specialIndex != value && !HasRiver) { specialIndex = value; RefreshSelfOnly(); } } }
De plus, lors de l'ajout d'une rivière, nous devrons nous débarrasser de tous les objets spéciaux. La rivière devrait les laver. Cela peut être fait en HexCell.SetOutgoingRiver
définissant l'index des objets spéciaux sur 0 dans la méthode . public void SetOutgoingRiver (HexDirection direction) { … hasOutgoingRiver = true; outgoingRiver = direction; specialIndex = 0; neighbor.RemoveIncomingRiver(); neighbor.hasIncomingRiver = true; neighbor.incomingRiver = direction.Opposite(); neighbor.specialIndex = 0; SetRoad((int)direction, false); }
Nous évitons les routes
Comme les rivières, les routes sont également mal assorties d'objets spéciaux, mais tout n'est pas si terrible. Vous pouvez même laisser les routes telles quelles. Certaines installations peuvent être compatibles avec les routes, d'autres non. Par conséquent, vous pouvez les rendre dépendants de l'objet. Mais nous allons vous faciliter la tâche.Objets sur la route.Dans ce cas, laissez les objets spéciaux vaincre la route. Par conséquent, lors de la modification de l'index des objets spéciaux, nous supprimerons également toutes les routes de la cellule. public int SpecialIndex { … set { if (specialIndex != value && !HasRiver) { specialIndex = value; RemoveRoads(); RefreshSelfOnly(); } } }
Et si nous supprimons un objet particulier?0, , . .
De plus, cela signifie que lors de l'ajout de routes, nous devrons effectuer des vérifications supplémentaires. Nous ajouterons des routes uniquement lorsqu'aucune des cellules n'est une cellule avec un objet spécial. public void AddRoad (HexDirection direction) { if ( !roads[(int)direction] && !HasRiverThroughEdge(direction) && !IsSpecial && !GetNeighbor(direction).IsSpecial && GetElevationDifference(direction) <= 1 ) { SetRoad((int)direction, true); } }
Évitez les autres objets
Les objets spéciaux ne peuvent pas être mélangés avec d'autres types d'objets. S'ils se chevauchent, cela semblera désordonné. Cela peut aussi dépendre d'un objet particulier, mais nous utiliserons la même approche.Un objet qui croise d'autres objets.Dans ce cas, nous supprimerons les petits objets, comme s'ils étaient sous l'eau. Cette fois, nous nous enregistrerons HexFeatureManager.AddFeature
. public void AddFeature (HexCell cell, Vector3 position) { if (cell.IsSpecial) { return; } … }
Évitez l'eau
Nous avons également un problème avec l'eau. Les caractéristiques spéciales persisteront-elles pendant les inondations? Puisque nous détruisons les petits objets dans les cellules inondées, faisons de même avec les objets spéciaux.Objets dans l'eau.Dans, HexGridChunk.Triangulate
nous effectuerons la même vérification d'inondation pour les objets spéciaux et ordinaires. void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } if (!cell.IsUnderwater && cell.IsSpecial) { features.AddSpecialFeature(cell, cell.Position); } }
Puisque les deux opérateurs if
vérifient maintenant si la cellule est sous l'eau, nous pouvons transférer le test et le réaliser une seule fois. void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } if (!cell.IsUnderwater) { if (!cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell, cell.Position); } if (cell.IsSpecial) { features.AddSpecialFeature(cell, cell.Position); } } }
Pour les expériences, un tel nombre d'objets nous suffira.paquet d'unité