Crear mapas a partir de características de ruido

Uno de los artículos más populares en mi sitio está dedicado a la generación de mapas poligonales ( traducción en Habré). Crear esas tarjetas requiere mucho esfuerzo. Pero no comencé con esto, sino con una tarea mucho más simple, que describiré aquí. Esta técnica simple le permite crear esas tarjetas en menos de 50 líneas de código:


No explicaré cómo dibujar esas tarjetas: depende del idioma, la biblioteca de gráficos, la plataforma, etc. Simplemente explicaré cómo llenar la matriz con datos del mapa.

El ruido


Una forma estándar de generar mapas 2D es utilizar el ruido con una banda de frecuencia limitada como bloque de construcción, como el ruido Perlin o el ruido simplex. Así es como se ve la función de ruido:

imagen

Asignamos un número de 0.0 a 1.0 a cada punto en el mapa. En esta imagen, 0.0 es negro y 1.0 es blanco. Aquí se explica cómo configurar el color de cada punto de la cuadrícula en la sintaxis de un lenguaje tipo 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); } } 

El bucle funcionará de la misma manera en Javascript, Python, Haxe, C ++, C #, Java y la mayoría de los otros lenguajes populares, por lo que lo mostraré en una sintaxis tipo C para que pueda convertirlo al idioma que necesita. En el resto del tutorial, mostraré cómo cambia el cuerpo del bucle ( value[y][x]=… línea value[y][x]=… ) al agregar nuevas funciones. La demostración mostrará un ejemplo completo.

En algunas bibliotecas, será necesario cambiar o multiplicar los valores resultantes para devolverlos en el rango de 0.0 a 1.0.

Altura


El ruido en sí mismo es solo una colección de números. Necesitamos darle significado . Lo primero en lo que puede pensar es vincular el valor de ruido a la altura (esto se llama un "mapa de altura"). Tomemos el ruido que se muestra arriba y dibujémoslo como altura:



El código se mantuvo casi igual, con la excepción del bucle interno. Ahora se ve así:

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

Sí, y eso es todo. Los datos del mapa permanecieron igual, pero ahora los llamaré elevation (altura), no value .

Tenemos muchas colinas, pero nada más. Que esta mal

Frecuencia


Se puede generar ruido a cualquier frecuencia . Hasta ahora he elegido solo una frecuencia. Veamos cómo afecta.

Intente cambiar el valor con el control deslizante (en el artículo original) y vea qué sucede a diferentes frecuencias:


Simplemente cambia la escala. Al principio esto no parece muy útil, pero no lo es. Tengo un tutorial más ( traducción en Habré), que explica la teoría : conceptos como frecuencia, amplitud, octavas, ruido rosa y azul, etc.

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

A veces también es útil recordar la longitud de onda , que es el recíproco de la magnitud. Cuando la frecuencia se duplica, el tamaño solo se reduce a la mitad. Doblar la longitud de onda todos los dobles. La longitud de onda es la distancia medida en píxeles / mosaicos / metros o cualquier otra unidad que haya seleccionado para los mapas. Está relacionado con la frecuencia: wavelength = map_size / frequency .

Octavas


Para hacer que el mapa de altura sea más interesante, agregaremos ruido con diferentes frecuencias :



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

Mezclemos grandes colinas de baja frecuencia con pequeñas colinas de alta frecuencia en un mapa. Mueva el control deslizante (en el artículo original) para agregar pequeñas colinas a la mezcla:


¡Ahora se parece mucho más al alivio fractal que necesitamos! Podemos obtener colinas y montañas escarpadas, pero aún no tenemos llanuras planas. Para hacer esto, necesitas algo más.

Redistribución


La función de ruido nos da valores entre 0 y 1 (o de -1 a +1, dependiendo de la biblioteca). Para crear llanuras planas, podemos elevar la altura a una potencia . Mueva el control deslizante (en el artículo original) para obtener diferentes grados.


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

Los valores altos reducen las alturas promedio a las llanuras , y los valores bajos elevan las alturas promedio hacia los picos de las montañas. Necesitamos omitirlos. Uso las funciones de potencia porque son más simples, pero puedes usar cualquier curva; Tengo una demo más complicada.

Ahora que tenemos un mapa de elevación realista, ¡agreguemos biomas!

Biomas


