WebGL 變形

Gregg Tavares
Gregg Tavares

WebGL 2D 翻譯

我們再繼續介紹 3D 模式,讓我們先讓 2D 持續一段時間。請耐心等候。這篇文章看似明顯差異,但我預計會在幾篇文章中進一步加油。

本文將接續討論「WebGL 基礎知識」系列課程。如果您尚未閱讀完整內容,建議您至少閱讀第一章,然後再回到這裡。「翻譯」是一門花俏的數學名稱,基本上也就是「要移動」的東西。我想把句子從英文移動成日文也沒關係,但在這個範例中,我們是討論如何移動幾何圖形。使用我們在第一篇所生成的範例程式碼,只要將傳遞到 setRectangle 的值變更為右邊,就能輕鬆翻譯矩形。下列範例根據先前的範例所得出。

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

到目前還不錯。但現在我們有想採用更複雜的形狀。 假設我們想繪製一個由 6 個三角形構成的「F」,

F 字母

以下是目前的程式碼,我們必須將 setRectangle 變更為更類似的結果。

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

如您所見,這無法成功擴大規模。如要以數百或數千行繪製非常複雜的幾何圖形,我們就必須編寫一些相當複雜的程式碼。除此之外,每次繪製 JavaScript 時,都必須更新所有點。現在有更簡單的方法。只要上傳幾何圖形,然後在著色器中進行翻譯即可。 使用全新著色器

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

我們再稍微重新建構程式碼一個人只需要設定幾何圖形一次

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

接著,我們只需更新 u_translation,以使用想要的翻譯來繪圖。

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

請注意,setGeometry 只會呼叫一次。它已不在 drawScene 內。

現在,繪製 WebGL 時幾乎是處理所有內容。我們只需要設定翻譯,然後要求繪圖。即使我們的幾何圖形包含數萬個點,主要程式碼也會維持不變。

WebGL 2D 旋轉

我會先承認,大家可能不知道該怎麼做才合理,但也可能是箇中原因。

首先,我想為您介紹「單位圓圈」。如果你記得自己在高中數學 (不要睡我了!),圓圈裡有一個半徑。圓形的半徑是指圓心與邊緣之間的距離。單位圓形是半徑 1.0 的圓形。

如果你記得第 3 年級基礎數學中將某個項目乘以 1,會保持不變。所以 123 * 1 = 123。基本原則單位圓形、半徑 1.0 的圓形也都是 1 形式。是旋轉 1。你可以將這個單位圓形乘上 1 相乘,就像將魔法移動到 1 一樣,我們會從單位圓上的任何點擷取 X 和 Y 值,並將我們的幾何圖形乘以。 以下是著色器的更新內容。

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

接著,我們會更新 JavaScript 來傳入這 2 個值。

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

運作方式嗯,看一下數學。

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;

保持這個矩形,然後您想要旋轉矩形。開始旋轉前的右上角是 3.0、9.0。我們挑選從 12 點順時針單位圈出 30 度的點。

30 度旋轉

圓圈上的位置是 0.50 和 0.87

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

這就是我們想要的

旋轉繪圖

順時針旋轉 60 度

旋轉 60 度

圓圈上的位置是 0.87 和 0.50

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

您可以看到當我們順時針旋轉該點時,X 值會變大,Y 值也會變小。如果持續超過 90 度 X,則會再次縮小,Y 也會開始變大。才能使系統旋轉。單位圓圈上的點名稱不一樣。它們稱為正弦和餘弦。因此,任何特定角度的正弦和餘弦都只需像這樣查詢正弦和餘弦。

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

如果您將程式碼複製並貼到 JavaScript 控制台,然後輸入 printSineAndCosignForAngle(30),畫面上就會顯示 s = 0.49 c= 0.87 (注意:我會將數字四捨五入)。 只要將全部完成後,就能將幾何圖形旋轉至任何您想要的角度。只要根據您要旋轉的角度的正弦和餘弦設定旋轉即可。

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

