| PRINCIPAL | GRAFICOS 3D | GRAFICOS 2D | MATEMATICAS | TUTORIALES | FRACTALES | FUENTES | LINKS |


COMPOSICIÓN Y ÚLTIMOS DETALLES
por Olmo del Corral (olmobrutall@hotmail.com)

Como digímos en un principio, las texturas procedurales están compuestas de mapas. Hasta ahora hemos conseguido crear los mapas Perlin Noise y Cellulas, pero no les hemos añadido color ni hemos probado a combinarlos. En esta sección tocaremos principalmente tres cosas:

1.- Utilización de la interpolación lineal para el coloreado con degradados.

2.- Algunas maneras de combinar mapas. Ejemplo: Displace.

3.- Componiendo el mapa Caustic.

4.- Prueba de nuestro programa en el entorno C++ Builder.

Coloreado con degradados:
Es el momento de darle color a los mapas para hacer texturas. Hay varias maneras, por ejemplo hacer mapas específicos para cada canal RGB, o modificar un mismo mapa y poner distintas modificaciones en distintos canales. Hay otra manera, la más sencilla, aplicar un degradado (Como los del Photoshop) que devuelva un color RGB para cada nivel en la escala de grises de los mapas.

Para empezar haremos uso de la interpolación lineal, pues en este caso solo vamos a tener dos valores conocidos (el color inicial y el final), y no merece la pena hacer una interpolación polinómica.

float InterLin(float a, float b, float x){     //a altura 1
return a+(b-a)*x; //b algura 2
} //x si = 0 entonces a, si = 1 entonces b

Ahora lo único que hay que hacer es conocer las coordenadas en el espacio RGB de los colores que queremos interpolar, es decir, saber que cantidad de Rojo, Verde y Azul tienen ambos colores. Para nuestro ejemplo utilizaremos los colores de fondo del agua:

 
R
G
B
20
68
82
 
91
187
201

Nuestra función color_agua ha de devolver el color (20,68,82) si se le pasa como parámetro el 0 (máxima oscuridad), y el color (91,187,201) si se le pasa el 1 (punto de máxima iluminación por la luz cáustica). Lo que ha de hacer por tanto es hacer una interpolación lineal de cada componente del color, pasando a InterLin el mismo parámetro que ella recibe, y luego componer las coordenadas en un int de color RGB.

int color_agua (float valor){
int r=InterLin(20, 91, valor);
int g=InterLin(68, 187, valor);
int b=InterLin(82, 201, valor);
return RGB(r,g,b);
}

También se puede jugar mucho con esto, por ejemplo, se puede utilizar un espacio de color distinto al RGB, en otro tutorial me gustaría hablar del espacio HSV (Tono, Saturación y Brillo) que permite hacer efectos arco-iris, de cambio de luminosidad, o de cambio de cantidad de color (saturación) con mucha facilidad.

Combinaciones de mapas:

Hasta ahora habíamos generado mapas: Algoritmos capaces de generar imágenes exentas de color que componen una textura procedural. Después de ver como podemos colorear un mapa, es el momento de ver las distintas formas que hay de componerlos:

- Superposición: Es sencillo conseguir que de la impresión de que dos mapas están superpuestos como hojas de papel cebolla. Lo único que habría que hacer es, para cada punto, que el color que se represente sea la media de los valores que ambos mapas dan para ese punto. Podemos hacer también que uno tenga más fuerza que otro (como si su papel fuera menos transparente) dándole más peso en una media ponderada:

float c_ajedrez= Ajedrez(i,j,...);
float c_perlin= PerlinNoise (i,j,...);
float c_final = c_ajedrez*0.25+c_perlin*0.75;
int color = colorear(c_final);
Pintar(i,j,color);

Así conseguiríamos una extraña textura, en un 25% un tablero de ajedrez y en un 75% un mapa Perlin Noise.

Otra variante de este efecto:
Multiplicación de componentes: De esta manera podemos conseguir que el mapa final solo aparezca con colores claros cuando ambos mapas son claros (pues ambas componentes están en [0..1]), se pueden conseguir efectos curiosos con ésta técnica. Sin embargo, las imágenes tienden a ser muy oscuras, efecto que se puede evitar haciendo la raíz cuadrada del color final.

