上节在绘制三角形的时候,简略解说了一些着色器,GLSL 的相干概念,可能看的云里雾里的。不要放心,在本节中,我将具体解说着色语言 GL Shader Language(GLSL)的一些根本的概念。
PS:
无非凡阐明,文中的 GLSL 均指 OpenGL ES 2.0 的着色语言。
GLSL (GL Shader Language)
在上一节中,咱们提到了 GLSL 的语法与 C 语言很相似,也看到了一个非常简单的着色器,如下:
VertexShader
"attribute vec4 aPosition; \n"
"void main() \n"
"{ \n"
"gl_Position = aPosition; \n"
"} \n";
Fragment Shader
"precision mediump float;\n"
"void main() \n"
"{ \n"
"gl_FragColor = vec4 (1.0, 0.0, 0.0, 1.0); \n"
"} \n";
和 C 语言程序对应,用 GLSL 写出的着色器,它同样包含:
- 变量 position
- 变量类型 vec4
- 限定符 attribute
- main 函数
- 根本赋值语句 gl_Position = aPosition
- 内置变量 gl_Position
- …
这所有,都是那么像 C 语言,所以,在把握 C 语言的根底上,GLSL 的学习老本是很低的。
学习一门语言,咱们无非是从 变量类型,构造体,数组,语句,函数,限定符等 方面开展。上面,咱们就照着这个程序,学习 GLSL。
变量
变量及变量类型
变量类型 | 形容 | 变量类别 |
---|---|---|
void | 用于无返回值的函数或空的参数列表 | |
float, int, bool | 浮点型,整型,布尔型的标量数据类型 | 标量 |
float, vec2, vec3, vec4 | 蕴含 1,2,3,4 个元素的浮点型向量 | 浮点型向量 |
int, ivec2, ivec3, ivec4 | 蕴含 1,2,3,4 个元素的整型向量 | 整型向量 |
bool, bvec2, bvec3, bvec4 | 蕴含 1,2,3,4 个元素的布尔型向量 | 布尔型向量 |
mat2, mat3, mat4 | 尺寸为 2 ×2,3×3,4×4 的浮点型矩阵 | 浮点矩阵 |
sampler2D, samplerCube | 示意 2D,立方体纹理的句柄 | 纹理句柄 |
除上述之外,着色器中还能够将它们形成数组或构造体,以实现更简单的数据类型。
PS:
GLSL 中没有指针类型。
标量
标量 (Scalar) 只是一个数字(或者说是仅有一个重量的向量), 是 只有大小没有方向的量
标量对应 C 语言的根底数据类型,它的结构和 C 语言统一,如下:
float mFloat = 1.0f;
bool mFlag = true;
mFloat = float(mFlag); // bool -> float
mFlag = bool(mFloat); // float -> bool
向量
向量最根本的定义就是一个方向。或者更正式的说,向量有一个方向 (Direction) 和大小 (Magnitude,也叫做强度或长度)。
当结构向量时,向量结构器中的各参数将会被转换成雷同的类型(浮点型、整型或布尔型)。往向量结构器中传递参数有两种模式:
- 如果向量结构器中只提供了一个标量参数,则向量中所有值都会设定为该标量值。
- 如果提供了多个标量值或提供了向量参数,则会从左至右应用提供的参数来给向量赋值,如果应用多个标量来赋值,则须要确保标量的个数要多于向量结构器中的个数。
向量结构器用法如下:
vec4 mVec4 = vec4(1.0); // mVec4 = {1.0, 1.0, 1.0, 1.0}
vec3 mVec3 = vec3(1.0, 0.0, 0.5); // mVec3 = {1.0, 0.0, 0.5}
vec3 tempVec3 = vec3(mVec3); // tempVec3 = mVec3
vec2 mVec2 = vec2(mVec3); // mVec2 = {mVec3.x, mVec3.y}
矩阵
单来说矩阵就是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的元素(Element)。上面是一个 2×3 矩阵的例子:
矩阵的构造方法则更加灵便,有以下规定:
- 如果对矩阵结构器只提供了一个标量参数,该值会作为矩阵的对角线上的值。例如
mat4(1.0)
能够结构一个 4 × 4 的单位矩阵 - 矩阵能够通过多个向量作为参数来结构,例如一个 mat2 能够通过两个 vec2 来结构
- 矩阵能够通过多个标量作为参数来结构,矩阵中每个值对应一个标量,依照从左到右的程序
除此之外,矩阵的构造方法还能够更灵便,只有有足够的组件来初始化矩阵,其结构器参数能够是标量和向量的组合。在 OpenGL ES 中,矩阵的值会以 列的程序来存储。在结构矩阵时,结构器参数会依照列的程序来填充矩阵,如下:
mat3 mMat3 = mat3(
1.0, 0.0, 0.0, // 第一列
0.0, 1.0, 0.0, // 第二列
0.0, 1.0, 1.0); // 第三列
向量和矩阵的重量
独自取得向量中的组件有两种办法:即应用 "."
符号或应用数组下标办法。根据形成向量的组件个数,向量的组件能够通过 {x, y, z, w}
,{r, g, b, a}
或 {s, t, r, q}
等操作来获取。之所以采纳这三种不同的命名办法,是因为向量经常会用来示意数学向量、色彩、纹理坐标等。其中的x
、r
、s
组件总是示意向量中的第一个元素,如下表:
重量拜访符 | 符号形容 |
---|---|
(x,y,z,w) | 与地位相干的重量 |
(r,g,b,a) | 与色彩相干的重量 |
(s,t,p,q) | 与纹理坐标相干的重量 |
不同的命名约定是为了方便使用,所以哪怕是形容地位的向量,也是能够通过 {r, g, b, a}
来获取。然而在应用向量时不能混用不同的命名约定,即不能应用 .xgr
这样的形式,每次只能应用同一种命名约定。当应用 "."
操作符时,还能够对向量中的元素从新排序,如下:
vec3 mVec3 = vec3(0.0, 1.0, 2.0); // mVec3 = {0.0, 1.0, 2.0}
vec3 mTemp;
mTemp = mVec3.xyz; // mTemp = {0.0, 1.0, 2.0}
mTemp = mVec3.xxx; // mTemp = {0.0, 0.0, 0.0}
mTemp = mVec3.zyx; // mTemp = {2.0, 1.0, 0.0}
除了应用 "."
操作符之外,还能够应用数组下标操作。在应用数组下标操作时,元素 [0]
对应的是 x
,元素 [1]
对应 y
,以此类推。
向量和矩阵的运算
绝大多数状况下,向量和矩阵的计算是 逐重量进行的(component-wise)。当运算符作用于向量或矩阵时,该运算独立地作用于向量或矩阵的每个重量。
以下是一些示例:
向量 / 矩阵与标量运算
标量 (Scalar) 只是一个数字(或者说是仅有一个重量的向量)。当把一个向量加 / 减 / 乘 / 除一个标量,咱们能够简略的把向量的每个重量别离进行该运算。对于加法来说会像这样:
其中的 + 能够是 +,-,·或÷,其中·是乘号。留神-和÷运算时不能颠倒(标量 -/÷向量),因为颠倒的运算是没有定义的。
相似的
矩阵与标量之间的加减定义如下:
留神,数学上是没有向量 / 矩阵与标量相加这个运算的,然而很多线性代数的库都对它有反对, 如 GLM
向量之间的运算
向量加减
向量的加法能够被定义为是重量的 (Component-wise) 相加,行将一个向量中的每一个重量加上另一个向量的对应重量:
就像一般数字的加减一样,向量的减法等于加上第二个向量的相同向量:
向量相乘
两个向量相乘是一种很奇怪的状况。一般的乘法在向量上是没有定义的,因为它在视觉上是没有意义的。然而在相乘的时候咱们有两种特定状况能够抉择:一个是点乘(Dot Product),记作,另一个是叉乘(Cross Product),记作。
- 点乘
两个向量的点乘等于它们的数乘后果乘以两个向量之间夹角的余弦值,两个向量的点击是一个标量。可能听起来有点费解,咱们来看一下公式:
2. 叉乘
叉乘只在 3D 空间中有定义,它须要两个不平行向量作为输出,生成一个正交于两个输出向量的第三个向量。如果输出的两个向量也是正交的,那么叉乘之后将会产生 3 个相互正交的向量。接下来的教程中这会十分有用。上面的图片展现了 3D 空间中叉乘的样子:
矩阵之间的运算
矩阵与矩阵之间的加减就是两个矩阵对应元素的加减运算,所以总体的规定和与标量运算是差不多的,只不过在雷同索引下的元素能力进行运算。这也就是说加法和减法只对同维度的矩阵才是有定义的。一个 3×2 矩阵和一个 2×3 矩阵(或一个 3×3 矩阵与 4×4 矩阵)是不能进行加减的。咱们看看两个 2×2 矩阵是怎么相加的:
同样的法令也实用于减法:
- 矩阵的数乘
和矩阵与标量的加减一样,矩阵与标量之间的乘法也是矩阵的每一个元素别离乘以该标量。上面的例子展现了乘法的过程:
当初咱们也就能明确为什么这些独自的数字要叫做标量 (Scalar) 了。简略来说,标量就是用它的值 缩放 (Scale) 矩阵的所有元素(译注:留神 Scalar 是由 Scale + -ar 演变过去的)。后面那个例子中,所有的元素都被放大了 2 倍。
到目前为止都还好,咱们的例子都不简单。不过矩阵与矩阵的乘法就不一样了。
- 矩阵相乘
矩阵之间的乘法不见得有多简单,但确实很难让人适应。矩阵乘法基本上意味着遵循规定好的法令进行相乘。当然,相乘还有一些限度:
- 只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵能力相乘。
- 矩阵相乘不恪守交换律(Commutative),也就是说 A⋅B≠B⋅AA⋅B≠B⋅A。
咱们先看一个两个 2×2 矩阵相乘的例子:
当初你可能会在想了:天哪,刚刚到底产生了什么? 矩阵的乘法是一系列乘法和加法组合的后果,它应用到了左侧矩阵的行和右侧矩阵的列。咱们能够看上面的图片:
构造体
与 C 语言类似,除了根本的数据类型之外,还能够将多个变量聚合到一个构造体中,下边的示例代码演示了在 GLSL 中如何申明构造体:
struct MyStruct
{
vec4 color;
vec2 position;
} myVertex;
首先,定义会产生一个新的类型叫做 MyStruct
,及一个名为 myVertex
的变量。构造体能够用结构器来初始化,在定义了新的构造体之后,还会定义一个与构造体类型名称雷同的结构器。结构器与构造体中的数据类型必须一一对应,如下:
myVertex = MyStruct(vec4(0.0, 1.0, 0.0, 0.0), // color
vec2(0.5, 0.5)); // position
构造体的结构器是基于类型的名称,以参数的模式来赋值。获取构造体内元素的办法和 C 语言中统一:
vec4 color = myVertex.color;
vec4 position = myVertex.position;
数组
除了构造体外,GLSL 中还反对数组。语法与 C 语言类似,创立数组的形式如下代码所示:
float floatArray[4];
vec4 vecArray[2];
与 C 语言不同,在 GLSL 中,对于数组有两点须要留神:
- 除了 uniform 变量之外,数组的索引只容许应用常数整型表达式。
- 在 GLSL 中不能在创立的同时给数组初始化,即数组中的元素须要在定义数组之后一一初始化,且数组不能应用 const 限定符。
函数
GLSL 函数的申明与 C 语言中很类似,无非就是返回值,函数名,参数列表。
GLSL 着色器同样是从 main 函数开始执行。另外,GLSL 也反对自定义函数。当然,如果一个函数在定以前被调用,则须要先申明其原型。
值得注意的一点是,GLSL 中函数不可能递归调用,且必须申明返回值类型(无返回值时申明为 void)。如下:
vec4 getPosition(){vec4 v4 = vec4(0.0f,0.0f,0.0f,1.0f);
return v4;
}
void doubleSize(inout float size){size= size*2.0 ;}
// 主函数
void main() {
float psize= 10.0;
doubleSize(psize);
gl_Position = getPosition();
gl_PointSize = psize;
}
限定符
存储限定符
在申明变量时,应依据须要应用存储限定符来润饰,相似 C 语言中的说明符。GLSL 中反对的存储限定符见下表:
限定符 | 形容 |
---|---|
< none: default > | 部分可读写变量,或者函数的参数 |
const | 编译时常量,或只读的函数参数 |
attribute | 由应用程序传输给顶点着色器的逐顶点的数据 |
uniform | 在图元处理过程中其值放弃不变,由应用程序传输给着色器 |
varying | 由顶点着色器传输给片段着色器中的插值数据 |
- 本地变量和函数参数只能应用 const 限定符,函数返回值和构造体成员不能应用限定符。
- 数据不能从一个着色器程序传递给下一个阶段的着色器程序,这样会阻止同一个着色器程序在多个顶点或者片段中进行并行计算。
- 不蕴含任何限定符或者蕴含 const 限定符的全局变量能够蕴含初始化器,这种状况下这些变量会在 main() 函数开始之后第一行代码之前被初始化,这些初始化值必须是常量表达式。
- 没有任何限定符的全局变量如果没有在定义时初始化或者在程序中被初始化,则其值在进入 main() 函数之后是未定义的。
- uniform、attribute 和 varying 限定符润饰的变量不能在初始化时被赋值,这些变量的值由 OpenGL ES 计算提供。
默认限定符
如果一个全局变量没有指定限定符,则该变量与应用程序或者其余正在运行的处理单元没有任何分割。不论是全局变量还是本地变量,它们总是在本人的处理单元被分配内存,因而能够对它们执行读和写操作。
const 限定符
任意根底类型的变量都能够申明为常量。常量示意这些变量中的值在着色器中不会发生变化,申明常量只须要在申明时加上限定符 const 即可,申明时必须赋初值。
const float zero = 0.0;
const float pi = 3.14159;
const vec4 red = vec4(1.0, 0.0, 0.0, 1.0);
const mat4 identity = mat4(1.0);
- 常量申明过的值在代码中不能再扭转,这一点和 C 语言或 C++ 一样。
- 构造体成员不能被申明为常量,然而构造体变量能够被申明为常量,并且须要在初始化时应用结构器初始化其值。
- 常量必须被初始化为一个常量表达式。数组或者蕴含数组的构造体不能被申明为常量(因为数组不能在定义时被初始化)。
attribute 限定符
GLSL 中另一种非凡的变量类型是 attribute 变量。attribute 变量只用于顶点着色器中,用来存储顶点着色器中每个顶点的输出(per-vertex inputs)。attribute 通常用来存储地位坐标、法向量、纹理坐标和色彩等。留神 attribute 是用来存储单个顶点的信息。如下是蕴含地位,色值 attribute 的顶点着色器示例:
attribute vec4 aPosition;
attribute vec4 aColor;
varying vec4 outColor;
void main(void) {
outColor = aColor;
gl_Position = aPosition;
}
着色器中的两个 attribute 变量 position
和 color
由应用程序加载数值。应用程序会创立一个顶点数组,其中蕴含了每个顶点的地位坐标和色值信息。可应用的最大 attribute 数量也是有下限的,能够应用 gl_MaxVertexAttribs
来获取,也能够应用内置函数 glGetIntegerv
来询问 GL_MAX_VERTEX_ATTRIBS
。OpenGL ES 2.0 实现反对的起码 attribute 个数是 8 个。
对于由 attribute 润饰的变量到底是如何赋值的
,咱们后面也说过,这里呢也再反复一下
glLinkProgram(m_program_id);
// 在 OpenGL 程序中获取对应属性名字在程序中的句柄
m_vertex_pos_handler = glGetAttribLocation(m_program_id, "aPosition");// 这里的 aPosition 对应着下面的 attribute vec4 aPosition;
// 应用 glVertexAttribPointer 办法
glVertexAttribPointer(m_vertex_pos_handler, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), m_vertex_coors);
uniform 限定符
uniform 是 GLSL 中的一种变量类型限定符,用于存储应用程序通过 GLSL 传递给着色器的只读值。uniform 能够用来存储着色器须要的各种数据,如变换矩阵、光参数和色彩等。传递给着色器的在所有的顶点着色器和片段着色器中放弃不变的的任何参数,基本上都应该通过 uniform 来存储。uniform 变量在全局区申明,以下是 uniform 的一些示例:
uniform mat4 viewProjMatrix;
uniform mat4 viewMatrix;
uniform vec3 lightPosition;
须要留神的一点是,顶点着色器和片段着色器共享了 uniform 变量的命名空间。对于连贯于同一个着色程序对象的顶点和片段着色器,它们共用同一组 uniform 变量,因而,如果在顶点着色器和片段着色器中都申明了 uniform 变量,二者的申明必须统一。当应用程序通过 API 加载了 uniform 变量时,该变量的值在顶点和片段着色器中都可能获取到。
另一点须要留神的是,uniform 变量通常是存储在硬件中的”常量区”,这一区域是专门调配用来存储常量的,然而因为这一区域尺寸十分无限,因而着色程序中能够应用的 uniform 的个数也是无限的。能够通过读取内置变量 gl_MaxVertexUniformVectors
和 gl_MaxFragmentUniformVectors
来取得,也能够应用 glGetIntegerv
查问 GL_MAX_VERTEX_UNIFORM_VECTORS
或者 GL_MAX_FRAGMENT_UNIFORM_VECTORS
。OpenGL ES 2.0 的实现必须提供至多 128 个顶点 uniform 向量及 16 片段 uniform 向量。
对于由 uniform 润饰的变量到底是如何赋值的
,咱们应用
// C function GLint glGetUniformLocation (GLuint program, const char *name)
varying 限定符
GLSL 中最初一个要说的存储限定符是 varying。varying 存储的是顶点着色器的输入,同时作为片段着色器的输出,通常顶点着色器都会把须要传递给片段着色器的数据存储在一个或多个 varying 变量中。这些变量在片段着色器中须要有绝对应的申明且数据类型统一,而后在光栅化过程中进行插值计算。以下是一些 varying 变量的申明:
顶点着色器和片段着色器中都会有 varying 变量的申明,因为 varying 是顶点着色器的输入且是片段着色器的输出,所以两处申明必须统一。与 uniform 和 attribute 雷同,varying 也有数量的限度,能够应用 gl_MaxVaryingVectors
获取或应用 glGetIntegerv
查问 GL_MAX_VARYING_VECTORS
来获取。OpenGL ES 2.0 实现中的 varying 变量最小反对数为 8。
回顾下最后那个着色器对应的 varying 申明:
// 顶点着色器
attribute vec4 aPosition;
attribute vec4 aColor;
varying vec4 outColor;
void main(void) {
outColor = aColor;
gl_Position = aPosition;
}
// 片段着色器
varying lowp vec4 outColor;
void main(void) {gl_FragColor = outColor;}
参数限定符
GLSL 提供了一种非凡的限定符用来定义某个变量的值是否能够被函数批改,详见下表:
限定符 | 形容 |
---|---|
in | 默认应用的缺省限定符,指明参数传递的是值,并且函数不会批改传入的值(C 语言中值传递) |
inout | 指明参数传入的是援用,如果在函数中对参数的值进行了批改,当函数完结后参数的值也会批改(C 语言中援用传递) |
out | 参数的值不会传入函数,然而在函数外部批改其值,函数完结后其值会被批改 |
应用的形式如下边的代码:
vec4 myFunc(inout float myFloat, // inout parameter
out vec4 myVec4, // out parameter
mat4 myMat4); // in parameter (default)
精度限定符
OpenGL ES 与 OpenGL 之间的一个区别就是在 GLSL 中引入了精度限定符。精度限定符可使着色器的编写者明确定义着色器变量计算时应用的精度,变量能够抉择被申明为低、中或高精度。精度限定符可告知编译器使其在计算时放大变量潜在的精度变动范畴,当应用低精度时,OpenGL ES 的实现能够更疾速和低功耗地运行着色器,效率的进步来自于精度的舍弃,如果精度抉择不合理,着色器运行的后果会很失真。
OpenGL ES 对各硬件并未强制要求多种精度的反对。其实现能够应用高精度实现所有的计算并且疏忽掉精度限定符,然而某些状况下应用低精度的实现会更有劣势,精度限定符能够指定整型或浮点型变量的精度,如 lowp
,mediump
,及 highp
,如下:
限定符 | 形容 |
---|---|
highp | 满足顶点着色语言的最低要求。对片段着色语言是可选项 |
mediump | 满足片段着色语言的最低要求,其对于范畴和精度的要求必须不低于 lowp 并且不高于 highp |
lowp | 范畴和精度可低于 mediump,但仍能够示意所有色彩通道的所有色彩值 |
具体用法参考以下示例:
highp vec4 position;
varying lowp vec4 color;
mediump float specularExp;
除了精度限定符,还能够指定默认应用的精度。如果某个变量没有应用精度限定符指定应用何种精度,则会应用该变量类型的默认精度。默认精度限定符放在着色器代码起始地位,以下是一些用例:
precision highp float;
precision mediump int;
当为 float
指定默认精度时,所有基于浮点型的变量都会以此作为默认精度,与此相似,为 int
指定默认精度时,所有的基于整型的变量都会以此作为默认精度。在顶点着色器中,如果没有指定默认精度,则 int
和 float
都应用 highp
,即顶点着色器中,未应用精度限定符指明精度的变量都默认应用最高精度。在片段着色器中,float
并没有默认的精度设置,即片段着色器中必须为 float
默认精度或者为每一个 float
变量指明精度。
此时咱们应该也能了解咱们始终以来的片段着色器的意思了
"precision mediump float; \n"// 定义 float 的默认精度,否则片段着色器会报错
"void main() \n"
"{ \n"
"gl_FragColor = vec4 (1.0, 0.0, 0.0, 1.0); \n"
"} \n";
小结
本章介绍了对于 GLSL 的一些根底语法语句之类的常识,总的来说 GLSL 根本还是与 C 语言有很多相似之处的,如果有 C 语言的根底的话,学习 GLSL 的语法并不难。
参考资料
GLSL2.0 官网参考文档