Optimización de GPU

Introducción

La demanda de nuevas características y avances gráficos casi garantiza que te encontrarás con cuellos de botella gráficos. Algunos de estos cuellos de botella pueden ocurrir en el lado de la CPU, por ejemplo, en los cálculos dentro del motor Godot para preparar objetos para su renderización. Los cuellos de botella también pueden ocurrir en la CPU en el controlador gráfico, que ordena las instrucciones para pasar al GPU, y en la transferencia de estas instrucciones. Y finalmente, también pueden ocurrir cuellos de botella en el propio GPU.

Los cuellos de botella en el renderizado ocurren de manera muy específica según el hardware. Las GPU móviles en particular pueden tener dificultades con escenas que se ejecutan fácilmente en computadoras de escritorio.

Comprender e investigar los cuellos de botella de la GPU es ligeramente diferente a la situación en la CPU. Esto se debe a que, a menudo, solo puedes cambiar el rendimiento de manera indirecta al modificar las instrucciones que le das a la GPU. Además, puede ser más difícil tomar medidas. En muchos casos, la única forma de medir el rendimiento es examinando los cambios en el tiempo empleado en renderizar cada cuadro.

Llamadas de dibujo, cambios de estado y APIs

Nota

La siguiente sección no es relevante para los usuarios finales, pero es útil para proporcionar información de fondo que es relevante en secciones posteriores.

Godot envía instrucciones al GPU a través de una API gráfica (OpenGL, OpenGL ES o Vulkan). La comunicación y la actividad del controlador involucradas pueden ser costosas, especialmente en OpenGL y OpenGL ES. Si podemos proporcionar estas instrucciones de una manera que sea preferida por el controlador y el GPU, podemos aumentar significativamente el rendimiento.

Casi todos los comandos de la API en OpenGL requieren cierta cantidad de validación para asegurarse de que el GPU esté en el estado correcto. Incluso los comandos aparentemente simples pueden generar una serie de tareas de mantenimiento en segundo plano. Por lo tanto, el objetivo es reducir al mínimo estas instrucciones y agrupar objetos similares tanto como sea posible para que puedan ser renderizados juntos o con el menor número posible de cambios costosos de estado.

Procesamiento por lotes 2D

En 2D, los costos de tratar cada elemento de forma individual pueden ser prohibitivamente altos, ya que fácilmente puede haber miles de ellos en la pantalla. Por eso se utiliza el batching en 2D. Se agrupan múltiples elementos similares y se renderizan en un lote (batch) mediante una única llamada de dibujo, en lugar de realizar una llamada de dibujo separada para cada elemento. Además, esto significa que los cambios de estado, los cambios de material y de textura se mantienen al mínimo.

Para obtener más información sobre el procesamiento por lotes en 2D, consulte Optimización mediante procesamiento por lotes.

Procesamiento por lotes 3D

En 3D, todavía buscamos minimizar las llamadas de dibujo y los cambios de estado. Sin embargo, puede ser más difícil agrupar varios objetos en una única llamada de dibujo. Las mallas en 3D tienden a estar compuestas por cientos o miles de triángulos, y combinar grandes mallas en tiempo real resulta prohibitivamente costoso. Los costos de unirlas rápidamente superan cualquier beneficio a medida que el número de triángulos crece por malla. Una alternativa mucho mejor es unir las mallas con anticipación (mallas estáticas en relación entre sí). Esto puede hacerse ya sea por artistas o programáticamente dentro de Godot.

También hay un costo asociado al agrupar objetos en 3D. Varios objetos renderizados como uno solo no pueden ser excluidos individualmente. Una ciudad completa que está fuera de la pantalla aún se renderizará si está unida a una única hoja de hierba que se encuentra en la pantalla. Por lo tanto, siempre debes tener en cuenta la ubicación y el culling (descarte) de los objetos al intentar agrupar objetos en 3D. A pesar de esto, los beneficios de unir objetos estáticos a menudo superan otras consideraciones, especialmente para un gran número de objetos distantes o de baja resolución.

