Rendimiento y limitaciones del 3D

Introducción

Godot sigue una filosofía de rendimiento equilibrado. En el campo del rendimiento, siempre hay compensaciones que consisten en la velocidad de intercambio en términos de usabilidad y flexibilidad. Algunos ejemplos prácticos de esto son:

  • Renderizar objetos eficientemente en grandes cantidades es fácil, pero cuando se debe renderizar una escena grande, puede volverse ineficiente. Para resolver esto, se debe agregar el cálculo de visibilidad al renderizado, lo que hace que el renderizado sea menos eficiente, pero, al mismo tiempo, se renderizan menos objetos, por lo que la eficiencia en general mejora.
  • La configuración de las propiedades de cada material para cada objeto que necesita ser renderizado también es lenta. Para resolver esto, los objetos se clasifican por material para reducir los costes, pero al mismo tiempo la clasificación tiene un coste.
  • En la física 3D ocurre una situación similar. Los mejores algoritmos para manejar grandes cantidades de objetos físicos (tales como SAP) son lentos en la inserción/eliminación de objetos y el ray-casting. Los algoritmos que permiten una inserción y eliminación más rápida, así como el ray-casting no serán capaces de manejar tantos objetos activos.

¡Y hay muchos más ejemplos de esto! Los motores de juegos se esfuerzan por ser de propósito general en la naturaleza, por lo que los algoritmos balanceados siempre son favorecidos sobre los algoritmos que pueden ser rápidos en algunas situaciones y lentos en otras… o los algoritmos que son rápidos pero hacen la usabilidad más difícil.

Godot no es una excepción y, aunque está diseñado para tener backends intercambiables para diferentes algoritmos, los predeterminados (o más bien, los únicos que están ahí por ahora) priorizan el equilibrio y la flexibilidad sobre el rendimiento.

Con esto claro, el objetivo de este tutorial es explicar cómo obtener el máximo rendimiento de Godot.

Renderización

El renderizado 3D es una de las áreas más difíciles de obtener un buen rendimiento, por lo que esta sección tendrá una lista de consejos.

Reutilizar shaders y materiales

