使用頂點數據的最佳做法

要使用OpenGL ES渲染框架,您的應用程序會配置圖形管道並提交要繪製的圖形基元。在某些應用程序中,所有基元都使用相同的流水線配置繪製;其他應用程序可以使用不同的技術來渲染框架的不同元素。但無論您在應用程序中使用哪些基元,或者如何配置管道,您的應用程序都會為OpenGL ES提供頂點。本章提供了頂點數據的刷新,並針對如何有效地處理頂點數據的目標建議進行跟蹤。

頂點由一個或多個屬性組成,例如位置,顏色,正常或紋理坐標。 OpenGL ES 2.0或3.0應用程序可以自由定義自己的屬性;頂點數據中的每個屬性對應於作為頂點着色器的輸入的屬性變量。 OpenGL 1.1應用程序使用固定功能管道定義的屬性。

您將屬性定義為由一到四個組件組成的向量。屬性中的所有組件共享一個公共數據類型。例如,顏色可能被定義為四個GLubyte組件(紅色,綠色,藍色,alpha)。當屬性加載到着色器變量中時,應用程序數據中未提供的任何組件將使用OpenGL ES的默認值填充。最後一個組件填充1,其他未指定組件填充0,如圖8-1所示。


8-1.png

您的應用程序可以將屬性配置為常量,這意味着與作為draw命令的一部分提交的所有頂點使用相同的值,或者數組,這意味着每個頂點都是該屬性的值。當您的應用程序在OpenGL ES中調用函數來繪製一組頂點時,頂點數據將從應用程序複製到圖形硬件。圖形硬件比頂點數據上的行為,處理着色器中的每個頂點,組裝基元並將其光柵化到幀緩衝區。 OpenGL ES的一個優點在於,它將一組功能標準化,將頂點數據提交給OpenGL ES,從而消除OpenGL提供的較舊且效率較低的機制。

必須提交大量基元來渲染框架的應用程序需要仔細管理其頂點數據以及如何將其提供給OpenGL ES。本章中描述的做法可以歸納為幾個基本原則:

  • 減小頂點數據的大小。
  • 減少OpenGL ES可以將頂點數據傳輸到圖形硬件之前必須進行的預處理
  • 減少將頂點數據複製到圖形硬件的時間
  • 減少對每個頂點執行的計算

簡化你的模型

基於iOS設備的圖形硬件非常強大,但它显示的圖像通常非常小。您不需要非常複雜的模型來在iOS上展示令人信服的圖形。減少用於繪製模型的頂點數量直接減少頂點數據的大小和對頂點數據執行的計算

您可以使用以下一些技術來降低模型的複雜性:

  • 以不同的細節級別提供模型的多個版本,並根據物體與相機的距離和显示器的尺寸在運行時選擇適當的模型
  • 使用紋理消除對某些頂點信息的需要。例如,可以使用凹凸貼圖將細節添加到模型中,而不添加更多的頂點數據
  • 一些模型添加頂點以改善照明細節或渲染質量。通常在光柵化階段為每個頂點計算值並在三角形內插插值時完成此操作。例如,如果您將聚光燈指向三角形的中心,則其效果可能不會被忽視,因為聚光燈中最亮的部分不會指向頂點。通過添加頂點,您可以提供額外的插值點,代價是增加頂點數據的大小和在模型上執行的計算。而不是添加額外的頂點,而是考慮將計算移動到管道的片段階段:
    • 如果您的應用程序使用OpenGL ES 2.0或更高版本,那麼您的應用程序會在頂點着色器中執行計算,並將其分配給變量。變化值由圖形硬件插值,並作為輸入傳遞給片段着色器。相反,將計算的輸入分配給變量,並在片段着色器中執行計算。這樣做會將從每個頂點成本執行該計算的成本更改為每個片段成本,從而降低頂點階段的壓力,並減少管道碎片階段的壓力。當您的應用程序在頂點處理中被阻止時,執行此操作,計算價格便宜,並且可以通過更改顯着減少頂點計數
    • 如果您的應用程序使用OpenGL ES 1.1,您可以使用DOT3照明來執行每個片段的照明。您可以通過添加凹凸貼圖紋理來保存正常信息,並使用GL_DOT3_RGB模式的紋理組合操作應用凹凸貼圖

避免在屬性數組中存儲常量

如果您的模型包含使用在整個模型中保持不變的數據的屬性,則不要為每個頂點複製該數據。 OpenGL ES 2.0和3.0應用程序可以設置不變的頂點屬性,也可以使用統一的着色器值來保存該值。 OpenGL ES 1.1應用程序應該使用諸如glColor4ub或glTexCoord2f的每頂點屬性函數。

