Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Animando milhares de peixes com MultiMeshInstance3D

Este tutorial explora uma técnica usada no jogo ABZU para renderizar e animar milhares de peixes usando animação de vértices e instanciação de malha estática.

No Godot, isso pode ser alcançado com um Shader customizado e um MultiMeshInstance3D. Usando a seguinte técnica, você pode renderizar milhares de objetos animados, mesmo em hardware de baixo custo.

Começaremos animando um peixe. Depois, veremos como estender essa animação para milhares de peixes.

Animando um peixe

Começaremos com um único peixe. Carregue o seu modelo de peixe em um MeshInstance3D e adicione um novo ShaderMaterial.

Aqui está o peixe que usaremos para as imagens de exemplo, você pode usar qualquer modelo de peixe que gostar.

../../../_images/fish.png

Nota

O modelo de peixe neste tutorial foi feito por QuaterniusDev e é compartilhado com uma licença creative commons. CC0 1.0 Universal (CC0 1.0) Dedicação ao Domínio Público https://creativecommons.org/publicdomain/zero/1.0/

Normalmente, você usaria ossos e um Skeleton3D para animar objetos. No entanto, os ossos são animados na CPU e, portanto, você acaba tendo que calcular milhares de operações a cada quadro, tornando impossível ter milhares de objetos. Usando animação de vértices em um vertex shader, você evita usar ossos e pode, em vez disso, calcular a animação completa em poucas linhas de código e inteiramente na GPU.

A animação será feita de quatro movimentos chave:

  1. Um movimento de um lado para o outro

  2. Um movimento de pivô em torno do centro do peixe

  3. Um movimento de onda panorâmico

  4. Um movimento de torção panorâmico

Todo o código para a animação estará no vertex shader com uniforms controlando a quantidade de movimento. Usamos uniforms para controlar a força do movimento para que você possa ajustar a animação no editor e ver os resultados em tempo real, sem que o shader precise ser recompilado.

Todos os movimentos serão feitos usando ondas de cosseno aplicadas ao VERTEX no espaço do modelo (model space). Queremos que os vértices estejam no espaço do modelo para que o movimento seja sempre relativo à orientação do peixe. Por exemplo, o movimento de um lado para o outro sempre moverá o peixe para frente e para trás em sua direção da esquerda para a direita, em vez de no eixo x na orientação do mundo.

Para controlar a velocidade da animação, começaremos definindo nossa própria variável de tempo usando TIME.

//time_scale is a uniform float
float time = TIME * time_scale;

O primeiro movimento que implementaremos é o de um lado para o outro. Ele pode ser feito deslocando VERTEX.x pelo cos de TIME. Cada vez que a malha for renderizada, todos os vértices se moverão para o lado pela quantidade de cos(time).

//side_to_side is a uniform float
VERTEX.x += cos(time) * side_to_side;

A animação resultante deve parecer algo como isto:

../../../_images/sidetoside.gif

Em seguida, adicionamos o pivô. Como o peixe está centralizado em (0, 0), tudo o que precisamos fazer é multiplicar VERTEX por uma matriz de rotação para que ele gire em torno do centro do peixe.

Construímos uma matriz de rotação da seguinte forma:

//angle is scaled by 0.1 so that the fish only pivots and doesn't rotate all the way around
//pivot is a uniform float
float pivot_angle = cos(time) * 0.1 * pivot;
mat2 rotation_matrix = mat2(vec2(cos(pivot_angle), -sin(pivot_angle)), vec2(sin(pivot_angle), cos(pivot_angle)));

E então a aplicamos nos eixos x e z multiplicando-a por VERTEX.xz.

VERTEX.xz = rotation_matrix * VERTEX.xz;

Com apenas o pivô aplicado, você deve ver algo assim:

../../../_images/pivot.gif

Os próximos dois movimentos precisam percorrer a espinha do peixe. Para isso, precisamos de uma nova variável, body. body é um float que é 0 na cauda do peixe e 1 na sua cabeça.

float body = (VERTEX.z + 1.0) / 2.0; //for a fish centered at (0, 0) with a length of 2

O próximo movimento é uma onda de cosseno que se move ao longo do comprimento do peixe. Para fazê-la se mover ao longo da espinha do peixe, deslocamos a entrada de cos pela posição ao longo da espinha, que é a variável que definimos acima, body.

//wave is a uniform float
VERTEX.x += cos(time + body) * wave;

Isso se parece muito com o movimento de um lado para o outro que definimos acima, mas neste, ao usar body para deslocar o cos, cada vértice ao longo da espinha tem uma posição diferente na onda, fazendo parecer que uma onda está se movendo ao longo do peixe.

../../../_images/wave.gif

O último movimento é a torção (twist), que é uma rotação panorâmica ao longo da coluna. Semelhante ao pivô, primeiro construímos uma matriz de rotação.

//twist is a uniform float
float twist_angle = cos(time + body) * 0.3 * twist;
mat2 twist_matrix = mat2(vec2(cos(twist_angle), -sin(twist_angle)), vec2(sin(twist_angle), cos(twist_angle)));

Aplicamos a rotação nos eixos xy para que o peixe pareça rolar em torno de sua espinha. Para que isso funcione, a espinha do peixe precisa estar centralizada no eixo z.

VERTEX.xy = twist_matrix * VERTEX.xy;

Aqui está o peixe com a torção aplicada:

../../../_images/twist.gif

Se aplicarmos todos esses movimentos um após o outro, obteremos um movimento fluido semelhante ao de uma água-viva.

../../../_images/all_motions.gif

Peixes normais nadam principalmente com a metade traseira de seus corpos. Consequentemente, precisamos limitar os movimentos de ondulação à metade traseira do peixe. Para fazer isso, criamos uma nova variável, mask.

