运算符和表达式

运算符

OpenGL 着色语言具有以下运算符。

优先级 运算符类别 运算符 结合性

1(最高)

圆括号分组

( )

不适用

2

数组下标
函数调用和构造器结构
字段或方法选择器,swizzle
后缀递增和递减

[ ]
( )
.
++ --

从左到右

3

前缀递增和递减
一元

++ --
+ - ~ !

从右到左

4

乘法

* / %

从左到右

5

加法

+ -

从左到右

6

按位移位

xref: []

从左到右

7

关系

< > <= >=

从左到右

8

相等

== !=

从左到右

9

按位与

&

从左到右

10

按位异或

^

从左到右

11

按位或

|

从左到右

12

逻辑与

&&

从左到右

13

逻辑异或

^^

从左到右

14

逻辑或

||

从左到右

15

选择

? :

从右到左

16

赋值
算术赋值

=
+= -=
*= /=
%= <<= >>=
&= ^= |=

从右到左

17(最低)

序列

,

从左到右

没有取地址运算符或解引用运算符。没有类型转换运算符;而是使用构造器。

数组操作

这些现在在“结构和数组操作”中描述。

函数调用

如果一个函数返回值,则对该函数的调用可以用作表达式,其类型将是用于声明或定义该函数的类型。

函数定义和调用约定在“函数定义”中讨论。

构造器

构造器使用函数调用语法,其中函数名称是类型,并且调用创建该类型的对象。构造器在初始化器和表达式中使用方式相同。(有关详细信息,请参阅“着色语言语法”。)参数用于初始化构造的值。构造器可以用于请求数据类型转换,以从一种标量类型更改为另一种标量类型,或使用较小的类型构建较大的类型,或将较大的类型缩减为较小的类型。

通常,构造器不是具有预定原型的内置函数。对于数组和结构,构造器中必须恰好有一个参数对应每个元素或成员。对于其他类型,参数必须提供足够数量的组件以执行初始化,并且包含过多无法全部使用的参数是编译时错误。详细规则如下。下面实际列出的原型仅是示例的子集。

转换和标量构造器

标量类型之间的转换按照以下原型指示完成

int(uint)     // converts an unsigned integer to a signed integer
int(bool)     // converts a Boolean value to an int
int(float)    // converts a float value to an int
int(double)   // converts a double value to a signed integer
uint(int)     // converts a signed integer value to an unsigned integer
uint(bool)    // converts a Boolean value to an unsigned integer
uint(float)   // converts a float value to an unsigned integer
uint(double)  // converts a double value to an unsigned integer
bool(int)     // converts a signed integer value to a Boolean
bool(uint)    // converts an unsigned integer value to a Boolean value
bool(float)   // converts a float value to a Boolean
bool(double)  // converts a double value to a Boolean
float(int)    // converts a signed integer value to a float
float(uint)   // converts an unsigned integer value to a float value
float(bool)   // converts a Boolean value to a float
float(double) // converts a double value to a float
double(int)   // converts a signed integer value to a double
double(uint)  // converts an unsigned integer value to a double
double(bool)  // converts a Boolean value to a double
double(float) // converts a float value to a double

当构造器用于将浮点类型转换为整数类型时,浮点值的小数部分将被删除。将负浮点值转换为 uint 是未定义的。

当整数值的精度位数多于单精度浮点尾数时,转换为 float 时会损失精度。

当构造器用于将任何整数或浮点类型转换为 bool 时,0 和 0.0 将转换为 false,而非零值将转换为 true。当构造器用于将 bool 转换为任何整数或浮点类型时,false 将转换为 0 或 0.0,而 true 将转换为 1 或 1.0。

构造器 int(uint) 保留参数中的位模式,如果设置了参数的符号位,这将更改参数的值。构造器 uint(int) 保留参数中的位模式,如果该参数为负数,则这将更改其值。

float(float) 这样的标识构造器也是合法的,但用处不大。

具有非标量参数的标量构造器可以用于从非标量中获取第一个元素。例如,构造器 float(vec3) 将选择 vec3 参数的第一个组件。

向量和矩阵构造器

构造器可以用于从一组标量、向量或矩阵创建向量或矩阵。这包括缩短向量的能力。

如果向量构造器只有一个标量参数,则该参数用于将构造的向量的所有分量初始化为该标量的值。如果矩阵构造器只有一个标量参数,则该参数用于初始化矩阵对角线上的所有分量,其余分量初始化为 0.0。

