Algoritmo de mosaico de mapas

153

El mapa

Estoy haciendo un juego de rol basado en mosaicos con Javascript, usando mapas de altura de ruido perlin, luego asigno un tipo de mosaico basado en la altura del ruido.

Los mapas terminan pareciéndose a esto (en la vista de minimapa).

ingrese la descripción de la imagen aquí

Tengo un algoritmo bastante simple que extrae el valor de color de cada píxel en la imagen y lo convierte en un número entero (0-5) dependiendo de su posición entre (0-255) que corresponde a un mosaico en el diccionario de mosaicos. Esta matriz de 200x200 se pasa al cliente.

Luego, el motor determina los mosaicos a partir de los valores en la matriz y los dibuja en el lienzo. Entonces, termino con mundos interesantes que tienen características de aspecto realista: montañas, mares, etc.

Ahora, lo siguiente que quería hacer era aplicar algún tipo de algoritmo de mezcla que haría que los mosaicos se mezclaran perfectamente con sus vecinos, si el vecino no es del mismo tipo. El mapa de ejemplo anterior es lo que el jugador ve en su minimapa. En pantalla ven una versión renderizada de la sección marcada por el rectángulo blanco; donde los mosaicos se representan con sus imágenes en lugar de como píxeles de un solo color.

Este es un ejemplo de lo que el usuario vería en el mapa, ¡pero no es la misma ubicación que muestra la ventana de arriba!

ingrese la descripción de la imagen aquí

Es desde este punto de vista que quiero que ocurra la transición.

El algoritmo

Se me ocurrió un algoritmo simple que atravesaría el mapa dentro de la ventana gráfica y representaría otra imagen sobre la parte superior de cada mosaico, siempre que estuviera al lado de un mosaico de diferente tipo. (¡No cambia el mapa! Solo renderizo algunas imágenes adicionales). La idea del algoritmo era perfilar a los vecinos del mosaico actual:

Un ejemplo de un perfil de mosaico

Este es un escenario de ejemplo de lo que el motor podría tener que representar, siendo el mosaico actual el marcado con la X.

Se crea una matriz 3x3 y se leen los valores a su alrededor. Entonces, para este ejemplo, la matriz se vería así.

[
    [1,2,2]
    [1,2,2]
    [1,1,2]
];

Mi idea era resolver una serie de casos para las posibles configuraciones de mosaico. En un nivel muy simple:

if(profile[0][1] != profile[1][1]){
     //draw a tile which is half sand and half transparent
     //Over the current tile -> profile[1][1]
     ...
}

Lo que da este resultado:

Resultado

Lo que funciona como una transición de [0][1]a [1][1], pero no de [1][1]a [2][1], donde queda un borde duro. Así que pensé que en ese caso tendría que usarse un mosaico de esquina. Creé dos hojas de sprites de 3x3 que pensé que contendrían todas las combinaciones posibles de fichas que podrían ser necesarias. Luego repliqué esto para todas las fichas que hay en el juego (Las áreas blancas son transparentes). Esto termina siendo 16 mosaicos para cada tipo de mosaico (no se utilizan los mosaicos centrales de cada hoja de sprites).

ArenaArena2

El resultado ideal

Entonces, con estos nuevos mosaicos y el algoritmo correcto, la sección de ejemplo se vería así:

Correcto

Sin embargo, cada intento que hice ha fallado, siempre hay algún defecto en el algoritmo y los patrones terminan siendo extraños. Parece que no puedo resolver todos los casos y, en general, parece una mala forma de hacerlo.

¿Una solución?

Entonces, si alguien pudiera proporcionar una solución alternativa sobre cómo podría crear este efecto, o qué dirección tomar para escribir el algoritmo de creación de perfiles, ¡estaría muy agradecido!

Dan Prince
fuente
77
Eche un vistazo a este artículo y también a los artículos vinculados, especialmente este . El blog en sí contiene muchas ideas que pueden servir como punto de partida. Aquí hay una descripción general.
Darcara
deberías simplificar tu algoritmo. compruebe esto: Autómatas celulares
bidimensionales

Respuestas:

117

La idea básica de este algoritmo es utilizar un paso de preprocesamiento para encontrar todos los bordes y luego seleccionar el mosaico de suavizado correcto de acuerdo con la forma del borde.

El primer paso sería encontrar todos los bordes. En el ejemplo a continuación, las fichas de borde marcadas con una X son todas fichas verdes con una ficha marrón como una o más de sus ocho fichas adyacentes. Con diferentes tipos de terreno, esta condición podría traducirse en que un mosaico sea un mosaico de borde si tiene vecinos de un número de terreno más bajo.

