OpenGL ES 2.0 知識串講 (4)——GLSL 語法(II)

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

 

上節回顧

上一節,我們講解了 Shader 的功能,並從預處理和註釋開始,講解 GLSL 的語法知識。想要學習和使用一門語言,必須先學習這門語言的語法,語法中除了上一節說到的預處理、註釋,還有更加重要的變量定義和使用,函數定義和使用, 以及 GLSL 的一些特殊語法。其中變量相關的知識包含變量類型,變量名,變量的操作等,這一節,我們將介紹變量的數據類型等相關知識。


GLSL 的變量

一個完整的程序,包括預處理、函數、變量等部分組成。這些部分合在一起, 詮釋了程序要做什麼事情,以及怎麼做。在基本的語言中,比如 C、C++,我們對這些已經很熟悉了,而 Shader 其實也就是使用 GLSL 語言編寫的完整程序。所以在 Shader 中,除了上一節所說的預處理,還有函數、變量等部分組成。下面, 我們來說一下 Shader 中的變量。

變量在使用之前先要進行定義,變量的定義是由變量類型和變量名組成的。 不存在默認的變量類型,所有的變量定義都必須明確一個變量類型,以及可選的變量類型修飾符。變量是通過特定一個變量類型,後面跟隨一個或者多個由逗號隔開的變量名來進行定義的。在許多情況下,我們也可以在定義變量的時候,在變量名後面追加一個=號和初始值來對變量進行初始化。


變量類型

我們先來說變量類型。首先,先回憶一下那些熟悉的變量類型。

void

代表空。可以把一個變量定義成 void,那麼也就是說明這個變量爲空,空不等於 0,不能對一個空的變量進行賦值。當 void 類型被用在函數中的時候,能用於函數返回類型,用於表明函數不返回任何值;或者是用來定義函數的傳入參數列表,表明函數使用一個空的參數列表,也就是不需要傳入任何參數。

bool

任何 bool 類型的變量都只能有 2 種取值,true 或者 false。但是在硬件層面, 其實沒有硬件直接支持這種類型的變量,所以,在硬件中,當處理到 bool 類型 的時候,可能會認爲比如 1、2 等爲 true,0 爲 false。true 和 false 這兩個關鍵字被定義爲 bool 常量。bool 變量定義的時候可以被初始化,在等號的右邊可以賦值任何 bool 類型的表達式,比如 true 或者 false 等。

bool 的初始化也可以使用 bool 類的構造函數。由於下面所有的類型的都會使用到構造函數,所以我們先把構造函數的知識再普及一下。對 C++熟悉的同學都會知道構造函數,在 C++中,構造函數是類被實例化的時候執行的第一個函數, 這個函數的特點就是函數名和類名完全一樣。

在 GLSL 中,也存在構造函數。當使用構造函數初始化某個變量的時候,構造函數的函數名就是該變量的類型名,構造函數傳入的值也就是初始化的值,當傳入值的類型與類型名,也就是構造函數函數名不符的時候,會做一次類型轉化。 比如下面這幾個例子。

bool(float) // converts a float value to a Boolean

int(bool) // converts a Boolean value to an int

float(int) // converts an integer value to a float

比如可以使用 bool 類型的構造函數,傳入 float 類型的值,先將 float 類型轉化成 bool 類型。類比一下,肯定也可以從 int 轉化成 bool 類型。

同樣的,int 類型的構造函數傳入 bool 類型的值,先將 bool 類型轉化成 int類型。類比一下,肯定也可以從 float 轉化成 int 類型。

還有,float 類型的構造函數傳入 int 類型的值,先將 int 類型轉化成 float 類型。類比一下,肯定也可以從 bool 轉化成 float 類型。 關於這些類型轉換,還有一點需要注意:比如 int(float),在這裏浮點類型的小數部分會被刪掉。再比如把 int 或者 float 轉換成 bool,那麼 0 或者 0.0 會被轉 換成 false,其餘爲 true。反之把 bool 轉換成 int 或者 float,那麼 false 會被轉換爲 0 或者 0.0,true 會被轉換成 1 或者 1.0。但是,比如 float(float),這樣類型不變的語法也合法,但是沒有任何意義。

關於 bool 類型,最後再說一下,條件語句中必須要使用到 bool 類型,這個等我們說到條件語句的時候再做說明。

