Mapas do Unity Hexagon: água, pontos de referência e muralhas da fortaleza

Partes 1-3: malha, cores e altura das células

Partes 4-7: solavancos, rios e estradas

Peças 8-11: água, formas terrestres e muralhas

Peças 12-15: salvar e carregar, texturas, distâncias

Partes 16-19: encontrando o caminho, esquadrões de jogadores, animações

Partes 20-23: Nevoeiro da Guerra, Pesquisa de Mapas, Geração de Procedimentos

Partes 24-27: ciclo da água, erosão, biomas, mapa cilíndrico

Parte 8: água


  • Adicione água às células.
  • Triangular a superfície da água.
  • Crie um surf com espuma.
  • Combine água e rios.

Já adicionamos apoio fluvial e, nesta parte, mergulharemos completamente as células na água.


A água está chegando.

Nível de água


A maneira mais fácil é implementar o apoio à água, definindo-o no mesmo nível. Todas as células cuja altura está abaixo deste nível são imersas em água. Mas uma maneira mais flexível seria manter a água em diferentes alturas; portanto, alteremos o nível da água. Para isso, a HexCell precisa monitorar seu nível de água.

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

Se desejar, você pode garantir que certas características do relevo não existam debaixo d'água. Mas, por enquanto, não farei isso. Coisas como estradas subaquáticas me agradam. Eles podem ser considerados áreas que foram inundadas recentemente.

Células de inundação


Agora que temos níveis de água, a questão mais importante é se as células estão submersas. Uma célula está submersa se o nível da água estiver acima da sua altura. Para obter essas informações, adicionaremos uma propriedade.

  public bool IsUnderwater { get { return waterLevel > elevation; } } 

Isso significa que quando o nível e a altura da água são iguais, a célula se eleva acima da água. Ou seja, a superfície real da água está abaixo dessa altura. Como nas superfícies do rio, vamos adicionar o mesmo deslocamento - HexMetrics.riverSurfaceElevationOffset . Mude seu nome para um mais geral.

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

Altere HexCell.RiverSurfaceY para que ele use o novo nome. Em seguida, adicionamos uma propriedade semelhante à superfície da água da célula inundada.

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

Edição de água


A edição do nível da água é semelhante à alteração da altura. Portanto, o HexMapEditor deve monitorar o nível de água ativo e se deve ser aplicado às células.

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

Adicione métodos para conectar esses parâmetros à interface do usuário.

  public void SetApplyWaterLevel (bool toggle) { applyWaterLevel = toggle; } public void SetWaterLevel (float level) { activeWaterLevel = (int)level; } 

E adicione o nível da água ao EditCell .

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

Para adicionar um nível de água à interface do usuário, duplique o rótulo e o controle deslizante de altura e altere-os. Lembre-se de anexar seus eventos aos métodos apropriados.


Controle deslizante de nível de água.

unitypackage

Triangulação da água


Para triangular a água, precisamos de uma nova malha com novo material. Primeiro, crie um sombreador de água , duplicando o sombreador de rio . Altere-o para que ele use a propriedade 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" } 

Crie um novo material com esse shader duplicando o material Water e substituindo-o por um shader. Deixe a textura do ruído, porque a usaremos mais tarde.


Material de água.

Adicione um novo filho à pré-fabricada duplicando o filho de Rivers . Ele não precisa de coordenadas UV e deve usar Água . Como sempre, faremos isso criando uma instância da pré-fabricada, alterando-a e aplicando as alterações na pré-fabricada. Depois disso, livre-se da instância.



Criança Objeto Água.

Em seguida, adicione suporte de malha de água ao 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(); } 

E conecte-o à criança pré-fabricada.


O objeto Água está conectado.

Hexágonos de água


Como a água forma uma segunda camada, vamos dar nosso próprio método de triangulação para cada uma das direções. Precisamos chamá-lo apenas quando a célula está imersa em água.

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

Como nos rios, a altura da superfície da água não varia muito em células com o mesmo nível de água. Portanto, parece que não precisamos de costelas complexas. Um simples triângulo 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 água.

Compostos de água


Podemos conectar células vizinhas à água com um quadrilátero.

  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); } 


Conexões das margens da água.

E preencha os cantos com um 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()) ); } } 


Articulações de cantos de água.

Agora temos células de água conectadas quando estão próximas. Eles deixam um espaço entre eles e as células secas com uma altura mais alta, mas deixaremos isso para mais tarde.

Níveis de água harmonizados


Nossa hipótese é que as células subaquáticas vizinhas tenham o mesmo nível de água. Se for assim, tudo ficará bem, mas se essa suposição for violada, ocorrerão erros.


Níveis de água inconsistentes.

Podemos fazer com que a água permaneça no mesmo nível. Por exemplo, quando o nível da água de uma célula inundada muda, podemos propagar as alterações nas células vizinhas para manter os níveis sincronizados. No entanto, esse processo deve continuar até encontrar células que não estão imersas na água. Essas células definem os limites da massa de água.

O perigo dessa abordagem é que ela pode sair rapidamente do controle. Se a edição não for bem-sucedida, a água poderá cobrir o mapa inteiro. Então todos os fragmentos terão que ser triangulados simultaneamente, o que levará a um grande salto nos atrasos.

Então, não vamos fazer isso ainda. Esse recurso pode ser adicionado em um editor mais complexo. Enquanto a consistência dos níveis de água, deixamos a consciência do usuário.

unitypackage

Animação aquática


Em vez de uma cor uniforme, criaremos algo que se assemelha a ondas. Como em outros shaders, por enquanto não nos esforçaremos para obter belos gráficos, precisamos apenas designar as ondas.


Água perfeitamente plana.

Vamos fazer o que fizemos com os rios. Amostra o ruído com a posição do mundo e o adicionamos a uma cor uniforme. Para animar a superfície, adicione tempo à 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; } 


Rolagem de água, tempo × 10.

Duas direções


Até agora, isso não é como ondas. Vamos complicar a imagem adicionando uma segunda amostra de ruído
e desta vez adicionando a coordenada U. Usamos um canal de ruído diferente para obter dois padrões diferentes como resultado. As ondas acabadas serão essas duas amostras empilhadas 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; 

Ao somar as duas amostras, obtemos resultados no intervalo de 0 a 2, portanto, precisamos redimensioná-lo para 0 a 1. Em vez de simplesmente dividir as ondas ao meio, podemos usar a função de smoothstep para criar um resultado mais interessante. Colocamos ¾ - 2 em 0-1 para que não haja ondas visíveis na superfície da água.

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


Duas direções, tempo × 10.

Ondas de mistura


Ainda é perceptível que temos dois padrões de ruído em movimento que realmente não mudam. Seria mais plausível se os padrões mudassem. Podemos perceber isso interpolando entre diferentes canais de amostras de ruído. Mas isso não pode ser feito da mesma maneira, caso contrário, toda a superfície da água mudará simultaneamente, e isso é muito perceptível. Em vez disso, criaremos uma onda de confusão.

Criaremos uma onda de mistura com a ajuda de um sinusóide, que se move diagonalmente ao longo da superfície da água. Faremos isso adicionando as coordenadas mundiais X e Z e usando a soma como entrada para a função sin . Diminua o zoom para obter bandas grandes o suficiente. E, claro, vamos adicionar o mesmo valor para animá-los.

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

As ondas senoidais variam entre -1 e 1, e precisamos de um intervalo de 0 a 1. Você pode obtê-lo ao quadrado a onda. Para ver um resultado isolado, use-o em vez da cor alterada como valor de saída.

  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); 


Ondas de mistura.

Para tornar as ondas de mistura menos visíveis, adicione algum ruído de ambas as amostras.

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


Ondas distorcidas de mistura.

Finalmente, usamos uma onda de mistura para interpolar entre os dois canais de ambas as amostras de ruído. Para variação máxima, use quatro canais diferentes.

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


Ondas de mistura, tempo × 2.

unitypackage

A costa


Acabamos com a água aberta, mas agora precisamos preencher a lacuna na água ao longo da costa. Como devemos obedecer aos contornos da terra, a água costeira requer uma abordagem diferente. Vamos dividir o TriangulateWater em dois métodos - um para águas abertas e outro para a costa. Para entender quando trabalhamos com a costa, precisamos olhar para a célula vizinha. Ou seja, no TriangulateWater teremos um vizinho. Se há um vizinho e ele não está debaixo d'água, então estamos lidando com a 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) { // HexCell neighbor = cell.GetNeighbor(direction); // if (neighbor == null || !neighbor.IsUnderwater) { // return; // } Vector3 bridge = HexMetrics.GetBridge(direction); … } } void TriangulateWaterShore ( HexDirection direction, HexCell cell, HexCell neighbor, Vector3 center ) { } 


Não há triangulação ao longo da costa.

Como a costa está distorcida, devemos distorcer os triângulos de água ao longo da costa. Portanto, precisamos do topo das arestas e um leque de 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); } 


Fãs de triângulos ao longo da costa.

Em seguida é uma tira de costelas, como em um alívio normal. No entanto, não precisamos nos limitar a apenas algumas áreas, porque chamamos TriangulateWaterShore apenas quando encontramos a costa, da qual a faixa é sempre necessária.

  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); 


Listras de costelas ao longo da costa.

Da mesma forma, também devemos adicionar um triângulo angular de 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()) ); } 


Os cantos das costelas ao longo da costa.

Agora temos água pronta para o litoral. Parte dela está sempre abaixo da malha de alívio, para que não haja furos.

Costa UV


Podemos deixar tudo como está, mas seria interessante se a água costeira tivesse seu próprio horário. Por exemplo, o efeito da espuma, que se torna maior ao se aproximar da costa. Para implementá-lo, o sombreador deve saber o quão perto o fragmento está da costa. Podemos transmitir essas informações através de coordenadas UV.

A água aberta não possui coordenadas UV e não precisa de espuma. É necessário apenas para a água perto da costa. Portanto, os requisitos para os dois tipos de água são bem diferentes. Seria lógico criar sua própria malha para cada tipo. Portanto, adicionamos suporte a outro objeto de malha no 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(); } 

Essa nova malha usará o 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 o objeto água, conecte-o à pré-fabricada e defina-o para que ele use coordenadas UV. Também criamos um shader e um material para a água costeira, duplicando o shader e o material da água existentes.


Facilidade na costa da água e material UV.

Mude o sombreador Water Shore para que, em vez de água, exiba as coordenadas UV.

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

Como ainda não foram definidas coordenadas, ela exibirá uma cor sólida. Graças a isso, é fácil ver que a costa realmente usa uma malha separada com material.


Malha separada para a costa.

Vamos colocar as informações da costa na coordenada V. No lado da água, atribua um valor 0, no lado da terra - valor 1. Como não precisamos transmitir mais nada, todas as coordenadas U serão simplesmente 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) ); } 


As transições para as costas estão incorretas.

