此文上接一系列文章,先从基础概念开始,上一篇是物体缩放。

之前的三篇文章讲了如何对二维物体进行平移,旋转,和 缩放。每种变换都改变了着色器并且这些变换还受先后顺序影响。在前例中我们先缩放,再旋转,最后平移,如果执行顺序不同结果也不同。

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

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

结果截然不同,更糟的是,针对第二种情况中的转换顺序,需要写一个新的着色器。

有些聪明的人可能已经想到了矩阵,对于二维我们使用 3x3 的矩阵,3x3 的矩阵就像是有 9 个格子的格网。

在计算的时候我们将位置坐标沿着矩阵列的方向依次相乘再将结果加起来。我们的位置信息只有两个值, x 和 y 。但是要进行运算需要三个值,所以我们将第三个值赋值为 1 。

在这个例子中结果将是

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

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

或者更简洁

其他的就不用关心了。这个看起来和平移例子中的代码有些相似。

同样的来实现旋转,在旋转章节提到过,旋转只需要和旋转角对应的正弦和余弦值

然后我们创建一个这样的矩阵

使用矩阵后得到

遮住没有意义的部分(和 0 或 1 相乘的部分)

然后得到简化版

正是我们在旋转例子中得到的结果。

最后是缩放,我们将两个缩放因子叫做 sx 和 sy 。

然后创建一个这样的矩阵

使用矩阵后得到

实际上是

简化后

和缩放例子相似。

现在你可能还会想“那又怎样,有什么意义?”,好像花了更多精力做之前做过的事情。

现在开始有趣的部分了,相乘后他们可以用一个矩阵代表三个变换,假定有一个方法m3.multiply可以将两个矩阵相乘并返回结果。

为了方便讲解,我们先创建平移,旋转和缩放矩阵。