Azulejos de borde.

Una vez que se detectan todos los mosaicos de bordes, lo siguiente que debe hacer es seleccionar el mosaico de suavizado correcto para cada mosaico de bordes. Aquí está mi representación de sus mosaicos de alisado.

Alisado de azulejos.

Tenga en cuenta que en realidad no hay tantos tipos diferentes de mosaicos. Necesitamos los ocho mosaicos exteriores de uno de los cuadrados de 3x3, pero solo los cuatro cuadrados de las esquinas del otro, ya que los mosaicos de borde recto ya se encuentran en el primer cuadrado. Esto significa que en total hay 12 casos diferentes entre los que debemos distinguir.

Ahora, mirando un mosaico de borde, podemos determinar en qué dirección gira el límite mirando sus cuatro mosaicos vecinos más cercanos. Al marcar un mosaico de borde con X, tal como se muestra arriba, tenemos los siguientes seis casos diferentes.

Seis casos.

Estos casos se utilizan para determinar el mosaico de suavizado correspondiente y podemos numerar los mosaicos de suavizado en consecuencia.

Alisado de azulejos con números.

Todavía hay una opción de aob para cada caso. Esto depende de qué lado esté el césped. Una forma de determinar esto podría ser hacer un seguimiento de la orientación del límite, pero probablemente la forma más sencilla de hacerlo es elegir un mosaico al lado del borde y ver qué color tiene. La imagen a continuación muestra los dos casos 5a) y 5b) que se pueden distinguir, por ejemplo, verificando el color del mosaico superior derecho.

Elegir 5a o 5b.

La enumeración final para el ejemplo original se vería así.

Enumeración final

Y después de seleccionar el mosaico de borde correspondiente, el borde se vería así.

Resultado final.

Como nota final, podría decir que esto funcionaría siempre que el límite sea algo regular. Más precisamente, los mosaicos de borde que no tienen exactamente dos mosaicos de borde, ya que sus vecinos deberán tratarse por separado. Esto ocurrirá para las fichas de borde en el borde del mapa que tendrá un solo borde vecino y para piezas de terreno muy estrechas donde el número de fichas de borde vecino podría ser tres o incluso cuatro.

usuario1884905
fuente
1
Esto es genial y muy útil para mí. Estoy lidiando con un caso en el que algunas fichas no pueden pasar directamente a otras. Por ejemplo, las baldosas de "tierra" pueden pasar a "hierba ligera" y la "hierba ligera" puede pasar a "hierba mediana". Tiled (mapeditor.org) hace un gran trabajo al manejar esto implementando algún tipo de búsqueda de árboles para el pincel de terreno; Sin embargo, todavía no puedo reproducirlo.
Clay
12

El siguiente cuadrado representa una placa de metal. Hay una "ventilación de calor" en la esquina superior derecha. Podemos ver cómo a medida que la temperatura de este punto permanece constante, la placa de metal converge a una temperatura constante en cada punto, siendo naturalmente más caliente cerca de la parte superior:

placa calefactora

El problema de encontrar la temperatura en cada punto se puede resolver como un "problema de valor límite". Sin embargo, la forma más sencilla de calcular el calor en cada punto es modelar la placa como una cuadrícula. Conocemos los puntos en la cuadrícula a temperatura constante. Configuramos la temperatura de todos los puntos desconocidos para que sean temperatura ambiente (como si la ventilación acabara de encenderse). Luego dejamos que el calor se extienda a través de la placa hasta llegar a la convergencia. Esto se hace por iteración: iteramos a través de cada punto (i, j). Establecemos punto (i, j) = (punto (i + 1, j) + punto (i-1, j) + punto (i, j + 1) + punto (i, j-1)) / 4 [a menos el punto (i, j) tiene una salida de calor de temperatura constante]

Si aplica esto a su problema, es muy similar, solo colores promedio en lugar de temperaturas. Probablemente necesite unas 5 iteraciones. Sugiero usar una cuadrícula de 400x400. Eso es 400x400x5 = menos de 1 millón de iteraciones que serán rápidas. Si solo usa 5 iteraciones, probablemente no tendrá que preocuparse por mantener los puntos de color constante, ya que no cambiarán demasiado de su original (de hecho, solo los puntos dentro de la distancia 5 del color pueden ser afectados por el color). Pseudocódigo:

