3D ortografico WebGL

Gregg Tavares
Gregg Tavares

3D ortografica WebGL

Questo post è la continuazione di una serie di post su WebGL. La prima è iniziata con i principi base e la precedente riguardava le matrici 2D relative alle matrici 2D. Se non li hai letti, consultali prima. Nell'ultimo post abbiamo visto come funzionavano le matrici 2D. Abbiamo parlato di traslazione, rotazione, ridimensionamento e persino proiezione dai pixel nello spazio dei clip possono essere eseguiti con una matrice e un po' di matematica. Per realizzare il 3D basta un piccolo passo. Nei nostri precedenti esempi 2D, avevamo punti 2D (x, y) moltiplicati per una matrice 3x3. Per il 3D abbiamo bisogno di punti 3D (x, y, z) e di una matrice 4x4. Prendiamo il nostro ultimo esempio e lo modifichiamo in 3D. Useremo di nuovo una F, ma questa volta una "F" in 3D. La prima cosa da fare è cambiare lo strumento Vertex Shaper per gestire 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 i 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);
}

Ora dobbiamo cambiare tutte le funzioni della matrice da 2D a 3D. Ecco le versioni 2D (precedenti) di makeTranslation, makeRotazione 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 3 funzioni di rotazione. Ne avevamo bisogno solo in 2D perché ruotavamo semplicemente attorno all'asse Z. Per il 3D, vogliamo anche poter ruotare attorno agli assi x e y. Puoi vederli osservandoli tutti molto simili. Se dovessimo risolverle, le vedresti semplificare 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 quello vecchio

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 vengono convertiti da pixel in spazio per clip. Per il nostro primo tentativo di espanderla 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 dovevamo convertire da pixel in clipspace per x e y, per z dobbiamo fare lo stesso. In questo caso sto creando anche le unità di pixel dello spazio Z. Inserirò un valore simile a width per la profondità, quindi il nostro spazio sarà compreso tra 0 e 0 in pixel di larghezza, da 0 a 0 pixel di altezza, ma per la profondità sarà da 0 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 abbiamo è che la nostra geometria è una F piatta che rende difficile visualizzare qualsiasi 3D. Per risolvere il problema, espandi la geometria in 3D. La nostra attuale F è fatta di 3 rettangoli, 2 triangoli ciascuno. Per il 3D sono necessari un totale di 16 rettangoli. Ne sono un bel po' qui. 16 rettangoli x 2 triangoli per rettangolo x 3 vertici per triangolo corrispondono a 96 vertici. Se vuoi vederli tutti, visualizza il codice sorgente nell'esempio. Dobbiamo disegnare più vertici in modo

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

Spostando i cursori è abbastanza difficile capire che è in 3D. Proviamo a colorare ogni rettangolo di un colore diverso. Per farlo, aggiungeremo un altro attributo al nostro Vertex Shader e una variabile per trasferirlo dall'ombreggiatura dei vertici allo Shader dei frammenti. Ecco il nuovo Vertex Shader

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

Dobbiamo usare quel colore nello strumento di shadowing dei frammenti

<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 località per fornire i colori, quindi configurare un altro buffer e un attributo per dargli 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);
}

Oh oh, che disastro? Bene, ho scoperto che tutte le parti di quella "F" 3D, fronte, retro, lati, ecc. sono disegnate nell'ordine in cui appaiono nella nostra geometria. Questo però non ci dà i risultati auspicati perché a volte quelle posteriori vengono disegnate dopo quelle davanti. In WebGL, i triangoli sono concepiti come frontale e posteriore. Un triangolo frontale ha i vertici che ruotano in senso orario. I vertici di un triangolo rivolto all'indietro i cui vertici vanno in senso antiorario.

Triangolo.

WebGL è in grado di disegnare solo triangoli rivolti in avanti o indietro. Possiamo attivare questa funzione

gl.enable(gl.CULL_FACE);

