WebGL-Transformationen

Gregg Tavares
Gregg Tavares

WebGL-2D-Übersetzung

Bevor wir uns mit 3D befassen, bleiben wir noch eine Weile bei 2D. Haben Sie bitte etwas Geduld. Dieser Artikel mag für manche vielleicht erstaunlich offensichtlich erscheinen, aber ich werde ihn in einigen Artikeln einbauen.

Dieser Artikel ist die Fortsetzung einer Reihe, die mit WebGL-Grundlagen beginnt. Wenn Sie ihn noch nicht gelesen haben, empfehle ich Ihnen, zumindest das erste Kapitel zu lesen und dann hierher zurückzukehren. Die Übersetzung ist ein ausgeklügelter mathematischer Name, der im Grunde bedeutet, etwas zu „verschieben“. Das Verschieben eines Satzes vom Englischen ins Japanische passt ebenfalls, aber in diesem Fall geht es um das Verschieben von Geometrie. Mit dem Beispielcode aus dem ersten Post könnten Sie unser Rechteck einfach übersetzen, indem Sie die an setRectangle übergebenen Werte ändern. Ist das richtig? Hier ein Beispiel, das auf unserem vorherigen Beispiel basiert.

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

So weit, so gut. Aber stellen Sie sich nun vor, wir möchten dasselbe mit einer komplizierteren Form machen. Angenommen, wir möchten ein „F“ zeichnen, das aus sechs Dreiecken wie diesem besteht.

F-Buchstabe

Mit dem aktuellen Code müssten wir setRectangle in etwa so ändern.

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

Sie sehen, dass sich das nicht gut skalieren lässt. Wenn wir eine sehr komplexe Geometrie mit Hunderten oder Tausenden von Linien zeichnen möchten, müssen wir ziemlich komplexen Code schreiben. Darüber hinaus muss JavaScript jedes Mal, wenn wir zeichnen, alle Punkte aktualisieren. Es gibt aber eine einfachere Methode. Laden Sie einfach die Geometrie hoch und führen Sie die Übersetzung im Shader durch. Hier ist der neue Shader.

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

und wir strukturieren den Code ein wenig um. Zum einen müssen wir die Geometrie nur einmal festlegen.

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

Dann müssen wir nur u_translation aktualisieren, bevor wir mit der gewünschten Übersetzung zeichnen.

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

Beachten Sie, dass setGeometry nur einmal aufgerufen wird. Es befindet sich nicht mehr in "drawScene".

Wenn wir jetzt zeichnen, macht WebGL praktisch alles. Wir legen nur eine Übersetzung fest und bitten sie, zu zeichnen. Selbst wenn unsere Geometrie Zehntausende von Punkten hätte, bliebe der Hauptcode gleich.

WebGL-2D-Drehung

Ich gebe zu, dass ich gleich zu Anfang keine Ahnung habe, ob meine Erklärungen sinnvoll sind, aber ich kann es auch versuchen.

Zunächst möchte ich Ihnen den sogenannten "Einheitskreis" vorstellen. Wenn Sie sich an die Mathe in der Junior High School (also nicht einschlafen!) erinnern, hat ein Kreis einen Radius. Der Radius eines Kreises ist der Abstand vom Kreismittelpunkt zum Rand. Ein Einheitskreis ist ein Kreis mit dem Radius 1,0.

Wie Sie sich noch an die einfachen Mathematik der 3. Klasse erinnern, bleibt alles gleich, wenn Sie etwas mit 1 multiplizieren. Also: 123 * 1 = 123. Ziemlich einfach, oder? Ein Einheitskreis, ein Kreis mit einem Radius von 1,0, ist auch eine Form von 1. Es ist eine rotierende 1. Sie können also etwas mit diesem Einheitskreis multiplizieren, was einer Art von Multiplikation mit 1 entspricht, außer dass es magisch geschieht und sich die Dinge drehen. Wir nehmen diesen X- und Y-Wert von einem beliebigen Punkt auf dem Einheitskreis und multiplizieren unsere Geometrie mit ihnen aus unserem vorherigen Beispiel. Hier sind die Aktualisierungen für unseren Shader.

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

Und wir aktualisieren den JavaScript-Code, damit wir diese beiden Werte übergeben können.

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

Warum funktioniert es? Schauen wir uns die Rechnung an.

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;

