天天看點

最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)

=====================================================

最簡單的視音頻播放示例系列文章清單:

最簡單的視音頻播放示例1:總述

最簡單的視音頻播放示例2:GDI播放YUV, RGB

最簡單的視音頻播放示例3:Direct3D播放YUV,RGB(通過Surface)

最簡單的視音頻播放示例4:Direct3D播放RGB(通過Texture)

最簡單的視音頻播放示例5:OpenGL播放RGB/YUV

最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)

最簡單的視音頻播放示例7:SDL2播放RGB/YUV

最簡單的視音頻播放示例8:DirectSound播放PCM

最簡單的視音頻播放示例9:SDL2播放PCM

=====================================================

本文記錄OpenGL播放視訊的技術。上一篇文章中,介紹了一種簡單的使用OpenGL顯示視訊的方式。但是那還不是OpenGL顯示視訊技術的精髓。和Direct3D一樣,OpenGL更好的顯示視訊的方式也是通過紋理(Texture)。本文介紹OpenGL通過紋理的方式顯示視訊的技術。

最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)

OpenGL中坐标和Direct3D坐标的不同

OpenGL中的紋理的坐标和Direct3D中的坐标是不一樣的。

在Direct3D中。紋理坐标如下圖所示。取值是0到1。坐标系原點在左上角。

最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)

物體表面坐标如下圖所示。取值是實際的像素值。坐标系原點在左上角。

最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)

OpenGL紋理坐标取值範圍是0-1,坐标原點位于左下角。這一點和Direct3D是不同的,Direct3D紋理坐标的取值雖然也是0-1,但是他的坐标原點位于左上角。

最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)

  在OpenGL中,物體表面坐标取值範圍是-1到1。坐标系原點在中心位置。

最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)

OpenGL視訊顯示的流程

有關紋理方面的知識已經在文章《最簡單的視音頻播放示例4:Direct3D播放RGB(通過Texture)》中有詳細的記錄。OpenGL中紋理的概念和Direct3D中紋理的概念基本上是等同的,是以不再重複記錄了。

本文記錄的程式,播放的是YUV420P格式的像素資料。上一篇文章中的程式也可以播放YUV420P格式的像素資料。但是它們的原理是不一樣的。上一篇文章中,輸入的YUV420P像素資料通過一個普通的函數轉換為RGB資料後,傳送給OpenGL播放。也就是像素的轉換是通過CPU完成的。本文的程式,輸入的YUV420P像素資料通過Shader轉換為YUV資料,傳送給OpenGL播放。像素的轉換是通過顯示卡上的GPU完成的。通過本程式,可以了解使用OpenGL進行GPU程式設計的基礎知識。

使用Shader通過OpenGL的紋理(Texture)播放視訊一般情況下需要如下步驟:

1. 初始化

1) 初始化

2) 建立視窗

3) 設定繪圖函數

4) 設定定時器

5) 初始化Shader

初始化Shader的步驟比較多,主要可以分為3步:建立Shader,建立Program,初始化Texture。

(1) 建立一個Shader對象
1)編寫Vertex Shader和Fragment Shader源碼。
2)建立兩個shader 執行個體 。
3)給Shader執行個體指定源碼。
4)線上編譯shaer源碼。
(2) 建立一個Program對象
1)建立program。
2)綁定shader到program。
3)連結program。
4)使用porgram。
(3) 初始化Texture。可以分為以下步驟。
1)定義定點數組
2)設定頂點數組
3)初始化紋理
6) 進入消息循環

2. 循環顯示畫面

1) 設定紋理

2) 繪制

3) 顯示

下面詳述一下使用Shader通過OpenGL的紋理的播放YUV的步驟。有些地方和上一篇文章是重複的,會比較簡單的提一下。

1. 初始化

1) 初始化

glutInit()用于初始化glut庫。它原型如下:

void glutInit(int *argcp, char **argv);
           

它包含兩個參數:argcp和argv。一般情況下,直接把main()函數中的argc,argv傳遞給它即可。

glutInitDisplayMode()用于設定初始顯示模式。它的原型如下。

void glutInitDisplayMode(unsigned int mode);
           

需要注意的是,如果使用雙緩沖(GLUT_DOUBLE),則需要用glutSwapBuffers ()繪圖。如果使用單緩沖(GLUT_SINGLE),則需要用glFlush()繪圖。

在使用OpenGL播放視訊的時候,我們可以使用下述代碼:

glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB );
           

2) 建立視窗

glutInitWindowPosition()用于設定視窗的位置。可以指定x,y坐标。

