Partes 1-3: malla, colores y alturas de celdaPartes 4-7: baches, ríos y caminosPartes 8-11: agua, accidentes geográficos y murallasPartes 12-15: guardar y cargar, texturas, distanciasPartes 16-19: encontrar el camino, escuadrones de jugadores, animacionesPartes 20-23: niebla de guerra, investigación de mapas, generación de procedimientosPartes 24-27: ciclo del agua, erosión, biomas, mapa cilíndricoParte 8: agua
- Agregue agua a las células.
- Triangular la superficie del agua.
- Crea un surf con espuma.
- Combina agua y ríos.
Ya hemos agregado soporte de río, y en esta parte sumergiremos completamente las células en agua.
Se acerca el agua.Nivel del agua
La forma más fácil es implementar el soporte de agua al establecerlo en el mismo nivel. Todas las celdas cuya altura está por debajo de este nivel están sumergidas en agua. Pero una forma más flexible sería mantener el agua a diferentes alturas, así que hagamos que el nivel del agua cambie. Para esto,
HexCell
necesita monitorear su nivel de agua.
public int WaterLevel { get { return waterLevel; } set { if (waterLevel == value) { return; } waterLevel = value; Refresh(); } } int waterLevel;
Si lo desea, puede asegurarse de que ciertas características del relieve no existieran bajo el agua. Pero por ahora no haré esto. Cosas como caminos submarinos me convienen. Pueden considerarse áreas que se han inundado recientemente.
Células de inundación
Ahora que tenemos niveles de agua, la pregunta más importante es si las células están bajo el agua. Una celda está debajo del agua si su nivel de agua está por encima de su altura. Para obtener esta información, agregaremos una propiedad.
public bool IsUnderwater { get { return waterLevel > elevation; } }
Esto significa que cuando el nivel y la altura del agua son iguales, la celda se eleva por encima del agua. Es decir, la superficie real del agua está por debajo de esta altura. Al igual que con las superficies de los ríos, agreguemos el mismo desplazamiento:
HexMetrics.riverSurfaceElevationOffset
. Cambie su nombre a uno más general.
Cambie
HexCell.RiverSurfaceY
para que use el nuevo nombre. Luego agregamos una propiedad similar a la superficie del agua de la celda inundada.
public float RiverSurfaceY { get { return (elevation + HexMetrics.waterElevationOffset) * HexMetrics.elevationStep; } } public float WaterSurfaceY { get { return (waterLevel + HexMetrics.waterElevationOffset) * HexMetrics.elevationStep; } }
Edición de agua
Editar el nivel del agua es similar a cambiar la altura. Por lo tanto,
HexMapEditor
debe monitorear el nivel de agua activo y si debe aplicarse a las celdas.
int activeElevation; int activeWaterLevel; … bool applyElevation = true; bool applyWaterLevel = true;
Agregue métodos para conectar estos parámetros con la interfaz de usuario.
public void SetApplyWaterLevel (bool toggle) { applyWaterLevel = toggle; } public void SetWaterLevel (float level) { activeWaterLevel = (int)level; }
Y agregue el nivel de agua a
EditCell
.
void EditCell (HexCell cell) { if (cell) { if (applyColor) { cell.Color = activeColor; } if (applyElevation) { cell.Elevation = activeElevation; } if (applyWaterLevel) { cell.WaterLevel = activeWaterLevel; } … } }
Para agregar un nivel de agua a la interfaz de usuario, duplique la etiqueta y el control deslizante de altura, y luego cámbielos. Recuerde adjuntar sus eventos a los métodos apropiados.
Control deslizante de nivel de agua.paquete de la unidadTriangulación de agua
Para triangular el agua, necesitamos una nueva malla con nuevo material. Primero, crea un sombreador de
agua , duplicando el sombreador de
río . Cámbielo para que use la propiedad de 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" }
Cree un nuevo material con este sombreador duplicando el material
Agua y reemplazándolo con un sombreador. Deja la textura de ruido, porque la usaremos más tarde.
Material de agua.Agregue un nuevo hijo al prefabricado duplicando el hijo de
Rivers . No necesita coordenadas UV, y debe usar
agua . Como de costumbre, haremos esto creando una instancia del prefab, cambiándola y luego aplicando los cambios al prefab. Después de eso, deshazte de la instancia.
Niño Objeto Agua.Luego, agregue soporte de malla de agua a
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(); }
Y conéctelo al niño prefabricado.
El objeto Agua está conectado.Hexágonos de agua
Como el agua forma una segunda capa, demos nuestro propio método de triangulación para cada una de las direcciones. Necesitamos llamarlo solo cuando la celda está sumergida en agua.
void Triangulate (HexDirection direction, HexCell cell) { … if (cell.IsUnderwater) { TriangulateWater(direction, cell, center); } } void TriangulateWater ( HexDirection direction, HexCell cell, Vector3 center ) { }
Al igual que con los ríos, la altura de la superficie del agua no varía mucho en las celdas con el mismo nivel de agua. Por lo tanto, no parece que necesitemos costillas complejas. Un triángulo simple será suficiente.
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); }
Hexágonos de agua.Compuestos de agua
Podemos conectar células vecinas con agua con un cuadrángulo.
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); }
Conexiones de los bordes del agua.Y llena las esquinas con un triángulo.
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()) ); } }
Articulaciones de esquinas de agua.Ahora tenemos células de agua conectadas cuando están cerca. Dejan un espacio entre ellos y las células secas con una altura más alta, pero lo dejaremos para más adelante.
Niveles de agua armonizados
Presumimos que las células submarinas vecinas tienen el mismo nivel de agua. Si esto es así, entonces todo se ve bien, pero si se viola esta suposición, se producen errores.
Niveles de agua inconsistentes.Podemos hacer que el agua permanezca en el mismo nivel. Por ejemplo, cuando cambia el nivel de agua de una celda inundada, podemos propagar los cambios a las celdas vecinas para mantener los niveles sincronizados. Sin embargo, este proceso debe continuar hasta que encuentre células que no estén sumergidas en agua. Estas celdas definen los límites de la masa de agua.
El peligro de este enfoque es que puede salirse rápidamente de control. Si la edición no se realiza correctamente, el agua puede cubrir todo el mapa. Luego, todos los fragmentos deberán triangularse simultáneamente, lo que dará lugar a un gran salto en los retrasos.
Así que no lo hagamos todavía. Esta característica se puede agregar en un editor más complejo. Si bien la consistencia de los niveles de agua, dejamos la conciencia del usuario.
paquete de la unidadAnimacion de agua
En lugar de un color uniforme, crearemos algo parecido a las ondas. Como en otros sombreadores, por ahora no buscaremos gráficos hermosos, solo necesitamos designar las ondas.
Agua perfectamente plana.Hagamos lo que hicimos con los ríos. Probamos el ruido con la posición del mundo y lo agregamos a un color uniforme. Para animar la superficie, agregue tiempo a la coordenada 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; }
Desplazamiento de agua, tiempo × 10.Dos direcciones
Hasta ahora esto no es para nada como olas. Vamos a complicar la imagen agregando una segunda muestra de ruido
y esta vez agregando la coordenada U. Usamos un canal de ruido diferente para obtener dos patrones diferentes como resultado. Las olas terminadas serán estas dos muestras apiladas juntas.
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;
Al sumar ambas muestras, obtenemos resultados en el intervalo 0–2, por lo que debemos volver a escalarlo a 0–1. En lugar de simplemente dividir las ondas por la mitad, podemos usar la función de paso
smoothstep
para crear un resultado más interesante. Ponemos ¾ - 2 en 0–1 para que no haya ondas visibles en la superficie del agua.
float waves = noise1.z + noise2.x; waves = smoothstep(0.75, 2, waves);
Dos direcciones, tiempo × 10.Olas de mezcla
Todavía se nota que tenemos dos patrones de ruido en movimiento que en realidad no cambian. Sería más plausible si los patrones cambiaran. Podemos darnos cuenta de esto interpolando entre diferentes canales de muestras de ruido. Pero esto no se puede hacer de la misma manera, de lo contrario, toda la superficie del agua cambiará simultáneamente, y esto es muy notable. En cambio, crearemos una ola de confusión.
Crearemos una onda de mezcla con la ayuda de una sinusoide, que se mueve diagonalmente a lo largo de la superficie del agua. Haremos esto agregando las coordenadas mundiales X y Z y usando la suma como entrada a la función
sin
. Alejar para obtener bandas lo suficientemente grandes. Y, por supuesto, agreguemos el mismo valor para animarlos.
float blendWave = sin((IN.worldPos.x + IN.worldPos.z) * 0.1 + _Time.y);
Las ondas sinusoidales oscilan entre -1 y 1, y necesitamos un intervalo de 0-1. Puedes conseguirlo cuadrando la ola. Para ver un resultado aislado, úselo en lugar del color cambiado como valor de salida.
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);
Olas de mezcla.Para que las ondas de mezcla sean menos visibles, agrégueles algo de ruido de ambas muestras.
float blendWave = sin( (IN.worldPos.x + IN.worldPos.z) * 0.1 + (noise1.y + noise2.z) + _Time.y ); blendWave *= blendWave;
Ondas distorsionadas de mezcla.Finalmente, usamos una onda de mezcla para interpolar entre los dos canales de ambas muestras de ruido. Para una variación máxima, tome cuatro canales diferentes.
float waves = lerp(noise1.z, noise1.w, blendWave) + lerp(noise2.x, noise2.y, blendWave)
Mezcla de olas, tiempo × 2.paquete de la unidadLa costa
Hemos terminado con aguas abiertas, pero ahora necesitamos llenar el vacío en el agua a lo largo de la costa. Como debemos conformarnos a los contornos de la tierra, el agua costera requiere un enfoque diferente. Dividamos
TriangulateWater
en dos métodos: uno para aguas abiertas y otro para la costa. Para entender cuándo trabajamos con la costa, tenemos que mirar la celda vecina. Es decir, en
TriangulateWater
obtendremos un vecino. Si hay un vecino y él no está bajo el agua, entonces estamos tratando con la costa.
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) {
No hay triangulación a lo largo de la costa.Como la costa está distorsionada, debemos distorsionar los triángulos de agua a lo largo de la costa. Por lo tanto, necesitamos la parte superior de los bordes y el abanico de los triángulos.
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); }
Fans de triángulos a lo largo de la costa.Lo siguiente es una tira de costillas, como en un alivio normal. Sin embargo, no estamos obligados a limitarnos solo a ciertas áreas, porque solo llamamos a
TriangulateWaterShore
cuando nos encontramos con la costa, para lo cual siempre se necesita la franja.
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);
Rayas de costillas a lo largo de la costa.Del mismo modo, también debemos agregar un triángulo angular cada vez.
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()) ); }
Las esquinas de las costillas a lo largo de la costa.Ahora tenemos agua lista para la costa. Parte de ella siempre está debajo de la malla de relieve, por lo que no hay agujeros.
Costa UV
Podemos dejar todo como está, pero sería interesante si el agua costera tuviera su propio horario. Por ejemplo, el efecto de la espuma, que se agranda al acercarse a la costa. Para implementarlo, el sombreador debe saber qué tan cerca está el fragmento de la costa. Podemos transmitir esta información a través de coordenadas UV.
El agua abierta no tiene coordenadas UV y no necesita espuma. Solo se necesita para el agua cerca de la costa. Por lo tanto, los requisitos para ambos tipos de agua son bastante diferentes. Sería lógico crear su propia malla para cada tipo. Por lo tanto, agregamos soporte para otro objeto de malla a 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(); }
Esta nueva malla usará
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()) ); } }
Duplique el objeto de agua, conéctelo al prefabricado y configúrelo para que use coordenadas UV. También creamos un sombreador y material para el agua costera, duplicando el sombreador y el material de agua existentes.
Instalaciones de orilla de agua y material UV.Cambie el sombreador
Water Shore para que, en lugar de agua, muestre coordenadas UV.
fixed4 c = fixed4(IN.uv_MainTex, 1, 1)
Como todavía no se han establecido coordenadas, mostrará un color sólido. Gracias a esto, es fácil ver que la costa en realidad usa una malla separada con material.
Malla separada para la costa.Pongamos la información de la costa en la coordenada V. En el lado del agua, asígnele un valor de 0, en el lado de la tierra - valor 1. Dado que no necesitamos transmitir nada más, todas las coordenadas U simplemente serán 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) ); }
Las transiciones a las costas están mal.El código anterior funciona para bordes, pero está mal en algunos ángulos. Si el próximo vecino está bajo el agua, entonces este enfoque será correcto. Pero cuando el próximo vecino no está bajo el agua, el tercer pico del triángulo estará bajo tierra.
waterShore.AddTriangleUV( new Vector2(0f, 0f), new Vector2(0f, 1f), new Vector2(0f, nextNeighbor.IsUnderwater ? 0f : 1f) );
Las transiciones a las costas son correctas.Espuma en la costa
Ahora que las transiciones a la costa se implementan correctamente, puede usarlas para crear un efecto de espuma. La forma más fácil es agregar el valor de la costa a un color 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; }
Espuma lineal.Para hacer la espuma más interesante, multiplíquela por el cuadrado de la sinusoide.
float foam = sin(shore * 10); foam *= foam * shore;
Espuma cuadrada sinusoide de desvanecimiento.Hagamos el frente de espuma más grande cuando nos acercamos a la orilla. Esto se puede hacer tomando su raíz cuadrada antes de usar el valor de la costa.
float shore = IN.uv_MainTex.y; shore = sqrt(shore);
La espuma se vuelve más gruesa cerca de la orilla.Agregue distorsión para que se vea más natural. Hagamos que la distorsión sea más débil cuando nos acercamos a la costa. Por lo tanto, será mejor alinear la costa.
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;
Espuma con distorsión.Y, por supuesto, estamos animando todo esto: tanto una sinusoide como distorsiones.
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;
Espuma animadaAdemás de la espuma entrante, también hay un retroceso. Agreguemos una segunda sinusoide, que se mueve en la dirección opuesta, para simularla. Hazlo más débil y agrega un cambio de tiempo. La espuma terminada será el máximo de estos dos sinusoides.
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;
Espuma de entrada y retroceso.Mezcla de olas y espuma
Hay una transición brusca entre el agua abierta y costera porque las olas de agua abierta no están incluidas en el agua costera. Para solucionar esto, debemos incluir estas ondas en el sombreador
Water Shore .
En lugar de copiar el código de onda,
pegémoslo en el archivo de inclusión
Water.cginc . De hecho, insertamos código para la espuma y las ondas, cada una como una función separada.
¿Cómo funcionan los archivos de inclusión de sombreador?La creación de sus propios archivos de sombreador de inclusión se trata en el tutorial de
Rendering 5, Luces múltiples .
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); }
Cambie el sombreador de
agua para que use el nuevo archivo de inclusión.
#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; }
En el sombreador
Water Shore , los valores se calculan tanto para espuma como para olas. Luego amortiguamos las olas a medida que nos acercamos a la orilla. El resultado final será un máximo de espuma y olas.
#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; }
Una mezcla de espuma y olas.paquete de la unidadDe nuevo sobre el agua costera
Parte de la malla costera está oculta debajo de la malla en relieve. Esto es normal, pero solo una pequeña parte está oculta. Desafortunadamente, los acantilados empinados esconden la mayor parte del agua costera y, por lo tanto, la espuma.
Agua costera casi oculta.Podemos manejar esto aumentando el tamaño de la franja de la costa. Esto se puede hacer reduciendo el radio de los hexágonos de agua. Para esto, además del coeficiente de integridad, necesitamos un coeficiente de agua
HexMetrics
, así como métodos para obtener ángulos de agua.
El coeficiente de integridad es 0.8. Para duplicar el tamaño de los compuestos de agua, necesitamos establecer el coeficiente de agua en 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; }
Utilizaremos estos nuevos métodos HexGridChunk
para encontrar los ángulos del agua. 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) ); … }
Usando esquinas de agua.La distancia entre los hexágonos del agua en realidad se duplicó. Ahora HexMetrics
también debería tener un método para crear puentes en el agua. public const float waterBlendFactor = 1f - waterFactor; public static Vector3 GetWaterBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * waterBlendFactor; }
Cambie HexGridChunk
para que use el nuevo método. 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()) ); … } }
Largos puentes en el agua.Entre las costillas de agua y tierra
Aunque esto nos da más espacio para la espuma, ahora se oculta aún más debajo del relieve. Idealmente, podremos usar una costilla de agua en el lado del agua y una costilla de tierra en el lado de tierra.No podemos usar un puente simple para encontrar el borde opuesto de la tierra, si comenzamos desde las esquinas del agua. En cambio, podemos ir en la dirección opuesta, desde el centro del vecino. Cambiar TriangulateWaterShore
para usar este nuevo enfoque.
Esquinas incorrectas de los bordes.Esto funcionó, solo que ahora nuevamente necesitamos considerar dos casos para triángulos angulares. HexCell nextNeighbor = cell.GetNeighbor(direction.Next()); if (nextNeighbor != null) {
Las esquinas correctas de los bordes.Esto funcionó bien, pero ahora que la mayor parte de la espuma es visible, se está volviendo bastante pronunciada. Para compensar esto, haremos que el efecto sea un poco más débil al reducir la escala del valor de la costa en el sombreador. shore = sqrt(shore) * 0.9
Listo de espuma.paquete de la unidadRíos submarinos
Terminamos con agua, al menos en aquellos lugares donde no fluyen ríos. Como el agua y los ríos aún no se notan, los ríos fluirán a través y debajo del agua.Ríos que fluyen en el agua.El orden en que se representan los objetos translúcidos depende de su distancia de la cámara. Los objetos más cercanos se representan en último lugar, por lo que están en la parte superior. Al mover la cámara, esto significará que a veces los ríos y el agua aparecerán uno encima del otro. Comencemos haciendo constante el orden de representación. Los ríos deben dibujarse sobre el agua para que las cascadas se muestren correctamente. Podemos implementar esto cambiando la cola del sombreador River . Tags { "RenderType"="Transparent" "Queue"="Transparent+1" }
Dibujamos los ríos al final.Ocultando río submarino
Aunque el lecho del río bien puede estar bajo el agua, y el agua puede fluir a través de él, no deberíamos ver esta agua. Y aún más, no debe representarse sobre una superficie de agua real. Podemos deshacernos del agua de los ríos submarinos agregando segmentos de río solo cuando la celda actual no está bajo el agua. 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; … } }
Para TriangulateConnection
comenzar, agregaremos un segmento de río cuando ni la celda actual ni la vecina están bajo el agua. 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 ); } }
No más ríos submarinos.Cascadas
Ya no hay ríos submarinos, pero ahora tenemos agujeros en esos lugares de los ríos donde se encuentran con la superficie del agua. Los ríos en el mismo nivel que el agua crean pequeños agujeros o superposiciones. Pero las más notables son las cascadas que faltan para los ríos que fluyen desde una altura mayor. Cuidemos de ellos primero.Un segmento de río con una cascada solía pasar a través de la superficie del agua. Como resultado, se encontró parcialmente arriba y parcialmente debajo del agua. Necesitamos mantener una parte por encima del nivel del agua, descartando todo lo demás. Tendrás que trabajar duro para esto, así que crea un método separado.El nuevo método requiere cuatro picos, dos niveles de río y un nivel de agua. Lo configuraremos para que miremos en la dirección de la corriente, hacia abajo de la cascada. Por lo tanto, los dos primeros picos y los lados izquierdo y derecho estarán en la parte superior, y los inferiores seguirán. 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); }
Llamaremos a este método TriangulateConnection
cuando un vecino esté bajo el agua y creamos una cascada. 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 ); } }
También necesitamos procesar las cascadas en la dirección opuesta, cuando la celda actual está bajo el agua, y la siguiente no. 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 ); }
Así que nuevamente tenemos el quad del río original. Luego necesitamos cambiar TriangulateWaterfallInWater
para que eleve los picos más bajos al nivel del agua. Desafortunadamente, cambiar solo las coordenadas Y no será suficiente. Esto puede empujar la cascada desde el acantilado, que puede formar agujeros. En su lugar, debe mover los vértices inferiores a los superiores utilizando la interpolación.InterpolarPara mover los picos inferiores hacia arriba, divida su distancia debajo de la superficie del agua por la altura de la cascada. Esto nos dará un valor de interpolador. 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);
Como resultado, obtenemos una cascada acortada que tiene la misma orientación. Sin embargo, dado que las posiciones de los vértices inferiores han cambiado, no se distorsionarán como los vértices originales. Esto significa que el resultado final aún no coincidirá con la cascada original. Para resolver este problema, necesitamos distorsionar manualmente los vértices antes de interpolar, y luego agregar el quad no distorsionado. 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);
Como ya tenemos un método para agregar triángulos no distorsionados, realmente no necesitamos crear uno para quads. Por lo tanto, agregamos el método necesario 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); }
Las cascadas terminan en la superficie del agua.paquete de la unidadEstuarios
Cuando los ríos fluyen a la misma altura que la superficie del agua, la malla del río toca la malla costera. Si fuera un río que fluye hacia el mar o hacia el océano, entonces habría una corriente del río con un oleaje. Por lo tanto, llamaremos a estas áreas estuarios.El río se encuentra con la costa sin distorsionar los picos.Ahora tenemos dos problemas con la boca. En primer lugar, los ríos cuádruples conectan la segunda y cuarta parte superior de las costillas, omitiendo la tercera. Dado que la costa del agua no usa el tercer pico, puede crear un agujero o superposición. Podemos resolver este problema cambiando la geometría de las bocas.El segundo problema es que existe una fuerte transición entre la espuma y los materiales fluviales. Para resolverlo, necesitamos otro material que realice la mezcla de los efectos de un río y el agua.Esto significa que las bocas requieren un enfoque especial, así que creemos un método separado para ellas. Debe llamarse TriangulateWaterShore
cuando hay un río moviéndose en la dirección actual. 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) { }
No se requiere una región que mezcle ambos efectos para llenar toda la tira. La forma trapezoidal será suficiente para nosotros. Por lo tanto, podemos usar dos triángulos costeros en los lados. 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) ); }
Agujero trapezoidal para el área de mezcla.Coordenadas UV2
Para crear un efecto de río, necesitamos coordenadas UV. Pero para crear un efecto de espuma, también necesita coordenadas UV. Es decir, al mezclarlos, necesitamos dos conjuntos de coordenadas UV. Afortunadamente, las mallas del motor Unity pueden soportar hasta cuatro conjuntos UV. Solo necesitamos agregar HexMesh
soporte para el segundo conjunto. 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); } … }
Para agregar un segundo conjunto de UV, duplicamos los métodos de trabajo con UV y cambiamos la forma en que lo necesitamos. 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)); }
Función River Shader
Como usaremos el efecto de río en dos sombreadores, moveremos el código del sombreador de río a la nueva función de archivo de inclusión de agua . 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; }
Cambie el sombreador River para usar esta nueva función. #include "Water.cginc" sampler2D _MainTex; … void surf (Input IN, inout SurfaceOutputStandard o) { float river = River(IN.uv_MainTex, _MainTex); fixed4 c = saturate(_Color + river); … }
Objetos bucales
Agregue una HexGridChunk
boca para apoyar el objeto de malla. 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(); }
Crea un sombreador, material y un objeto de la boca, duplicando la costa y cambiándola. Conéctelo al fragmento y haga que use las coordenadas UV y UV2.Objetos Estuarties.Triangulación de la boca
Podemos resolver el problema del agujero o la superposición colocando un triángulo entre el final del río y la mitad del borde del agua. Dado que nuestro sombreador bucal es un duplicado del sombreador costero, establecemos las coordenadas UV para que coincidan con el efecto de espuma. 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) ); }
Triángulo medioPodemos llenar todo el trapecio agregando un quad a ambos lados del triángulo 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);
Listo trapecio.Giremos la orientación cuádruple hacia la izquierda para que tenga una conexión diagonal acortada, y como resultado obtenemos geometría simétrica. 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 girado, geometría simétricaFlujo del rio
Para apoyar el efecto del río, necesitamos agregar coordenadas UV2. La parte inferior del triángulo del medio está en el medio del río, por lo que su coordenada U debe ser igual a 0.5. Como el río fluye en la dirección del agua, el punto izquierdo recibe la coordenada U igual a 1, y el derecho recibe la coordenada U con un valor de 0. Establecemos las coordenadas Y en 0 y 1, correspondientes a la dirección de la corriente. estuaries.AddTriangleUV2( new Vector2(0.5f, 1f), new Vector2(1f, 0f), new Vector2(0f, 0f) );
Los cuadrángulos en ambos lados del triángulo deben coincidir con esta orientación. Mantenemos las mismas coordenadas U para puntos que exceden el ancho del río. 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) );
Trapecio UV2.Para asegurarse de que establecemos las coordenadas UV2 correctamente, haga que el sombreador del Estuario las procese. Podemos acceder a estas coordenadas agregando a la estructura de entrada 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); … }
Coordenadas UV2.Todo se ve bien, puedes usar un sombreador para crear un efecto de río. void surf (Input IN, inout SurfaceOutputStandard o) { … float river = River(IN.uv2_MainTex, _MainTex); fixed4 c = saturate(_Color + river); … }
Usa UV2 para crear un efecto de río.Creamos los ríos de tal manera que al triangular las conexiones entre las celdas, las coordenadas del río V cambian de 0.8 a 1. Por lo tanto, aquí también deberíamos usar este intervalo, y no de 0 a 1. Sin embargo, la conexión costera es un 50% más que las conexiones de celda ordinarias . Por lo tanto, para el mejor ajuste con el curso del río, debemos cambiar los valores de 0.8 a 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) );
Flujo sincronizado del río y el estuario.Ajuste de flujo
Mientras el río se mueve en línea recta. Pero cuando el agua fluye hacia un área más grande, se expande. La corriente se curvará. Podemos simular esto doblando las coordenadas UV2.En lugar de mantener constantes las coordenadas U superiores fuera del ancho del río, muévalas 0.5. El punto más a la izquierda es 1.5, el más a la derecha es −0.5.Al mismo tiempo, expandimos el flujo moviendo las coordenadas U de los puntos inferiores izquierdo y derecho. Cambie el izquierdo de 1 a 0.7 y el derecho de 0 a 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) );
Expansión del río.Para completar el efecto de curvatura, cambie las coordenadas V de los mismos cuatro puntos. Dado que el agua fluye desde el final del río, aumentaremos las coordenadas de V de los puntos superiores a 1. Y para crear una mejor curva, aumentaremos las coordenadas de V de los dos puntos inferiores a 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) );
El curso curvo del río.Mezcla de río y costa
Todo lo que nos queda es mezclar los efectos de la costa y el río. Para hacer esto, usamos interpolación lineal, tomando el valor de la costa como interpolador. 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);
Aunque esto debería funcionar, puede obtener un error de compilación. El compilador se queja de la redefinición _MainTex_ST
. La razón es un error dentro del compilador de sombreador de superficie Unity causado por el uso simultáneo de uv_MainTex
y uv2_MainTex
. Necesitamos encontrar una solución alternativa.En lugar de usarlo uv2_MainTex
, tendremos que transferir las coordenadas UV secundarias manualmente. Para hacer esto, cambie el nombre uv2_MainTex
a riverUV
. Luego agregue una función de vértice al sombreador, que le asigna coordenadas. #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); … }
Interpolación basada en el valor de la costa.La interpolación funciona, con la excepción de los vértices izquierdo y derecho en la parte superior. En estos puntos el río debería desaparecer. Por lo tanto, no podemos usar el valor de la costa. Tendremos que usar un valor diferente, que en estos dos vértices es 0. Afortunadamente, todavía tenemos la coordenada U del primer conjunto UV, por lo que podemos almacenar este valor allí. 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) );
La combinación correctaAhora las bocas tienen una buena mezcla entre el río en expansión, el agua costera y la espuma. Aunque esto no crea una coincidencia exacta con las cascadas, este efecto también se ve bien con las cascadas.Estuarios en acciónunitpackageRíos que fluyen de cuerpos de agua
Ya tenemos ríos que fluyen hacia cuerpos de agua, pero no hay soporte para ríos que fluyan en una dirección diferente. Hay lagos de los que fluyen los ríos, por lo que también debemos agregarlos.Cuando un río fluye de un cuerpo de agua, en realidad fluye hacia una altitud más alta. Esto actualmente no es posible. Necesitamos hacer una excepción y permitir esta situación si el nivel del agua corresponde a la altura del punto objetivo. Agreguemos a un HexCell
método privado que verifica de acuerdo con nuestro nuevo criterio si el vecino es el punto objetivo correcto para el río de salida. bool IsValidRiverDestination (HexCell neighbor) { return neighbor && ( elevation >= neighbor.elevation || waterLevel == neighbor.elevation ); }
Usaremos nuestro nuevo método para determinar si es posible crear un río saliente. public void SetOutgoingRiver (HexDirection direction) { if (hasOutgoingRiver && outgoingRiver == direction) { return; } HexCell neighbor = GetNeighbor(direction);
Además, allí debe verificar el río al cambiar la altura de la celda o el nivel del agua. Creemos un método privado que haga esta tarea. void ValidateRivers () { if ( hasOutgoingRiver && !IsValidRiverDestination(GetNeighbor(outgoingRiver)) ) { RemoveOutgoingRiver(); } if ( hasIncomingRiver && !GetNeighbor(incomingRiver).IsValidRiverDestination(this) ) { RemoveIncomingRiver(); } }
Utilizaremos este nuevo método en las propiedades Elevation
y WaterLevel
. public int Elevation { … set { …
Salientes y entrantes de lagos fluvialesCambiar el rumbo
Creamos HexGridChunk.TriangulateEstuary
, sugiriendo que los ríos solo pueden fluir hacia cuerpos de agua. Por lo tanto, como resultado, el curso del río siempre se mueve en una dirección. Necesitamos revertir el flujo cuando se trata de un río que fluye de un cuerpo de agua. Para hacer esto, necesita TriangulateEstuary
saber sobre la dirección del flujo. Por lo tanto, le damos un parámetro booleano que determina si estamos tratando con un río entrante. void TriangulateEstuary ( EdgeVertices e1, EdgeVertices e2, bool incomingRiver ) { … }
Pasaremos esta información cuando llamemos a este método desde TriangulateWaterShore
. if (cell.HasRiverThroughEdge(direction)) { TriangulateEstuary(e1, e2, cell.IncomingRiver == direction); }
Ahora necesitamos expandir el flujo del río cambiando las coordenadas de UV2. Las coordenadas U para los ríos salientes deben reflejarse: −0.5 se convierte en 1.5, 0 se convierte en 1, 1 se convierte en 0 y 1.5 se convierte en −0.5.Con las coordenadas V, las cosas son un poco más complicadas. Si observa cómo trabajamos con las conexiones fluviales invertidas, 0.8 debería ser 0 y 1 debería ser −0.2. Esto significa que 1.1 se convierte en −0.3 y 1.15 se convierte en −0.35.Como en cada caso las coordenadas UV2 son muy diferentes, escribamos un código separado para ellas. 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) ); } }
El curso correcto de los ríos.paquete de la unidadParte 9: características de alivio
- Añadir objetos al relieve.
- Creamos soporte para niveles de densidad de objetos.
- Usamos varios objetos en el nivel.
- Mezcla tres tipos diferentes de objetos.
En esta parte hablaremos sobre agregar objetos al terreno. Crearemos objetos como edificios y árboles.Conflicto entre bosques, tierras agrícolas y urbanización.Agregar soporte para objetos
Aunque la forma del relieve tiene variaciones, hasta ahora no está sucediendo nada. Esta es una tierra sin vida. Para darle vida, necesitas agregar tales objetos. como árboles y casas. Estos objetos no son parte de la malla en relieve, pero serán objetos separados. Pero esto no nos impide agregarlos al triangular el terreno.HexGridChunk
No me importa cómo funciona la malla. Simplemente le ordena a uno de sus hijos que HexMesh
agregue un triángulo o un quad. Del mismo modo, puede tener un elemento hijo que se ocupe de la colocación de objetos en ellos.Administrador de objetos
Creemos un componente HexFeatureManager
que cuide los objetos dentro de un solo fragmento. Usamos el mismo esquema que en HexMesh
- darle métodos Clear
, Apply
y AddFeature
. Como el objeto debe colocarse en algún lugar, el método AddFeature
recibe el parámetro de posición.Comenzaremos con una implementación en blanco que no hará nada por ahora. using UnityEngine; public class HexFeatureManager : MonoBehaviour { public void Clear () {} public void Apply () {} public void AddFeature (Vector3 position) {} }
Ahora podemos agregar un enlace a dicho componente en HexGridChunk
. Luego puede incluirlo en el proceso de triangulación, como todos los elementos secundarios 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(); }
Comencemos colocando un objeto en el centro de cada celda void Triangulate (HexCell cell) { for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { Triangulate(d, cell); } features.AddFeature(cell.Position); }
Ahora necesitamos un administrador de objetos real. Agregue otro hijo al prefabricado Hex Grid Chunk y dele un componente HexFeatureManager
. Entonces puedes conectarle un fragmento.Un administrador de objetos agregado al fragmento prefabricado.Objetos prefabricados
¿Qué objeto de terreno crearemos? Para la primera prueba, un cubo es bastante adecuado. Creemos un cubo lo suficientemente grande, por ejemplo, con una escala de (3, 3, 3), y conviértalo en una casa prefabricada. También crea material para él. Usé el material predeterminado con rojo. Eliminemos su colisionador, porque no lo necesitamos.Cubo prefabricado.Los administradores de objetos necesitarán un enlace a este prefabricado, así que agréguelo HexFeatureManager
y luego conéctelos. Dado que se requiere acceso al componente de transformación para colocar el objeto, lo usamos como el tipo de enlace. public Transform featurePrefab;
Administrador de objetos con prefabricados.Crear instancias de objetos
¡La estructura está lista, y podemos comenzar a agregar características del terreno! Simplemente cree una instancia del prefab en HexFeatureManager.AddFeature
y establezca su posición. public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); instance.localPosition = position; }
Instancias de características del terreno.A partir de ahora, el terreno se llenará de cubos. Al menos las mitades superiores de los cubos, porque el origen local de la malla del cubo en Unity está en el centro del cubo, y la parte inferior está debajo de la superficie del relieve. Para colocar cubos en la topografía, necesitamos moverlos hasta la mitad de su altura. public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); position.y += instance.localScale.y * 0.5f; instance.localPosition = position; }
Cubos en la superficie del relieve.¿Qué pasa si usamos otra malla?. , , . .
Por supuesto, nuestras células están distorsionadas, por lo que debemos distorsionar la posición de los objetos. Así nos deshacemos de la perfecta repetibilidad de la malla. instance.localPosition = HexMetrics.Perturb(position);
Posiciones distorsionadas de objetos.Destrucción de objetos en relieve.
Cada vez que se actualiza un fragmento, creamos nuevos objetos de relieve. Esto significa que mientras creamos más y más objetos en las mismas posiciones. Para evitar duplicados, debemos deshacernos de los objetos viejos al limpiar un fragmento.La forma más rápida de hacerlo es creando un objeto contenedor del juego y convirtiendo todos los objetos de relieve en sus elementos secundarios. Luego, cuando se lo llame, Clear
destruiremos este contenedor y crearemos uno nuevo. El contenedor en sí será hijo de su gerente. 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); }
Probablemente, es ineficiente crear y destruir objetos de alivio cada vez., , . . . , , , . HexFeatureManager.Apply
. . , , .
paquete de la unidadColocación de objetos en relieve.
Mientras colocamos objetos en el centro de cada celda. Para las celdas vacías, esto parece normal, pero en las celdas que contienen ríos y carreteras, así como inundadas con agua, parece extraño.Los objetos están en todas partes.Por lo tanto, verifiquemos antes de colocar el objeto HexGridChunk.Triangulate
si la celda está vacía. if (!cell.IsUnderwater && !cell.HasRiver && !cell.HasRoads) { features.AddFeature(cell.Position); }
Alojamiento limitado.Un objeto por direccion
Solo un objeto por celda no es demasiado. Todavía hay mucho espacio para un montón de objetos. Por lo tanto, agregamos un objeto adicional al centro de cada uno de los seis triángulos de la celda, es decir, uno por dirección.Haremos esto con otro método Triangulate
, cuando sepamos que no hay río en la celda. Todavía tenemos que verificar si estamos bajo el agua y si hay un camino en la celda. Pero en este caso, solo nos interesan las carreteras que van en la dirección actual. 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)); } } … }
Muchas instalaciones, pero no en las proximidades de los ríos.¡Esto crea muchos más objetos! Aparecen cerca de las carreteras, pero aún evitan los ríos. Para colocar objetos a lo largo de los ríos, también podemos agregarlos dentro TriangulateAdjacentToRiver
. Pero de nuevo solo cuando el triángulo no está bajo el agua y no hay camino en él. 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)); } }
Los objetos aparecieron al lado de los ríos.¿Es posible renderizar tantos objetos?, dynamic batching Unity. , . batch. « », . instancing, dynamic batching.
paquete de la unidadVariedad de objetos
Todos nuestros objetos en relieve tienen la misma orientación, que parece completamente antinatural. Vamos a darle a cada uno un giro al azar. 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); }
Turnos aleatorios.Entonces el resultado se vuelve mucho más diverso. Desafortunadamente, cada vez que se actualiza un fragmento, los objetos reciben una nueva rotación aleatoria. La edición de celdas no debería cambiar los objetos en el vecindario, por lo que necesitamos un enfoque diferente.Tenemos una textura de ruido que siempre es la misma. Sin embargo, esta textura contiene ruido de gradiente de Perlin y es localmente consistente. Esto es exactamente lo que necesitamos al distorsionar las posiciones de los vértices en las celdas. Pero los giros no tienen que ser consistentes. Todos los giros deben ser igualmente probables y mixtos. Por lo tanto, necesitamos una textura con valores aleatorios sin gradiente, que se pueda muestrear sin filtrado bilineal. Esencialmente, esta es una cuadrícula hash que forma la base del ruido de gradiente.Crear una tabla hash
Podemos crear una tabla hash a partir de una matriz de valores flotantes y llenarla una vez con valores aleatorios. Gracias a esto, no necesitamos una textura en absoluto. Añádalo a HexMetrics
. Un tamaño de 256 por 256 es suficiente para una variación suficiente. 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; } }
Los valores aleatorios son generados por una fórmula matemática que siempre da los mismos resultados. La secuencia resultante depende del número de semillas, que por defecto es igual al valor actual del tiempo. Es por eso que en cada sesión de juego obtendremos resultados diferentes.Para garantizar que se recrean siempre objetos idénticos, necesitamos agregar el parámetro semilla al método de inicialización. 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; } }
Ahora que hemos inicializado el flujo de números aleatorios, siempre obtendremos la misma secuencia. Por lo tanto, los eventos aparentemente aleatorios que ocurren después de la generación del mapa también serán siempre los mismos. Podemos evitar esto almacenando el estado del generador de números aleatorios antes de inicializarlo. Después de completar el trabajo, podemos preguntarle el estado anterior. Random.State currentState = Random.state; Random.InitState(seed); for (int i = 0; i < hashGrid.Length; i++) { hashGrid[i] = Random.value; } Random.state = currentState;
La tabla hash se inicializa HexGrid
al mismo tiempo que asigna la textura de ruido. Es decir, en los métodos HexGrid.Start
y HexGrid.Awake
. Lo hacemos para que los valores no se generen más de lo necesario. public int seed; void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); … } void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); } }
La variable semilla genérica nos permite seleccionar el valor semilla para el mapa. Cualquier valor servirá. Elegí 1234.La elección de la semilla.Usando una tabla hash
Para usar la tabla hash, agregue al HexMetrics
método de muestreo. Al igual SampleNoise
, utiliza las coordenadas de la posición XZ para obtener el valor. El índice hash se encuentra restringiendo las coordenadas a valores enteros y luego obteniendo el resto de la división entera por el tamaño de la tabla. public static float SampleHashGrid (Vector3 position) { int x = (int)position.x % hashGridSize; int z = (int)position.z % hashGridSize; return hashGrid[x + z * hashGridSize]; }
¿Qué hace%?, , — . , −4, −3, −2, −1, 0, 1, 2, 3, 4 modulo 3 −1, 0, −2, −1, 0, 1, 2, 0, 1.
Esto funciona para coordenadas positivas, pero no para las negativas, porque para tales números el resto será negativo. Podemos arreglar esto agregando el tamaño de la tabla a los resultados negativos. int x = (int)position.x % hashGridSize; if (x < 0) { x += hashGridSize; } int z = (int)position.z % hashGridSize; if (z < 0) { z += hashGridSize; }
Ahora para cada unidad cuadrada creamos nuestro propio valor. Sin embargo, de hecho, no necesitamos tal densidad de tabla. Los objetos están separados entre sí. Podemos estirar la tabla reduciendo la escala de posición antes de calcular el índice. Un valor único para un cuadrado de 4 por 4 será suficiente para nosotros. 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]; }
Regresemos HexFeatureManager.AddFeature
y usemos nuestra nueva tabla hash para obtener el valor. Después de aplicarlo para especificar la rotación, los objetos permanecerán estacionarios al editar el terreno. 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); }
Umbral de colocación
Aunque los objetos tienen rotaciones diferentes, todavía se nota un patrón en su ubicación. Cada celda tiene siete objetos. Podemos agregar caos a este esquema, omitiendo arbitrariamente algunos de los objetos. ¿Cómo decidimos si agregar un objeto o no? ¡Por supuesto, verificando otro valor aleatorio!Es decir, ahora, en lugar de un valor hash, necesitamos dos. Su soporte se puede agregar utilizando hashes en lugar de una float
variable como el tipo de matriz de tabla Vector2
. Pero las operaciones vectoriales no tienen sentido para los valores hash, así que creemos una estructura especial para este propósito. Solo necesitará dos valores flotantes. Y agreguemos un método estático para crear un par de valores aleatorios. 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; } }
¿No necesita ser serializado?, , Unity. , .
Cámbielo HexMetrics
para que use la nueva estructura. 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) { … }
Ahora HexFeatureManager.AddFeature
tiene acceso a dos valores hash. Usemos el primero para decidir si agregar un objeto u omitirlo. Si el valor es igual o mayor que 0.5, omita. Al hacerlo, eliminaremos aproximadamente la mitad de los objetos. El segundo valor se usará como de costumbre para determinar la rotación. 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 densidad de los objetos se reduce en un 50%.paquete de la unidadDibujar objetos
En lugar de colocar objetos en todas partes, hagámoslos editables. Pero no dibujaremos objetos separados, sino que agregaremos el nivel de objetos a cada celda. Este nivel controlará la probabilidad de que aparezcan objetos en la celda. Por defecto, el valor es cero, es decir, los objetos estarán ausentes.Dado que los cubos rojos en nuestro terreno no se ven como objetos naturales, llamémoslos edificios. Representarán la urbanización. Agreguemos al HexCell
nivel de urbanización. public int UrbanLevel { get { return urbanLevel; } set { if (urbanLevel != value) { urbanLevel = value; RefreshSelfOnly(); } } } int urbanLevel;
Podemos hacer que el nivel de urbanización para una celda submarina sea igual a cero, pero esto no es necesario, de todos modos omitimos la creación de objetos submarinos. Y quizás en algún momento agregaremos cuerpos de agua de urbanización, como muelles y estructuras submarinas.Control deslizante de densidad
Para cambiar el nivel de urbanización, agregamos HexMapEditor
un control deslizante más en apoyo. 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(); } … } }
Agregue otro control deslizante a la interfaz de usuario y combínelo con los métodos adecuados. Colocaré un nuevo panel en el lado derecho de la pantalla para evitar desbordar el panel izquierdo.¿Cuántos niveles necesitamos? Detengámonos en cuatro, denotando cero, baja, media y alta densidad.Control deslizante de urbanización.Cambio de umbral
Ahora que tenemos el nivel de urbanización, necesitamos usarlo para determinar si colocar objetos. Para hacer esto, necesitamos agregar el nivel de urbanización como un parámetro adicional para HexFeatureManager.AddFeature
. Demos un paso más y simplemente transfiramos la celda. En el futuro, será más conveniente para nosotros.La forma más rápida de usar el nivel de urbanización es multiplicarlo por 0.25 y usar el valor como el nuevo umbral para saltar objetos. Debido a esto, la probabilidad de la aparición del objeto aumentará con cada nivel en un 25%. public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); if (hash.a >= cell.UrbanLevel * 0.25f) { return; } … }
Para que esto funcione, pasemos las celdas a 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)); } }
Dibujo de niveles de densidad de urbanización.paquete de la unidadVarios prefabricados de objetos en relieve
Las diferencias en la probabilidad de que aparezcan objetos no son suficientes para crear una separación clara entre los niveles bajos y altos de urbanización. En algunas celdas, simplemente habrá más o menos que el número esperado de edificios. Podemos hacer la diferencia más clara usando nuestro propio prefab para cada nivel.Nos deshacemos de los campos featurePrefab
en HexFeatureManager
y sustituirla por una serie de casas prefabricadas urbanización. Para obtener el prefab apropiado, restaremos uno del nivel de urbanización y usaremos el valor como índice. <del>
Cree dos duplicados de la prefabricación del objeto, cámbieles el nombre y cámbielos para que indiquen tres niveles diferentes de urbanización. El nivel 1 es de baja densidad, por lo que utilizamos un cubo con una unidad de longitud de un borde, que denota una choza. Escalaré el nivel 2 prefabricado a (1.5, 2, 1.5) para que parezca un edificio de dos pisos. Para edificios altos de nivel 3, utilicé la escala (2, 5, 2).Utilizando diferentes prefabricados para cada nivel de urbanización.Mezcla prefabricada
No estamos obligados a limitarnos a una separación estricta de los tipos de edificios. Puedes mezclarlos un poco, como sucede en el mundo real. En lugar de un umbral por nivel, usemos tres, uno para cada tipo de edificio.En el nivel 1 usamos la colocación de chozas en el 40% de los casos. No habrá otros edificios aquí en absoluto. Para el nivel usamos los tres valores (0.4, 0, 0).En el nivel 2, reemplaza las chozas con edificios más grandes y agrega un 20% de posibilidades de chozas adicionales. No haremos edificios altos. Es decir, usamos el umbral de tres valores (0.2, 0.4, 0).En el nivel 3, reemplazamos edificios medianos por edificios altos, reemplazamos chozas nuevamente y agregamos otro 20% de posibilidades de chozas. Los valores de umbral serán iguales a (0.2, 0.2, 0.4).Es decir, la idea es que con un aumento en el nivel de urbanización, mejoraremos los edificios existentes y agregaremos otros nuevos a los lugares vacíos. Para eliminar un edificio existente, necesitamos usar los mismos intervalos de valores hash. Si los hashes entre 0 y 0.4 en el nivel 1 eran chozas, entonces en el nivel 3 el mismo intervalo creará edificios altos. En el nivel 3, los edificios altos deben crearse con hashes en el rango de 0 a 0.4, edificios de dos pisos en el rango de 0.4 a 0.6 y chozas en el rango de 0.6 a 0.8. Si los verifica de mayor a menor, esto se puede hacer utilizando el triple de umbrales (0.4, 0.6, 0.8). Los umbrales de nivel 2 se convertirán en (0, 0.4, 0.6), y los umbrales de nivel 1 se convertirán en (0, 0, 0.4).Guardemos estos umbrales enHexMetrics
como una colección de matrices con un método que le permite obtener umbrales para un cierto nivel. Como solo estamos interesados en niveles con objetos, ignoramos el nivel 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]; }
A continuación, agregue al HexFeatureManager
método que utiliza el nivel y el valor hash para seleccionar el prefabricado. Si el nivel es mayor que cero, entonces obtenemos umbrales usando un nivel reducido en uno. Luego recorremos los umbrales hasta que uno de ellos exceda el valor hash. Esto significará que hemos encontrado un prefabricado. Si no lo encontramos, entonces devuelve nulo. 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; }
Este enfoque requiere reordenar enlaces a prefabricados para que pasen de alta a baja densidad.Orden prefabricado invertido.Usaremos nuestro nuevo método AddFeature
para seleccionar un prefabricado. Si no lo recibimos, nos saltamos el objeto. De lo contrario, cree una instancia y continúe como antes. public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position);
Mezcla los prefabricados.Variaciones de nivel
Ahora tenemos edificios bien mezclados, pero hasta ahora solo hay tres. Podemos aumentar aún más la variabilidad al vincular una colección de prefabricados a cada nivel de densidad de urbanización. Después de eso, será posible elegir uno de ellos al azar. Esto requerirá un nuevo valor aleatorio, así que agregue un tercer 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; }
Convirtámoslo HexFeatureManager.urbanPrefabs
en una matriz de matrices y agreguemos un PickPrefab
parámetro al método choice
. Lo usamos para seleccionar el índice de la matriz incorporada, multiplicándolo por la longitud de esta matriz y convirtiéndolo en entero. 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; }
Justifiquemos nuestra elección sobre el valor del segundo hash (B). Entonces debes pasar de B a 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); }
Antes de continuar, debemos considerar qué Random.value
podría devolver un valor de 1. Debido a esto, el índice de matriz puede ir más allá. Para evitar que esto suceda, escalemos ligeramente los valores hash. Simplemente los escalamos a todos para no preocuparnos por lo específico que usamos. 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; }
Desafortunadamente, el inspector no muestra matrices de matrices. Por lo tanto, no podemos configurarlos. Para evitar esta limitación, cree una estructura serializable en la que encapsular la matriz incorporada. Vamos a darle un método que convierta de elección al índice de matriz y devuelva un prefabricado. using UnityEngine; [System.Serializable] public struct HexFeatureCollection { public Transform[] prefabs; public Transform Pick (float choice) { return prefabs[(int)(choice * prefabs.Length)]; } }
En HexFeatureManager
lugar de las matrices incorporadas, utilizamos una matriz de tales colecciones.
Ahora podemos asignar varios edificios a cada nivel de densidad. Como son independientes, no tenemos que usar la misma cantidad por nivel. Acabo de usar dos opciones por nivel, agregando una opción más larga más baja a cada uno. Elegí las escalas para ellos (3.5, 3, 2), (2.75, 1.5, 1.5) y (1.75, 1, 1).Dos tipos de edificios por nivel de densidad.paquete de la unidadVarios tipos de objetos
En el esquema existente, podemos crear estructuras urbanas bastante dignas. Pero el alivio puede contener no solo edificios. ¿Qué tal granjas o plantas? Agreguemos a los HexCell
niveles y para ellos. No son mutuamente excluyentes y pueden mezclarse. 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;
Por supuesto, esto requiere soporte en HexMapEditor
dos controles deslizantes adicionales. 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; } … } }
Agréguelos a la IU.Tres controles deslizantes.Además, se necesitarán colecciones adicionales HexFeatureManager
. public HexFeatureCollection[] urbanCollections, farmCollections, plantCollections;
Tres colecciones de objetos en relieve.Creé para granjas y plantas dos prefabricados por nivel de densidad, así como para construir colecciones. Para todos ellos, usé cubos. Las granjas tienen material verde claro, las plantas tienen material verde oscuro.Hice cubos de granja con una altura de 0.1 unidades para indicar parcelas cuadradas de tierra agrícola. Como escalas de alta densidad, elegí (2.5, 0.1, 2.5) y (3.5, 0.1, 2). En promedio, los sitios tienen un área de 1.75 y un tamaño de 2.5 por 1.25. Se obtuvo un bajo nivel de densidad en el área 1 y un tamaño de 1,5 por 0,75.Las plantas prefabricadas denotan árboles altos y arbustos grandes. Los prefabricados de alta densidad son los más grandes, (1.25, 4.5, 1.25) y (1.5, 3, 1.5). Las escalas promedio son (0.75, 3, 0.75) y (1, 1.5, 1). Las plantas más pequeñas tienen tamaños (0.5, 1.5, 0.5) y (0.75, 1, 0.75).Selección de características de alivio
Cada tipo de objeto debe recibir su propio valor hash para que tengan diferentes patrones de creación y pueda mezclarlos. Agrega HexHash
dos valores adicionales. 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; }
Ahora tienes HexFeatureManager.PickPrefab
que trabajar con diferentes colecciones. Agregue un parámetro para simplificar el proceso. Además, cambie el hash utilizado por la variante del prefabricado seleccionado a D, y el hash para la rotación a 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); }
Actualmente AddFeature
selecciona urbanización prefabricada. Esto es normal, necesitamos más opciones. Por lo tanto, agregamos otro prefabricado de las granjas. Como valor hash, use B. La elección de la opción será nuevamente D. Transform prefab = PickPrefab( urbanCollections, cell.UrbanLevel, hash.a, hash.d ); Transform otherPrefab = PickPrefab( farmCollections, cell.FarmLevel, hash.b, hash.d ); if (!prefab) { return; }
¿Qué tipo de instancia prefabricada crearemos como resultado? Si uno de ellos resulta nulo, entonces la elección es obvia. Sin embargo, si ambos existen, entonces debemos tomar una decisión. Solo agreguemos el prefab con el valor hash más bajo. 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; }
Una mezcla de objetos urbanos y rurales.Luego, haga lo mismo con las plantas usando el valor del hash 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; }
Sin embargo, no podemos simplemente copiar el código. Cuando elegimos el objeto rural en lugar del urbano, necesitamos comparar el hash de las plantas con el hash de las granjas, y no con el urbano. Por lo tanto, necesitamos rastrear el hash que decidimos elegir y compararlo. 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; }
Una mezcla de objetos urbanos, rurales y vegetales.paquete de la unidadParte 10: paredes
- Encerramos las celdas.
- Construimos muros a lo largo de los bordes de las celdas.
- Pasemos por ríos y caminos.
- Evite el agua y conéctese con los acantilados.
En esta parte agregaremos entre las celdas de la pared.No hay nada más atractivo que un muro alto.Edición de pared
Para soportar las paredes, necesitamos saber dónde colocarlas. Los colocaremos entre las celdas a lo largo de los bordes que los conectan. Como los objetos ya existentes se encuentran en la parte central de las celdas, no debemos preocuparnos de que las paredes los atraviesen.Paredes a lo largo de los bordes.Las paredes son objetos del terreno, aunque grandes. Al igual que otros objetos, no los editaremos directamente. En cambio, cambiaremos las celdas. No tendremos segmentos separados de las paredes, pero nos ocuparemos de encerrar las celdas como un todo.Propiedad amurallada
Para admitir celdas cercadas, agregue a la HexCell
propiedad Walled
. Este es un simple cambio. Dado que los muros se encuentran entre las celdas, necesitamos actualizar tanto las celdas editadas como sus vecinas. public bool Walled { get { return walled; } set { if (walled != value) { walled = value; Refresh(); } } } bool walled;
Interruptor del editor
Para cambiar el estado "cercado" de las celdas, necesitamos agregar HexMapEditor
soporte para el cambio. Por lo tanto, agregamos otro campo OptionalToggle
y un método para configurarlo. OptionalToggle riverMode, roadMode, walledMode; … public void SetWalledMode (int mode) { walledMode = (OptionalToggle)mode; }
A diferencia de los ríos y las carreteras, los muros no van de celda en celda, sino que se encuentran entre ellos. Por lo tanto, no necesitamos pensar en arrastrar y soltar. Cuando el interruptor de pared está activo, simplemente establecemos el estado cercado de la celda actual en función del estado de este interruptor. void EditCell (HexCell cell) { if (cell) { … if (roadMode == OptionalToggle.No) { cell.RemoveRoads(); } if (walledMode != OptionalToggle.Ignore) { cell.Walled = walledMode == OptionalToggle.Yes; } if (isDrag) { … } } }
Duplicamos uno de los elementos anteriores de los interruptores de la interfaz de usuario y los cambiamos para que controlen el estado de "cercado". Los pondré en el panel de la interfaz de usuario junto con otros objetos.El interruptor "cercado".paquete de la unidadCreando muros
Como las paredes siguen los contornos de las celdas, no deberían tener una forma constante. Por lo tanto, no podemos simplemente usar un prefabricado para ellos, como lo hicimos con otras características del terreno. En cambio, necesitamos construir una malla, como hicimos con el relieve. Esto significa que nuestro fragmento prefabricado necesita otro elemento hijo HexMesh
. Duplique una de las otras mallas secundarias y haga que los nuevos objetos de Muros proyecten sombras. No necesitan nada excepto vértices y triángulos, por lo que todas las opciones HexMesh
deben estar deshabilitadas.Paredes Prefabricadas Subsidiarias.Será lógico que las paredes sean un objeto urbano, así que para ellas utilicé el material rojo de los edificios.Gestión de la pared
Como las paredes son objetos de alivio, deben lidiar con ellas HexFeatureManager
. Por lo tanto, le daremos al administrador de los objetos en relieve un enlace al objeto Muros y haremos que llame a los métodos Clear
y Apply
. public HexMesh walls; … public void Clear () { … walls.Clear(); } public void Apply () { walls.Apply(); }
Muros conectados al administrador de topografía.¿No deberían los muros ser hijos de las características?, . , Walls Hex Grid Chunk .
Ahora necesitamos agregar un método al administrador que nos permita agregarle muros. Como las paredes están a lo largo de los bordes entre las celdas, necesita conocer los vértices correspondientes de los bordes y las celdas. HexGridChunk
lo hará pasar TriangulateConnection
, en el momento de la triangulación de la célula y uno de sus vecinos. Desde este punto de vista, la celda actual está en el lado cercano de la pared, y la otra está en el lado más alejado. public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { }
Llamaremos a este nuevo método HexGridChunk.TriangulateConnection
después de completar todos los demás trabajos de conexión e inmediatamente antes de la transición al triángulo angular. Dejaremos que el administrador de los objetos de relieve decida por sí mismo dónde debería ubicarse realmente el muro. 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) { … } }
Construye un segmento de muro
Toda la pared serpenteará a través de varios bordes de las celdas. Cada borde contiene solo un elemento de pared. Desde el punto de vista de la celda cercana, el segmento comienza en el lado izquierdo de la costilla y termina en el derecho. Agreguemos a un HexFeatureManager
método separado que usa cuatro vértices en las esquinas de un borde. void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { }
Lados cercanos y lejanos.AddWall
Puede llamar a este método con los bordes primero y último de los bordes. Pero las paredes solo deben agregarse cuando tenemos una conexión entre una celda cercada y una celda no cercada. No importa cuál de las celdas esté adentro y cuál afuera, solo se tiene en cuenta la diferencia en sus estados. 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); } }
El segmento más simple de la pared es un quad, parado en el medio de la costilla. Encontraremos sus picos más bajos, interpolando al medio desde los picos más cercanos a los más lejanos. 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); }
¿Qué tan alto debe ser el muro? Vamos a establecer su altura a HexMetrics
. Los hice del tamaño de un nivel de altura de celda. public const float wallHeight = 3f;
HexFeatureManager.AddWallSegment
puede usar esta altura para colocar los vértices tercero y cuarto del quad, y también agregarlo a la malla 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);
Ahora podemos editar las paredes y se mostrarán como rayas cuádruples. Sin embargo, no veremos un muro continuo. Cada quad es visible solo en un lado. Su cara está dirigida hacia la célula desde la que se agregó.Cuadrículas de un lado.Podemos resolver rápidamente este problema agregando un segundo quad mirando hacia el otro lado. walls.AddQuad(v1, v2, v3, v4); walls.AddQuad(v2, v1, v4, v3);
Paredes bilaterales.Ahora todas las paredes son visibles en su totalidad, pero todavía hay agujeros en las esquinas de las celdas donde se unen las tres celdas. Los completaremos más tarde.Paredes gruesas
Aunque las paredes ya son visibles en ambos lados, no tienen grosor. De hecho, las paredes son delgadas, como el papel, y casi invisibles en cierto ángulo. Así que hagámoslos completos agregando grosor. Establecer su grosor en HexMetrics
. Elegí un valor de 0,75 unidades, me pareció adecuado. public const float wallThickness = 0.75f;
Para hacer dos paredes gruesas, debe separar dos quads a los lados. Deben moverse en direcciones opuestas. Un lado debe moverse hacia el borde cercano, el otro hacia el borde lejano. El vector de desplazamiento para esto es igual far - near
, pero para dejar la parte superior de la pared plana, necesitamos establecer su componente Y en 0.Dado que esto debe hacerse para los lados izquierdo y derecho del segmento de pared, agreguemos un HexMetrics
vector de desplazamiento al método para calcular esto. 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; }
Para que la pared permanezca en el centro de la costilla, la distancia real de movimiento a lo largo de este vector debe ser igual a la mitad del grosor de cada lado. Y para asegurarnos de que realmente nos movemos a la distancia correcta, normalizamos el vector de desplazamiento antes de escalarlo. return offset.normalized * (wallThickness * 0.5f);
Utilizamos este método HexFeatureManager.AddWallSegment
para cambiar la posición de los quads. Dado que el vector de desplazamiento va desde la celda más cercana a la lejana, restarlo del quad cercano y sumar a la lejana. 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);
Paredes con compensaciones.Los quads ahora están sesgados, aunque esto no es del todo notable.¿Son los espesores de pared iguales?, «-» . , . . , . , . , - , . .
Tapas de las paredes
Para hacer que el grosor de la pared sea visible desde arriba, necesitamos agregar un quad a la parte superior de la pared. La forma más fácil de hacer esto es recordar los dos vértices superiores del primer quad y conectarlos con los dos vértices superiores del segundo 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);
Paredes con encimeras.Arrinconar
Todavía tenemos agujeros en las esquinas de las celdas. Para llenarlos, necesitamos agregar un segmento al área triangular entre las celdas. Cada esquina conecta tres celdas. Cada celda puede o no tener una pared. Es decir, son posibles ocho configuraciones.Configuraciones de ángulo.Colocamos paredes solo entre celdas con diferentes estados vallados. Esto reduce el número de configuraciones a seis. En cada una de ellas, una de las celdas está dentro de la curva de las paredes. Consideremos esta celda como un punto de referencia alrededor del cual se curva la pared. Desde el punto de vista de esta celda, el muro comienza con un borde común con la celda izquierda y termina con un borde común con la celda derecha.Roles celulares.Es decir, necesitamos crear un método AddWallSegment
cuyos parámetros sean tres vértices de la esquina. Aunque podemos escribir código para triangular este segmento, de hecho es un caso especial del método AddWallSegment
. Un punto de anclaje juega el papel de ambos vértices cercanos. void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { AddWallSegment(pivot, left, pivot, right); }
A continuación, cree una variante del método AddWall
para los tres vértices del ángulo y sus celdas. El objetivo de este método es determinar el ángulo, que es el punto de referencia, si existe. Por lo tanto, debe considerar las ocho configuraciones posibles y solicitar AddWallSegment
seis de ellas. 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); } }
Para agregar segmentos de esquina, llame a este método al final HexGridChunk.TriangulateCorner
. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); }
Paredes con esquinas, pero todavía hay agujeros.Cerrar los agujeros
Todavía hay agujeros en las paredes porque la altura de los segmentos de la pared es variable. Mientras que los segmentos a lo largo de los bordes son de altura constante, los segmentos de esquina están entre dos bordes diferentes. Como cada borde puede tener su propia altura, aparecen agujeros en las esquinas.Para solucionar esto, cámbielo AddWallSegment
para que almacene por separado las coordenadas Y de los vértices superiores izquierdo y derecho. 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);
Muros cerrados.Las paredes ahora están cerradas, pero probablemente todavía veas agujeros en las sombras de la pared. Esto se debe al parámetro Normal Bias de la configuración de sombra direccional. Cuando es mayor que cero, los triángulos de los objetos que proyectan una sombra se mueven a lo largo de la superficie normal. Esto evita el sombreado automático, pero al mismo tiempo crea agujeros en los casos en que los triángulos miran en diferentes direcciones. En este caso, se pueden crear agujeros en las sombras de geometría fina, por ejemplo, como nuestras paredes.Puede deshacerse de estos artefactos de sombra reduciendo el sesgo normal a cero. O cambie el modo de muro de representación de malla Cast Shadows a Two Sided . Esto hará que el objeto de sombra proyecte ambos lados de cada triángulo de pared para renderizar, lo que cerrará todos los agujeros.No hay más agujeros en las sombras.paquete de la unidadPared de la repisa
Hasta ahora, nuestras paredes son lo suficientemente rectas. Para un terreno plano, esto no es malo en absoluto, pero parece extraño cuando las paredes coinciden con las repisas. Esto sucede cuando hay una diferencia de un nivel de altura entre celdas en lados opuestos de la pared.Muros rectos en las repisas.Sigue el borde
En lugar de crear un segmento para todo el borde, crearemos uno para cada parte de la tira de borde. Podemos hacer esto llamando cuatro veces AddWallSegment
en la versión de AddWall
borde. 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); } }
Muros curvos.Las paredes ahora repiten la forma de los bordes distorsionados. En combinación con las repisas, se ve mucho mejor. Además, crea paredes más interesantes en un relieve plano.Colocar muros en el suelo
Mirando las paredes en las repisas, puede encontrar un problema. ¡Las paredes cuelgan sobre el suelo! Esto es cierto para los bordes planos inclinados, pero generalmente no es tan notable.Paredes colgando en el aire.Para resolver el problema, necesitamos bajar las paredes. La forma más fácil es bajar toda la pared para que su parte superior permanezca plana. Al mismo tiempo, una parte de la pared en la parte superior bajará ligeramente hacia el relieve, pero esto nos conviene.Para bajar la pared, necesitamos determinar qué lado es más bajo, cerca o lejos. Podemos usar la altura del lado más bajo, pero no necesitamos bajar tanto. Puede interpolar la coordenada Y de menor a mayor con un desplazamiento de poco menos de 0,5. Dado que las paredes solo ocasionalmente se vuelven más altas que el escalón inferior de la repisa, podemos usar el escalón vertical de la repisa como compensación. Un espesor de pared diferente de la configuración de la repisa puede requerir un desplazamiento diferente.El muro bajado.Agreguemos al HexMetrics
método WallLerp
que trata con esta interpolación, además de promediar las coordenadas X y Z de los vértices cercano y lejano. Se basa en un método 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; }
Fuerce HexFeatureManager
este método para determinar los vértices izquierdo y derecho. void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { Vector3 left = HexMetrics.WallLerp(nearLeft, farLeft); Vector3 right = HexMetrics.WallLerp(nearRight, farRight); … }
Paredes de pie en el suelo.Cambio de distorsión de la pared
Ahora nuestras paredes están de acuerdo con las diferencias de altura. Pero todavía no se corresponden completamente con los bordes distorsionados, aunque están cerca de ellos. Esto sucedió porque primero determinamos la parte superior de las paredes y luego las distorsionamos. Dado que estos vértices están en algún lugar entre los vértices de los bordes cercanos y lejanos, su distorsión será ligeramente diferente.El hecho de que las paredes sigan incorrectamente las costillas no es un problema. Sin embargo, la distorsión de la parte superior de la pared cambia de otro modo el espesor relativamente uniforme. Si arreglamos los muros en base a vértices distorsionados, y luego agregamos quads sin distorsión, su grosor no debería variar mucho. 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); }
Las partes superiores sin distorsiones de las paredes.Gracias a este enfoque, las paredes ya no seguirán los bordes con tanta precisión como antes. Pero a cambio se volverán menos rotos y tendrán un grosor más constante.Espesor de pared más consistente.paquete de la unidadAgujeros en las paredes
Hasta ahora, hemos ignorado la posibilidad de que un río o una carretera crucen el muro. Cuando esto sucede, debemos hacer un agujero en la pared a través del cual pueda pasar un río o una carretera.Para hacer esto, agregue AddWall
dos parámetros booleanos para indicar si un río o camino pasa a través de un borde. Aunque podemos manejarlos de manera diferente, simplemente eliminemos los dos segmentos medios en ambos casos. 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) {
Ahora HexGridChunk.TriangulateConnection
debería proporcionar los datos necesarios. Como ya necesitaba la misma información, almacénela en variables booleanas y grabemos las llamadas a los métodos correspondientes solo una vez. 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); … }
Los agujeros en las paredes para el paso de ríos y caminos.Cubrimos las paredes
Estas nuevas aberturas crean lugares para completar las paredes. Necesitamos cerrar estos puntos finales con quads para que no podamos mirar a través de los lados de las paredes. Creemos un HexFeatureManager
método para este propósito AddWallCap
. Funciona como AddWallSegment
, pero solo necesita un par de picos cercanos y lejanos. Haz que agregue un quad, yendo desde el lado más cercano al más alejado de la pared. 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); }
Cuando AddWall
descubre que necesitamos un agujero, agregamos una cubierta entre el segundo y el cuarto par de bordes de los bordes. Para el cuarto par de vértices, debe cambiar la orientación, de lo contrario, la cara cuádruple se verá hacia adentro. 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); } … } }
Agujeros cerrados en las paredes.¿Qué pasa con los agujeros alrededor de los bordes del mapa?, . . , .
paquete de la unidadEvitar los acantilados y el agua.
Finalmente, veamos los bordes que contienen acantilados o agua. Dado que los acantilados son esencialmente paredes grandes, sería ilógico colocar una pared adicional sobre ellos. Además, se verá mal. Los muros submarinos también son completamente ilógicos, como lo es la restricción de los muros de la costa.Paredes en acantilados y en el agua.Podemos eliminar las paredes de estos bordes innecesarios con controles adicionales AddWall
. Una pared no puede estar debajo del agua, y una costilla común con ella no puede ser un acantilado. 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 ) { … } }
Se eliminaron las paredes obstructoras a lo largo de las costillas, pero las esquinas permanecieron en su lugar.Eliminar esquinas de la pared
Eliminar segmentos de esquina innecesarios requerirá un poco más de esfuerzo. El caso más simple es cuando la celda de soporte está bajo el agua. Esto asegura que no haya segmentos de pared cercanos que puedan conectarse. void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { if (pivotCell.IsUnderwater) { return; } AddWallSegment(pivot, left, pivot, right); }
No hay más células de soporte bajo el agua.Ahora tenemos que mirar otras dos celdas. Si uno de ellos está bajo el agua o conectado a la celda de soporte por una ruptura, entonces no hay pared a lo largo de esta costilla. Si esto es cierto para al menos un lado, entonces no debería haber un segmento de pared en esta esquina.Determinamos individualmente si hay una pared izquierda o derecha. Ponemos los resultados en variables booleanas para que sea más fácil trabajar con ellos. 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); }
Se eliminaron todos los ángulos interferentes.Cierra las esquinas
Cuando no hay muro en el borde izquierdo o derecho, el trabajo se completa. Pero si la pared está en una sola dirección, significa que hay otro agujero en la pared. Por lo tanto, debe cerrarlo. if (hasLeftWall) { if (hasRighWall) { AddWallSegment(pivot, left, pivot, right); } else { AddWallCap(pivot, left); } } else if (hasRighWall) { AddWallCap(right, pivot); }
Cerramos los muros.Conexión de paredes con acantilados.
En una situación, las paredes se ven imperfectas. Cuando la pared llega al fondo del acantilado, termina. Pero como los acantilados no son completamente verticales, se crea un agujero estrecho entre la pared y el borde del acantilado. En la cima del acantilado, tal problema no surge.Agujeros entre paredes y caras de acantilados.Sería mucho mejor si la pared continuara hasta el borde del acantilado. Podemos hacer esto agregando otro segmento de pared entre el extremo actual de la pared y la esquina superior del acantilado. Dado que la mayoría de este segmento estará oculto dentro del acantilado, podemos hacerlo sin reducir el grosor de la pared dentro del acantilado a cero. Por lo tanto, es suficiente para nosotros crear una cuña: dos quads que van al punto y un triángulo sobre ellos. Creemos un método para este propósito AddWallWedge
. Esto se puede hacer copiando AddWallCap
y agregando un punto de cuña. 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;
En las AddWallSegment
esquinas, llamaremos a este método cuando el muro va en una sola dirección y este muro está a una altura más baja que el otro lado. Es en estas condiciones que encontramos el borde de un acantilado. 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:
.En la parte anterior, agregamos soporte de pared. Estos son segmentos simples de pared recta sin diferencias aparentes. Ahora haremos que los muros sean más interesantes agregándoles torres.Los segmentos de muro deben crearse de manera procesal para que coincidan con el relieve. Esto no es necesario para las torres, podemos usar el prefabricado habitual.Podemos crear una torre simple de dos cubos con material rojo. La base de la torre tiene un tamaño de 2 por 2 unidades y una altura de 4 unidades, es decir, es más gruesa y más alta que la pared. Sobre este cubo colocaremos un cubo unitario que denota la parte superior de la torre. Como todos los otros prefabricados, estos cubos no requieren colisionadores.Como el modelo de la torre consta de varios objetos, los convertimos en hijos del objeto raíz. Colóquelos de modo que el origen local de la raíz esté en la base de la torre. Gracias a esto, podemos colocar las torres sin preocuparnos por su altura.Torre prefabricada.Agregue un enlace a este prefabricado HexFeatureManager
y conéctelo. public Transform wallTower;
Enlace a la torre prefabricada.Construyendo torres
Comencemos colocando torres en el medio de cada segmento de pared. Para hacer esto, crearemos una torre al final del método AddWallSegment
. Su posición será el promedio de los puntos izquierdo y derecho del segmento. 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); }
Una torre por segmento de pared.Tenemos muchas torres a lo largo del muro, pero su orientación no cambia. Necesitamos cambiar su rotación para que se alineen con la pared. Como tenemos los puntos derecho e izquierdo del muro, sabemos qué dirección es la correcta. Podemos utilizar este conocimiento para determinar la orientación del segmento del muro y, por lo tanto, de la torre.En lugar de calcular la rotación nosotros mismos, simplemente asignamos un Transform.right
vector a la propiedad . El código de unidad cambiará la rotación del objeto para que su dirección local derecha corresponda con el vector transmitido. 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);
Las torres están alineadas con la pared.¿Cómo funciona la asignación Transform.right?Quaternion.FromToRotation
. .
public Vector3 right { get { return rotation * Vector3.right; } set { rotation = Quaternion.FromToRotation(Vector3.right, value); } }
Reduce la cantidad de torres
Una torre por segmento de pared es demasiado. Hagamos que la adición de la torre sea opcional agregando un AddWallSegment
parámetro al booleano. Ajústelo al valor predeterminado false
. En este caso, todas las torres desaparecerán. 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); } }
Coloquemos las torres solo en las esquinas de las celdas. Como resultado, obtenemos menos torres con distancias bastante constantes entre ellas. void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … AddWallSegment(pivot, left, pivot, right, true); … }
Las torres están solo en las esquinas.Se ve lo suficientemente bien, pero es posible que necesitemos una colocación menos periódica de las torres. Al igual que con otras características del terreno, podemos usar la tabla hash para decidir si colocar la torre en una esquina. Para hacer esto, usamos el centro de la esquina para muestrear la tabla, y luego compararemos uno de los valores hash con el valor umbral de las torres. HexHash hash = HexMetrics.SampleHashGrid( (pivot + left + right) * (1f / 3f) ); bool hasTower = hash.e < HexMetrics.wallTowerThreshold; AddWallSegment(pivot, left, pivot, right, hasTower);
El valor umbral se refiere a HexMetrics
. Con un valor de 0.5, se crearán torres en la mitad de los casos, pero podemos crear muros con muchas torres o sin ellas. public const float wallTowerThreshold = 0.5f;
Torres aleatorias.Quitamos las torres de las laderas.
Ahora colocamos torres independientemente de la forma del terreno. Sin embargo, en las laderas de la torre parece ilógico. Aquí las paredes van en ángulo y pueden cortar la parte superior de la torre.Torres en las laderas.Para evitar pendientes, verificaremos si las celdas de las esquinas derecha e izquierda están a la misma altura. Solo en este caso es posible colocar una torre. 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);
No hay más torres en las paredes de las laderas.Ponemos las paredes y torres en el suelo.
Aunque evitamos las paredes en pendientes, el relieve en ambos lados de la pared puede tener alturas diferentes. Las paredes pueden correr a lo largo de las repisas, y las celdas de la misma altura pueden tener diferentes posiciones verticales. Debido a esto, la base de la torre puede estar en el aire.Torres en el aire.De hecho, las paredes en las pendientes también pueden colgar en el aire, pero esto no es tan notable como en las torres.Las paredes están en el aire.Esto se puede solucionar estirando la base de las paredes y torres hasta el suelo. Para hacer esto, agregue el desplazamiento Y para las paredes HexMetrics
. Una unidad abajo será suficiente. Aumenta la altura de las torres en la misma cantidad. public const float wallHeight = 4f; public const float wallYOffset = -1f;
Lo cambiamos HexMetrics.WallLerp
para que al determinar la coordenada Y, tenga en cuenta el nuevo desplazamiento. 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; }
También tenemos que cambiar el prefabricado de la torre, ya que la base ahora estará a una unidad por debajo del suelo. Por lo tanto, aumentamos la altura del cubo base en una unidad y, en consecuencia, cambiamos la posición local de los cubos.Muros y torres en el suelo.paquete de la unidadPuentes
En esta etapa, tenemos ríos y carreteras, pero las carreteras no pueden cruzar ríos de ninguna manera. Es el momento adecuado para agregar puentes.Comencemos con un cubo escalado simple que desempeñará el papel de un puente prefabricado. El ancho de los ríos varía, pero hay aproximadamente siete unidades de distancia entre los centros de carreteras en ambos lados. Por lo tanto, le damos una escala aproximada (3, 1, 7). Agregue material urbano rojo prefabricado y elimine su colisionador. Al igual que con las torres, coloque el cubo dentro del objeto raíz con la misma escala. Debido a esto, la geometría del puente en sí no será importante.Agregue un enlace al prefabricado del puente HexFeatureManager
y asígnele un prefabricado. public Transform wallTower, bridge;
Puente prefabricado asignado.Colocación de puentes
Para colocar el puente, necesitamos un método HexFeatureManager.AddBridge
. El puente debe ubicarse entre el centro del río y uno de los lados del río. public void AddBridge (Vector3 roadCenter1, Vector3 roadCenter2) { Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.SetParent(container, false); }
Transmitiremos centros de carreteras no distorsionados, por lo que tendremos que distorsionarlos antes de colocar el puente. roadCenter1 = HexMetrics.Perturb(roadCenter1); roadCenter2 = HexMetrics.Perturb(roadCenter2); Transform instance = Instantiate(bridge);
Para alinear correctamente el puente, podemos usar el mismo enfoque que al girar las torres. En este caso, los centros de carreteras definen el vector delantero del puente. Dado que permanecemos dentro de la misma celda, este vector definitivamente será horizontal, por lo que no necesitamos poner a cero su componente Y. Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.forward = roadCenter2 - roadCenter1; instance.SetParent(container, false);
Construimos puentes a través de ríos rectos.
Las únicas configuraciones de ríos que requieren puentes son rectas y curvas. Las carreteras pueden pasar por puntos finales, y en zigzag las carreteras solo pueden estar cerca.Para comenzar, descubramos ríos rectos. En el interior, el HexGridChunk.TriangulateRoadAdjacentToRiver
primer operador else if
organiza caminos en las cercanías de tales ríos. Por lo tanto, aquí agregaremos puentes.Estamos a un lado del río. El centro del camino se mueve desde el río, y luego el centro de la celda también cambia. Para encontrar el centro de la carretera en el lado opuesto, necesitamos mover la dirección opuesta en la misma cantidad. Esto debe hacerse antes de cambiar el centro mismo. 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; } … }
Puentes sobre ríos rectos.¡Han aparecido puentes! Pero ahora tenemos una instancia de puentes para cada dirección a través de la cual el río no fluye. Necesitamos asegurarnos de que solo se genere una instancia del puente en la celda. Esto se puede hacer eligiendo una dirección relativa al río y sobre la base de generar un puente. Puedes elegir cualquier dirección. roadCenter += corner * 0.5f; if (cell.IncomingRiver == direction.Next()) { features.AddBridge(roadCenter, center - corner * 0.5f); } center += corner * 0.25f;
Además, necesitamos agregar un puente solo cuando hay un camino a ambos lados del río. Por el momento, ya estamos seguros de que hay un camino en el lado actual. Por lo tanto, debe verificar si hay una carretera al otro lado del río. if (cell.IncomingRiver == direction.Next() && ( cell.HasRoadThroughEdge(direction.Next2()) || cell.HasRoadThroughEdge(direction.Opposite()) )) { features.AddBridge(roadCenter, center - corner * 0.5f); }
Puentes entre las carreteras a ambos lados.Puentes sobre ríos curvos
Los puentes sobre ríos curvos funcionan de manera similar, pero su topología es ligeramente diferente. Agregaremos un puente cuando estemos en el exterior de la curva. Esto sucede en el último bloque else
. Utiliza la dirección del medio para compensar el centro de la carretera. Tendremos que usar este desplazamiento dos veces con diferentes escalas, así que guárdelo en una 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; } … }
La escala de desplazamiento en el exterior de la curva es de 0.25 y en el interior HexMetrics.innerToOuter * 0.7f
. Lo usamos para colocar el puente. Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) );
Puentes sobre ríos curvos.Aquí nuevamente necesitamos evitar puentes duplicados. Podemos hacer esto agregando puentes solo desde la dirección media. Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; if (direction == middle) { features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); }
Y nuevamente, debe asegurarse de que el camino esté en el lado opuesto. if ( direction == middle && cell.HasRoadThroughEdge(direction.Opposite()) ) { features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); }
Puentes entre las carreteras a ambos lados.Escala del puente
Como distorsionamos el terreno, la distancia entre los centros de las carreteras y los lados opuestos del río varía. A veces los puentes son demasiado cortos, a veces demasiado largos.Distintas distancias pero constantes longitudes de puente.Aunque hemos creado un puente con una longitud de siete unidades, puede escalarlo para que coincida con la distancia real entre los centros de las carreteras. Esto significa que el modelo de puente está deformado. Dado que las distancias no varían mucho, la deformación puede ser más aceptable que los puentes no adecuados para la longitud.Para realizar una escala adecuada, necesitamos conocer la longitud inicial de la prefabricada del puente. Almacenaremos esta longitud en HexMetrics
. public const float bridgeDesignLength = 7f;
Ahora podemos asignar la escala a lo largo de la instancia Z del puente a la distancia entre los centros de las carreteras, dividida por la longitud original. Dado que la raíz del prefabricado del puente tiene la misma escala, el puente se estirará correctamente. 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 longitud cambiante de los puentes.Construcción de puentes
En lugar de un cubo simple, podemos usar un modelo de puente más interesante. Por ejemplo, puede crear un puente arqueado en bruto de tres cubos rotados y escalados. Por supuesto, puede crear modelos 3D mucho más complejos, incluidas partes de la carretera. Pero tenga en cuenta que todo el objeto estará ligeramente comprimido y estirado.Puentes arqueados de diferentes longitudes.paquete de la unidadObjetos especiales
Hasta ahora, nuestras células pueden contener objetos urbanos, rurales y vegetales. Aunque cada uno de ellos tiene tres niveles, todos los objetos son bastante pequeños en comparación con el tamaño de la celda. ¿Qué pasa si necesitamos un edificio grande, como un castillo?Agreguemos un tipo especial de objeto al terreno. Tales objetos son tan grandes que ocupan toda la celda. Cada uno de estos objetos es único y necesita su propio prefabricado. Por ejemplo, se puede crear un castillo simple a partir de un cubo central más cuatro torres de esquina. La escala (6, 4, 6) para el cubo central creará un bloqueo lo suficientemente grande, que sin embargo cabe incluso en una celda fuertemente deformada.Prefabricada del castillo.Otro objeto especial puede ser un zigurat, por ejemplo, construido con tres cubos colocados uno encima del otro. Para el cubo inferior, la escala (8, 2.5, 8) es adecuada.Zigurat prefabricado.Los objetos especiales pueden ser cualquiera, no necesariamente arquitectónicos. Por ejemplo, un grupo de árboles masivos de hasta diez unidades de altura puede indicar una celda llena de megaflora.Megaflora prefabricada.Agregue a la HexFeatureManager
matriz para rastrear estos prefabricados. public Transform[] special;
Primero, agregue un castillo a la matriz, luego zigurat y luego megaflora.Personalización de objetos especiales.Haciendo células especiales
Ahora HexCell
, se requiere un índice de objetos especiales, que determina el tipo de un objeto especial, si está allí. int specialIndex;
Al igual que otros objetos de socorro, demos la posibilidad de recibir y establecer este valor. public int SpecialIndex { get { return specialIndex; } set { if (specialIndex != value) { specialIndex = value; RefreshSelfOnly(); } } }
Por defecto, la celda no contiene un objeto especial. Denotamos esto por el índice 0. Agregue una propiedad que use este enfoque para determinar si una celda es especial. public bool IsSpecial { get { return specialIndex > 0; } }
Para editar celdas, agregue soporte para el índice de objetos especiales en HexMapEditor
. Funciona de manera similar a los niveles de las instalaciones urbanas, rurales y vegetales. 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; } … } }
Agregue un control deslizante a la interfaz de usuario para controlar el objeto especial. Como tenemos tres objetos, usamos el intervalo 0–3 en el control deslizante. Cero significará la ausencia de un objeto, uno - un castillo, dos - zigurat, tres - megaflora.Control deslizante para objetos especiales.Agregar objetos especiales
Ahora podemos asignar objetos especiales a las celdas. Para que aparezcan, necesitamos agregar a HexFeatureManager
otro método. Simplemente crea una instancia del objeto especial deseado y lo coloca en la posición deseada. Dado que cero indica la ausencia de un objeto, debemos restar la unidad del índice de objetos especiales de la celda antes de obtener acceso a la matriz de prefabricados. public void AddSpecialFeature (HexCell cell, Vector3 position) { Transform instance = Instantiate(special[cell.SpecialIndex - 1]); instance.localPosition = HexMetrics.Perturb(position); instance.SetParent(container, false); }
Vamos a darle al objeto una rotación arbitraria utilizando la tabla hash. 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); }
Al triangular una celda, HexGridChunk.Triangulate
verificaremos si la celda contiene un objeto especial. Si es así, llamamos a nuestro nuevo método, al igual que 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); } }
Objetos especiales. Son mucho más grandes de lo habitual.Evitar los ríos
Dado que los objetos especiales se encuentran en el centro de las celdas, no se combinan con los ríos, ya que colgarán sobre ellos.Objetos en los ríos.Para evitar que se creen objetos especiales en la parte superior de los ríos, cambiamos la propiedad HexCell.SpecialIndex
. Cambiaremos el índice solo cuando no haya ríos en la celda. public int SpecialIndex { … set { if (specialIndex != value && !HasRiver) { specialIndex = value; RefreshSelfOnly(); } } }
Además, al agregar un río, necesitaremos deshacernos de todos los objetos especiales. El río debería lavarlos. Esto se puede hacer HexCell.SetOutgoingRiver
estableciendo el índice de objetos especiales en 0 en el método . 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); }
Evitamos los caminos
Al igual que los ríos, los caminos también van mal con objetos especiales, pero no todo es tan terrible. Incluso puedes dejar las carreteras como están. Algunas instalaciones pueden ser compatibles con las carreteras, mientras que otras no. Por lo tanto, puede hacer que dependan del objeto. Pero lo haremos más fácil.Objetos en el camino.En este caso, deje que los objetos especiales derroten el camino. Por lo tanto, al cambiar el índice de objetos especiales, también eliminaremos todas las carreteras de la celda. public int SpecialIndex { … set { if (specialIndex != value && !HasRiver) { specialIndex = value; RemoveRoads(); RefreshSelfOnly(); } } }
¿Qué pasa si eliminamos un objeto en particular?0, , . .
Además, esto significa que al agregar carreteras, tendremos que realizar verificaciones adicionales. Agregaremos caminos solo cuando ninguna de las celdas sea una celda con un objeto especial. public void AddRoad (HexDirection direction) { if ( !roads[(int)direction] && !HasRiverThroughEdge(direction) && !IsSpecial && !GetNeighbor(direction).IsSpecial && GetElevationDifference(direction) <= 1 ) { SetRoad((int)direction, true); } }
Evitar otros objetos
Los objetos especiales no se pueden mezclar con otros tipos de objetos. Si se superponen, entonces se verá desordenado. También puede depender de un objeto en particular, pero usaremos el mismo enfoque.Un objeto que se cruza con otros objetos.En este caso, suprimiremos objetos más pequeños, como si estuvieran bajo el agua. Esta vez nos registraremos HexFeatureManager.AddFeature
. public void AddFeature (HexCell cell, Vector3 position) { if (cell.IsSpecial) { return; } … }
Evitar el agua
También tenemos un problema con el agua. ¿Las características especiales persistirán durante las inundaciones? Como destruimos objetos pequeños en celdas inundadas, hagamos lo mismo con objetos especiales.Objetos en el agua.En HexGridChunk.Triangulate
realizaremos la misma verificación de inundación para objetos especiales y ordinarios. 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); } }
Dado que ambos operadores if
ahora verifican si la celda está bajo el agua, podemos transferir la prueba y realizarla solo una vez. 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); } } }
Para los experimentos, tal cantidad de objetos será suficiente para nosotros.paquete de la unidad