当前位置: 首页>编程笔记>正文

渲染軟件哪個好用,Android平臺上基于OpenGl渲染yuv視頻

渲染軟件哪個好用,Android平臺上基于OpenGl渲染yuv視頻

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發布

更多音視頻開發文章,請看:音視頻開發專欄

介紹一個自己剛出爐的音視頻播放錄制開源項目

前言

這是我音視頻專欄的第一篇實例解析,也算是入門篇,重點講下如何使用OpenGl去渲染一個yuv視頻。

渲染軟件哪個好用?本篇博文涉及的知識點主要有三個:
1.yuv的概念
2.基于ndk進行C++程序的基本編寫
3.OpenGl紋理的繪制

本文將重點講知識點1和3,ndk開發部分就不細談,由于OpenGl知識體系龐大,本文也是根據重點來分析,所以如果沒有ndk開發基礎和OpenGl基礎的讀者看本文可能會比較困難。

談談YUV

YUV,是一種顏色編碼方法。常使用在各個影像處理組件中。Y”表示明亮度(Luminance、Luma),“U”和“V”則是色度、濃度(Chrominance、Chroma)相對我們都比較熟悉的編碼格式RGB,RGB訴求于人眼對色彩的感應,YUV則著重于視覺對于亮度的敏感程度。 YUV在對照片或影片編碼時,考慮到人類的感知能力,允許降低色度的帶寬。換句話說,也就是編碼的時候允許Y的量比UV要多,允許對圖片的UV分量進行下采樣,這樣數據占用的空間就比RGB更小(關于下采樣,簡單來說就是以比原來更低的采樣率進行采樣。詳細可以看下維基百科:Downsampling (signal processing)也可以看下知乎這篇文章:oversampling,undersampling,downsampling,upsampling 四個概念的區別和聯系是什么?)。

圖像中的Y, U,和V組成:
圖像中的Y', U,和V組成

這樣說有點抽象,可以看看微軟這篇有名的文章進行理解:Video Rendering with 8-Bit YUV Formats

代渲染、這里主要講yuv的兩個方面,分別是采樣格式和存儲格式。采樣格式簡單可以理解一張原圖,每個像素怎么采樣yuv各個分量,比如每隔幾個像素采一個y分量(或者u、v)。存儲格式簡單來說就是采樣之后,按照什么方式存儲,比如哪個字節存儲y,第幾個字節存儲u。

yuv采樣格式:

文章里面“YUV Sampling”一節詳細說明了各種不同格式的yuv是如何采樣的。
以下是對該章節的節選翻譯:

YUV的優點之一是,感知質量不會顯著下降的前提下,色度通道的采樣率與Y通道的采樣率相比更低。一般用一個叫做A:B:C(即y:u:v)的符號用來描述U和V相對于Y的采樣頻率,為了方便理解,使用圖來描述,圖中y分量使用x表示,uv使用o表示:

4:4:4:

意味著色度通道沒有向下采樣,也就是說yuv三個通道都是全采樣:

4:4:4

4:2:2:

android opencv。表示2:1水平下采樣,沒有垂直下采樣。每條掃描線包含四個Y樣本對應兩個U或V樣本。也就是水平方向按照y:uv使用2:1進行采樣,垂直方向全采樣的方式:

4:2:2

4:2:0:

表示2:1水平下采樣,2:1垂直下采樣。也就是水平方向按照y:uv使用2:1進行采樣,垂直方向按照y:uv使用2:1的方式:

4:2:0

注意這里4:2:0并不代表y:u:v = 4:2:0,這里指的是在每一行掃描時,只掃描一種色度分量(U 或者 V),和 Y 分量按照 2 : 1 的方式采樣。比如,第一行掃描時,YU 按照 2 : 1 的方式采樣,那么第二行掃描時,YV 分量按照 2:1 的方式采樣。所以y和u或者v的比都是2:1。

4:1:1:

opengl vulkan。表示4:1水平下采樣,沒有垂直下采樣。每條掃描線包含四個Y樣本對應于每一個U或V樣本。

4:1:1抽樣比其他格式更少見,本文不詳細討論。

yuv存儲格式:

YUV存儲格式有兩大類:planar 和 packed:
packed:Y、U和V組件存儲在一個數組中。每個像素點的Y,U,V是連續交錯存儲的。和RGB的存儲格式類似。
planar :Y、U和V組件存儲為三個獨立的數組中。

