Criando mapas a partir de recursos de ruído

Um dos artigos mais populares do meu site é dedicado à geração de mapas poligonais ( tradução em Habré). Criar tais cartões requer muito esforço. Mas não comecei com isso, mas com uma tarefa muito mais simples, que descreverei aqui. Essa técnica simples permite criar esses cartões em menos de 50 linhas de código:


Não vou explicar como comprar tais cartões: depende do idioma, biblioteca gráfica, plataforma etc. Vou apenas explicar como preencher a matriz com dados do mapa.

O barulho


A maneira padrão de gerar mapas 2D é usar o ruído com uma banda de frequência limitada como um componente básico, como o ruído Perlin ou o ruído simplex. Aqui está a aparência da função de ruído:

imagem

Atribuímos um número de 0,0 a 1,0 a cada ponto no mapa. Nesta imagem, 0.0 é preto e 1.0 é branco. Veja como definir a cor de cada ponto da grade na sintaxe de um idioma semelhante ao C:

for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { double nx = x/width - 0.5, ny = y/height - 0.5; value[y][x] = noise(nx, ny); } } 

O loop funcionará da mesma maneira em Javascript, Python, Haxe, C ++, C #, Java e na maioria das outras linguagens populares, portanto, mostrarei isso em uma sintaxe do tipo C para que você possa convertê-lo para o idioma necessário. No restante do tutorial, mostrarei como o corpo do ciclo muda ( value[y][x]=… linha value[y][x]=… ) ao adicionar novas funções. A demonstração mostrará um exemplo completo.

Em algumas bibliotecas, será necessário alterar ou multiplicar os valores resultantes para retorná-los no intervalo de 0,0 a 1,0.

Altura


O ruído em si é apenas uma coleção de números. Precisamos dar sentido a isso. A primeira coisa que você pode pensar é vincular o valor do ruído à altura (isso é chamado de "mapa de altura"). Vamos pegar o ruído mostrado acima e desenhá-lo como altura:



O código permaneceu quase o mesmo, exceto pelo loop interno. Agora fica assim:

 elevation[y][x] = noise(nx, ny); 

Sim, e é isso. Os dados do mapa permaneceram os mesmos, mas agora vou chamá-los de elevation (altura), não value .

Temos muitas colinas, mas nada mais. O que está errado?

Frequência


O ruído pode ser gerado em qualquer frequência . Até agora, escolhi apenas uma frequência. Vamos ver como isso afeta.

Tente alterar o valor com o controle deslizante (no artigo original) e veja o que acontece em diferentes frequências:


Apenas muda a escala. A princípio, isso não parece muito útil, mas não é. Eu tenho mais um tutorial ( tradução em Habré), que explica a teoria : conceitos como frequência, amplitude, oitavas, ruído rosa e azul, e assim por diante.

 elevation[y][x] = noise(freq * nx, freq * ny); 

Às vezes, também é útil recordar o comprimento de onda , que é o recíproco da magnitude. Quando a frequência é duplicada, o tamanho é reduzido apenas pela metade. Dobrar o comprimento de onda todos os dobra. Comprimento de onda é a distância medida em pixels / ladrilhos / metros ou em qualquer outra unidade que você selecionou para mapas. Está relacionado à frequência: wavelength = map_size / frequency .

Oitavas


Para tornar o mapa de altura mais interessante, adicionaremos ruído com diferentes frequências :



 elevation[y][x] = 1 * noise(1 * nx, 1 * ny); + 0.5 * noise(2 * nx, 2 * ny); + 0.25 * noise(4 * nx, 2 * ny); 

Vamos misturar grandes colinas de baixa frequência com pequenas colinas de alta frequência em um mapa. Mova o controle deslizante (no artigo original) para adicionar pequenas colinas à mistura:


Agora é muito mais parecido com o relevo fractal que precisamos! Podemos ter colinas e montanhas irregulares, mas ainda não temos planícies. Para fazer isso, você precisa de outra coisa.

Redistribuição


A função de ruído fornece valores entre 0 e 1 (ou de -1 a +1, dependendo da biblioteca). Para criar planícies planas, podemos elevar a altura a uma potência . Mova o controle deslizante (no artigo original) para obter diferentes graus.


 e = 1 * noise(1 * nx, 1 * ny); + 0.5 * noise(2 * nx, 2 * ny); + 0.25 * noise(4 * nx, 4 * ny); elevation[y][x] = Math.pow(e, exponent); 

