การเปลี่ยนรูปแบบ WebGL

การแปล 2 มิติของ WebGL

ก่อนจะไปดูภาพ 3 มิติ เรามาลองดูภาพ 2 มิติกันก่อน โปรดรอสักครู่ บทความนี้อาจดูชัดเจนมากสำหรับบางคน แต่เราจะค่อยๆ อธิบายในบทความ 2-3 บทความ

บทความนี้เป็นส่วนหนึ่งของชุดบทความที่เริ่มต้นด้วยพื้นฐานของ 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);
  }

จนถึงตอนนี้ทุกอย่างเรียบร้อยดี แต่ลองจินตนาการว่าเราต้องการทำสิ่งเดียวกันนี้กับรูปร่างที่ซับซ้อนมากขึ้น สมมติว่าเราต้องวาด "F" ที่ประกอบด้วยสามเหลี่ยม 6 รูปเช่นนี้

ตัวอักษร 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 จะทําเกือบทุกอย่าง สิ่งที่เราทําคือตั้งค่าการแปลและขอให้วาด แม้ว่าเรขาคณิตจะมีจุดหลายหมื่นจุด โค้ดหลักก็จะยังคงเหมือนเดิม

การหมุน 2 มิติของ WebGL

เราขอยอมรับตั้งแต่ต้นเลยว่าไม่รู้เลยว่าการอธิบายนี้จะฟังเข้าใจไหม แต่เอาเถอะ ลองดูกัน

ก่อนอื่น เราขอแนะนำสิ่งที่เรียกว่า "วงกลมหน่วย" หากคุณจำคณิตศาสตร์สมัยมัธยมต้นได้ (อย่าเพิ่งหลับไป!) วงกลมจะมีรัศมี รัศมีของวงกลมคือระยะทางจากศูนย์กลางของวงกลมถึงขอบ วงกลมหน่วยคือวงกลมที่มีรัศมี 1.0

หากจำได้จากคณิตศาสตร์พื้นฐานระดับชั้นประถมศึกษาปีที่ 3 การคูณด้วย 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 เพื่อให้ส่งค่า 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 ให้เลือกจุดบนวงกลมหน่วย 30 องศาตามเข็มนาฬิกาจาก 12 น.

การหมุน 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 หากต้องการแปลงจากมิลลิเมตรเป็นเมตร ให้หารด้วย 1,000 ฉันสามารถหารด้วย 1,000 ในใจ

เรเดียนกับองศามีความคล้ายคลึงกัน องศาทําให้คํานวณยาก หน่วยเรเดียนช่วยให้คณิตศาสตร์เป็นเรื่องง่าย วงกลมมี 360 องศา แต่มีเพียง 2π เรเดียน ดังนั้นการหมุนเต็มรอบคือ 2π เรเดียน การบิดครึ่งรอบเท่ากับ π เรเดียน การบิด 1/4 รอบ หรือ 90 องศาคือ π/2 เรเดียน ดังนั้นหากต้องการหมุน 90 องศา ให้ใช้ Math.PI * 0.5 หากต้องการหมุน 45 องศา ให้ใช้ Math.PI * 0.25 เป็นต้น

คณิตศาสตร์เกือบทั้งหมดที่เกี่ยวข้องกับมุม วงกลม หรือการหมุนจะทำงานได้ง่ายมากหากคุณเริ่มคิดเป็นเรเดียน ลองใช้ดู ใช้เรเดียน ไม่ใช่องศา ยกเว้นในการแสดงผล UI

สเกล 2 มิติของ WebGL

การปรับขนาดนั้นง่ายพอๆ กับการแปล

เราคูณตำแหน่งด้วยมาตราส่วนที่ต้องการ การเปลี่ยนแปลงจากตัวอย่างก่อนหน้านี้มีดังนี้

<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" มาตั้งแต่ "F"irst รู้จักแนวคิดนี้

เมทริกซ์ 2 มิติของ WebGL

