天天看點

《OpenGL程式設計指南》一2.4 資料塊接口

本節書摘來自華章出版社《opengl程式設計指南》一書中的第2章,第2.4節,作者 bill licea-kane ,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視

着色器與應用程式之間,或者着色器各階段之間共享的變量可以組織為變量塊的形式,并且有的時候必須采用這種形式。uniform變量可以使用uniform塊,輸入和輸出變量可以使用in和out塊,着色器的存儲緩存可以使用buffer塊。

它們的形式都是類似的。首先了解一下uniform塊的寫法。

《OpenGL程式設計指南》一2.4 資料塊接口

各種類型的塊接口的詳細介紹如下文所示。綜合來說,塊(block)開始部分的名稱(上面的代碼中為b)對應于外部通路時的接口名稱,而結尾部分的名稱(上面的代碼中為name)用于在着色器代碼中通路具體成員變量。

如果着色器程式變得比較複雜,那麼其中用到的uniform變量的數量也會上升。通常會在多個着色器程式中用到同一個uniform變量。由于uniform變量的位置是着色器連結的時候産生的(也就是調用gllinkprogram()的時候),是以它在應用程式中獲得的索引可能會有變化,即使我們給uniform變量設定的值可能是完全相同的。而uniform緩存對象(uniform buffer object)就是一種優化uniform變量通路,以及在不同的着色器程式之間共享uniform資料的方法。

正如你所知道的,uniform變量是同時存在于使用者應用程式和着色器當中的,是以需要同時修改着色器的内容并調用opengl函數來設定uniform緩存對象。

通路一組uniform變量的方法是使用諸如glmapbuffer()的opengl函數(參見第3章),但是我們需要在着色器中對它們的聲明方式略作修改。不再分别聲明每個uniform變量,而是直接将它們成組,形成一個類似結構體的形式,也就是uniform塊。一個uniform塊需要使用關鍵字uniform指定。然後将塊中所有需要用到的變量包含在一對花括号當中,如例2.3所示。

例2.3 聲明一個uniform塊

《OpenGL程式設計指南》一2.4 資料塊接口

注意,着色器中的資料類型有兩種:不透明的和透明的;其中不透明類型包括采樣器、圖像和原子計數器。一個uniform塊中隻可以包含透明類型的變量。此外,uniform塊必須在全局作用域内聲明。

uniform塊的布局控制

在uniform塊中可以使用不同的限制符來設定變量的布局方式。這些限制符可以用來設定單個的uniform塊,也可以用來設定所有後繼uniform塊的排列方式(需要使用布局聲明)。可用的限制符及其介紹如表2-12所示。

《OpenGL程式設計指南》一2.4 資料塊接口

例如,如果需要共享一個uniform塊,并且使用行主序的方式來存儲資料,那麼可以使用下面的代碼來聲明它:

《OpenGL程式設計指南》一2.4 資料塊接口

多個限制符可以通過圓括号中的逗号來分隔。如果需要對所有後繼的uniform塊設定同一種布局,那麼可以使用下面的語句:

《OpenGL程式設計指南》一2.4 資料塊接口

這樣一來,目前行之後的所有uniform塊都會使用這種布局方式,除非再次改變全局的布局,或者對某個塊的聲明單獨設定專屬的布局方式。

通路uniform塊中聲明的uniform變量

雖然uniform塊已經命名了,但是塊中聲明的uniform變量并不會受到這個命名的限制。也就是說,uniform塊的名稱并不能作為uniform變量的父名稱,是以在兩個不同名的uniform塊中聲明同名變量會在編譯時造成錯誤。然而,在通路一個uniform變量的時候,也不一定非要使用塊的名稱。

uniform變量是着色器與應用程式之間共享資料的橋梁,是以如果着色器中的uniform變量是定義在命名的uniform塊中,那麼就有必要找到不同變量的偏移值。如果擷取了這些變量的具體位置,那麼就可以使用資料對它們進行初始化,這一過程與處理緩存對象(使用glbufferdata()等函數)是一緻的。

首先假設已知應用程式的着色器中uniform塊的名字。如果要對uniform塊中的uniform變量進行初始化,那麼第一步就是找到塊在着色器程式中的索引位置。可以調用glgetuniformblockindex()函數傳回對應的資訊,然後在應用程式的位址空間裡完成uniform變量的映射。

gluint glgetuniformblockindex(gluint program, const char * uniformblockname);

傳回program中名稱為uniformblockname的uniform塊的索引值。如果uniformblockname不是一個合法的uniform程式塊,那麼傳回gl_invalid_index。

如果要初始化uniform塊對應的緩存對象,那麼我們需要使用glbindbuffer()将緩存對象綁定到目标gl_uniform_buffer之上,如後文中的示例所示(第3章将會給出更詳細的解釋)。

當對緩存對象進行初始化之後,我們需要判斷命名的uniform塊中的變量總共占據了多大的空間。我們可以使用函數glgetactiveuniformblockiv()并且設定參數為gl_uniform_block_data_size,這樣就可以傳回編譯器配置設定的塊的大小(根據uniform塊的布局設定,編譯器可能會自動排除着色器中沒有用到的uniform變量)。glgetactiveuniformblockiv()函數還可以用來擷取一個命名的uniform塊的其他一些相關參數。

在擷取uniform塊的索引之後,我們需要将一個緩存對象與這個塊相關聯。最常見的方法是調用glbindbufferrange(),或者如果uniform塊是全部使用緩存來存儲的,那麼可以使用glbindbufferbase()。

