Pular para o conteúdo
Fundamento · TransformaçõesIntermediário

Matrizes e MVP

Matrizes 4×4, coordenadas homogêneas e a cadeia de modelo, visão e projeção que leva um vértice até a tela.

Como funciona

Uma malha guarda os vértices em relação à própria origem, do jeito que o artista a modelou. Para desenhá-la, cada vértice precisa acabar num pixel: posicionado no mundo, visto pela câmera, projetado na tela. Cada um desses passos é uma matriz, e o produto delas é a matriz MVP (modelo, visão, projeção, das iniciais em inglês) que quase todo shader de vértice aplica. É a versão 3D das transformações 2D: a mesma ideia de levar coordenadas de um espaço para outro, com uma dimensão a mais e uma projeção no final.

Por que 4×4

Uma mat3 consegue girar e escalar um vetor 3D, mas não consegue transladá-lo: multiplicar o vetor nulo por qualquer matriz dá zero, então a origem nunca sai do lugar. A solução são as coordenadas homogêneas: escreva um ponto como vec4(x, y, z, 1.0) e coloque a translação na quarta coluna da matriz. A multiplicação soma a translação vezes esse 1:

mat4 translate(vec3 t) {
    return mat4(1.0, 0.0, 0.0, 0.0,   // coluna 0
                0.0, 1.0, 0.0, 0.0,   // coluna 1
                0.0, 0.0, 1.0, 0.0,   // coluna 2
                t.x, t.y, t.z, 1.0);  // coluna 3: a translação
}

Como na mat2, o GLSL preenche as matrizes coluna por coluna, então a translação fica no último grupo de quatro números, e m[3] lê essa coluna de volta.

O quarto componente também separa pontos de direções. Um ponto tem w = 1 e é movido pela translação. Uma direção (uma normal, a direção da luz, uma velocidade) tem w = 0, então a coluna de translação é multiplicada por zero e a direção só gira e escala. É o comportamento certo: mover um objeto não deveria mudar para onde a superfície dele aponta.

A cadeia

  • Modelo leva o vértice do espaço do objeto para o espaço do mundo: a escala, a rotação e a posição do objeto na cena. Cada objeto tem a sua.
  • Visão leva o espaço do mundo para o espaço de visão, em que a câmera fica na origem olhando ao longo de um eixo (−z no OpenGL e no WebGL). Ela é a inversa da matriz de modelo da própria câmera: mover a câmera para a direita é o mesmo que mover o mundo inteiro para a esquerda, a mesma inversão de mover o espaço em vez da forma no 2D.
  • Projeção leva o espaço de visão para o espaço de recorte (clip space). Uma projeção em perspectiva copia para w a distância à frente da câmera; uma ortográfica deixa w em 1.

As matrizes se aplicam da direita para a esquerda, então a mais perto do vetor age primeiro:

// shader de vértice
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
in vec3 aPosition;

void main() {
    vec4 world = uModel * vec4(aPosition, 1.0);
    // modelo primeiro, depois visão, depois projeção
    gl_Position = uProjection * uView * world;
}

O produto de matrizes é associativo, mas não comutativo: P * V * M pode ser agrupado como (P * V) * M e calculado uma vez, mas a ordem das três não pode mudar. Os programas costumam multiplicá-las na CPU, uma vez por objeto, e mandar uma matriz só, em vez de fazer duas multiplicações 4×4 a mais em cada vértice.

Espaço de recorte e a divisão

gl_Position fica no espaço de recorte, ainda com quatro dimensões. A GPU recorta os triângulos ao volume visível, em que x e y ficam entre −w e w, e z entre −w e w no OpenGL e no WebGL (entre 0 e w no Direct3D, no Vulkan e no Metal), e depois divide por w. O resultado são as coordenadas normalizadas de dispositivo (NDC): de −1 a 1 em x e y ao longo da viewport. Como uma matriz de perspectiva colocou a distância em w, essa divisão encolhe as coisas distantes em direção ao centro, e isso é a perspectiva. A transformação de viewport então leva as NDC a pixels, o gl_FragCoord que um shader de fragmento lê.

As convenções de profundidade variam. OpenGL e WebGL levam o z das NDC a −1..1; Direct3D, Vulkan e Metal usam 0..1, e muitas engines invertem isso (1 perto, 0 longe) para ganhar precisão. Código que lê a profundidade de volta precisa saber qual delas está recebendo.

Inversas

Voltar é multiplicar pela inversa. Desprojetar um pixel até um ponto no espaço do mundo, para um raio saindo da câmera ou para reconstruir uma posição a partir do buffer de profundidade, usa a inversa de P * V e depois divide por w:

// ndc: x, y e profundidade em −1..1
vec4 h = inverse(uProjection * uView) * vec4(ndc, 1.0);
vec3 worldPos = h.xyz / h.w;

O GLSL ES 3.00 tem inverse(), mas ela é cara; calcule as inversas na CPU e passe-as prontas sempre que puder.

As normais pedem cuidado. Com uma escala não uniforme (digamos 2 em x e 1 em y), transformar uma normal pela matriz de modelo a inclina, e ela deixa de ser perpendicular à superfície. A matriz certa para normais é a inversa transposta do bloco 3×3 superior da matriz de modelo, muitas vezes chamada de matriz normal:

mat3 normalMatrix = transpose(inverse(mat3(uModel))); // melhor calcular uma vez na CPU
vec3 n = normalize(normalMatrix * aNormal);

Com apenas rotação e escala uniforme, mat3(uModel) também funciona, seguida de normalize.

Nas engines e neste site

As engines montam essas matrizes para você e as expõem por nome: os shaders spatial do Godot têm MODEL_MATRIX, VIEW_MATRIX e PROJECTION_MATRIX, a Unity tem UNITY_MATRIX_MVP e companhia, e o three.js passa modelMatrix, viewMatrix e projectionMatrix. Os cards deste site entregam ao effect.glsl posições e normais já no espaço do mundo, então o código de um card raramente mexe na cadeia. Ela importa quando você move vértices, lê a profundidade ou porta um shader entre engines que diferem na orientação dos eixos e na faixa de profundidade.

Leitura complementar