OpenGL学习笔记:矩阵变换
文章目录
缩放
glm矩阵表示
glm缩放矩阵实现
位移
齐次坐标
glm位移矩阵实现
旋转
沿x轴旋转
沿y轴旋转
沿z轴旋转
沿任意轴旋转
glm旋转矩阵实现
矩阵的组合
glm矩阵组合使用
接上篇 OpenGL学习笔记:数学基础和常用矩阵总结(一)
缩放
前面说了一大堆的理论,现在终于可以来点实际应用了
对一个向量进行缩放(Scaling)就是对向量的长度进行缩放,而保持它的方向不变。
我们先来尝试缩放向量v ⃗ = ( 3 , 2 ) \vec{v} =(3,2)
v
=(3,2)。我们可以把向量沿着x轴缩放0.5,使它的宽度缩小为原来的二分之一;我们将沿着y轴把向量的高度缩放为原来的两倍。我们看看把向量缩放(0.5, 2)倍所获得的s ⃗ \vec{s}
s
是什么样的
这是我们想要的结果,但我们怎么计算出这个结果呢?如果是这种简单的缩放,我们可以把v ⃗ \vec{v}
v
的x分量乘以0.5,y分量乘以2来得到这个结果。但这种方法不适合用在计算机的计算中,为了加快运算速度,简化计算,往往使用矩阵。因此我们需要用构建一个缩放矩阵来实现缩放功能
回顾刚才单位矩阵的计算,第一个结果元素是矩阵的第一行的每个元素乘以向量的每个对应元素。因为每行的元素除了第一个都是0,可得:1 ⋅ 1 + 0 ⋅ 2 + 0 ⋅ 3 + 0 ⋅ 4 = 1 1\cdot 1+0\cdot 2+0\cdot 3+0\cdot 4=11⋅1+0⋅2+0⋅3+0⋅4=1,向量的其他3个元素同理。
如果我们把对角线上的1改变一下呢?比如说第一行的对角线是0.5,第二行的对角线是2构建一个矩阵,再和v ⃗ \vec{v}
v
相乘,得到下面的计算过程
( 0.5 0 0 2 ) × ( 3 2 ) = ( 0.5 × 3 + 0 × 2 0 × 3 + 2 × 2 ) = ( 0.5 × 3 2 × 2 ) = ( 1.5 4 ) \left(
0.50amp;0amp;2
0.5amp;00amp;2
\right) \times \left(
32
32
\right) = \left(
0.5×3+0×20×3+2×2
0.5×3+0×20×3+2×2
\right)= \left(
0.5×32×2
0.5×32×2
\right) = \left(
1.54
1.54
\right)
(
0.5
0
0
2
)×(
3
2
)=(
0.5×3+0×2
0×3+2×2
)=(
0.5×3
2×2
)=(
1.5
4
)
这样,我们就得到了我们想要的结果,( 0.5 0 0 2 ) \left(
0.50amp;0amp;2
0.5amp;00amp;2
\right)(
0.5
0
0
2
)就是我们的变换矩阵,下面,我们把这个变换矩阵推广一下,如果我们把缩放变量表示为( S 1 , S 2 , S 3 ) (S_1,S_2,S_3)(S
1
,S
2
,S
3
)我们可以为任意向量( x , y , z ) (x,y,z)(x,y,z)定义一个缩放矩阵:
( S 1 0 0 0 0 S 2 0 0 0 0 S 3 0 0 0 0 1 ) ⋅ ( x y z 1 ) = ( S 1 ⋅ x S 2 ⋅ y S 3 ⋅ z 1 ) \left(
S1000amp;0amp;S2amp;0amp;0amp;0amp;0amp;S3amp;0amp;0amp;0amp;0amp;1
S1amp;0amp;0amp;00amp;S2amp;0amp;00amp;0amp;S3amp;00amp;0amp;0amp;1
\right) \cdot \left(
xyz1
xyz1
\right) = \left(
S1⋅xS2⋅yS3⋅z1
S1⋅xS2⋅yS3⋅z1
\right)
⎝
⎜
⎜
⎛
S
1
0
0
0
0
S
2
0
0
0
0
S
3
0
0
0
0
1
⎠
⎟
⎟
⎞
⋅
⎝
⎜
⎜
⎛
x
y
z
1
⎠
⎟
⎟
⎞
=
⎝
⎜
⎜
⎛
S
1
⋅x
S
2
⋅y
S
3
⋅z
1
⎠
⎟
⎟
⎞
注意,第四个缩放向量仍然是1,因为在3D空间中缩放w分量是无意义的。w分量另有其他用途,在后面我们会看到。
glm矩阵表示
好,所有的理论知识都已经准备好了,下面我们来看一下程序中是怎么应用的,还记得OpenGL学习笔记:矩阵变换中我们用来做矩阵缩放的代码吗?
首先是创建缩放矩阵
glm::mat4 trans(1.0f);
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
1
2
然后是将缩放矩阵传递给着色器
unsigned int transformLoc = glGetUniformLocation(shaderProgram, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
1
2
最后是在着色器中对顶点向量进行缩放
gl_Position = transform * vec4(aPos, 1.0f);
1
接下来我们来扒一下这些代码都干了什么
首先是glm::mat4 trans(1.0f);创建一个单位矩阵,怎么创建的呢?我们来看看glm::mat4的构造函数
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER GLM_CONSTEXPR mat<4, 4, T, Q>::mat(T const& s)
{
// 每个元素都是矩阵的第一列,不是第一行
this->value[0] = col_type(s, 0, 0, 0);
this->value[1] = col_type(0, s, 0, 0);
this->value[2] = col_type(0, 0, s, 0);
this->value[3] = col_type(0, 0, 0, s);
}
1
2
3
4
5
6
7
8
9
简单目测一下,this->value应该是用来表示矩阵的,数组的每个元素都是一组向量(注意:这个数组的每一个元素是一个向量,这个向量对应的是矩阵中的一列,不是一行),并用变量s初始化,我们再来看一下this->value和col_type都是什么鬼
template<typename T, qualifier Q>
struct mat<4, 4, T, Q>
{
typedef vec<4, T, Q> col_type;
typedef vec<4, T, Q> row_type;
typedef mat<4, 4, T, Q> type;
typedef mat<4, 4, T, Q> transpose_type;
typedef T value_type;
private:
col_type value[4];
// 下面代码省略
}
1
2
3
4
5
6
7
8
9
10
11
12
13
确定了,value就是一个具有四个元素的col_type类型的数组,而col_type又是一个vec<4, T, Q>类型,从名字上猜测应该是一个向量类型,我们看一下vec的数据定义
// 删除无关代码
template<typename T, qualifier Q>
struct vec<4, T, Q>
{
// -- Data --
union { T x, r,s; };
union { T y, g, t; };
union { T z, b, p; };
union { T w, a, q; };
}
1
2
3
4
5
6
7
8
9
10
可以看到vec中存放了四个联合体,每个联合体就是一个向量分量,而模版T就是数据类型。
到这里我们就清楚glm中是怎么表示一个矩阵的了
我们给glm::mat4的构造函数传进来一个参数1,然后glm用这个参数1分别构造了四个向量,其中第一个向量的第一个分量是1,其他分量是0,而第二个向量的第二个分量是1,其他分量是0,第三、四个向量同样构造,然后在把这四个向量存放在一个具有四个元素的四维向量数组中,这个数组就是我们所构建的4 × 4 4\times44×4的单位矩阵了。
glm缩放矩阵实现
接下来我们再来看一下trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));都干了些什么事
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER mat<4, 4, T, Q> scale(mat<4, 4, T, Q> const& m, vec<3, T, Q> const& v)
{
mat<4, 4, T, Q> Result;
Result[0] = m[0] * v[0];
Result[1] = m[1] * v[1];
Result[2] = m[2] * v[2];
Result[3] = m[3];
return Result;
}
1
2
3
4
5
6
7
8
9
10
在scale函数中,形参m是我们传进来的单位矩阵,v是我们传进来的缩放倍数的向量。
v[0]很好理解,就是取出v的第一个分量,这里是0.5,m[0]也很好理解,就是取出m的第一个向量,前面我们已经分析过了,m是一个具有四个元素的四维向量数组,本例中应该是( 1 , 0 , 0 , 0 ) (1,0,0,0)(1,0,0,0),m[0] * v[0]就是用向量( 1 , 0 , 0 , 0 ) (1,0,0,0)(1,0,0,0)乘以0.5,结果应该是( 0.5 , 0 , 0 , 0 ) (0.5,0,0,0)(0.5,0,0,0)。虽然我们看懂了这句代码,但还是想看一看glm是怎么计算的。下面这段代码是vec<3, T, Q>重载的[]运算符,根据下标取出响应的xyz,对应的是scale函数中的v[0]。
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER GLM_CONSTEXPR T const& vec<3, T, Q>::operator[](typename vec<3, T, Q>::length_type i) const
{
assert(i >= 0 && i < this->length());
switch(i)
{
default:
case 0:
return x;
case 1:
return y;
case 2:
return z;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
下面这段代码是mat<4, 4, T, Q>重载的[],取出value中的第i个向量,对应的是scale函数中的m[0]。
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER GLM_CONSTEXPR typename mat<4, 4, T, Q>::col_type const& mat<4, 4, T, Q>::operator[](typename mat<4, 4, T, Q>::length_type i) const
{
assert(i < this->length());
return this->value[i];
}
1
2
3
4
5
6
下面的三个函数是glm对运算符的重载堆栈,第一层是mat<4, 4, T, Q>重载的,在这层重载中,用v重新构造了一个vec<4, T, Q>型的向量,再用这个向量乘以缩放比例,就是scale函数中的v[0]。然后再继续调用vec<4, T, Q>型的*重载,在这一层中先是用v[0]构造了一个四维向量,看下面的第四个函数,vec<4, T, Q>::vec(T scalar)构造方式是用scale给四维向量的四个分量赋值,得到了一个( s c a l e , s c a l e , s c a l e , s c a l e ) (scale,scale,scale,scale)(scale,scale,scale,scale)的四维向量,然后再调用了call函数,我们继续来到call函数中。在call函数中我们看到,对两个四维变量的四个分量依次相乘,再用四个结果构造了一个新的四维变量,最后返回到我们调用的scale函数中,将结果拷贝给Result[0]。
GLM_FUNC_QUALIFIER GLM_CONSTEXPR vec<4, T, Q> operator*(vec<4, T, Q> const& v, T const & scalar)
{
return vec<4, T, Q>(v) *= scalar;
}
template<typename U>
GLM_FUNC_QUALIFIER GLM_CONSTEXPR vec<4, T, Q> & vec<4, T, Q>::operator*=(U scalar)
{
return (*this = detail::compute_vec4_mul<T, Q, detail::is_aligned<Q>::value>::call(*this, vec<4, T, Q>(scalar)));
}
GLM_FUNC_QUALIFIER GLM_CONSTEXPR static vec<4, T, Q> call(vec<4, T, Q> const& a, vec<4, T, Q> const& b)
{
return vec<4, T, Q>(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);
}
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER GLM_CONSTEXPR vec<4, T, Q>::vec(T scalar)
: x(scalar), y(scalar), z(scalar), w(scalar)
{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
我们再来理一下这个过程
首先是我们代码中,我们以一个单位矩阵和一个缩放向量为参数调用glm的scale函数,scale函数首先取出单位矩阵的第一行的数据和缩放向量的第一个分量做乘法,结果得到一个缩放后的向量。
而glm计算向量数乘的方法是,首先用这个标量构建一个所有分量都是该标量的向量,再用这个向量和要计算的向量的分量依次相乘,将得到的结果构造一个结果向量。
而我们传进去的是一个四阶单位向量和一个三维的缩放比例,我们想要的结果是单位的第一二三行进行缩放,第四行还是1,所以scale函数只计算了前三行的比例缩放,第四行原样输出,至此,我们得到了一个这样的矩阵
( 0.5 0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 0 1 ) \left(
0.5000amp;0amp;0.5amp;0amp;0amp;0amp;0amp;0.5amp;0amp;0amp;0amp;0amp;1
0.5amp;0amp;0amp;00amp;0.5amp;0amp;00amp;0amp;0.5amp;00amp;0amp;0amp;1
\right)
⎝
⎜
⎜
⎛
0.5
0
0
0
0
0.5
0
0
0
0
0.5
0
0
0
0
1
⎠
⎟
⎟
⎞
这就是我们的缩放矩阵了,再将这个缩放矩阵传到着色器中,与顶点数据相乘,就得到了缩放后的顶点向量。这一过程是在GPU中进行的,因此,我们无法看到矩阵乘法是怎么做的。
不过不用担心,glm应该会有矩阵乘法的代码,本次没有用到,就先不研究了,等以后用到了在研究一下矩阵乘法在计算机中是怎么实现的。
位移
位移(Translation)是在原始向量的基础上加上另一个向量从而获得一个在不同位置的新向量的过程,从而在位移向量基础上移动了原始向量。我们已经讨论了向量加法,所以这应该不会太陌生。
我们先来尝试位移向量v ⃗ = ( 3 , 2 ) \vec{v} =(3,2)
v
=(3,2)。我们可以把向量沿着x轴向左平移1个单位,沿着y轴向右平移两个单位,下面是平移后所获得的s ⃗ \vec{s}
s
。
我们可以通过将向量v ⃗ \vec{v}
v
的x分量减1,y分量加2得到结果向量s ⃗ \vec{s}
s
。和缩放矩阵类似,我们需要一个位移矩阵。如果我们把位移向量表示为( T x , T y , T z ) (T_x,T_y,T_z)(T
x
,T
y
,T
z
),我们就能把位移矩阵定义为:
( 1 0 0 T x 0 1 0 T y 0 0 1 T z 0 0 0 1 ) ⋅ ( x y z 1 ) = ( x + T x y + T y z + T z 1 ) \left(
1000amp;0amp;1amp;0amp;0amp;0amp;0amp;1amp;0amp;Txamp;Tyamp;Tzamp;1
1amp;0amp;0amp;Tx0amp;1amp;0amp;Ty0amp;0amp;1amp;Tz0amp;0amp;0amp;1
\right) \cdot \left(
xyz1
xyz1
\right) = \left(
x+Txy+Tyz+Tz1
x+Txy+Tyz+Tz1
\right)
⎝
⎜
⎜
⎛
1
0
0
0
0
1
0
0
0
0
1
0
T
x
T
y
T
z
1
⎠
⎟
⎟
⎞
⋅
⎝
⎜
⎜
⎛
x
y
z
1
⎠
⎟
⎟
⎞
=
⎝
⎜
⎜
⎛
x+T
x
y+T
y
z+T
z
1
⎠
⎟
⎟
⎞
齐次坐标
这里有必要介绍一下齐次坐标。很多新手朋友一定会感到奇怪,为什么这个位移矩阵和上面的缩放矩阵都使用四维向量而不是三维向量?让我们用三维的来计算一下看会怎么样:
( 1 0 T x 0 1 T y 0 0 T z ) ⋅ ( x y z ) = ( 1 ⋅ x + 0 ⋅ y + T x ⋅ z 0 ⋅ x + 1 ⋅ y + T y ⋅ z 0 ⋅ x + 0 ⋅ y + T z ⋅ z ) = ( z ⋅ T x z ⋅ T y T z ) \left(
100amp;0amp;1amp;0amp;Txamp;Tyamp;Tz
1amp;0amp;Tx0amp;1amp;Ty0amp;0amp;Tz
\right) \cdot \left(
xyz
xyz
\right) = \left(
1⋅x+0⋅y+Tx⋅z0⋅x+1⋅y+Ty⋅z0⋅x+0⋅y+Tz⋅z
1⋅x+0⋅y+Tx⋅z0⋅x+1⋅y+Ty⋅z0⋅x+0⋅y+Tz⋅z
\right)= \left(
z⋅Txz⋅TyTz
z⋅Txz⋅TyTz
\right)
⎝
⎛
1
0
0
0
1
0
T
x
T
y
T
z
⎠
⎞
⋅
⎝
⎛
x
y
z
⎠
⎞
=
⎝
⎛
1⋅x+0⋅y+T
x
⋅z
0⋅x+1⋅y+T
y
⋅z
0⋅x+0⋅y+T
z
⋅z
⎠
⎞
=
⎝
⎛
z⋅T
x
z⋅T
y
T
z
⎠
⎞
可以看到,这完全不是我们想要的结果。这还只是小问题,在使用矩阵计算变换时,矩阵的乘积只能表示旋转和缩放,不能表示平移。为统一计算,引入了第四个分量w,这使得原本二维坐标变成三维,三维坐标变为四维,w称为比例因子,一个三维坐标的三个分量x,y,z用齐次坐标表示为变为x,y,z,w的四维空间,变换成三维坐标的方式是x/w,y/w,z/w。当w为0时,在数学上代表无穷远点,即并非一个具体的坐标位置,而是一个具有大小和方向的向量。从而,通过w我们就可以用同一系统表示两种不同的量。
在OpenGL中,作为坐标点时,w参数为1,否则为0,如此一来,所有的几何变换和向量运算都可以用相同的矩阵乘积进行运算和变换,当一个向量和一个矩阵相乘时所得的结果也是向量。
这里只是简单介绍一点齐次坐标的皮毛,想要深入了解的朋友可以自行去找相关的资料。
glm位移矩阵实现
和缩放一样,我们先找找之前章节中用到的位移功能
trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
1
关于glm中矩阵的创建和表示这里不在赘述,需要注意的是,在glm::mat4类中的向量数组,每个元素都是矩阵中的一列,不是一行,这一点在本小节中尤为重要,如果不清楚这点,本小节会看的很蒙逼。
我们来看下translate的函数实现
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER mat<4, 4, T, Q> translate(mat<4, 4, T, Q> const& m, vec<3, T, Q> const& v)
{
mat<4, 4, T, Q> Result(m);
Result[3] = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3];
return Result;
}
1
2
3
4
5
6
7
关于[]和*的重载在缩放小节中就说过了,这里不再多说。在translate中,首先用我们传进去的单位矩阵m拷贝构造了一个结果矩阵Result,然后取出m的第一列向量,与位移向量的x分量相乘,本例中得到的结果是( 0.5 , 0 , 0 , 0 ) (0.5,0,0,0)(0.5,0,0,0)。接着是取出第二、三列向量分别与位移向量y,z分量相乘,本例中得到的结果是( 0 , − 0.5 , 0 , 0 ) (0,-0.5,0,0)(0,−0.5,0,0)和( 0 , 0 , 0 , 0 ) (0,0,0,0)(0,0,0,0),最后将这三个向量和m的第四列向量( 0 , 0 , 0 , 1 ) (0,0,0,1)(0,0,0,1)加到一起,得到一个结果向量( 0.5 , − 0.5 , 0 , 1 ) (0.5,-0.5,0,1)(0.5,−0.5,0,1)。最后再将这个结果向量赋值给Result的第四列,得到下面这个位移矩阵:
( 1 0 0 0.5 0 1 0 − 0.5 0 0 1 0 0 0 0 1 ) \left(
1000amp;0amp;1amp;0amp;0amp;0amp;0amp;1amp;0amp;0.5amp;−0.5amp;0amp;1
1amp;0amp;0amp;0.50amp;1amp;0amp;−0.50amp;0amp;1amp;00amp;0amp;0amp;1
\right)
⎝
⎜
⎜
⎛
1
0
0
0
0
1
0
0
0
0
1
0
0.5
−0.5
0
1
⎠
⎟
⎟
⎞
结果得到了,我们再来看一下+的重载是怎么实现的
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER GLM_CONSTEXPR vec<4, T, Q> operator+(vec<4, T, Q> const& v1, vec<4, T, Q> const& v2)
{
return vec<4, T, Q>(v1) += v2;
}
template<typename U>
GLM_FUNC_QUALIFIER GLM_CONSTEXPR vec<4, T, Q> & vec<4, T, Q>::operator+=(vec<4, U, Q> const& v)
{
return (*this = detail::compute_vec4_add<T, Q, detail::is_aligned<Q>::value>::call(*this, vec<4, T, Q>(v)));
}
template<typename T, qualifier Q, bool Aligned>
struct compute_vec4_add
{
GLM_FUNC_QUALIFIER GLM_CONSTEXPR static vec<4, T, Q> call(vec<4, T, Q> const& a, vec<4, T, Q> const& b)
{
return vec<4, T, Q>(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
过程和缩放类似,这里就不跟着分析了。
位移矩阵有了,接下来就是把位移矩阵传递个着色器,由GPU去和顶点数据计算了,和缩放类似,不在多说。
旋转
旋转是非常麻烦的一个操作,让我们先来一个最简单的绕原点旋转的二维旋转矩阵,先看下图
首先,我们需要复习一下两角和公式
sin ( α + β ) = sin α ⋅ cos β + cos α ⋅ sin β cos ( α + β ) = cos α ⋅ cos β − sin α ⋅ sin β \sin(\alpha+\beta)=\sin\alpha\cdot\cos\beta+\cos\alpha\cdot\sin\beta \\ \cos(\alpha+\beta)=\cos\alpha\cdot\cos\beta-\sin\alpha\cdot\sin\beta
sin(α+β)=sinα⋅cosβ+cosα⋅sinβ
cos(α+β)=cosα⋅cosβ−sinα⋅sinβ
设点A坐标为( x , y ) (x,y)(x,y),点B坐标为( x ′ , y ′ ) (x',y')(x
′
,y
′
),|u ⃗ \vec{u}
u
|=r
cos ( α + β ) = x ′ r sin ( α + β ) = y ′ r ⇓ x ′ = r ⋅ cos ( α + β ) y ′ = r ⋅ sin ( α + β ) \cos(\alpha+\beta)=\frac{x'}{r} \\ \sin(\alpha+\beta)=\frac{y'}{r} \\ \Downarrow{} \\ x'=r\cdot\cos(\alpha+\beta) \\ y'=r\cdot\sin(\alpha+\beta) \\
cos(α+β)=
r
x
′
sin(α+β)=
r
y
′
⇓
x
′
=r⋅cos(α+β)
y
′
=r⋅sin(α+β)
再将x ′ y ′ x'y'x
′
y
′
带入到两角和公式
x ′ = r ⋅ cos α ⋅ cos β − r ⋅ sin α ⋅ sin β y ′ = r ⋅ sin α ⋅ cos β + r ⋅ sin α ⋅ sin β x'=r\cdot\cos\alpha\cdot\cos\beta-r\cdot\sin\alpha\cdot\sin\beta \\ y'=r\cdot\sin\alpha\cdot\cos\beta+r\cdot\sin\alpha\cdot\sin\beta \\
x
′
=r⋅cosα⋅cosβ−r⋅sinα⋅sinβ
y
′
=r⋅sinα⋅cosβ+r⋅sinα⋅sinβ
同x ′ y ′ x'y'x
′
y
′
一样,x xx和y yy可以写成下面这种形式
x = r ⋅ cos α y = r ⋅ sin α x=r\cdot\cos\alpha \\ y=r\cdot\sin\alpha \\
x=r⋅cosα
y=r⋅sinα
最后将x y xyxy带入到x ′ y ′ x'y'x
′
y
′
中
x ′ = x ⋅ cos β − y ⋅ sin β y ′ = x ⋅ sin β + y ⋅ cos β x'=x\cdot\cos\beta-y\cdot\sin\beta \\ y'=x\cdot\sin\beta+y\cdot\cos\beta
x
′
=x⋅cosβ−y⋅sinβ
y
′
=x⋅sinβ+y⋅cosβ
整理成矩阵格式就是
( x ′ y ′ ) = ( cos β − sin β sin β cos β ) ⋅ ( x y ) \left(
\begin{array}{ccc} x' \\ y' \end{array}
\begin{array}{ccc} x' \\ y' \end{array}
\right) = \left(
cosβsinβamp;−sinβamp;cosβ
cosβamp;−sinβsinβamp;cosβ
\right) \cdot \left(
xy
xy
\right)
(
x
′
y
′
)=(
cosβ
sinβ
−sinβ
cosβ
)⋅(
x
y
)
至此,最简单的一个向量旋转公式推导完成。
其他复杂的旋转矩阵就不再推导了,实在太麻烦,下面只给出结果
沿x轴旋转
( 1 0 0 0 0 cos θ − sin θ 0 0 sin θ cos θ 0 0 0 0 1 ) ⋅ ( x y z 1 ) = ( x cos θ ⋅ y − sin θ ⋅ z sin θ ⋅ y + cos θ ⋅ z 1 ) \left(
1000amp;0amp;cosθamp;sinθamp;0amp;0amp;−sinθamp;cosθamp;0amp;0amp;0amp;0amp;1
1amp;0amp;0amp;00amp;cosθamp;−sinθamp;00amp;sinθamp;cosθamp;00amp;0amp;0amp;1
\right) \cdot \left(
xyz1
xyz1
\right)= \left(
xcosθ⋅y−sinθ⋅zsinθ⋅y+cosθ⋅z1
xcosθ⋅y−sinθ⋅zsinθ⋅y+cosθ⋅z1
\right)
⎝
⎜
⎜
⎛
1
0
0
0
0
cosθ
sinθ
0
0
−sinθ
cosθ
0
0
0
0
1
⎠
⎟
⎟
⎞
⋅
⎝
⎜
⎜
⎛
x
y
z
1
⎠
⎟
⎟
⎞
=
⎝
⎜
⎜
⎛
x
cosθ⋅y−sinθ⋅z
sinθ⋅y+cosθ⋅z
1
⎠
⎟
⎟
⎞
沿y轴旋转
( cos θ 0 sin θ 0 0 1 0 0 − sin θ 0 cos θ 0 0 0 0 1 ) ⋅ ( x y z 1 ) = ( cos θ ⋅ x + sin θ ⋅ z y − sin θ ⋅ x + cos θ ⋅ z 1 ) \left(
cosθ0−sinθ0amp;0amp;1amp;0amp;0amp;sinθamp;0amp;cosθamp;0amp;0amp;0amp;0amp;1
cosθamp;0amp;sinθamp;00amp;1amp;0amp;0−sinθamp;0amp;cosθamp;00amp;0amp;0amp;1
\right) \cdot \left(
xyz1
xyz1
\right)= \left(
cosθ⋅x+sinθ⋅zy−sinθ⋅x+cosθ⋅z1
cosθ⋅x+sinθ⋅zy−sinθ⋅x+cosθ⋅z1
\right)
⎝
⎜
⎜
⎛
cosθ
0
−sinθ
0
0
1
0
0
sinθ
0
cosθ
0
0
0
0
1
⎠
⎟
⎟
⎞
⋅
⎝
⎜
⎜
⎛
x
y
z
1
⎠
⎟
⎟
⎞
=
⎝
⎜
⎜
⎛
cosθ⋅x+sinθ⋅z
y
−sinθ⋅x+cosθ⋅z
1
⎠
⎟
⎟
⎞
沿z轴旋转
( cos θ − sin θ 0 0 sin θ cos θ 0 0 0 0 1 0 0 0 0 1 ) ⋅ ( x y z 1 ) = ( cos θ ⋅ x − sin θ ⋅ y sin θ ⋅ x + cos θ ⋅ y z 1 ) \left(
cosθsinθ00amp;−sinθamp;cosθamp;0amp;0amp;0amp;0amp;1amp;0amp;0amp;0amp;0amp;1
cosθamp;−sinθamp;0amp;0sinθamp;cosθamp;0amp;00amp;0amp;1amp;00amp;0amp;0amp;1
\right) \cdot \left(
xyz1
xyz1
\right)= \left(
cosθ⋅x−sinθ⋅ysinθ⋅x+cosθ⋅yz1
cosθ⋅x−sinθ⋅ysinθ⋅x+cosθ⋅yz1
\right)
⎝
⎜
⎜
⎛
cosθ
sinθ
0
0
−sinθ
cosθ
0
0
0
0
1
0
0
0
0
1
⎠
⎟
⎟
⎞
⋅
⎝
⎜
⎜
⎛
x
y
z
1
⎠
⎟
⎟
⎞
=
⎝
⎜
⎜
⎛
cosθ⋅x−sinθ⋅y
sinθ⋅x+cosθ⋅y
z
1
⎠
⎟
⎟
⎞
沿任意轴旋转
对于任意轴旋转,一个简单的办法是拆分成x y z xyzxyz三轴分解旋转,即先沿x xx轴旋转,再沿y yy轴旋转,最后沿z zz轴旋转。但是,将一个旋转分解成三次,很容易出现万向节死锁。万向节死锁简单说就是当子旋转转到特定角度后转动,一旦转动父旋转,就会使子旋转丢失旋转角度,从而使某些角度永远也不能旋转到。关于万向节死锁的动画演示可以看这个视频欧拉旋转。
一个比较好的方法是有一个旋转矩阵可以对任意轴进行旋转,而不是将一个旋转分解成x y z xyzxyz三轴。这样的矩阵是存在的,但却非常麻烦,关于这个矩阵的推导,有兴趣的同学可以自行查找相关资料。具体公式见下面,其中( R x , R y , R z ) (R_x,R_y,R_z)(R
x
,R
y
,R
z
)就是这个任意轴向量
( cos θ + R x 2 ( 1 − cos θ ) R x R y ( 1 − cos θ ) − R z sin θ R x R z ( 1 − cos θ ) + R y sin θ 0 R y R x ( 1 − cos θ ) + R z sin θ cos θ + R y 2 ( 1 − cos θ ) R y R z ( 1 − cos θ ) − R x sin θ 0 R z R x ( 1 − cos θ ) − R y sin θ R z R y ( 1 − cos θ ) + R x sin θ cos θ + R z 2 ( 1 − cos θ ) 0 0 0 0 1 ) \left(
cosθ+R2x(1−cosθ)RyRx(1−cosθ)+RzsinθRzRx(1−cosθ)−Rysinθ0amp;RxRy(1−cosθ)−Rzsinθamp;cosθ+R2y(1−cosθ)amp;RzRy(1−cosθ)+Rxsinθamp;0amp;RxRz(1−cosθ)+Rysinθamp;RyRz(1−cosθ)−Rxsinθamp;cosθ+R2z(1−cosθ)amp;0amp;0amp;0amp;0amp;1
cosθ+Rx2(1−cosθ)amp;RxRy(1−cosθ)−Rzsinθamp;RxRz(1−cosθ)+Rysinθamp;0RyRx(1−cosθ)+Rzsinθamp;cosθ+Ry2(1−cosθ)amp;RyRz(1−cosθ)−Rxsinθamp;0RzRx(1−cosθ)−Rysinθamp;RzRy(1−cosθ)+Rxsinθamp;cosθ+Rz2(1−cosθ)amp;00amp;0amp;0amp;1
\right)
⎝
⎜
⎜
⎛
cosθ+R
x
2
(1−cosθ)
R
y
R
x
(1−cosθ)+R
z
sinθ
R
z
R
x
(1−cosθ)−R
y
sinθ
0
R
x
R
y
(1−cosθ)−R
z
sinθ
cosθ+R
y
2
(1−cosθ)
R
z
R
y
(1−cosθ)+R
x
sinθ
0
R
x
R
z
(1−cosθ)+R
y
sinθ
R
y
R
z
(1−cosθ)−R
x
sinθ
cosθ+R
z
2
(1−cosθ)
0
0
0
0
1
⎠
⎟
⎟
⎞
需要注意的是,这个旋转矩阵虽然能够极大的避免万向节死锁的出现,但并不能完全避免。想要彻底避免万向节死锁问题,真正的解决方案是使用四元数(Quaternion),它不仅更安全,而且计算会更有效率。由于我目前的学习进度还没有遇到四元数,暂时不多说,等以后遇到了再回来填坑。
glm旋转矩阵实现
前面的缩放和位移已经把glm的基础问题扒的差不多了,关于具体的计算机实现部分暂时没有值得深扒的,主要看一下数学上的算法实现即可,下面是旋转函数实现
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER mat<4, 4, T, Q> rotate(mat<4, 4, T, Q> const& m, T angle, vec<3, T, Q> const& v)
{
T const a = angle;
T const c = cos(a);
T const s = sin(a);
vec<3, T, Q> axis(normalize(v));
vec<3, T, Q> temp((T(1) - c) * axis);
mat<4, 4, T, Q> Rotate;
Rotate[0][0] = c + temp[0] * axis[0];
Rotate[0][1] = temp[0] * axis[1] + s * axis[2];
Rotate[0][2] = temp[0] * axis[2] - s * axis[1];
Rotate[1][0] = temp[1] * axis[0] - s * axis[2];
Rotate[1][1] = c + temp[1] * axis[1];
Rotate[1][2] = temp[1] * axis[2] + s * axis[0];
Rotate[2][0] = temp[2] * axis[0] + s * axis[1];
Rotate[2][1] = temp[2] * axis[1] - s * axis[0];
Rotate[2][2] = c + temp[2] * axis[2];
mat<4, 4, T, Q> Result;
Result[0] = m[0] * Rotate[0][0] + m[1] * Rotate[0][1] + m[2] * Rotate[0][2];
Result[1] = m[0] * Rotate[1][0] + m[1] * Rotate[1][1] + m[2] * Rotate[1][2];
Result[2] = m[0] * Rotate[2][0] + m[1] * Rotate[2][1] + m[2] * Rotate[2][2];
Result[3] = m[3];
return Result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
这个函数也没啥可说的,就是对上面的旋转矩阵的实现。
矩阵的组合
使用矩阵进行变换的真正力量在于,根据矩阵之间的乘法,我们可以把多个变换组合到一个矩阵中。让我们看看我们是否能生成一个变换矩阵,让它组合多个变换。假设我们有一个顶点( x , y , z ) (x, y, z)(x,y,z),我们希望将其缩放2倍,然后位移( 1 , 2 , 3 ) (1, 2, 3)(1,2,3)个单位。我们需要一个位移和缩放矩阵来完成这些变换。结果的变换矩阵看起来像这样:
T r a n s ⋅ S c a l e = ( 1 0 0 1 0 1 0 2 0 0 1 3 0 0 0 1 ) ⋅ ( 2 0 0 0 0 2 0 0 0 0 2 0 0 0 0 1 ) = ( 2 0 0 1 0 2 0 2 0 0 2 3 0 0 0 1 ) Trans\cdot Scale=\left(
1000amp;0amp;1amp;0amp;0amp;0amp;0amp;1amp;0amp;1amp;2amp;3amp;1
1amp;0amp;0amp;10amp;1amp;0amp;20amp;0amp;1amp;30amp;0amp;0amp;1
\right) \cdot \left(
2000amp;0amp;2amp;0amp;0amp;0amp;0amp;2amp;0amp;0amp;0amp;0amp;1
2amp;0amp;0amp;00amp;2amp;0amp;00amp;0amp;2amp;00amp;0amp;0amp;1
\right)= \left(
2000amp;0amp;2amp;0amp;0amp;0amp;0amp;2amp;0amp;1amp;2amp;3amp;1
2amp;0amp;0amp;10amp;2amp;0amp;20amp;0amp;2amp;30amp;0amp;0amp;1
\right)
Trans⋅Scale=
⎝
⎜
⎜
⎛
1
0
0
0
0
1
0
0
0
0
1
0
1
2
3
1
⎠
⎟
⎟
⎞
⋅
⎝
⎜
⎜
⎛
2
0
0
0
0
2
0
0
0
0
2
0
0
0
0
1
⎠
⎟
⎟
⎞
=
⎝
⎜
⎜
⎛
2
0
0
0
0
2
0
0
0
0
2
0
1
2
3
1
⎠
⎟
⎟
⎞
注意,当矩阵相乘时我们先写位移再写缩放变换的。矩阵乘法是不遵守交换律的,这意味着它们的顺序很重要。当矩阵相乘时,在最右边的矩阵是第一个与向量相乘的,所以你应该从右向左读这个乘法。建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移,否则它们会(消极地)互相影响。比如,如果你先位移再缩放,位移的向量也会同样被缩放,比如向某方向移动2米,2米也许会被缩放成1米!
用最终的变换矩阵左乘我们的向量会得到以下结果:
T r a n s ⋅ S c a l e = ( 2 0 0 1 0 2 0 2 0 0 2 3 0 0 0 1 ) ⋅ ( x y z 1 ) = ( 2 x + 1 2 y + 2 2 y + 3 1 ) Trans\cdot Scale=\left(
2000amp;0amp;2amp;0amp;0amp;0amp;0amp;2amp;0amp;1amp;2amp;3amp;1
2amp;0amp;0amp;10amp;2amp;0amp;20amp;0amp;2amp;30amp;0amp;0amp;1
\right) \cdot \left(
xyz1
xyz1
\right)= \left(
2x+12y+22y+31
2x+12y+22y+31
\right)
Trans⋅Scale=
⎝
⎜
⎜
⎛
2
0
0
0
0
2
0
0
0
0
2
0
1
2
3
1
⎠
⎟
⎟
⎞
⋅
⎝
⎜
⎜
⎛
x
y
z
1
⎠
⎟
⎟
⎞
=
⎝
⎜
⎜
⎛
2x+1
2y+2
2y+3
1
⎠
⎟
⎟
⎞
一定要注意,先执行的操作要在乘法的右边,否则结果是错误的,我们看下图
懒得手动去算了,我们找个矩阵计算器来算一下。上图中l1是缩放矩阵,m1是位移矩阵,m2是缩放在左位移在右,m3是位移在左,缩放在右。虽然先做的在右边有些反直觉,但由于矩阵乘法性质决定,先做的一定要放在右边,否则结果就是错误的,这是因为最右边的矩阵最先与待变换的向量相乘。
设位移矩阵为A,缩放矩阵为B,被操作向量为C,完整的计算过程为A ⋅ B ⋅ C A\cdot B\cdot CA⋅B⋅C,由于矩阵乘法满足结合律,所以有( A ⋅ B ) ⋅ C = A ⋅ ( B ⋅ C ) (A\cdot B)\cdot C=A\cdot (B\cdot C)(A⋅B)⋅C=A⋅(B⋅C)。因此,我们可以把这一复合变换过程理解为是从右向左依次做乘法,而根据这个顺序,右边的矩阵先和待变换矩阵进行计算。但又因为矩阵乘法不满足交换律,因此,必须把先做的事放在右边,顺序一定不能错。
glm矩阵组合使用
上面的先缩放,后位移怎么用glm去实现呢?看下面代码
// 首先创建一个四阶单位矩阵
glm::mat4 trans(1.0f);
// 由于位移是最后做的,所以位移矩阵应该放在左边,体现在代码中就是先将单位矩阵位移
trans = glm::translate(trans, glm::vec3(1f, 2f,3f));
trans = glm::scale(trans, glm::vec3(2, 2, 2));
// 创建好变换矩阵后将变换矩阵传递个顶点着色器
unsigned int transformLoc = glGetUniformLocation(shaderProgram, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
1
2
3
4
5
6
7
8
由矩阵乘法的性质决定,在代码中,后做的事要放在前面,先做的事要放在后面,我们来简单分析一下原因。
首先创建了一个单位矩阵、一个位移向量和一个缩放向量。glm::translate函数和glm::scale函数在前面的章节中都分析过,如果想要在代码中达到位移在左,缩放在右的目的,我们首先要把单位矩阵变成位移矩阵,然后再把缩放矩阵拿到位移变换后的单位矩阵中进行缩放变换。
————————————————
原文链接:https://blog.csdn.net/mumufan05/article/details/100045770
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」