De onde vêm os buracos nas malhas geradas
Por Oleg Sidorkin, CTO e cofundador da Cinevva

Gerei um pássaro com o TRELLIS.2, apliquei rigging e, assim que uma animação começou a rodar, surgiram buracos sob as duas asas. Fendas brilhantes pelas quais era possível ver diretamente através do corpo. O sistema de rigging levou a culpa primeiro — e ele realmente tinha um problema próprio —, mas os buracos já estavam na malha antes que qualquer osso a tocasse. Este post relata a investigação que veio em seguida: o que os buracos realmente são, as nove coisas que não os corrigiram, o motivo disso e a única coisa que funcionou. Estou incluindo os becos sem saída de propósito. A maioria deles corresponde às soluções óbvias que você tentaria, e cada uma nos ensinou algo de que a correção final dependia.
Primeiro passo: entrar em acordo sobre o que é um buraco
Nosso verificador de malhas relatou zero arestas de contorno, e um teste de paridade de raios indicou que a superfície estava fechada. Então eu disse à equipe que a malha estava limpa. Não estava, e a pessoa que havia marcado as fendas em vermelho tinha razão em insistir. Uma malha pode estar fechada e ainda ter túneis. A única medida que os detecta é a característica de Euler, V menos E mais F, que fornece o número de alças por casca. O pássaro tinha 69. Cada parede na exportação estava duplicada, portanto uma fenda na parede externa conectada à cavidade atrás dela é uma alça, não uma abertura, e nada que procure arestas abertas jamais vai detectá-la.
Segunda lição, ainda mais constrangedora: passei um tempo examinando pixels escuros em busca de buracos, porque buracos são escuros, até alguém me dizer para olhar primeiro para a imagem. Os buracos eram claros. Eram fendas revelando a textura verde da parede oposta através de uma dobra cinza. Um algoritmo que não foi direcionado para a coisa certa não diz nada, e a maneira mais barata de direcioná-lo é olhar.
Segundo passo: encontrar a etapa que os cria
O TRELLIS.2 constrói uma malha em três etapas. Um decodificador emite uma superfície bruta com resolução cúbica de 1024. Uma etapa de contorno dual em banda estreita refaz a malha. Um decimador a reduz para cem mil faces, e a textura é assada. Salvamos a geometria após cada etapa e disparamos o mesmo conjunto de raios através da raiz da asa em todas elas.
É na superfície bruta que tudo começa. Ela é uma sopa de superfícies abertas: 266.242 loops de contorno, a maioria deles composta por um único quad ausente, e 65.371 arestas não manifold. Na raiz da asa, um terço dos raios de teste atravessou direto. O preenchimento de buracos executado em seguida quase não fecha nada: 144 faces em cinco milhões. O remalhador trabalha com um campo de distância sem sinal porque precisa fazer isso. Ele não tem um interior ou exterior com que trabalhar, então envolve cada superfície com uma casca fina e deslocada. É por isso que cada parede fica duplicada, separada por cerca de quatro voxels, e é assim que cada faixa porosa se transforma em uma dobra com uma fenda. A decimação então piora a situação, e faz isso de maneira aleatória: a mesma malha bruta decimada duas vezes produziu 69 e 107 túneis.

