La tecnología Nanite de UE5 en WebGPU
(github.com/Scthe)- Nanite WebGPU es un proyecto que implementa renderizado estilo Nanite de UE5 dentro del navegador con WebGPU, e incluye jerarquía LOD de meshlets, rasterizador por software, billboard impostor y culling por instancia/meshlet
- La implementación prioriza la simplicidad de preprocesar entradas OBJ en el navegador y experimentar con cambios de rendimiento modificando ajustes con casillas y deslizadores
- La demo ofrece escenas como Jinx con 640M triangles, Lucy/Dragons y otras de hasta 1.7B triangles; los triángulos blancos de Jinx son el resultado del rasterizado por software procesado con compresión a 32 bits debido a las limitaciones de WebGPU
- Como WebGPU no tiene
atomic<u64>, es difícil implementar tal cual un rasterizador por software eficiente basado en visibility buffer; la implementación actual empaqueta depthu16y octahedron normal2*u8en 32 bits, aceptando artefactos de precisión - En una implementación real estilo Nanite, el desafío clave no es simplemente la cantidad de triángulos sino la simplificación de malla y el error metric; el autor aclara que su implementación, a diferencia de UE5, no incluye simplificación, streaming, compresión, visibility buffer ni 2-pass occlusion culling
Resumen del proyecto
- Nanite WebGPU es un proyecto que implementa renderizado estilo Nanite en el navegador web usando WebGPU
- Los elementos principales incluidos son meshlet LOD hierarchy, rasterizador por software, billboard impostor y culling por instancia y por meshlet
- Soporta modelos con texturas y per-vertex normal, y ofrece deslizadores o casillas para cada configuración
- WebGPU solo puede usarse en Chrome
- Toda la app puede ejecutarse incluso sin conexión usando Deno, y las pruebas unitarias de shaders también están hechas de esta forma
Escenas de demostración y escala
- La demo de Jinx usa 120×120 instancias y alcanza 640M triangles
- Un solo modelo de Jinx se simplifica de 44k triangles a 3k triangles, con 59 root meshlets
- Los triángulos blancos son triángulos rasterizados por software que están entre el área rasterizada por hardware y los impostors del fondo lejano
- Lucy and dragons coloca dos objetos con 70×70 instancias cada uno para formar 1.7B triangles
- En la escena de ejemplo, casi el 98% de los triángulos se rasterizan por software, y se indica que esta ruta es mucho más rápida que la de hardware
- Lucy es una escena de 110×110 instancias y 1.2B triangles
- Una sola Lucy statue se simplifica de 100k triangles a un único root meshlet de 86 triangles
- Dragons es una escena de 70×70 instancias y 1.2B triangles
- Un solo dragon se simplifica de 250k triangles a un único root meshlet de 102 triangles
- Bunnies es una escena de 500×500 instancias y 1.2B triangles
- Un solo bunny se simplifica de 5k triangles a un único root meshlet de 96 triangles
- Como el bunny es pequeño, la mayor parte se descarta con frustum culling
Funciones implementadas
-
Meshlet LOD hierarchy
- El mesh preprocessing se ejecuta en el navegador
- Usa meshoptimizer y METIS mediante WebAssembly
- También ofrece un exporter de archivos para no tener que esperar el preprocesamiento entre recargas de la página
-
Software rasterizer
- WebGPU no tiene
atomic<u64>, necesario para una implementación eficiente - La implementación actual empaqueta depth
u16y octahedron-encoded normals2*u8en 32 bits - Por la limitación de 32 bits, sacrifica mucho la precisión, y el objetivo es mostrar que el rasterizador funciona
- Esta limitación también afecta la depth pyramid usada para occlusion culling
- WebGPU no tiene
-
Billboard impostors
- Usa 12 imágenes alrededor del eje UP y las mezcla con dithering según la posición de la cámara
- No maneja vistas desde arriba o desde abajo
- Incluye tanto diffuse como normal para hacer shading en tiempo de ejecución
- Impostors preview es una demo con mayor tamaño de textura para impostor
Culling y rutas de renderizado
- El culling está compuesto por varias etapas
- Per-instance: frustum culling y occlusion culling
- Per-meshlet: frustum culling y occlusion culling
- Per-triangle: hardware backface culling y z-buffer
- WebGPU no tiene early-z
- El occlusion culling solo usa la depth pyramid creada a partir del depth buffer del frame anterior
- No hay reprojection ni enfoque two-pass
- Considera que la implementación actual ya descarta muchos triángulos y basta para evaluar mejoras de rendimiento
- Se puede alternar entre GPU-driven rendering y una implementación simple en CPU
- La versión de CPU no tiene muchas optimizaciones, pero permite ejecutar paso a paso con el depurador
- La función “Freeze culling” permite mover la cámara mientras se inspecciona solo lo que se dibujó en el último frame
Usabilidad y depuración
- El movimiento usa
[W, S, A, D], subir/bajar usa[Z, SPACEBAR]y desplazamiento rápido usa[Shift] - Si aparecen resultados extraños, se puede comprobar activando y desactivando opciones de culling
- La implementación tiene algunos bugs menores
- Los triángulos blancos son triángulos rasterizados por software
- Debido a la ausencia de
atomic<u64>en WebGPU, depth y normal se comprimen en 32 bits - El depth de 16-bit puede generar muchos artefactos como z-fighting o leaks
- Si se desactiva el rasterizador por software, es más fácil ver los raw Nanite meshlets, pero puede haber una gran caída de rendimiento
- Debido a la ausencia de
- Los FPS pueden variar por el VSync forzado del navegador; para revisar tiempos, se usa el botón “Profile”
Objetivos de diseño
- El primer objetivo del proyecto es la simplicidad
- Parte de un archivo OBJ y realiza todo el procesamiento dentro de la app
- No requiere pasos de preprocesamiento separados, como una exportación desde Blender
- La idea es que puedas poner un breakpoint en
loadObjFile()y seguir el flujo hasta que termine el primer frame
- El segundo objetivo es la experimentación
- Como está hecho como página web en lugar de usar Rust y Vulkan, puedes hacer clic en un enlace y cambiar casillas o sliders para ver cómo varía el rendimiento
- Puedes comprobar directamente qué opciones importan, por ejemplo cuando cambiar una sola configuración hace que los FPS caigan mucho
- Mucho código podría optimizarse, pero se considera que esas optimizaciones no son importantes hasta resolver el problema de simplification
Diferencias con UE5 Nanite
- El error metric usa un projected simplification error simple
- La meshlet simplification es simple
- No hay two-pass occlusion culling
- Agregarlo no es difícil, pero depurarlo es incómodo y genera interacciones con la configuración de la GUI
- Va en contra del objetivo porque perjudica la legibilidad del código
- No hay visibility buffer
- No puede implementarse por la limitación de
atomic<u64>
- No puede implementarse por la limitación de
- No hay built-in shadows ni multiview
- No hay work queue dentro del shader
- Para meshlet culling y LOD selection se hace dispatch de un thread por meshlet
- No hay VRAM eviction ni streaming para LOD que no se usan
- No hay compression
- Escala mal con muchos objetos diferentes
- En el caso de demo, el foco está en un uso de memoria predecible
- Hay que conocer de antemano el límite superior de meshlets dibujados para el buffer que guarda los datos entre stages
- No hay implementación de BVH ni jerarquía para instancias
- Se hace frustum y occlusion culling sobre todas las instancias
Qué condiciones implica la cantidad de triángulos
- Solo con la cifra de “decenas de miles de millones de triangles” es difícil juzgar el rendimiento, y este depende de muchos factores
-
Dense meshes
- Tener muchas dense mesh cerca puede afectar negativamente el rendimiento
- Aun así, si están lo bastante cerca como para cubrir gran parte de la pantalla, entra en acción el occlusion culling
- La dense geometry tiende a tener meshlets pequeños, que ocupan poco espacio en pantalla y son fáciles de descartar con occlusion/cone culling
-
Instance count
- Cada instancia usa VRAM porque lleva una matriz de transformación
mat4x3 - Durante el frame también hay que almacenar la lista de elementos a renderizar
- En el peor caso, cada instancia renderiza los meshlets más densos
- En esta implementación se asignan
instanceCount * bottomLevelMeshletsCount * sizeof(vec2u)bytes - El límite predeterminado de storage buffer de WebGPU en Chrome es 128 MB, y la escena de demo está ajustada con eso en mente
- Cada instancia usa VRAM porque lleva una matriz de transformación
-
Scene arrangement
- La escena de la app coloca los objetos en una cuadrícula
- Los objetos lejanos solo se ven parcialmente, pero el LOD de meshlet grueso cubre áreas más amplias y genera overdraw
- Una colocación en grid denso renderiza más triángulos cercanos, pero como la diferencia de profundidad es pequeña, favorece al occlusion culling
- Los objetos muy alejados pueden hacer que píxeles lejanos aleatorios contaminen la depth pyramid
Por qué la escena de Jinx es difícil
- El personaje de Jinx es delgado y tiene espacios vacíos entre los brazos y el torso, lo que dificulta el occlusion culling
- El modelo no se simplifica bien, así que incluso el LOD más grueso tiene 3k triangles
- Se da una situación donde se acumulan muchos triángulos de 1 píxel
- El rasterizador por software ayuda bastante, pero por la disposición de la escena la mayoría de las instancias se renderizan como impostor
- En las zonas cercanas se usa el hardware rasterizer, y hardware rasterizer, software rasterizer e impostor tienen fortalezas distintas
Lo sorprendente de la implementación de Nanite
- El objetivo del DAG no es solo usar menos triángulos en objetos lejanos, sino mantener una consistencia cercana a 1 pixel == 1 triangle en toda la pantalla
- Las mallas LOD discretas tienen geometría separada en cada LOD, así que responden mal cuando se necesita un paso intermedio; hace falta un LOD continuo
- La jerarquía de meshlets permite samplear la geometría en el nivel de detalle elegido
- Se invierte más tiempo en culling y en el trabajo con meshlets que en Nanite en sí
- Hacer que una jerarquía LOD de meshlets funcione es fácil; hacer que funcione de manera eficiente es difícil
- Si la malla no se simplifica limpiamente, aparecen casos como Jinx, con unos 3,000 triangles cubriendo un solo píxel
- Si se quieren pixel-sized triangles, hace falta un rasterizador por software, y el billboard impostor también es útil como fallback centrado en la estabilidad
Problema de mesh simplification
- La simplificación no es el problema de “tomar una malla y reducirla a X% de triangles”, sino un problema que se aborda dentro del contexto de meshlets y METIS
- UE5 usa su propio código de mesh simplification como primer paso del asset pipeline
- El costo reducido ahí produce beneficios en cadena para todo el sistema posterior
- En el material de SIGGRAPH de Brian Karis se indica que el grafo LOD termina en un solo root cluster, y que cualquier modelo puede simplificarse a 128 triangles
- El modelo de Jinx tenía un problema donde la simplificación se detenía en cierto punto
- Se daba el caso de meter X triangles de entrada y recibir de vuelta los mismos X triangles
- El algoritmo fallaba con un crash por assertion
- Después se modificó para permitir múltiples roots en el DAG cuando partes de la malla no se reducen lo suficiente
- El modelo bunny al principio se simplificó a un solo meshlet de 128 tris, pero al cambiar las condiciones se reveló el problema de no converger a una sola root
- Muchos meshlets no lograban reducir suficientemente la cantidad de triangles, y también había muchos meshlets “no llenos” con menos de 128 triangles
- Discusiones relacionadas:
Métrica de error
- Al renderizar una malla de 20,000,000 triángulos con una jerarquía de meshlets, hay que elegir qué meshlet es el meshlet “correcto”
- Este problema de selección es el núcleo de Nanite, y la simplificación, el DAG de meshlets y el rasterizador por software son casi prerrequisitos para abordarlo
- La implementación usa el projected simplification error que ofrece meshoptimizer
- El autor considera que esta métrica no es una buena métrica para Nanite, y que también deberían incluirse otros atributos de vértice en la función
- Debería ser posible asignar pesos por atributo
- Las normales del rostro de Jinx fueron un problema importante
- Materiales de referencia:
Por qué el rasterizador por software no genera texturas
- En un hardware rasterizer, según el resultado del depth test, se pueden escribir juntos la depth texture, el color y las normales
- Si varios threads escriben en el mismo píxel en un rasterizador por software, se produce una race condition
- La solución general es un visibility buffer
- Escribe el
sceneUniqueTriangleIddel triángulo más cercano en cada pixel - Lo combina con una profundidad de 32 bits para crear un valor de 64 bits y usa una operación atómica de 64 bits
- En un pass separado, vuelve a rasterizar el triángulo, calcula las barycentric coordinates y realiza el shading
- Escribe el
- WebGPU no tiene atomics de 64 bits, así que este método no puede usarse
- El objetivo de este proyecto es demostrar que el rasterizado por software funciona; por eso, el modelo rasterizado por software del fondo es blanco y solo ofrece un shading razonable
Evaluación sobre implementar Nanite por cuenta propia
- La respuesta más simple es usar UE5
- Para integrar esta tecnología en un motor existente, primero habría que implementar una graphics pipeline basada en compute y una GPU-driven rendering pipeline
- Antes aparecerán problemas como el multi-step culling, la gestión de chunks de escena/mundo y las mallas animadas
- Se propone el siguiente orden: una vez estabilizadas esas etapas, intentar el rasterizador por software y luego agregar una tecnología tipo Nanite
- En un toy renderer, una jerarquía básica de meshlets puede ser un proyecto de fin de semana, pero una implementación real tiene que lidiar con los problemas de simplificación y de la métrica de error
Herramientas utilizadas y materiales de referencia
- meshoptimizer es la base central de la implementación del proyecto
- Las versiones más nuevas de meshoptimizer incluyen
meshopt_SimplifySparsepara crear un clon de Nanite - El proyecto no se actualiza a esa versión para mantener el estado probado durante el desarrollo
- Las versiones más nuevas de meshoptimizer incluyen
- Se usa METIS
- Emscripten se usa para ejecutar meshoptimizer y METIS en el navegador
- Para el modelo de Jinx se usó Arcane - Jinx de Sketchfab, y se realizaron la fusión de texturas, ajustes de UV y la eliminación del arma
1 comentarios
Opiniones de Hacker News
Oh, qué bueno. Es, en cierto modo, una implementación de terceros de la reproducción de Nanite.
Nanite representa las mallas gráficas de una forma muy inteligente. No es un árbol, sino un grafo acíclico dirigido, así que la repetición se vuelve un enlace, no una copia; las mallas comparten submallas, y esas submallas a su vez también pueden compartirse. También admite niveles de detalle (LOD) dentro de la malla, de modo que las submallas se descartan cuando se vuelven lo suficientemente pequeñas.
Por eso puede manejar contenido repetido de gran tamaño con una cantidad finita de datos y tiempos de renderizado rápidos. La idea clave es que, como en la pantalla hay una cantidad limitada de píxeles, también hay un límite superior para el trabajo de renderizado que realmente hace falta.
El video de SIGGRAPH donde alguien de Epic lo explica es muy bueno. El diseño actual de las GPU no encaja bien con Nanite, así que para hacer más procesamiento en la GPU se necesitarían nuevas operaciones de hardware. Con Nvidia distraída por el mercado de IA, me pregunto si veremos ese tipo de cambios.
Para que este enfoque funcione, la escena tiene que tener mucho instancing. Encaja bien con juegos AAA que crean contenido amplio reutilizando un número limitado de objetos, como el salón de estatuas idénticas de la demo de Unreal Engine. Si miras un video de Cyberpunk 2077 y buscas barandales y montones de basura, verás que los mismos elementos se repiten una y otra vez en contextos totalmente distintos.
La creación de mallas Nanite es compleja porque tiene muchos offsets de enlaces internos y, hasta ahora, solo el editor de Unreal Engine se encargaba de eso. Como la parte de reproducción ya es open source, es muy probable que alguien cree también herramientas de autoría.
Dicho eso, los offsets internos del formato podrían ser una superficie de ataque, y parece posible abusar de contenido manipulado, como con archivos Microsoft Word .doc maliciosamente creados.
El DAG se crea dinámicamente a partir de los datos de vértices y no tiene relación con cómo el artista haya organizado las submallas. Tampoco es algo como “la repetición se convierte en enlaces”.
Además, como el resultado del corte del grafo cambia para cada instancia de objeto, no se puede usar instancing tradicional; no entiendo por qué piensan que la escena necesita mucho instancing para que funcione bien.
En ese entonces avancé bastante en algo parecido a Nanite, que llamaba mallas comprimidas. Fue la típica arrogancia de ingeniería mal encaminada.
El trabajo inicial parecía prometedor, pero cuanto más profundizaba en el problema, más complejo se volvía todo. Había que crear todo un pipeline de generación de assets, y era demasiado grande como para terminarlo a tiempo con resultados atractivos y sin reventar la memoria.
Llegué a algo que parecía renderizar mallas grandes con niveles de detalle en cascada, pero era muy lento y se veía pésimo si no exprimía la GPU para lograr precisión subpíxel. Fue un experimento divertido, pero también demasiado exigente para el hardware y demasiado grande para que lo llevara una sola persona.
Cuando Epic presentó Nanite, quedé realmente sorprendido. Hicieron lo que yo no pude, y de una forma muchísimo mejor que lo mejor que yo había imaginado. Es una de esas tecnologías que, cuando apareció, no era hype sino una solución real, y es una joya técnica del mundo gráfico moderno. Si Epic cotizara en bolsa, habría considerado invertir una buena cantidad de dinero solo por la tecnología Nanite.
También estuvo bueno un video corto de introducción de alto nivel. Se llama "What is virtualized micropolygon geometry? An explainer on Nanite": https://www.youtube.com/watch?v=-50MJf7hyOw
También hay un artículo técnico en profundidad escrito por uno de los autores de esa función: https://jms55.github.io/posts/2024-06-09-virtual-geometry-be...
Según entiendo la lógica de Nanite, mantiene la precisión de la malla aproximadamente alineada con una precisión de 1 píxel. Por ejemplo, si después de la proyección en perspectiva el error se queda alrededor de medio píxel, se puede usar una malla de menor detalle con coordenadas redondeadas a unos 10 bits.
Recuerdo vagamente que la cuantización cumplía dos funciones. No solo reduce el tamaño de almacenamiento de los datos, sino que también ayuda a la generación de niveles de detalle al ajustar vértices a la misma posición en el espacio, para luego poder eliminar duplicados.
Ya se mencionó varias veces en este hilo, pero Bevy también tiene una implementación de las ideas de Nanite. A veces se le llama geometría virtual. Yo soy su autor, así que puedo responder preguntas :)
Scthe hizo un trabajo realmente bueno en este proyecto. Conversamos sobre varias partes del proceso, y esa experiencia también ayudó a mejorar el código de Bevy: https://github.com/bevyengine/bevy/pull/15023
Siempre es bueno ver que más gente se meta en este campo. Nanite tiene muchísimas ideas geniales.
¿La demo está usando la cadena de agente de usuario para determinar la compatibilidad? Eso no está bien. La compatibilidad de funciones debería evaluarse caso por caso, detectando directamente una función específica o probándola
Yo no uso Chrome, uso Chromium, y también uso WebGPU todo el tiempo, pero la demo me dice que use Chrome. Éticamente no puedo hacer eso. De verdad quiero probar la demo; parece un proyecto en el que se invirtió mucho esfuerzo
Cuando creaba mi propio framework de componentes para el navegador, muchas veces la detección era imposible y tenía que depender del browser sniffing. El código de Modernizr tiene muchos hacks muy ingeniosos para detectar funciones, y a veces hacks bastante sucios. Se hizo un trabajo enorme para crear código de detección confiable, y la detección por lo general se hacía mediante efectos secundarios
Mi conjetura es que la detección de funciones Web3D no es sencilla. Con una búsqueda rápida tampoco encontré una biblioteca clara de detección de funciones Web3D
Parte del código de Modernizr para detectar soporte de
:checkedes así:Modernizr.addTest('checked', function(){
return Modernizr.testStyles('#modernizr input {width:100px} #modernizr :checked {width:200px;display:block}', function(elem, rule){
¿Activaste el permiso de WebGL para ese sitio en la configuración del sitio? En mi caso creo que venía desactivado por defecto
WebGPU error [frame][validation]: Fill size (7160950) is not a multiple of 4 bytes.
Esta es la tesis doctoral de 2009, algo enterrada, de Federico Ponchio, quien inventó el algoritmo de simplificación dinámica de mallas en el que se basó Nanite. También tiene muchas figuras
https://vcg.isti.cnr.it/~ponchio/download/ponchio_phd.pdf Son 107 páginas
Es genial que funcione hasta cierto punto, pero como WebGPU no tiene operaciones atómicas de 64 bits, hubo que hacer compromisos bastante burdos
Ojalá algún día se agreguen como una extensión opcional. Al menos en hardware de escritorio, el soporte por hardware es casi universal. AMD y Nvidia lo tienen desde hace mucho, y Apple lo soporta desde el M3
En un iPhone 12 Pro Max dice que no hay WebGPU, pero al activarlo en funciones experimentales sí funcionan otros sitios de demos de WebGPU[1]. ¿Alguien logró que esto funcione en iPhone? Sería bueno que la app web diera más detalles sobre qué falló
[1] https://webgpu.github.io/webgpu-samples/?sample=texturedCube
WebGPU error [init][validation]: 6 errors generated while compiling the shader: 50:22: unresolved call target 'pack4x8snorm' 50:9: cannot bitcast from 'â¥' to 'f32' 54:10: unresolved call target 'unpack4x8snorm' 59:22: unresolved call target 'pack4x8unorm' 59:9: cannot bitcast from 'â¥' to 'f32' 63:9: unresolved call target 'unpack4x8unorm'
El nombre y la descripción son muy confusos, y también parece haber posible infracción de marca registrada. A diferencia de lo que se afirma, no tiene absolutamente nada que ver con el Nanite real de UE5; parece ser una implementación de algo similar hecha por alguien no relacionado con UE5
También está Virtual Geometry de Bevy, que ofrece una funcionalidad similar. Está escrito en Rust e integrado en un motor de juegos, así que probablemente sea mucho más útil: https://jms55.github.io/posts/2024-06-09-virtual-geometry-be...
Si yo dijera “implementé GPT-3 de OpenAI en JS”, se entendería que tomé la arquitectura del whitepaper y la reimplementé
Esto en realidad no es UE5 Nanite corriendo en WebGPU. Es una implementación independiente de la misma idea que Nanite
Esta técnica está empezando a aparecer en varios lugares. Es cierto que Nanite hizo famosa la idea, pero Nanite no es el nombre de la técnica, sino el nombre de una implementación específica
Me pregunto cómo están otros motores en cuanto a niveles de detalle y sistemas similares.
Godot tiene una función automática de niveles de detalle y, por sí sola, se ve bastante bien: https://docs.godotengine.org/en/stable/tutorials/3d/mesh_lod...
Unity también tiene un sistema de niveles de detalle, pero, pese a la popularidad del motor, hay que crear manualmente los modelos de nivel de detalle: https://docs.unity3d.com/Manual/LevelOfDetail.html a menos que busques algún plugin en el Asset Store.
También vi un enfoque interesante en un motor menos conocido llamado NeoAxis: https://www.neoaxis.com/docs/html/NeoAxis_Levels.htm aunque en mi vieja RX580 el rendimiento era muy malo, y todavía no lo he probado en mi A580 actual.
A mi parecer, Unreal está bastante por delante de sus competidores en la capacidad de poner muchas cosas en pantalla. La desventaja es que los artistas empiezan a querer meter assets de mayor calidad en el juego, lo que puede inflar mucho el tamaño de instalación.
El costo de rendimiento también es alto. Nanite o la geometría virtual no encajan bien con las GPU modernas. Si no recuerdo mal, la tasa máxima de relleno era de alrededor de 1/4. Esto se debe a que la rasterización de la GPU no funciona por píxel como un shader, sino en bloques de 2x2 quads.
Cada vez que veo una escena renderizada con tanta geometría estática repetitiva, me viene a la mente la voz de aquel tipo molesto que hablaba de “detalle ilimitado” en un viejo video de vaporware.
Nanite, a diferencia de aquella cosa antigua, parece haber resuelto realmente ese problema. Recuerdo que de ese otro proyecto se decía que usaba algo como octrees.
Según rumores en línea, usaban octrees, pero los videos posteriores de Euclidean lo negaron tajantemente.