glutInitWindowSize()用于設定視窗的大小。可以設定視窗的寬,高。

glutCreateWindow()建立一個視窗。可以指定視窗的标題。

上述幾個函數十分基礎,不再詳細叙述。直接貼出一段示例代碼:

glutInitWindowPosition(100, 100);
glutInitWindowSize(500, 500);
glutCreateWindow("Simplest Video Play OpenGL");	
           

3) 設定繪圖函數

glutDisplayFunc()用于設定繪圖函數。作業系統在必要時刻就會調用該函數對窗體進行重新繪制操作。類似于windows程式設計中處理WM_PAINT消息。例如,當把視窗移動到螢幕邊上,然後又移動回來的時候,就會調用該函數對視窗進行重繪。它的原型如下。

void glutDisplayFunc(void (*func)(void));
           

其中(*func)用于指定重繪函數。

例如在視訊播放的時候,指定display()函數用于重繪:

glutDisplayFunc(&display);
           

4) 設定定時器

播放視訊的時候,每秒需要播放一定的畫面(一般是25幀),是以使用定時器每間隔一段時間調用一下繪圖函數繪制圖形。定時器函數glutTimerFunc()的原型如下。

void glutTimerFunc(unsigned int millis, void (*func)(int value), int value);
           

它的參數含義如下:

millis:定時的時間,機關是毫秒。1秒=1000毫秒。

(*func)(int value):用于指定定時器調用的函數。

value:給回調函數傳參。比較高端,沒有接觸過。

如果隻在主函數中寫一個glutTimerFunc()函數的話,會發現隻會調用該函數一次。是以需要在回調函數中再寫一個glutTimerFunc()函數,并調用回調函數自己。隻有這樣才能實作反反複複循環調用回調函數。

例如在視訊播放的時候,指定每40毫秒調用一次timeFunc ()函數:

主函數中:

glutTimerFunc(40, timeFunc, 0);
           

而後在timeFunc()函數中如下設定。

void timeFunc(int value){
    display();
    // Present frame every 40 ms
    glutTimerFunc(40, timeFunc, 0);
}
           

這樣就實作了每40ms調用一次display()。

5) 初始化Shader

初始化Shader的步驟比較多,主要可以分為3步:建立Shader,建立Program,初始化Texture。它們的步驟如下所示。

(1) 建立一個Shader對象

Shader有點類似于一個程式的編譯器。建立一個Shader可以分成以下4步:

1)編寫Vertex Shader和Fragment Shader源碼。

2)建立兩個shader 執行個體:glCreateShader()。

3)給Shader執行個體指定源碼:glShaderSource()。

4)線上編譯shaer源碼 glCompileShader()。

下面詳細分析這4步。

1) 編寫Vertex Shader和Fragment Shader源碼。

在這裡用到了一種新的語言:OpenGL Shader Language,簡稱GLSL。它是一種類似于C語言的專門為GPU設計的語言,它可以放在GPU裡面被并行運作。

OpenGL的着色器有.fsh和.vsh兩個檔案。這兩個檔案在被編譯和連結後就可以産生可執行程式與GPU互動。.vsh 是Vertex Shader(頂點着色器),用于頂點計算,可以了解控制頂點的位置,在這個檔案中我們通常會傳入目前頂點的位置,和紋理的坐标。.fsh 是Fragment Shader(片元着色器),在這裡面我可以對于每一個像素點進行重新計算。

下面這張圖可以更好的解釋Vertex Shader和Fragment Shader的作用。這張圖是OpenGL的渲染管線。其中的資訊太多先不一一記錄了。從圖中可以看出,Vertex Shader在前,Fragment Shader在後。

最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)

在這裡貼出本文的示例程式的fsh和vsh的代碼。

Shader.vsh

attribute vec4 vertexIn; 
attribute vec2 textureIn;
varying vec2 textureOut;
void main(void)
{
    gl_Position = vertexIn; 
    textureOut = textureIn;
}
           

Shader.fsh

varying vec2 textureOut;
uniform sampler2D tex_y;
uniform sampler2D tex_u;
uniform sampler2D tex_v;
void main(void)
{
    vec3 yuv;
    vec3 rgb;    
    yuv.x = texture2D(tex_y, textureOut).r;
    yuv.y = texture2D(tex_u, textureOut).r - 0.5;
    yuv.z = texture2D(tex_v, textureOut).r - 0.5;
    rgb = mat3( 1,       1,         1,
                0,       -0.39465,  2.03211,
                1.13983, -0.58060,  0) * yuv;    
    gl_FragColor = vec4(rgb, 1);
}
           

從上述代碼中可以看出GLSL的文法和C語言很類似。每一個Shader程式都有一個main函數,這一點和c語言是一樣的。這裡的變量命名規則保持跟c一樣就行了,注意gl_開頭的變量名是系統内置的變量。有以下幾種變量:

attribute:外部傳入vsh檔案的變量,每一個頂點都會有這兩個屬性。變化率高,用于定義每個點。

varying:用于 vsh和fsh之間互相傳遞的參數。

uniform:外部傳入vsh檔案的變量。變化率較低,對于可能在整個渲染過程沒有改變,隻是個常量。

上文代碼中使用了以下資料類型:

vec2:包含了2個浮點數的向量

vec3:包含了3個浮點數的向量

vec4:包含了4個浮點數的向量

sampler1D:1D紋理着色器

sampler2D:2D紋理着色器

sampler3D:3D紋理着色器

mat2:2*2維矩陣 

mat3:3*3維矩陣 

mat4:4*4維矩陣

上文代碼中還使用到了OpenGL的幾個全局變量:

gl_Position:原始的頂點資料在Vertex Shader中經過平移、旋轉、縮放等數學變換後,生成新的頂點位置(一個四維 (vec4) 變量,包含頂點的 x、y、z 和 w 值)。新的頂點位置通過在Vertex Shader中寫入gl_Position傳遞到渲染管線的後繼階段繼續處理。

gl_FragColor:Fragment Shader的輸出,它是一個四維變量(或稱為 vec4)。gl_FragColor 表示在經過着色器代碼處理後,正在呈現的像素的 R、G、B、A 值。

Vertex Shader是作用于每一個頂點的,如果Vertex有三個點,那麼Vertex Shader會被執行三次。Fragment Shader是作用于每個像素的,一個像素運作一次。從源代碼中可以看出,像素的轉換在Fragment Shader中完成。

在網上看到兩張圖可以很好地說明Vertex Shader和Fragment Shader的作用:

最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)
最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)

Vertex Shader(頂點着色器)主要是傳入相應的Attribute變量、Uniforms變量、采樣器以及臨時變量,最後生成Varying變量,以及gl_Posizion等變量。Fragment Shade(片元着色器)可以執行紋理的通路、顔色的彙總、霧化等操作,最後生成gl_FragColor變量。有高手總結如下:“vsh負責搞定像素位置,填寫gl_Posizion;fsh負責搞定像素外觀,填寫 gl_FragColor。”

2) 建立兩個shader 執行個體。

建立一個容納shader的容器。用glCreateShader ()建立一個容納shader的容器,它的原型如下:

int glCreateShader (int type)
           

其中type包含2種: 

GLES20.GL_VERTEX_SHADER:Vertex Shader.

GLES20.GL_FRAGMENT_SHADER:Fragment Shader. 

如果調用成功的話,函數将傳回一個整形的正整數作為Shader容器的id。

3) 給Shader執行個體指定源碼。

Shader容器中添加shader的源代碼。源代碼應該以字元串數組的形式表示。glShaderSource函數的原型如下: 

void glShaderSource (int shader, String string) 
           

參數含義如下: 

shader:是代表shader容器的id(由glCreateShader()傳回的整形數)。

strings:是包含源程式的字元串數組。

如果感覺通過“字元串數組”的方式寫源代碼不太習慣的話,可以把源代碼寫到單獨的一個文本檔案裡。然後在需要源代碼的時候,讀取該文本檔案中的所有内容。

4) 線上編譯Shader源碼。

使用glCompileShader()對shader容器中的源代碼進行編譯。函數的原型如下:  

void glCompileShader (int shader)
           

其中shader是代表Shader容器的id。

在編譯完成後,可能需要調試。調試一個Shader是非常困難的。Shader的世界裡沒有printf,無法在控制台中列印調試資訊。但是可以通過一些OpenGL提供的函數來擷取編譯和連接配接過程中的資訊。在編譯階段使用glGetShaderiv擷取編譯情況。glGetShaderiv()函數原型如下:

void glGetShaderiv (int shader, int pname, int[] params, int offset) 
           

參數含義: 

shader:一個shader的id; 

pname:使用GL_COMPILE_STATUS; 

params:傳回值,如果一切正常傳回GL_TRUE代,否則傳回GL_FALSE。

(2) 建立一個Program對象

Program有點類似于一個程式的連結器。program對象提供了把需要做的事連接配接在一起的機制。在一個program中,shader對象可以連接配接在一起。

