变量和类型

所有变量和函数必须在使用前声明。变量和函数名称是标识符。

没有默认类型。所有变量和函数声明都必须具有声明的类型,并且可以选择性地使用限定符。通过指定其类型,后跟一个或多个以逗号分隔的名称,来声明一个变量。在许多情况下,可以使用赋值运算符 (=) 在声明时初始化变量。

可以使用 struct 来定义用户定义的类型,以将现有类型的列表聚合为一个名称。

OpenGL 着色语言是类型安全的。类型之间存在一些隐式转换。关于何时以及如何发生这种情况,请参阅“隐式转换”部分以及本规范中其他部分的相关引用。

基本类型

定义

基本类型是指语言中由关键字定义的类型。

OpenGL 着色语言支持以下基本数据类型,分组如下。

透明类型

类型 含义

void

用于不返回值函数

bool

条件类型,取值为 true 或 false

int

有符号整数

uint

无符号整数

float

单精度浮点标量

double

双精度浮点标量

vec2

双分量单精度浮点向量

vec3

三分量单精度浮点向量

vec4

四分量单精度浮点向量

dvec2

双分量双精度浮点向量

dvec3

三分量双精度浮点向量

dvec4

四分量双精度浮点向量

bvec2

双分量布尔向量

bvec3

三分量布尔向量

bvec4

四分量布尔向量

ivec2

双分量有符号整数向量

ivec3

三分量有符号整数向量

ivec4

四分量有符号整数向量

uvec2

双分量无符号整数向量

uvec3

三分量无符号整数向量

uvec4

四分量无符号整数向量

mat2

2 × 2 单精度浮点矩阵

mat3

3 × 3 单精度浮点矩阵

mat4

4 × 4 单精度浮点矩阵

mat2x2

mat2 相同

mat2x3

具有 2 列和 3 行的单精度浮点矩阵

mat2x4

具有 2 列和 4 行的单精度浮点矩阵

mat3x2

具有 3 列和 2 行的单精度浮点矩阵

mat3x3

mat3 相同

mat3x4

具有 3 列和 4 行的单精度浮点矩阵

mat4x2

具有 4 列和 2 行的单精度浮点矩阵

mat4x3

具有 4 列和 3 行的单精度浮点矩阵

mat4x4

mat4 相同

dmat2

2 × 2 双精度浮点矩阵

dmat3

3 × 3 双精度浮点矩阵

dmat4

4 × 4 双精度浮点矩阵

dmat2x2

dmat2 相同

dmat2x3

具有 2 列和 3 行的双精度浮点矩阵

dmat2x4

具有 2 列和 4 行的双精度浮点矩阵

dmat3x2

具有 3 列和 2 行的双精度浮点矩阵

dmat3x3

dmat3 相同

dmat3x4

具有 3 列和 4 行的双精度浮点矩阵

dmat4x2

具有 4 列和 2 行的双精度浮点矩阵

dmat4x3

具有 4 列和 3 行的双精度浮点矩阵

dmat4x4

dmat4 相同

请注意,在以下表格中,当说“访问纹理”时,sampler* 不透明类型访问纹理,而 image* 不透明类型访问指定类型的图像。

浮点不透明类型

类型 含义

sampler1D
texture1D
image1D

用于访问 1D 纹理的句柄

sampler1DShadow

用于访问具有比较功能的 1D 深度纹理的句柄

sampler1DArray
texture1DArray
image1DArray

用于访问 1D 数组纹理的句柄

sampler1DArrayShadow

用于访问具有比较功能的 1D 数组深度纹理的句柄

sampler2D
texture2D
image2D

用于访问 2D 纹理的句柄

sampler2DShadow

用于访问具有比较功能的 2D 深度纹理的句柄

sampler2DArray
texture2DArray
image2DArray

用于访问 2D 数组纹理的句柄

sampler2DArrayShadow

用于访问具有比较功能的 2D 数组深度纹理的句柄

sampler2DMS
texture2DMS
image2DMS

用于访问 2D 多重采样纹理的句柄

sampler2DMSArray
texture2DMSArray
image2DMSArray

用于访问 2D 多重采样数组纹理的句柄

sampler2DRect
texture2DRect
image2DRect

用于访问矩形纹理的句柄

sampler2DRectShadow

用于访问具有比较功能的矩形纹理的句柄

sampler3D
texture3D
image3D

用于访问 3D 纹理的句柄

samplerCube
textureCube
imageCube

用于访问立方体贴图纹理的句柄

samplerCubeShadow

用于访问具有比较功能的立方体贴图深度纹理的句柄

samplerCubeArray
textureCubeArray
imageCubeArray

用于访问立方体贴图数组纹理的句柄

samplerCubeArrayShadow

用于访问具有比较功能的立方体贴图数组深度纹理的句柄

samplerBuffer
textureBuffer
imageBuffer

用于访问缓冲区纹理的句柄

subpassInput

用于访问浮点子通道输入的句柄

subpassInputMS

用于访问多采样浮点子通道输入的句柄

有符号整数不透明类型

类型 含义

isampler1D
itexture1D
iimage1D

用于访问整数 1D 纹理的句柄

isampler1DArray
itexture1DArray
iimage1DArray

用于访问整数 1D 数组纹理的句柄

isampler2D
itexture2D
iimage2D

用于访问整数 2D 纹理的句柄

isampler2DArray
itexture2DArray
iimage2DArray

用于访问整数二维数组纹理的句柄

isampler2DMS
itexture2DMS
iimage2DMS

用于访问整数二维多重采样纹理的句柄

isampler2DMSArray
itexture2DMSArray
iimage2DMSArray

用于访问整数二维多重采样数组纹理的句柄

isampler2DRect
itexture2DRect
iimage2DRect

用于访问整数二维矩形纹理的句柄

isampler3D
itexture3D
iimage3D

用于访问整数三维纹理的句柄

isamplerCube
itextureCube
iimageCube

用于访问整数立方体映射纹理的句柄

isamplerCubeArray
itextureCubeArray
iimageCubeArray

用于访问整数立方体贴图数组纹理的句柄

isamplerBuffer
itextureBuffer
iimageBuffer

用于访问整数缓冲区纹理的句柄

isubpassInput

用于访问整数子通道输入的句柄

isubpassInputMS

用于访问多重采样的整数子通道输入的句柄

无符号整数不透明类型

类型 含义

usampler1D
utexture1D
uimage1D

用于访问无符号整数一维纹理的句柄

usampler1DArray
utexture1DArray
uimage1DArray

用于访问无符号整数一维数组纹理的句柄

usampler2D
utexture2D
uimage2D

用于访问无符号整数二维纹理的句柄

usampler2DArray
utexture2DArray
uimage2DArray

用于访问无符号整数二维数组纹理的句柄

usampler2DMS
utexture2DMS
uimage2DMS

用于访问无符号整数二维多重采样纹理的句柄

usampler2DMSArray
utexture2DMSArray
uimage2DMSArray

用于访问无符号整数二维多重采样数组纹理的句柄

usampler2DRect
utexture2DRect
uimage2DRect

用于访问无符号整数矩形纹理的句柄

usampler3D
utexture3D
uimage3D

用于访问无符号整数三维纹理的句柄

usamplerCube
utextureCube
uimageCube

用于访问无符号整数立方体映射纹理的句柄

usamplerCubeArray
utextureCubeArray
uimageCubeArray

用于访问无符号整数立方体贴图数组纹理的句柄

usamplerBuffer
utextureBuffer
uimageBuffer

用于访问无符号整数缓冲区纹理的句柄

atomic_uint

用于访问无符号整数原子计数器的句柄

usubpassInput

用于访问无符号整数子通道输入的句柄

usubpassInputMS

用于访问多重采样的无符号整数子通道输入的句柄

采样器不透明类型

类型 含义

sampler

用于访问描述如何采样纹理的状态的句柄

samplerShadow

用于访问描述如何使用比较采样深度纹理的状态的句柄

此外,着色器可以使用数组和结构聚合这些基本类型来构建更复杂的类型。

没有指针类型。

在本规范中,聚合 指的是结构或数组。(矩阵和向量本身不是聚合。)聚合、矩阵和向量统称为复合类型

Void

不返回值的函数必须声明为 void。没有默认的函数返回类型。关键字 void 不能用于任何其他声明(除了空的形式或实际参数列表),否则会产生编译时错误。

布尔值

定义