O código acima funciona para arestas, mas está errado em alguns ângulos. Se o próximo vizinho estiver embaixo d'água, essa abordagem estará correta. Mas quando o próximo vizinho não estiver embaixo d'água, o terceiro pico do triângulo estará embaixo da terra.

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


As transições para as costas estão corretas.

Espuma na costa


Agora que as transições para a costa foram implementadas corretamente, você pode usá-las para criar um efeito de espuma. A maneira mais fácil é adicionar o valor da costa a uma cor 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 linear.

Para tornar a espuma mais interessante, multiplique-a pelo quadrado do sinusóide.

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


Espuma quadrada sinusóide desbotada.

Vamos aumentar a frente de espuma ao nos aproximarmos da costa. Isso pode ser feito usando a raiz quadrada antes de usar o valor da costa.

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


A espuma fica mais grossa perto da costa.

Adicione distorção para torná-la mais natural. Vamos tornar a distorção mais fraca ao nos aproximarmos da costa. Portanto, será melhor alinhar a 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 com distorção.

E, é claro, estamos animando tudo isso: sinusóide e distorções.

  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 Animada.

Além da espuma recebida, há também uma retirada. Vamos adicionar um segundo sinusóide, que se move na direção oposta, para simulá-lo. Torne-o mais fraco e adicione uma mudança de horário. A espuma acabada será o máximo desses dois sinusóides.

  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 e recuo.

Mistura de ondas e espuma


Há uma transição acentuada entre águas abertas e costeiras porque as ondas de águas abertas não estão incluídas na água costeira. Para consertar isso, precisamos incluir essas ondas no sombreador Water Shore .

Em vez de copiar o código de onda, vamos colá-lo no arquivo de inclusão Water.cginc . De fato, inserimos código nele para espuma e ondas, cada um como uma função separada.

Como os arquivos de inclusão do sombreador funcionam?
A criação de seus próprios arquivos de shader de inclusão é abordada no tutorial Rendering 5, Multiple Lights .

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

Altere o sombreador Water para que ele use o novo arquivo de inclusão.

  #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; } 

No sombreador Water Shore , os valores são calculados para espuma e ondas. Então abafamos as ondas quando nos aproximamos da costa. O resultado final será um máximo de espuma e ondas.

  #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; } 


Uma mistura de espuma e ondas.

unitypackage

Novamente sobre a água costeira


Parte da malha costeira está oculta sob a malha de alívio. Isso é normal, mas apenas uma pequena parte está oculta. Infelizmente, penhascos íngremes escondem a maior parte da água costeira e, portanto, espuma.


Água costeira quase escondida.

Podemos lidar com isso aumentando o tamanho da faixa da costa. Isso pode ser feito reduzindo o raio dos hexágonos da água. Para isso, além do coeficiente de integridade, precisamos de um coeficiente de água HexMetrics , além de métodos para obter ângulos de água.

O coeficiente de integridade é 0,8. Para dobrar o tamanho dos compostos de água, precisamos definir o coeficiente de água para 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; } 

Usaremos esses novos métodos HexGridChunkpara encontrar os ângulos da água.

  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 cantos de água.

A distância entre os hexágonos da água dobrou. Agora HexMetricstambém deve ter um método para criar pontes na água.

  public const float waterBlendFactor = 1f - waterFactor; public static Vector3 GetWaterBridge (HexDirection direction) { return (corners[(int)direction] + corners[(int)direction + 1]) * waterBlendFactor; } 

Mude HexGridChunkpara que ele use o novo 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()) ); … } } 


Longas pontes na água.

Entre as costelas de água e terra


Embora isso nos dê mais espaço para espuma, agora ainda mais está oculto sob o relevo. Idealmente, poderemos usar uma costela de água no lado da água e uma costela de terra no lado da terra.

Não podemos usar uma ponte simples para encontrar a margem oposta da terra, se começarmos pelos cantos da água. Em vez disso, podemos seguir na direção oposta, a partir do centro do vizinho. Mude TriangulateWaterShorepara usar essa nova abordagem.

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


Cantos errados das arestas.

Isso funcionou, só que agora precisamos considerar dois casos para triângulos angulares.

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


Os cantos corretos das arestas.

Isso funcionou bem, mas agora que a maior parte da espuma é visível, está se tornando bastante pronunciada. Para compensar isso, tornaremos o efeito um pouco mais fraco, reduzindo a escala do valor da costa no sombreador.

  shore = sqrt(shore) * 0.9; 


Espuma pronta.

unitypackage

Rios submarinos


Acabamos com água, pelo menos naqueles lugares onde não há rios. Como a água e os rios ainda não se notam, os rios fluem através e debaixo d'água.


Rios fluindo na água.

A ordem na qual os objetos translúcidos são renderizados depende da distância da câmera. Os objetos mais próximos são renderizados por último, então eles estão no topo. Ao mover a câmera, isso significa que, às vezes, rios e às vezes água aparecem uns sobre os outros. Vamos começar fazendo a ordem de renderização constante. Os rios devem ser desenhados em cima da água para que as cachoeiras sejam exibidas corretamente. Podemos implementar isso alterando a fila do shader do rio .

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


Nós desenhamos os rios por último.

Escondendo o rio debaixo d'água


Embora o leito do rio possa estar submerso e a água possa fluir através dele, não devemos ver essa água. E ainda mais, não deve ser renderizado sobre uma superfície de água real. Podemos nos livrar da água dos rios submarinos adicionando segmentos de rios somente quando a célula atual não estiver embaixo d'água.

  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 TriangulateConnectioncomeçar, adicionaremos um segmento de rio quando nem a célula atual nem a célula vizinha estiverem submersas.

  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 ); } } 


Não há mais rios subaquáticos.

Cachoeiras


Não há mais rios subaquáticos, mas agora temos buracos nos locais onde eles se encontram com a superfície da água. Rios no mesmo nível da água criam pequenos orifícios ou coberturas. Mas as mais notáveis ​​são as cachoeiras que faltam para os rios que fluem de uma altura maior. Vamos cuidar deles primeiro.

Um segmento de rio com uma cachoeira costumava passar pela superfície da água. Como resultado, ele se viu parcialmente acima e parcialmente debaixo d'água. Precisamos manter uma parte acima do nível da água, descartando todo o resto. Você precisará trabalhar duro para isso, então crie um método separado.

O novo método requer quatro picos, dois níveis de rio e um nível de água. Vamos configurá-lo para que olhemos na direção da corrente, descendo a cachoeira. Portanto, os dois primeiros picos e os lados esquerdo e direito estarão no topo e os mais baixos seguirão.

  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); } 

Vamos chamar esse método TriangulateConnectionquando um vizinho estiver embaixo d'água e criarmos uma cachoeira.

  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 ); } } 

Também precisamos processar as cachoeiras na direção oposta, quando a célula atual estiver embaixo d'água e a próxima não.

  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 ); } 

Então, novamente, obtemos o quad do rio original. Em seguida, precisamos mudar TriangulateWaterfallInWaterpara elevar os picos mais baixos ao nível da água. Infelizmente, alterar apenas as coordenadas Y não será suficiente. Isso pode empurrar a cachoeira do penhasco, que pode formar buracos. Em vez disso, você deve mover os vértices inferiores para os superiores usando interpolação.


Interpolar.

Para mover os picos mais baixos, divida a distância abaixo da superfície da água pela altura da cachoeira. Isso nos dará um 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, temos uma cachoeira encurtada que tem a mesma orientação. No entanto, como as posições dos vértices inferiores foram alteradas, elas não serão distorcidas como os vértices originais. Isso significa que o resultado final ainda não coincidirá com a cascata original. Para resolver esse problema, precisamos distorcer manualmente os vértices antes de interpolar e, em seguida, adicionar o quad não distorcido.

  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 já temos um método para adicionar triângulos não distorcidos, na verdade não precisamos criar um para quads. Portanto, adicionamos o método necessário 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); } 


As cachoeiras terminam na superfície da água.

unitypackage

Estuários


Quando os rios fluem na mesma altura que a superfície da água, a malha do rio toca a malha costeira. Se fosse um rio que flui para o mar ou para o oceano, haveria uma corrente do rio com um surf. Portanto, chamaremos essas áreas de estuários.


O rio encontra a costa sem distorcer os picos.

Agora temos dois problemas com a boca. Em primeiro lugar, os rios quádruplos conectam o segundo e o quarto topos das costelas, pulando o terceiro. Como a costa da água não usa o terceiro pico, ela pode criar um buraco ou se sobrepor. Podemos resolver esse problema alterando a geometria das bocas.

O segundo problema é que há uma transição acentuada entre a espuma e os materiais do rio. Para resolvê-lo, precisamos de outro material que realize a mistura dos efeitos de um rio e da água.

Isso significa que as bocas exigem uma abordagem especial, então vamos criar um método separado para elas. Deve ser chamado TriangulateWaterShorequando houver um rio se movendo na direção atual.

  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) { } 

Uma região que mistura ambos os efeitos não é necessária para preencher a faixa inteira. A forma trapezoidal será suficiente para nós. Portanto, podemos usar dois triângulos costeiros nas laterais.

  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) ); } 


Orifício trapezoidal para a área de mistura.

Coordenadas UV2


Para criar um efeito de rio, precisamos de coordenadas UV. Mas, para criar um efeito de espuma, você também precisa de coordenadas UV. Ou seja, ao misturá-los, precisamos de dois conjuntos de coordenadas UV. Felizmente, as malhas do mecanismo Unity podem suportar até quatro conjuntos de UV. Nós apenas precisamos adicionar HexMeshsuporte ao 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 adicionar um segundo conjunto de UVs, duplicamos os métodos de trabalho com UV e alteramos a maneira que precisamos.

  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)); } 

Função River Shader


Como usaremos o efeito do rio em dois sombreadores, moveremos o código do sombreador do rio para a nova função de arquivo de inclusão de água .

 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; } 

Mude o sombreador River para usar esse novo recurso.

  #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 de boca


Adicione uma HexGridChunkboca para apoiar o objeto de malha.

  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(); } 

Crie um shader, material e um objeto da boca, duplicando a costa e alterando-a. Conecte-o ao fragmento e faça com que ele use as coordenadas UV e UV2.


Estuários de objetos.

Triangulação da boca


Podemos resolver o problema do buraco ou sobreposição, colocando um triângulo entre o final do rio e o meio da beira da água. Como nosso shader de boca é uma duplicata do shader de costa, definimos as coordenadas UV para combinar com o efeito 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 do meio.

Podemos preencher todo o trapézio adicionando um quadrilátero nos dois lados do triângulo do meio.

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


Trapézio pronto.

Vamos virar a orientação quádrupla para a esquerda para que ela tenha uma conexão diagonal reduzida e, como resultado, obtemos geometria 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) ); // estuaries.AddQuadUV(0f, 0f, 0f, 1f); 


Quad girado, geometria simétrica

Fluxo do rio