如果从多个标量、一个或多个向量、一个或多个矩阵或它们的混合构造向量,则向量的分量将按顺序从参数的分量构造。参数将从左到右被消耗,并且每个参数的所有分量将在消耗下一个参数的任何分量之前按顺序被消耗。类似地,对于从多个标量或向量或它们的混合构造矩阵。矩阵分量将按列主顺序构造和消耗。在这些情况下,参数中必须提供足够的组件,以便为构造值中的每个组件提供初始化器。提供超出最后一个使用的参数的额外参数是编译时错误。

如果从一个矩阵构造另一个矩阵,那么结果中与参数中具有对应分量(列i,行j)的每个分量(列i,行j)将从那里初始化。所有其他分量将初始化为单位矩阵。如果矩阵参数被赋予矩阵构造函数,则具有任何其他参数是编译时错误。

如果构造函数的参数的基本类型(boolintfloatdouble)与被构造对象的基本类型不匹配,则使用标量构造规则(如上所述)转换参数。

以下是一些有用的向量构造函数

vec3(float)          // initializes each component of the vec3 with the float
vec4(ivec4)          // makes a vec4 with component-wise conversion
vec4(mat2)           // the vec4 is column 0 followed by column 1
vec2(float, float)   // initializes a vec2 with 2 floats
ivec3(int, int, int) // initializes an ivec3 with 3 ints
bvec4(int, int, float, float) // uses 4 Boolean conversions
vec2(vec3)           // drops the third component of a vec3
vec3(vec4)           // drops the fourth component of a vec4
vec3(vec2, float)    // vec3.x = vec2.x, vec3.y = vec2.y, vec3.z = float
vec3(float, vec2)    // vec3.x = float, vec3.y = vec2.x, vec3.z = vec2.y
vec4(vec3, float)
vec4(float, vec3)
vec4(vec2, vec2)

以下是其中一些示例

vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
vec4 rgba = vec4(1.0);      // sets each component to 1.0
vec3 rgb = vec3(color);     // drop the 4th component

要初始化矩阵的对角线,并将所有其他元素设置为零

mat2(float)
mat3(float)
mat4(float)

也就是说,对于所有\(i = j\),result[i][j] 设置为 float 参数,对于所有 ,则设置为 0。

要通过指定向量或标量来初始化矩阵,组件将按列主顺序分配给矩阵元素。

mat2(vec2, vec2);                 // one column per argument
mat3(vec3, vec3, vec3);           // one column per argument
mat4(vec4, vec4, vec4, vec4);     // one column per argument
mat3x2(vec2, vec2, vec2);         // one column per argument
dmat2(dvec2, dvec2);
dmat3(dvec3, dvec3, dvec3);
dmat4(dvec4, dvec4, dvec4, dvec4);
mat2(float, float,                // first column
     float, float);               // second column
mat3(float, float, float,         // first column
     float, float, float,         // second column
     float, float, float);        // third column
mat4(float, float, float, float,  // first column
     float, float, float, float,  // second column
     float, float, float, float,  // third column
     float, float, float, float); // fourth column
mat2x3(vec2, float,               // first column
       vec2, float);              // second column
dmat2x4(dvec3, double,            // first column
        double, dvec3);           // second column

只要有足够的组件来初始化矩阵,就存在从向量和标量构造矩阵的各种其他可能性。要从矩阵构造矩阵

mat3x3(mat4x4); // takes the upper-left 3x3 of the mat4x4
mat2x3(mat4x2); // takes the upper-left 2x2 of the mat4x4, last row is 0,0
mat4x4(mat3x3); // puts the mat3x3 in the upper-left, sets the lower right
                // component to 1, and the rest to 0

结构构造函数

一旦定义了结构,并且其类型被赋予名称,就可以使用相同的名称来构造该结构的实例。例如

struct light {
    float intensity;
    vec3 position;
};

light lightVar = light(3.0, vec3(1.0, 2.0, 3.0));

构造函数的参数将用于按顺序设置结构的成员,每个成员使用一个参数。每个参数的类型必须与其设置的成员的类型相同,或者是一个可以根据“隐式转换”部分转换为成员类型的类型。

结构构造函数可以用作初始化器或表达式。

数组构造函数

数组类型也可以用作构造函数名称,然后可以在表达式或初始化器中使用。例如,

