Renderizado a escala: Estrategias eficientes para el conteo masivo de objetos

Esta es la cuarta publicación de una serie escrita por Mega Cat Studios. En esta publicación, Matthew Wojtechko analiza las lecciones de rendimiento aprendidas al usar URP y HDRP, y explora soluciones avanzadas como implementaciones personalizadas de Batch Renderer Group para la representación de césped, donde cada milisegundo cuenta.
Lee las demás entradas de blog de esta serie:
- Escalado de flujos de trabajo de Unity : Lecciones aprendidas de proyectos medianos y grandes
- Batear mil veces: Cómo el sistema de entrada basado en eventos de Unity impulsa los controles en Backyard Baseball
- Cómo reinventar un clásico juego deportivo para una nueva generación con diseño de niveles, creación de mundos y efectos visuales.
Renderizar miles de objetos sin perder fotogramas es un desafío constante. Los densos paisajes urbanos y los extensos entornos naturales se han convertido en el sello distintivo de los videojuegos modernos. Pero si bien estos paisajes son impresionantes para los jugadores, pueden ser un dolor de cabeza para los ingenieros encargados de mantener una fluidez de 60 fotogramas por segundo (fps).
Y si el objetivo son plataformas con menos potencia, como dispositivos móviles o inalámbricos, no son solo esas vistas las que representan un problema. Cualquier entorno puede fácilmente sobrecargar el hardware si incluye algún nivel de detalle.
En Mega Cat Studios llevamos años exprimiendo al máximo las capacidades de los sistemas de renderizado de Unity, y hemos utilizado todos nuestros recursos para conseguir que el entrañable estilo artístico de Backyard Baseball funcionara en PC y consolas sin sacrificar la intención artística.
En esta publicación, exploraremos técnicas como el procesamiento por lotes estático y las texturas de animación de vértices, y compartiremos algunos de los problemas que encontramos en nuestro título 3D más grande hasta la fecha, todo con el objetivo de ayudarte a mantener una velocidad de fotogramas fluida a medida que aumenta exponencialmente la cantidad de objetos.