使用最小可接受類型的屬性

指定每個屬性組件的大小時,請選擇提供可接受結果的最小數據類型。以下是一些準則:

  • 使用四個無符號字節組件(GL_UNSIGNED_BYTE)指定頂點顏色
  • 使用2或4個無符號字節(GL_UNSIGNED_BYTE)或無符號短(GL_UNSIGNED_SHORT)指定紋理坐標。不要將多組紋理坐標包裝到單個屬性中
  • 避免使用OpenGL ES GL_FIXED數據類型。它需要與GL_FLOAT相同的內存量,但提供較小的值範圍。所有iOS設備都支持硬件浮點數,因此可以更快地處理浮點值。
  • OpenGL ES 3.0上下文支持更廣泛的小數據類型,例如GL_HALF_FLOAT和GL_INT_2_10_10_10_REV。這些通常為諸如法線等屬性提供足夠的精度,內存佔用小於GL_FLOAT
  • 如果指定較小的組件,請確保重新排列頂點格式,以避免您的頂點數據錯位。請參閱避免不對齊的頂點數據。

使用交錯頂點數據

您可以將頂點數據指定為一系列數組(也稱為數組結構),也可以將數組指定為每個元素包含多個屬性(結構體數組)的數組。 iOS上的首選格式是具有單個交錯頂點格式的結構體數組。交錯數據為每個頂點提供更好的內存位置。


8-2.png

此規則的一個例外是當您的應用程序需要以不同於其餘頂點數據的速率更新某些頂點數據時,或者如果某些數據可以在兩個或多個模型之間共享。在任一情況下,您可能需要將屬性數據分成兩個或更多個結構。


8-3.png

避免不對齊的頂點數據

當您設計頂點結構時,將每個屬性的開始對齊到一個偏移量,該偏移量是其組件大小或4個字節的倍數,以較大者為準。當屬性不對齊時,iOS必須在將數據傳遞到圖形硬件之前執行其他處理。

在圖8-4中,位置和正常數據分別定義為三個短整數,總共六個字節。正常數據從偏移量6開始,這是本機大小(2字節)的倍數,但不是4字節的倍數。如果這個頂點數據被提交到iOS,iOS將不得不花費更多的時間在將數據傳遞到硬件之前複製和對齊數據。要解決這個問題,在每個屬性之後明確地添加兩個字節的填充


8-4.png

使用三角條批量頂點數據

使用三角形條可以顯着減少OpenGL ES必須在模型上執行的頂點計算數量。在圖8-5的左側,使用總共九個頂點指定三個三角形。 C,E和G實際上指定相同的頂點!通過將數據指定為三角形條,您可以將頂點數從9減少到5


8-5.png

有時,您的應用程序可以將多個三角形條組合成一個較大的三角形條。所有條帶必須共享相同的渲染要求。意即

  • 您必須使用相同的着色器來繪製所有的三角形條
  • 您必須能夠渲染所有的三角形條,而不會改變任何OpenGL狀態。
  • 三角形條必須共享相同的頂點屬性

要合併兩個三角形條,請複製第一個條帶的最後一個頂點和第二個條帶的第一個頂點,如圖8-6所示。當該條提交給OpenGL ES時,三角形DEE,EEF,EFF和FFG被認為是退化的,不進行處理或光柵化


8-6.png

為獲得最佳性能,您的型號應作為單個索引三角形條提交。為了避免在頂點緩衝區中多次指定相同頂點的數據,請使用單獨的索引緩衝區,並使用glDrawElements函數(或者glDrawElementsInstance或glDrawRangeElements函數(如果適用))繪製三角形條。

在OpenGL ES 3.0中,您可以使用原始重新啟動功能來合併三角形條,而不使用簡併三角形。啟用此功能后,OpenGL ES將索引緩衝區中最大可能的值視為完成一個三角形條並啟動另一個三角形條的命令。清單8-1显示了這種方法
Listing 8-1

// Prepare index buffer data (not shown: vertex buffer data, loading vertex and index buffers)

GLushort indexData[11] = {

0, 1, 2, 3, 4, // triangle strip ABCDE

0xFFFF, // primitive restart index (largest possible GLushort value)

5, 6, 7, 8, 9, // triangle strip FGHIJ

};

// Draw triangle strips

glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);

glDrawElements(GL_TRIANGLE_STRIP, 11, GL_UNSIGNED_SHORT, 0);

Where possible, sort vertex and index data so triangles that share common vertices are drawn reasonably close to each other in the triangle strip. Graphics hardware often caches recent vertex calculations to avoid recalculating a vertex.

