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 matrizUma grade de números (mat2, mat3, mat4) que leva um vetor a outro numa única multiplicação. Rotação, escala e, com coordenadas homogêneas, translação e projeção são todas matrizes. Matriz Uma grade de números (mat2, mat3, mat4) que leva um vetor a outro numa única multiplicação. Rotação, escala e, com coordenadas homogêneas, translação e projeção são todas matrizes. Espaço de coordenadas O referencial em que uma posição ou direção é medida, como o espaço do objeto, do mundo, da câmera, de recorte ou da tela. Só dá para combinar valores do mesmo espaço.rotate(a) = [cos a −sin a; sin a cos a]
Por que 4×4
Uma Coordenadas homogêneas Um ponto 3D escrito com um quarto componente w. Com w = 1, uma matriz 4×4 consegue transladá-lo; com w = 0, ele é uma direção que ignora a translação. Dividir por w desfaz isso, e é assim que a projeção em perspectiva funciona.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êneasUm ponto 3D escrito com um quarto componente w. Com w = 1, uma matriz 4×4 consegue transladá-lo; com w = 0, ele é uma direção que ignora a translação. Dividir por w desfaz isso, e é assim que a projeção em perspectiva funciona.(x, y, z, w) → (x/w, y/w, z/w)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
wa distância à frente da câmera; uma ortográfica deixawem 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.