Hidrologia processual: simulação dinâmica de rios e lagos

Nota: o código fonte completo do projeto está publicado no Github [ aqui ]. O repositório também contém informações detalhadas sobre como ler e usar o código.

Após implementar a simulação de erosão hidráulica baseada em partículas, decidi que seria possível expandir esse conceito para simular outros aspectos da hidrologia da superfície.

Eu pesquisei os métodos existentes de geração processual de rios e lagos, mas os resultados encontrados não me agradaram.

O principal objetivo de muitos métodos é criar sistemas fluviais (muito bonitos) usando vários algoritmos (às vezes baseados em um mapa de elevação ou problema inverso criado anteriormente), mas eles não possuem uma forte relação realista entre a topografia e a hidrologia.

Além disso, o processamento do modelo da água no relevo como um todo é considerado em alguns recursos e é utilizada uma simulação de fluidos altamente complexos.

Neste artigo, demonstrarei minha tentativa de superar esses problemas com uma técnica que amplia a capacidade de simular a erosão hidráulica baseada em partículas. Explicarei também como, em geral, resolvo o problema da “água no alívio”.

No meu método, luto pela simplicidade e pelo realismo à custa de um ligeiro aumento na complexidade do sistema de erosão subjacente. Eu recomendo a leitura do meu artigo anterior sobre este sistema [ aqui , tradução em Habré], porque o novo modelo é baseado nele.


Este sistema é capaz de gerar rapidamente um terreno de aparência muito realista com hidrologia. Este vídeo foi renderizado em tempo real. O sistema é capaz de gerar um número infinito de tais paisagens.

Explicação: Como não sou geólogo, criei o sistema com base no meu conhecimento.

Conceito de hidrologia


Quero criar um sistema generativo que possa simular muitos fenômenos geográficos, incluindo:

  • Migração de rios e riachos
  • Cachoeiras naturais
  • Formação Canyon
  • Inchaço do solo e várzea

Consequentemente, os sistemas de hidrologia e topografia devem ser dinâmicos e intimamente relacionados. O sistema de erosão hidráulica baseado em partículas já possui os aspectos básicos necessários para isso:

  • O alívio afeta o movimento da água
  • Erosão e sedimentação afetam o terreno

De fato, este sistema simula a erosão causada pela chuva, mas não é capaz de transmitir muitas outras influências:

  • Em um fluxo em movimento , a água se comporta de maneira diferente.
  • Em uma piscina em, a água se comporta de maneira diferente

Nota: Mencionarei frequentemente córregos e piscinas . Assume-se no modelo que estes são fenômenos bidimensionais em larga escala. Eles reduzem bastante a complexidade do modelo.

A maioria dos fenômenos geográficos acima pode ser transmitida usando o modelo de fluxos e bacias. Idealmente, eles deveriam emprestar e aprimorar o realismo de um sistema baseado em partículas.

Modelo hidrológico simplificado


Armazenar informações sobre fluxos e conjuntos em uma ou mais estruturas de dados (gráficos, objetos etc.) é muito complexo e limita nossos recursos.

Portanto, nosso modelo hidrológico consiste em dois mapas: mapas de fluxo e mapas de bacias .

Nota: não esqueça que eles são modelados como sistemas 2D.

Mapas de Stream e Pool


O mapa de fluxo descreve a água corrente na superfície (córregos e rios). Ele armazena as posições médias das partículas no mapa. Informações antigas estão sendo excluídas lentamente.

O mapa da bacia descreve a água parada na superfície (poças, lagoas, lagos, oceanos). Ele armazena a profundidade da água na posição correspondente do mapa.

Nota: os mapas de fluxo e pool são matrizes do mesmo tamanho que o mapa de altura.


Alívio com impacto hidrológico. A camada de água na renderização é retirada de mapas hidrológicos.


Mapas hidrológicos combinados. Azul claro é um mapa de fluxo; azul escuro é um mapa de piscina.

Esses mapas são gerados e conectados por partículas que movem a água ao longo do terreno. A adição desses mapas hidrológicos também nos fornece informações independentes do tempo que permitem a interação de partículas com sua simulação separadamente. As partículas podem interagir através do uso desses cartões para obter parâmetros que afetam seu movimento.

