WebGL orthographique 3D

Gregg Tavares
Gregg Tavares

WebGL orthographie 3D

Ce message fait suite à une série de messages concernant WebGL. La première a commencé par les principes fondamentaux et la précédente concernait des matrices 2D concernant des matrices 2D. Si vous ne les avez pas lues, veuillez d'abord les consulter. Dans le dernier article, nous avons passé en revue le fonctionnement des matrices 2D. Nous avons parlé de la translation, de la rotation, de la mise à l'échelle et même de la projection de pixels dans l'espace des extraits à l'aide d'une seule matrice et d'une matrice magique. La 3D n'est que très loin. Dans nos exemples 2D précédents, nous avions des points 2D (x, y) que nous avions multipliés par une matrice 3x3. Pour utiliser des fonctions 3D, nous avons besoin de points 3D (x, y, z) et d'une matrice 4x4. Prenons notre dernier exemple et passons-le en 3D. Nous utiliserons à nouveau un "F", mais cette fois un "F" en 3D. Nous devons d'abord modifier le nuanceur de sommets pour qu'il gère la 3D. Voici l'ancien nuanceur.

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

Et voici le nouveau

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

C'est encore plus simple ! Nous devons ensuite fournir des données 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);
}

Ensuite, nous devons faire passer toutes les fonctions matricielles de 2D à 3D. Voici les versions 2D (avant) de makeTranslation, des fonctions makeRotation et 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
];
}

Et voici les nouvelles versions 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,
];
}

Notez que nous avons maintenant trois fonctions de rotation. Nous n'avions besoin que d'un seul en 2D car nous n'utilisions en fait que le tour de l'axe Z. Mais pour faire en 3D, nous voulons également pouvoir pivoter autour de l'axe des x et de l'axe des y. Vous pouvez voir qu'en les regardant, elles sont toutes très similaires. Si nous les travaillions, vous les verriez simplifier comme avant

Rotation Z

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

Rotation Y


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

Rotation X

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

Nous devons également mettre à jour la fonction de projection. Voici l'ancien

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

qui convertit les pixels en espace de découpe. Pour la première fois en 3D,

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

Tout comme nous devions convertir des pixels en espace de rognage pour x et y, nous devons procéder de la même manière pour z. Dans ce cas, je crée aussi les unités de pixels de l'espace Z. Je vais saisir une valeur semblable à width pour la profondeur. Notre espace sera donc de 0 à 0 pixel en largeur et de 0 à 0 pixels en hauteur, mais pour la profondeur, il sera -depth / 2 to +depth / 2. Enfin, nous devons mettre à jour le code qui calcule 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);

Le premier problème que nous avons est que notre géométrie est un F plat, ce qui rend difficile la visualisation de 3D. Pour résoudre ce problème, étendons la géométrie en 3D. Notre F actuel est composé de 3 rectangles de 2 triangles chacun. Pour la rendre 3D, il faudra un total de 16 rectangles. En voici quelques-uns à énumérer. 16 rectangles x 2 triangles par rectangle x 3 sommets par triangle correspondent à 96 sommets. Si vous souhaitez tous les voir, affichez le code source de l'échantillon. Nous devons dessiner plus de sommets pour

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

Si vous déplacez les curseurs, vous aurez du mal à déterminer qu'il s'agit d'une image 3D. Essayons de colorer chaque rectangle d’une couleur différente. Pour ce faire, nous ajouterons un autre attribut à notre nuanceur de sommets et une variante pour le transmettre du nuanceur de sommets au nuanceur de fragments. Voici le nouveau nuanceur de sommets

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

Nous devons utiliser cette couleur dans le nuanceur de fragments.

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

Nous devons rechercher l'emplacement pour fournir les couleurs, puis configurer un autre tampon et un autre attribut pour lui donner les couleurs.

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

Qu'est-ce que c'est que ce bazar ? Eh bien, il s'avère que toutes les différentes parties de ce "F" 3D (avant, arrière, côtés, etc.) sont dessinées dans l'ordre dans lequel elles apparaissent dans notre géométrie. Cela ne nous donne pas tout à fait les résultats souhaités, car parfois ceux à l'arrière sont dessinés après ceux de l'avant. Dans WebGL, les triangles ont le concept de face avant et de face arrière. Les sommets d'un triangle frontal sont orientés dans le sens des aiguilles d'une montre. Les sommets d'un triangle orienté vers l'arrière sont orientés dans le sens inverse des aiguilles d'une montre.

Triangle en forme de spirale.

WebGL peut dessiner uniquement des triangles pointant vers l'avant ou vers l'arrière. Nous pouvons activer cette fonctionnalité avec

gl.enable(gl.CULL_FACE);

