Opengl ES之YUV數(shù)據(jù)渲染

YUV回顧

記得在音視頻基礎(chǔ)知識(shí)介紹中,筆者專門介紹過YUV的相關(guān)知識(shí),可以參考:
《音視頻基礎(chǔ)知識(shí)-YUV圖像》

YUV數(shù)據(jù)量相比RGB較小,因此YUV適用于傳輸,但是YUV圖不能直接用于顯示,需要轉(zhuǎn)換為RGB格式才能顯示,因而YUV數(shù)據(jù)渲染實(shí)際上就是使用Opengl ES將YUV數(shù)據(jù)轉(zhuǎn)換程RGB數(shù)據(jù),然后顯示出來的過程。

也就是說Opengl ES之所以能渲染YUV數(shù)據(jù)其實(shí)就是使用了Opengl強(qiáng)大的并行計(jì)算能力,快速地將YUV數(shù)據(jù)轉(zhuǎn)換程了RGB數(shù)據(jù)。

本文首發(fā)于微信公總號(hào)號(hào):思想覺悟

更多關(guān)于音視頻、FFmpeg、Opengl、C++的原創(chuàng)文章請(qǐng)關(guān)注微信公眾號(hào):思想覺悟

YUV的格式比較多,我們今天就以YUV420SP為例,而YUV420SP又分為NV12NV21兩種,因此今天我們的主題就是如何使用Opengl ES對(duì)NV12NV21數(shù)據(jù)進(jìn)行渲染顯示。

在著色器中使用texture2D對(duì)YUV數(shù)據(jù)進(jìn)行歸一化處理后Y數(shù)據(jù)的映射范圍是0到1,而U和V的數(shù)據(jù)映射范圍是-0.5到0.5。

因?yàn)閅UV格式圖像 UV 分量的默認(rèn)值分別是 127 ,Y 分量默認(rèn)值是 0 ,8 個(gè) bit 位的取值范圍是 0 ~ 255,由于在 shader 中紋理采樣值需要進(jìn)行歸一化,所以 UV 分量的采樣值需要分別減去 0.5 ,確保 YUV 到 RGB 正確轉(zhuǎn)換。

YUV數(shù)據(jù)準(zhǔn)備

首先我們可以使用ffmpeg命令行將一張png圖片轉(zhuǎn)換成YUV格式的圖片:

ffmpeg -i 圖片名稱.png -s 圖片寬x圖片高 -pix_fmt nv12或者nv21 輸出名稱.yuv)

通過上面這個(gè)命令行我們就可以將一張圖片轉(zhuǎn)換成yuv格式的圖片,此時(shí)我們可以使用軟件YUVViewer看下你轉(zhuǎn)換的圖片對(duì)不對(duì),如果本身轉(zhuǎn)換出來的圖片就是錯(cuò)的,那么后面的程序就白搭了...

注意:轉(zhuǎn)換圖片的寬高最好是2的冪次方,筆者測(cè)試了下發(fā)現(xiàn)如果寬高不是2的冪次方的話雖然能正常轉(zhuǎn)換,但是查看的時(shí)候要么有色差,要么有缺陷,也有可能正常。

又或者你可以極客一點(diǎn),直接使用ffmpeg代碼解碼視頻的方式獲得YUV數(shù)據(jù)并保存,這個(gè)可以參考筆者之前的文章:

《FFmpeg連載3-視頻解碼》

同時(shí)在上面的文章中筆者也介紹了通過ffplay命令行的方式查看YUV數(shù)據(jù)的方法。

YUV數(shù)據(jù)渲染

YUV 渲染步驟:

  • 生成 2 個(gè)紋理,分別用于承載Y數(shù)據(jù)和UV數(shù)據(jù),編譯鏈接著色器程序;

NV21和NV12格式的YUV數(shù)據(jù)是只有兩個(gè)平面的,它們的排列順序是YYYY UVUV或者YYYY VUVU因此我們的片元著色器需要兩個(gè)紋理采樣。

  • 確定紋理坐標(biāo)及對(duì)應(yīng)的頂點(diǎn)坐標(biāo);
  • 分別加載 NV21 的兩個(gè) Plane 數(shù)據(jù)到 2 個(gè)紋理,加載紋理坐標(biāo)和頂點(diǎn)坐標(biāo)數(shù)據(jù)到著色器程序;
  • 繪制。

