解密影片魔法:將ExternalOES紋理轉化為TEXTURE_2D紋理

bjxiaxueliang發表於2023-12-22

在使用OpenGL ES進行圖形影像開發時,我們常使用GL_TEXTURE_2D紋理型別,它提供了對標準2D影像的處理能力。這種紋理型別適用於大多數場景,可以用於展示靜態貼圖、渲染2D圖形和進行影像處理等操作。
另外,有時我們需要從Camera或外部影片源讀取資料幀並進行處理。這時,我們會使用GL_TEXTURE_EXTERNAL_OES紋理型別。其專門用於對外部影像或實時影片流進行處理,可以直接從 BufferQueue 中接收的資料渲染紋理多邊形,從而提供更高效的影片處理和渲染效能。

在實際應用中,我們通常將GL_TEXTURE_2DGL_TEXTURE_EXTERNAL_OES這兩種紋理型別分開使用,並且它們互不干擾。實際上,這種情況佔據了80%的使用場景。我們可以根據具體需求選擇合適的紋理型別進行處理和渲染。
然而,有時候我們也會遇到一些特殊情況,需要將GL_TEXTURE_EXTERNAL_OES紋理轉化為GL_TEXTURE_2D紋理進行影片處理或計算。這種情況可能出現在需要對影片資料進行特殊的影像處理或者與GL_TEXTURE_2D紋理型別的其他渲染操作進行互動時。

當以上情況出現時,我們該如何處理呢?難道是直接將GL_TEXTURE_EXTERNAL_OES紋理賦值給GL_TEXTURE_2D紋理使用(經過實驗這種方式是不可用的)?
這裡對此情況,先給出解決方案,一般我們可以透過一些技術手段,如離屏渲染FrameBuffer幀緩衝區物件,將GL_TEXTURE_EXTERNAL_OES紋理轉換為GL_TEXTURE_2D紋理,並進行後續的處理和計算。
而此篇文章主要記錄,我是如何透過FrameBuffer幀緩衝區物件,將GL_TEXTURE_EXTERNAL_OES紋理資料轉化為GL_TEXTURE_2D紋理資料的!

  • 首先 回顧一下GL_TEXTURE_2D紋理與GL_TEXTURE_EXTERNAL_OES紋理;
  • GL_TEXTURE_EXTERNAL_OES紋理資料透過FrameBuffer轉化為GL_TEXTURE_2D紋理資料

一、TEXTURE_2DEXTERNAL_OES

在正式研究 “GL_TEXTURE_EXTERNAL_OES紋理資料轉化為GL_TEXTURE_2D紋理資料” 之前,先要搞清楚:

  • 什麼是GL_TEXTURE_2D紋理?
  • 什麼又是GL_TEXTURE_EXTERNAL_OES紋理?
  • GL_TEXTURE_2D紋理與GL_TEXTURE_EXTERNAL_OES紋理有什麼樣的區別?

1.1 GL_TEXTURE_2D紋理

GL_TEXTURE_2D 提供了對標準2D影像的處理能力,可以儲存靜態的貼圖影像或者幀緩衝區的渲染結果
其使用二維的紋理座標系,透過將紋理座標對映到紋理影像上的對應位置,可以實現紋理貼圖、紋理過濾、紋理環繞等操作

GL_TEXTURE_2D紋理的特點包括:

  • 使用二維紋理座標系進行操作;
  • 使用glTexImage2D函式載入紋理資料;
  • 透過紋理過濾和紋理環繞等方式進行紋理的取樣和處理;

GL_TEXTURE_2D紋理:建立、繫結、取樣、載入紋理影像

public static int createDrawableTexture2D(Context context, int drawableId) {
    // 生成紋理ID
    int[] textures = new int[1];
    GLES30.glGenTextures(1, textures, 0);
    // 繫結紋理
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[0]);
    // 紋理取樣方式
    GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
    GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
    GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
    GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
    // texImage2D載入影像資料
    InputStream is = context.getResources().openRawResource(drawableId);
    Bitmap bitmapTmp;
    try {
        bitmapTmp = BitmapFactory.decodeStream(is);
    } finally {
        try {
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    GLUtils.texImage2D(GLES30.GL_TEXTURE_2D,0, GLUtils.getInternalFormat(bitmapTmp), bitmapTmp, GLUtils.getType(bitmapTmp), 0 );
    bitmapTmp.recycle();
    return textures[0];
}

GL_TEXTURE_2D紋理:Shader處理階段(片元著色器)

precision mediump float;  
varying vec2 v_texture_coord;  
uniform sampler2D MAIN;  
void main() {  
   vec4 color=texture2D(MAIN, v_texture_coord);  
   gl_FragColor=color;  
}

GL_TEXTURE_2D紋理:紋理渲染

GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texId);
GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, mVertexCount);

