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.

Seu primeiro shader 3D

Você decidiu começar a escrever seu próprio shader Espacial (Spatial) personalizado. Talvez tenha visto um truque legal na internet feito com shaders, ou descobriu que o StandardMaterial3D não está atendendo totalmente às suas necessidades. De qualquer forma, você decidiu escrever o seu próprio e agora precisa descobrir por onde começar.

Este tutorial explicará como escrever um shader Espacial e abordará mais tópicos do que o tutorial de CanvasItem.

Os shaders espaciais possuem mais funcionalidades integradas do que os shaders de CanvasItem. A expectativa com shaders espaciais é que o Godot já forneceu a funcionalidade para casos de uso comuns e tudo o que o usuário precisa fazer no shader é definir os parâmetros adequados. Isso é especialmente verdadeiro para um fluxo de trabalho PBR (physically based rendering - renderização baseada em física).

Este é um tutorial em duas partes. Nesta primeira parte, criaremos um terreno usando deslocamento de vértices a partir de um mapa de altura (heightmap) na função vertex. Na segunda parte, pegaremos os conceitos deste tutorial e configuraremos materiais personalizados em um fragment shader ao criar um shader de água do oceano.

Nota

Este tutorial pressupõe algum conhecimento básico de shaders, como tipos (vec2, float, sampler2D) e funções. Se você não se sente confortável com esses conceitos, é melhor obter uma introdução suave no The Book of Shaders antes de concluir este tutorial.

Onde atribuir meu material

Em 3D, os objetos são desenhados usando Meshes (Malhas). Meshes são um tipo de recurso que armazena geometria (o formato do seu objeto) e materiais (a cor e como o objeto reage à luz) em unidades chamadas "surfaces" (superfícies). Uma Mesh pode ter múltiplas superfícies, ou apenas uma. Normalmente, você importaria uma malha de outro programa (por exemplo, Blender). Mas o Godot também possui algumas PrimitiveMeshes (Malhas Primitivas) que permitem adicionar geometria básica a uma cena sem importar Meshes.

Existem múltiplos tipos de nós que você pode usar para desenhar uma malha. O principal deles é o MeshInstance3D, mas você também pode usar GPUParticles3D, MultiMeshes (com um MultiMeshInstance3D), ou outros.

Normalmente, um material está associado a uma determinada superfície em uma malha, mas alguns nós, como o MeshInstance3D, permitem que você substitua (override) o material para uma superfície específica, ou para todas as superfícies.

Se você definir um material na superfície ou na própria malha, todas as MeshInstance3Ds que compartilham essa malha compartilharão esse material. No entanto, se você quiser reutilizar a mesma malha em várias instâncias de malha, mas ter materiais diferentes para cada instância, você deve definir o material na MeshInstance3D.

Para este tutorial, definiremos nosso material na própria malha, em vez de aproveitar a capacidade do MeshInstance3D de substituir materiais.

Configurando

Adicione um novo nó MeshInstance3D à sua cena.

Na aba do inspetor, defina a propriedade Mesh do MeshInstance3D para um novo recurso PlaneMesh, clicando em <empty> e escolhendo New PlaneMesh. Em seguida, expanda o recurso clicando na imagem de um plano que aparece.

Isso adiciona um plano à nossa cena.

Depois, na viewport, clique no botão Perspective (Perspectiva) no canto superior esquerdo. No menu que aparece, selecione Display Wireframe (Exibir Aramado).

Isso permitirá que você veja os triângulos que compõem o plano.

../../../_images/plane.webp

Agora defina o Subdivide Width (Subdividir Largura) e o Subdivide Depth (Subdividir Profundidade) do PlaneMesh para 32.

../../../_images/plane-sub-set.webp

Você pode ver que agora existem muito mais triângulos na MeshInstance3D. Isso nos dará mais vértices para trabalhar e, assim, nos permitirá adicionar mais detalhes.

../../../_images/plane-sub.webp

As PrimitiveMeshes, como o PlaneMesh, possuem apenas uma superfície, de modo que, em vez de uma matriz de materiais, existe apenas um. Defina o Material para um novo ShaderMaterial e, em seguida, expanda o material clicando na esfera que aparece.

Nota

Materiais que herdam do recurso Material, como o StandardMaterial3D e o ParticleProcessMaterial, podem ser convertidos em um ShaderMaterial e suas propriedades existentes serão convertidas em um shader de texto correspondente. Para fazer isso, clique com o botão direito no material na aba FileSystem (Sistema de Arquivos) e escolha Convert to ShaderMaterial (Converter para ShaderMaterial). Você também pode fazer isso clicando com o botão direito em qualquer propriedade que possua uma referência ao material no inspetor.

Agora defina o Shader do material para um novo Shader clicando em <empty> e selecionando New Shader... (Novo Shader...). Deixe as configurações padrão, dê um nome ao seu shader e clique em Create (Criar).

