De dónde vienen los agujeros en las mallas generadas
Por Oleg Sidorkin, CTO y cofundador de Cinevva

Generé un pájaro con TRELLIS.2, le hice el rigging y, en cuanto se reprodujo una animación, aparecieron agujeros bajo ambas alas. Eran rendijas brillantes a través de las cuales se podía ver directamente el interior del cuerpo. Primero culpamos al sistema de rigging, y sí tenía un problema propio, pero los agujeros ya estaban en la malla antes de que ningún hueso la tocara. Este artículo recoge la investigación que siguió: qué son realmente los agujeros, las nueve cosas que no los corrigieron, por qué y la única que sí funcionó. Incluyo los callejones sin salida a propósito. La mayoría son las soluciones obvias que cualquiera probaría, y cada una nos enseñó algo de lo que dependía la corrección final.
Primer paso: acordar qué es un agujero
Nuestro comprobador de mallas indicó que había cero aristas de contorno, y una prueba de paridad de rayos dijo que la superficie estaba cerrada. Así que le dije al equipo que la malla estaba limpia. No lo estaba, y la persona que había marcado las rendijas en rojo hizo bien en insistir. Una malla puede estar cerrada y aun así tener túneles. La única medida que los detecta es la característica de Euler, V menos E más F, que da el número de asas por componente conexa. El pájaro tenía 69. Todas las paredes de la exportación estaban duplicadas, de modo que una rendija en la pared exterior que conecta con la cavidad posterior es un asa, no una abertura, y nada que busque aristas abiertas llegará a detectarla.
Segunda lección, más vergonzosa: pasé un buen rato analizando píxeles oscuros en busca de agujeros, porque los agujeros son oscuros, hasta que me dijeron que mirara primero la imagen. Los agujeros eran brillantes. Eran rendijas por las que se veía la textura verde de la pared opuesta a través de un pliegue gris. Un algoritmo al que no has dirigido hacia lo correcto no te dice nada, y la forma más barata de dirigirlo es mirar.
Segundo paso: encontrar la etapa que los crea
TRELLIS.2 construye una malla en tres pasos. Un decodificador emite una superficie sin procesar a una resolución de 1024 al cubo. Una pasada de contorneado dual de banda estrecha la remalla. Un diezmador la reduce a cien mil caras y se hornea la textura. Volcamos la geometría después de cada paso y lanzamos el mismo conjunto de rayos a través de la raíz del ala en cada ocasión.
Todo empieza en la superficie sin procesar. Es una sopa de láminas abiertas: 266.242 bucles de contorno, la mayoría correspondientes a un único cuadrilátero ausente, y 65.371 aristas no múltiples. En la raíz del ala, un tercio de los rayos de prueba la atravesaron por completo. El rellenado de agujeros que se ejecuta a continuación no cierra casi nada: 144 caras de un total de cinco millones. El remallador trabaja con un campo de distancia sin signo porque no tiene alternativa. No dispone de un interior ni un exterior con los que trabajar, así que envuelve cada lámina con una fina envoltura desplazada. Por eso todas las paredes están duplicadas, separadas por unos cuatro vóxeles, y así es como cada franja porosa se convierte en un pliegue con una rendija. Después, el diezmado lo empeora, y además de forma aleatoria: la misma malla sin procesar, diezmada dos veces, produjo 69 y 107 túneles.

