Programación en Lenguaje Ensamblador

-El Verdadero Lenguaje de las Máquinas-

Minijuego en Ensamblador: Carga de Imágenes

–Como cargar gráficas de un archivo de imagen y usarlas como sprites–

Para este punto los módulos básicos de I/O (Entrada y Salida para los que no le entendieron al chiste del Juayderito) ya están completos y son funcionales. Aunque van a pasar un par de semanas antes de que entre de lleno la segunda fase. Antes hay que subir dos o tres entradas a esta web donde se describen algunos de estos módulos, y esta vez el primero que toca es el interesante tema de las graficas y como estas se cargan dentro de un juego en ensamblador.

sprite en paint

Como ya mencioné en la entrada anterior, mucha gente tiene la falsa creencia de que desplegar una imagen es cosa de una o dos instrucciones. Están desde los que creen que es tan simple como un tag de html hasta los que afirman es una función secreta y millonaria de una API exclusiva pero casi nadie se imagina lo que es abrir un archivo de imagen en un programa en ensamblador y aquí lo vamos a ver. Pues vamos a abrir en ensamblador esa sencilla imagen que se muestra junto con este párrafo, un humilde cuadrito de colores de 16 por 32 pixeles con una profundidad de 24 bits.

Si alguien no lo sabe, las imágenes planas usadas en un juego (incluyendo las texturas) no necesariamente son generadas por código. La mayor parte del tiempo estos dibujos que forman animaciones salen de archivos gráficos. Y aunque en esencia todos son matrices bidimensionales de puntos de colores existen diferentes formas en que se codifica la información en ellos dependiendo de lo que queramos guardar. Desde el punto de vista del programador, lo único que importa es conocer como la información es codificada en el interior de estas cadenas de bits y no si tales dibujos fueron hechos por un estudiante de artes digitales, un lead artist con experiencia profesional y gran peso en “la industria” o un loco del ensamblador que solo sabe usar Paint. Este último caso es el que vamos a ilustrar aquí.

En un juego las imagenes usadas se llaman sprites. El tema de los sprites merece su propia entrada, así que por ahora empecemos con como abrir un archivo gráfico de una buena vez. Para simplificar las cosas el archivo es extremadamente simple. Un rectángulo de 32 pixeles de ancho por 16 de altura con una profundidad de 24 bits por pixel. Y aunque con esa profundidad de color es posible lograr la nada despreciable cantidad de 16,777,216 colores diferentes de momento solo hay 8 colores con 2 tonalidades básicas: En hexadecimal, las lineas tienen los siguientes colores: 00000, 00007F, 007F00, 007F7F, 7F0000, 7F007F, 7F7F00, 7F7F7F, 0000FF, 00FF00, 00FFFF, FF0000, FF00FF, FFFF00, FFFFFF, FFFFFF.

Carga de un archivo de imagen paso a paso

Cada par de cifras representa 8 bits. Las dos cifras mas significativas son para el rojo, las 2 del centro para el verde y las dos últimas para el azul. Las lineas van del negro al blanco en valores sencillos de identificar. Los siguiente es guardar el archivo en un formato que no sea demasiado complicado de abrir. Como comenté en la entrada anterior el formato que estoy usando es el TGA versión 2 sin compresión. Como de momento estamos usando Paint lo que vamos a hacer es guardar el archivo como BMP de 24 bits y luego convertirlo a TGA con cualquier otra aplicación. En mi caso estoy usando el IrfanView, que es un visor de imagenes de uso libre que resulta extremadamente util para los interesados en programación gráfica porque tiene la capacidad de desplegar el contenido de las imagenes en formato hexadecimal. De hecho la segunda imagen es una toma de este editor.

encabezado TGA

En esta captura de pantalla se puede ver el encabezado de tan solo 18 bytes de un archivo TGA. A partir de la posición 12h comienza el contenido del archivo. Justo donde comienzan todas las letras F. Esto es porque está codificado de tal forma que el archivo comienza por el pixel de la esquina inferior izquierda que en este caso corresponde a las dos lineas de color blanco. De momento la mayoría del encabezado no es importante mas que el 20h y el 10h que se encuentan en las posiciones 0Ch y 0E al final del primer renglón. Se trata de valores de 16 bits que indican el ancho y la altura de la imagen medida en pixeles (32 y 16 para los profesionales de “la industria” que no saben leer hexadecimal). Nos basta con esto para reconstruir la imagen pero si tienen curiosidad, el 2 del primer renglón indica la versión TGA a la que pertenece y el 18 nos dice como se codifican los datos de la imagen, ahí es donde dice por ejemplo que está de cabeza. El resto de los ceros son valores aunque importantes no nos hacen falta para abrir la imagen.

Paso #1: Obtenen el handle del archivo de imagen.-
Antes de desplegar una imagen en la pantalla, primero debemos de cargar su archivo a la memoria del sistema tal cual está, en este caso no tenemos una función de la API que lo haga directamente y si la hubiera no la usaría por respeto a los 4 lectores que de verdad les interesa la programación en ensamblador. Primero llamamos a CreateFile con los parámetros OPEN_EXISTING, de lectura y escritura, la posición de la cadena ascii-z con el nombre del archivo y luego guardamos el valor retornado en el acumulador. Este entero de 32 bits es el handle y es con lo que vamos a manipular el archivo.

