Przekształcenia w WebGL

Gregg Tavares
Gregg Tavares

Tłumaczenie WebGL 2D

Zanim przejdziemy do 3D, pozostańmy przy 2D jeszcze przez chwilę. Proszę o wyrozumiałość. Dla niektórych ten artykuł może wydawać się oczywisty, ale wyjaśnię w kilku artykułach.

Ten artykuł jest kontynuacją serii, zaczynając od podstaw WebGL. Jeśli jeszcze jej nie znasz, przeczytaj przynajmniej pierwszy rozdział, a potem wróć tutaj. Tłumaczenie to skomplikowana nazwa matematyczna, która w zasadzie oznacza „poruszać się”. Myślę, że przeniesienie zdania z języka angielskiego na japoński też się sprawdza, ale w tym przypadku mówimy o ruchomej geometrii. Korzystając z przykładowego kodu, który zamieściliśmy w pierwszym poście, możesz łatwo przetłumaczyć nasz prostokąt, zmieniając wartości przekazywane do parametru setPercentage? Oto próbka na podstawie poprzedniej próbki.

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

Idzie Ci doskonale. Wyobraźmy sobie jednak, że chcemy uzyskać to samo, używając bardziej złożonego kształtu. Załóżmy, że chcemy narysować literę „F” złożoną z 6 trójkątów takich jak ten.

Litera F

Poniżej znajdziesz obecny kod, który musielibyśmy zmienić na podobny zestaw.

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

Mam nadzieję, że nie uda się tego odpowiednio skalować. Gdybyśmy chcieli narysować bardzo złożone elementy geometryczne przy użyciu setek czy tysięcy linii, musielibyśmy napisać dość złożony kod. Ponadto za każdym razem, gdy rysujemy, JavaScript musi aktualizować wszystkie punkty. Jest prostszy sposób. Po prostu prześlij geometrię i przeprowadź tłumaczenie w cieniu. Oto nowy program do cieniowania

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

a potem zmienimy jego strukturę. W przypadku jednego z nich wystarczy ustawić geometrię tylko raz.

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

Następnie musimy zaktualizować u_translation, zanim będziemy mogli korzystać z oczekiwanego tłumaczenia.

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

Uwaga: numer setGeometry został wywołany tylko raz. Nie ma go już w danym Scene.

Teraz przy rysowaniu WebGL robimy praktycznie wszystko. Teraz tylko ustawiamy tłumaczenie i prosimy je o rysowanie. Nawet gdyby nasza geometria miała dziesiątki tysięcy punktów, główny kod pozostanie bez zmian.

Rotacja 2D WebGL

przyznam na początku, że nie mam pojęcia, czy to wyjaśnić, ale chociaż w ogóle to mogę spróbować.

Na początku chcę przedstawić Ci tzw. „krąg jednostki”. Jeśli pamiętasz swoją matematykę z gimnazjum w szkole średniej (nie idź spać na mnie!), koło ma promień. Promień okręgu to odległość od jego środka do jego krawędzi. Koło jednostkowe to okrąg o promieniu 1,0.

Po pomnożeniu czegoś przez 1 z podstaw matematyki w klasie 3 nic się nie zmieni. Czyli 123 * 1 = 123. To dość podstawowe, prawda? Okrąg jednostkowy, okrąg o promieniu 1,0, jest również formą 1. Jest to 1 z obrotem. Możesz więc pomnożyć coś przez to koło jednostkowe, czyli w sposób podobny do pomnożenia przez 1. Nie dzieje się tak tylko magiczne zjawisko, a obiekty się obracają. Pobierzemy wartości X i Y z dowolnego punktu okręgu jednostkowego, a następnie pomnożymy przez nie geometrię przez te wartości z poprzedniej próbki. Oto aktualizacje naszego programu do cieniowania.

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

Następnie aktualizujemy JavaScript tak, aby można było przekazać te 2 wartości.

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

Dlaczego to działa? No to spójrz na matematykę.

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;

