语句和结构

OpenGL 着色语言的基本组成部分是

  • 语句和声明

  • 函数定义

  • 选择(if-elseswitch-case-default

  • 迭代(forwhiledo-while

  • 跳转(discardreturnbreakcontinue

着色器的整体结构如下

翻译单元 :

全局声明
翻译单元 全局声明

全局声明 :

函数定义
声明

也就是说,着色器是一系列声明和函数体。函数体定义为

函数定义 :

函数原型 { 语句列表 }

语句列表 :

语句
语句列表 语句

语句 :

复合语句
简单语句

花括号用于将一系列语句组合成复合语句。

复合语句 :

{ 语句列表 }

简单语句 :

声明语句
表达式语句
选择语句
迭代语句
跳转语句

简单的声明、表达式和跳转语句以分号结尾。

上面只是略有简化,“着色语言语法” 中指定的完整语法应作为最终规范。

声明和表达式已讨论过。

函数定义

如上面的语法所示,有效的着色器是一系列全局声明和函数定义。函数声明如下例所示

// prototype
returnType functionName (type0 arg0, type1 arg1, ..., typen argn);

函数定义如下

// definition
returnType functionName (type0 arg0, type1 arg1, ..., typen argn)
{
    // do some computation
    return returnValue;
}

其中 returnType 必须存在且不能为 void,或者

void functionName (type0 arg0, type1 arg1, ..., typen argn)
{
    // do some computation
    return; // optional
}

如果 returnValue 的类型与 returnType 不匹配,则必须在“隐式转换”中进行隐式转换,将 returnValue 的类型转换为 returnType,否则将导致编译时错误。

每个 typeN 都必须包含一个类型,并且可以选择包含参数限定符。声明中的形式参数名称(上面的 args)对于声明和定义形式都是可选的。

函数通过使用其名称后跟括号中的参数列表来调用。

允许使用数组作为参数和返回类型。在这两种情况下,数组都必须显式指定大小。数组通过仅使用其名称而不使用方括号来传递或返回,并且数组的大小必须与函数声明中指定的大小匹配。

结构体也允许作为参数类型。返回类型也可以是结构体。

有关声明和定义函数的语法的最终参考,请参见“着色语言语法”。

所有函数都必须在调用之前用原型声明或用主体定义。例如

float myfunc (float f,      // f is an input parameter
              out float g); // g is an output parameter

返回值的函数必须声明为 voidvoid 函数只能在没有返回参数的情况下使用 return,即使返回参数的类型为 void。Return 语句仅接受值

void func1() { }
void func2() { return func1(); } // illegal return statement

仅允许对函数的返回类型使用精度限定符。形式参数可以具有参数、精度和内存限定符,但不能有其他限定符。

不接受输入参数的函数不需要在参数列表中使用 void,因为需要原型(或定义),因此当声明空参数列表“()”时,不会出现歧义。提供习惯用法“(void)”作为参数列表是为了方便。

函数名称可以重载。只要参数类型不同,同一个函数名可以用于多个函数。如果一个函数名以相同的参数类型声明两次,那么返回类型和所有限定符也必须匹配,并且声明的是同一个函数。

例如,

vec4 f(in vec4 x, out vec4 y);       // (A)
vec4 f(in vec4 x, out uvec4 y);      // (B) okay, different argument type
vec4 f(in ivec4 x, out dvec4 y);     // (C) okay, different argument type
int f(in vec4 x, out vec4 y);        // error, only return type differs
vec4 f(in vec4 x, in vec4 y);        // error, only qualifier differs
vec4 f(const in vec4 x, out vec4 y); // error, only qualifier differs

当解析函数调用时,会搜索所有参数的精确类型匹配。如果找到精确匹配,则忽略所有其他函数,并使用精确匹配。如果找不到精确匹配,则将应用“隐式转换”部分的隐式转换以查找匹配项。输入参数(ininout 或默认)上的不匹配类型 必须 从调用参数类型转换为形式参数类型。输出参数(outinout)上的不匹配类型必须从形式参数类型转换为调用参数类型。

如果可以使用隐式转换找到多个匹配函数,则会搜索单个最佳匹配函数。为了确定最佳匹配,将比较每个函数参数和匹配函数对的调用参数和形式参数类型之间的转换。在执行这些比较之后,会比较每对匹配的函数。如果满足以下条件,则函数声明 A 被认为比函数声明 B 更好匹配:

  • 对于至少一个函数参数,A 中该参数的转换比 B 中相应的转换更好;并且

  • B中的转换优于A中相应转换的情况下,不存在函数参数。

如果单个函数声明被认为比其他每个匹配的函数声明都更匹配,则将使用该函数声明。否则,将会发生关于重载函数调用的编译时语义错误,表示调用不明确。

为了确定一个匹配中单个参数的转换是否优于另一个匹配中的转换,将按顺序应用以下规则:

  1. 完全匹配优于任何涉及隐式转换的匹配。

  2. 涉及从 floatdouble 的隐式转换的匹配优于涉及任何其他隐式转换的匹配。

  3. 涉及从 intuintfloat 的隐式转换的匹配优于涉及从 intuintdouble 的隐式转换的匹配。

如果以上规则均不适用于特定的一对转换,则认为这两个转换都不优于另一个。

对于以上示例函数原型(A),(B)和(C),以下示例显示了这些规则如何应用于不同的调用参数类型集

f(vec4, vec4)   // exact match of vec4 f(in vec4 x, out vec4 y)
f(vec4, uvec4)  // exact match of vec4 f(in vec4 x, out uvec4 y)
f(vec4, ivec4)  // matched to vec4 f(in vec4 x, out vec4 y)
                // (C) not relevant, can't convert vec4 to
                // ivec4. (A) better than (B) for 2nd
                // argument (rule 3), same on first argument.
f(ivec4, vec4); // NOT matched. All three match by implicit
                // conversion. (C) is better than (A) and (B)
                // on the first argument. (A) is better than
                // (B) and (C).

用户定义的函数可以有多个声明,但只能有一个定义。

着色器可以重新定义内置函数。如果在调用内置函数之前在着色器中重新声明了该函数(即,原型可见),则链接器只会尝试在与其链接的着色器集中解析该调用。

函数 main 用作着色器可执行文件的入口点。着色器不需要包含名为 main 的函数,但是一组链接在一起以形成单个着色器可执行文件的着色器中,必须有一个着色器包含 main 函数,否则会导致链接时错误。此函数不接受任何参数,不返回任何值,并且必须声明为 void 类型

void main()
{
    ...
}

函数 main 可以包含 return 的用法。有关更多详细信息,请参阅“跳转”。

使用任何其他参数或返回类型声明或定义函数 main 是编译时或链接时错误。

函数调用约定

函数通过值返回进行调用。这意味着在调用时将输入参数复制到函数中,并在函数退出之前将输出参数复制回调用方。由于该函数使用参数的本地副本,因此在函数内不存在关于变量别名的问题。为了控制通过函数定义或声明复制输入和/或输出哪些参数

  • 关键字 in 用作限定符,表示要复制输入参数,但不复制输出参数。

  • 关键字 out 用作限定符,表示要复制输出参数,但不复制输入参数。应尽可能使用此选项以避免不必要地复制输入参数。

  • 关键字 inout 用作限定符,表示要同时复制输入和输出参数。它的含义与同时指定 inout 相同。

  • 声明时没有此类限定符的函数参数与指定 in 的含义相同。

所有参数都在调用时进行求值,并且只求值一次,按照从左到右的顺序进行。对 in 参数的求值会导致一个值被复制到形式参数。对 out 参数的求值会导致一个左值,该左值用于在函数返回时复制输出值。对 inout 参数的求值会同时产生一个值和一个左值;该值在调用时被复制到形式参数,而左值用于在函数返回时复制输出值。

注意

由于 out 参数不会被复制到函数中,因此它们在函数开始时是未初始化的。在函数末尾,这些值将无条件地复制回调用方,如果该值在函数中没有被设置,则会导致传递的参数在调用方中变为未初始化。

将输出参数复制回调用方的顺序是未定义的。

如果上一节中描述的函数匹配需要参数类型转换,则这些转换将在复制输入和复制输出时应用。

在函数中,允许写入仅输入参数。仅修改函数的副本。这可以通过使用 const 限定符声明参数来防止。

当调用函数时,不能将求值结果为非左值的表达式传递给声明为 outinout 的参数,否则会导致编译时错误。

函数原型的语法可以非正式地表示为:

函数原型 :

返回类型 函数名 ( 参数限定符 类型说明符 名称 数组说明符 , …​ )

返回类型 :

类型说明符
精度限定符 类型说明符

参数限定符 :


参数限定符 参数限定符

参数限定符 :

const
in
out
inout
precise
内存限定符
精度限定符

名称 :


标识符

数组说明符 :


[ 整型常量表达式 ]

const 限定符不能与 outinout 一起使用,否则会导致编译时错误。以上内容同时用于函数声明(即原型)和函数定义。因此,函数定义可以有未命名的参数。

不允许静态递归,因此也不允许动态递归。如果程序的静态函数调用图包含循环,则存在静态递归。这包括通过声明为 subroutine uniform(如下所述)的变量的所有潜在函数调用。如果单个编译单元(着色器)包含静态递归或通过子例程变量进行递归的可能性,则会出现编译时或链接时错误。如果在任何时候控制流多次进入但未退出单个函数,则会发生动态递归。

子例程

子例程提供了一种机制,允许以这样一种方式编译着色器:可以在运行时更改一个或多个函数调用的目标,而无需重新编译任何着色器。例如,可以使用对多个照明算法的支持来编译单个着色器,以处理不同类型的灯光或表面材质。使用此类着色器的应用程序可以通过更改其子例程统一变量的值来切换照明算法。要使用子例程,必须声明子例程类型,将一个或多个函数与该子例程类型关联,并声明该类型的子例程变量。然后,通过使用函数调用语法(将函数名称替换为子例程变量的名称)来调用当前分配给变量函数的函数。子例程变量是统一变量,只能通过 OpenGL API 中的命令(UniformSubroutinesuiv)将它们分配给特定函数。

生成 SPIR-V 时,子例程功能不可用。

子程序类型使用类似于函数声明的语句进行声明,使用 subroutine 关键字,如下所示:

subroutine returnType subroutineTypeName(type0 arg0, type1 arg1,
                                         ..., typen argn);

与函数声明一样,形式参数名称(上面的 *args*)是可选的。函数通过使用 subroutine 关键字和函数匹配的子程序类型列表来定义函数,从而与匹配声明的子程序类型相关联。

subroutine(subroutineTypeName0, ..., subroutineTypeNameN)
returnType functionName(type0 arg0, type1 arg1, ..., typen argn)
{ ... } // function body

如果函数和每个关联的子程序类型之间的参数和返回类型不匹配,则会发生编译时错误。

subroutine 声明的函数必须包含一个函数体。重载函数不能用 subroutine 声明;如果任何着色器或阶段包含两个或多个同名函数,且该名称与子程序类型关联,则程序将无法编译或链接。

subroutine 声明的函数也可以通过静态使用 *functionName* 直接调用,就像非子程序函数声明和调用一样。

子程序类型变量必须是 *子程序 uniform*,并且在子程序 uniform 变量声明中使用特定的子程序类型声明。

subroutine uniform subroutineTypeName subroutineVarName;

子程序 uniform 变量的调用方式与函数调用方式相同。当子程序变量(或子程序变量数组的元素)与特定函数关联时,通过该变量的所有函数调用都将调用该特定函数。

与其他 uniform 变量不同,子程序 uniform 变量的作用域仅限于声明该变量的着色器执行阶段。

子程序变量可以声明为显式大小的数组,这些数组只能使用动态 uniform 表达式进行索引。

在任何其他位置使用 subroutine 关键字都会导致编译时错误,除非(如上所示)用于:

  • 在全局作用域声明子程序类型,

  • 将函数声明为子程序,或者

  • 在全局作用域声明子程序变量。

选择

着色语言中的条件控制流通过 ifif-elseswitch 语句完成。

选择语句 :

if ( *布尔表达式* ) *语句*
if ( *布尔表达式* ) *语句* else *语句*
switch ( *初始化表达式* ) { *switch-语句列表可选* }

其中 *switch-语句列表* 是一个嵌套的作用域,包含零个或多个 *switch-语句* 和该语言定义的其他语句的列表,其中 *switch-语句* 添加了一些标签形式。即:

switch-语句列表 :

switch-语句
*switch-语句列表* *switch-语句*

switch-语句 :

case *常量表达式* :
default : *语句*

请注意,以上语法的目的是为了辅助本节中的讨论;规范语法在“着色语言语法”中。

如果 if 表达式的求值结果为 true,则执行第一个 *语句*。如果求值结果为 false 且存在 else 部分,则执行第二个 *语句*。

任何类型求值结果为布尔值的表达式都可以用作条件表达式 *布尔表达式*。向量类型不被接受为 if 的表达式。

条件语句可以嵌套。

switch 语句中的 *初始化表达式* 的类型必须是标量整数。case 标签中的 *常量表达式* 值的类型也必须是标量整数。当任何一对这些值被测试为“值相等”且类型不匹配时,在进行比较之前,将进行隐式转换以将 int 转换为 uint(请参阅“隐式转换”)。如果 case 标签的 *常量表达式* 的值与 *初始化表达式* 的值相等,则将在该标签之后继续执行。否则,如果存在 default 标签,则将在该标签之后继续执行。否则,执行将跳过 switch 语句的其余部分。拥有多个 default 或重复的 *常量表达式* 将导致编译时错误。未嵌套在循环或其他 switch 语句(未嵌套或仅嵌套在 ifif-else 语句中)中的 break 语句也将跳过 switch 语句的其余部分。允许标签穿透,但标签和 switch 语句末尾之间没有语句会导致编译时错误。在第一个 case 语句之前,switch 语句中不允许出现任何语句。

casedefault 标签只能出现在 switch 语句中。casedefault 标签不能嵌套在其对应的 switch 中的其他语句或复合语句中。

迭代

允许使用 for、while 和 do 循环,如下所示:

for (init-expression; condition-expression; loop-expression)
    sub-statement
while (condition-expression)
    sub-statement
do
    statement
while (condition-expression)

有关循环的最终规范,请参阅“着色语言语法”。

for 循环首先计算 *初始化表达式*,然后计算 *条件表达式*。如果 *条件表达式* 的求值结果为 true,则执行循环体。执行循环体后,for 循环将计算 *循环表达式*,然后返回循环以计算 *条件表达式*,重复此过程,直到 *条件表达式* 的求值结果为 false。然后退出循环,跳过循环体及其 *循环表达式*。由 *循环表达式* 修改的变量在循环退出后仍保持其值,前提是它们仍在作用域内。在 *初始化表达式* 或 *条件表达式* 中声明的变量仅在 for 循环的子语句末尾之前有效。

while 循环首先计算 *条件表达式*。如果结果为 true,则执行循环体。然后重复此过程,直到 *条件表达式* 的求值结果为 false,退出循环并跳过循环体。在 *条件表达式* 中声明的变量仅在 while 循环的子语句末尾之前有效。

do-while 循环首先执行循环体,然后执行 *条件表达式*。重复此过程,直到 *条件表达式* 的求值结果为 false,然后退出循环。

*条件表达式* 的表达式必须求值为布尔值。

除了不能在其 *条件表达式* 中声明变量的 do-while 循环外,*条件表达式* 和 *初始化表达式* 都可以声明和初始化变量。变量的作用域仅持续到构成循环体的子语句末尾。

循环可以嵌套。

允许使用非终止循环。非常长或非终止循环的后果取决于平台。

跳转

以下是跳转:

跳转语句 :

continue ;
break ;
return ;
return *表达式* ;
discard ; // 仅在片段着色器语言中

没有“goto”或其他非结构化控制流。

continue 跳转仅在循环中使用。它会跳过其所在的内部循环体的其余部分。对于 whiledo-while 循环,此跳转到循环 *条件表达式* 的下一次计算,循环从该处按先前定义的方式继续。对于 for 循环,跳转到 *循环表达式*,然后跳转到 *条件表达式*。

break 跳转也只能在循环和 switch 语句中使用。它只是立即退出包含 break 的最内层循环或 switch 语句。不再执行 *条件表达式*、*循环表达式* 或 *switch-语句*。

discard 关键字仅允许在片元着色器中使用。它可以在片元着色器中用于放弃对当前片元的操作。该关键字会导致片元被丢弃,并且不会对任何缓冲区进行更新。先前对其他缓冲区(如着色器存储缓冲区)的写入不受影响。控制流会退出着色器,当此控制流不均匀(意味着图元中不同的片元采取不同的控制路径)时,后续的隐式或显式导数是未定义的。它通常会在条件语句中使用,例如

if (intensity < 0.0)
    discard;

片元着色器可能会测试片元的 alpha 值,并根据该测试丢弃片元。但是,应该注意的是,覆盖率测试是在片元着色器运行后进行的,并且覆盖率测试可以更改 alpha 值。

return 跳转会立即导致当前函数退出。如果它有expression,那么这就是该函数的返回值。

函数 main 可以使用 return。这仅仅导致 main 以与到达函数末尾相同的方式退出。它并不意味着在片元着色器中使用 discard。在定义输出之前在 main 中使用 return 将具有与在定义输出之前到达 main 末尾相同的行为。