En la mayoría de los juegos open world, el pasto es un elemento visual bastante común, que apoya a crear una imagen realista del juego. Para ello, es necesario dibujar una gran cantidad de polígonos que se mostrarán en la imagen y que se mostrarán como pasto al jugador.
Por supuesto, no es el único tipo de objeto que se renderiza en grandes cantidades. Otros ejemplos son los grupos de árboles (incluidas sus hojas), rocas, partículas personalizadas, grupos de peces o aves, o un campo de asteroides. En pocas palabras, elementos visuales que son relativamente repetitivos y utilizados para ocupar espacios o producir un efecto visual.
En Unity, la forma más fácil de renderizar objetos es utilizando un GameObject con los componentes MeshFilter y MeshRenderer, utilizando un mesh y materiales asociados para dibujar la geometría. Sin embargo, al tratar de renderizar miles de meshes, el rendimiento será disminuido.
Este rendimiento para un campo de pasto muy sencillo es inaceptable, ya que, sin considerar otros elementos de gameplay y visuales, el motor de juego no puede producir las imágenes con suficiente velocidad para una experiencia fluida.
Sin embargo, Unity también ofrece la posibilidad de dibujar geometría de modo manual, mediante la clase Graphics y el método DrawMeshInstanced. La siguiente imagen demuestra cómo se vería el resultado. Esto se conoce como GPU Instancing.
Como pueden ver, utilizando Instancing, el conteo de FPS aumenta casi al doble, mientras la duración de los threads se reduce significativamente. Las diferencias visuales que se aprecian en la imagen se deben al orden en que se renderizaron los objetos y el shader utilizado, pero con esta limitante, la mejora en el rendimiento es significativa.
Para utilizar este método, necesitamos un mesh (Unity recomienda que tenga más de 256 vertices), un material preparado para GPU instancing, generar las matrices de transformación para cada instancia, y el código para dibujar la geometría.
Dentro de Unity (aplica para otros engines también), y de forma simplificada, una matriz de transformación es un arreglo numérico de dos dimensiones que sirve para mover, escalar y rotar objetos, posiciones y direcciones. Por ejemplo, la siguiente matriz de transformación podría representar un objeto en las coordenadas (2,4,-1), y con una escala uniforme de 1.
Para Instancing, usaremos matrices semejantes para describir las posiciones, rotación y escala de los meshes que queremos generar. Para esto, usaremos el método Matrix4x4.TRS para generar esas matrices.
El siguiente bloque de código demuestra cómo utilizar esta función junto con DrawMeshInstanced, generando las matrices de transformación al inicializarse y renderizar las instancias en cada ciclo de Update. La clase se encarga de generar una las matrices de transformación para cada objeto al inicializar el GameObject. Cabe mencionar que DrawMeshInstanced sólo permite renderizar hasta 1023 objetos por invocación, por lo que la clase agrupa las matrices en arreglos de ese tamaño máximo, y guarda cada grupo en otra lista, que se usa durante el Update para dibujar la geometría.
Aquí está un ejemplo de un material activado para GPU instancing. Esto es necesario para los materiales que queramos utilizar con esta técnica.
Cabe mencionar que la técnica puede no funcionar en todos los sistemas, ya que deben contar con un GPU capaz de hacer Instancing. Así mismo, con esta técnica no es posible utilizar meshes animados con un esqueleto (SkinnedMeshRenderers), ya que las posiciones de cada hueso son calculadas en el lado del CPU y envíadas al GPU, rompiendo con el ciclo continuo necesario para la optimización. Sin embargo, es posible utilizar animaciones basadas en shaders. De igual forma, algunas optimizaciones especificas de Unity pueden lograrse con DOTS.
Para más información, puedes revisar la siguiente documentación:
- Artículo en Wikipedia sobre matrices de transformación: https://es.wikipedia.org/wiki/Matriz_de_transformaci%C3%B3n
- Documentación para GPU Instancing: https://docs.unity3d.com/Manual/GPUInstancing.html
- Referencia del método DrawMeshInstanced: https://docs.unity3d.com/ScriptReference/Graphics.DrawMeshInstanced.html
- Demostración de Instancing avanzado con SkinnedMeshRenderers: https://blog.unity.com/technology/animation-instancing-instancing-for-skinnedmeshrenderer
Ojalá les haya servido esta información, estoy seguro que abre nuevas posibilidades para renderizar objetos y ambientes más complejos.