Comenzando una serie de artículos sobre el tema vamos con el efecto fuego.
Se trata probablemente del efecto mas simple, no por ello, menos llamativo.
No vamos a realizar una simulación, ya que una tarea compleja de dinámica de fluídos; Nos limitaremos a implementar un algoritmo simple, conocido y utilizado desde la vieja escuela.
La implementación será realizada en Java (NetBeans 8.0.02 + JDK 1.7) por su portabilidad, no así por su rendimiento.
El efecto es de tipo "generación de textura en tiempo real". Se articularán las herramientas para poder reutilizar el presente ejemplo en futuros artículos (efectos de deformación, renderizado, etc).
A los bifes
Para la generación del efecto haremos uso de un buffer de efecto (cálculo) y otro de imagen (representación)
A su vez, el buffer de efecto tendrá un doble buffer (copia), ya que algunos de los procesos son destructivos (suavizado, enfriamiento, convección).
Cada elemento del buffer de efecto es una representación numérica de la intensidad del fuego en un punto del posterior volcado en pantalla.
A mayor valor numérico, fuego mas intenso (tendiendo a amarillo o blanco a nivel gráfico).
A la inversa, a menor valor numérico, menos intenso (tendiendo a rojo o negro a nivel gráfico).
A cada valor de intensidad se le asignará posteriormente un color (Paleta de colores).
El buffer de imagen tendrá las dimensiones del efecto que se quiera representar.
En el ejemplo de 256 x 128 pixeles (32768 pixels).
El buffer de efecto será ligeramente mas grande: Un par de pixeles en altura (parte inferior) en donde se generará la ignición. Este sector no se transferirá el buffer de imagen (no será visible).
Paleta de colores
Probablemente el punto crítico del efecto.
Con la paleta adecuada, el efecto es casi siempre agradable a la vista.
La paleta incorrecta, no solo da aspecto de irreal (colores); También afecta la convección y altura del efecto.
Para el ejemplo utilizaremos una paleta de 112 colores
16 colores [0..15] -> Negro a Rojo (menor intensidad)
32 colores [16..47] -> Rojo a Amarillo (pasando por naranja)
32 colores [48..80] -> Amarillo a Blanco
32 colores [80..112] -> Blancos (mayor intensidad)
Generación del efecto
Para simplificar la explicación, vamos a dividir la generación del efecto en las siguientes etapas:
(1) Ignición
(2) Propagación de calor
(3) Enfriamiento
(4) Convección
(1) Ignición (generación de llama)
La generación de llama consiste en agregar puntos de calor (valores altos) al buffer de efecto de forma aleatoria y normalmente en la parte baja (fuego desde abajo hacia arriba).
En cada ciclo de animación refrescaremos un par de líneas en la parte inferior del buffer de efecto, que, por lo anteriormente comentado, no formará parte del área visible del efecto.
Pseudocódigo de ignición
para cada x
buffer(x, 0) = numero_al_azar()
(2) Propagación de calor
La "magia" del efecto, es tan simple como un "suavizado" (smooth) del buffer de efectos.
Hay muchos tipos de "suavizado", para el ejemplo utilizaremos el mas simple y eficiente: Media aritmética.
En cada ciclo de la animación, a cada punto lo reemplazaremos por la media aritmética de él y sus vecinos:
Pseudocódigo de propagación del calor
para cada x, y
buffer(x, y) = (
buffer(x, y) +
buffer(x + 1, y) +
buffer(x - 1, y) +
buffer(x, y + 1) +
buffer(x + 1, y - 1)
) / 5
(3) Enfriamiento
La técnica consiste restar un valor aleatorio a cada elemento del buffer de efectos.
Lo de "valor aleatorio" es relativo; Utilizaremos una máscara (otro buffer) generada a partir de valores aleatorios, lo que contribuirá de manera significativa al rendimiento por no tener que generar valores en tiempo real.
Con una paleta estándar, al "restar" estamos oscureciendo o quitando intensidad al fuego en ese sector.
La finalidad es darle mas dinámica al efecto (movimiento), favorecer la convección, darle aspecto aleatorio a el efecto, etc.
Pseudocódigo de enfriamiento
para cada x, y
buffer(x, y) = buffer(x, y) - buffer_enfriamiento(x, y)
si buffer(x, y) < 0
buffer(x, y) = 0
Utilizaremos una máscara de enfriamiento (buffer circular simétrico) generada a partir de un relleno al azar suavizado, lo que le conferirá homogeneidad.
Ciclo a ciclo de la animación haremos un scroll ascendente a la máscara, lo que favorecerá la convección.
//relleno al azar
para cada x, y
buffer_enfriamiento(x, y) = numero_al_azar()
//suavizado
repetir 20
para cada x, y
buffer_enfriamiento(x, y) = (
buffer_enfriamiento(x, y) +
buffer_enfriamiento(x + 1, y) +
buffer_enfriamiento(x - 1, y) +
buffer_enfriamiento(x, y + 1) +
buffer_enfriamiento(x, y - 1)
) / 5
(4) Convección
Tiene la finalidad de darle "aspecto ascendente" a nuestras llamas.
En cada ciclo de animación, haremos un scroll ascendente de 1 o 2 líneas sobre el buffer de efecto.
Código fuente / binarios compilados
- Código fuente - Clase Fuego: Fuego.java
- Aplicación compilada: Fuego.jar
- Proyecto de NetBeans (8.0.2): Fuego.zip
Observaciones
Por cuestiones de rendimiento y compatibilidad no se utilizarán vectores de dos dimensiones, en su lugar usaremos vectores unidimensionales.
Se hará uso de un único tipo de entero (entero de 32bits con signo) por no detectarse variaciones significativas en el rendimiento y complicarse la implementación y legibilidad del código (casteos, control de desbordes, etc).
Variaciones del efecto
Controlando la ignición es posible lograr variaciones vistosas del efecto.
Propongo al lector en lugar de generar ignición en la parte baja del efecto, utilizar máscaras con texto, formas geométricas, gráfica, máscaras animadas (bolas de fuego), etc.
Doy por terminado este primer artículo, espero el contenido haya sido de utilidad.
Cualquier duda no duden en consultar, nos vemos en la siguiente entrada.
Saludos, Luis.-