Transformations WebGL

Gregg Tavares
Gregg Tavares

Traduction WebGL 2D

Avant de passer à la 3D, nous allons nous en tenir encore un peu plus à la 2D. Merci de patienter. Cet article peut sembler extrêmement évident pour certains, mais je vais approfondir ce sujet dans quelques articles.

Cet article fait suite à une série commençant par le cours Principes de base de WebGL. Si vous ne l'avez pas encore lu, nous vous conseillons de lire au moins le premier chapitre, puis de revenir ici. La traduction est un nom mathématique sophistiqué qui signifie essentiellement « déplacer » quelque chose. Je suppose que le déplacement d'une phrase de l'anglais vers le japonais convient également, mais dans le cas présent, il s'agit du déplacement de la géométrie. En utilisant l'exemple de code que nous avons présenté dans le premier article, vous pourriez facilement traduire notre rectangle en modifiant simplement les valeurs transmises à "setRectangle" ? Voici un exemple basé sur l'exemple précédent.

  // First lets make some variables 
  // to hold the translation of the rectangle
  var translation = [0, 0];
  // then let's make a function to
  // re-draw everything. We can call this
  // function after we update the translation.
  // Draw the scene.
  function drawScene() {
     // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);
    // Setup a rectangle
    setRectangle(gl, translation[0], translation[1], width, height);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }

Jusqu'ici tout va bien. Mais imaginez maintenant que nous voulions faire la même chose avec une forme plus compliquée. Supposons que nous voulions dessiner un « F » composé de 6 triangles comme celui-ci.

Lettre F

Eh bien, voici le code actuel que nous devrions remplacer setRectangle par quelque chose de plus semblable à celui-ci.

// Fill the buffer with the values that define a letter 'F'.
function setGeometry(gl, x, y) {
  var width = 100;
  var height = 150;
  var thickness = 30;
  gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([
          // left column
          x, y,
          x + thickness, y,
          x, y + height,
          x, y + height,
          x + thickness, y,
          x + thickness, y + height,

          // top rung
          x + thickness, y,
          x + width, y,
          x + thickness, y + thickness,
          x + thickness, y + thickness,
          x + width, y,
          x + width, y + thickness,

          // middle rung
          x + thickness, y + thickness * 2,
          x + width * 2 / 3, y + thickness * 2,
          x + thickness, y + thickness * 3,
          x + thickness, y + thickness * 3,
          x + width * 2 / 3, y + thickness * 2,
          x + width * 2 / 3, y + thickness * 3]),
      gl.STATIC_DRAW);
}

Vous pouvez voir que le scaling n'est pas optimal. Si nous voulons dessiner une géométrie très complexe avec des centaines ou des milliers de lignes, nous devons écrire un code assez complexe. De plus, chaque fois que nous dessinons JavaScript, nous devons mettre à jour tous les points. Il existe un moyen plus simple. Il vous suffit d'importer la géométrie et d'effectuer la translation dans le nuanceur. Voici le nouveau nuanceur

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

uniform vec2 u_resolution;
uniform vec2 u_translation;

void main() {
   // Add in the translation.
   vec2 position = a_position + u_translation;

   // convert the rectangle from pixels to 0.0 to 1.0
   vec2 zeroToOne = position / u_resolution;
   ...

et nous allons restructurer un peu le code. Dans un cas, il suffit de définir la géométrie une seule fois.

// 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,
          30, 0,
          0, 150,
          0, 150,
          30, 0,
          30, 150,

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

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

Il nous suffit ensuite de mettre à jour u_translation avant de dessiner avec la traduction souhaitée.

  ...
  var translationLocation = gl.getUniformLocation(
             program, "u_translation");
  ...
  // Set Geometry.
  setGeometry(gl);
  ..
  // Draw scene.
  function drawScene() {
    // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);

    // Set the translation.
    gl.uniform2fv(translationLocation, translation);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 18);
  }

Notez que setGeometry n'est appelé qu'une seule fois. Elle ne se trouve plus dans drawScene.