在可能的情況下,對頂點和索引數據進行排序,因此共享共同頂點的三角形在三角形條中相互相似。圖形硬件通常會緩存最近的頂點計算,以避免重新計算頂點

使用頂點緩衝區對象來管理複製頂點數據

清單8-2提供了一個簡單應用程序可用於向頂點着色器提供位置和顏色數據的函數。它啟用兩個屬性並配置每個屬性以指向交錯頂點結構。最後,它調用glDrawElements函數將模型渲染為單個三角形條
Listing 8-2

typedef struct _vertexStruct
{
    GLfloat position[2];
    GLubyte color[4];
} vertexStruct;

void DrawModel()
{
    const vertexStruct vertices[] = {...};
    const GLubyte indices[] = {...};

    glVertexAttribPointer(GLKVertexAttribPosition, 2, GL_FLOAT, GL_FALSE,
        sizeof(vertexStruct), &vertices[0].position);
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glVertexAttribPointer(GLKVertexAttribColor, 4, GL_UNSIGNED_BYTE, GL_TRUE,
        sizeof(vertexStruct), &vertices[0].color);
    glEnableVertexAttribArray(GLKVertexAttribColor);

    glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, indices);
}

此代碼有效,但效率低下。每次調用DrawModel時,將索引和頂點數據複製到OpenGL ES中,並傳輸到圖形硬件。如果頂點數據在調用之間沒有變化,則這些不必要的副本可能會影響性能。為避免不必要的副本,您的應用程序應將其頂點數據存儲在頂點緩衝對象(VBO)中。由於OpenGL ES擁有頂點緩衝區對象的內存,因此可將緩衝區存儲在圖形硬件更易於訪問的內存中,或將數據預處理為圖形硬件的首選格式

Note:當在OpenGL ES 3.0中使用頂點數組對象時,還必須使用頂點緩衝對象

清單8-3創建了一對頂點緩衝對象,一個用於保存頂點數據,另一個用於條帶索引。在每種情況下,代碼生成一個新對象,將其綁定為當前緩衝區,並填充緩衝區。當應用程序初始化時,將調用CreateVertexBuffers
Listing 8-3 Creating a vertex buffer object

