CAPÍTULO VII

Texturas

Ejemplo de mapeo de textura sobre una esfera. Texturas del planeta Tierra obtenidas de https://www.solarsystemscope.com/ bajo la Licencia CC BY 4.0.

En el capítulo anterior asumíamos que las propiedades reflectivas de un objeto eran las mismas para cada punto de su superficie, sin embargo, muy pocas superficies son así.

Por ejemplo, si quisiéramos representar una superficie plana con un patrón de colores sencillo como el de un tablero de ajedrez, podríamos utilizar $8\times 8=64$ cuadrados ordenados, alternando los materiales (blanco y negro) entre cada uno. Pero si consideramos un patrón de colores más complicado como un texto, una pintura o propiedades de materiales naturales como la madera y el mármol, se vuelve muy difícil de representar geométricamente, además de ser excesivamente caro de almacenar ya que se requeriría de un gran número de polígonos para tener un acabado más realista.

Del mismo modo, una superficie también puede presentar otro tipo de imperfecciones como rasgaduras, hendiduras, bultos y variaciones de colores, las cuales presentarían este mismo problema.

Las técnicas de mapeo de texturas que describirémos en este capítulo nos permitirán controlar las propiedades reflectivas de cada punto de una superficie, sin necesidad de agregar geometría extra en la mayoría de los casos, logrando hacer que nuestras superficies se vean más realistas en tiempo real.

Pipiline de texturas

El mapeo de texturas o texture mapping es una técnica que nos permite modelar de manera eficaz las variaciones de material y acabado de una superficie. Dicha técnica consiste en modificar las propiedades reflectivas y otros atributos de cada punto de la superficie utilizando los valores almacenados en uno o varios mapas de textura, los cuales suelen ser una imagen o una función procedural.

Los mapas de textura o simplemente texturas, son espacios de 1, 2, 3 o 4 dimensiones, en donde se almacena la información que queremos mapear a una superficie. Para acceder a los valores de una textura necesitamos especificar sus coordenadas de textura. Por ejemplo, una textura 1D puede usarse para determinar la coloración de un modelo de terreno de acuerdo a la altura de cada punto, o bien, para pintar una curva o una línea de un color a otro dado su parámetro $t$.

Ejemplo de un mapa de textura 1D.

Las texturas 2D o de imágenes por otra parte son las más utilizadas, y se trata de imágenes rasterizadas, las cuales son representas por arreglos bidimensionales de pixeles. Podemos pensarlas como una etiqueta que es "pegada" a lo largo de una superficie.

Ejemplo de un mapa de textura 2D.

Las texturas 3D o de volumen son descritas por un arreglo tridimensional de píxeles; sus coordenadas de textura son presentadas como vectores $(u,v,w)$, donde $w$ hace referencia a la profundidad del arreglo. Podemos pensarlas como un arreglo de texturas 2D, como se observa en la . Y debido a que suelen ser muy costosas de almacenar usando imágenes, se suele optar por usar texturas procedurales en su lugar, en donde las coordenadas son mapeadas por una función a un color.

Ejemplo de un mapa de textura 3D.

La muestra las diferentes etapas para aplicar una textura a una superficie, las cuales se describen a continuación. REF. .

Pipeline general de texturizado .

Función de proyección

El proceso inicia en la definición de las posiciones de los vértices de algún modelo, es decir, desde el espacio local, donde por medio de una función de proyección las coordenadas de textura son asignadas a los vértices como un atributo.

La función de proyección se encarga de mapear o proyectar un punto tridimensional al espacio de textura, en otras palabras, cada punto de la superficie deberá tener asignado un punto en la textura.

Por lo regular este mapeo es a un espacio 2D, también conocido como espacio $UV$ o $ST$, el cual se encuentra definido dentro del rango $[0,0]\times[1,1]$, nosotros nos referiremos a sus coordenadas simplemente como coordenadas $uv$. Comúnmente los programas de modelado permiten a los artistas definir las coordenadas de textura $uv$ por vértice, las cuales pueden ser inicializadas a partir de funciones de proyección o algoritmos de desenvolvimiento de malla, además de permitir ser editadas de manera manual. Los algoritmos de desenvolvimiento de malla pertenecen a un campo amplio llamado mesh parametrization. Algunas de la funciones de proyección más utilizadas son las cilíndricas, esféricas y planas, ver .

Funciones de proyección. Selecciona el tipo de proyección que quieras visualizar, así como la textura. Puedes mover la posición de la cámara con el ratón o dedo y acercarte o alejarte con la rueda del ratón.

La especificación de la función de proyección también depende de cómo esté definido el modelo, por ejemplo, en una superficie parámetrica las coordenadas $uv$ son obtenidas por definición. Las texturas volumétricas o 3D por otra parte suelen utilizar las mismas coordenadas del modelo como coordenadas de textura.

En el renderizado en tiempo real las funciones de proyección normalmente son aplicadas en la etapa de modelado y las coordenadas de textura almacenadas en los vértices.

Función de correspondencia

Texture wrapping modes. Las coordenadas de textura del plano están definidas dentro del rango $[-1,2]\times[-1,2]$, nótese que el punto $(0,0)$ se encuentra en la esquina inferior izquierda del cuadro central. Selecciona las diferentes funciones de correspondencia para cada coordenada y observa el resultado.

Una función de correspondencia convierte las coordenadas de textura en coordenadas en el espacio de textura, permitiendo una mayor flexibilidad durante la aplicación de la textura.

Por otra parte, ya que el espacio de textura 2D es un espacio rectangular definido dentro del rango $[0,0]\times[1,1]$, es importante especificar qué ocurre cuando nos salimos de este rango para evitar lidiar con excepciones. Para ello, es necesario especificar una función de correspondencia que mapee las coordenadas de $[-\infin,-\infin]\times[\infin,\infin]$ a $[0,1]^2$ de cierto modo, a este tipo de funciones se les conoce como texture wrapping y algunas de las más comúnes son descritas a continuación:

