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占用一个loction,所以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,并进行验证.

但是,不要太异想天开. 下面的代码就是错误的.

//vertex shader
out Data
{
    vec3 someAttribute[2];
    layout(location = 4) vec2 texCoord;	//错误
    float scale;
} vs_out;

这种写法无法为someAttribute推算出location. 所以编译器直接从语法上杜绝了此类写法,哪怕你为someAttribute[2]指定一个location也不行.

//vertex shader
out Data
{
    layout(location = 0) vec3 someAttribute[2];		//编译错误
    vec2 texCoord;
    float scale;
} vs_out;

不过,我们倒是可以为所有的成员显式指定location,并且不用为block指定起始location.

//vertex shader
out Data
{
    layout(location = 0) vec3 someAttribute[2];
    layout(location = 2) vec2 texCoord;
    layout(location = 3) float scale;
} vs_out;

有的同学可能会问,这样做有什么意义,还不如为一个block指定location方便啊.

当然有意义,我表演给你看.

//vertex shader
out Data
{
    layout(location = 0) vec3 someAttribute[2];
    layout(location = 2, component = 0) vec2 texCoord;
    layout(location = 2, component = 2) float scale;
    layout(location = 3) 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不能冲突了.

付出总是和回报成正比,手动挡就是比自动挡有操控感.

但是,单纯的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.

所以,除非你有充分的理由说服自己(例如,我不想用Vulkan

上一篇:c# – 如何使用新值更新XML节点?


下一篇:shader画圆利用smoothstep函数抗锯齿