Ese mapa fue la primera pista de verdad. La porosidad no es aleatoria. Se concentra en todas las uniones entre partes y en ningún otro sitio.
Las cosas que no funcionaron
Las probamos más o menos en orden, empezando por las más obvias.
| intento | resultado |
|---|---|
| resolución de 1536 en lugar de 1024 | peor en tres de los cinco modelos de prueba. Una mayor resolución capta más contactos finos, y cada uno es una unión nueva |
| la cadena de limpieza integrada | más aristas no múltiples, no menos |
| rellenar todos los agujeros pequeños con MeshLib | se rellenaron 266.000 bucles, pero la raíz del ala no cambió. La piel de esa zona nunca llegó a emitirse, así que no hay ningún bucle que rellenar |
| un remallado con signo a partir de un número de enrollamiento | fragmenta la malla en miles de componentes conexas. La salida sin procesar no tiene un interior al que asignar signo |
| una banda de contorneado más ancha, de 2 y 3 vóxeles | menos túneles en total, pero la rendija se ensancha en lugar de estrecharse. Los túneles pequeños se fusionan en otros grandes |
| cierre morfológico de la estructura de vóxeles | solidifica la envoltura; después, el decodificador emite superficie dentro de cada vóxel interior: 4 veces más caras y 969 túneles |
| remallado global por vóxeles de la malla final | género 0 y rendija sellada. También suaviza todos los bordes de las plumas. Se rechazó nada más verlo, y con razón |
| sesgo del umbral de cruce del decodificador | las aristas abiertas bajaron de 65.000 a 58.000. Ruido |
| completar los vóxeles vecinos ausentes | 560 casos en todo el pájaro. Ruido |

Vale la pena detenerse en el cierre global, porque es lo primero a lo que recurriría un modelador: aplicar un remallado por vóxeles con un tamaño apenas superior al hueco. Topológicamente, funciona. Pero también falsea el modelo. El generador produjo bordes de plumas nítidos y los estaríamos sacrificando a cambio de sellar la raíz del ala. Dos de los nueve intentos redujeron el número de túneles, y ambos se rechazaron en cuanto alguien miró el pájaro en lugar de la cifra.
Leer el código
Llegados a ese punto, la única opción honesta era dejar de tratar el decodificador como una caja negra y leer cómo se transforma su salida en una malla. TRELLIS.2 utiliza una representación llamada O-Voxel, y la extracción es breve. Cada vóxel activo contiene un vértice dual y tres logits, uno por cada arista de la cuadrícula que parte de su esquina, cada uno con el significado de «la superficie cruza esta arista». Un logit por encima de cero emite un cuadrilátero que conecta los cuatro vóxeles alrededor de esa arista. Si falta cualquiera de los cuatro, el cuadrilátero se descarta silenciosamente. Eso es todo. No hay ningún campo de signos, ninguna ocupación ni ningún concepto de interior o exterior en todo el modelo.
Modificamos el decodificador para volcar esos logits antes del umbral, usando el mismo pájaro, y los contamos.
Vecinos ausentes: 560 de 2,7 millones de vóxeles. Logits cercanos a cero: 8.600 de 8 millones. Ninguno de los dos es el problema. El problema aparece al observar las cuatro aristas alrededor de cada cara de la cuadrícula. Una superficie cerrada cruza el contorno de una cara un número par de veces. En 126.542 caras, el 2,3 % del total, los cuatro indicadores no concuerdan, y toda cara impar constituye por definición una arista abierta o no múltiple. Y discrepan con mucha seguridad. El indicador aislado de una cara con un solo cruce tiene un logit de +10, mientras que sus vecinos tienen -4. El coste mediano de invertir un indicador y restablecer la paridad es de 2,3 logits. No se trata de un umbral ligeramente desajustado. El modelo está seguro de una superficie que no puede cerrarse.