Para apoiar o efeito do rio, precisamos adicionar coordenadas UV2. A parte inferior do triângulo do meio fica no meio do rio, então sua coordenada U deve ser igual a 0,5. Como o rio flui na direção da água, o ponto esquerdo recebe a coordenada U igual a 1, e o direito recebe a coordenada U com o valor 0. Definimos as coordenadas Y em 0 e 1, correspondendo à direção da corrente.

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

Os quadrângulos dos dois lados do triângulo devem coincidir com essa orientação. Mantemos as mesmas coordenadas U para pontos que excedem a largura do rio.

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


Trapézio UV2.

Para garantir que definimos as coordenadas UV2 corretamente, faça com que o sombreador do estuário as processe . Podemos acessar essas coordenadas adicionando à estrutura 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.

Tudo parece bom, você pode usar um sombreador para criar um efeito de rio.

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


Use UV2 para criar um efeito de rio.

Criamos os rios de tal maneira que, ao triangular as conexões entre as células, as coordenadas do rio V mudam de 0,8 para 1. Portanto, aqui também devemos usar esse intervalo, e não de 0 para 1. No entanto, a conexão costeira é 50% mais que as conexões de células comuns . Portanto, para o melhor ajuste com o curso do rio, devemos alterar os valores de 0,8 para 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) ); 



Fluxo sincronizado do rio e estuário.

Ajuste de fluxo


Enquanto o rio está se movendo em linha reta. Mas quando a água flui para uma área maior, ela se expande. A corrente irá se curvar. Podemos simular isso dobrando as coordenadas UV2.

Em vez de manter as coordenadas U superiores constantes fora da largura do rio, mova-as em 0,5. O ponto mais à esquerda é 1,5, o mais à direita é -0,5.

Ao mesmo tempo, expandimos o fluxo movendo as coordenadas U dos pontos inferiores esquerdo e direito. Mude o esquerdo de 1 para 0,7 e o direito 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) ); 



Expansão do rio.

Para concluir o efeito de curvatura, altere as coordenadas V dos mesmos quatro pontos. Como a água flui para longe do final do rio, aumentaremos as coordenadas de V dos pontos superiores para 1. E para criar uma curva melhor, aumentaremos as coordenadas de V dos dois pontos inferiores para 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) ); 



O curso curvo do rio.

Rio e Costa Mix


Tudo o que resta para nós é misturar os efeitos da costa e do rio. Para fazer isso, usamos a interpolação linear, assumindo o valor da 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); 

Embora isso deva funcionar, você pode receber um erro de compilação. O compilador reclama de redefinição _MainTex_ST. O motivo é um erro no compilador de shader de superfície do Unity causado pelo uso simultâneo de uv_MainTexe uv2_MainTex. Precisamos encontrar uma solução alternativa.

Em vez de usá- uv2_MainTexlo, teremos que transferir as coordenadas UV secundárias manualmente. Para fazer isso, renomeie uv2_MainTexpara riverUV. Em seguida, adicione uma função de vértice ao sombreador, que atribui coordenadas a ele.

  #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); … } 


Interpolação baseada no valor da costa.

A interpolação funciona, com exceção dos vértices esquerdo e direito no topo. Nesses pontos, o rio deve desaparecer. Portanto, não podemos usar o valor da costa. Teremos que usar um valor diferente, que nesses dois vértices é 0. Felizmente, ainda temos a coordenada U do primeiro conjunto de UV, para que possamos armazenar esse valor lá.

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


A mistura certa.

Agora, a boca tem uma boa mistura entre o rio em expansão, a água costeira e a espuma. Embora isso não crie uma correspondência exata com as cachoeiras, esse efeito também fica bem com as cachoeiras.


Estuários em ação

unitypackage

Rios que fluem de corpos d'água


Já temos rios fluindo para corpos d'água, mas não há suporte para rios fluindo em uma direção diferente. Existem lagos dos quais os rios correm, então precisamos adicioná-los também.

Quando um rio flui para fora de um corpo d'água, ele realmente flui em direção a uma altitude mais alta. No momento, isso não é possível. Precisamos abrir uma exceção e permitir essa situação se o nível da água corresponder à altura do ponto alvo. Vamos adicionar a um HexCellmétodo privado que verifica, de acordo com nosso novo critério, se o vizinho é o ponto de destino correto para o rio de saída.

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

Usaremos nosso novo método para determinar se é possível criar um rio de saída.

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

Também é necessário verificar o rio ao alterar a altura da célula ou o nível da água. Vamos criar um método privado que fará essa tarefa.

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

Usaremos esse novo método nas propriedades Elevatione WaterLevel.

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


Saída e entrada de lagos fluviais.

Vire a maré


Criamos HexGridChunk.TriangulateEstuary, sugerindo que os rios só podem fluir para corpos d'água. Portanto, como resultado, o curso do rio sempre se move em uma direção. Precisamos reverter o fluxo ao lidar com um rio que sai de um corpo d'água. Para fazer isso, você precisa TriangulateEstuarysaber sobre a direção do fluxo. Portanto, damos a ele um parâmetro booleano que determina se estamos lidando com um rio de entrada.

  void TriangulateEstuary ( EdgeVertices e1, EdgeVertices e2, bool incomingRiver ) { … } 

Passaremos essas informações ao chamar esse método a partir de TriangulateWaterShore.

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

Agora precisamos expandir o fluxo do rio alterando as coordenadas do UV2. As coordenadas U para rios de saída precisam ser espelhadas: -0,5 se torna 1,5, 0 se torna 1, 1 se torna 0 e 1,5 se torna -0,5.

Com as coordenadas V, as coisas são um pouco mais complicadas. Se você observar como trabalhamos com conexões fluviais invertidas, 0,8 deve ser 0 e 1 deve ser -0,2. Isso significa que 1.1 se torna -0,3 e 1,15 se torna -0,35.

Como em cada caso as coordenadas UV2 são muito diferentes, vamos escrever um código separado para elas.

  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) ); } } 


O curso correto dos rios.

unitypackage

Parte 9: recursos de alívio


  • Adicione objetos ao relevo.
  • Criamos suporte para níveis de densidade de objetos.
  • Nós usamos vários objetos no nível.
  • Misture três tipos diferentes de objetos.

Nesta parte, falaremos sobre adicionar objetos ao terreno. Criaremos objetos como edifícios e árvores.


Conflito entre florestas, terras agrícolas e urbanização.

Adicionar suporte para objetos


Embora a forma do relevo tenha variações, até agora nada está acontecendo nele. Esta é uma terra sem vida. Para dar vida a ele, você precisa adicionar esses objetos. como árvores e casas. Esses objetos não fazem parte da malha de relevo, mas serão objetos separados. Mas isso não nos impede de adicioná-los ao triangular o terreno.

HexGridChunkNão importa como a malha funciona. Ele simplesmente ordena que um de seus filhos HexMeshadicione um triângulo ou quad. Da mesma forma, ele pode ter um elemento filho que lida com a colocação de objetos neles.

Gerenciador de Objetos


Vamos criar um componente HexFeatureManagerque cuida de objetos em um único fragmento. Nós usamos o mesmo esquema que em HexMesh- dê a ele métodos Clear, Applye AddFeature. Como o objeto precisa ser colocado em algum lugar, o método AddFeaturerecebe o parâmetro position.

Começaremos com uma implementação em branco que não fará nada por enquanto.

 using UnityEngine; public class HexFeatureManager : MonoBehaviour { public void Clear () {} public void Apply () {} public void AddFeature (Vector3 position) {} } 

Agora podemos adicionar um link para esse componente no HexGridChunk. Em seguida, você pode incluí-lo no processo de triangulação, como todos os elementos filhos 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(); } 

Vamos começar colocando um objeto no centro de cada célula

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

Agora precisamos de um gerenciador de objetos real. Adicione outro filho ao prefab Hex Grid Chunk e forneça um componente HexFeatureManager. Então você pode conectar um fragmento a ele.




Um gerenciador de objetos adicionado ao prefab do fragmento.

Objetos pré-fabricados


Que objeto de terreno criaremos? Para o primeiro teste, um cubo é bastante adequado. Vamos criar um cubo grande o suficiente, por exemplo, com uma escala de (3, 3, 3) e transformá-lo em uma pré-fabricada. Também crie material para ele. Eu usei o material padrão com vermelho. Vamos remover o colisor, porque não precisamos dele.


Cubo pré-fabricado.

Os gerenciadores de objetos precisarão de um link para essa pré-fabricada, portanto, adicione-o HexFeatureManagere conecte-o. Como o acesso ao componente de transformação é necessário para colocar o objeto, nós o usamos como o tipo de link.

  public Transform featurePrefab; 


Gerenciador de objetos com prefab.

Criando instâncias de objetos


A estrutura está pronta e podemos começar a adicionar recursos do terreno! Basta criar uma instância da pré-fabricada HexFeatureManager.AddFeaturee definir sua posição.

  public void AddFeature (Vector3 position) { Transform instance = Instantiate(featurePrefab); instance.localPosition = position; } 


Instâncias de recursos do terreno.

A partir de agora, o terreno será preenchido com cubos. Pelo menos as metades superiores dos cubos, porque a origem local da malha do cubo no Unity está no centro do cubo e a parte inferior está sob a superfície do relevo. Para colocar cubos na topografia, precisamos movê-los para metade da altura.

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


Cubos na superfície do relevo.

E se usarmos outra malha?
. , , . .

Obviamente, nossas células estão distorcidas, portanto, precisamos distorcer a posição dos objetos. Então nos livramos da repetibilidade perfeita da malha.

  instance.localPosition = HexMetrics.Perturb(position); 


Posições distorcidas de objetos.

Destruição de objetos de relevo


Cada vez que um fragmento é atualizado, criamos novos objetos de relevo. Isso significa que, enquanto criamos mais e mais objetos nas mesmas posições. Para evitar duplicatas, precisamos nos livrar de objetos antigos ao limpar um fragmento.

A maneira mais rápida de fazer isso é criando um objeto de contêiner de jogo e transformando todos os objetos de alívio em seus filhos. Então, quando chamado, Cleardestruiremos esse contêiner e criaremos um novo. O contêiner em si será filho de seu 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); } 

Provavelmente, é ineficiente criar e destruir objetos de socorro sempre.
, , . . . , , , . HexFeatureManager.Apply . . , , .

unitypackage

Colocação de objetos de relevo


Enquanto colocamos objetos no centro de cada célula. Para células vazias, isso parece normal, mas em células contendo rios e estradas, além de inundadas com água, parece estranho.


Objetos estão por toda parte.

Portanto, vamos verificar antes de colocar o objeto HexGridChunk.Triangulatese a célula está vazia.

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


Alojamento limitado.

Um objeto por direção


Apenas um objeto por célula não é demais. Ainda há muito espaço para um monte de objetos. Portanto, adicionamos um objeto adicional ao centro de cada um dos seis triângulos da célula, ou seja, um por direção.