Investiga qué está causando los problemas de renderizado y luego optimiza.
El renderizado es una colaboración entre la CPU y la GPU.
“La CPU es el entrenador y la GPU es el atleta.” "La CPU decide la jugada y la GPU la ejecuta", afirma Liam Dudas, ingeniero de optimización de Backyard Baseball.
En ocasiones, el trabajo que tiene que realizar la GPU consume más tiempo que otras tareas. Las mallas, los sombreadores y la iluminación complejos pueden provocar ralentizaciones, y la mejor manera de mejorar el rendimiento es reducir su complejidad pidiéndole al artista que optimice estos elementos.
Otras veces, no se trata de lo que la CPU le pide a la GPU que haga, sino de cómo se lo pide .
¿Está la CPU ejecutando sus instrucciones de forma óptima? ¿Está tardando mucho en decidir qué dibujar, haciendo que la CPU se entretenga en lugar de ejecutar el resto del juego? ¿La CPU proporciona información de forma redundante? Un buen entrenador puede inspirar una remontada inesperada de su equipo, pero cuando se trata de optimización, no podemos simplemente hacer que estos sistemas "se esfuercen más". Se trata de comprender la información con la que trabajan y cómo la comunican.
Esto se reduce a optimizar las llamadas de dibujo que la CPU pasa a la GPU. Cuanto más trabajo podamos incluir en una sola llamada, mejor, especialmente cuando hay decenas de miles de objetos que renderizar.
Existen muchos trucos para lograrlo, pero antes de intentarlo, ten en cuenta lo siguiente:
No optimices sin ton ni son, solo por la satisfacción de tener un código ingenioso. Siempre realice primero una evaluación comparativa. Descubre dónde está sufriendo más tu proyecto. La primera pregunta que siempre debes hacerte: ¿El cuello de botella está en la CPU o en la GPU?
Algunas de las soluciones que se presentan en esta publicación aumentan la velocidad de la CPU, otras la de la GPU. No conviene obsesionarse con optimizar un procesador cuando el otro es en realidad la causa de los picos de fotogramas. Una vez que decidas si tu juego depende principalmente de la CPU o de la GPU, investiga qué procedimiento tarda más y optimízalo. La guía de mejores prácticas de Unity para la creación de perfiles es un excelente punto de partida, ya que incluye un práctico diagrama de flujo que ofrece orientación sobre el a menudo misterioso proceso de creación de perfiles.
“Tengo que hacer muchos perfiles”, dice Liam. “Puedo notar cuándo la CPU está trabajando mucho y la GPU no, y viceversa. En realidad, nuestro mayor obstáculo han sido ambos en diferentes momentos.
Hemos incluido una gran cantidad de nuestros recursos favoritos al final de esta publicación para ayudarte en tu propio proceso de elaboración de perfiles.
Y una vez aclarado esto, exploremos las optimizaciones.
Simplificar y seleccionar
Al renderizar, necesitamos determinar qué mallas renderizar y dónde.
Cuantas más mallas tengamos y cuantas más llamadas de dibujo necesitemos, más tendrá que estar procesando la CPU para determinar qué hacer mientras la GPU espera su siguiente tarea.
Podríamos acelerar el proceso renderizando menos mallas. Por ejemplo, podrías reemplazar mallas complejas con impostores, vallas publicitarias o versiones de baja poligonalidad; o podrías dividir un mundo extenso en niveles más pequeños con menos elementos que renderizar a la vez. Estas son soluciones probadas y eficaces que deberías utilizar siempre que puedas, pero limitan el nivel de detalle en tus entornos.
Una optimización que evita esto, y que es sencilla de implementar, es el descarte por oclusión, donde la cámara comprueba qué hay dentro de su campo de visión y evita renderizar lo que el jugador no puede ver. Esto ahorra tiempo a costa de un mayor consumo de memoria, pero a menudo merece la pena en entornos con muchas mallas estáticas, como las habitaciones detalladas de un edificio de oficinas. Esta optimización es un estándar de la industria, pero a menudo no es suficiente por sí sola.
Agrupación estática
Otra forma de limitar el número de mallas que renderizamos sin sacrificar la variedad es combinándolas en una sola malla.
Puedes hacerlo tú mismo manualmente. O bien, siempre que las mallas compartan el mismo material, Unity puede hacerlo por usted mediante un proceso llamado procesamiento por lotes. Si se establece un objeto de juego como estático, Unity agrupará automáticamente todas las mallas con el mismo material para reducir las llamadas de dibujo. Utilice esta opción en cualquier elemento que permanezca estático, como árboles y paredes.
Esto reduce drásticamente el uso de la RAM y la carga de la CPU, lo que permite que la GPU renderice más contenido a la vez. Pero ten en cuenta que se utilizará más memoria de la GPU para almacenar las mallas combinadas.
El procesamiento por lotes estático suele ser nuestro proceso de optimización clave número 1, especialmente para Backyard Baseball . El sistema de producción por lotes de URP es sencillo, por lo que la parte más difícil es averiguar cómo utilizar la menor cantidad de materiales posible en la mayor cantidad de modelos posible. Ese delicado equilibrio es la razón por la que contamos con algunos de los gatos más creativos en nuestros equipos de optimización y arte.
Instanciación de GPU
Este es un buen momento para empezar a distinguir entre una cosa... y una instancia de esa cosa.
Digamos que tienes una malla. Una malla es un activo. Y su material es un activo. Son archivos individuales que existen en la estructura de archivos. Para cada objeto con apariencia 3D en tu juego, una malla describe su forma y, a continuación, un material describe el aspecto de su superficie.
Pero debido a que Unity simplifica sus detalles, algunos desarrolladores no se dan cuenta de que, cuando su juego está en funcionamiento, en realidad tienen múltiples copias de estos elementos instanciadas en el mundo del juego. Cuando tu jugador explora un bosque, se mueve a través de muchas copias de ese mismo árbol, almacenadas como un recurso en los archivos del juego.
Por eso utilizamos la instanciación de GPU para la vegetación en Backyard Baseball . En lugar de renderizar cada copia del árbol con llamadas individuales a la GPU, la CPU le pide a la GPU que renderice todos los modelos idénticos a la vez. Si no tienes cuidado, esto podría provocar que todos los árboles del bosque parezcan idénticos. Sin embargo, con algunos parámetros efectivos en el sombreador del material, por ejemplo, un valor para personalizar el color de las hojas, se puede pasar a la GPU un modelo y un material, junto con los colores únicos para cada árbol, en lugar de solicitar cada árbol individualmente.