Después medimos la propia franja, en el punto exacto de la superficie que nuestra cámara fija llevaba observando toda la semana. En un radio de doce vóxeles hay 2,1 veces más vóxeles activos de los que necesitaría una única lámina plana. En el pecho, como comparación, hay 1,2 veces más. La parte inferior del ala y el cuerpo son dos pieles que discurren dentro de una sola capa de vóxeles. La representación tiene un vértice por vóxel, por lo que no puede contener ambas. Durante el entrenamiento, la referencia para un vóxel de este tipo es un promedio por mínimos cuadrados de ambas pieles con indicadores de «cualquier cruce», algo que de por sí es incoherente, así que el modelo ha aprendido fielmente a emitir un peine en esa zona. El remallador envuelve el peine y lo convierte en una placa, y la placa tiene túneles.
El ancho de la franja se deduce de la geometría: aproximadamente un vóxel dividido por la tangente del ángulo de contacto. Dos superficies que se encuentran a noventa grados producen un pliegue de un vóxel y no causan problemas. Un ala pegada al cuerpo, pluma contra pluma, los dedos sobre el suelo: todos estos casos producen una franja de diez o quince vóxeles de ancho. Así se explica el mapa de porosidad.
Sí intenté la reparación basada en principios: inferir un signo de interior o exterior para cada esquina que explicara los indicadores de la mejor manera y, después, volver a derivarlos. No puede funcionar. El campo de signos solo existe en la envoltura de un vóxel emitida por el decodificador, y cualquier extensión fuera de esa envoltura se descontrola. Doce rondas duplicaron el número de vóxeles con basura.
Lo que haría un modelador
Un modelador nunca dejaría dos pieles separadas por una distancia mínima. Uniría el ala con el cuerpo y dejaría que hubiera un pliegue. El problema es que el cierre global hace eso en todas partes. Así que hay que hacerlo únicamente donde están las dos pieles.
El detector parte de una sola idea. Contar los vecinos activos de cada vóxel dentro de un radio de cuatro vóxeles. En una sola lámina, ese número es igual en todas partes y coincide con la mediana. Donde dos láminas comparten la misma capa, es aproximadamente el doble. Se marca todo lo que supere 1,5 veces la mediana, se expande el conjunto marcado un vóxel y se coloca un cubo unitario en cada posición. El espacio entre las pieles se vuelve sólido, la pasada de contorneado ve una única pared y ninguna otra parte del modelo se modifica.

Primero lo probamos en un recorte, usando MeshLib como sustituto del contorneado en la GPU. Cincuenta y tres componentes conexas y diecinueve asas se convirtieron en dos componentes y cinco asas. Después lo incorporamos al pipeline real, entre el decodificador y la exportación, y procesamos el mismo pájaro con el remallador y el horneado de texturas reales.

| túneles | aristas no múltiples | solapa en la raíz del ala | agujero en el lomo | |
|---|---|---|---|---|
| tal cual | 79,5 | 318 | presente | presente |
| relleno, radio 3 | 84,5 | 97 | desaparecida | desaparecido |
| relleno, radio 4 | 72 | 38 | desaparecida | desaparecido |
La solapa ha desaparecido. También ha desaparecido un segundo agujero en el lomo del pájaro, situado en la misma unión vista desde atrás, que ni siquiera estaba intentando corregir. Las plumas siguen separadas, los dedos siguen separados y la textura se hornea con normalidad porque los cubos quedan dentro de la envoltura, donde el volumen de atributos ya contiene datos. Añade seis segundos a un trabajo de cuatro minutos. Ahora está activado de forma predeterminada, con radio cuatro.

Lo que esto no corrige y lo que nos revela
El número de túneles apenas cambió, y quiero explicar con sinceridad por qué. Los túneles restantes están en las puntas de las plumas y de los dedos, donde termina una parte fina y la pared duplicada se cierra al estrecharse. Son pequeños, no se ven y obedecen a un mecanismo distinto. El relleno se dirige a los contactos entre partes, que es donde estaban los agujeros visibles. La solución real está en una etapa anterior. Un decodificador que predijera signos por vértice en lugar de indicadores por arista sería coherente con la paridad por construcción, que es como funcionaba la generación anterior de este modelo y la razón por la que sus mallas eran estancas. Eso requiere un cambio en el entrenamiento, no es algo que podamos corregir durante la inferencia. Hasta entonces, de la fórmula de las franjas se desprenden dos consejos prácticos. Si estás generando un personaje, una pose en la que las extremidades estén separadas del cuerpo producirá una malla más limpia que otra en la que estén pegadas a él. Y si estás desarrollando sobre un generador como este, evalúa tus mallas con la característica de Euler y examina el resultado con tus propios ojos antes de confiar en una cifra. La cifra decía que estaba limpia. Se equivocó dos veces.
El decodificador instrumentado, los volcados de cada etapa, el análisis de paridad y el rellenado ya forman parte de nuestro pipeline, así que el próximo modelo que produzca agujeros recibirá el mismo tratamiento en cuestión de minutos, en lugar de una semana.