基础知识

字符集和编译阶段

OpenGL 着色语言使用的源字符集是 UTF-8 编码方案中的 Unicode。

预处理后,在生成的 GLSL 令牌流中只允许使用以下字符

  • 字母 a-zA-Z 和下划线 (_)。

  • 数字 0-9

  • 符号句点 (.)、加号 (+)、短横线 (-)、斜杠 (/)、星号 (*)、百分号 (%)、尖括号 (<>)、方括号 ([])、圆括号 (())、花括号 ({})、插入符号 (^)、竖线 (|)、与号 (&)、波浪号 (~)、等号 (=)、感叹号 (!)、冒号 (:)、分号 (;)、逗号 (,) 和问号 (?)。

如果在 GLSL 令牌中使用任何其他字符,则会给出编译时错误。

没有双图或三图。除了用作行延续字符外,没有转义序列或反斜杠的其他用途。

行与编译器诊断消息和预处理器相关。它们由回车符或换行符终止。如果两者一起使用,则它仅计为一个换行符终止。对于本文档的其余部分,这些组合中的任何一个都简称为换行符。行可以是任意长度。

一般来说,该语言对字符集的使用区分大小写。

没有字符或字符串数据类型,因此不包括引号字符。

没有文件结束字符。

更正式地说,编译的发生就好像以下逻辑阶段按顺序执行一样

  1. 源字符串被连接以形成单个输入。所有提供的换行符都会保留。

  2. 行号根据所有存在的换行符进行标记,并且在稍后消除换行符时不会更改。

  3. 每当反斜杠 ('\') 紧接换行符出现时,两者都会被消除。(请注意,不替换空格,允许单个标记跨越换行符。)任何新形成的反斜杠后跟换行符不会被消除;仅消除那些最初在阶段 1 之后出现的配对。

  4. 所有注释都替换为一个空格。(请注意,“//”样式注释在其终止换行符之前结束,并且空格通常与预处理相关。)

  5. 完成预处理,生成一系列 GLSL 令牌,这些令牌由上面说明的字符集形成。

  6. 对 GLSL 令牌序列完成 GLSL 处理。

完整定义源字符串、注释、行编号、换行符消除和预处理的详细信息将在后续章节中讨论。这些章节之后的章节描述 GLSL 处理。

源字符串

单个着色器的源是一个由字符集中的字符组成的字符串数组。单个着色器由这些字符串的连接形成。每个字符串可以包含多行,以换行符分隔。字符串中不需要出现换行符;可以由多个字符串形成单行。当实现连接字符串以形成单个着色器时,不会插入换行符或其他字符。可以将多个着色器链接在一起以形成单个程序。

从编译着色器返回的诊断消息必须标识字符串中的行号以及消息应用到哪个源字符串。源字符串按顺序计数,第一个字符串为字符串 0。行号比已处理的换行符数量多 1,包括计算将由行延续字符 (\) 删除的换行符。

在注释处理或预处理之前,由行延续字符(在换行符之前)分隔的行连接在一起。这意味着不使用空格代替行延续字符。也就是说,可以通过连接一行的末尾字符和下一行的开头字符来形成单个令牌。

float f\
oo;
// forms a single line equivalent to "float foo;"
// (assuming '\' is the last character before the new-line and "oo" are
// the first two characters of the next line)

预处理器

有一个预处理器作为编译过程的一部分处理源字符串。除非如下所述,否则它的行为与 C++ 标准预处理器一样(请参阅“规范性引用”)。

预处理器指令的完整列表如下。

#
#define
#undef

#if
#ifdef
#ifndef
#else
#elif
#endif

#error
#pragma

#extension
#version

#line

以下运算符也可用

defined
##

每个数字符号 (#) 在其行中只能以空格或水平制表符开头。它也可以后跟空格和水平制表符,并在指令之前。每个指令都以换行符终止。预处理不会更改源字符串中换行符的数量或相对位置。预处理在换行符被行延续字符删除后进行。

行本身上的数字符号 (#) 将被忽略。任何未在上面列出的指令都将导致编译时错误。

#define#undef 功能的定义与 C++ 预处理器用于定义带有和不带有宏参数的宏的标准定义相同。

以下预定义宏可用

__LINE__
__FILE__
__VERSION__

__LINE__ 将替换为一个十进制整数常量,该常量比当前源字符串中前面的换行符的数量多 1。

__FILE__ 将替换为一个十进制整数常量,该常量说明当前正在处理哪个源字符串号。

__VERSION__ 将替换为一个十进制整数,反映 OpenGL 着色语言的版本号。本文档中描述的着色语言版本将使 __VERSION__ 替换为十进制整数 460。

按照惯例,所有包含两个连续下划线 (__) 的宏名称都保留供底层软件层使用。在着色器中定义或取消定义这样的名称本身不会导致错误,但可能会因同一名称的多个定义而导致意外行为。所有以 “GL_” (“GL” 后跟单个下划线)为前缀的宏名称也被保留,定义或取消定义此类名称会导致编译时错误。

实现必须支持最多 1024 个字符的宏名称长度。实现允许为长度大于 1024 个字符的宏名称生成错误,但也允许支持大于 1024 的长度。

#if#ifdef#ifndef#else#elif#endif 的定义与 C++ 预处理器中的标准操作相同,但以下情况除外:

  • #if#elif 后面的表达式进一步限制为对字面整数常量进行运算的表达式,加上被 defined 运算符使用的标识符。

  • 不支持字符常量。

可用的运算符如下所示。

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

1 (最高)

圆括号分组

( )

不适用

2

一元

defined
+ - ~ !

从右到左

3

乘法

* / %

从左到右

4

加法

+ -

从左到右

5

按位移位

xref: []

从左到右

6

关系

< > <= >=

从左到右

7

相等

== !=

从左到右

8

按位与

&

从左到右

9

按位异或

^

从左到右

10

按位或

|

从左到右

11

逻辑与

&&

从左到右

12 (最低)

逻辑或

||

从左到右

defined 运算符可以通过以下两种方式使用:

defined identifier
defined ( identifier )

可以使用标记粘贴 (##) 运算符将宏中的两个标记连接成一个标记,这与 C++ 预处理器的标准操作相同。结果必须是有效的单个标记,然后将对其进行宏扩展。也就是说,宏扩展仅在标记粘贴之后发生。没有其他基于数字符号的运算符(例如,没有 ##@),也没有 sizeof 运算符。

预处理器中将运算符应用于整数字面值的语义与 C++ 预处理器中的标准语义匹配,而不是 OpenGL 着色语言中的语义。

预处理器表达式将根据宿主处理器的行为进行求值,而不是根据着色器目标处理器的行为进行求值。

#error 将导致实现将编译时诊断消息放入着色器对象的信息日志中(有关如何访问着色器对象的信息日志,请参阅 OpenGL 规范的第 7.12 节“着色器、程序和程序管线查询”)。该消息将是 #error 指令之后的标记,直到第一个换行符。实现必须将 #error 指令的存在视为编译时错误。

#pragma 允许与实现相关的编译器控制。#pragma 后面的标记不受预处理器宏扩展的影响。如果实现不识别 #pragma 后面的标记,则它将忽略该编译指示。以下编译指示被定义为语言的一部分。

#pragma STDGL

STDGL 编译指示用于保留供此语言的未来版本使用的编译指示。任何实现都不得使用第一个标记是 STDGL 的编译指示。

#pragma optimize(on)
#pragma optimize(off)

可以用作关闭优化的一种方式,以帮助开发和调试着色器。它只能在函数定义之外使用。默认情况下,所有着色器都启用优化。调试编译指示

#pragma debug(on)
#pragma debug(off)

可以用于启用编译和使用调试信息注释着色器,以便它可以与调试器一起使用。它只能在函数定义之外使用。默认情况下,调试处于关闭状态。

着色器应声明它们所编写的语言版本。着色器所编写的语言版本由以下方式指定:

#version number profile_opt

其中 *number* 必须是语言的一个版本,遵循与上面的 __VERSION__ 相同的约定。任何使用 4.60 版本语言的着色器中都需要 “#version 460” 指令。任何表示编译器不支持的语言版本的 *number* 都会导致生成编译时错误。该语言的 1.10 版本不要求着色器包含此指令,并且不包含 #version 指令的着色器将被视为以 1.10 版本为目标。指定 #version 100 的着色器将被视为以 OpenGL ES 着色语言的 1.00 版本为目标。指定 #version 300 的着色器将被视为以 OpenGL ES 着色语言的 3.00 版本为目标。指定 #version 310 的着色器将被视为以 OpenGL ES 着色语言的 3.10 版本为目标。

如果提供了可选的 *profile* 参数,则它必须是 OpenGL 配置文件的名称。目前,有三种选择:

core
compatibility
es

*profile* 参数只能与 150 或更高版本一起使用。如果没有提供 profile 参数,并且版本是 150 或更高,则默认值为 core。如果指定了版本 300 或 310,则 profile 参数不是可选的,并且必须是 es,否则会导致编译时错误。es 配置文件的语言规范在 OpenGL ES 着色语言规范中指定。

声明不同版本的 corecompatibility 配置文件着色器可以链接在一起。但是,es 配置文件着色器不能与非 es 配置文件着色器或不同版本的 es 配置文件着色器链接,否则会导致链接时错误。当链接这些规则允许的版本着色器时,其余链接时错误将按照链接着色器的上下文版本的 GLSL 版本中的链接规则给出。着色器编译时错误仍然必须严格根据每个着色器中声明(或默认为)的版本给出。

除非另有说明,否则本规范正在记录核心配置文件,并且为核心配置文件指定的所有内容在兼容性配置文件中也可用。专门属于兼容性配置文件的功能在核心配置文件中不可用。生成 SPIR-V 时,兼容性配置文件功能不可用。

对于实现支持的每个配置文件,都有一个内置的宏定义。所有实现都提供以下宏:

#define GL_core_profile 1

提供 compatibility 配置文件的实现提供以下宏:

#define GL_compatibility_profile 1

提供 es 配置文件的实现提供以下宏:

#define GL_es_profile 1

#version 指令必须在着色器中出现,在任何其他内容之前,但注释和空格除外。

默认情况下,此语言的编译器必须为不符合此规范的着色器发出编译时语法、语义和语法错误。任何扩展行为都必须首先启用。使用 #extension 指令声明控制编译器关于扩展的行为的指令:

#extension extension_name : behavior
#extension all : behavior

其中 *extension_name* 是扩展的名称。本规范中未记录扩展名称。标记 all 表示该行为适用于编译器支持的所有扩展。*behavior* 可以是以下之一:

行为 效果

require

按照扩展 *extension_name* 指定的方式运行。
如果不支持扩展 *extension_name* 或指定了 all,则在 #extension 上给出编译时错误。

enable

按照扩展 *extension_name* 指定的方式运行。
如果不支持扩展 *extension_name*,则在 #extension 上发出警告。
如果指定了 all,则在 #extension 上给出编译时错误。

警告

行为如扩展 extension_name 指定的那样,但在任何可检测到的该扩展的使用上发出警告,除非此类使用受到其他启用或必需的扩展支持。
如果指定了 all,则在任何可检测到的任何扩展的使用上发出警告。
如果不支持扩展 *extension_name*,则在 #extension 上发出警告。

禁用

行为(包括发出错误和警告)就像扩展 extension_name 不是语言定义的一部分一样。
如果指定了 all,则行为必须恢复为正在编译的语言的非扩展核心版本。
如果不支持扩展 *extension_name*,则在 #extension 上发出警告。

extension 指令是一种简单的底层机制,用于设置每个扩展的行为。它不定义诸如哪些组合是合适的策略,这些策略必须在其他地方定义。指令的顺序在设置每个扩展的行为时很重要:稍后出现的指令会覆盖之前看到的指令。all 变体设置所有扩展的行为,覆盖所有先前发出的 extension 指令,但仅针对 warndisable行为

编译器的初始状态就像发出了以下指令一样

#extension all : disable

告诉编译器必须按照此规范完成所有错误和警告报告,忽略任何扩展。

每个扩展都可以定义其允许的范围粒度。如果没有任何说明,则粒度是一个着色器(即,单个编译单元),并且扩展指令必须在任何非预处理器标记之前出现。如果需要,链接器可以强制执行比单个编译单元更大的粒度,在这种情况下,每个涉及的着色器都必须包含必要的扩展指令。

不在线包含 #extension#version 指令的行上进行宏展开。

#line 在宏替换后,必须具有以下形式之一

#line line
#line line source-string-number

其中 linesource-string-number 是常量整数表达式。如果这些常量表达式不是整数文字,则行为未定义。在处理此指令(包括其换行符)后,实现将表现得就像它正在编译行号 line 和源字符串编号 source-string-number。后续的源字符串将按顺序编号,直到另一个 #line 指令覆盖该编号。

注意

一些实现允许在 #line 指令中使用常量表达式,而另一些则不允许。即使在支持表达式的地方,语法也是不明确的,因此结果取决于实现。例如,+ #line +2 +2 // 行号设置为 4,或文件设置为 2,行设置为 2

当为 OpenGL SPIR-V 编译着色器时,可以使用以下预定义的宏

#define GL_SPIRV 100

当以 Vulkan 为目标时,可以使用以下预定义的宏

#define VULKAN 100

注释

注释由 /**/ 分隔,或者由 // 和换行符分隔。// 样式注释包括初始 // 标记,并持续到但不包括终止换行符。/*...*/ 注释包括开始和结束标记。开始注释分隔符(/* 或 //)在注释内部不被识别为注释分隔符,因此注释不能嵌套。

在注释内部,可以使用任何字节值,但值为 0 的字节除外。不会为注释的内容给出错误,并且不需要对注释的内容进行验证。

通过行继续符 (\) 删除换行符在逻辑上发生在处理注释之前。也就是说,以行继续符 (\) 结尾的单行注释包括注释中的下一行。

// a single-line comment containing the next line \
a = b; // this is still in the first comment

标记

预处理后的语言是一系列标记。标记可以是

标记 :

关键字
标识符
整数常量
浮点常量
运算符
; { }

关键字

以下是语言中的关键字,并且(在预处理之后)只能按照本规范中的描述使用,否则会产生编译时错误

const uniform buffer shared attribute varying

coherent volatile restrict readonly writeonly

atomic_uint

layout

centroid flat smooth noperspective

patch sample

invariant precise

break continue do for while switch case default

if else

subroutine

in out inout

int void bool true false float double

discard return

vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4

uint uvec2 uvec3 uvec4

dvec2 dvec3 dvec4

mat2 mat3 mat4

mat2x2 mat2x3 mat2x4

mat3x2 mat3x3 mat3x4

mat4x2 mat4x3 mat4x4

dmat2 dmat3 dmat4

dmat2x2 dmat2x3 dmat2x4

dmat3x2 dmat3x3 dmat3x4

dmat4x2 dmat4x3 dmat4x4

lowp mediump highp precision

sampler1D sampler1DShadow sampler1DArray sampler1DArrayShadow

isampler1D isampler1DArray usampler1D usampler1DArray

sampler2D sampler2DShadow sampler2DArray sampler2DArrayShadow

isampler2D isampler2DArray usampler2D usampler2DArray

sampler2DRect sampler2DRectShadow isampler2DRect usampler2DRect

sampler2DMS isampler2DMS usampler2DMS

sampler2DMSArray isampler2DMSArray usampler2DMSArray

sampler3D isampler3D usampler3D

samplerCube samplerCubeShadow isamplerCube usamplerCube

samplerCubeArray samplerCubeArrayShadow

isamplerCubeArray usamplerCubeArray

samplerBuffer isamplerBuffer usamplerBuffer

image1D iimage1D uimage1D

image1DArray iimage1DArray uimage1DArray

image2D iimage2D uimage2D

image2DArray iimage2DArray uimage2DArray

image2DRect iimage2DRect uimage2DRect

image2DMS iimage2DMS uimage2DMS

image2DMSArray iimage2DMSArray uimage2DMSArray

image3D iimage3D uimage3D

imageCube iimageCube uimageCube

imageCubeArray iimageCubeArray uimageCubeArray

imageBuffer iimageBuffer uimageBuffer

struct

此外,当以 Vulkan 为目标时,还存在以下关键字

texture1D texture1DArray

itexture1D itexture1DArray utexture1D utexture1DArray

texture2D texture2DArray

itexture2D itexture2DArray utexture2D utexture2DArray

texture2DRect itexture2DRect utexture2DRect

texture2DMS itexture2DMS utexture2DMS

texture2DMSArray itexture2DMSArray utexture2DMSArray

texture3D itexture3D utexture3D

textureCube itextureCube utextureCube

textureCubeArray itextureCubeArray utextureCubeArray

textureBuffer itextureBuffer utextureBuffer

sampler samplerShadow

subpassInput isubpassInput usubpassInput

subpassInputMS isubpassInputMS usubpassInputMS

以下是为将来使用而保留的关键字。使用它们会导致编译时错误。

common partition active

asm

class union enum typedef template this

resource

goto

inline noinline public static extern external interface

long short half fixed unsigned superp

input output

hvec2 hvec3 hvec4 fvec2 fvec3 fvec4

filter

sizeof cast

namespace using

sampler3DRect

此外,所有包含两个连续下划线 (__) 的标识符都保留给底层软件层使用。在着色器中定义这样的名称本身不会导致错误,但可能会由于同一名称的多个定义而导致意外行为。

标识符

标识符用于变量名、函数名、结构名和字段选择器(字段选择器选择向量矩阵的组件,类似于结构成员)。标识符的形式如下

标识符 :

非数字
标识符 非数字
标识符 数字

非数字:以下之一

_ a b c d e f g h i j k l m n o p q r s t u v w x y z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

数字:以下之一

0 1 2 3 4 5 6 7 8 9

以 “gl_” 开头的标识符是保留的,一般来说,不能在着色器中声明;这会导致编译时错误。但是,如规范中所述,在某些情况下,先前声明的变量可以重新声明,并且仅为了这些特定目的,才允许在着色器中重新声明预先声明的 “gl_” 名称。

实现必须支持长度最多为 1024 个字符的标识符。实现允许为长度大于 1024 个字符的标识符生成错误,但也允许支持长度大于 1024 的字符。

定义

下面描述的一些语言规则取决于以下定义。

静态使用

如果着色器在预处理后包含一个会访问变量 x 的任何部分的语句,无论控制流是否会导致该语句被执行,则该着色器包含变量 x静态使用。这样的变量被称为被静态使用。如果访问是写入,则进一步称 x静态赋值

动态统一表达式和统一控制流

某些操作要求表达式是动态统一的,或者它位于统一控制流中。这些要求由以下一组定义定义。

调用是特定阶段的 main() 的单次执行,仅操作在该阶段的着色器中显式公开的数据量。(对额外数据实例的任何隐式操作都将构成额外的调用。)例如,在计算执行模型中,单次调用仅操作单个工作项,或者在顶点执行模型中,单次调用仅操作单个顶点。

调用组是共同处理特定计算工作组或图形操作的完整调用集,其中“图形操作”的范围是依赖于实现的,但至少与单个三角形或补丁一样大,并且最多与单个渲染命令一样大,由客户端 API 定义。

在单次调用中,单个着色器语句可以多次执行,从而产生该指令的多个动态实例。当该指令在循环中执行,或者在从多个调用站点调用的函数中执行,或者这些的多个组合时,可能会发生这种情况。不同的循环迭代和不同的动态函数调用站点链会产生该指令的不同动态实例。动态实例通过它们在调用中的控制流路径来区分,而不是通过哪个调用执行它来区分。也就是说,当 main() 的不同调用遵循相同的控制流路径时,它们会执行相同的指令动态实例。

当表达式的值对于执行该动态实例的所有调用(在调用组中)都相同时,该表达式对于使用它的动态实例是动态统一的。

当调用组中的所有调用执行相同的控制流路径(因此是相同指令动态实例序列)时,会发生统一控制流(或收敛控制流)。统一控制流是进入 main() 时的初始状态,并且一直持续到条件分支为不同的调用采用不同的控制路径(非统一或发散控制流)。这种发散可以重新收敛,所有调用再次执行相同的控制流路径,这重新建立了统一控制流的存在。如果在进入选择或循环时控制流是统一的,并且调用组中的所有调用随后都离开了该选择或循环,则控制流重新收敛为统一。

例如

main()
{
    float a = ...; // this is uniform control flow
    if (a < b) {   // this expression is true for some fragments, not all
        ...;       // non-uniform control flow
    } else {
        ...;       // non-uniform control flow
    }
    ...;           // uniform control flow again
}

非统一控制流的其他示例可能发生在某些调用执行其他调用不执行的迭代的循环内,在条件中断、继续、提前返回以及片段丢弃之后,当条件对某些片段为真而对其他片段为假时。

请注意,常量表达式是微不足道的动态统一。由此可见,基于这些的典型循环计数器也是动态统一的。