opengl es學習筆記

OpenGL ES

OpenGL 是一種應用程序編程接口,它是一種可以對圖形硬件設備特性進行訪問的軟件庫。

重點:OpenGL 是一種接口,既然是接口,那麼就必然要有實現。

事實上,它的實現是由顯示設備廠商提供的,而且依賴於廠商提供的硬件設備。

OpenGL 常用於 CAD、虛擬實境、科學可視化程序和電子遊戲開發。

在 Android 上使用的是 OpenGL ES,它是 OpenGL 的子集,在 OpenGL 的基礎之上裁剪掉了一些非必要的部分,主要是針對手機、PAD 和遊戲主機等嵌入式設備設計的。

在 Android 上開發 OpenGL 既可以使用 Java 也可以使用 C ,話不多說,擼起袖子就是幹!

渲染管線

703548-20190402212658795-1353723850.png

渲染管線也稱爲渲染流水線像素流水線像素管線,是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 就是通過 光柵化 技術的過程把每個點、直線及三角形分解成大量的小片段,它們可以映射到移動設備顯示屏的像素上,從而生成一幅圖像。這些片段類似於顯示屏上的像素,每一個都包含單一的純色。

如下圖所示:

640.png

OpenGL 通過光柵化技術把一條直線映射爲一個片段集合,顯示系統通常會把這些片段直接映射到屏幕上的像素,結果一個片段就對應一個像素。

明白了這樣的顯示原理,就可以在其中做一些操作了,這就是片段着色器的功能了。

幀緩衝(FrameBuffer Object )

frame buffer,即幀緩存,顧名思義,它就是能緩存一幀的這麼個東西,它有什麼用呢?大家回想我們之前的教程,我們都是通過一次渲染把內容渲染到屏幕(嚴格來說是渲染到GLSurfaceview上),如果我們的渲染由多個步驟組成,而每個步驟的渲染結果會給到下一個步驟作爲輸入,那麼就要用到 frame buffer.

640.jpg

frame buffer有一些個attachment,例如color attachmentdepth attachmentstencil attachmentframe 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);
    
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章