Valores altos abaixam as alturas médias das planícies e valores baixos elevam as alturas médias para os picos das montanhas. Precisamos omiti-los. Uso funções de poder porque são mais simples, mas você pode usar qualquer curva; Eu tenho uma demonstração mais complicada.

Agora que temos um mapa de elevação realista, vamos adicionar biomas!

Biomas


O barulho dá números, mas precisamos de um mapa com florestas, desertos e oceanos. A primeira coisa que você pode fazer é transformar pequenas alturas em água:


 function biome(e) { if (e < waterlevel) return WATER; else return LAND; } 

Uau, isso já está se tornando um mundo processualmente gerado! Temos água, grama e neve. Mas e se precisarmos de mais? Vamos fazer uma sequência de água, areia, grama, floresta, savana, deserto e neve:



Alívio baseado na altura

 function biome(e) { if (e < 0.1) return WATER; else if (e < 0.2) return BEACH; else if (e < 0.3) return FOREST; else if (e < 0.5) return JUNGLE; else if (e < 0.7) return SAVANNAH; else if (e < 0.9) return DESERT; else return SNOW; } 

Uau, isso parece ótimo! Para o seu jogo, você pode alterar os valores e os biomas. Crysis terá muito mais selva; Skyrim tem muito mais gelo e neve. Mas não importa como você altere os números, essa abordagem é bastante limitada. Os tipos de relevo correspondem a alturas, portanto formam faixas. Para torná-los mais interessantes, precisamos escolher biomas baseados em outra coisa. Vamos criar um segundo mapa de ruído para umidade.



Acima está o barulho das alturas; ruído de umidade inferior

Agora vamos usar altura e umidade juntos . Na primeira imagem mostrada abaixo, o eixo y é a altura (tirada da imagem acima) e o eixo x é a umidade (a segunda imagem é mais alta). Isso nos dá um mapa atraente:



Alívio baseado em dois valores de ruído

Pequenas alturas são oceanos e costas. Grandes alturas são rochosas e com neve. No meio, obtemos uma ampla gama de biomas. O código fica assim:

 function biome(e, m) { if (e < 0.1) return OCEAN; if (e < 0.12) return BEACH; if (e > 0.8) { if (m < 0.1) return SCORCHED; if (m < 0.2) return BARE; if (m < 0.5) return TUNDRA; return SNOW; } if (e > 0.6) { if (m < 0.33) return TEMPERATE_DESERT; if (m < 0.66) return SHRUBLAND; return TAIGA; } if (e > 0.3) { if (m < 0.16) return TEMPERATE_DESERT; if (m < 0.50) return GRASSLAND; if (m < 0.83) return TEMPERATE_DECIDUOUS_FOREST; return TEMPERATE_RAIN_FOREST; } if (m < 0.16) return SUBTROPICAL_DESERT; if (m < 0.33) return GRASSLAND; if (m < 0.66) return TROPICAL_SEASONAL_FOREST; return TROPICAL_RAIN_FOREST; } 

Se necessário, você pode alterar todos esses valores de acordo com os requisitos do seu jogo.

Se não precisarmos de biomas, os gradientes suaves (consulte este artigo ) podem criar cores:



Para biomas e gradientes, um único valor de ruído não fornece variabilidade suficiente, mas dois são suficientes.

Clima


Na seção anterior, usei a altitude como substituto da temperatura . Quanto maior a altura, menor a temperatura. No entanto, a latitude geográfica também afeta as temperaturas. Vamos usar a altura e a latitude para controlar a temperatura:


Perto dos pólos (grandes latitudes) o clima é mais frio, e no topo das montanhas (grandes alturas) o clima também é mais frio. Até agora, resolvi isso não muito difícil: para a abordagem correta desses parâmetros, você precisa de muitas configurações sutis.

Há também mudanças climáticas sazonais . No verão e no inverno, os hemisférios norte e sul ficam mais quentes e frios, mas no equador a situação não muda muito. Muito também pode ser feito aqui, por exemplo, é possível simular os ventos e as correntes oceânicas predominantes, o efeito dos biomas no clima e o efeito médio dos oceanos nas temperaturas.

As ilhas


Em alguns projetos, eu precisava que as bordas do mapa fossem de água. Isso transforma o mundo em uma ou mais ilhas. Existem várias maneiras de fazer isso, mas usei uma solução bastante simples em meu gerador de mapas de polígonos: Alterei a altura como e = e + a - b*d^c , onde d é a distância do centro (em uma escala de 0-1). Outra opção é alterar e = (e + a) * (1 - b*d^c) . A constante a eleva tudo, b abaixa as bordas e c controla a taxa de declínio.