Paso #2: Cargar el archivo en memoria tal cual se encuenta.-
Una vez que tenemos el handle llamamos GetFileSize con ese handle como argumento y obtenemos la extensión de ese archivo en bytes y con esa medida llamamos a VirtualAlloc para que nos ceda un segmento de memoria virtual de ese tamaño donde vamos a guardar el archivo que vamos a leer. El valor devuelto por VirtualAlloc es la posición de memoria inicial del segmento otorgado. Esa posición junto con la medida del archivo las enviamos como argumentos en la siguiente llamada a ReadFile. Por cierto, la posición de memoria que recibe como penúltimo argumento se usa para obtener la última posición leida dentro del archivo y es util cuando leemos partes pequeñas de archivos muy grandes. Hasta este punto ya tenemos el archivo cargado en la memoria.

Paso #3: Desempaquetar la imagen.-
Los dos pasos anteriores son buenos para cualquier tipo de archivo que necesitemos en el juego como mas sprites, sonidos, escenarios, o diálogos pero hasta ahora no hemos hecho nada con la imagen. De momento podemos empezar desechando el handle del archivo abierto con CloseHandle. Luego leemos los valores en la posición 0Ch que contienen el ancho y la altura en la imagen y los multiplicamos para obtener la cantidad de pixeles que ocupa la imagen ya abierta. Este valor lo desplazamos 2 bits a la izquierda que es lo mismo que multiplicarlo por 4 para obtener la verdadera cantidad de memoria que va a necesitar la imagen ya desempaquetada, pues en este minijuego vamos a usar una profundidad de color de 32 bits. Con ese valor creamos otro buffer en memoria virtual con VirtualAlloc donde vamos a guardar la imagen desempaquetada.

El siguiente paso es voltear la imagen, para esto necesitamos dos registros indice que pueden ser ESI y EDI (no olviden restaurar los valores cuando terminen de usarlos) y hacer que ESI apunte a la posición 12h del archivo y EDI al último entero de 32 bits del buffer donde la imagen se va a descomprimir, se hace un loop de tantos pasos como la longitud de la imagen en pixeles y en cada iteración vamos a hacer lo sigiente:

Cargar en EAX el valor al que apunta ESI
Eliminar los 8 bits mas altos de EAX haciendo un AND con FFFFFF
escribir el contenido de EAX a la posición apuntada por EDI
Sumarle a ESI el valor de 3
Sumarle a EDI el valor de 4

Escritura de la imagen en la RAM de Video

Si las cosas salieron bien, a partir de este punto ya podemos mandar a la pantalla la imagen del archivo y con un poco de ingenio a la hora de construir estructuras de datos hasta podemos obtener cientos de cuadros de animación a partir de uno solo de estos archivos. Todo es cuestión de dibujar cada cuadro de animación de modo que la imagen parezca una planilla de estampillas postales y una vez cargada la imagen en memoria apuntar a las secciones de cada uno de esos cuadros. Sin embargo, aunque ya tenemos la imagen lista para usarla en un juego aún estamos muy lejos de tener verdaderos sprites, primero debemos ver como es que realmente trabaja el sistema de video en Windows y sobre todo el concepto de PITCH, del que hablaré brevemente a continuación.

No existe una traducción sencilla al castellano de este término, lo mas cercano sería algo así como “levantar o bajar la mirada ligeramente”, en ambientes de audio y música pitch corresponde al ascenso y descenso de una frecuencia o timbre de voz, en gráficas por computadora pitch es un valor constante que se le suma a la posición final de una linea horizontal de una imagen para alcanzar el inico de la siguiente. En la última imagen de esta entrada en lugar de verse el rectángulo hecho por lineas una sobre otra estas son dibujadas de manera continua, haciendo una larga linea con segmentos de colores. Esto pasa porque se ha ignorado el Pitch y el contenido de la imagen en memoria se está copiando de manera lineal. Además la pantalla también tiene su propio pitch que no necesariamente corresponde con el ancho de esta en pixeles. El concepto de pitch lo voy a explicar a profundidad en la siguiente entrada, de momento y ya para terminar voy a hacer una descripción exhaustiva de la imagen final.

sprite sin pitch y debug

En la parte mas alta y casi invisible al ojo se encuentra la imagen que al no respetar el pitch ha quedado en una sola linea. Los números en el recuadro blanco son datos relacionados con la imagen, el primero son el largo y ancho de la imagen unidos en un solo valor de 32 bits (16 y 32). El segundo valor es el total de la memoria que ocupa la imagen una vez desplegada que es 16 por 32 por 4 bytes = 2408 = 800 bytes. Sobra mencionar que este cálculo es extremadamente sencillo cuando se hace con números hexadecimales. El tercer valor es la posición de memoria donde se va a desempaquetar la imagen que es la 380000h y la siguente la de la posición del último entero de 32 bits (llamado DWORD por la gente de Intel) y para conseguir esta posición basta con sumarle a la posición inicial de memoria del buffer virtual la extensión de la imagen y luego restarle el ancho de la profundidad en pixeles, si no hiciéramos esto el último valor se escribiría fuera del buffer y podríamos causar un fallo de protección general. El valor que contiene el 5 no se usó en esta ocasión y pueden ignorarlo. Los últimos tres valores son difíciles de describir en la imagen fija. Pues lo que hacía el programa era escribir cada pixel de la imagen de uno en uno de modo que el primero de esos tres era el color del pixel, el segundo la posición de memoria dentro del buffer y el tercero el offset dentro de la imagen. Estos números iban ascendiendo mientras la linea se dibujaba de izquierda a derecha hasta acabar en la imagen final que pueden ver aquí.

Bueno, por ahora mejor paso a la siguiente entrada porque esta nota es ahora una de las mas extensas que he escrito en los últimos meses, pero espero que al menos sirva para que vean lo que es abrir una imagen en ensamblador: Un proceso mucho mas dificil de lo que muchos piensan pero no tan dificil como para no hacerlo.

febrero 23, 2011 - Posted by | Uncategorized | , ,

No hay comentarios aún.

Deja un comentario