1 著色器和程序(Shaders and Programs)
1.1 著色器語言(Language Overview)
著色器的編程語言是基于C語言開發(fā)的霞怀,被稱為GLSL惫东。其和C語言最大的區(qū)別是它定義了向量好矩陣兩個數(shù)據(jù)類型柒桑,另外GLSL對于高并發(fā)進(jìn)行特殊優(yōu)化浑彰,通常在一個程序中,GPU同一時間會執(zhí)行上千個著色器的調(diào)用。為了達(dá)到上述要求溉潭,其犧牲了部分性能抱完,如在GLSL中禁止使用遞歸狗热,浮點(diǎn)數(shù)的運(yùn)算精度也沒有C語言中常用IEEE標(biāo)準(zhǔn)那么高卖宠。
1.1.1 數(shù)據(jù)類型(Data Types)
GLSL中定義的數(shù)據(jù)類型有標(biāo)量、向量坪稽、數(shù)組曼玩、結(jié)構(gòu)體以及一些用于標(biāo)識紋理和其他數(shù)據(jù)結(jié)構(gòu)的不透明數(shù)據(jù)類型(opaque data)。
標(biāo)量(Scalar Types)
GLSL中支持32和64位浮點(diǎn)型數(shù)據(jù)窒百,32位有符號和無符號整形數(shù)據(jù)演训,Boolean類型數(shù)據(jù)。
bool true 或者false
float IEEE-754格式32位浮點(diǎn)型數(shù)據(jù)
double IEEE-754格式64位浮點(diǎn)型數(shù)據(jù)
int 32位整形數(shù)據(jù)
unsigned int 32位無符號型數(shù)據(jù)
其中int和unsigned int能表示的數(shù)據(jù)范圍和C語言中一樣贝咙。float類型數(shù)據(jù)用1位表示符號位样悟,8位表示指部分,23位表示小數(shù)部分庭猩,其中8位的指數(shù)部分范圍為-127到+127窟她,會被修正為0到254。用b表示整個數(shù)據(jù)的二進(jìn)制位數(shù)據(jù)蔼水,e表示指數(shù)部分的值震糖,m表示小數(shù)部分的值,那么其最終的值可以通過以下公式獲得趴腋。
類似的吊说,double類型數(shù)據(jù)含1位符號位,11位指數(shù)位优炬,52位小數(shù)位颁井,其公式和上面公式類似,只有i的范圍取值為1到52蠢护,最后部分為e-1023兩處不同雅宾。
需要注意的是,GLSL并不嚴(yán)格要求執(zhí)行IEEE-754標(biāo)準(zhǔn)葵硕,對于一些與NaNs眉抬、infinites(無窮數(shù))和denormal(極小數(shù))類型數(shù)據(jù)運(yùn)算時會出現(xiàn)誤差,因此需要避免上述情況懈凹。GLSL并不支持異常檢測蜀变,當(dāng)做一些如和0相除的不合理操作時,只有運(yùn)行時才能發(fā)現(xiàn)介评。
向量和矩陣(Vectors and Matrices)
標(biāo)量 bool float double int unsigned int
2/3/4維向量 bvec2/3/4 vec2/3/4 dvec2/3/4 ivec2/3/4 uvec2/3/4
2*2/3*3/4*4矩陣 --- mat2/3/4 dmat2/3/4 --- ---
2*3矩陣 --- mat2*3 dmat2*3 --- ---
還支持 2*4/3*2/3*4/4*2/4*3/4*4矩陣
向量的構(gòu)造可以用標(biāo)量或者矩陣或者他們的混合模式库北,如。
vec3 foo = vec3(1.0); vec3 bar = vec3(foo);
vec4 baz = vec4(1.0, 2.0, 3.0, 4.0); vec4 bat = vec4(1.0, foo);
向量元素的獲取方式可以使用類似數(shù)組的方式,或者使用xyzw(坐標(biāo))贤惯、stpq(紋理坐標(biāo))、rgba(顏色)分別獲取各個元素棒掠。另外可以使用其成員構(gòu)建任意類型的向量孵构,如vec4 temp = vec4(bar.yzz, 1.0)
。
在GLSL中矩陣被看做為向量的數(shù)組烟很,每個向量表示矩陣的某一列颈墅,同時每個向量可以被看做數(shù)組,因此矩陣也被看做標(biāo)量的二維數(shù)組雾袱,以列優(yōu)先的方式遍歷整個矩陣恤筛。可以通過mat[m][n]的方式獲取其中的成員變量芹橡,其中m表示列毒坛,n表示行。+和-運(yùn)算和標(biāo)量運(yùn)算相似林说,※運(yùn)算不具有交換性煎殷,矩陣和向量除以標(biāo)量為其中各個元素分別除以標(biāo)量,矩陣和向量除以矩陣或向量時腿箩,兩個操作對象必須有相同的維度豪直。
數(shù)組和結(jié)構(gòu)體(Arrays and Structures)
數(shù)組的聲明方式和C++中類似,結(jié)構(gòu)體的聲明省略了關(guān)鍵字typedef珠移。在GLSL中數(shù)組的聲明方式有如下兩種弓乙。
float foo[5]; ivec2 bar[13]; dmat3 baz[29];
float[5] foo; ivec2[13] bar; dmat3[29] baz;
數(shù)組聲明時可以同時初始化數(shù)據(jù),OpenGL4.2以前只能使用第一種钧惧。
float[6] var = float[6](1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
float var[6] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0 };
結(jié)構(gòu)體以及結(jié)構(gòu)體數(shù)組的定義方式如下暇韧。
struct foo {
int a; vec2 b; mat4 c;
};
struct bar {
vec3 a; foo[7] b;
};
bar[29] baz;
對于數(shù)組變量,可以使用函數(shù).length
獲取數(shù)組的元素個數(shù)(GLSL不支持C++語法中的成員函數(shù)浓瞪,這是一個特例)锨咙。另外該函數(shù)也能獲取向量的元素個數(shù),以及矩陣的列數(shù)追逮。如酪刀。
float a[10]; // Declare an array of 10 elements
float b[a.length()]; // Declare an array of the same size
mat4 c;
float d = float(c.length()); // d is now 4
int e = c[0].length(); // e is the height of c (4)
int i;
// This loop iterates 10 times
for (i = 0; i < a.length(); i++) {
b[i] = a[i];
}
GLSL不直接支持多維數(shù)組,但是其支持將數(shù)組包裝到另外一個數(shù)組中钮孵。如下骂倘。
float a[10]; // "a" 數(shù)組包含10個浮點(diǎn)型數(shù)據(jù)
float b[10][2]; // "b" 數(shù)組包含兩個數(shù)組,其中每個數(shù)組包含10個浮點(diǎn)型數(shù)據(jù)
float c[10][2][5]; // "c" 數(shù)組包含5個數(shù)組巴席,其中每個數(shù)組包含2個數(shù)組历涝,其中每個數(shù)組包含10個元素
1.1.2 內(nèi)部函數(shù)(Built-In Functions)
GLSL包含上百個內(nèi)部函數(shù),大多數(shù)是用于處理紋理和內(nèi)存,這些函數(shù)在其相關(guān)章節(jié)中說明荧库,此處只關(guān)心處理數(shù)據(jù)的內(nèi)部函數(shù)堰塌,他們用于基礎(chǔ)數(shù)學(xué),矩陣分衫,向量场刑,數(shù)據(jù)包裝以及數(shù)據(jù)解包裝。
術(shù)語(Terminology)
GLSL中的函數(shù)支持函數(shù)重載蚪战,即函數(shù)名相同具有不同參數(shù)牵现。為了給數(shù)據(jù)類型分類以使其相關(guān)函數(shù)能更簡潔的表示,GLSL引入了一些標(biāo)準(zhǔn)術(shù)語邀桑。
genType表示單精度的標(biāo)量和向量數(shù)據(jù)瞎疼,genUType表示無符號整形的向量和標(biāo)量數(shù)據(jù),genIType表示有符號整形的向量和標(biāo)量數(shù)據(jù)壁畸,genDType表示雙精度的向量和標(biāo)量數(shù)據(jù)贼急,mat表示單精度的矩陣,dmat表示雙精度的矩陣捏萍。
內(nèi)置的矩陣和向量函數(shù)
正如前文所講竿裂,矩陣和向量在GLSL中是基本數(shù)據(jù)類型,它們通用+照弥、-腻异、*
運(yùn)算符號,此外它們有額外的函數(shù)这揣。函數(shù)matrixCompMult()
為對應(yīng)元素相乘(component-wise multiplication)悔常,連個矩陣大小必須完全相同。函數(shù)transpose()
用于矩陣轉(zhuǎn)置给赞。
函數(shù)inverse()
用于求矩陣的逆矩陣机打,需要注意的是該項(xiàng)計算非常消耗性能,最好在程序中計算然后通過統(tǒng)一變量的方式傳入著色器片迅,另外非方陣不支持該函數(shù)残邀。函數(shù)determinant()
用于計算方陣的行列式。最好需要注意的是對于病態(tài)矩陣(ill-conditioned matrices)(可以簡單理解為矩陣列向量線性相關(guān)性過大柑蛇,表示的特征太過于相似)芥挣,不存在逆運(yùn)算和行列式運(yùn)算,他們作為參數(shù)時會得到未定義的結(jié)果(undefined result)耻台。
函數(shù)outerProduct()
用于計算兩個向量的“外積”空免,兩個N維向量作為參數(shù),運(yùn)算時第一個向量作為1N的矩陣盆耽,第二個向量作為N1的矩陣蹋砚,用矩陣2矩陣1扼菠,返回結(jié)果為NN的矩陣。用于比較向量的函數(shù)有lessThan(), lessThanEqual(), greaterThan(), greaterThanEqual(), equal(), 和 notEqual()
坝咐,上述函數(shù)都有兩個相同大小和相同類型的向量作為參賽循榆,返回一個同等大小的Boolean向量(bvec2, bvec3, or bvec4),向量各元素相互比較結(jié)果和返回結(jié)果向量中元素一一對應(yīng)墨坚。
對于Boolean向量秧饮,可以使用函數(shù)any()和all()
測試測試其中某個元素或者所有元素是否為true。函數(shù)not()
可以對所有元素取反框杜。
另外處理向量的內(nèi)置函數(shù)還有浦楣,函數(shù)length()
返回向量的長度袖肥。函數(shù)distance()
返回向量表示兩個點(diǎn)之間的距離咪辱。函數(shù)normalize()
對向量進(jìn)行標(biāo)準(zhǔn)化。函數(shù)dot()和????cross()
分別用于向量的點(diǎn)成和叉乘椎组。
函數(shù)reflect()和refract()
通過一個平面的法向量計算入射向量的反射和折射向量油狂,另外后面一個函數(shù)需要竄擾額外的參數(shù)用于標(biāo)識折射角。函數(shù)faceforward()
傳入三個向量寸癌,如果后兩個向量內(nèi)積為負(fù)专筷,則返回第一個向量,反之返回第一個向量的負(fù)向量蒸苇,其中第一個和第三個參數(shù)分別兩個曲面的法向量磷蛹。
內(nèi)置數(shù)學(xué)函數(shù)(Built–In Math Functions)
GLSL中的通用數(shù)學(xué)函數(shù)包括abs(), sign(), ceil(), floor(), trunc(), round(), roundEven(), fract(), mod(), modf(), min(),和max()
。他們可以對標(biāo)量和向量使用溪烤。其中大多數(shù)函數(shù)和C語言標(biāo)注庫中的用法相同味咳,但是有部分例外。如函數(shù)roundEven()
并沒有C語言版本檬嘀,該函數(shù)取離參數(shù)最近的整數(shù)槽驶,但是當(dāng)參數(shù)小數(shù)部分為0.5時候,它總返回最近的偶數(shù)值鸳兽。
函數(shù)clamp()
有兩種不同的聲明如下掂铐。它將x中的值限定在minval和maxval指定的最小和最大值之間。
vec4 clamp(vec4 x, float minVal, float maxVal);
vec4 clamp(vec4 x, vec4 minVal, vec4 maxVal);
函數(shù)mix()
用于在連個輸入變量之間進(jìn)行插值運(yùn)算揍异,其計算過程可以表示為如下公式全陨。
vec4 mix(vec4 x, vec4 y, float a) {
return x + a * (y - x);
}
函數(shù)step()
定義為vec4 step(vec4 edge, vec4 x)
,如果x中相應(yīng)元素的值小于edge中對應(yīng)元素的值則返回0衷掷,反正則返回1烤镐。函數(shù)smoothstep()
定義為vec4 smoothstep(vec4 edge0, vec4 edge1, vec4 x)
,它通過內(nèi)部的計算生成0到1的值棍鳖,其計算規(guī)則如下炮叶。
vec4 smoothstep(vec4 edge0, vec4 edge1, vec4 x) {
vec4 t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
return t * t * (vec4(3.0) - 2.0 * t);
}
該函數(shù)產(chǎn)生一個埃爾米特插值曲線碗旅,通常用于標(biāo)識淡入淡出效果,其函數(shù)如下可以表示為下圖所示镜悉。
函數(shù)fma()
執(zhí)行其前兩個參數(shù)相乘結(jié)果加上第三個參數(shù)的操作祟辟,該函數(shù)生成的結(jié)果精度比代碼中分開計算更高。在部分GPU中侣肄,會針對該復(fù)合操作進(jìn)行特殊優(yōu)化旧困,使其執(zhí)行的效率高于分開執(zhí)行。
OpenGL中大多數(shù)函數(shù)用于浮點(diǎn)數(shù)的運(yùn)算稼锅,但是函數(shù)uaddCarry() 和 usubBorrow()
分別用于無符號整形標(biāo)量和向量的計算吼具,第一個函數(shù)將頭兩個參數(shù)的和放入第三個參數(shù)中,后一個函數(shù)將前兩個參數(shù)的差放入第三個函數(shù)中矩距。函數(shù)imulExtended() 和 umulExtended()
允許將兩個32位的無符號和有符號整形數(shù)據(jù)相乘拗盒,其結(jié)果為64位數(shù)據(jù)用兩個32位數(shù)據(jù)保存。
此外GLSL還支持三角函數(shù)sin(), cos(), 和 tan()
锥债,以及他們的反三角函數(shù)asin(), acos() 和 atan()
陡蝇,以及雙曲函數(shù)sinh(), cosh(), tanh(), asinh(), acosh(), 和 atanh()
。雙曲函數(shù)的計算表達(dá)式此處不描述哮肚,只描述其幾何意義登夫。
對于下圖中從原點(diǎn)發(fā)出的射線與單位雙曲線(x^2 - y^2 = 1
)相交于點(diǎn)(cosh a,sinh a)。這里的a為射線允趟、雙曲線和x軸圍成的面積的兩倍恼策。對于雙曲線上位于x軸下方的點(diǎn),這個面積被認(rèn)為是負(fù)值潮剪。其中cosh a就是a的雙曲余弦函數(shù)涣楷,其他雙曲線函數(shù)類似。
GLSL還支持冪函數(shù)鲁纠,指數(shù)函數(shù)和對數(shù)函數(shù)总棵,pow(genType x, genType y)
表示x^y
, exp(genType x)
表示e^x
, log(genType x)
取x的自然對數(shù), exp2(genType x)
表示2^x
, log2(genType x)
取以2為底的對數(shù), sqrt(genType x)
為開平方運(yùn)算,inversesqrt(genType x)
表示對x開平方后取其倒數(shù)改含。OpenGL中的運(yùn)算都是以弧度表示角度如π情龄,同時它提供函數(shù)radians和degress
分別將角度轉(zhuǎn)換為弧度和相反運(yùn)算。
內(nèi)置的數(shù)據(jù)操作函數(shù)
GLSL同時提供了函數(shù)用于獲取數(shù)據(jù)的類別結(jié)構(gòu)捍壤,函數(shù)frexp()
可以將一個浮點(diǎn)數(shù)劃分為小數(shù)部分和指數(shù)部分骤视。函數(shù)ldexp()
將小數(shù)部分和指數(shù)部分組合為一個新的浮點(diǎn)數(shù)據(jù)(這里的轉(zhuǎn)換是將位數(shù)據(jù)取出以浮點(diǎn)型數(shù)據(jù)編碼的方式生成新的數(shù))。函數(shù)intBitsToFloat() 和 uintBitsToFloat()
將一個整形數(shù)據(jù)轉(zhuǎn)換為浮點(diǎn)型數(shù)據(jù)鹃觉。函數(shù)floatBitsToInt() 和 floatBitsToUint()
將浮點(diǎn)型數(shù)據(jù)轉(zhuǎn)換為整形數(shù)據(jù)专酗。需要注意的是,在進(jìn)行轉(zhuǎn)換的時候盗扇,并非所有的位組合都會產(chǎn)生有效的浮點(diǎn)數(shù)據(jù)祷肯,可能得到極小值沉填、無效數(shù)據(jù)和無限值,可以通過函數(shù)isnan() 或者 isinf()
分別對結(jié)果進(jìn)行測試佑笋。
函數(shù)packUnorm4x8() 和 packSnorm4x8()
將vec4向量縮放至每個元素包裝為8位的無符號或者有符號的整形數(shù)據(jù)翼闹,然后將它們組合成為1個32位的無符號整形數(shù)據(jù)。函數(shù)unpackUnorm4x8() 和 unpackSnorm4x8()
執(zhí)行相反的操作蒋纬,函數(shù)packUnorm2x16(), packSnorm2x16(), unpackUnormx16(), 和 unpackSnorm16()
用于處理二維向量猎荠。
上述函數(shù)中的關(guān)鍵字norm指的為標(biāo)準(zhǔn)化,對于無符號和有符號的標(biāo)準(zhǔn)化數(shù)據(jù)蜀备,其對應(yīng)的浮點(diǎn)型數(shù)據(jù)范圍分別為0到1和-1到1关摇。這意味著將整數(shù)轉(zhuǎn)換為向量時,小于0或-255的數(shù)被映射為0.0碾阁,大于255的樹被映射為1.0输虱。
函數(shù)packDouble2x32() 和 unpackDouble2x32()
執(zhí)行針對雙精度數(shù)據(jù)相似的操作。函數(shù)packHalf2x16()
將2個32位的小數(shù)包裝為2個16位的小數(shù)然后再包裝為1個32位的uint數(shù)據(jù)瓷蛙。注意GLSL不直接支持16位的小數(shù)悼瓮,機(jī)軟數(shù)據(jù)能以這個格式被存在內(nèi)存中戈毒,但是GLSL包含函數(shù)在使用時將其解包為可用的數(shù)據(jù)類型。
函數(shù)bitfieldExtract()
從整數(shù)中提取部分為位生成一個新的整數(shù),函數(shù)bitfieldInsert()執(zhí)行相反操作玻靡。另外的位操作函數(shù)還有bitfieldReverse(), bitCount(), findLSB(), 和 findMSB()
分別用于位反序煞肾,有效位計數(shù),查找最低有效位和最高有效位道宅。
1.2 程序的編譯食听、鏈接和執(zhí)行(Compiling, Linking, and Examining Programs)
每個OpenGL的實(shí)現(xiàn)中都有一個編譯器和連接器,在編譯鏈接的過程中經(jīng)常會遇到一些錯誤污茵,OpenGL提供了很多函數(shù)來獲取這些錯誤信息樱报。
1.2.1 從編譯器獲取信息(Getting Information from the Compiler)
檢查著色器語法錯誤的第一步是檢查編譯器狀態(tài),調(diào)用函數(shù)void glGetShaderiv(GLuint shader, GLenum pname, GLint * params);
可以獲取編譯器狀態(tài)泞当,參數(shù)shader表示著色器的句柄迹蛤,參數(shù)pname表示查詢目的,參數(shù)params指定查詢結(jié)果的地址襟士。pname可選的類型可以使GL_COMPILE_STATUS表示編譯是否成功盗飒,成功返回1,失敗返回0陋桂。該函數(shù)還支持的pname有GL_SHADER_TYPE逆趣,GL_DELETE_STATUS,GL_SHADER_SOURCE_LENGTH嗜历,GL_INFO_LOG_LENGTH宣渗。
當(dāng)時獲取到日志長度后抖所,可以調(diào)用函數(shù)void glGetShaderInfoLog(GLuint shader, GLsizei bufSize, GLsizei * length, GLchar * infoLog);
獲得著色器的編譯日志。參數(shù)infolog指定了日志保存的地址痕囱。參數(shù)bufzie為準(zhǔn)備好的緩存字節(jié)大學(xué)部蛇,參數(shù)length用于接受寫入的日志長度。其使用方法如下咐蝇。
GLint status = 0;
glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);
if (status == 0) {
GLint logLen = 0;
glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLen);
GLchar *infoLog = malloc(sizeof(char) * logLen);
glGetShaderInfoLog(*shader, logLen, NULL, infoLog);
NSLog(@"Shader at: %@", path);
fprintf(stderr, "Info Log: %s\n", infoLog);
glDeleteShader(*shader);
free(infoLog);
return NO;
}
對于如下著色器代碼涯鲁。
#version 410 core //1
//2
layout (location = 0) out vec4 color; //3
//4
uniform scale; //5
uniform vec3 bias; //6
//7
void main(void) //8
{ //9
color = vec4(1.0, 0.5, 0.2, 1.0) * scale + bias; //10
} //11
使用上述日志輸出信息后可以得到如下所示的錯誤信息。
ERROR: 0:5: error(#12) Unexpected qualifier
ERROR: 0:10: error(#143) Undeclared identifier: scale
WARNING: 0:10: warning(#402) Implicit truncation of vector from
size: 4 to size: 3
ERROR: 0:10: error(#162) Wrong operand types: no operation "+" exists that takes a left-hand operand of type "4-component vector of vec4" and a right operand of type "uniform 3-component vector of vec3" (or there is no acceptable conversion)
ERROR: error(#273) 3 compilation errors. No code generated
可以看到有序,著色器中的錯日志分為警告?zhèn)€錯誤兩類抹腿,其后緊跟的是著色器代碼源的索引(需要注意的是函數(shù)glShaderSource()
允許為一個著色器對象分配多個著色器字符串),其后緊跟的是行號旭寿。
上文的第一個錯誤指的是第5行的變量缺少類型修飾符警绩。第二個錯誤表示第10行使用未定義的變量scale。第三個警告表示正在嘗試從vec4中截取vec3盅称。第四個錯誤表示無法將vec4和vec3執(zhí)行加法操作肩祥。
1.2.2 從連接器獲取信息(Getting Information from the Linker)
正如編譯作色器時會發(fā)生錯誤,鏈接程序時也可能會發(fā)生未知錯誤缩膝。獲取連接器的狀信息和獲取編譯器信息類似混狠,函數(shù)為void glGetProgramiv(GLuint program, GLenum pname, GLint * params);
,參數(shù)program為要查詢的程序句柄疾层。參數(shù)params指定了接受結(jié)果信息的地址将饺。pname為表示想要獲取的信息類型,其枚舉值很多痛黎,常見的有GL_DELETE_STATUS予弧,GL_LINK_STATUS,GL_INFO_LOG_LENGTH湖饱,GL_ATTACHED_SHADERS掖蛤,GL_ACTIVE_ATTRIBUTES(編譯器認(rèn)為頂點(diǎn)著色器使用的屬性個數(shù)),GL_ACTIVE_UNIFORMS(程序中使用的統(tǒng)一變量個數(shù))井厌,GL_ACTIVE_UNIFORM_BLOCKS
蚓庭。
獲取程序鏈接日志信息的函數(shù)為void glGetProgramInfoLog(GLuint program, GLsizei bufSize, GLsizei * length, GLchar * infoLog);
,其中各個參數(shù)和從編譯器中獲取日志信息函數(shù)的參數(shù)一致旗笔。假如現(xiàn)在有如下片段著色器代碼彪置。
#version 410 core
layout (location = 0) out vec4 color;
vec3 myFunction();
void main(void) {
color = vec4(myFunction(), 1.0);
}
正如C語言中可以將函數(shù)的實(shí)現(xiàn)和聲明分別放在兩個不同的文件中一樣,GLSL也允許著色器中的函數(shù)的聲明和實(shí)現(xiàn)在被鏈接到同一個程序的同類型的不同著色器字符串中(GLSL中允許將多個同類型的著色器字符串鏈接至一個程序?qū)ο?蝇恶。當(dāng)調(diào)用函數(shù)glLinkProgram()
拳魁,在本實(shí)例中,連接器將會查找所有的片段著色器字符串撮弧,如果未發(fā)現(xiàn)函數(shù)myFunction的實(shí)現(xiàn)潘懊,將會記錄錯誤日志如下姚糊。
Vertex shader(s) failed to link, fragment shader(s) failed to link. ERROR: error(#401) Function: myFunction() is not implemented
1.2.3 分離程序(Separate Programs)
到目前為止,本系列文章中使用的程序都可以被認(rèn)為是集成程序?qū)ο?monolithic program objects)授舟,即它們包括被激活各階段的著色器對象救恨。這種連接方式允許編譯器執(zhí)行一些內(nèi)部優(yōu)化,例如某個階段著色器代碼生成的結(jié)果在緊接階段永遠(yuǎn)不會被使用時释树,相關(guān)代碼將會被移除肠槽。然而,這種方式會犧牲應(yīng)用的靈活性奢啥,甚至可能損失部分性能秸仙。對于每一種頂點(diǎn)著色器、片段著色器等的組合桩盲,該方式都需要為其單獨(dú)分配一個程序寂纪,這種方式代價很大。
現(xiàn)在考慮如下情況赌结,當(dāng)需要使用一個頂點(diǎn)著色器捞蛋,多個片段著色器時,采用傳統(tǒng)的集成程序方式柬姚,需要分配多個程序?qū)ο竽馍迹藭r加入有多個頂點(diǎn)著色器,多個片段著色器伤靠,甚至多個階段捣域,此時分配的程序?qū)ο蠛苋菀着蛎浿辽锨€啼染,甚至更多宴合。
為了解決這個問題,OpenGL允許使用分離模式(separable mode)的程序?qū)ο蠹6臁_@種類型的程序?qū)ο罂梢园瑔蝹€或幾個階段的著色器對象卦洽。表示一個管道各個部分的程序?qū)ο罂梢员桓街谝粋€程序管道對象(program pipeline object)上,在運(yùn)行時它們將會被組合在一起而不是在鏈接的時候斜棚。OpenGL在每個程序?qū)ο髢?nèi)部仍會對代碼進(jìn)行優(yōu)化阀蒂,并且一個程序管道對著中的程序?qū)ο罂梢砸院苄〉男阅芟耐瓿汕袚Q操作。
要使用分離模式弟蚀,必須在鏈接程序?qū)ο笾罢{(diào)用函數(shù)glProgramParameteri()
和參數(shù)GL_PROGRAM_SEPARABLE蚤霞,GL_TRUE啟用分離模式的程序。該函數(shù)的效果還包含义钉,該程序中著色器如果有未被使用的輸出結(jié)果昧绣,相關(guān)代碼不會被移除。同時它也會組織內(nèi)部的數(shù)據(jù)布局捶闸,以便相鄰的程序之間夜畴,前者最后一個著色器的輸出數(shù)據(jù)和后者第一個著色器中具有相同布局的數(shù)據(jù)類型之間能夠進(jìn)行數(shù)據(jù)交流拖刃。緊接著,調(diào)用函數(shù)glGenProgramPipelines()
生成程序管道贪绘,再調(diào)用函數(shù)glUseProgramStages()
將程序綁定至程序管道對象之中兑牡。使用分類模式的例子如下。
// Create a vertex shader
GLuint vs = glCreateShader(GL_VERTEX_SHADER);
// Attach source and compile
glShaderSource(vs, 1, vs_source, NULL);
glCompileShader(vs);
// Create a program for our vertex stage and attach the vertex shader to it
GLuint vs_program = glCreateProgram();
glAttachShader(vs_program, vs);
// Important part - set the GL_PROGRAM_SEPARABLE flag to GL_TRUE *then* link
glProgramParameteri(vs_program, GL_PROGRAM_SEPARABLE, GL_TRUE); glLinkProgram(vs_program);
// Now do the same with a fragment shader
GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, fs_source, NULL);
glCompileShader(fs);
GLuint fs_program = glCreateProgram();
glAttachShader(fs_program, vs);
glProgramParameteri(fs_program, GL_PROGRAM_SEPARABLE, GL_TRUE); glLinkProgram(fs_program);
// The program pipeline represents the collection of programs in use: Generate the name for it here.
GLuint program_pipeline;
glGenProgramPipelines(1, &program_pipeline);
// Now, use the vertex shader from the first program and the fragment shader from the second program.
glUseProgramStages(program_pipeline, GL_VERTEX_SHADER_BIT, vs_program);
glUseProgramStages(program_pipeline, GL_FRAGMENT_SHADER_BIT, fs_program);
上述實(shí)例只是一個最簡單的引用税灌,我們甚至可以包含更多的程序?qū)ο缶蛘咴趩蝹€程序?qū)ο笾许n韓對個著色器。例如曲面細(xì)分控制和曲面細(xì)分評估函數(shù)通常緊密連接菱涤,很少被分開边酒。當(dāng)一個program中只包含1個著色器時,可以使用函數(shù)GLuint glCreateShaderProgramv (GLenum type, GLsizei count, const char ** strings);
創(chuàng)建程序狸窘。參數(shù)type為使用的著色器類型(GL_VERTEX_SHADER等)墩朦,參數(shù)count為數(shù)據(jù)源個數(shù),參數(shù)strings為數(shù)據(jù)源數(shù)組指針翻擒。該函數(shù)內(nèi)部會自動啟用分割程序氓涣,編譯著色器,附著著色器陋气,鏈接程序劳吠,刪除著色器的操作。
設(shè)置好各階段著色器后巩趁,函數(shù)glBindProgramPipeline()
可以將綁定程序管道痒玩,一旦某個程序管道被綁定至當(dāng)前上下文中,那么它將被用于渲染計算议慰。
接口匹配
GLSL提供了一套特殊的規(guī)則用于匹配某一階段的輸出結(jié)果和下一個階段的輸入結(jié)果蠢古。當(dāng)使用集成程序是,OpenGL連接器會對不正確的匹配生成錯誤信息别凹。
然而當(dāng)使用分離程序時草讶,當(dāng)切換program以及錯誤的排序都可能產(chǎn)生錯誤。因此在編程是注重這些規(guī)則從而避免上述問題十分重要炉菲。
總的說來相鄰著色器的輸出-輸入變量需要有相同的名字堕战、類型和結(jié)構(gòu),此外對于接口閉包和結(jié)構(gòu)體拍霜,其內(nèi)部的成員變量名字以及順序也必須相同嘱丢。對于數(shù)組變量,輸出和輸入的數(shù)組大學(xué)必須一致祠饺。唯一的特例是曲面細(xì)分著色器和幾何著色器的輸入以及輸出可以有單個的元素類型匹配數(shù)組的輸出類型越驻。
當(dāng)將多個階段著色器連接到一個程序中時,OpenGL會對著色器中的代碼進(jìn)行優(yōu)化,例如假如有頂點(diǎn)著色器和片段著色器伐谈,頂點(diǎn)著色其將一個常量直接寫入到片段著色中烂完,在編譯后OpenGL會移除頂點(diǎn)著色器的代碼,而是在片段著色器中會直接使用此常量诵棵。使用分類程序策略時抠蚣,不會有該效果。
當(dāng)多人進(jìn)行開發(fā)或者著色器數(shù)量不斷增加時履澳,記住每一個著色器中的輸出輸入變量非常困難嘶窄。然而,使用layout修飾符為著色器集合中的每個輸入輸出變量分配一個位置(Location)是可行的距贷。OpenGL可以使用每個輸入輸出變量的位置來完成匹配操作柄冲。這種情況下,變量的名字并不重要忠蝗,只要他們有相同的類型和修飾符现横。
void glGetProgramInterfaceiv(GLuint program,
GLenum programInterface,
GLenum pname,
GLint * params);
void glGetProgramResourceiv(GLuint program,
GLenum programInterface,
GLuint index,
GLsizei propCount,
const Glenum * props,
GLsizei bufSize,
GLsizei * length,
GLint * params);
上述兩個函數(shù)可以獲取各個變量的位置信息。第一個函數(shù)中阁最,參數(shù)program為需要查找的程序戒祠,參數(shù)programInterface可選GL_PROGRAM_INPUT或者GL_PROGRAM_OUTPUT
,為了繼續(xù)使用第二個函數(shù)速种,此處參數(shù)pname應(yīng)制定GL_ACTIVE_RESOURCES
姜盈,此時程序中使用的輸出或者輸入變量的個數(shù)將會被寫入地址params中。
第二個函數(shù)可以獲取變量的多種信息配阵,參數(shù)index指定變量在前一個函數(shù)獲取清單中的索引馏颂,propCount指定獲取屬性的個數(shù),參數(shù)props數(shù)組指定了需要獲取哪些描述信息棋傍,參數(shù)params指定了查詢結(jié)果寫入的數(shù)組地址救拉,參數(shù)bufSize指定了params中每個成員的內(nèi)存大小(the size of which (in elements) is given in bufSize),參數(shù)length通常直接指定為NULL(或者返回屬性個數(shù)寫入該地址中)舍沙。
props可選枚舉變量如下近上。
GL_TYPE 獲取變量類型
GL_ARRAY_SIZE 獲取數(shù)組元素個數(shù)(非數(shù)組變量返回0)
GL_REFERENCED_BY_VERTEX_SHADER,GL_REFERENCED_BY_TESS_CONTROL_SHADER拂铡,GL_REFERENCED_BY_TESS_EVALUATION_SHADER,
GL_REFERENCED_BY_GEOMETRY_SHADER葱绒,GL_REFERENCED_BY_FRAGMENT_SHADER感帅,GL_REFERENCED_BY_COMPUTE_SHADER
獲取變量是否被某個階段引用,引用返回非0值地淀,未引用返回0
GL_LOCATION 獲取變量的布局位置
GL_LOCATION_INDEX 只能在programInterface為GL_PROGRAM_OUTPUT時才能使用失球,獲取片段著色器輸出變量的位置
GL_IS_PER_PATCH 用于判斷曲面細(xì)分控制著色器的輸出變量或者曲面細(xì)分評價著色器的輸入變量是否聲明為分批接口
( if an output of a tessellation control shader or an input to a tessellation evaluation shader is declared as a per-patch interface)
void glGetProgramResourceName(GLuint program, GLenum programInterface, GLuint index,
GLsizei bufSize, GLsizei * length, char * name);
獲取變量的名字需要調(diào)用函數(shù)如上。參數(shù)program, programInterface, 和 index的含義和函數(shù)glGetProgramResourceiv
中對應(yīng)參數(shù)相同。參數(shù)bufSize指定參數(shù)name所分配內(nèi)存的大小实苞,參數(shù)length通常指定為NULL(否則返回實(shí)際名字長度)豺撑。一個獲取某個程序中活躍的輸出變量信息實(shí)例如下。
// Get the number of outputs
GLint outputs;
glGetProgramInterfaceiv(program, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES, &outputs);
// A list of tokens describing the properties we wish to query
static const GLenum props[] = { GL_TYPE, GL_LOCATION };
// Various local variables
GLint i;
GLint params[2];
GLchar name[64];
const char * type_name;
for (i = 0; i < outputs; i++) {
// Get the name of the output
glGetProgramResourceName(program, GL_PROGRAM_OUTPUT, i, sizeof(name), NULL, name);
// Get other properties of the output黔牵,這里buffersize使用2聪轿,而不是GLint的內(nèi)存大小4,還需驗(yàn)證
glGetProgramResourceiv(program, GL_PROGRAM_OUTPUT, i, 2, props, 2, NULL, params);
// type_to_name() is a function that returns the GLSL name of type given its enumerant value
type_name = type_to_name(params[0]);
// Print the result
printf("Index %d: %s %s @ location %d.\n", i, type_name, name, params[1]);
}
一個片段著色器的部分聲明代碼如下猾浦。
out vec4 color;
layout (location = 2) out ivec2 data;
out float extra;
對于上述代碼允許上述示例查詢輸出變量信息陆错,可以得到如下輸出結(jié)果。
Index 0: vec4 color @ location 0.
Index 1: ivec2 data @ location 2.
Index 2: float extra @ location 1.
輸出的變量索引和定義的變量索引相同金赦,當(dāng)聲明變量使用了布局位置信息時音瓷,OpenGL不做額外操作,否則會從0開始自動為變量設(shè)置位置信息夹抗。
1.2.4 著色器子程序(Shader Subroutines)
使用離散程序時绳慎,不同程序?qū)ο蟮那袚Q時仍耗費(fèi)性能。一個替代的方法是使用子程序漠烧,在著色器中它是一個Uniform變量偷线,可以理解為C語言中的函數(shù)指針,通過在一個著色器中聲明多個函數(shù)沽甥,而在渲染時決定具體使用某個函數(shù)從而使實(shí)現(xiàn)部分離散程序的功能声邦,同時優(yōu)化應(yīng)用性能。Subroutines在著色器中的聲明方式如下摆舟。
#version 430 core
// First, declare the subroutine type
subroutine vec4 sub_mySubroutine(vec4 param1);
// Next declare a couple of functions that can be used as subroutine...
subroutine (sub_mySubroutine) vec4 myFunction1(vec4 param1) {
return param1 * vec4(1.0, 0.25, 0.25, 1.0);
}
subroutine (sub_mySubroutine) vec4 myFunction2(vec4 param1) {
return param1 * vec4(0.25, 0.25, 1.0, 1.0);
}
// Finally, declare a subroutine uniform that can be "pointed"
// at subroutine functions matching its signature
subroutine uniform sub_mySubroutine mySubroutineUniform;
// Output color
out vec4 color;
void main(void) {
// Call subroutine through uniform
color = mySubroutineUniform(vec4(1.0));
}
每個Subroutine的子函數(shù)都有自己的索引值亥曹,在GLSL430及其以后,可以直接在著色器代碼中指定索引值恨诱,設(shè)置方式如下媳瞪。
layout (index = 2)
subroutine (sub_mySubroutine)
vec4 myFunction1(vec4 param1) {
return param1 * vec4(1.0, 0.25, 0.25, 1.0);
}
layout (index = 1);
subroutine (sub_mySubroutine)
vec4 myFunction2(vec4 param1) {
return param1 * vec4(0.25, 0.25, 1.0, 1.0);
}
對于GLSL430以前的版本,在鏈接程序后照宝,OpenGL會自動為subroutine的子函數(shù)分配索引值蛇受,調(diào)用函數(shù)為GLuint glGetProgramResourceIndex(GLuint program, GLenum programInterface, const char * name);
。其中參數(shù)program表示鏈接的程序厕鹃。參數(shù)programInterface根據(jù)具體要查找的著色器階段可選GL_VERTEX_SUBROUTINE, GL_TESS_CONTROL_SUBROUTINE, GL_TESS_EVALUATION_SUBROUTINE, GL_GEOMETRY_SUBROUTINE, GL_FRAGMENT_SUBROUTINE, 或者 GL_COMPUTE_SUBROUTINE
兢仰。參數(shù)name為函數(shù)的名稱。當(dāng)未查找到對應(yīng)函數(shù)時剂碴,返回GL_INVALID_VALUE把将。
同樣的,當(dāng)知道某個subroutine類型函數(shù)的索引時忆矛,可以調(diào)用函數(shù)void glGetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei * length, char * name);
獲取要查詢函數(shù)的名字察蹲。參數(shù)program和programInterface和上一個函數(shù)相同,參數(shù)index為需要查找的函數(shù)索引值,參數(shù)bufSize為函數(shù)名將要寫入的地址name分配的內(nèi)存空間大小洽议,函數(shù)名的字符數(shù)量將會被寫入到地址length中宗收。
void glGetProgramStageiv(GLuint program, GLenum shadertype, GLenum pname, GLint *values);
對于某個程序的某一個著色器階段,其中活躍的subroutine類型函數(shù)數(shù)量可以通過上述函數(shù)獲取亚兄。其中參數(shù)program表示要查詢的program混稽,參數(shù)shadertype為需要查詢的著色器階段,參數(shù)pname這里設(shè)置為GL_ACTIVE_SUBROUTINES儿捧,返回值會寫入在value地址內(nèi)荚坞。當(dāng)調(diào)用函數(shù)glGetActiveSubroutineName
時,其參數(shù)index必須為有效的索引值菲盾。
當(dāng)確定subroutine的索引后颓影,調(diào)用函數(shù)void glUniformSubroutinesuiv(GLenum shadertype, GLsizei count, const GLunit *indices);
可以設(shè)置subroutine類型的Uniform變量值,從而決定在著色器中具體調(diào)用的是哪個子函數(shù)。參數(shù)count指定了subroutine類型的Uniform變量個數(shù),參數(shù)indices數(shù)組中的每個索引對應(yīng)的成員會賦值給相同位置的uniform變量秉溉。這里通常只為一個Uniform變量賦值,要為多個Uniform變量賦值時璃俗,調(diào)用函數(shù)glGetSubroutineUniformLocation
獲取其位置,或者在著色器中指定位置悉默。
需要注意的是為subroutine類型Uniform變量賦值不同于普通的Uniform變量城豁。
- subroutine Uniform變量的值存儲在當(dāng)前上下文中,而不是program對象中抄课,這樣可以在同一個program對象不同上下文中存儲不同的Uniform變量值唱星。
- 當(dāng)調(diào)用函數(shù)
glUseProgram(), glUseProgramStages() 或者 glBindProgramPipeline
時,subroutine Uniform變量的值會丟失跟磨。這意味著當(dāng)使用一個新的program或者使用一個新的program階段時都必須重設(shè)這些變量间聊。 - program中每個階段的所有subroutine Uniform變量都必須賦值,調(diào)用函數(shù)
glUniformSubroutinesuiv
賦值時抵拘,超出count參數(shù)指定的變量都不會被賦值哎榴,此時調(diào)用這些類型的變量會造成應(yīng)用崩潰。
在鏈接program后僵蛛,調(diào)用以下代碼獲取subroutine子函數(shù)的索引值尚蝌。
subroutines[0] = glGetProgramResourceIndex(render_program, GL_FRAGMENT_SHADER_SUBROUTINE, "myFunction1");
subroutines[1] = glGetProgramResourceIndex(render_program, GL_FRAGMENT_SHADER_SUBROUTINE, "myFunction2");
在獲取索引值后,在程序中調(diào)用如下代碼以完成繪制操作墩瞳。
void subroutines_app::render(double currentTime) {
glUseProgram(render_program);
glUniformSubroutinesuiv(GL_FRAGMENT_SHADER, 1, &subroutines[1]);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
1.2.5 程序二進(jìn)制文檔(Program Binaries)
編譯鏈接完程序后驼壶,可以獲取程序的二進(jìn)制對象,在某個時刻能夠直接將該二進(jìn)制對象交給OpenGL處理喉酌,從而繞過編譯和鏈接的步驟。使用該特性需調(diào)用函數(shù)glProgramParameteri()
,參數(shù)pname設(shè)置為GL_PROGRAM_BINARY_RETRIEVABLE_HINT
泪电,其值設(shè)置為GL_TRUE般妙,然后在調(diào)用函數(shù)glLinkProgram()
。
要獲取二進(jìn)制對象相速,首先需要調(diào)用函數(shù)glGetProgramiv()
獲取二進(jìn)制對象的大小碟渺,參數(shù)pname設(shè)置為GL_PROGRAM_BINARY_LENGTH
,接下來調(diào)用函數(shù)void glGetProgramBinary (GLuint program, GLsizei bufsize, GLsizei * length, GLenum * binaryFormat, void * binary)
獲取二進(jìn)制對象突诬。
其中二進(jìn)制數(shù)據(jù)會被寫入?yún)?shù)binary指定的內(nèi)存中苫拍,數(shù)據(jù)格式會被寫入到地址binaryformat中,bufferzise為binary內(nèi)存空間的大小旺隙,實(shí)際寫入的數(shù)據(jù)大小將會被寫入地址length中绒极。二進(jìn)制文件的格式會和GPU及OpenGL驅(qū)動的制造商相關(guān)聯(lián)。一個獲取程序的二進(jìn)制文件的實(shí)例如下蔬捷。
// Create a simple program containing only a vertex shader
static const GLchar source[] = { ... };
// First create and compile the shader
GLuint shader;
shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(shader, 1, suorce, NULL);
glCompileShader(shader);
// Create the program and attach the shader to it
GLuint program;
program = glCreateProgram();
glAttachShader(program, shader);
// Set the binary retrievable hint and link the program
glProgramParameteri(program, GL_PROGRAM_BINARY_RETRIEVABLE_HINT, GL_TRUE); glLinkProgram(program);
// Get the expected size of the program binary
GLint binary_size = 0;
glGetProgramiv(program, GL_PROGRAM_BINARY_SIZE, &binary_size);
// Allocate some memory to store the program binary
unsigned char * program_binary = new unsigned char [binary_size];
// Now retrieve the binary from the program object
GLenum binary_format = GL_NONE;
glGetProgramBinary(program, binary_size, NULL, &binary_format, program_binary);
當(dāng)獲取到二進(jìn)制文件后垄提,可以將其存儲到磁盤上(可以壓縮),然后在下一次應(yīng)用啟動時直接加載周拐。需要注意的是铡俐,二進(jìn)制程序?qū)ο蟮母袷胶虶PU制造商以及OpenGL驅(qū)動相關(guān),因此不能在不同機(jī)器以及同一個機(jī)器不同驅(qū)動中共用妥粟。該機(jī)制不適用于發(fā)布程序审丘,更多的意義在于緩存程序?qū)ο蟆?/p>
在前文中使用的所有示例其program都很小,但是考慮到如游戲類的大型應(yīng)用勾给,使用該特性有非常明顯的有效滩报。游戲應(yīng)用中通常包含上千個著色器,在游戲啟動時需要編譯鏈接程序锦秒,這通常很耗時露泊,采用二進(jìn)制文件緩存策略能省去大量的時間。但是有一個問題需要注意旅择,對于復(fù)雜的應(yīng)用惭笑,OpenGL會在程序運(yùn)行是對著色器進(jìn)行重編譯。
OpenGL中大多數(shù)特性都能直接被現(xiàn)代的GPU直接支持生真,然而部分特性在著色器中并不支持沉噩,當(dāng)應(yīng)用編譯著色器時,OpenGL的實(shí)現(xiàn)會給大多數(shù)的特性實(shí)現(xiàn)默認(rèn)配置柱蟀,并在編譯時對著色器采用這些默認(rèn)配置川蒙。如果著色器中并未使用默認(rèn)配置,那么长已,OpenGL的實(shí)現(xiàn)至少需要重編譯該部分著色器代碼以應(yīng)對這種改變畜眨。這樣會導(dǎo)致應(yīng)用卡頓昼牛。
為了優(yōu)化上述問題,強(qiáng)烈建議在鏈接program之前康聂,將其GL_PROGRAM_BINARY_RETRIEVABLE _HINT
屬性設(shè)置為GL_TRUE
贰健,并且在進(jìn)行多次渲染步驟后再獲取二進(jìn)制文件。這樣可以在真正獲取二進(jìn)制文件之前讓應(yīng)用有時間進(jìn)行必要的重編譯恬汁,并且能再一個二進(jìn)制文件中保存多個程序的版本伶椿。以后再次加載二進(jìn)制文件時,OpenGL的實(shí)現(xiàn)需要使用某一個特別變量的時候氓侧,它會在該二進(jìn)制文件中找到重編譯的那部分可執(zhí)行代碼脊另。
在將二進(jìn)制文件載入OpenGL之前,需要為新建的program對象調(diào)用函數(shù)lProgramBinary()
约巷,參數(shù)binaryFormat偎痛,data和length分別設(shè)置為從函數(shù)glGetProgramBinary()
獲取的值。
1.3 總結(jié)(Summary)
該部分內(nèi)容討論了著色器以及它們是如何工作载庭,可編程程序語言GLSL以及OpenGL是如何使用該部分代碼看彼,以及它們和圖形管道的關(guān)系。
2 頂點(diǎn)處理和繪制命令(Vertex Processing and Drawing Commands)
2.1 頂點(diǎn)處理(Vertex Processing)
OpenGL運(yùn)行后第一個階段為頂點(diǎn)抓取階段(vertex fetch stage)囚聚,該階段抓取數(shù)據(jù)并傳入頂點(diǎn)著色器(vertex shader)靖榕。頂點(diǎn)著色器是可編程部分,用于確定模型頂點(diǎn)位置顽铸。
2.1.1 頂點(diǎn)著色器輸入(Vertex Shader Inputs)
到目前為止出現(xiàn)的示例中茁计,頂點(diǎn)著色器輸入的數(shù)據(jù)類型都是浮點(diǎn)型數(shù)據(jù),然而OpenGL支持大量的頂點(diǎn)屬性谓松,每個屬性都有自己的格式星压,頂點(diǎn)類型以及成員等。同樣的OpenGL也能從不同的緩存對象中讀取數(shù)據(jù)并輸入到每個屬性中鬼譬。函數(shù)glVertexAttribPointer()
可以為頂點(diǎn)著色器中的屬性賦值數(shù)據(jù)娜膘,此外OpenGL還提供了以下幾個輔助方法。
void glVertexAttribFormat(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset);
void glVertexAttribBinding(GLuint attribindex, GLuint bindingindex);
void glBindVertexBuffer(GLuint bindingindex, GLuint buffer, GLintptr offset, GLintptr stride);
為了說明上述方法优质,考慮有如下的頂點(diǎn)著色器代碼竣贪。
#version 430 core
// Declare a number of vertex attributes
layout (location = 0) in vec4 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 tex_coord;
// Note that we intentionally skip location 3 here
layout (location = 4) in vec4 color;
layout (location = 5) in int material_id;
上述的5個輸入變量可以用一個C語言中的結(jié)構(gòu)體變量來表,其定義如下巩螃。
typedef struct VERTEX_t {
vmath::vec4 position;
vmath::vec3 normal;
vmath::vec2 tex_coord;
GLubyte color[3];
int material_id;
} VERTEX;
為屬性position調(diào)用格式描述函數(shù)glVertexAttribFormat()
時演怎,參數(shù)size為4,參數(shù)type為GL_FLOAT避乏。屬性normal通常用于指定該幾何表面的法向量爷耀,參數(shù)size為4,參數(shù)type為GL_FLOAT拍皮。屬性tex_coord通常用于描述紋理的坐標(biāo)歹叮,參數(shù)size為2跑杭,參數(shù)type為GL_FLOAT。
對于屬性color盗胀,在著色器中定義的輸入可惜為vec4艘蹋,然而提供的原始數(shù)據(jù)類型為一個3字節(jié)的數(shù)組锄贼。此處的大小和類型都不相同票灰。OpenGL可以在讀取數(shù)據(jù)后將其進(jìn)行轉(zhuǎn)換再傳入著色器中。為了將3個單字節(jié)成員變量構(gòu)成的原始數(shù)據(jù)屬性和4個float類型變量構(gòu)成的著色器輸入屬性匹配宅荤,需要調(diào)用函數(shù)glVertexAttribFormat()
屑迂,參數(shù)size為3,參數(shù)type為GL_UNSIGNED_BYTE冯键。該數(shù)據(jù)類型表示的是非標(biāo)準(zhǔn)化數(shù)據(jù)惹盼,其取值位于0到255,在將數(shù)據(jù)傳入著色器時惫确,參數(shù)normalized設(shè)置為GL_TRUE時手报,OpenGL會將顏色的分量都除以255后便得到取值為0到1的標(biāo)準(zhǔn)化數(shù)據(jù),如果設(shè)置為GL_FALSE改化,OpenGL會將其值直接強(qiáng)制類型轉(zhuǎn)換掩蛤。
對于頂點(diǎn)著色器中的個輸入變量,其數(shù)據(jù)類型和取值范圍如下陈肛。后三個類型不能被標(biāo)準(zhǔn)化揍鸟。GL_FIXED是一個特殊的數(shù)據(jù)類型,有32位組成句旱,高16位為整數(shù)部分阳藻,低16位為小數(shù)部分,該類型也不能被標(biāo)準(zhǔn)化谈撒。
Type OpenGL Type Range
GL_BYTE GLbyte -128 to 127
GL_SHORT Glshort -32,768 to 32767
GL_INT GLint -2,147,483,648 to 2,147,483,647
GL_FIXED GLfixed -32,768 to 32767
GL_UNSIGNED_BYTE GLubyte 0 to 255
GL_UNSIGNED_SHORT GLushort 0 to 65535
GL_UNSIGNED_INT GLuint 4,294,967,295
GL_HALF_FLOAT GLhalf —
GL_FLOAT GLfloat —
GL_DOUBLE GLdouble —
除了上述的標(biāo)量腥泥,函數(shù)glVertexAttribFormat()也支持使用包裝變量,即用一個整形變量標(biāo)識多個成員啃匿。如格式GL_UNSIGNED_INT_2_10_10_10_REV和GL_INT_2_10_10_10_REV
蛔外,他們都將4個變量合成帶了一個32位的變量中。
格式GL_UNSIGNED_INT_2_10_10_10_REV
的x立宜、y冒萄、z分量為10位,w分量只有2位橙数,并且他們都是無符號整數(shù)尊流。因此x、y灯帮、z的取值范圍為0到1023崖技,w的取值范圍為0到3逻住。格式GL_INT_2_10_10_10_REV
類似,其x迎献、y瞎访、z的取值范圍為-512到511,w的取值范圍為-2到1吁恍。該格式并不是很有用扒秸,但是他可以用于標(biāo)識3維向量,盡管有2位的內(nèi)存空間會被浪費(fèi)冀瓦。
當(dāng)指定為上述兩個包裝數(shù)據(jù)格式時伴奥,蠶食size必須指定為4或者GL_BGRA。此時OpenGL會自動將輸入數(shù)據(jù)的分量順序RGB轉(zhuǎn)換為BGR翼闽。這樣能夠提升著色器的兼容性拾徙。注,BGRA的顏色順序廣泛運(yùn)用于圖像存儲中感局,是大多數(shù)圖像API的默認(rèn)順序尼啡。
回到前文聲明的頂點(diǎn)著色器,對于屬性material_id询微,需要傳入int類型的值崖瞭,因此需要調(diào)用函數(shù)glVertexAttribFormat()
的擴(kuò)展形式glVertexAttribIFormat()
,其參數(shù)含義相同拓提。不同的是該函數(shù)中不包含參數(shù)normalized读恃,這是因?yàn)樵摵瘮?shù)的type只能是GL_BYTE, GL_SHORT, GL_INT以及他們對應(yīng)的無符號類型,或者包裝的數(shù)據(jù)格式代态,該類型數(shù)據(jù)永遠(yuǎn)也不會被標(biāo)準(zhǔn)化處理寺惫。因此需要關(guān)聯(lián)自定義C語言的結(jié)構(gòu)體輸入,和著色器中對應(yīng)的屬性需要調(diào)用以下函數(shù)蹦疑。
// position
glVertexAttribFormat(0, 4, GL_FLOAT, GL_FALSE, offsetof(VERTEX, position));
// normal
glVertexAttribFormat(1, 3, GL_FLOAT, GL_FALSE, offsetof(VERTEX, normal));
// tex_coord
glVertexAttribFormat(2, 2, GL_FLOAT, GL_FALSE, offsetof(VERTEX, texcoord));
// color[3]
glVertexAttribFormat(4, 3, GL_UNSIGNED_BYTE, GL_TRUE, offsetof(VERTEX, color));
// material_id
glVertexAttribIFormat(5, 1, GL_INT, offsetof(VERTEX, material_id));
當(dāng)關(guān)聯(lián)元素數(shù)據(jù)存儲格式和著色器聲中數(shù)據(jù)聲明格式后西雀,需要指定數(shù)據(jù)的讀取緩存源(buffer)。將緩存映射至統(tǒng)一變量閉包和將緩存映射至頂點(diǎn)屬性類似歉摧。每個頂點(diǎn)著色器都可以包含不超過上限個數(shù)的屬性艇肴,OpenGL同樣也能從不高于上限個數(shù)的緩存中向著色器中傳遞數(shù)據(jù)。部分頂點(diǎn)屬性數(shù)據(jù)可以在一個緩沖中共享內(nèi)存空間叁温,其余的存儲在不同的緩存中再悼。通常并不會為每個頂點(diǎn)屬性指定緩存對象,相反的膝但,通常將輸入對象分組冲九,然后將這些組合緩存綁定集合相關(guān)聯(lián)。
在應(yīng)用中調(diào)用函數(shù)glVertexAttribBinding()
可以在緩存綁定點(diǎn)和頂點(diǎn)屬性之間建立映射關(guān)系跟束。參數(shù)attribindex為頂點(diǎn)屬性的索引莺奸,參數(shù)bindingindex是緩存綁定點(diǎn)的索引丑孩。此處將所有頂點(diǎn)屬性分為1組,并綁定至同一個綁定點(diǎn)灭贷。當(dāng)然也可以將它們綁定至多個綁定點(diǎn)温学,這里不再說明。
void glVertexAttribBinding(0, 0); // position
void glVertexAttribBinding(1, 0); // normal
void glVertexAttribBinding(2, 0); // tex_coord
void glVertexAttribBinding(4, 0); // color
void glVertexAttribBinding(5, 0); // material_id
最后只需調(diào)用函數(shù)glBindVertexBuffer()
將緩存對象綁定至綁定點(diǎn)即可甚疟。其中參數(shù)bindingindex為綁定點(diǎn)的索引仗岖,buffer為緩存對象的名字,Offset為頂點(diǎn)數(shù)據(jù)的偏移量古拴,參數(shù)stride為每個頂點(diǎn)數(shù)據(jù)起點(diǎn)之間的內(nèi)存間隔箩帚,他們單位都為字節(jié)。當(dāng)頂點(diǎn)數(shù)據(jù)緊密包裝時黄痪,參數(shù)stride可以設(shè)置為整個頂點(diǎn)數(shù)據(jù)的大小,即示例中的sizeof(VERTEX)盔然,否則需要計算真實(shí)的內(nèi)存間隔桅打。
上述為頂點(diǎn)著色器輸入數(shù)據(jù)的方式和首先創(chuàng)建緩存,然后調(diào)用函數(shù)glVertexAttribPointer
和函數(shù)glEnableVertexAttribArray
的方式有相同的效果愈案。
2.1.2 頂點(diǎn)著色器輸出(Vertex Shader Outputs)
當(dāng)頂點(diǎn)著色器對頂點(diǎn)數(shù)據(jù)處理后挺尾,需要輸出結(jié)果。前文中已經(jīng)使用過內(nèi)部變量gl_Position來創(chuàng)建輸出結(jié)果站绪。和gl_Position一樣遭铺,OpenGL還提供另外兩個內(nèi)部變量可以在頂點(diǎn)著色器中使用,它們被封裝在閉包gl_Pervertex中恢准,使用時可以直接訪問內(nèi)部數(shù)據(jù)不用帶閉包名魂挂。其聲明如下。
out gl_PerVertex {
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
};
gl_ClipDistance用于剪切圖元馁筐,其使用方式本章末尾介紹涂召。gl_PointSize用于控制渲染時的點(diǎn)大小。
默認(rèn)情況下敏沉,OpenGL使用單個片段的大小(a size of a single fragment)去繪制一個點(diǎn)果正。如前文在應(yīng)用中使用函數(shù)glPointSize()
直接改變點(diǎn)的大小。OpenGL在不同實(shí)現(xiàn)中支持的單個點(diǎn)渲染大小上限不盡相同盟迟,但是至少是64像素秋泳。函數(shù)glGetIntegerv()
和參數(shù)GL_POINT_SIZE_RANGE
可以獲取到一個尺寸為2的數(shù)組,第一個元素表示最小點(diǎn)尺寸攒菠,通常為1迫皱,第二個元素表示最大點(diǎn)尺寸。
OpenGL支持在著色器中設(shè)置點(diǎn)的大小要尔,在著色器中直接為變量gl_PointSize賦值即可舍杜。在此之前必須在應(yīng)用中調(diào)用函數(shù)glEnable(GL_PROGRAM_POINT_SIZE)
來啟用該功能新娜。
該特性的一種使用案例是根據(jù)點(diǎn)距離觀察著的距離來設(shè)置點(diǎn)的大小。在應(yīng)用中調(diào)用函數(shù)glPointSize()
設(shè)置點(diǎn)大小既绩,所有的點(diǎn)大小都一致概龄。而在著色器中設(shè)置點(diǎn)的大小具有更高的靈活性。該方式也能用于幾何著色器中饲握,或者在曲面細(xì)分評估著色器指定點(diǎn)模式(point_mode)時私杜,該方式也能用于曲面細(xì)分引擎內(nèi)。
下面公式用于計算基于距離的點(diǎn)尺寸衰減救欧,其中d表示點(diǎn)距眼睛的距離衰粹。可以將a笆怠、b铝耻、c、d四個值聲明為uniform類型變量蹬刷,并在繪制時不斷更新瓢捉,也可以將它們設(shè)置為有意義的常量。該等式中办成,當(dāng)b泡态、c為0,a不為0時迂卢,點(diǎn)大小和距離無關(guān)某弦,當(dāng)a、b為0而克,c不為0時靶壮,點(diǎn)大小和距離以二次函數(shù)遞減方式板鬓。
2.2 繪制命令(Drawing Commands)
到目前為止的示例中蛮位,渲染圖形都只調(diào)用了函數(shù)glDrawArrays()
薪伏。其實(shí)OpenGL提供了多個函數(shù)用于渲染模型麻蹋,他們被分為有索引-無索引的刻炒,直接-間接地蝌矛。沒類繪制函數(shù)都包含如下幾個部分碎罚。
2.2.1 有索引的繪制命令(Indexed Drawing Commands)
函數(shù)glDrawArrays()
為非索引繪圖命令欣硼。使用該方式繪制圖形時混卵,OpenGL從緩存中讀取數(shù)據(jù)后直接按原始順序傳入著色器之中映穗。有索引的繪圖方式包含了一些間接的步驟將這些緩存中的數(shù)據(jù)看做為一個數(shù)組,此時OpenGL不會有序的從數(shù)組中讀取數(shù)據(jù)幕随,相反的會從根據(jù)另外一個索引數(shù)組來確定讀取順序蚁滋。為了實(shí)現(xiàn)有索引的繪圖命令,必須在GL_ELEMENT_ARRAY_BUFFER
目標(biāo)上綁定一個緩存對象。該緩存對象包含需要繪制的頂點(diǎn)索引值辕录。接下來睦霎,就可以調(diào)用有索引的繪制函數(shù),這些函數(shù)的名字中都有關(guān)鍵字names走诞。
函數(shù)void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid * indices);
是最簡單的一個有索引的繪制命令副女。參數(shù)mode和參數(shù)type的含義和函數(shù)glDrawArrays()
中一致。參數(shù)type用于描述索引數(shù)據(jù)的格式蚣旱,可選GL_UNSIGNED_BYTE碑幅,GL_UNSIGNED_SHORT和GL_UNSIGNED_INT
,它們分別表示每個索引占用8塞绿、16沟涨、32位內(nèi)存空間。參數(shù)indices描述了當(dāng)前綁定至GL_ELEMENT_ARRAY_BUFFER
目標(biāo)的緩存中數(shù)據(jù)的偏移值异吻。下圖表示了通過函數(shù)glDrawElements()
指定的索引值是如何被使用的裹赴。
實(shí)際上,函數(shù)glDrawArrays()和glDrawElements()
是OpenGL支持的直接繪圖命令的子集涧黄。下面列舉了OpenGL中最普遍的繪圖命令篮昧,在OpenGL中所有的繪圖命令都由其中部分函數(shù)組成。
Draw Type Command
Direct, Non-Indexed glDrawArraysInstancedBaseInstance()
Direct, Indexed glDrawElementsInstancedBaseVertexBaseInstance()
Indirect, Non-Indexed glMultiDrawArraysIndirect()
Indirect, Indexed glMultiDrawElementsIndirect()
前文中有個示例為旋轉(zhuǎn)的立方體笋妥,之前使用了12個三角形(每個面兩個),36個頂點(diǎn)來繪制一個立方體窄潭。然而春宣,一個立方體實(shí)際只含有8個角,因此只需要8個頂點(diǎn)嫉你,通過有索引的繪圖命令可以減少頂點(diǎn)的數(shù)量月帝,特別是應(yīng)用在包含大量頂點(diǎn)的著色器上時。
此時可以定義8個頂點(diǎn)數(shù)據(jù)和36個索引數(shù)據(jù)來實(shí)現(xiàn)上述特性幽污,其實(shí)現(xiàn)代碼如下嚷辅。下述的代碼可以將內(nèi)存空間從432字節(jié)降低到144字節(jié)。
static const GLfloat vertex_positions[] = {
-0.25f, -0.25f, -0.25f, -0.25f, 0.25f, -0.25f, 0.25f, -0.25f, -0.25f, 0.25f, 0.25f, -0.25f,
0.25f, -0.25f, 0.25f, 0.25f, 0.25f, 0.25f, -0.25f, -0.25f, 0.25f, -0.25f, 0.25f, 0.25f,
};
static const GLushort vertex_indices[] = {
0, 1, 2, 2, 1, 3, 2, 3, 4, 4, 3, 5, 4, 5, 6, 6, 5, 7,
6, 7, 0, 0, 7, 1, 6, 0, 2, 2, 4, 6, 7, 5, 3, 7, 3, 1
};
glGenBuffers(1, &position_buffer);
glBindBuffer(GL_ARRAY_BUFFER, position_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertex_positions), vertex_positions, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(0);
glGenBuffers(1, &index_buffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(vertex_indices), vertex_indices, GL_STATIC_DRAW);
當(dāng)有了頂點(diǎn)數(shù)據(jù)和索引數(shù)據(jù)后距误,調(diào)用函數(shù)glDrawElements()
或者其擴(kuò)展函數(shù)即可以繪制模型簸搞,其繪制邏輯代碼如下。
// Clear the framebuffer with dark green
static const GLfloat green[] = { 0.0f, 0.25f, 0.0f, 1.0f };
glClearBufferfv(GL_COLOR, 0, green);
// Activate our program
glUseProgram(program);
// Set the model-view and projection matrices
glUniformMatrix4fv(mv_location, 1, GL_FALSE, mv_matrix);
glUniformMatrix4fv(proj_location, 1, GL_FALSE, proj_matrix);
// Draw 6 faces of 2 triangles of 3 vertices each = 36 vertices
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_SHORT, 0);
基礎(chǔ)頂點(diǎn)(The Base Vertex)
函數(shù)void glDrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, GLvoid * indices, GLint basevertex);
是函數(shù)glDrawElements
的擴(kuò)展形式准潭。該函數(shù)會在通過索引從數(shù)據(jù)數(shù)組緩存中取出頂點(diǎn)數(shù)據(jù)之前為索引添加偏移值趁俊。因此當(dāng)該函數(shù)參數(shù)basevertex為0時,其和函數(shù)glDrawElements
等價刑然。該操作的邏輯如下圖所示寺擂。
使用圖元重啟聯(lián)合頂點(diǎn)(Combining Geometry using Primitive Restart)
OpenGL中有很多工具可以條帶化幾何形(“stripify” geometry)。這些工具的原理是通過使用表示大量未連接的三角形集合的三角形混合體(triangle soup),并嘗試將其合成一個三角形條帶集合的方式來提高性能怔软。由于單個三角形由三個頂點(diǎn)表示垦细,但是三角形條帶可以將其降低至每個三角形由一個頂點(diǎn)表示(除了第一個三角形),因此該方案是可行的挡逼。通過將幾何形從三角形混合體向三角形條帶轉(zhuǎn)換可以減少需要處理的幾何體數(shù)據(jù)括改,同時應(yīng)用能夠運(yùn)行得更快。判斷一個工具的優(yōu)良的標(biāo)注是挚瘟,是否能夠形成更少的三角形條帶以及單個三角線條帶能夠包含更多的三角形叹谁。對于該算法有大量的研究,一個算法是否成功的判斷方式是使用心得條帶化器處理一些完善的模型乘盖,將處理結(jié)果的條帶數(shù)量和單個條帶的長度和最先進(jìn)的條帶化器比較焰檩。
三角形混合體的渲染可以只調(diào)用一次函數(shù)glDrawArrays
或者glDrawElements
,而三條形條帶集合的渲染需要多次調(diào)用函數(shù)(此處還有函數(shù)能夠一次渲染三角形條帶订框,該特例下文講解)析苫。這意味著使用三角形條帶在應(yīng)用中將會出現(xiàn)更多的函數(shù)調(diào)用,如果條帶化器性能較差或者模型不能被很好被條帶化穿扳,這種操作看上去將會浪費(fèi)性能衩侥。
OpenGL提供了圖元重啟特性(primitive restart)用于解決上述問題。圖元重啟適用的種類有GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_LINE_STRIP和GL_LINE_LOOP
矛物。該方法通知OpenGL一個條帶(或者扇區(qū)茫死,或者環(huán))已經(jīng)結(jié)束,另一個新的條帶(或者扇區(qū)履羞,或者環(huán))即將開始峦萎。為了標(biāo)識幾何形中某個條帶的結(jié)束和另一個條帶開始的位置,數(shù)組中會包含一個預(yù)留標(biāo)記忆首。隨著OpenGL從元素數(shù)組中抓取頂點(diǎn)數(shù)據(jù)爱榔,OpenGL會監(jiān)測這個預(yù)留標(biāo)記,當(dāng)檢查到該標(biāo)記時糙及,OPenGL會結(jié)束當(dāng)前條帶繪制详幽,并開啟一個新的條帶。
該特性默認(rèn)下是禁用的浸锨,可以調(diào)用函數(shù)glEnable(GL_PRIMITIVE_RESTART);
來啟用改特性唇聘,當(dāng)然調(diào)用函數(shù)glDisable(GL_PRIMITIVE_RESTART);
可以禁用該特性。為索引數(shù)組中某個元素配置標(biāo)記調(diào)用函數(shù)glPrimitiveRestartIndex(index);
揣钦,其參數(shù)index為某個條帶的最后一個頂點(diǎn)對應(yīng)的索引數(shù)組中元素在索引數(shù)組中的下標(biāo)雳灾。需要注意的是,該特性只對有索引的繪圖命令生效冯凹,否則將無效谎亩。
(多個標(biāo)記使用待研究)圖元重啟的默認(rèn)標(biāo)記索引值為0炒嘲,它幾乎是將會包含到模型中的一個真實(shí)頂點(diǎn)。在使用圖元重啟模式時匈庭,將重置索引設(shè)置一個新的值是一個不錯的選擇夫凸。一個不錯的值是使用期數(shù)據(jù)類型下的最大值,如4字節(jié)類型數(shù)據(jù)時使用0xFFFFFFFF阱持,因?yàn)樵撍饕祹缀醪豢赡苁且粋€有效的頂點(diǎn)索引夭拌。
大多數(shù)條帶化工具都包含一個是否使用重啟索引創(chuàng)建分離條帶或者創(chuàng)建單個條帶的選項(xiàng)。條帶化工具可能使用了一個預(yù)定義的索引或者直接輸出它在創(chuàng)建條帶化版本模型時使用的索引值(例如一個比頂點(diǎn)數(shù)組容量更大的值)衷咽。在確定這個值后必須調(diào)用函數(shù)glPrimitiveRestartIndex()
以使用工具的輸出值鸽扁。下圖說明了圖元重啟時標(biāo)記的工作原理。
上圖中镶骗,圓圈內(nèi)的數(shù)字表示頂點(diǎn)對于的索引值桶现。圖a中,該條帶由由17個頂點(diǎn)組成鼎姊,共形成了15個三角形骡和。在啟用圖元重啟屬性,并將重啟索引設(shè)置為8后相寇,OpenGL將會識別到該特殊標(biāo)記慰于,并作出響應(yīng),最后繪制出連個三角形條帶唤衫,分別還包含8個頂點(diǎn)和6個三角形婆赠。
2.2.2 舉例(Instancing)
當(dāng)大量重復(fù)繪制某個模型,比如繪制一片草地佳励,或者一片星空時可能會耗費(fèi)很多時間页藻。這中情況通常會出現(xiàn)上千個副本,他們都是單一幾何集合的重復(fù)植兰,每個副本之間只有很小的變化。一個簡單的應(yīng)用可能循環(huán)的繪制草葉璃吧,在每次繪制時調(diào)用函數(shù)glDrawArrays()
楣导,并在每次繪制迭代中更新Uniform類型變量的值。假定每片草葉都由一個包含4個三角形的條帶組成畜挨,這種簡單程序的代碼如下筒繁。
glBindVertexArray(grass_vao);
for (int n = 0; n < number_of_blades_of_grass; n++) {
SetupGrassBladeParameters();
glDrawArrays(GL_TRIANGLE_STRIP, 0, 6);
}
然而,一片草地上的葉子數(shù)量(number_of_blades_of_grass)可能達(dá)到上千個甚至高達(dá)幾百萬個巴元。每片草葉在屏幕上只占一小塊地方毡咏,并且每片草葉包含的頂點(diǎn)數(shù)量也很小。此時逮刨,GPU渲染單片草葉的時間并不長呕缭,但是會耗費(fèi)大量的時間來發(fā)送繪制命令。OpenGL通過舉例渲染來解決這個問題,該特性通知GPU繪制某個幾何圖形的副本恢总。舉例渲染的函數(shù)如下迎罗。
void glDrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
void glDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void * indices, GLsizei instancecount);
這兩個函數(shù)的功能和函數(shù)glDrawArrays()和glDrawElements()
類似。第一個函數(shù)參數(shù)mode片仿、first纹安、count以及第二個函數(shù)參數(shù)mode、count砂豌、type厢岂、indices和非舉例版本函數(shù)對應(yīng)參數(shù)含義相同。當(dāng)調(diào)用上述函數(shù)時阳距,OpenGL值做一次必要的渲染前準(zhǔn)備操作(如將頂點(diǎn)數(shù)據(jù)拷貝至GPU的內(nèi)存)塔粒,然后多次渲染同一個模型。另外相似的繪制函數(shù)如下娄涩。
void glDrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, GLvoid * indices, GLsizei instancecount, GLint basevertex);
glDrawElementsInstancedBaseInstance()窗怒;
// 以下兩個函數(shù)為所有的直接繪制函數(shù)的最復(fù)雜形態(tài),通過其參數(shù)basevertex和baseinstance設(shè)置為0蓄拣,參數(shù)instancecount設(shè)置為1可以得到和其一般函數(shù)效果類似的繪制操作
glDrawArraysInstancedBaseInstance()扬虚;
glDrawElementsInstancedBaseVertexBaseInstance();
讓舉例渲染和普通渲染不同以及強(qiáng)大的關(guān)鍵是OpenGL提供的內(nèi)置變量gl_InstanceID球恤。它以整形變量的形式出現(xiàn)辜昵,當(dāng)?shù)谝粋€頂點(diǎn)的拷貝被發(fā)送至OpenGL時,其值為0咽斧,每接收到1個拷貝堪置,其值加一,直到增長為例子數(shù)量再減1张惹。函數(shù)glDrawArraysInstanced()
的原理可以用如下代碼解釋舀锨。
// Loop over all of the instances (i.e., instancecount)
for (int n = 0; n < instancecount; n++) {
// Set the gl_InstanceID attribute - here gl_InstanceID is a C variable holding the location of the "virtual" gl_InstanceID input.
glVertexAttrib1i(gl_InstanceID, n);
// Now, when we call glDrawArrays, the gl_InstanceID variable in the shader will contain the index of the instance that’s being rendered.
glDrawArrays(mode, first, count);
}
同樣的函數(shù)glDrawElementsInstanced()
的實(shí)現(xiàn)原理也可以用如下代碼表示。
for (int n = 0; n < instancecount; n++) {
// Set the value of gl_InstanceID
glVertexAttrib1i(gl_InstanceID, n);
// Make a normal call to glDrawElements
glDrawElements(mode, count, type, indices);
}
當(dāng)然宛逗,gl_InstanceID不是一個真正的頂點(diǎn)屬性坎匿,不能通過函數(shù)glGetAttribLocation()
獲取到它的位置。該屬性的值由OpenGL負(fù)責(zé)管理雷激,并且其具有硬件加速機(jī)制替蔬,這意味著它的使用基本不會增加性能負(fù)擔(dān)。正是由于該變量的靈活使用以及舉例數(shù)組屎暇,才使得舉例渲染具有很強(qiáng)大的性能優(yōu)勢承桥。
gl_InstanceID的值可以被直接用于著色器函數(shù)的參數(shù),或者用作數(shù)組索引取獲取紋理或者統(tǒng)一變量數(shù)組中的成員根悼⌒滓欤回到草地的例子中蜀撑,需要找到一個方法使用變量gl_InstanceID繪制出在不同位置生長的上千片草葉。每片草葉由6個頂點(diǎn)唠帝,4個三角形的三角形帶組成屯掖。這里需要使用一點(diǎn)小技巧讓它們看上去都不相同。另外襟衰,使用著色器魔法贴铜,我們能夠讓草地上的每一片草葉看上去都不同從而得到有意思的結(jié)果。這里不會展示著色器代碼瀑晒,只討論如何使用gl_InstanceID為場景增加變化绍坝。
首先,必須為每一片草葉分配不同的位置苔悦。如果需要渲染的草葉數(shù)量是2的冪轩褐,那么可以將變量gl_InstanceID一半的二進(jìn)制位用于表示x坐標(biāo),剩余的二進(jìn)制位表示z坐標(biāo)玖详。在草地中把介,草地的平面為xz坐標(biāo)平面,而y坐標(biāo)表示其高度蟋座。在這個例子中拗踢,渲染2^20
片草葉(104,8576),使用0-9位表示x坐標(biāo)向臀,10-19位表示z坐標(biāo)巢墅,此時可以得到一個由草葉組成的網(wǎng)格,其中的每片草葉都可以看做是另外一片草葉平移所得券膀,其繪制效果如圖君纫。草地Demoyua。
上圖效果看上去草葉分布過于規(guī)律芹彬,并不像生活中的草地蓄髓。為了讓草地效果更逼真,需要將每片草葉的位置做一些隨機(jī)的微調(diào)整舒帮。生成隨機(jī)數(shù)的一個簡便方法是將一個隨機(jī)數(shù)種子乘以一個大數(shù)双吆,然后取其乘積二進(jìn)制位的子集,將該結(jié)果作為隨機(jī)函數(shù)輸出值并將其用于下一次迭代中的隨機(jī)數(shù)種子会前。這里并不需要一個完美的隨機(jī)數(shù)生成器,該簡單函數(shù)足夠滿足需求匾竿。另外通常在該類型算法中瓦宜,需要在下一次迭代中重用上一次生成的隨機(jī)數(shù),但是在該示例中岭妖,每次迭代使用的隨機(jī)種子直接指定為變量gl_InstanceID临庇,并且每次生成兩個連續(xù)的隨機(jī)數(shù)分別用于表示x和z值反璃。此時可以得到如下所示結(jié)果。
此時假夺,盡管每片草葉的位置上面有了隨機(jī)的改變淮蜈,但是每片草葉的形態(tài)仍是完全一致的。實(shí)際上已卷,使用了和生成隨機(jī)位置偏移一樣的隨機(jī)數(shù)生成器處理顏色梧田,以使得每片草葉的顏色有一定的差異。現(xiàn)在還需要為每片草葉的形態(tài)做出一些調(diào)整侧蘸,以使得草地看上去更加真實(shí)裁眯。因此,在該實(shí)例中選用紋理來保存草葉的朝向和長度信息讳癌。使用紋理中的紅色分量來表示長度值穿稳,將其和草葉的頂點(diǎn)坐標(biāo)的y值相乘從而使得草葉變長或變短。長度為0時草葉消失晌坤,長度為1時草葉為其最長值逢艘。按照上述設(shè)想,只需要設(shè)計一張紋理骤菠,其中每個紋素包含對應(yīng)坐標(biāo)草葉的長度信息即可完善該方案它改。
接下來需要調(diào)整草葉繞y軸上的旋轉(zhuǎn)角度,使用紋理中的綠色分量來保存角度信息娩怎,0表示不旋轉(zhuǎn)搔课,1表示旋轉(zhuǎn)360度。此時仍用前文提到的隨機(jī)數(shù)生成函數(shù)來初始化每個草葉的旋轉(zhuǎn)角度截亦。最后渲染結(jié)果如下爬泥。
此時草地的效果看上去仍然不是很真實(shí),所有的草葉都筆直向上崩瓤,并且都沒有移動袍啡。真正的草地會隨風(fēng)擺動,并且當(dāng)有物體從上面滾過時會被壓倒却桶。此時還需要讓草葉彎曲境输。這里使用紋理的藍(lán)色分量來表示彎曲因子。在使用綠色分量前先使用藍(lán)色分量的數(shù)據(jù)時草葉繞x軸旋轉(zhuǎn)(需要注意的是這里變化模型的參考坐標(biāo)系都是世界坐標(biāo)系)颖系。仍然用0表示未彎曲嗅剖,用1表示平躺在地上。通常嘁扼,草葉只會輕微彎曲信粮,因此該值一般較小。
最后趁啸,還需要控制草葉的顏色强缘。邏輯上看上去只需要將顏色信息存儲在一個大的紋理中即可督惰。這種方式在繪制一個復(fù)雜的包含線條、記號以及廣告等元素的運(yùn)動場時是一個很好的想法旅掂,但是此處用于存儲草的顏色確實(shí)是一種浪費(fèi)赏胚。更聰明的方法是使用前文紋理中剩余的alpha通道數(shù)據(jù)和一個調(diào)色板來完成顏色存儲需求。alpha通道中存儲在顏色通道中的索引值商虐,調(diào)色板中的顏色從枯草黃色逐漸過渡至翠綠色觉阅。應(yīng)用上述操作后期渲染結(jié)果如下(這里調(diào)色板的1維紋理數(shù)據(jù)未在原著示例中找到,依然使用隨機(jī)顏色)称龙。
最后的草坪包含上百萬片草葉留拾,它們均勻分布,另外應(yīng)用控制了他們的長度鲫尊,朝向痴柔,彎曲度以及顏色。輸入著色器的唯一變量為gl_InstanceID疫向,它使得每一片草葉都不盡相同咳蔚,發(fā)送給OpenGL的頂點(diǎn)數(shù)據(jù)總共只有6個頂點(diǎn)福贞,該示例中的渲染命令僅僅包含一句代碼glDrawArraysInstanced()
拥娄。
可以使用線性紋理采樣的方式使得不同區(qū)域之間的草葉平滑過渡,但是該方式得到的只是低分辨率的紋理肌幽。如果想生成草葉隨風(fēng)擺動舌涨,以及軍隊(duì)行軍經(jīng)過的踐踏感糯耍,只需在每次繪制前對紋理進(jìn)行適當(dāng)更新從而得到動畫效果。當(dāng)然囊嘉,由于gl_InstanceID被用于生成隨機(jī)數(shù)的隨機(jī)種子温技,因此在將其傳遞至隨機(jī)數(shù)生成器之前加上一個偏移值也能夠出現(xiàn)類似的動畫效果。
自動獲取數(shù)據(jù)(Getting Your Data Automatically)
當(dāng)使用繪制函數(shù)glDrawArraysInstanced()和glDrawElementsInstanced()
時扭粱,在著色器中就可以使用內(nèi)置的變量gl_InstanceID舵鳞,它表示當(dāng)前處理的幾何形在數(shù)組中的索引,并且每繪制一個實(shí)例琢蛤,該值加1蜓堕。當(dāng)為使用舉例繪制函數(shù)時,該值為0博其。
可以使用變量gl_InstanceID在和實(shí)例數(shù)組同等大小的數(shù)組中取值套才。事實(shí)上,此處可以假定任務(wù)著色器中包含一個舉例屬性(instanced attribute)慕淡。也就是說每當(dāng)繪制完一個實(shí)例時霜旧,該屬性的值將會被更新。OpenGL中將數(shù)據(jù)按這種方式讀入的操作由舉例數(shù)組特性支持。在使用舉例數(shù)組時挂据,在著色器中聲明變量的方式同往常一樣。其數(shù)據(jù)的讀也和普通屬性一樣采用函數(shù)glVertexAttribPointer()
儿普。通常崎逃,頂點(diǎn)屬性的更新規(guī)則為每處理一個頂點(diǎn),著色器中相應(yīng)的屬性會被更新一次眉孩。然而个绍,為了讓OpenGL在每繪制完成一個實(shí)例后再對屬性進(jìn)行更新,必須調(diào)用函數(shù)void glVertexAttribDivisor(GLuint index, GLuint divisor);
浪汪。
上述函數(shù)的參數(shù)index為屬性的位置巴柿,參數(shù)divisor為屬性每次更新的實(shí)例間隔。如果參數(shù)divisor為0死遭,那么該屬性的更新規(guī)則就是每個頂點(diǎn)操作更新一次广恢,如果其為非0值,那么每處理完divisor設(shè)置數(shù)量的實(shí)例呀潭,對應(yīng)屬性就會更新一次钉迷。例如,當(dāng)其值為1時钠署,更新策略就是每個實(shí)例更新一次糠聪。一個運(yùn)用該特性的實(shí)例是當(dāng)需要為每個實(shí)例繪制不同的顏色時。
為了使每個實(shí)例具有不同的位置谐鼎,添加屬性instance_position舰蟆,為了使每個實(shí)例有不同的顏色,添加屬性instance_color狸棍,使用該特性的頂點(diǎn)著色器代碼如下身害。
#version 430 core
in vec4 position;
in vec4 instance_color;
in vec4 instance_position;
out Fragment {
vec4 color;
} fragment;
uniform mat4 mvp;
void main() {
gl_Position = mvp * (position + instance_position);
fragment.color = instance_color;
}
現(xiàn)在著色器中包含了兩個位置變量,它們的更新策略分別是按照每個頂點(diǎn)和每個實(shí)例更新隔缀。函數(shù)glVertexAttribDivisor
可以用于任意類型的屬性题造,在一些高級的應(yīng)用中甚至還能用于矩陣頂點(diǎn)屬性,或者將轉(zhuǎn)換矩陣包裝到同一變量中猾瘸,而使用舉例數(shù)組存儲矩陣權(quán)重因子界赔。該特性能夠用于渲染軍隊(duì)場景,其中每個士兵擁有不同的姿勢牵触,每艘星際戰(zhàn)艦朝著不同的方向飛行淮悼。
片段著色器中的代碼很簡單,只需直接將輸入顏色輸出即可揽思。接下來袜腥,需要聲明數(shù)據(jù)并將數(shù)據(jù)填充到緩存中,然后將緩存綁定至頂點(diǎn)數(shù)組對象上钉汗。該部分代碼如下羹令。
static const GLfloat square_vertices[] =
{-1.0f, -1.0f, 0.0f, 1.0f, 1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f};
static const GLfloat instance_colors[] =
{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f};
static const GLfloat instance_positions[] =
{-2.0f, -2.0f, 0.0f, 0.0f, 2.0f, -2.0f, 0.0f, 0.0f, 2.0f, 2.0f, 0.0f, 0.0f, -2.0f, 2.0f, 0.0f, 0.0f};
GLuint offset = 0;
glGenVertexArrays(1, &square_vao);
glGenBuffers(1, &square_vbo);
glBindVertexArray(square_vao);
glBindBuffer(GL_ARRAY_BUFFER, square_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(square_vertices) + sizeof(instance_colors) + sizeof(instance_positions), NULL, GL_STATIC_DRAW);
glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(square_vertices), square_vertices);
offset += sizeof(square_vertices);
glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(instance_colors), instance_colors);
offset += sizeof(instance_colors); glBufferSubData(GL_ARRAY_BUFFER, offset,
sizeof(instance_positions), instance_positions); offset += sizeof(instance_positions);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (GLvoid *)sizeof(square_vertices));
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, (GLvoid *)(sizeof(square_vertices) + sizeof(instance_colors)));
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
接下來設(shè)置頂點(diǎn)屬性的更新規(guī)則鲤屡。
glVertexAttribDivisor(1, 1);
glVertexAttribDivisor(2, 1);
接下來繪制之前放入緩存重的4個幾何形實(shí)例。每個實(shí)例的instance color和instance position相同福侈,不同實(shí)例之間不同酒来。繪制部分代碼如下。
static const GLfloat black[] = { 0.0f, 0.0f, 0.0f, 0.0f };
glClearBufferfv(GL_COLOR, 0, black);
glUseProgram(instancingProg);
glBindVertexArray(square_vao);
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, 4);
繪制結(jié)果如下圖肪凛,此處僅僅處理了4個矩形堰汉。而對于GPU,它可以輕松的處理上千甚至上百外個實(shí)例而不會出現(xiàn)任何問題伟墙。Chapter 7/7.7-instancedattribs翘鸭。
在使用舉例頂點(diǎn)屬性的時候,可以使用函數(shù)glDrawArraysInstancedBaseInstance()
中的參數(shù)baseInstance(OpenGL 4.2)來修正舉例數(shù)組中數(shù)據(jù)的獲取是使用的索引值戳葵,其原理和參數(shù)basevertex類似就乓。當(dāng)其為0時,讀取數(shù)據(jù)的方式和不帶此參數(shù)的繪制命令相同譬淳。其實(shí)際的索引計算公式為档址。在接下來的例子中會用到該特性。
2.2.3 間接繪制(Indirect Draws)(要求OpenGL 4.3)
到目前為止用到的繪制函數(shù)都是直接繪制邻梆,需要在函數(shù)參數(shù)中指定頂點(diǎn)或者實(shí)例的數(shù)量守伸。此外,OpenGL還提供了一系列的繪制命令允許每次繪制的參數(shù)存在緩存對象中浦妄。這意味著在調(diào)用這些繪制函數(shù)時尼摹,不再需要制定相關(guān)參數(shù),而只需指定參數(shù)所在的緩存對象的位置剂娄。這樣還能為帶來兩個有趣的體驗(yàn)蠢涝。
第一,應(yīng)用可以在繪制之前就生成相關(guān)參數(shù)阅懦,甚至可以離線生成和二,然后將其載入OpenGL中用于渲染圖形。
第二耳胎,能夠在程序運(yùn)行的時候生成渲染參數(shù)惯吕,并在著色器中將這些參數(shù)存至緩存對象中,用于之后的渲染操作怕午。
OpenGL中包含4個間接繪圖命令废登。前兩個都有其對應(yīng)的直接繪制命令版本。如glDrawArraysIndirect(GLenum mode, const void * indirect)對應(yīng)glDrawArraysInstancedBaseInstance()郁惜,glDrawElementsIndirect(GLenum mode, GLenum type, const void * indirect)對應(yīng)glDrawElementsInstancedBaseVertexBaseInstance()
堡距。
對于上述兩個函數(shù),modes都表示圖元類型,可選枚舉變量有GL_TRIANGLES或者GL_PATCHES等羽戒。對于第二個函數(shù)缤沦,參數(shù)type是索引的數(shù)據(jù)格式,如GL_UNSIGNED_INT等易稠。兩個函數(shù)中的參數(shù)indirect都表示數(shù)據(jù)在綁定至GL_DRAW_INDIRECT_BUFFRT目標(biāo)的緩存對象中的偏移值疚俱。該緩存對象中的數(shù)據(jù)在兩個結(jié)構(gòu)中并不相同。使用c語言結(jié)構(gòu)體來解釋其中的數(shù)據(jù)格式如下缩多。
// 函數(shù)glDrawArraysIndirect()中使用的數(shù)據(jù)格式
typedef struct {
GLuint vertexCount;
GLuint instanceCount;
GLuint firstVertex;
GLuint baseInstance;
} DrawArraysIndirectCommand;
// 函數(shù)glDrawElementsIndirect()中使用的數(shù)據(jù)格式
typedef struct {
GLuint vertexCount;
GLuint instanceCount;
GLuint firstIndex;
GLint baseVertex;
GLuint baseInstance;
} DrawElementsIndirectCommand;
上述簡介函數(shù)的調(diào)用和調(diào)用其對應(yīng)的直接繪制函數(shù)類似,不同的是第二個函數(shù)中的firstindex單位是索引個數(shù)养晋,而在使用直接繪制函數(shù)glDrawElements()
時衬吆,其中的參數(shù)indexes是以字節(jié)為單位的,因此這些需要特別留心單位的轉(zhuǎn)化绳泉。上述兩個函數(shù)看上去已經(jīng)很便利逊抡,但是真正使得該特性強(qiáng)大的函數(shù)是他們的擴(kuò)展版本。
void glMultiDrawArraysIndirect(GLenum mode, const void * indirect, GLsizei drawcount, GLsizei stride);
void glMultiDrawElementsIndirect(GLenum mode, GLenum type, const void * indirect, GLsizei drawcount, GLsizei stride);
上述兩個函數(shù)用于處理一組繪制命令零酪,本質(zhì)上它們是在一個繪制命令數(shù)組中多次執(zhí)行了它們一般形式的繪制函數(shù)冒嫡。參數(shù)drawcount制定了數(shù)組中繪制命令結(jié)構(gòu)體的數(shù)量,參數(shù)stride制定了每個結(jié)構(gòu)體首地址之間的內(nèi)存間隔四苇,以字節(jié)為單位孝凌,如果該值為0,那么繪制命令結(jié)構(gòu)體為緊密包裝類型月腋。
上述函數(shù)每一次能處理的繪制命令集數(shù)量由可存儲該命令集的內(nèi)存空間決定蟀架。參數(shù)drawcount的大小可以高達(dá)上百萬,但是當(dāng)每個繪制命令通常占用16或者20字節(jié)榆骚,而需要繪制上百萬次時片拍,此時總共需要200億字節(jié)的可用內(nèi)存空間,并且會花上幾秒甚至幾分鐘來完成渲染操作妓肢。但是通過一個緩存一次處理上萬條繪制命令仍然是非常合理的捌省。在使用該特性時,可以預(yù)加載包含繪制命令參數(shù)數(shù)據(jù)的緩存對象碉钠,或者也可以直接使用在GPU上生成繪制命令的參數(shù)纲缓。當(dāng)再GPU上直接生成繪制命令的時候,在調(diào)用間接繪制命令之前不用關(guān)心這些相關(guān)繪制參數(shù)是否準(zhǔn)備就緒放钦,另外參數(shù)數(shù)據(jù)也不會從GPU傳遞到應(yīng)用中再傳遞回去色徘,這些數(shù)據(jù)一直保存在GPU中。
函數(shù)glMultiDrawArraysIndirect()
的使用簡單實(shí)例如下操禀。
typedef struct {
GLuint vertexCount;
GLuint instanceCount;
GLuint firstVertex;
GLuint baseInstance;
} DrawArraysIndirectCommand;
DrawArraysIndirectCommand draws[] =
{{42, 1, 0, 0}, {192, 1, 327, 0}, {99, 1, 901, 0}};
// Put "draws[]" into a buffer object
GLuint buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, buffer);
glBufferData(GL_DRAW_INDIRECT_BUFFER, sizeof(draws), draws, GL_STATIC_DRAW);
// This will produce 3 draws (the number of elements in draws[]), each
// drawing disjoint pieces of the bound vertex arrays
glMultiDrawArraysIndirect(GL_TRIANGLES, NULL, sizeof(draws) / sizeof(draws[0]), 0);
僅僅打包三條繪制命令并不能體現(xiàn)出該特性的強(qiáng)大褂策,為了充分展示其強(qiáng)大的性能,這里將繪制一個小行星帶,其中包含3萬個小行星斤寂。這里小行星帶的網(wǎng)格數(shù)據(jù)仍然存儲在原著定義的模型文件中耿焊。通過加載該模型文件將示例中的所有模型頂點(diǎn)數(shù)據(jù)加載到緩存對象中,并管理安置頂點(diǎn)數(shù)組對象遍搞。每個子對象都包含一個開始頂點(diǎn)以及描述該子對象的頂點(diǎn)個數(shù)罗侯。使用原書的方法get_sub_object_info()能夠獲取這些信息,當(dāng)然此處本文將會重寫該方法溪猿,在Objective-c的環(huán)境中實(shí)現(xiàn)钩杰。get_sub_object_count()可以獲取子對象的個數(shù)。因此可以以間接繪制的方式完成模型渲染诊县,其設(shè)置繪制命令緩存代碼如下讲弄。
object.load("media/objects/asteroids.sbm");
glGenBuffers(1, &indirect_draw_buffer);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, indirect_draw_buffer);
glBufferData(GL_DRAW_INDIRECT_BUFFER, NUM_DRAWS * sizeof(DrawArraysIndirectCommand), NULL, GL_STATIC_DRAW);
DrawArraysIndirectCommand * cmd = (DrawArraysIndirectCommand *) glMapBufferRange(GL_DRAW_INDIRECT_BUFFER, 0, NUM_DRAWS * sizeof(DrawArraysIndirectCommand), GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
for (i = 0; i < NUM_DRAWS; i++) {
object.get_sub_object_info(i % object.get_sub_object_count(), cmd[i].first, cmd[i].count);
cmd[i].primCount = 1;
cmd[i].baseInstance = i;
}
glUnmapBuffer(GL_DRAW_INDIRECT_BUFFER);
接下來,需要在著色器中獲取到當(dāng)前繪制小行星的索引值依痊,OpenGL并沒有提供現(xiàn)成的數(shù)據(jù)通信方式可以實(shí)現(xiàn)該需求避除。但是,多重間接繪制可以看做值舉例間接繪制胸嘁,因此可以通過使用舉例數(shù)組作為著色器的屬性瓶摆,從而將索引值傳入到著色器中。因此需要為間接繪制命令設(shè)置baseInstance為當(dāng)前索引值以保證著色器能夠正確的從舉例屬性數(shù)組中正確的取值性宏。其頂點(diǎn)著色器中輸入變量的聲明如下群井。
#version 430 core
layout (location = 0) in vec4 position;
layout (location = 1) in vec3 normal;
layout (location = 10) in uint draw_id;
變量position和normal的獲取和之前頂點(diǎn)數(shù)組注入數(shù)據(jù)相同。這里需要單獨(dú)將draw_id的數(shù)據(jù)存儲到一個緩存中衔沼,并將其綁定至當(dāng)前GPU上下文中蝌借。其代碼如下。
glBindVertexArray(object.get_vao());
glGenBuffers(1, &draw_index_buffer);
glBindBuffer(GL_ARRAY_BUFFER, draw_index_buffer);
glBufferData(GL_ARRAY_BUFFER, NUM_DRAWS * sizeof(GLuint), NULL, GL_STATIC_DRAW);
GLuint * draw_index = (GLuint *)glMapBufferRange(GL_ARRAY_BUFFER, 0, NUM_DRAWS * sizeof(GLuint), GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
for (i = 0; i < NUM_DRAWS; i++) {
draw_index[i] = i;
}
glUnmapBuffer(GL_ARRAY_BUFFER);
glVertexAttribIPointer(10, 1, GL_UNSIGNED_INT, 0, NULL);
glVertexAttribDivisor(10, 1);
glEnableVertexAttribArray(10);
著色器中得到當(dāng)前繪制圖形的索引值后指蚁,變可以計算每個小行星的位置菩佑,并且規(guī)律的分布它們。包含詳細(xì)計算代碼的著色器代碼此處省略凝化,詳見示例程序稍坯。其中使用了光照模式以增強(qiáng)立體感,該特性后面會再次講到搓劫。
渲染部分的代碼很簡單瞧哟,為了對比使用多重繪制和不使用多重繪制命令,這里通過宏定義定義了兩個版本的代碼枪向。通常不使用多重繪制命令耗時更長勤揩。
glBindVertexArray(object.get_vao());
if (mode == MODE_MULTIDRAW) {
glMultiDrawArraysIndirect(GL_TRIANGLES, NULL, NUM_DRAWS, 0);
} else if (mode == MODE_SEPARATE_DRAWS) {
for (j = 0; j < NUM_DRAWS; j++) {
GLuint first, count;
object.get_sub_object_info(j % object.get_sub_object_count(), first, count);
glDrawArraysInstancedBaseInstance(GL_TRIANGLES, first, count, 1, j);
}
}
最后繪制出的效果如下圖。Chapter 7/7..9-multidrawindirect秘蛔。
在原書中的例子陨亡,使用的GPU能夠以每秒60幀的性能繪制3萬個(實(shí)際上Demo中繪制了5萬個)不同的模型傍衡,也就是說每秒處理了180萬條繪制命令。每個模型有接近500個頂點(diǎn)负蠕,也就是說每秒渲染的頂點(diǎn)數(shù)高達(dá)10億個蛙埂。
靈活的使用draw_id而不是頂點(diǎn)屬性,能夠渲染出有著更多復(fù)雜變形的幾何體遮糖。例如绣的,可以使用紋理映射來處理物體表面細(xì)節(jié),將不同的表面存儲在一個紋理數(shù)組中欲账,再通過draw_id選取其中固定的某一層屡江。同樣的沒有理由規(guī)定存儲間接命令的緩存對象必須是靜態(tài)的,實(shí)際上赛不,可以受用很多技術(shù)直接在GPU上生成這些繪制命令盼理,他們能夠真正的實(shí)現(xiàn)動態(tài)渲染而不需要程序的介入。
2.3 存儲變換后頂點(diǎn)(Storing Transform Vertices)
OpenGL中允許將頂點(diǎn)俄删、曲面細(xì)分評價或者幾何著色器的結(jié)果存儲至一個或者多個緩存中。該特性被稱為轉(zhuǎn)換反饋(transform feedback)奏路,程序中在著色器管道的前端末尾使用該特性非常高效畴椰。盡管該特性在OpenGL圖形處理管道中是一個不可編程,固定的階段鸽粉,但是它仍然可以高效的裝配斜脂。當(dāng)使用轉(zhuǎn)換反饋后,當(dāng)前著色器管道的前端最后一個著色器會輸出一組特定的屬性触机,并將其寫入到一組緩存中帚戳。
當(dāng)幾何著色器不存在時,頂點(diǎn)或者曲面細(xì)分評估著色器處理的頂點(diǎn)結(jié)果將被記錄儡首。當(dāng)幾何著色器存在時片任,函數(shù)EmitVertex()
生成的頂點(diǎn)數(shù)據(jù)將會被存儲,記錄的數(shù)據(jù)量取決于著色器的中的代碼行為蔬胯。用于存儲上述數(shù)據(jù)的緩存被稱為轉(zhuǎn)換反饋緩存对供。轉(zhuǎn)換反饋類型的緩存中的數(shù)據(jù)可以通過兩種方式讀取,使用函數(shù)glGetBufferSubData()
獲取數(shù)據(jù)氛濒,或者直接使用函數(shù)glMapBuffer()
獲取數(shù)據(jù)在內(nèi)存中的地址产场。它們也可以用做接下來的繪制命令的數(shù)據(jù)源。該部分剩余的內(nèi)容都將圍繞頂點(diǎn)著色器作為管線前段最后階段來展開舞竿。但須注意這不是唯一的情形京景。
2.3.1 使用變換反饋(Using Transform Feedback)
建立轉(zhuǎn)換反饋之前,必須確定圖形管道前端部分哪些輸出結(jié)果需要被記錄骗奖。函數(shù)原型為确徙。
void glTransformFeedbackVaryings(GLuint program, GLsizei count, const GLchar * const * varying, GLenum bufferMode);
參數(shù)program為程序?qū)ο蟮拿中汛D(zhuǎn)換反饋的狀態(tài)有程序保存。這意味著在不同的程序中米愿,盡管使用了相同的著色器厦凤,但是它們?nèi)匀荒軌蛴涗洸煌捻旤c(diǎn)屬性集合。參數(shù)count為需要記錄的屬性個數(shù)育苟。參數(shù)varying是由c語言字符串組成的數(shù)組较鼓,其大小必須和count匹配,這些字符串指定了需要記錄的屬性违柏,它們和頂點(diǎn)著色器中的屬性標(biāo)識符一致博烂。參數(shù)buffermode變量記錄的模式,可選GL_SEPARATE_ATTRIBS和GL_INTERLEAVED_ATTRIBS漱竖。如果選interleaved禽篱,每個變量依次記錄在單個緩存中,反之它們都會記錄在各自的緩存中馍惹。
對于具有以下聲明的如下頂點(diǎn)著色器躺率。
out vec4 vs_position_out;
out vec4 vs_color_out;
out vec3 vs_normal_out;
out vec3 vs_binormal_out;
out vec3 vs_tangent_out;
為了將上述輸出變量存儲在一個交錯存儲轉(zhuǎn)換反饋緩存中,需要在程序中使用如下C語言代碼万矾。
static const char * varying_names[] = {
"vs_position_out",
"vs_color_out",
"vs_normal_out",
"vs_binormal_out",
"vs_tangent_out"
};
const int num_varyings = sizeof(varying_names) / sizeof(varying_names[0]);
glTransformFeedbackVaryings(program, num_varyings, varying_names, GL_INTERLEAVED_ATTRIBS);
并非從頂點(diǎn)(或者幾何)著色器中輸出的所有變量都需要存儲到轉(zhuǎn)換反饋緩存中悼吱。可以存儲輸出變量的子集到轉(zhuǎn)換反饋緩存中良狈,同時將更多的數(shù)據(jù)輸入到片段著色器中用于插值計算后添。同樣的,也可以存儲部分頂點(diǎn)著色器的數(shù)據(jù)到轉(zhuǎn)換反饋緩存中使得片段著色器不會獲得這些數(shù)據(jù)薪丁∮鑫鳎基于這個特性,頂點(diǎn)著色器中不活躍部分的輸出(不會被片段著色器使用)因?yàn)楸淮鎯χ赁D(zhuǎn)換反饋緩存中再次變得活躍严嗜。調(diào)用函數(shù)glTransformFeedbackVaryings()
指定要存儲的輸出子集后需要調(diào)用函數(shù)glLinkProgram(program);
重新鏈接程序粱檀。
當(dāng)改變轉(zhuǎn)換反饋捕獲的子集時,盡管有時這些改變并不會產(chǎn)生任何影響漫玄,但是仍然有必要調(diào)用函數(shù)再次鏈接程序梧税。一旦轉(zhuǎn)換反饋?zhàn)兞勘恢付ǎ⑶页绦虮怀晒︽溄映平鼈兙涂梢员徽J褂玫诙印T谡嬲东@轉(zhuǎn)換反饋?zhàn)兞恐埃枰獎?chuàng)建一個緩存并且將其綁定至一個帶索引的轉(zhuǎn)換反饋綁定點(diǎn)之上刨秆。在此之前還必須為該緩存分配內(nèi)存空間凳谦。代碼如下。
GLuint buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_TARNSFORM_FEEDBACK_BUFFER, buffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, size, NULL, GL_DYNAMIC_COPY);
上述代碼中的參數(shù)GL_DYNAMIC_COPY中DYNAMIC表示緩存中的數(shù)據(jù)經(jīng)常更新衡未,每次更新會使用多次尸执,COPY表示數(shù)據(jù)又OpenGL更新家凯,更新后的數(shù)據(jù)由OpenGL使用,用于類似于繪制等功能如失。
反饋緩存綁定點(diǎn)有多個绊诲,但他們都和一個統(tǒng)一緩存綁定點(diǎn)相關(guān),下圖描述了該關(guān)系結(jié)構(gòu)圖褪贵。
綁定反饋緩存調(diào)用函數(shù)glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, index, buffer);
掂之。參數(shù)GL_..._BUFFER表示緩存的用途,參數(shù)buffer為存儲轉(zhuǎn)換反饋結(jié)果的緩存名字脆丁,參數(shù)index為轉(zhuǎn)換反饋緩存綁定點(diǎn)的索引值世舰。這里需要注意的是調(diào)用該函數(shù)后不能再向其緩存內(nèi)讀寫數(shù)據(jù)或者分配內(nèi)存空間。但是槽卫,在將頂點(diǎn)緩存綁定至帶索引的綁定點(diǎn)同時跟压,OpenGL也將其綁定至通用頂點(diǎn)緩存綁定點(diǎn),只要在調(diào)用該函數(shù)后獲得通用緩存頂點(diǎn)仍能為緩存分配內(nèi)存空間歼培。
綁定反饋緩存更高級的函數(shù)為void glBindBufferRange(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
震蒋,該函數(shù)允許將緩存的一部分綁定至某個綁定點(diǎn),而通常的函數(shù)glBindBuffer()躲庄、glBindBufferBase()
只能將整個緩存綁定至單個索引喷好。該函數(shù)前三個參數(shù)的含義和函數(shù)glBindBufferBase()
相同。參數(shù)offset和zise分別表示需要綁定的緩存內(nèi)存地址的開始位置和大小读跷。使用該函數(shù)可以將輸出頂點(diǎn)屬性以GL_SEPARATE_ATTRIBS
模式分別存儲在單個緩存的不同區(qū)域中。如果應(yīng)用將所有的屬性打包在一個頂點(diǎn)緩存中禾唁,并且指定的偏移量為0效览,這樣就可以很輕易的將轉(zhuǎn)換反饋的輸出結(jié)果和頂點(diǎn)著色器的輸入相匹配。
如果轉(zhuǎn)換反饋屬性輸出結(jié)果的存儲方式為GL_INTERLEAVED_ATTRIBS荡短,那么數(shù)據(jù)將會以緊密包裝的形式寫入到0號轉(zhuǎn)換反饋綁定點(diǎn)(索引值為0)關(guān)聯(lián)的緩存中丐枉。如果存儲方式為GL_SEPARATE_ATTRIBS,那么頂點(diǎn)著色器的每一個轉(zhuǎn)換反饋屬性都會被保存在它們自己的緩存中(或者一個緩存的不同分區(qū))掘托。GPU能支持的最大轉(zhuǎn)換反饋綁定點(diǎn)個數(shù)可以通過函數(shù)glGetIntegerv()
和參數(shù)GL_MAX _TRANSFORM _FEEDBACK _SEPARATE _ATTRIBS獲得瘦锹。
在GL_INTERLEAVED_ATTRIBS模式下并沒有固定的轉(zhuǎn)換反饋?zhàn)兞孔畲髷?shù)量,例如單個緩存中寫入vec3類型的轉(zhuǎn)換反饋?zhàn)兞康臄?shù)量可以比vec4類型的變量闪盔。但是單個緩存中能夠支持的成員數(shù)量是有限制的弯院。同樣的使用函數(shù)glGetIntegerv()
和參數(shù)GL_ MAX_ TRANSFORM_ FEEDBACK_ INTERLEAVED_ COMPONENTS可以獲得。
如果需要可以在轉(zhuǎn)換反饋緩存存儲的輸出數(shù)據(jù)結(jié)構(gòu)的各個成員之間留出內(nèi)存間距泪掀。當(dāng)使用該方式保存數(shù)據(jù)時仔掸,內(nèi)存間距指向的內(nèi)存空間將會被直接跳過蝗碎,不做任何改變。要實(shí)現(xiàn)這個特性抛猖,需要在著色器的聲明和C語言定義的結(jié)構(gòu)體中都包含以下4個虛擬變量的其中一個,gl_SkipComponents1, gl_SkipComponents2, gl_SkipComponents3, or gl_SkipComponents4
啊易,此處被跳過的內(nèi)存空間由這些虛擬變量在著色器中定義的數(shù)據(jù)類型確定。
OpenGL還允許將一個子集交錯的保存在一個緩存中,剩余的子集保存在另外一個緩存中峡竣。要開啟這個特性,需要使用虛擬變量gl_NextBuffer
量九,它表示函數(shù)glTransformFeedbackVaryings()
在讀存儲下一個變量時將會移至下一個綁定點(diǎn)适掰。但是需要注意的是,此時只能使用參數(shù)GL_INTERLEAVED_ATTRIBS
以交錯的方式保存數(shù)據(jù)娩鹉。示例如下攻谁。
static const char * varying_names[] = {
"carrots",
"peas",
"gl_NextBuffer",
"beans",
"potatoes"
};
const int num_varyings = sizeof(varying_names) / sizeof(varying_names[0]);
glTransformFeedbackVaryings(program,
num_varyings,
varying_names,
GL_INTERLEAVED_ATTRIBS);
2.3.2 開始、暫停和停止轉(zhuǎn)換反饋(Starting, Pausing, and Stopping Transform Feedback)
當(dāng)設(shè)置緩存變量弯予、類型戚宦,準(zhǔn)備緩存對象等一系列準(zhǔn)備工作完成后,調(diào)用函數(shù)glBeginTransformFeedback(GLenum primitiveMode);
可以激活轉(zhuǎn)換反饋模式锈嫩。此時受楼,管線前端最后一個著色器處理完成后的頂點(diǎn)數(shù)據(jù)將會被存儲到頂點(diǎn)轉(zhuǎn)換反饋緩存中。參數(shù)primitiveMode表示幾何體的類型呼寸,可選值有GL_POINTS, GL_LINES, 和GL_TRIANGLES
艳汽。當(dāng)調(diào)用函數(shù)glDrawArrays()
或其他OpenGL繪圖命令時,基礎(chǔ)的幾何體類型必須和轉(zhuǎn)換反饋緩存中的幾何體類型一致对雪,或者必須包含一個輸出正確的的幾何體類型的幾何著色器河狐。例如,如果參數(shù)primitiveMode為GL_TRIANGLES瑟捣,管道前端的最后一個階段必須輸出三角形馋艺。這就意味著當(dāng)開啟幾何著色器后,其輸出的圖元必須是triangle_strip迈套,如果包含曲面細(xì)分評估著色器并且沒有幾何著色器時捐祠,輸出模式也必須為三角形,如果兩者都不包含時調(diào)用繪制函數(shù)時必須指定GL_TRIANGLES, GL_TRIANGLE_STRIP 或者 GL_TRIANGLE_FAN
桑李。
另外GL_PATCHES也能用于繪制命令的參數(shù)mode踱蛀,只要曲面細(xì)分評估著色器或者幾何著色器輸出了正確的圖元類型。當(dāng)轉(zhuǎn)換反饋模式激活后贵白,臨時暫停該功能可以調(diào)用函數(shù)glPauseTransformFeedback()
率拒,重啟該功能可以調(diào)用函數(shù)glResumeTransformFeedback()
,此時OpenGL將從Buffer中上次暫停時的位置繼續(xù)記錄禁荒,只要轉(zhuǎn)換反饋功能未暫停俏橘,OpenGL會持續(xù)記錄轉(zhuǎn)換反饋輸出的數(shù)據(jù),直到退出轉(zhuǎn)換反饋或者緩存空間耗盡圈浇。退出轉(zhuǎn)換反饋調(diào)用函數(shù)glEndTransformFeedback();
寥掐。
每次當(dāng)函數(shù)glBeginTransformFeedback()
調(diào)用靴寂,OpenGL會從當(dāng)前綁定的轉(zhuǎn)換反饋緩存的起始位置寫入數(shù)據(jù),這里可能會出現(xiàn)重寫現(xiàn)象召耘。需要注意的是當(dāng)轉(zhuǎn)換反饋特性處于激活狀態(tài)時百炬,某些操作是不被允許的,無論是否被暫停污它。例如改變緩存的綁定或者重新分配緩存的內(nèi)存空間剖踊。
2.3.3 使用轉(zhuǎn)換反饋結(jié)束管道(Ending the Pipeline with Transform Feedback)
在使用轉(zhuǎn)換反饋特性的應(yīng)用中,更多的是記錄轉(zhuǎn)換反饋階段生成的結(jié)果衫贬,并不需要真正的繪制任何東西德澈。由于光柵化階段在圖形處理管道中位于轉(zhuǎn)換反饋階段之后,因此可以通過調(diào)用函數(shù)glEnable(GL_RASTERIZER_DISCARD);
來關(guān)閉光柵化階段及其后續(xù)階段固惯。該函數(shù)調(diào)用后梆造,轉(zhuǎn)換反饋執(zhí)行后,OpenGL將不會繼續(xù)處理圖元數(shù)據(jù)葬毫。調(diào)用函數(shù)glDisable(GL _RASTERIZER _DISCARD);
可以重新開啟光柵化階段镇辉。
2.3.4 轉(zhuǎn)換反饋示例-物理模擬(Transform Feedback Example -- Physical Simulation)
在彈簧質(zhì)點(diǎn)(springmass)模型中,將會建立一個彈簧和質(zhì)量的物理模擬贴捡。每個代表單位質(zhì)量的頂點(diǎn)都和最大4個相鄰頂點(diǎn)咦彈性繩相連忽肛。除了一個常規(guī)的屬性數(shù)組,該示例中還使用一個紋理緩存對象(TBO)持有頂點(diǎn)位置數(shù)據(jù)烂斋。同一個緩存與TBO和為頂點(diǎn)著色器提供位置輸入的頂點(diǎn)屬性相關(guān)聯(lián)屹逛。這樣就可以隨意的獲取其他頂點(diǎn)的當(dāng)前位置。同時使用一個整形頂點(diǎn)屬性來持有相鄰頂點(diǎn)的索引值汛骂。此外罕模,還使用轉(zhuǎn)換反饋來存儲每次迭代算法中的每個質(zhì)點(diǎn)的位置和加速度。
對于每個頂點(diǎn)香缺,需要一個位置,加速度和質(zhì)量歇僧⊥颊牛可以將位置和質(zhì)量打包進(jìn)入一個頂點(diǎn)數(shù)組中,將加速度放入另外一個數(shù)組中诈悍。位置數(shù)組的每一個元素都是一個vec4變量祸轮,其中x、y侥钳、z分量保存了頂點(diǎn)的三維坐標(biāo)适袜,w分量保存了頂點(diǎn)的重量。加速度數(shù)組的每個元素為vec3類型變量舷夺。另外苦酱,使用一個ivec4的數(shù)組來保存關(guān)于將質(zhì)點(diǎn)連接在一起的彈簧信息售貌。每個頂點(diǎn)都包含1個ivec4變量,向量的4個分量分別表示連接頂點(diǎn)彈簧另外一端的頂點(diǎn)疫萤,該向量被稱為連接向量颂跨。當(dāng)對應(yīng)方向沒有連接時,對應(yīng)的分量值為-1扯饶。該示例描述的模型圖如下所示恒削。
連接向量中各個方向的質(zhì)點(diǎn)連接順序咦頂點(diǎn)12為例可以描述為<11,7尾序,13钓丰,17>。頂點(diǎn)14的鏈接向量可以描述為<13每币,9携丁,-1,19>脯爪。通過將連接向量的4個分量都設(shè)置為-1则北,可以固定一個頂點(diǎn),此時該頂點(diǎn)對應(yīng)的位置和加速度計算都會被跳過痕慢,同時將與該頂點(diǎn)相關(guān)聯(lián)的力設(shè)置為0尚揣。相應(yīng)的對每個頂點(diǎn)的初始化代碼此處省略,具體請參考示例源碼555掖举。
為了更新整個系統(tǒng)快骗,使用一個頂點(diǎn)著色器通過常規(guī)的頂點(diǎn)屬性獲取每個頂點(diǎn)自己的位置和連接向量。借下來使用連接向量(同時也是一個常規(guī)的頂點(diǎn)屬性)中的元素作為在TBO中的索引值塔次,從而獲得其當(dāng)前連接頂點(diǎn)的當(dāng)前位置方篮。TBO的初始化請參照示例源碼555。
對于每一個連接頂點(diǎn)励负,著色器能夠計算出它們之間的距離藕溅,這樣就能計算出他們之間虛擬彈簧的張力〖逃埽基于此巾表,可以計算出通過彈簧施加在質(zhì)點(diǎn)上的力,結(jié)合質(zhì)量計算出該張力產(chǎn)生加速度略吨,并且計算出下一次迭代中使用的新位置和加速度向量集币。這只是牛頓物理學(xué)和胡克定律。胡克定律的公式如下翠忠。
在該公式中F為彈簧的張力鞠苟,k是彈性系數(shù),x是彈簧形變長度。在該示例中当娱,彈簧的放松長度被設(shè)置為一個常量并存放在一個Uniform類型變量中吃既。x有正負(fù),正值表拉伸趾访,負(fù)值表示壓縮态秧。物理上的力是一個向量,此處力的表示方法如下扼鞋,其中d為沿著彈簧方向的標(biāo)準(zhǔn)向量申鱼。
如果簡單的將這個力直接施加在質(zhì)點(diǎn)上,系統(tǒng)將會震蕩云头,并且由于數(shù)值上的誤差捐友,系統(tǒng)最終將會變得不穩(wěn)定。現(xiàn)實(shí)生活中的彈簧系統(tǒng)都會由于摩擦力產(chǎn)生一定的損失溃槐,為了模擬這個特性可以將阻尼考慮到力的方程中匣砖。阻尼引起的力可以由以下方程表示。
其中c表示阻尼系數(shù)昏滴。理論情況下猴鲫,可以計算出每一條彈簧的阻力,在這個簡單系統(tǒng)中谣殊,基于質(zhì)點(diǎn)速度的力可以完成該任務(wù)拂共。同樣的,在每個時間階段使用初始速度來估計這個等式所需要的持續(xù)的差異姻几。在著色器中宜狐,通過將阻力和彈力相加計算出合力F。最后再將重力帶入等式中既可以得到每個質(zhì)量的最終合力可以表示為如下等式蛇捌。需要注意的是合力的計算方式應(yīng)該是所有向量力的和抚恒,彈簧力由于原書中使用的是作用點(diǎn)到施力點(diǎn)的標(biāo)準(zhǔn)向量和形變距離的負(fù)數(shù),因此需要添加負(fù)號络拌,而阻尼力又和速度的方向相反俭驮,因此也需要取負(fù)。
得到合力后春贸,根據(jù)牛頓定律可以很快的計算出每個質(zhì)點(diǎn)的加速度混萝。可以描述為如下等式祥诽。
這里譬圣,F(xiàn)為上一個等式所計算出的合力瓮恭,m是頂點(diǎn)的質(zhì)量(存儲在位置屬性的w分量中)雄坪,a是計算出的加速度。將初始加速度放到下面的等式中便可以計算出在確定時間的速度和位移。
此處u是初始速度(從速度屬性數(shù)組中獲取)维哈,v是最終速度绳姨,t是時間,s為位移阔挠。需要記住的是飘庄,這些變量都是向量。頂點(diǎn)著色器的源碼請參照Chapter 7/7.13-springmass购撼。
執(zhí)行該著色器后跪削,應(yīng)用會迭代更新緩存對象中的頂點(diǎn)數(shù)據(jù)。此時需要使用兩個緩存對象來保存頂點(diǎn)的位置和速度信息迂求,我們從一個buffer中讀取數(shù)據(jù)碾盐,并將新的數(shù)據(jù)寫入另外一個緩存中,在下一次迭代時交換兩個緩存對象的角色來實(shí)現(xiàn)數(shù)據(jù)的更新揩局。作為一個常量毫玖,每一次的連接信息都一致×瓒ⅲ可以通過之前設(shè)置好的VAO數(shù)組來實(shí)現(xiàn)該功能付枫。第一個VAO對象有一個位置和速度屬性集合,以及相同的連接信息驰怎。第二個VAO對象包含另外一組位置和速度屬性集合阐滩,以及相同的連接信息。
除了VBO數(shù)組砸西,我們還需要TBO數(shù)組叶眉。對于位置頂點(diǎn)緩存對象VBO,同時我們將其關(guān)聯(lián)至紋理緩存對象TBO芹枷。這也許看上去非常奇怪衅疙,但是在OpenGL的語法中,這是合法的鸳慈。我們可以通過兩個不同的方法從同一個緩存中讀取數(shù)據(jù)饱溢。為了完成上述目標(biāo),生成兩個紋理并將他們綁定至GL_TEXTURE_BUFFER綁定點(diǎn)走芋,并使用前文講到的glTexBuffer函數(shù)將緩存和紋理關(guān)聯(lián)绩郎。此時頂點(diǎn)位置屬性和samplerBuffer類型變量tex_Position中會有相同的數(shù)據(jù)。
應(yīng)用固定了部分頂點(diǎn)因此整個系統(tǒng)并不會全部墜落到屏幕底部翁逞。一旦我們掛載了所有緩存肋杖,只需調(diào)用函數(shù)glDrawArrays()就能模擬系統(tǒng)自由下落的物理現(xiàn)象。系統(tǒng)中的每個節(jié)點(diǎn)都有一個GL_POINTS圖元表示挖函。系統(tǒng)初始化后可以得到以下結(jié)果状植。
在每一幀,我們都會運(yùn)行物理模擬多次,每一次迭代都會交換VAO數(shù)組和TBO數(shù)組津畸。迭代循環(huán)的代碼請參照Chapter 7/7.13-springmass振定。每一次迭代所有節(jié)點(diǎn)的位置和速度信息都會被更新一次。通過減少時間梯度可以讓整個系統(tǒng)的模擬變得更加流暢肉拓,從而得到更好的視覺效果后频。
在迭代時,禁用光柵化功能暖途,使得數(shù)據(jù)經(jīng)歷了轉(zhuǎn)換反饋階段后不會再沿著圖形處理管道繼續(xù)流動卑惜。在迭代完成后重新啟用光柵化功能使圖形被渲染到屏幕上。在經(jīng)歷足夠多的迭代后驻售,我們能夠以我們希望的方式繪制出所有的頂點(diǎn)残揉。使用一個簡單的程序來渲染圖形,將系統(tǒng)中所有節(jié)點(diǎn)以點(diǎn)的方式繪制芋浮,他們之間的連接以線的方式繪制抱环。源碼請參照Chapter 7/7.13-springmass。繪制結(jié)果參照上圖纸巷。
在繪制出點(diǎn)后镇草,通過咦GL_LINES圖元類型和有索引繪制函數(shù)glDrawElements繪制出節(jié)點(diǎn)之間的連接線使物理模擬更加逼真。第二次繪制時可以使用相同的頂點(diǎn)位置瘤旨,但是我們需要構(gòu)建另外一個綁定至GL_ELEMENT_ARRAY的緩存對象梯啤,其中必須包含每個彈簧兩端的頂點(diǎn)索引值。額外的操作和源代碼請參照Chapter 7/7.13-springmass存哲。最終的繪制結(jié)果如下圖因宇。
當(dāng)然物理模擬(以及其生成的頂點(diǎn))可以被用于任何場景。例如祟偷,盡管還非巢旎基礎(chǔ),該技術(shù)可以用于模擬衣服的自然下墜修肠。該系統(tǒng)并不能處理內(nèi)部節(jié)點(diǎn)的相互作用(self-interaction)贺辰,但是這對于現(xiàn)實(shí)的衣服模擬并不重要。然而很多系統(tǒng)內(nèi)部的粒子相互作用都是通過一種特定的方式嵌施,該規(guī)律可以通過單個頂點(diǎn)著色器和轉(zhuǎn)換反饋模擬并建模饲化。
2.4 剪切(Clipping)
正如在章節(jié)跟隨管線“Following the Pipeline”中提到的,剪切階段確定了哪些圖元能夠完全顯示或者部分顯示吗伤,并且用它們構(gòu)建新的圖元以完整展示在整個視口內(nèi)吃靠。
點(diǎn)圖元的裁剪邏輯很簡單,如果該店的坐標(biāo)位于可視范圍內(nèi)就進(jìn)入下一階段足淆,反之則被丟棄巢块。線圖元的剪切稍復(fù)雜一點(diǎn)捺球,如果線的兩個端點(diǎn)都位于裁剪空間的同一個平面外(如兩個點(diǎn)的x分量都小于-1.0)(B),該線圖元直接被丟棄夕冲。如果線的兩個頂點(diǎn)都位于裁剪空間內(nèi),那么該圖元會被進(jìn)一步處理(A)裂逐。如果兩個端點(diǎn)一個位于剪裁空間內(nèi)部歹鱼,一個位于剪裁空間外部(C),或者這個線有一部分在剪裁空間內(nèi)卜高,那么該圖元將會被裁剪(D)弥姻,裁減掉超出剪裁空間的部分,形成一條新的更短線掺涛。剪裁邏輯示意如下庭敦。E是一個特殊案例,在確定被丟棄之前可能會經(jīng)過剪裁薪缆,涉及到剪裁數(shù)學(xué)邏輯秧廉,不展開。
三角形的裁剪問題看上去更加復(fù)雜拣帽,但實(shí)際上使用了相同的方式疼电。和點(diǎn)類似,如果三角形的三個頂點(diǎn)全部在剪裁空間外將會被丟棄(B)减拭,如果全部在空間內(nèi)將會被直接發(fā)送到下一個處理流程(A)蔽豺。如果三個頂點(diǎn)在剪裁空間內(nèi)外都有分布,那么它會被裁剪為多個三角形拧粪。下圖用兩維空間示意修陡,但是需要知道實(shí)際上剪裁空間是一個三維的模型。
防護(hù)帶(The Guard Band)
正如上圖所示可霎,三角形被剪切后會分裂為多個小三角形魄鸦,這會給以固定速率處理三角形的GPU帶來問題。在某些情況下癣朗,直接將這些三角形傳入下一個階段号杏,讓光柵化器丟棄不可見部分會使應(yīng)用運(yùn)行更快,提升性能斯棒。為了達(dá)到這個目的盾致,一些GPU帶有防護(hù)帶特性,它是位于剪裁空間外的區(qū)域荣暮,該空間內(nèi)的三角形盡管不可見但是仍不會被裁剪庭惜,直接進(jìn)入下一步處理流程。防護(hù)帶示意圖如下穗酥。
防護(hù)帶的存在并不會影響全部保留(A)和全部剔除(B)的三角形护赊,它們們?nèi)园粗暗倪壿嫳惶幚砘荻簟A硗猱?dāng)三角形超出了剪裁空間時,當(dāng)其未超出防護(hù)帶時會被發(fā)送至下一階段(C/D)骏啰,反之仍會被裁剪(E)节吮。
實(shí)際上防護(hù)帶的寬度(內(nèi)外矩形之間的間距)非常大,幾乎和視口空間一樣大判耕,只有繪制非常大的矩形時才會超出其邊界透绩。這些特性盡管不能夠以可視化方式呈現(xiàn),但是其能夠提升程序的性能壁熄。
2.4.1 用戶定義的剪切(User-Defined Clipping)
點(diǎn)位于平面哪一側(cè)的可以通過計算帶點(diǎn)到平面的有向距離確定帚豪,其值表示點(diǎn)到平面的距離,符號表示其位于哪一側(cè)草丧。OpenGL不一定采用這種方式狸臣,但是在自己的代碼中可以使用這個算法。
除了到視圖截頭椎體六個表面的六個距離外昌执,應(yīng)用中還可以使用另外一組距離烛亦,他們可以在頂點(diǎn)或者幾何著色器中設(shè)置。頂點(diǎn)著色器中可以通過內(nèi)置變量gl_ClipDistance[]來設(shè)置裁剪距離懂拾,該變量為一個浮點(diǎn)型的數(shù)組此洲。正如本章之前講到的gl_ClipDistance[]是gl_PerVertex閉包的成員,它能夠在最后一個著色器是頂點(diǎn)委粉、曲面細(xì)分評估或者幾何著色器的時候設(shè)置呜师。剪切距離能夠支持的個數(shù)取決于OpenGL的具體實(shí)現(xiàn)方式。這些距離可以看做是內(nèi)置的剪切距離贾节。在應(yīng)用中調(diào)用函數(shù)glEnable(GL_CLIP_DISTANCE0 + n);
可以啟用用戶自定義剪切距離功能汁汗。
這里這需要開啟的剪切距離索引值,他們可以在標(biāo)準(zhǔn)OpenGL頭文件中找到栗涂。最大值可以通過函數(shù)glGetIntegerv(GL_MAX_CLIP_DISTANCES)
獲得知牌。同時可以調(diào)用函數(shù)glDisable()
以及相同參數(shù)來關(guān)閉該功能。如果某個索引值的剪切距離沒有被啟用斤程,那么使用數(shù)組gl_ClipDistance[]
寫入值時將會自動被忽略角寸。
正如內(nèi)置的剪切平面一樣,寫入數(shù)組gl_ClipDistance[]
中的距離符號用于決定該頂點(diǎn)是位于用戶定義的裁剪空間內(nèi)部還是外部忿墅。如果單個三角形圖元的每個頂點(diǎn)的符號都為負(fù)扁藕,那么該三角形將會被裁剪。如果部分位于三角形外疚脐,部分位于三角形內(nèi)亿柑,那么OpenGL會對三角形內(nèi)的每一個像素進(jìn)行距離的線性插值運(yùn)算以決定它們是否可見。該功能使得用戶可以沿著任意平面集合裁剪集合圖形(點(diǎn)到平面的距離可以通過點(diǎn)乘獲得)棍弄。
數(shù)組gl_ClipDistance[]
中作為片段著色器的輸入望薄,在片段著色器內(nèi)部也是可用的疟游。任意片段只要在該數(shù)組中存在一個值為負(fù),那么它將被裁剪掉痕支,不會進(jìn)入到片段著色器颁虐。但是當(dāng)所有值為正時該片段能正常到達(dá)片段著色器,此時可以讀取對應(yīng)的gl_ClipDistance[]
值卧须。在示例程序中基于片段的裁剪距離接近0的程度減少其alpha值從而使用該功能來隱藏片段另绩。該特性使得通過頂點(diǎn)著色器沿著一個平面裁剪的大圖元以平滑的方式隱藏,或者在片段著色器中對它實(shí)現(xiàn)抗鋸齒效果而不會生成一個非常明顯的剪切邊。
需要注意的是如果一個圖元的所有頂點(diǎn)沿著一個同一個平面被裁剪掉,那么整個圖元都會被清除西设。但是在處理點(diǎn)圖元和線圖元的時候需要特別小心汁针。在繪制點(diǎn)圖元的時候可以通過變量gl_PointSize設(shè)置大于1的值,這時當(dāng)頂點(diǎn)的中心位于可視范圍外時津辩,盡管加大后的頂點(diǎn)部分位于可視范圍內(nèi)拆撼,但是整個頂點(diǎn)仍然會被裁剪掉。同樣的在繪制線圖元時可以設(shè)置線的寬度喘沿,它的處理邏輯和點(diǎn)圖元相同闸度。
下面代碼展示了頂點(diǎn)著色器如何寫入兩個裁剪距離。第一個裁剪距離為物體空間的頂點(diǎn)到一個四維向量定義的片面clip_plane蚜印。第二個裁剪距離為每個頂點(diǎn)到球的距離莺禁。首先獲取從物體空間中頂點(diǎn)到球體中心的向量長度,再將其減去球體的半徑(存儲在clip_sphere的w分量中)窄赋。
#version 410 core
// More uniforms here
// Clip plane
uniform vec4 clip_plane = vec4(1.0, 1.0, 0.0, 0.85);
uniform vec4 clip_sphere = vec4(0.0, 0.0, 0.0, 4.0);
void main() {
// Lighting code goes here
// Write clip distances
gl_ClipDistance[0] = dot(position, clip_plane);
gl_ClipDistance[1] = length(position.xyz / position.w - clip_sphere.xyz) - clip_sphere.w;
// Calculate the clip-space position of each vertex
gl_Position = proj_matrix * P;
}
裁剪結(jié)果如下圖所示哟冬,可以看見模型沿著屏幕和球體被裁剪,源代碼見Chapter 7-17 Clipdistance忆绰。
2.5 總結(jié)(Summary)
本章包含了OpenGL從應(yīng)用提供的緩存中讀取頂點(diǎn)數(shù)據(jù)的部分細(xì)節(jié)浩峡,以及如何匹配頂點(diǎn)著色器的輸入頂點(diǎn)數(shù)據(jù)以及應(yīng)用中輸入的頂點(diǎn)數(shù)據(jù)。同時也討論了頂點(diǎn)著色器的職責(zé)以及他能寫入的內(nèi)部輸出變量错敢。頂點(diǎn)著色器不僅能設(shè)置它所產(chǎn)生的頂點(diǎn)的位置翰灾,還能設(shè)置他渲染的頂點(diǎn)大小,甚至能控制剪切過程使得用戶可以根據(jù)任意形狀裁剪模型稚茅。
OpenGL提供轉(zhuǎn)換反饋功能纸淮,它的強(qiáng)大功能使得頂點(diǎn)著色器可以將任意數(shù)據(jù)存儲在緩存中。本章介紹了OpenGL如何沿著窗口的可見區(qū)域裁剪圖元亚享,以及圖元從一個裁剪空間中的應(yīng)用過度到多個裁剪空間的應(yīng)用萎馅。下一章將介紹圖形處理管道的前端終端曲面細(xì)分和幾何著色器。