Nehmen wir wieder ein Rechteck, das Sie drehen möchten. Vor dem Drehen befindet sich die obere rechte Ecke bei 3,0, 9,0. Nehmen wir bei 12 Uhr einen Punkt auf dem Einheitskreis bei 30 Grad im Uhrzeigersinn.

30-Grad-Drehung

Die Position auf dem Kreis dort ist 0,50 und 0,87.

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

Genau an dieser Stelle benötigen wir

Rotationszeichnung

Das Gleiche für 60 Grad im Uhrzeigersinn.

60-Grad-Drehung

Die Position auf dem Kreis dort ist 0,87 und 0,50.

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

Sie können sehen, dass der X-Wert größer und Y kleiner wird, wenn wir diesen Punkt nach rechts drehen. Bei weiteren 90 Grad würde X wieder kleiner und Y immer größer. Dieses Muster gibt uns die Drehung. Für die Punkte auf einem Einheitskreis gibt es noch eine andere Bezeichnung. Sie werden als Sinus und Kosinus bezeichnet. So können wir für jeden Winkel den Sinus und den Kosinus ermitteln.

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

Wenn du den Code kopierst, in deine JavaScript-Konsole einfügst und printSineAndCosignForAngle(30) eingibst, siehst du, dass s = 0.49 c= 0.87 ausgegeben wird (Hinweis: Ich habe die Zahlen abgerundet.) Mit allem können Sie Ihre Geometrie in jeden gewünschten Winkel drehen. Stellen Sie die Drehung einfach auf den Sinus und Kosinus des Winkels ein, in den Sie drehen möchten.

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

Ich hoffe, das war hilfreich. Kommen wir zu einer einfacheren. Skalieren.

Was ist Radianten?

Radianten sind Maßeinheiten, die bei Kreisen, Drehungen und Winkeln verwendet werden. So wie wir Entfernungen in Zoll, Yards, Metern usw. messen können, können wir Winkel in Grad oder Radianten messen.

Wahrscheinlich ist Ihnen schon bekannt, dass Mathematik mit metrischen Maßen einfacher ist als mit imperialen Maßen. Um von Zoll in Fuß zu gehen, dividieren wir durch 12. Um von Zoll in Yards zu gehen, dividieren wir durch 36. Ich weiß nicht, wie es dir geht, aber ich kann im Kopf nicht durch 36 dividieren. Mit metrischen Werten ist das viel einfacher. Um von Millimeter in Zentimeter zu gehen, dividieren wir durch 10. Zur Umrechnung von Millimeter in Meter dividieren wir durch 1000. Ich kann im Kopf durch 1.000 teilen.

Bogenmaß und Grad sind ähnlich. Grad erschweren das Rechnen. Radianten erleichtern die Berechnung. Ein Kreis hat 360 Grad, aber es gibt nur 2Π Radiant. Eine vollständige Drehung entspricht also 2Π Radiant. Eine halbe Drehung ist Π Radiant. Eine 1/4-Drehung, d. h. 90 Grad, entspricht Π/2 Radiant. Wenn Sie also etwas um 90 Grad drehen möchten, verwenden Sie einfach Math.PI * 0.5. Wenn du es um 45 Grad drehen möchtest, verwende Math.PI * 0.25 usw.

Fast alle Mathematik mit Winkeln, Kreisen oder Drehungen funktionieren ganz einfach, wenn Sie in Radianten denken. Probieren Sie es einfach aus. Verwenden Sie Radianten, nicht Grad, außer in UI-Anzeigen.

WebGL-2D-Skalierung

Die Skalierung ist genauso einfach wie die Übersetzung.

Wir multiplizieren die Position mit der gewünschten Skala. Hier sind die Änderungen im Vergleich zum vorherigen Beispiel.

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

und fügen den JavaScript-Code hinzu, der zum Festlegen der Skalierung beim Zeichnen erforderlich ist.

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

Zu beachten ist, dass unsere Geometrie bei der Skalierung mit einem negativen Wert umgedreht wird. Ich hoffe, diese letzten drei Kapitel waren hilfreich beim Verständnis von Übersetzung, Rotation und Skalierung. Als Nächstes sehen wir uns Matrizen an, die alle drei Aspekte zu einer viel einfacheren und oft nützlicheren Form kombinieren.

Warum ein „F“?

