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

奏響曲發表於2019-03-24

翻譯文

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


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

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

1.怎樣用頂點緩衝物件定義和渲染
2.單個緩衝區、所有資料打包進去、多個緩衝區之間的區別
3.問題和陷阱我們如何取處理它們
screenshot

什麼是頂點緩衝區物件?為什麼使用它們?

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

我們能做些什麼呢?我們可以使用頂點緩衝物件,而不是每幀從客戶端記憶體傳輸頂點資訊,資訊將被傳輸一次,然後渲染器將從該圖形儲存器快取中得到資料。

前提條件

請閱讀OpenGL Android課程一:入門介紹如何從客戶端的記憶體上傳頂點資料。瞭解OpenGL ES如何與頂點陣列一起工作對於理解本課至關重要。

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

一但瞭解瞭如何使用客戶端記憶體進行渲染,切換到使用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呼叫。當我們使用打包的緩衝作業時,這將派上用場。

一但資料存放到本地堆上,我們就不需要長時間持有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

相關文章