Mis tribulaciones sobre un motor isométrico

Avatar de Usuario
Chema
Mensajes: 2251
Registrado: 21 Jun 2012 20:13
Ubicación: Gijón
Agradecido : 1866 veces
Agradecimiento recibido: 596 veces
Contactar:

Mis tribulaciones sobre un motor isométrico

Mensajepor Chema » 12 Sep 2019 13:42

Como sabéis estoy siguiendo (con algo de retraso, pero ahí estamos) el curso de Mlake sobre programación del Amiga OCS en ensamblador (aquí). Para practicar me he puesto a construir un pequeño motor isométrico (así empecé programando en asm para el -0r1c así que me pareció una buena idea).

Tras montar mi kit de desarrollo cruzado con el Visual Code, vcc y vasm como puse aquí, me puse manos a la obra. Pues bien, abro este hilo con la intención de compartir con vosotros lo que vaya haciendo y pedir consejo e ideas. Sobre todo esto último. Tengo la impresión de que, a veces, estoy matando moscas a cañonazos, además de que llegará un momento en que necesite optimizar y empezarán mis dudas con el uso del blitter.

Os aviso. Esto es un -bRick

He tenido que sustituir en el código todos las sentencias if por si o me daba error al postear -nb

A ver, sin entrar en rollos, sabéis ya lo que son los juegos con perspectiva isométrica: básicamente un truco para hacer un falso 3D. En plan filmation como Knight Lore. Y si no, a la Wikipedia.

Primero las malas noticias. No hacemos 3D real (es decir no hay proyecciones ni escalados), así que no hay un método general que funcione bien. Es necesario imponer restricciones. La primera que vamos a poner es que todos los bloques sean del mismo tamaño.

Voy a mantenerlo todo lo más sencillo posible, así que queremos usar bloques como éste (con su máscara):
bloque.png
bloque.png (419 Bytes) Visto 856 veces

Lo he hecho de 32 pixels de ancho y de 8 pixels de alto (me gustan bajitos, pero eso es cosa mía). Como véis le he puesto una máscara, porque vamos a pintarlo como un BOB usando el blitter tal y como dice Mlake en su curso. De momento estoy usando 4 bitplanes y el modo entrelazado.

Para hacerlo todo muy simple, supongamos que queremos renderizar algo como esto:
suelo.png
suelo.png (22.03 KiB) Visto 856 veces


Veis que es una especie de suelo de 10x10 bloques (ocupa 320 pixels de ancho). Supongamos que el mapa es una matriz de 10x10 que contiene un 1 donde queremos poner un bloque y un 0 donde haya un hueco. Ahora son todo 1s. Supongamos que tenemos ya el código que usa el blitter para pintar un BOB en unas coordenadas de pantalla (x,y). Le pasamos el código del bob (un 1, por ejemplo) y la rutina sabe cuál es el gráfico y su máscara.

Para determinar la posición en pantalla de un bloque en unas coordenadas de mapa (i,j) podemos usar una función como ésta:

Código: Seleccionar todo

void isoToScreen(int i, int j, int *x, int *y){
    *x = (j - i) + (160-ancho_tile/2); 
    *y = (i + j) / 2 + (32+ alto_tile);
}

Así, sin optimizar ni nada. 160 es justo la mitad de la pantalla, 32 es la fila donde empezamos a pintar (podía ser otro valor), ancho_tile vale también 32 y alto_tile es justo la mitad del ancho (cosas de la perspectiva isométrica), es decir 16. Pero no le hagáis caso. El problema no es ahora mismo dónde va cada bloque, sino en qué orden los tenemos que ir pintando. Hay varias alternativas, pero la idea es ir de la esquina de arriba hacia abajo por filas. Algo así:
suelonum.png
suelonum.png (12.13 KiB) Visto 856 veces

Os pongo el código C que implementa este bucle de renderizado. Se basa en que pintamos 19 filas de bloques. Empezamos en mitad de la pantalla pintando un bloque. Bajamos el alto de un bloque, nos desplazamos la mitad del ancho a la derecha y pintamos dos. Después repetimos y pintamos 3 y así sucesivamente hasta llegar a la mitad (la fila 10), donde pintas 10 bloque y a partir de ahí todo al revés.

Código: Seleccionar todo

void RenderRoom(){
    int xx, xd, yd;
    int l, k, length, jo;
    int i, j;
    int tile_code;

    xx = 160 - (ancho_tile / 2);
    xd = xx;
    yd = 32 + alto_tile;
    l = 0;
    k = 0;
    length = 0;
    jo = 0;

    for (i = 0; i <= 18; i++){
        for (j = jo; j <= length; j++){
            // Get the tile to draw
            k = j;
            l = length + jo - j;       
            tile_code = map[k][l];
            si (tile_code)
                drawBlock(tile_code-1, xx, yd);
            xx +=  ancho_tile;
        }

       si (i < 9){
            length++;
            xd -= (ancho_tile / 2);
        }
        else{
            jo++;
            xd += (ancho_tile / 2);
        }
        xx = xd;
        yd += (alto_tile / 2);
   }
}

Hala, ya está. Además vamos calculando las posiciones (x,y) en pantalla, así que no necesitamos tirar de la función isoToScreen (ya os dije que, por ahora, ni caso). Por supuesto esto está sin optimizar. Se podría precalcular todo, almacenar los bloques ordenados así y con sus coordenadas en pantalla para posteriores renderizados y esas cosas. Tampoco estamos renderizando una escena que se salga por los límites de la pantalla ni nada. Pero por ahora es suficiente.

Pero así sólo tenemos el suelo, sin nada encima. El siguiente paso es poder añadir bloques sobre estos, es decir, más niveles. Esto es relativamente fácil. Basta con que nuestro mapa sea no solo una matriz de 10x10, sino que tenga una dimensión más. Podemos, entonces poner un bucle externo e ir pintando por niveles: primero el más bajo, luego el siguiente y así. Como las coordenadas y de pantalla crecen hacia abajo, para pintar un nivel encima de otro, hay que restar el alto de cada bloque (en este caso 8 pixels) multiplicado por el nivel que sea. El bucle queda:

Código: Seleccionar todo

void RenderRoom(){
    int xx, xd, yd;
    int l, k, length, jo, h;
    int i, j;
    int tile_code;

    // Go through all layers
    for (h = 0; h < num_layers; h++){

        xx = 160 - (ancho_tile / 2);
        xd = xx;
        yd = 32 + alto_tile;
        l = 0;
        k = 0;
        length = 0;
        jo = 0;

        for (i = 0; i <= 18; i++){
            for (j = jo; j <= length; j++){
                // Get the tile to draw
                k = j;
                l = length + jo - j;       
                tile_code = map[h][k][l];
                si (tile_code){
                    drawBlock(tile_code-1, xx, yd-h*layer_heigth);
                }
                xx +=  ancho_tile;
            }

            si (i < 9){
                length++;
                xd -= (ancho_tile / 2);
            }
            else{
                jo++;
                xd += (ancho_tile / 2);
            }
            xx = xd;
            yd += (alto_tile / 2);
        }
    }
}


De nuevo se podría optimizar, por supuesto. Os aseguro que esto funciona, pero sólo acabamos de empezar. Si os fijáis todos los bloques están como "atados" a posiciones fijas en una rejilla. Esto funciona bien para un escenario (imaginad el terreno y las construcciones en el juego Populous), pero fijo que vamos a querer montar sobre ese escenario personajes y objetos que no tengan esta restricción, y que puedan moverse suavemente pasando por posiciones intermedias y, por tanto, "pisar" más de una baldosa del suelo. Normalmente estos bloques, a los que llamaré "libres", tienen coordenadas en el mapa isométrico más "finas" o con más granularidad. Como si cada posición del mapa estuviese subdividida, en este caso, en 16 píxeles.