建立一個Program可以分成以下4步:

1)建立program:glCreateProgram()

2)綁定shader到program :glAttachShader()。

*每個program必須綁定一個Vertex Shader 和一個Fragment Shader。

3)連結program :glLinkProgram()。

4)使用porgram :glUseProgram()。

下面詳細分析這4步。

1) 建立program。

首先使用glCreateProgram ()建立一個容納程式(Program)的容器,我們稱之為程式容器。

函數的原型如下:

int glCreateProgram ()
           

如果函數調用成功将傳回一個整形正整數作為該着色器程式的id。

2) 綁定shader到program。

使用glAttachShader()将shader容器添加到程式中。這時的shader容器不一定需要被編譯,他們甚至不需要包含任何的代碼。

函數的原型如下:  

void glAttachShader (int program, int shader) 
           

參數含義: 

program:着色器程式容器的id。

shader:要添加的頂點或者片元shader容器的id。 

Vertex Shader和Fragment Shader需要分别将他們各自的兩個shader容器添加的程式容器中。

3) 連結program。

使用glLinkProgram()連結程式對象。

函數的原型如下:  

void glLinkProgram (int program) 
           

program是着色器程式容器的id。

如果任何類型為GL_VERTEX_SHADER的shader對象連接配接到program,它将産生在“頂點着色器”(Vertex Shader)上可執行的程式;如果任何類型為GL_FRAGMENT_SHADER的shader對象連接配接到program,它将産生在“像素着色器”(Pixel Shader)上可執行的程式。

在連結階段使用glGetProgramiv()擷取編譯情況。glGetProgramiv ()函數原型如下:

void glGetProgramiv (int program, int pname, int[] params, int offset) 
           

參數含義: 

program:一個着色器程式的id; 

pname:GL_LINK_STATUS; 

param:傳回值,如果一切正常傳回GL_TRUE代,否則傳回GL_FALSE。

通過glBindAttribLocation()把“頂點屬性索引”綁定到“頂點屬性名”。

void glBindAttribLocation(GLuint program,GLuint index,const GLchar* name);
           

參數含義:

program:着色器程式容器的id。

index:頂點屬性索引。

name:頂點屬性名。

4) 使用porgram。

在連結了程式以後,我們可以使用glUseProgram()函數來加載并使用連結好的程式。glUseProgram函數原型如下: 

void glUseProgram (int program) 
           

其中program是要使用的着色器程式的id。

(3) 初始化Texture

初始化Texture可以分為以下步驟。

1) 定義頂點數組

這一步需要初始化兩個數組,

2) 設定頂點數組

這一步通過glVertexAttribPointer()完成。glVertexAttribPointer()定義一個通用頂點屬性數組。當渲染時,它指定了通用頂點屬性數組從索引index處開始的位置和資料格式。

glVertexAttribPointer()原型如下。

void glVertexAttribPointer(  
	GLuint   index, 
	GLint   size, 
	GLenum   type, 
	GLboolean   normalized, 
	GLsizei   stride, 
	const GLvoid *   pointer); 
           

每個參數的含義:

index:訓示将被修改的通用頂點屬性的索引 

size:指點每個頂點元素個數(1~4)  

type:數組中每個元素的資料類型 

normalized:訓示定點資料值是否被歸一化(歸一化<[-1,1]或[0,1]>:GL_TRUE,直接使用:GL_FALSE)  

stride:連續頂點屬性間的偏移量,如果為0,相鄰頂點屬性間緊緊相鄰 

pointer:頂點數組 

使用函數glEnableVertexAttribArray()啟用屬性數組。預設狀态下,所有用戶端的能力被Disabled,包括所有通用頂點屬性數組。如果被Enable,通用頂點屬性數組中的值将被通路并被用于Rendering。函數的原型如下:

void glEnableVertexAttribArray( GLuint   index);
           

其中index用于指定通用頂點屬性的索引。

3) 初始化紋理

使用glGenTextures()初始化紋理,其原型如下。

glGenTextures(GLsizei n, GLuint *textures) 
           

參數含義:

n:用來生成紋理的數量

textures:存儲紋理索引的數組

glGenTextures()就是用來産生你要操作的紋理對象的索引的,比如你告訴OpenGL,我需要5個紋理對象,它會從沒有用到的整數裡傳回5個給你。

産生紋理索引之後,需要使用glBindTexture()綁定紋理,才能對該紋理進行操作。glBindTexture()告訴OpenGL下面對紋理的任何操作都是針對它所綁定的紋理對象的,比如glBindTexture(GL_TEXTURE_2D,1)即告訴OpenGL下面代碼中對2D紋理的任何設定都是針對索引為1的紋理的。