Al activar la opción "Habilitar instancia" en un material, le indicas a Unity que combine en la misma llamada de dibujo diferentes instancias de material que utilicen el mismo material. Esta es una excelente manera de equilibrar la variación y el rendimiento en tus mallas. Además, a diferencia del procesamiento por lotes estático, la instanciación por GPU se puede utilizar en objetos del juego que se mueven en la escena, por lo que no estás limitado a optimizar solo elementos como paredes y árboles, sino también accesorios como escombros de un naufragio o material de oficina.
Texturas de animación de vértices
El procesamiento por lotes estático y la instanciación por GPU son dos formas diferentes de optimizar mallas y materiales. Sin embargo, a veces son las animaciones las que provocan el cuello de botella. Tomemos como ejemplo el sencillo ejemplo del bosque que vimos antes. Ahora, imagina que sus ramas y hojas se mecen con la brisa, todo ello con una animación única y natural. Ejecutar 10.000 procesos de animación únicos de la forma habitual simplemente no será suficiente. ¡Si no tienes cuidado, la pobre CPU se quedará sin aire!
Un truco consiste en utilizar texturas de animación de vértices (VAT). Un programa de automatización de vídeo (VAT, por sus siglas en inglés) secuestra la potencia de procesamiento de una GPU para intentar realizar animaciones.
Para David Chávez Armenteros, desarrollador sénior de Mega Cat Studios, usar VAT para simular una multitud era una de sus implementaciones gráficas favoritas para optimizar.
“Al usar los renderizadores de malla con esqueleto estándar con Animator, solo se pueden renderizar unas pocas docenas de personajes antes de que el rendimiento empiece a disminuir”, afirma. “Pero al combinar VAT con la instanciación de GPU, se pueden mostrar miles de entidades animadas en pantalla simultáneamente.”
Con VAT, cada vértice de la malla lee su posición animada a partir de una textura en cada fotograma. Para ello, codificamos los datos de posición y rotación en valores RGB . A continuación, un sombreador especial lee todos los píxeles de una textura para determinar cómo mover todos los vértices.
Esto es lo que usamos para el césped que se mecía en Backyard Baseball . Es una forma eficiente de crear animaciones fluidas, pero recuerda que es puramente visual: el colisionador no cambia. Esto hace que VAT sea ideal para situaciones en las que el movimiento es esencial, pero la detección precisa de colisiones no lo es.

