(intento de) Efecto humo [SDL]

Solicite, consulte o publique recursos de referencia para desarrolladores.

(intento de) Efecto humo [SDL]

Notapor endaramiz » Sab Abr 25, 2009 5:09 pm

Este programa intenta simular una fuente de humo utilizando para ello SDL. El motivo de la publicación se encuentra en este tema.Como me temía, es bastante lento y tampoco es muy realista. Por estas razones, es muy posible que no sea una buena opción para un juego. Sin embargo, me he divertido bastante programándolo y me ha servido para aprender más cosas sobre el tratamiento de píxeles a bajo nivel. En el momento de escribir esto, estoy mal de tiempo, pero intentaré hacer una explicación cuando pueda. Si tienes alguna sugerencia (recién comienzo en este tema y me queda mucho por aprender...) o una duda concreta, no dudes en escribir. Si veo que hay interés, me daré más prisa en publicar la explicación.

Código: Seleccionar todo
#include <iostream>
#include <cmath>
#include <list>

#include <SDL/SDL.h>
using namespace std;

#define S_W 1024
#define S_H 768
#define bpp 2

int pow(int n, int e) {
    if (e == 0) return 1;
    return n*pow(n, e-1);
}

double min(double a, double b) {
    if (a <= b) return a;
    return b;
}

void draw_circle3(int x, int y, int r, double alp0, SDL_Surface* s);
void draw_circle3(int x, int y, int r, double alp0, SDL_Surface* s);

class Atomo {
    public:
        Atomo(int x, int y);
        void update(double ax);
        void draw(SDL_Surface* screen);
        bool finished();
        int prior;
    private:
        double x, y, dy;
        double dx;
        double r, dr;
        double alpha, dalpha;
};

Atomo :: Atomo(int x, int y) {
    prior = rand();
    this->x = x+rand()%5-2; this->y = y+rand()%5-2;
    dx = rand()%10/10.0-0.5; dy = 4;
    alpha = 0.3 + rand()%10/100.0; dalpha = 0.015;
    r = 2 + rand()%3; dr = 2.5;
}

void Atomo :: update(double ax) {
    dx += ax + rand()%10/10.0-0.5;
    x += dx;
    y -= dy + rand()%3-1;
    alpha += dalpha + rand()%5/100;
    r += dr + rand()%3-1;
}

void Atomo :: draw(SDL_Surface* screen) {
    draw_circle3(x, y, r, alpha, screen);
}

bool Atomo :: finished() {
    return alpha >= 0.93 or x >= S_W  or x < 0 or y < 0;
}


void put_pixel(SDL_Surface* screen, int row, int col, unsigned int color, double alp) {
    if (alp >= 1) return;
    Uint16 *pdest = (Uint16*)screen->pixels;
    pdest += screen->pitch/2*row;
    pdest += col;
    Uint16 c = *pdest;
    unsigned int r = (c&0x0000F800)>>11;
    unsigned int g = (c&0x000007E0)>>5;
    unsigned int b = (c&0x0000001F);
    unsigned int rgb_color = (int(r*alp + color*(1-alp))<<11) | (int(g*alp + ((color<<1)|1)*(1-alp))<<5) | (int(b*alp + color*(1-alp)));
    *pdest = rgb_color;
}

