OpenGL Android課程七:介紹Vertex Buffer Objects(頂點緩衝區對象,簡稱:VBOs)

翻譯文html

原文標題:Android Lesson Seven: An Introduction to Vertex Buffer Objects (VBOs) 原文連接:www.learnopengles.com/android-les…java


介紹Vertex Buffer Objects(頂點緩衝區對象,簡稱:VBOs)

在這節課中,咱們將介紹如何定義和如何去使用
頂點緩衝對象(VBOs)。下面是咱們要講到的幾點:

1.怎樣用頂點緩衝對象定義和渲染
2.單個緩衝區、全部數據打包進去、多個緩衝區之間的區別
3.問題和陷阱咱們如何取處理它們
screenshot

什麼是頂點緩衝區對象?爲何使用它們?

到目前爲止,咱們全部的課程都是將對象數據存儲在客戶端內存中,只有在渲染時將其傳輸到GPU中。沒有大量數據傳輸時,這很好,但隨着咱們的場景愈來愈複雜,有更多的物體和三角形,這會給GPU和內存增長額外的成本。android

咱們能作些什麼呢?咱們可使用頂點緩衝對象,而不是每幀從客戶端內存傳輸頂點信息,信息將被傳輸一次,而後渲染器將從該圖形存儲器緩存中獲得數據。git

前提條件

請閱讀OpenGL Android課程一:入門介紹如何從客戶端的內存上傳頂點數據。瞭解OpenGL ES如何與頂點數組一塊兒工做對於理解本課相當重要。github

更詳細的瞭解客戶端緩衝區

一但瞭解瞭如何使用客戶端內存進行渲染,切換到使用VBOs實際上並不太難。其主要的不一樣在於添加了一個上傳數據到圖形內存的額外步驟,以及渲染時添加了綁定這個緩衝區的額外調用。算法

本節課將使用四種不一樣的模式:編程

  1. 客戶端,單獨的緩衝區
  2. 客戶端,打包的緩衝
  3. 頂點緩衝對象,單獨的緩衝區
  4. 頂點緩衝對象,打包的緩衝

不管咱們是否使用頂點緩衝對象,咱們都須要先將咱們的數據存儲在客戶端本地緩衝區。會想到第一課中OpenGL ES 是一個本地系統庫,而java是運行在Android上的一個虛擬機中。如何去橋接這個距離?咱們須要使用一組特殊的緩衝區類來在本地堆上分配內存,並使使其供OpenGL訪問:數組

// Java 數組
float[] cubePositions;
...
// 浮點緩衝區
final FloatBuffer cubePositionsBuffer;
...
// 在本地堆上直接分配一塊內存
// 字節大小爲cubePositions的長度乘以每一個浮點數的字節大小
// 每一個float的字節大小爲4,由於float是32位或4字節
cubePositionsBuffer = ByteBuffer.allocateDirect(cubePositions.length * BYTES_PRE_FLOAT)
// 浮點會以大端(big-endian)或小段(little-endian)的順序排列
// 我想讓其同本地平臺相同的排列
.order(ByteOrder.nativeOrder())
// 在這個字節緩衝區上給咱們一個浮點視角
.asFloatBuffer();
複製代碼

將Java堆上數據轉換到本地堆上,就是兩方法調用的事情:緩存

// 將java堆上的數據拷貝到本地堆
cubePositionsBuffer.put(cubePositions)

// 重置這個緩衝區開始的緩衝位置
.position(0);
複製代碼

緩衝位置的目的是什麼?一般,Java沒有爲咱們提供一種在內存中使用指針,任意指定位置的方法。然而,設置緩衝區的位置在功能上等同於更改指向內存塊指針的值。經過改變指針的位置,咱們能夠將緩衝區中任意的內存位置傳遞給OpenGL調用。當咱們使用打包的緩衝做業時,這將派上用場。app

一但數據存放到本地堆上,咱們就不須要長時間持有float[]數組了,咱們可讓垃圾回收器清理它。