mask é um float que vai de 0 na frente do peixe a 1 no final, usando smoothstep para controlar o ponto em que a transição de 0 para 1 acontece.

//mask_black and mask_white are uniforms
float mask = smoothstep(mask_black, mask_white, 1.0 - body);

Abaixo está uma imagem do peixe com a mask usada como COLOR:

../../../_images/mask.png

Para a onda, multiplicamos o movimento por mask, o que o limitará à metade traseira.

//wave motion with mask
VERTEX.x += cos(time + body) * mask * wave;

Para aplicar a máscara à torção, usamos mix. O mix nos permite misturar a posição do vértice entre um vértice totalmente rotacionado e um que não está rotacionado. Precisamos usar mix em vez de multiplicar mask pelo VERTEX rotacionado porque não estamos adicionando o movimento ao VERTEX; estamos substituindo o VERTEX pela versão rotacionada. Se multiplicássemos isso por mask, encolheríamos o peixe.

//twist motion with mask
VERTEX.xy = mix(VERTEX.xy, twist_matrix * VERTEX.xy, mask);

Juntar os quatro movimentos nos dá a animação final.

../../../_images/all_motions_mask.gif

Vá em frente e brinque com os uniforms para alterar o ciclo de nado do peixe. Você descobrirá que pode criar uma grande variedade de estilos de nado usando esses quatro movimentos.

Criando um cardume de peixes

O Godot facilita a renderização de milhares de unidades de um mesmo objeto usando um nó MultiMeshInstance3D.

Um nó MultiMeshInstance3D é criado e usado da mesma forma que você criaria um nó MeshInstance3D. Para este tutorial, chamaremos o nó MultiMeshInstance3D de School (Cardume), porque ele conterá um cardume de peixes.

Assim que tiver um MultiMeshInstance3D, adicione um MultiMesh e, a esse MultiMesh, adicione sua Mesh com o shader de cima.

MultiMeshes desenham sua Mesh com três propriedades adicionais por instância: Transform (rotação, translação, escala), Color e Custom. Custom é usado para passar 4 variáveis de uso múltiplo usando uma Color.

instance_count especifica quantas instâncias da malha você deseja desenhar. Por enquanto, deixe instance_count em 0 porque você não pode alterar nenhum dos outros parâmetros enquanto instance_count for maior que 0. Definiremos o instance_count no GDScript mais tarde.

transform_format especifica se as transformações usadas são 3D ou 2D. Para este tutorial, selecione 3D.

Tanto para color_format quanto para custom_data_format, você pode escolher entre None, Byte e Float. None significa que você não passará esses dados (seja uma variável COLOR por instância ou INSTANCE_CUSTOM) para o shader. Byte significa que cada número que compõe a cor que você passa será armazenado com 8 bits, enquanto Float significa que cada número será armazenado em um número de ponto flutuante (32 bits). Float é mais lento porém mais preciso; Byte consumirá menos memória e será mais rápido, mas você poderá notar alguns artefatos visuais.

Agora, defina instance_count para o número de peixes que você deseja ter.

Em seguida, precisamos definir as transformações por instância.

Existem duas maneiras de definir transformações por instância para MultiMeshes. A primeira é inteiramente no editor e está descrita no tutorial de MultiMeshInstance3D.

A segunda é percorrer todas as instâncias em um laço e definir suas transformações no código. Abaixo, usamos o GDScript para percorrer todas as instâncias e definir suas transformações para uma posição aleatória.

for i in range($School.multimesh.instance_count):
  var position = Transform3D()
  position = position.translated(Vector3(randf() * 100 - 50, randf() * 50 - 25, randf() * 50 - 25))
  $School.multimesh.set_instance_transform(i, position)

Executar esse script colocará os peixes em posições aleatórias dentro de uma caixa ao redor da posição do MultiMeshInstance3D.

Nota

Se a performance for um problema para você, tente rodar a cena com menos peixes.

Notou como todos os peixes estão na mesma posição em seu ciclo de nado? Isso os faz parecer muito robóticos. O próximo passo é dar a cada peixe uma posição diferente no ciclo de nado para que todo o cardume pareça mais orgânico.

Animando um cardume de peixes

Um dos benefícios de animar os peixes usando funções cos é que eles são animados com apenas um parâmetro, time. Para dar a cada peixe uma posição única no ciclo de nado, só precisamos deslocar o time.

Fazemos isso adicionando o valor personalizado por instância INSTANCE_CUSTOM ao time.

float time = (TIME * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);

Em seguida, precisamos passar um valor para INSTANCE_CUSTOM. Fazemos isso adicionando uma linha dentro do laço for lá de cima. No laço for, atribuímos a cada instância um conjunto de quatro floats aleatórios para usar.

$School.multimesh.set_instance_custom_data(i, Color(randf(), randf(), randf(), randf()))

Agora todos os peixes têm posições únicas no ciclo de nado. Você pode dar a eles um pouco mais de individualidade usando INSTANCE_CUSTOM para fazê-los nadar mais rápido ou mais devagar ao multiplicar por TIME.

//set speed from 50% - 150% of regular speed
float time = (TIME * (0.5 + INSTANCE_CUSTOM.y) * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);

Você pode até experimentar mudar a cor por instância da mesma forma que mudou o valor personalizado por instância.

Um problema que você encontrará a este ponto é que os peixes estão animados, mas não estão se movendo. Você pode movê-los atualizando a transformação por instância de cada peixe a cada quadro. Embora fazer isso seja mais rápido do que mover milhares de MeshInstance3Ds por quadro, ainda assim provavelmente será lento.

No próximo tutorial, cobriremos como usar GPUParticles3D para tirar vantagem da GPU e mover cada peixe individualmente, mantendo os benefícios da instanciação.