一文學會TextureID渲染到Surface

2023-07-12 18:00:45

最近遇到一個需求,要求將一個GL_TEXTURE_2D型別的紋理ID寫入到ImageReader生成的Surface中。
其實這個需求與我之前寫過的一篇文章 一文學會MediaCodeC與OpenGL錄製mp4視訊需求比較接近,只需要對該案例原始碼進行一些改造即可。

在正式介紹實現之前,需先明確:

  • 什麼是 android.view.Surface
  • 如何向Surface中寫入資料?

一、Surface

什麼是android.view.Surface

  • 用高大上方式(讓人聽不懂的方式)表述如下:
    android.view.Surface 是 Android 系統中一個重要的圖形渲染類,它用於與硬體顯示層進行通訊,將圖形資料渲染到螢幕上,可以用於實現多種功能,如視訊播放、相機預覽、螢幕錄製等。
    Surface 物件代表了一個畫布,可以在其上繪製圖形,這些圖形將通過硬體顯示層呈現在螢幕上。可以在 SurfaceViewTextureViewWindowManager 等控制元件中使用 Surface 進行圖形渲染。此外,Surface 還可以用於視訊播放、相機預覽、螢幕錄製等功能。
  • 查閱官方描述、原始碼實現,總結為一句通俗易懂的話:
    Surface是一個Java層類,其持有一個Native (C層) 管理的影象緩衝區控制程式碼。由影象緩衝區的消費者建立(如MediaRecorder),通過Surface將控制程式碼傳遞給影象的生產者 (如MediaPlayer) 進行渲染。

1.1 官方描述

關於android.view.Surface官方描述如下:

翻譯過來就是:

Surface 持有一個由螢幕合成器管理(Native層管理)的原始緩衝區的控制程式碼。通常是由影象緩衝區的消費者(如 SurfaceTexture、MediaRecorder、Allocation等)建立或生成,並被傳遞給生產者(如 OpenGL、MediaPlayer、CameraDevice等)進行繪製。
因為,Surface 只是持有Native層緩衝區的控制程式碼,若Native層的指標被釋放後,則該Surface不再有效。

1.2 官方原始碼

檢視Surface 原始碼,可以看到Surface 通過持有Native層的控制程式碼mNativeObject來管理原始資料緩衝區。

1.3 官方檔案

檢視Surface 官方檔案,檢視其所有的公有方法:

看到上述公有方法後,發現除了通過lockCanvas()方法可以獲取一個Canvas物件,然後使用drawBitmap()等API寫入圖形資料外,並無其他有用的方法,幫助我們實現TextureID紋理ID的寫入。

二、向Surface寫入資料

要向 Surface 持有的 mNativeObject 控制程式碼中寫入影象資料,我現在已知有兩種方式:

  • 第一種方式是上文提到的通過Canvas寫入;
  • 第二種方式是通過OpenGL寫入,也就是本文的要介紹的重點;

2.1 通過Canvas寫入

上文介紹到,在檢視Surface的公有方法後,發現通過lockCanvas()方法可以獲取一個Canvas物件,然後使用drawBitmap()等API寫入圖形資料,操作步驟如下:

  • 通過 SurfaceHolder 獲取 Surface 物件;
  • 通過 Surface 物件的 lockCanvas() 方法獲取 Canvas 物件;
  • 在 Canvas 上進行繪製操作,例如呼叫 drawBitmap() 方法繪製點陣圖;
  • 通過 Canvas 物件的 unlockCanvasAndPost() 方法將繪製結果提交到 Surface 中,從而實現在螢幕上渲染資料。

以下為簡單的程式碼舉例:

SurfaceHolder holder = surfaceView.getHolder();
Surface surface = holder.getSurface();
Canvas canvas = surface.lockCanvas(null);
// 在 canvas 上進行繪製操作
canvas.drawBitmap(bitmap, 0, 0, null);
surface.unlockCanvasAndPost(canvas);

2.2 通過OpenGL寫入

通過OpenGL向Surface中寫入資料,其根本原理是:

將Surface繫結到一個EGLSurface上,然後通過OpenGL向EGLSurface渲染資料,最終將結果渲染到關聯的Surface上。具體實現是使用 EGL 提供的 eglCreateWindowSurface() 函數,將 EGLSurfaceSurface 物件關聯起來。然後就可以通過 OpenGL ES 將渲染結果繪製到 EGLSurface 中,最終渲染到Surface上。

其程式碼實現舉例如下所示:

// 獲取 EGLDisplay 物件
EGLDisplay mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
// 初始化 EGL 環境
int[] version = new int[2];
EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)
// 設定 attribList
int[] attribList = {
        EGL14.EGL_RED_SIZE, 8,
        EGL14.EGL_GREEN_SIZE, 8,
        EGL14.EGL_BLUE_SIZE, 8,
        EGL14.EGL_ALPHA_SIZE, 8,
        //
        EGL14.EGL_RENDERABLE_TYPE,
        EGL14.EGL_OPENGL_ES2_BIT,
        0x3142,
        1,
        EGL14.EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
EGL14.eglChooseConfig(mEglDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0);
// 獲取 EGLContext 上下文
EGLContext shareEglContext = inEglContext;
// 設定 EGLContext 屬性
final int[] attrib_list = {
        EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
        EGL14.EGL_NONE
};
// 獲取 EGLDisplay 物件
EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
// 建立一個 EGLSurface 物件(繫結surface)
EGLSurface eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, configs[0], surface, surfaceAttribs, 0);
// 將 EGLSurface 和 EGLContext 繫結到 EGLDisplay 上
EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);

// 渲染圖形 ...

// 交換前後緩衝(將egl渲染結果交換到surface上)
EGL14.eglSwapBuffers(eglDisplay, eglSurface);

三、原始碼下載

將一個GL_TEXTURE_2D型別的紋理ID寫入到ImageReader生成的Surface中,原始碼案例工程下載地址如下:
https://download.csdn.net/download/aiwusheng/87959680

案例程式碼實現流程流程如下:

  • OpenGLES3 中載入GL_TEXTURE_2D紋理,生成紋理ID;
  • 通過 EGL 構建 EGLDisplay 並繫結ImageReader提供的Surface
  • 在 EGL 執行緒中渲染GL_TEXTURE_2D對應的紋理圖形;
  • 在 EGL 執行緒中完成渲染後,通過eglSwapBuffers交換緩衝資料;
  • 在 ImageReader 中 onImageAvailable 中讀取Surface資料,並將資料儲存為一張Bitmap;
  • 將的Bitmap顯示到 ImageView 上(用於驗證紋理ID是否正常寫入到Surface)

案例原始碼效果圖如下圖所示:

= THE END =