希望以上說明對您有所幫助。接下來我要講解簡單的方法。擴充

什麼是弧度?

輻射是測量單位,與圓形、旋轉和角度搭配使用。就像我們可以透過英寸、碼、公尺等單位測量距離,也可以測量角度或弧度。

你或許已經知道,搭配公制測量的數學運算,比使用英制測量的數學更為簡單。為了從英寸到英尺,我們除以 12。為了從英寸變成碼,我們將除以 36。我不知道你,但我無法把頭除以 36。指標會更加容易從公釐到這裡,我們除以 10 公分。從公釐到公尺,將除以 1000。我可以除以 1000。

半徑和角度相近。分數會讓數學變得更加困難。拉迪亞人可以為您解惑。圓形中有 360 度,但只有 2π 弧度。因此完全轉回為 2π 弧度。半轉彎為 π 弧度。1/4 轉彎,也就是說 90 D 未知數為 π/2 弧度。因此,如果想旋轉 90 度,只要使用 Math.PI * 0.5 即可。如要旋轉 45 度,請使用 Math.PI * 0.25 等。

幾乎所有涉及角度、圓形或旋轉的數學運算,都相當簡單,如果你一開始就以弧度來思考。試試看吧。除了 UI 顯示畫面以外,請使用弧度而非度數。

WebGL 2D 比例

資源調度方式就像翻譯一樣簡單。

我們將位置乘以所需的尺度。以下是我們上一個範例的變更內容。

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

然後加入必要的 JavaScript 以便在繪製時設定比例

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

值得一提的是,使用負值進行縮放會翻轉我們的幾何圖形。希望以上 3 個章節都有助於瞭解平移、旋轉和縮放比例。接下來,我們會介紹矩陣的魔法,也就是將這 3 種元素結合起來,更方便,也更具實用價值。

為何出現「F」?

我第一次看到某人使用「F」是紋理。「F」本身不重要。重要的是,您可以從任何方向分辨方向。舉例來說,如果我們使用心形 ↂ 或三角形 △,則我們無法判斷是否已水平翻轉。圓形 ○ 則甚至可能更差。彩色矩形的每個角落應該都有不同顏色,但請務必記住哪個角落。F 的方向是立即識別的。

F 方向

我到這個概念「心」時,就用了「F」這個能判斷遊戲方向的形狀。

WebGL 2D 矩陣

在最後 3 個章節中,我們說明瞭如何翻譯幾何圖形、旋轉幾何圖形以及縮放幾何圖形。平移、旋轉和縮放都會視為一種「轉換」類型。這些轉換作業都需要變更著色器,這 3 項轉換作業則會因順序而異。

例如,這裡的範圍是 2、1、旋轉 30%,以及 100、0 的比例。

旋轉和平移

這裡的翻譯是 100,000,旋轉 30%,縮放 2、1

F 旋轉和縮放

結果大同小異。更糟的是,在需要第二個範例時,我們必須編寫不同的著色器,以新的所需順序進行平移、旋轉和縮放。嗯,有些人比我聰明,發現你可以透過矩陣數學完成所有事情,針對 2D ,我們使用 3x3 矩陣。3x3 矩陣就像有 9 個方塊的格狀清單,

1.0 2.0 3.0
4.0 5.0 6.0
7.0 8.0 9.0

如要計算,我們會將矩陣的兩欄下乘以矩陣,然後將得出的結果加總。我們的位置只有 2 個值 (x 和 y),但要執行這個計算,我們需要 3 個值,因此針對第三個值,我們使用 1。

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

你們大概會想著「哪裡?」假設我們要翻譯我們就要呼叫 tx 和 ty 的翻譯量我們來製作像這樣的矩陣

1.00.00.0
0.01.00.0
tx1.0

馬上一探究竟

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

如果你記得代數,可以刪除任何乘法乘以 0 的位置。以 1 相乘效果不會真的沒有問題,讓我們簡化以瞭解實際情況

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