- Un mapa como parámetro de otro: Por lo general la función de un mapa de una textura procedural se compone básicamente de dos parámetros (coordenada x y coordenada y), y un conjunto auxiliar de parámetros para configurar el efecto: Octavas, tamaño de las celdas, grosor de las líneas, rugosidad de la madera, etc.. Podemos introducir por uno de esos parámetros el valor obtenido por otro mapa en ese punto, por ejemplo, si tenemos un mapa Brick (ladrillo) que genera la textura de una pared al descubierto y en el que tenemos definido los parámetros, anchura y altura del ladrillo, y tamaño del borde:

float Brick(float x,y, float altura, anchura, borde);

Podríamos entonces pasar el resultado de un mapa Perlin Noise por el parámetro del borde:

float perlin = PerlinNoise(i,j,...);
float valor_final= Brick(i,j,50,150,5+5*perlin);


Así conseguiríamos que el tamaño del borde variara de 5 a 10 de manera aleatoria pero coherente, consiguiendo un efecto similar un borde mal acabado, como si el cemento hubiera sido puesto con no demasiada maestría.

DISPLACE:
Podría considerarse la combinación de mapas con Displace como un caso particular de uso de un mapa como parámetro de otro, sin embargo tiene ciertas particularidades:

- En el efecto displace se utilizan los valores de un mapa en los parámetros X e Y del otro (el que será desplazado), no en los parámetros auxiliares.

- No se pasa tan solo el valor del mapa como parámetro, sino la suma entre las coordenadas naturales (los índices i j del for) con éste, consiguiendo así que aunque el efecto final sea un Brick con una rugosidad a baja escala (los pixels se desplazan 10 posiciones), en las altas tendencias no varía, pues i sigue aumentando.

float perlin = PerlinNoise(i,j,...);//[0..1]
float valor_final= Brick(i+10*perlin,j,50,150,5);
int color = colorear(brick);


- Por lo general cuando se combinan mapas con una técnica displace, se utilizan dos valores de dos mapas distintos para sumar, cada uno, a la coordenada x y a la y del mapa que se quiere desplazar, para hacer que el efecto de deformación no sea unidimensional. De esta manera cuando el primer mapa tenga colores claros el mapa sobre el que se aplica el efecto se desplazará hacia la izquierda, y cuando tenga valores oscuros hacia la derecha. Análogamente, cuando el segundo mapa tenga valores claros se desplazará hacia arriba, y cuando sean oscuros hacia abajo.

float perlinx = PerlinNoise(i,j,..,123);
float perliny = PerlinNoise(i,j,...,52352); // donde el valor que varía de uno a otro es el seed. float valor_final= Brick(i+perlinx*20,j+perliny*20,50,150,5);
int color = colorear(brick);


Así conseguimos una pared de ladrillos completamente deformada. En este caso no es muy interesante, sin embargo con este efecto se consiguen realizar texturas como el mármol, desplazando un simple mapa de un degradado lineal vertical con un Perlin Noise con muchas octavas (para que tenga un gran nivel de detalle). El mapa de un degradado lineal sería:

float degradado(float x, float y, float periodo){ //y no vale para nada
x=(x - x*(x/periodo)) /periodo; // con lo que consigues reducir todas las x a valores entre [0..1]
x = min(2*x,-2x+2); // función que tiene como gráfica: /\ entre [0..1] sin que sus imágenes sobrepasen el 1.
}

Utilizaremos también el efecto de combinación de mapas displace en nuestro programa final para generar la textura de fondo de piscina, desplazando una textura cellular con una mapa Perlin Noise para dar más realismo, pues la textura del fondo de una piscina es, en síntesis, pequeñas parcelas como el cellular en las que se iluminan los bordes y, especialmente, las esquinas, con una enorme deformación.

Finalmente, la principal dificultad que tiene el mapa displace es la tentación que produce en algunos casos hacer el proceso de desplazamiento (suma del resultado del mapa 'desplazador' a las coordenadas naturales), en la instrucción de pintado y no en la de captación de color, lo explicaré con dos ejemplos:

// Textura Marble (mármol)
    // Incorrecto
     float perlinx = PerlinNoise(i,j,..,123); 
     float perliny = PerlinNoise(i,j,...,52352);
     float degradado = degradado (i,j,200);
     int color = colorear(degradado); // pasa de escala de grises, a un degradado de rosas, ocres, o algún otro color pastel          
Pintar(x-perlinx, y-perliny, color); //ERROR! desplaza en el momento de pintado

    // Correcto
     float perlinx = PerlinNoise(i,j,..,123); 
     float perliny = PerlinNoise(i,j,...,52352);
     float degradado = degradado (i+perlinx,j+perliny,200); // correcto, desplaza          en la instrucción de captación de valores (colores)
     int color = colorear(degradado); // pasa de escala de grises, a un degradado          de rosas, ocres, o algún otro color pastel para el marmol
     Pintar(x, y, color);

