shader之間的數據傳遞


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;

好了,不胡扯了,說了這么多,有的同學可能都蒙圈了,知道你們最喜歡看總結,我們一起來總結一下.

總結

只有兩個選擇:

  1. Location based matching

  2. Block based matching with location

因為從GLSL轉換為SPIR-V的時候,需要為每個輸入輸出成員指定location. Named base matchingBlock based matching without location轉換SPIR-V的時候會報錯.

所以,除非你有充分的理由說服自己(例如,我不想用Vulkan😞). 否則,就選上面這兩個.

完~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM