跳转到内容

GLSL 编程/向量和矩阵运算

来自维基教科书,开放的书籍,开放的世界

GLSL 的语法与 C 非常相似(因此也与 C++ 和 Java 相似);然而,它有内置的数据类型和用于浮点向量和矩阵的函数,这些函数是 GLSL 特有的。这里将讨论这些函数。有关 GLSL 的完整描述,请参阅““进一步阅读””部分的文献资料。

数据类型

[编辑 | 编辑源代码]

在 GLSL 中,vec2vec3vec4 类型分别代表 2D、3D 和 4D 浮点向量。(还有用于整数和布尔向量的类型,这里不讨论。)向量变量的定义方式与您在 C、C++ 或 Java 中定义这些类型的方式相同。

vec2 a2DVector;
vec3 three_dimensional_vector;
vec4 vector4;

用于浮点 2×2、3×3 和 4×4 矩阵的数据类型是:mat2mat3mat4

mat2 m2x2;
mat3 linear_mapping;
mat4 trafo;

精度限定符

[编辑 | 编辑源代码]

在声明浮点变量(包括向量和矩阵变量)时,您可以使用精度限定符 lowpmediumphighp 来建议一个精度,例如

lowp vec4 color; // for colors, lowp is usually fine
mediump vec4 position; // for positions and texture coordinates, mediump is usually ok
highp vec4 astronomical_position; // for some positions and time, highp is necessary
//(precision of time measurement decreases over time
//and things get jumpy if they rely on absolute time since start of the application)

其思想是,lowp 变量需要的计算时间和存储空间(以及带宽)都比 mediump 变量少,而 mediump 变量需要的计算时间比 highp 变量少。因此,为了获得最佳性能,您应该使用最低的精度,该精度仍然可以提供满意的结果。(这里只有少数例外情况,例如,访问 lowp 向量的单个元素可能比访问 mediump 向量慢,因为这需要额外的解码。)

请注意,决定实际使用哪种精度的决定权在于编译器和驱动程序;精度限定符只是程序员的建议。还要注意,lowp 变量的范围可能非常有限(例如,从 -2.0 到 2.0),因此,lowp 对颜色最有用,但通常不适合位置。

为了定义所有浮点变量的默认精度,您应该使用 precision 命令,例如

precision mediump float;

这将在未为浮点变量指定显式精度限定符的情况下使用 mediump。如果没有该命令,则浮点数的默认精度在 OpenGL 或 OpenGL ES 顶点着色器中为 highp,在 OpenGL 片段着色器中为 highp,在 OpenGL ES 片段着色器中为未定义。(然而,在 Unity 中,precision 命令不可用,顶点着色器的默认精度为 highp,片段着色器的默认精度为 mediump。)

构造函数

[编辑 | 编辑源代码]

向量可以通过与数据类型同名的构造函数进行初始化和转换

vec2 a = vec2(1.0, 2.0);
vec3 b = vec3(-1.0, 0.0, 0.0);
vec4 c = vec4(0.0, 0.0, 0.0, 1.0);

请注意,一些 GLSL 编译器会抱怨如果使用整数来初始化浮点向量;因此,最好始终包含小数点。

您也可以在构造函数中使用一个浮点数将所有分量设置为相同的值

vec4 a = vec4(0.0); // = vec4(0.0, 0.0, 0.0, 0.0)

将更高维向量转换为更低维向量也可以通过这些构造函数来实现

vec4 a = vec4(-1.0, 2.5, 4.0, 1.0);
vec3 b = vec3(a); // = vec3(-1.0, 2.5, 4.0)
vec2 c = vec2(b); // = vec2(-1.0, 2.5)

通过向这些构造函数提供正确数量的分量,可以将更低维向量转换为更高维向量

vec2 a = vec2(0.1, 0.2);
vec3 b = vec3(0.0, a); // = vec3(0.0, 0.1, 0.2)
vec4 c = vec4(b, 1.0); // = vec4(0.0, 0.1, 0.2, 1.0)

类似地,可以初始化和构造矩阵。请注意,在矩阵构造函数中指定的值将被消耗以填充第一列,然后是第二列,依此类推。

mat3 m = mat3(
   1.1, 2.1, 3.1, // first column (not row!)
   1.2, 2.2, 3.2, // second column
   1.3, 2.3, 3.3  // third column
);
mat3 id = mat3(1.0); // puts 1.0 on the diagonal
                     // all other components are 0.0
vec3 column0 = vec3(0.0, 1.0, 0.0);
vec3 column1 = vec3(1.0, 0.0, 0.0);
vec3 column2 = vec3(0.0, 0.0, 1.0);
mat3 n = mat3(column0, column1, column2); // sets columns of matrix n