Als ich zum ersten Mal ein „F“ auf einer Textur gesehen habe. Das „F“ selbst ist nicht wichtig. Wichtig ist, dass die Ausrichtung aus jeder Richtung erkannt werden kann. Wenn wir beispielsweise ein Herz externes oder Dreieck △ verwenden, könnten wir nicht feststellen, ob es horizontal gedreht wurde. Ein Kreis ○ wäre noch schlimmer. Ein farbiges Rechteck würde vermutlich mit verschiedenen Farben für jede Ecke funktionieren, aber dann müssten Sie sich merken, welche Ecke welche war. Die Ausrichtung eines F ist sofort zu erkennen.

F-Ausrichtung

Jede Form, deren Ausrichtung Sie erkennen können, würde funktionieren. Ich habe einfach das „F“ verwendet, seit ich die Idee zum ersten Mal kennengelernt habe.

WebGL-2D-Matrizen

In den letzten drei Kapiteln haben wir Ihnen gezeigt, wie Sie Geometrie übertragen, drehen und skalieren. Übersetzung, Drehung und Skalierung werden jeweils als eine Art von „Transformation“ betrachtet. Für jede dieser Transformationen waren Änderungen am Shader erforderlich und jede der drei Transformationen war reihenfolgeabhängig.

Hier sehen Sie zum Beispiel eine Skala von 2, 1, eine Drehung von 30 % und eine Übersetzung von 100, 0.

F-Drehung und Verschiebung

Und hier ist eine Übersetzung von 100,0, Drehung von 30% und Skalierung von 2, 1.

F-Drehung und -Skalierung

Die Ergebnisse sind völlig anders. Noch schlimmer: Wenn wir das zweite Beispiel brauchten, müssten wir einen anderen Shader schreiben, der die Übersetzung, Drehung und Skalierung in unserer neuen gewünschten Reihenfolge anwendete. Nun, einige Leute, die viel schlauer sind als ich, haben herausgefunden, dass man das Gleiche mit Matrizenberechnungen anstellen kann. Für 2d verwenden wir eine 3x3-Matrix. Eine 3 x 3-Matrix ist wie ein Raster mit 9 Feldern.

1.0 2 3
4.0 5 6.0
7.0 8.0 9.0

Für die Berechnung multiplizieren wir die Position in den Spalten der Matrix und addieren die Ergebnisse. Unsere Positionen haben nur 2 Werte, x und y, aber für diese Berechnung benötigen wir drei Werte, also verwenden wir 1 als dritten Wert. In diesem Fall lautet unser Ergebnis

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

Sie sehen sich das wahrscheinlich an und denken: „WAS SOLLTEN SIE TUN?“ Nehmen wir an, wir haben eine Übersetzung. Wir rufen den Betrag für die Übersetzung von tx und ty auf. Erstellen wir eine Matrix wie diese

1.00.00,0
0.01,00.0
txty1.0

Und jetzt schauen wir uns an,

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

Wenn du dich an deine Algebra erinnerst, können wir jeden Ort löschen, der mit Null multipliziert wird. Die Multiplikation mit 1 bewirkt nichts, daher vereinfachen wir die Abfrage, um zu sehen, was passiert.

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

Und extra ist uns gar nicht so wichtig. Das sieht überraschend wie der Übersetzungscode aus unserem Übersetzungsbeispiel aus. Nehmen wir die Rotation auf ähnliche Weise. Wie bereits im Rotationsbeitrag erwähnt, benötigen wir nur den Sinus und den Kosinus des Winkels, um den wir drehen möchten.

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

Und wir erstellen eine Matrix wie diese

c-s0.0
sc0.0
0,00.01.0

Bei Anwendung der Matrix erhalten wir das

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

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

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

Wenn wir alle Multiplikationen mit Nullen und Einsen schwärzen, erhalten wir

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

Genau das hatten wir in unserem Rotationsbeispiel. Und schließlich: Skalierung. Wir nennen unsere 2 Skalierungsfaktoren sx und sy und erstellen eine Matrix wie diese

SX0.00,0
0.0Sy0.0
0,00.01.0

Bei Anwendung der Matrix erhalten wir das

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

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

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

Das ist wirklich

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