Clique no shader no inspetor e o editor de shader deverá aparecer. Você está pronto para começar a escrever seu primeiro shader Espacial!

Magia dos shaders

../../../_images/shader-editor.webp

O novo shader já é gerado com uma variável shader_type, a função vertex() e a função fragment(). A primeira coisa que os shaders do Godot precisam é de uma declaração de que tipo de shader eles são. Neste caso, o shader_type é definido como spatial porque este é um shader espacial.

shader_type spatial;

A função vertex() determina onde os vértices da sua MeshInstance3D aparecem na cena final. Nós a usaremos para deslocar a altura de cada vértice e fazer nosso plano liso parecer um pequeno terreno.

Sem nada na função vertex(), o Godot usará seu shader de vértice padrão. Podemos começar a fazer alterações adicionando uma única linha:

void vertex() {
  VERTEX.y += cos(VERTEX.x) * sin(VERTEX.z);
}

Adicionando esta linha, você deve obter uma imagem como a mostrada abaixo.

../../../_images/cos.webp

Ok, vamos destrinchar isso. O valor y do VERTEX está sendo aumentado. E estamos passando os componentes x e z de VERTEX como argumentos para cos() e sin(); isso nos dá uma aparência de onda ao longo dos eixos x e z.

O que queremos alcançar é a aparência de colinas pequenas; afinal, cos() e sin() já se parecem um pouco com colinas. Fazemos isso escalando as entradas para as funções cos() e sin().

void vertex() {
  VERTEX.y += cos(VERTEX.x * 4.0) * sin(VERTEX.z * 4.0);
}
../../../_images/cos4.webp

Isso parece melhor, mas ainda está muito espetado e repetitivo, vamos torná-lo um pouco mais interessante.

Mapa de altura de ruído (Noise heightmap)

O ruído (noise) é uma ferramenta muito popular para simular a aparência de terrenos. Pense nele como algo semelhante à função cosseno, onde você tem colinas repetidas, exceto que, com o ruído, cada colina tem uma altura diferente.

O Godot fornece o recurso NoiseTexture2D para gerar uma textura de ruído que pode ser acessada a partir de um shader.

Para acessar uma textura em um shader, adicione o seguinte código próximo ao topo do seu shader, fora da função vertex().

uniform sampler2D noise;

Isso permitirá que você envie uma textura de ruído para o shader. Agora olhe no inspetor sob o seu material. Você deverá ver uma seção chamada Shader Parameters (Parâmetros do Shader).

Defina este parâmetro Noise para uma nova NoiseTexture2D. Em seguida, na sua NoiseTexture2D, defina a propriedade Noise para um novo FastNoiseLite. A classe FastNoiseLite é usada pela NoiseTexture2D para gerar um mapa de altura.

Depois de configurá-lo e deve ficar assim.

../../../_images/noise-set.webp

Agora, acesse a textura de ruído usando a função texture():

void vertex() {
  float height = texture(noise, VERTEX.xz / 2.0 + 0.5).x;
  VERTEX.y += height;
}

A função texture() recebe uma textura como primeiro argumento e um vec2 para a posição na textura como segundo argumento. Usamos os canais x e z de VERTEX para determinar onde na textura procurar.

Como as coordenadas do PlaneMesh estão dentro do intervalo [-1.0, 1.0] (para um tamanho de 2.0), enquanto as coordenadas da textura estão dentro de [0.0, 1.0], para remapear as coordenadas nós dividimos pelo tamanho do PlaneMesh por 2.0 e adicionamos 0.5.

A função texture() retorna um vec4 dos canais r, g, b, a na posição. Como a textura de ruído é em tons de cinza, todos os valores são iguais, de modo que podemos usar qualquer um dos canais como a altura. Neste caso, usaremos o canal r, ou x.

Nota

xyzw é o mesmo que rgba em GLSL, portanto, em vez de texture().x acima, poderíamos usar texture().r. Veja a documentação do OpenGL para mais detalhes.

Usando este código, você pode ver que a textura cria colinas com aparência aleatória.

../../../_images/noise.webp

No momento está muito espetado, queremos suavizar um pouco as colinas. Para fazer isso, usaremos um uniform. Você já usou um uniform acima para passar a textura de ruído, agora vamos aprender como eles funcionam.

Uniforms

As variáveis uniform permitem que você passe dados do jogo para o shader. Elas são muito úteis para controlar efeitos de shader. Uniforms podem ser de quase qualquer tipo de dado que possa ser usado no shader. Para usar um uniform, você o declara no seu Shader usando a palavra-chave uniform.

Vamos criar um uniform que altera a altura do terreno.

uniform float height_scale = 0.5;

O Godot permite que você inicialize um uniform com um valor; aqui, height_scale está definido como 0.5. Você pode definir uniforms a partir do GDScript chamando a função set_shader_parameter() no material correspondente ao shader. O valor passado do GDScript tem precedência sobre o valor usado para inicializá-lo no shader.