Para obtener más información sobre optimizaciones específicas de 3D, consulte Optimizando las prestaciones en 3D.

Reutilizar sombreadores y materiales

El renderizador Godot es un poco diferente a lo que existe. Está diseñado para minimizar los cambios de estado de la GPU tanto como sea posible. SpatialMaterial hace un buen trabajo al reutilizar materiales que necesitan sombreadores similares. Si se utilizan sombreadores personalizados, asegúrese de reutilizarlos tanto como sea posible. Las prioridades de Godot son:

  • Reutilización de materiales: Cuantos menos materiales diferentes haya en la escena, más rápido será el renderizado. Si una escena tiene una gran cantidad de objetos (en cientos o miles), intente reutilizar los materiales. En el peor de los casos, use atlas para disminuir la cantidad de cambios de textura.

  • Reutilización de Shaders: Si los materiales no se pueden reutilizar, al menos intente reutilizar shaders (o SpatialMaterials con diferentes parámetros, pero con la misma configuración).

Si una escena tiene, por ejemplo, 20.000 objetos con 20.000 materiales diferentes cada uno, el renderizado será lento. Si la misma escena tiene objetos de 20,000, pero solo usa materiales de 100, el renderizado será mucho más rápido.

Costo de píxeles frente a costo de vértice

Es posible que haya escuchado que cuanto menor sea el número de polígonos en un modelo, más rápido se renderizará. Esto es realmente relativo y depende de muchos factores.

En una PC y una consola modernas, el costo de vértice es bajo. Las GPU originalmente solo representaban triángulos. Esto significaba que cada cuadro:

  1. Todos los vértices tuvieron que ser transformados por la CPU (incluido el recorte).

  2. Todos los vértices debían enviarse a la memoria de la GPU desde la RAM principal.

Hoy en día, todo esto se maneja dentro de la GPU, aumentando enormemente el rendimiento. Los artistas 3D generalmente tienen la sensación equivocada sobre el rendimiento del multicuenta porque los DCC 3D (como Blender, Max, etc.) necesitan mantener la geometría en la memoria de la CPU para poder editarla, reduciendo el rendimiento real. Los motores de juegos dependen más de la GPU, por lo que pueden representar muchos triángulos de manera mucho más eficiente.

En los dispositivos móviles, la historia es diferente. Las GPU de PC y consola son monstruos de fuerza bruta que pueden extraer tanta electricidad como necesiten de la red eléctrica. Las GPU móviles están limitadas a una batería diminuta, por lo que deben ser mucho más eficientes energéticamente.

Para ser más eficientes, las GPU móviles intentan evitar sobregiro. El sobregiro se produce cuando el mismo píxel en la pantalla se representa más de una vez. Imagina una ciudad con varios edificios. Las GPU no saben qué es visible y qué está oculto hasta que lo dibujan. Por ejemplo, se puede dibujar una casa y luego otra casa frente a ella (lo que significa que la renderización se realizó dos veces para el mismo píxel). A las GPU de PC normalmente no les importa mucho esto y simplemente lanzan más procesadores de píxeles al hardware para aumentar el rendimiento (lo que también aumenta el consumo de energía).

Usar más energía no es una opción en dispositivos móviles, por lo que los dispositivos móviles usan una técnica llamada renderizado basado en mosaicos que divide la pantalla en una cuadrícula. Cada celda mantiene la lista de triángulos dibujados en ella y los ordena por profundidad para minimizar sobredibujar. Esta técnica mejora el rendimiento y reduce el consumo de energía, pero afecta el rendimiento de los vértices. Como resultado, se pueden procesar menos vértices y triángulos para dibujar.