¿Y cómo, o mejor dicho cuándo pintamos estos bloques libres? Ahí está el primer problema. Necesitamos tener un punto de referencia (ancla o punto de anclaje) que nos represente su posición. Podríamos pensar que, una vez determinado éste, basta pintarlos cuando toque, o sea, cuando pintemos en el bucle interno esas coordenadas. Por ejemplo:

Código: Seleccionar todo

        for (i = 0; i <= 18; i++){
            for (j = jo; j <= length; j++){
                // Get the tile to draw
                k = j;
                l = length + jo - j;       
                tile_code = map3[h][k][l];
                si (tile_code){
                    drawBlock(tile_code-1, xx, yd-h*layer_heigth);
                }

                // Now plot the sprite
                si( ( sprite_k >>3 == h) && ( (sprite_i) >> 4 == l) && ( (sprite_j) >> 4 == k) )       
                    drawBlock(0, sprite_x, sprite_y - sprite_k);
               
                xx +=  ancho_tile;
            }


Es decir, si tenemos un bloque libre (lo he llamado sprite) en esta altura y cuya "ancla" está sobre la posición que estamos pintando (por eso se divide su coordenada "fina" por 16), lo dibujamos.

Pero esto no funciona. Para ilustrarlo pongamos que usamos como "ancla" una de las esquinas derechas del bloque (da igual la delantera o la trasera para este ejemplo). La situación que tendríamos sería:


esquinadcha-abnum.png
esquinadcha-abnum.png (12.25 KiB) Visto 856 veces


Como véis, el bloque se ha pintado después del de su izquierda, que debería de ocultarlo parcialmente, y tenemos un error de renderizado. Ah, vale, es que hemos elegido mal el punto de anclaje... pongamos, entonces, una esquina de la izquierda. Ese error desaparece, pero tenemos este otro:
esquinaizda-arrnum.png
esquinaizda-arrnum.png (10.03 KiB) Visto 856 veces


Ups. Pues tampoco funciona. Ahora se ha pintado antes del de su derecha, generando otro error ¿y cómo lo solucionamos?
En realidad deberíamos haberlo pintado entre la segunda y la tercera filas. Y ¿cómo se hace eso? pues con cuidado. Fijaos en esta imagen, que nos dice cuándo pintar:
Traslíneanum.png
Traslíneanum.png (14.67 KiB) Visto 856 veces


Es entre la fila 2 y la 3 (o 1 y 2, si empezáis a contar en 0), o sea que nuestro bucle puede pasar a:

Código: Seleccionar todo

void RenderRoom()
{
    int xx, xd, yd;
    int l, k, length, jo, h;
    int i, j;

    int tile_code;

    // Go though all layers
    for (h = 0; h < num_layers; h++){

        xx = 160 - (ancho_tile / 2);
        xd = xx;
        yd = 32 + alto_tile;
        l = 0;
        k = 0;
        length = 0;
        jo = 0;

        for (i = 0; i <= 18; i++){
            for (j = jo; j <= length; j++){
                // Get the tile to draw
                k = j;
                l = length + jo - j;       
                tile_code = map[h][k][l];
                si (tile_code)
                    drawBlock(tile_code-1, xx, yd-h*layer_heigth);
                xx +=  ancho_tile;
            }
            // Now plot the sprite
           si ( ((sprite_k >> 3)==h) && ( ( (sprite_i >> 4) + (sprite_j >> 4) ) == (l+k) ) )
                drawBlock(0, sprite_x, sprite_y - sprite_k);
           
            si (i < 9){
                length++;
                xd -= (ancho_tile / 2);
            }
            else{
                jo++;
                xd += (ancho_tile / 2);
            }
            xx = xd;
            yd += (alto_tile / 2);
        }
    }
}


O sea, acabamos de pintar una fila y miramos si nuestro bloque libre cumple que:
- Está en ese nivel (su coordenada fina de altura entre 8 coincide con la capa que estamos pintando) -> (sprite_k >> 3)==h
- Le corresponde esta fila a su "ancla" (la suma de sus coordenadas finas isométricas dividida por 16 coincide con la de la fila que estamos pintando) -> ( (sprite_i >> 4) + (sprite_j >> 4) ) == (l+k) )

Nadie en su sano juicio lo implementaría así. Lo suyo es poner una lista enlazada de bloques libres por cada línea y capa (por ejemplo) que se actualiza cuando estos bloques se mueven, pero para ilustrar lo que hay que hacer vale.

¿O sea que ya está?

Bueno, pues casi, pero no del todo. Este método tiene un problema: los bloques tienen una altura limitada. Si queremos que uno sea un panel o una pared de 32 o 40 píxeles de altura, o bien lo dividimos en bloques, o aparecerán errores de renderizado. Me temo que igual que si nuestro bloque "libre" se mueve verticalmente. Esto, en ocasiones, es una limitación muy fuerte. Nos vendría bien poder pintar una pared alta de piedra y poner a media altura una antorcha en algunos casos, o una ventana. Ahorra memoria, y mejora la eficiencia (hay menos operaciones de blitter).

Pero para ello hay que usar otro método de dibujado, lo que se llama "rendering en columnas o apilado", donde pintamos un bloque y todos los que están encima. Pero en ese caso todo el trabajo se viene abajo porque ocurre esto:
Traslínea-stacked.png
Traslínea-stacked.png (22.04 KiB) Visto 856 veces


¿Véis como al pintar el suelo se superpone al bloque que, en teoría, está sobre él? Podéis hacer las variaciones que queráis, pero me parece que esto no tiene solución fácil. ¡Si alguien la conoce que me la diga!

Cuando desarrollé Space:1999 me encontré con el mismo problema y tuve que buscarme la vida para usar las máscaras de cada objeto para generar una máscara adicional con la que evitar que se pintase sobre ellos en estas circunstancias. Se trata de generar una máscara que indique, para los siguientes blits, dónde no deben estropear el fondo.

No es sencillo, pero no se me ocurre otro método. De hecho ya ni me acuerdo cómo lo tenía hecho, pero involucra varios blits de más.

Si alguien tiene una solución simple... yo no la he encontrado por ningún sitio.

Avatar de Usuario
explorer
Mensajes: 315
Registrado: 10 Ene 2016 18:43
Ubicación: Valladolid, España
Agradecido : 7 veces
Agradecimiento recibido: 282 veces
Contactar:

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor explorer » 12 Sep 2019 19:55

En Internet puedes encontrar descripciones del funcionamiento del motor Filmation que se usó en los Spectrum (sección Layout).

Naturalmente, está muy optimizado: una habitación tiene fondos y objetos. Los fondos son el suelo, las paredes y las puertas. Los objetos pueden ser fijos o móviles. Pero todos ellos lo importante es que tienen coordenadas (x,y,z), en píxeles (con el Spectrum, las coordenadas no superan el valor de 256).

Lo que no se explica ahí es el cómo se pinta.

En el caso del suelo, la forma más sencilla de pintado es filas×columnas. Solo es recorrer un doble bucle, muy parecido a como lo tienes hecho, pero en vez de atrás-adelante, es de derecha a izquierda (o de izquierda a derecha, según donde empieces a dibujar).

En el resto de objetos, el pintado se puede hacer así:

Cada objeto tiene sus coordenadas (x,y,z). Los tenemos almacenados en un vector.

Ordenamos el vector, de menos a más, por sus coordenadas Z.
Hacemos lo mismo para la coordenada Y.
Hacemos lo mismo para la coordenada X.

El proceso de pintado siempre empieza por los que tienen valores X, Y y Z más bajos.

De esta manera, los objetos que están "más atrás" (X e Y bajas) siempre se pintarán antes, independientemente de su altura (Z).

Ejemplo: tenemos dos objetos en escena:
Selección_067.png
Selección_067.png (71.42 KiB) Visto 800 veces


Pero con valores X,Y,Z distintos, aún estando la bola en el mismo sitio en pantalla, se ha de pintar en un momento diferente:
Selección_069.png
Selección_069.png (69.23 KiB) Visto 800 veces

