Sprites, BOBs y otras criaturas mágicas (V): El Blitter (III)
Introducción
Tras las dos últimas entregas en las que se detallaba el funcionamiento del Blitter, vamos a meternos de lleno en un ejemplo.
Bliteando, que es gerundio
¡Vamos con un ejemplo! En concreto, emplearé gráficos del que posiblemente sea el mejor shoot’em up horizontal que se pudo ver en un Amiga 500: Apidya. Los gráficos de los que vamos a partir serán un fondo del primer nivel, el gráfico para el BOB de la abeja protagonista y la máscara para dicho BOB.
Vamos a suponer lo siguiente:
- El tamaño de nuestro buffer de pantalla (FONDO, a partir de ahora) es de 320×256, en 32 colores (5 planos).
- El tamaño tanto de la imagen (BOB, a partir de ahora) como de la máscara de la abeja protagonista es de 32×24, también en 32 colores.
- Dispondremos de una zona de memoria (SAVEBUFFER, a partir de ahora) para salvar la porción de fondo sobre el que pintaremos la abeja. Este buffer tendrá un tamaño de 48×24 (y 5 planos, como el resto de los gráficos). La razón de que este buffer sea 16 pixels más ancho que la imagen es que, como se vio en el capítulo anterior, habrá ocasiones en que necesitaremos blitear una palabra extra (16 bits más) respecto al ancho de la imagen original.
- El ejemplo sobre el que vamos a trabajar consistirá en simular lo que ocurriría si moviésemos la abeja protagonista a lo largo de una trayectoria sencilla: de la posición (x = 32, y = 100) a (x = 37, y =100), es decir, 5 pixels a la derecha.
Fondo del primer nivel y BOB y máscara para la abeja protagonista.
1) Salvar el fondo (x = 32, y = 100)
A lo largo de este artículo voy a emplear una ficha como ésta para describir cada blit, es decir, cada operación del blitter que vayamos lanzando. Esta información representa cómo tendríamos que configurar el Blitter para que ejecutase ese blit en concreto:
- Canal: En esta columna aparecerán cada uno de los canales necesarios para ejecutar este blit. En el ejemplo que nos ocupa, sólo necesitaremos el canal A para leer los datos de la porción del fondo (32×24) que queremos salvar y el canal D para escribir dichos datos en el savebuffer. De esta forma, cuando la abeja se desplace, podremos restaurar dicha porción de fondo, tal y como ya se vio en la primera entrega de esta serie de artículos.
- Operación: Simplificando, podríamos decir que este sería el blit más sencillo posible: un canal de lectura (A, B o C) y el canal de destino (D). La operación en este caso consiste simplemente en copiar datos de A a D (D = A).
- Apunta a: Dirección de los datos tanto para lectura (canales A, B y C), como para escritura (canal D). En este caso, la dirección de los datos que leeremos a través del canal A será la de la porción del fondo a salvar. Para calcular su dirección, emplearemos la fórmula vista en la entrega anterior:
Dirección = DIRECCIÓN_INICIAL_BUFFER + (BOB_X / 8) + (BOB_Y * BYTES_POR_LÍNEA) = FONDO + (32 / 8) + (100 * 40 * 5)
Obsérvese que, en este caso, multiplicamos la coordenada Y (100) por el número de bytes de cada línea (320 / 8 = 40) y por el número de planos de la imagen (5, al trabajar en 32 colores).
- Módulo: Tal y como se comentó en la entrega anterior, el módulo “indica el número de bytes necesarios para saltar de una línea de los datos, leídos o escritos por el Blitter, a la siguiente”. En cuanto al fondo (canal A), estamos leyendo un rectángulo de 4 bytes de ancho (32 pixels) que forma parte de una imagen de 40 bytes de ancho (320 pixels). Una vez que el Blitter lee una línea de esta porción rectangular del fondo, tiene que saltar los siguientes 40 – 4 = 36 bytes para situarse en la línea siguiente. En el caso del savebuffer (canal D) el módulo es 6 – 4 = 2 , porque el savebuffer es 16 pixels (2 bytes) más ancho que el blit que estamos ejecutando.
- Desplazamiento: Tal y como se comentó en el artículo anterior el Blitter tiene la capacidad de “desplazar los datos de entrada (de los canales A y B) entre 0 y 15 bits, normalmente, a la derecha (aunque también es posible desplazarlos a la izquierda)”. Os recomiendo releer ese punto del capítulo 4 para refrescar este concepto. En este caso, como la coordenada x = 32 coincide exactamente con un byte par del fondo, no es necesario aplicar desplazamiento alguno.
- Máscara: El Blitter también puede “aplicar una máscara a los primeros y a los últimos 16 bits de cada fila de datos leídos a través del canal A, es decir, ignorar algunos de esos bits para que no formen parte ni de los desplazamientos, ni de las operaciones lógicas posteriores”. No profundizaré demasiado en este punto, pero estaré encantado de explicarlo más en detalle a quien esté interesado. Sólo decir, que en el blit que nos ocupa, no es necesario eliminar datos de la entrada (leemos 4 bytes y escribimos 4 bytes) por lo que no será necesario enmascarar nada: todos los pixels de la entrada estarán en la salida.
- Tamaño: El ancho del blit se mide en palabras de 16 bits (1 palabra = 2 bytes) y el alto se mide en líneas. El ancho del blit, en este caso, será de 32 pixels = 2 palabras. Para el alto hemos de tener en cuenta que cada línea que vemos, realmente, está formada por 5 líneas (una por cada plano) así que, sabiendo que el alto de la abeja es de 24 líneas, el alto del blit será: 24 * 5 = 120.
2) Dibujar el BOB (x = 32, y = 100)
Supongamos que nuestro jefe nos mete prisa (algo nada habitual, por otra parte…) para que tengamos listo el juego en dos días. Como programadores novatos que somos decidimos tirar por la vía rápida y dibujar la abeja prescindiendo de la máscara. Veamos cómo tendríamos que configurar el Blitter para dibujar la abeja sobre el fondo, prescindiendo de la máscara. Este sería nuestro blit para dibujar la abeja:
En este caso, no me detendré a detallar cada uno de los datos del blit, puesto que se trata de una operación muy similar a la comentada en el apartado anterior. Lo único reseñable es que, en el caso de la imagen de la abeja, el módulo es 0, puesto que el ancho del blit coincide con el ancho de dicha imagen (4 bytes en ambos casos): no hay bytes que saltar para pasar de una línea a la siguiente. Una vez configurado el Blitter según estos datos, obtendríamos este resultado:
Resultado de dibujar el BOB sin emplear la máscara
Nuestro gozo en un pozo… prescindir de la máscara de la abeja supone que sobre el fondo veamos un horrible rectángulo. El objetivo de la máscara es precisamente quedarnos solamente con aquellos pixels de la imagen original que queremos pintar. Esta sería la configuración correcta:
Respecto al primer intento estos son los ajustes necesarios:
- Canal: Por el canal A leeremos la imagen de la máscara, por el B la de la abeja y por el C leeremos los datos del fondo (después explicaré por qué). El D, como de costumbre, lo usaremos para escribir el resultado sobre el propio fondo.
- Operación: Atención a este punto, porque es especialmente importante. ¿Qué significa exactamente AB + ¬AC? AB significa que por cada pixel de la máscara (A) que valga 1, dejaremos pasar lo que leamos por el canal B, es decir, lo que venga de la imagen de la abeja. ¬AC indica que por cada pixel de la máscara que valga 0, dejaremos pasar lo que leamos por el canal C, que apunta a la porción del fondo sobre la que vamos a dibujar. Dicho de otra forma, estamos recortando la imagen de la abeja para quedarnos única y exclusivamente con la abeja en sí, dejando el fondo intacto en el resto del rectángulo de 32×24 sobre el que estamos dibujando.
- Apunta a: Fijaos en que el canal C apunta a la misma dirección del fondo sobre la que vamos a pintar. La razón, como he apuntado antes, es respetar el escenario en aquellos puntos del rectángulo de 32×24 que no contengan parte de la abeja.
Resultado de dibujar el BOB empleando la máscara
3) Restaurar el fondo (x = 32, y = 100)
En el primer paso salvamos la porción del fondo sobre la que íbamos a dibujar. Lo primero que haremos en el siguiente frame será restaurar dicho fondo (guardado en el savebuffer) para, a continuación, actualizar la posición del BOB y dar comienzo de nuevo al ciclo: salvar fondo, dibujar BOB, restaurar fondo, etc. El blit que necesitaremos ejecutar será el inverso al visto en aquel primer paso, por lo que no hay mucho más que decir al respecto:
4) Actualizar estado del BOB
En este paso no interviene el Blitter, pero será vital para los siguientes blits, puesto que se actualiza el estado de cada BOB, incluyendo sus coordenadas. En nuestro ejemplo actualizaremos la coordenada x que pasará de 32 a 37 (es decir, la abeja se estaría moviendo a 5 pixels por frame), mientras que la y se mantiene en 100.
En este segundo ciclo la cosa se complica, puesto que la coordenada x ya no es divisible por 16, lo cual derivará (como veremos enseguida) en que algunos de los blits a ejecutar sean algo más complicados. A tener en cuenta:
- En el frame anterior x valía 32 lo cual aportaba un 32 / 8 = 4 a la dirección del fondo. En este caso 37 / 8 sigue siendo 4, por lo que la dirección sobre dicho buffer seguirá siendo la misma. Sin embargo, si pintamos la abeja en esa misma dirección, evidentemente, la veríamos en la posición x = 32 de la pantalla y no es eso lo que queremos. ¿De dónde sacamos esos 5 pixels que nos separan de x = 37? En el primer caso 32 / 8 da como resto 0, por lo que no es necesario aplicar desplazamiento alguno. Sin embargo, en el segundo caso, el resto de dividir 32 / 8 y, por tanto el desplazamiento a aplicar, es 5.
- El ancho del blit pasa de 2 palabras (32 pixels) a 3 (48 pixels). Esto, como ya se comentó en la entrega anterior, es debido precisamente al desplazamiento aplicado y al ancho del BOB: 5 + 32 = 37, lo cual no cabe en 2 palabras de memoria (32 bits), necesitamos una extra, para un total de 3 (48 bits).
5) Salvar el fondo (x = 37, y = 100)
6) Dibujar el BOB (x = 37, y = 100)
- Máscara: Una vez más, y dada la complejidad de esta parte, sólo diré que al ser mayor el ancho del blit (48 pixels) que el del origen (32 pixels), hemos de emplear la máscara del canal A del Blitter para hacer “limpieza” de esos bits sobrantes.
- Módulo: Nótese que el módulo de la máscara y el BOB pasan a ser negativos (-2 bytes). La razón es que el blit tiene un ancho de 6 bytes y tanto el BOB como la máscara tienen un ancho de 4 bytes. Por cada línea, se leen 6 bytes del BOB (o de la máscara), tal y como se indica en el tamaño del blit. Como el ancho de la imagen es de 4 bytes, hemos de retroceder 2 para colocarnos al comienzo de la siguiente línea.
7) Restaurar el fondo (x = 37, y = 100)
Conclusión
Aquí finaliza esta miniserie de tres entregas dedicadas de manera exclusiva al Blitter. Espero que hayan servido para que los más curiosos se hayan hecho una idea bastante precisa de cómo funciona y, de paso, para poner en valor la dificultad que entraña desarrollar un juego o una demo para una máquina como el Amiga donde, incluso pintar un objeto en pantalla, puede suponer todo un reto. ¡A más ver!