3D ortografico WebGL

Gregg Tavares
Gregg Tavares

WebGL 3D ortografico

Questo post è la continuazione di una serie di post su WebGL. Il primo ha trattato i concetti di base, mentre il precedente riguardava le matrici 2D . Se non li hai letti, ti consigliamo di farlo prima di continuare. Nell'ultimo post abbiamo esaminato il funzionamento delle matrici 2D. Abbiamo parlato di traslazione, rotazione, ridimensionamento e persino della proiezione da pixel a spazio di ritaglio, tutte operazioni che possono essere eseguite con una matrice e un po' di matematica matriciale. La creazione di contenuti 3D è solo un piccolo passo avanti. 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 cambiamolo in 3D. Utilizzeremo di nuovo una F, ma questa volta una "F" 3D. La prima cosa da fare è modificare il vertex shader 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>

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

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

Nota che ora abbiamo 3 funzioni di rotazione. In 2D ne serviva solo uno, perché la rotazione avveniva solo attorno all'asse Z. Ora, per creare un'immagine 3D, vogliamo anche poter ruotare attorno all'asse X e all'asse Y. Come puoi vedere, sono tutti molto simili. Se li risolvessimo, vedresti che si semplificano proprio 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 quella vecchia

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 ha convertito i pixel in spazio di ritaglio. Per il nostro primo tentativo di espanderlo 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 clipspace per x e y, dobbiamo fare lo stesso per z. In questo caso, anche le unità dello spazio Z sono in pixel. Passerò un valore simile a width per la profondità, quindi il nostro spazio sarà largo da 0 a width pixel, alto da 0 a height pixel, ma per la profondità sarà da -depth / 2 a +depth / 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, espandiamo la geometria in 3D. La nostra F attuale è composta da 3 rettangoli, ciascuno con 2 triangoli. Per renderlo 3D, saranno necessari 16 rettangoli in totale. Sono troppi per elencarli qui. 16 rettangoli x 2 triangoli per rettangolo x 3 vertici per triangolo = 96 vertici. Se vuoi vederli tutti, visualizza il codice sorgente dell'esempio. Dobbiamo disegnare più vertici, quindi

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

Spostando i cursori è piuttosto difficile capire che si tratta di un'immagine 3D. Proviamo a colorare ogni rettangolo con un colore diverso. Per farlo, aggiungeremo un altro attributo al nostro vertex shader e un attributo varying per passarlo dal vertex shader al fragment shader. 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>

E dobbiamo usare quel colore nel fragment shader

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

Oh oh, che disastro! Beh, si scopre che tutte le varie parti della "F" 3D, fronte, retro, lati e così via, vengono disegnate nell'ordine in cui appaiono nella nostra geometria. Questo non ci dà i risultati desiderati, perché a volte gli elementi sullo sfondo vengono disegnati dopo quelli in primo piano. I triangoli in WebGL hanno il concetto di lato anteriore e lato posteriore. Un triangolo rivolto in avanti ha i vertici che vanno in senso orario. Un triangolo rivolto verso il retro ha i vertici che vanno in senso antiorario.

Avvolgimento a triangolo.

WebGL ha la capacità di disegnare solo triangoli rivolti in avanti o indietro. Possiamo attivare questa funzionalità con

gl.enable(gl.CULL_FACE);

che eseguiamo una sola volta, all'inizio del programma. Con questa funzionalità attivata, WebGL esegue per impostazione predefinita il "culling" dei triangoli rivolti verso il retro. "Culling" in questo caso è un termine sofisticato 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 di ritaglio. In altre parole, WebGL determina se un triangolo è anteriore o posteriore DOPO che hai applicato la matematica ai vertici nel vertex shader. Ciò significa, ad esempio, che un triangolo in senso orario scalato in X di -1 diventa un triangolo in senso antiorario o un triangolo in senso orario ruotato di 180 gradi attorno all'asse X o Y diventa un triangolo in senso antiorario. Poiché abbiamo disattivato CULL_FACE, possiamo vedere sia i triangoli in senso orario(anteriore) sia quelli in senso antiorario(posteriore). Ora che l'abbiamo attivato, ogni volta che un triangolo rivolto in avanti si capovolge a causa di ridimensionamento o rotazione o per qualsiasi motivo, WebGL non lo disegnerà. Questo è un aspetto positivo, poiché quando ruoti un oggetto in 3D, in genere vuoi che i triangoli rivolti verso di te siano considerati rivolti verso l'alto.

Ehi! Dove sono finiti tutti i triangoli? Si scopre che molti di loro sono rivolti nella direzione sbagliata. Ruotalo e li vedrai apparire quando guardi l'altro lato. Fortunatamente, è facile da risolvere. Basta guardare quali sono al contrario e scambiare due dei loro vertici. Ad esempio, se un triangolo rivolto verso sinistra è

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

invertiamo semplicemente gli ultimi due vertici per renderlo in avanti.

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

Ci siamo quasi, ma c'è ancora un problema. Anche con tutti i triangoli rivolti nella direzione corretta e con quelli rivolti verso il retro eliminati, abbiamo ancora punti in cui i triangoli che dovrebbero essere sul retro vengono disegnati sopra i triangoli che dovrebbero essere sul fronte. Inserisci il BUFFER DI PROFONDITÀ. 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. Mentre WebGL disegna ogni pixel di colore, può anche disegnare un pixel di profondità. Lo fa in base ai valori restituiti dallo Shading dei vertici per Z. Proprio come abbiamo dovuto convertire in spazio di ritaglio per X e Y, anche Z si trova nello spazio di ritaglio o (-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, controllerà il pixel di profondità corrispondente. Se il valore di profondità del pixel che sta per disegnare è 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 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 funzionalità quasi con la stessa facilità con cui abbiamo attivato l'eliminazione con

gl.enable(gl.DEPTH_TEST);

Dobbiamo anche cancellare il buffer di profondità riportandolo 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 spiegherò come dare una prospettiva.

Why is the attribute vec4 but gl.vertexAttribPointer size 3

Se hai un occhio attento ai dettagli, potresti aver notato che abbiamo definito i nostri due attributi come

attribute vec4 a_position;
attribute vec4 a_color;

entrambi di tipo "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 ciascuno di questi indica di estrarre solo 3 valori per attributo. Questo funziona perché nel vertex shader WebGL fornisce 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 vertex shader 2D abbiamo dovuto fornire esplicitamente il valore 1. Passavamo 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. Per il 3D, tuttavia, anche se non forniamo un valore "w", il valore predefinito è 1, che è ciò di cui abbiamo bisogno per il funzionamento della matematica delle matrici.