Nota: o mapa do fluxo é exibido após passar pela função de facilidade de entrada e é renderizado no terreno com base nisso. Para obter fluxos mais finos / mais nítidos (ou ignorar valores mais baixos em grandes áreas planas), essa exibição pode ser modificada ou definir valores-limite para ele.

Água como uma partícula


A água é apresentada na forma de uma massa discreta (“partículas”) com volume e movendo-se ao longo da superfície do relevo. Possui vários parâmetros que afetam seu movimento (fricção, taxa de evaporação, taxa de deposição, etc.).

Essa é a estrutura básica de dados usada para simular a hidrologia. As partículas não são armazenadas , mas são usadas apenas para interação entre mapas de alturas, fluxos e piscinas.

Nota: o conceito de partícula é explicado em mais detalhes em uma postagem anterior [ tradução em Habré] (e um número infinito de outros recursos).

Ciclo hidrológico e interação do mapa


Os mapas interagem entre si através de um ciclo hidrológico. O ciclo hidrológico consiste nas seguintes etapas:

  • Criando uma partícula no terreno
  • (.. ).
  • , .
  • , .
  • , ( ) .
  • .

Em todo o sistema, existem apenas dois algoritmos: Descida (descida) e Inundação (inundação) . As partículas descendentes alteram o mapa dos fluxos e as partículas de inundação alteram o mapa das bacias. Esses algoritmos são descritos em detalhes abaixo.


Diagrama unidimensional do modelo hidrológico. As partículas são criadas no terreno e processadas ciclicamente por dois algoritmos: Descida e Inundação. No processo, os mapas das bacias e fluxos mudam, afetando, por sua vez, o movimento das partículas.

Implementação


Abaixo, explicarei a implementação completa do sistema usado para gerar os resultados e apresentarei exemplos de código.

Nota: mostrarei apenas trechos de código relevantes. Mais informações podem ser encontradas no repositório no Github. Todas as partes relevantes do código estão no arquivo "water.h".

Classe de partículas


A estrutura da partícula Drop é idêntica à estrutura do sistema anterior. Agora, descida e inundação são membros da estrutura porque agem apenas em uma partícula de cada vez.

struct Drop{
  
  //... constructors

  int index;                         //Flat Array Index
  glm::vec2 pos;                     //2D Position
  glm::vec2 speed = glm::vec2(0.0);
  double volume = 1.0;
  double sediment = 0.0;

  //... parameters
  const double volumeFactor = 100.0; //"Water Deposition Rate"

  //Hydrological Cycle Functions
  void descend(double* h, double* stream, double* pool, bool* track, glm::ivec2 dim, double scale);
  void flood(double* h, double* pool, glm::ivec2 dim);
};

Um parâmetro adicional é o fator de volume, que determina como a inundação transfere o volume para o nível da água.

Algoritmo de descida


O algoritmo de descida é quase o mesmo que o algoritmo simples de erosão de partículas. Ele recebe “track” adicional de entrada - uma matriz na qual ele escreve todas as posições visitadas por ele. Uma matriz é necessária para criar mapas de fluxo no futuro.

void Drop::descend(double* h, double* stream, double* pool, bool* track, glm::ivec2 dim, double scale){

  glm::ivec2 ipos; 

  while(volume > minVol){

    ipos = pos; //Initial Position
    int ind = ipos.x*dim.y+ipos.y; //Flat Array Index

    //Register Position
    track[ind] = true;

    //...
  }
};

O conjunto de parâmetros é modificado pelos mapas de fluxo e conjunto:

//...
  //Effective Parameter Set
  double effD = depositionRate;
  double effF = friction*(1.0-0.5*stream[ind]);
  double effR = evapRate*(1.0-0.2*stream[ind]);
//...

Nota: Descobri que essa alteração de parâmetro funciona bem.

No sistema anterior, as partículas poderiam sair deste ciclo e serem destruídas apenas com completa evaporação ou indo além dos limites do relevo. Agora, duas condições de saída adicionais foram adicionadas aqui:

//... nind is the next position after moving the particle
  
  //Out-Of-Bounds
  if(!glm::all(glm::greaterThanEqual(pos, glm::vec2(0))) ||
     !glm::all(glm::lessThan((glm::ivec2)pos, dim))){
       volume = 0.0;
       break;
  }

  //Slow-Down
  if(stream[nind] > 0.5 && length(acc) < 0.01)
    break;

  //Enter Pool
  if(pool[nind] > 0.0)
    break;