cada una de las funciones puede ser aplicada en ambas coordenadas $u$ y $v$, o bien definir un modo distinto para cada una. En la puedes probar los diferentes modos de texture wrapping.

Otro ejemplo sería aplicar una transformación geométrica sobre las coordenadas de textura en el vertex shader o fragment shader, como se muestra en la , o bien, utilizar la API para definir un área de la textura sobre la cual se trabajará.

Transformaciones sobre las coordenadas de textura. La ilustración de la izquierda muestra el espacio de textura dentro del rango $[-1,2]\times[1,2]$ con el modo REPEAT de texture wrapping, el rectángulo amarillo está definido por las coordenadas de textura del plano renderizado que se ilustra a la derecha, señalando el área de la textura que se va a mapear sobre el plano de la derecha. Las coordenadas de textura son inicializadas en $[0,1]^2$. Aplica las transformaciones sobre las coordenadas de textura y obsérva cómo el rectángulo amarillo es transformado, afectando la imagen que es proyectada sobre el plano de la derecha.

La última función de correspondencia es implícita y consta de multiplicar las coordenadas de textura dentro del rango $[0,1]$ por la resolución de la imagen, para obtener así la ubicación del pixel de la textura.

Valores de la textura

Finalmente con las coordenadas en el espacio de textura podemos obtener o acceder a los valores del mapa de textura. Como mencionamos anteriormente, los mapas de texturas pueden ser una imagen o una función procedural. En el caso de ser una imagen, el valor es obtenido del pixel de la imagen correspondiente a las coordenadas de textura, este proceso es explicado en el siguiente tema. Mientras que para una función procedural, conocida como texturas procedurales, basta realizar el cálculo de la función asociada como textura.

El valor obtenido del mapa suele ser una tripleta $RGB$, la cual puede ser utilizada para reemplazar el color de la superficie. De manera similar, se puede obtener un color en escala de grises (ya que se utiliza el mismo valor en las tres componentes) para representar la intensidad de la componente especular, o bien, utilizar el color para representar una normal como veremos más adelante, entre otros valores que se quieran utilizar para modificar la ecuación del modelo de iluminación. Otro valor que puede obtenerse es la 4-tupla $RGBA$ donde $A$ representa la transparencia del color, la cual suele ser utilizada como máscara para definir los pixeles a considerar y a descartar de la textura.

Alpha Mapping. En este ejemplo los valores la textura (texeles) con $A < 0.5$ son descartados. Puedes mover la posición de la cámara con el ratón o dedo. Texturas de pasto obtenida de https://learnopengl.com/ bajo la Licencia CC BY 4.0.

Texturas en el pipeline gráfico

Por otro lado, volviendo al Pipeline gráfico, el mapeo de texturas es realizado principalmente dentro de la etapa de Rasterización y la primera del procesamiento de fragmentos, Pixel shading. Ya que durante la primera las coordenadas de textura (y otros atributos) que han sido asignados a los vertices de las primitivas son interpoladas entre sí para obtener las coordenadas de textura correspondiente a cada fragmento. Y en la segunda se determinaría el color del fragmento usando la ecuación de iluminación que se desee con los valores obtenidos del mapa de texturas dadas sus coordenadas de textura.

Texturas 2D o de imágenes

Una textura 2D es una imagen rasterizada o mapa de bits, el cual es representado por un arreglo bidimensional de pixeles. Los pixeles de la imagen son llamados como texeles (por texture element o texture pixel) para diferenciarlos de los pixeles de la pantalla. El mapeo de una imagen a una superficie 3D es una tarea bastante compleja ya que la mayoría de veces no se puede obtener el color del texel de manera directa con las coordenadas en el espacio de textura. En esta sección nos enfocaremos en los principales problemas a los que nos enfrentamos para obtener un valor de una imagen y los métodos que se emplean para solucionarlo.

Recordemos que las coordenadas de textura $(u,v)$ han sido asignadas a los vértices, y que son mapeadas por una función de correspondencia al espacio $[0,1]^2$. Ahora bien, dependiendo del API el origen del sistema puede estar ubicado en la esquina inferior izquierda de la textura, es decir, su esquina inferior es $(0,0)$ y su esquina superior es $(1,1)$, como es el caso de OpenGL, o bien, la esquina superior izquierda de la imagen es $(0,0)$ y su esquina inferior derecha es $(1,1)$, como es el caso de DirectX; nosotros usaremos el primer sistema de referencia. De modo que no importa el tamaño de la imagen siempre podremos acceder a cada texel variando los valores de $u$ y $v$.

Supongamos que queremos mapear una imagen de $n\times n$ a un cuadrado y que el cuadrado proyectado en la pantalla es del mismo tamaño que la textura. Entonces tendríamos una correspondencia uno a uno entre los pixeles que conforman la superficie (fragmentos) y texeles, por lo que podríamos acceder a

los valores del arreglo sin problemas, es decir, al fragmento con coordenadas $(u,v)$ le correspondería el texel imagen[u*n-1][v*n-1].

Mapeo directo de una textura de $5\times5$ pixeles sobre un cuadrado que ocupa $5\times5$ pixeles en la pantalla. Cada vértice del cuadrado está asociado a las coordenadas de textura que se muestran.

Vale la pena mencionar que la posición de los vértices no tienen que tener una relación en particular con las coordenadas de textura. En la se muestra un ejemplo de este caso, en donde se busca mapear un área de la imagen a una primitiva, aquí los vértices de la primitiva tienen asignadas las coordenadas de textura que conforman el triángulo naranja en la imagen.

Mapeo de una textura a una primitiva.