glBindTexture()函數的聲明如下所示:

void glBindTexture(GLenum target, GLuint texture );
           

函數參數的含義:

target:紋理被綁定的目标,它隻能取值GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D或者GL_TEXTURE_CUBE_MAP。

texture:紋理的名稱,并且,該紋理的名稱在目前的應用中不能被再次使用。

綁定紋理之後,就可以設定該紋理的一些屬性了。

紋理過濾函數glTexParameteri()可以用來确定如何把圖像從紋理圖象空間映射到幀緩沖圖象空間。即把紋理像素映射成像素。glTexParameteri()的原型如下。

void glTexParameteri(GLenum target,GLenum pname,GLint param);
           

部分參數功能說明如下:

pname:參數。可以指定為GL_TEXTURE_MAG_FILTER(放大過濾),GL_TEXTURE_MIN_FILTER(縮小過濾)等。

param:參數的值。例如GL_LINEAR(線性插值。使用距離目前渲染像素中心最近的4個紋素權重平均值),GL_NEAREST(臨近像素插值。該方法品質較差)

6) 進入消息循環

glutMainLoop()将會進入GLUT事件處理循環。一旦被調用,這個程式将永遠不會傳回。視訊播放的時候,調用該函數之後即開始播放視訊。

2. 循環顯示畫面

1) 設定紋理

使用glActiveTexture()選擇可以由紋理函數進行修改的目前紋理機關。後續的操作都是對選擇的紋理進行的。glActiveTexture()的原型如下。

void glActiveTexture(GLenum texUnit);
           

接着使用glBindTexture()告訴OpenGL下面對紋理的任何操作都是針對它所綁定的紋理對象的,這一點前文已經記錄,不再重複。

然後使用glTexImage2D()根據指定的參數,生成一個2D紋理(Texture)。相似的函數還有glTexImage1D、glTexImage3D。glTexImage2D()原型如下。

void glTexImage2D(	GLenum target,
 	GLint level,
 	GLint internalformat,
 	GLsizei width,
 	GLsizei height,
 	GLint border,
 	GLenum format,
 	GLenum type,
 	const GLvoid * data);
           

參數說明如下:

target:指定目标紋理,這個值必須是GL_TEXTURE_2D。

level:執行細節級别。0是最基本的圖像級别,n表示第N級貼圖細化級别。

internalformat:指定紋理中的顔色格式。可選的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE, GL_LUMINANCE_ALPHA 等幾種。

width:紋理圖像的寬度。

height:紋理圖像的高度。

border:邊框的寬度。必須為0。

format:像素資料的顔色格式, 不需要和internalformatt取值必須相同。可選的值參考internalformat。

type:指定像素資料的資料類型。可以使用的值有GL_UNSIGNED_BYTE,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4,GL_UNSIGNED_SHORT_5_5_5_1等。

pixels:指定記憶體中指向圖像資料的指針

glUniform()為目前程式對象指定Uniform變量的值。(注意,由于OpenGL由C語言編寫,但是C語言不支援函數的重載,是以會有很多名字相同字尾不同的函數版本存在。其中函數名中包含數字(1、2、3、4)表示接受該數字個用于更改uniform變量的值,i表示32位整形,f表示32位浮點型,ub表示8位無符号byte,ui表示32位無符号整形,v表示接受相應的指針類型。 )

2) 繪制

使用glDrawArrays()進行繪制。glDrawArrays()原型如下。

void glDrawArrays (GLenum mode, GLint first, GLsizei count);
           

參數說明:

mode:繪制方式,提供以下參數:GL_POINTS、GL_LINES、GL_LINE_LOOP、GL_LINE_STRIP、GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN。

first:從數組緩存中的哪一位開始繪制,一般為0。

count:數組中頂點的數量。

3) 顯示

如果使用“雙緩沖”方式的話,使用glutSwapBuffers()繪制。如果使用“單緩沖”方式的話,使用glFlush()繪制。glutSwapBuffers()的功能是交換兩個緩沖區指針,表現的形式即是把畫面呈現到螢幕上。

