第07章 輸入\輸出與語義綁定
我們最大的弱點在于放棄。成功的必然之路就是不斷的重來一次。
------ 托馬斯·愛迪生
第三章從 GPU 運行原理和數(shù)據(jù)流程的角度闡述了頂點著色程序和片段著色程序的輸入輸出,即,應(yīng)用程序(宿主程序)將圖元信息(頂點位置、法向量、紋理坐標(biāo)等)傳遞頂點著色程序;頂點著色程序基于圖元信息進行坐標(biāo)空間轉(zhuǎn)換,運算得到的數(shù)據(jù)傳遞到片段著色程序中;片段著色程序還可以接受從應(yīng)用程序中傳遞的紋理信息,將這些信息綜合起來計算每個片段的顏色值,最后將這些顏色輸送到真緩沖區(qū)(或顏色緩沖區(qū))中。
這些是頂點著色程序和片段著色程序的基本功能和數(shù)據(jù)輸入輸出,實際上現(xiàn)在著色程序已經(jīng)可以接受多種數(shù)據(jù)類型,并靈活的進行各種算法的處理,如,可以接受光源信息(光源位置、強度等)、材質(zhì)信息(反射系數(shù)、折射系數(shù)等)、運動控制信息(紋理投影矩陣、頂點運動矩陣等),可以在頂點程序中計算光線的折射方向,并傳遞到片段程序中進行光照計算。
這一章節(jié)中,我們將講解 Cg 語言通過何種機制確定數(shù)據(jù)類型和傳遞形式。讀者要抱著如下幾個問題閱讀本章節(jié):
1. 從應(yīng)用程序傳遞到 GPU 的數(shù)據(jù),分為圖元信息數(shù)據(jù)(在 GPU 處理的基本數(shù)據(jù)如頂點位置信息等)和其他的離散數(shù)據(jù)(在 GPU 運行流程中不會發(fā)生變化,如材質(zhì)對光的反射、折射信息),這兩種輸入數(shù)據(jù)如何區(qū)分?
2. 從應(yīng)用程序傳遞到 GPU 中的圖元信息如何區(qū)分類型,即,頂點程序怎么知道一個數(shù)據(jù)是位置數(shù)據(jù),而不是法向量數(shù)據(jù)?
3. 頂點著色程序與片段著色程序之間的數(shù)據(jù)傳遞如何進行?
7.1 Cg 關(guān)鍵字
關(guān)鍵字是語言本身所保留的一個字符串集合,用于代表特定的含義,如前面所講到的數(shù)據(jù)類型關(guān)鍵字 int 、float 等,以及結(jié)構(gòu)體關(guān)鍵字 struct。 Cg 中的關(guān)鍵字很多都是照搬 C\C++ 中的關(guān)鍵字,不過 Cg 中也創(chuàng)造了一系列獨特的關(guān)鍵字,這些關(guān)鍵字不但用于指定輸入圖元的數(shù)據(jù)含義(是位置信息,還是法向量信息),本質(zhì)也則對應(yīng)著這些圖元數(shù)據(jù)存放的硬件資源(寄存器或者紋理),稱之為語義詞(Semantics),通常也根據(jù)其用法稱之為綁定語義詞(binding semantics)。
除語義詞外,Cg 中還提供了三個關(guān)鍵字,in、out、inout,用于表示函數(shù)的輸入?yún)?shù)的傳遞方式,稱為輸入\輸出關(guān)鍵字,這組關(guān)鍵字可以和語義詞合用表達硬件上不同的存儲位置,即同一個語義詞,使用 in 關(guān)鍵字修飾和 out 關(guān)鍵詞修飾,表示的圖形硬件上不同的寄存器。
Cg 語言還提供兩個修飾符:uniform,用于指定變量的數(shù)據(jù)初始化方式;const 關(guān)鍵字的含義與 C\C++ 中相同,表示被修飾變量為常量變量。
下面將分別對上述的關(guān)鍵字進行詳細闡述。這一章非常關(guān)鍵,尤其是語義詞的使用方法和含義,再小的 Cg 程序都需要使用到語義詞。
7.2 uniform
Cg 語言輸入數(shù)據(jù)流分為兩類(參見文獻【3】 Program Inputs and Ooutputs):
1. Varying inputs, 即數(shù)據(jù)流輸入圖元信息的各種組成要素。從應(yīng)用程序輸入到 GPU 的數(shù)據(jù)除了頂點位置數(shù)據(jù),還有頂點的法向量數(shù)據(jù),紋理坐標(biāo)數(shù)據(jù)等。Cg 語言提供了一組語義詞,用以表明參數(shù)是由頂點的哪些數(shù)據(jù)初始化的。
2. Uniform inputs,表示一些與三維渲染有關(guān)的離散信息數(shù)據(jù),這些數(shù)據(jù)通常由應(yīng)用程序傳入,并通常不會隨著圖元信息的變化而變化,如材質(zhì)對光的反射信息、運動矩陣等。Uniform 修飾一個參數(shù),表示該參數(shù)的值由外部應(yīng)用程序初始化并傳入;例如在參數(shù)列表中寫:
uniform float brightness,
uniform float4x4 modelWorldProject
表示從“外部”傳入一個 float 類型數(shù)據(jù),和一個 4 階矩陣。“外部”的含義通常是用 OpenGL 或者 DirectX 所編寫的應(yīng)用程序。
使用 Uniform 修飾的變量,除了數(shù)據(jù)來源不同外,與其他變量我是完全一樣的。
需要注意的一點是:uniform 修飾的變量的值是從外部傳入的,所以在 Cg 程序(頂點程序和片段程序)中通常使用 uniform 參數(shù)修飾函數(shù)形參,不容許聲明一個用 uniform 修飾的局部變量!否則編譯時會出現(xiàn)錯誤提示信息:
Error C5056:‘uniform’ not allowed on local variable
7.3 const
Cg 語言也提供 const 修飾符,與 C\C++ 中含義一樣,被 const 所修飾的變量在初始化之后不能再去改變它的值。下面的例子程序中有一個聲明為 const 的變量被賦值修改:
const float a = 1.0;
a = 2.0; // 錯誤
float b = a++; // 錯誤
編譯時會出現(xiàn)錯誤提示信息:
error C1026: assignment to const variable.
const 修飾符與 uniform 修飾符是相互獨立的,對一個變量既可以單獨使用 const 或者 uniform,也可以同時使用。
7.4 輸入\輸出修飾符(in\out\inout)
參數(shù)傳遞是指:函數(shù)調(diào)用實參值初始化函數(shù)硬形參的過程。在 C\C++ 中,根據(jù)形參值的改變是否會導(dǎo)致實參值的改變,參數(shù)傳遞分為“值傳遞(pass-by-value)”和“引用傳遞(pass-by-reference)”。按值傳遞時,函數(shù)不會訪問當(dāng)前調(diào)用的實參,函數(shù)體處理的是實參的拷貝,也就是形參,所以形參值的改變不會影響是實參值;引用傳遞時,函數(shù)接收的是實參的存放地址,函數(shù)體中改變的是實參的值。C\C++ 采取指針機制構(gòu)建引用傳遞,所以通常引用傳遞也稱為“指針傳遞”。
Cg 語言中參數(shù)傳遞方式同樣分為“值傳遞”和“引用傳遞”,但指針機制并不被 GPU 硬件所支持,所以 Cg 語言采用不同的語法修飾符來區(qū)別“值傳遞”和“引用傳遞”。這些修飾符分別為:
1. in: 修飾一個形參只是用于輸入,進入函數(shù)體時被初始化,且該形參值的改變不會影響實參值,這是典型的值傳遞方式。
2. out: 修飾一個形參只是用于輸出的,進入函數(shù)體時并沒有被初始化,這種類型的形參一般是一個函數(shù)的運行結(jié)果。
3. inout: 修飾一個形參既用于輸入也用于輸出,這是典型的引用傳遞。
舉例如下:
void muFunc(out float x); // 形參x, 只用于輸出
void muFunc(inout float x); // 形參x, 既用于輸入時初始化,也用于輸出數(shù)據(jù)
void muFunc(in float x); // 形參x, 只用于輸入
void muFunc(float x); // 等價與 in float x,這種用法和C\C++完全一致
也可以使用 return 語句來代替 out 修飾符的使用。輸入\輸出修飾符通常和語義詞一起使用,表示頂點著色程序和片段著色程序的輸入輸出。
7.5 語義詞(Semantic)與語義綁定(Binding Semantics)
語義詞,表示輸入圖元的數(shù)據(jù)含義(是位置信息們還是法向量信息),也表明這些圖元數(shù)據(jù)存放的硬件資源(寄存器或者紋理緩沖區(qū))。頂點著色程序和片段著色程序中 Varying inputs 類型的輸入,必須和一個語義詞相綁定,這稱之為綁定語義(Binding semantics)。
7.5.1 輸入語義與輸出語義的區(qū)別
語義概念的提出和圖形流水線工作機制大有關(guān)系。從前面所講的 GPU 處理流程中可以看出,一個階段處理數(shù)據(jù),然后傳輸給下一個階段,那么每個階段之間的接口是如何確定的呢?例如:頂點處理器的輸入數(shù)據(jù)是處于模型空間的頂點數(shù)據(jù)(位置、法向量),輸出的是投影坐標(biāo)和光照顏色;片段處理器要講光照顏色做為輸入,問題是“片段處理器怎么知道光照顏色值的存放位置”?
在高級語言中(C\C++),數(shù)據(jù)從接口的一端流向另一端,是因為提供了數(shù)據(jù)存放的內(nèi)存位置(通常是指針信息);由于 Cg 語言并不支持指針機制,且圖形硬件處理過程中,數(shù)據(jù)通常暫存在寄存器中,故而在 Cg 語言中,通過引入語義綁定(binding semantics)機制,指定數(shù)據(jù)存放的位置,實際上就是將輸入\輸出數(shù)據(jù)和寄存器做一個映射關(guān)系(在 OpenGL Cg Profiles 中是這樣的,但在 DirectX-based Cg profiles 中則并沒有這種映射關(guān)系)。根據(jù)輸入語義,圖形處理器從某個寄存器取數(shù)據(jù);然后再將處理好的數(shù)據(jù),根據(jù)輸出語義,放到指定的寄存器。
記住這一點:語義,是兩個處理階段(頂點序、片段程序)之間的輸入\輸出數(shù)據(jù)和寄存器之間的橋梁,同時語義通常也表示數(shù)據(jù)的含義,如 POSITION 一般表示參數(shù)中存放的數(shù)據(jù)是頂點位置。
語義,分為輸入語義和輸出語義;輸入語義和輸出語義是有區(qū)別的。雖然一些參數(shù)經(jīng)常會使用相同的綁定語義詞,例如:頂點 Shader 的輸入?yún)?shù),POSITION 指應(yīng)用程序傳入的頂點位置,而輸出參數(shù)使用 POSITION 語義就表示要反饋給硬件光柵化的裁剪空間位置,光柵化把 POSITION 當(dāng)成一個位置信息。雖然兩個語義命名為 POSITION,但卻對應(yīng)著圖形流水線上不同的寄存器。
| 說明:在 OpenGL Cg Profiles 中,語義綁定指定了輸入\輸出數(shù)據(jù)和圖形硬件寄存器之間的對應(yīng)關(guān)系;但是在 DirectX Cg Profiles 中,則并非如此。在文獻【3】的第 25 頁寫到: In the OpenGL Cg Profiles, binding semantics implicitly specify the mapping of varying inputs to particular hardware. Howerver, in DirectX based Cg profiles there is no such implied mapping. 在文獻【3】的第 260 頁寫到: Often, these predefined names correspond to the names of hardware registers or API respurces. |
|---|
7.5.2 頂點著色程序的輸入語義
圖 13 所示的這組綁定語義關(guān)鍵字被 Cg 語言的所有 Vertex Profiles 所支持,一些 Profiles 支持額外的語義詞。