還有一點我們完全不在意這看起來有點出乎意料,與翻譯範例中的翻譯程式碼相似。同樣地,我們一起旋轉就像我們向旋轉貼文說的例子,只需要使用要旋轉的角度正弦和餘弦。

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

我們要建構像這樣的矩陣

c- 秒0.0
c0.0
0.00.01.0

套用我們得出的矩陣

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

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

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

塗黑所有文字乘以 0 和 1s

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

這正是輪替樣本中的確切值。最後就能擴大規模我們將 2 個縮放比例係數 Sx, Sy 然後建構像這樣的矩陣

0.00.0
0.0sy0.0
0.00.01.0

套用我們得出的矩陣

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

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

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

這確實是

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

這與縮放樣本相同。現在我確定您可能還在構思。怎麼了?重點。看來我們做的事情其實太多了? 這時魔法就能派上用場。之後,我們可以把矩陣相乘,然後一次套用所有變換。假設我們有 matrixMultiply 函式,該函式會使用兩個矩陣,相乘並傳回結果。為了讓畫面更加清楚,我們來建構函式,以便製作平移、旋轉和縮放的矩陣。

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

現在,讓我們變更著色器。舊版著色器看起來像這樣

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

新的著色器將變得更加簡單。

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

以下說明我們如何使用

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

即便如此,您可能會想知道,請問是什麼?這似乎不是很多好處。但是,如果想要變更順序,不必編寫新的著色器。我們只能改變算數

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

這對於套用這類矩陣來說特別重要,例如身體上的手臂、太陽周圍星球上的月亮或樹上的樹枝。如果是階層式動畫的簡單範例,您可繪製「F」5 次,但每次都應從前一個「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);
    }
  }

為此,我們導入了可製作識別矩陣的函式 makeIdentity。識別矩陣是能有效代表 1.0 的矩陣,因此如果您將識別資訊相乘,則不會有任何作用。就像這樣

X * 1 = X

我也知道

matrixX * identity = matrixX

以下是用來製作識別矩陣的程式碼。

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

舉例來說,到目前為止,在每個樣本中,「F」會在左上角旋轉。這是因為我們使用的數學運算一律會圍繞原點,而「F」的左上角是由原點旋轉 (0, 0) 但是現在,由於我們有矩陣數學運算,可以選擇套用變換的順序,因此可以在套用變形作業之前,先移動原點。

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

使用這項技術即可從任何點旋轉或縮放。現在您已瞭解 Photoshop 或 Flash 如何移動旋轉點。 讓我們更瘋狂吧。如果返回 WebGL 基礎知識的第一篇文章,您可能記得我們有著色器中的程式碼,會將像素從像素轉換成類似的裁剪空間。

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

如果依序檢視每一個步驟,第一個步驟「從像素轉換成 0.0 到 1.0」真的是縮放運算。第二種也是縮放作業。接下來是翻譯,最後一個則是以 Y 乘以 1。我們可以在傳入著色器的矩陣中執行所有動作。我們可以製作 2 個縮放矩陣,一個矩陣乘以 1.0/解析度,另一個乘以 2.0,另一個乘以 -1.0、-1.0 和第 4 個縮放 Y 乘以 -1,接著再乘以數學值。不過,我們只要製作一個函式,就能直接做出「投影」矩陣。

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

現在,我們可以進一步簡化著色器。以下是全新的頂點著色器。

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

在 JavaScript 中,我們需要將投影矩陣

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

此外,我們也移除了設定解析度的程式碼。最後一個步驟,我們已從較為複雜的著色器,從 6 到 7 個步驟變成簡單的著色器,只需 1 步驟就能實現矩陣數學的神奇效果。

希望這篇文章能幫助釐清矩陣數學的奧秘。我接著進入 3D 模式3D 矩陣的數學運算原則和用法相同。我一開始是運用 2D 設計,希望大家能保持簡單易懂。