WebGL Transforms 转换
在我们转向 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); }
在下面的示例中,我附加了几个滑块,它们将更新 translate[0] 和 translation[1] 并在它们更改时调用 drawScene。 拖动滑块以平移矩形。
到现在为止还挺好。 但现在想象一下,我们想用更复杂的形状做同样的事情。
假设我们想绘制一个由 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 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 几乎可以做所有事情。 我们所做的只是设置翻译并要求它绘制。 即使我们的几何图形有数万个点,主代码也将保持不变。
如果您愿意,您可以比较 使用上述复杂 JavaScript 更新所有点的版本 。
我希望这个例子不是太明显。 在下一章中,我们将继续讨论旋转。
WebGL 2D 旋转
我要先承认我不知道我如何解释这是否有意义,但到底是什么,不妨尝试一下。
首先,我想向您介绍所谓的“单位圆”。 如果你记得你的初中数学(别睡我!)一个圆有一个半径。 圆的半径是圆心到边缘的距离。 单位圆是半径为 1.0 的圆。
这是一个单位圆。
请注意,当您围绕圆圈拖动蓝色手柄时,X 和 Y 位置会发生变化。 这些代表该点在圆上的位置。 顶部 Y 为 1,X 为 0。右侧 X 为 1,Y 为 0。
如果你记得小学三年级的数学知识,如果你将某个东西乘以 1,它就会保持不变。 所以 123 * 1 = 123。很基本吧? 嗯,一个单位圆,一个半径为 1.0 的圆也是 1 的一种形式。它是一个旋转的 1。所以你可以乘以这个单位圆,在某种程度上它有点像乘以 1,除了魔法发生和事情旋转。
我们将从单位圆上的任何点获取 X 和 Y 值,并将几何乘以 之前样本 。
这是我们着色器的更新。
<script 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。 让我们在单位圆上从 12 点顺时针方向选择一个点。
圆圈上的位置是 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 度也一样
圆圈上的位置是 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 度是 π/2 弧度。 因此,如果您想将某些东西旋转 90 度,只需使用 Math.PI * 0.5
. 如果要将其旋转 45 度,请使用 Math.PI * 0.25
等等
如果您开始以弧度思考,几乎所有涉及角度、圆或旋转的数学运算都非常简单。 所以试试看。 使用弧度,而不是度数,除非在 UI 显示中。
WebGL 2D 比例
缩放就像翻译一样简单。
我们将位置乘以所需的比例。 以下是我们之前示例的更改。
<script 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 章有助于理解平移、旋转和缩放。 接下来,我们将讨论将所有这三个矩阵组合成更简单且通常更有用的形式的矩阵的魔力。
为什么是“F”?
我第一次看到有人使用“F”是在纹理上。 “F”本身并不重要。 重要的是你可以从任何方向分辨它的方向。 例如,如果我们使用心形或三角形,我们无法判断它是否水平翻转。 圆圈会更糟。 可以说,彩色矩形在每个角上都可以使用不同的颜色,但是您必须记住哪个角是哪个角。 F的方向是立即可识别的。
任何你能分辨出方向的形状都可以,自从我第一次接触这个想法以来,我就一直使用“F”。
WebGL 2D 矩阵
在最后 3 章中,我们讨论了如何平移几何、旋转几何和缩放几何。 平移、旋转和缩放都被认为是一种“变换”。 这些转换中的每一个都需要对着色器进行更改,并且 3 个转换中的每一个都依赖于顺序。 在 我们之前的示例 中,我们缩放、旋转、然后平移。 如果我们以不同的顺序应用它们,我们会得到不同的结果。
例如,这里的比例为 2, 1,旋转为 30%,平移为 100, 0。
这是 100,0 的平移,30% 的旋转和 2、1 的比例
结果完全不同。 更糟糕的是,如果我们需要第二个示例,我们必须编写一个不同的着色器,以新的期望顺序应用平移、旋转和缩放。
好吧,有些人比我聪明,发现你可以用矩阵数学做所有同样的事情。 对于 2d,我们使用 3×3 矩阵。 一个 3×3 矩阵就像有 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 | + | 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 |
如果你记得你的代数,我们可以删除任何乘以零的地方。 有效地乘以 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 | + | newY = | x | * | -s | + | extra = | x | * | 0.0 | + | |
y | * | s | + | y | * | c | + | y | * | 0.0 | + | ||||
1 | * | 0.0 | 1 | * | 0.0 | 1 | * | 1.0 |
简化我们得到
newX = x * c + y * s; newY = x * -s + y * c;
这正是我们在旋转样本中所拥有的。
最后是规模。 我们将调用我们的 2 个比例因子 sx 和 sy
我们建立一个这样的矩阵
sx | 0.0 | 0.0 |
0.0 | sy | 0.0 |
0.0 | 0.0 | 1.0 |
应用矩阵我们得到这个
newX = | x | * | sx | + | newY = | x | * | 0.0 | + | extra = | x | * | 0.0 | + | |
y | * | 0.0 | + | y | * | sy | + | y | * | 0.0 | + | ||||
1 | * | 0.0 | 1 | * | 0.0 | 1 | * | 1.0 |
这真的是
newX = | x | * | sx | + | newY = | x | * | 0.0 | + | extra = | x | * | 0.0 | + | |
y | * | 0.0 | + | y | * | sy | + | y | * | 0.0 | + | ||||
1 | * | 0.0 | 1 | * | 0.0 | 1 | * | 1.0 |
简化的是
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 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 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 ]; }
这是5 F。
再举一个例子,到目前为止,在每个样本中,我们的“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);
这是那个样本。 注意 F 围绕中心旋转和缩放。
使用该技术,您可以从任何点旋转或缩放。 现在您知道 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 和第四个缩放 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 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 开始,希望让它简单易懂。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论