const float c[3] = float[3](5.0, 7.2, 1.1);
const float d[3] = float[](5.0, 7.2, 1.1);

float g;
...
float a[5] = float[5](g, 1, g, 2.3, g);
float b[3];

b = float[3](g, g + 1.0, g + 2.0);

必须与正在构造的数组的大小完全相同的参数个数。如果构造函数中不存在大小,则将数组显式调整为提供的参数数量。参数按顺序分配,从元素 0 开始,分配给构造数组的元素。每个参数的类型必须与数组的元素类型相同,或者是一个可以根据“隐式转换”转换为数组元素类型的类型。

数组的数组的构造方式类似,并且任何维度的大小都是可选的

vec4 b[2] = ...;
vec4[3][2](b, b, b);    // constructor
vec4[][2](b, b, b);     // constructor, valid, size deduced
vec4[3][](b, b, b);     // constructor, valid, size deduced
vec4[][](b, b, b);      // constructor, valid, both sizes deduced

纹理组合采样器构造函数

纹理组合采样器构造函数仅在以 Vulkan 为目标时可用。

纹理组合采样器类型,如 sampler2D,可以使用相同类型的构造函数进行声明,并使用纹理和 samplersamplerShadow。例如

    layout(...) uniform sampler s;   // handle to filtering information
    layout(...) uniform texture2D t; // handle to a texture
    layout(...) in vec2 tCoord;
    ...
    texture(sampler2D(t, s), tCoord);

纹理组合采样器构造函数的结果不能分配给变量

    ... sampler2D sConstruct = sampler2D(t, s);  // ERROR

纹理组合采样器构造函数只能由函数参数使用。

数组的纹理组合采样器构造函数是非法的

    layout(...) uniform texture2D tArray[6];
    ...
    ... sampler2D[](tArray, s) ...  // ERROR

正式地

  • 每个纹理组合采样器类型都可以用作构造函数

  • 构造函数的类型必须与正在声明的变量的类型匹配

  • 构造函数的第一个参数必须是纹理类型

  • 构造函数的第二个参数必须是 samplersamplerShadow 类型的标量

  • 纹理类型的维度(1D、2D、3D、Cube、Rect、Buffer、MS 和 Array)必须与构造类型的维度匹配(也就是说,第一个参数的类型和构造函数的类型的后缀将以相同的方式拼写)

  • 没有控制流结构(例如,?:)会使用任何采样器类型

注意:构造函数和第二个参数之间允许存在阴影不匹配。纹理组合的非阴影采样器可以从 samplerShadow 构造,而纹理组合的阴影采样器可以从 sampler 构造。

向量和标量组件及长度

向量或标量的组件的名称用单个字母表示。为了方便起见,基于位置、颜色或纹理坐标向量的常见用法,每个组件都关联了几个字母。可以通过在变量名后跟句点 (.) 然后是组件名称来选择各个组件。

支持的组件名称为

{ x, y, z, w }

在访问表示点或法线的向量时很有用

{ r, g, b, a }

在访问表示颜色的向量时很有用

{ s, t, p, q }

在访问表示纹理坐标的向量时很有用

组件名称 xrs 例如是向量中同一(第一个)组件的同义词。它们也是标量中唯一组件的名称。

请注意,纹理坐标集的第三个组件已重命名为 p,以避免与颜色中的 r(表示红色)混淆。

访问超出类型声明的组件是编译时错误,因此,例如

vec2 pos;
float height;
pos.x       // is legal
pos.z       // is illegal
height.x    // is legal
height.y    // is illegal

组件选择语法允许通过在句点 (.) 后附加其名称(来自同一名称集)来选择多个组件。

vec4 v4;
v4.rgba;    // is a vec4 and the same as just using v4,
v4.rgb;     // is a vec3,
v4.b;       // is a float,
v4.xy;      // is a vec2,
v4.xgba;    // is illegal - the component names do not come from the same set

最多可以选择 4 个组件。

vec4 v4;
v4.xyzwxy;      // is illegal since it has 6 components
(v4.xyzwxy).xy; // is illegal since the intermediate value has 6
components

组件的顺序可以不同,以对其进行混合或复制

vec4 pos = vec4(1.0, 2.0, 3.0, 4.0);
vec4 swiz = pos.wzyx;   // swiz = (4.0, 3.0, 2.0, 1.0)
vec4 dup = pos.xxyy;    // dup = (1.0, 1.0, 2.0, 2.0)