ใน 3 บทที่ผ่านมา เราได้พูดถึงวิธีแปลเชิงเรขาคณิต หมุนเชิงเรขาคณิต และปรับขนาดเชิงเรขาคณิต การเปลี่ยนตำแหน่ง การหมุน และการปรับขนาดถือเป็น "การเปลี่ยนรูปแบบ" ประเภทหนึ่งๆ การเปลี่ยนรูปแบบแต่ละรายการเหล่านี้จำเป็นต้องมีการเปลี่ยนแปลงที่ชิดเดอร์ และการเปลี่ยนรูปแบบทั้ง 3 รายการจะขึ้นอยู่กับลำดับ

ตัวอย่างเช่น นี่คือมาตราส่วน 2, 1 การหมุน 30% และการแปล 100, 0

การหมุนและการเปลี่ยนตำแหน่ง F

และนี่คือการเปลี่ยนตำแหน่ง 100,0, การหมุน 30% และการปรับขนาด 2, 1

การหมุนและการปรับขนาด F

ผลลัพธ์จะแตกต่างกันอย่างสิ้นเชิง ที่แย่กว่านั้นคือ หากต้องการใช้ตัวอย่างที่ 2 เราจะต้องเขียนโปรแกรมเปลี่ยนสีแบบอื่นที่ใช้การแปล การหมุน และการปรับขนาดตามลำดับใหม่ที่ต้องการ แต่บางคนที่ฉลาดกว่าฉันมากพบว่าคุณทำสิ่งเดียวกันทั้งหมดได้ด้วยคณิตศาสตร์เมทริกซ์ สำหรับ 2 มิติ เราจะใช้เมทริกซ์ 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 เป็นค่าที่ 3 ในกรณีนี้ผลลัพธ์ที่ได้คือ

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
txขอบคุณ1.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-s0.0
sc0.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 และ 1 ทั้งหมดแล้ว เราจะได้

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

ซึ่งตรงกับที่เรามีในตัวอย่างภาพสลับ และสุดท้ายคือการปรับขนาด เราจะเรียกตัวประกอบมาตราส่วน 2 รายการว่า sx และ sy แล้วสร้างเมทริกซ์ดังนี้

sx0.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 ที่รับเมทริกซ์ 2 รายการ คูณกัน แล้วแสดงผลลัพธ์ มาดูการสร้างฟังก์ชันเพื่อสร้างเมทริกซ์สําหรับการเปลี่ยนรูปแบบ การหมุน และการปรับขนาดกันเพื่อให้เข้าใจได้ง่ายขึ้น

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

มาเปลี่ยนชิเดอร์กัน 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;
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);
  }

แต่คุณอาจสงสัยว่า "แล้วมันสำคัญยังไง" ดูเหมือนว่าจะไม่มีประโยชน์มากนัก แต่ตอนนี้หากต้องการเปลี่ยนลำดับ เราก็ไม่ต้องเขียน Shader ใหม่ เราเปลี่ยนการคำนวณได้

    ...
    // 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" คือการดำเนินการปรับขนาด ส่วนการดำเนินการที่ 2 ก็เป็นการดำเนินการเกี่ยวกับขนาดเช่นกัน รายการถัดไปคือการเปลี่ยนตำแหน่ง และรายการสุดท้ายจะปรับขนาด Y เป็น -1 ซึ่งเราทําได้ทั้งหมดในเมทริกซ์ที่เราส่งไปยังโปรแกรมเปลี่ยนสี เราอาจสร้างเมทริกซ์การปรับขนาด 2 รายการ โดยรายการหนึ่งจะปรับขนาดเป็น 1.0/ความละเอียด อีกรายการจะปรับขนาดเป็น 2.0 รายการที่ 3 จะแปลเป็น -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 ขั้นตอนเท่านั้น ทั้งหมดนี้เกิดขึ้นจากเวทมนตร์ของคณิตศาสตร์เมทริกซ์

เราหวังว่าบทความนี้จะทําให้คณิตศาสตร์เมทริกซ์เข้าใจง่ายขึ้น เราจะเปลี่ยนไปดูภาพ 3 มิติกัน ในเมทริกซ์ 3 มิติ คณิตศาสตร์จะเป็นไปตามหลักการและการใช้งานเดียวกัน เราเริ่มต้นด้วยภาพ 2 มิติเพื่อให้เข้าใจได้ง่าย