iterations = 5
for iteration in range(iterations):
    for i in range(400):
        for j in range(400):
            try:
                grid[i][j] = average(grid[i+1][j], grid[i-1][j],
                                     grid[i][j+1], grid[i][j+1])
            except IndexError:
                pass
robert king
fuente
¿podría ampliar esto un poco más? Tengo curiosidad y no puedo entender tu explicación. ¿Cómo se usa el valor de color promedio después de haber realizado las iteraciones?
Chii
1
Cada cuadrícula de puntos de cuadrícula [i] [j] se puede dibujar en el lienzo como un pequeño rectángulo (o píxel individual) del color apropiado.
Robert King
5

Bien, los primeros pensamientos son que automatizar una solución perfecta al problema requiere algunas matemáticas de interpolación bastante carnosas. Basado en el hecho de que mencionas imágenes de mosaico pre-renderizadas, supongo que la solución de interpolación completa no está garantizada aquí.

Por otro lado, como dijiste, terminar el mapa a mano conducirá a un buen resultado ... pero también supongo que cualquier proceso manual para corregir fallas tampoco es una opción.

Aquí hay un algoritmo simple que no da un resultado perfecto, pero que es muy gratificante en función del bajo esfuerzo que requiere.

En lugar de intentar mezclar CADA mosaico de borde, (lo que significa que necesita saber primero el resultado de mezclar los mosaicos adyacentes - interpolación, o necesita refinar todo el mapa varias veces y no puede confiar en mosaicos pregenerados) ¿Por qué no mezclar fichas en un patrón de tablero de damas alternativo?

[1] [*] [2]
[*] [1] [*]
[1] [*] [2]

Es decir, ¿solo se mezclan los mosaicos destacados en la matriz de arriba?

Suponiendo que los únicos pasos permitidos en el valor son uno a la vez, solo tiene unos pocos mosaicos para diseñar ...

A    [1]      B    [2]      C    [1]      D    [2]      E    [1]           
 [1] [*] [1]   [1] [*] [1]   [1] [*] [2]   [1] [*] [2]   [1] [*] [1]   etc.
     [1]           [1]           [1]           [1]           [2]           

Habrá 16 patrones en total. Si aprovecha la simetría rotacional y reflexiva, habrá aún menos.

'A' sería un mosaico de estilo simple [1]. 'D' sería una diagonal.

Habrá pequeñas discontinuidades en las esquinas de los mosaicos, pero serán menores en comparación con el ejemplo que usted dio.

Si puedo, actualizaré esta publicación con imágenes más adelante.

perfeccionista
fuente
Esto suena bien, me interesaría verlo con algunas imágenes para tener una mejor idea de lo que quieres decir.
Dan Prince
No puedo juntar ninguna imagen porque no tengo el software que pensé que tenía ... Pero he estado pensando y no es una solución tan buena como podría ser. Puede hacer transiciones diagonales, sin duda, pero este algoritmo de suavizado realmente no ayuda a otras transiciones. Ni siquiera puede garantizar que su mapa no contendrá transiciones de 90 grados. Lo siento, supongo que este es un poco decepcionante.
perfeccionista
3

Estaba jugando con algo similar a esto, no se terminó por varias razones; pero básicamente tomaría una matriz de 0 y 1, siendo 0 el suelo y 1 siendo una pared para una aplicación de generador de laberinto en Flash. Como AS3 es similar a JavaScript, no sería difícil volver a escribir en JS.

var tileDimension:int = 20;
var levelNum:Array = new Array();

levelNum[0] = [1, 1, 1, 1, 1, 1, 1, 1, 1];
levelNum[1] = [1, 0, 0, 0, 0, 0, 0, 0, 1];
levelNum[2] = [1, 0, 1, 1, 1, 0, 1, 0, 1];
levelNum[3] = [1, 0, 1, 0, 1, 0, 1, 0, 1];
levelNum[4] = [1, 0, 1, 0, 0, 0, 1, 0, 1];
levelNum[5] = [1, 0, 0, 0, 0, 0, 0, 0, 1];
levelNum[6] = [1, 0, 1, 1, 1, 1, 0, 0, 1];
levelNum[7] = [1, 0, 0, 0, 0, 0, 0, 0, 1];
levelNum[8] = [1, 1, 1, 1, 1, 1, 1, 1, 1];