語義詞 POSITION 等價與 POSITION,其他的語義詞也有類似的等價關(guān)系。為了說明語義詞的含義,舉例如下:
int float modelPos:POSITION
表示該參數(shù)中的數(shù)據(jù)是頂點位置坐標(biāo)(通常位于模型空間),屬于輸入?yún)?shù),語義詞 POSITION 是輸入語義,如果在 OpenGL 中則對應(yīng)為接受應(yīng)用程序傳遞的頂點數(shù)據(jù)的寄存器(圖形硬件上)。
int float4 modelNormal:NORMAL
表示該參數(shù)中的數(shù)據(jù)是頂點法向量坐標(biāo)(通常位于模型空間),屬于輸入?yún)?shù),語義詞 NORMAL 是輸入語義,如果在 OpenGL 中則對應(yīng)為接受應(yīng)用程序傳遞的頂點法向量的寄存器(圖形硬件上)。
注意,上面的參數(shù)都被聲明為四元向量,通常我們在應(yīng)用程序涉及的頂點位置和法向量都是三元向量,至于為什么要講三元向量變?yōu)樗脑蛄?,又稱齊次坐標(biāo),具體請看附錄 A。頂點位置坐標(biāo)傳入頂點著色程序中轉(zhuǎn)化為四元向量,最后一元數(shù)據(jù)為 1,而頂點法向量傳入頂點著色程序中轉(zhuǎn)化為四元向量,最后一元數(shù)據(jù)為 0。
7.5.3 頂點著色程序的輸出語義
頂點程序的輸出數(shù)據(jù)被傳入到片段程序中,所以頂點著色程序的輸出語義詞,通常也是片段程序的輸入語義詞,不過語義詞 POSITION 除外。
下面這些語義詞使用于所有的 Cg Vertex Profiles 作為輸出語義和 Cg Fragment Profiles 的輸入語義:POSITION,PSIZE,F(xiàn)OG,COLOR0-COLOR1,TEXCOORD0-TEXCOORD7。
頂點著色程序必須聲明一個輸出變量,并綁定 POSITION 語義詞,該變量中的數(shù)據(jù)將被用于,且只被用于光柵化!如果沒有聲明一個綁定 POSITION 語義詞的輸出變量,如下所示的代碼:
void main_v(float4 position:POSITION, /*out float4 oposition:POSITION,*/ uniform float4x4 modelViewProj)
{
//oposition = mul(modelViewProj, position);
}
在使用 vp20 和 vp30 編譯時會提示錯誤信息:error C6014: Required output 'HPOS' not written. 在使用 vs_2_0 和 vs_3_0 編譯時會提示錯誤信息:error C6014:Required output 'POSITION' not written.
為了保持頂點程序輸出語義和片段程序輸入語義的一致性,通常使用相同的 struct 類型數(shù)據(jù)作為兩者之間的傳遞,這是一種非常方便的寫法,推薦使用。例如:
struct VertexIn
{
float4 position:POSITION;
float4 normal:NORMAL;
};
struct VertexScreen
{
float4 oPosition:POSITION;
float4 objectPos:TEXCOORD0;
float4 objectNormal:TEXCOORD1;
};
注意:當(dāng)使用 struct 結(jié)構(gòu)中的成員變量綁定語義時,需要注意到頂點著色程序中使用的 POSITION 語義詞,是不會被片段程序所使用的。
如果需要從頂點著色程序向片段程序傳遞數(shù)據(jù),例如頂點投影坐標(biāo)、光照信息等,則可以聲明另外的參數(shù),綁定到 TEXCOORD 系列的語義詞進行數(shù)據(jù)傳遞,實際上 TEXCOORD 系列的語義詞通常被用于從頂點程序向片段程序之間傳遞數(shù)據(jù)。
當(dāng)然,你也可以選擇不使用 struct 結(jié)構(gòu),而直接在函數(shù)形參中進行語義綁定。無論使用何種方式,都要記住 Vertex Program 中的綁定語義(POSITION 除外)的輸出形參的數(shù)據(jù)會傳遞到 Fragment Program 中綁定相同語義的輸入形參中。
7.5.4 片段著色程序的輸出語義
片段著色程序的輸出語義詞較少,通常是 COLOR。 這是因為片段著色程序運行完畢后,就基本到了 GPU 流水線的末端了。片段程序必須聲明一個 out 向量(三元或者四元),綁定語義詞 COLOR,這個值將被用做該片段的最終顏色值。例如:
void main_v(out float4 color:COLOR)
{
color.xyz = float3(1.0, 1.0, 1.0);
color.w = 1,0;
}
一些 fragment profiles 支持輸出語義詞 DEPTH,與它綁定的輸出變量會設(shè)置片段的深度值;還有一些支持額外的顏色輸出,可以用于多渲染目標(biāo)(multiple render targets, MRTs)。
和頂點著色程序一樣,片段著色程序也可以將輸出對象放入一個結(jié)構(gòu)體中。不過,這種做法未必方便,理由是:片段著色程序的輸出對象少,最常用的就是顏色值(綁定輸出語義詞 COLOR),單獨的一個向量沒有必要放到結(jié)構(gòu)體中。而頂點著色程序輸出的對象很多,在有些光照或陰影計算中,往往要輸出頂點的世界坐標(biāo)、法向量、光的反射方向、折射方向、投影紋理坐標(biāo)等數(shù)據(jù),這些數(shù)據(jù)統(tǒng)一放到結(jié)構(gòu)體中方便管理。
7.5.5 語義綁定方法
入口函數(shù)輸入\輸出數(shù)據(jù)的綁定語義有4 種方法(文獻【3】第 260 頁)
1. 綁定語義放在函數(shù)的參數(shù)列表的參數(shù)聲明后面中:
[const][in|out|inout]<type><identifier>[:<binding-semantic>][=<initializer>]
其中,const 作為可選項,修飾形參數(shù)據(jù);in、out、inout 作為可選項,說明數(shù)據(jù)的類型;identifier 是必選項,形參變量名;一個冒號“:”加上一個綁定語義,是可選項;最后是初始化參數(shù),是可選項。如下代碼所示。形參列表中的參數(shù)一、參數(shù)而綁定到輸入語義;參數(shù)三、參數(shù)四綁定到輸出語義;盡管參數(shù)一和參數(shù)三的綁定語義詞一樣,但前者是輸入語義,后者是輸出語義,所以這兩個參數(shù)數(shù)據(jù)所對應(yīng)的硬件位置視不一樣的。
void main_v(float4 positon_obj:POSITION, float3 normal_obj:NORMAL,
out float4 oPosition:POSITION, out float4 oColor:COLOR,
uniform float4x4 modelViewProj)
{
...............
}
2. 綁定語義可以放在結(jié)構(gòu)體(struct)的成員變量后面:
struct<struct-tag>
{
<type><identifier>[:<binding-semantic>];
}
舉例如下,結(jié)構(gòu) C2Elv_Outpu 中的 2 個成員變量分別綁定到語義 POSITION 和 COLOR,然后在 C2Elv_green 頂點程序入口函數(shù)中輸出,所以 C2Elv_Outpu 中的語義是輸出語義。
struct C2Elv_Output
{
float4 position:POSITION;
float3 color:COLOR;
}
C2Elv_Output C2Elv_green(float2 position:POSITION)
{
C2Elv_Output OUT;
OUT.position = float4(position, 0, 1);
OUT.color = float3(0,1,0);
return OUT;
}
3. 綁定語義可以放在函數(shù)聲明的后面,其形式為:
<type><identifier>(<parameter-list>)[:<binding-semantic>]
{
<body>
}
如下代碼所示,頂點入口函數(shù)的聲明后帶有“COLOR”語義詞,表示該函數(shù)需要反饋一個顏色值,所以函數(shù)的返回類型為 float4, 函數(shù)體也必須以 return 語句結(jié)束。
float4 main_v(float4 positon:POSITION,
out float4 oposition:POSITION,
uniform float4x4 modelViewProj):COLOR
{
oposition = mul(modelViewProj, positon);
float4 ocolor = float4(1.0, 0,0,0);
return oclolor;
}
4. 最后一種語義綁定的方法是,將綁定語義詞放在全局非靜態(tài)變量的聲明后面。其形式為:
<type><identifier>[:<binding-semantic>][=<initializer>]
這種形式的結(jié)構(gòu)很不緊湊,也不利于代碼的維護和閱讀,所以并不常見,不建議讀者使用。事實上,我在學(xué)習(xí)和研究過程中也很少碰到這種形式。