Faremos isso em outro método Triangulate, quando soubermos que não há rio na célula. Ainda precisamos verificar se estamos debaixo d'água e se há uma estrada na cela. Mas, neste caso, estamos interessados ​​apenas nas estradas que seguem na direção atual.

  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)); } } … } 


Muitas instalações, mas não nas proximidades de rios.

Isso cria muito mais objetos! Eles aparecem perto das estradas, mas ainda evitam os rios. Para colocar objetos ao longo dos rios, também podemos adicioná-los dentro TriangulateAdjacentToRiver. Mas novamente apenas quando o triângulo não está embaixo d'água e não há estrada nele.

  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)); } } 


Objetos apareceram ao lado dos rios.

É possível renderizar tantos objetos?
, dynamic batching Unity. , . batch. « », . instancing, dynamic batching.

unitypackage

Variedade de objetos


Todos os nossos objetos de alívio têm a mesma orientação, que parece completamente antinatural. Vamos dar a cada um um toque aleatório.

  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); } 


Voltas aleatórias.

Portanto, o resultado se torna muito mais diversificado. Infelizmente, toda vez que um fragmento é atualizado, os objetos recebem uma nova rotação aleatória. A edição de células não deve alterar objetos na vizinhança; portanto, precisamos de uma abordagem diferente.

Temos uma textura de ruído que é sempre a mesma. No entanto, essa textura contém ruído de gradiente Perlin e é localmente consistente. É exatamente isso que precisamos ao distorcer as posições dos vértices nas células. Mas as curvas não precisam ser consistentes. Todos os turnos devem ser igualmente prováveis ​​e misturados. Portanto, precisamos de uma textura com valores aleatórios não gradientes, que possam ser amostrados sem filtragem bilinear. Essencialmente, essa é uma grade de hash que forma a base do ruído gradiente.

Criando uma tabela de hash


Podemos criar uma tabela de hash a partir de uma matriz de valores flutuantes e preenchê-la uma vez com valores aleatórios. Graças a isso, não precisamos de uma textura. Vamos adicioná-lo HexMetrics. Um tamanho de 256 por 256 é suficiente para variação 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; } } 

Valores aleatórios são gerados por uma fórmula matemática que sempre fornece os mesmos resultados. A sequência resultante depende do número de sementes, que por padrão é igual ao valor atual do tempo. É por isso que em cada sessão de jogo obteremos resultados diferentes.

Para garantir que objetos sempre idênticos sejam recriados, precisamos adicionar o parâmetro seed ao método de inicialização.

  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; } } 

Agora que inicializamos o fluxo de números aleatórios, sempre obteremos a mesma sequência. Portanto, eventos aparentemente aleatórios que ocorrem após a geração do mapa também serão sempre os mesmos. Podemos evitar isso armazenando o estado do gerador de números aleatórios antes de inicializá-lo. Depois de concluir o trabalho, podemos perguntar a ele o estado antigo.

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

A tabela de hash é inicializada HexGridao mesmo tempo em que atribui a textura do ruído. Ou seja, nos métodos HexGrid.Starte HexGrid.Awake. Fazemos isso para que os valores não sejam gerados com mais frequência do que o necessário.

  public int seed; void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); … } void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); } } 

A variável de semente genérica nos permite selecionar o valor de semente para o mapa. Qualquer valor serve. Eu escolhi 1234.


A escolha da semente.

Usando uma tabela de hash


Para usar a tabela de hash, adicione ao HexMetricsmétodo de amostragem. Assim SampleNoise, ele usa as coordenadas da posição XZ para obter o valor. O índice de hash é encontrado restringindo as coordenadas a valores inteiros e obtendo o restante da divisão inteira pelo tamanho da tabela.

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

O que% faz?
, , — . , −4, −3, −2, −1, 0, 1, 2, 3, 4 modulo 3 −1, 0, −2, −1, 0, 1, 2, 0, 1.

Isso funciona para coordenadas positivas, mas não para negativas, porque para esses números o restante será negativo. Podemos corrigir isso adicionando o tamanho da tabela aos resultados negativos.

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

Agora, para cada unidade quadrada, criamos nosso próprio valor. No entanto, de fato, não precisamos dessa densidade de tabela. Os objetos são espaçados um do outro. Podemos esticar a tabela reduzindo a escala de posição antes de calcular o índice. Um valor único para um quadrado de 4 por 4 será suficiente para nós.

  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]; } 

Vamos voltar HexFeatureManager.AddFeaturee usar nossa nova tabela de hash para obter o valor. Depois de aplicá-lo para especificar a rotação, os objetos permanecerão estacionários ao editar o 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); } 

Limite de posicionamento


Embora os objetos tenham rotações diferentes, um padrão ainda é perceptível em sua colocação. Cada célula possui sete objetos. Podemos adicionar caos a esse esquema, ignorando arbitrariamente alguns dos objetos. Como decidimos se adicionamos um objeto ou não? Claro, verificando outro valor aleatório!

Ou seja, agora, em vez de um valor de hash, precisamos de dois. Seu suporte pode ser adicionado usando hashes em vez de uma floatvariável como o tipo de matriz da tabela Vector2. Mas as operações de vetor não fazem sentido para valores de hash, então vamos criar uma estrutura especial para esse propósito. Ela precisará apenas de dois valores flutuantes. E vamos adicionar um método estático para criar um par de valores aleatórios.

 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; } } 

Não precisa ser serializado?
, , Unity. , .

Altere-o HexMetricspara que ele use a nova estrutura.

  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) { … } 

Agora HexFeatureManager.AddFeaturetem acesso a dois valores de hash. Vamos usar o primeiro para decidir se deseja adicionar ou não um objeto. Se o valor for igual ou superior a 0,5, pule. Ao fazer isso, nos livraremos de cerca de metade dos objetos. O segundo valor será usado como de costume para determinar a rotação.

  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); } 


A densidade dos objetos é reduzida em 50%.

unitypackage

Objetos de desenho


Em vez de colocar objetos em qualquer lugar, vamos editá-los. Mas não desenharemos objetos separados, mas adicionaremos o nível de objetos a cada célula. Este nível controlará a probabilidade de objetos aparecerem na célula. Por padrão, o valor é zero, ou seja, os objetos estarão ausentes.

Como os cubos vermelhos em nosso terreno não se parecem com objetos naturais, vamos chamá-los de edifícios. Eles representarão urbanização. Vamos adicionar ao HexCellnível de urbanização.

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

Podemos fazer com que o nível de urbanização de uma célula subaquática seja igual a zero, mas isso não é necessário; de qualquer maneira, pulamos a criação de objetos subaquáticos. E talvez em algum momento adicionaremos corpos d'água de urbanização, como docas e estruturas subaquáticas.

Controle deslizante de densidade


Para alterar o nível de urbanização, adicionamos HexMapEditormais um controle deslizante no suporte.

  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(); } … } } 

Adicione outro controle deslizante à interface do usuário e combine-o com os métodos apropriados. Vou colocar um novo painel no lado direito da tela para evitar transbordar o painel esquerdo.

Quantos níveis precisamos? Vamos nos debruçar sobre quatro, denotando zero, baixa, média e alta densidade.



Controle deslizante de urbanização.

Alteração de limite


Agora que temos o nível de urbanização, precisamos usá-lo para determinar se devemos colocar objetos. Para fazer isso, precisamos adicionar o nível de urbanização como um parâmetro adicional para HexFeatureManager.AddFeature. Vamos dar mais um passo e apenas transferir a própria célula. No futuro, será mais conveniente para nós.

A maneira mais rápida de usar o nível de urbanização é multiplicá-lo por 0,25 e usar o valor como o novo limite para ignorar objetos. Devido a isso, a probabilidade da aparência do objeto aumentará em cada nível em 25%.

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

Para que isso funcione, vamos passar as células para 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)); } } 


Desenho de níveis de densidade de urbanização.

unitypackage

Vários prefabs de objetos de alívio


As diferenças na probabilidade de os objetos aparecerem não são suficientes para criar uma clara separação entre os níveis baixo e alto de urbanização. Em algumas células, simplesmente haverá mais ou menos do que o número esperado de edifícios. Podemos deixar a diferença mais clara usando nossa própria casa pré-fabricada para cada nível.

Nós livrar-se dos campos featurePrefabem HexFeatureManagere substituí-lo com uma variedade de prefabs urbanização. Para obter a pré-fabricada apropriada, subtrairemos uma do nível de urbanização e usaremos o valor como um índice.

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

Crie duas duplicatas da pré-fabricada do objeto, renomeie e altere-as para que elas indiquem três níveis diferentes de urbanização. O nível 1 é de baixa densidade; portanto, usamos um cubo com um comprimento unitário de uma aresta, denotando um barraco. Vou escalar o nível 2 pré-fabricado para (1,5, 2, 1,5) para que pareça um prédio de dois andares. Para edifícios altos de nível 3, usei a balança (2, 5, 2).



Utilizando prefabs diferentes para cada nível de urbanização.

Mistura pré-fabricada


Não somos obrigados a nos limitar a uma separação estrita dos tipos de edifícios. Você pode misturá-los um pouco, como acontece no mundo real. Em vez de um limite por nível, vamos usar três, um para cada tipo de construção.

No nível 1, usamos a colocação de barracos em 40% dos casos. Não haverá outros edifícios aqui. Para o nível, usamos os três valores (0,4, 0, 0).

No nível 2, substitua os barracos por prédios maiores e adicione 20% de chance de barracos adicionais. Não faremos edifícios altos. Ou seja, usamos o limite de três valores (0,2, 0,4, 0).

No nível 3, substituímos prédios médios por altos, substituímos os barracos novamente e adicionamos mais 20% de chance de barracos. Os valores limite serão iguais a (0,2, 0,2, 0,4).

Ou seja, a ideia é que, com o aumento do nível de urbanização, atualizaremos os edifícios existentes e adicionaremos novos a lugares vazios. Para remover um edifício existente, precisamos usar os mesmos intervalos de valor de hash. Se os hashes entre 0 e 0,4 no nível 1 eram barracos, no nível 3 o mesmo intervalo criará edifícios altos. No nível 3, edifícios altos devem ser criados com hashes no intervalo de 0 a 0,4, edifícios de dois andares no intervalo de 0,4 a 0,6 e barracos no intervalo de 0,6 a 0,8. Se você os verificar do maior para o menor, isso poderá ser feito usando o triplo de limites (0,4, 0,6, 0,8). Os limites do nível 2 se tornarão (0, 0,4, 0,6) e os limites do nível 1 se tornarão (0, 0, 0,4).

Vamos salvar esses limites emHexMetricscomo uma coleção de matrizes com um método que permite obter limites para um determinado nível. Como estamos interessados ​​apenas nos níveis com objetos, ignoramos o nível 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]; } 

