WebGL ortográfico 3D

Gregg Tavares
Gregg Tavares

3D ortográfico WebGL

Esta postagem é a continuação de uma série sobre o WebGL. O primeiro começou com os conceitos básicos e o anterior foi sobre matrizes 2D sobre matrizes 2D. Se ainda não tiver lido, veja-os primeiro. Na última postagem, vimos como as matrizes 2d funcionavam. Falamos sobre translação, rotação, dimensionamento e até mesmo projeção de pixels no espaço de corte. Tudo isso pode ser feito usando uma matriz 1 e alguns cálculos de matriz mágica. Fazer 3D é apenas um pequeno passo a partir daí. Nos exemplos 2D anteriores, tivemos pontos 2D (x, y) que multiplicamos por uma matriz 3x3. Para criar elementos em 3D, precisamos de pontos 3D (x, y, z) e de uma matriz 4x4. Vamos pegar nosso último exemplo e alterá-lo para 3D. Usaremos um F novamente, mas desta vez um F 3D. A primeira coisa que precisamos fazer é mudar o sombreador de vértice para processar 3D. Este é o sombreador antigo.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform mat3 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
</script>

Aqui está o novo

<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;

uniform mat4 u_matrix;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
}
</script>

Ficou ainda mais simples! Em seguida, precisamos fornecer dados 3D.

...

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);

...

// Fill the buffer with the values that define a letter 'F'.
function setGeometry(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
        // left column
        0,   0,  0,
        30,   0,  0,
        0, 150,  0,
        0, 150,  0,
        30,   0,  0,
        30, 150,  0,

        // top rung
        30,   0,  0,
        100,   0,  0,
        30,  30,  0,
        30,  30,  0,
        100,   0,  0,
        100,  30,  0,

        // middle rung
        30,  60,  0,
        67,  60,  0,
        30,  90,  0,
        30,  90,  0,
        67,  60,  0,
        67,  90,  0]),
    gl.STATIC_DRAW);
}

Depois, precisamos mudar todas as funções de matriz de 2D para 3D Estas são as versões 2D (antes) de makeTranslation, makeRotação e makeScale

function makeTranslation(tx, ty) {
return [
1, 0, 0,
0, 1, 0,
tx, ty, 1
];
}

function makeRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c,-s, 0,
s, c, 0,
0, 0, 1
];
}

function makeScale(sx, sy) {
return [
sx, 0, 0,
0, sy, 0,
0, 0, 1
];
}

Estas são as versões atualizadas do 3D.

function makeTranslation(tx, ty, tz) {
return [
    1,  0,  0,  0,
    0,  1,  0,  0,
    0,  0,  1,  0,
    tx, ty, tz, 1
];
}

function makeXRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
};

function makeYRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);

return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
];
};

function makeZRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
    c, s, 0, 0,
-s, c, 0, 0,
    0, 0, 1, 0,
    0, 0, 0, 1,
];
}

function makeScale(sx, sy, sz) {
return [
sx, 0,  0,  0,
0, sy,  0,  0,
0,  0, sz,  0,
0,  0,  0,  1,
];
}

Agora temos três funções de rotação. Só precisávamos de uma em 2D, pois estávamos girando apenas em torno do eixo Z. Para fazer o 3D, também queremos girar em torno do eixo X e do eixo Y. Observe que eles são muito parecidos.

Rotação Z

newX = x * c + y * s;
newY = x * -s + y * c;

Rotação Y


newX = x * c + z * s;
newZ = x * -s + z * c;

Rotação X

newY = y * c + z * s;
newZ = y * -s + z * c;

Também precisamos atualizar a função de projeção. Aqui está o antigo

function make2DProjection(width, height) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
}

que converteu de pixels para espaço de corte. Em nossa primeira tentativa de expandi-lo para 3D, vamos tentar

function make2DProjection(width, height, depth) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
    2 / width, 0, 0, 0,
    0, -2 / height, 0, 0,
    0, 0, 2 / depth, 0,
-1, 1, 0, 1,
];
}