int

int 在硬件層非常重要,比如用於循環之類。但是,GLSL 不要求底層硬件對大數字 int 操作支持的非常好,因爲 int 類型的變量可以先轉換爲 float 類型再進行操作。所以,如果想在 Shader 中使用數字變量,儘可能創建 float 類型的變量。 關於 int,我們還要知道 GLSL 支持 10 進制、8 進制和 16 進制的常量。使用 8 進 制的時候,需要在常量前面加 0 做前綴,而 16 進制,需要以 0x 爲前綴,x 大寫小寫都行。在 int 常量中,不允許存在空格,即使是在 8 進制或者 16 進制的前綴後面。如果用於表示一個負數,前綴加一個負數符號-,該符號不屬於常量,int 常量沒有字符後綴。

float

float 類型在 Shader 中廣泛使用,比如放大縮小某個數值等等。float 常量, 除了我們熟知的 1.5 或 1.或.1 之外,還可以支持科學技術法中的 e,比如 1.5e8 就是 1.5*10 的 8 次方。當使用 e 的時候,e 前面的數值可以不包含小數點,比如 1e-10,我們認爲該常量爲 float 類型。而如果不使用 e,那麼 float 常量中一定要包含小數點。同樣的,float 常量中也不可以包含空格,如果用於表示一個負數,前綴的那個負數符號-,不屬於 float 常量。

以上這些類型都是屬於標量類型。下面,我們來說一些之前接觸的不多的變量類型。

vector

vector 在 C++的 stl 語法中也支持,所以大家對 vector 可能還是比較熟悉的。 GLSL 支持一種類似 vector 類型的變量類型。只是,在 GLSL 中分的更細緻。 我們知道 vector 和數組有些類似,就是把同一個數據類型的多個值保存在一起。下面我們來一一解釋 GLSL 中的這些變量類型。

首先 vec2,就是兩個 float 類型的值保存在了一起。剛纔我們已經說了 float爲 GLSL 最主要的變量類型,所以這種不加任何前綴的 vec 變量類型,就是用於保存 float 值的,對應的 vec3 和 vec4 分別就是三個 float 類型的值保存在一起, 和四個 float 類型的值保存在一起。 這三種變量類型是非常重要的。在 Shader 中,我們用到的座標值和顏色值, 都是用 vec4 保存,用於保存該座標點的 xyzw 值或者 rgba 值,而紋理座標值, 則使用 vec2 值來進行保存,這個等我們說到的時候再進行詳細說明。

然後 bvec2,這個以字母 b 爲前綴的變量類型,是用於將兩個 bool 類型的值保存在了一起。對應的 bvec3 和 bvec4 分別就是將三個 bool 類型的值保存在一起, 和四個 bool 類型的值保存在一起。

bvec 變量類型主要用於 vector 之間進行比較的時候使用的。

最後以字母 i 爲前綴的變量類型 ivec2、ivec3、ivec4 分別用於將兩個 int 類型的值保存在一起,三個 int 類型的值保存在一起,和四個 int 類型的值保存在一起。

在 GLSL 中定義這種 vec 的變量類型,是因爲在 Shader 中存在大量這種多 component 的變量進行各種操作的運算,而如果直接在 GPU 中運行這種 vector 級別的運算,比一個一個進行單值運算要快的多。所以爲了提高效率,在 shader 中定義了這種 vec2、3、4 的變量類型,然後將這些變量直接保存到 GPU 中對應硬件上,通過 GPU 的對應模塊,一次運算可以得到之前 2 次 3 次 4 次或者更多次運算的結果,這樣從帶寬、運算效率和功耗上,都會得到大大的優化,所以定義這種變量非常有必要。目前基本上所有 GPU 都已經支持了這種 vec2、3、4 變量類型的運算。