Em seguida, adicione ao HexFeatureManagermétodo que usa o nível e o valor do hash para selecionar a pré-fabricada. Se o nível for maior que zero, obteremos limites usando um nível reduzido em um. Em seguida, percorremos os limites até que um deles exceda o valor do hash. Isso significa que encontramos uma casa pré-fabricada. Se não encontrarmos, retorne 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; } 

Essa abordagem requer a reordenação de links para pré-fabricados, para que eles passem de alta a baixa densidade.


Ordem pré-fabricada invertida.

Usaremos nosso novo método AddFeaturepara selecionar uma pré-fabricada. Se não o recebermos, pularemos o objeto. Caso contrário, crie uma instância e continue como antes.

  public void AddFeature (HexCell cell, Vector3 position) { HexHash hash = HexMetrics.SampleHashGrid(position); // if (hash.a >= cell.UrbanLevel * 0.25f) { // return; // } // Transform instance = Instantiate(urbanPrefabs[cell.UrbanLevel - 1]); Transform prefab = PickPrefab(cell.UrbanLevel, hash.a); 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.b, 0f); instance.SetParent(container, false); } 


Misture as casas pré-fabricadas.

Variações de nível


Agora temos edifícios bem misturados, mas até agora existem apenas três. Podemos aumentar ainda mais a variabilidade vinculando uma coleção de pré-fabricados a cada nível de densidade de urbanização. Depois disso, será possível escolher um deles aleatoriamente. Isso exigirá um novo valor aleatório; portanto, adicione um terceiro 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; } 

Transformar HexFeatureManager.urbanPrefabsem uma matriz de matrizes, e adicionar ao método PickPrefabparâmetro choice. Nós o usamos para selecionar o índice da matriz interna, multiplicando-a pelo comprimento dessa matriz e convertendo-o em número inteiro.

  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; } 

Vamos justificar nossa escolha no valor do segundo hash (B). Então você precisa mudar de B para 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, precisamos considerar o que Random.valuepode retornar um valor de 1. Por causa disso, o índice da matriz pode ir além. Para impedir que isso aconteça, vamos dimensionar levemente os valores de hash. Simplesmente escalamos todos eles para não nos preocuparmos com o 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; } 

Infelizmente, o inspetor não exibe matrizes de matrizes. Portanto, não podemos configurá-los. Para contornar essa limitação, crie uma estrutura serializável na qual encapsular a matriz interna. Vamos dar a ela um método que converte da escolha no índice da matriz e retorna uma pré-fabricada.

 using UnityEngine; [System.Serializable] public struct HexFeatureCollection { public Transform[] prefabs; public Transform Pick (float choice) { return prefabs[(int)(choice * prefabs.Length)]; } } 

No HexFeatureManagerlugar das matrizes internas, usamos uma matriz dessas coleções.

 // public Transform[][] urbanPrefabs; public HexFeatureCollection[] urbanCollections; … 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 urbanCollections[i].Pick(choice); } } } return null; } 

Agora podemos atribuir vários edifícios a cada nível de densidade. Como eles são independentes, não precisamos usar a mesma quantidade por nível. Eu apenas usei duas opções por nível, adicionando uma opção mais baixa a cada uma. Escolhi as escalas para eles (3,5, 3, 2), (2,75, 1,5, 1,5) e (1,75, 1, 1).



Dois tipos de edifícios por nível de densidade.

unitypackage

Vários tipos de objetos


No esquema existente, podemos criar estruturas urbanas bastante dignas. Mas o alívio pode conter não apenas edifícios. E quanto a fazendas ou plantas? Vamos adicionar aos HexCellníveis e para eles. Eles não são mutuamente exclusivos e podem se misturar.

  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; 

Obviamente, isso requer suporte em HexMapEditordois controles deslizantes adicionais.

  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; } … } } 

Adicione-os à interface do usuário.


Três controles deslizantes.

Além disso, serão necessárias coleções adicionais HexFeatureManager.

  public HexFeatureCollection[] urbanCollections, farmCollections, plantCollections; 


Três coleções de objetos de relevo.

Criei para fazendas e fábricas dois pré-fabricados por nível de densidade, bem como para a construção de coleções. Para todos eles, usei cubos. As fazendas têm material verde claro, as plantas têm material verde escuro.

Fiz cubos agrícolas com uma altura de 0,1 unidades para indicar lotes quadrados de terras agrícolas. Como as escalas de alta densidade, eu escolhi (2,5, 0,1, 2,5) e (3,5, 0,1, 2). Em média, os sites têm uma área de 1,75 e um tamanho de 2,5 por 1,25. Um baixo nível de densidade foi obtido com uma área de 1 e um tamanho de 1,5 por 0,75.

As plantas pré-fabricadas denotam árvores altas e arbustos grandes. As casas pré-fabricadas de alta densidade são as maiores (1,25, 4,5, 1,25) e (1,5, 3, 1,5). As escalas médias são (0,75, 3, 0,75) e (1, 1,5, 1). As plantas menores têm tamanhos (0,5, 1,5, 0,5) e (0,75, 1, 0,75).

Seleção de recursos de alívio


Cada tipo de objeto deve receber seu próprio valor de hash para que eles tenham diferentes padrões de criação e você possa combiná-los. Adicione HexHashdois valores adicionais.

  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; } 

Agora você tem HexFeatureManager.PickPrefabque trabalhar com diferentes coleções. Adicione um parâmetro para simplificar o processo. Além disso, altere o hash usado pela variante da prefab selecionada para D e o hash da rotação para 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); } 

Atualmente AddFeatureseleciona a urbanização pré-fabricada. Isso é normal, precisamos de mais opções. Portanto, adicionamos outra pré-fabricada das fazendas. Como um valor de hash, use B. A escolha da opção será novamente D.

  Transform prefab = PickPrefab( urbanCollections, cell.UrbanLevel, hash.a, hash.d ); Transform otherPrefab = PickPrefab( farmCollections, cell.FarmLevel, hash.b, hash.d ); if (!prefab) { return; } 

Que tipo de instância pré-fabricada criaremos como resultado? Se um deles for nulo, a escolha é óbvia. No entanto, se os dois existirem, precisamos tomar uma decisão. Vamos apenas adicionar a pré-fabricada com o menor valor de hash.

  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; } 


Uma mistura de objetos urbanos e rurais.
Em seguida, faça o mesmo com as plantas usando o valor do 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; } 

No entanto, não podemos simplesmente copiar o código. Quando escolhemos o objeto rural em vez do objeto urbano, precisamos comparar o hash das plantas com o hash das fazendas, e não com o urbano. Portanto, precisamos rastrear o hash que decidimos escolher e comparar com ele.

  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; } 


Uma mistura de objetos urbanos, rurais e vegetais.

unitypackage

Parte 10: paredes


  • Fechamos as células.
  • Construímos paredes ao longo das bordas das células.
  • Vamos passar por rios e estradas.
  • Evite a água e conecte-se com falésias.

Nesta parte, adicionaremos entre as células da parede.


Não há nada mais convidativo do que um muro alto.

Edição de parede


Para apoiar as paredes, precisamos saber onde colocá-las. Vamos colocá-los entre as células ao longo das bordas que os conectam. Como os objetos já existentes estão localizados na parte central das células, não precisamos nos preocupar que as paredes passem por eles.


Paredes ao longo das bordas.

Paredes são objetos de terreno, embora grandes. Como outros objetos, não os editaremos diretamente. Em vez disso, mudaremos as células. Não teremos segmentos separados das paredes, mas estaremos envolvidos no fechamento das células como um todo.

Propriedade murada


Para apoiar as células fortificadas adicionar à HexCellpropriedade Walled. Esta é uma opção simples. Como as paredes estão localizadas entre as células, precisamos atualizar as células editadas e seus vizinhos.

  public bool Walled { get { return walled; } set { if (walled != value) { walled = value; Refresh(); } } } bool walled; 

Interruptor do editor


Para mudar o estado "protegido" das células, precisamos adicionar HexMapEditorsuporte para o switch. Portanto, adicionamos outro campo OptionalTogglee um método para defini-lo.

  OptionalToggle riverMode, roadMode, walledMode; … public void SetWalledMode (int mode) { walledMode = (OptionalToggle)mode; } 

Ao contrário dos rios e estradas, as paredes não vão de célula em célula, mas estão entre elas. Portanto, não precisamos pensar em arrastar e soltar. Quando a chave de parede está ativa, simplesmente configuramos o estado protegido da célula atual com base no estado dessa chave.

  void EditCell (HexCell cell) { if (cell) { … if (roadMode == OptionalToggle.No) { cell.RemoveRoads(); } if (walledMode != OptionalToggle.Ignore) { cell.Walled = walledMode == OptionalToggle.Yes; } if (isDrag) { … } } } 

Duplicamos um dos elementos anteriores das opções da interface do usuário e os alteramos para que eles controlem o estado de "esgrima". Vou colocá-los no painel da interface do usuário juntamente com outros objetos.


O interruptor "esgrima".

unitypackage

Criando paredes


Como as paredes seguem os contornos das células, elas não devem ter uma forma constante. Portanto, não podemos simplesmente usar uma pré-fabricada para eles, como fizemos com outros recursos do terreno. Em vez disso, precisamos construir uma malha, como fizemos com o alívio. Isso significa que nosso fragmento pré-fabricado precisa de outro elemento filho HexMesh. Duplique uma das outras malhas filho e faça com que os novos objetos Walls projetem sombras. Eles não precisam de nada, exceto vértices e triângulos, portanto, todas as opções HexMeshdevem estar desabilitadas.



Paredes pré-fabricadas subsidiárias.

Será lógico que as paredes são um objeto urbano, então, para elas, usei o material vermelho dos edifícios.

Gerenciamento de parede


Como as paredes são objetos de alívio, elas precisam lidar com elas HexFeatureManager. Portanto, forneceremos ao gerente dos objetos de alívio um link para o objeto Walls e faremos com que ele chame os métodos Cleare Apply.

  public HexMesh walls; … public void Clear () { … walls.Clear(); } public void Apply () { walls.Apply(); } 


Paredes conectadas ao gerenciador de topografia.

Walls não deveria ser filho de Features?
, . , Walls Hex Grid Chunk .

Agora precisamos adicionar um método ao gerente que nos permita adicionar paredes a ele. Como as paredes estão ao longo das bordas entre as células, ele precisa conhecer os vértices correspondentes das bordas e células. HexGridChunkfará com que ela atravesse TriangulateConnection, no momento da triangulação da célula e de um de seus vizinhos. Desse ponto de vista, a célula atual está no lado próximo da parede e a outra no lado oposto.

  public void AddWall ( EdgeVertices near, HexCell nearCell, EdgeVertices far, HexCell farCell ) { } 

Vamos chamar esse novo método HexGridChunk.TriangulateConnectionapós a conclusão de todos os outros trabalhos de conexão e imediatamente antes da transição para o triângulo angular. Vamos deixar o gerente dos objetos de socorro decidir por si mesmo onde a parede deve realmente estar localizada.

  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) { … } } 