GLuint    vertexBuffer;
GLuint    indexBuffer;
void CreateVertexBuffers()
{

    glGenBuffers(1, &vertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glGenBuffers(1, &indexBuffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

}

清單8-4將列表8-2修改為使用頂點緩衝對象。清單8-4的主要區別在於glVertexAttribPointer函數的參數不再指向頂點數組。相反,每個都是頂點緩衝區對象的偏移量。

Listing 8-4用頂點緩衝區對象繪製

void DrawModelUsingVertexBuffers()
{
    glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
    glVertexAttribPointer(GLKVertexAttribPosition, 2, GL_FLOAT, GL_FALSE,
        sizeof(vertexStruct), (void *)offsetof(vertexStruct, position));
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glVertexAttribPointer(GLKVertexAttribColor, 4, GL_UNSIGNED_BYTE, GL_TRUE,
        sizeof(vertexStruct), (void *)offsetof(vertexStruct, color));
    glEnableVertexAttribArray(GLKVertexAttribColor);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
    glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, (void*)0);
}

緩衝區使用提示

上一個例子初始化了頂點緩衝區一次,之後從不改變它的內容。您可以更改頂點緩衝區的內容。頂點緩衝對象設計的關鍵部分是應用程序可以通知OpenGL ES如何使用緩衝區中存儲的數據。 OpenGL ES實現可以使用這個提示來改變它用於存儲頂點數據的策略。在清單8-3中,對glBufferData函數的每個調用都提供了一個使用提示作為最後一個參數。將GL_STATIC_DRAW傳遞給glBufferData告訴OpenGL ES,這兩個緩衝區的內容從來不會改變,這給了OpenGL ES更多機會來優化數據存儲的方式和位置

OpenGL ES規範定義了以下用例

  • GL_STATIC_DRAW用於渲染多次的頂點緩衝區,其內容被指定一次,永不改變。
  • GL_DYNAMIC_DRAW用於渲染多次的頂點緩衝區,其內容在渲染循環期間發生變化。
  • GL_STREAM_DRAW用於渲染少量次數然後被丟棄的頂點緩衝區。

在iOS中,GL_DYNAMIC_DRAW和GL_STREAM_DRAW是等效的。您可以使用glBufferSubData函數更新緩衝區內容,但這樣做會導致性能損失,因為它會刷新命令緩衝區並等待所有命令完成。雙重或三重緩衝可以降低這種性能成本。 (請參閱使用雙緩衝來避免資源衝突。)為獲得更好的性能,請在OpenGL ES 3.0中使用glMapBufferRange函數或OpenGL ES 2.0或1.1中由EXT_map_buffer_range擴展提供的相應函數。

如果您的頂點格式中的不同屬性需要不同的使用模式,則將頂點數據拆分為多個結構,併為共享常見使用特徵的每個屬性集合分配單獨的頂點緩衝對象。清單8-5修改了上一個示例,以使用單獨的緩衝區來保存顏色數據。通過使用GL_DYNAMIC_DRAW提示分配顏色緩衝區,OpenGL ES可以分配該緩衝區,使您的應用程序保持合理的性能
Listing 8-5繪製具有多個頂點緩衝對象的模型

typedef struct _vertexStatic
{
    GLfloat position[2];
} vertexStatic;

typedef struct _vertexDynamic
{
    GLubyte color[4];
} vertexDynamic;

// Separate buffers for static and dynamic data.
GLuint    staticBuffer;
GLuint    dynamicBuffer;
GLuint    indexBuffer;

const vertexStatic staticVertexData[] = {...};
vertexDynamic dynamicVertexData[] = {...};
const GLubyte indices[] = {...};

void CreateBuffers()
{
// Static position data
    glGenBuffers(1, &staticBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, staticBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(staticVertexData), staticVertexData, GL_STATIC_DRAW);

// Dynamic color data
// While not shown here, the expectation is that the data in this buffer changes between frames.
    glGenBuffers(1, &dynamicBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, dynamicBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(dynamicVertexData), dynamicVertexData, GL_DYNAMIC_DRAW);

// Static index data
    glGenBuffers(1, &indexBuffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
}

void DrawModelUsingMultipleVertexBuffers()
{
    glBindBuffer(GL_ARRAY_BUFFER, staticBuffer);
    glVertexAttribPointer(GLKVertexAttribPosition, 2, GL_FLOAT, GL_FALSE,
        sizeof(vertexStruct), (void *)offsetof(vertexStruct, position));
    glEnableVertexAttribArray(GLKVertexAttribPosition);

    glBindBuffer(GL_ARRAY_BUFFER, dynamicBuffer);
    glVertexAttribPointer(GLKVertexAttribColor, 4, GL_UNSIGNED_BYTE, GL_TRUE,
        sizeof(vertexStruct), (void *)offsetof(vertexStruct, color));
    glEnableVertexAttribArray(GLKVertexAttribColor);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
    glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(GLubyte), GL_UNSIGNED_BYTE, (void*)0);
}

使用頂點數組對象合併頂點數組狀態更改

仔細看看清單8-5中的DrawModelUsingMultipleVertexBuffers函數。它支持許多屬性,綁定多個頂點緩衝區對象,並配置屬性以指向緩衝區。所有這些初始化代碼基本上是靜態的;沒有一個參數在幀間變化。如果每次應用程序呈現一個幀時都調用此函數,則會重新配置圖形管道,導致了很多不必要的開銷。如果應用程序繪製了許多不同類型的模型,則重新配置管道可能會成為瓶頸。相反,使用頂點數組對象來存儲完整的屬性配置。頂點數組對象是核心OpenGL ES 3.0規範的一部分,可通過OES_vertex_array_object擴展在OpenGL ES 2.0和1.1中提供

圖8-7显示了具有兩個頂點數組對象的示例配置。每個配置獨立於另一個;每個頂點數組對象可以引用一組不同的頂點屬性,它們可以存儲在同一個頂點緩衝區對象中,或者跨越多個頂點緩衝對象


8-7.png

清單8-6提供了用於配置上面显示的第一個頂點數組對象的代碼。它生成新的頂點數組對象的標識符,然後將頂點數組對象綁定到上下文。之後,它會調用配置頂點屬性,如果代碼沒有使用頂點數組對象。配置存儲到綁定的頂點數組對象而不是上下文
Listing 8-6 配置頂點數組對象

void ConfigureVertexArrayObject()
{
    // Create and bind the vertex array object.
    glGenVertexArrays(1,&vao1);
    glBindVertexArray(vao1);
         // Configure the attributes in the VAO.
    glBindBuffer(GL_ARRAY_BUFFER, vbo1);
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE,
        sizeof(staticFmt), (void*)offsetof(staticFmt,position));
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_UNSIGNED_SHORT, GL_TRUE,
        sizeof(staticFmt), (void*)offsetof(staticFmt,texcoord));
    glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
    glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE,
        sizeof(staticFmt), (void*)offsetof(staticFmt,normal));
    glEnableVertexAttribArray(GLKVertexAttribNormal);

    glBindBuffer(GL_ARRAY_BUFFER, vbo2);
    glVertexAttribPointer(GLKVertexAttribColor, 4, GL_UNSIGNED_BYTE, GL_TRUE,
        sizeof(dynamicFmt), (void*)offsetof(dynamicFmt,color));
    glEnableVertexAttribArray(GLKVertexAttribColor);

    // Bind back to the default state.
    glBindBuffer(GL_ARRAY_BUFFER,0);
    glBindVertexArray(0); 
}

