O que são shaders?

Introdução

Então, você decidiu experimentar os shaders. Você provavelmente já ouviu falar que eles podem ser usados para criar efeitos interessantes que são executados de forma incrivelmente rápida. Você também deve ter ouvido falar que eles são assustadores. Ambos são verdadeiros.

Shaders podem ser usados para criar uma ampla gama de efeitos (na verdade, tudo desenhado em um mecanismo de renderização moderno é feito com shaders).

Escrever shaders também pode ser muito difícil para pessoas não familiarizadas com eles. O Godot tenta tornar a escrita de shaders um pouco mais fácil, expondo muitos recursos integrados úteis e lidando com parte do trabalho de inicialização de nível inferior para você. No entanto, GLSL (a Linguagem de Shading do OpenGL, que o Godot usa) ainda não é intuitiva e é restritiva, especialmente para usuários que estão acostumados com GDScript.

Mas o que são eles?

Shaders são um tipo especial de programa executado em Unidades de Processamento Gráfico (GPUs). A maioria dos computadores tem algum tipo de GPU, seja integrado em sua CPU ou discreto (o que significa que é um componente de hardware separado, por exemplo, a típica placa de vídeo). As GPUs são especialmente úteis para renderização porque são otimizadas para executar milhares de instruções em paralelo.

A saída do shader normalmente são os pixels coloridos do objeto desenhado na janela de exibição. Mas alguns shaders permitem saídas especializadas (isso é especialmente verdadeiro para APIs como Vulkan). Shaders operam dentro da pipeline de shaders. O processo padrão é a pipeline de shader vertex -> fragment. O shader de vértice é usado para decidir onde cada vértice (ponto em um modelo 3D ou canto de um Sprite) vai e o shader de fragmento decide qual cor os pixels individuais recebem.

Suponha que você queira atualizar todos os pixels em uma textura para uma determinada cor, na CPU você escreveria:

for x in range(width):
  for y in range(height):
    set_color(x, y, some_color)

Em um shader você tem acesso apenas ao interior do loop, de modo que o que você escreve se parece com isto:

// function called for each pixel
void fragment() {
  COLOR = some_color;
}

Você não tem controle sobre como esta função é chamada. Portanto, você deve projetar seus shaders de maneira diferente de como projetaria programas na CPU.

Uma consequência da pipeline do shader é que você não pode acessar os resultados de uma execução anterior do shader, não pode acessar outros pixels do pixel que está sendo desenhado e não pode escrever fora do pixel atual que está sendo desenhado. Isto permite que a GPU execute o shader para pixels diferentes em paralelo, pois eles não dependem um do outro. Essa falta de flexibilidade foi projetada para funcionar com a GPU, o que permite que os shaders sejam incrivelmente rápidos.

What can they do

  • position vertices very fast

  • compute color very fast

  • compute lighting very fast

  • lots and lots of math

What can't they do

  • draw outside mesh

  • access other pixels from current pixel (or vertices)

  • store previous iterations

  • update on the fly (they can, but they need to be compiled)

Structure of a shader

In Godot, shaders are made up of 3 main functions: the vertex() function, the fragment() function and the light() function.

The vertex() function runs over all the vertices in the mesh and sets their positions as well as some other per-vertex variables.

The fragment() function runs for every pixel that is covered by the mesh. It uses the variables from the vertex() function to run. The variables from the vertex() function are interpolated between the vertices to provide the values for the fragment() function.

The light() function runs for every pixel and for every light. It takes variables from the fragment() function and from previous runs of itself.

Para mais informações sobre como os shaders operam especificamente no Godot, consulte o documento Shaders.

Aviso

The light() function won't be run if the vertex_lighting render mode is enabled, or if Rendering > Quality > Shading > Force Vertex Shading is enabled in the Project Settings. (It's enabled by default on mobile platforms.)

Technical overview

GPUs are able to render graphics much faster than CPUs for a few reasons, but most notably, because they are able to run calculations massively in parallel. A CPU typically has 4 or 8 cores while a GPU typically has thousands. That means a GPU can do hundreds of tasks at once. GPU architects have exploited this in a way that allows for doing many calculations very quickly, but only when many or all cores are doing the same calculation at once, but with different data.

That is where shaders come in. The GPU will call the shader a bunch of times simultaneously, and then operate on different bits of data (vertices, or pixels). These bunches of data are often called wavefronts. A shader will run the same for every thread in the wavefront. For example, if a given GPU can handle 100 threads per wavefront, a wavefront will run on a 10×10 block of pixels together. It will continue to run for all pixels in that wavefront until they are complete. Accordingly, if you have one pixel slower than the rest (due to excessive branching), the entire block will be slowed down, resulting in massively slower render times.

This is different from CPU-based operations. On a CPU, if you can speed up even one pixel, the entire rendering time will decrease. On a GPU, you have to speed up the entire wavefront to speed up rendering.