Además, el renderizado basado en mosaicos tiene problemas cuando hay objetos pequeños con mucha geometría dentro de una pequeña porción de la pantalla. Esto obliga a las GPU móviles a ejercer mucha presión sobre un mosaico de una sola pantalla, lo que disminuye considerablemente el rendimiento, ya que todas las demás celdas deben esperar a que se complete antes de mostrar el marco.

En resumen, no se preocupe por el recuento de vértices en dispositivos móviles, pero evite la concentración de vértices en partes pequeñas de la pantalla. Si un personaje, NPC, vehículo, etc.está lejos (lo que significa que parece pequeño), use un modelo de menor nivel de detalle (LOD). Incluso en las GPU de escritorio, es preferible evitar tener triángulos más pequeños que el tamaño de un píxel en la pantalla.

Presta atención al procesamiento adicional de vértices requerido al utilizar:

  • Skinning (animación esquelética)

  • Morphs (claves de forma)

  • Objetos iluminados por vértices (común en dispositivos móviles)

Los sombreadores de píxeles/fragmentos y la tasa de llenado

A diferencia del procesamiento de vértices, los costos del sombreado de fragmentos (por píxel) han aumentado drásticamente a lo largo de los años. Las resoluciones de pantalla han aumentado (el área de una pantalla 4K es de 8,294,400 píxeles, en comparación con los 307,200 de una antigua pantalla VGA de 640×480, es decir, 27 veces el área), pero también la complejidad de los sombreadores de fragmentos ha aumentado considerablemente. El renderizado basado en física requiere cálculos complejos para cada fragmento.

Puedes probar fácilmente si un proyecto tiene limitaciones de tasa de llenado. Desactiva la sincronización vertical (V-Sync) para evitar limitar los cuadros por segundo y luego compara los cuadros por segundo al ejecutar el proyecto en una ventana grande y en una ventana muy pequeña. También puedes beneficiarte de reducir el tamaño del mapa de sombras si estás utilizando sombras. Por lo general, encontrarás que la velocidad de cuadros por segundo aumenta considerablemente al utilizar una ventana pequeña, lo que indica que estás limitado en cierta medida por la tasa de llenado. Por otro lado, si hay poco o ningún aumento en la velocidad de cuadros por segundo, entonces tu cuello de botella se encuentra en otro lugar.

Puedes aumentar el rendimiento en un proyecto limitado por la tasa de llenado al reducir la cantidad de trabajo que debe realizar la GPU. Puedes lograr esto simplificando el sombreador (quizás desactivando opciones costosas si estás utilizando un SpatialMaterial), o reduciendo el número y tamaño de las texturas utilizadas.

Cuando te dirijas a dispositivos móviles, considera utilizar los shaders más simples que puedas permitirte razonablemente usar.

Leer texturas

Otro factor en los fragment shaders es el costo de leer las texturas. Leer texturas es una operación costosa, especialmente al leer de varias texturas en un solo fragment shader. Además, considera que el filtrado puede ralentizarlo aún más (filtrado trilineal entre mipmaps y promediando). Leer texturas también es costoso en términos de consumo de energía, lo cual es un gran problema en dispositivos móviles.

Si utilizas shaders de terceros o escribes tus propios shaders, intenta utilizar algoritmos que requieran la menor cantidad de lecturas de texturas posible.

Compresión de texturas

De forma predeterminada, Godot comprime las texturas de los modelos 3D cuando se importan mediante compresión de RAM de vídeo (VRAM). La compresión de la RAM de video no es tan eficiente en tamaño como PNG o JPG cuando se almacena, pero aumenta enormemente el rendimiento al dibujar texturas lo suficientemente grandes.

Esto se debe a que el objetivo principal de la compresión de texturas es la reducción del ancho de banda entre la memoria y la GPU.

En 3D, la forma de los objetos depende más de la geometría que de la textura, por lo que la compresión generalmente no se nota. En 2D, la compresión depende más de las formas dentro de las texturas, por lo que el resultado de la compresión es más visible.