此表示法比构造函数语法更简洁。要形成 r 值,它可以应用于任何生成向量或标量 r 值的表达式。

组件组表示法可以出现在表达式的左侧。

vec4 pos = vec4(1.0, 2.0, 3.0, 4.0);
pos.xw = vec2(5.0, 6.0);        // pos = (5.0, 2.0, 3.0, 6.0)
pos.wx = vec2(7.0, 8.0);        // pos = (8.0, 2.0, 3.0, 7.0)
pos.xx = vec2(3.0, 4.0);        // illegal - 'x' used twice
pos.xy = vec3(1.0, 2.0, 3.0);   // illegal - mismatch between vec2 and vec3

要形成 l 值,混合必须进一步应用于 l 值,并且不包含重复的组件。它会生成标量或向量类型的 l 值,具体取决于指定的组件数量。

数组下标语法也可以应用于向量(但不能应用于标量)以提供数字索引。所以在

vec4 pos;

pos[2] 指的是 pos 的第三个元素,等价于 pos.z。这允许对向量进行可变索引,以及一种通用的访问组件的方式。任何整数表达式都可以用作下标。第一个组件的索引为零。使用值为负数或大于等于向量大小的常量整数表达式读取或写入向量会导致编译时错误。当使用非常量表达式进行索引时,如果索引为负数或大于等于向量的大小,则行为未定义。

length() 方法可以应用于向量(但不能应用于标量)。结果是向量中组件的数量。例如,

vec3 v;
const int L = v.length();

将常量 L 设置为 3。向量的 .length() 返回的类型是 int,并且返回的值是一个常量表达式。

矩阵组件

可以使用数组下标语法访问矩阵的组件。对矩阵应用单个下标会将矩阵视为列向量的数组,并选择单个列,其类型是与矩阵的列大小相同大小的向量。最左边的列是第 0 列。第二个下标将对生成的向量进行操作,如前面为向量定义的那样。因此,两个下标选择一个列,然后选择一个行。

mat4 m;
m[1] = vec4(2.0);   // sets the second column to all 2.0
m[0][0] = 1.0;      // sets the upper left element to 1.0
m[2][3] = 2.0;      // sets the 4th element of the third column to 2.0

当使用非常量表达式访问矩阵边界之外的组件时,行为未定义。使用超出矩阵边界的常量表达式访问矩阵是编译时错误。

length() 方法可以应用于矩阵。结果是矩阵的列数。例如,

mat3x4 v;
const int L = v.length();

将常量 L 设置为 3。矩阵的 .length() 返回的类型是 int,并且返回的值是一个常量表达式。

结构体和数组操作

结构体的成员和数组的 length() 方法使用句点 (.) 选择。

总而言之,只允许以下运算符对数组和结构体作为整体进行操作

字段选择器

.

相等

== !=

赋值

=

三元运算符

?:

序列运算符

,

索引(仅限数组)

[ ]

只有当两个操作数具有相同的大小和类型时,才允许使用相等运算符和赋值运算符。操作数不能包含任何不透明类型。结构体类型必须是相同的声明结构体。两个数组操作数必须显式大小。当使用相等运算符时,当且仅当所有成员按组件相等时,两个结构体才相等;当且仅当所有元素按元素相等时,两个数组才相等。

数组元素使用数组下标运算符 ([ ]) 访问。访问数组元素的示例是

diffuseColor += lightIntensity[3] * NdotL;

数组索引从零开始。使用类型为 intuint 的表达式访问数组元素。

如果着色器使用小于 0 或大于等于数组声明大小时的索引下标数组,则行为未定义。

也可以使用方法运算符 (.) 和 length 方法访问数组,以查询数组的大小

lightIntensity.length() // return the size of the array

赋值

使用赋值运算符 (=) 将值赋值给变量名

lvalue-表达式 = rvalue-表达式