布尔类型 是任何布尔标量或向量类型(boolbvec2bvec3bvec4

为了更容易表达代码的条件执行,支持 bool 类型。不期望硬件直接支持这种类型的变量。它是一个真正的布尔类型,仅保存两个值之一,表示真或假。可以使用两个关键字 truefalse 作为字面布尔常量。布尔值可以声明并选择性地初始化,如下例所示

bool success;      // declare "success" to be a Boolean
bool done = false; // declare and initialize "done"

用于条件跳转的表达式(iffor?:whiledo-while)必须计算为 bool 类型。

整数

定义

整型类型 是任何有符号或无符号的标量或向量整数类型。它不包括数组和结构。

标量整型类型 是标量有符号或无符号整数类型

向量整型类型 是有符号或无符号整数的向量

完全支持有符号和无符号整数变量。在本文档中,术语整数一般是指包括有符号和无符号整数。

对于 OpenGL,无符号整数具有精确的 32 位精度。当以 Vulkan 为目标时,highp 无符号整数具有精确的 32 位精度。

对于 OpenGL,有符号整数使用 32 位,包括一个符号位,采用二进制补码形式。当以 Vulkan 为目标时,highp 有符号整数使用 32 位,包括一个符号位,采用二进制补码形式。

当以 Vulkan 为目标时,mediumplowp 整数由 SPIR-V 的 RelaxedPrecision 修饰符定义。

加法、减法和乘法导致溢出或下溢将导致正确结果 R 的低 32 位,其中 R 的计算精度足以避免溢出或下溢。除法导致溢出会导致未定义的值。

整数可以声明并选择性地使用整数表达式初始化,如下例所示

int i, j = 42; // default integer literal type is int
uint k = 3u;   // "u" establishes the type as uint

字面整数常量可以用十进制(10 进制)、八进制(8 进制)或十六进制(16 进制)表示,如下所示。

整数常量 :

十进制常量 整数后缀可选
八进制常量 整数后缀可选
十六进制常量 整数后缀可选

整数后缀 : 其中之一

u U

十进制常量 :

非零数字
十进制常量 数字

八进制常量 :

0
八进制常量 八进制数字

十六进制常量 :

0x 十六进制数字
0X 十六进制数字
十六进制常量 十六进制数字

数字 :

0
非零数字

非零数字 : 其中之一

1 2 3 4 5 6 7 8 9

八进制数字 : 其中之一

0 1 2 3 4 5 6 7

十六进制数字 : 其中之一

0 1 2 3 4 5 6 7 8 9
a b c d e f
A B C D E F

整型常量中的数字之间不允许有空格,包括前导 0 之后、常量的前导 0x0X 之后,以及后缀 uU 之前。在进行词法分析时,会先识别符合上述规则的最长匹配标记,然后再开始新的标记。当存在后缀 uU 时,字面值的类型为 uint,否则类型为 int。前导的一元减号 (-) 被解释为算术一元取反,而不是常量的一部分。因此,字面值本身总是以非负语法表示,尽管它们可能产生负值。

如果提供的字面整数的位模式无法放入 32 位中,则会产生编译时错误。字面值的位模式总是直接使用,不做修改。因此,如果一个有符号字面值的位模式包含一个设置的符号位,则会创建一个负值。

例如,

1             // OK. Signed integer, value 1
1u            // OK. Unsigned integer, value 1
-1            // OK. Unary minus applied to signed integer.
              // result is a signed integer, value -1
-1u           // OK. Unary minus applies to unsigned integer.
              // Result is an unsigned integer, value 0xffffffff
0xA0000000    // OK. 32-bit signed hexadecimal
0xABcdEF00u   // OK. 32-bit unsigned hexadecimal
0xffffffff    // OK. Signed integer, value -1
0x80000000    // OK. Evaluates to -2147483648
0xffffffffu   // OK. Unsigned integer, value 0xffffffff
0xfffffffff   // Error: needs more than 32 bits
3000000000    // OK. A signed decimal literal taking 32 bits.
              // It evaluates to -1294967296
2147483648    // OK. Evaluates to -2147483648 (the literal set the sign bit)
5000000000    // Error: needs more than 32 bits

浮点数

单精度和双精度浮点变量可用于各种标量计算。通常,“浮点”一词将同时指代单精度和双精度浮点数。浮点变量的定义如下例所示:

float a, b = 1.5;    // single-precision floating-point
double c, d = 2.0LF; // double-precision floating-point

作为处理单元的输入值,单精度或双精度浮点变量应匹配相应的 IEEE 754 浮点定义,以确保精度和动态范围。着色器中的浮点变量也根据 IEEE 754 规范对单精度浮点值进行编码(逻辑上,不一定是物理上)。虽然编码在逻辑上符合 IEEE 754,但运算(加法、乘法等)不一定按照 IEEE 754 的要求执行。有关精度和 NaN(非数字)和 Inf(正或负无穷大)的使用的更多详细信息,请参阅“范围和精度”。

浮点常量定义如下。

floating-constant :

fractional-constant exponent-partopt floating-suffixopt
digit-sequence exponent-part floating-suffixopt

fractional-constant :

digit-sequence . digit-sequence
digit-sequence .
. digit-sequence

exponent-part :

e signopt digit-sequence
E signopt digit-sequence

sign : 以下之一

+ -

digit-sequence :

数字
digit-sequence digit

floating-suffix : 以下之一

f F lf LF

如果存在指数部分,则不需要小数点 (.)。浮点常量中的任何位置都不能出现空格,包括后缀之前。在进行词法分析时,会先识别符合上述规则的最长匹配标记,然后再开始新的标记。当存在后缀 "lf" 或 "LF" 时,字面值的类型为 double。否则,字面值的类型为 float。前导的一元减号 (-) 被解释为一元运算符,而不是浮点常量的一部分。

向量

OpenGL 着色语言包括用于浮点值、整数和布尔值的通用 2、3 和 4 分量向量的数据类型。浮点向量变量可用于存储颜色、法线、位置、纹理坐标、纹理查找结果等。布尔向量可用于对数值向量进行分量式比较。以下是一些向量声明的示例:

vec2 texcoord1, texcoord2;
vec3 position;
vec4 myRGBA;
ivec2 textureLookup;
bvec3 less;

向量的初始化可以使用构造函数完成。请参阅“向量和矩阵构造函数”。

矩阵

OpenGL 着色语言内置了 2 × 2、2 × 3、2 × 4、3 × 2、3 × 3、3 × 4、4 × 2、4 × 3 和 4 × 4 的浮点数矩阵类型。以 "mat" 开头的矩阵类型具有单精度分量,而以 "dmat" 开头的矩阵类型具有双精度分量。类型中的第一个数字是列数,第二个数字是行数。如果只有一个数字,则矩阵是方形的。示例矩阵声明:

mat2 mat2D;
mat3 optMatrix;
mat4 view, projection;
mat4x4 view; // an alternate way of declaring a mat4
mat3x2 m;    // a matrix with 3 columns and 2 rows
dmat4 highPrecisionMVP;
dmat2x4 dm;

矩阵值的初始化使用构造函数(在“向量和矩阵构造函数”中描述)按列主顺序完成。

不透明类型

定义

不透明类型是一种类型的内部结构对语言隐藏的类型。

以下部分列出的不透明类型声明的是实际是不透明句柄,指向其他对象的变量。这些对象通过内置函数访问,而不是通过直接读取或写入声明的变量。它们只能声明为函数参数或 uniform 限定的变量(请参阅“Uniform 变量”)。唯一接受内存限定符的不透明类型是图像类型。除了数组索引、结构成员选择和括号外,不允许不透明变量作为表达式中的操作数;此类使用会导致编译时错误。

当在着色器中聚合为数组时,除非另有说明,否则不透明类型只能使用动态统一的整数表达式(请参阅“动态统一表达式”)进行索引;否则,结果是未定义的。

不透明变量不能被视为左值;因此不能用作 outinout 函数参数,也不能对其赋值。任何此类使用都会导致编译时错误。但是,它们可以作为具有匹配类型和内存限定符的 in 参数传递。不能使用初始化器声明它们。

由于单个不透明类型声明有效地声明了两个对象,即不透明句柄本身和它指向的对象,因此既有存储限定符也有内存限定符的空间。存储限定符将限定不透明句柄,而内存限定符将限定它指向的对象。

纹理组合采样器

纹理组合采样器类型(例如,sampler2D)是基本类型表中描述的采样器类型,作为访问纹理的句柄。(它们不包括 samplersamplerShadow。)对于每种纹理目标,以及对于每种浮点数、整数和无符号整数数据类型,都有不同的纹理组合采样器类型。纹理访问通过内置的纹理函数(在“纹理函数”中描述)完成,而纹理组合采样器用于指定要访问的纹理以及如何对其进行过滤。

纹理组合采样器类型是不透明类型,声明和行为如上所述对不透明类型描述的一样。

图像

图像类型是不透明类型,声明和行为如上所述对不透明类型描述的一样。它们可以用内存限定符进一步限定。

图像变量是指向一维、二维或三维图像的句柄,这些图像对应于绑定到图像单元的纹理图像的单个级别中的全部或一部分。对于每个纹理目标,以及每种浮点型、整型和无符号整型数据类型,都有不同的图像变量类型。图像访问应使用与绑定到图像单元的纹理级别目标相匹配的图像类型,或者对于3D或数组图像的非分层绑定,应使用与图像层维度相匹配的图像类型(即,3D、2D数组、立方体或立方体数组的层应使用image2D,1D数组的层应使用image1D,2D多重采样数组的层应使用image2DMS)。如果图像目标类型与以此方式绑定的图像不匹配,如果数据类型与绑定的图像不匹配,或者如果格式布局限定符与OpenGL 规范第 8.25 节“纹理图像加载和存储”中所述的图像单元格式不匹配,则图像访问的结果是未定义的,但不包括程序终止。

图像变量在“图像函数”中描述的图像加载、存储和原子函数中使用,以指定要访问的图像。

原子计数器

原子计数器类型(例如 atomic_uint)是不透明的计数器句柄,其声明和行为如上面不透明类型所述。它们声明的变量指定在使用“原子计数器函数”中描述的内置原子计数器函数时要访问的计数器。它们按照“原子计数器布局限定符”中所述绑定到缓冲区。

结构的成员不能声明为原子计数器类型。

面向 Vulkan 时,原子计数器类型不可用。

纹理、samplersamplerShadow 类型

纹理(例如,texture2D)、samplersamplerShadow 类型是不透明类型,其声明和行为如上面不透明类型所述。这些类型仅在面向 Vulkan 时可用。纹理变量是指向一维、二维和三维纹理、立方体贴图等的句柄,如基本类型表中枚举的那样。对于每个纹理目标,以及每种浮点型、整型和无符号整型数据类型,都有不同的纹理类型。纹理可以与 samplersamplerShadow 类型的变量组合,以创建纹理组合的采样器类型(例如,sampler2D 或 sampler2DShadow)。这通过构造函数完成,例如,sampler2D(texture2D, sampler)sampler2DShadow(texture2D, sampler)sampler2DShadow(texture2D, samplerShadow)sampler2D(texture2D, samplerShadow),并在 5.4 节“构造函数”中进行了更详细的描述。

子通道输入

子通道输入类型仅在面向 Vulkan 时可用。

子通道输入类型(例如,subpassInput)是不透明类型,其声明和行为如上面不透明类型所述。

子通道输入类型是指向二维单采样或多采样图像的句柄,对于每种浮点型、整型和无符号整型数据类型,都有不同的类型。

子通道输入类型仅在片段着色器中可用。在任何其他阶段中使用它们是编译时错误。

结构体

可以使用 struct 关键字将其他已定义的类型聚合到结构体中来创建用户定义的类型。例如,

struct light {
    float intensity;
    vec3 position;
} lightVar;

在这个例子中,light 成为新类型的名称,而 lightVar 成为 light 类型的变量。要声明新类型的变量,请使用其名称(不带关键字 struct)。

light lightVar2;

更正式地说,结构的声明如下。但是,最终的语法在“着色语言语法”中给出。

结构体定义 :

限定符opt struct 名称opt_ { 成员列表 } 声明符opt ;

成员列表 :

成员声明 ;
成员声明 成员列表 ;

成员声明 :

基本类型 声明符 ;

其中 名称 成为用户定义的类型,并且可以用于声明此新类型的变量。名称 与其他变量、类型和函数共享相同的命名空间。所有先前可见的具有该名称的变量、类型、构造函数或函数都将被隐藏。可选的 限定符 仅适用于任何 声明符,而不是为 名称 定义的类型的一部分。

结构体必须至少有一个成员声明。不支持位域。成员类型必须已经定义(没有向前引用)。

成员声明可以包含精度限定符,但是使用任何其他限定符会导致编译时错误。如果成员声明不包含精度限定符,则成员的精度将按照 默认精度限定符 中描述的在结构类型声明时进行推断。

如果成员声明包含初始化器,则会导致编译时错误。成员声明符可以包含数组。此类数组必须指定大小,并且大小必须是大于零的常量整型表达式(请参见“常量表达式”)。结构的每个级别都有其自己的用于在成员声明符中给出的名称的命名空间;此类名称只需要在该命名空间内是唯一的。

不支持匿名结构。不支持嵌入式结构定义。这些会导致编译时错误。

struct S { float f; }; // Allowed: S is defined as a structure.

struct T {
    S;              // Error: anonymous structures disallowed
    struct { ... }; // Error: embedded structures disallowed
    S s;            // Allowed: nested structure with a name.
};

可以使用构造函数在声明时初始化结构体,如“结构体构造函数”中所述。

对类型或限定符的任何使用限制也适用于包含该类型或限定符的成员的任何结构体。这也适用于作为结构体的结构体成员,递归地适用。

数组

相同类型的变量可以通过声明一个名称,后跟括在可选大小的方括号 ([ ]) 中,从而聚合到数组中。如果存在,数组大小必须是大于零的常量整型表达式(请参见“常量表达式”)。大小参数的类型可以是带符号或无符号整数,并且类型的选择不会影响生成的数组的类型。数组只有一个维度(“[ ]”中的一个数字),但是,可以声明数组的数组。任何类型都可以构成数组。

数组对象有 3 种类型

显式大小

数组中的元素数量是显式给定的。

运行时大小

元素的数量没有给出,并且该数组是着色器存储块(参见“接口块”部分)最后声明的成员的最外层维度。 数组大小在运行时从支持着色器存储块的数据存储的大小推断得出。

无大小

数组元素的数量没有给出,并且该数组不是运行时大小的。

无大小的数组在经过显式大小的初始化器或使用显式大小的重新声明后,可能会变为显式大小的(显式大小和运行时大小的数组不能被重新声明)。使用不同的底层成员类型重新声明数组是编译时错误。

但是,请注意,除非另有说明,否则不能重新声明块;因此,用户声明的块中的无大小的数组成员不能通过块重新声明来确定大小。

如果任何初始化器之后,数组包含任何类型(该类型是或包含无大小的数组),则会发生编译时错误。

如果发生以下情况,则会发生编译时错误:

  • 使用除常量整型表达式之外的任何内容索引无大小的数组。

  • 将无大小的数组声明为函数的形参。

  • 将无大小的数组声明为函数的返回类型。

  • 将运行时大小或无大小的数组作为函数参数传递。

  • 使用负的常量表达式索引任何数组。

  • 使用大于或等于声明大小的常量表达式索引显式大小的数组。

  • 使用小于或等于着色器中之前用于索引数组的任何常量索引的大小重新声明无大小的数组。

使用大于或等于数组大小或小于 0 的非常量表达式索引数组会导致未定义的行为。

注意

GLSL 范围之外的一些功能,例如 OpenGL 或 Vulkan 的“稳健缓冲区访问”功能,可能会进一步限制此处哪些行为是有效的。 通常,这些功能会防止越界索引导致程序终止,并可能确定必须返回哪些值。

注意

所有数组本质上都是同质的;由具有相同类型和大小的元素构成,但有一个例外。 着色器存储块的数组(其最后一个成员是运行时大小的数组)允许各个块具有不同的大小,因此尾随数组中的元素数量也不同。

以下是一些数组声明的示例:

float frequencies[3];
uniform vec4 lightPosition[4u];
light lights[];            // Unsized. Valid in GLSL, illegal in ESSL.
const int numLights = 2;
light lights[numLights];
vec4 a[3][2];

// a shader storage block, introduced in section 4.3.7 "Buffer Variables"
buffer b {
    float u[]; // an error, unless u gets statically sized by link time
    vec4 v[];  // okay, v will be sized dynamically, if not statically
} name[3];     // when the block is arrayed, all u will be the same size,
               // but not necessarily all v, if sized dynamically

可以通过指定非数组类型(type_specifier_nonarray :)后跟 array_specifier : 来形成数组类型。 请注意,在这种上下文中,构造type [size] 并不总是产生长度为 size、类型为 type 的数组。例如,

float[5]    // an array of size [5] of float
float[2][3] // an array of size [2] of array of size [3] of float,
            // not size [3] of float[2]

这种数组类型可以在可以使用任何其他类型的任何地方使用,包括作为函数的返回值、作为数组的构造函数以及在声明中。

// As a function return type
float[5] foo() { }
// As an array constructor
float[5](3.4, 4.2, 5.0, 5.2, 1.1)
// In declaring an unnamed parameter
void foo(float[5])
// In normal declarations
float[5] a;

// The following 3 declarations are equivalent:
vec4 a[3][2]; // size-3 array of size-2 array of vec4
vec4[2] a[3];
vec4[3][2] a;

如果这种数组类型是无大小的,并且用作构造函数,则数组的大小将从构造函数参数推断得出。例如,

float a[5] = float[5](3.4, 4.2, 5.0, 5.2, 1.1);
float a[5] = float[](3.4, 4.2, 5.0, 5.2, 1.1);  // Constructor also of type float[5]

从运行时大小或无大小的数组赋值或向其赋值(而不是特定元素)是编译时错误。 请注意,这是一个罕见的情况,其中初始化器和赋值似乎具有不同的语义。 无大小数组的初始化器是有效的,并且会确定数组的大小,但是等效的赋值是无效的。 例如,

float a[5];
float b[];
// An initializer sizes an array ...
float c[] = a;  // c is explicitly size 5
// ... but the equivalent assignment is not valid
float d[];
d = a;          // Error. Assignment to an unsized array
// It is never valid to assign from an unsized array
float e[] = b;  // Error. b is unsized so cannot be assigned

或者,可以使用初始化列表语法来初始化数组的数组

vec4 a[3][2] = { vec4[2](vec4(0.0), vec4(1.0)),
                 vec4[2](vec4(0.0), vec4(1.0)),
                 vec4[2](vec4(0.0), vec4(1.0)) };

对于数组的数组,任何无大小的维度都由初始化器显式确定大小

vec4 a[][] = { vec4[2](vec4(0.0), vec4(1.0)), // okay, size to a[3][2]
               vec4[2](vec4(0.0), vec4(1.0)),
               vec4[2](vec4(0.0), vec4(1.0)) };

当在透明内存中(例如在统一块中)时,布局是内部(在声明中是最右边的)维度迭代速度快于外部维度。 也就是说,对于上述情况,内存中的顺序将是

低地址 : a[0][0] : a[0][1] : a[1][0] : a[1][1] : a[2][0] : a[2][1] : 高地址

length() 方法

可以使用 length() 方法获取数组中的元素数量

float a[5];
a.length(); // returns 5

vec4 a[3][2];
a.length()    // returns 3
a[x].length() // returns 2

返回值类型为 int

在无大小的数组上使用 length() 方法是编译时错误。 当且仅当数组是显式大小的,返回值才是常量表达式。

length() 方法返回编译时常量时,将解析应用 length() 方法的表达式,并服从正常的语言规则,但不会取消引用任何数组。 这意味着即使索引在运行时越界,返回的值也是明确定义的。 但是,请注意,由于仍然会检查作为常量表达式的索引,因此对于常量越界索引仍会生成错误。

length() 方法返回编译时常量,并且应用 length() 方法的表达式包含任何副作用(例如,在表达式中写入左值,或本身具有副作用的函数调用),则行为是未定义的。 实现可以给出编译时或链接时错误,但这不是必需的。

float a, b;
float[2](a=3.0, ++b).length(); // Behavior undefined. Illegal side effects

float c[5][3];
c[7].length(); // Error. Static indexing out of bounds.
c[i].length(); // Valid, returns 3 even if i < 0 or i >= 5 at runtime.

struct S {
    float a[3];
} s[5];
s[i+3].a.length(); // Valid. Returns 3 for all inputs i.
s[i++].a.length(); // Behavior undefined. Illegal side-effects.

buffer B {
    float x[3];
    float y[];
} b[5];
b[i++].x.length(); // Behaviour undefined. Illegal side-effects.
b[i++].y.length(); // Valid. i is incremented and b dereferenced. The runtime size
                   // of y is returned if 0 <= x < 5, behavior undefined if not.

精度使用与其他没有内在精度的情况相同的规则来确定。 请参阅“精度限定符”。

隐式转换

在某些情况下,表达式及其类型将隐式转换为不同的类型。 下表显示了所有允许的隐式转换

表达式的类型 可以隐式转换为

int

uint

int
uint

float

int
uint
float

double

ivec2

uvec2

ivec3

uvec3

ivec4

uvec4

ivec2
uvec2

vec2

ivec3
uvec3

vec3

ivec4
uvec4

vec4

ivec2
uvec2
vec2

dvec2

ivec3
uvec3
vec3

dvec3

ivec4
uvec4
vec4

dvec4

mat2

dmat2

mat3

dmat3

mat4

dmat4

mat2x3

dmat2x3

mat2x4

dmat2x4

mat3x2

dmat3x2

mat3x4

dmat3x4

mat4x2

dmat4x2

mat4x3

dmat4x3

没有隐式数组或结构转换。 例如,int 数组不能隐式转换为 float 数组。

完成隐式转换时,它与使用构造函数在显式转换下完成的转换相同。 通过构造函数的显式转换在 转换和标量构造函数 中描述。

在为二元运算符执行隐式转换时,可能存在多个数据类型可以将两个操作数转换为这些数据类型。 例如,当将 int 值添加到 uint 值时,这两个值都可以隐式转换为 uintfloatdouble。 在这种情况下,如果任一操作数具有浮点类型,则选择浮点类型。 否则,如果任一操作数具有无符号整数类型,则选择无符号整数类型。 否则,选择有符号整数类型。 如果操作数可以隐式转换为来自同一基本数据类型的多个数据类型,则使用组件大小最小的类型。

上表中的转换仅在本规范的其他部分指示时完成。

初始化器

在声明时,可以提供变量的初始值,指定为等号 (=) 后跟一个初始化器。 初始化器是赋值表达式或用花括号括起来的初始化器列表。 初始化器的语法为

初始化器 :

赋值表达式
{ 初始化列表 }
{ 初始化列表 , }

初始化列表 :

初始化器
初始化列表 , 初始化器

赋值表达式 是一个普通表达式,只是括号外的逗号 (,) 被解释为初始值设定项的结束,而不是序列运算符。如下文更详细的解释,这允许创建嵌套的初始值设定项:变量类型及其初始值设定项在嵌套、每一级的组件/元素/成员的数量以及组件/元素/成员的类型方面必须完全匹配。全局范围内的赋值表达式可以包含对用户定义函数的调用。

初始值设定项中的赋值表达式必须与它初始化的对象的类型相同,或者必须是可以根据“隐式转换”转换为对象类型的类型。由于这些包括构造函数,因此可以使用构造函数或初始值设定项列表来初始化复合变量;并且初始值设定项列表中的元素可以是构造函数。

如果初始值设定项是用大括号括起来的初始值设定项列表,则声明的变量必须是向量、矩阵、数组或结构。

int i = { 1 }; // illegal, i is not a composite

用一对匹配的大括号括起来的初始值设定项列表应用于一个复合类型。这可能是正在声明的变量,也可能是正在声明的变量中包含的复合类型。来自初始值设定项列表的各个初始值设定项按顺序应用于复合类型的元素/成员。

如果复合类型具有向量类型,则列表中的初始值设定项按顺序应用于向量的分量,从分量 0 开始。初始值设定项的数量必须与分量的数量匹配。

如果复合类型具有矩阵类型,则列表中的初始值设定项必须是向量初始值设定项,并且按顺序应用于矩阵的列,从第 0 列开始。初始值设定项的数量必须与列的数量匹配。

如果复合类型具有结构类型,则列表中的初始值设定项按结构中声明的顺序应用于结构的成员,从第一个成员开始。初始值设定项的数量必须与成员的数量匹配。

应用这些规则,以下矩阵声明是等效的

mat2x2 a = mat2(  vec2( 1.0, 0.0 ), vec2( 0.0, 1.0 ) );
mat2x2 b =      { vec2( 1.0, 0.0 ), vec2( 0.0, 1.0 ) };
mat2x2 c =      {     { 1.0, 0.0 },     { 0.0, 1.0 } };

以下所有声明都会导致编译时错误。

float a[2] = { 3.4, 4.2, 5.0 };         // illegal
vec2 b = { 1.0, 2.0, 3.0 };             // illegal
mat3x3 c = { vec3(0.0), vec3(1.0), vec3(2.0), vec3(3.0) }; // illegal
mat2x2 d = { 1.0, 0.0, 0.0, 1.0 };      // illegal, can't flatten nesting
struct {
    float a;
    int b;
} e = { 1.2, 2, 3 };                    // illegal

在所有情况下,应用于对象的最内层初始值设定项(即,不是用大括号括起来的初始值设定项列表)的类型必须与正在初始化的对象的类型相同,或者必须是可以根据“隐式转换”转换为对象类型的类型。在后一种情况下,在执行赋值之前,将对初始值设定项进行隐式转换。

struct {
    float a;
    int b;
} e = { 1.2, 2 }; // legal, all types match
struct {
    float a;
    int b;
} e = { 1, 3 };   // legal, first initializer is converted

以下所有声明都会导致编译时错误。

int a = true;                         // illegal
vec4 b[2] = { vec4(0.0), 1.0 };       // illegal
mat4x2 c = { vec3(0.0), vec3(1.0) };  // illegal

struct S1 {
    vec4 a;
    vec4 b;
};

struct {
    float s;
    float t;
} d[] = { S1(vec4(0.0), vec4(1.1)) }; // illegal

如果为未定大小的数组提供了初始值设定项(任一种形式),则数组的大小由初始值设定项中顶层(非嵌套)初始值设定项的数量确定。以下所有声明都会创建显式大小为 5 个元素的数组

float a[] = float[](3.4, 4.2, 5.0, 5.2, 1.1);
float b[] = { 3.4, 4.2, 5.0, 5.2, 1.1 };
float c[] = a;                          // c is explicitly size 5
float d[5] = b;                         // means the same thing

在为正在初始化的复合类型提供的初始值设定项列表中,初始值设定项的数量太少或太多是编译时错误。也就是说,数组的所有元素、结构的所有成员、矩阵的所有列以及向量的所有分量都必须恰好有一个初始值设定项表达式存在,且没有未使用的初始值设定项。

作用域

变量的作用域由其声明位置决定。如果它在所有函数定义之外声明,则它具有全局作用域,该作用域从声明它的位置开始,并持续到它声明所在的着色器的末尾。如果在 while 测试或 for 语句中声明,则其作用域为后续子语句的末尾。如果在 ifelse 语句中声明,则其作用域为该语句的末尾。(有关语句和子语句的位置,请参阅“选择”和“迭代”。)否则,如果它在复合语句中声明为语句,则其作用域为该复合语句的末尾。如果它在函数定义中声明为参数,则其作用域到该函数定义的末尾。函数的参数声明和主体一起构成嵌套在全局作用域中的单个作用域。 if 语句的表达式不允许声明新变量,因此不形成新的作用域。

在声明中,名称的作用域在存在初始值设定项的情况下紧接在初始值设定项之后开始,或者在不存在初始值设定项的情况下紧接在正在声明的名称之后开始。以下是一些示例

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

struct S
{
    int x;
};

{
    S S = S(0); // 'S' is only visible as a struct and constructor
    S;          // 'S' is now visible as a variable
}

int x = x; // Error if x has not been previously defined.
           // If the previous definition of x was in this
           // same scope, this causes a redeclaration error.

int f( /* nested scope begins here */ int k)
{
    int k = k + 3; // redeclaration error of the name k
    ...
}

int f(int k)
{
    {
        int k = k + 3; // 2nd k is parameter, initializing nested first k
        int m = k;     // use of new k, which is hiding the parameter
    }
}

对于 forwhile 循环,子语句本身不会为变量名引入新的作用域,因此以下代码会产生重新声明的编译时错误

for ( /* nested scope begins here */ int i = 0; i < 10; i++) {
    int i; // redeclaration error
}

do-while 循环的主体引入一个新作用域,该作用域仅在 dowhile 之间持续(不包括 while 测试表达式),无论主体是简单还是复合的

int i = 17;
do
    int i = 4;  // okay, in nested scope_
while (i == 0); // i is 17, scoped outside the do-while body

switch (…​) 后面的语句形成一个嵌套作用域。

给定作用域中的所有变量名、结构类型名称和函数名共享同一个命名空间。可以在同一作用域中重新声明函数名,参数相同或不同,而不会出错。可以在同一作用域中重新声明隐式大小的数组,该数组与相同基本类型的数组相同。否则,在一个编译单元中,不能在同一作用域中重新声明声明的名称;这样做会导致重新声明编译时错误。如果嵌套的作用域重新声明了外部作用域中使用的名称,则它会隐藏该名称的所有现有用法。没有办法访问隐藏的名称或使其取消隐藏,除非退出隐藏它的作用域。

内置函数的作用域在用户声明全局变量的全局作用域之外的作用域中。也就是说,着色器的全局作用域(可用于用户定义的函数和全局变量)嵌套在包含内置函数的作用域内。当在嵌套的作用域中重新声明函数名时,它会隐藏在外部作用域中声明的具有该名称的所有函数。函数声明(原型)不能出现在函数内部;它们必须位于全局作用域,或者对于内置函数,位于全局作用域之外,否则会导致编译时错误。

共享全局变量是在同一语言(即,同一阶段,例如顶点)内独立编译的单元(着色器)中以相同名称声明的全局变量,这些变量在制作单个程序时链接在一起。(在其他章节中讨论了在两种不同的着色器语言之间形成接口的全局变量。)共享全局变量共享相同的命名空间,并且必须使用相同的类型声明。它们将共享相同的存储。

共享全局数组必须具有相同的基本类型和相同的显式大小。在一个着色器中隐式大小的数组可以由同一阶段的另一个着色器显式指定大小。如果一个阶段中没有着色器具有数组的显式大小,则使用该阶段中最大的隐式大小(比使用的最大索引大 1)。没有跨阶段的数组大小调整。如果在声明它的阶段内没有对隐式大小的数组的静态访问,则该数组的大小为 1,这在数组在与与其他阶段或应用程序共享的接口块中声明时是相关的(其他未使用的数组可能会被优化器删除)。

共享全局标量必须具有完全相同的类型名称和类型定义。结构体必须具有相同的名称、类型名称序列、类型定义和成员名称才能被视为同一类型。此规则递归地应用于嵌套或嵌入类型。如果一个共享全局变量有多个初始化器,则这些初始化器都必须是常量表达式,并且它们必须都具有相同的值。否则,将导致链接时错误。(仅具有一个初始化器的共享全局变量不要求该初始化器为常量表达式。)

存储限定符

变量声明中,在类型前面最多可以指定一个存储限定符。这些限定符总结如下:

存储限定符 含义

<无:默认>

局部读/写内存,或函数的输入参数

const

值不能被更改的变量

in

从前一个阶段链接到着色器,变量被复制进来

out

从着色器链接到后续阶段,变量被复制出去

attribute

仅兼容性配置文件和顶点语言;在顶点着色器中与 in 相同

uniform

在正在处理的图元中值不发生变化,uniforms 形成着色器、API 和应用程序之间的链接

varying

仅兼容性配置文件和顶点及片段语言;在顶点着色器中与 out 相同,在片段着色器中与 in 相同

buffer

值存储在缓冲区对象中,可以由着色器调用和 API 读取或写入

shared

仅计算着色器;变量存储在工作组中的所有工作项之间共享

一些输入和输出限定变量最多可以使用一个额外的辅助存储限定符进行限定

辅助存储限定符 含义

centroid

基于质心的插值

sample

逐样本插值

patch

每个细分面片属性

并非所有限定组合都是允许的。辅助存储限定符只能与 inout 存储限定符一起使用。其他限定规则将在后续章节中定义。

局部变量只能使用 const 存储限定符(或不使用存储限定符)。

请注意,函数参数可以使用 constinout 限定符,但作为参数限定符。参数限定符在“函数调用约定”中讨论。

函数返回类型和结构体成员不使用存储限定符。

全局声明中的初始化器只能用于没有存储限定符、具有 const 限定符或具有 uniform 限定符的全局变量声明中。在声明中或由应用程序未初始化的没有存储限定符的全局变量将不会被初始化,而是以未定义的值进入 main()

当比较一个着色器阶段的输出与后续着色器阶段的输入时,如果它们的辅助限定符(或缺少)不相同,则输入和输出不匹配。

默认存储限定符

如果全局变量上没有限定符,则该变量与应用程序或在其他管道阶段运行的着色器没有链接。对于全局或局部无限定变量,声明将似乎分配与目标处理器相关的内存。此变量将提供对已分配内存的读/写访问。

常量限定符

可以使用 const 限定符声明命名编译时常量或只读变量。 const 限定符可以与任何非 void 透明基本数据类型以及这些类型的结构体和数组一起使用。在声明之外写入 const 变量是编译时错误,因此它们必须在声明时初始化。例如,

const vec3 zAxis = vec3 (0.0, 0.0, 1.0);
const float ceiling = a + b; // a and b not necessarily constants

结构体成员不能用 const 限定。结构体变量可以声明为 const,并使用结构体构造函数或初始化器进行初始化。

全局范围的 const 声明的初始化器必须是常量表达式,如“常量表达式”中所定义。

常量表达式

SPIR-V 特化常量在 GLSL 中表示为带有布局限定符 constant_idconst,如“特化常量限定符”中所述。

一个常量表达式是以下之一:

  • 字面值(例如,5true)。

  • 使用 const 限定符和初始化器声明的变量,其中初始化器是一个常量表达式。这包括使用特化常量布局限定符声明的 const,例如 layout(constant_id = …​),以及那些没有特化常量布局限定符声明的 const

  • 限定为 const 的内置变量。

  • 由对所有都是常量表达式的操作数进行运算而形成的表达式,包括获取常量数组的元素,或常量结构的成员,或常量向量的分量。但是,序列运算符 (,) 和赋值运算符 (=, +=, …​) 的最低优先级运算符不包括在可以创建常量表达式的运算符中。此外,使用特化常量作为索引的数组访问不会产生常量表达式。

  • 在显式大小的对象上有效使用 length() 方法,无论对象本身是否为常量(隐式大小或运行时大小的数组不会返回常量表达式)。

  • 参数均为常量表达式的构造函数。

  • 仅对于非特化常量:某些内置函数调用的返回值,其参数均为常量表达式,至少包括以下列表。任何其他不访问内存(不是纹理查找函数、图像访问、原子计数器等)、具有非 void 返回类型、没有 out 参数且不是噪声函数的内置函数也可能被视为常量。当使用作为特化常量的参数调用函数时,结果不是常量表达式。

    • 角度和三角函数

      • radians

      • degrees

      • sin

      • cos

      • asin

      • acos

    • 指数函数

      • pow

      • exp

      • log

      • exp2

      • log2

      • sqrt

      • inversesqrt

    • 常用函数

      • abs

      • sign

      • floor

      • trunc

      • round

      • ceil

      • mod

      • min

      • max

      • clamp

    • 几何函数

      • length

      • dot

      • normalize

  • 用户定义的函数(非内置函数)的函数调用不能用于形成常量表达式。

常量整数表达式是一个求值为标量有符号或无符号整数的常量表达式。

常量表达式将以不变的方式求值,以便在多个着色器中出现相同的常量表达式时创建相同的值。有关如何创建不变表达式的更多详细信息,请参阅“不变限定符”,有关表达式如何求值的详细信息,请参阅“精度限定符”。

常量表达式会遵守 preciseinvariant 限定符,但始终会以不变的方式进行求值,而与此类限定的使用无关,以便在多个着色器中出现相同的常量表达式时创建相同的值。有关如何创建不变表达式的更多详细信息,请参阅“不变限定符”和“精确限定符”。

常量表达式可以由主机平台进行求值,因此不需要计算出与在着色器执行目标上求值的相同表达式相同的值。但是,主机必须使用与目标将使用的精度相同或更高的精度。当无法确定精度限定时,表达式将以 highp 精度进行求值。请参阅“默认精度限定符”。

特殊化常量表达式永远不会由编译器前端进行求值,而是保留表达式的操作,以便稍后在主机上进行求值。

输入变量

着色器输入变量使用 in 存储限定符声明。它们构成 API 管道的前一阶段与声明着色器之间的输入接口。输入变量必须在全局范围内声明。在着色器执行开始时,来自前一管道阶段的值将复制到输入变量中。写入声明为输入的变量是编译时错误。

只有静态读取的输入变量才需要由前一阶段写入;允许有多余的输入变量声明。下表显示了这一点。

不匹配输入变量的处理

消耗着色器(输入变量)

未声明

已声明但无静态使用

已声明且有静态使用

生成着色器(输出变量)

未声明

允许

允许

链接时错误

已声明但无静态使用

允许

允许

允许(值未定义)

已声明且有静态使用

允许

允许

允许(值可能未定义)

消耗错误仅基于静态使用。对于编译器可以推断出可能导致消耗未定义值的任何动态使用,编译可能会生成警告,但不会生成错误。

有关内置输入名称的列表,请参阅“内置变量”。

顶点着色器输入变量(或属性)接收每个顶点的数据。在顶点着色器输入上使用辅助存储或插值限定符是编译时错误。复制的值由 API 或通过使用布局标识符 location 建立。

使用以下任何类型声明或包含以下任何类型的顶点着色器输入是编译时错误

顶点着色器中的示例声明

in vec4 position;
in vec3 normal;
in vec2 texCoord[4];

预计图形硬件将具有少量固定的向量位置,用于传递顶点输入。因此,OpenGL 着色语言将每个非矩阵输入变量定义为占用一个这样的向量位置。可以使用的位置数量存在一个与实现相关的限制,如果超出此限制,则会导致链接时错误。(未静态使用的已声明输入变量不计入此限制。)标量输入与 vec4 计入此限制的数量相同,因此应用程序可能需要考虑将四个不相关的浮点输入组合到一个向量中,以更好地利用底层硬件的功能。矩阵输入将占用多个位置。使用的位置数量将等于矩阵中的列数。

细分控制、求值和几何着色器输入变量获取前一个活动着色器阶段中具有相同名称的输出变量写入的每个顶点值。对于这些输入,允许使用 centroid 和插值限定符,但它们不起作用。由于细分控制、细分求值和几何着色器对一组顶点进行操作,因此每个输入变量(或输入块,请参阅下面的接口块)都需要声明为数组。例如,

in float foo[]; // geometry shader input for vertex "out float foo"

此类数组的每个元素都对应于正在处理的图元的顶点之一。每个数组可以选择声明一个大小。对于几何着色器,数组大小将由输入 layout 声明设置(或如果提供,则必须与该声明一致),该声明确定输入图元的类型,如稍后在“输入布局限定符”中所述。

某些输入和输出是数组化的,这意味着对于两个着色器阶段之间的接口,输入或输出声明都需要额外的数组索引级别才能使声明匹配。例如,在顶点着色器和几何着色器之间的接口中,顶点着色器输出变量和几何着色器具有相同名称的输入变量必须具有匹配的类型,不同之处在于几何着色器将比顶点着色器多一个数组维度,以便允许顶点索引。如果此类数组化接口变量未声明必要的附加输入或输出数组维度,则会导致链接时错误。几何着色器输入、细分控制着色器输入和输出以及细分求值输入相对于其他着色器输入和输出都具有额外的数组级别。这些输入和输出被称为每个顶点数组化的输入和输出。数组化接口的组件限制(例如,gl_MaxTessControlInputComponents)是每个顶点的限制,而不是整个接口的限制。

对于非数组化接口(意味着阶段之间的数组维度保持不变),如果输入变量未声明为与匹配的输出变量相同的类型(包括数组维度),则会发生链接时错误。

链接时类型匹配规则适用于所有已声明的输入和输出变量,无论它们是否使用。

此外,细分求值着色器支持使用 patchin 限定符声明的每个补丁输入变量。每个补丁输入变量都填充有细分控制着色器写入的每个补丁输出变量的值。每个补丁输入可以声明为一维数组,但不会按顶点编号进行索引。将 patch 限定符应用于输入只能在细分求值着色器中完成。与其他输入变量一样,每个补丁输入必须使用与前一个(细分控制)着色器阶段的每个补丁输出相同的类型和限定进行声明。在任何其他阶段使用 patch 输入都是编译时错误。

使用以下任何类型声明或包含以下任何类型的细分控制、细分求值或几何着色器输入是编译时错误

片段着色器输入获取每个片段的值,通常从前一个阶段的输出中插值而来。还可以应用辅助存储限定符 centroidsample,以及插值限定符 flatnoperspectivesmooth

使用以下任何类型声明或包含以下任何类型的片段着色器输入是编译时错误

片元着色器输入,如果其类型是或包含整型或双精度浮点类型,则必须使用插值限定符 flat 进行限定。

片元输入声明如下例所示

in vec3 normal;
centroid in vec2 TexCoord;
noperspective in float temperature;
flat in vec3 myColor;
noperspective centroid in vec2 myTexCoord;

片元着色器输入构成与顶点处理管道中最后一个活动着色器的接口。对于此接口,最后一个活动着色器阶段的输出变量和片元着色器中同名的输入变量必须在类型和限定符上匹配,但有几个例外:存储限定符当然必须不同(一个是 in,另一个是 out)。此外,插值限定符(例如 flat)和辅助限定符(例如 centroid)可能不同。这些不匹配在任何阶段之间都是允许的。当插值或辅助限定符不匹配时,片元着色器中提供的限定符将取代先前阶段中提供的限定符。如果片元着色器中完全缺少任何此类限定符,则将使用默认值,而不是先前阶段中声明的任何限定符。也就是说,重要的是片元着色器中声明的内容,而不是先前阶段着色器中声明的内容。

当使用来自两个独立程序对象的着色器形成着色器阶段之间的接口时,在程序链接时无法检测到输入和输出之间的不匹配。当此类接口上的输入和输出之间存在不匹配时,跨接口传递的值将部分或完全未定义。

着色器可以通过使用输入和输出布局限定符(“输入布局限定符”和“输出布局限定符”部分)或通过使用块或变量的相同输入和输出声明来确保此类接口之间的匹配。接口匹配的完整规则可在 OpenGL 规范的 7.4.1 节“着色器接口匹配”中找到。

计算着色器不允许用户定义的输入变量,并且不与任何其他着色器阶段形成正式接口。有关内置计算着色器输入变量的描述,请参见“计算着色器特殊变量”。计算着色器的所有其他输入都是通过图像加载、纹理提取、从 uniform 或 uniform 缓冲区的加载或用户提供的其他代码显式检索的。不允许在计算着色器中重新声明内置输入变量。

Uniform 变量

uniform 限定符用于声明全局变量,其值在整个正在处理的图元中相同。所有 uniform 变量都是只读的,并且在链接时或通过 API 在外部初始化。链接时的初始值是变量的初始值设定项的值(如果存在),如果不存在初始值设定项,则为 0。不透明类型不能具有初始值设定项,否则会导致编译时错误。当目标为 Vulkan 时,在块外声明 uniform 变量是编译时错误。

示例声明如下

uniform vec4 lightPosition;
uniform vec3 color = vec3(0.7, 0.7, 0.2); // value assigned at link time

uniform 限定符可以与任何基本数据类型一起使用,或者在声明其类型为结构体或这些类型的数组的变量时使用。

每个着色器类型可以使用的 uniform 存储量存在一个与实现相关的限制,如果超过此限制,将导致编译时或链接时错误。声明但未使用的 uniform 变量不计入此限制。着色器中使用的用户定义的 uniform 变量的数量和内置 uniform 变量的数量相加,以确定是否超过了可用的 uniform 存储。

当链接到程序或可分离程序时,着色器中的 Uniform 都共享一个全局名称空间。因此,所有静态使用的同名 uniform 变量的类型、初始值设定项和任何位置说明符必须在链接到单个程序的所有着色器中匹配。但是,不需要在所有链接的着色器中重复初始值设定项或位置说明符。虽然此单一 uniform 名称空间是跨阶段的,但 uniform 变量名称的作用域是每个阶段:如果一个 uniform 变量名称在一个阶段(例如,顶点着色器)中声明,但未在另一个阶段(例如,片元着色器)中声明,则该名称仍然可以在另一个阶段用于其他用途。

输出变量

着色器输出变量使用 out 存储限定符声明。它们构成声明着色器和 API 管道后续阶段之间的输出接口。输出变量必须在全局作用域中声明。在着色器执行期间,它们将像正常的无限定全局变量一样运行。它们的值在着色器退出时复制到后续的管道阶段。只需要写入后续管道阶段读取的输出变量;允许有冗余的输出变量声明。

没有 inout 存储限定符来声明一个变量名作为着色器的输入和输出。此外,不能使用 inout 限定符声明一个变量,这会导致编译时或链接时错误。输出变量必须使用与输入变量不同的名称声明。但是,在具有实例名称的接口块内嵌套输入或输出允许使用相同的名称,一个通过块实例名称引用。

顶点、细分评估和几何输出变量输出每个顶点数据,并使用 out 存储限定符声明。只能在细分控制着色器中对输出应用 patch。在任何其他阶段的输出上使用 patch 是编译时错误。

声明顶点、细分评估、细分控制或几何着色器输出,或其中包含以下任何类型的内容是编译时错误

各个输出的声明如下例所示

out vec3 normal;
centroid out vec2 TexCoord;
invariant centroid out vec4 Color;
flat out vec3 myColor;
sample out vec4 perSampleColor;

它们也可以出现在接口块中,如“接口块”中所述。接口块允许更简单地将数组从顶点添加到几何着色器的接口。它们还允许片元着色器对于给定的顶点着色器具有与几何着色器相同的输入接口。

细分控制着色器输出变量用于输出每个顶点和每个补丁数据。每个顶点输出变量都以数组形式(请参见“输入变量”下的 arrayed)声明,并使用不带 patch 限定符的 out 限定符进行声明。每个补丁输出变量使用 patchout 限定符声明。

由于细分控制着色器生成包含多个顶点的数组图元,因此每个顶点输出变量(或输出块,请参见下面的接口块)都需要声明为数组。例如,

out float foo[]; // feeds next stage input "in float foo[]"

此类数组的每个元素对应于正在生成的图元的一个顶点。每个数组可以选择声明一个大小。数组大小将由(或如果提供,则必须与)输出布局声明保持一致,该输出布局声明建立输出补丁中的顶点数,如稍后在“细分控制输出”中所述。

每个细分控制着色器调用都有一个对应的输出补丁顶点,并且只有当每个顶点输出属于该对应的顶点时,才能向其分配值。如果一个每个顶点输出变量被用作左值,则如果指示顶点索引的表达式不是标识符 *gl_InvocationID*,则是编译时或链接时错误。

除非使用内置函数 barrier(),否则细分控制着色器调用的执行顺序相对于同一输入补丁的其他调用是未定义的。这提供了一些对相对执行顺序的控制。当着色器调用调用 barrier() 时,其执行会暂停,直到所有其他调用都到达相同的执行点。在调用 barrier() 之前执行的任何调用所执行的输出变量赋值,在调用 barrier() 返回后,对任何其他调用都可见。

由于细分控制着色器调用在屏障之间以未定义的顺序执行,因此每个顶点或每个补丁输出变量的值有时将是未定义的。将着色器执行的开始和结束以及每次调用 barrier() 都视为同步点。在以下三种情况下,输出变量的值将是未定义的:

  1. 在执行开始时。

  2. 在每个同步点,除非

    • 该值在前一个同步点之后是明确定义的,并且自那以后没有被任何调用写入过,或者

    • 该值自前一个同步点以来被恰好一个着色器调用写入过,或者

    • 该值自前一个同步点以来被多个着色器调用写入过,并且所有此类调用执行的最后一次写入都写入了相同的值。

  3. 当被着色器调用读取时,如果

    • 该值在前一个同步点是未定义的,并且自那以后没有被相同的着色器调用写入过,或者

    • 输出变量在前一个和下一个同步点之间被任何其他着色器调用写入,即使该赋值发生在读取之后的代码中。

片段输出输出每个片段的数据,并使用 out 存储限定符声明。在片段着色器输出声明中使用辅助存储限定符或插值限定符是编译时错误。使用以下任何类型声明或包含片段着色器输出是编译时错误:

片段输出的声明如以下示例所示:

out vec4 FragmentColor;
out uint Luminosity;

计算着色器没有内置的输出变量,不支持用户定义的输出变量,并且不与任何其他着色器阶段形成正式的接口。计算着色器的所有输出都采用副作用的形式,例如图像存储和原子计数器的操作。

缓冲区变量

buffer 限定符用于声明全局变量,这些变量的值存储在通过 API 绑定的缓冲区对象的数据存储中。缓冲区变量可以读取和写入,底层的存储在所有活动的着色器调用之间共享。单个着色器调用中的缓冲区变量内存读取和写入按顺序处理。但是,在一个调用中执行的读取和写入相对于另一个调用执行的读取和写入的顺序在很大程度上是未定义的。可以使用内存限定符来限定缓冲区变量,从而影响底层内存的访问方式,如“内存限定符”中所述。

buffer 限定符可用于声明接口块(请参阅“接口块”),这些接口块随后被称为着色器存储块。在块外声明缓冲区变量是编译时错误。

// use buffer to create a buffer block (shader storage block)
buffer BufferName { // externally visible name of buffer
    int count;      // typed, shared memory...
    ...             // ...
    vec4 v[];       // last member may be an array that is not sized
                    // until after link time (dynamically sized)
} Name;             // name of block within the shader

对于每种类型的着色器使用的着色器存储块的数量、程序使用的着色器存储块的总数以及每个单独的着色器存储块所需的存储量,存在与实现相关的限制。如果超过这些限制中的任何一个,将导致编译时或链接时错误。

如果多个着色器链接在一起,则它们将共享一个全局缓冲区变量命名空间。因此,所有具有相同名称的声明的缓冲区变量的类型必须在链接到单个程序的所有着色器中匹配。

共享变量

shared 限定符用于声明在计算着色器工作组中的所有工作项之间共享存储的全局变量。声明为 shared 的变量只能在计算着色器中使用(请参阅“计算处理器”)。任何其他 shared 变量的声明都是编译时错误。共享变量是隐式一致的(请参阅“内存限定符”)。

声明为 shared 的变量不能有初始化器,并且其内容在着色器执行开始时是未定义的。写入 shared 变量的任何数据都将对同一工作组内的其他工作项(执行相同的着色器)可见。

在没有同步的情况下,着色器的不同调用对同一个 shared 变量的读取和写入顺序是未定义的。

为了实现对 shared 变量的读取和写入的排序,必须使用 barrier() 函数(请参阅“着色器调用控制函数”)采用控制流屏障。

单个程序中声明为 shared 的所有变量的总大小有限制。此限制以基本机器单位表示,可以通过使用 OpenGL API 查询 MAX_COMPUTE_SHARED_MEMORY_SIZE 的值来确定。

接口块

输入、输出、uniform 和缓冲区变量声明可以分组到命名的接口块中,以提供比单独声明所能实现的更粗粒度的支持。它们可以具有可选的实例名称,在着色器中使用该名称来引用其成员。一个可编程阶段的输出块由后续可编程阶段中相应的输入块支持。uniform 块由应用程序使用缓冲区对象支持。缓冲区块,也称为着色器存储块,也由应用程序使用缓冲区对象支持。在顶点着色器中使用输入块或在片段着色器中使用输出块是编译时错误。这些用法保留供将来使用。

接口块声明在语法中定义如下:

interface-block :

type_qualifier block-name { member-list } instance-nameopt ;

block-name :

identifier

成员列表 :

成员声明
member-declaration member-list

成员声明 :

layout-qualifieropt qualifiersopt type declarators ;

instance-name :

identifier
identifier array-specifier

下面讨论上述每个元素。

首先,一个示例:

uniform Transform {
    mat4 ModelViewMatrix;
    mat4 ModelViewProjectionMatrix;
    uniform mat3 NormalMatrix;      // allowed restatement of qualifier
    float Deformation;
};

以上示例建立了一个名为“Transform”的 uniform 块,其中包含分组在其中的四个 uniform。

类型限定符 决定了块将成为哪个接口的一部分,并且可以选择性地将其他限定符应用于该块。如果它不包含存储限定符 inoutuniformbuffer 中的一个,则会发生编译时错误。它可以选择性地包含 布局限定符辅助存储限定符 patch精确限定符buffer 块还可以包含 内存限定符。包含任何其他限定符都是编译时错误。

成员列表 声明了要分组到块中的变量。类型和声明符与其他块外部的输入、输出、uniform 和 buffer 变量声明相同,但以下情况例外:

  • 不允许使用初始化器

  • 不允许使用不透明类型

  • 结构定义不能嵌套在块内部

任何这些都会导致编译时错误。

如果在成员声明中未使用任何可选限定符,则成员的限定包括由 接口限定符 确定的所有 inoutpatchuniformbuffer。如果使用可选限定符,它们可以包括插值限定符、辅助存储限定符、精度限定符和存储限定符,并且它们必须声明与块的接口限定符一致的输入、输出或 uniform 成员:输入变量、输出变量、uniform 变量和 buffer 成员只能分别位于 in 块、out 块、uniform 块和着色器存储块中。

对于成员的存储限定符,重复使用 inoutpatchuniformbuffer 接口限定符是可选的。例如,

in Material {
    smooth in vec4 Color1; // legal, input inside in block
    smooth vec4 Color2;    // legal, 'in' inherited from 'in Material'
    vec2 TexCoord;         // legal, TexCoord is an input
    uniform float Atten;   // illegal, mismatched storage qualifier
};

uniformbuffer 存储块的成员在内存中始终表示为 highp,无论与声明关联的任何精度限定符如何。当从这些变量读取值或将值写入这些变量时,它们会按照 精度之间的转换 中描述的方式转换为声明的精度或从声明的精度转换而来。着色器内对值的操作将正常使用声明的精度进行。

一个着色器接口 被定义为以下之一:

  • 程序中声明的所有 uniform 变量和 uniform 块。这涵盖一个程序中链接在一起的所有编译单元。

  • 程序中声明的所有 buffer 块。

  • 相邻可编程管线阶段之间的边界:这涵盖了第一个阶段的所有编译单元中声明的所有输出和第二个阶段的所有编译单元中声明的所有输入。请注意,就此定义而言,片段着色器和之前的着色器被认为具有共享边界,即使在实践中,传递到片段着色器的所有值首先会通过光栅化器和插值器。

块名称(块名称)用于在着色器接口中进行匹配:一个管线阶段的输出块将与后续管线阶段中具有相同名称的输入块匹配。对于 uniform 或着色器存储块,应用程序使用块名称来标识块。块名称在着色器内除了接口匹配之外没有其他用途;在全局范围内使用块名称作为块名称以外的任何其他用途(例如,使用块名称作为全局变量名称或函数名称)是保留的,这是编译时错误。在同一个着色器中,即使块内容相同,在同一个着色器接口(如上定义)中为多个块声明使用相同的块名称也是编译时错误。

着色器接口(如上定义)中匹配的块名称必须在具有相同数量的声明、相同类型的序列和相同的成员名称序列方面匹配,并且还必须具有匹配的成员式布局限定(见下一节)。匹配的 uniform 或着色器存储块名称(但不是输入或输出块名称)还必须要么全部缺少实例名称,要么全部具有实例名称,将其成员置于同一作用域级别。当匹配的块名称上存在实例名称时,允许实例名称不同;它们不需要匹配,块也可以匹配。此外,如果匹配的块声明为数组,则数组大小也必须匹配(或遵循连续着色器阶段之间着色器接口的数组匹配规则)。任何不匹配都会生成链接时错误。允许在一个着色器的不同着色器接口中具有不同的块名称定义,例如,允许输入块和输出块具有相同的名称。

如果未使用实例名称(实例名称),则在块内部声明的名称的作用域为全局级别,并且像在块外部声明一样访问它们。如果使用实例名称(实例名称),则它会将所有成员置于其自己的命名空间内的作用域内,使用字段选择器(.)运算符进行访问(类似于结构)。例如,

in Light {
    vec4 LightPos;
    vec3 LightColor;
};
in ColoredTexture {
    vec4 Color;
    vec2 TexCoord;
} Material;           // instance name
vec3 Color;           // different Color than Material.Color
vec4 LightPos;        // illegal, already defined
...
... = LightPos;       // accessing LightPos
... = Material.Color; // accessing Color in ColoredTexture block

在着色语言之外(即在 API 中),成员的标识方式类似,但始终使用块名称代替实例名称(API 访问的是着色器接口,而不是着色器)。如果没有实例名称,则 API 不使用块名称来访问成员,只使用成员名称。

在着色器接口内,所有相同全局名称的声明都必须针对同一个对象,并且必须在类型上匹配,以及在是否声明没有实例名称的块的变量或成员上匹配。API 也需要此名称来唯一标识着色器接口中的对象。如果任何特定的着色器接口包含以下情况,则会出现链接时错误:

  • 两个不同的块,每个块都没有实例名称,并且每个块都具有相同名称的成员,或者

  • 块外部的变量,以及没有实例名称的块,其中变量与块中的成员具有相同的名称。

out Vertex {
    vec4 Position;  // API transform/feedback will use "Vertex.Position"
    vec2 Texture;
} Coords;           // shader will use "Coords.Position"
out Vertex2 {
    vec4 Color;     // API will use "Color"
    float Color2;
};

// in same program as Vertex2 above:
out Vertex3 {
    float Intensity;
    vec4 Color;     // ERROR, name collision with Color in Vertex2
};
float Color2;       // ERROR, collides with Color2 in Vertex2

对于声明为数组的块,在访问成员时还必须包含数组索引,如本示例所示:

uniform Transform { // API uses "Transform[2]" to refer to instance 2
    mat4 ModelViewMatrix;
    mat4 ModelViewProjectionMatrix;
    vec4 a[]; // array will get implicitly sized
    float Deformation;
} transforms[4];
...
... = transforms[2].ModelViewMatrix; // shader access of instance 2
// API uses "Transform.ModelViewMatrix" to query an offset or other query
transforms[x].a.length(); // same length for 'a' for all x
Transform[x];             // illegal, must use 'transforms'
Transform.a.length();     // illegal, must use 'transforms'
...transforms[2].a[3]...  // if these are the only two dereferences of 'a',
...transforms[3].a[7]...  // then 'a' must be size 8, for all
transforms[x]

对于声明为数组的 uniform 或着色器存储块,每个单独的数组元素都对应于一个单独的缓冲区对象绑定范围,支持该块的一个实例。由于数组大小表示所需的缓冲区对象数量,因此 uniform 和着色器存储块数组声明必须指定数组大小。uniform 或着色器存储块数组只能使用动态 uniform 整数表达式进行索引,否则结果是未定义的。

当使用 OpenGL API 入口点来标识块数组中单个块的名称时,名称字符串可以包含数组索引(例如,Transform[2])。当使用 OpenGL API 入口点引用块成员的偏移量或其他特征时,不得指定数组索引(例如,Transform.ModelViewMatrix)。

细分控制、细分求值和几何着色器输入块必须声明为数组,并遵循各自阶段的所有着色器输入的数组声明和链接规则。所有其他输入和输出块数组都必须指定数组大小。

每个着色器阶段可以使用的统一缓冲区块(uniform block)和着色器存储块(shader storage block)的数量都有实现相关的限制。如果超出任何一个限制,将会导致链接时错误。

布局限定符

布局限定符可以出现在几种声明形式中。它们可以作为接口块定义或块成员的一部分出现,如上一节的语法所示。它们也可以仅与一个接口限定符一起出现,以建立使用该限定符声明的其他内容的布局。

布局限定符 接口限定符 ;

或者,它们可以与使用接口限定符声明的单个变量一起出现

布局限定符 接口限定符 声明 ;

布局声明只能在全局作用域或块成员中进行,并且只能在以下小节中指示的位置进行;它们的详细信息特定于接口限定符,并会单独讨论。

布局限定符 展开为

布局限定符 :

layout ( 布局限定符ID列表 )

布局限定符ID列表 :

布局限定符ID
布局限定符ID , 布局限定符ID列表

布局限定符ID :

布局限定符名称
布局限定符名称 = 布局限定符值
shared

布局限定符值 :

整数常量表达式

用于布局限定符名称的标记是标识符,而不是关键字,但是 shared 关键字允许作为布局限定符ID。通常,它们可以以任何顺序列出。只有在下面明确指出时,才存在与顺序相关的含义。同样,这些标识符不区分大小写,除非另有明确说明。

一个声明中可以出现多个布局限定符。此外,同一个布局限定符名称可以在一个布局限定符内或同一声明中的多个布局限定符中出现多次。当同一个布局限定符名称在一个声明中出现多次时,最后一次出现会覆盖前一次或多次出现。此外,如果这样的布局限定符名称会影响后续的声明或其他可观察的行为,则只有最后一次出现才会产生任何影响,其行为就像声明中较早的出现不存在一样。对于覆盖布局限定符名称的情况也是如此,其中一个覆盖另一个(例如,row_majorcolumn_major);只有最后一次出现会产生任何影响。

整数常量表达式在“常量表达式”中定义为常量整数表达式,如果整数常量表达式是特化常量,则这是一个编译时错误。

下表总结了布局限定符的使用。它显示了每个限定符可以应用于哪些类型的声明。这些都将在以下各节中详细讨论。

布局限定符 仅限定符 单个变量 块成员 允许的接口

shared
packed
std140
std430

X

X

uniform / buffer

row_major
column_major

X

X

X

binding =

仅不透明类型

X

offset =

仅原子计数器

X

align =

X

X

set =

仅不透明类型

X

uniform / buffer(仅限 Vulkan)

push_constant

X

uniform(仅限 Vulkan)

input_attachment_index =

仅子通道类型

uniform(仅限 Vulkan)

location =

X

uniform / buffer 和子例程变量

location =

X

X

X1

所有 in / out,计算着色器除外

component =

X

X

index =

X

片段 out 和子例程函数

triangles
quads
isolines

X

细分求值 in

equal_spacing
fractional_even_spacing
fractional_odd_spacing

X

细分求值 in

cw
ccw

X

细分求值 in

point_mode

X

细分求值 in

points

X

几何 in/out

[ points ]
lines
lines_adjacency
triangles
triangles_adjacency

X

几何 in

invocations =

X

几何 in

origin_upper_left
pixel_center_integer

gl_FragCoord

片段 in

early_fragment_tests

X

local_size_x =
local_size_y =
local_size_z =

X

计算 in

local_size_x_id =
local_size_y_id =
local_size_z_id =

X

计算 in(仅限 SPIR-V)

xfb_buffer =
xfb_stride =

X

X

X

X

顶点、细分和几何 out

xfb_offset =

X

X

X

vertices =

X

细分控制 out

[ points ]
line_strip
triangle_strip

X

几何 out

max_vertices =

X

stream =

X

X

X

X

depth_any
depth_greater
depth_less
depth_unchanged

gl_FragDepth

片段 out

constant_id =

仅标量

const(仅限 SPIR-V)

rgba32f
rgba16f
rg32f
rg16f
r11f_g11f_b10f
r32f
r16f
rgba16
rgb10_a2
rgba8
rg16
rg8
r16
r8
rgba16_snorm
rgba8_snorm
rg16_snorm
rg8_snorm
r16_snorm
r8_snorm
rgba32i
rgba16i
rgba8i
rg32i
rg16i
rg8i
r32i
r16i
r8i
rgba32ui
rgba16ui
rgb10_a2ui
rgba8ui
rg32ui
rg16ui
rg8ui
r32ui
r16ui
r8ui

仅图像类型

uniform

1

位置限定符不允许用于数组块的成员,除了每个顶点的数组(请参阅“接口块”)。

输入布局限定符

特定于特定着色器语言的布局限定符将在下面的单独章节中讨论。

除了计算着色器之外的所有着色器都允许在输入变量声明、输入块声明和输入块成员声明中使用 location 布局限定符。其中,变量和块成员(而不是块)还允许使用 component 布局限定符。

布局限定符ID :

location = 布局限定符值
component = 布局限定符值

例如,

layout(location = 3) in vec4 normal;
const int start = 6;
layout(location = start + 2) in vec4 v;

将建立着色器输入 normal 被分配到向量位置编号 3,而 v 被分配到位置编号 8。对于顶点着色器输入,位置指定从中获取输入值的顶点属性的编号。对于所有其他着色器类型的输入,位置指定一个向量编号,该编号可用于与前一个着色器阶段的输出进行匹配,即使该着色器位于不同的程序对象中。

以下语言描述了给定类型消耗多少个位置。但是,几何着色器输入、细分控制着色器输入和输出以及细分求值输入相对于其他着色器输入和输出具有额外的数组级别。在考虑类型消耗多少位置之前,会从类型中删除此外部数组级别。

除了以 Vulkan 为目标时,如果顶点着色器输入是任何标量或向量类型,它将消耗单个位置。如果非顶点着色器输入,或者以 Vulkan 为目标时的任何阶段输入,是标量或向量类型(除了 dvec3dvec4),它将消耗单个位置,而类型 dvec3dvec4 将消耗两个连续的位置。

如果声明的输入(在如上所述可能移除外部数组层级后)是一个大小为n的数组,并且每个元素占用m个位置,那么它将被分配从指定位置开始的m * n个连续位置。例如,

layout(location = 6) in vec4 colors[3];

将确定着色器输入colors被分配到向量位置编号 6、7 和 8。

如果声明的输入是一个 n × m 的矩阵,它将被分配从指定位置开始的多个位置。每个矩阵分配的位置数量与 m 分量的 n 元素数组相同。例如,

layout(location = 9) in mat4 transforms[2];

将确定着色器输入transforms被分配到向量位置 9-16,其中transforms[0]被分配到位置 9-12,而transforms[1]被分配到位置 13-16。

如果声明的输入是一个结构体或块,其成员将按照声明顺序分配连续的位置,第一个成员被分配到布局限定符中提供的位置。对于结构体,此过程应用于整个结构体。在结构体的成员上使用 location 限定符是编译时错误。对于块,此过程应用于整个块,或者直到到达第一个带有 location 布局限定符的成员为止。

当一个块成员用 location 限定符声明时,其位置来自该限定符;成员的 location 限定符会覆盖块级别的声明。后续成员再次基于最新的位置分配连续的位置,直到下一个声明了 location 限定符的成员。位置使用的值不必按递增顺序声明。

如果一个块没有块级别的 location 布局限定符,则要求它的所有或没有成员都带有 location 布局限定符,否则会导致编译时错误。对于声明为数组的某些块,location 只能在块级别应用:当一个块被声明为数组,并且每个块数组元素的每个成员都需要额外的位置时,在块成员上指定位置是编译时错误。对于数组化接口(通常由于接口扩展而具有额外的数组层级),在应用此规则之前会去除外部数组。

在生成 SPIR-V 时,所有带有 inout 限定符的用户声明(非内置)变量和块(或其所有成员)都必须具有着色器指定的 location。否则,会生成编译时错误。

块和结构体成员消耗的位置通过递归应用上述规则来确定,就像结构体成员被声明为具有相同类型的输入变量一样。例如

layout(location = 3) in struct S
{
    vec3 a;                      // gets location 3
    mat2 b;                      // gets locations 4 and 5
    vec4 c[2];                   // gets locations 6 and 7
    layout(location = 8) vec2 A; // ERROR, can't use on struct member
} s;
layout(location = 4) in block
{
    vec4 d;                      // gets location 4
    vec4 e;                      // gets location 5
    layout(location = 7) vec4 f; // gets location 7
    vec4 g;                      // gets location 8
    layout(location = 1) vec4 h; // gets location 1
    vec4 i;                      // gets location 2
    vec4 j;                      // gets location 3
    vec4 k;                      // ERROR, location 4 already used
};

着色器可用的输入位置数量是有限的。对于顶点着色器,限制是公布的顶点属性数量。对于所有其他着色器,限制是与实现相关的,并且必须不小于公布的最大输入组件计数的四分之一。

如果任何附加的着色器使用的位置大于或等于支持的位置数量,则程序将无法链接,除非依赖于设备的优化能够使程序适应可用的硬件资源。

如果显式位置分配使得链接器无法在没有显式分配的情况下为其他变量找到空间,则程序将无法链接。

为了确定非顶点输入是否与前一个着色器阶段的输出匹配,location 布局限定符(如果有)必须匹配。

如果顶点着色器输入变量在着色器文本中没有分配位置,但在 OpenGL API 中指定了位置,则将使用 API 分配的位置。否则,此类变量将由链接器分配位置。有关更多详细信息,请参阅OpenGL 规范的第 11.1.1 节“顶点属性”。如果同一语言的多个着色器中声明的输入变量具有冲突的位置,则会发生链接时错误。

component 限定符允许更精细地指定标量和向量的位置,精确到位置内消耗的单个组件。在不指定 location 限定符的情况下使用 component 是编译时错误(顺序无关紧要)。位置内的组件是 0、1、2 和 3。从组件 N 开始的变量或块成员将消耗组件 NN+1N+2、…​ 直到其大小。如果此组件序列大于 3,则为编译时错误。标量 double 将消耗其中两个组件,而 dvec2 将消耗位置内的所有四个组件。dvec3dvec4 只能在不指定 component 的情况下声明。 dvec3 将消耗第一个位置的所有四个组件以及第二个位置的组件 0 和 1。这使得组件 2 和 3 可用于其他组件限定的声明。

例如

// a consumes components 2 and 3 of location 4
layout(location = 4, component = 2) in vec2 a;

// b consumes component 1 of location 4
layout(location = 4, component = 1) in float b;

// ERROR: c overflows component 3
layout(location = 3, component = 2) in vec3 c;

// d consumes components 2 and 3 of location 5
layout(location = 5, component = 2) in double d;

// ERROR: e overflows component 3 of location 6
layout(location = 6, component = 2) in dvec2 e;

// ERROR: f overlaps with g
layout(location = 7, component = 0) in vec2 f;
layout(location = 7, component = 1) in float g;

layout(location = 8) in dvec3 h; // components 0,1,2 and 3 of location 8
                                 // and components 0 and 1 of location 9
layout(location = 9, component = 2) in double i; // okay, compts 2 and 3

如果变量是一个数组,则数组的每个元素按顺序分配到连续的位置,但所有元素都在每个位置内的相同指定组件处。例如

// component 3 is consumed in each of 6 locations
layout(location = 2, component = 3) in float d[6];

也就是说,位置 2 组件 3 将保存 d[0],位置 3 组件 3 将保存 d[1],…​ 一直到位置 7 组件 3 保存 d[5]

这允许将两个数组打包到同一组位置中

// e consumes beginning (components 0, 1 and 2) of each of 6 slots
layout(location = 0, component = 0) in vec3 e[6];

// f consumes last component of the same 6 slots
layout(location = 0, component = 3) in float f[6];

如果将此应用于数组的数组,则会删除所有级别的数组,以获取按位置分配给指定组件的元素。这些非数组化的元素将按照“数组”中指定的数组的数组的顺序填充位置。

component 限定符应用于矩阵、结构体、块或包含任何这些的数组是编译时错误。使用 component 1 或 3 作为 doubledvec2 的开头是编译时错误。在程序中为同一变量指定不同的组件是链接时错误。

位置别名是指导致两个变量或块成员具有相同的位置编号。组件别名是指为两个位置别名分配相同(或重叠)的组件编号。(回想一下,如果未使用 component,则组件从 0 开始分配。)除一种情况外,仅当不导致组件别名时才允许位置别名;导致组件别名是编译时或链接时错误。此外,当进行位置别名时,共享该位置的别名必须具有相同的底层数值类型和位宽(浮点或整数,32 位与 64 位等)以及相同的辅助存储和插值限定。允许组件别名的一种例外情况是,当针对 OpenGL 时,允许顶点着色器的两个输入变量(非块成员)具有组件别名。此顶点变量组件别名仅用于支持顶点着色器,其中每个执行路径最多访问每个别名组件的一个输入。如果实现检测到通过顶点着色器可执行文件的每个路径都访问别名到任何单个组件的多个输入,则允许但不要求生成链接时错误。

细分评估输入

下面描述了细分评估着色器允许的其他输入布局限定符标识符。

布局限定符ID :

primitive_mode
vertex_spacing
ordering
point_mode

primitive-mode 用于指定细分基元生成器将使用的细分基元模式。

primitive-mode:

triangles
quads
isolines

如果存在,primitive-mode 指定细分图元生成器应将三角形细分为更小的三角形,将四边形细分为三角形,或将四边形细分为一系列直线。

第二组布局标识符,vertex spacing(顶点间距),用于指定细分图元生成器在细分边缘时使用的间距。

vertex-spacing(顶点间距):

equal_spacing
fractional_even_spacing
fractional_odd_spacing

equal_spacing 指定将边缘划分为一系列大小相等的线段;

fractional_even_spacing 指定将边缘划分为偶数个等长线段,外加两个额外的较短的“小数”线段;或者

fractional_odd_spacing 指定将边缘划分为奇数个等长线段,外加两个额外的较短的“小数”线段。

第三组布局标识符,ordering(排序),指定细分图元生成器是按照顺时针还是逆时针顺序生成三角形,具体取决于OpenGL规范中描述的坐标系。

ordering:

cw
ccw

标识符 cwccw 分别表示顺时针和逆时针三角形。如果细分图元生成器不生成三角形,则忽略顺序。

最后,point mode(点模式)指示细分图元生成器应为细分图元中的每个不同顶点生成一个点,而不是生成直线或三角形。

point-mode(点模式):

point_mode

在单个输入布局声明中,可以指定这些标识符中的任何一个或全部多次。如果在程序的细分评估着色器中多次声明了图元模式、顶点间距或排序,则所有此类声明都必须使用相同的标识符。

程序中的至少一个细分评估着色器(编译单元)必须在其输入布局中声明图元模式。声明顶点间距、排序或点模式标识符是可选的。程序中的所有细分评估着色器不需要都声明图元模式。如果省略了间距或顶点排序声明,细分图元生成器将分别使用相等间距或逆时针顶点排序。如果省略点模式声明,细分图元生成器将根据图元模式生成直线或三角形。

几何着色器输入

几何着色器输入的其他布局限定符标识符包括primitive(图元)标识符和一个invocation count(调用计数)标识符

布局限定符ID :

points
lines
lines_adjacency
triangles
triangles_adjacency
invocations = layout-qualifier-value

标识符 pointslineslines_adjacencytrianglestriangles_adjacency 用于指定几何着色器接受的输入图元类型,并且只能接受其中一个。程序中的至少一个几何着色器(编译单元)必须声明此输入图元布局,并且程序中的所有几何着色器输入布局声明都必须声明相同的布局。程序中的所有几何着色器不需要都声明输入图元布局。

标识符 invocations 用于指定为每个接收到的输入图元调用几何着色器可执行文件的次数。调用计数声明是可选的。如果在程序中的任何几何着色器中都未声明调用计数,则每个输入图元将运行一次几何着色器。如果声明了调用计数,则所有此类声明都必须指定相同的计数。如果着色器指定的调用计数大于实现相关的最大值,或者小于或等于零,则会产生编译时错误。

例如,

layout(triangles, invocations = 6) in;

将建立几何着色器的所有输入都是三角形,并且对于处理的每个三角形,几何着色器可执行文件运行六次。

当存在输入图元布局限定符时,根据下表,所有几何着色器输入无大小数组声明都将由较早的输入图元布局限定符确定大小。

布局 输入数组的大小

points

1

lines

2

lines_adjacency

4

triangles

3

triangles_adjacency

6

本质声明的输入数组 gl_in[] 的大小也将由任何输入图元布局声明确定。因此,表达式

gl_in.length()

将返回上表中的值。

对于声明时没有数组大小的输入(包括本质声明的输入,即 gl_in),必须在任何使用方法 length() 或其他任何需要已知数组大小的数组用法之前声明布局。

如果布局声明的数组大小(来自上表)与同一着色器中输入变量声明中指定的所有显式数组大小不匹配,则会发生编译时错误。以下包括编译时错误的示例

// code sequence within one shader...
in vec4 Color1[];     // legal, size still unknown
in vec4 Color2[2];    // legal, size is 2
in vec4 Color3[3];    // illegal, input sizes are inconsistent
layout(lines) in;     // legal for Color2, input size is 2, matching Color2
in vec4 Color4[3];    // illegal, contradicts layout of lines
layout(lines) in;     // legal, matches other layout() declaration
layout(triangles) in; // illegal, does not match earlier layout() declaration

如果程序中并非所有几何着色器中提供的所有大小(已确定大小的输入数组和布局大小)都匹配,则会发生链接时错误。

片段着色器输入

片段布局限定符的其他标识符包括以下用于 gl_FragCoord 的标识符

布局限定符ID :

origin_upper_left
pixel_center_integer

默认情况下,OpenGL 中的 gl_FragCoord 假定窗口坐标的左下角为原点,并假定像素中心位于半像素坐标处。例如,对于窗口中最左下角的像素,将返回 (x, y) 位置 (0.5, 0.5)。可以通过使用 origin_upper_left 限定符重新声明 gl_FragCoord 来更改原点,从而将 gl_FragCoord 的原点移动到窗口的左上角,其中 y 的值向窗口底部增加。还可以通过 pixel_center_integerxy 中将返回的值移动半个像素,使其看起来像素居中于整数像素偏移量。这将默认情况下由 gl_FragCoord 返回的 (x, y) 值 (0.5, 0.5) 移动到 (0.0, 0.0) (使用 pixel_center_integer)。

面向 Vulkan 时,将假定并要求 gl_FragCoord 的左上角为原点,并且像素中心位于半像素坐标处。可以通过使用 origin_upper_left 标识符重新声明 gl_FragCoord 来显式设置此原点。

重新声明按以下方式进行

in vec4 gl_FragCoord; // redeclaration that changes nothing is allowed

// All the following are allowed redeclaration that change behavior
layout(origin_upper_left) in vec4 gl_FragCoord;
layout(pixel_center_integer) in vec4 gl_FragCoord;
layout(origin_upper_left, pixel_center_integer) in vec4 gl_FragCoord;

如果在程序中的任何片段着色器中重新声明了 gl_FragCoord,则必须在该程序中静态使用了 gl_FragCoord 的所有片段着色器中重新声明它。单个程序中所有片段着色器中对 gl_FragCoord 的所有重新声明都必须具有相同的限定符集。在任何着色器中,对 gl_FragCoord 的第一个重新声明必须出现在任何使用 gl_FragCoord 之前。内置的 gl_FragCoord 仅在片段着色器中预先声明,因此在任何其他着色器语言中重新声明它都会导致编译时错误。

使用 origin_upper_left 和/或 pixel_center_integer 限定符重新声明 glFragCoord 仅影响 gl_FragCoord.xgl_FragCoord.y。它对光栅化、转换或 API 管道或语言功能的任何其他部分都没有影响。

片段着色器允许仅在 in 上(而不是使用变量声明)使用以下布局限定符

布局限定符ID :

early_fragment_tests

请求在执行片段着色器之前执行片段测试,如OpenGL 规范的 15.2.4 节“早期片段测试”中所述。

例如,

layout(early_fragment_tests) in;

指定此项将使每次片段测试在执行片段着色器之前执行。如果未声明此项,则每次片段测试将在执行片段着色器之后执行。只需要一个片段着色器(编译单元)声明此项,尽管可以有多个。如果至少有一个声明了此项,则启用它。

计算着色器输入

计算着色器输入没有布局位置限定符。

计算着色器输入的布局限定符标识符是工作组大小限定符

布局限定符ID :

local_size_x = 布局限定符值
local_size_y = 布局限定符值
local_size_z = 布局限定符值

local_size_xlocal_size_ylocal_size_z 限定符用于分别在第一、第二和第三维度上声明计算着色器的工作组固定大小。如果着色器没有指定其中一个维度的大小,则该维度的大小将为 1。

例如,计算着色器中的以下声明

layout(local_size_x = 32, local_size_y = 32) in;

用于声明一个二维计算着色器,其工作组大小为 32 X 32 个元素,这等效于第三维度大小为 1 的三维计算着色器。

再举一个例子,声明

layout(local_size_x = 8) in;

有效地指定了正在编译一个一维计算着色器,其大小为 8 个元素。

如果着色器在任何维度上的固定工作组大小小于或等于零,或者大于实现支持的该维度最大大小,则会导致编译时错误。此外,如果同一个着色器中声明了多次这样的布局限定符,则所有这些声明都必须设置相同的工作组大小,并且将其设置为相同的值;否则会导致编译时错误。如果附加到单个程序对象的多个计算着色器声明了固定的工作组大小,则这些声明必须相同;否则会导致链接时错误。

此外,如果一个程序对象包含任何计算着色器,则至少必须有一个着色器包含输入布局限定符,该限定符为程序指定固定的工作组大小,否则会发生链接时错误。

输出布局限定符

一些输出布局限定符适用于所有着色器阶段,而一些仅适用于特定阶段。后者将在下面的单独章节中讨论。

与输入布局限定符一样,除了计算着色器之外的所有着色器都允许在输出变量声明、输出块声明和输出块成员声明中使用 location 布局限定符。其中,变量和块成员(但不包括块)还允许使用 component 布局限定符。

布局限定符ID :

location = 布局限定符值
component = 布局限定符值

location 限定符和 component 限定符应用于块和结构的使用方法和规则与“输入布局限定符”中描述的完全相同。不允许对输出变量或成员进行组件别名。

片段着色器允许额外的 index 输出布局限定符

布局限定符ID :

index = 布局限定符值

每个限定符最多可以出现一次。如果指定了 index,则还必须指定 location。如果未指定 index,则使用值 0。例如,在片段着色器中,

layout(location = 3) out vec4 color;

将确定片段着色器输出 color 被分配给片段颜色 3 作为混合方程的第一个(索引为零)输入。并且,

layout(location = 3, index = 1) out vec4 factor;

将确定片段着色器输出 factor 被分配给片段颜色 3 作为混合方程的第二个(索引为一)输入。

对于片段着色器输出,location 和 index 指定接收输出值的颜色输出编号和索引。对于所有其他着色器阶段的输出,location 指定一个向量编号,该编号可用于与后续着色器阶段中的输入进行匹配,即使该着色器位于不同的程序对象中。

如果声明的输出是标量或向量类型(dvec3dvec4 除外),则它将消耗一个 location。类型为 dvec3dvec4 的输出将消耗两个连续的 location。类型为 doubledvec2 的输出在所有阶段都只消耗一个 location。

如果声明的输出是一个数组,则它将被分配从指定的 location 开始的连续 location。例如,

layout(location = 2) out vec4 colors[3];

将确定 colors 被分配给向量 location 编号 2、3 和 4。

如果声明的输出是一个 n × m 矩阵,则它将被分配多个 location,从指定的 location 开始。分配的 location 数量将与 n 元素 m 分量向量数组的分配数量相同。

如果声明的输出是一个结构,则其成员将按声明的顺序分配连续的 location,第一个成员分配为结构指定的 location。结构成员消耗的 location 数量是通过递归应用上述规则来确定的,就像结构成员被声明为相同类型的输出变量一样。

location 布局限定符可以用于声明为结构的输出变量。但是,在结构成员上使用 location 限定符是一个编译时错误。Location 布局限定符可以用于输出块和输出块成员。

着色器可用的输出 location 数量是有限的。对于片段着色器,限制是已公布的绘制缓冲区数量。

对于所有其他着色器,限制是与实现相关的,并且必须不小于已公布的最大输出组件计数(计算着色器没有输出)的四分之一。如果任何附加的着色器使用的 location 大于或等于支持的 location 数量,则程序将无法链接,除非设备相关的优化能够使程序适应可用的硬件资源。

如果在编译时已知链接将失败,则也可能会给出编译时错误。负输出 location 将导致编译时错误。如果片段着色器将布局索引设置为小于 0 或大于 1,则也是编译时错误。

如果发生以下任何情况,则会发生编译时或链接时错误

  • 任何两个片段着色器输出变量被分配到相同的 location 和索引。

  • 如果来自同一顶点、细分或几何着色器阶段的任何两个输出变量被分配到相同的 location。

对于片段着色器输出,可以使用 layout 限定符或通过 OpenGL API 分配 location。

对于所有着色器类型,如果显式 location 分配导致链接器无法在没有显式分配的情况下为其他变量找到空间,则程序将无法链接。

如果在着色器文本中没有分配 location 或索引的输出变量通过 OpenGL API 指定了 location,则将使用 API 分配的 location。否则,此类变量将由链接器分配一个 location。所有此类分配的颜色索引均为零。有关更多详细信息,请参见OpenGL 规范的第 15.2 节“着色器执行”。如果使用冲突的 location 或索引值在同一语言的多个着色器中声明了一个输出变量,则会发生链接时错误。

为了确定非片段输出是否与后续着色器阶段的输入匹配,location 布局限定符(如果有)必须匹配。

变换反馈布局限定符

顶点、细分和几何阶段允许着色器控制变换反馈。执行此操作时,着色器将指示正在使用的变换反馈缓冲区、哪些输出变量将写入哪些缓冲区以及每个缓冲区的布局方式。为了实现这一点,着色器允许在输出声明中使用以下布局限定符标识符

布局限定符ID :

xfb_buffer = 布局限定符值
xfb_offset = 布局限定符值
xfb_stride = 布局限定符值

任何静态使用(预处理后)任何这些 xfb_ 限定符的着色器都将导致该着色器处于变换反馈捕获模式,因此负责描述变换反馈设置。此模式将把由 xfb_offset 直接或间接选择的任何输出捕获到变换反馈缓冲区。

xfb_buffer 限定符指定哪个变换反馈缓冲区将捕获通过 xfb_offset 选择的输出。 xfb_buffer 限定符可以应用于限定符 out、输出变量、输出块和输出块成员。处于变换反馈捕获模式的着色器具有初始全局默认值。

layout(xfb_buffer = 0) out;

可以通过在接口限定符 out 上使用 xfb_buffer 声明不同的缓冲区来更改此默认值。这是更改全局默认值的唯一方法。当声明变量或输出块时,如果没有 xfb_buffer 限定符,则它将继承全局默认缓冲区。当声明变量或输出块时,如果有 xfb_buffer 限定符,则它具有声明的缓冲区。一个块的所有成员都继承该块的缓冲区。允许一个成员声明 xfb_buffer,但它必须与从其块继承的缓冲区匹配,否则会导致编译时错误。

layout(xfb_buffer=2, xfb_offset=0) out block { // block's buffer is 2
    layout(xfb_buffer = 2) vec4 v; // okay, matches the inherited 2
    layout(xfb_buffer = 3) vec4 u; // ERROR, mismatched buffer
    vec4 w; // inherited
};
layout(xfb_offset=16) out vec4 t;  // initial default is buffer 0
layout(xfb_buffer=1) out;          // new global default of 1
out block {                        // block has buffer 1
    vec4 x;                        // x has buffer 1 (not captured)
    layout(xfb_buffer = 1) vec4 y; // okay (not captured)
    layout(xfb_buffer = 0) vec4 z; // ERROR, mismatched buffer
};
layout(xfb_offset=0) out vec4 g;   // g has buffer 1
layout(xfb_buffer=2) out vec4 h;   // does not change global default
layout(xfb_offset=16) out vec4 j;  // j has buffer 1

请注意,这意味着转到变换反馈缓冲区的所有块成员将转到同一缓冲区。

当一个块被声明为数组时,块数组元素 0 的所有成员都将按照前面的描述,被声明或继承的 xfb_buffer 捕获。通常,大小为 *N* 的块数组由 *N* 个连续的缓冲区捕获,块数组元素 *E* 的所有成员都由缓冲区 *B* 捕获,其中 *B* 等于声明或继承的 xfb_buffer 加 *E*。

指定一个 xfb_buffer(包括捕获块数组所需的任何额外缓冲区)小于零或大于或等于实现相关的常量 *gl_MaxTransformFeedbackBuffers* 时,会发生编译时或链接时错误。

xfb_offset 限定符在变换反馈缓冲区中分配一个字节偏移量。只有变量、块成员或块可以使用 xfb_offset 进行限定。如果一个块使用 xfb_offset 进行限定,则其所有成员都会被分配变换反馈缓冲区偏移量。如果一个块未使用 xfb_offset 进行限定,则该块的任何未使用 xfb_offset 限定的成员都不会被分配变换反馈缓冲区偏移量。只有被分配了偏移量的变量和块成员才会被捕获(因此,可以捕获块的适当子集)。每次在着色器中写入此类变量或块成员时,写入的值都会在分配的偏移量处被捕获。如果着色器调用期间没有写入此类块成员或变量,则分配的偏移量处的缓冲区内容将是未定义的。即使没有静态写入分配了变换反馈偏移量的变量或成员,该空间仍然会在缓冲区中分配,并且仍然会影响步长。

使用 xfb_offset 限定的变量和块成员可以是标量、向量、矩阵、结构以及这些类型的(大小调整)数组。偏移量必须是第一个限定变量或块成员的第一个组件大小的倍数,否则会导致编译时错误。此外,如果应用于包含 double 的聚合类型,则偏移量也必须是 8 的倍数,并且缓冲区中占用的空间将是 8 的倍数。给定的偏移量应用于限定实体的第一个成员的第一个组件。然后,在限定实体内,后续组件按照顺序依次分配给下一个可用的、对齐到该组件大小倍数的偏移量。聚合类型被展平到组件级别以获得此组件序列。将 xfb_offset 应用于未调整大小的数组的声明会导致编译时错误。

不允许在输出缓冲区中进行别名操作:指定具有重叠变换反馈偏移量的变量会导致编译时或链接时错误。

xfb_stride 限定符指定每个捕获的顶点消耗多少字节。它适用于该声明的变换反馈缓冲区,无论它是继承的还是显式声明的。它可以应用于变量、块、块成员,或者仅应用于限定符 out。如果缓冲区正在捕获具有双精度组件的任何输出,则步长必须是 8 的倍数,否则必须是 4 的倍数,否则会导致编译时或链接时错误。如果任何 xfb_offset 溢出 xfb_stride,则会导致编译时或链接时错误,无论它是在 xfb_stride 之前还是之后声明的,或者在不同的编译单元中声明的。虽然可以为同一缓冲区多次声明 xfb_stride,但为同一缓冲区指定不同的步长值会导致编译时或链接时错误。

例如

// buffer 1 has 32-byte stride
layout(xfb_buffer = 1, xfb_stride = 32) out;

// same as previous example; order within layout does not matter
layout(xfb_stride = 32, xfb_buffer = 1) out;

// everything in this block goes to buffer 0
layout(xfb_buffer = 0, xfb_stride = 32) out block1 {
    layout(xfb_offset = 0) vec4 a;  // a goes to byte offset 0 of buffer 0
    layout(xfb_offset = 16) vec4 b; // b goes to offset 16 of buffer 0
};

layout(xfb_buffer = 3, xfb_offset = 12) out block2 {
    vec4 v;  // v will be written to byte offsets 12 through 27 of buffer
    float u; // u will be written to offset 28
    layout(xfb_offset = 40) vec4 w;
    vec4 x;  // x will be written to offset 56, the next available offset
};

layout(xfb_buffer = 2, xfb_stride = 32) out block3 {
    layout(xfb_offset = 12) vec3 c;
    layout(xfb_offset = 24) vec3 d; // ERROR, requires stride of 36
    layout(xfb_offset = 0) vec3 g;  // okay, increasing order not required
};

如果未为缓冲区指定 xfb_stride,则缓冲区的步长将是容纳放置在最高偏移量处的变量所需的最小步长,包括任何所需的填充。例如

// if there no other declarations for buffer 3, it has stride 32
layout(xfb_buffer = 3) out block4 {
    layout(xfb_offset = 0) vec4 e;
    layout(xfb_offset = 16) vec4 f;
};

结果步长(隐式或显式)除以 4 必须小于或等于实现相关的常量 *gl_MaxTransformFeedbackInterleavedComponents*。

细分控制输出

除了变换反馈布局限定符之外,细分控制着色器仅允许在接口限定符 out 上使用输出布局限定符,而不允许在输出块、块成员或变量声明上使用。细分控制着色器允许的输出布局限定符标识符是

布局限定符ID :

vertices = 布局限定符值

标识符 vertices 指定由细分控制着色器生成的输出面片中的顶点数量,该数量还指定细分控制着色器被调用的次数。输出顶点计数小于或等于零,或者大于实现相关的最大面片大小会导致编译时或链接时错误。

内部声明的细分控制输出数组 *gl_out[]* 也会根据任何输出布局声明调整大小。因此,表达式

gl_out.length()

将返回先前输出布局限定符中指定的输出面片顶点计数。对于声明时未指定数组大小的输出(包括内部声明的输出,例如 *gl_out*),必须在任何使用方法 length() 或其他需要知道其大小的数组使用之前声明一个布局。

如果输出布局限定符中指定的输出面片顶点计数与同一着色器中任何输出变量声明中指定的数组大小不匹配,则会导致编译时错误。

程序中的所有细分控制着色器布局声明必须指定相同的输出面片顶点计数。在任何包含细分控制着色器的程序中,必须至少有一个布局限定符指定输出面片顶点计数;但是,并非所有细分控制着色器都需要进行这样的声明。

几何输出

几何着色器可以有三种额外的输出布局标识符类型:输出图元类型、最大输出顶点计数和每个输出编号。图元类型和顶点计数标识符仅允许在接口限定符 out 上使用,而不允许在输出块、块成员或变量声明上使用。流标识符允许在接口限定符 out、输出块和变量声明上使用。

几何着色器输出的布局限定符标识符为

布局限定符ID :

points
line_strip
triangle_strip
max_vertices = 布局限定符值
stream = 布局限定符值

原始类型标识符 pointsline_striptriangle_strip 用于指定几何着色器生成的输出图元类型,并且只能接受其中一个。程序中至少一个几何着色器(编译单元)必须声明输出图元类型,并且程序中所有几何着色器输出图元类型声明都必须声明相同的图元类型。程序中并非所有几何着色器都必须声明输出图元类型。

顶点计数标识符 max_vertices 用于指定着色器在单次调用中将发出的最大顶点数。程序中至少一个几何着色器(编译单元)必须声明最大输出顶点数,并且程序中所有几何着色器输出顶点数声明都必须声明相同的计数。程序中并非所有几何着色器都必须声明计数。

在这个例子中,

layout(triangle_strip, max_vertices = 60) out; // order does not matter
layout(max_vertices = 60) out;  // redeclaration okay
layout(triangle_strip) out;     // redeclaration okay
layout(points) out;             // error, contradicts triangle_strip
layout(max_vertices = 30) out;  // error, contradicts 60

几何着色器的所有输出都是三角形,并且着色器最多会发出 60 个顶点。最大顶点数大于 gl_MaxGeometryOutputVertices 是一个错误。

标识符 stream 用于指定几何着色器输出变量或块与特定的顶点流(从零开始编号)相关联。可以通过限定接口限定符 out 在全局范围内声明默认流编号,如本例所示

layout(stream = 1) out;

在此类声明中指定的流编号会替换任何先前的默认值,并应用于所有后续块和变量声明,直到建立新的默认值。初始默认流编号为零。

每个输出块或非块输出变量都与一个顶点流相关联。如果块或变量使用 stream 标识符声明,则它与指定的流相关联;否则,它与当前默认流相关联。可以使用 stream 标识符声明块成员,但指定的流必须与包含块关联的流匹配。一个例子

layout(stream=1) out;           // default is now stream 1
out vec4 var1;                  // var1 gets default stream (1)
layout(stream=2) out Block1 {   // "Block1" belongs to stream 2
    layout(stream=2) vec4 var2; // redundant block member stream decl
    layout(stream=3) vec2 var3; // ILLEGAL (must match block stream)
    vec3 var4;                  // belongs to stream 2
};
layout(stream=0) out;           // default is now stream 0
out vec4 var5;                  // var5 gets default stream (0)
out Block2 {                    // "Block2" gets default stream (0)
    vec4 var6;
};
layout(stream=3) out vec4 var7; // var7 belongs to stream 3

几何着色器发出的每个顶点都分配给一个特定的流,并且所发出顶点的属性取自分配给目标流的输出块和变量的集合。在发出每个顶点后,所有输出变量的值都变为未定义。此外,与每个顶点流关联的输出变量可以共享存储空间。写入与一个流关联的输出变量可能会覆盖与任何其他流关联的输出变量。在发出每个顶点时,几何着色器应写入与该顶点将要发出的流关联的所有输出,而不能写入与任何其他流关联的输出。

如果多次声明几何着色器输出块或变量,则所有此类声明都必须将变量与同一顶点流关联。如果任何流声明指定了不存在的流编号,则着色器将无法编译。

内置几何着色器输出始终与顶点流零关联。

程序中所有的几何着色器输出布局声明都必须声明相同的布局和相同的 max_vertices 值。如果程序中有几何着色器,则该程序中的某处必须至少有一个几何输出布局声明,但并非所有几何着色器(编译单元)都必须声明它。

片段输出

可以使用以下布局限定符之一重新声明内置片段着色器变量 gl_FragDepth

布局限定符ID :

depth_any
depth_greater
depth_less
depth_unchanged

gl_FragDepth 的布局限定符约束任何着色器调用写入的 gl_FragDepth 最终值的意图。如果所有与布局限定符一致的 gl_FragDepth 值都将失败(或通过),则允许 GL 实现执行优化,假设给定片段的深度测试失败(或通过)。这可能包括如果片段因被遮挡而被丢弃且着色器没有副作用,则跳过着色器执行。如果 gl_FragDepth 的最终值与其布局限定符不一致,则相应片段的深度测试结果是未定义的。但是,在这种情况下不会生成错误。如果深度测试通过且启用了深度写入,则写入深度缓冲区的值始终是 gl_FragDepth 的值,无论它是否与布局限定符一致。

默认情况下,gl_FragDepth 被限定为 depth_any。当 gl_FragDepth 的布局限定符为 depth_any 时,着色器编译器将记录对 gl_FragDepth 的任何赋值,以未知的方式修改它,并且深度测试始终在着色器执行后执行。当布局限定符为 depth_greater 时,GL 可以假设 gl_FragDepth 的最终值大于或等于片段的插值深度值,由 gl_FragCoordz 分量给出。当布局限定符为 depth_less 时,GL 可以假设对 gl_FragDepth 的任何修改只会减小其值。当布局限定符为 depth_unchanged 时,着色器编译器将遵循对 gl_FragDepth 的任何修改,但其余的 GL 可以假设 gl_FragDepth 没有被赋予新值。

gl_FragDepth 的重新声明按如下方式执行

// redeclaration that changes nothing is allowed
out float gl_FragDepth;

// assume it may be modified in any way
layout(depth_any) out float gl_FragDepth;

// assume it may be modified such that its value will only increase
layout(depth_greater) out float gl_FragDepth;

// assume it may be modified such that its value will only decrease
layout(depth_less) out float gl_FragDepth;

// assume it will not be modified
layout(depth_unchanged) out float gl_FragDepth;

如果程序中的任何片段着色器中重新声明了 gl_FragDepth,则必须在程序中所有对 gl_FragDepth 有静态赋值的片段着色器中重新声明它。单个程序中所有片段着色器中对 gl_FragDepth 的所有重新声明都必须具有相同的限定符集。在任何着色器中,对 gl_FragDepth 的第一次重新声明必须出现在任何使用 gl_FragDepth 之前。内置 gl_FragDepth 仅在片段着色器中预先声明,因此在任何其他着色器语言中重新声明它会导致编译时错误。

Uniform 变量布局限定符

布局限定符可用于 uniform 变量和子例程 uniform。uniform 变量和子例程 uniform 的布局限定符标识符为

布局限定符ID :

location = 布局限定符值

位置标识符可以与默认块uniform变量和子例程uniform变量一起使用。位置指定了API可以引用uniform变量并更新其值的位置。uniform数组的各个元素被分配连续的位置,第一个元素占据location位置。在程序中链接的共享相同位置的默认块uniform变量声明必须在名称、类型、限定符和数组性方面匹配。对于数组,它们的数组维度和数组大小必须匹配。对于结构体,此规则递归地应用于所有成员。在同一着色器阶段,任何两个子例程uniform变量都不能具有相同的位置,否则将生成编译时或链接时错误。默认块uniform变量位置的有效范围是 0 到实现定义的最大uniform位置数减 1。子例程uniform变量的有效位置范围是 0 到每个阶段实现定义的最大子例程uniform位置数减 1。

位置可以分配给默认块uniform数组和结构体。第一个最内部的标量、向量或矩阵成员或元素占据指定的location,编译器为下一个最内部的成员或元素分配下一个递增的位置值。每个后续最内部的成员或元素都会为整个结构体或数组获得递增的位置。此规则适用于嵌套的结构体和数组,并为每个最内部的标量、向量或矩阵成员提供唯一的位置。对于没有明确大小的数组,其大小基于其静态使用情况计算。当链接器为没有明确位置的uniform生成位置时,它假设所有具有明确位置的uniform的所有数组元素和结构体成员都被使用,并且即使该元素或成员被认为未使用,链接器也不会生成冲突的位置。

当为接受单个(默认块)非不透明uniform变量的API生成SPIR-V时,如果在声明它们时不包含位置,则会产生编译时错误。

当以Vulkan为目标时,push_constant限定符用于声明整个块,并表示一组push常量,如Vulkan API所定义。将其应用于uniform块声明之外的任何内容,或在不以Vulkan为目标时,会产生编译时错误。块中的值将按照Vulkan API规范进行初始化。用layout(push_constant)声明的块可以选择包含一个实例名称。每个阶段只能有一个 push_constant 块,否则将导致编译时或链接时错误。push常量数组只能使用动态统一索引进行索引。使用push_constant声明的uniform块使用与其他块不同的资源;并且是单独计算的。

子例程函数布局限定符

布局限定符可以用于子例程函数。子例程函数的布局限定符标识符是

布局限定符ID :

index = 布局限定符值

着色器中每个具有索引限定符的子例程都必须给定一个唯一的索引,否则将生成编译时或链接时错误。索引必须在 0 到实现定义的最大子例程数减 1 的范围内。建议(但不是必需)着色器分配一组从零开始的紧密打包的索引值,以便OpenGL子例程函数枚举API为所有活动的索引返回一个非空名称。

Uniform和着色器存储块布局限定符

布局限定符可以用于uniform和着色器存储块,但不能用于非块uniform声明。uniform和着色器存储块的布局限定符标识符(和shared关键字)是

布局限定符ID :

shared
packed
std140
std430
row_major
column_major
binding = layout-qualifier-value
offset = layout-qualifier-value
align = layout-qualifier-value

这些对正在声明的变量的使用没有任何语义影响;它们仅描述数据在内存中的布局方式。例如,矩阵语义始终是基于列的,如本规范的其余部分所述,无论正在使用什么布局限定符。

Uniform和着色器存储块布局限定符可以在全局范围内声明,也可以在单个uniform或着色器存储块上声明,或者在单个块成员声明上声明。

在全局范围内,uniform块的默认布局建立为

layout(layout-qualifier-id-list) uniform;

着色器存储块的默认布局建立为

layout(layout-qualifier-id-list) buffer;

完成此操作后,将首先继承先前的默认限定,然后按照下面列出的每个限定符的覆盖规则进行覆盖。结果将成为后续uniform或着色器存储块定义的作用域内的新默认限定。

在生成SPIR-V时,编译的初始状态就好像声明了以下内容

layout(std140, column_major) uniform;
layout(std430, column_major) buffer;

但是,当声明push_constant时,缓冲区的默认布局将是std430。没有全局设置此默认值的方法。

在不生成SPIR-V时,编译的初始状态就好像声明了以下内容

layout(shared, column_major) uniform;
layout(shared, column_major) buffer;

Uniform和着色器存储块可以使用可选的布局限定符声明,它们的单个成员声明也可以。这种块布局限定仅限于块的内容。与全局布局声明一样,块布局限定首先从当前的默认限定继承,然后覆盖它。类似地,单个成员布局限定的作用域仅限于成员声明,并从块的限定继承并覆盖它。

shared限定符仅覆盖std140std430packed限定符;其他限定符被继承。编译器/链接器将确保包含此定义的多个程序和可编程阶段为此块共享相同的内存布局,只要所有数组都使用显式大小声明,并且所有矩阵都具有匹配的row_major和/或column_major限定(可能来自块定义之外的声明)。这允许使用相同的缓冲区来支持不同程序中的相同块定义。当生成SPIR-V时使用shared限定符是编译时错误。

packed限定符仅覆盖std140std430shared;其他限定符被继承。当使用packed时,不保证可共享的布局。编译器和链接器可以根据哪些变量被积极使用以及其他标准来优化内存使用。必须查询偏移量,因为没有其他方法可以保证哪些(和哪些)变量驻留在块内。

在程序中的多个阶段访问同一个打包的 uniform 或着色器存储块是一个链接时错误。尝试跨程序访问同一个打包的 uniform 或着色器存储块可能会导致成员偏移量冲突,并读取未定义的值。但是,实现可以通过使用打包块的规范布局来帮助应用程序管理打包块。在生成 SPIR-V 时使用 packed 限定符是一个编译时错误。

std140std430 限定符仅覆盖 packedsharedstd140std430 限定符;其他限定符会被继承。std430 限定符仅支持着色器存储块;在 uniform 块上使用 std430 限定符的着色器将会导致编译时错误,除非它也用 push_constant 声明。

布局由本节明确确定,如OpenGL 规范的 7.6.2.2 节 “标准 Uniform 块布局”中所述。因此,如上面的 shared 一样,生成的布局可以在程序之间共享。

成员声明的布局限定符不能使用 sharedpackedstd140std430 限定符。这些限定符只能在全局范围(没有对象时)或在块声明中使用,否则会产生编译时错误。

row_majorcolumn_major 限定符仅影响矩阵的布局,包括它们所应用到的结构和数组中包含的所有矩阵,直到嵌套的所有深度。这些限定符可以应用于其他类型,但不会产生任何效果。

row_major 限定符仅覆盖 column_major 限定符;其他限定符会被继承。矩阵行内的元素在内存中是连续的。

column_major 限定符仅覆盖 row_major 限定符;其他限定符会被继承。矩阵列内的元素在内存中是连续的。

binding 限定符指定与 uniform 或着色器存储块对应的 uniform 缓冲区绑定点,该绑定点将用于获取块的成员变量的值。为全局范围或块成员声明指定 binding 限定符是一个编译时错误。任何没有 binding 限定符声明的 uniform 或着色器存储块最初都被分配到绑定点零。在程序链接后,使用或不使用 binding 限定符声明的 uniform 和着色器存储块的绑定点可以通过 API 更新。

当与 OpenGL 一起使用时,如果 binding 限定符与实例化为数组的 uniform 块或着色器存储块一起使用,则数组的第一个元素采用指定的块绑定,而每个后续元素采用下一个连续的绑定点。对于数组的数组,每个元素(例如 a[2][3] 的 6 个元素)都会获得一个绑定点,并且它们按照“数组”中描述的数组的数组排序进行排序。

当以 Vulkan 为目标时,如果 binding 限定符与实例化为数组的 uniform 块或缓冲区块一起使用,则整个数组仅采用提供的绑定号。下一个连续的绑定号可用于不同的对象。对于数组的数组,描述符集访问中使用的描述符集数组元素编号按照“数组”中描述的数组的数组排序进行排序。

如果任何 uniform 或着色器存储块实例的绑定点小于零,或者大于或等于相应的实现相关的最大缓冲区绑定数,则会发生编译时错误。当 binding 限定符与大小为 *N* 的数组实例化的 uniform 或着色器存储块一起使用时,数组的所有元素(从 binding 到 *binding + N - 1*)都必须在此范围内。为多个 uniform 块或多个缓冲区块使用相同的绑定号是一个编译时或链接时错误。

set 限定符仅在以 Vulkan 为目标时可用。它指定此对象所属的描述符集。将 set 应用于独立的限定符、块的成员或未以支持描述符集的 API 为目标是一个编译时错误。将 set 应用于限定为 push_constant 的块是一个编译时错误。默认情况下,任何未声明 set 标识符的非 push-constant uniform 或着色器存储块都会被分配到描述符集 0。类似地,任何未声明 set 标识符的声明为 uniform 的采样器、纹理或子通道输入类型也会被分配到描述符集 0。

如果应用于声明为数组的对象,则数组的所有元素都属于指定的 set

生成 SPIR-V 时,setbinding 值超过前端配置提供的最大值是一个编译时错误。

当在 layout 声明中列出多个参数时,其效果将与它们一次声明一个的效果相同,从左到右依次进行,每个参数依次继承并覆盖先前限定的结果。

例如

layout(row_major, column_major)

导致限定为 column_major。其他示例

layout(shared, row_major) uniform; // default is now shared and row_major

layout(std140) uniform Transform { // layout of this block is std140
    mat4 M1;                       // row major
    layout(column_major) mat4 M2;  // column major
    mat3 N1;                       // row major
};

uniform T2 {                       // layout of this block is shared
    ...
};

layout(column_major) uniform T3 {  // shared and column major
    mat4 M3;                       // column major
    layout(row_major) mat4 m4;     // row major
    mat3 N2;                       // column major
};

当以 Vulkan 为目标时,块和块成员的 offsetalign 限定符只能与 uniformbuffer 块一起使用。当不以 Vulkan 为目标时,它们只能与使用 std140std430 布局声明的块一起使用。

offset 限定符只能在块成员上使用。offset 限定符强制限定的成员从指定的 *layout-qualifier-value* 开始或之后开始,这将是它从缓冲区开始的字节偏移量。拥有任何位于块的其他成员内的偏移量(无论是显式的还是分配的)是一个编译时错误。当不生成 SPIR-V 时,指定小于块中前一个成员偏移量的偏移量是一个编译时错误。在同一程序中以相同块名称链接在一起的两个块必须具有完全相同的一组使用 offset 限定的成员,并且它们的 *layout-qualifier-value* 值必须相同,否则会导致链接时错误。指定的偏移量必须是它限定的块成员类型的基础对齐的倍数,否则会导致编译时错误。

align 限定符使每个块成员的起始位置具有最小字节对齐。它不影响每个成员的内部布局,内部布局仍然遵循 std140std430 规则。指定的对齐必须大于 0 并且是 2 的幂,否则会导致编译时错误。

成员的 *实际对齐* 将是指定的 align 对齐和成员类型的标准(例如 std140)基本对齐的较大值。成员的 *实际偏移量* 的计算方式如下:如果声明了 offset,则从该偏移量开始,否则从紧跟前一个成员(按声明顺序)的偏移量开始。如果生成的偏移量不是 *实际对齐* 的倍数,则将其增加到第一个是 *实际对齐* 倍数的偏移量。这将导致该成员将具有的 *实际偏移量*。

align 应用于数组时,它仅影响数组的开头,而不影响数组的内部步幅。offsetalign 限定符都可以在声明中指定。

当在代码块上使用 align 限定符时,其效果等同于对该代码块中的每个成员都使用相同的 align 值进行限定,并产生与这样做相同的编译时结果和错误。如前所述,单个成员可以指定自己的 align,这将覆盖代码块级别的 align,但仅对该成员生效。

示例

layout(std140) uniform block {
 vec4 a;                         // a takes offsets 0-15
 layout(offset = 32) vec3 b;     // b takes offsets 32-43
 layout(offset = 40) vec2 c;     // ERROR, lies within previous member
 layout(offset = 48) vec2 d;     // d takes offsets 48-55
 layout(align = 16) float e;     // e takes offsets 64-67
 layout(align = 2) double f;     // f takes offsets 72-79
 layout(align = 6) double g;     // ERROR, 6 is not a power of 2
 layout(offset = 80) float h;    // h takes offsets 80-83
 layout(align = 64) dvec3 i;     // i takes offsets 128-151
 layout(offset = 164, align = 8)
 float j;                        // j takes offsets 168-171
};

不透明 Uniform 布局限定符

不透明 uniform 变量可以使用 uniform 布局限定符进行绑定。

布局限定符ID :

binding = layout-qualifier-value

binding 限定符指定变量的绑定点。任何未声明 binding 限定符的不透明变量的默认绑定为零。

当与 OpenGL 一起使用时,如果 binding 限定符与数组一起使用,则数组的第一个元素采用指定的绑定点,后续每个元素采用下一个连续的绑定点。对于数组的数组,每个元素(例如 a[2][3] 的 6 个元素)都获得一个绑定点,并且它们的顺序按照“数组”中描述的数组的数组顺序排列。

当以 Vulkan 为目标时,如果 binding 限定符与数组一起使用,则整个数组仅采用提供的绑定编号。下一个连续的绑定编号可用于不同的对象。

如果 binding 小于零,或大于或等于实现相关的最大支持绑定点数,则会发生编译时错误。当 binding 限定符与大小为 N 的数组一起使用时,数组的所有元素从 bindingbinding + N - 1 都必须在此范围内。对于多个原子计数器使用相同的 binding 编号是编译时或链接时错误,除非共享同一绑定的原子计数器的 offset 都不同。

如果程序中的两个着色器为同一个不透明 uniform 名称指定不同的 *layout-qualifier-value* 绑定,则会导致链接时错误。但是,为同一名称的一些声明指定绑定,而其他声明不指定绑定并非错误,如下例所示。

// in one shader...
layout(binding=3) uniform sampler2D s; // s bound to point 3

// in another shader...
uniform sampler2D s;                   // okay, s still bound at 3

// in another shader...
layout(binding=4) uniform sampler2D s; // ERROR: contradictory bindings

原子计数器布局限定符

以 Vulkan 为目标时,原子计数器不可用。

原子计数器布局限定符可以用于原子计数器声明或全局范围,以建立默认值。原子计数器限定符为:

布局限定符ID :

binding = layout-qualifier-value
offset = layout-qualifier-value

每个绑定都有一个初始值为 0 的默认偏移量,并在每个包含 atomic_uint 类型的声明之后更新。如果这样的声明没有声明变量,则它为命名的绑定建立默认值。如果任何此类声明不包含 binding 布局限定符,则会发生编译时错误。

如果声明包含 offset 限定符,则在该声明中使用该偏移量,否则使用命名绑定的默认偏移量。声明的每个原子计数器都分配给命名缓冲区绑定点,位于当前偏移量处,然后将偏移量增加 4。原子计数器数组为每个成员分配一个这样的偏移量,如果多个变量在同一语句中声明,则它们将按照从左到右的顺序分配偏移量。在为变量分配偏移量(如果有)后,绑定的默认偏移量将设置为当前偏移量值。

例如,

layout(binding = 2, offset = 4) uniform atomic_uint a;

将建立原子计数器 a 的不透明句柄将绑定到原子计数器缓冲区绑定点 2,在该缓冲区中偏移量为 4 个基本机器单元。绑定点 2 的默认 offset 将在后自增 4(一个原子计数器的大小)。

将原子计数器绑定到大于或等于 gl_MaxAtomicCounterBindings 的绑定值是编译时错误。声明一个原子计数器的偏移量使得包含它的缓冲区大于 gl_MaxAtomicCounterBufferSize 是编译时错误。声明一个原子计数器的偏移量未对齐到 4 的倍数是编译时错误。声明一个未确定大小的 atomic_uint 数组是编译时错误。声明两个具有相同绑定和相同偏移量的原子计数器是编译时或链接时错误。

原子声明的示例

layout(binding = 2, offset = 4)  uniform atomic_uint;      // Sets binding's default
                                                           // offset = 4
layout(binding = 2)              uniform atomic_uint a;    // offset 4
layout(binding = 2)              uniform atomic_uint b;    // offset 8
layout(binding = 3)              uniform atomic_uint c[2]; // offsets 0, 4
layout(binding = 2)              uniform atomic_uint d;    // offset 12
layout(binding = 4, offset = 16) uniform atomic_uint e;    // offset 16
layout(binding = 4)              uniform atomic_uint f;    // offset 20

layout(offset = 8)              uniform atomic_uint ea;  // error, no binding
                                                         // specified
layout(binding = 2, offset = 6) uniform atomic_uint eb;  // error, offset not aligned
layout(binding = 3, offset = 4) uniform atomic_uint ec;  // error, overlaps c[1]
layout(binding = 3, offset = 4) uniform atomic_uint;     // OK, no counter declared
layout(binding = 3)             uniform atomic_uint ed;  // error, overlaps c[1]

格式布局限定符

格式布局限定符可以用于图像变量声明(那些使用关键字中包含“image”的基本类型声明的变量)。图像变量声明的格式布局限定符标识符为:

布局限定符ID :

float-image-format-qualifier
int-image-format-qualifier
uint-image-format-qualifier
binding = layout-qualifier-value

float-image-format-qualifier :

rgba32f
rgba16f
rg32f
rg16f
r11f_g11f_b10f
r32f
r16f
rgba16
rgb10_a2
rgba8
rg16
rg8
r16
r8
rgba16_snorm
rgba8_snorm
rg16_snorm
rg8_snorm
r16_snorm
r8_snorm

int-image-format-qualifier :

rgba32i
rgba16i
rgba8i
rg32i
rg16i
rg8i
r32i
r16i
r8i

uint-image-format-qualifier :

rgba32ui
rgba16ui
rgb10_a2ui
rgba8ui
rg32ui
rg16ui
rg8ui
r32ui
r16ui
r8ui

格式布局限定符指定与声明的图像变量关联的图像格式。对于任何图像变量声明,只能指定一个格式限定符。对于具有浮点组件类型(关键字以“image”开头)、有符号整数组件类型(关键字以“iimage”开头)或无符号整数组件类型(关键字以“uimage”开头)的图像变量,使用的格式限定符必须分别匹配 *float-image-format-qualifier*、*int-image-format-qualifier* 或 *uint-image-format-qualifier* 语法规则。声明一个格式限定符与图像变量类型不匹配的图像变量是编译时错误。

用于图像加载或原子操作的任何图像变量都必须指定格式布局限定符;将没有格式布局限定符声明的图像 uniform 变量或函数参数传递给图像加载或原子函数是编译时错误。

没有使用 writeonly 限定的 Uniform 必须具有格式布局限定符。请注意,传递给函数以进行读取访问的图像变量不能声明为 writeonly,因此必须使用格式布局限定符声明。

binding 限定符在“不透明 Uniform 布局限定符”中进行了描述。

子通道输入限定符

仅当以 Vulkan 为目标时,子通道输入才可用。

子通道输入使用基本 subpassInput 类型声明。它们必须使用布局限定符 input_attachment_index 声明,否则会产生编译时错误。例如:

layout(input_attachment_index = 2) uniform subpassInput t;

这选择了要从中读取的子通道输入。分配给 input_attachment_index 的值,例如 i (input_attachment_index = i),选择通道输入列表中的该项(第 i 项)。有关通道和输入列表的更多详细信息,请参见 API 文档。

如果声明大小为 N 的数组,它将使用 N 个连续的 input_attachment_index 值,从提供的那个值开始。

声明具有相同 input_attachment_index 的不同变量是编译时或链接时错误。这包括数组声明使用的隐式 input_attachment_index 中的任何重叠。

如果分配给 input_attachment_index 的值大于或等于 gl_MaxInputAttachments,则会发生编译时错误。

插值限定符

可以进行插值的输入和输出可以使用以下最多一个插值限定符来进一步限定

限定符 含义

smooth

透视校正插值

flat

无插值

noperspective

线性插值

插值的存在和类型由上述插值限定符以及辅助存储限定符 centroidsample 控制。当不存在插值限定符时,使用平滑插值。使用多个插值限定符是编译时错误。辅助存储限定符 patch 不用于插值;将插值限定符与 patch 一起使用是编译时错误。

使用 flat 限定的变量将不会进行插值。相反,它在图元内的每个片段都将具有相同的值。此值将来自单个触发顶点,如 API 中所述。使用 flat 限定的变量也可以使用 centroidsample 限定,这将与仅使用 flat 限定的含义相同。

使用 smooth 限定的变量将在渲染的图元上以透视校正的方式进行插值。透视校正的方式的插值在 OpenGL 规范 的第 14.5 节“线段”中的等式 14.7 中指定。

使用 noperspective 限定的变量必须在屏幕空间中线性插值,如 OpenGL 规范 的第 3.5 节“线段”中的等式 3.7 中所述。

当禁用多重采样光栅化时,或者对于未使用 centroidsample 限定的片段着色器输入变量,分配的变量的值可以在像素内的任何位置进行插值,并且可以为像素内的每个样本分配单个值,只要 OpenGL 规范 允许。

启用多重采样光栅化时,可以使用 centroidsample 来控制限定片段着色器输入的采样位置和频率。如果片段着色器输入使用 centroid 限定,则可以为像素中的所有样本分配一个变量的单个值,但该值必须在像素内和正在渲染的图元内的某个位置进行插值,包括图元覆盖的任何像素样本。由于变量插值的位置在相邻像素中可能不同,并且导数可以通过计算相邻像素之间的差异来计算,因此中心采样输入的导数可能不如非中心插值变量的导数精确。如果片段着色器输入使用 sample 限定,则必须为像素中每个覆盖的样本分配该变量的单独值,并且该值必须在各个样本的位置进行采样。

如果在同一阶段内,相同名称的变量的插值限定符不匹配,则会出现链接时错误。

在兼容性配置文件中重新声明内置插值变量

当使用兼容性配置文件时,以下预声明的变量可以使用插值限定符重新声明

顶点、细分控制、细分评估和几何语言

gl_FrontColor
gl_BackColor
gl_FrontSecondaryColor
gl_BackSecondaryColor

片段语言

gl_Color
gl_SecondaryColor

例如,

in vec4 gl_Color;            // predeclared by the fragment language
flat in vec4 gl_Color;       // redeclared by user to be flat
flat in vec4 gl_FrontColor;  // input to geometry shader, no "gl_in[]"
flat out vec4 gl_FrontColor; // output from geometry shader

理想情况下,这些变量应该作为接口块重新声明的一部分进行重新声明,如 “兼容性配置文件内置语言变量” 中所述。但是,为了上述目的,它们可以在全局范围内作为单独的变量重新声明,而不是在接口块之外。此类重新声明还允许将变换反馈限定符 xfb_bufferxfb_stridexfb_offset 添加到输出变量。(在变量上使用 xfb_buffer 不会更改全局默认缓冲区。)如果着色器同时具有接口块重新声明和接口块重新声明之外的该接口块成员的单独重新声明,则会导致编译时错误。

如果 gl_Color 使用插值限定符重新声明,则 gl_FrontColorgl_BackColor(如果它们被写入)也必须使用相同的插值限定符重新声明,反之亦然。如果 gl_SecondaryColor 使用插值限定符重新声明,则 gl_FrontSecondaryColor 和 _gl_BackSecondaryColor _(如果它们被写入)也必须使用相同的插值限定符重新声明,反之亦然。仅当程序中的着色器中静态使用了预声明变量时,才需要对这些预声明变量进行限定符匹配。

参数限定符

除了精度限定符和内存限定符外,参数还可以具有以下参数限定符。

限定符 含义

<无:默认>

in 相同

const

对于无法写入的函数参数

in

对于传递到函数中的函数参数

out

对于从函数中传递回来的函数参数,但传递时不初始化以供使用

inout

对于传递到函数中和从函数中传递出的函数参数

参数限定符在 “函数调用约定” 中进行了更详细的讨论。

精度和精度限定符

当不以 Vulkan 为目标时:添加精度限定符是为了与 OpenGL ES 的代码可移植性,而不是为了功能。它们与 OpenGL ES 中的语法相同,如下所述,但它们没有语义意义,包括对用于存储或操作变量的精度没有影响。如果扩展在 OpenGL ES 2.0 规范中为精度限定符添加了相同的语义和功能,则允许该扩展为该目的重用以下关键字。

当以 Vulkan 为目标时:对于接口匹配,一致变量以及一致和缓冲区块成员必须具有相同的精度限定。链接到同一着色器阶段的不同编译单元中声明的全局变量必须使用相同的精度限定来声明。

为了确定一个着色器阶段的输出是否与下一个阶段的输入匹配,精度限定符不需要匹配。

本规范仅规定了存储和操作值的最低所需精度。实现可以自由地以更高的精度计算和/或存储任何结果。如有必要,可以使用不变性限定符来控制此差异。

范围和精度

highp 单精度和双精度浮点变量的精度由 IEEE 754 标准针对 32 位和 64 位浮点数定义。

这包括对 NaN(非数字)、Inf(正或负无穷大)以及正负零的支持。

以下规则适用于单精度和双精度操作的 highp:有符号无穷大和零的生成遵循 IEEE 标准,但受限于下表允许的精度。任何输入到着色器或由着色器中任何操作可能生成的次正规(非规格化)值都可能被刷新为 0。舍入模式无法设置且未定义,但必须对结果的影响不超过 1 ULP。不要求生成 NaN。不支持信号 NaN,并且永远不会引发异常。对 NaN 进行操作的内置函数(包括内置函数)不要求返回 NaN 作为结果。但是,如果生成了 NaN,则 isnan() 必须返回正确的值。

精度以 ULP(末位单位)为单位的最大相对误差表示,除非另有说明。

对于单精度操作,精度要求如下

操作 精度

a + b, a - b, a * b

正确舍入。

<, <=, ==, >, >=

正确结果。

a / b, 1.0 / b

对于 |b|[2-126, 2126] 范围内的,为 2.5 ULP。

a * b + c

正确舍入的单个操作或两个正确舍入的操作序列。

fma()

继承自 a * b + c

pow(x, y)

继承自 exp2(y * log2(x))。

exp(x), exp2(x)

(3 + 2 ⋅ |x|) ULP。

log(), log2()

[0.5,2.0] 范围之外为 3 ULP。
[0.5,2.0] 范围内的绝对误差 < 2-21

sqrt()

继承自 1.0 / inversesqrt()。

inversesqrt()

2 ULP。

类型之间的隐式和显式转换

正确舍入。

规范中定义的内置函数,其方程式由上述操作构建,则继承上述误差。这些包括例如几何函数、通用函数和许多矩阵函数。上面未列出且未定义为上述方程式的内置函数具有未定义的精度。这些包括例如三角函数和行列式。

双精度操作的精度至少与单精度相同。

精度之间的转换

在同一类型内,从较低精度到较高精度的转换必须是精确的。当从较高精度转换为较低精度时,如果该值可以由目标精度的实现表示,则转换也必须是精确的。如果该值不可表示,则行为取决于类型

  • 对于有符号和无符号整数,值会被截断;目标精度中不存在的位置中的位设置为零。(位置从零开始,并且为此目的,最低有效位被认为是位置零。)

  • 对于浮点值,该值应钳制为 +Inf 或 -Inf,或者钳制为实现支持的最大值或最小值。虽然此行为依赖于实现,但在给定的实现中应该是一致的。

精度限定符

任何单精度浮点数、整数或不透明类型声明都可以在类型前面加上以下精度限定符之一

限定符 含义

highp

对于整数为 32 位二进制补码,对于 float 为 32 位 IEEE 754 浮点数

mediump

当以 Vulkan 为目标时为 SPIR-V RelaxedPrecision,否则为 none。

lowp

当以 Vulkan 为目标时为 SPIR-V RelaxedPrecision,否则为 none。

例如

lowp float color;
out mediump vec2 P;
lowp ivec2 foo(lowp mat3);
highp mat4 m;

字面常量没有精度限定符。布尔变量也没有。

对于此段,“操作”包括运算符、内置函数和构造函数,“操作数”包括函数参数和构造函数参数。与任何操作关联的精度限定是该操作消耗的操作数(如果有)的最高精度限定。在没有操作数具有精度限定的情况下,将使用表达式中下一个消耗操作的操作数的精度限定。此规则可以递归应用,直到找到具有精度限定的操作数。如有必要,它还将包括赋值的左值的精度限定、初始值设定项的声明变量的精度限定、函数调用参数的形式参数的精度限定,或函数返回值的函数返回类型的精度限定。如果无法通过此方法确定精度,例如,如果整个表达式仅由没有精度限定符的操作数组成,并且结果未被分配或作为参数传递,则它将以该类型的默认精度或更高的精度进行评估。当这种情况发生在片段着色器中时,必须定义默认精度。

例如,考虑以下语句

uniform highp float h1;
highp float h2 = 2.3 * 4.7; // operation and result are highp
precision
mediump float m;
m = 3.7 * h1 * h2; // all operations are highp precision
h2 = m * h1; // operation is highp precision
m = h2 - h1; // operation is highp precision
h2 = m + m; // addition and result at mediump precision
void f(highp float p);
f(3.3); // 3.3 will be passed in at highp precision

精度限定符与其它限定符一样,不影响变量的基本类型。特别是,没有用于精度转换的构造函数;构造函数仅转换类型。同样,精度限定符与其它限定符一样,不影响基于参数类型进行函数重载。正如在“函数调用约定”中所讨论的那样,函数输入和输出是通过复制完成的,因此限定符不必匹配。

变量的精度在变量声明时确定,并且不能随后更改。

如果未指定常量整数或常量浮点表达式的精度,则评估在 highp 下执行。此规则不影响表达式的精度限定。

常量表达式的评估必须是不变的,并且通常会在编译时执行。

默认精度限定符

精度语句

precision precision-qualifier type;

可用于建立默认的精度限定符。type 字段可以是 intfloat 或任何不透明类型,并且 precision-qualifier 可以是 lowpmediumphighp

任何其他类型或限定符都将导致编译时错误。如果 typefloat,则该指令应用于未进行精度限定的单精度浮点类型(标量、向量和矩阵)声明。如果 typeint,则该指令应用于所有未进行精度限定的整数类型(标量、向量、有符号和无符号)声明。这包括全局变量声明、函数返回声明、函数参数声明和局部变量声明。

未进行精度限定的声明将使用最新的仍在作用域内的 precision 语句中指定的精度限定符。precision 语句具有与变量声明相同的范围规则。如果在复合语句中声明,则其效果在该声明的最小内部语句的末尾停止。嵌套范围中的精度语句会覆盖外部范围中的精度语句。同一基本类型的多个精度语句可以出现在同一范围内,后面的语句会覆盖该范围内的较早语句。

对于任何接受精度限定符的类型,默认精度限定符将是 highp。因为所有需要精度限定符的类型都有默认精度,所以省略精度限定符不会出现错误。

可用的精度限定符

内置宏 GL_FRAGMENT_PRECISION_HIGH 定义为 1

#define GL_FRAGMENT_PRECISION_HIGH 1

此宏在除计算以外的所有语言中都可用。

可变性与不变性限定符

在本节中,可变性指的是在不同程序中,相同表达式可能得到不同值的可能性。例如,考虑以下情况:两个顶点着色器,分别位于不同的程序中,都使用相同的表达式设置 gl_Position,并且当两个着色器运行时,该表达式的输入值是相同的。由于两个着色器的独立编译,当两个着色器运行时,分配给 gl_Position 的值可能不完全相同。在这个例子中,这可能会导致多通道算法中几何体对齐的问题。

一般来说,允许着色器之间存在这种可变性。当特定输出变量不存在这种可变性时,该变量被称为是不变的

不变性限定符

为了确保特定的输出变量是不变的,需要使用 invariant 限定符。它可以用于限定先前声明的变量为不变的

invariant gl_Position; // make existing gl_Position be invariant
out vec3 Color;
invariant Color;       // make existing Color be invariant

或者在声明变量时作为声明的一部分

invariant centroid out vec3 Color;

只有着色器输出的变量才能作为不变性的候选者。这包括用户定义的输出变量和内置输出变量。由于只有输出可以声明为不变的,所以一个着色器阶段的输出仍然会匹配后续阶段的输入,而无需将输入声明为不变的。

重新声明内置变量时,不会使用块上的输入或输出实例名称。

invariant 关键字后面可以跟一个用逗号分隔的先前声明的标识符列表。所有 invariant 的使用都必须在全局范围内或在块成员上,并且在任何被声明为不变的变量使用之前。

为了保证两个程序中特定输出变量的不变性,以下条件也必须成立:

  • 输出变量在两个程序中都声明为不变的。

  • 必须将相同的值输入到表达式消耗的所有着色器输入变量,以及影响输出变量值的控制流中。

  • 对于任何影响输出变量值的纹理函数调用,纹理格式、纹素值和纹理过滤的设置方式必须相同。

  • 所有输入值都以相同的方式操作。消耗表达式中的所有操作以及任何中间表达式都必须相同,具有相同的操作数顺序和相同的结合性,以给出相同的求值顺序。中间变量和函数必须声明为相同类型,并具有相同的显式或隐式精度限定符。任何影响输出值的控制流必须相同,并且任何用于确定此控制流的表达式也必须遵循这些不变性规则。

  • 导致设置不变输出变量的所有数据流和控制流都位于单个编译单元中。

本质上,导致不变输出的所有数据流和控制流必须匹配。

最初,默认情况下,允许所有输出变量是可变的。要强制所有输出变量是不变的,请使用 pragma

#pragma STDGL invariant(all)

在着色器中的所有声明之前。如果此 pragma 在任何变量或函数的声明之后使用,那么行为不变的输出集合是未定义的。

一般来说,不变性是以牺牲优化的灵活性为代价来保证的,因此使用不变性可能会降低性能。因此,使用此 pragma 的目的是作为调试辅助工具,以避免单独声明所有输出变量为不变的。

常量表达式的不变性

必须保证常量表达式的不变性。如果一个特定的常量表达式再次出现在同一个着色器或不同的着色器中,则它必须求值得到相同的结果。这包括出现在相同语言的两个着色器或两种不同语言的着色器中的相同表达式。

当按照上述对不变变量的描述进行操作时,常量表达式必须求值得到相同的结果。

精确限定符

某些算法要求浮点计算严格按照源代码中指定的操作顺序进行,并始终如一地处理所有操作,即使实现支持可以使用更高性能产生几乎等效结果的优化。例如,许多 GL 实现支持“乘加”指令,该指令可以计算如下浮点表达式:

result = (a * b) + (c * d);

两次操作而不是三次操作;一个乘法和一个乘加,而不是两次乘法和一次加法。浮点乘加的结果可能并不总是与先进行乘法产生浮点结果然后再进行浮点加法相同。因此,在这个例子中,两个乘法运算不会得到一致的处理;这两个乘法运算实际上可能会出现不同的精度。

当进行细分时,需要保持一致的关键计算会出现,细分的中间点在不同的方向合成,但需要产生相同的结果,如下面的图所示。

precise

在没有任何限定符的情况下,允许实现执行优化,这些优化有效地修改了用于计算表达式的操作的顺序或数量,即使这些优化可能会产生相对于未优化代码略有不同的结果。

precise 限定符确保用于计算变量值的操作按照其声明的顺序执行,并且操作符保持一致性。该顺序由运算符优先级和括号确定,如“运算符”中所述。运算符一致性意味着对于每个运算符,其结果始终以相同的精度计算。具体而言,编译器生成的代码计算的值必须遵守以下恒等式

  1. a + b = b + a

  2. a * b = b * a

  3. a * b + c * d = b * a + c* d = d * c + b * a = <任何其他数学上有效的组合>

同时,以下情况是被禁止的

  1. a + (b + c) 不允许变成 (a + b) + c

  2. a * (b * c) 不允许变成 (a * b) * c

  3. a * b + c 不允许变成单个操作 fma(a, b, c)

其中 abcd 是标量或向量,而不是矩阵。(矩阵乘法通常不满足交换律。)着色器编写者的责任是根据这些规则表达计算,编译器的责任是遵循这些规则。请参阅 gl_TessCoord 的描述,了解细分阶段负责遵循的规则,这些规则与上述规则相结合,可以避免细分时的裂缝。

例如,

precise out vec4 position;

声明用于生成 position 值的操作必须严格按照源代码中指定的顺序执行,并且所有运算符都必须一致地处理。与 invariant 限定符(参见“不变性限定符”)一样,precise 限定符可以用于限定内置的或先前声明的用户定义变量为精确的

out vec3 Color;
precise Color; // make existing Color be precise

当应用于块或结构类型的变量时,precise 递归地应用于每个包含的成员。

只有当一个右值的结果最终在同一个函数中被一个限定为 precise 的左值使用时,此限定符才会影响该函数中右值的计算。函数中的任何其他表达式都不会受到影响,包括未声明为 precise 的返回值和输出参数,但这些返回值和输出参数最终会被函数外部的限定为 precise 的变量使用。不受影响的表达式还包括选择和迭代语句中的控制表达式以及三元运算符 (?:) 中的条件。

以下是一些 precise 用法的示例

in vec4 a, b, c, d;
precise out vec4 v;

float func(float e, float f, float g, float h)
{
    return (e*f) + (g*h); // no constraint on order or operator consistency
}

float func2(float e, float f, float g, float h)
{
    precise float result = (e*f) + (g*h); // ensures same precision for the two multiplies
    return result;
}

float func3(float i, float j, precise out float k)
{
    k = i * i + j;        // precise, due to <k> declaration
}

void main()
{
    vec3 r = vec3(a * b);             // precise, used to compute v.xyz
    vec3 s = vec3(c * d);             // precise, used to compute v.xyz
    v.xyz = r + s;                    // precise
    v.w = (a.w * b.w) + (c.w * d.w);  // precise
    v.x = func(a.x, b.x, c.x, d.x);   // values computed in func() are NOT precise
    v.x = func2(a.x, b.x, c.x, d.x);  // precise!
    func3(a.x * b.x, c.x * d.x, v.x); // precise!
}

为了确定一个着色器阶段的输出是否与下一阶段的输入匹配,输入和输出之间的 precise 限定符不需要匹配。

所有常量表达式的计算都如同存在 precise 一样,无论它是否实际存在。但是,如“常量表达式”中所述,没有要求编译时常量表达式的计算结果与对应的非常量表达式的计算结果相同。

内存限定符

着色器存储块、在着色器存储块中声明的变量以及声明为图像类型的变量(关键字中带有 “image” 的基本不透明类型),可以用以下一个或多个内存限定符进一步限定

限定符 含义

coherent

内存变量,其读取和写入与其他着色器调用的读取和写入是一致的

volatile

内存变量,其基础值可能在着色器执行期间的任何时间被当前着色器调用之外的某些源更改

restrict

内存变量,在该变量的使用是相关着色器阶段中读取和写入底层内存的唯一方式

readonly

内存变量,可用于读取底层内存,但不能用于写入底层内存

writeonly

内存变量,可用于写入底层内存,但不能用于读取底层内存

使用 coherent 限定符声明的图像变量的内存访问,与其他着色器调用对相同位置的访问一致。特别是,当读取声明为 coherent 的变量时,返回的值将反映其他着色器调用执行的先前完成的写入结果。当写入声明为 coherent 的变量时,写入的值将反映在其他着色器调用执行的后续一致读取中。

OpenGL 规范的 7.12 节“着色器内存访问”中所述,着色器内存的读取和写入在很大程度上以未定义的顺序完成。如果需要保证单个着色器调用执行的内存访问的完成和相对顺序,可以使用内置函数 memoryBarrier()。

当使用未声明为 coherent 的变量访问内存时,实现可能会缓存着色器访问的内存,以便为将来访问相同地址提供服务。内存存储可能会以这样一种方式缓存,即写入的值可能对访问同一内存的其他着色器调用不可见。实现可能会缓存内存读取获取的值,并将相同的值返回给任何访问同一内存的着色器调用,即使自第一次内存读取以来底层内存已被修改。虽然未声明为 coherent 的变量可能不利于在着色器调用之间进行通信,但使用非一致访问可能会提高性能。

使用 volatile 限定符声明的图像变量的内存访问必须将底层内存视为好像它可以在着色器执行期间的任何时间被执行着色器调用之外的某些源读取或写入。当读取 volatile 变量时,其值必须从底层内存重新获取,即使执行读取的着色器调用先前已从同一内存中获取其值。当写入 volatile 变量时,其值必须写入底层内存,即使编译器可以明确地确定其值将被后续写入覆盖。由于读取或写入 volatile 变量的外部源可能是另一个着色器调用,因此声明为 volatile 的变量会自动被视为一致的。

使用 restrict 限定符声明的图像变量的内存访问可以在假设用于执行内存访问的变量是使用相关着色器阶段访问底层内存的唯一方式的情况下进行编译。这允许编译器以不允许对未限定的图像变量的方式合并或重新排序使用 restrict 限定的图像变量的加载和存储,因为编译器可以假设底层图像不会被其他代码读取或写入。应用程序有责任确保使用 restrict 限定的变量引用的图像内存不会在同一作用域中使用其他变量引用;否则,对 restrict 限定的变量的访问将具有未定义的结果。

使用 readonly 限定符声明的图像变量的内存访问只能读取底层内存,底层内存被视为只读内存且不能写入。将使用 readonly 限定的图像变量传递给 imageStore() 或其他修改图像内存的内置函数是编译时错误。

使用 writeonly 限定符声明的图像变量的内存访问只能写入底层内存;无法读取底层内存。将使用 writeonly 限定的图像变量传递给 imageLoad() 或其他读取图像内存的内置函数是编译时错误。

一个变量可以同时限定为 readonlywriteonly,禁止读取和写入。这样的变量仍然可以用于某些查询,例如 imageSize() 和 .length()。

内存限定符 coherentvolatilerestrictreadonlywriteonly 可用于声明缓冲区变量(即着色器存储块的成员)。当使用内存限定符声明缓冲区变量时,上述涉及图像变量的内存访问所指定的行为同样适用于涉及该缓冲区变量的内存访问。向使用 readonly 限定的缓冲区变量赋值或从使用 writeonly 限定的缓冲区变量读取是编译时错误。允许组合 readonly writeonly

此外,内存限定符可以用于着色器存储块的块级声明,包括组合 readonly writeonly。当使用内存限定符限定块声明时,就好像它的所有成员都使用相同的内存限定符声明一样。例如,块声明

coherent buffer Block {
    readonly vec4 member1;
    vec4 member2;
};

等效于

buffer Block {
    coherent readonly vec4 member1;
    coherent vec4 member2;
};

内存限定符仅支持在图像变量、缓冲区变量和着色器存储块的声明中使用;在任何其他声明中使用这些限定符都是错误的。

调用用户自定义函数时,带有 coherentvolatilereadonlywriteonly 限定符的不透明类型变量,不能传递给形参缺少这些限定符的函数。(有关函数调用的更多详细信息,请参阅“函数定义”。) 形参可以有任何额外的内存限定符,但只有 restrict 可以被形参移除,前提是形参缺少 restrict 限定符。对于非不透明的参数类型,被调用函数操作的值是复制输入/复制输出的,因此不需要匹配内存限定符。

当调用内置函数时,生成的代码应基于调用参数的实际限定,而不是基于原型中形参上指定的内存限定符列表。

vec4 funcA(restrict image2D a) { ... }
vec4 funcB(image2D a) { ... }
layout(rgba32f) uniform image2D img1;
layout(rgba32f) coherent uniform image2D img2;

funcA(img1);        // OK, adding "restrict" is allowed
funcB(img2);        // illegal, stripping "coherent" is not

布局限定符不能用于形参,并且布局限定不包含在参数匹配中。

请注意,在图像变量声明中使用 const 是限定所声明变量的常量性,而不是它引用的图像。限定符 readonly 限定图像内存(通过该变量访问时),而 const 限定变量本身。

专用化常量限定符

专用化常量仅用于 SPIR-V,并使用 constant_id 布局限定符声明。例如

layout(constant_id = 17) const int arraySize = 12;

上面的代码创建了一个默认值为 12 的专用化常量。数字 17 是作者选择的 ID 示例,API 或其他工具可以在以后通过此 ID 引用此特定的专用化常量。如果在最终降低之前从未更改,它将保留值 12。在 SPIR-V 生成标量 boolintuintfloatdouble 以外的任何内容上使用 constant_id 限定符是编译时错误。

内置常量可以声明为专用化常量。例如

layout(constant_id = 31) gl_MaxClipDistances; // add specialization_id

该声明仅使用先前声明的内置变量的名称,并带有 constant_id 布局限定符声明。在常量被使用后执行此操作是编译时错误:常量严格来说要么是非专用化常量,要么是专用化常量,不能两者兼有。

可以使用 local_size_{xyz}_id 限定符专门化内置常量向量 *gl_WorkGroupSize*,以单独为组件提供 ID。例如

layout(local_size_x_id = 18, local_size_z_id = 19) in;

这将使 *gl_WorkGroupSize.y* 成为非专用化常量,而 *gl_WorkGroupSize* 成为部分专用化的向量。它的 *x* 和 *z* 组件可以在生成 SPIR-V 之后,使用 ID 18 和 19 进行后续专门化。这些 ID 的声明独立于工作组大小的声明

layout(local_size_x = 32, local_size_y = 32) in;   // size is (32,32,1)
layout(local_size_x_id = 18) in;                   // constant_id for x
layout(local_size_z_id = 19) in;                   // constant_id for z

声明 local_size_xlocal_size_ylocal_size_z 的现有规则不会更改。对于本地大小 ID,为同一本地大小 ID 提供不同的 ID 值,或者在任何使用之后提供它们是编译时错误。否则,顺序、放置、语句数量和复制不会导致错误。

只有当两个数组使用相同的符号大小,且不涉及任何操作时,两个使用专用化常量调整大小的数组才是相同的类型。例如

layout(constant_id = 51) const int aSize = 20;
const int pad = 2;
const int total = aSize + pad; // specialization constant
int a[total], b[total];        // a and b have the same type
int c[22];                     // different type than a or b
int d[aSize + pad];            // different type than a, b, or c
int e[aSize + 2];              // different type than a, b, c, or d

包含使用专用化常量调整大小的数组的类型不能进行比较、作为聚合分配、使用初始化程序声明或用作初始化程序。但是,它们可以作为参数传递给具有相同类型的形参的函数。只有声明为数组的数组的最外层维度可以是专用化常量,否则会导致编译时错误。

块内的数组可以使用专用化常量调整大小,但该块将具有静态布局。更改专用化的大小不会重新布局该块。在没有显式偏移量的情况下,布局将基于数组的默认大小。

限定的顺序和重复

当声明中存在多个限定符时,它们可以以任何顺序出现,但它们都必须出现在类型之前。 layout 限定符是唯一可以多次出现的限定符。此外,一个声明最多只能有一个存储限定符,最多一个辅助存储限定符,最多一个插值限定符。如果使用了 inout,则不能使用 inout。可以使用多个内存限定符。违反这些规则中的任何一条都会导致编译时错误。

空声明

空声明 是指没有变量名称的声明,这意味着该声明不会实例化任何对象。通常,允许使用空声明。有些在声明结构时很有用,而许多其他则不起作用。例如

int;               // No effect
struct S {int x;}; // Defines a struct S

导致编译时或链接时错误的限定符组合,与声明是否为空无关,例如

invariant in float x; // Error. An input cannot be invariant.
invariant in float;   // Error even though no variable is declared.