如果使用较小的矩阵来构造较大的矩阵,则额外的行和列将设置为它们在单位矩阵中应该具有的值

mat2 m2x2 = mat2(
   1.1, 2.1, 
   1.2, 2.2
);
mat3 m3x3 = mat3(m2x2); // = mat3(
   // 1.1, 2.1, 0.0,   
   // 1.2, 2.2, 0.0,
   // 0.0, 0.0, 1.0)
mat2 mm2x2 = mat2(m3x3); // = m2x2

如果使用较大的矩阵来构造较小的矩阵,则将选择较大矩阵的顶部、左侧子矩阵,例如,在上一示例的最后一行。

向量的分量可以通过使用 [] 运算符进行数组索引来访问(索引从 0 开始),或者通过使用 . 运算符和元素名称 x, y, z, wr, g, b, as, t, p, q 来访问

vec4 v = vec4(1.1, 2.2, 3.3, 4.4);
float a = v[3]; // = 4.4 
float b = v.w; // = 4.4
float c = v.a; // = 4.4
float d = v.q; // = 4.4

还可以通过扩展 . 符号(“swizzling”)来构造新的向量

vec4 v = vec4(1.1, 2.2, 3.3, 4.4);
vec3 a = v.xyz; // = vec3(1.1, 2.2, 3.3) 
vec3 b = v.bgr; // = vec3(3.3, 2.2, 1.1)
vec2 c = v.tt; // = vec2(2.2, 2.2)

矩阵被认为是由列向量组成的,这些列向量可以通过使用 [] 运算符进行数组索引来访问。可以使用上面讨论的方法来访问结果(列)向量的元素

mat3 m = mat3(
   1.1, 2.1, 3.1, // first column 
   1.2, 2.2, 3.2, // second column
   1.3, 2.3, 3.3  // third column
);
vec3 column3 = m[2]; // = vec3(1.3, 2.3, 3.3)
float m20 = m[2][0]; // = 1.3
float m21 = m[2].y; // = 2.3

运算符

[编辑 | 编辑源代码]

如果二元运算符 *, /, +, -, =, *=, /=, +=, -= 用于相同类型的向量之间,它们将按分量进行操作

vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = vec3(0.1, 0.2, 0.3);
vec3 c = a + b; // = vec3(1.1, 2.2, 3.3)
vec3 d = a * b; // = vec3(0.1, 0.4, 0.9)

特别要注意,a * b 表示两个向量的按分量乘积,这在线性代数中并不常见。

对于矩阵,这些运算符也按分量进行操作,除了 * 运算符,它表示矩阵乘积,例如

  

在 GLSL 中

mat2 a = mat2(1., 2.,  3., 4.);
mat2 b = mat2(10., 20.,  30., 40.);
mat2 c = a * b; // = mat2(
   // 1. * 10. + 3. * 20., 2. * 10. + 4. * 20., 
   // 1. * 30. + 3. * 40., 2. * 30. + 4. * 40.)

对于逐元素矩阵乘法,提供了内置函数 matrixCompMult

* 运算符也可以用来将浮点数(即标量)乘以向量或矩阵的所有元素(从左或右)。

vec3 a = vec3(1.0, 2.0, 3.0);
mat3 m = mat3(1.0);
float s = 10.0;
vec3 b = s * a; // vec3(10.0, 20.0, 30.0)
vec3 c = a * s; // vec3(10.0, 20.0, 30.0)
mat3 m2 = s * m; // = mat3(10.0)
mat3 m3 = m * s; // = mat3(10.0)

此外,* 运算符可用于对应维度的矩阵向量乘法,例如

  

在 GLSL 中

vec2 v = vec2(10., 20.);
mat2 m = mat2(1., 2.,  3., 4.);
vec2 w = m * v; // = vec2(1. * 10. + 3. * 20., 2. * 10. + 4. * 20.)

注意向量必须从右边乘以矩阵。

如果向量从 **左边** 乘以矩阵,则结果对应于从左边将行向量乘以矩阵。这相当于将列向量从右边乘以 **转置** 矩阵。

在组件中

        

因此,从左侧将向量乘以矩阵对应于从右侧将向量乘以转置矩阵。

vec2 v = vec2(10., 20.);
mat2 m = mat2(1., 2.,  3., 4.);
vec2 w = v * m; // = vec2(1. * 10. + 2. * 20., 3. * 10. + 4. * 20.)

由于没有内置函数来计算转置矩阵,因此此技术非常有用:只要需要将向量乘以转置矩阵,就可以将其从左侧乘以原始矩阵。此技术的几个应用在“应用矩阵变换”部分进行了描述。

内置向量和矩阵函数

[edit | edit source]

逐元素函数

[edit | edit source]

如前所述,函数