for (var rows:int = 0; rows < levelNum.length; rows++)
{
    for (var cols:int = 0; cols < levelNum[rows].length; cols++)
    {
        // set up neighbours
        var toprow:int = rows - 1;
        var bottomrow:int = rows + 1;

        var westN:int = cols - 1;
        var eastN:int = cols + 1;

        var rightMax =  levelNum[rows].length;
        var bottomMax = levelNum.length;

        var northwestTile =     (toprow != -1 && westN != -1) ? levelNum[toprow][westN] : 1;
        var northTile =         (toprow != -1) ? levelNum[toprow][cols] : 1;
        var northeastTile =     (toprow != -1 && eastN < rightMax) ? levelNum[toprow][eastN] : 1;

        var westTile =          (cols != 0) ? levelNum[rows][westN] : 1;
        var thistile =          levelNum[rows][cols];
        var eastTile =          (eastN == rightMax) ? 1 : levelNum[rows][eastN];

        var southwestTile =     (bottomrow != bottomMax && westN != -1) ? levelNum[bottomrow][westN] : 1;
        var southTile =         (bottomrow != bottomMax) ? levelNum[bottomrow][cols] : 1;
        var southeastTile =     (bottomrow != bottomMax && eastN < rightMax) ? levelNum[bottomrow][eastN] : 1;

        if (thistile == 1)
        {
            var w7:Wall7 = new Wall7();
            addChild(w7);
            pushTile(w7, cols, rows, 0);

            // wall 2 corners

            if      (northTile === 0 && northeastTile === 0 && eastTile === 1 && southeastTile === 1 && southTile === 1 && southwestTile === 0 && westTile === 0 && northwestTile === 0)
            {
                var w21:Wall2 = new Wall2();
                addChild(w21);
                pushTile(w21, cols, rows, 270);
            }

            else if (northTile === 0 && northeastTile === 0 && eastTile === 0 && southeastTile === 0 && southTile === 1 && southwestTile === 1 && westTile === 1 && northwestTile === 0)
            {
                var w22:Wall2 = new Wall2();
                addChild(w22);
                pushTile(w22, cols, rows, 0);
            }

            else if (northTile === 1 && northeastTile === 0 && eastTile === 0 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 1 && northwestTile === 1)
            {
                var w23:Wall2 = new Wall2();
                addChild(w23);
                pushTile(w23, cols, rows, 90);
            }

            else if (northTile === 1 && northeastTile === 1 && eastTile === 1 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 0 && northwestTile === 0)
            {
                var w24:Wall2 = new Wall2();
                addChild(w24);
                pushTile(w24, cols, rows, 180);
            }           

            //  wall 6 corners

            else if (northTile === 1 && northeastTile === 1 && eastTile === 1 && southeastTile === 0 && southTile === 1 && southwestTile === 1 && westTile === 1 && northwestTile === 1)
            {
                var w61:Wall6 = new Wall6();
                addChild(w61);
                pushTile(w61, cols, rows, 0); 
            }

            else if (northTile === 1 && northeastTile === 1 && eastTile === 1 && southeastTile === 1 && southTile === 1 && southwestTile === 0 && westTile === 1 && northwestTile === 1)
            {
                var w62:Wall6 = new Wall6();
                addChild(w62);
                pushTile(w62, cols, rows, 90); 
            }

            else if (northTile === 1 && northeastTile === 1 && eastTile === 1 && southeastTile === 1 && southTile === 1 && southwestTile === 1 && westTile === 1 && northwestTile === 0)
            {
                var w63:Wall6 = new Wall6();
                addChild(w63);
                pushTile(w63, cols, rows, 180);
            }

            else if (northTile === 1 && northeastTile === 0 && eastTile === 1 && southeastTile === 1 && southTile === 1 && southwestTile === 1 && westTile === 1 && northwestTile === 1)
            {
                var w64:Wall6 = new Wall6();
                addChild(w64);
                pushTile(w64, cols, rows, 270);
            }

            //  single wall tile

            else if (northTile === 0 && northeastTile === 0 && eastTile === 0 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 0 && northwestTile === 0)
            {
                var w5:Wall5 = new Wall5();
                addChild(w5);
                pushTile(w5, cols, rows, 0);
            }

            //  wall 3 walls

            else if (northTile === 0 && eastTile === 1 && southTile === 0 && westTile === 1)
            {
                var w3:Wall3 = new Wall3();
                addChild(w3);
                pushTile(w3, cols, rows, 0);
            }

            else if (northTile === 1 && eastTile === 0 && southTile === 1 && westTile === 0)
            {
                var w31:Wall3 = new Wall3();
                addChild(w31);
                pushTile(w31, cols, rows, 90);
            }

            //  wall 4 walls

            else if (northTile === 0 && eastTile === 0 && southTile === 1 && westTile === 0)
            {
                var w41:Wall4 = new Wall4();
                addChild(w41);
                pushTile(w41, cols, rows, 0);
            }

            else if (northTile === 1 && eastTile === 0 && southTile === 0 && westTile === 0)
            {
                var w42:Wall4 = new Wall4();
                addChild(w42);
                pushTile(w42, cols, rows, 180);
            }

            else if (northTile === 0 && northeastTile === 0 && eastTile === 1 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 0 && northwestTile === 0)
            {
                var w43:Wall4 = new Wall4();
                addChild(w43);
                pushTile(w43, cols, rows, 270);
            }

            else if (northTile === 0 && northeastTile === 0 && eastTile === 0 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 1 && northwestTile === 0)
            {
                var w44:Wall4 = new Wall4();
                addChild(w44);
                pushTile(w44, cols, rows, 90);
            }

            //  regular wall blocks

            else if (northTile === 1 && eastTile === 0 && southTile === 1 && westTile === 1)
            {
                var w11:Wall1 = new Wall1();
                addChild(w11);
                pushTile(w11, cols, rows, 90);
            }

            else if (northTile === 1 && eastTile === 1 && southTile === 1 && westTile === 0)
            {
                var w12:Wall1 = new Wall1();
                addChild(w12);
                pushTile(w12, cols, rows, 270);
            }

            else if (northTile === 0 && eastTile === 1 && southTile === 1 && westTile === 1)
            {
                var w13:Wall1 = new Wall1();
                addChild(w13);
                pushTile(w13, cols, rows, 0);
            }

            else if (northTile === 1 && eastTile === 1 && southTile === 0 && westTile === 1)
            {
                var w14:Wall1 = new Wall1();
                addChild(w14);
                pushTile(w14, cols, rows, 180);
            }

        }
        // debug === // trace('Top Left: ' + northwestTile + ' Top Middle: ' + northTile + ' Top Right: ' + northeastTile + ' Middle Left: ' + westTile + ' This: ' + levelNum[rows][cols] + ' Middle Right: ' + eastTile + ' Bottom Left: ' + southwestTile + ' Bottom Middle: ' + southTile + ' Bottom Right: ' + southeastTile);
    }
}