YUV與RGB的轉(zhuǎn)換格式圖:

YUV與RGB的轉(zhuǎn)換公式

在OpenGLES的內(nèi)置矩陣實(shí)際上是一列一列地構(gòu)建的,比如YUV和RGB的轉(zhuǎn)換矩陣的構(gòu)建是:

// 標(biāo)準(zhǔn)轉(zhuǎn)換,舍棄了部分小數(shù)精度
mat3 convertMat = mat3(1.0, 1.0, 1.0,      //第一列
                       0.0,-0.338,1.732, //第二列
                       1.371,-0.698, 0.0);//第三列

OpenGLES 實(shí)現(xiàn) YUV 渲染需要用到 GL_LUMINANCE 和 GL_LUMINANCE_ALPHA 格式的紋理,其中 GL_LUMINANCE 紋理用來加載 NV21 Y Plane 的數(shù)據(jù),GL_LUMINANCE_ALPHA 紋理用來加載 UV Plane 的數(shù)據(jù)。

廢話少說,show me the code

YUVRenderOpengl.h

#ifndef NDK_OPENGLES_LEARN_YUVRENDEROPENGL_H
#define NDK_OPENGLES_LEARN_YUVRENDEROPENGL_H
#include "BaseOpengl.h"

class YUVRenderOpengl: public BaseOpengl{

public:
    YUVRenderOpengl();

    virtual ~YUVRenderOpengl();

    virtual void onDraw() override;

    // 設(shè)置yuv數(shù)據(jù)
    virtual void setYUVData(void *y_data,void *uv_data, int width, int height, int yuvType);

private:
    GLint positionHandle{-1};
    GLint textureHandle{-1};
    GLint y_textureSampler{-1};
    GLint uv_textureSampler{-1};
    GLuint y_textureId{0};
    GLuint uv_textureId{0};
};

#endif //NDK_OPENGLES_LEARN_YUVRENDEROPENGL_H

YUVRenderOpengl.cpp


#include "YUVRenderOpengl.h"

#include "../utils/Log.h"

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

// 片元著色器 nv12
//static const char *fragment = "#version 300 es\n"
//                              "precision mediump float;\n"
//                              "out vec4 FragColor;\n"
//                              "in vec2 TexCoord;\n"
//                              "uniform sampler2D y_texture; \n"
//                              "uniform sampler2D uv_texture;\n"
//                              "void main()\n"
//                              "{\n"
//                              "vec3 yuv;\n"
//                              "yuv.x = texture(y_texture, TexCoord).r;\n"
//                              "yuv.y = texture(uv_texture, TexCoord).r-0.5;\n"
//                              "yuv.z = texture(uv_texture, TexCoord).a-0.5;\n"
//                              "vec3 rgb =mat3( 1.0,1.0,1.0,\n"
//                              "0.0,-0.344,1.770,1.403,-0.714,0.0) * yuv;\n"
//                              "FragColor = vec4(rgb, 1);\n"
//                              "}";

/**
 *  僅僅是以下兩句不同而已
 *  "yuv.y = texture(uv_texture, TexCoord).r-0.5;\n"
 *  "yuv.z = texture(uv_texture, TexCoord).a-0.5;\n"
 */
// 片元著色器nv21 僅僅是
static const char *fragment = "#version 300 es\n"
                              "precision mediump float;\n"
                              "out vec4 FragColor;\n"
                              "in vec2 TexCoord;\n"
                              "uniform sampler2D y_texture; \n"
                              "uniform sampler2D uv_texture;\n"
                              "void main()\n"
                              "{\n"
                              "vec3 yuv;\n"
                              "yuv.x = texture(y_texture, TexCoord).r;\n"
                              "yuv.y = texture(uv_texture, TexCoord).a-0.5;\n"
                              "yuv.z = texture(uv_texture, TexCoord).r-0.5;\n"
                              "vec3 rgb =mat3( 1.0,1.0,1.0,\n"
                              "0.0,-0.344,1.770,1.403,-0.714,0.0) * yuv;\n"
                              "FragColor = vec4(rgb, 1);\n"
                              "}";

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