Avatar de Usuario
explorer
Mensajes: 315
Registrado: 10 Ene 2016 18:43
Ubicación: Valladolid, España
Agradecido : 7 veces
Agradecimiento recibido: 282 veces
Contactar:

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor explorer » 13 Sep 2019 02:05

Se me ha olvidado algo importante: hay que recordar que en un videojuego, lo mismo que en una película, no siempre es real lo que estamos viendo.

Quiero decir que si en un decorado hay elementos que están pegados al borde, es obvio que todos los objetos móviles pasarán por delante de ellos (o por detrás), así que esos objetos, en realidad, formarán parte del decorado de las paredes y el suelo, y por lo tanto no los consideramos a la hora de ejecutar el proceso de redibujado (o los tenemos que redibujar siempre).

El proceso más complicado, creo yo, es redibujar solo las partes que se han modificado. Hace falta una rutina que diga qué intervalos de X,Y y Z son necesarios repintar dado un rectángulo de pantalla "estropeado" (a veces es más sencillo partir de ahí que no calcular los volúmenes de colisión en 3D de un objeto con todos los demás).

Avatar de Usuario
Kyp
Mensajes: 406
Registrado: 03 Oct 2013 17:13
Agradecido : 19 veces
Agradecimiento recibido: 84 veces

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor Kyp » 13 Sep 2019 09:51

Muy interesante. Hace tiempo empecé un remake del Knight Lore y me quedé atascado también en el punto en que hay que pintar objetos que están entre medias de varios bloques. Al final lo dejé aparcado. Seguiré con mucho interés este hilo -thanks

Avatar de Usuario
Chema
Mensajes: 2251
Registrado: 21 Jun 2012 20:13
Ubicación: Gijón
Agradecido : 1866 veces
Agradecimiento recibido: 596 veces
Contactar:

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor Chema » 13 Sep 2019 11:12

Mil gracias a todos por los aportes, aquí y los que me han llegado a través de otros medios. En efecto el problema es siempre el de los bloques que ocupan posiciones intermedias. Si te fijas, Explorer, el bucle que puse lo que hace en realidad es dibujar los bloques del mapa (los fijos) desde el más alejado del punto de vista del jugador al más cercano. Es cierto que se pueden usar otros métodos, pero no recuerdo dónde leí que éste es el que menos problemas da luego.

Vamos que si cambias la función drawBlock(codigo, x,y) por Lista.Add(codigo,x,y) generas una lista ordenada sin comparaciones y, además, con las coordenadas de pantalla correctas. Por supuesto se pueden tratar escenarios más grandes de los que solo se ve un trozo, pero por simplificar lo dejé así.

También das en el clavo con lo de las optimizaciones en los juegos de Spectrum. Allí la rejilla es, normalmente, de 8x8 (para 256 pixeles de ancho) y las paredes son fondo, como el suelo, así que sólo se tratan unos pocos bloques y las puertas, que pueden ser objetos especiales.

Y, por supuesto, con lo de actualizar la pantalla sólo donde cambia. Incluso el Amiga no va a poder hacer todos esos blits (solo el suelo lleva 100) a un framerate decente. Quiero implementar un algoritmo de dirty rectangles para actualizar sólo lo que hace falta. Algo así, pero sólo con una región, hice en Space:1999. Pero no quería meterme en eso ahora, hasta dar con un algoritmo de dibujado correcto y simple. También en Space:1999 me encontré con este problema y, como digo, hice algo que se me antoja complicado y lento y quería ver si (como creo) es una chapuza y hay opciones mejores.

Tal y como lo dejé en el último bucle, todo funciona bien, excepto cuando empecemos a querer mover objetos y éstos se pueden quedar entre dos capas. Hay, creo recordar, dos opciones:
1- Pintas por capas, primero el suelo completo, luego la capa con lo que está encima a primera altura. Y así. Cualquier objeto entre dos capas haya que dividirlo en dos bloques o habrá problemas. Además ningún objeto puede ser más alto que la altura de la capa (que no tiene por qué ser constante, por cierto: es típico tener suelo y, a la misma altura, sombra y luego una capa mucho más alta con objetos y otra encima para, digamos tejados).
2- Pintas apilado, o sea, coges una posición y pintas hacia arriba todos los bloques, pasas a la siguiente y lo mismo y así. Con esto puedes tener objetos altos, te olvidas de problemas como los anteriores y puedes tener decoraciones (como ventanas o antorchas o signos que poner en las paredes). Pero aparecen errores de renderizado como el de la última imagen que puse (el suelo está en la primera capa y el bloque en la segunda).

El segundo método es el que usé en Space:1999, como digo, y permite hacer cosas muy chulas (como dejar cosas sobre una silla o en una estantería), pero obliga a ir corrigiendo esos errores de renderizado. El proceso que implementé es una chapuza lentísima, pero no se me ocurre una manera de que no aparezcan o de hacerlo más rápidamente.

descarga.png
descarga.png (9.51 KiB) Visto 731 veces


A ver si busco un hueco para poner algún ejemplo con gráficos más ilustrativos.

(Como escribo estos ladrillos en huecos y a toda prisa, me explico fatal... -banghead )

Avatar de Usuario
Jinks
Mensajes: 1619
Registrado: 09 Oct 2013 16:47
Agradecido : 140 veces
Agradecimiento recibido: 167 veces
Contactar:

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor Jinks » 13 Sep 2019 11:35

Quizás lo voy a decir sea una obviedad y no solucione nada.
O puede que alguno de los anteriores bucles ya lo implemente de forma directa o indirecta.
O que se haya dicho antes y yo no me haya enterado.

El orden de pintado debería ser del objeto más lejano al más cercano.

Ale, ya lo he soltado.

¿Lejano o cercano respecto a qué? Al observador. Pero es difícil situar al observador porque al no ser 3D real el observador no es un punto. Sería, ¿un plano? Quizás un plano paralelo a la pantalla. (sin tener en cuenta que al no ser paralelo al "suelo" ambos planos temrinarían cortándose)

En el caso de la cuadrícula del suelo, el objeto más lejano sería el que en el "rombo" resultante queda dibujado en la esquina superior [1]. Con la altura la distancia al observador se reduce, porque como estamos viendo la cara superior de las baldosas, significa que el observador está por encima del suelo (ve el "suelo", no el "techo", que sería la cara inferior de las baldosas). Por lo tanto, los objetos más altos quedan más cerca al observador que los más bajos. Y por ello, una baldosa [2] apoyada justo sobre la de la esquina superior, es más cercana y habría que pintarla después.

Y a una determinada altura, las baldosas que en el dibujo quedan por debajo [3 y 4], también son más cercanas. Por eso la fila de dos baldosas que "tocan" a la de la esquina superior se dibujan después de ella.

Ahora sabemos que la baldosa [2] y las baldosas [3 y 4] están más cerca del observador que la baldosa [1]. ¿En qué orden las dibujo? Pues suponiendo que todos los bloques van ocupar un mismo tamaño, la baldosa [2] está una unidad más cerca en una dimensión que la baldosa [1], y las baldosas [3 y 4] también están una unidad más cerca en una dimensión que la baldosa [1]. Por lo tanto las 3 baldosas [2, 3 y 4] están igual de cerca y en principio, a igual distancia, daría igual el orden en que se pintan. Pero sería cuestión de probar. Y ojo, que esto valdría si los bloques fueran igual de altos que de anchos, siendo "bajitos" no sé si habría que ajustar algo.

Con los objetos en posiciones intermedias: Pues supongo que habría que asignarles coordenadas intermedias y averiguar la distancia al observador de esas coordenadas intermedias y dibujarlos como cualquier otro objeto: Después de todos los objetos más lejanos, y antes de todos los objetos más cercanos. Ojo, que además de posiciones intermedias en un mismo plano, también podrían situarse en alturas intermedias.