# called from the MeshInstance3D
mesh.material.set_shader_parameter("height_scale", 0.5)

Nota

Alterar uniforms em nós baseados em Spatial é diferente de nós baseados em CanvasItem. Aqui, definimos o material dentro do recurso PlaneMesh. Em outros recursos de malha, você pode precisar primeiro acessar o material chamando surface_get_material(). Enquanto na MeshInstance3D você acessaria o material usando get_surface_material() ou material_override.

Lembre-se de que a string passada para set_shader_parameter() deve corresponder exatamente ao nome da variável uniform no shader. Você pode usar a variável uniform em qualquer lugar dentro do seu shader. Aqui, usaremos ela para definir o valor da altura em vez de multiplicar arbitrariamente por 0.5.

VERTEX.y += height * height_scale;

Agora parece muito melhor.

../../../_images/noise-low.webp

Usando uniforms, podemos até alterar o valor a cada quadro para animar a altura do terreno. Combinado com Tweens, isso pode ser especialmente útil para animações.

Interagindo com a luz

Primeiro, desative o aramado (wireframe). Para fazer isso, abra o menu Perspective no canto superior esquerdo da viewport novamente e selecione Display Normal (Exibir Normal). Além disso, na barra de ferramentas da cena 3D, desative a pré-visualização da luz solar (sunlight).

../../../_images/normal.webp

Note como a cor da malha fica plana. Isso ocorre porque a iluminação nela está plana. Vamos adicionar uma luz!

Primeiro, adicionaremos uma OmniLight3D à cena e a arrastaremos para cima, para que fique acima do terreno.

../../../_images/light.webp

Você pode ver a luz afetando o terreno, mas parece estranho. O problema é que a luz está afetando o terreno como se ele fosse um plano liso. Isso ocorre porque o shader de luz usa as normais da Mesh para calcular a luz.

As normais são armazenadas na Mesh, mas estamos alterando o formato da Mesh no shader, então as normais não estão mais corretas. Para consertar isso, podemos recalcular as normais no shader ou usar uma textura de normal que corresponda ao nosso ruído. O Godot facilita ambos para nós.

Você pode calcular a nova normal manualmente na função vertex e depois apenas definir NORMAL. Com a NORMAL definida, o Godot fará todos os cálculos complexos de iluminação por nós. Abordaremos esse método na próxima parte deste tutorial; por enquanto, leremos as normais de uma textura.

Em vez disso, confiaremos na NoiseTexture novamente para calcular as normais para nós. Fazemos isso passando uma segunda textura de ruído.

uniform sampler2D normalmap;

Defina este segundo uniform de textura para outra NoiseTexture2D com outro FastNoiseLite. Mas desta vez, marque a opção As Normal Map (Como Mapa de Normais).

../../../_images/normal-set.webp

Quando temos normais que correspondem a um vértice específico, definimos NORMAL, mas se você tiver um mapa de normais que vem de uma textura, defina a normal usando NORMAL_MAP na função fragment(). Dessa forma, o Godot cuidará de mapear a textura ao redor da malha automaticamente.

Por fim, para garantir que estamos lendo dos mesmos lugares na textura de ruído e na textura do mapa de normais, vamos passar a posição VERTEX.xz da função vertex() para a função fragment(). Fazemos isso usando uma varying.

Acima da função vertex(), defina uma varying vec2 chamada tex_position. E dentro da função vertex() atribua VERTEX.xz para tex_position.

varying vec2 tex_position;

void vertex() {
  tex_position = VERTEX.xz / 2.0 + 0.5;
  float height = texture(noise, tex_position).x;
  VERTEX.y += height * height_scale;
}

E agora podemos acessar tex_position a partir da função fragment().

void fragment() {
  NORMAL_MAP = texture(normalmap, tex_position).xyz;
}

Com as normais no lugar, a luz agora reage à altura da malha dinamicamente.

../../../_images/normalmap.webp

Podemos até arrastar a luz ao redor e a iluminação será atualizada automaticamente.

../../../_images/normalmap2.webp

Código completo

Aqui está o código completo para este tutorial. Você pode ver que ele não é muito longo, já que o Godot cuida da maior parte da parte difícil para você.

shader_type spatial;

uniform float height_scale = 0.5;
uniform sampler2D noise;
uniform sampler2D normalmap;

varying vec2 tex_position;

void vertex() {
  tex_position = VERTEX.xz / 2.0 + 0.5;
  float height = texture(noise, tex_position).x;
  VERTEX.y += height * height_scale;
}

void fragment() {
  NORMAL_MAP = texture(normalmap, tex_position).xyz;
}

Isso é tudo para esta parte. Esperamos que agora você entenda os conceitos básicos de shaders de vértice no Godot. Na próxima parte deste tutorial, escreveremos uma função fragment para acompanhar esta função vertex e abordaremos uma técnica mais avançada para transformar este terreno em um oceano de ondas em movimento.