TYPE matrixCompMult(TYPE a, TYPE b) // component-wise matrix product

对矩阵类型 mat2mat3mat4(表示为 TYPE)计算逐元素乘积。

以下函数对类型为 float, vec2, vec3vec4 的变量(表示为 TYPE)逐元素执行操作。

TYPE min(TYPE a, TYPE b) // returns a if a < b, b otherwise
TYPE min(TYPE a, float b) // returns a if a < b, b otherwise
TYPE max(TYPE a, TYPE b) // returns a if a > b, b otherwise
TYPE max(TYPE a, float b) // returns a if a > b, b otherwise
TYPE clamp(TYPE a, TYPE minVal, TYPE maxVal) 
   // = min(max(a, minVal), maxVal) 
TYPE clamp(TYPE a, float minVal, float maxVal) 
   // = min(max(a, minVal), maxVal) 
TYPE mix(TYPE a, TYPE b, TYPE wb) // = a * (TYPE(1.0) - wb) + b * wb
TYPE mix(TYPE a, TYPE b, float wb) // = a * TYPE(1.0 - wb) + b * TYPE(wb)

还有更多内置函数,这些函数也按逐元素方式工作,但对于向量来说不太有用,例如,abssignfloorceilfractmodstepsmoothstepsqrtinversesqrtpowexpexp2loglog2radians(将度转换为弧度)、degrees(将弧度转换为度)、sincostanasinacosatan(带一个参数和带两个参数分别用于带符号分子和带符号分母)、lessThanlessThanEqualgreaterThangreaterThanEqualequalnotEqualnot

几何函数

[edit | edit source]

以下函数对于向量运算特别有用。TYPE 可以是:float, vec2, vec3vec4(每行仅一项)。

vec3 cross(vec3 a, vec3 b) // = vec3(a[1] * b[2] - a[2] * b[1], 
   // a[2] * b[0] - a[0] * b[2], 
   // a[0] * b[1] - a[1] * b[0]) 
float dot(TYPE a, TYPE b) // = a[0] * b[0] + a[1] * b[1] + ... 
float length(TYPE a) // = sqrt(dot(a, a))
float distance(TYPE a, TYPE b) // = length(a - b)
TYPE normalize(TYPE a) // = a / length(a)
TYPE faceforward(TYPE n, TYPE i, TYPE nRef) 
   // returns n if dot(nRef, i) < 0, -n otherwise
TYPE reflect(TYPE i, TYPE n) // = i - 2. * dot(n, i) * n  
   // this computes the reflection of vector 'i' 
   // at a plane of normalized(!) normal vector 'n'

物理函数

[edit | edit source]

函数

TYPE refract(TYPE i, TYPE n, float r)

计算折射光线的方向,其中 i 指定入射光线的归一化(!) 方向,n 指定两种光学介质(例如空气和水)界面的归一化(!) 法向量。向量 n 应指向 i 所来的方向,即 ni 的点积应为负数。浮点数 r 是光线所来介质的折射率与表面另一侧介质的折射率的比率。因此,如果光线从空气(折射率约为 1.0)射入水(折射率 1.33)表面,则比率 r 为 1.0 / 1.33 = 0.75。函数的计算方法为

float d = 1.0 - r * r * (1.0 - dot(n, i) * dot(n, i));
if (d < 0.0) return TYPE(0.0); // total internal reflection
return r * i - (r * dot(n, i) + sqrt(d)) * n;

如代码所示,如果发生全内反射(参见维基百科中的条目),即光线未穿过两种材料之间的界面,则函数将返回长度为 0 的向量。

进一步阅读

[edit | edit source]

OpenGL 的所有 GLSL 详细信息都在 “OpenGL 着色语言 4.10.6 规范” 中,可在Khronos OpenGL 网站上获取。

关于 OpenGL 的 OpenGL 着色语言的更易理解的描述可以在许多关于 OpenGL 的书的最新版本中找到,例如,在 Dave Shreiner 于 2009 年由 Addison-Wesley 出版发行的 “OpenGL 编程指南:学习 OpenGL 版本 3.0 和 3.1 的官方指南”(第 7 版)第 15 章中。

OpenGL ES 2.0 的所有 GLSL 详细信息都在 “OpenGL ES 着色语言 1.0.17 规范” 中,可在“Khronos OpenGL ES API 注册表”上获取。

关于 OpenGL ES 着色语言的更易理解的描述可以在 Aaftab Munshi、Dan Ginsburg 和 Dave Shreiner 合著的 “OpenGL ES 2.0 编程指南” 第 5 章和附录 B 中找到(参见其网站)。


< GLSL 编程

除非另有说明,否则本页上的所有示例源代码均为公有领域。
华夏公益教科书