Assim como precisávamos converter de pixels para clipspace para x e y, para z, precisamos fazer a mesma coisa. Nesse caso, também estou criando unidades de pixel com espaço Z. Vou transmitir um valor semelhante a width para a profundidade, de modo que o espaço será de 0 a largura de pixels de largura, de 0 a pixels de altura, mas para profundidade, será -profundidade / 2 a +profundidade / 2. Por fim, precisamos atualizar o código que calcula a matriz.

// Compute the matrices
var projectionMatrix =
    make2DProjection(canvas.width, canvas.height, canvas.width);
var translationMatrix =
    makeTranslation(translation[0], translation[1], translation[2]);
var rotationXMatrix = makeXRotation(rotation[0]);
var rotationYMatrix = makeYRotation(rotation[1]);
var rotationZMatrix = makeZRotation(rotation[2]);
var scaleMatrix = makeScale(scale[0], scale[1], scale[2]);

// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationZMatrix);
matrix = matrixMultiply(matrix, rotationYMatrix);
matrix = matrixMultiply(matrix, rotationXMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
matrix = matrixMultiply(matrix, projectionMatrix);

// Set the matrix.
gl.uniformMatrix4fv(matrixLocation, false, matrix);

O primeiro problema que temos é que nossa geometria é um F plano, o que dificulta a visualização de um 3D. Para corrigir isso, vamos expandir a geometria para 3D. Nosso F atual é composto por três retângulos, dois triângulos cada. Para torná-lo 3D, são necessários um total de 16 retângulos. São algumas para listar aqui. 16 retângulos x 2 triângulos por retângulo x 3 vértices por triângulo equivalem a 96 vértices. Se você quiser ver todos, acesse o código-fonte na amostra. Temos que desenhar mais vértices,

// Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);

Ao mover os controles deslizantes, é muito difícil dizer que é 3D. Vamos tentar colorir cada retângulo com uma cor diferente. Para fazer isso, vamos adicionar outro atributo ao sombreador de vértice e uma variável para transmiti-lo do sombreador de vértice para o de fragmento. Este é o novo sombreador de vértice

<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
attribute vec4 a_color;

uniform mat4 u_matrix;

varying vec4 v_color;

void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;

// Pass the color to the fragment shader.
v_color = a_color;
}
</script>

Precisamos usar essa cor no sombreador de fragmento

<script id="3d-vertex-shader" type="x-shader/x-fragment">
precision mediump float;

// Passed in from the vertex shader.
varying vec4 v_color;

void main() {
gl_FragColor = v_color;
}
</script>

Precisamos procurar o local para fornecer as cores e configurar outro buffer e atributo para fornecer as cores.

...
var colorLocation = gl.getAttribLocation(program, "a_color");

...
// Create a buffer for colors.
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(colorLocation);

// We'll supply RGB as bytes.
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

// Set Colors.
setColors(gl);

...
// Fill the buffer with colors for the 'F'.