Não estou completamente satisfeito com isso e ainda há muito a ser explorado. Deveria ser Manhattan ou distância euclidiana? Deveria depender da distância ao centro ou da distância até a borda? A distância deve ser ao quadrado, ou linear, ou ter algum outro grau? Deveria ser adição / subtração, multiplicação / divisão ou qualquer outra coisa? No artigo original, tente Adicionar, a = 0,1, b = 0,3, c = 2,0 ou tente Multiplicar, a = 0,05, b = 1,00, c = 1,5. As opções adequadas a você dependem do seu projeto.

Por que manter as funções matemáticas padrão? Como contei no meu artigo sobre danos no RPG ( tradução em Habré), todos (inclusive eu) usam funções matemáticas, como polinômios, distribuições exponenciais, etc., mas no computador não podemos nos limitar a eles. Podemos pegar qualquer função de formação e usá-la aqui, usando a tabela de pesquisa e = e + height_adjust[d] . Até agora, não estudei esse assunto.

Ruído espetado


Em vez de elevar a altura a uma potência, podemos usar o valor absoluto para criar picos agudos:

 function ridgenoise(nx, ny) { return 2 * (0.5 - abs(0.5 - noise(nx, ny))); } 

Para adicionar oitavas, podemos variar as amplitudes das altas frequências para que apenas as montanhas recebam o ruído adicional:

 e0 = 1 * ridgenoise(1 * nx, 1 * ny); e1 = 0.5 * ridgenoise(2 * nx, 2 * ny) * e0; e2 = 0.25 * ridgenoise(4 * nx, 4 * ny) * (e0+e1); e = e0 + e1 + e2; elevation[y][x] = Math.pow(e, exponent); 


Como não tenho muita experiência com essa técnica, preciso experimentar para aprender como usá-la bem. Também pode ser interessante misturar ruídos pontiagudos de baixa frequência com ruídos não pontiagudos de alta frequência.

Terraços


Se arredondarmos a altura para os próximos n níveis, obteremos terraços:


Este é o resultado da aplicação da função de redistribuição de altura no formato e = f(e) . Acima, usamos e = Math.pow(e, exponent) para afiar os picos das montanhas; aqui usamos e = Math.round(e * n) / n para criar terraços. Se você usar uma função que não seja degrau, os terraços poderão ser arredondados ou ocorrer apenas em determinadas alturas.

Posicionamento em árvore