ce que nous faisons une seule fois, au début de notre programme. Lorsque cette fonctionnalité est activée, WebGL utilise par défaut la sélection des triangles faisant face à l'arrière. Dans ce cas, le mot « couler » est un mot fantaisiste pour « ne pas dessiner ». Notez qu'en ce qui concerne WebGL, le fait qu'un triangle soit considéré ou non comme orienté dans le sens des aiguilles d'une montre ou en sens inverse dépend des sommets de ce triangle dans l'espace des extraits. En d'autres termes, WebGL détermine si un triangle se trouve au premier plan ou à l'arrière APRÈS avoir appliqué des calculs mathématiques aux sommets du nuanceur de sommets. Cela signifie, par exemple, qu'un triangle dans le sens des aiguilles d'une montre mis à l'échelle de X par -1 devient un triangle dans le sens inverse des aiguilles d'une montre ou qu'un triangle orienté dans le sens des aiguilles d'une montre pivoté de 180 degrés autour de l'axe X ou Y devient un triangle dans le sens inverse des aiguilles d'une montre. Comme CULL_FACE est désactivé, nous pouvons voir les triangles dans le sens des aiguilles d'une montre(avant) et dans le sens inverse des aiguilles d'une montre(arrière). Maintenant que nous l'avons activée, chaque fois qu'un triangle frontal se retourne, que ce soit à cause d'une mise à l'échelle ou d'une rotation, ou pour quelque raison que ce soit, WebGL ne le dessine pas. C'est une bonne chose, car lorsque vous faites pivoter les éléments en 3D, vous souhaitez généralement que les triangles qui vous font face soient considérés comme faisant face.

Mais Où sont passés tous les triangles ? Il s’avère que beaucoup d’entre eux sont confrontés à la mauvaise voie. Faites-le pivoter pour les voir apparaître de l'autre côté. Heureusement, c'est facile à résoudre. Nous examinons simplement ceux qui sont en arrière et échangeons deux de leurs sommets. Par exemple, si un triangle arrière est

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

il suffit de retourner les deux derniers sommets pour avancer.

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

C'est plus proche, mais il reste un autre problème. Même si tous les triangles sont orientés dans la bonne direction et que ceux qui font face à l'arrière sont sélectionnés, les triangles qui doivent être à l'arrière sont dessinés au-dessus de triangles qui doivent se trouver au premier plan. Saisissez le TAMPON PROFOND. Un tampon de profondeur, parfois appelé Z-Buffer, est un rectangle de depth pixels, soit un pixel de profondeur pour chaque pixel de couleur utilisé pour créer l'image. Lorsque WebGL dessine chaque pixel de couleur, il peut également dessiner une profondeur de pixel. Elle le fait en fonction des valeurs renvoyées par le nuanceur de sommets pour Z. Tout comme nous devions le convertir en espace d'extraits pour X et Y, "Z" se trouve dans l'espace des extraits ou (-1 à +1). Cette valeur est ensuite convertie en une valeur d'espace de profondeur (0 à +1). Avant de dessiner un pixel de couleur, WebGL vérifie la profondeur correspondante. Si la valeur de la profondeur du pixel qu'il est sur le point de dessiner est supérieure à la valeur du pixel de profondeur correspondant, WebGL ne dessine pas le nouveau pixel de couleur. Sinon, elle dessine à la fois le nouveau pixel de couleur avec la couleur de votre nuanceur de fragments ET le pixel de profondeur avec la nouvelle valeur de profondeur. Cela signifie que les pixels qui se trouvent derrière les autres pixels ne seront pas dessinés. Nous pouvons activer cette fonctionnalité presque aussi simplement que nous avons activé la sélection avec

gl.enable(gl.DEPTH_TEST);

Nous devons également réinitialiser la mémoire tampon de profondeur pour la rétablir à 1,0 avant de commencer à dessiner.

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

Dans le prochain message, je vous expliquerai comment faire en sorte qu'elle prenne du recul.

Pourquoi l'attribut vec4 est-il de taille 3, mais gl.vertexAttribPointer ?

Pour ceux d'entre vous qui ont le souci du détail, vous avez peut-être remarqué que nous avons défini nos deux attributs :

attribute vec4 a_position;
attribute vec4 a_color;

qui sont tous deux "vec4". Mais lorsque nous avons indiqué à WebGL comment extraire les données de nos tampons, nous avons utilisé

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

Le chiffre "3" dans chacun d'eux indique de ne extraire que trois valeurs par attribut. Cela fonctionne, car dans le nuanceur de sommets, WebGL fournit des valeurs par défaut pour ceux que vous ne fournissez pas. Les valeurs par défaut sont 0, 0, 0, 1, où x = 0, y = 0, z = 0 et w = 1. C'est pourquoi dans notre ancien nuanceur de sommets 2D, nous devions indiquer explicitement le 1. Nous transmettions x et y, et nous avions besoin d'un 1 pour z, mais comme la valeur par défaut de z est 0, nous devions explicitement indiquer 1. Pour la 3D, même si nous ne fournissons pas de "w", la valeur par défaut est 1, ce dont nous avons besoin pour que le calcul matriciel fonctionne.