El ruido da números, pero necesitamos un mapa con bosques, desiertos y océanos. Lo primero que puede hacer es convertir pequeñas alturas en agua:


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

¡Vaya, esto ya se está convirtiendo en un mundo generado por procedimientos! Tenemos agua, hierba y nieve. Pero, ¿y si necesitamos más? Hagamos una secuencia de agua, arena, hierba, bosque, sabana, desierto y nieve:



Alivio basado en la 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; } 

Wow, eso se ve genial! Para tu juego, puedes cambiar los valores y los biomas. Crysis tendrá mucha más jungla; Skyrim tiene mucho más hielo y nieve. Pero no importa cómo cambie los números, este enfoque es bastante limitado. Los tipos de relieve corresponden a las alturas, por lo tanto, forman tiras. Para hacerlos más interesantes, necesitamos elegir biomas basados ​​en otra cosa. Creemos un segundo mapa de ruido para la humedad.



Arriba está el ruido de las alturas; fondo - ruido de humedad

Ahora usemos altura y humedad juntos . En la primera imagen que se muestra a continuación, el eje y es la altura (tomada de la imagen de arriba) y el eje x es la humedad (la segunda imagen es más alta). Esto nos da un mapa convincente:



Alivio basado en dos valores de ruido

Las pequeñas alturas son océanos y costas. Grandes alturas son rocosas y nevadas. En el medio, obtenemos una amplia gama de biomas. El código se ve así:

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

Si es necesario, puedes cambiar todos estos valores de acuerdo con los requisitos de tu juego.

Si no necesitamos biomas, los gradientes suaves (vea este artículo ) pueden crear colores:



Tanto para los biomas como para los gradientes, un solo valor de ruido no proporciona suficiente variabilidad, pero dos es suficiente.

Clima


En la sección anterior, usé la altitud como un sustituto de la temperatura . Cuanto mayor sea la altura, menor será la temperatura. Sin embargo, la latitud geográfica también afecta las temperaturas. Usemos la altura y la latitud para controlar la temperatura:


Cerca de los polos (grandes latitudes) el clima es más frío, y en las cimas de las montañas (grandes alturas) el clima también es más frío. Hasta ahora no he trabajado mucho: para el enfoque correcto de estos parámetros, necesita una gran cantidad de configuraciones sutiles.

También hay cambio climático estacional . En verano e invierno, los hemisferios norte y sur se vuelven más cálidos y fríos, pero en el ecuador la situación no cambia mucho. Aquí también se puede hacer mucho, por ejemplo, uno puede simular los vientos y las corrientes oceánicas predominantes, el efecto de los biomas en el clima y el efecto promedio de los océanos en las temperaturas.

Las islas


En algunos proyectos, necesitaba que los bordes del mapa fueran agua. Esto convierte el mundo en una o más islas. Hay muchas maneras de hacer esto, pero utilicé una solución bastante simple en mi generador de mapas poligonales: cambié la altura como e = e + a - b*d^c , donde d es la distancia desde el centro (en una escala de 0-1). Otra opción es cambiar e = (e + a) * (1 - b*d^c) . La constante a eleva todo hacia arriba, b baja los bordes hacia abajo c controla la tasa de disminución.


No estoy completamente satisfecho con esto y queda mucho por explorar. ¿Debería ser Manhattan o la distancia euclidiana? ¿Debería depender de la distancia al centro o de la distancia al borde? ¿Debería la distancia ser cuadrada, lineal o tener algún otro grado? ¿Debería ser suma / resta, o multiplicación / división, o algo más? En el artículo original, intente Agregar, a = 0.1, b = 0.3, c = 2.0, o intente Multiplicar, a = 0.05, b = 1.00, c = 1.5. Las opciones que más le convengan dependen de su proyecto.

¿Por qué apegarse a las funciones matemáticas estándar? Como dije en mi artículo sobre el daño en RPG ( traducción en Habré), todos (incluyéndome a mí) usan funciones matemáticas, como polinomios, distribuciones exponenciales, etc., pero en la computadora no podemos limitarnos a ellas. Podemos tomar cualquier función de formación y usarla aquí, usando la tabla de búsqueda e = e + height_adjust[d] . Hasta ahora no he estudiado este tema.

Ruido de punta


En lugar de elevar la altura a una potencia, podemos usar el valor absoluto para crear picos agudos:

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

