Cuando pienso en el primer Tomb Raider (Core Design, 1996) no veo en mi mente aquellos personajes poligonales y entornos cuadriculados como realmente eran. Desde los primeros juegos renderizados en 3D hasta los faraónicos proyectos de la última generación de consolas los gráficos han sufrido una evolución increíble, que a menudo se emborrona en nuestra memoria por la curiosa capacidad del cerebro de mezclar imaginación y sentimientos con recuerdos. Pero ¿qué mejor forma de apreciar ese avance que ver de dónde han salido los efectos gráficos más modernos? Tal vez una imagen con la evolución del modelo de Lara Croft. Por si acaso, aquí tenéis ambas cosas.
La última vez que os dejé habíamos logrado trasladar a la pantalla un modelo iluminado con un solo color uniforme. Evidentemente, el trabajo necesario para pintar un modelo 3D en un juego aún no ha acabado. Queda mucho para llegar al último modelo de Lara Croft, incluso queda mucho para llegar al primero. Es el momento de hablar de texturas, shaders, mapas y juntar todas esas piezas sueltas para entender el funcionamiento de la computación gráfica moderna.
Texturas
El fascinante mundo en el que vivimos está lleno de texturas. Basta con mirar alrededor para advertirlo: Las vetas de la madera de la estantería, la pintura gotelé de la pared, nuestra piel, el estampado del sofá en el que leemos indie-o-rama … Esas texturas nos ayudan a diferenciar distintos materiales, y por tanto, distintos objetos. Por lo pronto, podemos clasificar las texturas en táctiles (algo así como las variaciones en la superficie percibidas mediante el tacto) y visuales (las variaciones de color percibidas mediante la vista). No es difícil imaginar por qué nos centramos en las últimas cuando tratamos con videojuegos, al menos hasta que se mejore la tecnología háptica.
Si queremos añadir textura a los modelos, y que tengan un aspecto adecuado, necesitamos agregar información de color que la geometría en sí misma no admite. Para ello hay que introducir el archiconocido concepto de mapeado de texturas. Típicamente, en el ámbito de los videojuegos o la animación por ordenador, una textura implica simplemente una imagen digital, un mapa de bits normalmente cuadrado, que se aplica sobre la superficie de un modelo o polígono. Para aplicarla correctamente, necesitamos saber qué punto de esta imagen corresponde a qué vértice del modelo, por lo que se hace imprescindible añadir dos coordenadas más a cada vértice: Las coordenadas U y V. Estas coordenadas, son números entre 0 y 1 que especifican un punto de la textura —siendo el (0,0) la esquina inferior izquierda y el (1,1) la superior derecha—, y se recopilan en lo que se conoce como un mapa UV.
La generación de este mapa UV es un proceso costoso, denominado (cruelmente para los que no pronuncian bien el inglés) unwrap, y en la mayoría de casos no se puede hacer automáticamente (al menos por completo). Será el modelador el que tenga que construirlo a base de desenvolver, aplanar la geometría y colocar cada cara del modelo en el lugar adecuado de la textura, trabajo muchas veces tedioso y complicado. Imaginad las líneas en un papel del recortable de un cubo: seis cuadrados, uno por cara, unidos de forma que, al recortarlo y doblarlo adecuadamente, encaja y forma la figura. Ese dibujo sería el Unwrap. Ahora imaginad que tenéis que hacer lo mismo con un modelo de miles de caras… Bueno pues, en realidad no es tan feo como lo estoy pintando, pero es un trabajo que muchos modeladores odian profundamente, con razón.
Total que, si hemos sobrevivido al proceso, y disponemos de esas coordenadas UV adjuntadas a los vértices, a la hora de pintar el modelo en la pantalla sólo necesitamos hallar qué píxel de textura (también llamado texel) corresponde a cada píxel de pantalla que abarca el polígono. Para ello, simplemente hay que interpolar esas coordenadas UV a lo largo de la superficie de cada cara y así podemos obtener el texel correspondiente, y por tanto el color. Conviene añadir que esa interpolación no es una interpolación lineal, y hay que usar una fórmula un poco más compleja para corregir la perspectiva. Además aquí entra en juego también el filtrado de texturas (que es un mundo en sí mismo) para evitar pixelaciones, aliasing, patrones Moiré etc.
Shaders
Históricamente, debido a la complejidad que fueron adquiriendo los cálculos y a la necesidad creciente de realizarlos millones de veces por fotograma, se hizo imprescindible disponer de un hardware específico que se encargara de realizar los cómputos sin saturar el procesador principal de las máquinas. Así nacieron las primeras tarjetas aceleradoras gráficas o tarjetas 3D, como la clásica Voodoo de 3DFX (la recuerdo bien porque el FIFA 98 (EA Sports, 1997) se me colgaba en el descanso del partido, pero ¡qué bien se veía! No volví a jugar un partido entero).
Estos primeros modelos de tarjetas incluían un chip diseñado específicamente para procesar gráficos 3D (Graphics Processing Unit), y proporcionaban a los desarrolladores una serie de herramientas y librerías para el renderizado de gráficos, que permitían incluir mayores tamaños de texturas, aplicar filtros, etc. Por aquel entonces, con la tecnología en expansión, era habitual que el hardware de las distintas marcas de tarjetas, así como el de las consolas, fuera bastante distinto, dando muchos quebraderos de cabeza a los desarrolladores, que se veían obligados a modificar el programa para compatibilizar el juego con una u otra tecnología.
La evolución de los GPUs desembocó en darle mayor libertad a los desarrolladores permitiéndoles programar parte del proceso de renderizado para ser ejecutado por la tarjeta gráfica. Debido al éxito de la nueva técnica, pronto desembocó en un estándar o modelo unificado, que las tarjetas gráficas actuales implementan, y al que llamamos shader model o Unified shader model. Con la primera XBox y el exitoso Halo (Bungie, 2001) comenzó a verse el verdadero potencial de los shaders con efectos como el bump mapping.
Entonces, ¿qué es un shader? Un shader es un programa ejecutado por el GPU, nada más y nada menos. Es el encargado de realizar todas las operaciones que os he ido mostrando estos días (y muchas más), y se programa en un lenguaje propio, siendo muy corriente el lenguaje CG desarrollado por nVidia y con una sintaxis muy parecida al C.
Hay tres tipos de shaders:
-
Vertex shader: Recibe la información por cada vértice, y se encarga de transformarlo, sus coordenadas, las coordenadas de textura, las normales, etcétera.
Geometry shader: Recibe la información de la geometría, por ejemplo un triángulo, y permiten crear o eliminar nuevos vértices, líneas o triángulos. Este tipo de shader es relativamente nuevo, y aún no se ha visto mucho, dado que la actual generación de consolas no lo implementa.
Pixel shader o fragment shader: Recibe la información asociada a cada píxel que ocupa la geometría de un modelo en pantalla, básicamente, la información que ha proporcionado el Vertex Shader ya interpolada para cada píxel, y se encarga de obtener un color final.
Efectos con shaders
El programa del juego se comunica con los shaders para proporcionarles la información que necesiten mediante parámetros (texturas o valores numéricos), y recibirá el resultado final, que a su vez, se puede utilizar como parámetro en otro shader. Esta cadena de alteraciones matemáticas permite toda la gama de efectos gráficos y de iluminación que vemos en los juegos.
Por ejemplo, se puede usar una textura para pasar información adicional a los shaders, si hemos generado la información UV del modelo, como ya hemos visto. Una textura en escala de grises proporciona un valor entre 0 (negro) y 1 (blanco), que podemos obtener por cada píxel. Con esto podemos marcar zonas del modelo. Lo que es muy útil, porque gracias a ello somos capaces de, por ejemplo, modificar valores de iluminación, como la propia intensidad de la luz, haciendo que algunas zonas ignoren los cálculos de iluminación y se vean siempre de un color puro, por ejemplo si queremos que los ojos rojos de algún personaje maligno siempre se vean encendidos. También se podría utilizar para modificar la intensidad del efecto especular, en lo que se conoce como specular mapping, resultando ciertas partes del modelo más o menos reflectantes, para representar un suelo mojado, o el sudor de la piel. O modificar las normales añadiendo información de rugosidad al modelo, efecto llamado bump mapping, tratando esa textura como un mapa de alturas a partir del cual calcular una nueva normal.
Un efecto un poco más avanzado es el normal mapping, del que hemos disfrutado de lo lindo en esta generación de consolas. Es muy parecido al bump mapping (de hecho se suele considerar un tipo de bump mapping), pero en lugar de usarse una textura en escala de grises, se utiliza una textura a color que proporciona en este caso 3 valores (R, G y B). En esos tres valores podemos especificar una nueva normal directamente, lo que conduce a resultados más precisos y más manejables. La generación de estos mapas de normales, sin embargo no es tan sencilla como la de un mapa de alturas, haciéndose inevitable recurrir a software específico (habitualmente Z-brush, o Mudbox), que permiten generar mapas de normales de gran calidad. Para mejorarlo, además del mapa de normales, se puede usar un mapa de alturas en escala de grises (que también puede ir incluido en el alpha del mapa de normales), y, con unos cálculos bastante complicados, obtener una mayor sensación de relieve y profundidad, en lo que se conoce como Parallax Normal Mapping. En la imagen, generada usando los materiales por defecto en Unity, se pueden observar las diferencias entre los distintos efectos si nos fijamos en los brillos al borde de las hendiduras. Es importante darse cuenta de que, aunque no se aprecie en una imagen estática, estos brillos están calculados en tiempo real, y reaccionan a los cambios en la dirección de la luz y la posición de la cámara, dando una sensación muy realista, a pesar de que el modelo es un simple cubo y no tiene ninguna hendidura.
En Resumen
En la calidad visual de un juego influyen tanto los modeladores y artistas que esculpen cada vértice de los modelos y colorean cada píxel de las texturas como los programadores gráficos capaces de entender la tecnología y la ciencia para crear los efectos.
Se puede añadir mucho detalle a los modelos con un mapeado UV, buenas texturas y los shaders adecuados, creando materiales realistas o fantásticos sin necesidad de usar más vértices o geometría. Además gracias a que se puede usar directamente el render generado como parámetro al igual que si fuera una textura más, se pueden crear efectos a pantalla completa, como difuminados y desenfoques, o espejos y reflexiones detalladas. Es posible también modificar la posición de las coordenadas UV para mover o distorsionar texturas. O variar la posición de vértices para crear ondulaciones en el agua, añadir nieve a un terreno en tiempo real, o animar la malla de un personaje según sus huesos (sí, eso también se hace con shaders y se suele llamar skinning). Las posibilidades son ilimitadas si se sabe cómo y dónde tocar (no busquéis dobles sentidos).
Pero, incluso para los menos duchos en la materia, tampoco es necesario tanto álgebra, cálculo, y whisky a palo seco para crear un juego en 3D, ya que los motores de juegos suelen venir con un surtido de materiales y shaders con los efectos más comunes, que no tendremos que programar (conviene conocer un poco la nomenclatura, eso sí). Con todo ello, sólo hará falta modelar, texturizar y animar, que no es poco.
Todo lo que he ido mostrando en los últimos artículos es sólo una pequeñísima muestra del increíble trabajo y evolución técnica que llevan actualmente los motores de juegos en su interior. Junto con todos los avances en programación gráfica, hoy en día, es casi obligatorio incluir además sistemas de partículas, motores de física (que podría dar lugar a otra interminable serie de artículos), modelos de particionamiento espacial para aligerar cargas y renderizados, sistemas de comunicación a través de la red y otros muchos avances técnicos que hacen posible tanto los grandes juegos AAA como los no tan grandes.