| PRINCIPAL | GRAFICOS 3D | GRAFICOS 2D | MATEMATICAS | TUTORIALES | FRACTALES | FUENTES | LINKS |
ÍNDICE E INTRODUCCIÓN
por Olmo del Corral (olmobrutall@hotmail.com)
ÍNDICE
- Introducción:
Qué son las texturas procedurales? Ventajas y desventajas.
Tipos de mapas: Geométricos y aleatorios
Manos a la Obra: Normas de estilo y la función IntNoise
- Composición y últimos detalles de la
textura cáustica
DOWNLOADS:
CODIGO FUENTE FINAL
DE LA APLICACIÓN (25 kb)
EJECUTABLE
(28 kb)
Ambos requieren Borland C++ Builder 5
Qué son
las texturas procedurales?
Son imágenes que no se almacenan en el disco duro como mapas de bits (BMP, JPG, EPS, PSD...) sino que se almacena el procedimiento, el algoritmo (matemático) que es capaz de generarlo.
Ventajas:
Son Densas: Así como un mapa de bits está definido de manera
discreta (sabemos el color del punto [3,4]) una textura procedural está
definida para todo R^n (R^2 o R^3 por lo general), esto es... que podemos pedirle
el color del punto [3.1415.., 2.1232..] ¿y para qué vale esto?.
Pues porque así nos podremos acercar a ella tanto como queramos, y nunca
veremos esos molestos cuadraditos típicos del zoom, o de tener la pared
muy cerca en el Counter Strike ;). Además, si la textura está
bien hecha, podremos ver nuevos detalles mientras más nos acerquemos.
Son Infinitas: Como hemos dicho, están definidas para todo el espacio, esto quiere decir que podemos consultar sin problemas que color tiene el punto [2e234,-5e324], además, la textura no se repetirá (al menos que así lo queramos) en ningún momento. ¿Imaginan ir jugando a un juego de carreras, conduciendo durante 3.000 Km sobre un camino de arena, y que este no se repita en todo el viaje??? Algo imposible con una textura en mapa de bits.
Ocupan poco espacio: Una imagen grande, en mapa de bits... ocupa mucho!. Por ejemplo un bmp de 256*256 pixels a color de 24 bits son 1.5 Megas, bien, una buena compresión puede bajar esto hasta al 5% más o menos, pero no puede hacer milagros. Sin embargo, una textura procedural son unas pocas líneas de código. Además, donde las texturas procedurales son útiles es donde los bmp hacen aguas: Texturas muy grandes, de mucha resolución o las hasta ahora inaccesibles texturas 3D:
Texturas 3D: Por lo general las texturas siempre van a estar pegadas a superficies, son por tanto, bidimensionales, sin embargo, en ocasiones es útil (y cada vez más) que la textura esté definida para todo el espacio, y lo que veamos sea un 'corte' de esta textura que ocupa todo un volumen. Por ejemplo, todos sabemos que la madera de un tronco tiene una textura de círculos concéntricos marrones, pero en realidad... sabemos que eso círculos siguen por centro del tronco, que son cilindros concéntricos marrones en realidad. Bien, si definimos la textura en 3D, podriamos... tallar el árbol!!. Y ver en todo momento como va quedando nuestra figuirita de madera, y como las vetas la van atravesando. Un bitmap no puede hacer esto, generaría ficheros demasiado grandes. Por ej. Una bitmap de 256*256*256 pixels a 24 bits ocuparía 402 Megas!!!.
Desventajas:
No se dibujan, se 'inventan': Como hemos dicho, las texturas procedurales están definidas por un algortimo matemático capaz de crearlas, y... alguien tiene que hacer ese algoritmo. No se pueden pintar texturas procedurales con el Photoshop. Se pintan con el Visual C++, Renderman, Builder... este tutorial pretende iniciarte en esta técnica.
CPU: Todo lo que hemos liberado de disco duro, ahora lo tiene que hacer la CPU procesando el algoritmo que crea la textura. Lo cual quiere decir que una vez que has sintetizado una textura, te queda aún la mitad del trabajo: Optimizarla
Las texturas procedurales nunca reemplazarán a los mapas de bits, pero podrán relevarles en algunas tareas.
Por lo general, las texturas procedurales están compuestas de mapas. Los mapas son texturas sencillas, por lo general en blanco y negro, que se fusionan con otros produciendo texturas complejas, rellenando canales de color, canales bump, canales de brillo...algunos tipos de mapas:
Tipos de mapas:
Mapas geométricos: Se repiten pues no tienen aleatoriedad. Algunas texturas con este tipo de mapas:
Son (al menos en teoría) fáciles de sintetizar, hacen mucho uso de la función MOD o de operadores binarios. Se repiten. No veremos este tipo de mapas en este tutorial.
Mapas aleatorios (y fractales): Hacen uso de funciones aleatorias. Son por tanto impredecibles a baja escala, pero suelen seguir patrones regulares: En un camino de piedras siempre hay piedras, pero no hay dos piedras iguales. Algunas texturas con mapas aleatorios:
Estas texturas son mucho más complicadas de sintetizar y también requieren más ciclos de proceso, sin embargo hay algunos algoritmos 'llave' de sumo interés. Por ejemplo el algoritmo del mapa Cellular es usado por toda la fila de abajo. Y creo que casi toda la segunda fila hace uso del mapa Perlin Noise. Estudiaremos estos dos mapas en este tutorial.
Para estas texturas es sumamente interesante una Función Matemática (para cada elemento devuelve otro) de apariencia aleatoria: Nuestra función IntNoise. La estudiaremos más adelante.
Podéis encontrar estos y otros buenos ejemplos de mapas y texturas en la pagina comercial: http://www.shaders.org/
Manos a la Obra:
Por fin nos adentramos en la programación de texturas procedurales. Como objetivo de este tutorial tengo el hacer la textura de los reflejos de la luz en el suelo de una piscina (Sombra Cáustica), que aparece como fondo en el título, elijo esta porque hace uso de dos mapas básicos muy típicos: Perlin Noise, y Cellular.
Antes de adentrarnos con el código voy a dar unos cuantos consejos, unas... normas de estilo a la hora de hacer texturas procedurales.
En general cualquiera textura generada por un algoritmo es una textura procedural, sin embargo, miremos estos dos pseudo-códigos:
void dibujar_ajedrez(int tam_casillas){ //partiendo de un fondo blanco for(int y=1; y<8;y++){ //for vertical de casillas for (int x< =0; x<8;x++){ //for horizontal de casillas if((x+y) mod 2 = 1){ //si la casilla es negra for (int j=0;j< tam_casillas){ //for vertical de píxeles de una casilla for (int i=0;i< tam_casillas{ //for horizontal de píxele es una casilla Pintar(x*tam_casillas+i,y*tam_casillas+j)=RGB(0,0,0); }} } }} ----------------------------------------------------- int ajedrez(x,y,tam_casillas){ if((int(x/tam_casillas)+int(y/tam_casillas))mod 2 = 1) {return 0;} //negro else {return = 255;} // blanco void dibujar_ajedrez_bien(int tam_casillas){ for(int y=1; y< fin_y;y++){ //for vertical de todos los pixels for (int x<=0; x< fin_x;x++){ //for horizontal de todos los pixels int color=ajedrez(x,y,tam_casillas); Pintar(x,y)=RGB(color,color,color); }}
Ambos producen más o menos la misma imagen (cuadrados blancos y negros alternativamente), sin embargo las estrategias son distintas, en el primer caso, el que considero incorrecto, se utiliza un orden conveniente a la hora de pitar los puntos, mientras que en el segundo caso, se utiliza un orden estricto (de arriba a abajo de izquierda a derecha, aunque podría ser cualquier otro), y una función que devuelve el color tan solo sabiendo de que punto se trata, independientemente del orden de petición. Para los que tengan conocimientos de matemáticas, existen ciertas similitudes entre el primer ejemplo y una curva o superficie paramétrica, y el segundo con una curva o superficie implícita.
Yo recomiendo el segundo ejemplo por una razón sencilla: En la pantalla, en una área rectangular plana, puedes elegir el orden de pintado, pero 'en la vida real', cuado utilices la textura en una escena 3D, o como una capa de otra textura, probablemente solo te interese el color de ciertos puntos, y no todos (las caras visibles, o los que aparezcan en un reflejo con RayTracing...). Podríamos calcularlos todos en orden a conveniencia, almacenarlos en una matríz, y luego consultar los necesarios, pero como las texturas procedurales son (o pueden ser) infinitas, no sabríamos hasta donde tendríamos que precalcular la textura.
Por tanto, a partir de ahora, los mapas de texturas serán FUNCIONES
de consulta de colores (más bien de valores, como veremos) y no PROCEDIMIENTOS
de pintado (a pesar de que se llamen texturas procedurales). Y habrá
un procedimiento principal que consulte y pinte todos los puntos, como en el
segundo ejemplo.
Siento haber sido tan castrante y haber comenzado el tutorial prohibiendo cosas, pero comenzar con una estrategia errónea trae muchos problemas.
La Funcion IntNoise:
Como hemos visto, las texturas más bonitas y complicadas tienen factores
aleatorios, sin embargo, ¿cómo conseguir estos números
aleatorios?.
Usualmente la técnica consistía en, antes de dibujar la textura,
llamar a un procedimiento void inicializacion(); que precalculara esos numeros
aleatorios y los dejara, por lo general, en una matríz en un orden a
conveniencia. Nos vemos entonces con el problema de que no podemos calcular
infinitos numeros aleatorios y luego usarlos. Tampoco podemos usar simplemente
una funcion aleatoria basadada en el reloj del sistema, pues es obvio que la
textura no puede, por ejemplo, cambiar a voluntad de un frame a otro:
- Matriz precalculada de números aleatorios: Es estable pero finita, no se pueden calcular demasiados numeros aleatorios.
- Calculo de números aleatorios en tiempo real con función Rnd() : Es infinito, pero incontrolables, en el próximo frame la piedra de 8 cm del camino será de 4 cm, o de 6..., además, debido a que las funciones de los mapas son implícitas, sería complicado reunir un grupo de puntos en un solo valor aleatorio.
- IntNoise!: Es infinita, y es función, luego el número aleatorio IntNoise(5) es el mismo que IntNoise(5) en otro momento. IntNoise(x)=IntNoise(x)=aleatorio (wow!)IntNoise, como buena función aleatoria No Es Continua! (bueno, nunca podría serlo porque esta definida para enteros... :P ) . Me refiero a que IntNoise(x) no tiene porque parecerse en NADA a IntNoise(x+1). Prueba de ello es que la textura 255*IntNoise(i+j*ancho) es un White Noise, de apariencia similar a lo que aparece cuando enciendes la televisión sin conectar la antena.
/// Algoritmo con y sin IntNoise de una textura de cuadrados en distintos grises aleatorios. // ******** Sin IntNoise ********* int Colores[CuadradosX][CuadradosY] void Inicializar(){ for(j=0,j< CuadradosY; j++){ for(i=0;i< CuadradosX; i++){ Colores[i][j]=int(Rnd*255); //Donde Rnd da valores entre [0..1] aleatorios } } void Pintar(int tam_celda){ inicializar(); int color; for(y=0,y< MaxY; y++){ for(x=0;x< MaxX; x++){ color=Colores[int(x/tam_celda)][int(y/tam_celda)]; Pintar[x,y]=RGB(color,color,color) }} //********* Con IntNoise******** void Pintar(int tam_celda){ // Ya no hace falta inicializar!!! int color; for(y=0,y< MaxY; y++){ for(x=0;x< MaxX; x++){ color=int(255*IntNoise[int(x/tam_celda)*const+int(y/tam_celda)]); Pintar[x,y]=RGB(color,color,color) }} |
Como se puede ver, con la función IntNoise, nos hemos ahorrado la inicialización
y ahora tenemos un algoritmo oque es capaz de dibujar cuadraditos de distintos
grises hasta que se canse, sin ninguna limitación, y con la seguridad
de que, cuando volvamos a consultar los colores de la textura, seguirán
siendo los mismos.
Hay un pequeño problema. La variable const que no está
declarada, indica que ahí hay que poner un numero. Para poder indexar
coordenadas de dos integers en un solo integer: esto es, para hace una aplicación
biyectiva entre R^2 y R. Esto es algo realmente complicado para un plano infinito
(hay métodos pero muy costosos en cálculo y no merece la pena).
Si sabes que la textura va a tener un ancho definido, y que vas a necesitar
N números aleatorios por cada fila, entonces N es el valor para tu constante
(cualquier numero mayor también valdría), sino, basta con poner
un numero suficientemente alto, 5253 por ejemplo, que garantice que no será
apreciable la repetición de los números aleatorios. (¡¡Que
no de la textura!!, esta, como hemos dicho, no se repite si no queremos.)
El núcleo de éste programa está en la línea: IntNoise[int(x/tam_celda)*const+int(y/tam_celda)] pues utiliza una técnica que se repetirá a menudo en el uso de IntNoise: Si tenemos un grupo de pixels que han de compartir un mismo numero aleatorio (el color de una piedra, o la altura relativa de una montaña...) hemos de conseguir que todos estos invoquen a IntNoise con el mismo argumento.
Bien, ya sabemos que propiedades tiene nuestra función (matemática) IntNoise, ahora hemos de conseguir una implementación. La mejor que he conseguido es la que hace uso Hugo Elias en su pagina para la implementación del Perlin Noise, os recomiendo que la miréis, yo no voy a hacer una traducción más de esa página,¡he visto ya traducciones al Ruso y al Japonés!, sin embargo explicaré llegado el momento ciertas cosas que allí no quedan demasiado claras. Bien, esta es una pequeña modificación del IntNoise de esa página, que tiene imagenes en [0..1] y no en [-1..1] como la original:
float IntNoise(register int x) { x = (x<<13)^x; return (((x * (x * x * 15731 + 789221) + 1376312589) & 0x7fffffff) / 2147483648.0); } |
ANTERIOR
|
INDICE
|
| PRINCIPAL | GRAFICOS 3D | GRAFICOS 2D | MATEMATICAS | TUTORIALES | FRACTALES | FUENTES | LINKS |