使用客戶端緩衝區進行渲染很是簡單,咱們僅須要啓動對應屬性的頂點素組,並將指針傳遞給咱們的數據:

// 傳入位置信息
GLES20.glEnableVertexAttribArray(mPositionHandle);
GLES20.glVertexAttriPointer(mPositionHandle, POSITION_DATA_SIZE,
    GLES20.GL_FLOAT, false, 0, mCubePositions)
複製代碼

glVertexAttriPointer參數說明:

  • mPositionHandle: 咱們着色器程序的位置屬性索引
  • POSITION_DATA_SIZE: 定義這個屬性須要多少個float元素
  • GL_FLOAT: 每一個元素的類型
  • false: 定點數據因該標準化嗎?因爲咱們使用的是浮點數據,所以不適用。
  • 0: 跨度,設置0,覺得着應安順序讀取。第一課中設置爲7,表示每次讀取跨度7個位置
  • mCubePositions: 指向緩衝區的的指針,包含全部位置數據

使用打包緩衝區

使用打包緩衝區是很是類似的,替換了每一個位置、法線等的緩衝區,如今一個緩衝區將包含全部這些數據。不一樣點看下面:

使用單緩衝區

positions = X,Y,Z,X,Y,Z,X,Y,Z,...
colors = R,G,B,A,R,G,B,A,...
textureCoordinates = S,T,S,T,S,T...
複製代碼

使用打包緩衝區

buffer = X,Y,Z,R,G,B,A,S,T...
複製代碼

使用打包緩衝區的好處是它將會使GPU更高效的渲染,由於渲染三角形所需的全部信息都位於內存同一塊地方。缺點是,若是咱們使用動態數據,更新可能會更困難,更慢。

當咱們使用打包緩衝區時,咱們須要如下幾種方式更改渲染調用。首先,咱們須要告訴OpenGL跨度(stride) ,定義一個頂點的字節數。

final int stride = (POSITION_DATA_SIZE + NORMAL_DATA_SIZE + TEXTURE_COORDINATE_DATA_SIZE)
    * BYTES_PER_FLOAT;

// 傳入位置信息
mCubeBuffer.position(0);
GLES20.glEnableVertexAttribArray(mPositionHandle);
GLES20.glVertexAttribPointer(mPositionHandle, POSITION_DATA_SIZE,
    GLES20.GL_FLOAT, false, stride, mCubeBuffer);

// 傳入法線信息
mCubeBuffer.position(POSITION_DATA_SIZE);
GLES20.glEnableVertexAttribArray(mNormalHandle);
GLES20.glVertexAttribPointer(mNormalHandle, NORMAL_DATA_SIZE,
    GLES20.GL_FLOAT, false, stride, mCubeBuffer);
...
複製代碼

這個跨度告訴OpenGL ES下一個頂點的一樣的屬性要再跨多遠才能找到。例如:若是元素0是第一個頂點的開始位置,而且這裏每一個頂點有8個元素,而後這個跨度將是8個元素,也就是32個字節。下一個頂點的位置將找到第8個元素,下下個頂點的位置將找到第16個元素,以此類推。

請記住,傳遞給glVertexAttriPointer的跨度單位是字節,而不是元素,所以請記住進行該轉換。

注意,當咱們從指定位置切換到指定法線時,咱們要更改緩衝區的其實位置。這是咱們以前提到的指針算法,這是咱們在使用OpengGL ES時用Java作的方式。咱們仍然使用同一個緩衝區mCubeBuffer,可是咱們告訴OpenGL從位置數據後的第一個元素開始讀取法線信息。咱們也告訴OpenGL下一個法線要跨越8個元素(也能夠說是32個字節)開始。

Dalvik和本地堆上的內存