Lorsque nous dessinons WebGL, WebGL fait pratiquement tout. Tout ce que nous faisons, c'est définir une traduction et lui demander de dessiner. Même si notre géométrie comportait des dizaines de milliers de points, le code principal resterait le même.

Rotation 2D WebGL

J'admets d'emblée que je ne sais pas si la façon dont j'explique cela aura du sens, mais ce que je peux faire.

Commençons par vous présenter ce que l'on appelle un "cercle d'unité". Vous vous souvenez de vos maths au collège (ne dormez pas avec moi !), un cercle a un rayon. Le rayon d'un cercle est la distance entre son centre et son bord. Un cercle unitaire est un cercle dont le rayon est de 1.

Souvenez-vous des mathématiques de base de CE2 en multipliant quelque chose par 1. Cela ne change pas. Donc 123 * 1 = 123. Plutôt basique, non ? Eh bien, un cercle unitaire, un cercle avec un rayon de 1,0 est également une forme de 1. C'est un 1 en rotation. On peut multiplier un nombre par le cercle de l'unité. C'est un peu comme la multiplication par 1, sauf que la magie opère et que les choses tournent. Nous prenons ces valeurs X et Y à partir de n'importe quel point du cercle unitaire, et nous multiplions notre géométrie par celle de notre échantillon précédent. Voici les mises à jour apportées à notre nuanceur.

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

uniform vec2 u_resolution;
uniform vec2 u_translation;
uniform vec2 u_rotation;

void main() {
  // Rotate the position
  vec2 rotatedPosition = vec2(
     a_position.x * u_rotation.y + a_position.y * u_rotation.x,
     a_position.y * u_rotation.y - a_position.x * u_rotation.x);

  // Add in the translation.
  vec2 position = rotatedPosition + u_translation;

Et nous mettons à jour le JavaScript afin de pouvoir transmettre ces deux valeurs.

  ...
  var rotationLocation = gl.getUniformLocation(program, "u_rotation");
  ...
  var rotation = [0, 1];
  ..
  // Draw the scene.
  function drawScene() {
    // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);

    // Set the translation.
    gl.uniform2fv(translationLocation, translation);

    // Set the rotation.
    gl.uniform2fv(rotationLocation, rotation);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 18);
  }

Pourquoi est-ce que cela fonctionne ? Regarde les calculs.

rotatedX = a_position.x * u_rotation.y + a_position.y * u_rotation.x;
rotatedY = a_position.y * u_rotation.y - a_position.x * u_rotation.x;

Sachant que vous avez un rectangle et que vous souhaitez le faire pivoter. Avant de commencer à le faire pivoter, assurez-vous que l'angle supérieur droit se trouve à 3,0 ou 9,0. Choisissons un point sur le cercle de l'unité à 30 degrés dans le sens des aiguilles d'une montre à partir de 12 heures.

Rotation à 30°

La position sur le cercle est 0,50 et 0,87

3.0 * 0.87 + 9.0 * 0.50 = 7.1
9.0 * 0.87 - 3.0 * 0.50 = 6.3

C'est exactement ce que nous devons faire.

Dessin en rotation

Idem pour un angle de 60 degrés dans le sens des aiguilles d'une montre

Rotation à 60°

La position sur le cercle est 0,87 et 0,50

3.0 * 0.50 + 9.0 * 0.87 = 9.3
9.0 * 0.50 - 3.0 * 0.87 = 1.9

Comme vous pouvez le voir, lorsque nous faisons pivoter ce point dans le sens des aiguilles d'une montre vers la droite, la valeur X augmente et l'axe Y diminue. Si vous continuez à dépasser 90 degrés X, vous commencerez à réduire à nouveau et Y à grossir. Ce modèle fait tourner la machine. Les points d'un cercle d'unités portent un autre nom. On les appelle le sinus et le cosinus. Donc, pour n'importe quel angle donné, nous pouvons simplement rechercher le sinus et le cosinus comme ceci.

function printSineAndCosineForAnAngle(angleInDegrees) {
  var angleInRadians = angleInDegrees * Math.PI / 180;
  var s = Math.sin(angleInRadians);
  var c = Math.cos(angleInRadians);
  console.log("s = " + s + " c = " + c);
}