lvalue-表达式 求值为左值。赋值运算符将 rvalue-表达式 的值存储到左值中,并返回一个具有 lvalue-表达式 的类型和精度的右值。lvalue-表达式rvalue-表达式 必须具有相同的类型,或者表达式必须具有“隐式转换”部分表格中的类型,该类型转换为 lvalue-表达式 的类型,在这种情况下,在赋值完成之前,将对 rvalue-表达式 进行隐式转换。任何其他所需的类型转换都必须通过构造函数显式指定。如果左值不可写,则会发生编译时错误。作为内置类型的变量,整个结构体或数组、结构体成员、应用字段选择器 (.) 以选择组件或不带重复字段的混合的左值、括号内的左值以及使用数组下标运算符 ([ ]) 取消引用的左值都是左值。其他二元或一元表达式、函数名、带有重复字段的混合以及常量不能是左值。三元运算符 (?:) 也不能用作左值。使用不正确的表达式作为左值会导致编译时错误。

赋值左侧的表达式在赋值右侧的表达式之前求值。

其他赋值运算符是

  • 加到 (+=)

  • 从…减去 (-=)

  • 乘到 (*=)

  • 除到 (/=)

  • 模到 (%=)

  • 左移 (<<=)

  • 右移 (>>=)

  • 与到 (&=)

  • 或到 (|=)

  • 异或到 (^=)

其中通用表达式

左值 op= 表达式

等效于

左值 = 左值 op 表达式

其中 左值 是由 lvalue-表达式 返回的值,op 如下所述,并且 lvalue-表达式表达式 必须满足 op 和等号 (=) 的语义要求。

在写入(或初始化)变量之前读取变量是合法的,但该值是未定义的。

表达式