若是你在本地堆上分配大量內存把並將其釋放,您早晚會遇到心愛的OutOfMemoryError ,背後有幾個緣由:

  1. 您可能認爲經過讓引用超出範圍而自動釋放了內存,可是本地內存彷佛須要一些額外的GC週期才能徹底清理,若是沒有足夠可用的內存而且還沒有釋放本地內存,Dalvik將拋出異常。
  2. 本地堆可能會碎片化,調用allocateDirect()將會莫名其妙失敗,儘管彷佛有足夠的內存可用。有時它有助於進行較小的分配,釋放它,而後再次嘗試更大的分配。

如何能避免這些問題?除了但願Google在將來的版本中改進Dalvik的行爲以外,並很少。或者經過本地代碼進行分配或預先分配一大塊內存來自行管理堆,並根據此分離緩衝區。

注意:這些信息最初寫於2012年初,如今Android使用了一個名爲ART的不一樣運行時,它可能在相同程度上不會遇到這些問題。

移動到頂點緩衝區對象

如今咱們已經回顧了使用客戶端緩衝區,讓咱們繼續討論頂點緩衝區對象!首先,咱們須要回顧幾個很是重要的問題:

1. 緩衝區必須建立在一個有效的OpenGL上下文中

這彷佛是一個明顯的觀點,可是它僅僅提醒你必須等到onSurfaceCreated()執行,而且你必須注意OpenGL ES調用是在GL線程上完成的。 看這個文檔:iOS OpenGL ES編程指南,它多是爲iOS寫的,可是OpenGL ES在Android的行爲和這相同。

2. 頂點緩衝區對象使用不當會致使圖形驅動程序崩潰

當你使用頂點緩衝對象時,須要注意傳遞的數據。不當的值將會致使OpenGL ES系統庫或圖形驅動庫本地崩潰。在個人Nexus S上,一些遊戲徹底卡在個人手機上或致使手機重啓,由於圖形驅動由於他們的指令崩潰。並不是全部的崩潰都會鎖定您的設備,但至少您不會看到「此應用已中止工做」的對話框。您的活動將在沒有警告的狀況下從新啓動,您將得到惟一的信息多是日誌中的本地調試跟蹤。

上傳頂點數據到GPU

要上傳數據到GPU,咱們須要像之前同樣建立客戶端緩衝區的相同步驟:

...
cubePositionsBuffer = ByteBuffer.allocateDirect(cubePositions.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
cubePositionsBuffer.put(cubePositions).position(0);
...
複製代碼

一旦咱們有了客戶端緩衝區,咱們就能夠建立一個頂點緩衝區對象,並使用一下指令將數據從客戶端內存上傳到GPU:

// 首先,咱們要儘量的申請更多的緩衝區
// 這將爲咱們提供這些緩衝區的handle
final int buffers[] = new int[3];
GLES20.glGenBuffers(3, buffers, 0);

// 綁定這個緩衝區,未來的指令將單獨影響此緩衝區
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0]);

// 客戶端內存中的數據轉移到緩衝區
// 咱們能在這次調動後釋放客戶端內存
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, cubePositionsBuffer.capacity() * BYTES_PER_FLOAT,
    cubePositionsBuffer, GLES20.GL_STATIC_DRAW);

// 重要提醒:完成緩衝後,從緩衝區取消綁定
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
複製代碼

一旦數據上傳到了OpenGL ES,咱們就能夠釋放這個客戶端內存,由於咱們不須要再繼續保留它。這是glBufferData的解釋:

  • GL_ARRAY_BUFFER: 這個緩衝區包含頂點數據數組
  • cubePositionsBuffer.capacity() * BYTES_PER_FLOAT: 這個緩衝區因該包含的字節數
  • cubePositionsBuffer: 將要拷貝到這個頂點緩衝區對象的源
  • GL_STATIC_DRAW: 這個緩衝區不會動態更新

咱們對glVertexAttribPointer的調用看起來有點兒不一樣,由於最後一個參數如今是偏移量而不是指向客戶端內存的指針:

