3D ortografico WebGL

Gregg Tavares
Gregg Tavares

3D ortogonale WebGL

Questo post è la continuazione di una serie di post su WebGL. Il primo iniziava con i principi fondamentali e il precedente riguardava le matrici 2D sulle matrici 2D. Se non li hai letti, consultali prima. Nell'ultimo post abbiamo esaminato il funzionamento delle matrici 2D. Abbiamo parlato di come la traduzione, la rotazione, la scalatura e persino la proiezione dai pixel allo spazio clip possono essere eseguite con una matrice e alcune operazioni matematiche magiche sulle matrici. Passare al 3D è solo un piccolo passo. Nei nostri esempi 2D precedenti avevamo punti 2D (x, y) che abbiamo moltiplicato per una matrice 3x3. Per il 3D abbiamo bisogno di punti 3D (x, y, z) e di una matrice 4x4. Prendiamo l'ultimo esempio e trasformiamolo in 3D. Useremo di nuovo una F, ma questa volta una F 3D. La prima cosa da fare è modificare lo shader vertex in modo che gestisca il 3D. Ecco il vecchio shader.

<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>

Ed ecco il nuovo

<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>

È diventato ancora più semplice! Poi dobbiamo fornire dati 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);
}

A questo punto dobbiamo cambiare tutte le funzioni della matrice da 2D a 3D. Ecco le versioni 2D (precedenti) di makeTranslation, makeRotation 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
];
}

Ed ecco le versioni 3D aggiornate.

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,
];
}

Ora abbiamo tre funzioni di rotazione. Ne serviva solo uno in 2D perché giravamo solo attorno all'asse Z. Ora, però, per fare il 3D vogliamo anche poter ruotare attorno all'asse x e all'asse y. Puoi vedere che sono tutti molto simili. Se li calcolassimo, vedresti che si semplificano come prima

Rotazione Z

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

Rotazione Y


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

Rotazione X

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

Dobbiamo anche aggiornare la funzione di proiezione. Ecco l'immagine precedente

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
];
}

che è stata convertita da pixel a spazio del clip. Per il nostro primo tentativo di espansione in 3D, proviamo

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,
];
}

Proprio come abbiamo dovuto convertire da pixel a spazio clip per x e y, dobbiamo fare lo stesso per z. In questo caso, sto creando anche le unità di pixel dello spazio Z. Passerò un valore simile a width per la profondità, quindi il nostro spazio avrà una larghezza da 0 a larghezza pixel, un'altezza da 0 a altezza pixel, ma per la profondità sarà da -profondità / 2 a +profondità / 2. Infine, dobbiamo aggiornare il codice che calcola la matrice.

// 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);

Il primo problema è che la nostra geometria è una F piatta, il che rende difficile vedere il 3D. Per risolvere il problema, espande la geometria in 3D. La nostra F attuale è composta da 3 rettangoli, ciascuno con 2 triangoli. Per renderlo 3D, sarà necessario un totale di 16 rettangoli. Sono un bel po' da elencare. 16 rettangoli x 2 triangoli per rettangolo x 3 vertici per triangolo = 96 vertici. Se vuoi visualizzarli tutti, visualizza il codice sorgente del Sample. Dobbiamo disegnare più vertici, quindi

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

È abbastanza difficile capire che si tratta di un'immagine 3D se si spostano i cursori. Proviamo a colorare ogni rettangolo con un colore diverso. Per farlo, aggiungeremo un altro attributo al nostro shader vertex e un parametro per trasmetterlo dall'shader vertex allo shader fragment. Ecco il nuovo shader vertex

<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>

E dobbiamo usare questo colore nello shader del frammento

<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>

Dobbiamo cercare la posizione per fornire i colori, quindi configurare un altro buffer e un altro attributo per assegnare i colori.

...
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);
}

Uh oh, che pasticcio. Bene, risulta che tutte le varie parti della "F" 3D, anteriore, posteriore, laterali e così via, vengono disegnate nell'ordine in cui appaiono nella nostra geometria. Questo non ci dà i risultati desiderati, perché a volte quelli in fondo vengono disegnati dopo quelli in primo piano. I triangoli in WebGL hanno il concetto di fronte e retro. Un triangolo rivolto verso l'esterno ha i vertici in senso orario. Un triangolo rivolto all'indietro ha i vertici in senso antiorario.

Avvolgimento a triangolo.

WebGL è in grado di disegnare solo triangoli rivolti in avanti o all'indietro. Possiamo attivare questa funzionalità con

gl.enable(gl.CULL_FACE);

