Opengl ES之矩陣變換(上)

2023-03-21 15:01:31

前言

說到矩陣變換,我們第一時間想到的就是大學時代的線性代數這些複雜的東西,突然有了一種令人從入門到放棄的念頭,不慌,作為了一個應用層的CV工程師,
在實際應用中線性代數哪些複雜的計算根本不用我們自己去算,絕大部分情境下直接使用Matrix這個類或者glm這個庫即可。

關於矩陣與向量的相關知識,矩陣的加減乘除等規則,這裡就不展開細說,感興趣的同學自行查閱線性代數即可,不過這些規則忘記了也沒關係,反正有API可用。

我們知道在Opengl中有很多中座標系,在Opengl中矩陣的一大作用就是將座標從一個座標系轉換到另一個座標系下,同時還可以通過矩陣實現一些形變的效果,
今天我們就使用矩陣的方式搭配Opengl ES實現平移、縮放、旋轉等一些形變變換的效果。

通常來說在Opengl ES中的矩陣都是一個4X4的矩陣,也就是一個包含16個元素的一維陣列。

下面以Matrix這個類介紹一下矩陣變換的一些常用方法。下面介紹的矩陣變換所參考的座標系統都是一樣的,均是下圖這個:

單位矩陣

所謂的單位矩陣就是左上角到右下角對角線值均為1的矩陣,又成為單元矩陣。使用Matrix.setIdentityM方法可以將一個矩陣變為單位矩陣。

矩陣平移

矩陣平移所使用的方法是Matrix.translateM

需要注意的是在Opengl在頂點座標的值是在-1到1之間,因此translateX的範圍可以為-2到2。為什麼呢?因為-1到1的距離是2,因此往最多可以往左移動2,同理,最多可以往右移動2。

矩陣旋轉

矩陣旋轉所使用的方法是Matrix.rotateM,其中第三個引數是表示選旋轉的角度,後面的三個引數xyz代表的是繞那個軸旋轉,繞那個軸旋轉就把那個軸的引數設定成1,其他軸設定成0即可。

矩陣縮放

矩陣縮放所使用的方法是Matrix.scaleM

組合矩陣的寫法

假如有以下形變步驟,先繞Z軸旋轉90度,再向X軸平移0.5,最後X軸縮放0.9倍,那麼最終這個形變矩陣該如何計算呢?是以下這個寫法嗎?

Matrix.rotateM(mvpMatrix, 0, 90, 0, 0, 1);
Matrix.translateM(mvpMatrix, 0, 0.5, 0, 0);
Matrix.scaleM(mvpMatrix, 0, 0.9, 1f, 0f);

不是的,組合矩陣的寫法有一個規則,這個規則大家一定要記住:

在組合矩陣時,先進行縮放操作,然後是旋轉,最後才是位移,但是寫法需要反正寫,也就是先寫translateM,然後rotateM,最後scaleM

如果不這樣寫會發生什麼呢?例如順著寫,先寫scaleM,然後是rotateM,最後寫translateM,測試時就會出現問題,旋轉超過180度之後再移動,就會出現移動方向相反的情況。

因此以上例子正確的寫法應該是這樣子的:

Matrix.translateM(mvpMatrix, 0, 0.5, 0, 0);
Matrix.rotateM(mvpMatrix, 0, 90, 0, 0, 1);
Matrix.scaleM(mvpMatrix, 0, 0.9, 1f, 0f);

show me code

在Opengl ES中可以使用mat4來表示一個4X4的矩陣,我們將總的變換矩陣在CPU中計算好之後以uniform的形式傳遞到著色器中去。
在頂點著色器中將矩陣與頂點座標相乘的結果作為新的頂點輸出座標即可完成矩陣變換。

以下是MatrixTransformOpengl.cpp的詳細程式碼:

// 頂點著色器
static const char *ver = "#version 300 es\n"
                         "in vec4 aPosition;\n"
                         "in vec2 aTexCoord;\n"
                         "out vec2 TexCoord;\n"
                         "uniform mat4 mvpMatrix;\n"
                         "void main() {\n"
                         "  TexCoord = aTexCoord;\n"
                         "  gl_Position = mvpMatrix * aPosition;\n"
                         "}";