//...

Se a partícula não possui aceleração suficiente e é cercada por outras partículas, ou entra diretamente na piscina, completa prematuramente a descida com todo o volume restante e prossegue para o algoritmo de inundação.

Nota: a condição de transbordamento também redefine o volume para que as partículas não passem para o algoritmo de inundação.

Algoritmo de inundação


Uma partícula com o volume restante pode inundar de sua posição atual. Isso acontece se ele parar de descer (não há aceleração) ou entrar em um pool existente.

O algoritmo de inundação transfere o volume da partícula para o aumento do nível da água, alterando o mapa da bacia. A técnica é aumentar gradualmente o nível da água em uma fração do volume da partícula usando o "plano de teste". À medida que o nível da água aumenta, o volume de partículas diminui.


Animação de algoritmo de inundação. O plano de teste e o nível da água aumentam gradualmente, reduzindo o volume de partículas. Se um vazamento for encontrado, o volume restante será movido para o ponto de vazamento para realizar a descida.

Em cada etapa, realizamos um preenchimento de inundação a partir da posição da partícula (ou seja, verificamos recursivamente as posições dos vizinhos), adicionando todas as posições localizadas acima do plano original (nível atual da água) e abaixo do plano de teste ao "conjunto de inundação". Esta é a área de alívio que faz parte da piscina.

Durante o preenchimento, verificamos vazamentos. Estes são pontos do conjunto de inundações que estão abaixo do plano de teste E do plano original. Se encontrarmos vários pontos de vazamento, selecionamos o mais baixo.

void Drop::flood(double* height, double* pool, glm::ivec2 dim){

  index = (int)pos.x*dim.y + (int)pos.y;
  double plane = height[index] + pool[index];  //Testing Plane
  double initialplane = plane;                 //Water Level

  //Flood Set
  std::vector<int> set;
  int fail = 10; //Just in case...

  //Iterate while particle still has volume
  while(volume > minVol && fail){

    set.clear();
    bool tried[dim.x*dim.y] = {false};

    //Lowest Drain
    int drain;
    bool drainfound = false;

    //Recursive Flood-Fill Function
    std::function<void(int)> fill = [&](int i){

      //Out of Bounds
      if(i/dim.y >= dim.x || i/dim.y < 0) return;
      if(i%dim.y >= dim.y || i%dim.y < 0) return;

      //Position has been tried
      if(tried[i]) return;
      tried[i] = true;

      //Wall / Boundary of the Pool
      if(plane < height[i] + pool[i]) return;

      //Drainage Point
      if(initialplane > height[i] + pool[i]){

        //No Drain yet
        if(!drainfound)
          drain = i;

        //Lower Drain
        else if( pool[drain] + height[drain] < pool[i] + height[i] )
          drain = i;

        drainfound = true;
        return; //No need to flood from here
      }

      //Part of the Pool
      set.push_back(i);
      fill(i+dim.y);    //Fill Neighbors
      fill(i-dim.y);
      fill(i+1);
      fill(i-1);
      fill(i+dim.y+1);  //Diagonals (Improves Drainage)
      fill(i-dim.y-1);
      fill(i+dim.y-1);
      fill(i-dim.y+1);
    };

    //Perform Flood
    fill(index);

    //...

Nota: para simplificar, um algoritmo de preenchimento de oito vias é usado aqui . No futuro, será possível implementá-lo com mais eficiência.

Depois de identificar os muitos pontos de inundação e vazamento, alteramos o nível da água e o mapa da piscina.

Se um ponto de vazamento for encontrado, movemos a partícula (e seu volume de "transbordamento") para o ponto de vazamento, para que possa começar a descer novamente. Em seguida, o nível da água desce até a altura do ponto de vazamento.

    //...

    //Drainage Point
    if(drainfound){

      //Set the Particle Position
      pos = glm::vec2(drain/dim.y, drain%dim.y);

      //Set the New Waterlevel (Slowly)
      double drainage = 0.001;
      plane = (1.0-drainage)*initialplane + drainage*(height[drain] + pool[drain]);

      //Compute the New Height
      for(auto& s: set) //Iterate over Set
        pool[s] = (plane > height[s])?(plane-height[s]):0.0;

      //Remove some sediment
      sediment *= 0.1;
      break;
    }

    //...

Nota: quando o nível da água diminui devido a vazamentos, descobri que isso funciona melhor com uma baixa taxa de vazamentos. Além disso, a eliminação de parte das rochas sedimentares ajuda na implementação.

Devido a isso, novas partículas que entram no pool preenchido movem instantaneamente seu volume para o ponto de vazamento, porque adicionar volume ao pool desloca o mesmo volume dele.

Se o ponto de vazamento não for encontrado, calculamos o volume sob o plano de teste e o comparamos com o volume da partícula. Se for menor, removemos o volume da partícula e ajustamos o nível da água. Então o avião de teste se eleva. Se for maior, o plano de teste é abaixado. O processo é repetido até que a partícula fique sem espaço ou seja encontrado um ponto de vazamento.

    //...

    //Get Volume under Plane
    double tVol = 0.0;
    for(auto& s: set)
      tVol += volumeFactor*(plane - (height[s]+pool[s]));

    //We can partially fill this volume
    if(tVol <= volume && initialplane < plane){

      //Raise water level to plane height
      for(auto& s: set)
        pool[s] = plane - height[s];

      //Adjust Drop Volume
      volume -= tVol;
      tVol = 0.0;
    }

    //Plane was too high and we couldn't fill it
    else fail--;

    //Adjust Planes
    float approach = 0.5;
    initialplane = (plane > initialplane)?plane:initialplane;
    plane += approach*(volume-tVol)/(double)set.size()/volumeFactor;
  }

  //Couldn't place the volume (for some reason)- so ignore this drop.
  if(fail == 0)
    volume = 0.0;

} //End of Flood Algorithm