Diese entspricht unserem Skalierungsbeispiel. Ich bin mir sicher, dass Sie noch darüber nachdenken. Was ist mein Vorteil? Was bringt es? Das erscheint mir nach einer Menge Arbeit, nur um dasselbe zu tun, was wir bereits gemacht haben? Hier kommt die Magie ins Spiel. Wie sich herausstellt, können wir Matrizen multiplizieren und alle Transformationen auf einmal anwenden. Angenommen, wir haben die Funktion matrixMultiply, die zwei Matrizen multipliziert und das Ergebnis zurückgibt. Zur Verdeutlichung wollen wir Funktionen zum Erstellen von Matrizen für Übersetzung, Rotation und Skalierung erstellen.

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

Ändern wir nun unseren Shader. Der alte Shader sah so aus:

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

Unser neuer Shader ist viel einfacher.

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

Und so verwenden wir sie

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

Aber vielleicht fragen Sie sich, was passiert. Das scheint kein großer Vorteil zu sein . Wenn wir jetzt jedoch die Reihenfolge ändern möchten, müssen wir keinen neuen Shader schreiben. Wir können einfach die Berechnung ändern.

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

Matrizen wie diese sind besonders wichtig für hierarchische Animationen wie Arme an einem Körper, Monde auf einem Planeten um eine Sonne oder Zweige an einem Baum. Für ein einfaches Beispiel einer hierarchischen Animation zeichnen wir unser „F“ fünfmal, aber beginnen wir jedes Mal mit der Matrix aus dem vorherigen „F“.

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

Dazu haben wir die Funktion makeIdentity eingeführt, mit der eine Identitätsmatrix erstellt wird. Eine Identitätsmatrix ist eine Matrix, die 1,0 darstellt, sodass bei einer Multiplikation mit der Identität nichts geschieht. Genau wie

X * 1 = X

also auch

matrixX * identity = matrixX

Hier ist der Code zum Erstellen einer Identitätsmatrix.

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

Ein weiteres Beispiel: In jedem der bisherigen Beispiele rotiert unser 'F' um die obere linke Ecke. Das liegt daran, dass die von uns verwendete Mathematik immer um den Ursprung rotiert und sich die obere linke Ecke von „F“ am Ursprung befindet (0, 0). Da wir jetzt Matrixberechnungen durchführen und die Reihenfolge auswählen können, in der die Transformationen angewendet werden, können wir den Ursprung verschieben, bevor die restlichen Transformationen angewendet werden.

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

Mit dieser Technik können Sie von jedem Punkt aus drehen oder skalieren. Jetzt wissen Sie, wie Sie mit Photoshop oder Flash den Drehpunkt verschieben können. Lass uns noch verrückter werden. Wenn Sie zum ersten Artikel zu den WebGL-Grundlagen zurückkehren, erinnern Sie sich vielleicht, dass wir Code im Shader haben, um von Pixeln in Clipspace zu konvertieren, der so aussieht.

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

Wenn Sie sich jeden dieser Schritte der Reihe nach ansehen, handelt es sich beim ersten Schritt, "convert from pixel to 0.0 to 1.0", eigentlich um einen Skalierungsvorgang. Der zweite Vorgang ist ebenfalls ein Skalierungsvorgang. Der nächste ist eine Übersetzung und der letzte skaliert Y um -1. Das können wir alles in der Matrix tun, die wir an den Shader übergeben. Wir könnten zwei Matrizen erstellen, eine mit 1,0/Auflösung, die andere mit 2,0, eine dritte mit -1,0/-1,0 und eine vierte zur Skalierung von Y mit -1, und multiplizieren sie anschließend miteinander. Da die Mathematik einfach ist, erstellen wir stattdessen einfach eine Funktion, die direkt eine „Projektion“-Matrix für eine bestimmte Auflösung erstellt.

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

Jetzt können wir den Shader noch weiter vereinfachen. Hier ist der neue Vertex-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>

Und in JavaScript müssen wir mit der Projektionsmatrix multiplizieren.

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

Wir haben auch den Code zum Festlegen der Auflösung entfernt. Mit diesem letzten Schritt sind wir von einem ziemlich komplizierten Shader mit 6-7 Schritten zu einem sehr einfachen Shader mit nur einem Schritt übergegangen, was die Magie der Matrizenberechnung verbessert.

Ich hoffe, dieser Artikel hat dazu beigetragen, die Matrizenberechnungen zu entmystifizieren. Als Nächstes beschäftige ich mich mit 3D. Die 3D-Matrixberechnungen folgen den gleichen Prinzipien und der gleichen Verwendung. Ich habe mit 2D angefangen, um es hoffentlich leicht verständlich zu machen.