OpenGL ES 2.0 知識串講 (5)——GLSL 語法(III)

出處:電子設備中的畫家|王爍 於 2017 年 7 月 11 日發表,原文鏈接(http://geekfaner.com/shineengine/blog6_OpenGLESv2_5.html)

 

上節回顧

上一節瞭解了 GLSL 中的變量類型,有一些類型是原本所熟知的,而有些類型是 GLSL 特有的。在上一節結尾的時候也提到了變量的範圍,其中講到兩個 shader 所在的空間是獨立的,但是由於 shader 本身的功能,需要在不同的 shader 之間傳遞數據,也需要與 OpenGL ES 進行交互,那麼就需要有辦法進行跨越空間的交流。在 C 語言中,是通過 static 和 extern 這兩個變量修飾符,在 C++中,是通過 public。而在 Shader 中也存在類似的修飾符,並且 shader 中的修飾符種類很多,且非常重要。那麼這一節,我們詳細講解的 GLSL 變量的修飾符。


GLSL 的變量修飾符

GLSL 中,變量的定義除了上一節介紹的知識點外,還有一個重要的知識點, 就是每個變量都有修飾符,且修飾符也分爲很多種。

在定義變量的時候,除了要指明必須的變量類型和變量名,我們還可以在變量類型之前加修飾符。在 C、C++中我們也都見過修飾符,比如 const 就屬於修飾符,假如我要定義一個 const 的 float 類型變量,可以寫是 const float x = 1.5, 那麼這個變量 x 就成了一個只讀的變量,x 也就成了一個常量 1.5。其中 float 是變量類型,x 是變量,=1.5 是對變量的初始化,而 const 就是修飾符。

在 C 或者 C++中,我們可能會修飾符用的很少,而在 GLSL 中,存在着大量的修飾符,而且這些修飾符會被廣泛使用着。

這一節,將重點講解變量的這些修飾符。


存儲修飾符

修飾符也分種類,就好像形容詞一樣,一個變量只能有一個變量類型,但是它可以有很多形容詞。首先說的修飾符,叫做存儲修飾符。存儲修飾符,顧名思義就是用於區分不同變量的存儲空間的。

我們知道 shader 分爲 Vertex shader 和 Fragment shader。那麼可以把它們看作是兩個獨立的空間,而 OpenGL ES 又可以看作是第三個獨立空間。但是這三個空間又存在大量的聯繫,比如 OpenGL ES 需要向 VS 和 PS 中傳輸數據,VS 也需要向 PS 傳輸數據。那麼使用什麼變量來進行數據傳輸,非常重要。那麼說到這裏,大家就可以猜到,其實是通過存儲修飾符來區分不同的變量,讓這些變量來進行不同空間的數據傳輸的。

< none: default >

存儲修飾符分 5 種,第一種是沒有存儲修飾符,那麼這種變量也就被稱爲 local 變量,只能在一個空間中使用的。

然而在一個空間中也存在全局變量。也就是在 main 函數以及其他函數之外定義一個變量,這個變量可以被初始化,但是隻能初始化成一個常量表達式。而如果全局變量不被初始化,那麼它不會有默認值,處理的時候也會按照 undefine 進行處理。

無論是全局變量還是普通的 local 變量,只要沒有被存儲修飾符,就會通過它所在空間的那個 shader 進行內存分配。而對應的變量名也就是用於訪問那一塊內存使用。

const

其實也就是我們熟悉的 const,這種變量可以被認爲是編譯時的常量。使用這種常量比直接使用數字常量看起來更加方便一些。提高代碼的可讀性。

const 修飾的變量也是 local 變量,且在當前 shader 中屬於只讀變量。const 變量只能在定義的時候進行初始化,如果在定義之後再進行賦值,就會出錯。

struct 的結構體的成員,不可以被 const 修飾,但是使用自定義 struct 變量類型創建的變量,可以被 const 修飾,然後通過 struct 的構造函數進行初始化。

對 const 變量進行初始化的時候也必須使用常量表達式。

然而 array,或者包含 array 的 struct,由於 array 不能被初始化,所以它們不能被 const 修飾。

const 還有另外一種用法,就是作爲函數的參數,表明傳入的參數是隻讀的。

函數參數也只能使用 const 這一種存儲修飾符。而函數返回值則不適用存儲修飾符。

attribute

用處是從 OpenGL ES 中向 VS 中傳輸數據的時候使用,OpenGL ES 可以通過 API 得到指定 VS 中某個 attribute 的位置,然後通過這個位置以及另外一個 API, 將數據傳輸到 VS 中。主要是用於傳入頂點座標、顏色等信息的。

attribute 變量只能被定義在 VS 中,如果在別的 shader 中定義 attribute 變量會出現錯誤。

attribute 變量在 shader 中屬於只讀變量。

attribute 修飾符只能修飾 float、vec2、vec3、vec4、mat2、mat3、mat4 類型的變量,可以看出這些全部都是隻包含 float 類型變量的變量。

在說 vec 類型的變量的時候,我們就說了,GPU 支持 vec 類型變量的目的是爲了減少運算次數,達到一次運算得到之前多次運算結果的目的。而且 GPU 硬件爲了配合支持這種運算,也支持 vec 這種類型的格式。而在這裏,attribute 也支持 vec 類型的變量,也就是可以將 CPU 傳輸過來的數據,保存到 GPU 中支持這種格式的硬件中。但是 GPU 中的這種硬件也是有限的,也就是說只能保存一定數量的 vec 類型變量。而這種類型的變量又非常好用,所以需要儘可能多的使用這種類型的變量,於是,硬件層面上,將 attribute 所能存放的區域全部都使用的是這種類型的硬件,所以如果 attribute 修飾的變量爲 float,也會佔用一個 vec4 的位置,所以 attribute 應該儘可能的組合成 vec4 類型的變量。而 mat4 則佔用了 4 個 vec4 的位置,mat2 佔用了 2 個,mat3 佔用了 3 個。雖然 GPU 中,attribute 所能存放的區域都使用這種類型的硬件,但是依然是有限的,所以硬件支持的在一個 shader 中使用的 attribute 的數量是有限的,這個限制在 Khronos(OpenGL ES Spec 的制定者)那裏有最低限制,但是每個平臺不同,支持的 attribute 數量也就不同。

attribute 修飾符不能修飾 array 或者 struct 類型的變量。原因也就比較簡單了,因爲 array 和 struct 不容易控制他們的尺寸和數量。

這裏需要注意的一點是,如果一個 attribute 在 shader 中被定義了,但是沒有被使用,則在計算 shader 中 attribute 數量的時候,它不會被計算上,因爲它會被 shader 優化掉。

另外,shader 中所有的 attribute,都必須是全局變量,也就是 attribute 的定義必須在 main 函數以及其他函數之外。

uniform

用處是從 OpenGL ES 中同時向 VS 和 PS 傳輸數據的時候使用。OpenGL ES 可以通過 API 得到 shader 中某個 uniform 的位置,然後通過這個位置以及另外一個 API,將數據傳輸到 shader 中的該 uniform。在 shader 中,uniform 屬於只讀數據。 uniform 修飾符修飾的變量,可以是任何類型,甚至是 array 和 struct。

和 attribute 類型,uniform 也受到了限制,不同的是,attribute 是數量受到了限制,而 uniform 是尺寸受到了限制,因爲畢竟 uniform 支持 array 和 struct, 只限制數量意義不大。至於這個限制也在 Khronos(OpenGL ES Spec 的制定者) 那裏有最低限制,但是每個平臺不同,支持的 uniform 的尺寸也就不同。同樣的, 如果一個 uniform 被定義了,但是沒有被使用,則它也不會被加入用於計算 uniform 的尺寸,因爲它也會被優化掉。

但是還有一點需要注意,除了開發者定義的 uniform,shader 本身還會有 build-in 的 unform,這些 uniform 也會被加入用於計算 uniform 的尺寸,用於判斷是否超過限制,build-in 的 uniform 我們會在後面進行解釋說明。

如果超過限制了的話,會導致 shader 編譯時錯誤或者鏈接時的錯誤。

uniform 被稱爲 global 變量,區別於 local 變量以及 attribute 中的全局變量, uniform 不止是在當前 shader 必須是全局變量,如果一個 VS 和一個 PS 被鏈接在一起使用,那麼它們會使用同一個 global uniform name space,也就是說,如果 VS 和 PS 中分別定義了一個變量名相同的 uniform,那麼它們的變量類型和精度修飾符等信息必須完全一樣,然後可以認爲這兩個變量是一個變量。

uniform 主要用於傳入矩陣、紋理貼圖等信息的。

varying

用處是從 VS 向 PS 傳輸數據的時候使用。在說 OpenGL ES pipeline 的時候我們介紹過,VS 運算得到的結果是 OpenGL ES 傳入的幾個圖形關鍵點的最終座標, 然後從 VS 到 PS 的時候會經過光珊化,光珊化會根據這些關鍵點生成很多點用來組成圖形的形狀。那麼假如在 VS 中定義一個 varying 變量,那麼 VS 中運算的每一個點都會包含一個 varying 變量對應的值,而 VS 只會針對 OpenGL ES 制定的幾個點做運算,假如 OpenGL ES 只是要畫一個三角形,那麼經過 VS,就會得到這個三角形三個點的頂點座標值。而如果將三個點的顏色通過 attribute 傳入 VS, 那麼 VS 就知道這三個點的顏色值,然後再通過 varying 把這三個點顏色傳入 PS, 那麼在傳遞的過程中,光珊化的時候,會根據原本那三個頂點包含的顏色值,進行插值,賦值給產生的新點。然後在 PS 中,會對所有產生的點進行運算,而針對每個點進行運算的時候,每個點都會有一個 varying 值,而這個 varying 值都是經過光珊化產生的新值。

光珊化受到 single-sample 和 multi-sample 的影響,也就是插值的算法不同, 等以後講 OpenGL ES 算法的時候再進行詳細講解,這裏就不進行展開說明了。

varying 在 VS 中是可讀可寫的,但是如果在其還沒有被寫之前,就對其進行讀取,那麼讀到的將會是 undefine。

varying 在 PS 中是可讀不可寫的,讀取的就是 PS 當前處理像素點經過光珊化後生成的 varying 值。

類似於 uniform,如果一個 VS 和一個 PS 鏈接在了一起,那麼如果在 VS 和 PS 中分別定義了一個變量名相同的 varying 變量,那麼它們的類型必須相同,否則鏈接就會出錯。這兩個變量的精度修飾符可以不同。

如果在 PS 中沒有定義 varying,或者定義了沒有使用。那麼在 VS 中,無論是沒有定義 varying,還是定義了沒有使用,或者是定義了且使用了,那麼都沒有問題。

如果在 PS 中定義了並且使用了一個 varying,但是在 VS 中沒有定義,則會出錯;而如果在 VS 中定義了沒有使用,沒有問題,但是 PS 中讀取到的這個 varying 的值則是 undefine;而如果在 VS 中定義了且使用了,如果使用的方式是給該 varying 賦值,那麼沒有問題,如果使用的方式沒有給 varying 賦值,也沒有問題, 只是 PS 讀取到的這個 varying 的值還是 undefine。

解釋一下這裏所指的使用,只是單純的說在 shader 中有一個語句中出現了這個 varying,不管是對這個 varying 進行賦值還是讀取,甚至該語句沒有被真正執行到,都算是對這個 varying 進行了使用。

varying 修飾符修飾的變量,可以是 float、vec2、vec3、vec4、mat2、mat3、mat4 或者是它們對應的 array 類型。但是不可以是 struct。

varying 變量也必須是全局變量,也就是 varying 的定義必須在 main 函數以及其他函數之外。

所以,我們看到了通過存儲修飾符,可以實現 OpenGL ES、VS、PS 之間的數據傳輸,但是我們延伸思考一下,由於 VS 都會對多個頂點進行運算,那麼在 VS 的多次運算之間,是否可以實現數值傳輸呢,也就是將上一個頂點的運算結果傳 給下一個頂點運算使用。那麼我來借用剛纔那個畫三角形的例子解釋一下,在那個例子中 VS 做了三次運算,PS 做了更多次的運算。但是在 VS 的三次運算之間是不能做數據傳輸的,PS 的也同樣,不能將上一個頂點運算的結果傳給下一個頂點使用。這是由於在 GPU 中,一般都是有很多核的,可能會同時進行多個頂點的運算,比如 VS 中可能三個頂點的運算是同時運行的,如果上一個頂點運算的結果可以傳給下一個頂點運算時使用,會導致不能並行進行頂點運算,這樣會導致 GPU 失去其相對於 CPU 的優勢。

上個課時我們提到了共享全局,在 GLSL 中,唯一的共享全局就是 uniform。 varying 不被認爲是共享全局的,因爲它必須在 VS 和 PS 中同時定義,並且通過 VS 傳給光珊化,再傳給 PS。共享全局的變量必須有相同的名字、類型、存儲、精度修飾符。


參數修飾符

下面要說的修飾符,叫做參數修飾符。

參數修飾符是用於函數的參數列表中。在別的有些語言中,也出現過類似的修飾符。參數修飾符分爲四種。

< none: default > in

一般默認,函數的傳入參數是將一個變量傳入函數中。所以如果函數的傳入參數中沒有參數修飾符,就等同於使用了 in 這個參數修飾符。

在函數中,如果一個參數是使用 in 修飾,而且沒有被 const 修飾,那麼它在函數中也可以被修改,但是其實修改的是函數中的那個變量的副本,並非傳入的那個函數,所以在函數之外,這個參數的值是不變的。

out

函數的參數在傳入的時候沒有被初始化,然後應該在函數中進行賦值,然後在函數外,也就是在調用函數的代碼塊中被使用。

out 變量不能被 const 修飾,如果函數中沒有對 out 變量進行賦值,那麼 out 變量爲 undefine。

Inout

函數的參數被傳入,然後在函數中再次被進行更新賦值,然後在函數外,也會被使用。inout 變量不能被 const 修飾。

這裏先說一下這些參數的大概意義,具體的等講到 GLSL 函數語法的時候再 進行詳細解釋說明。


精度修飾符

第三種修飾符,精度修飾符。其實在說存儲修飾符的時候,我們就稍微提到了精度修飾符。當時我們說,針對 uniform,如果把一個 VS 和 PS 鏈接在一起, 且兩個 shader 中分別定義了變量名相同的 uniform,那麼它們的精度修飾符也必須相同;而針對 varying,如果把一個 VS 和 PS 鏈接在一起,且兩個 shader 中分 別定義了變量名相同的 varying,那麼它們的精度修飾符可以不相同。

精度修飾符是什麼,顧名思義,精度修飾符就是用於決定所定義的變量最小可支持的範圍和精度。

先來解釋一下什麼是最小可支持的範圍和精度。在 shader 中,對 VS 和 PS 中的 float 和 int 都是有最小可支持的範圍和精度的。

比如在 VS 中,float 類型的變量支持的範圍至少爲負的 2 的 62 次方,到 2 的 62 次方。精度最少是 65536 分之 1。而 VS 中的 int 的範圍,至少是負的 2 的 16 次方,到 2 的 16 次方。

當然在 PS 中,希望 float 類型支持的變量的範圍和精度和 VS 中一樣,當然這樣對 PS 的要求比較高,所以在 PS 中,至少,float 類型的變量的範圍至少爲負 16384,到 16384,也就是負的 2 的 14 次方,到 2 的 14 次方。精度最少是 1024 分之 1。而 PS 中的 int 的範圍,至少是負 2 的 10 次方,到 2 的 10 次方。

現在我們知道 VS 和 PS 中 float 和 int 的最小可支持範圍和精度了。下面我們來解釋精度修飾符。

精度修飾符一共有三種。從高到低分別是 highp、mediump、lowp。舉個例子,比如 highp float x,就是定義了一個變量類型爲 float 的 x,而這個 x 的範圍 和精度爲 highp。

highp

是 VS 支持的最低要求,也就是剛纔所說的,float 類型的變量的範圍至少爲 負的 2 的 62 次方,到 2 的 62 次方。精度最少是 65536 分之 1。int 的範圍,至 少是負的 2 的 16 次方,到 2 的 16 次方。

mediump

是 PS 支持的最低要求,也就是剛纔所說的,float 類型的變量的範圍至少爲 負的 2 的 14 次方,到 2 的 14 次方。精度最少是 1024 分之 1。int 的範圍,至少 是負的 2 的 10 次方,到 2 的 10 次方。

lowp

float 類型的變量的範圍至少爲負 2,到 2。精度最少是 256 分之 1。int 的範 圍,至少是負的 2 的 8 次方,到 2 的 8 次方。

這些最小可支持範圍只是說硬件必須支持這些範圍和精度,而如果硬件做的更好,可以支持更大的範圍和更小的精度,那麼也沒有問題。

如果在 shader 中使用了硬件不支持的範圍和精度,比如在 PS 中,由於不要求硬件支持 highp,而剛好硬件確實不支持 highp。那麼如果在 PS 中使用了 highp, 就會產生編譯或者鏈接錯誤。

以上我們說了 VS 和 PS 中最基本的範圍和精度,和三種精度修飾符所支持的最基本的範圍和精度,而真正究竟支持的範圍和精度是什麼,每個平臺都不同, 在 OpenGL ES 中可以通過 OpenGL ES 的 API 進行查詢。

在 shader 中,也有一個預留的宏定義,如果 PS 也支持 highp。那麼宏定義 GL_FRAGMENT_PRECISION_HIGH 的值被定義爲 1,而如果不支持那麼廣的範圍和高精度,則該宏定義則沒有被定義,同時,由於 highp 這個修飾符在 PS 中是一個可選的特性,所以默認#extension 是把這個特性 disable 的。所以,如果想在 PS 中使用 highp,需要先通過#ifdef 判斷宏定義 GL_FRAGMENT_PRECISION_HIGH 是否被定義,如果定義了,則通過#extension 把特性 enable。

數字常數和 bool 變量都沒有精度修飾符。float 或者 int 的構造函數中的傳參如果在聲明構造函數的是時候沒有寫明精度修飾符,那麼傳入的參數就也沒有精度修飾符。

一般情況下,經過操作的精度以及運算得到的結果的精度應該不低於運算時傳入參數的精度,只是在少量的 buildin 的運算中,比如 atan,運算得到的結果的精度低於運算時傳入參數的精度。

如果某個參加運算的參數沒有精度修飾符,那麼就以另外一個參加運算的參數的精度修飾符爲準,如果都沒有,那麼就看下一個操作中的參數的精度修飾符。 以此類推,一直到找到一個精度修飾符爲止。這裏的下一個操作包括初始化賦值、 包括作爲別的函數的傳入參數、包括作爲別的函數的返回參數。如果依然找不到一個精度修飾符,那麼就認爲當前的精度修飾符爲默認值。一會我們將說一下什麼是默認的精度修飾符。

如果一個 float 操作運算的結果超出了保存結果的變量的範圍,那麼結果可能是該變量範圍的最大值或者表現成無窮大,反之,如果小於變量的範圍,則結果可能是該變量範圍的最小值或者表現成無窮小。同時,可能會導致越界、生成 NaN,或者異常。

類似的,如果 float 操作運算的結果太趨近於 0,以至於不在保存結果的變量的精度範圍內,則結果爲 0 或者是無窮小,但是如果表現成無窮小的話,正負符號一定要表達準確。

如果是 int 超出了範圍,那麼就會得到一個 undefine 值。

精度修飾符,類似於其他修飾符,不影響變量的基本變量類型。沒有構造函數可以使得一個變量從一個精度變成另外一個精度,構造函數只能轉換變量類型。

同樣的,精度修飾符,和其他修飾符一樣,也與函數重載無關,因爲我們知道如果兩個函數,函數名一致,也都只有一個參數,如果參數變量類型不同,那 麼就算是重載,但是如果參數變量類型相同,只是修飾符不同,那麼這不是重載。

還有,結構體的成員中,可以包含精度修飾符,但是不能包含別的修飾符。

在 struct 前面還可以加修飾符,但是修飾符並不屬於這個自定義變量類型 struct 的一部分,它只用來修飾當前定義的這個變量。

剛纔我們提到了默認的精度修飾符。

默認的精度修飾符是通過這個表達式創建的:precision 後面跟一個精度表達式比如 highp,mediump,lowp,在後面跟變量類型比如 float 或者 int 或者是 sample 類型(sample2D、sampleCube,因爲我們提到過 Sample 類型原則上是 int 類型的變種)。如果使用其他修飾符或者變量類型都會導致出錯。

那麼這樣,就確定了默認的精度表達式。

如果變量類型使用的是 float,那麼所有 float 相關的變量類型,比如 vec2 或 者 mat3 等,定義的變量如果沒有寫明精度修飾符或者無法判斷其精度修飾符, 那麼就使用默認精度修飾符。

同樣的,如果變量類型使用的是 int,影響的是所有 int 相關的變量類型,比 如 ivec2、ivec3 等。

這裏說到的變量包含了局部變量,全局變量,函數傳入參數、函數返回值。

默認精度修飾符也是有使用範圍的,比如全局的,或者局部的。如果一個變量沒有辦法判斷其精度修飾符,那麼就使用最近的一個且在使用範圍的默認精度修飾符。和變量一樣,如果只是某個代碼塊定義的一個默認精度修飾符,那麼出了這個代碼塊,就無效了。局部的默認精度修飾符在所在代碼塊中,會覆蓋掉全局的默認精度修飾符,依此類推嵌套的代碼塊也是裏面的在所在代碼塊中會覆蓋掉外面的。如果同一個代碼塊中出現兩個針對同一變量類型的默認精度修飾符, 則後寫的那個會覆蓋先寫的的那個。

在 VS 中,針對 float,int,sample2d,sampleCube 都有 buildin 的默認全局精度修飾符,其中 float 和 int 都是 highp,sample2D 和 sampleCube 都是 lowp。 在 PS 中,只針對 int,sample2d,samplecube 有 buildin 的默認全局精度修 飾符,其中 int 爲 mediump,sample2D 和 sampleCube 都是 lowp。PS 中 float 沒 有 buildin 的默認全局精度修飾符,所以開發者要麼把所有 float 相關變量類型定 義的變量全部加上精度修飾符,要麼就自定義一個默認全局精度修飾符。比如, 在 PS 中,使用 precision lowp float;,也就是定義了這個 PS 中,所有的 float 如果 沒有註明精度修飾符,那麼默認是 lowp 的。

只有精度修飾符可以用來修飾一個函數的 return 變量。


恆定修飾符

最後一種修飾符,恆定修飾符。

首先先舉個例子,假如有兩個 vertex shader,在這兩個 shader 中,使用同樣的表達式對 gl_Position 進行賦值,而且表達式的輸入參數完全一樣,但是即使是這樣,當這兩個 shader 運行的時候,gl_Position 的值也有可能不同。這就是 shader 的特性,當然這裏所說的不同,只是略微的不同,差別會非常小,一般情況下, 這種誤差一般是可以接受的(除非是 multi-pass 等機制可能會造成差別, multi-pass 屬於高級算法,這裏我們就不做多的介紹)。

簡單的解釋一下,是這樣的,在 shader 中,如果我們將一個變量定義成一個值,比如定義 a 爲 3.0,那麼 shader 並不會把 3.0 保存起來,而是在使用到 a 的時候,再根據場景重新計算,假如 a 的精度修飾符爲 lowp,那麼當它和 mediump 的 float 運算以及與和 lowp 的 float 運算,a 在這兩次運算中的值會因爲精度不同而不同。所以運算的結果也就不同。

invariant

而爲了避免這種差別,變量可以被定義成 invariant。可以將某個變量定義成 invariant,也可以定義一個全局的 invariant,使得該 shader 中所有可以被定義成 invariant 的變量都定義成 invariant。

如果想將一個特定的變量定義成 invariant,有兩種方式。一是將一個已經定義過的變量定義成 invariant,方法就是先定義一個變量,然後再用 invariant 恆定修飾符聲明一遍,這種方式的話,invariant 後面可以跟多個使用逗號分割的已經定義過的變量;二是在定義變量的時候直接講變量定義成 invariant,這種比較簡 單,比如invariant varying medium vec3 Color;

這兩種方式,是 invariant 恆定修飾符僅有的兩種使用方式。並且只有如下幾 種變量:VS 和 PS 的 buildin 的輸出變量,PS 的 buildin 的輸入變量,以及輸出 VS 的 varying 和輸入 PS 的 varying。

invariant 變量必須在變量被使用之前,就進行聲明。

爲了確保剛纔那個例子中兩個 shader 中特定的那個輸出的恆定,以下幾個條件必須滿足:

  • 兩個 shader 中特定的那個輸出必須要被聲明成 invariant。
  • 兩個 shader 的輸入參數必須完全一樣,因爲這些輸入參數可能會被用於輸出變量的賦值或者影響輸出變量賦值的條件判斷。
  • shader 中所用到的所有紋理,包含紋理內容,紋理格式,紋理屬性等信息必須完全一樣,因爲他們也可能對輸出變量產生影響。
  • 所有的輸入參數都受到相同的操作。所有條件表達式和中間表達式中的操作都必須完全一致,使用的參數順序一致,結合方式一致,就是看起來完全一致。中間變量和函數都必須定義成相同的類型(這裏的類型包含精度修飾符),所有會影響這個輸出值的流程都必須一致。

總的來說,就是所有的會影響到這個 invariant 輸出變量的數據流和控制流都必須一致。

初始狀態下,所有的輸出變量都是 variant 的,如果想讓所有的輸出變量都變成 invariant 的,可以直接在 shader 任何聲明之前,使用語法#pragma STDGL invaraint(all)。如果這個語法在變量或者函數聲明之後,那麼那些 invariant 的輸出變量會變成 undefine。

invariant 其實是在放棄了優化,損失了性能的情況下做到的,所以一般用於 debug 使用。

常量表達式無論在哪裏,結果都是恆定的,不管是在 VS 和 PS 中,或者兩個不同的 VS 中,只要它們的輸出參數一致,運算操作符一致,順序一致,精度一致,那麼得到的結果就是一致的。

被鏈接的一對 VS 和 PS,雖然精度修飾符可以不一致,但是恆定修飾符必須一致。

針對 buildin 的變量,只有在 gl_Position invariant 的時候,gl_FragCoord 才能 是 invariant;只有 gl_PointSize invariant 的時候,gl_PointCoord 才能是 invariant。 不能聲明 gl_FrontFacing 是 invariant,因爲它將聽從 gl_Position 來確定其是否是 invariant。

以上我們講述了一共四種修飾符,它們都可以用於聲明變量,如果在一個變量中使用多個修飾符,那麼這些修飾符必須有順序,順序是:

invariant-qualifier storage-qualifier precision-qualifier

storage-qualifier parameter-qualifier precision-qualifier


GLSL 的函數

截至到這裏,GLSL 中變量的部分就已經全部講完了,由於 GLSL 中的函數和 C、C++的非常類似,所以就不單獨開新的一節對函數從語法角度進行講解了。 就在這裏,對 GLSL 中的函數稍微進行一點說明。

函數在使用之前要先進行定義。

函數的參數中如果有數組,那麼數組一定要明確其長度。

如果函數會返回一個值,那麼可以將調用該函數當作表達式,這個表達式的類型就是函數返回值的類型。

如果一個函數確定有輸出函數,但是在函數體中沒有寫 return 語句,那麼返回 undefine。

函數 input 和 output 主要是通過複製,複製的時候修飾符不一定必須完全一致。

GLSL 中的函數支持重載。重載的函數函數名一樣,而傳入參數的類型不同。

如果只是輸出參數不同,或者傳入參數的修飾符不同,則不算重載。

VS 和 PS 中都必須有一個 main 函數,main 函數的傳入參數爲空,也不做任何 return,輸出爲 void。函數中也可以使用 return,這樣就會導致 main 函數提前結束。

在循環語句中,for 和 while 語句中都可以定義並初始化一個變量,但是do-while 卻不行。

跳轉語句:discard,discard 只能用在 PS 中,用於拋棄對當前像素的計算,由於 PS 之後還要做一些 test 然後更新 color buffer 等,而這個像素由於被拋棄了, 那麼也就不會更新該像素點的 buffer 信息了。最常見的用法就是在 PS 中檢測當前點的 alpha 是否小於 0,如果小於的話就會被拋棄。

關於函數的語法,也就先說明這麼多。

本節教程就到此結束,希望大家繼續閱讀我之後的教程。

謝謝大家,再見!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章