Construa um segmento de parede


A parede inteira serpenteará através de várias bordas das células. Cada aresta contém apenas um elemento de parede. Do ponto de vista da célula próxima, o segmento começa no lado esquerdo da costela e termina no lado direito. Vamos adicionar a um HexFeatureManagermétodo separado que usa quatro vértices nos cantos de uma aresta.

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { } 


Lados próximos e distantes.

AddWallpode chamar esse método com a primeira e a última arestas das arestas. Mas paredes só devem ser adicionadas quando tivermos uma conexão entre uma célula cercada e uma célula não cercada. Não importa qual das células está dentro e qual está fora, apenas a diferença em seus estados é levada em consideração.

  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); } } 

O segmento mais simples da parede é um quad, localizado no meio da costela. Vamos encontrar seus picos mais baixos, interpolando para o meio, do mais próximo ao mais distante.

  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); } 

Qual deve ser a altura da parede? Vamos definir sua altura para HexMetrics. Eu criei o tamanho de um nível de altura de célula.

  public const float wallHeight = 3f; 

HexFeatureManager.AddWallSegmentpode usar essa altura para posicionar o terceiro e o quarto vértices do quad e também adicioná-lo à malha 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); 

Agora podemos editar as paredes e elas serão exibidas como faixas quad. No entanto, não veremos uma parede contínua. Cada quad é visível apenas de um lado. Seu rosto é direcionado para a célula da qual foi adicionado.


Quadriláteros unilaterais.

Podemos resolver rapidamente esse problema adicionando um segundo quad voltado para o outro lado.

  walls.AddQuad(v1, v2, v3, v4); walls.AddQuad(v2, v1, v4, v3); 


Paredes bilaterais.

Agora todas as paredes são visíveis por inteiro, mas ainda existem buracos nos cantos das células onde as três células se encontram. Vamos preenchê-los mais tarde.

Paredes grossas


Embora as paredes já sejam visíveis nos dois lados, elas não têm espessura. De fato, as paredes são finas, como papel, e quase invisíveis em um determinado ângulo. Então, vamos torná-los inteiros adicionando espessura. Defina a espessura em HexMetrics. Eu escolhi um valor de 0,75 unidades, me pareceu adequado.

  public const float wallThickness = 0.75f; 

Para fazer duas paredes grossas, você precisa separar dois quadriláteros para os lados. Eles devem se mover em direções opostas. Um lado deve se mover em direção à borda próxima e o outro em direção à borda mais distante. O vetor de deslocamento para isso é igual far - near, mas para deixar a parte superior da parede plana, precisamos definir seu componente Y como 0.

Como isso precisa ser feito para os lados esquerdo e direito do segmento de parede, vamos adicionar um HexMetricsvetor de deslocamento ao método para calcular isso.

  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 a parede permaneça no centro da nervura, a distância real do movimento ao longo desse vetor deve ser igual à metade da espessura de cada lado. E para garantir que realmente movemos a distância certa, normalizamos o vetor de deslocamento antes de escalá-lo.

  return offset.normalized * (wallThickness * 0.5f); 

Usamos esse método HexFeatureManager.AddWallSegmentpara alterar a posição dos quadriláteros. Como o vetor de deslocamento vai da célula mais próxima para a célula remota, subtraia-o do quad próximo e adicione-o à célula remota.

  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 com compensações.

Os quadriláteros agora são tendenciosos, embora isso não seja totalmente perceptível.

As espessuras das paredes são iguais?
, «-» . , . . , . , . , - , . .

Tampos das paredes


Para tornar a espessura da parede visível de cima, precisamos adicionar um quad ao topo da parede. A maneira mais fácil de fazer isso é lembrando os dois vértices superiores do primeiro quad e conectando-os com os dois vértices superiores do 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 com tampos.

Encurralamento


Ainda temos buracos nos cantos das células. Para preenchê-los, precisamos adicionar um segmento à área triangular entre as células. Cada canto conecta três células. Cada célula pode ou não ter uma parede. Ou seja, oito configurações são possíveis.


Configurações de ângulo.

Colocamos paredes apenas entre células com diferentes estados cercados. Isso reduz o número de configurações para seis. Em cada uma delas, uma das células está dentro da curva das paredes. Vamos considerar essa célula como um ponto de referência em torno do qual a parede é curva. Do ponto de vista dessa célula, a parede começa com uma aresta comum com a célula esquerda e termina com uma aresta comum com a célula direita.


Funções da célula.

Ou seja, precisamos criar um método AddWallSegmentcujos parâmetros sejam três vértices do canto. Embora possamos escrever código para triangular esse segmento, na verdade, é um caso especial do método AddWallSegment. Um ponto de ancoragem desempenha o papel de ambos os vértices próximos.

  void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { AddWallSegment(pivot, left, pivot, right); } 

Em seguida, crie uma variante do método AddWallpara os três vértices do ângulo e suas células. O objetivo deste método é determinar o ângulo, que é o ponto de referência, se existir. Portanto, ele deve considerar todas as oito configurações possíveis e solicitar AddWallSegmentseis delas.

  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 adicionar segmentos de canto, chame esse método no 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 com cantos, mas ainda existem buracos.

Feche os orifícios


Ainda existem orifícios nas paredes porque a altura dos segmentos da parede é variável. Enquanto os segmentos ao longo das arestas são de altura constante, os segmentos de canto estão entre duas arestas diferentes. Como cada aresta pode ter sua própria altura, os furos aparecem nos cantos.

Para corrigir isso, altere AddWallSegment-o para que armazene separadamente as coordenadas Y dos vértices superiores esquerdo e direito.

  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); 


Paredes fechadas.

As paredes agora estão fechadas, mas você provavelmente ainda vê buracos nas sombras da parede. Isso é causado pelo parâmetro Normal Bias da configuração de sombra direcional. Quando é maior que zero, os triângulos dos objetos que projetam uma sombra se movem ao longo do normal para a superfície. Isso evita a auto-sombra, mas ao mesmo tempo cria buracos nos casos em que os triângulos olham em direções diferentes. Nesse caso, buracos podem ser criados nas sombras da geometria fina, por exemplo, como nossas paredes.

Você pode se livrar desses artefatos de sombra abaixando o viés normal para zero. Ou altere o modo de parede do renderizador de malha Cast Shadows para Dois lados . Isso fará com que o objeto de projeção de sombra renderize os dois lados de cada triângulo da parede para renderização, o que fechará todos os furos.


Não há mais buracos nas sombras.

unitypackage

Parede de borda


Até agora, nossas paredes são retas o suficiente. Para um terreno plano, isso não é nada ruim, mas parece estranho quando as paredes coincidem com as bordas. Isso acontece quando há uma diferença de um nível de altura entre as células em lados opostos da parede.


Paredes retas nas bordas.

Siga a borda


Em vez de criar um segmento para toda a aresta, criaremos um para cada parte da faixa da aresta. Podemos fazer isso chamando quatro vezes AddWallSegmentna versão AddWalledge.

  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); } } 


Paredes curvas.

As paredes agora repetem a forma das arestas distorcidas. Em combinação com as bordas, parece muito melhor. Além disso, cria paredes mais interessantes em um relevo plano.

Colocando paredes no chão


Olhando para as paredes nas bordas, você pode encontrar um problema. As paredes estão penduradas no chão! Isso é verdade para bordas planas inclinadas, mas geralmente não é tão perceptível.


Paredes penduradas no ar.

Para resolver o problema, precisamos abaixar os muros. A maneira mais fácil é abaixar a parede inteira para que seu topo permaneça plano. Ao mesmo tempo, uma parte da parede no lado superior abaixará um pouco o relevo, mas isso nos convém.

Para abaixar a parede, precisamos determinar qual lado é mais baixo - próximo ou distante. Podemos apenas usar a altura do lado mais baixo, mas não precisamos ir tão baixo. Você pode interpolar a coordenada Y de baixo para alto com um deslocamento de pouco menos de 0,5. Como as paredes apenas ocasionalmente se tornam mais altas que o degrau mais baixo da borda, podemos usar o degrau vertical da borda como um deslocamento. Uma espessura de parede diferente da configuração da borda pode exigir um deslocamento diferente.


A parede abaixada.

Vamos adicionar ao HexMetricsmétodo WallLerpque lida com essa interpolação, além de calcular a média das coordenadas X e Z dos vértices próximos e distantes. É baseado em um 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; } 

Force HexFeatureManagereste método a determinar os vértices esquerdo e direito.

  void AddWallSegment ( Vector3 nearLeft, Vector3 farLeft, Vector3 nearRight, Vector3 farRight ) { Vector3 left = HexMetrics.WallLerp(nearLeft, farLeft); Vector3 right = HexMetrics.WallLerp(nearRight, farRight); … } 


Paredes em pé no chão.

Mudança na distorção da parede


Agora nossas paredes estão de acordo com as diferenças de altura. Mas eles ainda não correspondem totalmente às bordas distorcidas, embora estejam perto delas. Isso aconteceu porque primeiro determinamos o topo das paredes e depois as distorcemos. Como esses vértices estão em algum lugar entre os vértices das arestas próxima e distante, sua distorção será ligeiramente diferente.

O fato de as paredes seguirem incorretamente as costelas não é um problema. No entanto, a distorção dos topos da parede muda de outra forma uma espessura relativamente uniforme. Se organizarmos paredes com base em vértices distorcidos e adicionarmos quads não distorcidos, sua espessura não deverá variar muito.

  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); } 


Os topos não distorcidos das paredes.

Graças a essa abordagem, as paredes não seguirão mais as bordas com a mesma precisão de antes. Mas, em troca, ficarão menos quebrados e terão uma espessura mais constante.


Espessura de parede mais consistente.

unitypackage

Buracos nas paredes


Até agora, ignoramos a possibilidade de um rio ou estrada atravessar o muro. Quando isso acontece, devemos fazer um buraco na parede através da qual um rio ou estrada pode passar.

Para fazer isso, adicione AddWalldois parâmetros booleanos para indicar se um rio ou estrada passa por uma borda. Embora possamos lidar com eles de maneira diferente, vamos remover os dois segmentos do meio nos dois 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) { // Leave a gap. } else { 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); } } 

Agora, ele HexGridChunk.TriangulateConnectiondeve fornecer os dados necessários. Como ele já precisava das mesmas informações, vamos armazená-las em variáveis ​​booleanas e gravar as chamadas para os métodos correspondentes apenas uma 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); … } 


Os buracos nas paredes para a passagem de rios e estradas.

Cobrimos as paredes


Essas novas aberturas criam locais para completar as paredes. Precisamos fechar esses pontos finais com quadríceps para não podermos olhar através dos lados das paredes. Vamos criar um HexFeatureManagermétodo para esse fim AddWallCap. Funciona assim AddWallSegment, mas só precisa de um par de picos próximos. Faça-o adicionar um quadrilátero, indo do lado mais próximo ao outro lado da parede.

  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); } 