Przyjmijmy, że masz prostokąt, który chcesz obrócić. Zanim zaczniesz go obracać, jego prawy górny róg ma wartość 3,0, 9,0. Wybierzmy punkt na okręgu jednostki przesuniętym o 30 stopni w prawo od godziny 12.

Obrót o 30 stopni

Pozycja na okręgu wynosi 0,50 i 0,87.

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

Potrzebujemy go właśnie na tym etapie.

Rysunek obrotowy

Tak samo dla 60 stopni w prawo

Obrót o 60 stopni

Pozycja na okręgu wynosi 0,87 i 0,50.

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

Jak widać, przy obracaniu tego punktu w prawo wartość X staje się większa, a Y maleje. Przy 90 stopniach X ponownie zacznie się zmniejszać, a Y zacznie się powiększać. Ten wzór daje nam obrót. Okrąg jednostkowy ma inną nazwę. Są to tzw. sinus i cosinus. Dla każdego kąta możemy więc po prostu wyszukać sinus i cosinus w ten sposób.

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

Jeśli skopiujesz kod i wkleisz go w konsoli JavaScriptu oraz wpiszesz printSineAndCosignForAngle(30), kod zostanie wyświetlony s = 0.49 c= 0.87 (uwaga: liczby zostały zaokrąglone). Po połączeniu wszystkich elementów możesz obracać geometrię pod dowolnym kątem. Po prostu ustaw obrót na sinus i cosinus kąta, pod który chcesz obrócić.

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

Mam nadzieję, że te informacje okażą się przydatne. Potem prostsza wersja. Skalowanie:

Czym są radiany?

Radian to jednostka miary używana w przypadku okręgów, obrotu i kątów. Podobnie jak odległość w calach, ydach czy metrach, możemy też mierzyć kąty w stopniach lub radianach.

Pewnie wiesz, że matematyka z pomiarami metryk jest łatwiejsza niż matematyka z pomiarami imperialnymi. Aby przejść z cali na stopy, dzielimy przez 12. Aby przejść z cali na jardy, dzielimy przez 36. Nie wiem jak Ty, ale nie mogę dzielić w głowie przez 36. Jest to znacznie łatwiejsze w przypadku danych. Aby przejść z milimetrów na cm, dzielimy przez 10. Dzielimy to z milimetrów na metry przez 1000. Mogę podzielić w głowie przez 1000.

Radian i stopnie są podobne. Stopnie sprawiają, że matematyka jest trudna. Radiany ułatwiają liczenie. W okręgu jest 360 stopni, a radiany 2π. Zatem pełny obrót to radiany 2π. Połowa obrotu to π radiany. 1/4 obrót, tj. 90 stopni, to π/2 radian. Jeśli więc chcesz obrócić coś o 90 stopni, użyj funkcji Math.PI * 0.5. Jeśli chcesz obrócić go o 45 stopni, użyj Math.PI * 0.25 itp.

Prawie wszystkie działania matematyczne obejmujące kąty, okręgi i obrót działają w prosty sposób, jeśli zaczniesz myśleć w radianach. Spróbuj. Używaj radianów, a nie stopni, z wyjątkiem informacji wyświetlanych w interfejsie.

Skala 2D WebGL

Skalowanie jest równie łatwe jak tłumaczenie.

Mnożymy pozycję przez żądaną skalę. Oto zmiany w porównaniu z poprzednim przykładem.

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

i dodajemy JavaScript niezbędny do określenia skali podczas rysowania.

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

Jedną z rzeczy, które warto zauważyć, jest to, że skalowanie o wartość ujemną odwraca naszą geometrię. Mam nadzieję, że te 3 ostatnie rozdziały pomogły zrozumieć tłumaczenie, obrót i skalę. W następnej kolejności omówimy magię, która polega na połączeniu wszystkich tych 3 elementów w prostszą i często bardziej przydatną formę.

Dlaczego litera „F”?