// 片元著色器
static const char *fragment = "#version 300 es\n"
                              "precision mediump float;\n"
                              "out vec4 FragColor;\n"
                              "in vec2 TexCoord;\n"
                              "uniform sampler2D ourTexture;\n"
                              "void main()\n"
                              "{\n"
                              "    FragColor = texture(ourTexture, TexCoord);\n"
                              "}";


// 使用繪製兩個三角形組成一個矩形的形式(三角形帶)
// 第一第二第三個點組成一個三角形,第二第三第四個點組成一個三角形
const static GLfloat VERTICES[] = {
        1.0f,-1.0f, // 右下
        1.0f,1.0f, // 右上
        -1.0f,-1.0f, // 左下
        -1.0f,1.0f // 左上
};

// 貼圖紋理座標(參考手機螢幕座標系統,原點在左上角)
//由於對一個OpenGL紋理來說,它沒有內在的方向性,因此我們可以使用不同的座標把它定向到任何我們喜歡的方向上,然而大多數計算機影象都有一個預設的方向,它們通常被規定為y軸向下,X軸向右
const static GLfloat TEXTURE_COORD[] = {
        1.0f,1.0f, // 右下
        1.0f,0.0f, // 右上
        0.0f,1.0f, // 左下
        0.0f,0.0f // 左上
};

MatrixTransformOpengl::MatrixTransformOpengl():BaseOpengl() {
    initGlProgram(ver,fragment);
    positionHandle = glGetAttribLocation(program,"aPosition");
    textureHandle = glGetAttribLocation(program,"aTexCoord");
    textureSampler = glGetUniformLocation(program,"ourTexture");
    matrixHandle = glGetUniformLocation(program,"mvpMatrix");
}

MatrixTransformOpengl::~MatrixTransformOpengl() noexcept {
    LOGD("MatrixTransformOpengl解構函式");
}

void MatrixTransformOpengl::setMvpMatrix(float *mvp) {
    for (int i = 0; i < 16; ++i) {
        mvpMatrix[i] = mvp[i];
    }
}

void MatrixTransformOpengl::setPixel(void *data, int width, int height, int length) {
    LOGD("texture setPixel");
    imageWidth = width;
    imageHeight = height;
    glGenTextures(1, &textureId);

    // 啟用紋理,注意以下這個兩句是搭配的,glActiveTexture啟用的是那個紋理,就設定的sampler2D是那個
    // 預設是0,如果不是0的話,需要在onDraw的時候重新啟用一下?
//    glActiveTexture(GL_TEXTURE0);
//    glUniform1i(textureSampler, 0);

// 例如,一樣的
    glActiveTexture(GL_TEXTURE2);
    glUniform1i(textureSampler, 2);
    // 繫結紋理
    glBindTexture(GL_TEXTURE_2D, textureId);
    // 為當前繫結的紋理物件設定環繞、過濾方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
    // 生成mip貼圖
    glGenerateMipmap(GL_TEXTURE_2D);

    glBindTexture(GL_TEXTURE_2D, textureId);

    // 解繫結
    glBindTexture(GL_TEXTURE_2D, 0);
}

void MatrixTransformOpengl::onDraw() {

//    glViewport(0,0,imageWidth,imageHeight);

    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(program);

    // 啟用紋理
    glActiveTexture(GL_TEXTURE2);
    glUniform1i(textureSampler, 2);

    // 繫結紋理
    glBindTexture(GL_TEXTURE_2D, textureId);

    // 設定矩陣
    glUniformMatrix4fv(matrixHandle, 1, GL_FALSE,mvpMatrix);

    /**
     * size 幾個數位表示一個點,顯示是兩個數位表示一個點
     * normalized 是否需要歸一化,不用,這裡已經歸一化了
     * stride 步長,連續頂點之間的間隔,如果頂點直接是連續的,也可填0
     */
    // 啟用頂點資料
    glEnableVertexAttribArray(positionHandle);
    glVertexAttribPointer(positionHandle,2,GL_FLOAT,GL_FALSE,0,VERTICES);

    // 紋理座標
    glEnableVertexAttribArray(textureHandle);
    glVertexAttribPointer(textureHandle,2,GL_FLOAT,GL_FALSE,0,TEXTURE_COORD);

    // 4個頂點繪製兩個三角形組成矩形
    glDrawArrays(GL_TRIANGLE_STRIP,0,4);

    glUseProgram(0);

    // 禁用頂點
    glDisableVertexAttribArray(positionHandle);
    if(nullptr != eglHelper){
        eglHelper->swapBuffers();
    }

    glBindTexture(GL_TEXTURE_2D, 0);
}