簡單解釋一下雙緩沖技術。當我們進行複雜的繪圖操作時,畫面便可能有明顯的閃爍。這是由于繪制的東西沒有同時出現在螢幕上而導緻的。使用雙緩沖可以解決這個問題。所謂雙緩沖技術, 是指使用兩個緩沖區: 前台緩沖和背景緩沖。前台緩沖即我們看到的螢幕,背景緩沖則在記憶體當中,對我們來說是不可見的。每次的所有繪圖操作不是在螢幕上直接繪制,而是在背景緩沖中進行, 當繪制完成時,再把繪制的最終結果顯示到螢幕上。

glutSwapBuffers()函數執行之後,緩沖區指針交換,兩個緩沖的“角色”也發生了對調。原先的前台緩沖變成了背景緩沖,等待進行下一次繪制。而原先的背景緩沖變成了前台緩沖,展現出繪制的結果。

視訊顯示(使用Texture)流程總結

上文流程的函數流程可以用下圖表示。

最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)

代碼

源代碼如下所示。

/**
 * 最簡單的OpenGL播放視訊的例子(OpenGL播放YUV)[Texture]
 * Simplest Video Play OpenGL (OpenGL play YUV) [Texture]
 *
 * 雷霄骅 Lei Xiaohua
 * [email protected]
 * 中國傳媒大學/數字電視技術
 * Communication University of China / Digital TV Technology
 * http://blog.csdn.net/leixiaohua1020
 *
 * 本程式使用OpenGL播放YUV視訊像素資料。本程式支援YUV420P的
 * 像素資料作為輸入,經過轉換後輸出到螢幕上。其中用到了多種
 * 技術,例如Texture,Shader等,是一個相對比較複雜的例子。
 * 适合有一定OpenGL基礎的初學者學習。
 *
 * 函數調用步驟如下: 
 *
 * [初始化]
 * glutInit(): 初始化glut庫。
 * glutInitDisplayMode(): 設定顯示模式。
 * glutCreateWindow(): 建立一個視窗。
 * glewInit(): 初始化glew庫。
 * glutDisplayFunc(): 設定繪圖函數(重繪的時候調用)。
 * glutTimerFunc(): 設定定時器。
 * InitShaders(): 設定Shader。包含了一系列函數,暫不列出。
 * glutMainLoop(): 進入消息循環。
 *
 * [循環渲染資料]
 * glActiveTexture(): 激活紋理機關。
 * glBindTexture(): 綁定紋理
 * glTexImage2D(): 根據像素資料,生成一個2D紋理。
 * glUniform1i(): 
 * glDrawArrays(): 繪制。
 * glutSwapBuffers(): 顯示。
 *
 * This software plays YUV raw video data using OpenGL.
 * It support read YUV420P raw file and show it on the screen.
 * It's use a slightly more complex technologies such as Texture,
 * Shaders etc. Suitable for beginner who already has some 
 * knowledge about OpenGL.
 *
 * The process is shown as follows:
 *
 * [Init]
 * glutInit(): Init glut library.
 * glutInitDisplayMode(): Set display mode.
 * glutCreateWindow(): Create a window.
 * glewInit(): Init glew library.
 * glutDisplayFunc(): Set the display callback.
 * glutTimerFunc(): Set timer.
 * InitShaders(): Set Shader, Init Texture. It contains some functions about Shader.
 * glutMainLoop(): Start message loop.
 *
 * [Loop to Render data]
 * glActiveTexture(): Active a Texture unit 
 * glBindTexture(): Bind Texture
 * glTexImage2D(): Specify pixel data to generate 2D Texture
 * glUniform1i(): 
 * glDrawArrays(): draw.
 * glutSwapBuffers(): show.
 */

#include <stdio.h>

#include "glew.h"
#include "glut.h"

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <string.h>

//Select one of the Texture mode (Set '1'):
#define TEXTURE_DEFAULT   0
//Rotate the texture
#define TEXTURE_ROTATE    0
//Show half of the Texture
#define TEXTURE_HALF      1

const int screen_w=500,screen_h=500;
const int pixel_w = 320, pixel_h = 180;
//YUV file
FILE *infile = NULL;
unsigned char buf[pixel_w*pixel_h*3/2];
unsigned char *plane[3];


GLuint p;                
GLuint id_y, id_u, id_v; // Texture id
GLuint textureUniformY, textureUniformU,textureUniformV;


#define ATTRIB_VERTEX 3
#define ATTRIB_TEXTURE 4