El renderizador de Godot es un poco diferente a cualquier otro. Está diseñado para minimizar al máximo los cambios de estado de la GPU. class_SpatialMaterial hace un buen trabajo en la reutilización de materiales que necesitan shaders similares pero, si se utilizan shaders personalizados, asegúrate de reutilizarlos tanto como sea posible. Las prioridades de Godot serán así:

  • 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 (cientos o miles) intenta reutilizar los materiales o, en el peor de los casos, utiliza un atlas.
  • Reutilización de Shaders: Si los materiales no pueden ser reutilizados, al menos intenta reutilizar los shaders (o SpatialMaterials con diferentes parámetros pero 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 20.000 objetos, pero sólo utiliza 100 materiales, el renderizado será más rápido.

Coste de los píxeles vs coste de los vértices

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

En un PC y consola modernos, el costo de los vértices es bajo. Las GPUs originalmente sólo renderizaban triángulos, así como todos los vértices:

  1. Tuvo que ser transformado por la CPU (incluyendo el recorte).
  2. Tuvo que enviarse a la memoria de la GPU desde la memoria RAM principal.

Hoy en día, todo esto se maneja dentro de la GPU, por lo que el rendimiento es extremadamente alto. Los artistas 3D suelen tener una idea equivocada sobre el rendimiento de los recuentos porque los DCC 3D (como Blender, Max, etc.) necesitan mantener la geometría en la memoria de la CPU para poder editarla, lo que reduce el rendimiento real. La verdad es que un modelo renderizado por un motor 3D es mucho más óptimo que la forma en que los DCCs 3D los muestran.

En dispositivos móviles, la historia es diferente. Los GPU para computadoras y consolas son monstruos de fuerza bruta que pueden extraer tanta electricidad como necesitan de la red eléctrica. Las GPU móviles están limitadas a una batería pequeña, por lo que deben ser mucho más eficientes en términos de energía.

Para ser más eficientes, las GPU móviles intentan evitar el sobregiro. Esto significa que el mismo píxel en la pantalla se procesa (como en, con cálculo de iluminación, etc.) más de una vez. Imagine una ciudad con varios edificios, las GPU no saben qué es visible y qué está oculto hasta que lo dibujen. Se podría dibujar una casa y luego otra casa enfrente (¡la representación ocurrió dos veces para el mismo píxel!). A las GPU de PC normalmente no les importa mucho esto y solo lanzan más procesadores de píxeles al hardware para aumentar el rendimiento (pero esto también aumenta el consumo de energía).

En los dispositivos móviles, no es posible obtener más energía para consumir, por lo que se utiliza una técnica llamada «Tile Based Rendering» (casi todos los equipos móviles utilizan una variante), que divide la pantalla en una cuadrícula. Cada celda mantiene la lista de triángulos dibujados hacia ella y los ordena por profundidad para minimizar el overdraw. Esta técnica mejora el rendimiento y reduce el consumo de energía, pero afecta al rendimiento de los vértices. Como resultado, se pueden procesar menos vértices y triángulos para dibujar.

Generalmente, esto no es tan malo, pero hay un caso en la esquina del móvil que debe evitarse, que es tener objetos pequeños con mucha geometría dentro de una pequeña porción de la pantalla. Esto obliga a las GPUs móviles a ejercer mucha presión sobre una única celda de la pantalla, lo que reduce considerablemente el rendimiento (ya que todas las demás celdas deben esperar a que se complete para mostrar el cuadro).

Para resumir, no te preocupes tanto por el número de vértices en los móviles, sino que evita la concentración de vértices en pequeñas partes de la pantalla. Si, por ejemplo, un personaje, NPC, vehículo, etc. está lejos (por lo que parece pequeño), utiliza en su lugar un modelo de nivel de detalle (LOD) más pequeño.

Una situación adicional en la que se debe tener en cuenta el coste de los vértices son los objetos que tienen un procesamiento extra por vértice, como por ejemplo:

  • Skinning (animación esquelética)
  • Morphs (claves de forma)
  • Objetos con vértices iluminados (común en móviles)

Compresión de texturas

Godot ofrece comprimir texturas de modelos 3D cuando se importan (compresión VRAM). La compresión de RAM de vídeo no es tan eficiente en tamaño como la de PNG o JPG cuando se almacena, pero aumenta enormemente el rendimiento cuando se dibuja.

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.

Objetos transparentes

Como se mencionó antes, Godot clasifica los objetos por material y shader para mejorar el rendimiento. Esto, sin embargo, no se puede hacer en objetos transparentes. Los objetos transparentes se renderizan de atrás hacia adelante para hacer que se mezclen con lo que hay detrás del trabajo. Por lo tanto ¡intenta reducir al mínimo la transparencia de los objetos! Si un objeto tiene una sección pequeña con transparencia, intenta hacer de esa sección un material independiente.

Nivel de detalle (LOD)

Como también se mencionó anteriormente, el uso de objetos con menos vértices puede mejorar el rendimiento en algunos casos. Godot tiene un sistema simple para cambiar el nivel de detalle, los objetos basados en GeometryInstance tienen un rango de visibilidad que puede ser definido. Tener varios objetos GeometryInstance en diferentes rangos funciona como LOD.

Usar la instanciación (MultiMesh)

Si hay que dibujar varios objetos iguales en el mismo lugar o cercanos entre sí, intenta usar MultiMesh en su lugar. MultiMesh permite dibujar decenas de miles de objetos con un coste de rendimiento muy bajo, lo que lo hace ideal para multitudes, hierba, partículas, etc.

Bake de iluminación

Las luces pequeñas no suelen ser un problema de rendimiento. Las sombras un poco más. En general, si varias luces necesitan afectar una escena, lo ideal es hacer un bake (Baked lightmaps). El baking también puede mejorar la calidad de la escena añadiendo rebotes de luz indirectos.

Si se trabaja en móvil, se recomienda trabajar con texturas, ya que este método es más rápido.