Primeramente comentar que para conseguir, al menos en teoría pues veremos que no es así, el mismo efecto desplazando, en un algoritmo, en el la instrucción de captación de colores, y en otro, en el momento de pintado, hemos de sumar en uno y restar en otro, sino se consiguen efectos inversos. Después de este pequeño detalle veremos porqué la solución de abajo es óptima, mientras que la de arriba es un error:

- La solución de arriba no respeta la primera ( y creo que única) restricción que puse en la primera sección acerca del orden estricto de pintado, no nos podemos permitir la libertad de pintar a nuestro antojo los puntos, pues nuestras texturas han de esta preparadas para devolver un color para el punto pedido, no para un punto 'de por ahí cerca' como hace este,

- La solución de arriba no garantiza que se pinten todos los puntos pues estamos desplazando aleatoriamente puntos por la pantalla en el ultimo momento, lo más probable es que hay muchos puntos que queden huérfanos, sin valor, y queden pintados de blanco, o del color de fondo, pues la textura queda como 'rota' en cuanto haya algún cambio de valor en el mapa desplazador.

- En ocasiones la textura intentaría pintarse fuera del área de pintado, pues pintaría en coordenadas no válidas, lo que puede dar error.

- A nosotros nos puede interesar, en ocasiones, encapsular varios mapas para producir, no otra textura, sino un mapa más complejo que haga uso de otros (ej: float Marmol(float i,j,periodo,rugosidad); ) , en este caso es imprescindible que cuando le pida el color en el punto (x,y) me devuelva el color de ese punto, no del punto (x-perlinx, y-perliny) por ejemplo.

Combinaciones de mapas:
No voy a fingir en esta sección que 'está muy claro' como se compone una textura de luces cáusticas del fondo de una piscina. La realidad es que el proceso de composición de una textura es, en gran medida un proceso de prueba y error y... afortunadamente en este caso, de algún que otro chapuzón en la piscina para ver el grado de realismo ;). Voy a dar por supuesto que están ya implementados y se entiende lo que hacen las siguientes funciones:

- float PerlinNoise(float x,float y,int width,int octaves,int seed)
- float Cellular(float x,float y,int width,int tam_cas, int seed) - int color_agua (float valor)

La implementación del mapa cellular está configurada para devolver el valor de primero*primero/(segundo*tercero) pues me ha parecido la configuración más apropiada (enfatiza el brillo de los vértices de los polígonos y tiene un aspecto curvo)

Además vamos a trabajar durante todo el proceso de cálculo con coordenadas de 0 a 1 para los colores (y no de 0 a 255 como hemos echo en toros casos). Será color_agua() quien acabe componiendo el color con la escala 'normal' de 0 a 255. Vamos allá:
Estando declarado:

int dispx,dispy;
double colr,colr1;


colr = Cellular(i,j,width,64,seed);
colr=color_agua(colr);
Con esto simplemente crearemos un mapa cellular base, con un tamaño de celda de 64 pixels, y luego lo pintamos con el degradado de azules-turquesas que definimos en la función color_agua. Por ahora el parecido con el fondo de una piscina es pura coincidencia, pero tiempo al tiempo.

colr = Cellular(i,j,width,64,seed);
colr1 = Cellular(i,j,width,48,seed+1000);
colr=(colr*0.60+colr1*0.40);
colr=color_agua(colr);
Un detalle que me fijé en una de mis expediciones submarinas por la piscina es que la iluminación cáustica tiene un poco de autosimilitud (que al bajar la escala se repiten las mismas formas, como en los fractales), así que es interesante utilizar una segunda configuración del mapa celular, esta vez con algo menos de tamaño de las celdas, y superponerlos con una media ponderada (como vimos antes) para dar algo más de peso al mapa primario de las células grandes.

    colr = Cellular(i,j,width,64,seed);
colr *= colr;
colr1 = Cellular(i,j,width,48,seed);
colr1 *= colr1;
colr=(colr*0.60+colr1*0.40);
En este paso lo que he echo es multiplicar el valor de ambos mapas por si mismo, esto es, hacer sus cuadrados. Puesto que ambos están en el intervalo [0..1], el efecto será que los números bajos ( 0.1 , 0.25, 0.5...) bajaran (0.01, 0.0625, 0.25 ..) mientras que los valores altos (0.8, 0.9..) permanecerán más o menos estables
(0.64, 0.81..). De esta manera conseguimos hacer que la luz parezca mas intensa donde da, más contrastada, y mas escasa donde no llega.

    dispx= PerlinNoise(i,j,width,3,seed)/3;