着色语言中的表达式由以下内容构建

  • bool 类型、所有整数类型、所有浮点类型、所有向量类型和所有矩阵类型的常量。

  • 所有类型的构造函数。

  • 所有类型的变量名。

  • 应用 length() 方法的数组、向量或矩阵表达式。

  • 带下标的数组。

  • 返回值的函数调用。在某些情况下,如下所述,也允许在表达式中使用返回 void 的函数调用。

  • 组件字段选择器和数组下标结果。

  • 带括号的表达式。任何表达式,包括具有 void 类型的表达式,都可以用括号括起来。括号可用于对操作进行分组。括号内的操作先于跨括号的操作完成。

  • 算术二元运算符加 (+)、减 (-)、乘 (*) 和除 (/) 对整数和浮点标量、向量和矩阵进行运算。如果操作数中的基本类型不匹配,则应用“隐式转换”中的转换来创建匹配的类型。所有算术二元运算符都会生成与它们操作的操作数相同的基本类型(有符号整数、无符号整数、单精度浮点数或双精度浮点数),在操作数类型转换之后。转换后,以下情况有效

    • 两个操作数都是标量。在这种情况下,应用该操作,产生一个标量。

    • 一个操作数是标量,另一个操作数是向量或矩阵。在这种情况下,将标量运算独立应用于向量或矩阵的每个组件,从而产生相同大小的向量或矩阵。

    • 两个操作数是大小相同的向量。在这种情况下,按组件执行操作,从而产生相同大小的向量。

    • 运算符可以是加法 (+)、减法 (-) 或除法 (/),操作数是具有相同行数和列数的矩阵。在这种情况下,运算是按分量进行的,结果得到相同大小的矩阵。

    • 运算符是乘法 (*),其中两个操作数都是矩阵,或者一个操作数是向量,另一个操作数是矩阵。右向量操作数被视为列向量,左向量操作数被视为行向量。在所有这些情况下,都要求左操作数的列数等于右操作数的行数。然后,乘法 (*) 运算执行线性代数乘法,产生一个对象,其行数与左操作数相同,列数与右操作数相同。“向量和矩阵运算”更详细地解释了如何对向量和矩阵进行运算。

      所有其他情况都会导致编译时错误。

      使用内置函数 dotcrossmatrixCompMultouterProduct 分别获得向量点积、向量叉积、矩阵分量乘法和列向量乘以行向量的矩阵积。

  • 运算符模数 (%) 对有符号或无符号整数或整数向量进行运算。如果操作数中的基本类型不匹配,则应用“隐式转换”中的转换来创建匹配的类型。操作数不能是大小不同的向量;这是一个编译时错误。如果一个操作数是标量,而另一个操作数是向量,则将该标量按分量应用于该向量,从而得到与该向量相同的类型。如果两者都是大小相同的向量,则按分量计算结果。对于任何使用第二个操作数为零计算的组件,结果值都是未定义的,而其他具有非零第二个操作数的组件的结果仍然是已定义的。如果两个操作数都是非负的,则余数是非负的。如果一个或两个操作数为负数,则结果是未定义的。运算符模数 (%) 未为任何其他数据类型(非整数类型)定义。

  • 算术一元运算符加号 (+)、负号 (-)、后置和前置递增和递减 (--++) 对整数或浮点数值(包括向量和矩阵)进行运算。所有一元运算符都按分量对其操作数进行运算。它们的结果与它们运算的类型相同。对于后置和前置递增和递减,表达式必须是可写的左值。前置递增和前置递减将 1 或 1.0 添加或减去到它们操作的表达式的内容,并且前置递增或前置递减表达式的值是该修改的结果值。后置递增和后置递减表达式将 1 或 1.0 添加或减去到它们操作的表达式的内容,但结果表达式具有后置递增或后置递减执行之前的表达式的值。

  • 关系运算符大于 (>)、小于 (<)、大于等于 (>=) 和小于等于 (<=) 仅对标量整数和标量浮点表达式进行运算。结果是标量布尔值。要么操作数的类型必须匹配,要么将应用“隐式转换”中的转换以获得匹配的类型。要在向量上进行分量关系比较,请使用内置函数 lessThanlessThanEqualgreaterThangreaterThanEqual

  • 相等运算符等于 (==) 和不等于 (!=) 对除不透明类型、包含不透明类型的聚合、子例程统一变量和包含子例程统一变量的聚合之外的所有类型进行运算。它们的结果是标量布尔值。如果操作数类型不匹配,则必须从“隐式转换”应用转换到一个操作数,使其匹配,在这种情况下,会进行此转换。对于向量、矩阵、结构和数组,一个操作数的所有分量、成员或元素必须等于另一个操作数中的相应分量、成员或元素,才能将操作数视为相等。要获得向量的分量相等结果,请使用内置函数 equalnotEqual

  • 逻辑二元运算符与 (&&)、或 (||) 和异或 (^^) 仅对两个标量布尔表达式进行运算。结果是标量布尔值。与 (&&) 仅在左操作数的计算结果为 true 时才计算右操作数。或 (||) 仅在左操作数的计算结果为 false 时才计算右操作数。异或 (^^) 将始终计算两个操作数。

  • 逻辑一元运算符非 (!)。它仅对标量布尔表达式进行运算。结果是标量布尔值。要在向量上进行运算,请使用内置函数 not

  • 序列 (,) 运算符通过返回逗号分隔的表达式列表中最右侧表达式的类型和值来对表达式进行运算。所有表达式都按从左到右的顺序求值。序列运算符的操作数可以具有 void 类型。不透明类型不能与序列 (,) 运算符一起使用。

  • 三元选择运算符 (?:)。它对三个表达式 (exp1 ? exp2 : exp3) 进行运算。此运算符计算第一个表达式,该表达式必须产生标量布尔值。如果结果为 true,它将选择计算第二个表达式,否则它将选择计算第三个表达式。仅计算第二个和第三个表达式中的一个。第二个和第三个表达式不能是不透明类型,否则会发生编译时错误。否则,第二个和第三个表达式可以是任何类型,包括 void,只要它们的类型匹配,或者在“隐式转换”中存在可应用于其中一个表达式的转换以使它们的类型匹配即可。这个生成的匹配类型是整个表达式的类型。

  • 按位取反运算符 (~)。操作数必须是有符号或无符号整数或整数向量类型,结果是其操作数的按位取反;每个分量的每个位(包括任何符号位)都将被取反。

  • 移位运算符 (xref:) 和 ([])。对于两个运算符,操作数都必须是有符号或无符号整数或整数向量。一个操作数可以是有符号的,而另一个操作数是无符号的。在所有情况下,结果类型将与左操作数的类型相同。如果第一个操作数是标量,则第二个操作数也必须是标量。如果第一个操作数是向量,则第二个操作数必须是标量或与第一个操作数大小相同的向量,并且结果是按分量计算的。如果右操作数为负数,或大于或等于左表达式基本类型的位数,则结果是未定义的。E1 << E2 的值是 E1(解释为位模式)左移 E2 位。E1 >> E2 的值是 E1 右移 E2 位。如果 E1 是有符号整数,则右移将扩展符号位。如果 E1 是无符号整数,则右移将零扩展。

  • 位运算符包括按位与 (&)、按位异或 (^) 和按位或 (|)。操作数必须为有符号或无符号整数类型或整数向量类型。操作数不能是大小不同的向量;这属于编译时错误。如果一个操作数是标量,另一个是向量,则将标量逐分量地应用于向量,结果的类型与向量相同。如果操作数的基本类型不匹配,则应用“隐式转换”中的转换来创建匹配的类型,这将成为结果的基本类型。对于按位与 (&),结果是操作数的按位与函数。对于按位异或 (^),结果是操作数的按位异或函数。对于按位或 (|),结果是操作数的按位或函数。