java層的MatrixActivity.java範例程式碼如下:

public class MatrixActivity extends BaseGlActivity {

    private MatrixTransformOpengl matrixTransformOpengl;
    // 遵守先縮放再旋轉最後平移的順序
    // 首先執行縮放,接著旋轉,最後才是平移。這就是矩陣乘法的工作方式。
    private final float[] mvpMatrix = new float[16];
    // 因為在Opengl在頂點座標的值是在-1到1之間,因此translateX的範圍可以為-2到2。
    private float translateX = 0;
    private float scaleX = 1;
    private float rotationZ = 0;

    @Override
    public int getLayoutId() {
        return R.layout.activity_gl_matrix;
    }

    @Override
    public BaseOpengl createOpengl() {
        matrixTransformOpengl = new MatrixTransformOpengl();
        return matrixTransformOpengl;
    }

    @Override
    public Bitmap requestBitmap() {
        BitmapFactory.Options options = new BitmapFactory.Options();
        // 不縮放
        options.inScaled = false;
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_boy, options);

        // 設定一下矩陣
        Matrix.setIdentityM(mvpMatrix, 0);
        matrixTransformOpengl.setMvpMatrix(mvpMatrix);

        return bitmap;
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        findViewById(R.id.bt_translate).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (null != matrixTransformOpengl) {
                    translateX += 0.1;
                    if(translateX >=2 ){
                        translateX = 0f;
                    }
                    updateMatrix();
                }
            }
        });

        findViewById(R.id.bt_scale).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (null != matrixTransformOpengl) {
                    scaleX += 0.1;
                    updateMatrix();
                }
            }
        });

        findViewById(R.id.bt_rotate).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (null != matrixTransformOpengl) {
                    rotationZ += 10;
                    updateMatrix();
                }
            }
        });

        findViewById(R.id.bt_reset).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (null != matrixTransformOpengl) {
                    translateX = 0;
                    scaleX = 1;
                    rotationZ = 0;
                    updateMatrix();
                }
            }
        });

    }

    private void updateMatrix() {
        Matrix.setIdentityM(mvpMatrix, 0);
        // 重點註釋
        // 在組合矩陣時,先進行縮放操作,然後是旋轉,最後才是位移,但是寫法需要反正寫,也就是先寫translateM,然後rotateM,最後scaleM
        // 如果不這樣寫會發生什麼呢?例如順這寫,先寫scaleM,然後是rotateM,最後寫translateM,測試時就會出現問題,旋轉超過180度之後再移動,就會出現移動方向相反的情況
        Matrix.translateM(mvpMatrix, 0, translateX, 0, 0);
        Matrix.rotateM(mvpMatrix, 0, rotationZ, 0, 0, 1);
        Matrix.scaleM(mvpMatrix, 0, scaleX, 1f, 0f);
        matrixTransformOpengl.setMvpMatrix(mvpMatrix);
        myGLSurfaceView.requestRender();
    }
}

系列教學原始碼

https://github.com/feiflyer/NDK_OpenglES_Tutorial

後續demo如果有完善可能會更新。

Opengl ES系列入門介紹

Opengl ES之EGL環境搭建
Opengl ES之著色器
Opengl ES之三角形繪製
Opengl ES之四邊形繪製
Opengl ES之紋理貼圖
Opengl ES之VBO和VAO
Opengl ES之EBO
Opengl ES之FBO
Opengl ES之PBO
Opengl ES之YUV資料渲染
YUV轉RGB的一些理論知識
Opengl ES之RGB轉NV21
Opengl ES之踩坑記
Opengl ES之矩陣變換(上)
Opengl ES之矩陣變換(下)
Opengl ES之水印貼圖

關注我,一起進步,人生不止coding!!!