Introducción de Nanite en el capítulo 4 del modo Battle Royale de Fortnite

26 de enero de 2023
Hola, soy Graham Wihlidal, miembro del equipo de ingeniería (gráficos) en Epic Games. Hoy vengo a mostrar las interesantes funciones y mejoras que hemos desarrollado este año para poder implementar Nanite en el capítulo 4 del modo Battle Royale de Fortnite. Estas funciones están disponibles en Unreal Engine 5.1 (como Beta) para que las pruebes y trastees con ellas.

Unreal Engine 5.0 se lanzó con Nanite ya en fase de producción. Aunque la versión inicial ya permitía hacer muchas cosas increíbles, no incluía soporte para la enorme cantidad de funciones disponibles para mallas que no fueran de Nanite. Por ello, decidimos centrarnos en perfeccionar la funcionalidad principal de Nanite.

En el futuro, nos gustaría extender el soporte a muchas áreas que Nanite todavía no tiene cubiertas. Los usuarios han estado solicitando mucho soporte para funciones como la compensación de la posición en el entorno, la compensación de la profundidad del píxel, los UV personalizados, materiales de dos caras y materiales con máscara. A principios de año, el equipo asumió el reto de tratar de implementar el soporte para algunas de estas funciones, lo que nos obligó a resolver numerosos problemas en absoluto triviales.

Las GPU comenzaron con la denominada canalización de funciones fijas. En ella, el método de transformación de la geometría y de inscripción de la profundidad y el color venía integrado en el hardware. Por ello, solo podía configurarse con un conjunto de funciones predefinidas muy limitado. Posteriormente, gracias al código de sombreado, el hardware se volvió «programable», ofreciendo así nuevas posibilidades para los gráficos y permitiendo utilizar funciones difíciles o casi imposibles de realizar mediante la canalización de funciones fijas.

Desde sus inicios, Nanite siempre ha soportado sombreadores de gráficos de materiales «programables» que controlan el color de salida. No obstante, el propio rasterizador (el que decide cómo se colocan los vértices en la pantalla y qué píxeles quedan cubiertos por los triángulos) era, en definitiva, una «función fija»: se seguía implementado como código de sombreado, pero los creadores de contenido no podían controlar dicha lógica.

Para poder dar soporte a las funciones antes mencionadas en Nanite, necesitábamos hacer que el propio rasterizador fuese programable.

Prototipo inicial

Nos propusimos crear un prototipo de la arquitectura que necesitábamos para soportar la lógica de gráficos de materiales en el rasterizador. A este prototipo lo denominamos «Rasterizador programable de Nanite».

Las siguientes imágenes muestran el prototipo inicial de un material con máscaras animado sobre la malla de un camión de basura de Nanite.
 
El prototipo logró demostrar si el rasterizador programable era posible o no, pero quedaba mucho trabajo por delante para que estuviera listo para producción y para que fuese eficiente.

Definimos unos objetivos claros para impulsar el desarrollo de este trabajo:
  • Mantener el perfil de rendimiento de la ruta rápida de «función fija» existente (implementar el rasterizador programable no debería ralentizar el contenido existente).
  • Garantizar que las rutas de función fija y rasterizador programable compartan, en gran medida, la misma ruta de código (por razones de mantenimiento).
  • Tener en cuenta que el rasterizador programable lo usará con frecuencia una gran cantidad de contenido, por lo que los evaluadores simples deberían ofrecer un rendimiento solo ligeramente más lento que el rasterizador de función fija.
  • Hacer una sola vez el trabajo de recorte de instancias y de selección de conjuntos.
  • Minimizar el impacto de la memoria adicional en la escena de GPU y en Nanite.
  • Entregar esto como una función de producción en UE 5.1.

El prototipo inicial estaba codificado para admitir solo un único material programable en la escena. De este modo, el siguiente paso fue un pase de «agrupación» del rasterizador, lo que permitiría admitir escenas de juegos reales, compuestas por cientos de materiales, con lo que posteriormente pudimos probar contenido real.
¡Imágenes del entorno de juego medieval convertidas casi totalmente a Nanite!
Visualización de triángulos de Nanite
«Contenedores» de rasterizador únicos (materiales) en la escena
Aunque la escena de prueba de juego medieval estuviese operativa, quedaba mucho trabajo de optimización y de desarrollo de características para conseguir que el marco del rasterizador programable de Nanite se pudiera incluir en un juego.