che viene eseguita una sola volta, all'inizio del programma. Con questa funzionalità attivata, WebGL "elimina" per impostazione predefinita i triangoli rivolti all'indietro. In questo caso, "eliminazione" è una parola sofisticata per "non disegnare". Tieni presente che, per quanto riguarda WebGL, il fatto che un triangolo sia considerato in senso orario o antiorario dipende dai vertici del triangolo nello spazio clip. In altre parole, WebGL scopre se un triangolo è anteriore o posteriore DOPO che hai applicato la matematica ai vertici nello shader vertex. Ciò significa che, ad esempio, un triangolo in senso orario con una scala in X di -1 diventa un triangolo antiorario o un triangolo in senso orario ruotato di 180 gradi attorno all'asse X o Y diventa un triangolo antiorario. Poiché CULL_FACE è stato disattivato, possiamo vedere i triangoli sia in senso orario(anteriore) che antiorario(posteriore). Ora che abbiamo attivato questa opzione, ogni volta che un triangolo rivolto verso l'esterno si gira a causa di ridimensionamento o rotazione o per qualsiasi altro motivo, WebGL non lo disegna. È un bene, perché quando ruoti qualcosa in 3D, solitamente vuoi che i triangoli rivolti verso di te siano considerati anteriori.

Ehi! Dove sono finiti tutti i triangoli? Abbiamo scoperto che molti di loro si stanno muovendo nella direzione sbagliata. Ruotalo e vedrai che appaiono quando guardi dall'altro lato. Per fortuna, si rimedia facilmente. Basta guardare quali sono in ordine inverso e scambiare 2 dei loro vertici. Ad esempio, se un triangolo rivolto verso il basso è

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

Basta capovolgere gli ultimi 2 vertici per farli andare in avanti.

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

Ci sei quasi, ma c'è ancora un problema. Anche se tutti i vertici sono rivolti nella direzione corretta e quelli rivolti all'indietro vengono eliminati, ci sono ancora punti in cui i vertici che dovrebbero essere in primo piano vengono disegnati sopra quelli che dovrebbero essere in secondo piano. Inserisci il BUFFER PROFONDO. Un buffer di profondità, a volte chiamato buffer Z, è un rettangolo di depth pixel, un pixel di profondità per ogni pixel di colore utilizzato per creare l'immagine. Poiché WebGL disegna ogni pixel di colore, può anche disegnare un pixel di profondità. Lo fa in base ai valori restituiti dallo shader vertex per Z. Proprio come abbiamo dovuto convertire in spazio clip per X e Y, anche Z è nello spazio clip o (da -1 a +1). Questo valore viene poi convertito in un valore dello spazio di profondità (da 0 a +1). Prima che WebGL disegni un pixel di colore, controlla il pixel di profondità corrispondente. Se il valore di profondità del pixel che sta per essere disegnato è maggiore del valore del pixel di profondità corrispondente, WebGL non disegna il nuovo pixel di colore. In caso contrario, disegna sia il nuovo pixel di colore con il colore dello shader di frammento sia il pixel di profondità con il nuovo valore di profondità. Ciò significa che i pixel che si trovano dietro altri pixel non verranno disegnati. Possiamo attivare questa funzionalità quasi con la stessa semplicità con cui abbiamo attivato il culling con

gl.enable(gl.DEPTH_TEST);

Prima di iniziare a disegnare, dobbiamo anche ripristinare il buffer di profondità su 1.0.

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

Nel prossimo post spiegherò come dare prospettiva.

Perché l'attributo è vec4, ma la dimensione di gl.vertexAttribPointer è 3

Se ti occupi di dettagli, potresti aver notato che abbiamo definito i nostri due attributi come

attribute vec4 a_position;
attribute vec4 a_color;

Entrambi sono "vec4", ma quando diciamo a WebGL come estrarre i dati dai nostri buffer abbiamo utilizzato

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

Il numero "3" in ogni caso indica di estrarre solo 3 valori per attributo. Questo funziona perché nello shader vertex WebGL fornisce i valori predefiniti per quelli che non fornisci. I valori predefiniti sono 0, 0, 0, 1 dove x = 0, y = 0, z = 0 e w = 1. Ecco perché nel nostro vecchio shader vertex 2D dovevamo fornire esplicitamente il valore 1. Stavamo passando x e y e ci serviva un 1 per z, ma poiché il valore predefinito per z è 0, abbiamo dovuto fornire esplicitamente un 1. Per il 3D, tuttavia, anche se non forniamo un valore "w", il valore predefinito è 1, che è ciò che occorre per il funzionamento della matematica della matrice.