y、u、v每個采樣點使用8bit存儲。

接下來詳細講下集中常見的yuv格式存儲方式:

4:2:2格式:

什么渲染平臺好?主要有兩種具體格式:

YUY2:

屬于packed類型,YUY2格式,數據可視為unsigned char數組。第一個字節包含第一個Y樣本,第二個字節包含第一U (Cb)樣本,第三字節包含第二Y樣本,第四個字節包含第V (Cr)樣本,以此類推,如圖:
YUY2
可以看到,Y0 和 Y1 公用 U0 V0 分量,Y2 和 Y3 公用 U1 V1 分量,以此類推。

UYVY:

也是屬于屬于packed類型的,和YUY2和類似,只是存儲方向是相反的:
UYVY

4:2:0格式

該格式又包含多種存儲方式,這里重點將以下幾種:

YUV 420P 和 YUV 420SP 都是基于 Planar 平面模式 進行存儲的,先存儲所有的 Y 分量后, YUV420P 類型就會先存儲所有的 U 分量或者 V 分量,而 YUV420SP 則是按照 UV 或者 VU 的交替順序進行存儲了,具體查看看下圖(圖來源于:音視頻基礎知識—像素格式YUV):

YUV420P:

keyshot云渲染平臺、(這里需要敲黑板,因為本文播放的yuv就是YUV420P格式,熟悉它的存儲格式才可以理解代碼中讀取視頻幀數據的邏輯)
YUV420P
正是因為 YUV420P是2:1水平下采樣,2:1垂直下采樣,所以y分量數量等于視頻寬高,u和v分量都是視頻寬乘以高/4

YUV420SP

YUV420SP
4:2:0格式還有YV12、YU12、NV12 、NV21等存儲格式,這里因為篇幅關系就不做細談。

yuv轉RGB:

目前一般解碼后的視頻格式為yuv,但是一般顯卡渲染的格式是RGB,所以需要把yuv轉化為RGB。

關于yuv轉RGB這里有個公式可以知己使用:
在這里插入圖片描述
或者直接用yuv的矩陣乘以以下矩陣得到對應的RGB矩陣:
在這里插入圖片描述

yuv就先介紹到這里,熟悉yuv對于后面yuv視頻播放至關重要。

談談OpenGl

云渲染平臺怎么使用。OpenGL是行業領域中最為廣泛接納的 2D/3D 圖形 API。OpenGL是一個跨平臺的軟件接口語言,用于調用硬件的2D、3D圖形處理器。由于只是軟件接口,所以具體底層實現依賴硬件設備制造商。

關于OpenGl的知識,可能寫20篇博文也介紹不完,這里只介紹和當前播放yuv相關的,不會很詳細,詳細教程可以看這個網站:歡迎來到OpenGL的世界(以下描述也部分節選該網站)

安卓使用的是OpenGl ES版本,即OpenGL的一個子集,裁剪了一些功能,專門使用在嵌入式設備。

OpenGL圖形渲染管線

首先要解釋的是OpenGl的圖形渲染管線:指的是一堆原始圖形數據途經一個輸送管道,期間經過各種變化處理最終出現在屏幕的過程。分為兩個主要部分:第一部分把你的3D坐標轉換為2D坐標,第二部分是把2D坐標轉變為實際的有顏色的像素。

圖形渲染管線接受一組3D坐標,然后把它們轉變為你屏幕上的有色2D像素輸出。圖形渲染管線可以被劃分為幾個階段,每個階段將會把前一個階段的輸出作為輸入。

網絡渲染平臺,當今大多數顯卡都有成千上萬的小處理核心,它們在GPU上為每一個(渲染管線)階段運行各自的相互獨立的并行處理小程序,從而在圖形渲染管線中快速處理你的數據。這些小程序叫做著色器(Shader),因為它們運行在GPU中,所以解放了CPU的省生產力

圖形渲染管線的每個階段的展示:

在這里插入圖片描述

圖形渲染管線的第一個部分是頂點著色器(Vertex Shader),它把一個單獨的頂點作為輸入。頂點著色器主要的目的是把3D坐標轉為另一種3D坐標(坐標系統的轉化),同時頂點著色器允許我們對頂點屬性進行一些基本處理。頂點著色器代碼是每個頂點執行一次。

