OpenGL學習(十)-- 著色語言 GLSL 語法介紹
我的
OpenGL
專題學習目錄,希望和大家一起學習交流進步!
- OpenGL學習(一)-- 術語瞭解
- OpenGL學習(二)-- Xcode 搭建 OpenGL 環境
- OpenGL學習(三)-- OpenGL 基礎渲染
- OpenGL學習(四)-- 正面&背面剔除和深度測試
- OpenGL學習(五)-- 裁剪與混合
- OpenGL學習(六)-- 基礎紋理
- OpenGL學習(七)-- 基礎變化綜合練習實踐總結
- OpenGL學習(八)-- OpenGL ES 初探(上)
- OpenGL學習(九)-- OpenGL ES 初探(下)GLKit
- OpenGL學習(十)-- 著色語言 GLSL 語法介紹
- OpenGL學習(十一)-- 用 GLSL 實現載入圖片
- OpenGL學習(十二)-- OpenGL ES 紋理翻轉的策略對比
一、簡介
GLSL
(OpenGL Shading Language) 全稱 OpenGL 著色語言,是用來在 OpenGL 中著色程式設計的語言,也即開發人員寫的短小的自定義程式,他們是在圖形卡的 GPU上執行的,代替了固定的渲染管線的一部分,使渲染管線中不同層次具有可程式設計性。
GLSL
其使用 C 語言作為基礎高階著色語言,避免了使用匯編語言或硬體規格語言的複雜性。
二、變數命名
GLSL
的變數命名方式與 C 語言類似,可使用字母,數字以及下劃線,不能以數字開頭。還需要注意的是,變數名不能以 gl_
GLSL
保留的字首,用於 GLSL
的內部變數。
三、資料型別
1、基本資料型別
型別 | 描述 |
---|---|
void | 跟 C 語言的 void 類似,表示空型別。作為函式的返回型別,表示這個函式不返回值。 |
bool | 布林型別,true 或 false,以及可以產生布爾型的表示式。 |
int | 有符號整型 |
uint | 無符號整形 |
float | 浮點型 |
2、特殊型別--紋理取樣型別
型別 | 描述 |
---|---|
sampler1D | 用於內建的紋理函式中引用指定的 1D紋理的控制代碼。只可以作為一致變數或者函式引數使用 |
sampler2D | 二維紋理控制代碼 |
sampler3D |
三維紋理控制代碼 |
samplerCube | cube map 紋理控制代碼 |
sampler1DShadow | 一維深度紋理控制代碼 |
sampler2DShadow | 二維深度紋理控制代碼 |
3、聚合型別(向量和矩陣型別)
(1)向量型別
型別 | 描述 |
---|---|
vec2,vec3,vec4 | 2分量、3分量和4分量浮點向量 |
ivec2,ivec3,ivec4 | 2分量、3分量和4分量整數向量 |
uvec2,uvec3,uvec4 | 2分量、3分量和4分量無符號整數向量 |
bvec2,vbec3,bvec4 | 2分量、3分量和4分量布林向量 |
A、????向量宣告--4分量的float 型別向量
vec4 V1;
複製程式碼
B、????宣告向量並對其進行構造
vec4 V2 = vec4(1,2,3,4);
複製程式碼
C、????向量運算
vec4 v;
vec4 vOldPos = vec4(1,4);
vec4 vOffset = vec4(1,4);
//注意:接下來假設所有參與運算的變數已定義並賦值。
v = vOldPos + vOffset;
v = vNewPos;
v += vec4(10,10,10);
v = vOldPos * vOffset;
v *= 5;
複製程式碼
D、????向量元素的獲取(成分選擇)
向量中單獨的成分可以通過 {x,y,z,w},{r,g,b,a} 或者 {s,t,p,q} 的記法來表示。這些不同的記法用於 頂點,顏色,紋理座標。在成分選擇中,你不可以混合使用這些記法。其中 {s,q} 中的 p 替換了紋理的 r 座標,因為與顏色 r 重複了。下面是用法舉例: 例如有向量 v1 和 v2:
vec3 v1 = {0.5,0.35,0.7};
vec4 v2 = {0.1,0.2,0.3,0.4};
複製程式碼
可以通過 {x,q} 來取出向量中的元素值。 通過 x,w:
v2.x = 3.0f;
v2.xy = vec2(3.0f,4.0f);
v2.xyz = vec3(3,0f,4,5.0f);
複製程式碼
通過 r,a:
v2.r = 3.0f;
v2.rgba = vec4(1.0f,1.0f,1.0f);
複製程式碼
通過 s,q,r:
v2.stqr = vec2(1.0f,0.0f,1.0f);
複製程式碼
錯誤示例:
float myQ = v1.q;// 出錯,陣列越界訪問,q代表第四個元素
float myRY = v1.ry; // 不合法,混合使用記法
複製程式碼
向量還支援一次性對所有分量操作
v1.x = v2.x +5.0f;
v1.y = v2.y +4.0f;
v1.z = v2.z +3.0f;
v1.xyz = v2.xyz + vec3(5.0f,4.0f,3.0f);
複製程式碼
(2)矩陣型別
型別 | 描述 |
---|---|
mat2 或 mat2x2 | 2x2的浮點數矩陣型別 |
mat3 或 mat3x3 | 3x3的浮點數矩陣型別 |
mat4 或 mat4x4 | 4x4的浮點數矩陣型別 |
mat2x3 | 2列3行的浮點矩陣(OpenGL的矩陣是列主順序的) |
mat2x4 | 2列4行的浮點矩陣 |
mat3x2 | 3列2行的浮點矩陣 |
mat3x4 | 3列4行的浮點矩陣 |
mat4x2 | 4列2行的浮點矩陣 |
mat4x3 | 4列3行的浮點矩陣 |
建立矩陣:
mat4 m1,m2,m3;
複製程式碼
構造單元矩陣:
mat4 m2 = mat4(1.0f,0.0f
0.0f,1.0f);
複製程式碼
或者
mat4 m4 = mat4(1.0f);
複製程式碼
4、陣列
GLSL
中只可以使用一維的陣列。陣列的型別可以是一切基本型別或者結構體。下面的幾種陣列宣告是合法的:
float floatArray[4];
vec4 vecArray[2];
float a[4] = float[](1.0,2.0,3.0,4.0);
vec2 c[2] = vec2[2](vec2(1.0,2.0),vec2(3.0,4.0));
複製程式碼
陣列型別內建了一個length()
函式,可以返回陣列的長度。
lightPositions.length() // 返回陣列的長度
複製程式碼
5、結構體
結構體可以組合基本型別和陣列來形成使用者自定義的型別。在定義一個結構體的同時,你可以定義一個結構體例項。或者後面再定義。
struct fogStruct {
vec4 color;
float start;
float end;
vec3 points[3]; // 固定大小的陣列是合法的
} fogVar;
複製程式碼
可以通過 = 為結構體賦值,或者使用 ==,!= 來判斷兩個結構體是否相等。
fogVar = fogStruct(vec4(1.0,0.0,1.0),0.5,2.0);
vec4 color = fogVar.color;
float start = fogVar.start;
複製程式碼
三、修飾符
1、變數儲存限定符
限定符 | 描述 |
---|---|
(預設的可省略)只是普通的本地變數,可讀可寫,外部不可見,外部不可訪問 | |
const | 常量值必須在宣告時初始化,它是隻讀的不可修改的 |
varying | 頂點著色器的輸出,主要負責在 vertex 和 fragment 之間傳遞變數。例如顏色或者紋理座標,(插值後的資料)作為片段著色器的只讀輸入資料。必須是全域性範圍宣告的全域性變數。可以是浮點數型別的標量,向量,矩陣。不能是陣列或者結構體。 |
uniform | 一致變數。在著色器執行期間一致變數的值是不變的。與 const 常量不同的是,這個值在編譯時期是未知的是由著色器外部初始化的。一致變數在頂點著色器和片段著色器之間是共享的。它也只能在全域性範圍進行宣告。 |
attribute | 表示只讀的頂點資料,只用在頂點著色器中。資料來自當前的頂點狀態或者頂點陣列。它必須是全域性範圍宣告的,不能再函式內部。一個 attribute 可以是浮點數型別的標量,向量,或者矩陣。不可以是陣列或則結構體 |
centorid varying | 在沒有多重取樣的情況下,與 varying 是一樣的意思。在多重取樣時,centorid varying 在光柵化的圖形內部進行求值而不是在片段中心的固定位置求值。 |
invariant | (不變數)用於表示頂點著色器的輸出和任何匹配片段著色器的輸入,在不同的著色器中計算產生的值必須是一致的。所有的資料流和控制流,寫入一個 invariant 變數的是一致的。編譯器為了保證結果是完全一致的,需要放棄那些可能會導致不一致值的潛在的優化。除非必要,不要使用這個修飾符。在多通道渲染中避免 z-fighting 可能會使用到。 |
2、函式引數限定符
GLSL
允許自定義函式,但引數預設是以值形式(in
限定符)傳入的,也就是說任何變數在傳入時都會被拷貝一份,若想以引用方式傳參,需要增加函式引數限定符。
限定符 | 描述 |
---|---|
in | 用在函式的引數中,表示這個引數是輸入的,在函式中改變這個值,並不會影響對呼叫的函式產生副作用。(相當於C語言的傳值),這個是函式引數預設的修飾符 |
out | 用在函式的引數中,表示該引數是輸出引數,值是會改變的。 |
inout | 用在函式的引數,表示這個引數即是輸入引數也是輸出引數。 |
其中使用 inout 方式傳遞的引數便與其他 OOP 語言中的引用傳遞類似,引數可讀寫,函式內對引數的修改會影響到傳入引數本身。 eg:
vec4 getPosition(out vec4 p){
p = vec4(0.,0.,1.);
return v4;
}
void doubleSize(inout float size){
size= size * 3.0 ;
}
複製程式碼
三、GLSL 中的運算
⚠️注意 GLSL
中沒有隱式轉換,即便在多維向量中也沒有,類似下面這樣的的賦值都是錯誤的:
// ⚠️錯誤
int a = 2.0;
vec4 v4=vec4(1.0,1.0,1.0);
複製程式碼
1、不同型別 float 與 int 間的運算:
float 與 int 之間進行運算,需要進行一次顯示轉換,以下表達式都是正確的:
int a = int(2.0);
float a = float(2);
int a = int(2.0)*2 + 1;
float a = float(2)*6.0+2.3;
複製程式碼
2、float 與 vec(向量)、mat(矩陣) 的運算:
- 逐分量運算 vec,mat 這些型別其實是由 float 複合而成的,當它們與float 運算時,其實就是在每一個分量上分別與 float 進行運算,這就是所謂的 逐分量運算。GLSL 裡,大部分涉及 vec,mat 的運算都是逐分量運算,但也並不全是。下文中就會講到特例。 逐分量運算 是線性的,這就是說 vec 與 float 的運算結果是還是 vec。
int 與 vec,mat 之間是不可運算的,因為 vec 和 mat 中的每一個分量都是 float 型別的,無法與 int 進行逐分量計算。
下面枚舉了幾種 float 與 vec,mat 運算的情況:
vec3 a = vec3(1.0,3.0);
mat3 m = mat3(1.0);
float s = 10.0;
vec3 b = s * a; // vec3(10.0,20.0,30.0)
vec3 c = a * s; // vec3(10.0,30.0)
mat3 m2 = s * m; // = mat3(10.0)
mat3 m3 = m * s; // = mat3(10.0)
複製程式碼
3、vec(向量) 與 vec(向量)運算:
兩向量間的運算首先要保證運算元的階數都相同,否則不能計算。例如: vec3*vec2
和 vec4+vec3
等等都是不行的。
它們的計算方式是兩運算元在同位置上的分量分別進行運算,其本質還是逐分量進行的,這和上面所說的 float 型別的逐分量運算可能有一點點差異,相同的是 vec 與 vec 運算結果還是 vec,且階數不變。
vec3 a = vec3(1.0,3.0);
vec3 b = vec3(0.1,0.3);
vec3 c = a + b; // = vec3(1.1,2.2,3.3);
vec3 d = a * b; // = vec3(0.1,0.4,0.9);
複製程式碼
4、vec(向量) 與 mat(矩陣):
要保證運算元的階數相同,且 vec 與 mat 間只存在乘法運算。 它們的計算方式和線性代數中的矩陣乘法相同,不是逐分量運算。
vec2 v = vec2(10.,20.);
mat2 m = mat2(1.,2.,3.,4.);
vec2 w = m * v; // = vec2(1. * 10. + 3. * 20.,2. * 10. + 4. * 20.)
vec2 v = vec2(10.,4.);
vec2 w = v * m; // = vec2(1. * 10. + 2. * 20.,3. * 10. + 4. * 20.)
複製程式碼
5、mat(矩陣) 與 mat(矩陣):
⚠️要保證運算元的階數相同。
在 mat 與 mat 的運算中,除了乘法是線性代數中的矩陣乘法外,其餘的運算仍為逐分量運算。簡單說就是隻有乘法是特殊的,其餘都和 vec 與 vec 運算類似。
mat2 a = mat2(1.,4.);
mat2 b = mat2(10.,20.,30.,40.);
mat2 c = a * b; // mat2(1.*10.+3.*20.,2.*10.+4.*20.,1.* 30.+3.*40.,2.* 30.+4.*40.);
mat2 d = a+b;// mat2(1.+10.,2.+20.,3.+30.,4.+40);
複製程式碼
四、型別轉換
GLSL
可以通過建構函式進行顯式轉換,方法如下:
bool t= true;
bool f = false;
int a = int(t); // true轉換為1或1.0
int a1 = int(f);// false轉換為0或0.0
float b = float(t);
float b1 = float(f);
bool c = bool(0);// 0或0.0轉換為false
bool c1 = bool(1);// 非0轉換為true
bool d = bool(0.0);
bool d1 = bool(1.0);
複製程式碼
五、精度限定
GLSL
在進行光柵化著色的時候,會產生大量的浮點數運算,這些運算可能是當前裝置所不能承受的,所以 GLSL
提供了 3 種浮點數精度,我們可以根據不同的裝置來使用合適的精度。
在變數前面加上 highp
、mediump
、lowp
即可完成對該變數的精度宣告:
lowp float color;
varying mediump vec2 Coord;
lowp ivec2 foo(lowp mat3);
highp mat4 m;
複製程式碼
我們一般在 片元著色器(fragment shader) 最開始的地方加上 precision mediump float;
便設定了預設的精度,這樣所有沒有顯式表明精度的變數都會按照設定好的預設精度來處理。
六、控制語句
在語法上,GLSL
與 C 非常相似, 也有 if else、for、while、do while
,使用 continue
跳入下一次迴圈,break
結束迴圈。
for (l = 0; l < numLights; l++) {
if (!lightExists[l]);
continue;
color += light[l];
}
while (i < num) {
sum += color[i];
i++;
}
do {
color += light[lightNum];
lightNum--;
} while (lightNum > 0)
複製程式碼
除了這些,GLSL
還多了一種特殊的控制語句 discard
,它會立即跳出片元著色器,並不在向下任何語句。也就不執行後面的片段著色操作,片段也不會寫入幀緩衝區。
if (true)
discard;
複製程式碼
⚠️注意 GLSL
函式中沒有遞迴!
以上的總結參考了並部分摘抄了以下文章,非常感謝以下作者的分享!:
轉載請備註原文出處,不得用於商業傳播——凡幾多