Wiki
Tutoriales Programacion Metaballs
Metaballs
1. ¿Qué son las Metaballs?
En este tutorial pretendo explicar cómo se consigue el efecto de gotas de agua mezclándose, alargándose y estirándose, según se acercan unas a otras. El efecto es parecido al de una serie de esferas moviéndose y deformándose como si fueran gotas de mercurio.
Requerimientos imprescindibles: Conocer el lenguaje C y OpenGL o alguna otra API de 3D (por ejemplo, Direct3D). Yo usaré OpenGL porque es la única que sé, y el compilador Visual C++ 6.0.
2. Explicación en 2D
Para ver como se consiguen las Metaballs en 3D vamos a explicar el proceso en 2D en primer lugar, ya que aclarará mucho la comprensión del efecto.
Imaginemos los ejes de coordenadas en el cuadrante positivo para la X y la Y. Dividimos ese plano en una malla de puntos equidistantes. Nos quedaría algo así:
fotos.miarroba.com/fotos/8/d/8d725fde.jpg
Esto es discretizar el plano, pues sólo usaremos esos puntos para dibujar, ya que usar todo el plano sería computacionalmente costosísimo, pues habría que hacer cálculos para todos los puntos del plano y ¿cuántos puntos tiene un plano?, infinitos, ya que nos vamos a mover por los números reales, por eso sólo usaremos unos pocos puntos.
Ahora colocamos en ese plano, por ejemplo, 2 metacircunferencias, y les asignamos un valor a sus coordenadas (posicionar la metacircunferencia) y a su MASA, ya que nos vamos a basar en el campo gravitatorio para calcular las metacircunferecias. Éstas las vamos a representar con 3 valores en 2D y 4 en 3D, que serán: coordenadas espaciales y masa (en el caso de 2D, una metacircunferencia estará definida por X, Y y MASA).
Ahora debemos realizar el siguiente cálculo para cada uno de puntos pertenecientes a la malla del plano: calcularemos un valor de campo en función de la distancia desde ese punto al centro de cada metacircunferencia y sumarlos (cada punto de esa matriz de puntos también tendrá sus coordenadas y su MASA o valor de campo). Por ejemplo, si tenemos N metacircunferencias, el cálculo para cada punto, basándonos en la fórmula del cálculo del campo gravitatorio que ejerce una masa sobre un punto sería:
(F=MASA/DISTANCIA_MASA_AL_PUNTO²)
aux=0.0; para cada punto de la matriz hacer:
aux = metacircunferencia1.MASA/(distancia(punto_actual.posición, metacircunferencia1.posición)²);
aux+= metacircunferencia2.MASA/(distancia(punto_actual.posición metacircunferencia2.posición)²);
.
.
.
aux+= metacircunferenciaN.MASA/(distancia(punto_actual.posición, metacircunferenciaN.posición)²);
FinPara
punto_de_matriz.MASA=aux;
Para aclarar el anterior algoritmo, tanto el punto_actual, como la metacircunferenciaX tienen la misma estructura:
struct MC
{
float posición;
float MASA
};
struct posición
{
float X;
float Y;
};
La distancia entre dos puntos (p1 y p2 por ejemplo) se calcula mediante la fórmula: raiz_cuadrada((p1.x-p2.x)²+(p1.y-p2.y)²)
(Una cosa a tener en cuenta es que hay que controlar que la distancia no sea 0 ya que nos daría una excepción de división por 0)
En este punto ya tenemos la matriz totalmente calculada, ahora es cuando nos preguntamos ¿cómo de grande queremos que sea la metacircunferencia? Esto viene dado por dos valores, uno es la masa de la metacircunferencia (a más masa, mayor será el cociente de antes) y otro es un nuevo valor, al cual llamaré CAMPO_LIMITE, y quiere decir qué valores de la matriz de puntos van a estar dentro de nuestra MC (metacircunferencia, para abreviar) y cuáles fuera, de esa manera estableceremos el límite de la MC.
Ya tenemos la matriz calculada y establecido el valor límite, ahora tenemos que pintar la MC. ¿Cómo lo hacemos? Pues para eso, vamos a recorrer la matriz de puntos; en vez de hacerlo punto a punto lo haremos en grupos de cuatro (a esto en 3D lo llamaremos Marching Boxes, ya que en 3D será un "cubo" de 8 vértices, y en 2D es una celda de 4 puntos). Bien, recorriendo de esta forma la matriz podemos encontrarnos con los siguientes casos en función de que algunos de los puntos de la celda tengan un valor de MASA mayor que el de CAMPO_LIMITE (los puntos rojos), a lo que llamaré "estar dentro del campo":
fotos.miarroba.com/fotos/4/5/45a69b1a.jpg
Esta es la tabla de todas las combinaciones posibles de que entren vértices o de que dejen de entrar en el campo (los que están dentro del campo son, recordad, los que tuvieran más masa que el CAMPO_LIMITE, y están en color rojo). El caso 1 y el caso 16 se tratan igual (si están todos los vértices o bien fuera, o bien dentro del campo no se pinta nada). En los demás casos hay que ver cómo serían las líneas que se dibujarían según cada caso. Lo muestro en el siguiente gráfico:
fotos.miarroba.com/fotos/9/b/9b9d49db.jpg
Aunque no os lo creáis, todos estos casos forman las MC, ya que los puntos rojos tienden a colocarse uniformemente alrededor de la MC. Aquí os pongo un ejemplo de cómo quedarían dos MC suficientemente acercadas como para que compartan puntos rojos:
fotos.miarroba.com/fotos/a/9/a99ee126.jpg
Estaría bien que comprobarais con la tabla de casos cómo se van dibujando las rayas, comenzando a recorrer la matriz por donde queráis y sin saltaros ningún punto.
Lo cerca o lejos que están las rayas de los puntos rojos se puede determinar de dos maneras: a) simplemente colocando los extremos de cada línea a medio camino entre el punto rojo y el negro (forma chapuza, pero que cuela si usamos mucha resolución en la matriz de puntos), o b) interpolando en función del valor del CAMPO_LIMITE entre el punto rojo y en el punto negro, de esta manera, el extremo de la raya estará proporcionalmente más acercado al punto con mayor MASA.
Como curiosidad: ¿os acordáis de las isobaras de los mapas del tiempo? Pues son aquellas líneas (curvas) que unen los puntos con igual presión atmosférica, pues las rayas que hemos dibujado son las líneas que unen los puntos con "igual" (según la resolución de puntos de malla que uséis) campo gravitatorio generado por la MC. En física se las llama superficies equiescalares (cuando son superficies, en nuestro caso son líneas equiescalares), ya que unen los puntos con el mismo valor escalar de campo (sí, soy un pesado, pero yo también me aburro de hablar sólo de Metaballs :P).
Con más resolución que la que yo he utilizado como ejemplo, los lados de las MC aumentarían, pareciéndose cada vez más a una circunferencia, y para realizar una animación, sólo habría que mover el centro de las MC y volver a calcularlo todo, haciendo todo eso una vez por frame.
El concepto ya está explicado, ahora vamos a por las verdaderas Metaballs, en 3D.
3. MetaBalls en 3D
Bueno, pues en 3D la cosa es exactamente igual que en 2D, excepto por que en vez de utilizar una celda de 4 puntos, vamos a usar una caja de 8 vértices, y esto complica mucho los cálculos. De mano ya no tenemos sólo 16 casos, sino 256, y además ya no son rayitas lo que tenemos que dibujar, sino uno o varios triángulos de cada vez, ¿vais viendo por dónde van los tiros?
Para empezar ya no tenemos plano, sino espacio, el cual debemos dividir en una matriz tridimensional de puntos (a ser posible que tenga la misma dimensión de alto, ancho y profundidad). Nuestra unidad mínima de análisis de la malla serán los cubos formados por 8 vértices, de esta manera recorreremos la matriz, pero de cubo en cubo.
fotos.miarroba.com/fotos/3/7/374bbdb2.jpg
Ejemplo de MarchingBox
Pues vuestra malla han de ser muchas de esas marchings apiladas.
Ahora tenemos que calcular toda la matriz tridimensional (la malla) con los valores de campo, igual que en 2D, pero añadiendo en las ecuaciones la coordenada Z, que antes no la metíamos (para hacer este cálculo no hace falta que recorramos mediante marchings, ya que sólo nos importa que cada vértice de la malla tenga su valor de MASA).
Una vez hecho todo esto SÍ deberemos recorrer Marching Box a Marching Box, pues cada marching nos dará qué caso tenemos que pintar, en función de qué vértices estén dentro del campo y cuáles fuera. Para que os hagáis una idea, los casos en 3D los trato de la siguiente manera, hay 16 casos básicos, todos los demás derivan de éstos, aplicándoles rotaciones, y además el caso básico 0 y el caso básico 255 son el mismo ya que uno tiene todos vértices fuera del campo y el otro los tiene todos dentro de él, así que no se pinta nada en ninguno de los dos casos, además hay otro caso que también se puede eliminar, ya que se deduce de los demás (para más información bajaos el programa que realicé para ver los casos básicos, y que contiene un readme.txt con todo lo relacionado sobre el tratamiento de los casos. Podéis bajároslo pinchando aquí, sobre todo si queréis implementar las metaballs por vosotros mismos).
fotos.miarroba.com/fotos/9/1/913c6f6a.jpg
Esta es una captura de pantalla de ese programa.
Aquí no se ven muy bien, pero con el programa podréis rotar las cajas a vuestro gusto.
Creo que no me queda más que decir, todo lo necesario para realizar Metaballs que no está aquí, sino que está en el archivo readme.txt del programa que os muestra los casos básicos. Lo he incluido ahí porque es información bastante técnica, y sólo le interesaría a aquellas personas que vayan a hacer Metaballs y no a las que sólo quieran echarle un vistazo general al método que he utilizado aquí.
¡Ah, sí! Una cosa más, para mover las Metaballs, sólo tenéis que mover su centro. Y además, cuando hayáis acabado de dibujar el último Marching Box de la malla, habréis acabado un frame.
Podéis bajaros mi programa, versión ejecutable o el código completo del proyecto para Visual C++ 6.0, el cual implementa las Metaballs; pero leed el readme.txt que viene con él (sí, otro readme.txt :P).
En este programa he utilizado normales de cara y no de vértice, ya que en este caso es bastante complicado, y costoso computacionalmente hablando, el implementarlo, por eso las metaballs no se verán como bolas, sino como bolas con caras. Si hubiera utilizado iluminación de vértice, habrían quedado perfectamente redondeadas :P.
Por último os voy a poner un algoritmo en pseudocódigo de nivel 1 sobre cómo hacer las metaballs. Cada llamada a ese algoritmo, generaría un frame de la animación:
calcular_valores_de_campo_de_la_matriz();
para z=1 hasta maxZ hacer
inicio
para y=1 hasta maxY hacer
inicio
para x=1 hasta maxX hacer
inicio
asignar_valores_a_los_vertices_de_la_marching_box(mBox, x, y, z);
caso=calcular_de_que_caso_se_trata_segun_los_vertices_de_la_MB_que_esten_dentro_del_campo(mBox);
caso=caso - 1; // a causa de que el caso 0 ha sido eliminado (readme.txt del programa Casos)
dibujar_caso(mBox,caso);
fin
fin
fin
actualizar_posiciones_de_las_metaballs();
4. Aclaraciones
Ante todo, una serie de aclaraciones: Me reservo el Copyright sobre el código que os ofrezco, de manera que sólo pretendo que me aviséis si vais a utilizarlo en vuestros proyectos (más que nada por curiosidad :P), pero podéis hacer con él lo que queráis.
El programa que he realizado y que ilustra el efecto Metaball, no es metodológicamente exhaustivo, ni está verificado formalmente, pero no "rompe", y funciona de acuerdo con las especificaciones, así que por favor, no me envíen mails diciendo si esto se podría haber programado mejor o de otra manera, ya que estoy muy seguro de que se puede hacer mucho mejor de como yo lo he hecho, pero sí acepto todas las dudas que podáis tener.
Tutorial original extraído de la página web Codepixel escrito por Jacobo Rodríguez Villar, reformateado por pi^m y levemente corregido gramaticalmente por Firefox.
[mailto:ffelagund(signo_de_arroba)telecable.es E-mail del autor]