Gdy po raz pierwszy zobaczyłam, że ktoś używa litery „F”, była tekstura. Sama litera „F” nie ma znaczenia. Ważne jest, aby można było określić jej orientację z dowolnego kierunku. Gdybyśmy na przykład wykorzystali serce ♥ lub trójkąt △, nie wiemy, czy zdjęcie zostało obrócone w poziomie. Krąg ○ jest jeszcze gorszy. Kolorowy prostokąt zapewne sprawdzałby się w różnych kolorach na każdym rogu, ale trzeba było pamiętać, który jest który. Format litery F jest natychmiast rozpoznawalny.

Orientacja F

Każdy kształt, który dałoby się rozpoznać w orientacji, sprawdzi się. W pierwszej kolejności używałem litery „F”.

Matryce WebGL 2D

W ostatnich 3 rozdziałach omówiliśmy sposób tłumaczenia geometrii, obracania geometrii i skalowania geometrii. Przesunięcie, obrót i skala to jeden z rodzajów „przekształcenia”. Każda z tych przekształceń wymagała zmian w cieniowaniu, a każda z nich była zależna od kolejności.

Na przykład jest to skala 2, 1, obrót 30% i przesunięcie 100, 0.

Obrót i przesunięcie F

A oto przesunięcie 100,0, obrót 30% i skala 2, 1.

Obrót i skala F

Wyniki są zupełnie inne. Co gorsza, gdybyśmy potrzebowali drugiego przykładu, musielibyśmy napisać inny program do cieniowania, który uwzględniłby przesunięcie, obrót i skalowanie w nowej kolejności. Niektórzy ludzie są dużo mądrzejsi ode mnie i odkryli, że z matematyki macierzystej wszystko potrafi zrobić to samo. W przypadku grafiki 2D używamy matrycy 3 x 3. Macierz 3 x 3 jest jak siatka z 9 polami.

1.0 2,0 3,0
4.0 5,0 6.0
7.0 z Androidem 8.0 9.0

Aby wykonać obliczenia, mnożymy pozycję w dół kolumn macierzy i dodajemy wyniki. Nasze pozycje mają tylko dwie wartości – x i y, ale do wykonania obliczeń potrzebujemy 3 wartości. Trzecią wartością będzie 1. W tym przypadku wynik będzie miał postać

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

Zastanawiasz się pewnie nad tym i zastanawiasz się, gdzie to jest. Załóżmy, że mamy tłumaczenie. Nazywamy kwotę, którą chcemy przetłumaczyć (tx i ty). Utwórzmy taką macierz

1.00.00.0
0.01.00.0
txty1.0

A teraz wypróbuj

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

Jeśli pamiętasz swoją algebrę, możesz usunąć dowolne miejsce mnożone przez 0. Pomnożenie przez 1 nic nie daje, więc uprośćmy to, aby zobaczyć, co się dzieje.

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

A poza tym tak naprawdę nie zależy nam na tym. Wygląda to zaskakująco jak kod tłumaczenia z naszego przykładu tłumaczenia. Zrób to samo. Jak już wspomnieliśmy przy obracaniu, potrzebujemy tylko sinusa i cosinusa kąta, pod którym ma być obrócony kąt.

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

Tworzymy taką macierz

z– s0.0
sz0.0
0.00.01.0

Stosuję macierz, który otrzymujemy

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

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

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

Zamazując wszystkie, pomnóż wynik przez 0 i 1

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

Dokładnie tego, co mieliśmy w próbce rotacji. I wreszcie należy skalować. Nazywamy 2 czynniki skali „sx” i „sy”. Utworzymy macierz w postaci tej

sx0.00.0
0.0Sy0.0
0.00.01.0

Stosuję macierz, który otrzymujemy

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

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

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

co jest naprawdę

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

Jest taka sama jak w naszej próbce skalowania. Na pewno wciąż jeszcze myślisz. Co z tego wynika? O co chodzi? Wydaje mi się, że to dużo pracy, żeby stworzyć coś, co do tej pory robiliśmy? Tutaj wkracza do akcji magia. Okazuje się, że można mnożyć macierze i stosować wszystkie przekształcenia jednocześnie. Przyjmijmy, że mamy funkcję matrixMultiply, która pobiera dwa macierze, mnoży je i zwraca wynik. Aby to wyjaśnić, zajmijmy się funkcjami tworzącymi macierze przesunięcia, obrotu i skali.

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