void draw_circle3(int x, int y, int r, double alp0, SDL_Surface* s) {
        int j = 0;
        for (int ii = 1; ii <= r and y+ii < S_H; ++ii)
                put_pixel(s, y+ii,x,31-(1-cos((M_PI/2)*(pow(ii,2)+pow(j,2))/double(pow(r,2))))*20, 0);
        for (int ii = 1; ii <= r and y+ii >= 0; ++ii)
                put_pixel(s, y-ii,x,31-(1-cos((M_PI/2)*(pow(ii,2)+pow(j,2))/double(pow(r,2))))*20, 0);
        for (int ii = 1; ii <= r and x-ii >= 0; ++ii)
                put_pixel(s, y,x-ii,31-(1-cos((M_PI/2)*(pow(ii,2)+pow(j,2))/double(pow(r,2))))*20, 0);
        for (int ii = 1; ii <= r and x+ii < S_W; ++ii)
                put_pixel(s, y,x+ii,31-(1-cos((M_PI/2)*(pow(ii,2)+pow(j,2))/double(pow(r,2))))*20, 0);
        put_pixel(s, y,x,31-(1-cos((M_PI/2)*(pow(j,2)+pow(j,2))/double(pow(r,2))))*25, 0);

        for ( j = 1; j <= r; ++j) {
            int i = sqrt(r*r-(j*j));
            for (int ii = 1; ii <= i and y+ii < S_H and x+j < S_W; ++ii)
                put_pixel(s, y+ii,x+j,31-(1-cos((M_PI/2)*(pow(ii,2)+pow(j,2))/double(pow(r,2))))*20, min((1-cos((M_PI/2)*(pow(ii,2)+pow(j,2))/double(pow(r,2))))*0.99 + alp0, 1));
            for (int ii = 1; ii <= i and y+ii < S_H and x-j >= 0; ++ii)
                put_pixel(s, y+ii,x-j,31-(1-cos((M_PI/2)*(pow(ii,2)+pow(j,2))/double(pow(r,2))))*20, min((1-cos((M_PI/2)*(pow(ii,2)+pow(j,2))/double(pow(r,2))))*0.99 + alp0, 1));
            for (int ii = 1; ii <= i and y-ii >= 0 and x+j < S_W; ++ii)
                put_pixel(s, y-ii,x+j,31-(1-cos((M_PI/2)*(pow(ii,2)+pow(j,2))/double(pow(r,2))))*20, min((1-cos((M_PI/2)*(pow(ii,2)+pow(j,2))/double(pow(r,2))))*0.99 + alp0, 1));
            for (int ii = 1; ii <= i and y-ii >= 0 and x-j >= 0; ++ii)
                put_pixel(s, y-ii,x-j,31-(1-cos((M_PI/2)*(pow(ii,2)+pow(j,2))/double(pow(r,2))))*20, min((1-cos((M_PI/2)*(pow(ii,2)+pow(j,2))/double(pow(r,2))))*0.99 + alp0, 1));
        }
}

int main(int argc, char** argv) {
    cerr << SDL_Init(SDL_INIT_VIDEO);
    SDL_Surface *screen = SDL_SetVideoMode(S_W, S_H, bpp*8, SDL_HWSURFACE);
    SDL_ShowCursor(SDL_DISABLE);

    list<Atomo*> atomos;
   
    SDL_FillRect(screen, &screen->clip_rect, 0xF800);
    SDL_Event event;
    bool salir = false;
    int frame;
    while(not salir) {
        SDL_FillRect(screen, &screen->clip_rect, 0xF800);
        int t0 = SDL_GetTicks();
        if (!(++frame%5)) {
            Atomo* patomo = new Atomo(S_W/2, S_H - 20);
            list<Atomo*>::iterator ita = atomos.begin();
            while (ita != atomos.end() and (*ita)->prior > patomo->prior)
                ++ita;
            atomos.insert(--ita, patomo);
        }

        list<Atomo*>::iterator ita = atomos.begin();
        while (ita != atomos.end()) {
            if ((*ita)->finished()) {
                delete *ita;
                ita = atomos.erase(ita);
            }
            else {
                (*ita)->update(0.2);
                (*ita)->draw(screen);
                ++ita;
            }
        }
        int t1 = SDL_GetTicks();
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
                case SDL_KEYDOWN:
                    switch (event.key.keysym.sym) {
                        case SDLK_ESCAPE:
                            salir = true;
                        break;
                    }
                break;
                case SDL_QUIT:
                    salir = true;
                break;
            }
        }
        cerr << "\rt. blit: " << t1-t0 << "  ";
        SDL_Flip(screen);
        SDL_Delay(2);
    }
    cerr << endl;
    SDL_Quit();
}


Las cruces que salen no son un bug, sirven para comprobar el efecto alfa. El código no es una versión definitiva, es más bien para juguetear un poco.

Saludos.
Avatar de Usuario
endaramiz
 
Mensajes: 283
Registrado: Vie Ago 31, 2007 9:25 am
Ubicación: Barcelona

Notapor lacabra25 » Dom Abr 26, 2009 12:25 am

la cuestión de que se ejecutase lento el código, viendolo un poco por encima, podria ser debido a todos los bucles for que usas para dibujar cada particula de la nube de humo, no entiendo a que se deben tantos for ya que si se supone que se va a dibujar particulas que formaran una nube de humo estas no deberian ser demasiado grandes, ninguna deberia llegar a los 10 pixelesy esto tirando al alza, por lo que aun usando circulos de un determinado radio seria suficiente un for para trazar circulos que fuesen desde un radio igual al radio de la particula hasta un radio de un pixel (dibujar una particula mas pequeña que un pixel o de un pixel coma nosecuanto no tiene sentido, por lo que se usarian radios de numeros de pixeles enteros), dentro de este for solo es necesario un for para la coordenada x (solo en positivo pues dado que un circulo es simetrico se sobreentiende que la negativa tendra el mismo valor absoluto y solo cambia el signo) y dentro otro para la coordenada y), en total solo se necesitarian 3 for por particula, y no tantos bucles como se ve que hay.