Esse mapa foi a primeira pista de verdade. A porosidade não é aleatória. Ela aparece em todas as junções entre partes e em nenhum outro lugar.
As coisas que não funcionaram
Nós as testamos aproximadamente na ordem do quão óbvias pareciam.
| tentativa | resultado |
|---|---|
| resolução 1536 em vez de 1024 | pior em três de cinco modelos de teste. Mais resolução resolve mais contatos finos, e cada um deles é uma nova junção |
| a sequência integrada de limpeza | mais arestas não manifold, não menos |
| preencher todos os buracos pequenos com o MeshLib | 266 mil loops preenchidos, raiz da asa inalterada. A superfície ali nunca foi emitida, portanto não há loop para preencher |
| um remalhamento com sinal a partir de um número de enrolamento | fragmenta a malha em milhares de cascas. A saída bruta não tem interior ao qual atribuir um sinal |
| uma banda de contorno mais larga, de 2 e 3 voxels | menos túneis no total, mas a fenda fica mais larga, não mais estreita. Túneis pequenos se fundem em grandes |
| fechamento morfológico da estrutura de voxels | solidifica a casca; o decodificador então emite superfície dentro de cada voxel interior, quadruplica o número de faces e produz 969 túneis |
| remalhamento global por voxels da malha final | gênero 0 e fenda selada. Também suavizou as bordas de todas as penas. Rejeitado só de olhar, corretamente |
| ajuste do limiar de cruzamento do decodificador | arestas abertas de 65 mil para 58 mil. Ruído |
| completar voxels vizinhos ausentes | 560 casos no pássaro inteiro. Ruído |

Vale a pena examinar melhor o fechamento global, porque é o que um modelador tentaria primeiro: aplicar um remalhamento por voxels com um tamanho ligeiramente maior que a lacuna. Topologicamente, funciona. Mas também é uma mentira sobre o modelo. O gerador produziu bordas nítidas nas penas, e estaríamos trocando-as por uma raiz de asa selada. Duas das nove tentativas reduziram a quantidade de túneis, e ambas foram rejeitadas no momento em que alguém olhou para o pássaro em vez de olhar para o número.
Lendo o código
Nesse ponto, a única atitude honesta era parar de tratar o decodificador como uma caixa-preta e ler como sua saída se transforma em uma malha. O TRELLIS.2 usa uma representação chamada O-Voxel, e a extração é curta. Cada voxel ativo contém um vértice dual e três logits, um para cada aresta da grade que sai de seu canto, cada um significando “a superfície cruza esta aresta”. Um logit acima de zero emite um quad conectando os quatro voxels ao redor dessa aresta. Se qualquer um dos quatro estiver ausente, o quad é descartado silenciosamente. É só isso. Não há campo de sinais, ocupação, interior ou exterior em nenhum ponto do modelo.
Alteramos o decodificador para salvar esses logits antes do limiar, no mesmo pássaro, e fizemos a contagem.
Vizinhos ausentes: 560 entre 2,7 milhões de voxels. Logits próximos de zero: 8.600 entre 8 milhões. Nenhum dos dois é o problema. O problema aparece quando observamos as quatro arestas ao redor de cada face da grade. Uma superfície fechada cruza o contorno de uma face um número par de vezes. Em 126.542 faces, 2,3% delas, os quatro indicadores discordam, e toda face ímpar é, por definição, uma aresta aberta ou não manifold. E eles discordam com confiança. O indicador isolado em uma face de contagem um tem logit +10, enquanto seus vizinhos têm -4. O custo mediano para inverter um indicador e restaurar a paridade é de 2,3 logits. Não se trata de um limiar ligeiramente incorreto. O modelo tem certeza de uma superfície que não consegue se fechar.