dispy= PerlinNoise(i,j+5,width,3,seed)/3; colr = Cellular(i+dispx,j+dispy,width,64,seed);
colr *= colr;
colr1 = Cellular(i+dispy,j+dispx+2312,width,48,seed);
colr1 *= colr1;
colr=(colr*0.60+colr1*0.40);
Aquí encontramos el meollo de la cuestión. Hemos echo un displace de dos mapas celular con dos mapas Perlin Noise. Teóricamente necesitaríamos 4 (2 para el primero: x e y, y otros dos para el segundo), pero hemos reutilizado cada mapa una vez más para ahorrar cálculo, total, no se nota, pues hemos invertido el orden (el que desplaza las x en uno desplaza las y en otro) y además hemos desplazado un mapa 2312 pixels (para deshorientar). Conseguimos así un efecto ya bastante parecido al fondo cáustico. Pero falta algún detalle.


Cambiando las instrucciones:

    colr = Cellular(i*0.6+dispx,j+dispy,width,64,seed);
colr1 = Cellular(i*0.6+dispy,j+dispx+2312,width,48,seed);

Conseguimos que se 'alargue' el eje de las x, así podemos 'orientar' un poco las sombras de la misma manera que están orientadas las ondas en la superficie que las producen. Así quedaría finalmente.

4.- Prueba de nuestro programa en el entorno C++ Builder.

He programado nuestro generador de texturas en C++ Builder pues es el entorno que mas facilita la creación de una interface, para el volcado a pantalla he utilizado el siguiente método: (con un TImage creado llamado Image1 en Form1)

Graphics::TBitmap *pBitmap = new Graphics::TBitmap();
pBitmap->Width=Image1->Width;
pBitmap->Height=Image1->Height;

Hago un bitmap (clase que almacena en memoria una imagen) de las dimensiones del control Image1 (donde pintaremos)

pBitmap->Canvas->Pixels[i][j]=colr;

Para cada punto pinto en el bitmap, alojado en memoria RAM como he dicho.

Image1->Canvas->Draw(0,0,pBitmap);


Finalmente, después del for, hago un volcado del bitmap en Ram al Image1 (en memoria de video). Este procedimiento pasa de los 1.5 segundos que tarda en pintarse 512*512 puntos directamente en el Image, a 0.5 segundos (en mi ordenador, y descontando todos los cálculos). Además al formulario le he añadido una barra de progreso, un label que indica el tiempo tardado, y un TextBox para introducir el seed y producir una imagen distinta pero de las mismas características.
El Programa principal, finalmente, quedaría así:

void __fastcall TForm1::Button1Click(TObject *Sender)
         {
         Graphics::TBitmap *pBitmap = new Graphics::TBitmap();
         pBitmap->Width=Image1->Width;
         pBitmap->Height=Image1->Height;
         clock_t start=clock();
         ProgressBar1->Max = Image1->Height;
         ProgressBar1->Position = 0;
         int height = Image1->Height;
         int width = Image1->Width;
         int seed = StrToInt(Edit1->Text);
         int dispx,dispy;
         double colr,colr1;
    for (int j=0; j<height; j++){
      for (int i=0; i< width; i++){
         dispx= PerlinNoise(i,j,width,3,seed)/3;
         dispy= PerlinNoise(i,j+5,width,3,seed)/3;
         colr = Cellular(i*0.6+dispx,j+dispy,width,64,seed);
         colr *= (colr);
         colr1 = Cellular(i*0.6+dispy,j+dispx,width,48,seed);
         colr1 *= (colr1);
         colr=(colr*0.60+colr1*0.30);
         colr=color_agua(colr);
         pBitmap->Canvas->Pixels[i][j]=colr;
       }
       ProgressBar1->Position++;
     }
         AnsiString as;
         
         Image1->Canvas->Draw(0,0,pBitmap);
         pBitmap->SaveToFile("C:\causctic.bmp");
         Label1->Caption=IntToStr(clock()-start)+" ms" ;         delete pBitmap;
         }

DOWNLOADS:

CODIGO FUENTE FINAL DE LA APLICACIÓN (25 kb)
EJECUTABLE (28 kb)

Ambos requieren Borland C++ Builder 5

SIGUIENTE

 


| PRINCIPAL | GRAFICOS 3D | GRAFICOS 2D | MATEMATICAS | TUTORIALES | FRACTALES | FUENTES | LINKS |