Nótese que el triángulo de la primitiva tiene una forma diferente que el triángulo formado por las coordenadas de textura, lo cual provoca que la imagen se deforme un poco.

Desafortunadamente, cuando una textura es aplicada sobre una superficie los texeles no suelen tener esta relación de correspondencia uno a uno con los fragmentos de la superficie, como vimos en el primer ejemplo. Y además sabemos que los texeles tienen coordenadas enteras, por lo que no podemos acceder a valores intermedios. Entonces, ¿qué pasa con las coordenadas que no intersecan exactamente con un pixel? La respuesta dependerá del tipo de método de filtro que se seleccione para solucionar cada caso.

La proyección de un pixel al espacio de textura es un cuadrilátero el cual puede ser más pequeño o bien, el pixel puede llegar a cubrir varios texeles. En el primer caso, necesitamos aumentar o estirar la imagen por lo que se deberá aplicar un filtro de magnificación ( (izquierda)), mientras que en el segundo, necesitamos encoger la textura, por lo que se aplica un filtro de minimización ( (derecha)).

Métodos de filtrado: Magnificación y Minimización.

Magnificación

Las dos técnicas de filtrado más comunes para resolver este problema son el filtro caja o nearest neighbor y el filtro bilineal o de interpolación bilineal.

En el filtro caja para cada pixel se toma el texel más cercano a su centro. En esta técnica los texeles individuales se hacen más evidentes y es el típico efecto de pixelado que obtenemos al ampliar una imagen, como se muestra en la . Aunque termina dando un resultado de baja calidad, el cálculo es muy rápido pues solo se requiere buscar un texel por pixel.

Por otro lado, en la interpolación bilineal, por cada pixel se toman los cuatro texeles más cercanos a su centro con los cuales se realiza una interpolación bilineal, obteniendo así un valor combinado para el pixel. Con esta técnica el resultado es una imagen más borrosa, disminuyendo considerablemente el efecto de pixeleado que produce la técnica anterior, véase la .

Comparación entre filtro de caja y bilineal utilizando una imagen de $36\times36$ pixeles.

Por ejemplo, supongamos que tenemos las coordenadas en el espacio de textura de un fragmento $(P_u, P_v) = (61.24,52.18)$. Para encontrar a sus cuatro vecinos más cercanos necesitamos primero restarle $(0.5,0.5)$ a las coordenadas para estar en el centro del pixel, teniendo (60.74, 51.68). Ahora, considerando solo la parte entera del centro del pixel, es decir con $(x,y) = (60, 51)$, podemos encontrar a los cuatro texeles cercanos $(x,y), (x+1,y), (x,y+1)$ y $(x+1,y+1)$ como se muestra en la .

Magnificación. Interpolación bilineal.