由於 vec 類型中包含了多個成員變量,我們如果想訪問 vec 類型變量的每一個成員,只需要在變量名後面加一個點和一個字母即可。而字母也有很多種,比 如(x、y、z、w)是當 vec 類型保存的變量爲位置座標的時候,對應的 4 個成員名,(r、g、b、a)是 vec 類型保存的變量爲位置顏色的時候,對應的 4 個成員名,再比如(s、t、p、q)是 vec 類型保存的變量爲紋理信息的時候,對應的 4 個成員名,其中第三個成員名 p 本來應該是 r,但是爲了與 rgba 的 r 區分,就使用了 p。如果使用了超過所定義 vec 範圍的成員,就會出錯,比如定義了 vec2 location,location.x 沒有問題,但是使用 location.z 就會出錯了。一次可以選擇多個成員,比如 vec4 color,color.rgba 得到的是一個 vec4 類型變量,color.xyz 得到的是一個 vec3 類型變量。注意一定要使用同一組字母,如果是 color.xyba 就會出錯了。最多隻能選擇 4 個成員,如果寫 color.rgbarg,那麼會就出錯。但是要注意的是針對 vec2 的 location,使用 location.xyxy 則沒有問題。在 vec 類型中有一種 語法叫做 swizzle,就是將某個 vec 類型變量的成員調換位置,賦值給新的 vec 中, 比如 vec4 location1 = location.yyxx。

使用這種表達方式還可以對類型中的某些成員進行賦值,比如 location.x= 1.0,location.yx = vec2(1.0, 1.1)。但是需要注意的是比如 location.xx = vec2(1.0, 1.1) 是錯的,因爲 x 會被使用兩次,location.xy = vec3(1.0, 1.1, 1.2)也是錯誤的,因爲將一個 vec3 賦值給了一個 vec2,並且沒有使用 vec2 的構造函數做轉換。

除了使用這種點加字母的形式,也可以使用[]的形式,比如 location[1]就是 location 的第二個成員,等同於 location.y。而 location[4]就是錯誤的,因爲超出範圍了。

需要注意的是,比如 float 等標量並非等同於只包含一個成員的 vector,所以不能使用點或者[]來獲取所謂標量中唯一的那個成員,不然會出錯。

在定義這種變量的時候,也可以同時進行初始化,vec 的初始化基本上也是使用構造函數,比如下面這幾個例子。

vec3(float) // initializes each component of with the float

vec3(float, vec2) // vec3.x = float, vec3.y = vec2.x, vec3.z = vec2.y

在定義這種變量的時候,也可以同時進行初始化,但是由於 GLSL 中使用到的變量的數值大多都是從 OpenGL ES 傳入的,所以在這裏也就不細講 vec 這個類 型變量的初始化了。等說 mat 構造函數的時候,再在一起說明。

在標量數據類型的構造函數中,支持傳入 vec 類型這種非標量的數據,比如 傳入一個 vec3 的變量,那麼會把 vec3 變量的第一個值作爲輸出傳給 float 值。

mat

mat 變量類型和 vec 有點類似,只是 vec 保存的是一緯的,比如 vec2、vec3、 vec4 分別就是保存 2 個、3 個或者 4 個 float 類型的變量。但是 mat 變量類型是 二緯的,用於保存類似矩陣。

比如 mat4,用於保存 4*4 個 float 類型的變量,也就是保存了 16 個 float 類型的變量。圖形學學習者不可避免的都要接觸到矩陣變量,用於進行矩陣變換,比如本地座標系中的座標向世界座標系中進行轉換的時候,要用對應的座標點與轉換矩陣進行對應的運算。

而在 Shader 中,我們就要完成類似的運算,將傳入的座標點的值用 vec4 保存,將傳入的轉換矩陣用 mat4 保存,然後將它們相乘,按照數學中矩陣運算的算法,得到轉換後的座標點值。

所以 mat 這一系列的變量,在 Shader 中的使用也是非常廣泛的,除了剛纔我們解釋的 mat4,還有 mat2 和 mat3,分別是用於保存 2*2 個 float 類型的變量以及 3*3 個 float 類型的變量。Mat 只支持 float 類型,並不支持 bool 和 int,從這也可以看出 shader 中 float 類型是多麼的重要。

在硬件中,mat 變量是以列進行保存的,比如 mat2 中有 4 個變量,對應的矩陣位置分別爲左上,左下,右上,右下。然而這 4 個變量在硬件中,會按照左上,左下,右上,右下這種順序進行保存。也就是第一列保存完畢,再保存第二列,mat3 和 mat4 也是這樣的保存順序。無論是寫入還是讀取都是按照這樣的順序進行。

