Métodos para desenhar outlines em videogames
(ameye.dev)- As principais formas de desenhar outlines ao redor de objetos no Unity são efeito de rim, expansão de vértices, buffer com blur, algoritmo Jump Flood e detecção de bordas, cada uma com custos diferentes de qualidade, desempenho e configuração
- O efeito de rim aproxima o Fresnel pelo produto escalar entre a normal e a direção de visão, destacando a borda interna, mas o resultado varia bastante entre modelos arredondados e modelos com cantos vivos
- A expansão de vértices cria o contorno ampliando uma malha duplicada por trás da original; quando processada em espaço de clip, a largura na tela pode ficar mais consistente
- O buffer com blur e o algoritmo Jump Flood seguem uma abordagem de desenhar a silhueta em um buffer e depois expandi-la, cada um com pontos fortes para linhas suaves ou outlines muito largos
- A detecção de bordas cria linhas encontrando descontinuidades nos buffers de profundidade, normal e cor em tela cheia, mas exige ajuste fino de limiares e modulação para reduzir linhas indesejadas
Por que usar renderização de outlines
- Outlines são usados em jogos para criar estilo visual ou como apoio de gameplay, como destacar ou selecionar objetos
- Sable usa outlines para um estilo parecido com história em quadrinhos
- The Last of Us usa outlines para destacar inimigos quando o jogador entra em modo furtivo
- As cinco técnicas envolvem compromissos diferentes entre desempenho, qualidade visual e quantidade de configuração manual
Efeito de rim: destaque da borda interna com aproximação de Fresnel
- O efeito de rim aproxima o efeito Fresnel para renderizar uma linha na borda/rim do objeto
- O cálculo se baseia no produto escalar entre o vetor normalizado da normal
Ne a direção de visão normalizadaV- O valor
1.0 - saturate(dot(N, V))é elevado à potênciaPpara definir a intensidade do efeito - É mais uma aproximação de outline do que um Fresnel fisicamente correto
- O valor
- Na implementação em Unity, um shader customizado controla a largura, potência, suavidade e cor do outline
- O exemplo usa
smoothstep,lerp,_OutlineWidth,_OutlineSoftness,_OutlinePowere_OutlineColor
- O exemplo usa
- Esse método sempre aparece como linha interna e não fica visível na borda externa do objeto
- Funciona bem em objetos arredondados e suaves, mas em cubos ou modelos complexos a largura da linha pode ficar irregular ou nem parecer um outline
Expansão de vértices: ampliar uma malha duplicada para criar uma linha externa
- A expansão de vértices renderiza novamente uma cópia do objeto ou malha original, expande seus vértices por trás do original para formar uma versão maior e a renderiza com cor sólida
-
Escolha da direção de expansão
- A qualidade do outline muda bastante dependendo da direção em que os vértices são deslocados
- Mover na direção da posição do vértice faz a malha inflar
- No espaço local, a posição do vértice pode ser usada como um vetor do centro do objeto até o vértice
- Se não for normalizada, vértices mais distantes do centro se movem mais
- Usar
normalize(positionOS) * widthpermite um deslocamento mais uniforme no espaço do objeto - Mover na direção do vetor normal gera bons resultados em objetos com bordas suaves, como esferas ou cápsulas
- Em objetos com arestas vivas, como cubos, podem surgir lacunas no outline
- Também é possível usar vertex colors como direção de expansão
- Nesse caso, normais customizadas são geradas e armazenadas no canal de vertex colors da malha
- Se você fizer bake de normais suaves esféricas em um cubo usando vertex colors, pode obter um outline melhor
- A desvantagem é a configuração manual de criar normais customizadas para cada malha, embora isso possa ser automatizado com script
-
Escolha do espaço de expansão
- Na etapa de vértice do shader, as coordenadas do vértice começam no espaço do objeto e são transformadas para espaço de clip pela matriz MVP
- O fluxo é espaço de objeto/modelo/local → espaço do mundo → espaço da câmera/view → espaço de clip → espaço de tela → espaço de viewport/janela
- Expandir no espaço do objeto pode distorcer o outline porque a transformação MVP ainda não foi aplicada
- Podem surgir problemas de escala ao aplicar a matriz do modelo
- Ao passar de espaço de clip para espaço de tela, há redução por perspectiva por causa da divisão em perspectiva
- Parte do deslocamento em 3D pode acontecer na direção da câmera e não contribuir para a largura do outline na tela
- Expandir no espaço de clip permite criar um outline mais limpo e com largura quase constante na tela
- A posição do vértice e a normal são transformadas para espaço de clip e só as coordenadas
xeysão modificadas - Divide-se pelo tamanho horizontal e vertical da tela para corrigir a proporção
- Multiplica-se por
wdo espaço de clip para que a divisão em perspectiva posterior não altere o resultado final da largura do outline - Multiplica-se por
width * 2para que uma unidade de largura corresponda a 1 pixel na tela - A abordagem em espaço de clip também depende das normais da malha
- Sem normais customizadas, podem surgir lacunas em arestas vivas
- Se as normais estiverem configuradas incorretamente e apontarem na direção oposta, os vértices do outline também se moverão para o lado errado e criarão lacunas
- Há mais explicações em creating an outline in clip space
-
Masking
- Na malha duplicada, só a parte saliente do outline deve ficar visível
- A forma mais comum é fazer culling da geometria front-facing da malha duplicada e usar as backfaces para formar o outline
- O teste de profundidade usa less than or equal to para que as backfaces só apareçam na posição do outline
- Outra opção é usar uma máscara de stencil para impedir que a malha duplicada apareça na frente da original
- Nesse caso, não é necessário culling
- Não surgem linhas internas no objeto
- Quando dois objetos se sobrepõem, o outline aparece apenas ao redor de cada objeto
Buffer com blur: expandir a silhueta com desfoque
- A abordagem com buffer de blur renderiza a silhueta do objeto em um buffer e depois a expande com blur, usando o resultado para renderizar o outline
-
Buffer de silhueta
- O primeiro passo é criar o buffer de silhueta
- Cada objeto é renderizado em uma textura com um shader de saída em cor sólida
- Se todas as silhuetas forem renderizadas em branco, no final é possível multiplicar pela cor desejada do outline para obter uma única cor
- Se cada objeto precisar de uma cor de outline diferente, cada silhueta pode ser renderizada em uma cor específica
-
Pass de blur
- O pass de blur é usado para expandir o buffer de silhueta
- Normalmente usa-se box blur ou Gaussian blur
- Para melhorar o desempenho, o buffer de silhueta pode ser reduzido antes do blur
- O pass de blur é custoso porque calcula a média ou média ponderada de vários pixels ao redor de cada pixel
- É melhor executar o blur em 2 passes
- Em filtros separáveis como box blur e Gaussian blur, a complexidade do algoritmo pode cair de
O(N²)paraO(2N) - Primeiro aplica-se blur na vertical e depois o resultado é aplicado novamente na horizontal para gerar o resultado final
- A largura do outline é controlada pelo parâmetro
_KernelSizedo shader de blur
-
Pass de outline e masking
- Depois do pass de blur, a silhueta desfocada é composta com a cena original para criar o outline
- O buffer com blur é adequado para outlines suaves ou brilhantes
- Se aplicar um step ao resultado do blur, também é possível renderizar um outline rígido
- Como na expansão de vértices, pode-se usar uma máscara de stencil para que o outline seja renderizado apenas atrás da geometria
- O impacto no desempenho pode ser maior do que em outras abordagens
Algoritmo Jump Flood: lidar com outlines muito largos
- A quarta técnica renderiza outlines com o algoritmo Jump Flood
- A vantagem é permitir outlines muito largos com custo de desempenho razoável
- A explicação detalhada continua em The Quest for Very Wide Outlines, de Ben Golus
Detecção de bordas: encontrar descontinuidades na tela inteira
- A abordagem de detecção de bordas desenha linhas encontrando descontinuidades da cena em um pass de tela cheia
- As descontinuidades podem ser detectadas em valores do buffer de profundidade, vetores normais, cores de albedo ou outros dados disponíveis durante a renderização
-
Roberts cross
- O Roberts cross operator é um operador diferencial que calcula a soma dos quadrados das diferenças entre pixels diagonais
- Em uma implementação real, detecta-se a borda aplicando convolução da imagem original com o kernel
- São usados 2 kernels, um para a direção
xe outro para a direçãoy - O tamanho do kernel é
2 x 2 - Só são necessárias 4 amostras ao redor de um pixel
- É um operador simples, mas pode gerar bons resultados
-
Sobel operator
- O Sobel operator também usa 2 kernels, um para a direção
xe outro para a direçãoy - O kernel de Sobel tem tamanho
3 x 3e usa 9 amostras ao redor de um pixel - Mais detalhes sobre o funcionamento do filtro Sobel podem ser vistos neste blog post on Sobel filters
- O Sobel operator também usa 2 kernels, um para a direção
-
Fontes de descontinuidade
- A abordagem mais comum é procurar descontinuidades nas texturas de profundidade, normal e cor que o pipeline de renderização gera para a cena
- O pass de detecção de bordas faz sampling dessas texturas e usa os operadores mencionados acima para detectar descontinuidades
- As bordas resultantes podem ser desenhadas por causa de uma descontinuidade encontrada em qualquer um dos três buffers
- Essa abordagem aplica outlines a todos os objetos que escrevem nesses buffers, então o controle por objeto é menor
- Permitir múltiplas fontes de descontinuidade torna o sistema de outlines mais robusto
- Algumas bordas são detectadas nas três fontes ao mesmo tempo
- Muitas bordas são detectadas apenas pela contribuição de uma fonte específica
- É possível atribuir pesos e limiares diferentes para cada fonte e assim controlar o resultado visual do outline
-
Modulação da detecção de bordas
- Aplicar apenas o operador de detecção de bordas ao buffer de descontinuidade pode dificultar a obtenção de um resultado sem artefatos
- Em muitos pipelines de renderização, o buffer de profundidade é implementado de forma não linear
- Quando dois objetos próximos à câmera estão separados por 1 m, a diferença de profundidade é maior do que a de dois objetos distantes separados pela mesma medida
- Para compensar isso, o limiar de detecção de descontinuidade de profundidade pode ser modulado pelo próprio buffer de profundidade
- Geometrias próximas precisam de uma descontinuidade maior no valor de profundidade antes de serem detectadas como borda
- Também podem surgir bordas indesejadas em ângulos rasantes pequenos
- É possível modular usando uma máscara de Fresnel criada com o produto escalar entre o vetor normal
Ne a direção de visãoV - Essa máscara é do mesmo tipo usado na abordagem de efeito de rim
- Outras técnicas de modulação também podem ser usadas, mas a escolha depende do efeito visual desejado
-
Fontes de descontinuidade customizadas
- Também é possível fornecer fontes de descontinuidade customizadas ao shader de outline
- Trata-se de uma render texture criada diretamente no processo de renderização e que armazena dados customizados para usar na geração do outline
- Como é possível controlar exatamente quais objetos escrevem nesse buffer customizado, também é possível controlar quais objetos receberão outline
- Um exemplo é renderizar as vertex colors da malha em uma textura para criar uma fonte de descontinuidade
- Outra forma é colorir faces de acordo com a posição no mundo ou criar um buffer customizado que combine informações dos buffers de profundidade e normal
- Mais informações estão em Linework section map
1 comentários
Opiniões no Hacker News
O artigo linkado sobre o Jump Flood Algorithm foi muito bom: https://bgolus.medium.com/the-quest-for-very-wide-outlines-b...
É interessante pensar em várias abordagens que podem ser usadas no nível de pixel/texel, e aqui também o campo de distância assinado (SDF) é usado de uma forma inteligente, fazendo boa parte do trabalho
O resultado de criar contornos da largura desejada em tempo linear é impressionante quando comparado a uma abordagem de força bruta para larguras grandes
Seja em uma abordagem baseada em vetores, em funções como no trabalho de Inigo Quilez, ou em raster baseado em texels/voxels como no artigo, recomendo muito SDF
O Houdini também tem bom suporte a SDFs raster e um conjunto maduro de ferramentas de SDF, então vale a pena dar uma olhada nem que seja na versão gratuita
A desvantagem é que, para rodar na GPU, ele precisa de escrita com acesso aleatório, ou seja, compute shaders
Se CPU estiver bom, há algumas implementações
JavaScript: https://parmanoir.com/distance/
C: https://github.com/983/df
C++: https://github.com/opencv/opencv/blob/4.x/modules/imgproc/sr...
Python: https://github.com/pymatting/pymatting/blob/afd2dec073cb08b8...
Porque a qualidade é boa e dá para renderizar efeitos baseados em distância, como contornos pulsantes
Esta ferramenta de desenho de linhas 3D também grava o SDF em uma textura pequena e faz sampling em runtime: https://x.com/alexanderameye/status/1663523972485357569
SDF é realmente poderoso
Se a mesma abordagem for estendida até radiance cascades, pode ficar ainda mais rápida, o que é bem interessante
Algum dia quero me aprofundar em gráficos 3D estilizados como um projeto de P&D
Houve bastante progresso recentemente, mas ainda parece haver muita fruta fácil de colher
Tenho curiosidade sobre como reduzir os detalhes de modelos 3D com toon rendering quando a câmera se afasta, e como fazer uma transição natural entre aparências mais estilizadas e menos estilizadas
Também há a questão de saber se é possível renderizar de forma convincente, em uma cena 3D, fundos em aquarela de animações 2D desenhadas à mão, e como animar suavemente, no espaço da tela, marcas de pincel e textura de papel
Também segue em aberto como representar elementos como fumaça, fogo, árvores, grama, lama, chuva, pelos e água em jogos 3D estilizados
Tenho curiosidade se, em jogos com câmera livre, seria possível automatizar a deformação sutil de modelos para que fiquem melhores a partir do ângulo atual da câmera, como em animações desenhadas à mão
Outra grande pergunta é como seriam o editor de malhas e o editor de cenários ideais para um renderizador estilizado, e se superfícies 3D e rigs fisicamente corretos são realmente necessários ou se poderiam ser definidos de forma mais abstrata
Também é interessante pensar se daria para criar jogos 2D procedurais renderizando pixel art retrô a partir de modelos 3D simples; e, quando duas malhas se cruzam por acaso, talvez a estilização possa ser usada para tornar essa interseção menos perceptível
Só essas perguntas já poderiam ocupar a carreira de dez pessoas, mas acho que isso é até bom
O filtro Kuwahara também parece convincente o bastante para a maioria
Para ferramentas de edição voltadas a renderizadores estilizados, Blender + Rigify + shape keys + um pouco de magia com drivers já foi suficiente para as minhas necessidades
Texturização no Blender é trabalhosa, mas suportável em nível de hobby; se for preciso mais controle de renderização não fotorrealista, o fork da DillonGoo Studio pode ser melhor: https://www.dillongoostudios.com/gooengine
Já experimentei criar pixel art a partir de modelos 3D renderizando animações/modelos em baixa resolução; o resultado ficou ok, mas exigiu tentativa e erro
Lembro que também houve casos com pós-processamento mais sofisticado para eliminar coisas como cintilação de pixels
Baixa resolução: https://x.com/Navy_Green/status/1525564342975995904
Estabilização: https://x.com/Navy_Green/status/1693820282245431540
Comecei minha carreira com apps de VR, mas logo migrei para desenvolvimento web porque o mercado era melhor
Textos assim me fazem sentir saudade daquela área
Trabalhar com gráficos 3D, colisão e shaders tinha uma sensação meio mágica difícil de encontrar em outras áreas
Na prática, é como criar mundos e reproduzir física, e a matemática também aparece de forma muito mais prática e frequente do que em outras áreas da programação
A satisfação no trabalho é muito maior, e a profundidade e o teto dos temas também são maiores
Estou nisso há 4 anos e nunca bati na parede do tédio como acontecia no desenvolvimento web
Não há nada como chegar ao trabalho na segunda-feira, abrir o editor do motor, ver o pequeno mundo em que estou trabalhando sendo renderizado e pensar em qual recurso legal vou colocar a seguir
Já na área de arte técnica, se sugarem os resultados e alimentarem uma rede neural com eles, as partes interessantes e gratificantes desaparecem, e para o trabalho profissional sobra apenas o trabalho chato de sempre, ou até mais dele
O valor do conjunto inteiro de habilidades também cai, e o pessoal de tecnologia age como se soubesse mais sobre esse trabalho do que quem o faz, enquanto acusa quem não fica feliz porque empresas do Vale do Silício destruíram o mercado ao lançar resultados C- a preço F, sem remunerar as pessoas que criaram os dados originais
Pesando prós e contras, acho que a escolha de migrar para desenvolvimento web foi a correta
No meu jogo Astral Divide, criei uma técnica que não está no artigo
Ela é parecida com Blurred Buffer, mas não faz um passe de blur; usa as bordas criadas pelo antialiasing
Desenho o objeto em branco opaco sobre um fundo preto transparente e, no fragment shader, filtro com um limiar fixo apenas os pixels cujo canal alfa não é totalmente opaco nem totalmente transparente
O resultado fica bom o suficiente, o custo de desempenho é baixo e a implementação é bem simples
Antigamente, ao implementar uma ferramenta de recorte em um software de edição de vídeo, eu precisava desfocar temporariamente a área fora do recorte durante a edição
Como não queria adicionar mais um passe de blur caro, apenas alterei o mipmap bias para renderizar uma textura de resolução mais baixa, e a filtragem de textura fez todo o trabalho de graça
Comparando com um efeito de blur de recorte parecido do PowerPoint, ficou quase igual; na verdade, o PowerPoint tinha um pouco de banding de cor, e a minha implementação não tinha
De modo parecido, em uma situação sem antialiasing em resolução total, a maior parte do canvas era composta por retângulos 2D rotacionáveis, então dava para ver serrilhado nas bordas
Em vez de ativar antialiasing em tela cheia, aumentei um pouco todos os retângulos e reduzi proporcionalmente as coordenadas UV, fazendo com que a borda visível ficasse dentro do retângulo 3D real; mais uma vez, a filtragem de textura resolveu de graça
Ou a ideia é manter o alfa e definir só a cor, para que o contorno fique suave, ainda que um pouco esmaecido?
As notas são excelentes
Recentemente, procurando abordagens de detecção de bordas, vi um bom método feito pelo desenvolvedor de Mars First Logistics: https://www.reddit.com/r/Unity3D/comments/taq2ou/improving_e...
O artigo é ótimo e o resultado também é muito bonito: https://ameye.dev/notes/rendering-outlines/edge-detection/co...
Parece uma cena da HQ holandesa Franka
Um artigo realmente excelente, e a experiência de leitura também é ótima
Ele explica conceitos difíceis em termos que qualquer pessoa consegue entender, os diagramas e exemplos são bons, e o espaçamento e a tipografia tornam a legibilidade de altíssimo nível
Fico curioso sobre o que motivou a criação do tema atual e se você já pensou em criar uma plataforma de publicação voltada a engenheiros
Arte técnica é, de longe, meu primeiro amor em software
Gostaria que o pipeline de compute shaders para efeitos de pós-processamento no Godot fosse mais fácil
A configuração atual de plugins de compositor tem bastante boilerplate
Este repositório é um bom exemplo de pós-processamento no Godot: https://github.com/sphynx-owner/JFA_driven_motion_blur_demo
Acho que a primeira vez que vi esse efeito foi em Wacky Races, no Dreamcast
Na época havia muito marketing dizendo que era o primeiro jogo a introduzir esse efeito e que os desenvolvedores praticamente o tinham inventado
Não sei se isso era verdade ou exagero publicitário, mas, como jogador, com certeza foi a primeira vez que experimentei aquilo
Sinceramente, o Dreamcast foi o primeiro hardware capaz de processar esse efeito em tempo real com alta qualidade
Desenvolvi o efeito de cel shading do jogo de Dreamcast Looney Tunes: Space Race, e o fiz logo na primeira semana em que recebemos o kit de desenvolvimento do Dreamcast
A Infogrames Sheffield, que fez Wacky Racers, viu uma versão inicial da nossa implementação e adicionou um efeito parecido ao jogo deles
Ficava bonito, mas entrou no fim da produção e não foi otimizado em torno desse efeito como o nosso jogo foi
A equipe de Jet Grind Radio também criou o mesmo efeito de forma independente e lançou antes de nós
O algoritmo era exatamente o mesmo, mas o uso era diferente: eles aceitaram de propósito os contornos irregulares, largos e recortados, enquanto a Sheffield e nós estávamos lutando contra essa característica para chegar a um estilo artístico mais uniforme e tradicional
Cerca de um ano depois, em Dragons Lair 3D no Xbox, parece que alguém encontrou uma forma de rodar cel shading por detecção de bordas em tempo real
Cheguei a fazer uma implementação de teste dessa abordagem no Dreamcast, mas o desempenho não era nem de longe suficiente para aplicá-la a vários personagens ao mesmo tempo enquanto o jogo rodava
Não sei se foi porque o Xbox era mais potente ou porque o algoritmo era mais inteligente, mas o resultado é inegável
Se você quer fazer um jogo que pareça uma animação desenhada à mão de verdade, pessoalmente ainda acho que esse é o método de maior qualidade
Algum dia quero encontrar uma desculpa para implementá-lo de novo, e agora acho que desempenho já não seria um problema
Uma coincidência bem incrível
Preciso mesmo começar com programação de shaders e renderização 3D
Artigos assim são muito bons, e eu gostaria de realmente conseguir escrever shaders