void glbindbufferrange(glenum target, gluint index, gluint buffer, glintptr offset, glsizeiptr size);

void glbindbufferbase(glenum target, gluint index, gluint buffer);

将緩存對象buffer與索引為index的命名uniform塊關聯起來。target可以是gl_uniform_buffer(對于uniform塊)或者gl_transform_feedback_buffer(用于transform feedback,參見第5章)。index是uniform塊的索引。offset和size分别指定了uniform緩存映射的起始索引和大小。

調用glbindbufferbase()等價于調用glbindbufferrange()并設定offset為0,size為緩存對象的大小。

在下列情況下調用這兩個函數可能會産生opengl錯誤gl_invalid_value:size小于0;offset + size大于緩存大小;offset或size不是4的倍數;index小于0或者大于等于gl_max_uniform_buffer_bindings的傳回值。

當建立了命名uniform塊和緩存對象之間的關聯之後,隻要使用緩存相關的指令即可對塊内的資料進行初始化或者修改。

我們也可以直接設定某個命名uniform塊和緩存對象之間的綁定關系,也就是說,不使用連結器内部自動綁定塊對象并且查詢關聯結果的方式。如果多個着色器程式需要共享同一個uniform塊,那麼你可能需要用到這種方法。這樣可以避免對于不同的着色器程式同一個塊有不同的索引号。如果需要顯式地控制一個uniform塊的綁定方式,可以在調用gllinkprogram()之前調用gluniformblockbinding()函數。

glint gluniformblockbinding(gluint program, gluint uniformblockindex, gluint uniformblockbinding);

顯式地将塊uniformblockindex綁定到uniformblockbinding。

在一個命名的uniform塊中,uniform變量的布局是通過各種布局限制符在編譯和連結時控制的。如果使用了預設的布局方式,那麼需要判斷每個變量在uniform塊中的偏移量和資料存儲大小。為此,需要調用兩個指令:glgetuniformindices()負責擷取指定名稱uniform變量的索引位置,而glgetactiveuniformsiv()可以獲得指定索引位置的偏移量和大小,如例2.4所示。

void glgetuniformindices(gluint program, glsizei uniformcount, const char* uniformnames, gluint uniformindices);

傳回所有uniformcount個uniform變量的索引位置,變量的名稱通過字元串數組uniformnames來指定,程式傳回值儲存在數組uniformindices當中。在uniformnames中的每個名稱都是以null來結尾的,并且uniformnames和uniformindices的數組元素數都應該是uniformcount個。如果在uniformnames中給出的某個名稱不是目前啟用的uniform變量名稱,那麼uniformindices中對應的位置将會記錄為gl_invalid_index。

例2.4 初始化一個命名uniform塊中的uniform變量

《OpenGL程式設計指南》一2.4 資料塊接口
《OpenGL程式設計指南》一2.4 資料塊接口
《OpenGL程式設計指南》一2.4 資料塊接口
《OpenGL程式設計指南》一2.4 資料塊接口
《OpenGL程式設計指南》一2.4 資料塊接口
《OpenGL程式設計指南》一2.4 資料塊接口
《OpenGL程式設計指南》一2.4 資料塊接口

glsl中的buffer塊,或者對于應用程式而言,就是着色器的存儲緩存對象(shader storage buffer object),它的行為類似uniform塊。不過兩者之間有兩個決定性的差别,使得buffer塊的功能更為強大。首先,着色器可以寫入buffer塊,修改其中的内容并呈現給其他的着色器調用或者應用程式本身。其次,可以在渲染之前再決定它的大小,而不是編譯和連結的時候。例如:

《OpenGL程式設計指南》一2.4 資料塊接口

如果在着色器中沒有給出上面的數組的大小,那麼可以在應用程式中編譯和連接配接之後,渲染之前設定它的大小。着色器中可以通過length()方法擷取渲染時的數組大小。

着色器可以對buffer塊中的成員執行讀或寫操作。寫入操作對着色器存儲緩存對象的修改對于其他着色器調用都是可見的。這種特性對于計算着色器非常有意義,尤其是對非圖像的記憶體區域進行處理的時候。

有關buffer塊的記憶體限制符(例如coherent)以及原子操作的相關深入讨論請參見第11章。

設定着色器存儲緩存對象的方式與設定uniform緩存的方式類似,不過glbindbuffer()和glbufferdata()需要使用gl_shader_storage_buffer作為目标參數。我們可以在11.2節中看到一個更完整的例子。

如果你不需要寫入緩存中,那麼可以直接使用uniform塊,并且硬體裝置本身可能也沒有足夠的資源空間來支援buffer塊,但是uniform塊通常是足夠的。

着色器變量從一個階段輸出,然後再輸入到下一個階段中,這一過程可以使用塊接口來表示。使用邏輯上成組的方式來進行組織也更有利于判斷兩個階段的資料接口是否一緻,同樣對單獨程式的連結也會變得更為簡單。

例如,一個頂點着色器的輸出可能為:

《OpenGL程式設計指南》一2.4 資料塊接口

頂點着色器可以輸出材質和光照的資訊,并且都分成獨立的資料塊。opengl着色語言中内置的接口同樣也是以塊的方式存在的,例如gl_pervertex,其中包含了内置變量gl_position等資訊。我們可以在附錄c中找到一個完整的内置變量清單。