Opengl ES之矩陣變換(上)

来源:https://www.cnblogs.com/goFlyer/archive/2023/03/21/17239870.html
-Advertisement-
Play Games

華為HMS Core應用內支付服務(In-App Purchases,IAP)為應用提供便捷的應用內支付體驗和簡便的接入流程。該服務支持客戶端和服務端兩種開發形式,具體可以參考官方文檔。 往期文章:常見問題總結(2)中分享總結了有關無法拉起支付頁面的常見問題,本文將對近期開發者們較為關註的一些集成應 ...


前言

說到矩陣變換,我們第一時間想到的就是大學時代的線性代數這些複雜的東西,突然有了一種令人從入門到放棄的念頭,不慌,作為了一個應用層的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!!!
微信掃碼關註


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Linux 中的 Bash 腳本支持對變數的操作,下麵鹹魚將介紹 Linux Bash Shell 中關於變數的 5 個易錯點 因為編程習慣,這類現象往往發生在大多數使用過其他流行編程語言的程式員身上 變數賦值 對於許多編程語言(例如 Python),變數賦值的時候在等號兩邊添加空格是一個好的習慣 ...
  • MySQL基礎:函數 函數是指一段可以直接被另一段程式調用的程式或代碼。 字元串函數 MySQL中內置了很多字元串函數,常用的幾個如下: | 函數 | 功能 | | : : | : : | | CONCAT(S1,S2,...Sn) | 字元串拼接,將S1,S2,...Sn拼接成一個字元串 | | ...
  • SQL:DML、DQL、DCL DML:Data Manipulation Language(數據操作語言) DML用來對資料庫中的數據記錄進行增刪改操作。 DML-添加數據 給指定欄位添加數據(一條數據) INSERT INTO 表名(欄位名1,欄位名2,...) VALUES(值1,值2,... ...
  • 摘要:本文就針對因USING子句的書寫方式可能導致MERGE INTO語句的執行不下推的場景,對USING子句的SQL語句進行改寫一遍,整個SQL語句可以下推。 本文分享自華為雲社區《GaussDB(DWS)運維 -- values子句做MERGE數據源導致SQL執行不下推的改寫方案》,作者: 譡里 ...
  • 什麼是主數據? 主數據是一組用於提供有關業務數據(如位置、客戶、產品、資產等)情境的標識符。它是企業或單位內運行業務必不可少的核心數據。否則,將無法統一比較系統之間的數據。但是,並非所有主數據都是一樣的。被指定為主數據的數據類型可能因行業而異。即使在同一行業的不同業務實體中,主數據的示例也可能是離散 ...
  • 眾所周知Redis有以下幾種常見的數據類型 String(字元串)、List(列表)、Set(集合)、Hash(哈希)、Sorted set(有序集合)、Stream(流)、Geo(地理空間索引)、Bitmap(點陣圖)、HyperLogLog(基數統計)等。 我們最常用的就是String(字元串)... ...
  • 資料庫優化是一個綜合工程,不僅僅是需要DBA參與,更重要的是研發設計人員針對PG資料庫的特點來進行相關的優化設計。不過對於DBA來說,一旦接到上線和運維任務,基本上都是木已成舟,軟體設計方面留下的坑已經挖好,DBA的作為已經十分有限了。不過既然要乾運維,那麼少不了就要參與優化。PG的優化工作該如何開... ...
  • SparseArray家族 SparseArray基於鍵值對存儲數據,key為int,value為object,簡單使用如下: //聲明 SparseArray<String> sparseArray= new SparseArray<>(); //增加元素,append方式 sparseArray ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...