Android OpenGL ES 2.0(七)--- 頂點緩衝區對象

原文鏈接:https://www.learnopengles.com/android-lesson-seven-an-introduction-to-vertex-buffer-objects-vbos/

本文從下面鏈接翻譯過來:

Android Lesson Seven: An Introduction to Vertex Buffer Objects (VBOs)

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

1.如何定義頂點緩衝區以及利用頂點緩衝區進行渲染。
2.使用單個緩衝區打包所有的數據(比如位置,顏色,
法線,紋理)與使用多個緩衝區分別保存數據的區別。
3.問題和陷阱,以及如何處理它們。

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

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

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

前提條件

請閱讀Android OpenGL ES 2.0(一)---入門,介紹如何從客戶端的內存上傳頂點數據。瞭解OpenGL ES如何與頂點數組一起工作對於理解本課至關重要。

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

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

本節課將使用四種不同的模式:

  • 客戶端,單獨的緩衝區
  • 客戶端,打包的緩衝
  • 頂點緩衝對象,單獨的緩衝區
  • 頂點緩衝對象,打包的緩衝

無論我們是否使用頂點緩衝對象,我們都需要先將我們的數據存儲在客戶端本地緩衝區。回顧第一課,OpenGL ES是一個本地用C語言實現的native系統庫,而java是運行在Android上的一個虛擬機中。爲了在虛擬機與Native系統之間搭建一座橋樑,我們需要使用一組特殊的緩衝區類來在native堆上分配內存,並使其可供OpenGL訪問:

// Java array.
float[] cubePositions;
...
// Floating-point buffer
final FloatBuffer cubePositionsBuffer;
...
 
// Allocate a direct block of memory on the native heap,
// size in bytes is equal to cubePositions.length * BYTES_PER_FLOAT.
// BYTES_PER_FLOAT is equal to 4, since a float is 32-bits, or 4 bytes.
cubePositionsBuffer = ByteBuffer.allocateDirect(cubePositions.length * BYTES_PER_FLOAT)
 
// Floats can be in big-endian or little-endian order.
// We want the same as the native platform.
.order(ByteOrder.nativeOrder())
 
// Give us a floating-point view on this byte buffer.
.asFloatBuffer();

將Java堆上數據轉換到native堆上,調用兩個方法就可以:

// Copy data from the Java heap to the native heap.
cubePositionsBuffer.put(cubePositions)
 
// Reset the buffer position to the beginning of the buffer.
.position(0);

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

一但數據存放到native堆上,我們就不需要持有float[]數組了,我們可以讓垃圾回收器清理它。

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

// Pass in the position information
GLES20.glEnableVertexAttribArray(mPositionHandle);
GLES20.glVertexAttribPointer(mPositionHandle, POSITION_DATA_SIZE,
    GLES20.GL_FLOAT, false, 0, mCubePositions);

glVertexAttribPointer方法的參數說明:

  • mPositionHandle: 着色器程序的位置屬性索引
  • POSITION_DATA_SIZE: 定義這個屬性需要多少個float元素
  • GL_FLOAT: 每個元素的類型
  • false: 定點數據因該標準化嗎?由於我們使用的是浮點數據,因此不適用。
  • 0: 跨度,設置0,表示位置應該按照順序讀取。
  • mCubePositions: 指向緩衝區的的指針,包含所有位置數據

使用打包緩衝區(Working with packed buffers)

使用打包緩衝區是非常相似的,替換了每個位置、法線等的緩衝區,現在一個緩衝區將包含所有這些數據。不同點看下面:

使用單緩衝區

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;
 
// Pass in the position information
mCubeBuffer.position(0);
GLES20.glEnableVertexAttribArray(mPositionHandle);
GLES20.glVertexAttribPointer(mPositionHandle, POSITION_DATA_SIZE,
    GLES20.GL_FLOAT, false, stride, mCubeBuffer);
 
// Pass in the normal information
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的跨度單位是字節,而不是元素,因此請記住進行轉換。

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

Dalvik和本地堆上的內存

如果在native堆上分配大量內存並將其釋放,則遲早會遇到OutOfMemoryError。 背後有幾個原因:

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

如何能避免這些問題?除了希望Google在未來的版本中改進Dalvik的行爲之外,並不多。或者通過本地代碼進行分配或預先分配一大塊內存來自行管理堆,並根據此分離緩衝區。

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

移動到頂點緩衝區對象

現在我們已經回顧了使用客戶端緩衝區,讓我們繼續討論頂點緩衝區對象!首先,我們需要回顧幾個非常重要的問題:

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

這似乎是一個明顯的觀點,但是它僅僅提醒你必須等到onSurfaceCreated()執行,並且你必須注意OpenGL ES調用是在GL線程上完成的。

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

當你使用頂點緩衝對象時,需要注意傳遞的數據。不當的值將會導致OpenGL ES系統庫或圖形驅動庫本地崩潰。在我的Nexus S上,一些遊戲完全卡在我的手機上或導致手機重啓,因爲在執行這些指令時圖形驅動崩潰了。並非所有的崩潰都會鎖定您的設備,但至少您不會看到“此應用已停止工作”的對話框。您的Activity將在沒有警告的情況下重新啓動,您唯一將獲得信息的方法可能是從日誌中的本地調試進行跟蹤。

將頂點數據上傳到GPU

要上傳數據到GPU,我們需要像以前一樣創建客戶端緩衝區的相同步驟:

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

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

// First, generate as many buffers as we need.
// This will give us the OpenGL handles for these buffers.
final int buffers[] = new int[3];
GLES20.glGenBuffers(3, buffers, 0);
 
// Bind to the buffer. Future commands will affect this buffer specifically.
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0]);
 
// Transfer data from client memory to the buffer.
// We can release the client memory after this call.
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, cubePositionsBuffer.capacity() * BYTES_PER_FLOAT,
    cubePositionsBuffer, GLES20.GL_STATIC_DRAW);
 
// IMPORTANT: Unbind from the buffer when we're done with it.
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);

一旦數據上傳到了OpenGL ES,我們就可以釋放這個客戶端內存,因爲我們不需要再繼續保留它。這是glBufferData的解釋:

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

我們對glVertexAttribPointer的調用看起來有點兒不同,因爲最後一個參數現在是偏移量而不是指向客戶端內存的指針:

// Pass in the position information
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,因爲官方SDK缺少此特定函數調用。

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

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;
 
// Pass in the position information
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mCubeBufferIdx);
GLES20.glEnableVertexAttribArray(mPositionHandle);
mGlEs20.glVertexAttribPointer(mPositionHandle, POSITION_DATA_SIZE, 
    GLES20.GL_FLOAT, false, stride, 0);
 
// Pass in the normal information
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 ES2.0是一個無效枚舉。這是以前的固定寫法延續下來的,在OpenGLES2.0中,這些東西由着色器處理,因此不需要使用glEnable或glDisable
  • 如何使用不同的路徑進行渲染,而不添加太多的if語句和條件。

進一步練習

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

可以從GitHub上的項目站點下載本課程的完整源代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章