有关表达式语法的完整规范,请参阅“着色语言语法”。

向量和矩阵运算

除少数例外,运算都是逐分量的。通常,当运算符对向量或矩阵进行操作时,它会以逐分量的方式独立地对向量或矩阵的每个分量进行操作。例如,

vec3 v, u;
float f;
v = u + f;

等效于

v.x = u.x + f;
v.y = u.y + f;
v.z = u.z + f;

并且

vec3 v, u, w;
w = v + u;

等效于

w.x = v.x + u.x;
w.y = v.y + u.y;
w.z = v.z + u.z;

以及大多数运算符和所有整数和浮点向量和矩阵类型都是如此。例外情况是矩阵乘以向量,向量乘以矩阵,以及矩阵乘以矩阵。这些不是逐分量运算,而是执行正确的线性代数乘法。

vec3 v, u;
mat3 m;
u = v * m;

等效于

u.x = dot(v, m[0]); // m[0] is the left column of m
u.y = dot(v, m[1]); // dot(a,b) is the inner (dot) product of a and b
u.z = dot(v, m[2]);

并且

u = m * v;

等效于

u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z;
u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z;
u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z;

并且

mat3 m, n, r;
r = m * n;

等效于

r[0].x = m[0].x * n[0].x + m[1].x * n[0].y + m[2].x * n[0].z;
r[1].x = m[0].x * n[1].x + m[1].x * n[1].y + m[2].x * n[1].z;
r[2].x = m[0].x * n[2].x + m[1].x * n[2].y + m[2].x * n[2].z;
r[0].y = m[0].y * n[0].x + m[1].y * n[0].y + m[2].y * n[0].z;
r[1].y = m[0].y * n[1].x + m[1].y * n[1].y + m[2].y * n[1].z;
r[2].y = m[0].y * n[2].x + m[1].y * n[2].y + m[2].y * n[2].z;
r[0].z = m[0].z * n[0].x + m[1].z * n[0].y + m[2].z * n[0].z;
r[1].z = m[0].z * n[1].x + m[1].z * n[1].y + m[2].z * n[1].z;
r[2].z = m[0].z * n[2].x + m[1].z * n[2].y + m[2].z * n[2].z;

对于其他大小的向量和矩阵也是如此。

越界访问

在上面描述的数组、向量、矩阵和结构访问的子节中,任何越界访问都会产生未定义的行为。但是,如果通过 API 启用了健壮的缓冲区访问,则此类访问将在活动程序的内存范围内绑定。将无法访问其他程序的内存,并且访问不会导致程序异常终止。越界读取返回未定义的值,包括来自活动程序的其他变量的值或零。越界写入可能会被丢弃或覆盖活动程序的其他变量,具体取决于计算索引的值以及该值与活动程序内存范围的关系。需要为越界访问定义行为的应用程序应在取消引用数组之前检查所有计算索引的范围。

特化常量运算

特化常量运算仅在以 SPIR-V 为目标时可用。

本节中讨论的某些运算可能仅适用于特化常量,并且仍然产生作为特化常量的结果。下面列出了执行此操作的运算。当特化常量与这些运算符之一以及另一个常量或特化常量一起运算时,结果隐式地为特化常量。

  • int()、uint() 和 bool() 构造函数,用于从以下任何类型转换为以下任何类型

    • int

    • uint

    • bool

  • 上述转换构造函数的向量版本

  • 允许上述的隐式转换

  • 混合 (例如 foo.yx)

  • 以下应用于整数或无符号整数类型时

    • 一元负号 (-)

    • 二元运算 (+-*/%)

    • 移位 (xref:[])

    • 按位运算 (&|^)

  • 以下应用于整数或无符号整数标量类型时

    • 比较 (==!=>>=<)

  • 以下应用于布尔标量类型时

    • 非 (!)

    • 逻辑运算 (&&||^^)

    • 比较 (==!=)

  • 三元运算符 (?:)