A altura do plano é ajustada proporcionalmente à diferença de volumes escalados pela área da superfície da piscina (ou seja, pelo tamanho do conjunto). Usando o coeficiente de proximidade, você pode aumentar a estabilidade de como o avião atinge o nível correto de água.

Envoltório de erosão


A classe mundial contém todos os três mapas na forma de matrizes comuns:

class World {

public:
  void generate();            //Initialize
  void erode(int cycles);     //Erode with N Particles

  //...

  double heightmap[256*256] = {0.0};
  double waterstream[256*256] = {0.0};
  double waterpool[256*256] = {0.0};

};

Nota: O mapa de altura é inicializado usando o ruído Perlin.

Cada etapa hidrológica para uma partícula individual consiste no seguinte:

//...

//Spawn Particle
glm::vec2 newpos = glm::vec2(rand()%(int)dim.x, rand()%(int)dim.y);
Drop drop(newpos);

int spill = 5;
while(drop.volume > drop.minVol && spill != 0){

  drop.descend(heightmap, waterstream, waterpool, track, dim, scale);

  if(drop.volume > drop.minVol)
    drop.flood(heightmap, waterpool, dim);

  spill--;
}

//...

O parâmetro derramamento determina quantas vezes uma partícula pode entrar na piscina e deixá-la novamente antes de ser simplesmente destruída. Caso contrário, as partículas morrem quando seu volume é esgotado.

Nota: as partículas raramente entram nas piscinas e as deixam mais de uma ou duas vezes antes de evaporar completamente durante as etapas de descida, mas adicionei isso apenas por precaução.

A função erosão envolve esse código e executa etapas hidrológicas para partículas de N, alterando diretamente o mapa de fluxo:

void World::erode(int N){

  //Track the Movement of all Particles
  bool track[dim.x*dim.y] = {false};

  //Simulate N Particles
  for(int i = 0; i < N; i++){
   
    //... simulate individual particle

  }

  //Update Path
  double lrate = 0.01;  //Adaptation Rate
  for(int i = 0; i < dim.x*dim.y; i++)
    waterstream[i] = (1.0-lrate)*waterstream[i] + lrate*((track[i])?1.0:0.0);

}

Aqui, o array de faixas é passado para a função de descida. Descobri que rastrear simultaneamente o movimento de várias partículas e as alterações correspondentes fornecem melhores resultados para o mapa de fluxo. A taxa de adaptação determina a rapidez com que as informações antigas são excluídas.

Árvores


Apenas por diversão, adicionei árvores para ver se a simulação de erosão poderia ser melhorada ainda mais. Eles são armazenados na sala de aula do mundo como um vetor.