Al principio, quedó claro que el objetivo era que el capítulo 4 de Battle Royale de Fortnite funcionara con Nanite, pero aún no teníamos soporte para las funciones necesarias para su adopción a gran escala. Incluso mallas aparentemente simples, como piezas de construcción opacas, necesitaban la compensación de la posición en el entorno para animar el icónico efecto de «rebote» al recibir daño. Fortnite se convirtió en el primer usuario del rasterizador programable.

Casos de uso en Fortnite

Elementos animados

El primer caso de uso compatible es el de elementos de malla estáticos opacos simples, los cuales utilizan la compensación de la posición en el entorno para la animación secundaria. Si bien estos elementos no requieren que Nanite se renderice de manera eficiente, el rendimiento de los mapas de sombras virtuales se escala mucho mejor con las mallas de Nanite, por lo que era importante renderizar la mayor parte posible de la escena usando Nanite.


 

 

Edificaciones

Un aspecto importante de Fortnite son los diversos bloques de edificación, y ya se ha demostrado que Nanite puede gestionar grandes ciudades sin complicaciones. Lo más lógico, por tanto, era usar Nanite para todas las mallas de construcción, aumentando así la fidelidad visual, eliminando el salto brusco entre niveles de detalle y ofreciendo un mejor rendimiento.
Construcción
En Fortnite hay demasiadas mallas de edificios como para reconstruirlas todas desde cero en todas las plataformas. Por ello, hemos creado un proceso offline que puede recoger texturas y reglas de desplazamiento creadas por artistas y producir con ellas mallas de Nanite desplazadas de gran calidad, con un número de triángulos mucho mayor que el de las versiones tradicionales.

Nota: Esto no es un desplazamiento en «tiempo de ejecución», sino un proceso offline de Fortnite que es específico para la creación de mallas desplazadas.

Además de las mejoras visuales en el renderizado de la vista principal, las mallas desplazadas de Nanite también han mejorado en gran medida la calidad del mapa de sombras virtuales, ya que detalles geométricos como los ladrillos, anteriormente representados en la textura como áreas pintadas en 2D, ahora se desplazan al propio espacio 3D. Esto otorga a las superficies una mayor profundidad y detalle, lo que permite que se apliquen las sombras y siluetas adecuadas.
 
Los edificios en Fortnite son todos mallas estáticas opacas y pueden renderizarse por completo con nuestro conjunto de características de Nanite, disponibles en Unreal Engine 5.0, a excepción del efecto de «oscilación» que se produce temporalmente cuando una construcción sufre daños (por ejemplo, cuando un jugador la golpea con un pico).

La oscilación es una pista animada simple que se aplica a través de la compensación de la posición en el entorno del material, y la evaluación siempre se efectúa, incluso cuando la oscilación o tambaleo no se produce visualmente (se utiliza un peso cero).

Teniendo una cantidad significativa de mallas de construcción en Fortnite, implementamos una optimización dirigida a este patrón, en el que se puede activar un modo especial (r.OptimizedWPO) y luego, independientemente de si un material tiene o no lógica para impulsar la compensación de la posición en el entorno, Nanite solo evaluará esta lógica si un componente primitivo dado tiene la función «Evaluate World Position Offset» activada (que es la configuración predeterminada).
Cuando Nanite realiza el pase de «agrupación de rasterizador» antes mencionado, cualquier elemento primario que normalmente habría tomado la ruta programable pero que tiene la opción «Evaluate World Position Offset» desactivada, tomará la ruta de rasterizador de la función fija estándar.

