YUV回顧
記得在音影片基礎知識介紹中,筆者專門介紹過YUV的相關知識,可以參考:
《音影片基礎知識-YUV影像》
YUV資料量相比RGB較小,因此YUV適用於傳輸,但是YUV圖不能直接用於顯示,需要轉換為RGB格式才能顯示,因而YUV資料渲染實際上就是使用Opengl ES將YUV資料轉換程RGB資料,然後顯示出來的過程。
也就是說Opengl ES之所以能渲染YUV資料其實就是使用了Opengl強大的平行計算能力,快速地將YUV資料轉換程了RGB資料。
本文首發於微信公總號號:思想覺悟
更多關於音影片、FFmpeg、Opengl、C++的原創文章請關注微信公眾號:思想覺悟
YUV的格式比較多,我們今天就以YUV420SP為例,而YUV420SP又分為NV12
和NV21
兩種,因此今天我們的主題就是如何使用Opengl ES對NV12
和NV21
資料進行渲染顯示。
在著色器中使用texture2D
對YUV資料進行歸一化處理後Y資料的對映範圍是0到1,而U和V的資料對映範圍是-0.5到0.5。
因為YUV格式影像 UV 分量的預設值分別是 127 ,Y 分量預設值是 0 ,8 個 bit 位的取值範圍是 0 ~ 255,由於在 shader 中紋理取樣值需要進行歸一化,所以 UV 分量的取樣值需要分別減去 0.5 ,確保 YUV 到 RGB 正確轉換。
YUV資料準備
首先我們可以使用ffmpeg
命令列將一張png圖片轉換成YUV格式的圖片:
ffmpeg -i 圖片名稱.png -s 圖片寬x圖片高 -pix_fmt nv12或者nv21 輸出名稱.yuv)
透過上面這個命令列我們就可以將一張圖片轉換成yuv格式的圖片,此時我們可以使用軟體YUVViewer
看下你轉換的圖片對不對,如果本身轉換出來的圖片就是錯的,那麼後面的程式就白搭了...
注意:轉換圖片的寬高最好是2的冪次方,筆者測試了下發現如果寬高不是2的冪次方的話雖然能正常轉換,但是檢視的時候要麼有色差,要麼有缺陷,也有可能正常。
又或者你可以極客一點,直接使用ffmpeg程式碼解碼影片的方式獲得YUV資料並儲存,這個可以參考筆者之前的文章:
同時在上面的文章中筆者也介紹了透過ffplay
命令列的方式檢視YUV資料的方法。
YUV資料渲染
YUV 渲染步驟:
- 生成 2 個紋理,分別用於承載Y資料和UV資料,編譯連結著色器程式;
NV21和NV12格式的YUV資料是隻有兩個平面的,它們的排列順序是YYYY UVUV
或者YYYY VUVU
因此我們的片元著色器需要兩個紋理取樣。
- 確定紋理座標及對應的頂點座標;
- 分別載入 NV21 的兩個 Plane 資料到 2 個紋理,載入紋理座標和頂點座標資料到著色器程式;
- 繪製。
YUV與RGB的轉換格式圖:
在OpenGLES的內建矩陣實際上是一列一列地構建的,比如YUV和RGB的轉換矩陣的構建是:
// 標準轉換,捨棄了部分小數精度
mat3 convertMat = mat3(1.0, 1.0, 1.0, //第一列
0.0,-0.338,1.732, //第二列
1.371,-0.698, 0.0);//第三列
OpenGLES 實現 YUV 渲染需要用到 GL_LUMINANCE 和 GL_LUMINANCE_ALPHA 格式的紋理,其中 GL_LUMINANCE 紋理用來載入 NV21 Y Plane 的資料,GL_LUMINANCE_ALPHA 紋理用來載入 UV Plane 的資料。
廢話少說,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;
// 設定yuv資料
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"
// 頂點著色器
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"
"}";
// 使用繪製兩個三角形組成一個矩形的形式(三角形帶)
// 第一第二第三個點組成一個三角形,第二第三第四個點組成一個三角形
const static GLfloat VERTICES[] = {
0.5f,-0.5f, // 右下
0.5f,0.5f, // 右上
-0.5f,-0.5f, // 左下
-0.5f,0.5f // 左上
};
// 貼圖紋理座標(參考手機螢幕座標系統,原點在左上角)
//由於對一個OpenGL紋理來說,它沒有內在的方向性,因此我們可以使用不同的座標把它定向到任何我們喜歡的方向上,然而大多數計算機影像都有一個預設的方向,它們通常被規定為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) {
// 準備y資料紋理
glGenTextures(1, &y_textureId);
glActiveTexture(GL_TEXTURE2);
glUniform1i(y_textureSampler, 2);
// 繫結紋理
glBindTexture(GL_TEXTURE_2D, y_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_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);
// 準備uv資料紋理
glGenTextures(1, &uv_textureId);
glActiveTexture(GL_TEXTURE3);
glUniform1i(uv_textureSampler, 3);
// 繫結紋理
glBindTexture(GL_TEXTURE_2D, uv_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);
// 注意寬高
// 注意要使用 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 幾個數字表示一個點,顯示是兩個數字表示一個點
* 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);
}
注意看著色器程式碼的註釋,NV12和NV21的渲染僅僅是著色器程式碼有細小差別而已。
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);
// 注意申請磁碟寫許可權
// 複製資源
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的偏遠著色器有點不一樣的,需要手動改下除錯 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();
}
}
}
這個主要看懂loadYuv
方法,對於YUV資料的讀取即可。
思考
都說YUV的格式較多,本文我們介紹瞭如何使用Opengl ES渲染YUV420SP資料,那麼對於YUV420P資料,使用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
關注我,一起進步,人生不止coding!!!