mat 的成員變量可以通過[]來訪問,如果一個 matrix 變量名後面跟一個[],[] 中寫入一個 int 常量,比如 mat4 a,獲取 a[0],那麼就是說把這個 mat 按列分成幾個 vector,a[0]就是 a 這個矩陣第一列的 vector,如果 matrix 變量名後面跟兩個[],那麼第二個[]就是作用在 vector 上的,比如 a[0][1]就是選擇了 a 這個矩陣第一列這個 vector 上的第二個元素。當使用 a[0]或者 a[0][1]的時候,就要把其當作 vect4 或者 float 來看。如果[]中的常量超過範圍,則報錯。

mat 變量主要是通過構造函數來進行初始化。

這裏我們將 vec 和 mat 的構造函數放在一起進行講解,構造函數的傳入參數可以是一套標量或者 vectors,甚至可以是 matrix。可以從大類型轉成小類型,和小類型轉成大類型。

比如,如果將一個標量傳入 vector 的構造函數中,那麼生成的這個 vector 中所有的值都是這個標量值。比如 vec3(float)。如果將一個標量傳入 matrix 的 構造函數中,那麼生成的這個 matrix 中對角線上的所有的值都是這個標量值, 其餘的將都是 0.0。比如 mat2(float)。 如果一個 vector 的構造函數中傳入多個標量、vector、matrix,或者是它們的混合體,vector 的成員會按照從左向右的順序,從參數中獲取值來進行賦值。 每個參數被使用完畢之後,才使用下一個參數進行賦值。比如 vec3(float,vec2)。 如果參數多了,沒關係,多的參數會被丟棄,比如 vec3(vec4),那麼 vec4 的第四個成員會被丟棄。matrix 的構造函數也一樣,matrix 的成員會按照列的方式從參數中讀取數據。必須傳入足夠的參數,來初始化 matrix 的成員。而且也不能傳多。比如 mat2(vec2,vec2),mat3(vec3,vec3,vec3),mat4(vec4,vec4, vec4,vec4),再比如可以使用 4 個或者 9 個或者 16 個 float 來構造 mat2,mat3, mat4。但是如果使用 mat2(vec3,vec3,vec3)就是錯誤的。

如果通過一個 matrix 來對另外一個 matrix 進行賦值,那麼傳入參數的第 i 行第 j 列,會按照相同的位置傳給被賦值的 matrix。其他沒有被賦值的地方會從 單位陣的對應位置獲取數據。如果通過 matrix 來對 matrix 進行賦值,那麼傳入參數中只能只有一個 matrix,而不能存在其他參數。比如 mat4(mat2)。

假如使用標量來賦值,但是被構造的類型與傳入標量類型不符合,那麼會對標量類型進行類型轉換。比如 vec4(int)。這樣會把 int 先轉化成 float,然後將 vec4 的四個成員變量都賦值成這個 float 值。

我們把 vec 和 mat 的操作也放在一起講。實際上對 vec 類型變量,或者 mat 類型變量做操作,就相當於對這樣變量的每個成員一一做操作。比如加法,那麼 vec2+vec2 等於 vec2,實際的執行過程也就是把兩個 vec2 的 x 相加,得到結果的 vec2 的 x。把兩個 vec2 的 y 相加,得到結果 vec2 的 y。

但是有一個是例外的,就是乘法。使用 vec 乘以 mat,或者 mat 乘以 vec, 或者 mat 乘以 mat。

vec3v,u; mat3m; 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;

比如 mat 乘以 vec,就是像我們這個例子中這樣,得到的結果是一個 vec, 結果中 vec 的 x 是 mat 的第一列的第一個成員乘以乘數 vec 的 x,加上 mat 的第 二列的第一個成員乘以乘數 vec 的 y,再加上 mat 的第三列的第一個成員乘以乘 數 vec 的 z。結果中 vec 的 y 是 mat 的第一列的第二個成員乘以乘數 vec 的 x,加 上 mat 的第二列的第二個成員乘以乘數 vec 的 y,再加上 mat 的第三列的第二個成員乘以乘數 vec 的 z。結果中 vec 的 z,就是 mat 的第一列的第三個成員乘以 乘數 vec 的 x,加上 mat 的第二列的第三個成員乘以乘數 vec 的 y,再加上 mat 的第三列的第三個成員乘以乘數 vec 的 z。而 vec 乘以 mat,得到的結果也是一個 vec,結果中 vec 的 x 是乘數 vec 乘以 mat 的第一列,y 是乘數 vec 乘以 mat 的第二列,z 是乘數 vec 乘以 mat 的第三列;而如果 mat 乘以 mat,那麼就是拿第一個 mat 的第 i 行乘以第二個 mat 的第 j 列,得到結果中 mat 的第 i 行 j 列的 一個成員。