function pushTile(til:Object, tx:uint, ty:uint, degrees:uint):void
{
    til.x = tx * tileDimension;
    til.y = ty * tileDimension;
    if (degrees != 0) tileRotate(til, degrees);
}

function tileRotate(tile:Object, degrees:uint):void
{
    // http://www.flash-db.com/Board/index.php?topic=18625.0
    var midPoint:int = tileDimension/2;
    var point:Point=new Point(tile.x+midPoint, tile.y+midPoint);
    var m:Matrix=tile.transform.matrix;
    m.tx -= point.x;
    m.ty -= point.y;
    m.rotate (degrees*(Math.PI/180));
    m.tx += point.x;
    m.ty += point.y;
    tile.transform.matrix=m;
}

Básicamente, esto verifica cada mosaico a su alrededor, de izquierda a derecha, de arriba a abajo y supone que los mosaicos de borde son siempre 1. También me he tomado la libertad de exportar las imágenes como un archivo para usar como clave:

Azulejos de la pared

Esto está incompleto y probablemente sea una forma hacky de lograrlo, pero pensé que podría ser de algún beneficio.

Editar: captura de pantalla del resultado de ese código.

Resultado generado

Ben
fuente
1

Sugeriría algunas cosas:

  • no importa cuál sea el mosaico "central", ¿verdad? podría ser 2, pero si todos los demás son 1, mostraría 1?

  • solo importa cuáles son las esquinas, cuando hay una diferencia en los vecinos inmediatos de la parte superior o lateral. Si todos los vecinos inmediatos son 1 y una esquina es 2, mostraría 1.

  • Probablemente precalcularía todas las combinaciones posibles de vecinos, creando una matriz de 8 índices con los primeros cuatro indicando los valores de los vecinos superiores / inferiores, y el segundo indicando las diagonales:

bordes [N] [E] [S] [W] [NE] [SE] [SW] [NW] = cualquier desplazamiento en el sprite

entonces en su caso, [2] [2] [1] [1] [2] [2] [1] [1] = 4 (el 5to sprite).

en este caso, [1] [1] [1] [1] sería 1, [2] [2] [2] [2] sería 2, y el resto tendría que ser resuelto. Pero la búsqueda de un mosaico en particular sería trivial.

elijah
fuente