OpenGL ES 的渲染管線包含有一個可編程的頂點階段和一個可編程的片段階段。其餘的階段則有固定的功能,應用程序對其行爲的控制非常有限。每個可編程階段中編譯單元的集合組成了一個着色器。在OpenGL ES 2.0 中,每個着色器只支持一個編譯單元。着色程序則是一整套編譯好並鏈接在一起的着色器的集合。着色器 shader 的編寫需要使用着色語言 GL Shader Language(GLSL),GLSL 的語法與 C 語言很類似。
// 頂點着色器 .vsh
attribute vec4 position;
attribute vec4 color;
varying vec4 colorVarying;
void main(void) {
colorVarying = color;
gl_Position = position;
}
// 片段着色器 .fsh
varying lowp vec4 colorVarying;
void main(void) {
gl_FragColor = colorVarying;
}
習慣上,我們一般把頂點着色器命名爲 xx.vsh,片段着色器命名爲 xx.fsh。當然,你喜歡怎麼樣就怎麼樣~
和 C 語言程序對應,用 GLSL 寫出的着色器,它同樣包括:
- 變量 position
- 變量類型 vec4
- 限定符 attribute
- main 函數
- 基本賦值語句 colorVarying = color
- 內置變量 gl_Position
這一切,都是那麼像…所以,在掌握 C 語言的基礎上,GLSL 的學習成本是很低的。
變量
變量及變量類型
變量類別 | 變量類型 | 描述 |
---|---|---|
空 | void | 用於無返回值的函數或空的參數列表 |
標量 | float, int, bool | 浮點型,整型,布爾型的標量數據類型 |
浮點型向量 | float, vec2, vec3, vec4 | 包含1,2,3,4個元素的浮點型向量 |
整數型向量 | int, ivec2, ivec3, ivec4 | 包含1,2,3,4個元素的整型向量 |
布爾型向量 | bool, bvec2, bvec3, bvec4 | 包含1,2,3,4個元素的布爾型向量 |
矩陣matrix | mat2, mat3, mat4 | 尺寸爲2x2,3x3,4x4的浮點型矩陣 |
紋理句柄 | sampler2D, samplerCube | 表示2D,立方體紋理的句柄 |
除上述之外,着色器中還可以將它們構成數組或結構體,以實現更復雜的數據類型。
GLSL中沒有指針類型。
變量構造器和類型轉換
對於變量運算,GLSL 中有非常嚴格的規則,即只有類型一致時,變量才能完成賦值或其它對應的操作。可以通過對應的構造器來實現類型轉換。
標量
標量對應 C 語言的基礎數據類型,它的構造和 C 語言一致,如下:
float myFloat = 1.0;
bool myBool = true;
myFloat = float(myBool); // bool -> float
myBool = bool(myFloat); // float -> bool
向量
當構造向量時,向量構造器中的各參數將會被轉換成相同的類型(浮點型、整型或布爾型)。往向量構造器中傳遞參數有兩種形式:
- 如果向量構造器中只提供了一個標量參數,則向量中所有值都會設定爲該標量值。
- 如果提供了多個標量值或提供了向量參數,則會從左至右使用提供的參數來給向量賦值,如果使用多個標量來賦值,則需要確保標量的個數要多於向量構造器中的個數。
向量構造器用法如下:
vec4 myVec4 = vec4(1.0); // myVec4 = {1.0, 1.0, 1.0, 1.0}
vec3 myVec3 = vec3(1.0, 0.0, 0.5); // myVec3 = {1.0, 0.0, 0.5}
vec3 temp = vec3(myVec3); // temp = myVec3
vec2 myVec2 = vec2(myVec3); // myVec2 = {myVec3.x, myVec3.y}
myVec4 = vec4(myVec2, temp, 0.0); // myVec4 = {myVec2.x, myVec2.y , temp, 0.0 }
矩陣
矩陣的構造方法則更加靈活,有以下規則:
- 如果對矩陣構造器只提供了一個標量參數,該值會作爲矩陣的對角線上的值。例如 mat4(1.0) 可以構造一個 4 × 4 的單位矩陣
- 矩陣可以通過多個向量作爲參數來構造,例如一個 mat2 可以通過兩個 vec2 來構造
- 矩陣可以通過多個標量作爲參數來構造,矩陣中每個值對應一個標量,按照從左到右的順序
除此之外,矩陣的構造方法還可以更靈活,只要有足夠的組件來初始化矩陣,其構造器參數可以是標量和向量的組合。在 OpenGL ES 中,矩陣的值會以列的順序來存儲。在構造矩陣時,構造器參數會按照列的順序來填充矩陣,如下:
mat3 myMat3 = mat3(1.0, 0.0, 0.0, // 第一列
0.0, 1.0, 0.0, // 第二列
0.0, 1.0, 1.0); // 第三列
向量和矩陣的分量
單獨獲得向量中的組件有兩種方法:即使用 “.” 符號或使用數組下標方法。依據構成向量的組件個數,向量的組件可以通過 {x, y, z, w} , {r, g, b, a} 或 {s, t, r, q} 等 swizzle 操作來獲取。之所以採用這三種不同的命名方法,是因爲向量常常會用來表示數學向量、顏色、紋理座標等。其中的x、r、s 組件總是表示向量中的第一個元素,如下表:
分量訪問符 | 符號描述 |
---|---|
(x,y,z,w) | 與位置相關的分量 |
(r,g,b,a) | 與顏色相關的分量 |
(s,t,p,q) | 與紋理座標相關的分量 |
不同的命名約定是爲了方便使用,所以哪怕是描述位置的向量,也是可以通過 {r, g, b, a} 來獲取。但是在使用向量時不能混用不同的命名約定,即不能使用 .xgr 這樣的方式,每次只能使用同一種命名約定。當使用 “.” 操作符時,還可以對向量中的元素重新排序,如下:
vec3 myVec3 = vec3(0.0, 1.0, 2.0); // myVec3 = {0.0, 1.0, 2.0}
vec3 temp;
temp = myVec3.xyz; // temp = {0.0, 1.0, 2.0}
temp = myVec3.xxx; // temp = {0.0, 0.0, 0.0}
temp = myVec3.zyx; // temp = {2.0, 1.0, 0.0}
除了使用 “.” 操作符之外,還可以使用數組下標操作。在使用數組下標操作時,元素 [0] 對應的是 x,元素 [1] 對應 y,以此類推。值得注意的是,在 OpenGL ES 2.0 中的某些情況下,數組下標不支持使用非常數的整型表達式(如使用整型變量索引),這是因爲對於向量的動態索引操作,某些硬件設備處理起來很困難。在 OpenGL ES 2.0 中僅對 uniform 類型的變量支持這種動態索引。
矩陣可以認爲是向量的組合。例如一個 mat2 可以認爲是兩個 vec2,一個 mat3 可以認爲是三個 vec3 等等。對於矩陣來說,可以通過數組下標 “[]” 來獲取某一列的值,然後獲取到的向量又可以繼續使用向量的操作方法,如下:
mat4 myMat4 = mat4(1.0); // Initialize diagonal to 1.0 (identity)
vec4 col0 = myMat4[0]; // Get col0 vector out of the matrix
float m1_1 = myMat4[1][1]; // Get element at [1][1] in matrix
float m2_2 = myMat4[2].z; // Get element at [2][2] in matrix
向量和矩陣的操作
絕大多數情況下,向量和矩陣的計算是逐分量進行的(component-wise)。當運算符作用於向量或矩陣時,該運算獨立地作用於向量或矩陣的每個分量。
以下是一些示例:
vec3 v, u;
float f;
v = u + f;
等價於:
v.x = u.x + f;
v.y = u.y + f;
v.z = u.z + f;
再如:
vec3 v, u, w;
w = v + u;
等價於:
w.x = v.x + u.x;
w.y = v.y + u.y;
w.z = v.z + u.z;
對於整型和浮點型的向量和矩陣,絕大多數的計算都同上,但是對於向量乘以矩陣、矩陣乘以向量、矩陣乘以矩陣則是不同的計算規則。這三種計算使用線性代數的乘法規則,並且要求參與計算的運算數值有相匹配的尺寸或階數。
例如:
vec3 v, u;
mat3 m;
向量乘以矩陣
u = v * m;
等價於:
u.x = dot(v, m[0]); // m[0] is the left column of m
u.y = dot(v, m[1]); // dot(a,b) is the inner (dot) product of a and b
u.z = dot(v, m[2]);
再如:
矩陣乘以向量
u = m * v;
等價於:
u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z;
u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z;
u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z;
再如:
矩陣乘以矩陣
mat m, n, r;
r = m * n;
等價於:
r[0].x = m[0].x * n[0].x + m[1].x * n[0].y + m[2].x * n[0].z;
r[1].x = m[0].x * n[1].x + m[1].x * n[1].y + m[2].x * n[1].z;
r[2].x = m[0].x * n[2].x + m[1].x * n[2].y + m[2].x * n[2].z;
r[0].y = m[0].y * n[0].x + m[1].y * n[0].y + m[2].y * n[0].z;
r[1].y = m[0].y * n[1].x + m[1].y * n[1].y + m[2].y * n[1].z;
r[2].y = m[0].y * n[2].x + m[1].y * n[2].y + m[2].y * n[2].z;
r[0].z = m[0].z * n[0].x + m[1].z * n[0].y + m[2].z * n[0].z;
r[1].z = m[0].z * n[1].x + m[1].z * n[1].y + m[2].z * n[1].z;
r[2].z = m[0].z * n[2].x + m[1].z * n[2].y + m[2].z * n[2].z;
對於2階和4階的向量或矩陣也是相似的規則。
結構體
與 C 語言相似,除了基本的數據類型之外,還可以將多個變量聚合到一個結構體中
struct customStruct
{
vec4 color;
vec2 position;
} customVertex;
首先,定義會產生一個新的類型叫做 customStruct ,及一個名爲 customVertex 的變量。結構體可以用構造器來初始化,在定義了新的結構體之後,還會定義一個與結構體類型名稱相同的構造器。構造器與結構體中的數據類型必須一一對應,如下:
customVertex = customStruct(vec4(0.0, 1.0, 0.0, 0.0), // color
vec2(0.5, 0.5)); // position
結構體的構造器是基於類型的名稱,以參數的形式來賦值。獲取結構體內元素的方法和C語言中一致:
vec4 color = customVertex.color;
vec4 position = customVertex.position;
數組
除了結構體外,GLSL 中還支持數組。 語法與 C 語言相似,創建數組的方式如下代碼所示:
float floatArray[4];
vec4 vecArray[2];
與C語言不同,在GLSL中,關於數組有兩點需要注意:
- 除了uniform 變量之外,數組的索引只允許使用常數整型表達式。
- 在GLSL 中不能在創建的同時給數組初始化,即數組中的元素需要在定義數組之後逐個初始化,且數組不能使用 const 限定符。