下面要說的這兩種變量類型,在別的語言中完全沒有見過,屬於 GLSL 特有的兩種變量類型。

sample2D sampleCube

在 OpenGL ES 中有一個名詞叫做 texture,中文名是紋理貼圖,在遊戲中無論多麼絢麗的效果,都是由紋理貼圖來完成的。之前我們介紹過,如果在一個球上貼上一張地球的紋理貼圖,那麼這個球就變成了地球儀。所以貼圖的主要用處就是給一副畫賦予顏色。在 OpenGL ES 的整個 pipeline 中,貼圖的使用也佔據着一席之地,主要使用方法是在 OpenGL ES 中生成貼圖,然後傳給 GLSL 使用。而 sample2D 和 sampleCube 就是用於保存從 OpenGL ES 傳入,在 Shader 中使用的 2D 貼圖或者 CubeMap 貼圖的 handle。

當 Shader 拿到貼圖的 handle 之後,可以將其用於一些紋理貼圖相關的 buildin 函數。

由於 sample 變量是用於保存紋理貼圖的,而紋理貼圖又是由 OpenGL ES 傳入的,所以 sample 變量就不需要考慮其初始化,因爲它們的值全部是由 OpenGL ES API 傳入。

其實,某種意義上它們屬於 int 類型的變種。這個等我們之後在專門介紹紋理的課程中再做具體解釋說明。這裏只要知道在 Shader 中,有這麼一類,共兩 種變量類型,用於保存紋理貼圖的 handle 的。

如果以上的變量類型都屬於簡單數據類型,那麼下面這兩種就屬於複雜數據類型。

struct

開發者可以通過 struct 把一系列已知的變量類型封裝在一個名字中,創建屬於自己的變量類型。

比如我想創建一種變量類型,包含一個 float 變量和一個 vec3 變量,而我給這種自定義的變量類型取名叫做 type1,在創建這種變量類型的時候,我還想定義一個這種變量類型的變量 x,那麼可以這麼寫:

struct type1 {float a; vec3 b;}x;

可以看到左大括號前面,指定的是這種自定義變量類型的類型名 type1,右 大括號後面是我剛定義的這種變量類型的一個變量實例 x。

在這種自定義變量類型定義好之後,如果還想使用這個變量類型定義新的變量實例 x1,那麼就和定義別的類型的變量一樣,直接寫 type1 x1 即可。

需要注意的是,這裏我們創建了一個自定義變量類型 type1,假如在此之前, 我們定義了一個變量或者函數或者另外一個自定義變量類型也叫做 type1,那麼之前的那個 type1 從這裏開始就會失效,從現在開始,在當前 namespace,當前代碼塊中,type1 指的就是這個新的自定義變量類型。

struct 的結構體主體部分,必須包含至少一個成員,比如我們定義的這個 struct 中就有兩個成員。

struct 成員的類型必須是已經定義好的,不支持嵌套定義等。

struct 的成員在聲明的時候不能進行初始化。

成員可以是 array,但是在定義的時候需要明確一個大於 0 的 int 常量,表明該 array 的尺寸。

struct 可以是嵌套的,且每一級都是一個獨立的 namespace,定義的變量名只需要在當前級是唯一的即可。

C 語言中,我們可以用 struct 來製作位域,可以將一個 32bit 的 int 拆成多個成員,每個成員位數不定,而總位數位爲 32bit。這種用法叫做 bit fields,目前 GLSL 中的 struct 還不支持這種用法。

如果在一個自定義變量類型 struct type2 中的成員包含一個 struct type1,那 麼在定義 struct type2 的時候,需要在其中聲明一個 type1 的變量,而不能直接在 type2 的結構體中寫一個 type1。