De resto solo puedo decirte que habria que pasarle el debuger para ver en que es en lo que falla, en que va lento y eso, para poder tratar mas dicho tema.

Buen trabajo, la programacion orientada a objetos no es que me guste precisamente pero esta bien la idea del efecto de humo (no se si el tema de la velocidad de ejecucion tendra que ver con que uses objetos, puesto que en teoria estos se supone que son datos mas complejos que una simple estructura de datos).
Esta cuenta ahora a pasado a la cuenta jhg
Avatar de Usuario
lacabra25
 
Mensajes: 222
Registrado: Mié Abr 02, 2008 9:45 pm
Ubicación: Tenerife (España)

Notapor sofoke » Dom Abr 26, 2009 1:53 am

Ya he probado el efecto que acabas de crear, me parece muy interesante, como dices es algo lento; y como comenta nuestro amigo podriamos hacerle unas pequeñas modificaciones para ver si se acelera su ejecucion (lo cual seria bueno) checare mas tranquilamente tu efecto (todo el codigo) y comentare mis experiencias...
...cuando lo popular no es suficiente...
Gnu-Linux-y-Más
Avatar de Usuario
sofoke
 
Mensajes: 102
Registrado: Jue May 24, 2007 8:10 pm
Ubicación: México

Notapor endaramiz » Dom Abr 26, 2009 3:14 pm

lacabra25 escribió:seria suficiente un for para trazar circulos que fuesen desde un radio igual al radio de la particula hasta un radio de un pixel (dibujar una particula mas pequeña que un pixel o de un pixel coma nosecuanto no tiene sentido, por lo que se usarian radios de numeros de pixeles enteros), dentro de este for solo es necesario un for para la coordenada x (solo en positivo pues dado que un circulo es simetrico se sobreentiende que la negativa tendra el mismo valor absoluto y solo cambia el signo) y dentro otro para la coordenada y), en total solo se necesitarian 3 for por particula, y no tantos bucles como se ve que hay.

Hay que tener cuidado con ese algoritmo porque dependiendo de como sea el algoritmo de dibujar el círculo puedes obtener resultados no deseados. Del mismo modo que pasa con Pygame si haces:
Código: Seleccionar todo
import pygame
screen = pygame.display.set_mode((200,200))
pygame.draw.circle(screen, (255, 255, 255), (100, 100), 70, 50)
pygame.display.flip()

No es simple coincidencia, es el algoritmo que usa pygame:
Código: Seleccionar todo
[En draw.c/static PyObject* circle(PyObject* self, PyObject* arg)]
[...]
if(!width)
      draw_fillellipse(surf, (Sint16)posx, (Sint16)posy, (Sint16)radius, (Sint16)radius, color);
   else
      for(loop=0; loop<width; ++loop)
         draw_ellipse(surf, posx, posy, radius-loop, radius-loop, color);
[...]


Las razones por la que he hecho tantos bucles, a parte de la anterior, es porque dibujo la cruz aparte (los 4 bucles primeros) ya que sinó surgen problemas con la transparencia porque se dibujan 2 veces esos píxeles (0 == -0). Los otros son para comprobar los casos en los que hay que dibujar un píxel fuera del clip_rect de la pantalla. Como empiezo desde el eje horizontal hasta el borde de la circunferencia, si un píxel "se sale de la pantalla" (*1) puedo ahorrarme unas cuantas iteraciones del bucle porque todos los demás también "estarán fuera". Aunque haga más bucles, al final me ahorro más iteraciones que si comprobase la posición a la hora de dibujar el píxel (en los casos de que hayan píxeles fuera de la pantalla). La diferencia entre una forma u otra creo que debe ser bastante peña y depende de como traduce el compilador y esas cosas. Se podría comprobar con el profiler...



lacabra25 escribió:De resto solo puedo decirte que habria que pasarle el debuger para ver en que es en lo que falla, en que va lento y eso, para poder tratar mas dicho tema.
El principal problema es que se dibujan muchos píxeles. Al principio me iba a 160 ms por frame porque ocupaba casi toda la pantalla. Para cambiar esto, se tendría que rediseñar el código desde el principio con una idea distinta. Con la idea actual, manera más efectiva de ganar velocidad es que cuanto más pobre visualmente sea (menos bolas de humo, más pequeñas, que desaparecen más rápido...), más rápido irá.

lacabra25 escribió:Buen trabajo, la programacion orientada a objetos no es que me guste precisamente pero esta bien la idea del efecto de humo (no se si el tema de la velocidad de ejecucion tendra que ver con que uses objetos, puesto que en teoria estos se supone que son datos mas complejos que una simple estructura de datos).
Gracias. Sí, pero tampoco sé si afectará mucho. Igualmente, este es el estilo de programación con el que mejor me desenvuelvo. De otra manera, no sé si podría programarlo.