圖元裝配(Primitive Assembly)階段將頂點著色器輸出的所有頂點作為輸入(如果是GL_POINTS,那么就是一個頂點),并所有的點裝配成指定圖元的形狀。比如將頂點裝配為三角形或者矩形。

渲染100、幾何著色器的輸出會被傳入光柵化階段(Rasterization Stage),這里它會把圖元映射為最終屏幕上相應的像素,生成供片段著色器(Fragment Shader)使用的片段(Fragment,OpenGL渲染一個像素所需的所有數據)。

片段著色器的主要目的是計算一個像素的最終顏色,這也是所有OpenGL高級效果產生的地方。片段著色器是每個片段(像素)執行一次

而我們要處理的,主要就是頂點著色器和片段著色器的代碼邏輯,著色器是用叫GLSL的類C語言寫成的,它包含一些針對向量和矩陣操作的有用特性。詳細語法見著色器

OpenGL坐標系

要寫頂點著色器代碼,首先就要知道OpenGL頂點坐標系:

按照慣例,OpenGL是一個右手坐標系。簡單來說,就是正x軸在你的右手邊,正y軸朝上,而正z軸是朝向后方的。想象你的屏幕處于三個軸的中心,則正z軸穿過你的屏幕朝向你:

在這里插入圖片描述

(這里要提的一點事,OpenGl在執行頂點著色器之后,會像流水線一樣將坐標進行5個步驟的變換:局部坐標–世界坐標–觀察坐標–裁剪坐標–屏幕坐標,這里因為實例是2D的,暫時還不需要關心這些)

現在需要記得的是,OpenGL僅當3D坐標在3個軸(x、y和z)上都為-1.0到1.0的范圍內時才處理它。所有在所謂的標準化設備坐標(Normalized Device Coordinates)范圍內的坐標才會最終呈現在屏幕上(在這個范圍以外的坐標都不會顯示)。

2D情況下,既不考慮z軸,則一般來說頂點坐標系如下所示:

頂點坐標系

OpenGL紋理繪制

通過頂點著色器和片段著色器,我們可以指定要繪制的物體形狀大小以及顏色,但是如果我們要做類似將一張圖片繪制上去,該如何做呢?

OpenGl提供了紋理這個概念,讓你可以將一張圖片“貼”到你想要的位置。
(詳細見 紋理)

那么紋理是如何“貼”到圖形上去的呢?其實就是對圖片進行采樣,再將采樣到的顏色數據繪制到圖形相應的位置。

為了能夠把紋理映射(Map)到我們的圖形上,我們需要指定圖形的每個頂點各自對應紋理的哪個部分。所以圖形的每個頂點都會關聯一個紋理的坐標,用來標明該從紋理圖像的哪個部分采樣。

通俗來說,就是比方你頂點坐標提供的是一個矩形,現在要將一張圖片(紋理)“貼”到矩形上,那么需要指定一個紋理坐標,告訴OpenGl矩形光柵化處理后的每個片段對應圖片的哪個像素的顏色。紋理坐標,簡單來說就是以一張紋理圖片的某個點作為原點的坐標系。

類似下圖所示:
在這里插入圖片描述
由上圖可以看到紋理坐標系的模樣了,不過在Android平臺,紋理坐標如下:

在這里插入圖片描述

即以圖片的左上角為原點的坐標系。

所以在提供了頂點坐標和紋理坐標之后,OpenGL就知道如何通過采樣紋理上的像素的顏色數據,將顏色繪制到頂點坐標所表達的圖形上的對應位置。

紋理就先講到這里,還有許多具體的采樣細節需要注意,還請看詳細教程紋理

程序實例分析

所謂工欲善其事必先利其器,基礎知識講得差不多了,那么又要進入最重要的將代碼環節了,這里使用的yuv格式為yuv420p

這里使用cmake進行構建,native-lib為項目自定義的動態庫名稱,其余需要鏈接的動態庫如下配置:

