- Com apenas 240 linhas de código GLSL, a animação do Rick é criada, e o shader da GPU calcula a cor de cada pixel ao longo do tempo sem usar imagens nem bibliotecas
- O ponto central é que
color_for_pixel(pixel, time) é executada para todos os pixels, e a signed distance field (SDF) é a estrutura que representa interior, exterior e contorno das formas com valores de distância
- O rosto é montado combinando formas SDF como
round_rect(), circle(), star(), bezier() e parabola() com deslocamento, escala, espelhamento, repetição e união
- As ondulações do cabelo, a repetição dos dentes, o contorno dos lábios, o piscar, o movimento das pupilas e o balanço da cabeça são adicionados gradualmente com domain warping,
mod(), sin(time) e noise()
- A versão final inclui até o fundo do portal, mas como o editor não tem exportação de vídeo, é necessário um fluxo separado no macOS com
glslviewer e ffmpeg
Estrutura do shader que calcula a cor por pixel
- A animação é escrita em OpenGL Shading Language (GLSL)
- É possível executar e modificar os exemplos na hora no editor de live coding da página
- O ponto de entrada básico é a função
color_for_pixel(vec2 pixel, float time)
- A GPU executa essa função para todos os pixels da prévia
- A pergunta que a função responde é: “que cor este pixel deve ter neste momento?”
- Um exemplo simples coloca os valores
pixel.x, pixel.y e time nos canais de cor para visualizar a posição do pixel e a variação no tempo
time é o número de segundos desde a última edição e continua aumentando
Como as formas são desenhadas com SDF
- Um círculo pode ser desenhado calculando a distância do pixel até a origem com
length(pixel) e comparando com o raio
- A função
circle() não retorna apenas um bool indicando dentro ou fora, mas sim a distância até a borda
- Dentro da forma: valor negativo
- Fora da forma: valor positivo
- Na borda: valor próximo de 0
- Esse tipo de função é uma função de signed distance field (SDF)
- Com o valor de distância, preenchimento e contorno podem ser tratados da mesma forma
- Se
dist < -0.01, usa a cor interna
- Se
dist < 0.0, usa contorno preto
- Várias formas SDF podem ser unidas com
min()
- Se a cabeça e as orelhas do Rick tiverem contornos tratados separadamente, surge uma linha desnecessária entre elas
- Ao unir os dois valores de distância com
min(), sobra apenas o contorno da união da cabeça com as orelhas
- Existem outras formas de combinação, e os materiais de referência ligados são 2D distance functions de Inigo Quilez e conteúdos sobre combinação de primitivas
As formas que compõem o rosto do Rick
- O formato da cabeça é feito com
round_rect(), e as orelhas também são adicionadas com round_rect() separado
- A imagem do Rick do pôster da 1ª temporada é sobreposta piscando na prévia para ajustar os valores
- Muitos números foram encontrados nesse processo de tentativa e erro
- Os valores de cor foram obtidos com a ferramenta conta-gotas de um editor de imagens
- Os olhos são compostos por SDF circular e uma
star() de 6 pontas
- O globo ocular é levemente esticado na vertical com transformação de coordenadas como
pixel.y *= .93
- A pupila arredonda as pontas subtraindo um pequeno valor de distância da SDF de estrela de 6 pontas
- Os dois olhos são feitos com espelhamento lateral usando
pixel.x = abs(pixel.x), sem duplicar código
- Nariz, boca e sobrancelhas são desenhados com
bezier()
- O cabelo começa a partir de uma
star() de 11 pontas alongada verticalmente
- A ordem do código que desenha as formas também afeta o resultado
- A primeira cor retornada é a forma que aparece na frente
- Dentes e língua só são desenhados dentro da condicional do formato da boca, então não saem para fora dela
Técnicas para refinar cabelo, dentes e linhas decorativas
- O cabelo rígido em forma de estrela vira uma ondulação com domain warping
- Domain warping é uma técnica que embaralha as coordenadas com offsets aleatórios baseados na posição do pixel
- A mesma posição recebe o mesmo offset, permitindo uma distorção consistente ao longo do tempo
- Como referência, há o material domain warping de Inigo Quilez
- Os dentes são feitos criando um com a SDF
parabola() e repetindo horizontalmente com mod()
mod(pixel.x, width) faz a coordenada x recomeçar do 0 em intervalos regulares, repetindo a mesma forma
pixel.y = abs(pixel.y)-.06 espelha os dentes de cima e de baixo
- Um deslocamento em y baseado em
pow(pixel.x, 2.) ajusta os dentes à curva do sorriso
- Condições como
abs(pixel.x+.06) < .194 limitam a repetição infinita
- As linhas dos lábios e abaixo dos olhos são desenhadas empurrando o contorno SDF para fora
- Um contorno normal é
abs(distance_to_shape) < thickness
- Um contorno deslocado para fora é
abs(distance_to_shape - outset) < thickness
- A linha abaixo dos olhos recebe ainda condições de posição para aparecer só em uma área específica abaixo deles
Como criar movimento com o tempo
- A animação mais simples é feita colocando
sin(time) no código
sin() envolve o valor crescente de time entre -1 e 1, criando uma animação repetitiva
- O intervalo é ajustado com escala e offset, como em
sin(time)*.5 + .5
- A rotação da cabeça do Rick, a rotação da língua e a altura das sobrancelhas se movem assim
- Para o cálculo de rotação, é adicionada a função
rotateAt()
- O piscar é implementado trocando o que é desenhado conforme o tempo
- Se
mod(time, 2.) < .09, desenha os olhos fechados
- Caso contrário, desenha os olhos abertos e as pupilas
- O movimento irregular das pupilas usa
noise()
- Para evitar que os olhos se movam suavemente o tempo todo, aplica-se
round(time) antes de passar o valor para noise()
- O cabelo se move com mais flexibilidade usando time domain warping
- Em vez de distorcer o espaço, o valor do tempo é atrasado de acordo com a posição
- Quanto mais perto da ponta do cabelo, diferente fica o atraso, então o conjunto não gira rigidamente e sim se curva
Fundo do portal e finalização
- A versão final é concluída com a adição de um efeito de portal atrás do Rick
- O efeito de portal é baseado em um efeito criado pela usuária do ShaderToy valena, e uma nota diz que é uma versão reduzida por FabriceNeyret2
- O código prioriza legibilidade acima de desempenho
- O resultado completo reúne, em um único exemplo, combinação de formas, SDF, warping, repetição, transições baseadas em tempo e movimento baseado em ruído necessários para animação 2D com shader
Fluxo de exportação de vídeo
- O editor da página ainda não consegue exportar a animação em vídeo
- Como solução temporária, é proposto um fluxo no macOS usando glslviewer e ffmpeg
- Um exemplo de instalação das dependências é o seguinte
brew install glslviewer ffmpeg
- O script de exportação cria um diretório temporário e executa
glslViewer em modo headless
- Resolução:
1920x1080
- Sequência: de
0 a 7 segundos
- Taxa de quadros:
60
- Arquivo de saída:
animation.mp4
- Um exemplo de live coding local usa o seguinte comando
glslViewer shader.frag -w 575 -h 324 --noncurses -x 0 -y 0
Superamostragem e origem do texto
- O motivo de as bordas das formas parecerem suaves é que superamostragem foi aplicada por trás
- A superamostragem funciona chamando
color_for_pixel() em 9 posições dentro de um único pixel da tela e exibindo a média
- Ao usar
#version 300 es, o modo “pro” do editor é ativado e a superamostragem automática é removida
- O ponto de partida do texto foi o vídeo publicado há 8 meses I Made a 3D Modeler, in C, in a Week
- Esse vídeo inclui uma animação explicando o algoritmo marching cubes
- A percepção foi de que seria difícil fazer isso com precisão e rapidez em programas comuns de animação, então a solução foi começar a criar em código
- Depois, como as pessoas perguntaram como a animação foi feita, isso foi organizado neste texto
1 comentários
Comentários do Hacker News
Basicamente, a ideia é trocar a função
stepporaastep: https://github.com/glslify/glsl-aastepÉ um estilo de trabalho bem diferente do desenvolvimento de web, protocolos e aplicações com que muita gente está acostumada: denso e iterativo
É bem satisfatório mudar um
floate apertarshift-enterpara ver o resultado na horacanvasdo JavaScript ou p5.js em cima dele passa essa mesma sensaçãoNão sei bem se isso se refere à programação gráfica em geral ou, mais especificamente, ao trabalho com shaders de GPU
Também existe uma playlist relacionada do Inigo Quilez no YouTube: https://www.youtube.com/watch?v=0ifChJ0nJfM&list=PL0EpikNmjs...
Programação de shaders realmente é um universo à parte, e o texto é muito bom
Fico curioso sobre como seria fazer isso em Vulkan ou WebGPU/WebGL
No caso do Vulkan, mais precisamente, é algo como GLSL para SPIR-V
O WebGPU no navegador tecnicamente não usa GLSL diretamente, mas implementações nativas de WebGPU podem aceitar GLSL, e também dá para converter
Ou então é só usar WGSL, que é quase igual a GLSL, exceto pelo fato de usar uma sintaxe inspirada em Rust em vez daquela com cara de C
Parece que chegar a 240 linhas com os valores decimais certos por tentativa e erro deve ter levado bastante tempo
Busca binária é rápida mesmo quando feita à mão
O uniform é passado para o shader e pode ser atualizado sem recompilar