EDITO: Los números que he ido dando a las baldosas no son los que aparecen en tus imágenes, se los he ido asignando en el orden en el que las he ido mencionando yo.

dragonet80
Mensajes: 285
Registrado: 28 Nov 2017 12:36
Agradecido : 158 veces
Agradecimiento recibido: 90 veces

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor dragonet80 » 13 Sep 2019 17:52

Interesante hilo.

Chema, según como has enumerado los bloques, cogiendo las filas según la distancia al observador fuera de la pantalla y empezando por el mas lejano o "profundo" el error que veo en el primer ejemplo es que pintes el 4 después del 3. El 4 debería ser el 3 i el 3 debería ser el 4. Respecto a tu vista desde fuera de la pantalla el 4 estaría en la fila 2,5.
Y en el último ejemplo los de columnas superiores siempre se han de dibujar antes que los de columnas inferiores.
Resumiendo, el pintado es de mas lejos a mas cerca y de mas abajo a mas arriba.

Si siempre colocas objetos en filas y columnas "enteros" no habrá problemas. Si los mueves, por tanto usas "decimales", es cuando te vienen esos problemas. Tendrías que tener un método que te compuebe cada bloque según sus coordenadas si está en una fila mas cercana o mas lejana (o para optimizar, comprobarlo solo en los que no sean "enteros").

Avatar de Usuario
Mlake
Mensajes: 75
Registrado: 27 Mar 2019 19:54
Agradecido : 34 veces
Agradecimiento recibido: 164 veces

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor Mlake » 13 Sep 2019 18:12

Con tanto numero me he perdido.... :D

¿Seria muy coñazo hacer un dual playfield?
Un plano para el "fondo" y otro con los "objetos intermedios" (muebles, puertas...) y sprites que pueden pasar sobre/entre/bajo los playfield.

O cambiar los bobs entre playfields para poder pasar por delante/detras de los "muebles".

O incluso hacer los muebles con sprites y jugar con la prioridad.

Avatar de Usuario
kikems
Mensajes: 3392
Registrado: 30 May 2013 19:23
Agradecido : 978 veces
Agradecimiento recibido: 1350 veces

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor kikems » 13 Sep 2019 19:39

Mlake escribió:Con tanto numero me he perdido.... :D

¿Seria muy coñazo hacer un dual playfield?
Un plano para el "fondo" y otro con los "objetos intermedios" (muebles, puertas...) y sprites que pueden pasar sobre/entre/bajo los playfield.

O cambiar los bobs entre playfields para poder pasar por delante/detras de los "muebles".

O incluso hacer los muebles con sprites y jugar con la prioridad.


Has creado un monstruo. -thumbup . Chema ahora es tu Godzilla particular.

Avatar de Usuario
minter
Mensajes: 2534
Registrado: 22 Jul 2014 18:51
Agradecido : 2396 veces
Agradecimiento recibido: 1043 veces

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor minter » 13 Sep 2019 20:19

Chema estaba esperando el invierno para picar código. :)
Seguro que está preparando un por de un juego de Oric. Aunque no se si el Amiga tiene suficuente rendimiento para mover un juego de Oric. -507

jltursan
Mensajes: 2588
Registrado: 20 Sep 2011 13:59
Agradecido : 180 veces
Agradecimiento recibido: 501 veces

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor jltursan » 13 Sep 2019 21:06

Aquí hay una buena lectura en relación a la técnica "Filmation": http://bannalia.blogspot.com/2008/02/filmation-math.html

Avatar de Usuario
Kyp
Mensajes: 406
Registrado: 03 Oct 2013 17:13
Agradecido : 19 veces
Agradecimiento recibido: 84 veces

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor Kyp » 14 Sep 2019 17:33

jltursan escribió:Aquí hay una buena lectura en relación a la técnica "Filmation": http://bannalia.blogspot.com/2008/02/filmation-math.html

Muy interesante éste enlace. Sobre todo cuando cuenta que Knight Lore no tiene en cuenta esos casos en que la ordenación es imposible -rofl
Me lo guardo para cuando retome el tema.

Avatar de Usuario
Chema
Mensajes: 2251
Registrado: 21 Jun 2012 20:13
Ubicación: Gijón
Agradecido : 1866 veces
Agradecimiento recibido: 596 veces
Contactar:

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor Chema » 15 Sep 2019 23:54