void display(void){
    if (fread(buf, 1, pixel_w*pixel_h*3/2, infile) != pixel_w*pixel_h*3/2){
        // Loop
        fseek(infile, 0, SEEK_SET);
        fread(buf, 1, pixel_w*pixel_h*3/2, infile);
    }
	//Clear
	glClearColor(0.0,255,0.0,0.0);
	glClear(GL_COLOR_BUFFER_BIT);
	//Y
	//
    glActiveTexture(GL_TEXTURE0);
	
    glBindTexture(GL_TEXTURE_2D, id_y);
	
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, pixel_w, pixel_h, 0, GL_RED, GL_UNSIGNED_BYTE, plane[0]); 
	
	glUniform1i(textureUniformY, 0);    
	//U
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, id_u);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, pixel_w/2, pixel_h/2, 0, GL_RED, GL_UNSIGNED_BYTE, plane[1]);       
    glUniform1i(textureUniformU, 1);
	//V
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, id_v);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, pixel_w/2, pixel_h/2, 0, GL_RED, GL_UNSIGNED_BYTE, plane[2]);    
    glUniform1i(textureUniformV, 2);   

    // Draw
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
	// Show
	//Double
    glutSwapBuffers();
	//Single
	//glFlush();
}

void timeFunc(int value){
    display();
    // Timer: 40ms
    glutTimerFunc(40, timeFunc, 0);
}

char *textFileRead(char * filename)
{
    char *s = (char *)malloc(8000);
    memset(s, 0, 8000);
    FILE *infile = fopen(filename, "rb");
    int len = fread(s, 1, 8000, infile);
    fclose(infile);
    s[len] = 0;
    return s;
}

//Init Shader
void InitShaders()
{
    GLint vertCompiled, fragCompiled, linked;
    
    GLint v, f;
    const char *vs,*fs;
	//Shader: step1
    v = glCreateShader(GL_VERTEX_SHADER);
    f = glCreateShader(GL_FRAGMENT_SHADER);
	//Get source code
    vs = textFileRead("Shader.vsh");
    fs = textFileRead("Shader.fsh");
	//Shader: step2
    glShaderSource(v, 1, &vs,NULL);
    glShaderSource(f, 1, &fs,NULL);
	//Shader: step3
    glCompileShader(v);
	//Debug
    glGetShaderiv(v, GL_COMPILE_STATUS, &vertCompiled);
    glCompileShader(f);
    glGetShaderiv(f, GL_COMPILE_STATUS, &fragCompiled);

	//Program: Step1
    p = glCreateProgram(); 
	//Program: Step2
    glAttachShader(p,v);
    glAttachShader(p,f); 

    glBindAttribLocation(p, ATTRIB_VERTEX, "vertexIn");
    glBindAttribLocation(p, ATTRIB_TEXTURE, "textureIn");
	//Program: Step3
    glLinkProgram(p);
	//Debug
    glGetProgramiv(p, GL_LINK_STATUS, &linked);  
	//Program: Step4
    glUseProgram(p);


	//Get Uniform Variables Location
	textureUniformY = glGetUniformLocation(p, "tex_y");
	textureUniformU = glGetUniformLocation(p, "tex_u");
	textureUniformV = glGetUniformLocation(p, "tex_v"); 

#if TEXTURE_ROTATE
    static const GLfloat vertexVertices[] = {
        -1.0f, -0.5f,
         0.5f, -1.0f,
        -0.5f,  1.0f,
         1.0f,  0.5f,
    };    
#else
	static const GLfloat vertexVertices[] = {
		-1.0f, -1.0f,
		1.0f, -1.0f,
		-1.0f,  1.0f,
		1.0f,  1.0f,
	};    
#endif

#if TEXTURE_HALF
	static const GLfloat textureVertices[] = {
		0.0f,  1.0f,
		0.5f,  1.0f,
		0.0f,  0.0f,
		0.5f,  0.0f,
	}; 
#else
	static const GLfloat textureVertices[] = {
		0.0f,  1.0f,
		1.0f,  1.0f,
		0.0f,  0.0f,
		1.0f,  0.0f,
	}; 
#endif
	//Set Arrays
    glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, vertexVertices);
	//Enable it
    glEnableVertexAttribArray(ATTRIB_VERTEX);    
    glVertexAttribPointer(ATTRIB_TEXTURE, 2, GL_FLOAT, 0, 0, textureVertices);
    glEnableVertexAttribArray(ATTRIB_TEXTURE);


	//Init Texture
    glGenTextures(1, &id_y); 
    glBindTexture(GL_TEXTURE_2D, id_y);    
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
    glGenTextures(1, &id_u);
    glBindTexture(GL_TEXTURE_2D, id_u);   
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
    glGenTextures(1, &id_v); 
    glBindTexture(GL_TEXTURE_2D, id_v);    
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

}