1.2 GL_TEXTURE_EXTERNAL_OES紋理型別

根據AOSP: SurfaceTexture 文件描述,GL_TEXTURE_EXTERNAL_OES 是一種特殊的紋理型別,主要用於處理外部影像或影片資料,如從攝像頭捕捉的實時影像和外部影片流。
GL_TEXTURE_EXTERNAL_OES 相對於 GL_TEXTURE_2D 最大的特點就是 GL_TEXTURE_EXTERNAL_OES可直接從 BufferQueue 中接收的資料渲染紋理多邊形

GL_TEXTURE_EXTERNAL_OES紋理型別的特點包括:

  • 需採用特殊的取樣器型別紋理著色器擴充套件
  • 使用二維紋理座標系進行操作,與GL_TEXTURE_2D相似。
  • 專門用於處理外部影像或影片資料,可直接從 BufferQueue 中接收的資料渲染紋理多邊形,從而提供更高效的影片處理和渲染效能。

對於此,官方文件中提供了一個 Grafika 的連續拍攝案例工程,並給出瞭如下參考流程圖。
Google官方Grafika案例流程

透過閱讀 Grafika 的連續拍攝案例,我們得知:

  • 首先,需建立一個OES紋理ID,用於接收Camera影像資料
// GL_TEXTURE_EXTERNAL_OES: 紋理建立、繫結、取樣
public static int createTextureOES() {
    // 建立OES紋理ID
    int[] textures = new int[1];
    GLES30.glGenTextures(1, textures, 0);
    TextureUtil.checkGlError("glGenTextures");
    // 繫結OES紋理ID
    int texId = textures[0];
    GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
    TextureUtil.checkGlError("glBindTexture " + texId);
    // OES紋理取樣
    GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
    GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
    GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
    GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
    TextureUtil.checkGlError("glTexParameter");
    return texId;
}
  • 完成OES紋理ID建立後,透過oesTextureId建立一個影像消費者SurfaceTexture,將SurfaceTexture設定為預覽的PreviewTexture;
// 傳入一個OES紋理ID
SurfaceTexture mSurfaceTexture = new SurfaceTexture(oesTextureId);  
// 將 SurfaceTexture 設定為預覽的 PreviewTexture
Camera.setPreviewTexture(mSurfaceTexture);
  • 或者透過SurfaceTexture建立Surface,將Surface物件傳遞給MediaPlayerMediaCodec進行影片幀資料獲取;
// 傳入一個OES紋理ID
SurfaceTexture mSurfaceTexture = new SurfaceTexture(oesTextureId);  
// 建立 Surface 
Surface mSurface = new Surface(mSurfaceTexture);
// 將 Surface 設定給 MediaPlayer 外部影片播放器,獲取影片幀資料
MediaPlayer.setSurface(surface);
// GL_TEXTURE_EXTERNAL_OES紋理:Shader處理階段(片元著色器)
#extension GL_OES_EGL_image_external : require  
precision mediump float;  
varying vec2 v_texture_coord;  
uniform samplerExternalOES MAIN;  
void main() {  
   vec4 color=texture2D(MAIN, v_texture_coord);  
   gl_FragColor=color;  
}

GL_TEXTURE_EXTERNAL_OES紋理:紋理渲染

// 紋理渲染階段:GL_TEXTURE_EXTERNAL_OES紋理
GLES30.glBindTexture(GLES30.GL_TEXTURE_EXTERNAL_OES, texId);
GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, mVertexCount);

1.3 關於兩者的區別 我的個人理解

關於兩者的區別,我的個人理解:
GL_TEXTURE_2D紋理型別與GL_TEXTURE_EXTERNAL_OES紋理型別,在資料來源紋理資料的儲存格式上存在差異。

  • 資料來源方面
    一個來源於glTexImage2D載入的二維影像資料
    一個來源與影像消費者Surface對應的BufferQueue
  • 紋理儲存格式
    GL_TEXTURE_EXTERNAL_OES資料來源於外部影片源或Camera,其資料格式可能為YUV或RGB;
    GL_TEXTURE_2D的資料格式則依賴於開發中setEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize)的配置,可能是RGBA8888,也可能是RGBA4444等等。