Para agregar octavas, podemos variar las amplitudes de las frecuencias altas para que solo las montañas reciban el ruido 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); 


No tengo mucha experiencia con esta técnica, así que necesito experimentar para aprender a usarla bien. También puede ser interesante mezclar ruido puntiagudo de baja frecuencia con ruido no puntiagudo de alta frecuencia.

Terrazas


Si redondeamos la altura a los siguientes n niveles, obtenemos terrazas:


Este es el resultado de aplicar la función de redistribución de altura en la forma e = f(e) . Arriba, usamos e = Math.pow(e, exponent) para agudizar los picos de las montañas; aquí usamos e = Math.round(e * n) / n para crear terrazas. Si utiliza una función que no sea de paso, las terrazas se pueden redondear o aparecer solo a ciertas alturas.

Colocación de árboles


Usualmente usamos ruido fractal para la altura y la humedad, pero también se puede usar para colocar objetos espaciados de manera desigual, como árboles y piedras. Para la altura usamos amplitudes altas con frecuencias bajas ("ruido rojo"). Para colocar objetos necesita usar amplitudes altas con frecuencias altas ("ruido azul"). A la izquierda hay un patrón de ruido azul; a la derecha hay lugares donde el ruido es mayor que los valores adyacentes:


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

Al elegir diferentes R para cada bioma, podemos obtener una densidad variable de árboles:



Es genial que ese ruido se pueda usar para colocar árboles, pero otros algoritmos a menudo son más efectivos y crean una distribución más uniforme: manchas Poisson, mosaicos Van o tramado gráfico.

Hasta el infinito y más allá


Los cálculos del bioma en la posición (x, y) son independientes de los cálculos de todas las demás posiciones. Este cálculo local tiene dos propiedades convenientes: se puede calcular en paralelo y se puede usar para terrenos infinitos. Coloque el cursor del mouse en el minimapa (en el artículo original) a la izquierda para generar el mapa a la derecha. Puede generar cualquier parte de la tarjeta sin generar (e incluso sin almacenar) la tarjeta completa.



Implementación


El uso de ruido para generar terreno es una solución popular, y en Internet puede encontrar tutoriales para muchos idiomas y plataformas diferentes. El código para generar tarjetas en diferentes idiomas es aproximadamente el mismo. Aquí está el bucle más simple en tres 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 las bibliotecas de ruido son muy parecidas. Pruebe opensimplex para Python , o libnoise para C ++ , o simplex-noise para Javascript. Para los idiomas más populares, hay muchas bibliotecas de ruido. O puede aprender cómo funciona el ruido Perlin o darse cuenta usted mismo del ruido. No lo hice

En diferentes bibliotecas de ruido para su idioma, los detalles de la aplicación pueden variar ligeramente (algunos números de retorno en el rango de 0.0 a 1.0, otros en el rango de -1.0 a +1.0), pero la idea básica es la misma. Para un proyecto real, es posible que necesite ajustar la función de noise y el objeto gen en una clase, pero estos detalles son irrelevantes, por lo que los hice globales.

Para un proyecto tan simple, no importa qué ruido use: ruido Perlin, ruido simplex, ruido OpenSimplex, ruido de valor, compensación de punto medio, algoritmo de diamante o la transformación inversa de Fourier. Cada uno de ellos tiene sus pros y sus contras, pero para un generador de tarjetas similar, todos crean más o menos los mismos valores de salida.

La representación del mapa depende de la plataforma y el juego, por lo que no lo implementé; Este código solo es necesario para generar alturas y biomas, cuya representación depende del estilo utilizado en el juego. Puede copiar, portar y usarlo en sus proyectos.

Los experimentos


Observé mezclar octavas, elevar grados a una potencia y combinar alturas con humedad para crear un bioma. Aquí puede estudiar un gráfico interactivo que le permite experimentar con todos estos parámetros, que muestra en qué consiste el código:


Aquí hay un código de muestra:

 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 */ } } 

Hay una dificultad: para el ruido de las alturas y la humedad, es necesario usar una semilla diferente, de lo contrario resultarán iguales, y las tarjetas no se verán tan interesantes. En Javascript, uso la biblioteca prng-parkmiller ; en C ++, puede usar dos objetos independientes de linear_congruential_engine ; en Python, puede crear dos instancias separadas de una clase random.Random .

