矩阵变换(Matrix Transformations)
Unigine中的大量计算都是通过矩阵来执行的。 事实上,矩阵变换是3D引擎的主要概念之一。 本文通过用法举例对矩阵变换作出了解释。
另请参阅
- Unigine数据类型mat4和dmat4。
- 数学矩阵函数(Math Matrix Functions)章节中的描述。
变换(Transformations)
简单地说,3D图形中的矩阵(Matrix)就是按行和列排列的数字所组成的数组:
通常使用的是4x4矩阵。 矩阵的这种大小(4x4)是由3D空间中的平移状态引起的。 当您将新节点放进虚拟世界中时,该节点就具有了4x4大小的世界变换矩阵(World Transform Matrix),这种类型的矩阵定义了节点在虚拟世界中的位置。
在Unigine中,矩阵是以列为主的(面向列)。 因此,变换矩阵的第一列代表的是X向量v1,第二列代表的是Y向量v2,第三列代表的是Z向量v3,而第四列则代表的是平移向量t。 前三列显示的是轴的方向以及原点的缩放比例。 最后一列包含了局部原点相对于世界原点的平移。
单位矩阵(Identity Matrix)
世界原点使用的是如下类型矩阵:
该型矩阵被称为单位矩阵,其主对角线上为1,其它位置全为0。 如果某个矩阵将与单位矩阵相乘,那它不会发生任何改变,原因是:所得矩阵与相乘之前的矩阵是一样的。
如果局部原点使用的是单位矩阵,那就意味着局部原点和世界原点是重合的。
旋转
要想改变局部原点的朝向,就须更改矩阵的前三列。
要想沿不同的轴旋转原点,就须使用合适的矩阵:
上面给出的矩阵中,α是沿轴的旋转角度。
下面的矩阵表示的是局部原点沿Y轴作了45度的旋转:
平移
变换矩阵的最后一列给出了局部原点在虚拟世界中相对于世界原点的位置。 下面的矩阵显示了原点的平移。 平移向量t为(3, 0, 2)。
缩放
向量的长度表示的是沿轴的缩放比例系数。
要想计算向量长度(也称为大小(magnitude)),就须找出向量分量的平方和的平方根。 计算公式如下:
|vector length| = √(x² + y² + z²)
下面的矩阵将局部原点沿所有轴都按比例放大了2个单位。
累积变换(Cumulating Transformations)
矩阵变换(缩放,旋转和平移)的次序是非常重要的。
累积变换的次序如下:
- 平移
- 旋转
- 缩放
下面是变换次序的公式:
TransformedVector = TranslationMatrix * RotationMatrix * ScaleMatrix * Vector
下面给出的例子向您展示了局部原点相对于世界原点的不同位置。
平移 * 旋转次序 |
旋转 * 平移次序 |
在左图中,局部原点首先会被平移,之后再被旋转;在右图中,局部原点首先首先会被旋转,之后再被平移。 其中所有的值(旋转角度,平移向量)都是一样的,只不过结果不同。
例子
这个例子向您展示了在选择了矩阵变换的其它种次序时会有什么样的结果。
下面给出的代码例子是将一个ObjectMeshStatic对象添加给了虚拟世界。 第一种情况我们使用平移 * 旋转的次序,第二种情况使用旋转 * 平移的次序。
// 声明ObjectMeshStatic
ObjectMeshStatic mesh;
/* 将mesh作为节点添加给Editor
*/
Node add_editor(Node node) {
engine.editor.addNode(node);
return node_remove(node);
}
/* world(世界)脚本文件的init()功能
*/
int init() {
/* 创建一摄像机并将其添加给虚拟世界
*/
Player player = new PlayerSpectator();
player.setDirection(Vec3(0.755f,-1.0f,0.25f));
engine.game.setPlayer(player);
// 将mesh添加给虚拟世界并设置材质
mesh = add_editor(new ObjectMeshStatic("matrix_project/meshes/statue.mesh"));
mesh.setMaterial("mesh_base","*");
// 创建绕Z轴作270度角旋转的矩阵
mat4 rotation = rotateZ(-90);
log.message("rotation matrix is: %s\n",typeinfo(rotation));
// 创建矩阵变换
mat4 translation = translate(vec3(0,7,0));
log.message("translation matrix is: %s\n",typeinfo(translation));
// 通过当前变换矩阵来累积变换
mat4 transform = box.getTransform() * translation * rotation;
mesh.setTransform(transform);
log.message("transformation matrix is:%s\n",typeinfo(mesh.getTransform()));
return 1;
}
要想更改次序,那只需修改累积变换这一行代码即可:
mat4 transform = box.getTransform() * rotation * translation;
其结果会是不同的。 下图就给出了不同之处(摄像机是被放置在同一位置上的)。
平移 * 旋转的次序 |
旋转 * 平移的次序 |
上面两幅图给出了网格相对于世界原点的位置。
矩阵层级(Matrix Hierarchy)
另一个重要概念就是矩阵层级。 当某个节点被作为另一个节点的子节点添加进虚拟世界中时,它就具有了相对于父节点的变换矩阵。 就因为这个,Node类才会拥有这几个不同函数:getTransform(),setTransform()和getWorldTransform(),setWorldTransform();前两个函数会返回局部变换矩阵(Local Transform Matrix),而后两个则会返回世界变换矩阵。
那么,使用矩阵层级的原因是什么呢? 是为了相对其它节点来移动节点。 在您移动父节点时,子节点也会被移动,这就是关键所在。
父节点的原点与世界原点是重合的 |
父节点的原点被移动了,同时子节点的原点也被移动了 |
上面两幅图给出了矩阵层级的要点。 当某个父节点的原点(节点)被移动时,子节点的原点也将被移动,且子节点的局部变换矩阵不会被更改。 不过子节点的世界变换矩阵将被更改。 如果您需要子节点相对于世界原点的世界变换矩阵,那就应使用函数getWorldTransform()和setWorldTransform();假如您需要子节点相对于父节点的局部变换矩阵时,那就应使用函数getTransform()和setTransform()。
例子
请您思考下面的例子,它向您展示了局部变换矩阵和世界变换矩阵间的不同。
这段代码取自UnigineScript脚本的world(世界)脚本文件。 其通过box.mesh文件创建了两个节点(子节点和父节点)。
/* 为节点声明ObjectMeshStatic对象
*/
ObjectMeshStatic box_1;
ObjectMeshStatic box_2;
/* 将mesh作为节点添加给Editor
*/
Node add_editor(Node node) {
engine.editor.addNode(node);
return node_remove(node);
}
/* world(世界)脚本文件的init()功能
*/
int init() {
/* 创建一摄像机并将其添加给虚拟世界
*/
Player player = new PlayerSpectator();
player.setDirection(Vec3(0.755f,-1.0f,0.25f));
engine.game.setPlayer(player);
// 将box网格添加给Editor
box_1 = add_editor(new ObjectMeshStatic("matrix_project/meshes/box.mesh"));
// 为第一个box网格设置变换和材质
mat4 transform = translate(vec3(0.0f,0.0f,0.0f));
box_1.setWorldTransform(mat4(transform));
box_1.setMaterial("mesh_base","*");
// 将第二个box网格添加给Editor
box_2 = add_editor(new ObjectMeshStatic("matrix_project/meshes/box.mesh"));
// 为第二个box网格设置变换和材质
mat4 transform1 = translate(vec3(0.0f,2.0f,1.0f));
box_2.setWorldTransform(Mat4(transform1));
box_2.setMaterial("mesh_base","*");
// 将第二个box网格作为子节点添加第一个box网格
box_1.addChild(box_2);
// 显示子节点的变换矩阵和世界变换矩阵
log.message("transformation matrix of the child node: %s\n",typeinfo(box_2.getTransform()));
log.message("world transformation matrix of the child node: %s\n",typeinfo(box_2.getWorldTransform()));
return 1;
}
待执行完这段代码之后,我们会得到如下结果:
transformation matrix of the child node: dmat4: (1 0 0 0) (0 1 0 0) (0 0 1 0) (0 2 1 1)
world transformation matrix of the child node: dmat4 (1 0 0 0) (0 1 0 0) (0 0 1 0) (0 2 1 1)
这两个矩阵是相同的,原因是父节点box_1做了零变换(0,0,0)。 这意味着它的原点和世界原点是重合的。 如果我们将第一个mesh的变换改为别的,例如:
mat4 transform = translate(vec3(2.0f,2.0f,2.0f));
我们就会得到另外的结果:
transformation matrix of the child node: dmat4: (1 0 0 0) (0 1 0 0) (0 0 1 0) (0 2 1 1)
world transformation matrix of the child node: dmat4 (1 0 0 0) (0 1 0 0) (0 0 1 0) (2 4 3 1)
正如您看到的,局部变换矩阵仍然一样,只有世界变换矩阵发生了改变。 这意味着,第二个节点相对于第一个节点是处于同一位置的,不过这第二个节点还拥有另一个相对于世界原点的位置,原因是父节点的位置发生了改变。
父节点的原点与世界原点是重合的 |
父节点被移动了,同时子节点也会被自动移动 |