Si vous copiez et collez le code dans votre console JavaScript et que vous saisissez printSineAndCosignForAngle(30), vous voyez s = 0.49 c= 0.87 (remarque: j'ai arrondi les nombres.) Si vous assemblez le tout, vous pouvez faire pivoter votre géométrie dans l'angle de votre choix. Il vous suffit de définir la rotation sur le sinus et le cosinus de l'angle souhaité.

  ...
  var angleInRadians = angleInDegrees * Math.PI / 180;
  rotation[0] = Math.sin(angleInRadians);
  rotation[1] = Math.cos(angleInRadians);

J'espère que ces informations vous ont été utiles. Passons à un exemple plus simple. Évoluez.

Que sont les radians ?

Les radians sont une unité de mesure utilisée avec les cercles, la rotation et les angles. Tout comme nous pouvons mesurer une distance en pouces, en yards, en mètres, etc., nous pouvons mesurer des angles en degrés ou en radians.

Vous savez probablement qu'il est plus facile de faire des mathématiques avec des mesures métriques que des mesures impériales. Pour passer des pouces aux pieds, divisons par 12. Pour passer des pouces aux yards, nous divisons par 36. Je ne sais pas pour vous, mais je ne peux pas diviser par 36 dans ma tête. Avec les métriques, c'est beaucoup plus facile. Pour passer des millimètres aux centimètres, nous divisons la valeur par 10. Nous divisons le millimètre au mètre par 1 000. Je peux diviser par 1 000 dans ma tête.

Les radians et les degrés sont similaires. Les degrés compliquent les calculs. Les radians facilitent les calculs. Il y a 360 degrés dans un cercle, mais il n'y a que 2π radians. Un tour complet est donc de 2π radians. Un demi-tour correspond à π radians. Un 1/4 de tour, c'est-à-dire une dégression de 90 degrés correspond à π/2 radian. Ainsi, si vous voulez faire pivoter quelque chose de 90 degrés, il vous suffit d'utiliser Math.PI * 0.5. Pour la faire pivoter de 45 degrés, utilisez Math.PI * 0.25, etc.

Presque tous les calculs mathématiques impliquant des angles, des cercles ou des rotations fonctionnent très simplement lorsque l'on commence à penser en radians. Essayez donc. Utilisez des radians, et non des degrés, sauf dans les écrans d'interface utilisateur.

Échelle 2D WebGL

Le scaling est aussi simple que la traduction.

Nous multiplions la position par l'échelle souhaitée. Voici les modifications apportées à l'exemple précédent.

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

uniform vec2 u_resolution;
uniform vec2 u_translation;
uniform vec2 u_rotation;
uniform vec2 u_scale;

void main() {
  // Scale the positon
  vec2 scaledPosition = a_position * u_scale;

  // Rotate the position
  vec2 rotatedPosition = vec2(
     scaledPosition.x * u_rotation.y +
        scaledPosition.y * u_rotation.x,
     scaledPosition.y * u_rotation.y -
        scaledPosition.x * u_rotation.x);

  // Add in the translation.
  vec2 position = rotatedPosition + u_translation;

et nous ajoutons le JavaScript nécessaire pour définir l'échelle lors du dessin.

  ...
  var scaleLocation = gl.getUniformLocation(program, "u_scale");
  ...
  var scale = [1, 1];
  ...
  // Draw the scene.
  function drawScene() {
    // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);

    // Set the translation.
    gl.uniform2fv(translationLocation, translation);

    // Set the rotation.
    gl.uniform2fv(rotationLocation, rotation);

    // Set the scale.
    gl.uniform2fv(scaleLocation, scale);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 18);
  }

Il est à noter que la mise à l'échelle par une valeur négative inverse notre géométrie. J'espère que ces trois derniers chapitres vous ont été utiles pour comprendre la traduction, la rotation et l'échelle. Ensuite, nous allons aborder la magie qui consiste à combiner les matrices dans une forme beaucoup plus simple et souvent plus utile.

Pourquoi un "F" ?

La première fois que j'ai vu quelqu'un utiliser un "F", c'était sur une texture. Le F lui-même n'est pas important. Ce qui est important, c'est que vous puissiez déterminer son orientation depuis n'importe quelle direction. Par exemple, si nous avons utilisé un cœur ♥ ou un triangle △, nous ne savions pas s'il était retourné horizontalement. Un cercle ○ serait encore pire. Un rectangle coloré fonctionnerait sans doute avec des couleurs différentes à chaque coin, mais vous devriez alors vous souvenir de quel angle se trouve tel quel. L'orientation d'un F est instantanément reconnaissable.

Orientation F

N'importe quelle forme dont vous pouvez déterminer l'orientation fonctionnerait, je viens d'utiliser le « F » depuis que j'ai été le premier à avoir eu cette idée.

Matrices WebGL 2D

Dans les trois derniers chapitres, nous avons expliqué comment traduire une géométrie, faire pivoter une géométrie et mettre à l'échelle une géométrie. La traduction, la rotation et l'échelle sont chacune considérées comme un type de "transformation". Chacune de ces transformations nécessitait des modifications au niveau du nuanceur, et chacune des trois transformations dépendait de l'ordre.

Par exemple, voici une échelle de 2, 1, une rotation de 30 % et une translation de 100, 0.

Rotation F et translation

Nous avons ici une translation de 100,0, une rotation de 30% et une échelle de 2, 1.

Rotation et échelle F

Les résultats sont complètement différents. Pire encore, si nous avions besoin du deuxième exemple, nous devrions écrire un autre nuanceur qui applique la translation, la rotation et la mise à l'échelle dans le nouvel ordre souhaité. Eh bien, certaines personnes bien plus intelligentes que moi ont compris qu'on pouvait faire les mêmes choses qu'avec les calculs matriciels. Pour l'étude 2D, nous utilisons une matrice 3x3. Une matrice 3x3 est comme une grille à 9 cases.

1,0 2,0 3,0
4.0 5,0 6,0
7,0 8.0 9.0

Pour effectuer le calcul, nous multiplions la position dans les colonnes de la matrice et nous additionnons les résultats. Nos positions n'ont que 2 valeurs, x et y, mais pour effectuer ce calcul, nous avons besoin de 3 valeurs. Nous utiliserons 1 pour la troisième valeur.

newX = x * 1.0 + y * 4.0 + 1 * 7.0

newY = x * 2.0 + y * 5.0 + 1 * 8.0

extra = x * 3.0 + y * 6.0 + 1 * 9.0

Vous êtes probablement en train de regarder cela et de vous demander "À QUOI FAIRE ?". Supposons que nous ayons une traduction. Nous appellerons le montant à traduire tx et ty. Créons une matrice comme celle-ci

1,00,00.0
0,01.00,0
txty1,0

Et maintenant, regardez

newX = x * 1.0 + y * 0.0 + 1 * tx

newY = x * 0.0 + y * 1.0 + 1 * ty

extra = x * 0.0 + y * 0.0 + 1 * 1

Si vous vous souvenez de votre algèbre, nous pouvons supprimer tout lieu qui multiplie par zéro. Multiplier par 1 n'a aucun effet. Voyons ce qui se passe en simplifiant le processus.

newX = x + tx;
newY = y + ty;

Et en plus, nous ne nous soucions pas vraiment. Cela ressemble étonnamment au code de traduction de notre exemple de traduction. De même, faisons la rotation. Comme nous l'avons indiqué dans l'article de rotation, nous avons simplement besoin du sinus et du cosinus de l'angle auquel nous voulons effectuer la rotation.

s = Math.sin(angleToRotateInRadians);
c = Math.cos(angleToRotateInRadians);

Nous construisons une matrice comme celle-ci

c-s0,0
sc0,0
0.00,01,0

En appliquant la matrice, nous obtenons ceci

newX = x * c + y * s + 1 * 0

newY = x * -s + y * c + 1 * 0

extra = x * 0.0 + y * 0.0 + 1 * 1

En noircissant tous ces éléments, nous obtenons

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

C'est exactement ce que nous avons dans notre échantillon de rotation. Enfin, évoluez. Nous appellerons nos deux facteurs d'échelle sx et sy, et nous construisons une matrice

SX0,00.0
0,0sy0,0
0.00,01,0

En appliquant la matrice, nous obtenons ceci

newX = x * sx + y * 0 + 1 * 0

newY = x * 0 + y * sy + 1 * 0

extra = x * 0.0 + y * 0.0 + 1 * 1

qui est vraiment

newX = x * sx;
newY = y * sy;

Ce qui est identique à notre échantillon de mise à l'échelle. Vous pensez peut-être encore. Quel en est l'intérêt ? À quoi ça sert ? Cela semble être beaucoup de travail juste pour faire la même chose que nous faisions déjà ? C'est là que la magie opère. Il s'avère que nous pouvons multiplier des matrices et appliquer toutes les transformations en même temps. Supposons que nous disposons de la fonction matrixMultiply, qui prend deux matrices, les multiplie et renvoie le résultat. Pour plus de clarté, créons des fonctions permettant de créer des matrices pour la translation, la rotation et l'échelle.

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

Modifions à présent notre nuanceur. L'ancien nuanceur ressemblait à ceci :

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

uniform vec2 u_resolution;
uniform vec2 u_translation;
uniform vec2 u_rotation;
uniform vec2 u_scale;

void main() {
  // Scale the positon
  vec2 scaledPosition = a_position * u_scale;

  // Rotate the position
  vec2 rotatedPosition = vec2(
     scaledPosition.x * u_rotation.y + scaledPosition.y * u_rotation.x,
     scaledPosition.y * u_rotation.y - scaledPosition.x * u_rotation.x);

  // Add in the translation.
  vec2 position = rotatedPosition + u_translation;
  ...

Notre nouveau nuanceur sera beaucoup plus simple.

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

uniform vec2 u_resolution;
uniform mat3 u_matrix;

void main() {
  // Multiply the position by the matrix.
  vec2 position = (u_matrix * vec3(a_position, 1)).xy;
  ...

Voici comment nous les utilisons

  // Draw the scene.
  function drawScene() {
    // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);

    // Compute the matrices
    var translationMatrix =
       makeTranslation(translation[0], translation[1]);
    var rotationMatrix = makeRotation(angleInRadians);
    var scaleMatrix = makeScale(scale[0], scale[1]);

    // Multiply the matrices.
    var matrix = matrixMultiply(scaleMatrix, rotationMatrix);
    matrix = matrixMultiply(matrix, translationMatrix);

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

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 18);
  }

Pourtant, vous vous demandez peut-être : alors ? Cela ne semble pas être un avantage très important . Mais si nous voulons changer l'ordre, nous n'avons pas besoin d'écrire un nouveau nuanceur. Nous pouvons simplement changer le calcul.

    ...
    // Multiply the matrices.
    var matrix = matrixMultiply(translationMatrix, rotationMatrix);
    matrix = matrixMultiply(matrix, scaleMatrix);
    ...

Pouvoir appliquer des matrices de ce type est particulièrement important pour l'animation hiérarchique, comme les bras sur un corps, les lunes sur une planète autour d'un soleil ou les branches d'un arbre. Pour un exemple simple d'animation hiérarchique, nous allons dessiner notre "F" cinq fois, mais à chaque fois, commençons par la matrice du "F" précédent.

  // Draw the scene.
  function drawScene() {
    // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);

    // Compute the matrices
    var translationMatrix = makeTranslation(translation[0], translation[1]);
    var rotationMatrix = makeRotation(angleInRadians);
    var scaleMatrix = makeScale(scale[0], scale[1]);

    // Starting Matrix.
    var matrix = makeIdentity();

    for (var i = 0; i < 5; ++i) {
      // Multiply the matrices.
      matrix = matrixMultiply(matrix, scaleMatrix);
      matrix = matrixMultiply(matrix, rotationMatrix);
      matrix = matrixMultiply(matrix, translationMatrix);

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

      // Draw the geometry.
      gl.drawArrays(gl.TRIANGLES, 0, 18);
    }
  }

Pour ce faire, nous avons introduit la fonction makeIdentity, qui crée une matrice d'identité. Une matrice identité est une matrice qui représente efficacement 1,0 de sorte que rien ne se passe si vous multipliez par l'identité. Comme

X * 1 = X

ainsi

matrixX * identity = matrixX

Voici le code permettant de créer une matrice d'identité.

function makeIdentity() {
  return [
    1, 0, 0,
    0, 1, 0,
    0, 0, 1
  ];
}

Un autre exemple : dans chaque sample jusqu'à présent, notre "F" tourne autour de son coin supérieur gauche. En effet, le calcul que nous utilisons effectue toujours une rotation autour de l'origine et le coin supérieur gauche de notre "F" se trouve à l'origine, (0, 0). Toutefois, comme nous pouvons effectuer des calculs matriciels et choisir l'ordre d'application des transformations, nous pouvons déplacer l'origine avant que les autres transformations ne soient appliquées.

    // make a matrix that will move the origin of the 'F' to
    // its center.
    var moveOriginMatrix = makeTranslation(-50, -75);
    ...

    // Multiply the matrices.
    var matrix = matrixMultiply(moveOriginMatrix, scaleMatrix);
    matrix = matrixMultiply(matrix, rotationMatrix);
    matrix = matrixMultiply(matrix, translationMatrix);

Cette technique vous permet d'effectuer une rotation ou une mise à l'échelle à partir de n'importe quel point. Vous savez maintenant comment utiliser Photoshop ou Flash pour déplacer le point de rotation. Allons encore plus loin. Si vous revenez au premier article sur les principes de base de WebGL, vous vous souvenez peut-être que le nuanceur contient du code qui ressemble à ceci pour convertir les pixels en espace d'extraits.

  ...
  // convert the rectangle from pixels to 0.0 to 1.0
  vec2 zeroToOne = position / u_resolution;

  // convert from 0->1 to 0->2
  vec2 zeroToTwo = zeroToOne * 2.0;

  // convert from 0->2 to -1->+1 (clipspace)
  vec2 clipSpace = zeroToTwo - 1.0;

  gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

Si vous examinez chacune de ces étapes l'une après l'autre, la première étape qui consiste à "convertir des pixels de 0,0 à 1,0" est en fait une opération de mise à l'échelle. Le second est une opération de scaling. La suivante est une translation, et la dernière mise à l'échelle Y par -1. Tout cela est possible dans la matrice que nous transmettons au nuanceur. Nous pourrions créer deux matrices d'échelle, l'une pour obtenir une échelle de 1,0/résolution, l'autre pour obtenir une échelle de 2,0, une troisième pour traduire par -1,0, -1,0 et une quatrième pour mettre à l'échelle Y par -1, puis les multiplier ensemble, mais comme le calcul est simple, nous allons simplement créer une fonction qui génère directement une matrice de "projection" pour une résolution donnée.

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

Nous pouvons maintenant simplifier davantage le nuanceur. Voici le nouveau nuanceur de sommets.

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

En JavaScript, nous devons multiplier la valeur par la matrice de projection.

  // Draw the scene.
  function drawScene() {
    ...
    // Compute the matrices
    var projectionMatrix =
       make2DProjection(canvas.width, canvas.height);
    ...

    // Multiply the matrices.
    var matrix = matrixMultiply(scaleMatrix, rotationMatrix);
    matrix = matrixMultiply(matrix, translationMatrix);
    matrix = matrixMultiply(matrix, projectionMatrix);
    ...
  }

Nous avons également supprimé le code qui définit la résolution. Avec cette dernière étape, nous sommes passés d'un nuanceur assez complexe de 6 à 7 étapes à un nuanceur très simple avec un seul pas. Tout cela grâce à la magie des calculs matriciels.

J'espère que cet article vous a aidé à démystifier les calculs matriciels. Passons maintenant à la 3D. En 3D, les calculs matriciels suivent les mêmes principes et la même utilisation. J'ai commencé avec la 2D pour que les choses restent simples à comprendre.