{"id":2776,"date":"2021-01-17T19:38:57","date_gmt":"2021-01-17T19:38:57","guid":{"rendered":"http:\/\/blog.rastersoft.com\/?p=2776"},"modified":"2023-12-25T19:48:08","modified_gmt":"2023-12-25T19:48:08","slug":"pintando-en-el-spectrum-6","status":"publish","type":"post","link":"https:\/\/blog.rastersoft.com\/?p=2776","title":{"rendered":"Pintando en el Spectrum (6)"},"content":{"rendered":"\n<p>En la entrada anterior expliqu\u00e9 c\u00f3mo crear un sprite, y puse una demostraci\u00f3n que muestra c\u00f3mo funciona, moviendo un sprite de un c\u00edrculo en vertical. Y aunque en apariencia funciona bien cuando el fondo est\u00e1 vac\u00edo, si ponemos alguna imagen de fondo (en este ejemplo pongo \u00abbasura\u00bb copiada desde la zona de la ROM) y movemos el sprite por encima, vemos que hay un problema: aunque el sprite es un c\u00edrculo, los l\u00edmites del cuadrado que delimitan los caracteres son visibles.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter\"><video height=\"480\" style=\"aspect-ratio: 640 \/ 480;\" width=\"640\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/ejemplo_sprite1b-1.mp4\"><\/video><\/figure>\n\n\n\n<p>Lo l\u00f3gico ser\u00eda que en las zonas externas del c\u00edrculo pudi\u00e9semos ver el fondo, y que \u00e9ste s\u00f3lo estuviese tapado en donde est\u00e1 el c\u00edrculo en s\u00ed.<\/p>\n\n\n\n<p>Pues bien: este efecto es relativamente sencillo de conseguir, y para eso s\u00f3lo necesitamos utilizar <strong>m\u00e1scaras<\/strong>. Se trata de dividir el sprite en dos im\u00e1genes independientes: una contiene el sprite en s\u00ed, tal y como hasta ahora, y la otra es una <em>m\u00e1scara de transparencia<\/em>, donde un bit puesto a 1 indica que ese pixel del sprite es transparente y se debe ver el fondo, y un bit a 0 especifica que el pixel correspondiente del sprite es opaco y debe pintarse.<\/p>\n\n\n\n<p>En esta imagen vemos el sprite, la m\u00e1scara, y c\u00f3mo queda la transparencia al final:<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><a href=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/mascara1.png\" rel=\"lightbox-0\"><img loading=\"lazy\" decoding=\"async\" width=\"675\" height=\"226\" src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/mascara1.png\" alt=\"\" class=\"wp-image-2787\" srcset=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/mascara1.png 675w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/mascara1-300x100.png 300w\" sizes=\"auto, (max-width: 675px) 100vw, 675px\" \/><\/a><\/figure><\/div>\n\n\n\n<p>Como vemos, en la m\u00e1scara, la zona alrededor del c\u00edrculo es de color negro (bit a 1), lo que significa que es transparente, mientras que la zona interna es blanca (bit a 0), lo que significa que es opaca. A la derecha podemos ver las zonas transparentes con un damero de ajedrez. Vemos que hemos dejado un margen de un p\u00edxel alrededor del c\u00edrculo para que se vea mejor.<\/p>\n\n\n\n<p>Todo esto est\u00e1 muy bien, pero \u00bfc\u00f3mo hacemos para pintarlo? La clave est\u00e1 en utilizar operaciones l\u00f3gicas: antes de copiar un byte del sprite en el buffer de la pantalla, tenemos que leer el byte que ya hay y hacer un AND l\u00f3gico con la m\u00e1scara. De esta manera, los bits del fondo que coincidan con un bit a 1 de la m\u00e1scara se quedar\u00e1n tal y como est\u00e1n, mientras que los bits que coincidan con un 0 en la m\u00e1scara se pondr\u00e1n a 0, \u00abdejando un hueco\u00bb en donde podremos pintar luego el sprite. Por supuesto, para ello no podemos tampoco copiar directamente el byte del sprite, sino que tenemos que hacer un OR l\u00f3gico de lo que haya en pantalla y el sprite, de manera que se mezclen.<\/p>\n\n\n\n<p>Ve\u00e1moslo de manera gr\u00e1fica: supongamos que tenemos como fondo un damero de ajedrez, y queremos pintar encima nuestro sprite c\u00edrculo. Primero aplicamos la m\u00e1scara usando el operador AND entre cada byte de ella y el que hay en pantalla en la posici\u00f3n correspondiente, y almacenamos el resultado de nuevo en pantalla. Esto borrar\u00e1 s\u00f3lo las partes que la m\u00e1scara indica que son opacas, dejando intactas aquellas zonas marcadas como transparentes.<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><a href=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/aplicar_mascara.png\" rel=\"lightbox-1\"><img loading=\"lazy\" decoding=\"async\" width=\"900\" height=\"226\" src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/aplicar_mascara.png\" alt=\"\" class=\"wp-image-2788\" srcset=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/aplicar_mascara.png 900w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/aplicar_mascara-300x75.png 300w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/aplicar_mascara-768x193.png 768w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><\/a><\/figure><\/div>\n\n\n\n<p>Hecho esto, copiamos los bytes del sprite en la misma zona pero utilizando esta vez la operaci\u00f3n OR, de manera que \u00abse mezcle\u00bb con lo que hab\u00eda. Dado que previamente hab\u00edamos \u00abhecho un agujero\u00bb con la m\u00e1scara, los p\u00edxeles del sprite se mezclar\u00e1n exclusivamente donde nos interesa, dejando inalterados el resto de zonas.<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><a href=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/aplicar_mascara2.png\" rel=\"lightbox-2\"><img loading=\"lazy\" decoding=\"async\" width=\"900\" height=\"226\" src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/aplicar_mascara2.png\" alt=\"\" class=\"wp-image-2789\" srcset=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/aplicar_mascara2.png 900w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/aplicar_mascara2-300x75.png 300w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/aplicar_mascara2-768x193.png 768w\" sizes=\"auto, (max-width: 900px) 100vw, 900px\" \/><\/a><\/figure><\/div>\n\n\n\n<p>Sin embargo, a la hora de implementarlo nos encontramos con el problema de que es un proceso muy ineficiente, pues tenemos que, literalmente, imprimir dos sprites realmente: primero la m\u00e1scara, y luego los p\u00edxeles. Y por si fuera poco tenemos que hacer tres operaciones en memoria por cada sprite en lugar de dos, como hasta ahora: leer el dato de la pantalla, leer el byte del sprite (ya sea pixels o m\u00e1scara), y escribir el nuevo valor.<\/p>\n\n\n\n<p>La soluci\u00f3n consiste en aplicar ambas operaciones a la vez. Para ello empezamos leyendo el primer byte de la zona de pantalla, hacemos AND con el primer byte de la m\u00e1scara, luego OR con el primer byte de los p\u00edxeles, y finalmente escribimos el valor resultante en la memoria de pantalla. Hecho esto, incrementamos en uno los punteros de los p\u00edxeles y de la m\u00e1scara, pasamos a la siguiente direcci\u00f3n de pantalla, y repetimos el proceso.<\/p>\n\n\n\n<p>Este sistema permite aumentar mucho el rendimiento, pues no s\u00f3lo reducimos los accesos a memoria a s\u00f3lo dos tercios, sino que, adem\u00e1s, se pueden compartir muchas operaciones comunes, como la inicializaci\u00f3n de los bucles (e incluso la sobrecarga que supone ejecutar el bucle en s\u00ed). Sin embargo, todav\u00eda podemos optimizarlo un poquito m\u00e1s. Para entenderlo, pensemos en c\u00f3mo almacenamos los p\u00edxeles y la m\u00e1scara del sprite: la opci\u00f3n m\u00e1s inmediata es colocar la m\u00e1scara justo a continuaci\u00f3n de los p\u00edxeles, de manera que si conocemos la direcci\u00f3n inicial y el tama\u00f1o del sprite en caracteres, podemos obtener la direcci\u00f3n de la m\u00e1scara simplemente con la operaci\u00f3n MASCARA = P\u00cdXELES + ANCHO * ALTO * 8. El resultado ser\u00eda como tener un sprite del doble de tama\u00f1o, y almacenado as\u00ed:<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><a href=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/sprite_consecutivo.png\" rel=\"lightbox-3\"><img loading=\"lazy\" decoding=\"async\" width=\"225\" height=\"450\" src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/sprite_consecutivo.png\" alt=\"\" class=\"wp-image-2793\" srcset=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/sprite_consecutivo.png 225w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/sprite_consecutivo-150x300.png 150w\" sizes=\"auto, (max-width: 225px) 100vw, 225px\" \/><\/a><\/figure><\/div>\n\n\n\n<p>Es sencillo de describir, pero recordemos que el Z80 no tiene instrucci\u00f3n de multiplicaci\u00f3n, lo que supone que hacer ese c\u00e1lculo sea lento y ocupe bastante c\u00f3digo. Adem\u00e1s, nos obliga a tener tres punteros: uno para los p\u00edxeles, otro para la m\u00e1scara, y otro para el buffer de pantalla.<\/p>\n\n\n\n<p>Sin embargo, existe una manera de evitar todo esto, y es <em>entrelazar<\/em> los datos de la m\u00e1scara y de los p\u00edxeles; esto es, almacenar en memoria un byte de m\u00e1scara, un byte de p\u00edxeles, un byte de m\u00e1scara, un byte de p\u00edxeles&#8230; as\u00ed:<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><a href=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/sprite_intercalado.png\" rel=\"lightbox-4\"><img loading=\"lazy\" decoding=\"async\" width=\"450\" height=\"225\" src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/sprite_intercalado.png\" alt=\"\" class=\"wp-image-2792\" srcset=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/sprite_intercalado.png 450w, https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/sprite_intercalado-300x150.png 300w\" sizes=\"auto, (max-width: 450px) 100vw, 450px\" \/><\/a><\/figure><\/div>\n\n\n\n<p>La ventaja de este sistema es que s\u00f3lo necesitamos dos punteros: uno que apunte al sprite, y otro al buffer de pantalla. La mec\u00e1nica es sencilla: leemos byte del puntero del buffer de pantalla y hacemos un AND con el byte del puntero del sprite. Incrementamos en uno el puntero del sprite y hacemos un OR del byte al que apunta con el resultado anterior. Hecho esto, incrementamos de nuevo el puntero del sprite y pasamos a la siguiente direcci\u00f3n de memoria del buffer de pantalla (seg\u00fan estemos todav\u00eda en el mismo scanline o tengamos que pasar al siguiente). Lo curioso es que pocos juegos utilizan este sistema, a pesar de sus claras ventajas. Un ejemplo de juego que lo usa es <a href=\"https:\/\/en.wikipedia.org\/wiki\/Knight_Lore\" target=\"_blank\" rel=\"noreferrer noopener\">Knight Lore<\/a>, uno de los primeros juegos en utilizar m\u00e1scaras en sus sprites.<\/p>\n\n\n\n<p>El c\u00f3digo para hacer esto ser\u00eda el siguiente:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted mycode\">pintar_sprite:\n    push HL\n    exx\n    pop HL      ; metemos la direcci\u00f3n del sprite en HL'\n    exx\n    set 7, D    ; DE ya casi tiene la direcci\u00f3n, le falta el bit 7 de D\n    ex de, hl   ; metemos la direcci\u00f3n de la pantalla en HL\n    sla B\n    sla B       ; rotamos tres veces B, que es igual que multiplicar por 8\n    sla B       ; as\u00ed tenemos en B la altura en scanlines\n    ld A, 32\n    sub C       ; calculamos cuanto tenemos que sumar para pasar al\n    ld E, A     ; siguiente scanline, y lo almacenamos en DE\n    ld D, 0\nbucle:\n    push BC     ; guardamos B (pues es el contador de scanlines) y C\n    ld B, C     ; preparamos el bucle interno\nbucle2:\n    ld a, (hl)  ; leemos el byte actual de la pantalla\n    exx\n    and (hl)    ; aplicamos la m\u00e1scara\n    inc hl\n    or (hl)     ; aplicamos los p\u00edxeles\n    inc hl\n    exx\n    ld (hl), a  ; almacenamos el resultado en la pantalla\n    inc hl      ; y pasamos a la siguiente posici\u00f3n de la pantalla\n    djnz bucle2 ; terminamos el scanline\n    add HL, DE  ; y pasamos al siguiente scanline\n    pop BC      ; recuperamos el n\u00famero de scanlines que nos quedan\n    djnz bucle  ; y repetimos hasta hacer todos los scanlines\n\n    call paint_screen ; llamamos a la funci\u00f3n que vuelca el buffer\n                      ; en la pantalla (si hemos pintado todos los\n                      ; sprites)<\/pre>\n\n\n\n<p>y reemplazar\u00eda al c\u00f3digo que escribimos en la entrada anterior. Existe un cambio extra a mayores: dado que ahora cada scanline ocupa el doble (pues contiene p\u00edxeles y m\u00e1scara), en la parte donde comprobamos si el sprite est\u00e1 fuera de la pantalla por arriba tenemos que multiplicar por 16 en lugar de por 8, lo que se consigue a\u00f1adiendo un <em>add A, A<\/em> extra.<\/p>\n\n\n\n<p>Y este es el resultado: como se ve, es much\u00edsimo mejor que el original, pues ya no hay ese cuadrado tan feo alrededor del sprite que hace que parezca <em>pegado encima<\/em>, sino que \u00e9ste realmente aparece integrado con el fondo.<\/p>\n\n\n\n<figure class=\"wp-block-video aligncenter\"><video height=\"480\" style=\"aspect-ratio: 640 \/ 480;\" width=\"640\" controls src=\"https:\/\/blog.rastersoft.com\/wp-content\/uploads\/2021\/01\/demo_sprites_mask-1.mp4\"><\/video><\/figure>\n\n\n\n<p>Como cabe suponer, mi editor de sprites <a href=\"https:\/\/www.rastersoft.com\/programas\/zxspriter.html\" target=\"_blank\" rel=\"noreferrer noopener\">ZXSpriter<\/a> almacena los sprites precisamente en este formato. Pero no s\u00f3lo eso, sino que si se incluyen atributos de color, puede tambi\u00e9n incluirlos <em>entrelazados<\/em> (aunque en este caso, es opcional). De esta manera, el sprite tendr\u00e1 ocho scanlines con los bytes de m\u00e1scara y p\u00edxeles entrelazados, tal y como hemos visto ya, y justo a continuaci\u00f3n los bytes con los atributos de color para ese conjunto de scanlines; a continuaci\u00f3n otros ocho scanlines, y otra vez los atributos de color para ese conjunto de scanlines. Y as\u00ed sucesivamente. Esto permite pintar sprites de colores de manera \u00f3ptima.<\/p>\n\n\n\n<p>El c\u00f3digo del ejemplo con m\u00e1scaras est\u00e1 disponible aqu\u00ed: <a rel=\"noreferrer noopener\" href=\"http:\/\/www.rastersoft.com\/ejemplo_sprite2.asm\" target=\"_blank\">ejemplo de sprites con m\u00e1scaras<\/a>. En \u00e9l vuelvo a hacer uso de los registros alternativos (BC&#8217;, DE&#8217; y HL&#8217;), y del hecho de que el registro A no se intercambia con A&#8217; cuando se ejecuta la instrucci\u00f3n EXX, para tener en HL la direcci\u00f3n de la pantalla y en HL&#8217; la del sprite.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>En la entrada anterior expliqu\u00e9 c\u00f3mo crear un sprite, y puse una demostraci\u00f3n que muestra c\u00f3mo funciona, moviendo un sprite de un c\u00edrculo en vertical. Y aunque en apariencia funciona bien cuando el fondo est\u00e1 vac\u00edo, si ponemos alguna imagen de fondo (en este ejemplo pongo \u00abbasura\u00bb copiada desde la zona de la ROM) y &hellip; <a href=\"https:\/\/blog.rastersoft.com\/?p=2776\" class=\"more-link\">Seguir leyendo <span class=\"screen-reader-text\">Pintando en el Spectrum (6)<\/span> <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5,17,7],"tags":[20],"class_list":["post-2776","post","type-post","status-publish","format-standard","hentry","category-programacion","category-retrocomputacion","category-tutoriales","tag-pintando-en-el-spectrum"],"_links":{"self":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2776","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2776"}],"version-history":[{"count":11,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2776\/revisions"}],"predecessor-version":[{"id":2809,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=\/wp\/v2\/posts\/2776\/revisions\/2809"}],"wp:attachment":[{"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2776"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2776"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.rastersoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2776"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}