cosa che facciamo una sola volta, all'inizio del nostro programma. Quando questa funzionalità è attiva, WebGL utilizza per impostazione predefinita i triangoli rivolti verso l'esterno. "Culling" in questo caso è una parola sofisticata che significa "non disegnare". Tieni presente che, per quanto riguarda WebGL, la presenza o meno di un triangolo in senso orario o antiorario dipende dai vertici di quel triangolo nello spazio di clip. In altre parole, WebGL determina se un triangolo è anteriore o posteriore DOPO aver applicato la formula matematica ai vertici dello strumento di orientamento vertice. Ciò significa che, ad esempio, un triangolo in senso orario scalato in X di -1 diventa un triangolo antiorario oppure un triangolo in senso orario ruotato di 180 gradi intorno all'asse X o Y diventa un triangolo antiorario. Poiché CULL_FACE è stato disattivato, possiamo vedere i triangoli in senso orario(anteriore) e antiorario(indietro). Ora che lo attiviamo, ogni volta che un triangolo anteriore si capovolge a causa del ridimensionamento o della rotazione o per qualsiasi motivo, WebGL non lo disegna. Questa è una cosa positiva poiché quando giri qualcosa in 3D in genere vuoi che i triangoli rivolti verso di te siano considerati frontali.

Ciao! Dove sono finiti tutti i triangoli? Ho scoperto che molti di loro stanno guardando nel modo sbagliato. Ruotalo e le vedrai apparire dall'altra parte. Per fortuna è facile da risolvere. Ora vediamo quali sono indietro e scambiano due vertici. Ad esempio, se un triangolo all'indietro è

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

basta capovolgere gli ultimi 2 vertici per andare avanti.

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

La procedura è più vicina, ma c'è ancora un altro problema. Anche se tutti i triangoli sono rivolti nella direzione corretta e con quelli posteriori ridimensionati, abbiamo comunque punti in cui i triangoli che dovrebbero trovarsi all'indietro vengono disegnati in triangoli che dovrebbero trovarsi di fronte. Inserisci il BUFFER DI PROFONDITÀ. Un buffer di profondità, chiamato a volte buffer Z, è un rettangolo di depth pixel, un pixel di profondità per ogni pixel colore utilizzato per creare l'immagine. Poiché WebGL disegna ogni pixel di colore, può anche tracciare un pixel di profondità. Lo fa in base ai valori restituiti dallo Shadr Vertex per Z. Proprio come abbiamo dovuto convertire in spazio di clip per X e Y, così in Z è nell'area di clip o (da -1 a +1). Quel valore viene quindi convertito in un valore di profondità dello spazio (da 0 a +1). Prima di disegnare un pixel a colori, WebGL controlla la profondità corrispondente. Se il valore di profondità del pixel che sta per disegnare è superiore al valore del pixel di profondità corrispondente, WebGL non disegna il nuovo pixel colore. In caso contrario, disegna sia il nuovo pixel di colore con il colore dello strumento di identificazione dei frammenti 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 funzione con la stessa facilità con cui abbiamo attivato l'eliminazione

gl.enable(gl.DEPTH_TEST);

Dobbiamo anche cancellare il buffer di profondità a 1,0 prima di iniziare a disegnare.

// 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 vedremo come sfruttare la prospettiva.

Perché l'attributo vec4 è di dimensione 3 e gl.vertexAttribPointer

Chi è interessato ai dettagli potrebbe 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 buffer che abbiamo utilizzato

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

Quel "3" in ognuno di questi indica solo di estrarre 3 valori per attributo. Questo funziona perché nello strumento Vertex Shadr WebGL fornisce valori predefiniti per quelli non forniti da te. I valori predefiniti sono 0, 0, 0, 1, dove x = 0, y = 0, z = 0 e w = 1. Questo è il motivo per cui nel nostro vecchio Vertex Shaper 2D dovevamo fornire esplicitamente l'1. Stavamo passando x e y e avevamo bisogno di un 1 per z, ma poiché il valore predefinito per z è 0, abbiamo dovuto fornire esplicitamente un 1. Tuttavia, nel caso del 3D, anche se non forniamo "w", il valore predefinito è 1, che è ciò di cui abbiamo bisogno affinché la matematica della matrice funzioni.