Geralmente usamos ruído fractal para altura e umidade, mas também pode ser usado para colocar objetos espaçados de maneira desigual, como árvores e pedras. Para altura, usamos amplitudes altas com baixas frequências ("ruído vermelho"). Para colocar objetos, você precisa usar altas amplitudes com altas frequências ("ruído azul"). À esquerda, há um padrão de ruído azul; à direita, são locais onde o ruído é maior que os valores adjacentes:


 for (int yc = 0; yc < height; yc++) { for (int xc = 0; xc < width; xc++) { double max = 0; //     for (int yn = yc - R; yn <= yc + R; yn++) { for (int xn = xc - R; xn <= xc + R; xn++) { double e = value[yn][xn]; if (e > max) { max = e; } } } if (value[yc][xc] == max) { //    xc,yc } } } 

Escolhendo R diferente para cada bioma, podemos obter uma densidade variável de árvores:



É ótimo que esse ruído possa ser usado para colocar árvores, mas outros algoritmos geralmente são mais eficazes e criam uma distribuição mais uniforme: pontos Poisson, ladrilhos Van ou pontilhamento gráfico.

Para o infinito e além


Os cálculos do bioma na posição (x, y) são independentes dos cálculos de todas as outras posições. Essa computação local possui duas propriedades convenientes: pode ser calculada em paralelo e pode ser usada para terrenos sem fim. Coloque o cursor do mouse no minimapa (no artigo original) à esquerda para gerar o mapa à direita. Você pode gerar qualquer parte do cartão sem gerar (e mesmo sem armazenar) o cartão inteiro.



Implementação


Usar o ruído para gerar terreno é uma solução popular e, na Internet, você encontra tutoriais para diversos idiomas e plataformas. O código para gerar cartões em diferentes idiomas é aproximadamente o mesmo. Aqui está o loop mais simples em três idiomas diferentes:

  • Javascript:

     let gen = new SimplexNoise(); function noise(nx, ny) { // Rescale from -1.0:+1.0 to 0.0:1.0 return gen.noise2D(nx, ny) / 2 + 0.5; } let value = []; for (let y = 0; y < height; y++) { value[y] = []; for (let x = 0; x < width; x++) { let nx = x/width - 0.5, ny = y/height - 0.5; value[y][x] = noise(nx, ny); } } 
  • C ++:

     module::Perlin gen; double noise(double nx, double ny) { // Rescale from -1.0:+1.0 to 0.0:1.0 return gen.GetValue(nx, ny, 0) / 2.0 + 0.5; } double value[height][width]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { double nx = x/width - 0.5, ny = y/height - 0.5; value[y][x] = noise(nx, ny); } } 
  • Python:

     from opensimplex import OpenSimplex gen = OpenSimplex() def noise(nx, ny): # Rescale from -1.0:+1.0 to 0.0:1.0 return gen.noise2d(nx, ny) / 2.0 + 0.5 value = [] for y in range(height): value.append([0] * width) for x in range(width): nx = x/width - 0.5 ny = y/height - 0.5 value[y][x] = noise(nx, ny) 

Todas as bibliotecas de ruído são muito parecidas. Tente opensimplex para Python , ou libnoise para C ++ , ou simplex-noise para Javascript. Para os idiomas mais populares, existem muitas bibliotecas de ruído. Ou você pode aprender como funciona o ruído Perlin ou perceber o ruído você mesmo. Eu não fiz isso.

Em diferentes bibliotecas de ruído para o seu idioma, os detalhes do aplicativo podem variar um pouco (alguns números de retorno no intervalo de 0,0 a 1,0, outros no intervalo de -1,0 a +1,0), mas a idéia básica é a mesma. Para um projeto real, pode ser necessário agrupar a função de noise e o objeto gen em uma classe, mas esses detalhes são irrelevantes, então eu os tornei globais.

Para um projeto tão simples, não importa o ruído usado: ruído Perlin, ruído simplex, ruído OpenSimplex, ruído de valor, deslocamento do ponto médio, algoritmo de diamante ou a transformada inversa de Fourier. Cada um deles tem seus prós e contras, mas para um gerador de cartões semelhante, todos criam mais ou menos os mesmos valores de saída.

A renderização do mapa depende da plataforma e do jogo, então eu não o implementei; esse código é necessário apenas para gerar alturas e biomas, cuja renderização depende do estilo usado no jogo. Você pode copiar, portar e usá-lo em seus projetos.

Os experimentos


Eu olhei para misturar oitavas, elevar graus a uma potência e combinar alturas com umidade para criar um bioma. Aqui você pode estudar um gráfico interativo que permite experimentar todos esses parâmetros, o que mostra em que consiste o código:


Aqui está um código de exemplo:

 var rng1 = PM_PRNG.create(seed1); var rng2 = PM_PRNG.create(seed2); var gen1 = new SimplexNoise(rng1.nextDouble.bind(rng1)); var gen2 = new SimplexNoise(rng2.nextDouble.bind(rng2)); function noise1(nx, ny) { return gen1.noise2D(nx, ny)/2 + 0.5; } function noise2(nx, ny) { return gen2.noise2D(nx, ny)/2 + 0.5; } for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { var nx = x/width - 0.5, ny = y/height - 0.5; var e = (1.00 * noise1( 1 * nx, 1 * ny) + 0.50 * noise1( 2 * nx, 2 * ny) + 0.25 * noise1( 4 * nx, 4 * ny) + 0.13 * noise1( 8 * nx, 8 * ny) + 0.06 * noise1(16 * nx, 16 * ny) + 0.03 * noise1(32 * nx, 32 * ny)); e /= (1.00+0.50+0.25+0.13+0.06+0.03); e = Math.pow(e, 5.00); var m = (1.00 * noise2( 1 * nx, 1 * ny) + 0.75 * noise2( 2 * nx, 2 * ny) + 0.33 * noise2( 4 * nx, 4 * ny) + 0.33 * noise2( 8 * nx, 8 * ny) + 0.33 * noise2(16 * nx, 16 * ny) + 0.50 * noise2(32 * nx, 32 * ny)); m /= (1.00+0.75+0.33+0.33+0.33+0.50); /* draw biome(e, m) at x,y */ } } 

Existe uma dificuldade: para ruídos de altura e umidade, é necessário usar uma semente diferente; caso contrário, elas serão as mesmas e as cartas não parecerão tão interessantes. Em Javascript, eu uso a biblioteca prng-parkmiller ; em C ++, você pode usar dois objetos linear_congruential_engine separados; no Python, você pode criar duas instâncias separadas de uma classe random.Random .

Pensamentos


Eu gosto dessa abordagem para mapear a geração por sua simplicidade . É rápido e requer muito pouco código para produzir resultados decentes.

Não gosto das limitações dele nessa abordagem. Cálculos locais significam que cada ponto é independente de todos os outros. Áreas diferentes do mapa não estão conectadas entre si . Cada lugar no mapa "parece" o mesmo. Não há restrições globais, por exemplo, “deve haver de 3 a 5 lagos no mapa” ou recursos globais, como um rio que flui do topo do pico mais alto para o oceano. Além disso, não gosto do fato de que, para obter uma boa imagem, você precisa configurar os parâmetros por um longo tempo.

Por que eu recomendo? Penso que este é um bom ponto de partida, especialmente para jogos indie e compotas de jogos. Dois dos meus amigos escreveram a versão inicial de Realm of the Mad God em apenas 30 dias para um concurso de jogos . Eles me pediram para ajudar a criar mapas. Eu usei essa técnica (além de mais alguns recursos que acabaram não sendo muito úteis) e fiz um mapa para eles. Alguns meses depois, depois de receber feedback dos jogadores e estudar cuidadosamente o design do jogo, criamos um gerador de mapas mais avançado, baseado nos polígonos de Voronoi, descritos aqui ( tradução em Habré). Este gerador de cartões não usa as técnicas descritas neste artigo. Ele usa ruído para criar mapas de uma maneira completamente diferente.

Informações Adicionais


muitas coisas legais que você pode fazer com as funções de ruído. Se você pesquisar na Internet, poderá encontrar opções como turbulência, ondulação, multifractal sulcado, amortecimento de amplitude, terraços, ruído voronoi, derivados analíticos, distorção de domínio e outros. Você pode usar esta página como fonte de inspiração. Não os considero aqui, meu artigo se concentra na simplicidade.

Este projeto foi influenciado pelos meus projetos anteriores de geração de mapas:

  • Usei o ruído geral de Perlin no meu primeiro gerador de cartões Reino do Deus Louco . Nós o usamos nos primeiros seis meses de teste alfa e, em seguida, o substituímos por um gerador de mapas nos polígonos Voronoi , criado especialmente para os requisitos de jogabilidade que determinamos durante o teste alfa. Os biomas e suas cores para o artigo são retirados desses projetos.
  • Ao estudar o processamento de sinais de áudio, escrevi um tutorial sobre ruído que explica conceitos como frequência, amplitude, oitavas e a "cor" do ruído. Os mesmos conceitos que funcionam para o som também se aplicam à geração de cartões com base em ruído. Naquele momento, criei uma geração bruta de alívio de demonstração , mas não a terminei.
  • Às vezes, experimento encontrar limites. Eu queria saber quanto código é minimamente necessário para criar mapas atraentes. Neste mini-projeto, cheguei a zero linha de código - tudo é feito com filtros de imagem (turbulência, limites, gradientes de cores). Isso me deixou feliz e triste. Até que ponto a geração de mapas pode ser realizada por filtros de imagem? Em grande o suficiente. Tudo o que foi descrito acima sobre o "esquema de gradientes de cores suaves" é retirado desta experiência. A camada de ruído é um filtro de imagem de turbulência; oitavas são imagens sobrepostas; A ferramenta de graduação é chamada de "correção de curva" no Photoshop.

O que me incomoda um pouco é que a maior parte do código que os desenvolvedores de jogos escrevem para a geração de terreno com base em ruído (incluindo deslocamento do ponto médio) acaba sendo a mesma que nos filtros de som e imagem. Por outro lado, ele cria resultados bastante decentes em apenas algumas linhas de código, e foi por isso que escrevi este artigo. Este é um ponto de referência rápido e fácil . Normalmente, não uso essas cartas por muito tempo, mas as substituo por um gerador de mapas mais complexo assim que descobrir quais tipos de cartas são mais adequados ao design do jogo. Para mim, esse é um padrão padrão: começar com algo extremamente simples e substituí-lo somente depois que eu entender melhor o sistema com o qual estou trabalhando.

muito mais coisas que podem ser feitas com ruído, no artigo que mencionei apenas algumas. Experimente o Noise Studio para testar interativamente vários recursos.

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


All Articles