Quando AddWalldescobre que precisamos de um furo, adicionamos uma cobertura entre o segundo e o quarto pares de arestas. Para o quarto par de vértices, é necessário mudar a orientação, caso contrário, a face quádrupla olhará para dentro.

  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); } … } } 


Orifícios fechados nas paredes.

E os buracos nas bordas do mapa?
, . . , .

unitypackage

Evitando falésias e água


Finalmente, vamos olhar para as bordas que contêm penhascos ou água. Como os penhascos são essencialmente grandes paredes, seria ilógico colocar uma parede adicional sobre eles. Além disso, ficará ruim. Paredes subaquáticas também são completamente ilógicas, como é a restrição pelas paredes da costa.


Paredes nas falésias e na água.

Podemos remover as paredes dessas arestas desnecessárias com verificações adicionais AddWall. Uma parede não pode estar debaixo d'água, e uma costela comum com ela não pode ser um penhasco.

  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 ) { … } } 


As paredes obstrutivas ao longo das costelas foram removidas, mas os cantos permaneceram no lugar.

Remoção de cantos da parede


A remoção de segmentos de canto desnecessários exigirá um pouco mais de esforço. O caso mais simples é quando a célula de suporte está embaixo d'água. Isso garante que não haja segmentos de parede próximos que possam ser conectados.

  void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { if (pivotCell.IsUnderwater) { return; } AddWallSegment(pivot, left, pivot, right); } 


Não há mais células de suporte subaquáticas.

Agora precisamos olhar para outras duas células. Se um deles estiver embaixo d'água ou conectado à célula de suporte por um intervalo, não haverá parede ao longo dessa costela. Se isso for verdade para pelo menos um lado, não deverá haver um segmento de parede nesse canto.

Determinamos individualmente se existe uma parede esquerda ou direita. Colocamos os resultados em variáveis ​​booleanas para facilitar o trabalho.

  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); } 


Removidos todos os ângulos de interferência.

Feche os cantos


Quando não há parede na borda esquerda ou direita, o trabalho é concluído. Mas se a parede estiver em apenas uma direção, isso significa que há outro buraco na parede. Portanto, você precisa fechá-lo.

  if (hasLeftWall) { if (hasRighWall) { AddWallSegment(pivot, left, pivot, right); } else { AddWallCap(pivot, left); } } else if (hasRighWall) { AddWallCap(right, pivot); } 


Fechamos as paredes.

Conexão de paredes com falésias


Em uma situação, as paredes parecem imperfeitas. Quando a parede atinge o fundo do penhasco, termina. Mas como os penhascos não são completamente verticais, um buraco estreito é criado entre a parede e a borda do penhasco. No topo do penhasco, esse problema não surge.


Buracos entre paredes e faces de falésias.

Seria muito melhor se a parede continuasse até a beira do precipício. Podemos fazer isso adicionando outro segmento de parede entre a extremidade atual da parede e o canto superior do penhasco. Como a maior parte desse segmento ficará oculta dentro do penhasco, podemos fazer isso sem reduzir a espessura da parede para zero. Assim, basta criar uma cunha: dois quadriláteros indo ao ponto e um triângulo em cima deles. Vamos criar um método para esse fim AddWallWedge. Isso pode ser feito copiando AddWallCape adicionando um ponto de cunha.

  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; // walls.AddQuadUnperturbed(v1, v2, v3, v4); walls.AddQuadUnperturbed(v1, point, v3, pointTop); walls.AddQuadUnperturbed(point, v2, pointTop, v4); walls.AddTriangleUnperturbed(pointTop, v3, v4); } 

Nos AddWallSegmentcantos, chamaremos esse método quando a parede for em apenas uma direção e essa parede estiver a uma altura mais baixa que o outro lado. É nessas condições que encontramos a beira de um penhasco.

  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); } } 


, .

unitypackage

11:


  • .
  • .
  • .


.


Na parte anterior, adicionamos suporte de parede. Estes são segmentos simples de parede reta, sem diferenças aparentes. Agora vamos tornar as paredes mais interessantes adicionando torres a elas.

Os segmentos da parede devem ser criados proceduralmente para corresponder ao relevo. Isso não é necessário para as torres, podemos usar a pré-fabricada usual.

Podemos criar uma torre simples de dois cubos com material vermelho. A base da torre tem um tamanho de 2 por 2 unidades e uma altura de 4 unidades, ou seja, é mais espessa e mais alta que a parede. Acima deste cubo, colocaremos um cubo unitário que indica o topo da torre. Como todos os outros pré-fabricados, esses cubos não requerem colisores.

Como o modelo da torre consiste em vários objetos, nós os tornamos filhos do objeto raiz. Coloque-os de forma que a origem local da raiz esteja na base da torre. Graças a isso, podemos colocar as torres sem se preocupar com sua altura.


Torre pré-fabricada.

Adicione um link a esta pré-fabricada HexFeatureManagere conecte-o.

  public Transform wallTower; 


Link para a torre pré-fabricada.

Torres de construção


Vamos começar colocando torres no meio de cada segmento de parede. Para fazer isso, criaremos uma torre no final do método AddWallSegment. Sua posição será a média dos pontos esquerdo e direito do 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); } 


Uma torre por segmento de parede.

Temos muitas torres ao longo da parede, mas a orientação delas não muda. Precisamos mudar a rotação deles para que eles se alinhem à parede. Como temos os pontos direito e esquerdo do muro, sabemos qual direção está certa. Podemos usar esse conhecimento para determinar a orientação do segmento da parede e, portanto, da torre.

Em vez de calcular a rotação, nós simplesmente atribuímos um Transform.rightvetor à propriedade . O código da unidade alterará a rotação do objeto para que sua direção local direita corresponda ao vetor 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); 


As torres estão alinhadas com a parede.

Como funciona a atribuição Transform.right?
Quaternion.FromToRotation . .

 public Vector3 right { get { return rotation * Vector3.right; } set { rotation = Quaternion.FromToRotation(Vector3.right, value); } } 

Reduza o número de torres


Uma torre por segmento de parede é demais. Vamos tornar a adição da torre opcional, adicionando um AddWallSegmentparâmetro ao booleano. Defina-o para o valor padrão false. Nesse caso, todas as torres desaparecerão.

  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); } } 

Vamos colocar as torres apenas nos cantos das células. Como resultado, temos menos torres com distâncias razoavelmente constantes entre elas.

  void AddWallSegment ( Vector3 pivot, HexCell pivotCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … AddWallSegment(pivot, left, pivot, right, true); … } 


As torres estão apenas nos cantos.

Parece bom o suficiente, mas podemos precisar de um posicionamento menos periódico das torres. Como em outros recursos do terreno, podemos usar a tabela de hash para decidir se a torre deve ser encurralada. Para fazer isso, usamos o centro do canto para amostrar a tabela e, em seguida, compararemos um dos valores de hash com o valor limite das torres.

  HexHash hash = HexMetrics.SampleHashGrid( (pivot + left + right) * (1f / 3f) ); bool hasTower = hash.e < HexMetrics.wallTowerThreshold; AddWallSegment(pivot, left, pivot, right, hasTower); 

O valor limite refere-se a HexMetrics. Com um valor de 0,5, serão criadas torres na metade dos casos, mas podemos criar paredes com muitas torres ou sem elas.

  public const float wallTowerThreshold = 0.5f; 


Torres aleatórias.

Removemos as torres das encostas


Agora colocamos torres, independentemente da forma do terreno. No entanto, nas encostas da torre parecem ilógicas. Aqui, as paredes são inclinadas e podem atravessar o topo da torre.


Torres nas encostas.

Para evitar declives, verificaremos se as células dos cantos direito e esquerdo estão na mesma altura. Somente neste caso é possível colocar uma 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); 


Não há mais torres nas paredes das encostas.

Colocamos as paredes e torres no chão


Embora evitemos paredes nas encostas, o relevo nos dois lados da parede ainda pode ter alturas diferentes. Paredes podem correr ao longo de bordas, e células da mesma altura podem ter diferentes posições verticais. Por esse motivo, a base da torre pode estar no ar.


Torres no ar.

De fato, as paredes nas encostas também podem pairar no ar, mas isso não é tão perceptível quanto nas torres.


Paredes estão no ar.

Isso pode ser corrigido esticando a base das paredes e torres até o chão. Para fazer isso, adicione o deslocamento Y para as paredes HexMetrics. Uma unidade desativada será suficiente. Aumente a altura das torres na mesma quantidade.

  public const float wallHeight = 4f; public const float wallYOffset = -1f; 

Nós a alteramos HexMetrics.WallLerppara que, ao determinar a coordenada Y, leve em consideração o novo deslocamento.

  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; } 

Também precisamos alterar a casa pré-fabricada da torre, pois a base agora será uma unidade abaixo do solo. Portanto, aumentamos a altura do cubo base em uma unidade e, consequentemente, alteramos a posição local dos cubos.



Paredes e torres no chão.

unitypackage

Pontes


Nesta fase, temos rios e estradas, mas as estradas não podem atravessar rios de forma alguma. É a hora certa de adicionar pontes.

Vamos começar com um cubo escalado simples que desempenhará o papel de uma ponte pré-fabricada. A largura dos rios varia, mas existem aproximadamente sete unidades de distância entre os centros rodoviários de ambos os lados. Portanto, damos uma escala aproximada (3, 1, 7). Adicione material urbano vermelho pré-fabricado e livre-se de seu colisor. Como nas torres, coloque o cubo dentro do objeto raiz com a mesma escala. Devido a isso, a geometria da ponte em si não será importante.

Adicione um link à pré HexFeatureManager- fabricada da ponte e atribua uma pré-fabricada.

  public Transform wallTower, bridge; 


Pré-fabricada de ponte atribuída.

Colocação de pontes


Para colocar a ponte, precisamos de um método HexFeatureManager.AddBridge. A ponte deve estar localizada entre o centro do rio e uma das laterais do rio.

  public void AddBridge (Vector3 roadCenter1, Vector3 roadCenter2) { Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.SetParent(container, false); } 

Nós transmitiremos os centros de estradas sem distorção e, portanto, precisaremos distorcê-los antes de colocar a ponte.

  roadCenter1 = HexMetrics.Perturb(roadCenter1); roadCenter2 = HexMetrics.Perturb(roadCenter2); Transform instance = Instantiate(bridge); 

Para alinhar corretamente a ponte, podemos usar a mesma abordagem que ao virar as torres. Nesse caso, os centros das estradas definem o vetor avançado da ponte. Como permanecemos na mesma célula, esse vetor definitivamente será horizontal, portanto não precisamos zerar seu componente Y.

  Transform instance = Instantiate(bridge); instance.localPosition = (roadCenter1 + roadCenter2) * 0.5f; instance.forward = roadCenter2 - roadCenter1; instance.SetParent(container, false); 