// 貼圖紋理坐標(biāo)(參考手機(jī)屏幕坐標(biāo)系統(tǒng),原點(diǎn)在左上角)
//由于對(duì)一個(gè)OpenGL紋理來說,它沒有內(nèi)在的方向性,因此我們可以使用不同的坐標(biāo)把它定向到任何我們喜歡的方向上,然而大多數(shù)計(jì)算機(jī)圖像都有一個(gè)默認(rèn)的方向,它們通常被規(guī)定為y軸向下,X軸向右
const static GLfloat TEXTURE_COORD[] = {
        1.0f,1.0f, // 右下
        1.0f,0.0f, // 右上
        0.0f,1.0f, // 左下
        0.0f,0.0f // 左上
};

YUVRenderOpengl::YUVRenderOpengl() {
    initGlProgram(ver,fragment);
    positionHandle = glGetAttribLocation(program,"aPosition");
    textureHandle = glGetAttribLocation(program,"aTexCoord");
    y_textureSampler = glGetUniformLocation(program,"y_texture");
    uv_textureSampler = glGetUniformLocation(program,"uv_texture");
    LOGD("program:%d",program);
    LOGD("positionHandle:%d",positionHandle);
    LOGD("textureHandle:%d",textureHandle);
    LOGD("y_textureSampler:%d",y_textureSampler);
    LOGD("uv_textureSampler:%d",uv_textureSampler);
}

YUVRenderOpengl::~YUVRenderOpengl() {

}

void YUVRenderOpengl::setYUVData(void *y_data, void *uv_data, int width, int height, int yuvType) {

    // 準(zhǔn)備y數(shù)據(jù)紋理
    glGenTextures(1, &y_textureId);
    glActiveTexture(GL_TEXTURE2);
    glUniform1i(y_textureSampler, 2);

    // 綁定紋理
    glBindTexture(GL_TEXTURE_2D, y_textureId);
    // 為當(dāng)前綁定的紋理對(duì)象設(shè)置環(huán)繞、過濾方式
    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_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, y_data);
    // 生成mip貼圖
    glGenerateMipmap(GL_TEXTURE_2D);

    glBindTexture(GL_TEXTURE_2D, y_textureId);
    // 解綁定
    glBindTexture(GL_TEXTURE_2D, 0);

    // 準(zhǔn)備uv數(shù)據(jù)紋理
    glGenTextures(1, &uv_textureId);
    glActiveTexture(GL_TEXTURE3);
    glUniform1i(uv_textureSampler, 3);

    // 綁定紋理
    glBindTexture(GL_TEXTURE_2D, uv_textureId);
    // 為當(dāng)前綁定的紋理對(duì)象設(shè)置環(huán)繞、過濾方式
    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);
    // 注意寬高
    // 注意要使用 GL_LUMINANCE_ALPHA
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, width/2, height/2, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, uv_data);
    // 生成mip貼圖
    glGenerateMipmap(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, uv_textureId);
    // 解綁定
    glBindTexture(GL_TEXTURE_2D, 0);
}

