OpenGL ES
OpenGL 是一種應用程序編程接口,它是一種可以對圖形硬件設備特性進行訪問的軟件庫。
重點:OpenGL 是一種接口,既然是接口,那麼就必然要有實現。
事實上,它的實現是由顯示設備廠商提供的,而且依賴於廠商提供的硬件設備。
OpenGL 常用於 CAD、虛擬實境、科學可視化程序和電子遊戲開發。
在 Android 上使用的是 OpenGL ES,它是 OpenGL 的子集,在 OpenGL 的基礎之上裁剪掉了一些非必要的部分,主要是針對手機、PAD 和遊戲主機等嵌入式設備設計的。
在 Android 上開發 OpenGL 既可以使用 Java 也可以使用 C ,話不多說,擼起袖子就是幹!
渲染管線
渲染管線也稱爲渲染流水線
或像素流水線
或像素管線
,是GPU處理圖形信號的相互獨立的並行處理單元。
這張圖展示了我們調用OpenGL的drawXXX()
方法後執行的流程,我們傳遞的頂點首先會經過頂點着色器vertex shader的處理,一般會在裏面做頂點變換相關的邏輯,然後進行圖元裝配,再經過幾何着色器geometry shader,這個着色器相對來說使用得少一些,可暫時先忽略,然後接下來就是光柵化,所謂光柵化就是把我們要渲染的圖像打碎成屏幕上的像素,因爲最終要顯示到屏幕上,就必須將圖形對應到像素上,光柵化完成後,我們就有了要渲染的圖形對應的像素,此時像素還沒有顏色,需要我們填上顏色,這時就到達到了片段着色器fragment shader,在fragment shader中我們通常進行顏色的計算,確定對應的像素顯示什麼顏色,fragment shader將在下篇文章中介紹。
在整個渲染管線中,vertex shader、geometry shader和fragment shader這三部分是可編程分部,可編寫shader代碼實現相應的功能,我們目前重點關注vertex shader和fragment shader。
這裏特別注意一點,我們的shader代碼並不是像普通程序那樣,一次性輸入所有頂點,然後再輸出,例如對於vertex shader,我們傳遞了3個頂點,並不是3個頂點一起執行一次vertex shader,而是分別對這3個頂點執行一次,也就是執行了3次。對於fragment shader也是類似的,並不是執行一次爲所有的像素填充顏色,而是對每個像素都執行一次。這個特點有時讓初學者感到困惑。
座標
下面是紋理座標系統和位置座標系統之間的對應關係:
內存拷貝
當我們定義好頂點座標,那麼就可以將頂點座標傳入渲染管線,進行一些列操作。但是我們如何傳給Opengl呢?
OpenGL 的實現是由顯示設備廠商提供的,它作爲本地系統庫直接運行在硬件上。而我們定義的頂點 Java 代碼是運行在虛擬機上的,這就涉及到了如何把 Java 層的內存複製到 Native 層了。
一種方法是直接使用JNI
開發,直接調用本地系統庫,也就是用 C++
來開發 OpenGL,這種實現肯定要學會的。
另一種方法就是在 Java 層把內存塊複製到 Native 層。
使用ByteBuffer.allocateDirect()
方法就可以分配一塊 Native 內存,這塊內存不會被 Java 的垃圾回收器管理。
ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * (Float.SIZE / 8));
vbb.order(ByteOrder.nativeOrder()); //設置字節順序
FloatBuffer vertexBuf = vbb.asFloatBuffer(); //轉換爲Float型緩衝
vertexBuf.put(vertices); //向緩衝區中放入頂點座標數據
vertexBuf.position(0); //設置緩衝區起始位置
在allocateDirect
方法分配了內存並指定了大小之後,下一步就是告訴 ByteBuffer 按照本地字節序
組織它的內容。本地字節序是指,當一個值佔用多個字節時,比如 32 位整型數,字節按照從最重要位到最不重要位或者相反順序排列。
接下來asFloatBuffer
方法可以得到一個反映底層字節的 FloatBuffer 類實例,避免直接操作單獨的字節,而是使用浮點數。
最後,通過put
方法就可以把數據從 Java 層內存複製到 Native 層了,當進程結束時,這塊內存就會被釋放掉。
頂點着色器(Vertex Shader)
主要負責描繪圖形,也就是根據頂點座標,建立圖形模型。
根據上圖的渲染管線,頂點着色器到片段着色器之間,還要經過組裝圖元
和光柵化圖元
。
片段着色器 (Fragment Shader)
片段着色器的主要目的就是告訴 GPU 每個片段的最終顏色應該是什麼。
光柵化技術
移動設備的顯示屏由成百上千個小的、獨立的部件組成,他們稱爲像素
。每個像素通常由三個單獨的子組件構成,它們發出紅色、綠色和藍色的光,因爲每個像素都非常小,人的眼睛會把紅色、綠色和藍色的光混合在一起,從而創造出巨量的顏色範圍。
OpenGL 就是通過 光柵化 技術的過程把每個點、直線及三角形分解成大量的小片段,它們可以映射到移動設備顯示屏的像素上,從而生成一幅圖像。這些片段類似於顯示屏上的像素,每一個都包含單一的純色。
如下圖所示:
OpenGL 通過光柵化技術把一條直線映射爲一個片段集合,顯示系統通常會把這些片段直接映射到屏幕上的像素,結果一個片段就對應一個像素。
明白了這樣的顯示原理,就可以在其中做一些操作了,這就是片段着色器的功能了。
幀緩衝(FrameBuffer Object )
frame buffer,即幀緩存,顧名思義,它就是能緩存一幀的這麼個東西,它有什麼用呢?大家回想我們之前的教程,我們都是通過一次渲染把內容渲染到屏幕(嚴格來說是渲染到GLSurfaceview上),如果我們的渲染由多個步驟組成,而每個步驟的渲染結果會給到下一個步驟作爲輸入,那麼就要用到 frame buffer.
frame buffer
有一些個attachment
,例如color attachment
、depth attachment
、stencil attachment
,frame buffer
具有什麼樣的功能,就與frame buffer
綁定的attachment
有關。
其中color attachment
就是用來綁定texture
的,當將一個color attachment
綁定到一個texture
上後,就可以用這個frame buffer
來承載渲染的結果,渲染的結果實際上是到了這個綁定的texture
上。
depth attachment
是用來存儲深度信息的,在3D渲染時纔會用到,stencil attachment
則是在模板測試時會用到,這裏先不介紹。
可以看到,frame buffer
本身其實並不會存儲數據,都是通過attachment
去綁定別的東西來存儲相應的數據,我們今天要講的就是color attachment
,我們會將frame buffer
中的一個attachment
綁定到一個texture
上,然後先將第一步的效果渲染到這個frame buffer
上作爲中間結果,然後將這個texture
作爲第二步的輸入。
EGL
一句總結就是:EGL
是連接OpenGL ES
與本地窗口系統的橋樑。
我們知道OpenGL
是跨平臺的,但是不同平臺上的窗口系統是不一樣的,它就需要一個東西幫助OpenGL
與本地窗口系統進行對接、管理及執行GL
命令等。
這聽起來挺底層的,我們爲什麼需要去了解這個呢?我舉幾個例子,比如你想把你的GL
邏輯多線程化,以提升效率,如果不瞭解EGL
,直接把GL
操作簡單地拆分到多個線程中執行,會發現有問題,後文也會提到,再比如,你想用MediaCodec
做視頻編解碼,你會發現,也常常需要了解EGL
,特別是當你想在編碼前、解碼後做OpenGL
特效處理時,比如將原視頻進行OpenGL ES
特效渲染然後編碼保存,或者是解碼原視頻然後進行OpenGL ES
特效渲染再顯示出來。編碼時需要將要編碼的幀渲染到MediaCodec
給你的一塊surface
上,而這些操作需要有EGL
才能做,而解碼時是解碼到一塊你自己指定的surface
上,此時你也沒有一個現成的EGL
環境,如果你想解碼出來先用OpenGL ES
做些特效處理再顯示出來,那麼這時也需要EGL
環境。
//EGL創建步驟
EGLDisplay eglDisplay = EGL14.EGL_NO_DISPLAY;
EGLContext eglContext = EGL14.EGL_NO_CONTEXT;
EGLConfig eglConfig = null;
int glVersion = -1;
//1.獲取顯示設備
//這裏獲取的是default的顯示設備,大多數情況下我們都是獲取default,因爲大多數情況下設備只有一個屏幕
eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
//2.初始化設備
//這裏初始化完成後,會返回給我們支持的EGL的主版本和子版本
int[] version = new int[2];
EGL14.eglInitialize(eglDisplay, version, 0, version, 1);
//3.選擇config
//attribList是我們期望的配置,我們這裏的配置是將RGBA顏色深度設置爲8位,並將OpenGL ES版本設置爲2和3,表示同時支持OpenGL 2和OpenGL 3,最後以一個EGL14.EGL_NONE作爲結束符。
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 | EGLExt.EGL_OPENGL_ES3_BIT_KHR,
EGL14.EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
EGL14.eglChooseConfig(eglDisplay, attribList,
0, configs, 0, configs.length,numConfigs, 0);
//eglConfig是返回的儘可能接近我們期望的配置的列表,通常我們取第0個來使用,即最符合我們期望配置。
eglConfig = configs[0];
//4.創建EGL Context
int[] attrib3_list = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
EGL14.EGL_NONE
};
//注意第三個參數,它是指定一個共享的EGL Context,共享後,2個EGL Context可以相互使用對方創建的texture等資源,默認情況下是不共享的,但不是所有資源都能共享,例如program就是不共享的。
eglContext = EGL14.eglCreateContext(
eglDisplay,
eglConfig,
EGL14.EGL_NO_CONTEXT,
attrib3_list,
0
);
//5.創建EGL Surface
//可以理解成是一個用於承載顯示內容的東西,這裏有2種EGL Surface可以選擇,一種是window surface,一種是pbuffer surface,
int[] surfaceAttribs = {
EGL14.EGL_NONE
};
EGLSurface eglSurface1 = EGL14.eglCreateWindowSurface(
eglDisplay, eglConfig, surface,surfaceAttribs, 0);
/*
int[] surfaceAttribs2 = {
EGL14.EGL_WIDTH, width,
EGL14.EGL_HEIGHT, height,
EGL14.EGL_NONE
};
EGLSurface eglSurface2 = EGL14.eglCreatePbufferSurface(
eglDisplay,
eglConfig,
surfaceAttribs2,
0
);
*/
//6.綁定EGL
//一個線程只能綁定一個EGL環境,如果之前綁過其它的,後面又綁了一個,那就會是最後綁的那個。至此,就能讓一個線程擁有EGL環境了,此後就可以順利地做GL操作了。
EGL14.eglMakeCurrent(
eglDisplay,
eglSurface,
eglSurface,
eglContext
);
常用代碼
-
創建紋理
//獲取bitmap BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = false; Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId, options); //創建紋理 int[] textureIds = new int[1]; glGenTextures(1, textureIds, 0); //綁定紋理 glBindTexture(GL_TEXTURE_2D, textureIds[0]); // 設置縮小的情況下過濾方式 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); // 設置放大的情況下過濾方式 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 加載紋理到 OpenGL,讀入 Bitmap 定義的位圖數據,並把它複製到當前綁定的紋理對象 // 當前綁定的紋理對象就會被附加上紋理圖像。 texImage2D(GL_TEXTURE_2D, 0, bitmap, 0); bitmap.recycle(); //解除與紋理的綁定,避免用其他的紋理方法意外地改變這個紋理 glBindTexture(GL_TEXTURE_2D, 0);
-
幀緩衝 FBO(FrameBuffer Object)
int frameBuffer = 0; int frameBufferTexture = 0; //創建FBO int[] framebuffers = new int[1]; GLES20.glGenFramebuffers(framebuffers.length, framebuffers, 0); frameBuffer = framebuffers[0]; //創建FBO需要綁定的texture int[] textures = new int[1]; GLES20.glGenTextures(textures.length, textures, 0); frameBufferTexture = textures[0]; glBindTexture(GLES20.GL_TEXTURE_2D, framebufferTexture); //設置texture過濾方式 GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); //設置texture環繞方式 GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); //給texture分配內存,對於這個texture,我們只分配內存,而不去填充它,因此最後的參數爲null GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); //把texture綁定到顏色附件 GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffer); GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, frameBufferTexture, 0);