Construímos pontes através de rios retos


As únicas configurações de rio que exigem pontes são retas e curvas. As estradas podem atravessar pontos finais, e nas estradas em zigue-zague só podem estar por perto.

Para começar, vamos descobrir rios retos. No interior, o HexGridChunk.TriangulateRoadAdjacentToRiverprimeiro operador else iforganiza estradas nas proximidades desses rios. Portanto, aqui vamos adicionar pontes.

Estamos de um lado do rio. O centro da estrada se move do rio e, em seguida, o centro da célula também muda. Para encontrar o centro da estrada no lado oposto, precisamos mover a direção oposta na mesma quantidade. Isso deve ser feito antes de alterar o próprio centro.

  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; } … } 


Pontes sobre rios retos.

Pontes apareceram! Mas agora temos uma instância de pontes para cada direção através da qual o rio não flui. Precisamos garantir que apenas uma instância da ponte seja gerada na célula. Isso pode ser feito escolhendo uma direção em relação ao rio e com base em gerar uma ponte. Você pode escolher qualquer direção.

  roadCenter += corner * 0.5f; if (cell.IncomingRiver == direction.Next()) { features.AddBridge(roadCenter, center - corner * 0.5f); } center += corner * 0.25f; 

Além disso, precisamos adicionar uma ponte somente quando houver uma estrada nos dois lados do rio. No momento, já temos certeza de que há uma estrada no lado atual. Portanto, você precisa verificar se há uma estrada do outro lado do rio.

  if (cell.IncomingRiver == direction.Next() && ( cell.HasRoadThroughEdge(direction.Next2()) || cell.HasRoadThroughEdge(direction.Opposite()) )) { features.AddBridge(roadCenter, center - corner * 0.5f); } 


Pontes entre as estradas de ambos os lados.

Pontes sobre rios curvos


Pontes sobre rios curvos funcionam de maneira semelhante, mas sua topologia é um pouco diferente. Adicionaremos uma ponte quando estivermos do lado de fora da curva. Isso acontece no último bloco else. Ele usa a direção do meio para deslocar o centro da estrada. Nós precisaremos usar esse deslocamento duas vezes com escalas diferentes, portanto salve-o em uma variável.

  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; } … } 

A escala de deslocamento do lado de fora da curva é de 0,25 e do lado de dentro HexMetrics.innerToOuter * 0.7f. Nós a usamos para colocar a ponte.

  Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); 


Pontes sobre rios curvos.

Aqui, novamente, precisamos evitar pontes duplicadas. Podemos fazer isso adicionando pontes apenas na direção do meio.

  Vector3 offset = HexMetrics.GetSolidEdgeMiddle(middle); roadCenter += offset * 0.25f; if (direction == middle) { features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); } 

E, novamente, você precisa ter certeza de que a estrada está do lado oposto.

  if ( direction == middle && cell.HasRoadThroughEdge(direction.Opposite()) ) { features.AddBridge( roadCenter, center - offset * (HexMetrics.innerToOuter * 0.7f) ); } 


Pontes entre as estradas de ambos os lados.

Escala de ponte


Como distorcemos o terreno, a distância entre os centros das estradas e os lados opostos do rio varia. Às vezes as pontes são muito curtas, às vezes muito longas.


Distâncias variáveis, mas comprimentos constantes da ponte.

Embora tenhamos criado uma ponte com sete unidades, você pode escalá-la para corresponder à distância real entre os centros das estradas. Isso significa que o modelo da ponte está deformado. Como as distâncias não variam muito, a deformação pode ser mais aceitável do que pontes que não são adequadas para o comprimento.

Para executar o dimensionamento adequado, precisamos saber o comprimento inicial da pré-fabricada da ponte. Armazenaremos esse comprimento em HexMetrics.

  public const float bridgeDesignLength = 7f; 

Agora podemos atribuir a escala ao longo da instância Z da ponte à distância entre os centros das estradas, dividida pelo comprimento original. Como a raiz da pré-fabricada da ponte tem a mesma escala, a ponte será esticada corretamente.

  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); } 


O comprimento variável das pontes.

Construção de ponte


Em vez de um cubo simples, podemos usar um modelo de ponte mais interessante. Por exemplo, você pode criar uma ponte em arco aproximado de três cubos dimensionados e girados. Obviamente, você pode criar modelos 3D muito mais complexos, incluindo partes da estrada. Mas observe que o objeto inteiro será ligeiramente compactado e esticado.



Pontes arqueadas de diferentes comprimentos.

unitypackage

Objetos especiais


Até agora, nossas células podem conter objetos urbanos, rurais e vegetais. Embora cada um deles tenha três níveis, todos os objetos são bem pequenos se comparados ao tamanho da célula. E se precisarmos de um prédio grande, como um castelo?

Vamos adicionar um tipo especial de objeto ao terreno. Tais objetos são tão grandes que ocupam a célula inteira. Cada um desses objetos é único e precisa de sua própria pré-fabricação. Por exemplo, um castelo simples pode ser criado a partir de um cubo central mais quatro torres de canto. A escala (6, 4, 6) para o cubo central criará uma trava suficientemente grande, que, no entanto, cabe mesmo em uma célula fortemente deformada.


Casa pré-fabricada do castelo.

Outro objeto especial pode ser um zigurate, por exemplo, construído com três cubos colocados um em cima do outro. Para o cubo inferior, a balança (8, 2,5, 8) é adequada.


Zigurate pré-fabricado.

Objetos especiais podem ser quaisquer, não necessariamente arquitetônicos. Por exemplo, um grupo de árvores maciças com até dez unidades de altura pode indicar uma célula cheia de megaflora.


Megaflora pré-fabricada.

Adicione à HexFeatureManagermatriz para rastrear esses prefabs.

  public Transform[] special; 

Primeiro, adicione um castelo à matriz, depois o zigurate e depois a megaflora.


Personalização de objetos especiais.

Tornando as células especiais


Agora HexCell, é necessário um índice de objetos especiais, que determina o tipo de um objeto especial, se ele estiver lá.

  int specialIndex; 

Como outros objetos de alívio, permita-lhe receber e definir esse valor.

  public int SpecialIndex { get { return specialIndex; } set { if (specialIndex != value) { specialIndex = value; RefreshSelfOnly(); } } } 

Por padrão, a célula não contém um objeto especial. Denotamos isso pelo índice 0. Adicione uma propriedade que use essa abordagem para determinar se uma célula é especial.

  public bool IsSpecial { get { return specialIndex > 0; } } 

Para editar células, adicione suporte ao índice de objetos especiais no HexMapEditor. Funciona de maneira semelhante aos níveis de instalações urbanas, rurais e vegetais.

  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; } … } } 

Adicione um controle deslizante à interface do usuário para controlar o objeto especial. Como temos três objetos, usamos o intervalo de 0 a 3 no controle deslizante. Zero significa a ausência de um objeto, um - um castelo, dois - zigurate, três megaflora.


Controle deslizante para objetos especiais.

Adicionando objetos especiais


Agora podemos atribuir objetos especiais às células. Para que eles apareçam, precisamos adicionar a HexFeatureManageroutro método. Ele simplesmente cria uma instância do objeto especial desejado e o coloca na posição desejada. Como zero indica a ausência de um objeto, devemos subtrair a unidade do índice de objetos especiais da célula antes de obter acesso à matriz de prefabs.

  public void AddSpecialFeature (HexCell cell, Vector3 position) { Transform instance = Instantiate(special[cell.SpecialIndex - 1]); instance.localPosition = HexMetrics.Perturb(position); instance.SetParent(container, false); } 

Vamos dar ao objeto uma rotação arbitrária usando a tabela de 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); } 

Ao triangular uma célula, HexGridChunk.Triangulateverificaremos se a célula contém um objeto especial. Nesse caso, chamamos nosso novo método, exatamente como 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 especiais. Eles são muito maiores que o normal.

Evite os rios


Como objetos especiais estão localizados no centro das células, eles não se combinam com os rios, porque ficam suspensos acima deles.


Objetos nos rios.

Para impedir que objetos especiais sejam criados no topo dos rios, alteramos a propriedade HexCell.SpecialIndex. Mudaremos o índice somente quando não houver rios na célula.

  public int SpecialIndex { … set { if (specialIndex != value && !HasRiver) { specialIndex = value; RefreshSelfOnly(); } } } 

Além disso, ao adicionar um rio, precisaremos nos livrar de todos os objetos especiais. O rio deve lavá-los. Isso pode ser feito HexCell.SetOutgoingRiverconfigurando o índice de objetos especiais como 0 no 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 estradas


Como os rios, as estradas também não funcionam bem com objetos especiais, mas nem tudo é tão terrível. Você pode até deixar as estradas como estão. Algumas instalações podem ser compatíveis com as estradas, enquanto outras não. Portanto, você pode torná-los dependentes do objeto. Mas vamos facilitar.


Objetos na estrada.

Nesse caso, deixe os objetos especiais derrotarem a estrada. Portanto, ao alterar o índice de objetos especiais, também removeremos todas as estradas da célula.

  public int SpecialIndex { … set { if (specialIndex != value && !HasRiver) { specialIndex = value; RemoveRoads(); RefreshSelfOnly(); } } } 

E se excluirmos um objeto específico?
0, , . .

Além disso, isso significa que, ao adicionar estradas, teremos que realizar verificações adicionais. Adicionaremos estradas apenas quando nenhuma das células for uma célula com um objeto especial.

  public void AddRoad (HexDirection direction) { if ( !roads[(int)direction] && !HasRiverThroughEdge(direction) && !IsSpecial && !GetNeighbor(direction).IsSpecial && GetElevationDifference(direction) <= 1 ) { SetRoad((int)direction, true); } } 

Evite outros objetos


Objetos especiais não podem ser misturados com outros tipos de objetos. Se eles se sobrepuserem, ficará desarrumado. Também pode depender de um objeto específico, mas usaremos a mesma abordagem.


Um objeto que se cruza com outros objetos.

Nesse caso, suprimiremos objetos menores, como se estivessem debaixo d'água. Desta vez, faremos o check-in HexFeatureManager.AddFeature.

  public void AddFeature (HexCell cell, Vector3 position) { if (cell.IsSpecial) { return; } … } 

Evite a água


Também temos um problema com a água. Os recursos especiais persistirão durante as inundações? Como destruímos objetos pequenos em células inundadas, vamos fazer o mesmo com objetos especiais.


Objetos na água.

Em HexGridChunk.Triangulate, executaremos a mesma verificação de inundação para objetos especiais e comuns.

  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); } } 

Como ifagora os dois operadores verificam se a célula está submersa, podemos transferir o teste e executá-lo apenas uma 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 experimentos, esse número de objetos será suficiente para nós.

unitypackage

Source: https://habr.com/ru/post/pt425463/


All Articles