A modo de advertencia, la mayoría de los dispositivos Android no admiten la compresión de texturas con transparencia (sólo opacas), así que tenlo en cuenta.

Nota

Incluso en entornos 3D, las texturas de "pixel art" deberían tener la compresión de VRAM desactivada, ya que esto afectará negativamente su apariencia sin mejorar significativamente el rendimiento debido a su baja resolución.

Postprocesamiento y sombras

Los efectos de posprocesamiento y las sombras también pueden ser costosos en términos de actividad de fragment shading. Siempre prueba el impacto de estos en diferentes hardware.

Reducir el tamaño de los shadowmaps puede aumentar el rendimiento, tanto en términos de escritura como de lectura de los shadowmaps. Además, la mejor manera de mejorar el rendimiento de las sombras es desactivar las sombras para tantas luces y objetos como sea posible. Las OmniLights/SpotLights más pequeñas o distantes a menudo pueden tener sus sombras desactivadas con un impacto visual mínimo.

Transparencia y mezcla

Los objetos transparentes presentan problemas particulares para la eficiencia del renderizado. Los objetos opacos (especialmente en 3D) pueden ser renderizados básicamente en cualquier orden y el Z-buffer se encargará de asegurar que solo los objetos más cercanos se sombreen. Sin embargo, los objetos transparentes o con mezcla son diferentes. En la mayoría de los casos, no pueden depender del Z-buffer y deben ser renderizados en "orden de pintor" (es decir, de atrás hacia adelante) para lucir correctamente.

Los objetos transparentes también son particularmente problemáticos en términos de fill rate, ya que cada elemento debe ser dibujado incluso si otros objetos transparentes se dibujarán encima más adelante.

Los objetos opacos no tienen que hacer esto. Por lo general, pueden aprovechar el Z-buffer al escribir en él solo en primer lugar, y luego realizar el fragment shader solo en el fragmento "ganador", es decir, en el objeto que se encuentra en primer plano en un píxel en particular.

La transparencia es particularmente costosa cuando se superponen varios objetos transparentes. Por lo general, es mejor utilizar áreas transparentes lo más pequeñas posible para minimizar los requisitos de fill rate, especialmente en dispositivos móviles, donde el fill rate es muy costoso. De hecho, en muchas situaciones, renderizar geometría opaca más compleja puede resultar más rápido que utilizar transparencia para "engañar".

Asesoramiento multiplataforma

Si tienes como objetivo lanzar en múltiples plataformas, realiza pruebas tempranas y frecuentes en todas tus plataformas, especialmente en dispositivos móviles. Desarrollar un juego en escritorio pero intentar realizar el port a móvil en el último momento es una receta para el desastre.

En general, debes diseñar tu juego para el nivel más bajo de rendimiento y luego agregar mejoras opcionales para plataformas más potentes. Por ejemplo, es posible que desees utilizar el backend de GLES2 tanto para plataformas de escritorio como para dispositivos móviles si estás apuntando a ambos.

Renderizadores móviles/tileados

Como se describió anteriormente, las GPUs en dispositivos móviles funcionan de manera dramáticamente diferente a las GPUs en computadoras de escritorio. La mayoría de los dispositivos móviles utilizan renderizadores de tipo tileado. Los renderizadores de tipo tileado dividen la pantalla en pequeños azulejos de tamaño regular que se ajustan en la memoria caché súper rápida, lo cual reduce la cantidad de operaciones de lectura/escritura en la memoria principal.

Sin embargo, existen algunas desventajas. El renderizado tileado puede hacer que ciertas técnicas sean mucho más complicadas y costosas de realizar. Los azulejos que dependen de los resultados del renderizado en diferentes azulejos o de que los resultados de operaciones anteriores se conserven pueden ser muy lentos. Ten mucho cuidado al probar el rendimiento de los shaders, las texturas del viewport y el procesamiento posterior.