void YUVRenderOpengl::onDraw() {

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

    // 激活紋理
    glActiveTexture(GL_TEXTURE2);
    // 綁定紋理
    glBindTexture(GL_TEXTURE_2D, y_textureId);
    glUniform1i(y_textureSampler, 2);

    // 激活紋理
    glActiveTexture(GL_TEXTURE3);
    // 綁定紋理
    glBindTexture(GL_TEXTURE_2D, uv_textureId);
    glUniform1i(uv_textureSampler, 3);

    /**
     * size 幾個(gè)數(shù)字表示一個(gè)點(diǎn),顯示是兩個(gè)數(shù)字表示一個(gè)點(diǎn)
     * normalized 是否需要?dú)w一化,不用,這里已經(jīng)歸一化了
     * stride 步長(zhǎng),連續(xù)頂點(diǎn)之間的間隔,如果頂點(diǎn)直接是連續(xù)的,也可填0
     */
    // 啟用頂點(diǎn)數(shù)據(jù)
    glEnableVertexAttribArray(positionHandle);
    glVertexAttribPointer(positionHandle,2,GL_FLOAT,GL_FALSE,0,VERTICES);

    // 紋理坐標(biāo)
    glEnableVertexAttribArray(textureHandle);
    glVertexAttribPointer(textureHandle,2,GL_FLOAT,GL_FALSE,0,TEXTURE_COORD);

    // 4個(gè)頂點(diǎn)繪制兩個(gè)三角形組成矩形
    glDrawArrays(GL_TRIANGLE_STRIP,0,4);

    glUseProgram(0);

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

    glBindTexture(GL_TEXTURE_2D, 0);
}

注意看著色器代碼的注釋,NV12和NV21的渲染僅僅是著色器代碼有細(xì)小差別而已。

YUVRenderActivity.java

public class YUVRenderActivity extends BaseGlActivity {

    // 注意改成你自己圖片的寬高
    private int yuvWidth = 640;
    private int yuvHeight = 428;

    private String nv21Path;
    private String nv12Path;
    private Handler handler = new Handler(Looper.getMainLooper());

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 注意申請(qǐng)磁盤寫權(quán)限
        // 拷貝資源
        nv21Path = getFilesDir().getAbsolutePath() + "/nv21.yuv";
        FileUtils.copyAssertToDest(this,"nv21.yuv",nv21Path);

        nv12Path = getFilesDir().getAbsolutePath() + "/nv12.yuv";
        FileUtils.copyAssertToDest(this,"nv12.yuv",nv12Path);
    }

    @Override
    public BaseOpengl createOpengl() {
        YUVRenderOpengl yuvRenderOpengl = new YUVRenderOpengl();
        return yuvRenderOpengl;
    }

    @Override
    protected void onResume() {
        super.onResume();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // 注意nv12和nv21的偏遠(yuǎn)著色器有點(diǎn)不一樣的,需要手動(dòng)改下調(diào)試  YUVRenderOpengl.cpp
//                if(!TextUtils.isEmpty(nv12Path)){
//                    loadYuv(nv12Path,BaseOpengl.YUV_DATA_TYPE_NV12);
//                }

                if(!TextUtils.isEmpty(nv21Path)){
                    loadYuv(nv21Path,BaseOpengl.YUV_DATA_TYPE_NV21);
                }
            }
        },200);
    }

    @Override
    protected void onStop() {
        handler.removeCallbacksAndMessages(null);
        super.onStop();
    }

    private void loadYuv(String path,int yuvType){
        try {
            InputStream inputStream = new FileInputStream(new File(path));
            Log.v("fly_learn_opengl","---length:" + inputStream.available());
            byte[] yData = new byte[yuvWidth * yuvHeight];
            inputStream.read(yData,0,yData.length);
            byte[] uvData = new byte[yuvWidth * yuvHeight / 2];
            inputStream.read(uvData,0,uvData.length);
            Log.v("fly_learn_opengl","---read:" + (yData.length + uvData.length) + "available:" + inputStream.available());
            myGLSurfaceView.setYuvData(yData,uvData,yuvWidth,yuvHeight);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

這個(gè)主要看懂loadYuv方法,對(duì)于YUV數(shù)據(jù)的讀取即可。

思考

都說YUV的格式較多,本文我們介紹了如何使用Opengl ES渲染YUV420SP數(shù)據(jù),那么對(duì)于YUV420P數(shù)據(jù),使用Opengl ES如何渲染呢?歡迎關(guān)注評(píng)論解答交流。

專欄系列

Opengl ES之EGL環(huán)境搭建
Opengl ES之著色器
Opengl ES之三角形繪制
Opengl ES之四邊形繪制
Opengl ES之紋理貼圖
Opengl ES之VBO和VAO
Opengl ES之EBO
Opengl ES之FBO
Opengl ES之PBO

關(guān)注我,一起進(jìn)步,人生不止coding?。?!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容