WebGL מבצע טרנספורמציה

Gregg Tavares
Gregg Tavares

תרגום WebGL 2D

לפני שנעבור לתלת-ממד, בואו ניצמד בדו-ממד עוד קצת זמן. סבלנות היא שם המשחק. אולי מאמר זה ייראה ברור מאוד לחלק, אבל אציג נקודה מסוימת בכמה מאמרים.

המאמר הזה הוא המשך של סדרה שמתחילה ב-WebGL Fundamentals. אם לא קראת אותו, מומלץ לקרוא לפחות את הפרק הראשון ואז לחזור לכאן. תרגום הוא שם מתמטי מתוחכם, שלמעשה פירושו "להעביר" משהו. אני מניח שהעברת משפט מאנגלית ליפנית גם כן מתאימה, אבל במקרה הזה אנחנו מדברים על גיאומטריה נעה. בעזרת הקוד לדוגמה שהגענו איתו לפוסט הראשון, תוכל לתרגם בקלות את המלבן פשוט על ידי שינוי הערכים שהועברו ל-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);
  }

עד עכשיו, הכל טוב. אבל עכשיו נניח שאנחנו רוצים לעשות את אותו הדבר עם צורה מורכבת יותר. נניח שאנחנו רוצים לצייר F שמורכב מ-6 משולשים כאלה.

אות גדולה

בהמשך מופיע הקוד הנוכחי, שנצטרך לשנות את 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 מתבצעת פעם אחת בלבד. הוא כבר לא נמצא בתוך DrawingsScene.

עכשיו כשאנחנו מציירים את WebGL, הוא עושה כמעט הכול. כל מה שאנחנו עושים הוא להגדיר תרגום ולבקש ממנו לצייר. גם אם לגיאומטריה שלנו יהיו עשרות אלפי נקודות, הקוד העיקרי יישאר ללא שינוי.

סיבוב דו-ממד של WebGL

אני מודה מראש שאין לי מושג אם אני אסביר את זה בקול, אבל מה מתחשק לי לנסות.

קודם כל ברצוני להציג לך מה שנקרא "מעגל יחידה". אם אתם זוכרים את החישובים המתמטיים של חטיבת הביניים (אל תרדו עלי!), יש לכם רדיוס מעגלי. הרדיוס של מעגל הוא המרחק בין מרכז המעגל לקצה. מעגל יחידה הוא מעגל ברדיוס של 1.0.

אם זוכרים מתמטיקה בסיסית של כיתה ג', אם מכפילים משהו ב-1 הוא לא משתנה. למשל, 123 * 1 = 123. די בסיסי, נכון? ובכן, מעגל יחידה, מעגל ברדיוס של 1.0 הוא גם צורה של 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 כדי שנוכל להעביר את שני הערכים האלה.

  ...
  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. בוחרים נקודה על עיגול היחידה ב-30 מעלות בכיוון השעון החל משעה 12:00.

סיבוב של 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. יכול לחלק ב-1,000 בראשי.

רדיאן לעומת מעלות דומים. המעלות מקשות את החישוב. רדיאן עושים את החשבון בקלות. יש במעגל 360 מעלות, אבל יש רק 2± רדיאנים. לכן סיבוב מלא הוא 2± רדיאנים. חצי סיבוב נחשב לרדיאנים ~. סיבוב של 1/4, כלומר 90 דגרס הוא רדיאן DDEX/2. אם רוצים לסובב משהו ב-90 מעלות, פשוט משתמשים בפונקציה Math.PI * 0.5. כדי לסובב אותו ב-45 מעלות, צריך להשתמש ב-Math.PI * 0.25 וכו'.

כמעט כל סוגי המתמטיקה שכוללים זוויות, מעגלים או סיבוב פועלים בצורה פשוטה אם מתחילים לחשוב ברדיאנים. אז כדאי לנסות. יש להשתמש ברדיאנים, ולא במעלות, מלבד בתצוגות ממשק משתמש.

קנה מידה 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 הטרנספורמציות הייתה תלויה בסדר.

לדוגמה, כאן מוצג סולם של 2, 1, סיבוב של 30% ותרגום של 100, 0.

סיבוב F ותרגום

הנה תרגום של 100,0, סיבוב של 30% וסולם של 2, 1

סיבוב וקנה מידה של F

התוצאות שונות לחלוטין. ואפילו יותר גרוע, אם היינו צריכים את הדוגמה השנייה, היינו צריכים לכתוב תוכנת הצללה (shader) שונה שהחילה את התרגום, את הסיבוב ואת קנה המידה בסדר החדש הרצוי. יש אנשים הרבה יותר חכמים ממני, גיליתי שאפשר לעשות את אותם הדברים בעזרת מתמטיקה של מטריצה. בדו-ממד אנחנו משתמשים במטריצה של 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
txty1.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

אם אתם זוכרים את האלגברה שלכם, אנחנו יכולים למחוק כל מקום שמכפיל את הערך באפס. הכפלה ב-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 ואנחנו בונים מטריצה כזו

Sx0.00.0
0.0סי0.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, אולי חשוב שתזכרו שיש לנו קוד בהצללה (shader) כדי להמיר מפיקסלים למרחב קליפס שנראה כך.

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

עכשיו אפשר לפשט את תוכנת ההצללה עוד יותר. זהו כלי ההצללה (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>

וב-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 שלבים, והצללה (shader) פשוטה מאוד, עם רק שלב אחד בקסם של מתמטית מטריצות.

אני מקווה שהמאמר הזה עזר להבהיר את המסתורין במתמטיקה של מטריצות. אני אעבור לתלת-ממד בשלב הבא. במתמטיקה של מטריצות תלת-ממדיות, נעשה שימוש באותם עקרונות ואותם שימושים. התחלתי עם דו-ממד כדי שיהיה פשוט להבין אותו.