Esta optimización ha demostrado ser útil en varios sitios (incluyendo la desactivación de la compensación de la posición en el entorno cuando aumenta la distancia), pero, sobre todo, ha resultado vital para la oscilación de las edificaciones. Hemos desactivado la función «Evaluate World Position Offset» por defecto en todas las mallas de edificios y hemos ajustado el código del juego en Fortnite para poder establecer este valor mediante programación, en función de si el edificio estaba siendo dañado (y por tanto tambaleándose).
Junto con esta nueva optimización, añadimos una vista de depuración de Nanite de la función «Evaluate World Position Offset» (r.Nanite.Visualize EvaluateWPO), la cual aparece en verde si una malla está evaluando la compensación de posición en el entorno y en rojo si no lo hace.
Con esta optimización, casi todas las mallas de construcción tomarán la ruta de función fija, a excepción de un grupo de mallas que, ocasionalmente, tomarán la ruta de oscilación programable cuando sea necesario.
Foto de ejemplo
r.OptimizedWPO activado y desactivado

Árboles

Los árboles fueron el área en la que dedicamos más tiempo a la creación de prototipos y desarrollo. Para el capítulo 4 queríamos áreas boscosas exuberantes, por lo que necesitábamos una solución eficiente con un rendimiento predecible. Los árboles se basaban en una función completamente nueva de Nanite que nunca habíamos incluido en un título, por lo que necesitamos mucho trabajo de creación de prototipos y de optimización para llegar a determinar el enfoque que finalmente usaríamos.
Construcción
Nuestros experimentos iniciales usaban materiales con máscara y tarjetas para los árboles.
Para el contenido de Fortnite, descubrimos que, por lo general, era más rápido evitar usar los materiales con máscara y aumentar el número de triángulos de la malla y mantener los materiales opacos, especialmente para los árboles y la hierba. Esto se debe principalmente a que los materiales con máscara en Nanite tienen un coste significativo durante el pase básico de sombreado, ya que nos hace recalcular las coordenadas baricéntricas de los triángulos para cada píxel, y el espacio negativo en los mapas alfa implica un coste adicional sobre los píxeles rerrenderizados.

Área de conservación

Tras convertir los árboles de Fortnite para Nanite, nos percatamos de que las copas desaparecerían en la distancia a causa de un proceso de simplificación en el que las hojas se vuelven más finas o, en algunos casos, desaparecen. A partir de cierto punto, no era posible simplificar cada hoja individualmente más allá de un único triángulo y, al mismo tiempo, había que eliminar las hojas para reducir el número de triángulos. Quitar hojas de esta forma hace que el follaje se vaya diluyendo visualmente.

Para corregirlo, añadimos una nueva lógica al generador de Nanite (una opción llamada «Preserve Area» o área de conservación, que se puede activar en los «Nanite Settings» de la malla). Esa lógica redistribuye el área perdida a los triángulos restantes, dilatando los bordes de los límites abiertos. En el caso de las hojas, lo que hace es aumentar el tamaño de las hojas restantes. Aunque esto parezca extraño si se hace cerca, cuando se activa el área de conservación a distancia da la sensación de que mantiene la densidad que debería haber.

Esta función debe activarse única y exclusivamente en mallas de follaje que tengan este problema.
 
Sin área de conservación

 
Con área de conservación
Animación de viento
Sería extraño que los árboles tuvieran una buena calidad de renderizado pero no tuviesen una animación de viento. Tradicionalmente, Fortnite obtenía la animación del viento con una lógica compleja que activaba la compensación de la posición en el entorno. Dado que los árboles de Nanite tienen bastantes más vértices (en torno a 300-500 mil) que sus contrapartes que no son de Nanite (en torno a 10-20 mil), y tras evaluar la lógica de posición en el entorno dentro del rasterizador de Nanite, exploramos otras formas de animar los árboles de manera que se redujera el coste de evaluación por vértice.
Nuestros experimentos nos llevaron a convertir una compleja simulación de viento en una textura que podíamos muestrear controlando la compensación de la posición en el entorno, en lugar de tener que analizar un montón de cálculos matemáticos complejos para cada vértice.
De la geometría del árbol podemos extraer el punto de giro de cada rama y el nivel dentro de la jerarquía, así como la rama matriz de cada rama. Podemos elaborar un esqueleto a partir de esta información e introducirlo en una simulación de Houdini Vellum .
El punto de giro y la orientación de cada rama de la simulación se pueden codificar como un valor de píxel en una imagen, muestrearse en un sombreador de materiales e indexarse usando un valor de UV personalizado, codificado en el recurso de malla que se usa para seleccionar la fila de píxeles más adecuada para la rama.