find_library( # Sets the name of the path variable.log-lib# Specifies the name of the NDK library that# you want CMake to locate.log )target_link_libraries( # Specifies the target library.native-libGLESv2EGLandroid# Links the target library to the log library# included in the NDK.${log-lib} )

Java層首先創建一個集成GLSurfaceView的類:

public class YuvPlayer extends GLSurfaceView implements Runnable, SurfaceHolder.Callback, GLSurfaceView.Renderer {//這里將yuv視頻文件放在sdcard目錄中private final static String PATH = "/sdcard/sintel_640_360.yuv";public YuvPlayer(Context context, AttributeSet attrs) {super(context, attrs);setRenderer(this);}@Overridepublic void surfaceCreated(SurfaceHolder holder) {new Thread(this).start();}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {}@Overridepublic void run() {loadYuv(PATH,getHolder().getSurface());}//定義一個native方法加載yuv視頻文件public native void loadYuv(String url, Object surface);@Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {}@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {}@Overridepublic void onDrawFrame(GL10 gl) {}
}

進入native層的loadYuv方法:

Java_com_example_yuvopengldemo_YuvPlayer_loadYuv(JNIEnv *env, jobject thiz, jstring jUrl,jobject surface) {const char *url = env->GetStringUTFChars(jUrl, 0);//打開yuv視頻文件	FILE *fp = fopen(url, "rb");if (!fp) {//打Log方法LOGD("oepn file %s fail", url);return;}LOGD("open ulr is %s", url);

首先是從Java層傳入的jstring變量轉為char*,然后打開yuv視頻文件。

接下來是初始化EGL:

這里簡單解釋下EGL是什么。

EGL?是Khronos呈現api(如OpenGL ES或OpenVG)與底層本機平臺窗口系統之間的接口。它處理圖形上下文管理、表面/緩沖區綁定和呈現同步,并使用其他Khronos api支持高性能、加速、混合模式的2D和3D呈現。EGL還提供了Khronos之間的互操作能力,以支持在api之間高效地傳輸數據——例如在運行OpenMAX AL的視頻子系統和運行OpenGL ES的GPU之間。

通俗來講就是,EGL是渲染API(如OpenGL, OpenGL ES, OpenVG)和本地窗口系統之間的接口。EGL可以理解為OpenGl ES ES和設備之間的橋梁,EGL是為OpenGl提供繪制表面的。因為OpenGl是跨平臺的,當它訪問不同平臺的設備的時候需要EGL作為中間的適配器。

在這里插入圖片描述
EGL的使用步驟:
在這里插入圖片描述
具體的代碼:

//1.獲取原始窗口
ANativeWindow *nwin = ANativeWindow_fromSurface(env, surface);//獲取OpenGl ES的渲染目標。Display(EGLDisplay) 是對實際顯示設備的抽象。EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);if (display == EGL_NO_DISPLAY) {LOGD("egl display failed");return;}//2.初始化egl與 EGLDisplay 之間的連接,后兩個參數為主次版本號if (EGL_TRUE != eglInitialize(display, 0, 0)) {LOGD("eglInitialize failed");return;}//創建渲染用的surface//2.1 surface配置EGLConfig eglConfig;EGLint configNum;EGLint configSpec[] = {EGL_RED_SIZE, 8,EGL_GREEN_SIZE, 8,EGL_BLUE_SIZE, 8,EGL_SURFACE_TYPE, EGL_WINDOW_BIT,EGL_NONE};if (EGL_TRUE != eglChooseConfig(display, configSpec, &eglConfig, 1, &configNum)) {LOGD("eglChooseConfig failed");return;}//2.2創建surface(將egl和NativeWindow進行關聯,即將EGl和設備屏幕連接起來。最后一個參數為屬性信息,0表示默認版本)。Surface(EGLSurface)是對用來存儲圖像的內存區FrameBuffer 的抽象。這就是我們要渲染的SurfaceEGLSurface winSurface = eglCreateWindowSurface(display, eglConfig, nwin, 0);if (winSurface == EGL_NO_SURFACE) {LOGD("eglCreateWindowSurface failed");return;}//3 創建關聯上下文const EGLint ctxAttr[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};//創建egl關聯OpenGl的上下文環境 EGLContext 實例。EGL_NO_CONTEXT表示不需要多個設備共享上下文。Context (EGLContext) 存儲 OpenGL ES繪圖的一些狀態信息。上面的代碼只是egl和設備窗口的關聯,這里是和OpenGl的關聯EGLContext context = eglCreateContext(display, eglConfig, EGL_NO_CONTEXT, ctxAttr);if (context == EGL_NO_CONTEXT) {LOGD("eglCreateContext failed");return;}//將EGLContext和opengl真正關聯起來。綁定該線程的顯示設備及上下文//兩個surface一個讀一個寫。if (EGL_TRUE != eglMakeCurrent(display, winSurface, winSurface, context)) {LOGD("eglMakeCurrent failed");return;}

創建初始化EGL,接下來就是真正的OpenGl繪制代碼。

先看下著色器代碼。看著色器代碼之前,先了解下GLSL一些基礎:
常見的變量類型:
attritude:一般用于各個頂點各不相同的量。如頂點位置、紋理坐標、法向量、顏色等等。
uniform:一般用于對于物體中所有頂點或者所有的片段都相同的量。比如光源位置、統一變換矩陣、顏色等。
varying:表示易變量,一般用于頂點著色器傳遞到片段著色器的量。
vec2:包含了2個浮點數的向量
vec3:包含了3個浮點數的向量
vec4:包含了4個浮點數的向量
sampler1D:1D紋理著色器
sampler2D:2D紋理著色器
sampler3D:3D紋理著色器

首先編寫頂點著色器代碼:

//頂點著色器,每個頂點執行一次,可以并行執行
#define GET_STR(x) #x
static const char *vertexShader = GET_STR(attribute vec4 aPosition;//輸入的頂點坐標,會在程序指定將數據輸入到該字段attribute vec2 aTextCoord;//輸入的紋理坐標,會在程序指定將數據輸入到該字段varying vec2 vTextCoord;//輸出的紋理坐標,輸入到片段著色器void main() {//這里其實是將上下翻轉過來(因為安卓圖片會自動上下翻轉,所以轉回來。也可以在頂點坐標中就上下翻轉)vTextCoord = vec2(aTextCoord.x, 1.0 - aTextCoord.y);//直接把傳入的坐標值作為傳入渲染管線。gl_Position是OpenGL內置的gl_Position = aPosition;}
);

這里邏輯很簡單。使用兩個attribute變量,一個接受頂點坐標,一個接收紋理坐標,這里以標準的OpenGl的紋理坐標為標準,即和安卓平臺是上下翻轉關系的(在本文OpenGL紋理繪制一節有說到),所以對傳進來的紋理坐標在0.0~1。0之間進行上下翻轉,再賦值給varying類型變量vTextCoord,vTextCoord將通過渲染管線傳給片段著色器。最后將傳進來的頂點坐標賦值給gl_Position ,gl_Position 是OpenGL內置的表示頂點坐標的變量。gl_Position 被賦值之后,將通過渲染管線傳給后面的階段,在圖元裝配的時候,將頂點連接起來。在光柵化圖元的時候,將兩個頂點之間的線段分解成大量的小片段,varying數據在這個過程中計算生成,記錄在每個片段中,之后傳遞給片段著色器

然后編寫片段著色器代碼:

//圖元被光柵化為多少片段,就被調用多少次
static const char *fragYUV420P = GET_STR(precision mediump float;//接收從頂點著色器、光柵化處理傳來的紋理坐標數據varying vec2 vTextCoord;//輸入的yuv三個紋理uniform sampler2D yTexture;//y分量紋理uniform sampler2D uTexture;//u分量紋理uniform sampler2D vTexture;//v分量紋理void main() {//存放采樣之后的yuv數據vec3 yuv;//存放yuv數據轉化后的rgb數據vec3 rgb;//對yuv各個分量對應vTextCoord的像素進行采樣。這里texture2D得到的結果是一個vec4變量,它的r、g、b、a的值都為采樣到的那個分量的值//將采樣到的y、u、v分量的數據分別保存在vec3 yuv的r、g、b(或者x、y、z)分量yuv.r = texture2D(yTexture, vTextCoord).g;yuv.g = texture2D(uTexture, vTextCoord).g - 0.5;yuv.b = texture2D(vTexture, vTextCoord).g - 0.5;//這里必須把yuv轉化為RGBrgb = mat3(1.0, 1.0, 1.0,0.0, -0.39465, 2.03211,1.13983, -0.5806, 0.0) * yuv;//gl_FragColor是OpenGL內置的,將rgb數據賦值給gl_FragColor,傳到渲染管線的下一階段 ,gl_FragColor 表示正在呈現的像素的 R、G、B、A 值。 gl_FragColor = vec4(rgb, 1.0);}
);

這里要將yuv三個分量分別用三層紋理來渲染,然后將多層紋理混合一起顯示。代碼中三個sampler2D類型變量就是紋理圖片,需要從外部程序傳入。然后通過texture2D方法采樣得到對應紋理坐標位置的顏色數據,將yuv三個分量的采樣值放入vec3 類型變量yuv的三個分量中,因為OpenGl只支持RGB的渲染,所以需要將vec3類型的 yuv通過公式轉為一個rgb 的vec3 類型變量。最后將rgb 變量構建一個vec4變量,作為最終顏色賦值給gl_FragColor 。

著色器代碼定義完,接下來就是渲染邏輯部分。

首先是將前面的定義的著色器加載、編譯以及創建、鏈接、激活著色器程序:

GLint vsh = initShader(vertexShader, GL_VERTEX_SHADER);GLint fsh = initShader(fragYUV420P, GL_FRAGMENT_SHADER);//創建渲染程序GLint program = glCreateProgram();if (program == 0) {LOGD("glCreateProgram failed");return;}//向渲染程序中加入著色器glAttachShader(program, vsh);glAttachShader(program, fsh);//鏈接程序glLinkProgram(program);GLint status = 0;glGetProgramiv(program, GL_LINK_STATUS, &status);if (status == 0) {LOGD("glLinkProgram failed");return;}LOGD("glLinkProgram success");//激活渲染程序glUseProgram(program);

其中initShader函數:

GLint initShader(const char *source, GLint type) {//創建shaderGLint sh = glCreateShader(type);if (sh == 0) {LOGD("glCreateShader %d failed", type);return 0;}//加載shaderglShaderSource(sh,1,//shader數量&source,0);//代碼長度,傳0則讀到字符串結尾//編譯shaderglCompileShader(sh);GLint status;glGetShaderiv(sh, GL_COMPILE_STATUS, &status);if (status == 0) {LOGD("glCompileShader %d failed", type);LOGD("source %s", source);return 0;}LOGD("glCompileShader %d success", type);return sh;
}

傳入頂點坐標數組給頂點著色器:

//加入三維頂點數據。這里就是整個屏幕的矩形。static float ver[] = {1.0f, -1.0f, 0.0f,-1.0f, -1.0f, 0.0f,1.0f, 1.0f, 0.0f,-1.0f, 1.0f, 0.0f};//獲取頂點著色器的aPosition屬性引用GLuint apos = static_cast<GLuint>(glGetAttribLocation(program, "aPosition"));glEnableVertexAttribArray(apos);//將頂點坐標傳入頂點著色器的aPosition屬性//各個參數意義:apos:頂點著色器中aPosition變量的引用。3表示數組中三個數字表示一個頂點。GL_FLOAT表示數據類型是浮點數。//GL_FALSE表示不進行歸一化。0表示stride(跨距),在數組表示多種屬性的時候使用到,這里因為這有一個屬性,設置為0即可。ver表示所傳入的頂點數組地址glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 0, ver);

(習慣了Java開發的同學恐怕看到這種代碼很不習慣吧??)

傳入紋理坐標數組給頂點著色器:

//加入紋理坐標數據,這里是整個紋理。static float fragment[] = {1.0f, 0.0f,0.0f, 0.0f,1.0f, 1.0f,0.0f, 1.0f};將紋理坐標數組傳入頂點著色器的aTextCoord屬性GLuint aTex = static_cast<GLuint>(glGetAttribLocation(program, "aTextCoord"));glEnableVertexAttribArray(aTex);//各個參數意義:aTex :頂點著色器中aTextCoord變量的引用。2表示數組中三個數字表示一個頂點。GL_FLOAT表示數據類型是浮點數。//GL_FALSE表示不進行歸一化。表示stride(跨距),在數組表示多種屬性的時候使用到,這里因為這有一個屬性,設置為0即可。fragment表示所傳入的頂點數組地址glVertexAttribPointer(aTex, 2, GL_FLOAT, GL_FALSE, 0, fragment);

如果能把傳入頂點坐標數組給頂點著色器理解,這一段就沒有什么難度了。

接著是紋理對象的處理:

這里要講一下幾個概念:紋理對象、紋理目標、紋理單元
1.紋理對象是我們創建的用來存儲紋理的顯存,在實際使用過程中使用的是創建后返回的紋理ID。
2.紋理目標可以簡單理解為紋理的類型,比如指定是渲染2D還是3D等。
3.紋理單元:紋理的操作容器,有GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2等,紋理單元的數量是有限的,最多16個。 所以在最多只能同時操作16個紋理。可以簡單理解為第幾層紋理。

創建紋理對象:

   //指定紋理變量在哪一層紋理單元渲染glUniform1i(glGetUniformLocation(program, "yTexture"), GL_TEXTURE0);glUniform1i(glGetUniformLocation(program, "uTexture"), GL_TEXTURE1);glUniform1i(glGetUniformLocation(program, "vTexture"), GL_TEXTURE2);//紋理IDGLuint texts[3] = {0};//創建3個紋理對象,并且得到各自的紋理ID。之后對紋理的操作就可以通過該紋理ID進行。glGenTextures(3, texts);

將紋理對象和相應的紋理目標進行綁定:

//yuv視頻寬高
int width = 640;
int height = 360;
//通過 glBindTexture 函數將紋理目標和以texts[0]為ID的紋理對象綁定后,對紋理目標所進行的操作都反映到該紋理對象上glBindTexture(GL_TEXTURE_2D, texts[0]);//縮小的過濾器(關于過濾詳細可見 [紋理](https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/))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,//指定要Mipmap的等級GL_LUMINANCE,//gpu內部格式,告訴OpenGL內部用什么格式存儲和使用這個紋理數據。 亮度,灰度圖(這里就是只取一個亮度的顏色通道的意思,因這里只取yuv其中一個分量)width,//加載的紋理寬度。最好為2的次冪height,//加載的紋理高度。最好為2的次冪0,//紋理邊框GL_LUMINANCE,//數據的像素格式 亮度,灰度圖GL_UNSIGNED_BYTE,//一個像素點存儲的數據類型NULL //紋理的數據(先不傳,等后面每一幀刷新的時候傳));

這里要注意視頻的寬高一定設置正確,不然渲染的數據就都是錯誤的。

這里要說明下glTexImage2D第三個參數,告訴OpenGL內部用什么格式存儲和使用這個紋理數據(一個像素包含多少個顏色成分,是否壓縮)。常用的常量如下:

在這里插入圖片描述

這里yuv三個分量的代碼都是一樣的,只是傳入的寬高不同,對于u和v來說,寬高各位視頻寬高的二分之一:

//設置紋理的格式和大小glTexImage2D(GL_TEXTURE_2D,0,//細節基本 默認0GL_LUMINANCE,//gpu內部格式 亮度,灰度圖(這里就是只取一個顏色通道的意思)width / 2,height / 2,//v數據數量為屏幕的4分之10,//邊框GL_LUMINANCE,//數據的像素格式 亮度,灰度圖GL_UNSIGNED_BYTE,//像素點存儲的數據類型NULL //紋理的數據(先不傳));

為什么是width / 2,height / 2呢?還記得上文說過的yuv420p的采樣和存儲格式么? YUV420P是2:1水平下采樣,2:1垂直下采樣,所以y分量數量等于視頻寬乘以高,u和v分量都是視頻寬/2乘以高/2。

從視頻文件中讀取yuv數據到內存中:

	unsigned char *buf[3] = {0};buf[0] = new unsigned char[width * height];//ybuf[1] = new unsigned char[width * height / 4];//ubuf[2] = new unsigned char[width * height / 4];//v//循環讀出每一幀for (int i = 0; i < 10000; ++i) {//讀一幀yuv420p數據if (feof(fp) == 0) {//讀取y數據fread(buf[0], 1, width * height, fp);//讀取u數據fread(buf[1], 1, width * height / 4, fp);//讀取v數據fread(buf[2], 1, width * height / 4, fp);}

還是回顧剛才敲黑板的地方,由圖可得yuv420p中,是先存儲視頻寬高個y元素,再存儲視頻寬乘以高/4個u,再存儲視頻寬乘以高/4個v,所以for循環中讀取一幀才按照yuv的順序和數量依次讀到內存的數組中。
在這里插入圖片描述
在讀出一幀后,更新數據到紋理對象上。
buf[0]即y分量的數據渲染到紋理上:

//激活第一層紋理,綁定到創建的紋理glActiveTexture(GL_TEXTURE0);//綁定y對應的紋理glBindTexture(GL_TEXTURE_2D, texts[0]);//替換紋理,比重新使用glTexImage2D性能高多glTexSubImage2D(GL_TEXTURE_2D, 0,0, 0,//相對原來的紋理的offsetwidth, height,//加載的紋理寬度、高度。最好為2的次冪GL_LUMINANCE, GL_UNSIGNED_BYTE,buf[0]);

u和v也是一樣,只是寬高換為width / 2, height / 2。

最后將畫面顯示出來:

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
//窗口顯示,交換雙緩沖區
eglSwapBuffers(display, winSurface);

如此循環,就將每一幀渲染出來,也就播放了yuv視頻:

這里我使用ffmpeg命令將《龍貓》中截取10秒的視頻轉化為yuv,錄屏的gif不知為何總是上傳不了,所以這里只上傳了一張截圖 = = 。
在這里插入圖片描述

雖然只是10秒的視頻,但是已經超過github的最大上傳量,所以視頻沒有上傳。各位如果需要可以自己用ffmpeg命令轉換任何一個格式支持視頻文件為yuv420p格式來運行。

接觸音視頻開發領域時間不長,如有錯誤疏漏,請各位指正~

項目地址:YuvVideoPlayerDemo

參考文獻:

learnopengl
Video Rendering with 8-Bit YUV Formats
音視頻基礎知識—像素格式YUV
《OpenGl超級寶典 第五版》
Android OpenGL ES 視頻應用開發教程目錄
Android 自定義相機開發(三) —— 了解下EGL

原創不易,如果你覺得好,隨手點贊,也是對筆者的肯定~

https://www.nshth.com/bcbj/338445.html
>

相关文章:

  • 渲染軟件哪個好用
  • 代渲染
  • Android opencv
  • opengl vulkan
  • 什么渲染平臺好
  • keyshot云渲染平臺
  • 云渲染平臺怎么使用
  • 網絡渲染平臺
  • 編程語言難度排名,8 月最新編程語言排行榜
  • 手機usb調試被禁用怎么恢復,解決安卓手機USB接口被外設占用導致無法調試的問題
  • 手機上的安卓模擬器,連接手機模擬器
  • 搜狗輸入法怎么手寫和拼音一起輸入,零彝輸入法用戶協議
  • ubuntu自帶gcc編譯器嗎,安裝ubuntu20.04(安裝vim、gcc、VMtools、中文輸入法、漢化、修改IP、無法連網問題)
  • 輸入法哪個最好用,android ip格式化輸入法,Android設置默認輸入法
  • blkmov指令使用例子,ORB-SLAM2代碼解析
  • windows補丁kb3033929怎么安裝,Win8.1 kb2919355安裝不上怎么辦?
  • 淘寶店鋪如何增加流量,淘寶賣家開店怎么做有效減少淘寶垃圾流量
  • 商標使用必須加TM或R嗎,商標中R標和TM標的區別
  • 沒有商標可以上速賣通嘛,速賣通商標授權怎么弄?速賣通官方授權模板書分享
  • 商標中R跟C分別代表什么,商標TM和R有什么區別
  • 商標中R跟C分別代表什么,CSDN Markdown 商標標志 C、TM、R
  • 有關向量的重要結論,專題-句向量(Sentence Embedding)
  • 信息安全等級保護的5個級別,信息安全等級保護措施之網絡安全技術
  • 書是黃金屋下一句是什么,書中的“黃金屋”
  • gps定位,定位iowait問題
  • 渲染軟件哪個好用,Android平臺上基于OpenGl渲染yuv視頻
  • C# wpf 通過HwndHost渲染視頻
  • h5商城源碼,H5全新紅包直通車網站源碼 包含多款游戲已對接支付
  • android基礎面試題及答案,安卓手機系統開發教程!BTAJ面試有關散列(哈希)表的面試題詳解,大廠直通車!
  • 中交第一公路勘察設計研究院,緯地道路縱斷面設計教程_直通車 | 中交一公局公路勘察設計院有限公司招聘公告...
  • arduino怎么把程序傳到板上,STM32替換Arduino直通車
  • 記錄2015年年初跳槽的經歷!
  • 什么情況下可以跳槽,記錄 2015 年年初跳槽的經歷!
  • 聚合支付公司前十,聚合支付行業的2019年終總結大會!細品,你細品~
  • mastercam后處理論壇,mastercam2017后處理升級_如何升級Mastercam 9.1版后處理?
  • 動態表情包制作,android 視頻轉表情,視頻怎么轉gif?好用軟件分享,自己也能制作出搞笑表情包...
  • pc頁面怎么打開,頁面的版心html,關于PC端網頁版心及網頁自適應問題
  • webp圖片怎樣改成jpg,如何給圖片更改格式?jpg轉webp怎么操作