Finalmente podemos calcular la interpolación bilineal entre los cuatro texeles y como parámetro dentro de esta relación la parte fraccionaria del centro del pixel es decir $(u',v') = (0.74, .61)$.Esto es $$ \begin{aligned} \mathbf{b}(P_u,P_v) =& (1-u')(1-v')C(x,y) + u'(1-v')C(x+1,y) + \\& (1-u')v'C(x,y+1) + u'v'C(x+1,y+1) \end{aligned} $$ donde $C(x,y)$ es la función que te devuelve el color del textel de la imagen, es equivalente a imagen[x][y]. Como podemos notar el color del texel más cercano a las coordenadas $(u',v')$ o centro de las coordenadas en el espacio de textura son las que tendrán mayor influencia en el valor final.

Si bien esta técnica suele dar un mejor resultado que la anterior, puede no ser muy buena opción para ciertos patrones como el del tablero de ajedrez, ya que se mezclarían los colores dando una escala de grises entre cada transición de color, véase .

Magnificación de una textura con patrón de tablero de ajedrez: Filtro de caja y filtro bilineal.

Minimización y Mipmapping

Minimización con filtro de caja, filtro bilineal y mipmapping.

Cuando una textura es minimizada, varios texeles de la textura corresponden a un pixel o fragmento. De modo que para obtener el color correcto de cada fragmento deberíamos calcular la influencia que tienen cada uno de los texeles que éste cubre, lo cual resulta imposible de calcular de manera precisa en tiempo real.

Si utilizamos el filtro de caja se tomarían texeles arbitrariamente lejanos entre sí, pudiendo producir un mapeo sin sentido. Como se puede ver en la (izquierda superior) este filtro puede causar muchos problemas de aliasing o efecto escalera ya que solo un texel es elegido entre varios, estas distorsiones son más notables cuando la superficie se mueve con respecto al observador y a esto se le conoce como temporal aliasing.

Por otra parte, el filtro bilineal es ligeramente mejor ya que en lugar de tomar un texel combina cuatro, sin embargo cuando el pixel llega a cubrir más de cuatro texeles el filtro comienza a fallar de manera similar al anterior y produce aliasing, como se muestra en la (derecha superior).

El método más utilizado que ayuda a mitigar este problema de manera eficiente es el de mipmapping, donde mip proviene de la frase en latín "multum in parvo" que quiere decir "muchas cosas en un lugar pequeño". En este método de filtrado se genera un conjunto de versiones más pequeñas de la imagen original, cada una un cuarto más pequeña que la anterior antes de que comience el renderizado.

Pirámide de mipmaps. Textura obtenida de ambientCG.com bajo la Licencia CC0 1.0 Universal.
Creación de mipmap con una textura con $2\times2$ texeles. El pixel cubre exactamente los cuatro texeles de la textura, por lo que el nuevo mipmap contará de una textura con un solo texel, y como valor tendrá el promedio de los cuatro texeles de la textura original.

Consideremos el caso ideal, donde tenemos una textura de $2\times2$ texeles y la proyección de un pixel cubre exactamente estos cuatro texeles. Podemos crear entonces una versión más pequeña de la imagen hecha sólo con un texel, el cual tendrá como color el valor promedio de los cuatro texeles de la textura original, por lo que este valor será el que nos interesará acceder como se ilustra en la .

Este proceso se puede generalizar para una imagen con un ancho $w$ y altura $h$ donde el fragmento cubra un área arbitraria de texeles. De modo que cuando se cree la textura, también se deberán crear las versiones más pequeñas de la misma, reduciendo $\frac{1}{2}$ el ancho y alto de manera recursiva hasta tener una textura con al menos una de sus dimensiones igual a $1$, como se muestra en la . Cada una de las imágenes creadas se les llama mipmap y es asociada a un nivel, donde el primer nivel es el $0$ y está asociado a la textura original, el nivel $1$ para el primer mipmap y así sucesivamente. Al conjunto de mipmaps se le conoce como cadena de mipmaps o pirámide de mipmaps.

Nótese que para construir la piramide de mipmaps como hemos visto, se requiere que la textura original tenga dimensiones de potencia de dos (POT), es decir, $w = 2^n$ y $h = 2^m$. También como pudimos observar los mipmaps se vuelven pequeños muy rápido, ya que se van encogiendo un cuarto de su área cada vez, el total de memoria utilizada es solo un tercio más que la memoria utilizada por la textura original.

Por otro lado, la mayoría de las GPUs modernas ya permiten manejar texturas con dimensiones diferentes a potencias de dos (NPOT). Para ello, se suele escalar las dimensiones de la textura a la siguiente potencia de dos más cercana antes de empezar a generar los mipmaps, por lo que se necesita una mayor cantidad memoria y hace que su manejo sea más lento. Por estas razónes es que se recomienda en general usar imágenes con dimensiones iguales a potencia de dos. Además de que algunas GPUs (inlcluyendo modernas) siguen teniendo problemas para mostrar texturas con dimensiones NPOT, como generar alguna distorsión en la imagen original al ser escalada.

Ahora bien, cuando se está renderizado, y el proceso de texturizado se esté ejecutando, para cada fragmento se elige el nivel del mipmap a utilizar calculando qué tan grande es la proyección del pixel, es decir, qué tantos texles cubre de la textura original. Por ejemplo, si el pixel cubre cuatro texeles, entonces se utiliza el mipmap del nivel $1$, si cubre 16 se elige el del nivel $2$ y así sucesivamente. Para tener mejores resultados se realiza un filtrado trilineal entre los dos mipmaps más cercanos y se interpolan sus dos valores obtenidos.

Para calcular el nivel del mipmap $\lambda$ a utilizar en un fragmento, también referido como nivel de detalle, necesitamos calcular primero el número de texeles que cubre el pixel, siendo $\rho= texeles/pixel$. Una forma es calculando el borde de mayor tamaño del cuadrilátero formado por la proyección del pixel en el espacio de textura. Sea $(x,y)$ las coordenadas del pixel (coordenadas de pantalla) y $(u,v)$ las coordenadas en el espacio de textura del pixel, siendo más específicos, expresado en texeles $([0,0]\times[2^n,2^m])$, entonces podemos calcular $\rho$ como $$ \rho = \text{max}\Biggl(\Biggl\lVert \begin{bmatrix} \dfrac{\partial u}{\partial x} \\ \dfrac{\partial v}{\partial x} \end{bmatrix} \Biggr\rVert, \Biggl\lVert \begin{bmatrix} \dfrac{\partial u}{\partial y} \\ \dfrac{\partial v}{\partial y} \end{bmatrix} \Biggr\rVert \Biggr)$$ donde cada diferencial es la razón del cambio en la textura con respecto a un eje de la pantalla. Por ejemplo $\frac{\partial u}{\partial x}$ es qué tanto cambió la coordenada de textura $u$ a lo largo del eje $x$ para un pixel, obsérvese la .

Finalmente podemos obtener el nivel de detalle como $$ \lambda = log_2\rho $$

Estimación del tamaño de un pixel en el espacio de textura.

O bien, para un mejor resultado podemos hacer una interpolación trilineal entre los dos nivles más cercanos $ \lfloor \lambda \rfloor $ y $\lfloor \lambda \rfloor + 1$. En donde primero se hacen dos interpolaciones bilineales en cada uno de los mipmaps usando las coordenadas de textura y por último los valores obtenidos de los mipmaps son interpolados linealmente con la parte flotante de $\lambda$.

De este modo, no importa la cantidad de texeles que un pixel cubra, siempre se tardará la misma cantidad de tiempo en calcular su color. Pues en lugar de promediar todos los texeles contenidos por pixel, se interpolan conjuntos de texeles ya precalculados. No obstante, este método sigue presentando algunos defectos. Uno de los más importantes es el exceso de desenfoque, y esto ocurre debido a que solo podemos acceder a áreas cuadrádas de la textura.

Digamos que un framento cubre un gran número de texeles en dirección de $u$ y muy pocos en dirección de $v$, por lo que nos interesa un área rectangular en la textura. Esto sucede cuando miramos a lo largo de una superficie formando un ángulo oblicuo. E incluso podría llegar a ser necesario hacer una minificación a

lo largo de un eje, y una magnificación a lo largo del otro. Podemos observar este defecto en las líneas que se dezplazan en la distancia de la . El filtro más común para corregir este defecto es el Anisotropic Filtering, el cual reutiliza la piramide de mipmaps.

Mapeo de materiales

Empecemos con uno de los usos más comúnes y sencillos del mapeo de texturas. Como mencionamos anteriormente, podemos usar una textura para modificar las propiedades de los materiales que conformen la superficie, específicando qué áreas de la superficie tienen qué material. De este modo cuando se vaya a evaluar la ecuación de la iluminación del punto podrémos modificar sus parámetros con la información especificada en el mapa de textura.

El párametro que más se suele modificar es el del color de la superfie, donde se reemplaza el color difuso por el valor del mapa, a esta textura se le conoce como diffuse map o abeldo map. Otro mapa que también suele ser utilizado junto con el diffuse map, es el mapa especular o gloss map, que es un mapa en escala de grises usado para modificar la insensidad del componente especular, dando así un efecto más realista. Por ejemplo, en la , se utlizan ambos mapas () para describir una caja de madera con partes metálicas.

Mapa difuso y mapa especular de una caja. Texturas obtenidas de https://learnopengl.com/ bajo la Licencia CC BY 4.0.

De manera similar, cualquier parámetro de la fórmula de iluminación puede ser modificado utilizando un mapa de textura, ya sea reemplazando un valor, multiplicándolo, sumándolo, o cambiándolo de alguna otra forma.

Mapeo de materiales. El lector puede mover la cámara con el ratón o dedo y alejarse o acercase con la rueda del ratón. Texturas de la caja obtenidas de https://learnopengl.com/ bajo la Licencia CC BY 4.0. Textura de la esfera obtenida de ambientCG.com.

Mapeo de normales y espacio tangente

Como mencionábamos al principio del capítulo la mayoría de las superficies en el mundo real no son totalmente lisas y pueden presentar muchos detalles. Con el mapeo de normales también conocido como Bump mapping, podemos lograr crear una ilusión de relieve sobre la superficie sin necesidad de agregar o modificar la geometría del objeto a una escala muy pequeña. Hasta ahora nos hemos limitado a calcular la normal de un punto por medio de una interpolación entre las normales definidas en los vértices, lo cual nos impide iluminar cualquier detalle dentro del triángulo de la malla. La técina de mapeo de normales consiste en utilizar el color de los texeles de una imagen para almacenar los vectores que se usarán para modificar la dirección de las normales de cada fragmento, cambiando así la apariencia de la superficie como se puede observar en la .

Normal mapping. Puedes dar clic en Normal mapping para ver la diferencia. Mueve la posición de la cámara con el ratón o dedo, o bien, puedes alejarla o acercarla con la rueda del ratón. Texturas obtenidas de ambientCG.com.

Un mapa de normales codifica los vectores normales $(x,y,z)$ que van de $[-1,1]$ en colores. El vector $(0,0,1)$ representa una normal que no es alterada, mientras que cualquier otro vector representa una modificación de la normal. Por ejemplo, si consideramos una textura de $8-$bits, entonces el $0$ representaría a $-1$, y $255$ a $1$. En la se muestra un mapa de normales, y como podemos observar el color que predomina es un color morado pastel, esto es ya que el vector inalterado $(0,0,1)$ es represetado con el color $(\frac{1}{2}, \frac{1}{2}, 1)$, o bien, $(128,128,255)$ para el ejemplo anterior. Otros colores como el rosa y verde pastel hacen referencia a las normales que están apuntando en dirección al eje $x$ y $y$ positivos respectivamente.

Ahora bien, si aplicamos esta misma textura sobre un plano que apunta en dirección al eje $z$ positivo, entonces podemos simplemente extraer la normal del mapa para cada punto de la superficie y usarla directamente en la fórmula de iluminación. Sin embargo, esto dejaría de funcionar correctamente si el plano apuntara hacia una dirección diferente, ya que la mayoría de los vectores

Ejemplo de Mapa de normales.

en el mapa están apuntando hacia $z+$. Este comportamiento se ilustra en la .

Para solucionar este problema necesitamos hacer que el vector inalterado $(0,0,1)$ del mapa corresponda con el vector normal interpolado del fragmento, en otras palabras alinear el espacio del mapa de normales a la superficie.

Mapeo incorrecto de normales. Puedes mover la posición de la cámara con el ratón o dedo, así como alejarla o acercarla con la rueda del ratón.

Para ello necesitamos construir un sistema de coordenadas en cada vértice de la superficie, en donde la normal del vértice siempre apunte hacia $z+$. A este sistema se le conoce como espacio tangente y es en donde están definidos nuestros vectores normales del mapa. La base ortogonal del espacio tangente se define con el vector normal del vértice y dos vectores tangentes a la superficie: el vector tangente y el vector bitangente.

Una vez establecido el sistema de coordenadas del espacio tangente podemos realizar la transformación de los vectores de un espacio a otro, es decir, ya sea usar las normales del mapa transformadas al espacio de vista (por fragmento), o bien, transformar la dirección del vector de la luz al espacio tangente (por vértice) para luego ser interpolado y calcular adecuadamente la intensidad de la luz utilizando la ecuación de Lambert. Vale la pena notar que el primer acercamiento es más costoso que el segundo.

Nuestra meta es entonces encontrar la matriz de cambio de base que nos permita transformar los vectores entre el espacio de vista y el tangete. El caso más intuitivo es transformar los vectores del espacio tangente al espacio de vista, ya que sabemos que el eje $z$ del espacio tangente siempre será mapeado al vector normal del vértice. Por otra parte, queremos que su eje $x$ corresponda a la dirección $u$ del mapa de normales y el eje $y$ se alinee con la dirección $v$ del mapa.

Para encontrar los vectores tangentes de un solo vértice consideremos lo siguiente. Digamos que tenemos un triángulo $P_0, P_1$ y $P_2$, con $(u_0, v_0), (u_1,v_1)$ y $(u_2,v_2)$ como las coordenadas de textura de los vértices. Sea $P_0$ el vértice de interés tenemos que $$ \mathbf{Q}_1 = P_1 - P_0 $$ $$ \mathbf{Q}_2 = P_2 - P_0 $$ entonces necesitamos resolver $$ \mathbf{Q}_1 = (u_1-u_0) \mathbf{T} + (v_1-v_0)\mathbf{B} = \varDelta u_1\mathbf{T} + \varDelta v_1\mathbf{B}$$ $$ \mathbf{Q}_2 = (u_2-u_1) \mathbf{T} + (v_2-v_1)\mathbf{B} = \varDelta u_2\mathbf{T} + \varDelta v_2\mathbf{B}$$ donde $\mathbf{T}$ es el vector tangente y $\mathbf{B}$ el vector bitangente, los cuales están alineados con el mapa de textura, véase .

Espacio tangente.

Podemos reescribir este sistema de ecuaciones de la siguiente forma $$ \begin{bmatrix} (\mathbf{Q}_1)_x & (\mathbf{Q}_1)_y & (\mathbf{Q}_1)_z \\ (\mathbf{Q}_2)_x & (\mathbf{Q}_2)_y & (\mathbf{Q}_2)_z \end{bmatrix} = \begin{bmatrix} \varDelta u_1 & \varDelta v_1 \\ \varDelta u_2 & \varDelta v_2 \end{bmatrix} \begin{bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \end{bmatrix} $$

Multiplicando ambas partes por la inversa de la matriz de las coordendades de textura tenemos $$ \begin{bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z \end{bmatrix} = \dfrac{1}{u_1v_2 - u_2v_1} \begin{bmatrix} \varDelta v_2 & -\varDelta v_1 \\ -\varDelta u_2 & \varDelta u_1 \end{bmatrix} \begin{bmatrix} (\mathbf{Q}_1)_x & (\mathbf{Q}_1)_y & (\mathbf{Q}_1)_z \\ (\mathbf{Q}_2)_x & (\mathbf{Q}_2)_y & (\mathbf{Q}_2)_z \end{bmatrix} $$ Obteniendo así a los vectores (no normalizados) $\mathbf{T}$ y $\mathbf{B}$ para el triángulo definido por $P_0, P_1$ y $P_2$, con los cuales podemos construir la matriz de cambio que nos permite transformar nuestros vectores del espacio tangente al espacio de vista.

$$ \mathbf{v}_{vista} = \begin{bmatrix} T_x & B_x & N_x \\ T_y & B_y & N_y \\ T_z & B_z & N_z \end{bmatrix} \begin{bmatrix} v_x \\ v_y \\ v_z \end{bmatrix} $$

Ahora, para transformar en sentido contrario, es decir, del espacio de vista al espacio tangente, podemos tomar la inversa de la matriz. Para simplifcar los cálculos podemos ortogonalizar la matriz con el algoritmo de Gram-Schmidt y obtener así la inversa de la matriz con su transpuesta. Esto es obtener los nuevos vectores $\mathbf{T'}$ y $ \mathbf{B'}$ como sigue $$ \mathbf{T'} = \mathbf{T} - (\mathbf{N} \cdot \mathbf{T})\mathbf{N}$$ $$ \mathbf{B'} = \mathbf{B} - (\mathbf{N} \cdot \mathbf{B})\mathbf{N} - (\mathbf{T'} \cdot \mathbf{B})\mathbf{T'}$$ Finalmente los vectores serian normalizados y almacenados en los vértices para construir la matriz $$ \mathbf{v}_{tangente} = \begin{bmatrix} T'_x & T'_y & T'_z \\ B'_x & B'_y & B'_z \\ N_x & N_y & N_z \end{bmatrix} \begin{bmatrix} v_x \\ v_y \\ v_z \end{bmatrix} $$

Otra optimización sería calcular el vector bitangente con el pruducto cruz $$B' = m(\mathbf{N}\times\mathbf{T'})$$ donde $m= \pm 1$ es la dirección del vector en el espacio tangente y es igual al determinante de la matriz. De esta manera simplemente se guardaría en cada vértice el valor de $m$ en lugar del vector $\mathbf{B'}$.

Mapeo de desplazamiento

Un problema con el mapeo de normales es que algunos detalles como elevaciones o bultos no proyectan una sombra y la silueta no se ve afectada por las irregularidades que la superficie debería presentar. Esto se debe a que realmente no estamos modificando la geometría del objeto. El mapeo de desplazamiento por otra parte consiste en modficiar la geometría usando una textura, siendo más específicos, modifica la posición de los vértices de acuerdo a los valores de desplazamiento almacenados en el mapa de textura llamado mapa de desplazamiento. Dicho mapa puede ser una imagen en escala de grises ya que solo nos interesa extraer un solo valor, o bien, una textura procedural.

Para cada vértice se realiza un desplazamiento en dirección de su normal, esto es $$ \mathbf{v} = \mathbf{v} + d(u,v) \cdot \mathbf{n}$$ donde $d$ es la función que obtiene el valor de desplazamiento del mapa dadas las coordenadas de textura del vértice.

De manera similar, podemos aplicar el desplazamiento para cada fragmento antes de evaluar la ecuación de iluminación, generando la ilusión de una superficie rugosa, sin necesidad de agregar más geometría.

Desafortunadamente, esta técnica requiere de un mayor número de vértices para lograr que la superficie se vea más realista, por lo que necesitará de más procesamiento y memoria.

Mapeo de desplazamiento. Modifica el valor de la altura y el número de subdivisiones de la esfera, nótese que la definición de las elevaciones aumenta con un mayor número de subdivisiones. El lector puede mover la posición de la cámara con el ratón o dedo, así como también alejarla o acercarla con la rueda del ratón. Textura de la luna obtenida de https://www.solarsystemscope.com/s bajo la Licencia CC BY 4.0.

Texturas procedurales

Ejemplos de texturas procedurales geométricas básicas. Se muestran distintas texturas procedurales geométricas, las cuales se muestran en función de las coordenadas de textura 2D ($uv$) asignadas a cada vértice (izquierda), y sobre las coordenadas 3D de la superficie (derecha). Puedes elegir la textura a utilizar, así como el objeto. Modifica el valor Escalar para aplicar una transformación de escalamiento sobre las coordenadas y observa las diferencias. En algunos casos, las características visuales de la textura tienen un aspecto parecido debido a que las coordenadas de textura y las del objeto tienen más o menos el mismo tamaño, por lo que es necesario aumentar el factor de escalamiento. Las coordenadas del cubo se encuentran dentro del rango $[-.5,.5]^3$ mientras que las de la tétera estan entre $[-1,1]^3.$ Se puede mover la posición de la cámara con el ratón o dedo, así como alejarla o acercarla con la rueda del ratón.

Una textura procedural es una función o algoritmo que devuelve un color para cada punto de la superficie de un objeto. Dicha función puede depender de sus coordenadas de textura $(u,v)$, o bien de sus coordenadas en el espacio $(x,y,z)$, a esta última se les conoce como texturas de volumen o 3D.

Una de las ventajas que tienen las texturas procedurales sobre las imágenes es que pueden ser renderizadas en cualquier resolución, por lo que no presentará los problemas que suelen aparecer cuando se renderiza una imagen de cerca. Sin embargo, aún pueden presentar efectos de aliasing .

Otra ventaja es que no se necesita de memoria extra para los mapas, ya que los valores son calculados en tiempo real.

Por otra parte, si el algoritmo es bastante complejo podría tomar mucho más tiempo que simplemente muestrear una imagen, además de que podría ser mucho más complicado crear un algoritmo que describa lo que queremos dibujar, o bien, no estar a nuestro alcance.

Si bien, los colores calculados por la función procedural suelen ser utilizados para reemplzar el color difuso de la superficie, también pueden ser utilizdos como mapas de texturas de las dos técinas anteriores, permitiéndonos modificar la dirección de las normales, o bien, la geometría de la superficie, otro aspecto que también podría modificarse es la transparencia de la misma. De hecho, también podría realizarse una combinación de éstas técinas de mapeado, incluyendo el de imágenes, para crear así un aspecto mucho más realista.

Las texturas procedurales son excelentes para simular superficies naturales como la madera, el mármol, granito, nubes, corteza de árbol, fractales, etc. Podemos dividirlas en dos tipos: geométricas y aleatorias.

Las texturas procedurales geométricas son aquellas que como su nombre lo indica generan patrones geométricos, algunos ejemplos son líneas, puntos, triángulos, cículos, estrellas, patrones hexágonales (panal de abejas), el patrón de un tablero de ajedrez, una pared de ladrillos, fractales, entre muchas otras. En la se muestran algunos ejemplos básicos de este tipo.

Mientras que las texturas procedurales aleatorias son aquellas que requieren de una función pseudo-aleatoria, la cual suele estar correlacionada espacialmente, es decir que si dos puntos se encuentran cerca entonces compartiran parte de sus propiedaes, pero si se encuentran muy separados entonces sus propiedades calculadas serán muy independientes entre sí. Una de las funciones más utilizadas es la función de ruido de Perlín, la cual permite simular nubes, explosiones, humo, fuego, entre muchos otros efectos, en la se muestra ejemplos de esta función. Una variación de esta función, es la función de turbulencia.

Otra función es la textura celular, con la cual se crean patrones que parecen células, losas, piel de lagarto, entre otras, del mismo modo en la se ejemplifica el ruido de Worley que una función de este tipo. Otro ejemplo sería el resultado de una simulación física o de algún otro proceso interactivo, como las ondas de agua o la propagación de grietas.

Por otro lado, la combinación de ambos tipos de funciones puede producir un mejor resultado. Un ejemplo sería un patron de ladrillos o losas irregulares.

Ejemplos de texturas procedurales aleatorias. Elige el tipo de función a utilizar, así como la figura sobre la cual quieres que se proyecte. Cambia el valor Escalar de las coordenadas de textura pausando la Animación. También puesde mover la posición de la cámara con el ratón o dedo, así como alejarla o acercarla con la rueda del ratón.

Te invitamos a revisar la página de Shadertoy para ver increíbles ejemplos de texturas procedurales.

Mapeo ambiental (environment mapping)

El mapeo ambiental más conocido como environment mapping es el conjunto de técinas que nos permiten simular el efecto de reflexión del entorno sobre un objeto brillante en tiempo real, así como también simular el efecto de refracción de un objeto translucido o transparente. En el mapeo ambiental el objeto es rodeado por una superficie tridimensional cerrada sobre la cual es proyectada el entorno, el cual es almacenado en una textura o conjunto de texturas llamadas como mapa de entorno o environment map.

El caso más básico de enviroment mapping es el reflection mapping, en donde la superficie refleja su entorno como un espejo, consiguiendo una apariencia cromada. De manera similar a la reflexión especular, ésta depende de la posición del observador, solo que en este caso se calcula el vector de reflexión $\mathbf{R}$ entre el vector de vista $\mathbf{V}$ y la normal $\mathbf{N},$ esto es $$\mathbf{R} = 2(\mathbf{N}\cdot\mathbf{V})\mathbf{N} - \mathbf{V}$$ una vez calculado este vector se procede a calcular las coordenadas de textura del mapa de entorno y se obtiene el color. De esta manera las coordenadas de textura cambiaran cuando el observador se mueva, haciendo que parezca que el objeto está reflejando su entorno.

Vector de reflexión entre el vector normal y de vista.

El mapa de entorno suele ser una textura a la cual se accede utilizando coordenadas esféricas, o bien, un conjunto de 6 texturas. Nosotros nos enfocaremos en el segundo caso.

En 1986, Green introdujo el Cube mapping, este método es el más utilizado hoy en día, y su proyección está implementada en los GPUs modernos. Las texturas de mapas de cubo o cube maps están compuestos por 6 imágenes cuadradas, cada una asociada a una cara del cubo y que en conjunto forman un entorno, éstas suelen ser visualizadas con un diagrama de cruz ().

Ejemplo de environment map de Green. Cube map obtenido de http://www.humus.name/index.php?page=Textures bajo la Licencia CC BY 3.0.

Para acceder a la textura, se toman las coordenadas de los vectores que especifican la dirección de un rayo que van desde el centro del cubo hacia fuera del cubo. La coordenada de mayor magnitud del vector selecciona la cara del mapa, luego para obtener las coordenadas $(u,v)$ se dividen las otras dos coordenadas con el valor absoluto de la coordenada de mayor magnitud, y como ahora se encuentran dentro del rango $[-1,1]$ solo falta mapearlas dentro del rango $[0,1]$. Por ejemplo, sea $\mathbf{b}$ nuestro vector, el cual apunta en dirección a la cara derecha, entonces su coordenada $x$ es la coordenada de mayor magnitud. En este caso calculamos $$ (u,v) = \bigg(\dfrac{z+x}{2|x|}, \dfrac{y+x}{2|x|}\bigg) $$ esto se resuelve de manera análoga para las demás caras.

De manera similar, realizamos este mismo cálculo para cualquer dirección de reflección $\mathbf{R}$, véase .

Cube mapping. El observador $\mathbf{V}$ ve el objeto y el vector de reflección $\mathbf{R}$ es calculado con $\mathbf{V}$ y $\mathbf{N}$. Con el vector de reflexión se accede al mapa ambiental, convirtiendo sus coordenadas a coordenadas de textura.

Comúnmente también queremos observar el entorno que es reflejado sobre el objeto, para ello simplemente se dibuja un gran cubo usando los mapas del entorno con el objeto dentro de éste. A esta técnica se le conoce como Skybox y es ampliamente utilizada en videojuegos. El skybox nos permite representar el entorno de fondo a un bajo costo, y por lo general contiende objetos distantes al observador como el sol, montañas y nubes, aunque también puede describir escenas más pequeñas como la de una habitación. Las coordenadas de textura son obtenidas directamente por las coordenadas del cubo, es decir calculamos a $\mathbf{b}$ con los vértices de cada cara y los vectores interpolados entre éstas.

Ahora bien, consideremos los siguientes dos casos. El primero es cuando queremos mover los objetos dentro del skybox, para esto el skybox se mantendría estático, por lo que no tendremos ningún problema obteniendo los valores del mapa de la manera que ya hemos visto. El segundo caso es cuando queremos mover la posición de la cámara, entonces la transformación de vista tendría que ser aplicada tanto al objeto con cube mapping como al skybox, sin embargo, el que el skybox se mueva no quiere decir que la textura lo haga también, es decir, el objeto estará extrayendo los valores del mapa en su posición original.

Por ejemplo si rotamos $-40$ grados la vista, como se muestra en la , entonces obtendremos al punto rosa sobre el mapa de cubo original (el cubo azul), y lo que nos interesa es obtener el punto azul de la textura. Por lo que necesitaríamos simplemente rotar al vector de $\mathbf{R}$ de regreso para obtener al punto correcto en la textura. Esto es aplicar una transformación inversa de la transformación de vista.

Correción del vector de reflexión.

Cabe aclarar que el cube mapping no nos permite reflejar los otros objetos en la escena, ya que se trata siemplemente de una manera de mapear las texturas del cubo sobre el objeto. El objeto a renderizar también puede combinar esta técina junto alguna otra de las técnica de mapeado para crear superficies que no sean totalmente reflexivas.

Si es de tu interés puedes revisar esta página para conocer un poco de la historia del environment mapping.

Cube mapping. Mueve la posición de la cámara con el ratón o dedo y observa cómo cambia la parte del entorno que es reflejada sobre el objeto de acuerdo a su posición. Texturas de una parte de la plaza frente a la Basílica de San Pedro obtenidas de http://www.humus.name/index.php?page=Textures.

Referencias bibliográficas

Eck, David. Introduction to Computer Graphics. Version 1.2. 2018. http://math.hws.edu/graphicsbook Eck, David. Generated Textures Coordinates. Live Demos. https://math.hws.edu/graphicsbook/demos/c7/generated-texcoords.html Eck, David. Textures and Texture Transformations. Live Demos. https://math.hws.edu/eck/cs424/graphicsbook2018/demos/c4/texture-transform.html Eck, David. 2D and 3D Procedural Textures in WebGL. Live Demos. https://math.hws.edu/graphicsbook/demos/c7/procedural-textures.html Eck, David. WebGL Reflection Map With Skybox. Live Demos. https://math.hws.edu/eck/cs424/graphicsbook2018/source/webgl/skybox-and-env-map.html de Vries, Joey. 2014. Lighting maps. LearnOpenGL. https://learnopengl.com/Lighting/Lighting-maps de Vries, Joey. 2014. Blending. LearnOpenGL. https://learnopengl.com/Advanced-OpenGL/Blending
Brown, C. Wayne. s.f. 11.9 - Heightmaps / Displacement Maps. Learn Computer Graphics using WebGL. Recuperado el día 14 de Junio del 2021 de https://csawesome.runestone.academy/runestone/books/published/ learnwebgl2/11_surface_properties/09_heightmaps.html de Vries, Joey. 2014. Cubemaps. LearnOpenGL. https://learnopengl.com/Advanced-OpenGL/Cubemaps Cube mapping. 10 de Diciembre del 2020. Wikipedia. https://en.wikipedia.org/w/index.php?title=Cube_mapping&direction=prev&oldid=1042079006 Kromster. 1 de Febrero del 2011. Re: Should I use textures not sized to a power of 2?. [Comentario en el foro Should I use textures not sized to a power of 2?] Stackexchange. https://gamedev.stackexchange.com/questions/7927/should-i-use-textures-not-sized-to-a-power-of-2