int main(int argc, char* argv[])
{
	//Open YUV420P file
	if((infile=fopen("../test_yuv420p_320x180.yuv", "rb"))==NULL){
		printf("cannot open this file\n");
		return -1;
	}

	//YUV Data
    plane[0] = buf;
    plane[1] = plane[0] + pixel_w*pixel_h;
    plane[2] = plane[1] + pixel_w*pixel_h/4;

    //Init GLUT
    glutInit(&argc, argv);  
	//GLUT_DOUBLE
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA /*| GLUT_STENCIL | GLUT_DEPTH*/);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(screen_w, screen_h);
    glutCreateWindow("Simplest Video Play OpenGL (Texture)");
	printf("Lei Xiaohua\n");
	printf("http://blog.csdn.net/leixiaohua1020\n");
    printf("Version: %s\n", glGetString(GL_VERSION));
    GLenum l = glewInit();

    glutDisplayFunc(&display);
    glutTimerFunc(40, timeFunc, 0); 

    InitShaders();

    // Begin!
    glutMainLoop();

    return 0;
}
           

Shader.vsh

attribute vec4 vertexIn; 
attribute vec2 textureIn;
varying vec2 textureOut;
void main(void)
{
    gl_Position = vertexIn; 
    textureOut = textureIn;
}
           

Shader.fsh

varying vec2 textureOut;
uniform sampler2D tex_y;
uniform sampler2D tex_u;
uniform sampler2D tex_v;
void main(void)
{
    vec3 yuv;
    vec3 rgb;    
    yuv.x = texture2D(tex_y, textureOut).r;
    yuv.y = texture2D(tex_u, textureOut).r - 0.5;
    yuv.z = texture2D(tex_v, textureOut).r - 0.5;
    rgb = mat3( 1,       1,         1,
                0,       -0.39465,  2.03211,
                1.13983, -0.58060,  0) * yuv;    
    gl_FragColor = vec4(rgb, 1);
}
           

代碼注意事項

1. 目前支援讀取YUV420P格式的像素資料。

2. 視窗的寬高為screen_w,screen_h。像素資料的寬高為pixel_w,pixel_h。它們的定義如下。

//Width, Height    
const int screen_w=500,screen_h=500;    
const int pixel_w=320,pixel_h=180;    
           

3. 通過代碼前面的宏,可以選擇幾種不同的紋理映射方式

//Select one of the Texture mode (Set '1'):  
#define TEXTURE_DEFAULT 1  
//Rotate the texture  
#define TEXTURE_ROTATE  0  
//Show half of the Texture  
#define TEXTURE_HALF    0  
           

第一種是正常的映射方式,第二種是“旋轉”的方式,第三種是隻映射一半的方式。

結果

程式運作結果如下。預設的紋理映射:

最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)

“旋轉”:

最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)

一半紋理:

最簡單的視音頻播放示例6:OpenGL播放YUV420P(通過Texture,使用Shader)

下載下傳

代碼位于“Simplest Media Play”中

SourceForge項目位址: https://sourceforge.net/projects/simplestmediaplay/

CSDN下載下傳位址: http://download.csdn.net/detail/leixiaohua1020/8054395

注:

該項目會不定時的更新并修複一些小問題,最新的版本請參考該系列文章的總述頁面:

 《最簡單的視音頻播放示例1:總述》

上述工程包含了使用各種API(Direct3D,OpenGL,GDI,DirectSound,SDL2)播放多媒體例子。其中音頻輸入為PCM采樣資料。輸出至系統的聲霸卡播放出來。視訊輸入為YUV/RGB像素資料。輸出至顯示器上的一個視窗播放出來。

通過本工程的代碼初學者可以快速學習使用這幾個API播放視訊和音頻的技術。

一共包括了如下幾個子工程:

simplest_audio_play_directsound: 使用DirectSound播放PCM音頻采樣資料。

simplest_audio_play_sdl2: 使用SDL2播放PCM音頻采樣資料。

simplest_video_play_direct3d: 使用Direct3D的Surface播放RGB/YUV視訊像素資料。

simplest_video_play_direct3d_texture: 使用Direct3D的Texture播放RGB視訊像素資料。

simplest_video_play_gdi: 使用GDI播放RGB/YUV視訊像素資料。

simplest_video_play_opengl: 使用OpenGL播放RGB/YUV視訊像素資料。

simplest_video_play_opengl_texture: 使用OpenGL的Texture播放YUV視訊像素資料。

simplest_video_play_sdl2: 使用SDL2播放RGB/YUV視訊像素資料。

繼續閱讀