Texturas com atualização automática
Quando é possível paralelizar simulações ou tarefas de renderização, geralmente é melhor executá-las na GPU. Neste artigo, explicarei uma técnica que usa esse fato para criar truques visuais impressionantes com sobrecarga de baixo desempenho. Todos os efeitos que demonstrarei são implementados usando texturas que, quando atualizadas, "
se renderam "; a textura é atualizada quando um novo quadro é renderizado e o próximo estado da textura depende completamente do estado anterior. Nessas texturas, você pode desenhar, causando certas alterações, e a própria textura, direta ou indiretamente, pode ser usada para renderizar animações interessantes. Eu os chamo de
texturas convolucionais .
Figura 1: buffer duplo de convoluçãoAntes de prosseguir, precisamos resolver um problema: a textura não pode ser lida e escrita ao mesmo tempo, APIs gráficas como OpenGL e DirectX não permitem isso. Como o próximo estado da textura depende do anterior, precisamos contornar essa limitação. Preciso ler de uma textura diferente, não daquela em que estou escrevendo.
A solução é
buffer duplo . A Figura 1 mostra como funciona: de fato, em vez de uma textura, existem duas, mas uma é gravada e uma é lida da outra. A textura que está sendo gravada é chamada de
buffer de fundo e a textura renderizada é chamada de
buffer de frente . Como o teste convolucional é "gravado em si mesmo", o buffer secundário em cada quadro grava no buffer primário e, em seguida, o primário é renderizado ou usado para renderização. No próximo quadro, as funções mudam e o buffer primário anterior é usado como fonte para o próximo buffer primário.
Ao renderizar o estado anterior em uma nova textura de convolução, o shader de fragmento (ou
shader de pixel ) fornece efeitos e animações interessantes. O sombreador determina como o estado muda. O código fonte para todos os exemplos do artigo (e outros) pode ser encontrado no
repositório no GitHub .
Exemplos simples de aplicação
Para demonstrar essa técnica, escolhi uma simulação bem conhecida na qual, ao atualizar, o estado depende completamente do estado anterior: o
jogo Conway “Life” . Essa simulação é realizada em uma grade de quadrados, cada célula viva ou morta. As regras para o seguinte estado da célula são simples:
- Se uma célula viva tem menos de dois vizinhos, mas fica morta.
- Se uma célula viva tem dois ou três vizinhos vivos, ela permanece viva.
- Se uma célula viva tem mais de três vizinhos vivos, ela se torna morta.
- Se uma célula morta tem três vizinhos vivos, ela se torna viva.
Para implementar este jogo como uma textura convolucional, interpreto a textura como a grade do jogo, e o shader é processado com base nas regras acima. Um pixel transparente é uma célula morta e um pixel branco opaco é uma célula viva. Uma implementação interativa é mostrada abaixo. Para acessar a GPU, uso
myr.js , que requer o
WebGL 2 . Os navegadores mais modernos (por exemplo, Chrome e Firefox) podem funcionar com ele, mas se a demonstração não funcionar, provavelmente o navegador não a suporta. Use o mouse (ou tela sensível ao toque) [no artigo original] para desenhar células vivas na textura.
O código do fragment shader (no GLSL, porque eu uso o WebGL para renderização) é mostrado abaixo. Primeiro, implementei a função
get
, que me permite ler um pixel de um deslocamento específico do atual. A variável
pixelSize
é um vetor 2D pré-criado que contém o deslocamento UV de cada pixel e a função
get
utiliza para ler a célula vizinha. Em seguida, a função
main
determina a nova cor da célula com base no estado atual (ativo) e no número de vizinhos vivos.
uniform sampler2D source; uniform lowp vec2 pixelSize; in mediump vec2 uv; layout (location = 0) out lowp vec4 color; int get(int dx, int dy) { return int(texture(source, uv + pixelSize * vec2(dx, dy)).r); } void main() { int live = get(0, 0); int neighbors = get(-1, -1) + get(0, -1) + get(1, -1) + get(-1, 0) + get(1, 0) + get(-1, 1) + get(0, 1) + get(1, 1); if (live == 1 && neighbors < 2) color = vec4(0); else if (live == 1 && (neighbors == 2 || neighbors == 3)) color = vec4(1); else if (live == 1 && neighbors == 3) color = vec4(0); else if (live == 0 && neighbors == 3) color = vec4(1); else color = vec4(0); }
Outra textura convolucional simples é um
jogo com areia caindo , em que o usuário pode jogar areia colorida no local, que cai e forma montanhas. Embora sua implementação seja um pouco mais complicada, as regras são mais simples:
- Se não houver areia sob um grão de areia, ela cai um pixel.
- Se houver areia sob um grão de areia, mas ela pode deslizar 45 graus para a esquerda ou direita, o fará.
A administração neste exemplo é igual à do jogo "Life". Como, nessas regras, a areia pode cair a uma velocidade de apenas um pixel por quadro, a fim de acelerar um pouco o processo, a textura por quadro é atualizada três vezes. O código fonte do aplicativo está
aqui .
Um passo à frente
Figura 2: Ondas de pixel.Os exemplos acima usam textura convolucional diretamente; seu conteúdo é renderizado na tela como está. Se você interpreta imagens apenas como pixels, os limites de uso dessa técnica são muito limitados, mas, graças aos equipamentos modernos, eles podem ser expandidos. Em vez de contar pixels como cores, os interpretarei de maneira um pouco diferente, que pode ser usada para criar animações de mais uma textura ou modelo 3D.
Primeiro, interpretarei a textura convolucional como um mapa de altura. A textura simulará
ondas e
vibrações no plano aquático e os resultados serão usados para renderizar reflexos e ondas sombreadas. Não precisamos mais ler a textura como uma imagem, para que possamos usar seus pixels para armazenar qualquer informação. No caso de um sombreador de água, armazenarei a altura da onda no canal vermelho e o pulso da onda no canal verde, conforme mostrado na Figura 2. Os canais azul e alfa ainda não são usados. As ondas são criadas desenhando pontos vermelhos em uma textura convolucional.
Não considerarei a metodologia para atualizar o mapa de altura, emprestado do site de
Hugo Elias , que parece ter desaparecido da Internet. Ele também aprendeu sobre esse algoritmo com um autor desconhecido e o implementou em C para execução na CPU. O código fonte do aplicativo abaixo está
aqui .
Aqui, usei um mapa de altura apenas para compensar a textura e adicionar sombreamento, mas na terceira dimensão, aplicativos muito mais interessantes podem ser implementados. Quando uma textura convolucional é interpretada por um sombreador de vértice, um plano subdividido plano pode ser distorcido para criar ondas tridimensionais. Você pode aplicar o sombreamento e a iluminação usuais à forma resultante.
Vale notar que os pixels na textura convolucional do exemplo mostrado acima às vezes armazenam valores muito pequenos que não devem desaparecer devido a erros de arredondamento. Portanto, os canais de cores dessa textura devem ter uma resolução mais alta, e não os 8 bits padrão. Neste exemplo, aumentei o tamanho de cada canal de cores para 16 bits, o que deu resultados bastante precisos. Se você não estiver armazenando pixels, geralmente precisará aumentar a precisão da textura. Felizmente, as APIs gráficas modernas suportam esse recurso.
Usamos todos os canais
Figura 3: Grama de pixel.No exemplo da água, apenas os canais vermelho e verde são usados, mas no próximo exemplo, aplicaremos todos os quatro. É simulado um campo com grama (ou árvores), que pode ser movido usando o cursor. A Figura 3 mostra quais dados são armazenados em um pixel. O deslocamento é armazenado nos canais vermelho e verde, e a velocidade é armazenada nos canais azul e alfa. Essa velocidade é atualizada para mudar para a posição de repouso com um movimento de onda gradualmente diminuindo.
No exemplo da água, a criação de ondas é bastante simples: é possível desenhar pontos na textura, e a mistura alfa fornece formas suaves. Você pode criar facilmente vários pontos sobrepostos. Neste exemplo, tudo é mais complicado porque o canal alfa já está em uso. Não podemos desenhar um ponto com um valor alfa de 1 no centro e 0 a partir da borda, porque isso dará à grama um impulso desnecessário (já que o impulso vertical é armazenado no canal alfa). Nesse caso, um sombreador separado foi escrito para desenhar o efeito na textura convolucional. Esse sombreador garante que a mistura alfa não produza efeitos inesperados.
O código fonte do aplicativo pode ser encontrado
aqui .
A grama é criada em 2D, mas o efeito funcionará em ambientes 3D. Em vez de deslocamento de pixel, os vértices são deslocados, o que também é mais rápido. Além disso, com a ajuda de picos, outro efeito pode ser percebido: força diferente dos galhos - a grama se dobra facilmente com o menor vento e as árvores fortes flutuam apenas durante tempestades.
Embora existam muitos algoritmos e shaders para criar os efeitos do vento e do deslocamento da vegetação, essa abordagem tem uma séria vantagem: desenhar efeitos em uma textura convolucional é um processo de baixo custo. Se o efeito for aplicado em um jogo, o movimento da vegetação poderá ser determinado por centenas de influências diferentes. Não apenas o personagem principal, mas também todos os objetos, animais e movimentos podem influenciar o mundo à custa de custos insignificantes.
Outros casos de uso e falhas
Você pode criar muitas outras aplicações de tecnologia, por exemplo:
- Usando uma textura convolucional, você pode simular a velocidade do vento. Na textura, você pode desenhar obstáculos que fazem o ar circular ao redor deles. Partículas (chuva, neve e folhas) podem usar essa textura para voar em torno de obstáculos.
- Você pode simular a propagação de fumaça ou fogo.
- A textura pode codificar a espessura de uma camada de neve ou areia. Traços e outras interações com a camada podem criar amassados e impressões na camada.
Ao usar esse método, existem dificuldades e limitações:
- É difícil ajustar as animações para alterar as taxas de quadros. Por exemplo, em um aplicativo com areia caindo, grãos de areia caem a uma velocidade constante - um pixel por atualização. Uma solução possível pode ser a atualização de texturas convolucionais com uma frequência constante, semelhante à maneira como a maioria dos motores físicos funciona; o mecanismo de física funciona a uma frequência constante e seus resultados são interpolados.
- Transferir dados para a GPU é um processo rápido e fácil; no entanto, recuperar dados não é tão fácil. Isso significa que a maioria dos efeitos gerados por esta técnica é unidirecional; eles são transferidos para a GPU, e a GPU faz seu trabalho sem mais intervenção e feedback. Se eu quisesse incorporar o comprimento de onda do exemplo da água em cálculos físicos (por exemplo, para que os navios oscilassem junto com as ondas), precisaria de valores da textura convolucional. A recuperação de dados de textura de uma GPU é um processo muito lento que não precisa ser feito em tempo real. A solução para esse problema pode ser a implementação de duas simulações: uma com alta resolução para gráficos de água como textura convolucional e outra com baixa resolução na CPU para física de água. Se os algoritmos forem os mesmos, as discrepâncias podem ser bastante aceitáveis.
As demos neste artigo podem ser otimizadas ainda mais. No exemplo da grama, você pode usar uma textura com resolução muito menor sem defeitos perceptíveis; isso vai ajudar muito em grandes cenas. Outra otimização: você pode usar uma taxa de atualização mais baixa, por exemplo, em cada quarto quadro ou em um quarto por quadro (já que essa técnica não causa problemas nas atualizações segmentadas). Para manter uma taxa de quadros suave, o estado anterior e atual da textura convolucional pode ser interpolado.
Como as texturas convolucionais usam buffer duplo interno, você pode usar as duas texturas ao mesmo tempo para renderizar. O buffer primário é o estado atual e o secundário é o anterior. Isso pode ser útil para interpolar a textura ao longo do tempo ou para calcular derivadas para valores de textura.
Conclusão
GPUs, especialmente em programas 2D, geralmente ficam ociosas. Embora pareça que ele só possa ser usado na renderização de cenas 3D complexas, a técnica demonstrada neste artigo mostra pelo menos uma outra maneira de usar o poder da GPU. Usando os recursos para os quais a GPU foi desenvolvida, você pode implementar efeitos e animações interessantes que geralmente são muito caros para a CPU.