function setColors(gl) {
gl.bufferData(
    gl.ARRAY_BUFFER,
    new Uint8Array([
        // left column front
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,
    200,  70, 120,

        // top rung front
    200,  70, 120,
    200,  70, 120,
    ...
    ...
    gl.STATIC_DRAW);
}

Ah, não! Acontece que todas as várias partes do 3D "F", da frente, do verso, das laterais etc. são desenhadas na ordem em que aparecem na nossa geometria. Isso não nos dá os resultados desejados, porque às vezes os que estão na parte de trás são desenhados depois dos que estão na frente. Triângulos no WebGL têm o conceito de frente e de trás. Um triângulo voltado à frente tem os vértices no sentido horário. Um triângulo voltado para trás tem os vértices no sentido anti-horário.

Triângulo de curva.

O WebGL tem a capacidade de desenhar apenas triângulos voltados para a frente ou para trás. Podemos ativar esse recurso

gl.enable(gl.CULL_FACE);

o que fazemos apenas uma vez, logo no início do programa. Com esse recurso ativado, o WebGL usa como padrão os triângulos voltados para trás. "Culling", neste caso, é uma palavra chique para "não desenhar". Observe que, na medida em que o WebGL é considerado, se um triângulo é considerado ou não no sentido horário ou anti-horário depende dos vértices desse triângulo no clipspace. Em outras palavras, o WebGL descobre se um triângulo está em frente ou em volta DEPOIS de aplicar os cálculos aos vértices no sombreador de vértice. Isso significa, por exemplo, que um triângulo no sentido horário dimensionado em X por -1 se torna um triângulo no sentido anti-horário ou um triângulo no sentido horário girado 180 graus ao redor do eixo X ou Y se torna um triângulo no sentido anti-horário. Como o CULL_FACE foi desativado, podemos ver triângulos no sentido horário(frontal) e anti-horário(verso). Agora que o recurso foi ativado, todas as vezes que um triângulo virado para frente for girado por causa de escalonamento ou rotação ou por qualquer motivo, o WebGL não vai desenhá-lo. Isso é bom, já que, ao virar algo em 3D, geralmente, os triângulos que estão virados para você serão considerados de frente.

Oi! Para onde foram todos os triângulos? Acontece que muitos deles estão encarando o lado errado. Gire-o e você os verá aparecendo ao olhar para o outro lado. Felizmente, isso é fácil de corrigir. Vamos apenas observar quais deles estão para trás e trocar dois dos vértices deles. Por exemplo, se um triângulo para trás for

1,   2,   3,
40,  50,  60,
700, 800, 900,

basta virar os últimos dois vértices para avançar.

1,   2,   3,
700, 800, 900,
40,  50,  60,

Já temos mais um problema, mas ainda há mais um problema. Mesmo com todos os triângulos voltados para a direção correta e com os que estão voltados para trás sendo selecionados, ainda há lugares em que os triângulos que deveriam estar atrás estão sendo desenhados sobre triângulos que precisam estar na frente. Insira o DEPTH BUFFER. Um buffer de profundidade, às vezes chamado de Z-Buffer, é um retângulo de depth pixels, um pixel de profundidade para cada pixel de cor usado para criar a imagem. À medida que o WebGL desenha cada pixel de cor, ele também pode renderizar um pixel de profundidade. Ele faz isso com base nos valores que retornamos do sombreador de vértice para Z. Assim como tivemos que converter em espaço de corte para X e Y, então Z está no espaço de corte ou (-1 para +1). Em seguida, esse valor é convertido em um valor de profundidade de espaço (0 a +1). Antes de o WebGL renderizar um pixel de cor, ele verificará o pixel de profundidade correspondente. Se o valor de profundidade do pixel que está prestes a ser desenhado for maior que o valor do pixel de profundidade correspondente, o WebGL não renderizará o novo pixel de cor. Caso contrário, o novo pixel de cor será renderizado com a cor do sombreador de fragmento E o pixel de profundidade com o novo valor de profundidade. Isso significa que os pixels que estão atrás de outros pixels não serão desenhados. É possível ativar esse recurso de forma tão simples quanto ativamos a seleção com

gl.enable(gl.DEPTH_TEST);

Também precisamos remover o buffer de profundidade para 1.0 antes de começar a desenhar.

// Draw the scene.
function drawScene() {
// Clear the canvas AND the depth buffer.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
...

Na próxima postagem, mostrarei como dar perspectiva a tudo.

Por que o atributo vec4 é 3, mas o tamanho de gl.vertexAttribPointer é 3

Para quem entende de detalhes, já deve ter notado que definimos nossos dois atributos como

attribute vec4 a_position;
attribute vec4 a_color;

ambos são "vec4", mas quando informamos à WebGL como extrair dados dos buffers, usamos

gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);

Esse "3" em cada um indica a extração de apenas três valores por atributo. Isso funciona porque, no sombreador de vértice, a WebGL fornece padrões para aqueles que você não fornece. Os padrões são 0, 0, 0, 1, em que x = 0, y = 0, z = 0 e w = 1. É por isso que, no antigo sombreador de vértice 2D, tivemos que fornecer explicitamente o 1. Estávamos passando x e y e precisávamos de 1 para z, mas como o padrão para z é 0, tivemos que fornecer 1 explicitamente. No entanto, para 3D, mesmo que não forneçamos um "w", o padrão é 1, que é o que precisamos para que o matemático da matriz funcione.