shader之間傳遞數據實在是太常用了. 下面我們總結幾種shader之間傳遞數據的方法.
Name based matching
最簡單,也是最常用的一種傳遞方式是依靠名字進行匹配. 例如我們從vertex shader向fragment shader傳遞顏色:
//vertex shader
#version 460 core
out vec4 color;
void main(void)
{
color = ...;
...
}
//fragment shader
#version 460 core
in vec4 color;
out vec4 outputColor;
void main(void)
{
outputColor = color;
}
對於只有兩個shader stage的程序,這種方式非常方便.
假如我們在vertex shader和fragment shader中間插入geometry shader,並且將color從vertex shader傳遞到geometry shader,然后再傳遞到fragment shader. 就會寫出類似的代碼:
//vertex shader
out vec4 color;
----------------------
//geometry shader
in vec4 color[];
out vec4 colorFromGeom;
----------------------
//fragment shader
in vec4 colorFromGeom;
如果我們繪制某些圖元的時候不需要geometry shader,直接拿上面的vertex shader和fragment shader是沒法使用的,因為color和colorFromGeom名字不相同. 那我們就只能重寫一個fragment shader,僅僅把名字colorFromGeom改成color,以便與vertex shader匹配起來. 但是這樣我們就必須同時維護兩個幾乎一樣的fragment shader!
Location based matching
我們可以為變量分配一個location,只要輸出變量的location與輸入變量的location相同,它們就能匹配成功,即使名字不一樣也沒關系!舉個最簡單的例子:
//vertex shader
layout (location = 0) out vec3 normalOut;
layout (location = 1) out vec4 colorOut;
-----------------------------------------
//fragment shader
layout (location = 0) in vec3 normalIn;
layout (location = 1) in vec4 colorIn;
因為vertex shader中的normalOut與fragment shader中的normalIn的location都是0,所以它們能夠匹配起來. colorOut和colorIn也是一樣.
這樣,剛剛提到的Named based matching的問題就能夠得到解決.
//vertex shader
layout (location = 0) out vec3 normalOut;
layout (location = 1) out vec4 colorOut;
-----------------------------------------
//geometry shader
layout (location = 0) in vec3 normalIn[];
layout (location = 1) in vec4 colorIn[];
layout (location = 0) out vec3 normalOut; //in和out修飾的變量,即使location相同也沒關系
layout (location = 1) out vec4 colorOut;
-----------------------------------------
//fragment shader
layout (location = 0) in vec3 normalIn;
layout (location = 1) in vec4 colorIn;
可以看到即使去掉geometry shader,vertex shader的normalOut、colorOut也能與fragment shader的normalIn、colorIn匹配起來.
這種方法也引入了一個新的問題,考慮如下代碼:
//vertex shader
layout (location = 0) out vec3 someAttribute[2];
layout (location = 1) out vec4 colorOut; //錯誤,colorOut和someAttribute[1]同時占用location 1
void main(void)
{
someAttribute[0] = ...;
someAttribute[1] = ...;
colorOut = ...;
...
}
由於一個location最多存放 4*32 = 128 個字節,也就是最多能夠存放4個int或者float類型的數據. someAttribute[0]會占用location 0. someAttribute[1]和colorOut會由於同時占用location 1 而沖突,所以我們需要把colorOut的location改為2,使之獨享一個location.
//vertex shader
layout (location = 0) out vec3 someAttribute[2];
layout (location = 2) out vec4 colorOut; //正確,colorOut和someAttribute[2]不再同時占用相同的location
也就是說,這種方法需要我們自己推算輸入輸出變量的location. 如果我們把someAttribute[2]改為someAttribute[3],那colorOut的location就需要改為3. 維護這些變量的location稍微有一丟丟麻煩.
Block based matching
第三種方法是通過把輸入或者輸出變量打成一個組,即interface block.
//vertex shader
#version 460 core
out Data //matched by "Data"
{
vec3 normal;
float scale;
vec3 color;
vec2 texCoord;
} vs_out;
void main(void)
{
vs_out.normal = ...; //referenced by "vs_out"
vs_out.scale = ...;
}
//fragment shader
#version 460 core
in Data //matched by "Data"
{
vec3 normal;
float scale;
vec3 color;
vec2 texCoord;
} fs_in; //referenced by "fs_in"
...
interface block有點像結構體. 它們通過block name進行匹配,也就是以Data這個名字,使vertex shader輸出數據和fragment shader的輸入數據對應起來;然后通過instance name(vs_out,fs_in)進行引用,例如vs_out.normal.
就算中間插入一個geometry shader,那也是OK的.
//geometry shader
in Data
{
vec3 normal;
float scale;
vec3 color;
vec2 texCoord;
} gs_in[];
out Data
{
vec3 normal;
float scale;
vec3 color;
vec2 texCoord;
} gs_out;
甚至,我們可以省略instance name.
#version 460 core
out Data
{
vec3 normal;
float scale;
vec3 color;
vec2 texCoord;
};
void main(void)
{
normal = ...;
scale = ...;
}
引用的時候直接用數據成員的名字. 不過這樣退化成了name based matching,沒有實際意義.
有一點需要注意,就是我們不僅需要匹配block name,member name也是需要匹配的. 例如,下面的代碼就是錯誤的.
//vertex shader
out Data
{
vec3 normal;
float scale;
vec3 color;
vec2 texCoord;
} vs_out;
-------------------
//fragment shader
in Data
{
vec3 Normal; //錯誤,Normal無法和normal匹配
float scale;
vec3 color;
vec2 texCoord;
} fs_in;
這種方法看起來完美無缺啊. 既能夠解決name based matching的代碼不能復用問題,還不用維護location based matching的location. 但是,我還有殺手鐧.
Block based matching with location
其實,我們也可以為interface block指定一個起始的location.
//vertex shader
layout(location = 0) out Data
{
vec3 someAttribute[2];
vec2 texCoord;
float scale;
} vs_out;
-------------------
//fragment shader
layout(location = 0) in Data
{
vec3 some Attribute[2];
vec2 texCoord;
float scale;
} fs_in;
編譯器會根據起始的location為每個數據成員分配一個location.
如果我們寫出類似的代碼:
//vertex shader
layout(location = 0) out Data
{
vec3 someAttribute[2];
vec2 texCoord;
float scale;
} vs_out;
layout(location = 3) out vec3 color_vs_out; //錯誤,color_vs_out的location必須大於等於4
someAttribute占用兩個location,texCoord占用一個location,scale占用一個location,所以color_vs_out的location必須要大於等於4.
為block指定location之后,還可以為成員重新指定location.
//vertex shader
layout(location = 0) out Data
{
vec3 someAttribute[2];
layout(location = 4) vec2 texCoord;
float scale;
} vs_out;
同學們可以自行推算someAttribute和scale兩個成員的location,並進行驗證.
但是,不要太異想天開,譬如不為block指定location,只給成員指定了location.
//vertex shader
out Data
{
vec3 someAttribute[2];
layout(location = 4) vec2 texCoord; //錯誤,其它成員沒有分配location
float scale;
} vs_out;
這種寫法無法為someAttribute推算出location. 所以編譯器直接從語法上杜絕了此類寫法,哪怕你為第一個成員someAttribute[2]指定一個location也不行.
//vertex shader
out Data
{
layout(location = 0) vec3 someAttribute[2]; //錯誤,仍然無法為其它成員分配location
vec2 texCoord;
float scale;
} vs_out;
不過,我們倒是可以為所有的成員顯式指定location,並且不用為block指定起始location.
//vertex shader
out Data
{
layout(location = 0) vec3 someAttribute[2]; //正確,為所有的成員分配了location
layout(location = 2) vec2 texCoord;
layout(location = 3) float scale;
} vs_out;
有的同學可能會問,這樣做有什么意義,還不如為一個block指定location方便啊.
當然有意義,我表演給你看.
//vertex shader
//vertex shader,以下兩種方式都可以
out Data
{
layout(location = 0) vec3 someAttribute[2];
layout(location = 2, component = 0) vec2 texCoord; //component同location一樣,也是表示起始位置
layout(location = 2, component = 2) float scale; //根據上一個變量的占用空間,推算出location和component
layout(location = 3) vec4 color;
} vs_out;
/**********************************************************************************/
layout(location = 0) out Data
{
vec3 someAttribute[2];
layout(location = 2, component = 0) vec2 texCoord;
layout(location = 2, component = 2) float scale;
vec4 color;
} vs_out;
剛才我們提到過,每個location總計4*32 = 128個字節的容量. 可以認為總共有4個component,每個component的容量是32字節. 上面的代碼,texCoord和scale均占用的是location 2,texCoord占用兩個component(0和1),scale占用一個component(2). 有了component關鍵字,我們就能夠充分利用location的空間.
而且,如果和不使用component關鍵字比起來,使用component的代碼可讀性更好:
//vertex shader
out Data
{
layout(location = 0) vec3 someAttribute[2];
layout(location = 2) vec2 texCoord_and_scale; //兩個屬性存放到一個變量里,可讀性變差
layout(location = 3) vec4 color;
} vs_out;
不過,這樣我們又要保證component的占位不能沖突了.
付出總是和回報成正比,手動擋就是比自動擋有操控感.
但是,單純的location based matching是不能使用component關鍵字的,例如:
layout(location = 2, component = 0) vec2 texCoord; //錯誤,component只能在interface block中使用
layout(location = 2, component = 2) float scale;
好了,不胡扯了,說了這么多,有的同學可能都蒙圈了,知道你們最喜歡看總結,我們一起來總結一下.
總結
只有兩個選擇:
-
用Location based matching
-
用Block based matching with location
因為從GLSL轉換為SPIR-V的時候,需要為每個輸入輸出成員指定location. Named base matching和Block based matching without location轉換SPIR-V的時候會報錯.
所以,除非你有充分的理由說服自己(例如,我不想用Vulkan😞). 否則,就選上面這兩個.
完~