Y toda optimización conlleva una contrapartida. El IVA libera la CPU, a costa de una carga adicional en la RAM. Todo esto debe sopesarse con otras soluciones de renderizado presentes en el juego.
“Lo más complejo del sistema fue equilibrar el uso de la memoria y los cálculos de selección basados en la GPU para lidiar con los recursos limitados de los dispositivos inalámbricos”, dice Jordan Latta, artista técnico de Backyard Baseball .
URP vs HDRP
La primera decisión que debes tomar al optimizar los gráficos es qué pipeline de renderizado elegir al iniciar un proyecto. El Universal Render Pipeline (URP) ofrece un buen rendimiento básico y es absolutamente imprescindible para dispositivos móviles e inalámbricos. El High Definition Render Pipeline (HDRP) está diseñado para generar imágenes de alta fidelidad y ofrece un comportamiento de instanciación más especializado.
En general, recomendamos usar URP en lugar de HDRP en Unity. Quizás usted tenga un caso de uso que requiera HDRP, pero nosotros nos guiamos por este sencillo principio: Mantén tu renderizado lo más eficiente posible.
HDRP ofrece algunas capacidades de alta gama, pero si sabes que no buscas una fidelidad visual realista, te recomendamos que no lo uses.
Más trucos de optimización
A veces, el problema de tener miles de objetos en pantalla no radica en el renderizado en absoluto. Es tu propio código. En esos casos, aquí tienes algunos trucos que puedes emplear:
- Consolida miles de bucles de actualización en uno solo. En lugar de asignar a cada GameObject su propio MonoBehaviour, consolídalos en uno solo para que compartan la misma función Update. Un gestor centralizado ayuda a evitar miles de bucles de actualización individuales.
- En lugar de ejecutar todo en el mismo hilo, utilice multihilo. Analiza si el sistema de trabajos y el compilador Burst podrían ser buenas opciones para tu juego.
- Saca provecho de los alijos. En lugar de obtener la cámara principal o la transformación de un GameObject en cada fotograma, guárdalas en caché una sola vez y luego reutiliza esa referencia. Tarodev demuestra la mejora del rendimiento y explica también estas otras optimizaciones.
Quizás la mayor mejora en el rendimiento que puedes obtener para tu próximo proyecto sea usar la última versión LTS de Unity. Unity 6.3 LTS cuenta con numerosas mejoras, incluidas optimizaciones para mejorar el rendimiento.
En el caso de Backyard Baseball , nuestra optimización más sencilla y efectiva consistió en controlar el uso de texturas.
Existen algunas maneras ingeniosas de usar accidentalmente el doble de texturas de las que necesitas:
- Shaders : Si un sombreador utiliza una textura en dos ocasiones diferentes, y lo hace sin compartir la misma referencia, puede cargar la textura en múltiples ubicaciones de la memoria.
- Scripts : Del mismo modo, diferentes áreas de tu código fuente pueden acabar cargando la misma textura a través de diferentes referencias.
- Version Control : Cuando dos desarrolladores añaden el mismo archivo a su rama local, a veces ambos archivos se conservan al fusionar las ramas, ocupando un espacio de almacenamiento innecesario.
Intentamos ser conscientes de estos escollos para poder prevenirlos incluso antes de que ocurran, pero ser demasiado estrictos ralentiza el ciclo de iteración acelerado. Ahí es donde entran en juego nuestros expertos en optimización, que auditan los sombreadores, los recursos y los scripts cuando el proyecto entra en las fases finales de su ciclo de desarrollo.
En Mega Cat Studios , estas estrategias nos permiten renderizar escenas que eran impensables hace tan solo unos años.
Para nosotros, el renderizado es prácticamente magia: podemos crear cualquier cosa que podamos imaginar en tiempo real, pero solo después de estudiar cuidadosamente los textos secretos (documentación de Unity , publicaciones de blog, tutoriales de YouTube). Por suerte, contamos con algunos genios en nuestro equipo a quienes les encanta sumergirse en esa documentación y compartir los hallazgos con el equipo.
Y por supuesto, los que realmente se benefician son nuestros jugadores. Esperamos que los aficionados disfruten explorando Backyard Baseball sin siquiera detenerse a pensar en la cantidad de sangre, sudor y magia que se invirtió para que ese mundo se sintiera real.
Más información
- Guías de creación de perfiles de Unity
- Guía de mejores prácticas para la creación de perfiles en Unity
- Guía definitiva para la creación de perfiles de juegos en Unity 6 | Unity
- Optimiza el rendimiento de tus juegos para consolas y PC en Unity (edición Unity 6) | Unity
- Optimización del rendimiento para juegos móviles, XR y web (edición Unity 6) | Unity
- Documentación de Unity
- Anuncio de las nuevas características de Unity 6
- Tutoriales oficiales de optimización de Unity
- Cómo renderizar 2 millones de objetos a 120 FPS, según Tarodev.
