C语言标量变向量的函数,GLSL 详解(基础篇)
上節在繪制三角形的時候,簡單講解了一些著色器,GLSL 的相關概念,可能看的云里霧里的。不要擔心,在本節中,我將詳細講解著色語言 GL Shader Language(GLSL)的一些基本的概念。
PS:
無特殊說明,文中的 GLSL 均指 OpenGL ES 2.0 的著色語言。
概覽
OpenGL ES 的渲染管線包含有一個可編程的頂點階段的一個可編程的片段階段。其余的階段則有固定的功能,應用程序對其行為的控制非常有限。每個可編程階段中編譯單元的集合組成了一個著色器。在OpenGL ES 2.0 中,每個著色器只支持一個編譯單元。著色程序則是一整套編譯好并鏈接在一起的著色器的集合。著色器 shader 的編寫需要使用著色語言 GL Shader Language(GLSL),GLSL 的語法與 C 語言很類似。
在上一節中,我們看到了一個非常簡單的著色器,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 頂點著色器 .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 的學習成本是很低的。
學習一門語言,我們無非是從變量類型,結構體,數組,語句,函數,限定符等方面展開。下面,我們就照著這個順序,學習 GLSL。
使用 GLSL 構建著色器
1. 變量
變量及變量類型變量類別
變量類型
描述
空
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個元素的布爾型向量
矩陣
mat2, mat3, mat4
尺寸為2x2,3x3,4x4的浮點型矩陣
紋理句柄
sampler2D, samplerCube
表示2D,立方體紋理的句柄
除上述之外,著色器中還可以將它們構成數組或結構體,以實現更復雜的數據類型。
PS:
GLSL 中沒有指針類型。
變量構造器和類型轉換
對于變量運算,GLSL 中有非常嚴格的規則,即只有類型一致時,變量才能完成賦值或其它對應的操作。可以通過對應的構造器來實現類型轉換。
標量
標量對應 C 語言的基礎數據類型,它的構造和 C 語言一致,如下:
1
2
3
4
5float myFloat = 1.0;
bool myBool = true;
myFloat = float(myBool); // bool -> float
myBool = bool(myFloat); // float -> bool
向量
當構造向量時,向量構造器中的各參數將會被轉換成相同的類型(浮點型、整型或布爾型)。往向量構造器中傳遞參數有兩種形式:
如果向量構造器中只提供了一個標量參數,則向量中所有值都會設定為該標量值。
如果提供了多個標量值或提供了向量參數,則會從左至右使用提供的參數來給向量賦值,如果使用多個標量來賦值,則需要確保標量的個數要多于向量構造器中的個數。
向量構造器用法如下:
1
2
3
4
5
6
7vec4 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 中,矩陣的值會以列的順序來存儲。在構造矩陣時,構造器參數會按照列的順序來填充矩陣,如下:
1
2
3mat3 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 這樣的方式,每次只能使用同一種命名約定。當使用 "." 操作符時,還可以對向量中的元素重新排序,如下:
1
2
3
4
5vec3 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 等等。對于矩陣來說,可以通過數組下標 “[]” 來獲取某一列的值,然后獲取到的向量又可以繼續使用向量的操作方法,如下:
1
2
3
4mat4 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)。當運算符作用于向量或矩陣時,該運算獨立地作用于向量或矩陣的每個分量。
以下是一些示例:
1
2
3vec3 v, u;
float f;
v = u + f;
等價于:
1
2
3v.x = u.x + f;
v.y = u.y + f;
v.z = u.z + f;
再如:
1
2vec3 v, u, w;
w = v + u;
等價于:
1
2
3w.x = v.x + u.x;
w.y = v.y + u.y;
w.z = v.z + u.z;
對于整型和浮點型的向量和矩陣,絕大多數的計算都同上,但是對于向量乘以矩陣、矩陣乘以向量、矩陣乘以矩陣則是不同的計算規則。這三種計算使用線性代數的乘法規則,并且要求參與計算的運算數值有相匹配的尺寸或階數。
例如:
1
2
3vec3 v, u;
mat3 m;
u = v * m;
等價于:
1
2
3u.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]);
再如:
1u = m * v;
等價于:
1
2
3u.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;
再如:
1
2mat m, n, r;
r = m * n;
等價于:
1
2
3
4
5
6
7
8
9r[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階的向量或矩陣也是相似的規則。
2. 結構體
與 C 語言相似,除了基本的數據類型之外,還可以將多個變量聚合到一個結構體中,下邊的示例代碼演示了在GLSL中如何聲明結構體:
1
2
3
4
5struct customStruct
{
vec4 color;
vec2 position;
} customVertex;
首先,定義會產生一個新的類型叫做 customStruct ,及一個名為 customVertex 的變量。結構體可以用構造器來初始化,在定義了新的結構體之后,還會定義一個與結構體類型名稱相同的構造器。構造器與結構體中的數據類型必須一一對應,如下:
1
2customVertex = customStruct(vec4(0.0, 1.0, 0.0, 0.0), // color
vec2(0.5, 0.5)); // position
結構體的構造器是基于類型的名稱,以參數的形式來賦值。獲取結構體內元素的方法和C語言中一致:
1
2vec4 color = customVertex.color;
vec4 position = customVertex.position;
3. 數組
除了結構體外,GLSL 中還支持數組。 語法與 C 語言相似,創建數組的方式如下代碼所示:
1
2float floatArray[4];
vec4 vecArray[2];
與C語言不同,在GLSL中,關于數組有兩點需要注意:
除了 uniform 變量之外,數組的索引只允許使用常數整型表達式。
在 GLSL 中不能在創建的同時給數組初始化,即數組中的元素需要在定義數組之后逐個初始化,且數組不能使用 const 限定符。
4. 語句
運算符
下表展示了 GLSL 中支持的運算符:
優先級
運算符類別
運算符
結合方向
1 (最高)
成組操作
()
從左向右
數組下標,函數調用與構造函數,訪問分量或結構體的字段,后置自增和自減
[] () . ++ –
從左向右
3
前置自增和自減,一元正/負數,一元邏輯非
++ – + - !
從右向左
4
乘法,除法
* /
從左向右
5
加法,減法
+ -
從左向右
6
關系比較操作
< > <= >=
從左向右
7
相等操作
== !=
從左向右
8
邏輯與
&&
從左向右
9
邏輯異或
^^
從左向右
10
邏輯或
\ \
\
\ 從左向右
11
三元選擇操作(問號表達式)
?:
從右向左
12
賦值與算數賦值
= += -= *= /=
從右向左
13(最低)
操作符序列
,
從左向右
絕大多數的運算符與 C 語言中一致。與 C 語言不同的是:GLSL 中對于參與運算的數據類型要求比較嚴格,即運算符兩側的變量必須有相同的數據類型。對于二目運算符(*,/,+,-),操作數必須為浮點型或整型,除此之外,乘法操作可以放在不同的數據類型之間如浮點型、向量和矩陣等。
前面矩陣的行數就是結果矩陣的行數,后面矩陣的列數就是結果矩陣的列數。
比較運算符僅能作用于標量,對于向量的比較,GLSL 中有內置的函數,稍后會介紹。
流程控制語句
流程控制語句與 C 語言非常相似,以下示例代碼是 if-else 的使用:
1
2
3
4
5if (color.a < 0.25) {
color *= color.a;
} else {
color = vec4(0.0);
}
判斷的內容必須是布爾值或布爾表達式,除了基本的 if-else 語句,還可以使用 for 循環,在使用 for 循環時也有一些約束,如循環變量的值必須是編譯時已知。如下:
1
2
3for (int i = 0; i < 3; i++) {
sum += i;
}
在 GLSL 中使用循環時一定要注意:只有一個循環變量,循環變量必須使用簡單的語句來增減(如 i++, i–, i+=constant, i-=constant等),循環終止條件也必須是循環變量和常量的簡單比較,在循環內部不能改變循環變量的值。
以下代碼是 GLSL 中不支持的循環用法的示例:
1
2
3
4
5
6
7
8
9
10
11float myArr[4];
for (int i = 0; i < 3; i++) {
// 錯誤, [ ]中只能為常量或 uniform 變量,不能為整數量變量(如:i,j,k)
sum += myArr[i];
}
...
uniform int loopIter;
// 錯誤, 循環變量 loopIter 的值必須是編譯時已知
for (int i = 0; i < loopIter; i++) {
sum += i;
}
5. 函數
GLSL 函數的聲明與 C 語言中很相似,無非就是返回值,函數名,參數列表。
GLSL 著色器同樣是從 main 函數開始執行。另外, GLSL 也支持自定義函數。當然,如果一個函數在定以前被調用,則需要先聲明其原型。
值得注意的一點是,GLSL 中函數不能夠遞歸調用,且必須聲明返回值類型(無返回值時聲明為void)。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14vec4 getPosition(){
vec4 v4 = vec4(0.,0.,0.,1.);
return v4;
}
void doubleSize(inout float size){
size= size*2.0 ;
}
void main(){
float psize= 10.0;
doubleSize(psize);
gl_Position = getPosition();
gl_PointSize = psize;
}
6. 限定符
存儲限定符
在聲明變量時,應根據需要使用存儲限定符來修飾,類似 C 語言中的說明符。GLSL 中支持的存儲限定符見下表:
限定符
描述
< none: default >
局部可讀寫變量,或者函數的參數
const
編譯時常量,或只讀的函數參數
attribute
由應用程序傳輸給頂點著色器的逐頂點的數據
uniform
在圖元處理過程中其值保持不變,由應用程序傳輸給著色器
varying
由頂點著色器傳輸給片段著色器中的插值數據
本地變量和函數參數只能使用 const 限定符,函數返回值和結構體成員不能使用限定符。
數據不能從一個著色器程序傳遞給下一個階段的著色器程序,這樣會阻止同一個著色器程序在多個頂點或者片段中進行并行計算。
不包含任何限定符或者包含 const 限定符的全局變量可以包含初始化器,這種情況下這些變量會在 main() 函數開始之后第一行代碼之前被初始化,這些初始化值必須是常量表達式。
沒有任何限定符的全局變量如果沒有在定義時初始化或者在程序中被初始化,則其值在進入 main() 函數之后是未定義的。
uniform、attribute 和 varying 限定符修飾的變量不能在初始化時被賦值,這些變量的值由 OpenGL ES 計算提供。
默認限定符
如果一個全局變量沒有指定限定符,則該變量與應用程序或者其他正在運行的處理單元沒有任何聯系。不管是全局變量還是本地變量,它們總是在自己的處理單元被分配內存,因此可以對它們執行讀和寫操作。
const 限定符
任意基礎類型的變量都可以聲明為常量。常量表示這些變量中的值在著色器中不會發生變化,聲明常量只需要在聲明時加上限定符 const 即可,聲明時必須賦初值。
1
2
3
4const float zero = 0.0;
const float pi = 3.14159;
const vec4 red = vec4(1.0, 0.0, 0.0, 1.0);
const mat4 identity = mat4(1.0);
常量聲明過的值在代碼中不能再改變,這一點和 C 語言或 C++ 一樣。
結構體成員不能被聲明為常量,但是結構體變量可以被聲明為常量,并且需要在初始化時使用構造器初始化其值。
常量必須被初始化為一個常量表達式。數組或者包含數組的結構體不能被聲明為常量(因為數組不能在定義時被初始化)。
attribute 限定符
GLSL 中另一種特殊的變量類型是 attribute 變量。attribute 變量只用于頂點著色器中,用來存儲頂點著色器中每個頂點的輸入(per-vertex inputs)。attribute 通常用來存儲位置坐標、法向量、紋理坐標和顏色等。注意 attribute 是用來存儲單個頂點的信息。如下是包含位置,色值 attribute 的頂點著色器示例:
1
2
3
4
5
6
7
8
9
10// 頂點著色器 .vsh
attribute vec4 position;
attribute vec4 color;
varying vec4 colorVarying;
void main(void){
colorVarying = color;
gl_Position = position;
}
著色器中的兩個 attribute 變量 position 和 color 由應用程序加載數值。應用程序會創建一個頂點數組,其中包含了每個頂點的位置坐標和色值信息??墒褂玫淖畲?attribute 數量也是有上限的,可以使用 gl_MaxVertexAttribs 來獲取,也可以使用內置函數 glGetIntegerv 來詢問 GL_MAX_VERTEX_ATTRIBS。OpenGL ES 2.0 實現支持的最少 attribute 個數是8個。
uniform 限定符
uniform 是 GLSL 中的一種變量類型限定符,用于存儲應用程序通過 GLSL 傳遞給著色器的只讀值。uniform 可以用來存儲著色器需要的各種數據,如變換矩陣、光參數和顏色等。傳遞給著色器的在所有的頂點著色器和片段著色器中保持不變的的任何參數,基本上都應該通過 uniform 來存儲。uniform 變量在全局區聲明,以下是 uniform 的一些示例:
1
2
3uniform mat4 viewProjMatrix;
uniform mat4 viewMatrix;
uniform vec3 lightPosition;
需要注意的一點是,頂點著色器和片段著色器共享了 uniform 變量的命名空間。對于連接于同一個著色程序對象的頂點和片段著色器,它們共用同一組 uniform 變量,因此,如果在頂點著色器和片段著色器中都聲明了 uniform 變量,二者的聲明必須一致。當應用程序通過 API 加載了 uniform 變量時,該變量的值在頂點和片段著色器中都能夠獲取到。
另一點需要注意的是,uniform 變量通常是存儲在硬件中的”常量區”,這一區域是專門分配用來存儲常量的,但是由于這一區域尺寸非常有限,因此著色程序中可以使用的 uniform 的個數也是有限的??梢酝ㄟ^讀取內置變量 gl_MaxVertexUniformVectors 和 gl_MaxFragmentUniformVectors 來獲得,也可以使用 glGetIntegerv 查詢 GL_MAX_VERTEX_UNIFORM_VECTORS 或者 GL_MAX_FRAGMENT_UNIFORM_VECTORS 。OpenGL ES 2.0 的實現必須提供至少 128 個頂點 uniform 向量及 16 片段 uniform 向量。
varying 限定符
GLSL 中最后一個要說的存儲限定符是 varying。varying 存儲的是頂點著色器的輸出,同時作為片段著色器的輸入,通常頂點著色器都會把需要傳遞給片段著色器的數據存儲在一個或多個 varying 變量中。這些變量在片段著色器中需要有相對應的聲明且數據類型一致,然后在光柵化過程中進行插值計算。以下是一些 varying 變量的聲明:
1
2varying vec2 texCoord;
varying vec4 color;
頂點著色器和片段著色器中都會有 varying 變量的聲明,由于 varying 是頂點著色器的輸出且是片段著色器的輸入,所以兩處聲明必須一致。與 uniform 和 attribute 相同,varying 也有數量的限制,可以使用 gl_MaxVaryingVectors 獲取或使用 glGetIntegerv 查詢 GL_MAX_VARYING_VECTORS 來獲取。OpenGL ES 2.0 實現中的 varying 變量最小支持數為 8。
回顧下最初那個著色器對應的 varying 聲明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 頂點著色器 .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;
}
invariant 限定符
invariant 可以作用于頂點著色器輸出的任何一個 varying 變量。
當著色器被編譯時,編譯器會對其進行優化,這種優化操作可能引起指令重排序(instruction reordering),指令重排序可能引起的結果是當兩個著色器進行相同的計算時無法保證得到相同的結果。
例如,在兩個頂點著色器中,變量 gl_Position 使用相同的表達式賦值,并且當著色程序運行時,在表達式中傳入相等的變量值,則兩個著色器中 gl_Position 的值無法保證相等,這是因為兩個著色器是分別單獨編譯的。這將會引起 multi-pass 算法的幾何不一致問題。
通常情況下,不同著色器之間的這種值的差異是允許存在的。如果要避免這種差異,則可以將變量聲明為invariant,可以單獨指定某個變量或進行全局設置。
使用 invariant 限定符可以使輸出的變量保持不變。invariant 限定符可以作用于之前已聲明的變量使其具有不變性,也可以在聲明變量時直接作為聲明的一部分,可參考以下兩段示例代碼:
1
2
3varying mediump vec3 Color;
// 使已存在的 color 變量不可變
invariant Color;
或
1invariant varying mediump vec3 Color;
以上是僅有的使用 invariant 限定符情境。如果在聲明時使用 invariant 限定符,則必須保證其放在存儲限定符(varying)之前。
只有以下變量可以聲明為 invariant:
由頂點著色器輸出的內置的特殊變量
由頂點著色器輸出的 varying 變量
向片段著色器輸入的內置的特殊變量
向片段著色器輸入的 varying 變量
由片段著色器輸出的內置的特殊變量
為保證由兩個著色器輸出的特定變量的不變性,必須遵循以下幾點:
該輸出變量在兩個著色器中都被聲明為 invariant
影響輸出變量的所有表達式、流程控制語句的輸入值必須相同
對于影響輸出值的所有紋理函數,紋理格式、紋理元素值和紋理過濾必須一致
對輸入值的所有操作都必須一致。表達式及插值計算的所有操作必須一致,相同的運算數順序,相同的結合性,并且按相同順序計算。插值變量和插值函數的聲明,必須有相同類型,相同的顯式或隱式的精度precision限定符。影響輸出值的所有控制流程必須相同,影響決定控制流程的表達式也必須遵循不變性的規則。
最基本的一點是:所有的 invariant 輸出量的上游數據流或控制流必須一致。
初始的默認狀態下,所有的輸出變量不具備不變性,可以在所有的聲明之前使用以下 pragma 語句強制所有輸出變量 invariant:
1#pragma STDGL invariant(all)
輸出變量的不變性通常會以優化過程的靈活性為代價,所以使用 invariant 會犧牲整體性能。因此慎用以上的全局設置方法,可以將其用作協助 Debug 的一種方法。
另一點需要說明的是,這里的不變性指的是對于同一 GPU 的不變性,并不保證不同 OpenGL ES 實現之間的不變性。
參數限定符
GLSL 提供了一種特殊的限定符用來定義某個變量的值是否可以被函數修改,詳見下表:
限定符
描述
in
默認使用的缺省限定符,指明參數傳遞的是值,并且函數不會修改傳入的值(C 語言中值傳遞)
inout
指明參數傳入的是引用,如果在函數中對參數的值進行了修改,當函數結束后參數的值也會修改(C 語言中引用傳遞)
out
參數的值不會傳入函數,但是在函數內部修改其值,函數結束后其值會被修改
使用的方式如下邊的代碼:
1
2
3vec4 myFunc(inout float myFloat, // inout parameter
out vec4 myVec4, // out parameter
mat4 myMat4); // in parameter (default)
以下是一個示例函數,函數定義用來計算基礎的漫反射光照:
1
2
3vec4 diffuse(vec3 normal, vec3 light, vec4 baseColor){
return baseColor * dot(normal, light);
}
精度限定符
OpenGL ES 與 OpenGL 之間的一個區別就是在 GLSL 中引入了精度限定符。精度限定符可使著色器的編寫者明確定義著色器變量計算時使用的精度,變量可以選擇被聲明為低、中或高精度。精度限定符可告知編譯器使其在計算時縮小變量潛在的精度變化范圍,當使用低精度時,OpenGL ES 的實現可以更快速和低功耗地運行著色器,效率的提高來自于精度的舍棄,如果精度選擇不合理,著色器運行的結果會很失真。
OpenGL ES 對各硬件并未強制要求多種精度的支持。其實現可以使用高精度完成所有的計算并且忽略掉精度限定符,然而某些情況下使用低精度的實現會更有優勢,精度限定符可以指定整型或浮點型變量的精度,如 lowp,mediump,及 highp,如下:
限定符
描述
highp
滿足頂點著色語言的最低要求。對片段著色語言是可選項
mediump
滿足片段著色語言的最低要求,其對于范圍和精度的要求必須不低于lowp并且不高于highp
lowp
范圍和精度可低于mediump,但仍可以表示所有顏色通道的所有顏色值
具體用法參考以下示例:
1
2
3highp vec4 position;
varying lowp vec4 color;
mediump float specularExp;
除了精度限定符,還可以指定默認使用的精度。如果某個變量沒有使用精度限定符指定使用何種精度,則會使用該變量類型的默認精度。默認精度限定符放在著色器代碼起始位置,以下是一些用例:
1
2precision highp float;
precision mediump int;
當為 float 指定默認精度時,所有基于浮點型的變量都會以此作為默認精度,與此類似,為 int 指定默認精度時,所有的基于整型的變量都會以此作為默認精度。在頂點著色器中,如果沒有指定默認精度,則 int 和 float 都使用 highp,即頂點著色器中,未使用精度限定符指明精度的變量都默認使用最高精度。在片段著色器中,float 并沒有默認的精度設置,即片段著色器中必須為 float 默認精度或者為每一個 float 變量指明精度。OpenGL ES 2.0 并未要求其實現在片段著色器中支持高精度,可用是否定義了宏 GL_FRAGMENT_PRECISION_HIGH 來判斷是否支持在片段著色器中使用高精度。
在片段著色器中可以使用以下代碼:
1
2
3
4
5#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
這么做可以確保無論實現支持中精度還是高精度都可以完成著色器的編譯。注意不同實現中精度的定義及精度的范圍都不統一,而是因實現而異的。
精度修飾符聲明了底層實現存儲這些變量時,必須要使用的最小范圍和精度。實現可能會使用比要求更大的范圍和精度,但絕對不會比要求少。以下是精度修飾符要求的最低范圍和精度:
浮點數范圍
浮點數大小范圍
浮點數精度范圍
整數范圍
highp
(-2^62 , 2^62)
(2^-62 ,2^62)
相對:2^-16
(-2^16 , 2^16)
mediump
(-2^14 , 2^14)
(2^-14 ,2^14)
相對:2^-10
(-2^10 , 2^10)
lowp
(-2, 2)
(2^-8 ,2)
絕對:2^-8
(-2^8 , 2^8)
在具體實現中,著色器編譯器支持的不同著色器類型和數值形式的實際的范圍及精度可用以下函數獲取:
1void GetShaderPrecisionFormat( enum shadertype, enum precisiontype, int *range, int *precision );
其中, shadertype 必須是 VERTEX_SHADER 或 FRAGMENT_SHADER;precisiontype 必須是 LOW_FLOAT、MEDIUM_FLOAT、HIGH_FLOAT、LOW_INT、MEDIUM_INT 或 HIGH_INT。
range 是指向含有兩個整數的數組的指針,這兩個整數將會返回數值的范圍。如果用 min 和 max 來代表對應格式的最小和最大值,則 range 中返回的整數值可以定義為:
1
2range[0] = log2(|min|)
range[1] = log2(|max|)
precision 是指向一個整數的指針,返回的該整數是對應格式的精度的位數(number of bits)用 log2 取對數的值。
Q:如何確定精度:
A:變量的精度首先是由精度限定符決定的,如果沒有精度限定符,則要尋找其右側表達式中,已經確定精度的變量,一旦找到,那么整個表達式都將在該精度下運行。
如果找到多個,則選擇精度較高的那種,如果一個都找不到,則使用默認或更大的精度類型。
1
2
3
4
5
6
7
8
9uniform highp float h1;
highp float h2 = 2.3 * 4.7; // 運算過程和結果都是 highp
mediump float m;
m = 3.7 * h1 * h2; // 運算過程是 highp
h2 = m * h1; // 運算過程是 highp
m = h2 – h1; // 運算過程是 highp
h2 = m + m; // 運算過程和結果都是 mediump
void f(highp float p); // 形參 p 是 highp
f(3.3); // 傳入的 3.3 是 highp
Q:限定符的順序
A:當需要用到多個限定符的時候要遵循以下順序:
在一般變量中:invariant > storage > precision (storage:存儲,precision:精度)
在函數參數中:storage > parameter > precision (parameter:參數)
我們來舉例說明:
1
2
3
4
5invariant varying lowp float color; // invariant > storage > precision
void doubleSize(const in lowp float s){ //storage > parameter > precision
float s1=s;
}
總結
以上是生活随笔為你收集整理的C语言标量变向量的函数,GLSL 详解(基础篇)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C语言 int 转单精度浮点,单精度浮点
- 下一篇: android的动态注册,Android