A consecuencia de ello, el rasterizador Nanite solo necesita buscar una única posición y un cuaternión para calcular la compensación. Ya no depende de múltiples lecturas de textura dependientes. Actualmente, este enfoque solo admite animaciones rígidas, ya que cada rama tiene el mismo valor de UV.
Recorte a distancia
La simulación de viento en los árboles es muy importante cerca de la cámara, pero muy poco perceptible a distancia. Para optimizar el rendimiento, añadimos la capacidad de Nanite para desactivar los cálculos de la compensación de la posición en el entorno a una distancia definida por el artista. Esta optimización se basa en el modo «Evaluate World Position Offset».

Hierba

Hemos añadido compatibilidad con hierba de Paisaje para generar instancias de mallas de Nanite, incluida la compatibilidad con el recorte a distancia inherente a este sistema. El enfoque de recursos adoptado aquí es similar al que tomamos con los árboles. Utilizamos materiales opacos con geometría real para las briznas de hierba, pero solo cálculos matemáticos sencillos para accionar la animación de la compensación de la posición en el entorno.
 

 

Paisaje (Nanite)

Dada la naturaleza del funcionamiento de los mapas de sombras virtuales, las mallas grandes que no son de Nanite renderizan las sombras más lentamente que una malla de Nanite del mismo tamaño. Esto resulta especialmente problemático en Paisaje, ya que se trata de una malla masiva que no es de Nanite y que demanda mucho tiempo de GPU. Implementamos una función de renderizado experimental de Nanite para Paisaje, lanzada en UE 5.1, que convierte el campo de altura de Paisaje en una malla de Nanite a la hora de construir. La conversión no incrementa la calidad visual, pero proporciona todas las características de rendimiento del recorte y la rasterización de Nanite cuando se renderiza en el pase básico o en mapas de sombras virtuales.
Lumen tiene una ruta especializada para realizar el seguimiento del campo de altura de «Paisaje», y Nanite no admite el renderizado en texturas virtuales en tiempo de ejecución actualmente, por lo que en esos casos no se usa la representación de Nanite pero sí el campo de altura.
 

Qué nos queda por hacer

Tal vez lo más importante que nos queda por implementar es el cálculo preciso (por fotograma) del volumen de delimitación de conjuntos e instancias, que coincida con la animación realizada por la compensación de la posición en el entorno. Este aspecto es vital para garantizar que la eliminación de obstrucciones de Nanite elimine tantos conjuntos como sea posible antes de la rasterización.
 

Por el momento, usamos los límites de la pose de referencia (ignorando la compensación de la posición en el entorno), ya que, o bien es demasiado conservadora (rasteriza más grupos de los necesarios) o provoca artefactos visuales en el caso de que la compensación de la posición en el entorno anime los conjuntos totalmente fuera de los límites de la pose de referencia (hay trozos de la malla que desaparecen). Implementamos un soporte aproximado para la Escala de límites en componentes primitivos (de forma similar a cómo abordan este problema los que no son de Nanite). En ellos, los límites se pueden escalar en una cifra arbitraria, pero esto hace que el recorte sea aún más conservador, derivando en un coste de rendimiento innecesario.

También queremos seguir optimizando el sistema de materiales en Nanite para reducir el coste tanto de los materiales con máscara como de los materiales con compensación de profundidad del píxel.

¡Nos complace anunciar esta nueva función para Nanite en Unreal Engine 5.1! Tenemos muchas ganas de ver el contenido que los desarrolladores crean con ella.
Para obtener más información sobre las características a las que se hace referencia en este blog, puedes consultar la documentación de Nanite.

    ¡Hazte con Unreal Engine hoy mismo!

    Disfruta de la herramienta de creación más accesible y avanzada del mundo.
    Unreal Engine viene con todas las funciones y acceso ilimitado al código fuente, ¡listo para usar!