Wiki


Este tutorial fue alojado originalmente en Proyecto Euler, escrito por Adler? y corregido y remaquetado por stage7.

Introducción

Los blobs es un efecto sencillo, pero que si se optimiza adecuadamente y se calibra para pulir el resultado final puede quedar bastante bien.

El efecto trata de observar el potencial creado en un punto por una o más cargas electricas. Recordemos que la fórmula del potencial eléctrico para una carga es:

k·q/r

donde k = 9·10^9, q es el valor de la carga en Coulombios (C), y r es la distancia del punto a la carga en metros. Nosotros vamos a trabajar con cargas de 1 C, aunque también se puede tratar con cargas de diferente valor. Además, vamos a suponer que la distancia entre dos píxeles es de 1 metro. (aunque eso ni en la pantalla gigante de la Euskal ;)

How to

El algoritmo es muy sencillo: tenemos que recorrer todos los puntos de la pantalla, asignándole el valor de la fórmula k·q/r por cada una de las cargas que tengamos. Así, para dos cargas tendríamos:

    int x, y;
    int carga1_x, carga1_y, carga2_x, carga2_y;

    pbuf = buf;     // punteros a nuestro buffer
    for (y = 0; y < RESY; y++)
    {
        for (x = 0; x < RESX; x++)
        {
            *pbuf = 9000000000 / ( sqrt( (x-carga1_x) * (x-carga1_x)
                    + (y-carga1_y) * (y-carga1_y) / 7000000 )
                    + 9000000000 / ( sqrt( (x-carga2_x) * (x-carga2_x)
                    + (y-carga2_y) * (y-carga2_y) / 7000000 );
            pbuf++;
        }
    }

La última división es para convertir potenciales en colores: así conseguimos que ¿todos? los píxeles estén entre 0 y 255. En realidad, tendremos que asegurarnos que este número no sobrepase el 255, ya que en puntos próximos a la carga obtendremos valores que tienden a infinito. Es más, también hay que comprobar que no estemos exactamente encima de la carga, ya que en ese caso obtendríamos una división por cero.

Y esto hay que hacerlo para cada fotograma, moviendo las cargas por la pantalla mediante alguna función que dependa de senos y cosenos.

Pero está claro que hecho así sería demasiado lento, ya que para cada píxel tendríamos una división, una raíz cuadrada y dos multiplicaciones por carga.

Optimización del efecto

Precalcular

La más importante optimización en este caso consiste en precalcular todo lo que se pueda.

Así, vamos a hacer una tabla donde para una x y una y nos diga el color correspondiente a esa distancia de la carga. O sea, que tabla[x][y] indica el color que hay que poner cuando estemos a una distancia (x, y) de la carga. Esa distancia es (abs(x_actual - x_carga), abs(y_actual - y_carga)).

A la hora de construir la tabla tenemos que mirar el caso especial en el que estemos justo encima de la carga, o sea, la posición tabla[0][0]. Si el valor para este caso lo obtenemos de la misma forma que el resto de los elementos de la tabla, nos daría un error al dividir por cero.

Después el problema se reduce a acceder a la tabla tantas veces como cargas tengamos, sumando los colores obtenidos por cada una de las cargas. Para este paso necesitamos usar un entero, para luego poner el píxel correspondiente a 255 si esta variable vale >= 255, y asignarle la variable en otro caso.

Coma fija

Lo siguiente que tendríamos que hacer para optimizar este efecto es eliminar todas las variables en coma flotante que tengamos, y usar coma fija, ya que la unidad de coma flotante de los PC's de hoy en día sigue siendo bastante lenta.

Interpolar

Y para los que todavía quieran más velocidad (no os lancéis todos de golpe ;) queda un recurso que nos hace ganar bastante velocidad, y en este caso no nos hace perder calidad gráfica: la interpolación. Siguiendo una buena estrategia de interpolación se pueden calcular la cuarta parte de los píxeles de la forma explicada arriba y el resto calcularlo con una suma y un desplazamiento de bits.

Por ejemplo, en el gráfico siguiente primero se calculan los puntos marcados con una "X" siguiendo el método explicado anteriormente:

                        X·X·X·X·X
                        ·········
                        X·X·X·X·X
                        ·········
                        X·X·X·X·X

Después interpolo los puntos marcados con una "O" (sumo el punto de su izquierda con el de su derecha y divido por dos):

                        XOXOXOXOX
                        ·········
                        XOXOXOXOX
                        ·········
                        XOXOXOXOX

Y por último calculo los puntos restantes marcados con una "I", interpolando entre el punto que tiene arriba y el que tiene abajo:

                        XOXOXOXOX
                        IIIIIIIII
                        XOXOXOXOX
                        IIIIIIIII
                        XOXOXOXOX

Variantes

Este efecto queda muy bien si se hace en truecolor, asignándole un color a cada carga.

Código de ejemplo en C++

#include <mem.h>        // memcpy(), memset()
#include <stdlib.h>     // malloc(), free(), abs()
#include <conio.h>      // getch(), kbhit(), inp()
#include <math.h>       // sin(), cos()

// Resolucion
#define RESX 320
#define RESY 200

int frames = 0;

/***************************************************************************/
// Pasa a modo 320x200x8

void SetMode13h()
{
    __asm{
        mov ax,13h
        int 10h
    }
}

/***************************************************************************/
// Pasa a modo texto

void SetMode03h()
{
    __asm{
        mov ax,03h
        int 10h
    }
}

/***************************************************************************/
// Sincroniza los frames con el refresco vertical

void vsync()
{
    while(!(inp(0x3dA) & 8));

    while(inp(0x3dA) & 8);
}

/***************************************************************************/
// Copia el buffer intermedio a la memoria de video

void vuelca_buffer (char *buf)
{
    vsync();
    memcpy( (char *) (long) 0xa0000, buf, RESX*RESY);

    frames++;
}

/***************************************************************************/
// Pone toda la pantalla en negro (bueno, en el color 0)

#define limpia_buffer(buf)          
{                                   
    memset(buf, 0, RESX*RESY);      
}

/***************************************************************************/
void PonPaletaBlobs(void)
{
    outp(0x3c8, 0);
    for (int cont = 0; cont < 64; cont++)
    {
        outp(0x3c9, 0);         // rojo
        outp(0x3c9, 0);         // verde
        outp(0x3c9, cont);      // azul
    }
    for (cont = 0; cont < 128; cont++)
    {
        outp(0x3c9, 0);         // rojo
        outp(0x3c9, cont/2);    // verde
        outp(0x3c9, 63);        // azul
    }
    for (cont = 0; cont < 64; cont++)
    {
        outp(0x3c9, cont);      // rojo
        outp(0x3c9, 63);        // verde
        outp(0x3c9, 63);        // azul
    }
}

/****************************************************************************/
// Blobs sin interpolacion en coma flotante

void main ()
{
    SetMode13h();  // pasamos al modo 13h

    int x, y;

    // Creamos el buffer intermedio
    char *buf;
    char *pbuf = buf = (char*) malloc((unsigned long)RESX*RESY);

    // Reservamos memoria para la tabla potenciales/distancia
    float **tabla = (float**) malloc(RESY*sizeof(float));
    for (y=0; y<RESY; y++)
    {
        tabla[y] = (float*) malloc(RESX*sizeof(float));
    }

    // Precalculamos la tabla potenciales/distancia
    for (y=0; y<RESY; y++)
    {
        for (x=0; x<RESX; x++)
        {
            // 35156250
            if ((!x) && (!y)) tabla[y][x] = 9000000000/7000000;
            else tabla[y][x] = (9000000000.0 / (sqrt(x*x + y*y) * 7000000.0));
        }
    }

    // Cambiamos la paleta.
    PonPaletaBlobs();
 
    // Declaramos las cargas
    float alfa = 0;
    int x1, y1, x2, y2, x3, y3, x4, y4, x5, y5, x6, y6;

    // Y empezamos
    int subtotal;
    limpia_buffer(buf);

    while (kbhit()) getch();    // limpiamos el buffer del teclado

    do {
        // Movemos un poquillo las cargas...
        x1 = 60 * cos (alfa)    + 30 * sin (-alfa)  + RESX/2;
        y1 = 30 * cos (-alfa*2) + 60 * sin (alfa)   + RESY/2;
        x2 = 30 * cos (alfa)    + 60 * sin (alfa*2) + RESX/2;
        y2 = 60 * cos (alfa)    + 30 * sin (alfa)   + RESY/2;
        x3 = 45 * cos (-alfa)   + 45 * sin (alfa)   + RESX/2;
        y3 = 45 * cos (alfa*2)  + 45 * sin (-alfa)  + RESY/2;
        x4 = 75 * cos (alfa)    + 15 * sin (alfa*2) + RESX/2;
        y4 = 15 * cos (-alfa)   + 75 * sin (alfa*2) + RESY/2;
        x5 = 35 * cos (alfa)    + 10 * sin (alfa)   + RESX/2;
        y5 = 10 * cos (alfa*2)  + 35 * sin (-alfa)  + RESY/2;
        x6 = 40 * cos (-alfa)   + 30 * sin (alfa*2) + RESX/2;
        y6 = 40 * cos (alfa)    + 10 * sin (alfa)   + RESY/2;
        alfa += 0.05;

        pbuf = buf;
        for (y = 0; y < RESY; y++)
        {
            for (x = 0; x < RESX; x++)
            {
                subtotal = ( tabla[ abs((y1-y)) ] [ abs(x1-x) ] 
                           + tabla[ abs((y2-y)) ] [ abs(x2-x) ]
                           + tabla[ abs((y3-y)) ] [ abs(x3-x) ]
                           + tabla[ abs((y4-y)) ] [ abs(x4-x) ]
                           + tabla[ abs((y5-y)) ] [ abs(x5-x) ]
                           + tabla[ abs((y6-y)) ] [ abs(x6-x) ] );

                if (subtotal > 255) subtotal = 255;
                *(pbuf++) = subtotal;
            }
        }

        vuelca_buffer(buf);
    } while (!kbhit());

    // Liberamos la memoria que habiamos reservado
    free(buf);
    for (y=0; y<RESY; y++)
    {
        free (tabla[y]);
    }
    free(tabla);

    SetMode03h();  // volvemos al modo texto
}

Información de contacto

Si tenéis alguna duda o comentario o habéis descubierto algún error podéis escribirme a la siguiente dirección de e-mail: adler at hardnull dot org.

~~Adler (Jose Manuel Gómez Poveda) Coder of NULL demogroup

LogIn



Olvidé la contraseña
Registrarse

DVDs

DVD #1 / 2008
Descargar

Subscribirse

Sigue los cambios a través de estos feeds RSS:

Feed icon Noticias / Foros
Feed icon Cambios en el Wiki