Este fin de semana no he podido hacer nada :( . Lo que decís es muy interesante. Por aclarar un poco, el último bucle que puse lo que hace es precisamente intentar determinar el orden correcto de dibujado. Por supuesto, es trampa. Se trata de evitar hacer proyecciones y operaciones 3D complejas, así que siempre habrá trampa.

Y funciona bien con algunas restricciones, en este caso que los bloques tengan todos un tamaño máximo y poniendo cuidado con bloques que queden a media altura entre dos capas.

En cuanto pueda pongo algún ejemplo de en qué casos funciona y en qué casos no y explico el por qué de mi última imagen (con un error de renderizado en el suelo), que solo ha confundido. Y cómo lo hice en Space:1999, que no tiene errores de renderizado (o ninguno que yo haya visto)-

Lo de Mlake y los trucos de dual playfield, sprites y esas cosas, ya para cuando yo aprenda más... ahora me queda grande XD

Avatar de Usuario
explorer
Mensajes: 315
Registrado: 10 Ene 2016 18:43
Ubicación: Valladolid, España
Agradecido : 7 veces
Agradecimiento recibido: 282 veces
Contactar:

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor explorer » 17 Sep 2019 03:11

Analizando el código del Knight Lore, este es el procedimiento que utiliza para hacer el pintado de los objetos.

(Esto es muy largo. Saltar a la subrutina calc_display_order_and_render).

Código: Seleccionar todo

Esta subrutina se llama desde el bucle principal del juego.
Se encarga de hacer una lista de los objetos que hay que pintar en pantalla.

RAM:CE62 list_objects_to_draw:
RAM:CE62        push    ix
RAM:CE64
RAM:CE64        ld      b, #40                 ; máximo de objetos posibles en pantalla
RAM:CE66        ld      de, #32                ; tamaño de cada objeto = 32 bytes
RAM:CE69        ld      ix, #graphic_objs_tbl  ; base de la tabla de objetos
RAM:CE6D        ld      hl, #objects_to_draw   ; base de la tabla de los objetos a pintar
RAM:CE70        ld      c, #0                  ; índice a cada objeto
RAM:CE72
RAM:CE72 loc_CE72:
RAM:CE72        ld      a, 0(ix)               ; número de objeto
RAM:CE75        and     a                      ; ¿es nulo?
RAM:CE76        jr      Z, loc_CE80            ; sí, saltar a siguiente
RAM:CE78
RAM:CE78        bit     4, 7(ix)               ; ¿está puesta la bandera de dibujar?
RAM:CE7C        jr      Z, loc_CE80            ; no, saltar
RAM:CE7E
RAM:CE7E        ld      (hl), c                ; añadir el índice del objeto a la lista
RAM:CE7F        inc     hl                     ; puntero a siguiente dirección
RAM:CE80
RAM:CE80 loc_CE80:
RAM:CE80        inc     c                      ; siguiente índice de objeto
RAM:CE81        add     ix, de                 ; siguiente objeto en la tabla
RAM:CE83        djnz    loc_CE72               ; bucle por todos los objetos
RAM:CE83
RAM:CE85        ld      a, #0xFF
RAM:CE87        ld      (hl), a                ; bandera de fin de lista
RAM:CE88
RAM:CE88        pop     ix
RAM:CE8A        ret

Es decir, por cada pantalla tenemos una tabla de objetos inicial (graphic_objs_tbl). A medida que pasa el tiempo,
algunos de esos objetos desaparecen, por lo que su bandera de dibujado cambia. Aquellos que sí que hay que pintar
quedan en la tabla de objetos a pintar (objects_to_draw).


A continuación se llama a esta subrutina, que es la que controla el pintado.

RAM:D59F render_dynamic_objects:
RAM:D59F        xor     a
RAM:D5A0        ld      (objs_wiped_cnt), a           ; contador de objetos limpiados = 0
RAM:D5A3        push    ix
RAM:D5A5
RAM:D5A5        ld      a, (render_status_info)       ; estado actual del pintado (0:sí se pinto la pantalla antes. 1: no)
RAM:D5A8        and     a                             ; ¿es el primer dibujado?
RAM:D5A9        jp      NZ, loc_D653                  ; sí, no es necesario limpiar nada; ir a pintado
RAM:D5AC

Aquí comienza un proceso de "limpieza" de la pantalla.

RAM:D5AC        ld      hl, #objects_to_draw          ; 48 bytes
RAM:D5AF        ld      (tmp_objects_to_draw), hl     ; almacenamos el puntero en un espacio temporal
RAM:D5B2
RAM:D5B2 wipe_next_object:
RAM:D5B2        ld      hl, (tmp_objects_to_draw)     ; recuperamos el puntero al objeto
RAM:D5B5        ld      a, (hl)                       ; índice de objeto
RAM:D5B6        inc     hl                            ; puntero a siguiente objeto
RAM:D5B7        ld      (tmp_objects_to_draw), hl     ; y lo guardamos
RAM:D5BA        cp      #0xFF                         ; ¿fin de la lista?
RAM:D5BC        jp      Z, loc_D653                   ; sí, saltar a pintado
RAM:D5BF
RAM:D5BF        call    get_ptr_object                ; obtenemos puntero a la información de ese objeto en hl
RAM:D5C2
RAM:D5C2        push    hl                            ; la pasamos a ix como base de indexación de sus datos
RAM:D5C3        pop     ix
RAM:D5C5
RAM:D5C5        bit     5, 7(ix)                      ; ¿el objeto se ha limpiado de pantalla?
RAM:D5C9        jr      Z, wipe_next_object           ; sí, pasa al siguiente
RAM:D5CB
RAM:D5CB        res     5, 7(ix)                      ; indicamos que se ha borrado de pantalla (es lo que vamos a hacer ahora)

Vamos a comprobar el "desplazamiento" del objeto por la pantalla, para quedarnos con las posiciones del rectángulo más grande que contenga
tanto su posición actual como la anterior.

Primero con la coordenada X

RAM:D5CF        ld      a, 26(ix)                     ; valor actual de posición objeto pixel X en pantalla (X_actual)
RAM:D5D2        sub     30(ix)                        ; valor anterior (X_anterior)
RAM:D5D5        jp      C, loc_D649                   ; si X_anterior > X_actual, c = X_actual
RAM:D5D8        ld      c, 30(ix)                     ; si no,                    c = X_anterior (es decir, nos quedamos con la X menor)
RAM:D5DB
RAM:D5DB loc_D5DB:
RAM:D5DB        ld      a, 30(ix)                     ; a = X_anterior
RAM:D5DE        rrca                                  ; a = a / 8
RAM:D5DF        rrca
RAM:D5E0        rrca
RAM:D5E1        and     #0x1F                         ; valor byte pixel X_anterior
RAM:D5E3        add     a, 28(ix)                     ; + valor ancho anterior (en bytes)
RAM:D5E6
RAM:D5E6        ld      e, a                          ; e = byte pixel X_anterior + ancho anterior
RAM:D5E7
RAM:D5E7        ld      a, 26(ix)                     ; a = X_actual
RAM:D5EA        rrca                                  ; a = a / 8
RAM:D5EB        rrca
RAM:D5EC        rrca
RAM:D5ED        and     #0x1F                         ; valor byte pixel X_actual
RAM:D5EF        add     a, 24(ix)                     ; + valor ancho actual (en bytes)
RAM:D5F2
RAM:D5F2        cp      e                             ; comparar las posiciones
RAM:D5F3        jr      C, loc_D5F6                   ; para quedarnos con la X mayor
RAM:D5F5        ld      e, a
RAM:D5F6
RAM:D5F6 loc_D5F6:
RAM:D5F6        ld      a, c                          ; a = X menor de la posición actual y anterior
RAM:D5F7        rrca                                  ; a = a / 8
RAM:D5F8        rrca
RAM:D5F9        rrca
RAM:D5FA        and     #0x1F                         ; byte correspondiente a ese X
RAM:D5FC        ld      b, a                          ; b = a
RAM:D5FD
RAM:D5FD        ld      a, e                          ; a = X mayor de la posición actual y anterior + anchos
RAM:D5FE        sub     b
RAM:D5FF        ld      h, a                          ; h = número de bytes a limpiar

Ahora un proceso similar para la coordenada Y.

RAM:D600        ld      a, 27(ix)                     ; valor pixel Y_actual
RAM:D603        sub     31(ix)                        ; ¿Y_actual < Y_anterior?
RAM:D606        jr      C, loc_D64E                   ; sí, b = Y_actual
RAM:D608        ld      b, 31(ix)                     ; no, b = Y_anterior  (o sea, b = menor de Y_actual e Y_anterior, o sea, la más alta)
RAM:D60B
RAM:D60B loc_D60B:
RAM:D60B        ld      a, 31(ix)                     ; a = Y_anterior
RAM:D60E        add     a, 29(ix)                     ; a = a + altura anterior (en líneas)
RAM:D611        ld      e, a                          ; e = a
RAM:D612
RAM:D612        ld      a, 27(ix)                     ; a = Y_actual
RAM:D615        add     a, 25(ix)                     ; a = a + altura actual (en líneas)
RAM:D618        cp      e                             ; ¿cuál es la posición más baja en pantalla?
RAM:D619        jr      NC, loc_D61C                  ; al final, a = tiene la posición más baja
RAM:D61B        ld      a, e
RAM:D61C
RAM:D61C loc_D61C:
RAM:D61C        sub     b
RAM:D61D        ld      l, a                          ; l = posición más baja - posición más alta = altura del rectángulo
RAM:D61E
RAM:D61E        ld      a, b                          ; a = Y más alta en pantalla
RAM:D61F        cp      #192                          ; ¿está por debajo de la pantalla? Es decir, el objeto está fuera de pantalla
RAM:D621        jr      NC, wipe_next_object          ; sí, no hay que limpiar nada, pasamos al siguiente objeto
RAM:D623
RAM:D623        add     a, l                          ; a = a + altura del rectángulo = Y más baja
RAM:D624        sub     #192                          ; todas estas líneas, para calcular la Y dentro de pantalla
RAM:D626        jr      C, loc_D62C                   ; con lo cómodo que sería poner l = 192
RAM:D628        neg
RAM:D62A        add     a, l
RAM:D62B        ld      l, a

Terminamos aquí con
h = ancho (en bytes)
l = alto (en líneas) del rectángulo
b = Y más pequeño
c = X más pequeño (o sea, bc contiene las coordenadas de la posición más arriba a la derecha del rectángulo

RAM:D62C
RAM:D62C loc_D62C:
RAM:D62C        call    calc_vram_addr                ; calcula la dirección en pantalla a partir de bc, dejando la dirección en de
RAM:D62F        call    calc_vidbuf_addr              ; lo mismo para el búfer secundario, quedando la dirección en bc
RAM:D632
RAM:D632        ld      a, l                          ; hl <-> bc
RAM:D633        ld      l, c
RAM:D634        ld      c, a
RAM:D635        ld      a, h
RAM:D636        ld      h, b
RAM:D637        ld      b, a
RAM:D638
RAM:D638        ld      a, (objs_wiped_cnt)           ; incrementamos el número de objetos a borrar
RAM:D63B        inc     a
RAM:D63C        ld      (objs_wiped_cnt), a
RAM:D63F
RAM:D63F                                              ; guardamos en la pila, los datos de cada rectángulo
RAM:D63F        push    bc                            ; tamaño del rectángulo en bytes y líneas; b = ancho, c = alto
RAM:D640        push    de                            ; dirección en la memoria de vídeo
RAM:D641        push    hl                            ; dirección dentro del búfer de memoria
RAM:D642
RAM:D642        xor     a                             ; vaciar el sprite
RAM:D643        call    fill_window                   ; rellena un rectángulo a partir de hl
RAM:D646
RAM:D646        jp      wipe_next_object
RAM:D649 ; -----------------------------------------------------------------
RAM:D649 ; Funciones auxiliares para los cálculos de más arriba
RAM:D649 loc_D649:                                    ; pixel X
RAM:D649        ld      c, 26(ix)
RAM:D64C        jr      loc_D5DB
RAM:D64E ; -----------------------------------------------------------------
RAM:D64E
RAM:D64E loc_D64E:                                    ; pixel Y
RAM:D64E        ld      b, 27(ix)
RAM:D651        jr      loc_D60B
RAM:D653 ; -----------------------------------------------------------------
RAM:D653

Hasta ahora, lo que ha hecho es borrar (limpiar) las zonas rectangulares correspondientes al movimiento de los sprites, en el búfer secundario.

Ahora es cuando nos acercamos a lo interesante. Redibujamos la pantalla.

RAM:D653 loc_D653:
RAM:D653        call    calc_display_order_and_render ; se ordenan los objetos de atrás-adelante y se pintan en el búfer secundario
RAM:D656        call    print_sun_moon                ; se pinta el sol o la luna
RAM:D659        call    display_objects_carried       ; se pinta la lista de objetos que se portan
RAM:D65C
RAM:D65C        ld      hl, #objs_wiped_cnt           ; número de objetos que hay que borrar
RAM:D65F        ld      a, (rendered_objs_cnt)
RAM:D662        add     a, (hl)                       ; y los sumamos a los objetos a presentar (esto sirve más adelante para calcular los bucles de retardo en el bucle principal)
RAM:D663        ld      (rendered_objs_cnt), a

Ahora es el momento de copiar todos los rectángulos (con sus datos almacenados en la pila) desde el búfer a la memoria de vídeo.


RAM:D666
RAM:D666 loc_D666:
RAM:D666        ld      hl, #objs_wiped_cnt           ; contador de rectángulos
RAM:D669        ld      a, (hl)
RAM:D66A        and     a                             ; ¿se han copiado todos?
RAM:D66B        jr      Z, loc_D679                   ; sí, salimos
RAM:D66D        dec     (hl)                          ; no, descontamos uno
RAM:D66E
RAM:D66E        pop     hl                            ; fuente
RAM:D66F        pop     de                            ; destino
RAM:D670        pop     bc                            ; tamaño
RAM:D671        ld      a, b                          ; preparamos los datos para el ldir de dentro de blit_to_screen
RAM:D672        ld      b, c                          ; líneas
RAM:D673        ld      c, a                          ; bytes/línea
RAM:D674        call    blit_to_screen                ; copiar de búfer a vídeo RAM
RAM:D677        jr      loc_D666                      ; loop
RAM:D679 ; -----------------------------------------------------------------
RAM:D679
RAM:D679 loc_D679:
RAM:D679        pop     ix
RAM:D67B        ret

Aquí está el meollo de la cuestión: crea la lista de objetos a pintar en pantalla.
El proceso es comparar cada objeto con todos los demás siguiendo una ordenación de burbuja.
Cuando se encuentran dos objetos coincidentes (en algunas combinaciones de dimensiones, no en todas), se almacenan los objetos "más profundos" en una lista temporal.
Esa lista temporal tiene un máximo de 8 entradas (se espera entonces que el máximo de objetos coincidentes en el mismo espacio es de 8).
Los objetos se van colocando de atrás hacia adelante.
Si se encuentra un objeto por detrás de los demás, se pinta al búfer secundario.

RAM:CEBB calc_display_order_and_render:
RAM:CEBB        xor     a                             ; ponemos a cero el contador de objetos pintados
RAM:CEBC        ld      (rendered_objs_cnt), a
RAM:CEBF        push    ix
RAM:CEC1        push    iy
RAM:CEC3
RAM:CEC3 process_remaining_objs:
RAM:CEC3        ld      de, #objects_to_draw          ; tabla de objetos que sí que hay que pintar
RAM:CEC6
RAM:CEC6 loc_CEC6:
RAM:CEC6        ld      a, (de)                       ; número de objeto
RAM:CEC7        inc     de                            ; apuntar a siguiente objeto
RAM:CEC8        cp      #0xFF                         ; ¿fin de lista?
RAM:CECA        jp      Z, render_done                ; sí, salimos
RAM:CECD        bit     7, a                          ; ¿ya está pintado?
RAM:CECF        jr      NZ, loc_CEC6                  ; sí, pasamos al siguiente
RAM:CED1
RAM:CED1        call    get_ptr_object                ; hl = puntero a tabla de descripción genérica del objeto 1
RAM:CED4        ld      (render_obj_1), de            ; guardamos puntero en sitio temporal 1
RAM:CED8        push    hl
RAM:CED9        pop     ix                            ; ix = hl, base de datos del objeto 1
RAM:CEDB
RAM:CEDB loc_CEDB:                                    ; bucle para todos los objetos
RAM:CEDB        ld      a, (de)                       ; objeto a pintar
RAM:CEDC        inc     de                            ; siguiente objeto
RAM:CEDD        cp      #0xFF                         ; ¿fin de lista?
RAM:CEDF        jp      Z, render_obj_no1             ; sí, terminamos el bucle interior
RAM:CEE2        bit     7, a                          ; ¿ya está pintado?
RAM:CEE4        jr      NZ, loc_CEDB                  ; sí, pasamos al siguiente
RAM:CEE6
RAM:CEE6        call    get_ptr_object                ; hl = puntero a tabla de descripción genérica del objeto 2
RAM:CEE9        ld      (render_obj_2), de            ; guardamos puntero en sitio temporal 2
RAM:CEED
RAM:CEED        push    hl
RAM:CEEE        pop     iy                            ; iy = hl, base de datos del objeto 2
RAM:CEF0        push    ix
RAM:CEF2        pop     bc                            ; bc = ix, base de datos del objeto 1
RAM:CEF3
RAM:CEF3        and     a                             ;
RAM:CEF4        sbc     hl, bc                        ; ¿son el mismo objeto?
RAM:CEF6        jr      Z, loc_CEDB                   ; sí, pasamos al siguiente objeto
RAM:CEF8
RAM:CEF8        ld      c, #0                         ; c marcará la relación de colisión 3D entre los dos objetos
RAM:CEFA
RAM:CEFA        ld      a, 3(iy)                      ; Z2
RAM:CEFD        add     a, 6(iy)                      ; sumar altura (H2)
RAM:CF00        ld      l, a
RAM:CF01        ld      a, 3(ix)                      ; Z1
RAM:CF04        sub     l                             ; Z1-(Z2+H2)
RAM:CF05        jr      NC, loc_CF16                  ; no coinciden, (C += 0)
RAM:CF07
RAM:CF07        ld      a, 3(ix)                      ; Z1
RAM:CF0A        add     a, 6(ix)                      ; add H1
RAM:CF0D        ld      l, a
RAM:CF0E        ld      a, 3(iy)                      ; Z2
RAM:CF11        sub     l                             ; Z2-(Z1+H1)
RAM:CF12        jr      C, loc_CF15                   ;    coinciden (C += 1)
RAM:CF14        inc     c                             ; no coinciden (C += 2)
RAM:CF15 loc_CF15:
RAM:CF15        inc     c
RAM:CF16
RAM:CF16 loc_CF16:
RAM:CF16        ld      a, 2(iy)                      ; Y2
RAM:CF19        add     a, 5(iy)                      ; sumar profundidad (D2)
RAM:CF1C        ld      l, a
RAM:CF1D        ld      a, 2(ix)                      ; Y1
RAM:CF20        sub     5(ix)                         ; Y1-D1
RAM:CF23        sub     l                             ; Y1-D1-(Y2+D2)
RAM:CF24        jr      NC, loc_CF3C                  ; no coinciden (C += 0)
RAM:CF26        ld      a, 2(ix)                      ; Y1
RAM:CF29        add     a, 5(ix)                      ; sumar profundidad (D1)
RAM:CF2C        ld      l, a
RAM:CF2D        ld      a, 2(iy)                      ; Y2
RAM:CF30        sub     5(iy)                         ; Y2-D2
RAM:CF33        sub     l                             ; Y2-D2-(Y1+D1)
RAM:CF34        ld      a, c
RAM:CF35        jr      C, loc_CF39                   ;    coinciden (C += 3)
RAM:CF37        add     a, #3                         ; no coinciden (C += 6)
RAM:CF39 loc_CF39:
RAM:CF39        add     a, #3
RAM:CF3B        ld      c, a
RAM:CF3C
RAM:CF3C loc_CF3C:
RAM:CF3C        ld      a, 1(iy)                      ; X2
RAM:CF3F        add     a, 4(iy)                      ; sumar ancho (W2)
RAM:CF42        ld      l, a
RAM:CF43        ld      a, 1(ix)                      ; X1
RAM:CF46        sub     4(ix)                         ; X1-W1
RAM:CF49        sub     l                             ; X1-W1-(X2+W2)
RAM:CF4A        jr      NC, loc_CF62                  ; no coinciden (C += 0)
RAM:CF4C        ld      a, 1(ix)                      ; X1
RAM:CF4F        add     a, 4(ix)                      ; sumar ancho (W1)
RAM:CF52        ld      l, a
RAM:CF53        ld      a, 1(iy)                      ; X2
RAM:CF56        sub     4(iy)                         ; X2-W2
RAM:CF59        sub     l                             ; X2-W2-(X1+W1)
RAM:CF5A        ld      a, c
RAM:CF5B        jr      C, loc_CF5F                   ;    coinciden (C += 9 )
RAM:CF5D        add     a, #9                         ; no coinciden (C += 18)
RAM:CF5F
RAM:CF5F loc_CF5F:
RAM:CF5F        add     a, #9
RAM:CF61        ld      c, a
RAM:CF62
RAM:CF62 loc_CF62:
RAM:CF62        ld      l, c                          ; el valor de C determina el tipo de colisión
RAM:CF63        ld      bc, #off_CF69                 ; jump table
RAM:CF66        jp      jump_to_tbl_entry             ; salto a la subrutinas que están más abajo según la siguiente tabla
RAM:CF66 ; ---------------------------------------------------------------------------
RAM:CF69 off_CF69:                                    ;         X    Y    Z    n: no coinciden por un extremo; N: no coinciden por el otro
RAM:CF69        .dw continue_1                        ; C = 0   n    n    n
RAM:CF6B        .dw continue_1                        ; C = 1   n    n    S
RAM:CF6D        .dw continue_1                        ; C = 2   n    n    N
RAM:CF6F        .dw d_3467121516                      ; C = 3   n    S    n
RAM:CF71        .dw d_3467121516                      ; C = 4   n    S    S
RAM:CF73        .dw continue_1                        ; C = 5   n    S    N
RAM:CF75        .dw d_3467121516                      ; C = 6   n    N    n
RAM:CF77        .dw d_3467121516                      ; C = 7   n    N    S
RAM:CF79        .dw continue_1                        ; C = 8   n    N    N
RAM:CF7B        .dw continue_1                        ; C = 9   S    n    n
RAM:CF7D        .dw continue_2                        ; C = 10  S    n    S
RAM:CF7F        .dw continue_2                        ; C = 11  S    n    N
RAM:CF81        .dw d_3467121516                      ; C = 12  S    S    n
RAM:CF83        .dw objs_coincide                     ; C = 13  S    S    S
RAM:CF85        .dw continue_2                        ; C = 14  S    S    N
RAM:CF87        .dw d_3467121516                      ; C = 15  S    N    n
RAM:CF89        .dw d_3467121516                      ; C = 16  S    N    S
RAM:CF8B        .dw continue_1                        ; C = 17  S    N    N
RAM:CF8D        .dw continue_1                        ; C = 18  N    n    n
RAM:CF8F        .dw continue_2                        ; C = 19  N    n    S
RAM:CF91        .dw continue_2                        ; C = 20  N    n    N
RAM:CF93        .dw continue_1                        ; C = 21  N    S    n
RAM:CF95        .dw continue_2                        ; C = 22  N    S    S
RAM:CF97        .dw continue_2                        ; C = 23  N    S    N
RAM:CF99        .dw continue_1                        ; C = 24  N    N    n
RAM:CF9B        .dw continue_1                        ; C = 25  N    N    S
RAM:CF9D        .dw continue_1                        ; C = 26  N    N    N
RAM:CF9F ; ---------------------------------------------------------------------------
RAM:CF9F
RAM:CF9F continue_1:
RAM:CF9F        jp      loc_CEDB                      ; pasamos a siguiente objeto en el bucle más interno
RAM:CFA2 ; ---------------------------------------------------------------------------
RAM:CFA2
RAM:CFA2 continue_2:
RAM:CFA2        jp      loc_CEDB                      ; pasamos a siguiente objeto en el bucle más externo
RAM:CFA5 ; ---------------------------------------------------------------------------
RAM:CFA5
RAM:CFA5 d_3467121516:
RAM:CFA5        ld      hl, (render_obj_2)            ; objeto que sigue al 2
RAM:CFA8        dec     hl                            ; apuntamos al 2
RAM:CFA9        ld      c, (hl)                       ; número de objeto 2
RAM:CFAA        ld      de, #render_list              ; tabla de 8 bytes
RAM:CFAD
RAM:CFAD loc_CFAD:
RAM:CFAD        ld      a, (de)                       ; ¿entrada vacía?
RAM:CFAE        cp      #0xFF
RAM:CFB0        jr      Z, loc_CFB8                   ; sí, encontrada entrada vacía
RAM:CFB2        cp      c                             ; ¿ya está listado el objeto 2?
RAM:CFB3        jr      Z, loc_CFCE                   ; sí, no hace falta meterlo en la lista
RAM:CFB5        inc     de                            ; siguiente entrada
RAM:CFB6        jr      loc_CFAD
RAM:CFB8 ; ---------------------------------------------------------------------------
RAM:CFB8
RAM:CFB8 loc_CFB8:
RAM:CFB8        ld      a, c                          ; índice del objeto 2
RAM:CFB9        ld      (de), a                       ; meter en la entrada
RAM:CFBA        inc     de                            ; incrementamos puntero para siguiente entrada
RAM:CFBB        ld      a, #0xFF
RAM:CFBD        ld      (de), a                       ; lo marcamos como vacía
RAM:CFBE        push    iy
RAM:CFC0        pop     ix                            ; objeto 1 = objeto 2
RAM:CFC2        ld      hl, (render_obj_2)            ; objeto que sigue al 2
RAM:CFC5        ld      (render_obj_1), hl            ; establecer al objeto que sigue al 1
RAM:CFC8        ld      de, #objects_to_draw          ; reiniciamos la comparación desde el principio
RAM:CFCB        jp      loc_CEDB                      ; pasamos a siguiente objeto en el bucle más externo
RAM:CFCE ; ---------------------------------------------------------------------------
RAM:CFCE
RAM:CFCE loc_CFCE:
RAM:CFCE        ld      hl, #objects_to_draw
RAM:CFD1
RAM:CFD1 loc_CFD1:
RAM:CFD1        ld      a, (hl)                       ; número de objeto
RAM:CFD2        inc     hl
RAM:CFD3        cp      #0xFF                         ; ¿fin de lista?
RAM:CFD5        jp      Z, process_remaining_objs     ; sí, reiniciamos la comparación desde el principio
RAM:CFD8        cp      c                             ; ¿es lo que estamos buscando?
RAM:CFD9        jr      NZ, loc_CFD1                  ; no, repetimos
RAM:CFDB        push    iy
RAM:CFDD        pop     ix                            ; objeto 1 = objeto 2
RAM:CFDF        jr      render_obj                    ; ir a renderizar
RAM:CFE1 ; ---------------------------------------------------------------------------
RAM:CFE1
RAM:CFE1 objs_coincide:                               ; caso de que los objetos coincidan completamente
RAM:CFE1        ld      a, 0(ix)                      ; número objeto 1
RAM:CFE4        sub     #0x60 ; '`'                   ; número objeto 1 - 0x60
RAM:CFE6        cp      #7                            ; ¿es un objeto especial?
RAM:CFE8        jr      NC, loc_CFF0                  ; no, saltamos
RAM:CFEA        ld      0(ix), #187                   ; sí, lo marcamos como parpadeante
RAM:CFEE        jr      loc_CFFD
RAM:CFF0 ; ---------------------------------------------------------------------------
RAM:CFF0
RAM:CFF0 loc_CFF0:
RAM:CFF0        ld      a, 0(iy)                      ; número objeto 2
RAM:CFF3        sub     #0x60 ; '`'                   ; número objeto 2 - 0x60
RAM:CFF5        cp      #7                            ; ¿objeto especial?
RAM:CFF7        jr      NC, loc_CFFD                  ; no, saltamos
RAM:CFF9        ld      0(iy), #187                   ; sí, lo marcamos como parpadeante
RAM:CFFD
RAM:CFFD loc_CFFD:
RAM:CFFD        jp      loc_CEDB                      ; pasamos a siguiente objeto en el bucle más externo
RAM:D000 ; ---------------------------------------------------------------------------
RAM:D000
RAM:D000 render_obj_no1:                              ; fin del bucle interior
RAM:D000        ld      hl, (render_obj_1)
RAM:D003
RAM:D003 render_obj:
RAM:D003        dec     hl                            ; volvemos a la entrada por la que estamos buscando
RAM:D004        set     7, (hl)                       ; lo marcamos como pintado
RAM:D006
RAM:D006        ld      a, #0xFF
RAM:D008        ld      (render_list), a              ; ponemos a cero la lista de objetos que coinciden
RAM:D00B
RAM:D00B        ld      hl, #rendered_objs_cnt        ; contamos un objeto más a pintar
RAM:D00E        inc     (hl)
RAM:D00F
RAM:D00F        call    calc_pixel_XY_and_render      ; se pinta el objeto en el búfer secundario
RAM:D012        jp      process_remaining_objs        ; reiniciamos la comparación desde el principio
RAM:D015 ; ---------------------------------------------------------------------------
RAM:D015
RAM:D015 render_done:
RAM:D015        pop     iy
RAM:D017        pop     ix
RAM:D019        ret

Lo interesante es ver que no todas las combinaciones de coincidencias entre dos objetos tienen el mismo procedimiento.

Al final, se trata de decidir, según la coincidencia entre dos objetos cuál debe pintarse primero. Y colocarles en una lista y empezar a pintar por el que está más atrás.

Avatar de Usuario
Chema
Mensajes: 2251
Registrado: 21 Jun 2012 20:13
Ubicación: Gijón
Agradecido : 1866 veces
Agradecimiento recibido: 596 veces
Contactar:

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor Chema » 17 Sep 2019 18:19

Uy que interesante la info sobre el Knight Lore. Hala!, más para leer con calma.

En todo caso, como ya vimos antes, hay problemas. Jon Ritmann (el de Batman y Head Over Heels) hizo algo similar. Hay un documento con un correo contestando a alguien que le preguntaba acerca de todo esto... a ver si lo encuentro...

Lo tengo, pero es un pdf y no lo puedo pegar aquí (lo adjunto comprimido en zip). Habla de la idea de tener una lista enlazada con los bloques ordenados, como ya se sugirió más arriba, pero ya indica que esto no resuelve todos los casos. Sobre todo, dice, hay un problema con los bloques demasiado altos y pone este ejemplo:
Jon-1.png
Jon-1.png (5.04 KiB) Visto 478 veces


Note the example above, no one block is fully in front of another, the only way to sort them is to have the tall object split into two parts.
Básicamente que el bloque alto hay que dividirlo en dos partes porque si no no hay manera de ordenarlos.

Si os fijáis lo mismo ocurre si el bloque alto fuese la mitad de alto, pero estuviese a media altura (parte oculto, parte no). Creo que eso ilustra el único caso en que mi bucle anterior falla.

Supongo que es posible dividir sobre la marcha cualquier bloque que esté entre dos capas en dos bloques más pequeños, de manera que pintando por capas todo vaya bien. Incluso es posible hacer esto más o menos rápidamente.

Entonces es posible, con un bucle como ese, renderizar correctamente la escena con esas restricciones. O eso, o crear la lista enlazada a partir de un mapa y actualizarla cuando un objeto se mueva tal y como dice Ritman en el pdf adjunto, vaya. En todo caso es lo mismo.

Por otra parte, como os digo, las capas no tienen por qué tener todas la misma altura. Se puede tener una de suelo, otra que se pinta después con sombras, otra sobre éstas mucho más altas con objetos como árboles, paredes, etc. y otra más sobre ésta con tejados de las casas y cosas a más altura. Si los objetos no pueden encontrarse a medias entre dos capas, todo va bien. Si pueden, hay que partirlos.

Si no se nos ocurre nada más, entonces puedo pasar a explicar por qué, a veces, puede ser mejor no renderizar por capas y quitar la restricción de altura de los bloques, que es lo que hice en Space:1999, y los problemas que aparecen en ese caso.

Por cierto, en 2008 Ignacio Pérez Gil hizo un motor genérico isométrico para PC llamado isomot (http://retrospec.sgn.net/users/ignacio/isoesp.htm) del cual hay fuentes. Tengo que estudiarlo a fondo, porque no había visto la versión final (cuando hice Space:1999 creo que no estaba liberada, o no la encontré). Viendo el código me parece que hace algo similar a mi ñapa para ese juego...
Adjuntos
Jon_Ritmans_Isometric_Tutorial.zip
(82.53 KiB) Descargado 19 veces

Avatar de Usuario
Kyp
Mensajes: 406
Registrado: 03 Oct 2013 17:13
Agradecido : 19 veces
Agradecimiento recibido: 84 veces

Re: Mis tribulaciones sobre un motor isométrico

Mensajepor Kyp » 17 Sep 2019 19:39

Chema escribió:Sobre todo, dice, hay un problema con los bloques demasiado altos y pone este ejemplo:
Jon-1.png

Note the example above, no one block is fully in front of another, the only way to sort them is to have the tall object split into two parts.
Básicamente que el bloque alto hay que dividirlo en dos partes porque si no no hay manera de ordenarlos.

Ese mismo dibujo me hice yo -rofl

Chema escribió:Si os fijáis lo mismo ocurre si el bloque alto fuese la mitad de alto, pero estuviese a media altura (parte oculto, parte no). Creo que eso ilustra el único caso en que mi bucle anterior falla.

Exacto.

Chema escribió:Supongo que es posible dividir sobre la marcha cualquier bloque que esté entre dos capas en dos bloques más pequeños, de manera que pintando por capas todo vaya bien. Incluso es posible hacer esto más o menos rápidamente.

En algo así estaba pensando yo también. Ahora estoy liado con otras cosas pero me están entrando unas ganas de retomar este tema...


Volver a “Software & OS Amiga”

¿Quién está conectado?

Usuarios navegando por este Foro: No hay usuarios registrados visitando el Foro y 0 invitados