Pensamientos


Me gusta este enfoque para la generación de mapas por su simplicidad . Es rápido y requiere muy poco código para producir resultados decentes.

No me gustan sus limitaciones en este enfoque. Los cálculos locales significan que cada punto es independiente de todos los demás. Las diferentes áreas del mapa no están conectadas entre sí . Cada lugar en el mapa "parece" lo mismo. No hay restricciones globales, por ejemplo, "debería haber de 3 a 5 lagos en el mapa" o características globales, como un río que fluye desde la cima del pico más alto hacia el océano. Además, no me gusta el hecho de que para obtener una buena imagen es necesario configurar los parámetros durante mucho tiempo.

¿Por qué lo recomiendo? Creo que este es un buen punto de partida, especialmente para juegos independientes y jams de juegos. Dos de mis amigos escribieron la versión inicial de Realm of the Mad God en solo 30 días para un concurso de juegos . Me pidieron que ayudara a crear mapas. Utilicé esta técnica (además de algunas características más que resultaron no ser muy útiles) e hice un mapa para ellas. Unos meses más tarde, después de recibir comentarios de los jugadores y estudiar cuidadosamente el diseño del juego, creamos un generador de mapas más avanzado basado en polígonos Voronoi, descrito aquí ( traducción en Habré). Este generador de tarjetas no utiliza las técnicas descritas en este artículo. Utiliza el ruido para crear mapas de una manera completamente diferente.

Información adicional


Hay muchas cosas interesantes que puedes hacer con las funciones de ruido. Si busca en Internet, puede encontrar opciones tales como turbulencia, ondulación, multifractal acanalado, amortiguación de amplitud, en terrazas, ruido voronoi, derivados analíticos, deformación de dominios y otros. Puede usar esta página como fuente de inspiración. No los considero aquí; mi artículo se centra en la simplicidad.

Este proyecto fue influenciado por mis proyectos de generación de mapas anteriores:

  • Usé el ruido general de Perlin para mi primer generador de cartas del Reino del Dios Loco . Lo usamos durante los primeros seis meses de prueba alfa, y luego lo reemplazamos con un generador de mapas en polígonos Voronoi , especialmente creado para los requisitos de juego que determinamos durante la prueba alfa. Los biomas y sus colores para el artículo se toman de estos proyectos.
  • Al estudiar el procesamiento de señales de audio, escribí un tutorial de ruido que explica conceptos como frecuencia, amplitud, octavas y el "color" del ruido. Los mismos conceptos que funcionan para el sonido también se aplican a la generación de tarjetas basadas en ruido. En ese momento, creé una generación de socorro de demostración en bruto, pero no los terminé.
  • A veces experimento para encontrar límites. Quería saber cuánto código se necesita mínimamente para crear mapas atractivos. En este mini proyecto, llegué a cero líneas de código: todo se hace con filtros de imagen (turbulencia, umbrales, gradientes de color). Esto me hizo feliz y triste. ¿En qué medida se puede realizar la generación de mapas mediante filtros de imagen? En lo suficientemente grande. Todo lo descrito anteriormente sobre el "esquema de gradientes de color suaves" se toma de este experimento. La capa de ruido es un filtro de imagen de turbulencia; las octavas son imágenes superpuestas entre sí; La herramienta de grado se llama "corrección de curva" en Photoshop.

Lo que me molesta un poco es que la mayoría del código que escriben los desarrolladores de juegos para la generación de terreno basada en el ruido (incluido el desplazamiento del punto medio) resulta ser el mismo que en los filtros de sonido e imagen. Por otro lado, crea resultados bastante decentes en solo unas pocas líneas de código, por eso escribí este artículo. Este es un punto de referencia rápido y fácil . Por lo general, no uso esas tarjetas durante mucho tiempo, pero las reemplazo con un generador de mapas más complejo tan pronto como descubro qué tipos de tarjetas se adaptan mejor al diseño del juego. Este es un patrón estándar para mí: comenzar con algo extremadamente simple y luego reemplazarlo solo después de comprender mejor el sistema con el que estoy trabajando.

Hay muchas más cosas que se pueden hacer con el ruido, en el artículo que mencioné solo algunas. Pruebe Noise Studio para probar de forma interactiva varias funciones.

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


All Articles