- 数据块接口
- uniform 块
- 指定着色器中的 uniform 块
- uniform 块的布局控制
- 访问 uniform 块中声明的 uniform 变量
- 从应用程序中访问 uniform 块
- buffer 块
- in/out 块、位置和分量
- 缓存对象的布局
- 使用标准布局限定符
- std140 布局规则
- std430 布局规则
- References
着色器与应用程序之间,或者着色器各阶段之间共享的变量可以组织为变量块的形式,并且有的时候必须采用这种形式。uniform 变量可以使用 uniform 块,输入和输出变量可以使用 in 和 out 块,着色器的存储缓存可以使用 buffer 块。
他们的形式都是类似的。首先了解一下 uniform 块的写法,第一个变量是匿名数据表,第二个是有名称的数据表。
uniform b { // 限定符可以为 uniform、in、out 或者 buffer
vec4 v1; // 块中的变量列表
bool v2; // ...
}; // 访问匿名块成员时使用 v1、v2
或者
uniform b { // 限定符可以为 uniform、in、out 或者 buffer
vec4 v1; // 块中的变量列表
bool v2; // ...
} name; // 访问有名块成员时使用 name.v1、name.v2
各种类型的块接口的详细介绍如下文所示。综合来说,块(block)开始部分的名称(上面的代码中为 b)对应于外部访问时的接口名称,而结尾部分的名称(上面的代码中为 name)用于在着色器代码中访问具体成员变量。
uniform 块如果着色器程序变得比较复杂,那么其中用到的 uniform 变量的数量也会上升。通常会在多个着色器程序中用到同一个 uniform 变量。由于 uniform 变量的位置是着色器链接的时候产生的(也就是调用 glLinkProgram() 的时候),因此它在应用程序中获得的索引可能会有变化,即使我们给 uniform 变量设置的值可能是完全相同的。而 uniform 缓存对象(uniform buffer object)就是一种优化 uniform 变量访问,以及在不同的着色器程序之间共享 uniform 数据的方法。
正如你所知道的,uniform 变量是同时存在于用户应用程序和着色器当中的,因此需要同时修改着色器的内容并调用 OpenGL 函数来设置 uniform 缓存对象。
指定着色器中的 uniform 块访问一组 uniform 变量的方法是使用诸如 glMapBuffer() 的 OpenGL 函数,但是我们需要在着色器中对它们的声明方式略作修改。不再分别声明每个 uniform 变量,而是直接将它们成组,形成一个类似结构体的形式,也就是 uniform 块。一个 uniform 块需要使用关键字 uniform 指定。然后将块中所有需要用到的变量包含在一对花括号中。
如下所示:
声明一个 uniform 块
uniform Matrices {
mat4 ModeView;
mat4 Projection;
mat4 Color;
};
注意,着色器中的数据类型有两种:不透明 的 和 透明 的;其中不透明类型包括采样器(sampler)、图像(image)和原子计数器(atomic counter)。一个 uniform 块中可可以包含透明类型的变量。此外, uniform 块必须在全局作用域内声明。
GLSL 透明 的基础数据类型
类型描述floatIEEE 32 位浮点数doubleIEEE 64 位浮点数int有符号二进制补码的 32 位整数uint无符号的 32 位整数bool布尔值这些基础数据类型都是透明的
uniform 块的布局控制在 uniform 块中可以使用不同的限制符来设置变量的布局方式。这些先支付可以用来设置单个的 uniform 块,也可以用来设置所有后续 uniform 块的排列方式(需要使用布局声明)。可用的限制符及其减少如下表所示:
布局限制符描述binding = N设置缓存的绑定位置,需要用到 OpenGL APIshared设置 uniform 块是多个程序间共享(这时默认的布局方式,与 shared 存储限制符不存在混淆)packed设置 uniform 块占用最小的内存空间,但是这样会禁止程序间共享这个块std140使用标准布局方式来设置 uniform 块或者着色器存储的 buffer 块std430使用标准布局方式来设置 uniform 块offset = N强制设置成员变量位于缓存的 N 字节偏移处align = N强制设置成员变量的偏移位置是 N 的倍数row_major使用行主序的方式来存储 uniform 块中的矩阵column_major使用列主序的方式来存储 uniform 块中的矩阵(这也是默认的顺序)例如,如果需要共享一个 uniform 块,并且使用行主序的方式来存储数据,那么可以使用下面的代码来声明它:
layout (shared, row_major) uniform { ... };
多个限制符可以通过圆括号中的逗号来分隔。如果需要对所有后续的 uniform 块设置同一种布局,那么可以使用下面的语句:
layout (packed, column_major) uniform;
这样一来,当前行之后的所有 uniform 块都会使用这种布局方式,除非再次改变全局的布局,或者对某个块的声明单独设置专属的布局方式。
如果你在着色器和应用程序之间共享了一块缓存,那么这两者都需要确认成员变量所处的内存偏移地址。因此,这里就需要明确布局设置,也就是 std140 和 std430 所提供的功能。
虽然 std140 和 std430 已经提供了比较合理的显示缓存布局,但是用户可能还是希望更好的缓存布局控制方式。此时可以通过 offset 限制符来控制成员的精确位置,或者用 align 限制符来设置一个模糊的对齐方式。你可以只对某些成员应用这些限制符,从而确保应用程序和着色器之间的布局是同步的。
连续的无限制符成员变量会自动进行偏移位置的对齐,这也是 std140 和 std430 的标准。
#version 440
layout (std140) uniform b {
float size; // 默认从 0 字节位置开始
layout(offset=32) vec4 color; // 从 32 字节开始
layout(align=1024) vec4 a[12]; // 从下一个 1024 倍数的字节位置开始
} buf; // 从 a[12] 之后的偏移地址开始
在用户程序设置缓存结构体的时候,可以使用 C/C++ struct 结构体形式的语言工具,也可以直接向缓存的偏移地址写入数据。这里位移的问题就是偏移值和对齐方式的可读性。成员变量的地址偏移值是逐渐增加的,并且必须按照 std140 或者 std430 的规则对齐。总之,对于浮点数和双精度浮点数的结构体来说,对齐过程是自然的,只不过 std140 需要对类似 vec4 这样的类型增加一个额外的 16 字节对齐的限制。
注意 N 的定义:GLSL 的布局限制符在任何时候都是 layout(ID = N) 的形式,这里的 N 必须是一个非负整数。对于 #version 430 或者更早版本来说,它必须是一个字面整数值。不过从 #version 440 开始,N 也可以是一个常整数的表达式了。
访问 uniform 块中声明的 uniform 变量虽然 uniform 块已经命名了,但是块中声明的 uniform 变量并不会受到这个命名的限制。也就是说,uniform 块的命名并不能作为 uniform 变量的父名称,因此在两个不同名的 uniform 块中声明同名变量会在编译时造成错误。然而,在访问一个 uniform 变量的时候,也不一定非要使用块的名称。
从应用程序中访问 uniform 块uniform 变量是着色器与应用程序之间共享数据的桥梁,因此如果着色器中的 uniform 变量是定义在命名的 uniform 块中,那么就有必要找到不同变量的偏移值。如果获取了这些变量的具体位置,那么就可以使用数据对它们进行初始化,这一过程与处理缓存对象(使用 glNamedBufferSubData() 等函数)是一致的。
首先假设已经应用程序的着色器中 uniform 块的名字。如果要对 uniform 块中的 uniform 变量进行初始化,那么第一步就是找到块在着色器程序中的索引位置。可以调用 glGetUniformBlockIndex() 函数返回对应的信息,然后在应用程序的地址空间里完成 uniform 变量的映射。
GLuint glGetUniformBlockIndex(GLuint program, const char * uniformBlockName);
返回 program 中名称为 uniformBlockName 的 uniform 块的索引值。如果 uniformBlockName 不是一个合法的 uniform 程序块,那么返回 GL_INVALID_INDEX。
如果要初始化 uniform 块对应的缓存对象,那么我们需要使用 glBindBuffer() 将缓存对象绑定到目标 GL_UNIFORM_BUFFER 之上。
当对缓存对象进行初始化之后,我们需要判断命名的 uniform 块中的变量总共占据了多大的空间。我们可以使用函数 glGetActiveUniformBlockiv() 并且设置参数为 GL_UNIFORM_BLOCK_DATA_SIZE,这样就可以返回编译器分配的块的大小(根据 uniform 块的布局设置,编译器可能会自动排除着色器中没有用到的 uniform 变量)。glGetActiveUniformBlockiv() 函数还可以用来获取一个明明的 uniform 块的其他一些相关参数。
在获取 uniform 块的索引之后,我们需要将一个缓存对象与这个块相关联。最常见的方法是调用 glBindBufferRange(),或者如果 uniform 块是全部使用缓存来存储的,那么可以使用 glBindBufferBase()。
void glBindBufferRange(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size);
void glBindBufferBase(GLenum target, GLuint index, GLuint buffer);
将缓存对象 buffer 与索引为 index 的命名 uniform 块关联起来。target 必须是支持索引的某个缓存绑定目标。index 是 uniform 块的索引。offset 和 size 分别制定了 uniform 缓存映射的起始索引和大小。
调用 glBindBufferBase() 等价于调用 glBindBufferRange() 并设置 offset 为 0,size 为缓存对象的大小。
在下列情况下调用这两个函数可能会产生 OpenGL 错误 GL_INVALID_VALUE:size 小于 0;offset+size 大于缓存大小;offset 或 size 不是 4 的倍数;index 小于 0 或者大于等于 target 设置的绑定目标所支持的最大索引数。
当建立了命名 uniform 块和缓存对象之间的关联之后,只要使用缓存相关的命名即可对块内的数据进行初始化或者修改。
我们也可以直接设置某个命名 uniform 块和缓存对象之间的绑定关系,也就是说,不使用链接器内部自动绑定块对象并且查询关联结果的方式。如果多个着色器程序需要共享同一个 uniform 块,那么你可能需要用到这种方法。这样可以避免对于不同的着色器程序同一个块有不同的索引号。如果需要显示地控制一个 uniform 块的绑定方式,可以在调用 glLinkProgram() 之前调用 glUniformBlockBinding() 函数。
GLint glUniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);
显示地将块 uniformBlockIndex 绑定到 uniofrmBlockBinding。
在一个命名的 uniform 块中,uniform 变量的布局是通过各种布局限制符在编译和链接时控制的。如果使用了默认的布局方式,那么需要判断每个变量在 uniform 块中的偏移量和数据存储大小。为此,需要调用两个命令:glGetUniformIndices() 负责获取指定名称 uniform 变量的索引位置,而 glGetActiveUniformsiv() 可以获得指定索引位置的偏移量和大小。
void glGetUniformIndices(GLuint program, GLsizei uniformCount, const char** uniformNames, GLuint* uniformIndeices);
返回所有 uniformCount 个 uniform 变量的索引位置,变量的名称通过字符串数组 uniformNames 来指定,程序返回值保存在数组 uniformIndices 当中。在 uniformNames 中的每个名称都是以 NULL 来结尾的,并且 uniformNames 和 uniformIndices 的数组元素数都应该是 uniformCount 个。如果在 uniformNames 中给出的某个名称不是当前启用的 uniform 变量名称,那么 uniformIndices 中对应的位置将会记录为 GL_INVALID_INDEX。
例子,初始化一个命名 uniform 块中的 uniform 变量
const char* vShader = {
"#version 330 core\n"
"uniform Uniforms {"
" vec3 translation;"
" float scale;"
" vec4 rotation;"
" bool enabled;"
"};"
"in vec2 vPos;"
"in vec3 vColor;"
"out vec4 fColor;"
"void main()"
"{"
" vec3 pos = vec3(vPos, 0.0);"
" float angle = radians(rotation[0]);"
" vec3 axis = normalize(rotation.yzw);"
" mat3 I = mat3(1.0);"
" mat3 S = mat3( 0, -axis.z, axis.y, "
" axis.z, 0, -axis.x, "
" -axis.y, axis.x, 0);"
" mat3 uuT = outerProduct(axis, axis);"
" mat3 rot = uuT + cos(angle) * (I - uut) + sin(angle)*S;"
" pos *= scale;"
" pos *= rot;"
" pos += translation;"
" fColor = vec4(scale, scale, scale, 1);"
" gl_Position = vec4(pos, 1);"
"}";
};
const char* fShader = {
"#version 330 core\n"
"uniform Uniforms {"
" vec3 translation;"
" float scale;"
" vec4 rotation;"
" bool enabled;"
"};"
"in vec4 fColor;"
"out vec4 color;"
"void main()"
"{"
" color = fColor;"
"}"
};
// 用于将 GLSL 类型转换为存储大小的辅助函数
size_t
TypeSize(GLenum type) {
size_t size;
#define CASE(Enum, Count, Type) \
case Enum: size = Count * sizeof(Type); break;
switch (type) {
CASE(GL_FLOAT, 1, GLfloat);
CASE(GL_FLOAT_VEC2, 2, GLfloat);
CASE(GL_FLOAT_VEC3, 3, GLfloat);
CASE(GL_FLOAT_VEC4, 4, GLfloat);
CASE(GL_INT, 1, GLint);
CASE(GL_INT_VEC2, 2, GLint);
CASE(GL_INT_VEC3, 3, GLint);
CASE(GL_INT_VEC4, 4, GLint);
CASE(GL_UNSIGNED_INT, 1, GLuint);
CASE(GL_UNSIGNED_INT_VEC2, 2, GLuint);
CASE(GL_UNSIGNED_INT_VEC3, 3, GLuint);
CASE(GL_UNSIGNED_INT_VEC4, 4, GLuint);
CASE(GL_BOOL, 1, GLboolean);
CASE(GL_BOOL_VEC2, 2, GLboolean);
CASE(GL_BOOL_VEC3, 3, GLboolean);
CASE(GL_BOOL_VEC4, 4, GLboolean);
CASE(GL_FLOAT_MAT2, 4, GLfloat);
CASE(GL_FLOAT_MAT2x3, 6, GLfloat);
CASE(GL_FLOAT_MAT2x4, 8, GLfloat);
CASE(GL_FLOAT_MAT3, 9, GLfloat);
CASE(GL_FLOAT_MAT3x2, 6, GLfloat);
CASE(GL_FLOAT_MAT3x4, 12, GLfloat);
CASE(GL_FLOAT_MAT4, 16, GLfloat);
CASE(GL_FLOAT_MAT4x2, 8, GLfloat);
CASE(GL_FLOAT_MAT4x3, 12, GLfloat);
#undef CASE
default:
fprintf(stderr, "Unknown type : 0x%x\n", type);
exit(EXIT_FAILURE);
break;
}
return size;
}
void
init()
{
GLuint program;
glClearColor(1, 0, 0, 1);
ShaderInfo shaders[] = {
{ GL_VERTEX_SHADER, vShader },
{ GL_FRAGMENT_SHADER, fShader },
{ GL_NONE, NULL }
};
program = LoadShaders(shaders);
glUseProgram(program);
/* 初始化 uniform 块 "Uniforms" 中的变量 */
GLuint uboIndex;
GLint uboSize;
GLuint ubo;
GLvoid *buffer;
/* 查找 "Uniforms" 的 uniform 缓存索引,并判断整个块的大小 */
uboIndex = glGetUniformBlockIndex(program, "Uniforms");
glGetActiveUniformBlockiv(program, uboIndex,
GL_UNIFORM_BLOCK_DATA_SIZE, &uboSize);
buffer = malloc(uboSize);
if (buffer == NULL) {
fprintf(stderr, "Unable to allocate buffer\n");
exit(EXIT_FAILURE);
}
else {
enum { Translation, Scale, Rotation, Enabled, NumUniforms };
/* 准备存储在缓存对象中的值 */
GLfloat scale = 0.5;
GLfloat translation[] = { 0.1, 0.1, 0.0 };
GLfloat rotation[] = { 90, 0.0, 0.0, 1.0 };
GLboolean enabled = GL_TRUE;
/* 我们可以建立一个变量名称数组,对应块中已知的 uniform 变量 */
const char* names[NumUniforms] = {
"translation",
"scale",
"rotation",
"enabled"
};
/* 查询对应的属性,以判断向数据缓存中写入数值的位置 */
GLuint indices[NumUniforms];
GLint size[NumUniforms];
GLint offset[NumUniforms];
GLint type[NumUniforms];
glGetUnformIndices(program, NumUniforms, names, indices);
glGetActiveUniformsiv(program, NumUniforms, indices,
GL_UNIFORM_OFFSET, offset);
glGetActiveUniformsiv(program, NumUniforms, indices,
GL_UNIFORM_SIZE, size);
glGetActiveUniformsiv(program, NumUniforms, indices,
GL_UNIFORM_TYPE, type);
/* 将 uniform 变量值拷贝到缓存中 */
memcpy(buffer + offset[Scale], &scale,
size[Scale] * TypeSize(type[Scale]));
memcpy(buffer + offset[Translation], &translation,
size[Translation] * TypeSize(type[Translation]));
memcpy(buffer + offset[Rotation], &scale,
size[Rotation] * TypeSize(type[Rotation]));
memcpy(buffer + offset[Enabled], &scale,
size[Enabled] * TypeSize(type[Enabled]));
/* 建立 uniform 缓存对象,初始化存储内容,并且与着色器程序建立关联 */
glGenBuffers(1, &ubo);
glBindBuffer(GL_UNIFORM_BUFFER, ubo);
glBufferData(GL_UNIFORM_BUFFER, uboSize,
buffer, GL_STATIC_RAW);
glBindBufferBase(GL_UNIFORM_BUFFER, uboIndex, ubo);
}
...
}
buffer 块
GLSL 中的 buffer 块,或者对于应用程序而言,就是着色器的 存储缓存对象(shader storage buffer object),它的行为类似 uniform 块。不过两者之间有两个决定性的差别,使得 buffer 块的功能更为强大。首先,着色器可以写入 buffer 块,修改其中的内容并呈现给其他的着色器调用或者应用程序本身。其次,可以在渲染之前再决定它的大小,而不是编译和链接的时候。例如:
buffer BufferObject { // 创建一个可读写的 buffer 块
int mode; // 序言(preamble)成员
vec4 points[]; // 最后一个成员可以是未定义大小的数组
};
如果在着色器中没有给出上面的数组的大小,那么可以在应用程序中编译和链接之后,渲染之前的设置它的大小。着色器中可以通过 length() 方法获取渲染时的数组大小。
着色器可以对 buffer 块中的成员执行读或写操作。写入操作对着色器存储缓存对象的修改对于其他的着色器调用都是可见的。这种特性对于计算着色器非常有意义,尤其是对非图像的内存区域进行处理的时候。
有关 buffer 块的内存限制符(例如 coherent)以及原子操作的相关深入讨论请参见第 11 章。
设置着色器存储缓存对象的方式与设置 uniform 缓存的方式类似,不过 glBindBuffer()、glBindBufferRange() 和 glBindBufferBase() 需要使用 GL_SHADER_STORAGE_BUFFER 作为目标参数。我们可以在 11.2 节中看到一个更完整的例子。
如果你不需要写入缓存中,那么可以直接使用 uniform 块,并且硬件设备本身可能也没有足够的资源空间来支持 buffer 块,但是 uniform 块通常是足够的。此外,记住 buffer 块只可以使用 std430 布局,而 uniform 块可以选择 std140 或者 std430 布局。
in/out 块、位置和分量着色器变量从一个阶段输出,然后再输入到下一个阶段中,这一过程可以使用块接口来表示。使用逻辑上成组的方式来进行组织也更有利于判断两个阶段的数据接口是否一致,同样对单独程序的链接也会变得更为简单。
例如,一个顶点着色器的输出可能为:
out Lighting {
vec3 normal;
vec2 bumpCoord;
};
它必须与片段着色器的输入是匹配的:
out Lighting {
vec3 normal;
vec2 bumpCoord;
};
顶点着色器可以输出材质和光照的信息,并且都分成独立的数据块。
在本书中,layout(location = N) 被用于每个独立的输入和输出变量,但是从 OpenGL 4.4 版本开始,它也可以作用于输入和输出块的成员,显示地设置它们的位置:
#verison 440
in Lighting {
layout(location=1) vec3 normal;
layout(location=2) vec2 bumpCoord;
};
无论这些 location 位置信息是否在块中,都是可以等价于一个 vec4 的。如果用户希望把多个小的对象设置到同一个位置上,那么也可以使用分量(component)关键字:
#version 440
in Lighting {
layout(location=1, component=0) vec2 offset;
layout(location=1, component=2) vec2 bumpCoord;
};
与其声明一个 vec4 combined,然后使用 combined.xy 和 combined.zw 来模拟 offset 和 bumpCoord,这个方法显然要好得多。它在块的外部也是可以使用的。
OpenGL 着色器语言内置的接口同样也是以块的方式存在的,例如 gl_PerVertex,其中包含了内置变量 gl_Position 等信息。我们可以在附录 C 中找到一个完整的内置变量列表。
缓存对象的布局(这是 OpenGL RedBook 第九版 - 附录H的内容)
使用标准布局限定符当分类 uniform buffer 或者着色器存储缓存中大量变量时,或者想在着色器外读写这些值时,需要知道每个变量的偏移。可以查询这些偏移,但是对于大的 uniform 集合,这个过程需要很多查询,这时繁重的。作为一种可选方案,标准布局限定符需要 GLSL 着色器编译器根据一组规则组织变量,这样就可以预测式地计算块中任意成员的偏移。
为了使能使用 std140 布局,需要为块的声明添加 layout 指示,如下所示:
layout (std140) uniform UniformBlock {
// 声明变量
};
std140 限定符也适用于着色器存储缓存对象。而布局限定符 std430 只提供给着色器存储缓存对象,如下所示:
layout (std430) buffer BufferBlock {
// 声明变量
};
为了使用 std140 或 std430 布局规则,块中一个成员的偏移需要是块中之前成员的对齐值(alignment)和大小的累计总和(这些是否需要在变量之前声明,还有争议),并且要提高到成员的对齐值的程度。第一个成员的开始偏移值总是零。
std140 布局规则表 H-1 所示的规则是 GLSL 编译器用来在 std140 的 uniform 块中放置成员时使用的规则。这个特征只适用于 GLSL 版本 1.40 或者更高版本。
表 H-1 std 140 布局规则
变量类型变量大小和对齐值标量 bool、int、uint、float 和 double大小和对齐值都是在基本机器类型的标量大小(例如,sizeof(GLfloat))两个分量的向量(例如 ivec2)大小和对齐值是基础的标量类型大小的两倍三分量向量(例如 vec3)和四分量向量(例如 vec4)大小和对齐值是基础标量类型大小的四倍标量或者向量的数组数组中每个元素的大小与元素类型的大小相同,是舍入到 vec4 的大小的倍数。这也是数组的对齐值。数组的大小是数组中元素数目的元素大小倍数列优先矩阵或者 R 行 C 列的列优先矩阵的数组与 N 个包含 C 分量的向量的数组布局相同,其中 N 是列的总数行优先矩阵或者有 R 行 C列的行优先矩阵的数组与 N 个包含 C 分量的向量的数组的布局相同,其中 N 是总行数单结构体定义或者结构体的数组结构对齐值是最大结构成员的对齐值,根据前面的规则,舍入到 vec4 的大小的倍数。每个结构从这个对齐值开始,大小是它的成员需要的空间,根据前面的规则,舍入到结构体对齐值的倍数 std430 布局规则表 H-2 所示的规则集是 GLSL 编译器用来在 std430 形式的 uniform 块中放置成员使用的。这个特征只适用于 GLSL 版本 4.30 或者更高。
表 H-2 std430 布局规则
变量类型变量大小和对齐值标量 bool、int、uint、float 和 double大小和对齐值是基本机器类型中的标量大小两分量向量(例如 ivec2)大小和对齐值是基础的标量类型大小的两倍三分量向量(例如 vec3)和四分量向量(例如 vec4)大小和对齐值是基础的标量类型大小的四倍。但是,这只在成员不是数组或者嵌套结构体的一部分时是正确的标量或者向量的数组数组中每个元素的大小与元素类型的大小相同,这里三分量向量不能舍入得到四分量向量的大小。这也是数组的对齐值。数组的大小是数组中元素数目的元素大小倍列优先矩阵或者 C 列 R 行列优先矩阵的数组与 N 个包含 R 分量的向量布局相同,其中 N 是列的总数行优先矩阵或者 R 行 C 列的行优先矩阵的数组与 N 个包含 C 分量的向量的数组布局相同,其中 N 是总行数单结构体定义或者结构体的数组结构对齐值与最大结构成员的对齐值相同,其中三分量向量不能舍入到死分量向量的大小。每个结构从这个对齐值开始,大小是它的成员需要的空间,根据前面的规则,舍入到结构体对齐值的倍数 References- OpenGL RedBook 第九版 - 第2章
- OpenGL RedBook 第九版 - 附录H