Depois medimos a própria faixa, no ponto exato da superfície para o qual nossa câmera fixa havia olhado durante toda a semana. Dentro de um raio de doze voxels, há 2,1 vezes mais voxels ativos do que uma única superfície plana precisaria. No peito, para comparação, há 1,2 vez mais. A parte inferior da asa e o corpo são duas superfícies passando dentro de uma única camada de voxels. A representação tem um vértice por voxel, portanto não consegue armazenar ambas. Durante o treinamento, o valor de referência para esse voxel é uma média de mínimos quadrados das duas superfícies com indicadores de “qualquer cruzamento”, o que por si só é inconsistente; assim, o modelo aprendeu fielmente a emitir um pente ali. O remalhador envolve o pente em uma placa, e a placa tem túneis.
A largura da faixa decorre da geometria: aproximadamente um voxel dividido pela tangente do ângulo de contato. Duas superfícies que se encontram a noventa graus produzem uma dobra de um voxel e não causam problemas. Uma asa apoiada contra um corpo, pena sobre pena, dedos no chão — todos esses casos produzem uma faixa de dez ou quinze voxels de largura. É a explicação do mapa de porosidade.
Eu também tentei o reparo baseado em princípios: inferir, para cada canto, um sinal de interior/exterior que melhor explicasse os indicadores e então recalculá-los. Não pode funcionar. O campo de sinais só existe na casca de um voxel emitida pelo decodificador, e qualquer extensão para fora dessa casca sai do controle. Doze rodadas disso dobraram a quantidade de voxels com lixo.
O que um modelador faria
Um modelador nunca deixaria duas superfícies separadas pela espessura de um fio de cabelo. Ele uniria a asa ao corpo e deixaria uma dobra. O fechamento global faz isso em todos os lugares, e esse é o problema. Portanto, faça isso apenas onde estão as duas superfícies.
O detector segue uma ideia simples. Conte os vizinhos ativos de cada voxel dentro de quatro voxels. Em uma única superfície, esse número é igual em todos os lugares e corresponde à mediana. Onde duas superfícies compartilham a camada, ele é aproximadamente o dobro. Marque tudo que estiver acima de 1,5 vez a mediana, expanda o conjunto marcado em um voxel e coloque um cubo unitário em cada posição. O espaço entre as superfícies se torna sólido, a etapa de contorno enxerga uma única parede e nada mais no modelo é alterado.

Primeiro testamos em um recorte, usando o MeshLib no lugar do contorno na GPU. Cinquenta e três cascas e dezenove alças se tornaram duas cascas e cinco alças. Depois colocamos a correção no pipeline real, entre o decodificador e a exportação, e processamos o mesmo pássaro com o remalhador real e o bake de textura real.

| túneis | arestas não manifold | aba na raiz da asa | buraco nas costas | |
|---|---|---|---|---|
| como está | 79,5 | 318 | presente | presente |
| preenchimento, raio 3 | 84,5 | 97 | ausente | ausente |
| preenchimento, raio 4 | 72 | 38 | ausente | ausente |
A aba desapareceu. Um segundo buraco nas costas do pássaro, na mesma junção vista por trás — um que eu nem estava tentando corrigir —, também desapareceu. As penas continuam separadas, os dedos continuam separados e o bake da textura ocorre normalmente, porque os cubos ficam dentro da casca, onde o volume de atributos já contém dados. Isso acrescenta seis segundos a um processo de quatro minutos. Agora está ativado por padrão, com raio quatro.

O que isso não corrige e o que isso revela
A quantidade de túneis praticamente não mudou, e quero explicar com honestidade o motivo. Os túneis restantes ficam nas pontas das penas e dos dedos, onde uma parte fina termina e a parede duplicada se fecha em uma pinça. Eles são pequenos, não ficam visíveis e resultam de um mecanismo diferente. O preenchimento trata os contatos entre partes, que é onde estavam os buracos visíveis. A correção de verdade precisa acontecer em uma etapa anterior. Um decodificador que preveja sinais por vértice em vez de indicadores por aresta seria consistente em termos de paridade por construção, que é como funcionava a geração anterior deste modelo e por que suas malhas eram estanques. Isso exige uma mudança no treinamento, não é algo que possamos corrigir na inferência. Até lá, duas conclusões práticas decorrem da fórmula das faixas. Se você estiver gerando um personagem, uma pose em que os membros fiquem afastados do corpo produzirá uma malha mais limpa do que uma em que fiquem encostados nele. E, se estiver desenvolvendo algo sobre um gerador como este, avalie suas malhas com a característica de Euler e examine o resultado com seus próprios olhos antes de confiar em um número. O número indicou que estava tudo certo. Ele errou duas vezes.
O decodificador instrumentado, os dumps de cada etapa, a análise de paridade e o preenchimento agora fazem parte do nosso pipeline, então o próximo modelo que apresentar buracos receberá o mesmo tratamento em questão de minutos, em vez de uma semana.