由於兩者資料來源和紋理儲存格式的差異,兩種紋理型別是不能直接進行轉化的。

  • 首先,其在紋理取樣階段Shader處理階段紋理渲染階段均不同程度的存在差異(這一點在上一節的兩者對比的程式碼舉例中可以證明)。
  • 其次,如果需要在處理和計算階段將GL_TEXTURE_EXTERNAL_OES紋理轉換GL_TEXTURE_2D紋理,通常需要使用離屏渲染幀緩衝區物件等技術手段。

二、EXTERNAL_OES轉化為TEXTURE_2D紋理資料

這裡直接介紹轉化過程

OES紋理資料轉化TEXTURE2D紋理資料

  • 首先,需建立一個OES紋理ID(相關程式碼舉例在前文已經給出);
  • 完成OES紋理ID建立後,透過oesTextureId建立一個影像消費者SurfaceTexture(相關程式碼舉例在前文已經給出);
  • 透過SurfaceTexture建立Surface,將Surface物件傳遞給MediaPlayer,獲取Sdcard中對應路徑的影片幀資料獲取(相關程式碼舉例在前文已經給出);
  • 建立FRAMEBUFFER幀緩衝區,並繫結GL_TEXTURE_2D空白紋理物件;
public static int createEmptyTexture2DBindFrameBuffer(int[] frameBuffer, int texPixWidth, int texPixHeight) {
    // 建立紋理ID
    int[] textures = new int[1];
    GLES30.glGenTextures(1, textures, 0);
    // 繫結GL_TEXTURE_2D紋理
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[0]);
    // 紋理取樣
    GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
    GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
    GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
    GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
    // 建立一個空的2D紋理物件,指定其基本引數,並繫結到對應的紋理ID上
    GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, texPixWidth, texPixHeight,0, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, null);
    // 取消繫結紋理
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, GLES30.GL_NONE);

    /**
     * 幀緩衝區
     */
    // 建立幀緩衝區
    GLES30.glGenFramebuffers(1, frameBuffer, 0);
    // 將幀緩衝物件繫結到OpenGL ES上下文的幀緩衝目標上
    GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, frameBuffer[0]);
    // 使用GLES30.GL_COLOR_ATTACHMENT0將紋理作為顏色附著點附加到幀緩衝物件上
    GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, textures[0], 0);
    // 取消繫結緩衝區
    GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, GLES30.GL_NONE);

    return textures[0];
}
  • GL_TEXTURE_EXTERNAL_OES紋理渲染FRAMEBUFFER幀緩衝區中;
// 啟用紋理
GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
// 將所需的紋理物件繫結到Shader中紋理單元0上
GLES30.glUniform1i(mOesTextureIdHandle, 0);
// 繫結紋理
GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
// 繫結FRAMEBUFFER緩衝區
GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, tex2DFrameBufferId);
// 繪製矩形
GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, mVertexCount);
// 取消FRAMEBUFFER的繫結
GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, GLES30.GL_NONE);
  • 最後,繪製渲染GL_TEXTURE_2D紋理,完成紋理影像的顯示。
// 啟用紋理
GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
// 將所需的紋理物件繫結到Shader中紋理單元0上
GLES30.glUniform1i(mTex2DIdHandle, 0);
// 繫結紋理
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, tex2DId);
// 繪製矩形
GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, mVertexCount);

三、原始碼下載

ExternalOES紋理資料 轉換為 TEXTURE-2D紋理資料:
https://download.csdn.net/download/aiwusheng/88650498

工程程式碼截圖

參考

AOSP:SurfaceTexture
https://source.android.google.cn/docs/core/graphics/arch-st?hl=zh-c

Github:Google Grafika
https://github.com/google/grafika/blob/master/app/src/main/java/com/android/grafika/ContinuousCaptureActivity.java

OpenGL渲染管線:
https://xiaxl.blog.csdn.net/article/details/121467207

紋理ID 離屏渲染 寫入到Surface中:
https://xiaxl.blog.csdn.net/article/details/131682521

MediaCodeC與OpenGL硬編碼錄製mp4:
https://xiaxl.blog.csdn.net/article/details/72530314

相關文章