// 傳入位置信息
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mCubePositionsBufferIdx);
GLES20.glEnableVertexAttribArray(mPositionHandle);
mGlEs20.glVertexAttribPointer(mPositionHandle, POSITION_DATA_SIZE, GLES20.GL_FLOAT, false, 0, 0);
...
複製代碼

像之前同樣,咱們綁定到緩衝區,而後啓用頂點數組。因爲緩衝區早已綁定,當從緩衝區讀取數據時,咱們僅須要告訴OpenGL開始的偏移。由於咱們使用的特定的緩衝區,咱們傳入偏移量0。另請注意,咱們使用自定義綁定來調用glVertexAttribPointer,由於官方SKD缺乏此特定函數調用。

一旦咱們用緩衝區繪製完成,咱們應該解除它:

GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
複製代碼

當咱們不想在保留緩衝區時,咱們能夠釋放內存:

final int[] buffersToDelete = new int[] { mCubePositionsBufferIdx, mCubeNormalsBufferIdx,
    mCubeTexCoordsBufferIdx };
GLES20.glDeleteBuffers(buffersToDelete.length, buffersToDelete, 0);
複製代碼

打包頂點緩衝區對象

咱們還可使用單個緩衝區打包頂點緩衝區對象的全部頂點數據。打包頂點緩衝區的建立和上面相同,惟一的區別是咱們從打包客戶端緩衝區開始。打包緩衝區渲染也是同樣的,除了咱們須要傳偏移量,就像在客戶端內存中使用打包緩衝區同樣:

final int stride = (POSITION_DATA_SIZE + NORMAL_DATA_SIZE + TEXTURE_COORDINATE_DATA_SIZE)
    * BYTES_PER_FLOAT;

// 傳入位置信息
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mCubeBufferIdx);
GLES20.glEnableVertexAttribArray(mPositionHandle);
mGlEs20.glVertexAttribPointer(mPositionHandle, POSITION_DATA_SIZE,
    GLES20.GL_FLOAT, false, stride, 0);

// 傳入法線信息
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mCubeBufferIdx);
GLES20.glEnableVertexAttribArray(mNormalHandle);
mGlEs20.glVertexAttribPointer(mNormalHandle, NORMAL_DATA_SIZE,
    GLES20.GL_FLOAT, false, stride, POSITION_DATA_SIZE * BYTES_PER_FLOAT);
...
複製代碼

注意:偏移量須要以字節爲單位指定。與以前同樣解除綁定和刪除緩衝區的相同注意事項也適用。

將頂點數據放到一塊兒

這節課已構建了多立方體組成的立方體,每一個面的立方體數量體相同。它將在1x1x1立方體和16x16x16立方體之間構創建方體。因爲每一個立方體共享相同的法線和紋理數據,所以在初始化客戶端緩衝區時將重複複製此數據。全部立方體都將在同一個緩衝區對象中結束。

您能夠查看課程中的代碼並查看使用和不使用VBO,以及使用和不使用打包緩衝區進行渲染的示例。檢查代碼以查看如何處理一下某些操做:

  • 經過runOnUiThread()將事件從OpenGL線程發佈回UI主線程
  • 異步生成頂點數據
  • 處理內存溢出異常
  • 咱們移除了glEnable(GL_TEXTURE_2D)的調用,由於它實際在OpenGL ES 2是一個無效枚舉。這是之前的固定寫法延續下來的,在OpenGLES2中,這些東西由着色器處理,所以不須要使用glEnableglDisable
  • 怎樣使用不一樣的方式進行渲染,而不添加太多的if語句和條件。

進一步練習

您什麼時候使用頂點緩衝區?何時從客戶端內存傳輸數據更好?使用頂點緩衝區對象有哪些缺點?您將如何改進異步加載代碼?

教程目錄

打包教材

能夠在Github下載本課程源代碼:下載項目
本課的編譯版本也能夠再Android市場下:google play 下載apk
爲了方便你們下載,「我」也編譯了個apk,:github download

相關文章
相關標籤/搜索