*1: En realidad el concepto de que una superficie sea un matriz de píxeles es errónea. Es simplemente una fila (array). Por lo tanto, si se intenta dibujar más a la derecha de la anchura de la pantalla, salta al principio de la fila siguiente. Si se intenta dibujar "por arriba o por abajo" de la pantalla, se modifica memoria que no pertenece al array de píxeles con las consecuencias catastróficas que eso conlleva. Por esta razón, cuando se declara la screen, automáticamente se le asigna un clip_rect para que luego trabaje de la forma esperada SDL_BlitSurface.
Este efecto se puede comprobar en el código actual si una bola sale por la derecha de la pantalla ya que hay un bug en el código: la eliminación de las bolas se tiene que hacer después del update y antes del draw (si se pone el update después del draw, se soluciona).

Ok sofoke, si tienes dudas en alguna parte, ya sabes... Y ya avisaréis si conseguís mejoras :lol: .

Saludos.
Avatar de Usuario
endaramiz
 
Mensajes: 283
Registrado: Vie Ago 31, 2007 9:25 am
Ubicación: Barcelona

Notapor lacabra25 » Lun Abr 27, 2009 7:15 pm

endaramiz escribió:(*1)

OK, yo para el tema grafico siempre he usado SDL y como esta al copiar a pantalla las superficies comprueba automaticamente si se sale o no y se encarga de hacerlo no tuve en cuenta este detalle.
Esta cuenta ahora a pasado a la cuenta jhg
Avatar de Usuario
lacabra25
 
Mensajes: 222
Registrado: Mié Abr 02, 2008 9:45 pm
Ubicación: Tenerife (España)

Notapor endaramiz » Mar Jun 23, 2009 2:49 pm

Explicación de put_pixel
Código: Seleccionar todo
void put_pixel(SDL_Surface* screen, int row, int col, unsigned int color, double alp) {

Esta función se encarga de dibujar un píxel en una 'Surface' con un parámetro alpha que va de 0 a 1 siendo 0 sin transparencia.
Los parámetros son, respectivamente, 'Surface' destino, fila, columna, color del píxel a dibujar y valor alpha.
Código: Seleccionar todo
    if (alp >= 1) return;

si la transparencia es igual o superior a 100%, se sale de la función
Código: Seleccionar todo
    Uint16 *pdest = (Uint16*)screen->pixels;
    pdest += screen->pitch/2*row;
    pdest += col;

Se hace que el puntero pdest apunte al píxel (de 16 bits) de la 'Surface' destino donde se quiere dibujar.
Código: Seleccionar todo
    Uint16 c = *pdest;
    unsigned int r = (c&0xF800)>>11;
    unsigned int g = (c&0x07E0)>>5;
    unsigned int b = (c&0x001F);

Aquí se descompone el color del píxel elegido por partes roja, verde y azul. En este ejemplo, al ser una prueba rápida, está construida específicamente para una 'Surface' con profindidad de 16 bits por píxel (bpp). Pero utilizando la información del SDL_PixelFormat se puede generalizar.
En 16 bpp, el color se descompone por bits de la siguiente manera: RRRR RGGG GGGB BBBB. El rojo es de 5 bits, el verde de 6 y el azul de 5. Ya que el ojo humano es más sensible al verde. Por lo tanto, para obtener el rojo, se tiene que desplazar 11 bits a la derecha.
El parámetro color no hace falta descomponerlo porque es de solo 5 bits y solo sirve para generar una escala de grises haciendo que tanto la parte roja del píxel destino, la verde y la azul tomen el valor de color: C4,C3,C2,C1, C0,C4,C3,C2, C1,C0,1,C4, C3,C2,C1,C0
Código: Seleccionar todo
    unsigned int rgb_color = (int(r*alp + color*(1-alp))<<11) | (int(g*alp + ((color<<1)|1)*(1-alp))<<5) | (int(b*alp + color*(1-alp)));
    *pdest = rgb_color;
}

Aquí es donde se mezclan los dos colores. La mezcla se hace con los valores rojo, verde y azul de los dos colores de forma separada.
Para hacer la mezcla, se hace, con la media ponderada. Por ejemplo si alpha vale 0.25, sería 25%de r (o g o b) + 75% de color.
Una vez hecho esto con r, g y b, y desplazarlos a su respectivo lugar, solo queda sumar los bits con la or bit a bit.
Avatar de Usuario
endaramiz
 
Mensajes: 283
Registrado: Vie Ago 31, 2007 9:25 am
Ubicación: Barcelona


Volver a Artículos, traducciones y documentación

¿Quién está conectado?

Usuarios navegando por este Foro: No hay usuarios registrados visitando el Foro y 1 invitado