As árvores são criadas aleatoriamente no mapa em locais onde não há piscinas e riachos fortes, e o terreno não é muito íngreme. Eles também têm a chance de criar árvores adicionais ao seu redor.

No processo de criação de árvores, elas escrevem no mapa de densidade da vegetação em um determinado raio ao seu redor. A alta densidade da vegetação reduz a transferência de massa entre partículas descendentes e topografia. Isto é para simular como as raízes mantêm o solo no lugar.

//... descend function
double effD = depositionRate*max(0.0, 1.0-treedensity[ind]);
//...

As árvores morrem se estiverem na piscina ou o fluxo abaixo delas é muito poderoso. Além disso, eles têm uma probabilidade aleatória de morte.


Graças ao sombreamento e aos mapas normais, até sprites de árvores muito simples tornam o relevo mais bonito.

Nota: o modelo da árvore pode ser encontrado no arquivo "vegetação.h" e na função "Mundo :: crescer ()".

Outros detalhes


Os resultados são visualizados usando um wrapper OpenGL caseiro, que é apresentado aqui .

resultados


As partículas podem ser criadas no mapa de acordo com qualquer distribuição que você precisar. Nas minhas demos, criei-as com uma distribuição uniforme em cartões 256 × 256.

Como pode ser visto abaixo, a simulação resultante é altamente dependente da escolha do relevo original. O sistema é capaz de simular um grande número de fenômenos naturais. Não consegui apenas adequadamente desfiladeiros. Eles podem precisar de uma simulação muito longa e lenta.

Outros fenômenos podem ser observados no sistema, como cachoeiras, tortuosidades e deltas de rios, lagos, inchaço do solo e assim por diante.

O sistema também é muito bom em distribuir fluxos e piscinas em locais onde muita chuva se acumula, e não em locais aleatórios. Portanto, a hidrologia gerada está intimamente relacionada ao terreno.


Simulação hidrológica em tempo real em uma grade de 256 × 256. O relevo original é relativamente suave, o que permite que os fluxos apareçam rapidamente. No início, pode-se observar a criação mais simples de piscinas e vazamentos, após o que grandes fluxos aparecem e permanecem.

Comparação dos efeitos do estreitamento dos fluxos


Para comparar a diferença criada ao vincular um mapa a um sistema de erosão, você pode simular a hidrologia no mesmo mapa, ativando e desativando efeitos diferentes.

Simulei o mesmo terreno três vezes:

  • Erosão baseada em partículas (erosão básica) que recebe mapas de fluxo e piscina. Piscinas ainda afetam geração
  • Erosão básica com parâmetros alterados por mapas hidrológicos (em combinação com erosão)
  • Erosão combinada com parâmetros alterados por mapas hidrológicos e afetando a erosão por árvores

Nota: este é um sistema bastante caótico, e o tipo de hidrologia que aparece é altamente dependente do terreno. É difícil encontrar um exemplo "muito revelador" de alívio.


Renderização em relevo do sistema base


Renderização combinada da elevação do sistema


Renderização da topografia de um sistema com árvores

Uma observação interessante: combinar mapas hidrológicos com parâmetros de partículas na verdade estreita os leitos dos rios. Particularmente em regiões planares, as partículas são menos distribuídas e se fundem em uma pequena quantidade de fluxos mais fortes.

Atrito e evaporação reduzidos lidam com sucesso com o fato de que as partículas começam a preferir os canais já existentes.

Outros efeitos são mais visíveis com a observação direta do relevo.


Mapa hidrológico do sistema base


Mapa hidrológico do sistema combinado


Mapa hidrológico do sistema com árvores

Nota: esses resultados foram gerados em exatamente 60 segundos do tempo de simulação.

As árvores também afetam o estreitamento. Eles aprimoram o processo de cortar caminhos claros em áreas íngremes. Eles forçam os fluxos a permanecerem nos canais já estabelecidos e, portanto, reduzem a tortuosidade. Assim, a localização das árvores afeta o movimento da água.


Um exemplo de registro de como a localização das árvores pode ajudar a manter a localização dos fluxos. É o mesmo alívio de antes, com todos os efeitos ativados.

Impacto na piscina


O sistema de criação de piscinas funciona bem e permite a existência de várias massas de água com diferentes alturas no mesmo relevo. Eles também podem se espalhar e esvaziar.


