二维矩阵转化 】每种变换都改变了着色器并且这些变换还受先后顺序影响。在 前例中我们先缩放,再旋转,最后平移,如果执行顺序不同 结果也不同。

一. 矩阵转化的基本原理

例如这是缩放 2, 1 ,旋转30度,然后平移 100, 0 的结果。
image.png

平移 100, 0 ,旋转30度,然后缩放 2, 1 的结果
image.png
聪明的人可能已经想到了矩阵,对于二维我们使用 3x3 的矩阵, 3x3 的矩阵就像是有9个格子的格网
1.0 2.0 3.0
4.0 5.0 6.0
7.0 8.0 9.0
在计算的时候我们将位置坐标沿着矩阵列的方向依次相乘再将结果加起来。 我们的位置信息只有两个值, x 和 y 。但是要进行运算需要三个值, 所以我们将第三个值赋值为 1 。
在这个例子中结果将是

newX = x * 1.0 + newY = x * 2.0 + extra = x * 3.0 +
y * 4.0 + y * 5.0 + y * 6.0 +
1 * 7.0 1 * 8.0 1 * 9.0

你可能会想“这样做有什么意义?”,好吧,假设我们要进行平移, 平移的量为 tx 和 ty ,然后定义一个这样的矩阵
1.0 0.0 0.0
0.0 1.0 0.0
tx ty 1.0
然后计算结果

newX = x * 1.0 + newY = x * 0.0 + extra = x * 0.0 +
y * 0.0 + y * 1.0 + y * 0.0 +
1 * tx 1 * ty 1 * 1.0

如果你还记得线性代数的知识,我们可以删除和 0 相乘的部分, 和 1 相乘相当于没变,所以简化后为

newX = x * 1.0 + newY = x * 0.0 + extra = x * 0.0 +
y * 0.0 + y * 1.0 + y * 0.0 +
1 * tx 1 * ty 1 * 1.0

或者更简洁
newX = x + tx;
newY = y + ty;

其他的就不用关心了。这个看起来和平移例子中的代码有些相似。
同样的来实现旋转,在旋转章节提到过,旋转只需要和旋转角对应的正弦和余弦值
s = Math.sin(angleToRotateInRadians);
c = Math.cos(angleToRotateInRadians);
然后我们创建一个这样的矩阵

c -s 0.0
s c 0.0
0.0 0.0 1.0

使用矩阵后得到

newX = x * c + newY = x * -s + extra = x * 0.0 +
y * s + y * c + y * 0.0 +
1 * 0.0 1 * 0.0 1 * 1.0

遮住没有意义的部分(和 0 或 1 相乘的部分)
然后得到简化版
newX = x c + y s;
newY = x -s + y c;