var m3 = {translation: function(tx, ty) {return [1, 0, 0,0, 1, 0,tx, ty, 1,];},rotation: function(angleInRadians) {var c = Math.cos(angleInRadians);var s = Math.sin(angleInRadians);return [c,-s, 0,s, c, 0,0, 0, 1,];},scaling: function(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() {// 缩放vec2 scaledPosition = a_position * u_scale;// 旋转vec2 rotatedPosition = vec2(scaledPosition.x * u_rotation.y + scaledPosition.y * u_rotation.x,scaledPosition.y * u_rotation.y - scaledPosition.x * u_rotation.x);// 平移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() {// 将位置乘以矩阵vec2 position = (u_matrix * vec3(a_position, 1)).xy;...

这是使用的方法

// 绘制场景
function drawScene() {,,,// 计算矩阵var translationMatrix = m3.translation(translation[0], translation[1]);var rotationMatrix = m3.rotation(angleInRadians);var scaleMatrix = m3.scaling(scale[0], scale[1]);// 矩阵相乘var matrix = m3.multiply(translationMatrix, rotationMatrix);matrix = m3.multiply(matrix, scaleMatrix);// 设置矩阵gl.uniformMatrix3fv(matrixLocation, false, matrix);// 绘制图形gl.drawArrays(gl.TRIANGLES, 0, 18);
}

这个例子用的新代码,滑块没变,还是对应平移,旋转和缩放,但是他们在着色器中做的事情是相似的。

CodePen 地址

像这样的矩阵相乘对层级变换至关重要,比如身体的手臂部分运动,月球属于绕太阳转动的地球的一部分,或者树上的树枝。写一个简单的层级运动的例子,我们来画 5 个 'F' ,并且每个 'F' 都以前一个的矩阵为基础。

// 绘制场景function drawScene() {// 清空画布gl.clear(gl.COLOR_BUFFER_BIT);// 计算矩阵var translationMatrix = m3.translation(translation[0], translation[1]);var rotationMatrix = m3.rotation(angleInRadians);var scaleMatrix = m3.scaling(scale[0], scale[1]);// 初始矩阵var matrix = m3.identity();for (var i = 0; i < 5; ++i) {// 矩阵相乘matrix = m3.multiply(matrix, translationMatrix);matrix = m3.multiply(matrix, rotationMatrix);matrix = m3.multiply(matrix, scaleMatrix);// 设置矩阵gl.uniformMatrix3fv(matrixLocation, false, matrix);// 绘制图形gl.drawArrays(gl.TRIANGLES, 0, 18);}}

在这个例子中用到了一个新方法,m3.identity,这个方法创建一个单位矩阵。单位矩阵就像 1.0 一样,和它相乘的矩阵不会变化,就像

同样的

这是创建单位矩阵的代码。

var m3 = {identity function() {return [1, 0, 0,0, 1, 0,0, 0, 1,];},...

这是5个 F 。

CodePen 地址

再看一个例子,之前的每个例子中 'F' 都是绕着它的左上角旋转(当然,改变转换顺序的那个例子除外)。这是因为我们总是绕原点旋转,而 'F' 的原点就是左上角,也就是 (0, 0) 。

现在我们可以使用矩阵运算,并且自定义转换的顺序。所以让我们改变旋转的中心

// 创建一个矩阵,可以将原点移动到 'F' 的中心
var moveOriginMatrix = m3.translation(-50, -75);
...// 矩阵相乘
var matrix = m3.multiply(translationMatrix, rotationMatrix);
matrix = m3.multiply(matrix, scaleMatrix);
matrix = m3.multiply(matrix, moveOriginMatrix);

这是结果,注意到 F 现在绕中心旋转和缩放了。

CodePen 地址

通过这种方式你可以绕任意点旋转和缩放,所以现在你可能知道为什么 PhotoShop 或 Flash 可以让你移动旋转中心。

还可以做更有趣的事情,如果你回想第一篇文章WebGL 基础概念,可能会记得在着色器中我们将像素坐标转换到裁剪空间,这是当时的代码

...
// 从像素坐标转换到 0.0 到 1.0
vec2 zeroToOne = position / u_resolution;// 再把 0->1 转换 0->2
vec2 zeroToTwo = zeroToOne * 2.0;// 把 0->2 转换到 -1->+1 (裁剪空间)
vec2 clipSpace = zeroToTwo - 1.0;gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

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

var m3 = {projection: function(width, height) {// 注意:这个矩阵翻转了 Y 轴,所以 0 在上方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() {// 使位置和矩阵相乘gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);}</script>

在 JavaScript 中需要乘上投影矩阵

// 绘制场景
function drawScene() {
...// 计算矩阵
var projectionMatrix = m3.projection(gl.canvas.clientWidth, gl.canvas.clientHeight);...// 矩阵相乘
var matrix = m3.multiply(projectionMatrix, translationMatrix);
matrix = m3.multiply(matrix, rotationMatrix);
matrix = m3.multiply(matrix, scaleMatrix);...
}

这里还去除了设置分辨率的代码,通过使用矩阵,我们就把着色器中 6-7 步的操作在一步中完成。

CodePen 地址

在继续之前我们可以先简化一下操作,虽然先创建一些矩阵再将它们相乘很常见,但是按照我们的顺序依次操作矩阵也比较常见,比较高效的做法是创建这样的方法

var m3 = {...translate: function(m, tx, ty) {return m3.multiply(m, m3.translation(tx, ty));},rotate: function(m, angleInRadians) {return m3.multiply(m, m3.rotation(angleInRadians));},scale: function(m, sx, sy) {return m3.multiply(m, m3.scaling(sx, sy));},...};

这能够让我们将 7 行的矩阵代码转换成 4 行

// 计算矩阵
var matrix = m3.projection(gl.canvas.clientWidth, gl.canvas.clientHeight);
matrix = m3.translate(matrix, translation[0], translation[1]);
matrix = m3.rotate(matrix, angleInRadians);
matrix = m3.scale(matrix, scale[0], scale[1]);

这是结果

CodePen 地址

最后一件事,我们之前使用了多种矩阵顺序。第一例中使用

translation * rotation * scale // 平移 * 旋转 * 缩放

第二例中使用

scale * rotation * translation // 缩放 * 旋转 * 平移

然后观察了它们的区别。

这有两种方式解读矩阵运算,给定这样一个表达式

projectionMat * translationMat * rotationMat * scaleMat * position

第一种可能是多数人觉得比较自然的方式,从右向左解释

首先将位置乘以缩放矩阵获得缩放后的位置

scaledPosition = scaleMat * position

然后将缩放后的位置和旋转矩阵相乘得到缩放旋转位置

rotatedScaledPosition = rotationMat * scaledPosition

然后将缩放旋转位置和平移矩阵相乘得到缩放旋转平移位置

translatedRotatedScaledPosition = translationMat * rotatedScaledPosition

最后和投影矩阵相乘得到裁剪空间中的坐标

clipspacePosition = projectioMatrix * translatedRotatedScaledPosition

第二种方式是从左往右解释,在这个例子中每一个矩阵改变的都是画布的坐标空间,画布的起始空间是裁剪空间的范围(-1 到 +1),矩阵从左到右改变着画布所在的空间。

第一步:没有矩阵(或者单位矩阵)

白色区域是画布,蓝色是画布以外,我们正在裁剪空间中。传递到这里的点需要在裁剪空间内。

第二步:matrix = m3.projection(gl.canvas.clientWidth, gl.canvas.clientHeight);

现在我们在像素空间,X 范围是 0 到 400 ,Y 范围是 0 到 300,0,0 点在左上角。传递到这里的点需要在像素空间内,你看到的闪烁是 Y 轴上下颠倒的原因。

第三步:matrix = m3.translate(matrix, tx, ty);

原点被移动到了 tx, ty (150, 100),所以空间移动了。

第四步:matrix = m3.rotate(matrix, rotationInRadians);

空间绕 tx, ty 旋转

第五步:matrix = m3.scale(matrix, sx, sy);

之前的旋转空间中心在 tx, ty ,x 方向缩放 2 ,y 方向缩放 1.5

在着色器中执行 gl_Position = matrix * position; ,position 被直接转换到这个空间。

选一个你容易接受的方式去理解吧。

另外,如果你想精通矩阵运算就看看这个神奇的视频吧!线性代数的本质:(https://www.bilibili.com/video/av5987715?from=search&seid=3000802315191985263)

WebGL 理论基础 - 二维矩阵相关推荐

  1. WebGL 理论基础 - 二维缩放

    转载自知乎 本文原文地址:https://zhuanlan.zhihu.com/p/56594572 ------------------------------------------------- ...

  2. 机器学习之数学基础(二)~数组、向量、矩阵、向量空间、二维矩阵

    1. 概述 在学习机器学习(machine learning)或模式识别(pattern recognition)过程中,我经常会困惑于向量.数组和矩阵这三种数据结构,而在学习张学工教授<模式识 ...

  3. python 搜索二维矩阵

    搜索二维矩阵 编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值.该矩阵具有如下特性: 每行中的整数从左到右按升序排列. 每行的第一个整数大于前一行的最后一个整数. 示例 1: 输入:m ...

  4. python 二维矩阵翻转

    二维矩阵翻转 # N * N的二维矩阵 # 沿上下中线翻转 # 沿左右中线翻转 # 沿-45度对角线翻转 # 沿45度对角线翻转matrix3 = [[1, 2, 3, 4, 5], [6, 7, 8 ...

  5. matlab找出二维矩阵中最大值的位置或者最小值的位置

    matlab寻找最大值或者最小值是通过max和min命令 对应二维矩阵寻找最大元素就是max(max(A)),注意二维矩阵要写两个max 找对应位置用find函数 举个例子: >> A=[ ...

  6. 20190110-用笨办法找到二维矩阵的鞍点

    1:找出一个多维数组的鞍点,即该元素在该行上最大,在该列上最小,也可能没有鞍点 a = [     [1,2,3,4],     [4,5,6,2],     [7,0,5,2],     [11,1 ...

  7. LeetCode 73矩阵置零74搜素二维矩阵75颜色分类

    新人公众号(求支持):bigsai 专注于Java.数据结构与算法,一起进大厂不迷路! 算法文章题解全部收录在github仓库bigsai-algorithm,求star! 关注回复进群即可加入力扣打 ...

  8. 【Matlab】如何对二维矩阵进行线性/非线性插值?

    1.Introduction 最近又遇到了矩阵插值(重采样)的问题,在最开始写博客的时候就写了篇关于 Excel 重采样的文章,不过在逐渐熟悉 Matlab 的过程中,还是决定看看能不能编程自动实现, ...

  9. 【Matlab】一种超简单的二维矩阵降维方法

    1.Introduction Matlab里图像处理时,经常会把一维数组转二维数组,二维数组转一维,如下图所示: 一般经常使用的函数是 reshape ,可以在不同维度之间进行转换,不过需要事先计算数 ...

最新文章

  1. 卡莱特led显示屏调试教程_恒彩光电重庆P3 LED显示屏项目顺利完工
  2. 009_InputNumber计数器
  3. java servicefactory_Java DirectoryServiceFactory.getDirectoryService方法代碼示例
  4. yolov4的全面详解
  5. JS OOP -02 深入认识JS中的函数
  6. 初识 Angularjs1.x ,了解5个W和1个H
  7. Nginx (LNMP+https)
  8. ARP协议以及攻击欺骗和防御
  9. PHP中的方形按钮怎么敲,php 魔术方法使用说明
  10. android之list排序
  11. 软考高级系统分析师下午历年真题
  12. java删除某些段落word_Java 批量删除Word中的空白段落
  13. 单片机c语言6种开方,单片机快速开平方的算法
  14. 微信多开无法连接服务器,ios丨微信多开联网失败解决方法
  15. android 多渠道覆盖,Android多渠道包
  16. 使用JS打印网页内容及图片
  17. mac系统重置Mysql密码
  18. LuceneElasticSeach
  19. 关于BH1750的使用说明
  20. 史上最全的用Python操控手机APP攻略!建议收藏!

热门文章

  1. sublime2 中emmet的使用指南
  2. 在桌面Linux环境下开发图形界面程序的方案对比
  3. Android 获取屏幕高度,虚拟导航键检测
  4. Python choices()函数详解、random模块下的常用函数
  5. MY2NJ、MY2N-GS八座继电器使用教程及换向接法
  6. 赞丽生活、趣步的区块链电商模式解析
  7. ChatGPT的GPT-3.5-Turbo的免费国产镜像,Tokens消耗太快,更别说GPT4.0,不收钱咋办?
  8. SV绿皮书笔记(五)
  9. 电信管理论坛将Nexign发表的文章纳入基准报告
  10. emacs配置文件浅谈