Texture Packaging: optimizar el rendimiento
La optimización de recursos siempre debe ser una prioridad cuando desarrollamos videojuegos. Este proceso no tiene un espacio fijo dentro del ciclo de producción de un proyecto, sino más bien, es una práctica que se debe desempeñar en el momento que se considera oportuno.
Dentro de las muchas técnicas de optimización que existen en el desarrollo de videojuegos y 3D en general, una de las más famosas y utilizadas es el empaquetado de texturas.
Existen dos variantes referentes al empaquetado de texturas: Los Sprite/Texture Atlas y el Channel Packaging. Nos centraremos en la segunda opción.
Utilizaré Unreal Engine para ejemplificar este post. En la práctica, el tema abordado sirve para cualquier engine.
¿Cómo impacta una textura el rendimiento de un videojuego?
Normalmente, las texturas implican uso de recursos importantes: el espacio en memoria que consumen y la cantidad de llamadas gráficas que hacen.
Por un lado, trabajar con texturas de alta resolución implica mayor cantidad de tiempo de lectura y mayor consumo de memoria. Esto se traduce (a grandes rasgos) en juegos pesados con desempeño pobre. Una textura 4K puede llegar a ser demasiado costosa de acuerdo a su nivel de compresión.
Por el otro lado, cada textura genera una llamada gráfica a la GPU. Más llamadas gráficas se traducen en más instrucciones de shader, lo que a su vez significa más carga de trabajo para la GPU. Imagina una escena con 100 objetos en pantalla y cada uno de ellos utiliza 5 o 6 mapas de textura con resolución 4K. No parece óptimo… y no hemos incluido sombras e iluminación aún.
Visualicemos un ejemplo práctico para entender lo anterior. En Unreal un shader sencillo para una superficie metálica (basado en mapas de textura, por supuesto) luciría de la siguiente manera:
Así es como comúnmente nuestro material luce cuando lo aplicamos a una superficie. Solo tuvimos que arrastrar nuestras texturas al editor de shaders y conectarlas a nuestro Main Material Node. Sencillo. Veamos que ocurre detrás de esto.
En realidad, no está nada mal. Nuestro shader ejecuta 158 instrucciones de shading para nuestra GPU y un conteo de 8 Texture Samplers. ¡Perfecto!, no está roto, así que no requiere que lo arregles… pero, ¿y si lo volvemos aún mejor?.
¿Qué es el Channel Packaging?
Literalmente “Channel Packaging“ es una práctica que nos permite empaquetar diferentes texturas en los canales de información de color de otra textura. La textura resultante almacena estos valores en los canales RGBA de forma independiente.
El ejemplo anterior se conoce como textura ARM. Una textura ARM se compone del empaquetado de las texturas Ambient Occlusion, Roughness y Metallic en los canales RGB de una textura respectivamente.
El empaquetado ARM es una forma convencional de organizar los mapas de textura en los distintos canales, sin embargo, puedes hacerlo con diferentes combinaciones y mapas. Otra combinacion común es el empaquetado DRM (Displacement, Roughness, Metallic).
Aunque el empaquetado depende del artista técnico o requerimientos del proyecto, hay un par de parámetros que se deben tener en cuenta para la buena práctica:
1. Single Channel Textures
Los colores que percibimos en una pantalla se forman a través de los colores RGB (Red, Green, Blue). La emulación de otros colores corre a cargo de la mezcla producida por estos tres colores más la variación de su brillo y la saturación.
La información que manejan estos canales de forma individual para cualquier shader, siempre está contenida en un rango definido y normalizado que va desde 0 a 1, donde 0 significa que no contiene información (negro) y 1 que contiene la mayor cantidad de información posible (blanco).
Esto quiere decir que la información que pueden contener los canales de una textura van de negro a blanco, pasando por una escala de grises. Interesante ¿no?, ya que precisamente las texturas Ambien Occlusion, Roughness y Metallic son texturas con este tipo de información.
Por conclusión, una textura empaquetada por canales debe ser una textura Single Channel, es decir, con información de negro a blanco y con escalas de grises.
En videojuegos y en 3D existe la textura Diffuse o Base Color, la cual es la representación del color que nuestro material/objeto tomara en el shading. Estas texturas dependen de los tres canales de color (RGB) por lo cual no pueden formar parte de un empaquetado por canales.
2. Percepción del color
El ojo humano está constituido por células conocidas como conos, las cuales nos permiten ver los colores que nos rodean. La percepción de los colores se delimita por el espectro de onda a la que estos conos reaccionan.
Los colores más fáciles de ver para nuestros ojos son los colores verde y amarillo, mientras que los más complicados son los extremos de la onda, es decir rojo y violeta.
Este dato biológico nos concluye que podemos empaquetar nuestro mapa de textura con la mayor cantidad de información contenida dentro del canal Green de nuestra textura final. De este modo la percepción de nuestros ojos sobre ese mapa de textura mantendrá mayor detalle gracias a esa información más perceptible.
Channel Packaging con GIMP
Para crear un Channel Packaging de forma sencilla con GIMP, abriremos en nuestro editor de imágenes un archivo editable por cada mapa de textura que deseamos integrar en el empaquetado. Utilizando el ejemplo anterior tendremos algo como esto:
Ahora crearemos un nuevo archivo editable, con la misma resolución de nuestros mapas de textura. Es importante verificar que nuestro editable se encuentre en formato RGB:
Una vez este lista la configuración, seguiremos esta serie de pasos para cada uno de nuestros mapas de textura single channel:
1. Seleccionar todo el mapa single channel con Ctrl+A y copiar la selección con Ctrl+C
2. Ir al nuevo archivo editable vacío y abrir la pestaña “Channels” de la derecha. Seleccionar el canal al cual vamos a integrar nuestro mapa de textura previamente copiado.
3. Con el canal seleccionado (el color más oscuro destaca el canal seleccionado) presionaremos Ctrl+V y, sin soltar el botón Ctrl, presionaremos una vez más V.
4. Al finalizar con un mapa, deseleccionamos el canal y procedemos con el siguiente. Una vez terminado el proceso tendremos un resultado parecido a esto:
De esta forma podemos generar nuestros propios mapas de textura personalizados y empaquetados, listos para ser importados dentro de nuestros proyectos en Unreal Engine, Unity, Godot o cualquier otro motor. Ahora veamos cuál es el resultado dentro de Unreal.
Impacto de las texturas empaquetadas en un videojuego
Utilizando el ejemplo anterior en Unreal Engine, vamos a reconfigurar nuestro shader para que haga uso de la nueva textura empaquetada que construimos anteriormente con GIMP.
Como puedes ver, el resultado es practicamente el mismo que en su versión con mapas individuales. No hay diferencia. Como esta textura ARM está conformada por el conjunto de mapas, utilizamos la salida independiente de cada canal en el nodo de textura para leer la información que contiene.
Ahora veamos que impacto tiene esta configuración en el rendimiento.
En comparación con el primer ejemplo, ahora nuestro shader procesa 156 instrucciones y 6 texture samplers, es decir, 2 instrucciones de shading y 2 texture samplers menos. Los stats actuales muestran una mejora de rendimiento que quizá parezca poco, pero debemos recordar que en la optimización siempre es mejor sacar el máximo rendimiento posible.
Esta técnica te ayudará a ahorrar valiosos recursos, sobre todo cuando comiences a trabajar con shaders complejos.
Siguientes pasos
Con esta técnica estas un paso adelante en tu camino como desarrollador, pero no acaba aquí. Existen más combinaciones de empaquetado de texturas que puedes crear, no solamente ARM y DRM.
Este es el primer nivel de empaquetado posible. Aquí creamos un shader con tres texturas (Diffuse, ARM y Normal) pero, ¿sabías que puedes empaquetar a un segundo nivel y reducir esas tres texturas a solamente dos?
Es importante que recuerdes que esto es solo una técnica de optimización de muchas que existen, pero es un primer paso que ahora debes practicar para pulir tus habilidades. Comienza traduciendo tus shaders a texturas empaquetadas, prueba distintas combinaciones y crea cosas increíbles.