OpenGL ES教程——变换

637 阅读7分钟

image.png

到目前为止,我们仍然只是写一些静态的效果,比如画三角形,绘制纹理等,但如果想要绘制一些动态的效果呢?那就需要用到矩阵了。

矩阵的应用范围很广,我们可以借助矩阵进行缩放、旋转、位移等操作。我们输入的顶点坐标是向量,但也可以看作是N×1的矩阵,通过矩阵变化就能改变顶点坐标值,这样图形自然就变换了

1、向量

向量有两个最基本的元素:

  • 大小(或者称为长度):假设向量在二维坐标系上的终点坐标为(x,y),那么大小为x2+y2\sqrt{x^2 + y^2}
  • 方向:从原点指向(x、y)点

image.png

上图中的v和w向量,可以认为是同一个向量,因为它们的大小及方向均相同。

1.1 向量与标量运算

标量(Scalar)只是一个数字(或者说是仅有一个分量的向量)。当把一个向量加/减/乘/除一个标量,我们可以简单的把向量的每个分量分别进行该运算。对于加法来说会像这样:

image.png

1.2 向量取反

对一个向量取反(Negate)会将其方向逆转。一个指向东北的向量取反后就指向西南方向了。我们在一个向量的每个分量前加负号就可以实现取反了(或者说用-1数乘该向量):

image.png

1.3 向量加减

向量的加法可以被定义为是分量的(Component-wise)相加,即将一个向量中的每一个分量加上另一个向量的对应分量:

image.png

向量v = (4, 2)k = (1, 2)可以直观地表示为:

image.png

向量相减和相加类似,如下图所示:

image.png

1.4 向量乘法

向量乘法有两种,点乘和叉乘。

  • 点乘:两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值。可能听起来有点费解,我们来看一下公式:

image.png

  • 叉乘:叉乘只在3D空间中有定义,它需要两个不平行向量作为输入,生成一个正交于两个输入向量的第三个向量。如果输入的两个向量也是正交的,那么叉乘之后将会产生3个互相正交的向量。接下来的教程中这会非常有用。下面的图片展示了3D空间中叉乘的样子:

image.png

image.png

2、矩阵

矩阵的基础加减法以及乘法,这里不再细说了,线性代数应该都还没有忘记。

2.1、矩阵和向量相乘

向量,前文我们就已经说过了,可以认为是一个N×1的矩阵。这样,矩阵和向量就能够相乘了。可有同学会问了,为什么向量是N×1的矩阵,而不是1×N的矩阵,这个问题问得好,因为我也不知道。。。

不过,大家一定要记住这个结论,因为矩阵相乘不支持交换率,向量的形式决定了矩阵相乘时的位置。

例如,现在要进行矩阵变化,变化矩阵有两个,分别是T和M,它们都是4×4的矩阵,需求是先进行T变换,再进行M变换,那我们怎么来写呢?

M × T × Vec4

为什么要这么写呢?因为向量是N×1的矩阵,所以只能把向量放在变换乘号的右边(如果向量放乘号左边,这两个矩阵无法相乘),如此则意味着,向量会先乘T,再乘M,如此则实现了先T变换,再M变换(变换的实质顺序要从右往左看)。

另外,进行矩阵变换时,一般我们用单位矩阵来做变换,为什么呢?

image.png

单位矩阵乘向量,向量完全没有变化,可以把单位矩阵理解为矩阵世界里的单位1。

2.1、缩放

我们从单位矩阵了解到,每个对角线元素会分别与向量的对应元素相乘。如果我们把1变为3会怎样?这样子的话,我们就把向量的每个元素乘以3了,这事实上就把向量缩放3倍。如果我们把缩放变量表示为(S1,S2,S3)(S1,S2,S3)我们可以为任意向量(x,y,z)(x,y,z)定义一个缩放矩阵:

image.png

注意,第四个缩放向量仍然是1,因为在3D空间中缩放w分量是无意义的。w分量另有其他用途,在后面我们会看到。