要繪製,代碼綁定頂點數組對象,然後像以前一樣提交繪圖命令

注意:在OpenGL ES 3.0中,不允許頂點數組數據的客戶端存儲 - 頂點數組對象必須使用頂點緩衝對象

為了獲得最佳性能,您的應用程序應該配置每個頂點數組對象一次,並且不要在運行時更改它。如果需要在每個幀中更改頂點數組對象,則可以創建多個頂點數組對象。例如,使用雙緩衝的應用程序可能會為奇數幀配置一組頂點數組對象,併為偶數幀配置第二組。每組頂點數組對象將指向用於呈現該幀的頂點緩衝區對象。當頂點數組對象的配置不會改變時,OpenGL ES可以緩存有關頂點格式的信息,並改進如何處理這些頂點屬性

將緩衝區映射到客戶端內存以實現快速更新

OpenGL ES應用程序設計中更具挑戰性的問題之一是使用動態資源,特別是如果您的頂點數據需要更改每個幀。高效地平衡CPU和GPU之間的并行性需要仔細地管理應用程序內存空間和OpenGL ES內存之間的數據傳輸。傳統的技術,如使用glBufferSubData功能,可以降低性能,因為它們會迫使GPU在傳輸數據時等待,即使可能會從相同緩衝區中的其他位置的數據渲染

例如,您可能需要修改頂點緩衝區並在每次通過高幀率渲染循環時繪製其內容。渲染的最後一幀的繪圖命令可能仍在使用GPU,而CPU正在嘗試訪問緩衝存儲器以準備繪製下一幀,導致緩衝區更新調用阻止進一步的CPU工作,直到GPU完成。您可以通過手動將CPU和GPU訪問同步到緩衝區來提高這種情況下的性能

glMapBufferRange函數提供了一種更有效的方法來動態更新頂點緩衝區。 (此功能可用作OpenGL ES 3.0中的核心API,並通過OpenGL ES 1.1和2.0中的EXT_map_buffer_range擴展名使用)。使用此函數可獲取指向OpenGL ES內存區域的指針,然後可以使用該指針寫入新數據。 glMapBufferRange函數允許將緩衝區數據存儲的任何子範圍映射到客戶端內存中。它還支持使用該功能與OpenGL同步對象時進行異步緩衝區修改的提示,如清單8-7所示
Listing8-7用手動同步動態更新頂點緩衝區

GLsync fence;
GLboolean UpdateAndDraw(GLuint vbo, GLuint offset, GLuint length, void *data) {
    GLboolean success;

    // Bind and map buffer.
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    void *old_data = glMapBufferRange(GL_ARRAY_BUFFER, offset, length,
        GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT |
        GL_MAP_UNSYNCHRONIZED_BIT );

    // Wait for fence (set below) before modifying buffer.
    glClientWaitSync(fence, GL_SYNC_FLUSH_COMMANDS_BIT,
        GL_TIMEOUT_IGNORED);

    // Modify buffer, flush, and unmap.
    memcpy(old_data, data, length);
    glFlushMappedBufferRange(GL_ARRAY_BUFFER, offset, length);
    success = glUnmapBuffer(GL_ARRAY_BUFFER);

    // Issue other OpenGL ES commands that use other ranges of the VBO's data.

    // Issue draw commands that use this range of the VBO's data.
    DrawMyVBO(vbo);

    // Create a fence that the next frame will wait for.
    fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
    return success;
}

此示例中的UpdateAndDraw函數使用glFenceSync函數在提交使用特定緩衝區對象的繪圖命令之後立即建立同步點或柵欄。然後使用glClientWaitSync函數(在下一遍遍歷渲染循環中)來檢查該同步點,然後再修改緩衝區對象。如果這些繪圖命令在渲染循環回來之前在GPU上完成執行,則CPU執行不會阻止,並且UpdateAndDraw函數繼續修改緩衝區並繪製下一個幀。如果GPU尚未完成這些命令,則glClientWaitSync功能會阻止進一步的CPU執行,直到GPU到達柵欄。通過手動將同步點放置在代碼周圍的潛在資源衝突周圍,可以最大限度地減少CPU等待GPU的時間