Zmieńmy teraz nasz cieniowanie. Stary program do cieniowania wyglądał tak

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

Nasz nowy program do cieniowania będzie znacznie prostszy.

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

Oto jak wykorzystujemy

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

Być może zastanawiasz się, co? Nie wygląda to na dużą korzyść . Teraz jeśli chcemy zmienić kolejność, nie musimy pisać nowego programu do cieniowania. Możemy po prostu zmienić liczenie.

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

Możliwość zastosowania takich matryc jest szczególnie ważna w przypadku animacji hierarchicznych, takich jak ręce na ciele, księżyce na planecie wokół Słońca czy gałęzie na drzewie. Prosty przykład animacji hierarchicznej pozwala narysować literę „F” 5 razy, ale za każdym razem zaczynamy od macierzy z poprzedniej wartości „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);
    }
  }

W tym celu wprowadziliśmy funkcję makeIdentity, która tworzy macierz tożsamości. Macierz tożsamości to macierz, która faktycznie reprezentuje 1,0, dzięki czemu po pomnożeniu przez tożsamość nic się nie wydarzy. Tak jak

X * 1 = X

też

matrixX * identity = matrixX

Oto kod do utworzenia macierzy tożsamości.

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

I jeszcze jeden przykład. Do tej pory w każdej próbce litera „F” obraca się wokół lewego górnego rogu. Dzieje się tak, ponieważ nasze obliczenia matematyczne zawsze obracają się wokół początku, a lewy górny róg „F” znajduje się na początku (0, 0). Teraz, gdy mamy działanie matematyczne, możemy wybrać kolejność stosowania przekształceń, możemy przesunąć punkt początkowy, zanim zastosujemy pozostałe przekształcenia.

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

Korzystając z tej techniki, możesz obracać widok i skalować go w dowolnym punkcie. Wiesz już, jak program Photoshop lub Flash pozwala przesunąć punkt obrotu. Zróbmy jeszcze więcej szaleństwa. Jeśli wrócisz do pierwszego artykułu o podstawach WebGL, być może pamiętasz, że w cieniowaniu znajduje się kod do konwertowania pikseli w obiekt clipspace, który wygląda tak.

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

Jeśli analizujesz każdy z tych kroków po kolei, pierwszy krok „przekonwertuj z pikseli na 0,0 na 1,0” to tak naprawdę operacja skalowania. Druga to operacja na skalę. Następne jest tłumaczenie, a ostatnie skaluje się z Y do -1. Wszystko to robimy w matrycy, którą przekazujemy do cieniowania. Moglibyśmy utworzyć 2 macierze skali, jedną ze skalą 1,0/rozdzielczość, drugą do skalowania w 2,0, 3, aby przełożyć ją przez -1,0,-1,0 i czwartą, aby skalować Y przez -1, a następnie pomnożyć je przez siebie. Ponieważ obliczenia są proste, utworzymy funkcję, która bezpośrednio tworzy macierz „rzutowania” dla danego rozwiązania.

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

Teraz możemy jeszcze bardziej uprościć program do cieniowania. Oto cały nowy cieniowanie wierzchołków.

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

W JavaScripcie musimy pomnożyć wynik przez macierz odwzorowania,

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

Usunęliśmy też kod określający rozdzielczość. W ostatnim kroku zrezygnowaliśmy ze skomplikowanego programu do cieniowania obejmującego 6–7 kroków. Teraz wystarczy 1 krok, aby przejść do magii matrycy macierzy.

Mam nadzieję, że ten artykuł pomógł Ci zrozumieć matematykę macierzową. Przejdę do trybu 3D. W przypadku macierzy 3D matematyka jest oparta na tych samych zasadach i zastosowaniu. Zaczęłam od 2D, żeby ułatwić zrozumienie.