2.2、位移

和缩放矩阵一样,在4×4矩阵上有几个特别的位置用来执行特定的操作,对于位移来说它们是第四列最上面的3个值。如果我们把位移向量表示为(Tx,Ty,Tz)(Tx,Ty,Tz),我们就能把位移矩阵定义为:

image.png

2.3、旋转

旋转的公式难以理解。在旋转时通常要指定两个参数,一是旋转的角度,另一个就是旋转轴。旋转角度特别好理解,旋转轴是什么?事实上我们一直接触的是二维坐标系,没有接触过三维坐标,如果在3D空间中就能明白旋转轴是什么了。简单来说,二维坐标系上,旋转轴都是Z轴

image.png

公式这里就不贴了,大家可以自行研究。

虽然公式没有贴,但我们有更加人性化的api接口可以贴,现在一般用glm库进行矩阵变换,变换的接口为:

glm::rotate(trans, glm::radians((float)degree), glm::vec3(0.0f, 0.0f, 1.0f));

3、实践

glm三大变换矩阵接口为:

glm::mat4 trans(1.0f);
//位移
trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
//旋转
trans = glm::rotate(trans, glm::radians((float)mDegree), glm::vec3(0.0f, 0.0f, 1.0f));
//缩放
trans = glm::scale(mat, glm::vec3(0.8, 0.8, 0.8));

现在我想要实现“不停地旋转一张图片,然后位移图片到左下角”,该怎么办呢?

在上文中提到了,变换矩阵组合的顺序问题,先旋转再位移,矩阵顺序就得这么来: 位移 × 旋转 × 向量

参见上一篇文章中对uniform变量的说明,可以让GlSurfaceView不停地执行onDraw方法,然后在draw方法中不停地对uniform变量赋值。

本例中的片段着色器可以不用更改,只需要改顶点着色器:

#version 300 es
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texCoord;
out vec2 v_texCoord;
uniform mat4 transform;

void main() {
    //顶点着色器这里也是矩阵乘以向量,向量在乘号右边
    gl_Position = transform * a_position;
    v_texCoord = a_texCoord;
}

我们再看看draw方法是怎么写的?

void TransformSample::draw() {
    if (m_ProgramObj == 0) {
        return;
    }
    glClearColor(1.0f, 1.0f, 1.0f, 1);
    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(m_ProgramObj);
    glBindVertexArray(mVaoId);

    //指定片段着色器中名为lyfId的变量,使用序号为0的纹理
    GLUtils::setUniformValue1i(m_ProgramObj, "lyfId", 0);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, mLyfTextureId);

    //测试代码,位移
//    glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);
//    glm::mat4 trans(1.0f);
//    trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));
//    vec = trans * vec;
//    LOGI("x = %f, y = %f, z = %f", vec.x, vec.y, vec.z);

    //测试代码,旋转缩放
//    glm::mat4 trans(1.0f);
//    trans = glm::rotate(mat, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0f));
//    trans = glm::scale(mat, glm::vec3(0.8, 0.8, 0.8));

    mDegree += 1.0f;
    glm::mat4 trans(1.0f);
    trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
    trans = glm::rotate(trans, glm::radians((float)mDegree), glm::vec3(0.0f, 0.0f, 1.0f));
    unsigned int transformLoc = glGetUniformLocation(m_ProgramObj, "transform");
    glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (const void*)0);
}

请注意,这里原本是想用时间戳来转float,但有两个问题:

  • 时间戳是long,直接转float会损失精度,导致值变化得很慢,一秒钟才能变一次
  • 如果用求余的方式缩小原值,再转float,这样变化慢的问题解决了,但有另外一个问题,角度变化太快了,因为GLSurfaceView的刷新时间真的快,角度变化稍微快一点,就不太好看了

基于以上两个原因,所以本人这里用了一个成员变量,每次绘制时自动加1,这样效果较好。

最后,我们看看效果:

1671361530532.gif