正是我们在旋转例子中得到的结果。
同理我们可以在缩放的例子中的得到
newX = x * sx;
newY = y * sy;
现在你可能还会想“那又怎样,有什么意义?”, 好像花了更多精力做之前做过的事情。
现在开始有趣的部分了,相乘后他们可以用一个矩阵代表三个变换, 假定有一个方法m3.multiply可以将两个矩阵相乘并返回结果。
为了方便讲解,我们先创建平移,旋转和缩放矩阵。

  1. var m3 = {
  2. translation: function(tx, ty) {
  3. return [
  4. 1, 0, 0,
  5. 0, 1, 0,
  6. tx, ty, 1,
  7. ];
  8. },
  9. rotation: function(angleInRadians) {
  10. var c = Math.cos(angleInRadians);
  11. var s = Math.sin(angleInRadians);
  12. return [
  13. c,-s, 0,
  14. s, c, 0,
  15. 0, 0, 1,
  16. ];
  17. },
  18. scaling: function(sx, sy) {
  19. return [
  20. sx, 0, 0,
  21. 0, sy, 0,
  22. 0, 0, 1,
  23. ];
  24. },
  25. };
  26. // 现在该修改着色器了,原来的着色器像这样
  27. <script id="vertex-shader-2d" type="x-shader/x-vertex">
  28. attribute vec2 a_position;
  29. uniform vec2 u_resolution;
  30. uniform vec2 u_translation;
  31. uniform vec2 u_rotation;
  32. uniform vec2 u_scale;
  33. void main() {
  34. // 缩放
  35. vec2 scaledPosition = a_position * u_scale;
  36. // 旋转
  37. vec2 rotatedPosition = vec2(
  38. scaledPosition.x * u_rotation.y + scaledPosition.y * u_rotation.x,
  39. scaledPosition.y * u_rotation.y - scaledPosition.x * u_rotation.x);
  40. // 平移
  41. vec2 position = rotatedPosition + u_translation;
  42. ...
  43. // 新着色器就简单多了。
  44. <script id="vertex-shader-2d" type="x-shader/x-vertex">
  45. attribute vec2 a_position;
  46. uniform vec2 u_resolution;
  47. uniform mat3 u_matrix;
  48. void main() {
  49. // 将位置乘以矩阵
  50. vec2 position = (u_matrix * vec3(a_position, 1)).xy;
  51. ...
  52. // 使用方法
  53. // 绘制场景
  54. function drawScene() {
  55. ,,,
  56. // 计算矩阵
  57. var translationMatrix = m3.translation(translation[0], translation[1]);
  58. var rotationMatrix = m3.rotation(angleInRadians);
  59. var scaleMatrix = m3.scaling(scale[0], scale[1]);
  60. // 矩阵相乘
  61. var matrix = m3.multiply(translationMatrix, rotationMatrix);
  62. matrix = m3.multiply(matrix, scaleMatrix);
  63. // 设置矩阵
  64. gl.uniformMatrix3fv(matrixLocation, false, matrix);
  65. // 绘制图形
  66. gl.drawArrays(gl.TRIANGLES, 0, 18);
  67. }

二 . 点在二维空间中点基本运动

  1. <canvas id="canvas"></canvas>
  2. <div id="uiContainer">
  3. <div id="ui">
  4. <div id="x"></div>
  5. <div id="y"></div>
  6. <div id="angle"></div>
  7. <div id="scaleX"></div>
  8. <div id="scaleY"></div>
  9. </div>
  10. </div>
  11. <!-- vertex shader -->
  12. <script id="vertex-shader-2d" type="x-shader/x-vertex">
  13. attribute vec2 a_position;
  14. uniform vec2 u_resolution;
  15. uniform mat3 u_matrix;
  16. void main() {
  17. // Multiply the position by the matrix.
  18. vec2 position = (u_matrix * vec3(a_position, 1)).xy;
  19. // convert the position from pixels to 0.0 to 1.0
  20. vec2 zeroToOne = position / u_resolution;
  21. // convert from 0->1 to 0->2
  22. vec2 zeroToTwo = zeroToOne * 2.0;
  23. // convert from 0->2 to -1->+1 (clipspace)
  24. vec2 clipSpace = zeroToTwo - 1.0;
  25. gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
  26. }
  27. </script>
  28. <!-- fragment shader -->
  29. <script id="fragment-shader-2d" type="x-shader/x-fragment">
  30. precision mediump float;
  31. uniform vec4 u_color;
  32. void main() {
  33. gl_FragColor = u_color;
  34. }
  35. </script>
  36. <script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script>
  37. <script src="https://webglfundamentals.org/webgl/resources/webgl-lessons-ui.js"></script>
  38. "use strict";
  39. function main() {
  40. // Get A WebGL context
  41. /** @type {HTMLCanvasElement} */
  42. var canvas = document.querySelector("#canvas");
  43. var gl = canvas.getContext("webgl");
  44. if (!gl) {
  45. return;
  46. }
  47. // setup GLSL program
  48. var program = webglUtils.createProgramFromScripts(gl, ["vertex-shader-2d", "fragment-shader-2d"]);
  49. // look up where the vertex data needs to go.
  50. var positionLocation = gl.getAttribLocation(program, "a_position");
  51. // lookup uniforms
  52. var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
  53. var colorLocation = gl.getUniformLocation(program, "u_color");
  54. var matrixLocation = gl.getUniformLocation(program, "u_matrix");
  55. // Create a buffer to put positions in
  56. var positionBuffer = gl.createBuffer();
  57. // Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
  58. gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  59. // Put geometry data into buffer
  60. setGeometry(gl);
  61. var translation = [100, 150];
  62. var angleInRadians = 0;
  63. var scale = [1, 1];
  64. var color = [Math.random(), Math.random(), Math.random(), 1];
  65. drawScene();
  66. // Setup a ui.
  67. webglLessonsUI.setupSlider("#x", {value: translation[0], slide: updatePosition(0), max: gl.canvas.width });
  68. webglLessonsUI.setupSlider("#y", {value: translation[1], slide: updatePosition(1), max: gl.canvas.height});
  69. webglLessonsUI.setupSlider("#angle", {slide: updateAngle, max: 360});
  70. webglLessonsUI.setupSlider("#scaleX", {value: scale[0], slide: updateScale(0), min: -5, max: 5, step: 0.01, precision: 2});
  71. webglLessonsUI.setupSlider("#scaleY", {value: scale[1], slide: updateScale(1), min: -5, max: 5, step: 0.01, precision: 2});
  72. function updatePosition(index) {
  73. return function(event, ui) {
  74. translation[index] = ui.value;
  75. drawScene();
  76. };
  77. }
  78. function updateAngle(event, ui) {
  79. var angleInDegrees = 360 - ui.value;
  80. angleInRadians = angleInDegrees * Math.PI / 180;
  81. drawScene();
  82. }
  83. function updateScale(index) {
  84. return function(event, ui) {
  85. scale[index] = ui.value;
  86. drawScene();
  87. };
  88. }
  89. // Draw the scene.
  90. function drawScene() {
  91. webglUtils.resizeCanvasToDisplaySize(gl.canvas);
  92. // Tell WebGL how to convert from clip space to pixels
  93. gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  94. // Clear the canvas.
  95. gl.clear(gl.COLOR_BUFFER_BIT);
  96. // Tell it to use our program (pair of shaders)
  97. gl.useProgram(program);
  98. // Turn on the attribute
  99. gl.enableVertexAttribArray(positionLocation);
  100. // Bind the position buffer.
  101. gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  102. // Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
  103. var size = 2; // 2 components per iteration
  104. var type = gl.FLOAT; // the data is 32bit floats
  105. var normalize = false; // don't normalize the data
  106. var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
  107. var offset = 0; // start at the beginning of the buffer
  108. gl.vertexAttribPointer(
  109. positionLocation, size, type, normalize, stride, offset);
  110. // set the resolution
  111. gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
  112. // set the color
  113. gl.uniform4fv(colorLocation, color);
  114. // Compute the matrices
  115. var translationMatrix = m3.translation(translation[0], translation[1]);
  116. var rotationMatrix = m3.rotation(angleInRadians);
  117. var scaleMatrix = m3.scaling(scale[0], scale[1]);
  118. // Multiply the matrices.
  119. var matrix = m3.multiply(scaleMatrix, rotationMatrix);
  120. matrix = m3.multiply(matrix, translationMatrix);
  121. // Set the matrix.
  122. gl.uniformMatrix3fv(matrixLocation, false, matrix);
  123. // Draw the geometry.
  124. var primitiveType = gl.TRIANGLES;
  125. var offset = 0;
  126. var count = 18; // 6 triangles in the 'F', 3 points per triangle
  127. gl.drawArrays(primitiveType, offset, count);
  128. }
  129. }
  130. var m3 = {
  131. translation: function(tx, ty) {
  132. return [
  133. 1, 0, 0,
  134. 0, 1, 0,
  135. tx, ty, 1,
  136. ];
  137. },
  138. rotation: function(angleInRadians) {
  139. var c = Math.cos(angleInRadians);
  140. var s = Math.sin(angleInRadians);
  141. return [
  142. c,-s, 0,
  143. s, c, 0,
  144. 0, 0, 1,
  145. ];
  146. },
  147. scaling: function(sx, sy) {
  148. return [
  149. sx, 0, 0,
  150. 0, sy, 0,
  151. 0, 0, 1,
  152. ];
  153. },
  154. multiply: function(a, b) {
  155. var a00 = a[0 * 3 + 0];
  156. var a01 = a[0 * 3 + 1];
  157. var a02 = a[0 * 3 + 2];
  158. var a10 = a[1 * 3 + 0];
  159. var a11 = a[1 * 3 + 1];
  160. var a12 = a[1 * 3 + 2];
  161. var a20 = a[2 * 3 + 0];
  162. var a21 = a[2 * 3 + 1];
  163. var a22 = a[2 * 3 + 2];
  164. var b00 = b[0 * 3 + 0];
  165. var b01 = b[0 * 3 + 1];
  166. var b02 = b[0 * 3 + 2];
  167. var b10 = b[1 * 3 + 0];
  168. var b11 = b[1 * 3 + 1];
  169. var b12 = b[1 * 3 + 2];
  170. var b20 = b[2 * 3 + 0];
  171. var b21 = b[2 * 3 + 1];
  172. var b22 = b[2 * 3 + 2];
  173. return [
  174. b00 * a00 + b01 * a10 + b02 * a20,
  175. b00 * a01 + b01 * a11 + b02 * a21,
  176. b00 * a02 + b01 * a12 + b02 * a22,
  177. b10 * a00 + b11 * a10 + b12 * a20,
  178. b10 * a01 + b11 * a11 + b12 * a21,
  179. b10 * a02 + b11 * a12 + b12 * a22,
  180. b20 * a00 + b21 * a10 + b22 * a20,
  181. b20 * a01 + b21 * a11 + b22 * a21,
  182. b20 * a02 + b21 * a12 + b22 * a22,
  183. ];
  184. },
  185. };
  186. // Fill the buffer with the values that define a letter 'F'.
  187. function setGeometry(gl) {
  188. gl.bufferData(
  189. gl.ARRAY_BUFFER,
  190. new Float32Array([
  191. // left column
  192. 0, 0,
  193. 30, 0,
  194. 0, 150,
  195. 0, 150,
  196. 30, 0,
  197. 30, 150,
  198. // top rung
  199. 30, 0,
  200. 100, 0,
  201. 30, 30,
  202. 30, 30,
  203. 100, 0,
  204. 100, 30,
  205. // middle rung
  206. 30, 60,
  207. 67, 60,
  208. 30, 90,
  209. 30, 90,
  210. 67, 60,
  211. 67, 90,
  212. ]),
  213. gl.STATIC_DRAW);
  214. }
  215. main();

三. 再一步转化

逐步观察,首先,“从像素坐标转换到 0.0 到 1.0”, 事实上是一个缩放变换,第二步也是缩放变换,接着是一个平移和一个 Y 为 -1 的缩放。我们可以将这些操作放入一个矩阵传给着色器, 创建两个缩放矩阵,一个缩放 1.0/分辨率,另一个缩放 2.0 , 第三个平移 -1.0,-1.0 然后第四个将 Y 缩放 -1。 然后将他们乘在一起,由于运算很简单,所以我们就直接定义一个 projection 方法,根据分辨率直接生成矩阵。

  1. var m3 = {
  2. projection: function(width, height) {
  3. // 注意:这个矩阵翻转了 Y 轴,所以 0 在上方
  4. return [
  5. 2 / width, 0, 0,
  6. 0, -2 / height, 0,
  7. -1, 1, 1
  8. ];
  9. },
  10. // 矩阵部分的代码可以进一步修改
  11. // 绘制场景
  12. function drawScene() {
  13. ...
  14. // 计算矩阵
  15. var projectionMatrix = m3.projection(
  16. gl.canvas.clientWidth, gl.canvas.clientHeight);
  17. ...
  18. // 矩阵相乘
  19. var matrix = m3.multiply(projectionMatrix, translationMatrix);
  20. matrix = m3.multiply(matrix, rotationMatrix);
  21. matrix = m3.multiply(matrix, scaleMatrix);
  22. ...
  23. }
  24. <script id="vertex-shader-2d" type="x-shader/x-vertex">
  25. attribute vec2 a_position;
  26. uniform mat3 u_matrix;
  27. void main() {
  28. // 使位置和矩阵相乘
  29. gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
  30. }
  31. </script>

终极公式 :gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1)