類似於 vector 中獲取成員或者 swizzle 語法,struct 也可以使用點來指定成員, struct 支持.,==,!=,=操作。等於操作符和賦值操作符只支持兩個操作數的類型是相同的結構體。只有當兩個操作數的所有成員都一樣,才認爲兩個結構體一樣。等於操作符和賦值操作符不支持包含 array 或 sample 類型的 struct。

struct 的初始化主要是通過 struct 的構造函數進行。

比如剛纔我們定義的 struct 類型 type1,那麼構造函數的函數名就是 type1, 傳入參數與struct的成員對應,那麼type1的構造函數就是type1 name1 = type1 (3.0,(1.0, 1.1, 1.2);

傳入參數必須按照 struct 中聲明的成員的順序和類型。

假如 struct 的任何成員變量有任何限制,那麼該 struct 也受到相應的限制。 struct 可以被當作函數輸入參數,而且如果 struct 中不包含 array,則也可以當作函數輸出函數。

array

array 也屬於大衆數據類型,同樣類型的多個變量可以被放入一個 array 中, 只需要定義一個名字後面加[],[]中填寫一個數字即可。這個數字代表着 array 的尺寸,必須是一個大於 0 的 int 常量表達式。比如我們定義一個 float 的 array, float a[5]。如果我們使用一個 index 超過或者等於 array 的尺寸,那麼會出現錯誤, 比如我們使用 a[5]或者 a[6]就會出錯。如果我們使用一個負的 index 也會 出錯,比如 a[-1]。這裏的出錯導致的結果根據平臺的不同而不同,可能會得到未定義數值,也可能直接導致 memory crash。

array 唯一支持的操作符就是[],而我們平時的使用方式都是,使用[]得 到 array 中的某一個元素,這個元素可能是 float,可能是 int,也可能是其他,然後針對這個元素進行操作。

在 GLSL 中只支持一維數組,所有的基本類型或者 struct 都可以組裝成 array。 在 shader 中不支持在定義 array 的時候進行初始化。

array 可以被當作函數輸入參數,不能當作函數輸出函數。 最後還要說一點,GLSL 不支持指針。

GLSL 語言,屬於類型安全的語言,不支持類型之間的隱式類型轉換。以上就是 GLSL 的全部數據類型。


變量的範圍

在這一節的最後,說一下關於變量範圍的知識點。變量的範圍決定了變量的可見域。GLSL 使用了嵌套式範圍系統,允許在一個 Shader 中出現多個相同名字的變量,只是這些名字要定義在不同的 namespace 中。

所謂嵌套的範圍系統,我們用變量定義來解釋一下:假如變量定義中在所有函數之外,那麼它就有了全局定義,可見域就是從它被定義開始,一直到當前 shader 的結束。這個也就是嵌套範圍系統中最外面的套。其次,就是定義在一個函數中,或者定義在一個任意語句塊中,變量的可見域也就是變量被定義開始, 一直到語句塊的結尾處。

變量在被定義之後就開始生效,比如下面這個例子,在外面的範圍定義了 x =1,在內部的範圍定義了 x=2,然後緊接着定義 y=x。那麼由於內部範圍中 x =2 已經生效了,那麼 y 也就是等於 2 了。

int x=1; {int x = 2, y = x; // y is initialized to '2’ }

再比如下面這個例子,S 是一個結構體,先定義了一個 S 類型的變量 S,然後在這句話結束之後,變量 S 纔開始生效,所以在第二行,使用的 S 就是變量 S 了。

struct S { int x;};

{

S S = S(0,0); // 'S' is only visible as a struct and constructor

S; // 'S' is now visible only as a variable

}

我們剛纔說了 GLSL 使用嵌套式範圍系統,在同一個範圍不能定義兩個變量名相同的變量,在不同的範圍可以。根據作用域的不同,一個變量會覆蓋另外一個變量,並且在該作用域中,無法訪問被覆蓋的變量。

GLSL 中還存在一種範圍類型,叫做共享全局,共享全局的變量意思就是變量可以被多個 shader 訪問。vertex shader 和 fragment shader 分別擁有一個自己的全局範圍,函數定義只能定義在全局範圍中,不能定義在語句塊中。而共享全局是一塊獨立的範圍。關於 Shader 中存在哪些共享全局,我們將在下一節進行說明。

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

謝謝大家,再見!

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