Um exemplo de um vídeo da formação de piscinas em um relevo de linha de base mais áspero, com uma grande diferença de elevação. O lago superior está fisicamente localizado acima do lago inferior e descarrega a água resultante diretamente no lago inferior.

Nota: Eu recebi várias sementes, nas quais três lagos corriam um para o outro sequencialmente, mas não queria gastar muito tempo encontrando o caminho certo para este artigo. Eu já gerei muitas fotos e vídeos.

De tempos em tempos, o nível de altura da piscina aumenta. Eu acho que isso acontece quando o nível da água está próximo ao nível de vazamento e muita água é adicionada. Este efeito pode ser reduzido reduzindo a taxa de vazamento na função de inundação.


Após outro minuto de geração, várias novas piscinas apareceram por conta própria.


Em um ângulo mais acentuado, a diferença nas alturas da bacia é mais perceptível.


O mapa hidrológico mostra claramente que a bacia central se funde com a bacia inferior.

A execução do algoritmo de inundação leva ao fato de que os pools veem uma "parede" na borda do mapa. Isso é perceptível nas imagens mostradas acima.

Outra possível melhoria poderia ser a adição do nível do mar ao mundo, para que as piscinas observassem vazamentos nas bordas do mapa ao nível do mar, caso contrário, elas simplesmente transbordariam.

Velocidade de simulação


O tempo de cada etapa de descida e inundação varia de partícula para partícula, mas permanece uma ordem de magnitude (cerca de 1 microssegundo). Com fluxos constantes, as partículas se movem pelo mapa mais rapidamente.

O tempo de inundação varia em proporção ao tamanho da piscina, porque a operação de vazamento é a etapa mais cara. Quanto maiores as piscinas, maior a área para a qual você precisa aumentar o nível da água. Grandes piscinas são definitivamente o gargalo do sistema. Se você tiver idéias para aumentar a velocidade, me avise.

O tempo de descida varia em proporção ao tamanho do mapa e muitos outros parâmetros, incluindo taxa de atrito e evaporação.

Todos os vídeos deste artigo são gravados em tempo real, ou seja, em geral, a simulação é rápida.

Nota: logo após a publicação deste artigo, fiz uma pequena alteração no método de cálculo das normais da superfície, o que aumentou a velocidade da simulação. O efeito disso é muito perceptível durante a simulação, mas é difícil compará-lo devido a grandes variações no tempo necessário para iniciar e inundar. De acordo com minhas estimativas, a velocidade da simulação dobrou.

Vídeos mais bonitos



Simulação de hidrologia em terrenos verticais irregulares.


Um pouco de alívio mais suave. Alguns lagos flutuam um pouco.

Simulação de hidrologia em um terreno plano e liso.

Vídeos subsequentes foram gravados após a melhoria da velocidade.


Terreno ainda mais plano e suave.


Vídeo com um rio formado a partir de vários riachos conectados. O rio principal tem dois pontos nos quais incorpora fluxos menores, e vemos como cresce em tamanho.


Um alívio mais desigual com a formação de rios.

Eu posso continuar para sempre.

Conclusão e trabalho para o futuro


Essa técnica simples ainda é baseada em partículas, mas consegue transmitir muitos efeitos adicionais. Ele fornece uma maneira fácil de simular o movimento da água em larga escala sem simular totalmente a dinâmica dos fluidos. Além disso, ela trabalha com um modelo de água intuitivo.

Nas simulações, podemos observar o surgimento de rios sinuosos, cachoeiras, lagos e transfusões de lagos, rios que se separam em áreas planas em deltas, etc.

Seria interessante adicionar diferentes tipos de solo na forma de um mapa do solo a partir do qual os parâmetros de erosão poderiam ser obtidos.

Você também pode alterar facilmente o sistema de árvores para criar diferentes tipos de árvores que mantêm o solo no lugar.

É difícil transmitir a variedade de resultados gerados em várias fotos e vídeos; portanto, você deve tentar fazer isso sozinho, é muito interessante. Eu recomendo experimentar os parâmetros originais do mapa, incluindo ruído de oitava e escala do mapa.

Se você tiver perguntas ou comentários, pode entrar em contato comigo. Eu trabalhei bastante nesse projeto, então meu cérebro estava aglomerado e tenho certeza de que perdi alguns aspectos interessantes.

All Articles