【OpenGL ES系列教程】着色语言 Shading Language(一)

由于Android平台下的可编程图形硬件支持是 OpenGL ES 2.0标准,因此本教程向巴友们介绍 OpenGL ES着色语言。 OpenGL ES 着色语言是一种高级的图形编程语言。其源自于应用广泛的C语言,同时具有RendeMan以及其他着色语言的一些优良特性,易于被开发人员掌握。 OpenGL ES 的着色语言主要包括以下特性:
OpenGL ES 2.0着色语言是一种高级的过程语言(注意,不是面向对象的)。
对顶点着色器、片元着色器使用的是同样的语言。
基于C/C++的语法及流程控制。
完美支持向量与矩阵的各种操作。
通过类型限定符来管理输入与输出。
拥有大量的内置函数来提供丰富的功能。
一、着色语言基础
数据类型概述

  1. 标量
    标量也被称为“无向量”其值只有大小,并不具有方向。标量之间的运算遵循简单的代数法则,如质量、密度、体积、时间以及温度等都属于标量。OpenGL ES着色语言支持的标量类型有布尔型(bool)、整形(int)和浮点型(float)。
  2. 向量
    OpenGL ES着色语言中,向量可以看做是用同样类型的标量组成,其基本类型也分为bool、int和float三种。每个向量可以由2个、3个、4个相同的标量组成,具体情况如下:
    向量类型
    说明
    向量类型
    说明
    vec2
    包含了2个浮点数的向量
    ivec4
    包含了4个整数的向量
    vec3
    包含了3个浮点数的向量
    bvec2
    包含了2个布尔数的向量
    vec4
    包含了4个浮点数的向量
    bvec3
    包含了3个布尔数的向量
    ivec2
    包含了2个整数的向量
    bvec4
    包含了4个布尔数的向量
    ivec3
    包含了3个整数的向量
    向量在着色器代码的开发中有着十分重要的作用,可以很方面的存储以及存储颜色、位置、纹理坐标等不仅包含一个组成部分的量。开发中,有时可能需奥单独访问向量中的某个分量,基本的语法为“<向量名>.<分量名>”,根据目的的不同,主要有以下几种用法:
    将一个向量看做颜色时,可以使用r,g,b,a四个分量名,分别代表红、绿、蓝、透明度4个色彩通道。具体用法如下:
    [代码]xml代码:
//给向量aColor的红色通道赋值
aColor.r = 0.6;

将一个向量看做位置时,可以使用x,y,z,w等4个分量名,分别代表X轴,Y轴,Z轴和向量的模四个分量,具体用法和颜色类似。
将一个向量看做纹理坐标时,可以使用s,t,p,q四个分量名,期分别代表纹理坐标的不同分量,具体用法同颜色。(对纹理坐标中的s,t等分量巴友可能不是很明白,不用担心,在后面介绍纹理贴图的教程会进行详细的介绍)
访问向量中的各个不同的分量不但可以采用“.”加上不同的分量名,还可以将向量看做一个数组,用下标来进行访问,具体用法如下:
[代码]xml代码:

//给向量aColor的红色通道赋值
aColor[0] = 0.6;
  1. 矩阵
    有一些基础的开发人员都知道,3D场景中的移位、旋转、缩放等变换都是由矩阵的运算来实现的。因此3D场景的开发中会非常多的使用矩阵,矩阵按尺寸分为2x2矩阵、3x3矩阵、4x4矩阵,具体情况如下表所示:
    矩阵类型
    说明
    mat2
    2x2浮点数矩阵
    mat3
    3x3浮点数矩阵
    mat4
    4x4浮点数矩阵
    对于矩阵的访问,可以讲矩阵作为列向量的数组来访问。如matrix为一个mat4,可以使用matrix[2]取到该矩阵的第三列,其为一个vec4;也可以使用matix[2][2]取得第三列向量的第3个分量。
  2. 采样器
    采样器是着色语言中不同于C语言的一种特殊的基本数据类型,其专门用来进行纹理采样的相关操作。一般情况下,一个采样器变量代表一幅或一套纹理贴图,其具体情况如下:
    采样器
    说明
    sampler2D
    用于访问二维纹理
    smapler3D
    用于访问三维纹理
    samplerCube
    用于访问立方贴图纹理
    需要注意的是,与前面介绍的几种变量不同,采样器变量不能再着色器中初始化。一般情况下采样器变量都用uniform限定符来修饰,从宿主语言(如java)接受传递进着色器的值。
  3. 结构体
    OpenGL ES着色语言还提供了类似C语言中的用户自定义结构体,同样也是使用struct关键字进行声明。其基本用法如下:
    [代码]xml代码:
struct info{
vec3 color;
vec3 position;
vec2 textureCoor;
}
  1. 数组
    声明数组的方式主要有两种,
    在声明数组的同时,指定数组的大小:
    [代码]xml代码:
vec3 position[20];

在声明数组时,也可以不指定数组的大小,但是必须符合下列两种情况之一。
u 引用数组之前,要再次使用第一种声明方式来生命该数组:
[代码]xml代码:

//声明了一个大小不定的vec3数组
vec3 position[];
//再次声明该数组,并且指定大小。
vec3 position[5];

u 代码中访问数组的下标都是编译时常量,这时编译器会自动创建适当大小的数组,使得数组尺寸足够存储编译器看到的最大索引值对应的元素。
[代码]xml代码:

//声明了一个大小不定的vec3数组
vec3 position[];
//position需要一个大小为4的数组
position[3] = vec3(3.0);
//position需要一个大小为21的数组
position[20] = vec3(6.0);
  1. 空类型使用void表示,仅用来声明不返回任何值得函数。例如在顶点着色器以及片元着色器中必须存在的main函数就是一个返回值为空的函数,代码如下:
    [代码]xml代码:
void main() {
}

数据类型的基本使用

  1. 声明、作用域及初始化
    变量的声明以及作用域与Java/C++语法类似,可以再任何需要的位置声明变量,同时期作用域也同样分为局部变量和全局变量:
    [代码]xml代码:
//声明了全局变量a和b
int a,b;
//声明了全局变量aPosition并赋值
vec3 aPosition = vec3(1.0, 2.2, 3.3);
void myFunction() {
    //声明了局部变量c并赋值
    int c = 14;
    //给全局变量a赋值
    a = 4;
    //给全局变量b赋值
    b = a * c;
}

向量的初始化还有一些很灵活的技巧,巴友们体会一下下面的代码:
[代码]xml代码:

//声明浮点变量a并赋值
float a = 12.3;
//声明浮点变量b并赋值
float b = 11.4;
//声明2维向量va并赋值
vec2 va = vec2(2.3, 2.5);
//声明2维向量vb并赋值
vec2 vb = vec2(a, b);
//声明3维向量vc并赋值
vec3 vc = vec3(vb, 12.5);
//声明4维向量vd并赋值
vec4 vd = vec4(va, vb);
//声明4维向量ve并赋值, 相当于vec4(0.2 , 0.2 , 0.2, 0.2);
vec4 ve = vec4(0.2);
  1. 运算符
    与大多数编程语言类似,常见的运算符都可以在该语言中使用。下面按照优先级顺序列出了OpenGL ES着色语言中可以使用的运算符:
    运算符
    说明
    运算符
    说明
    []
    用于索引
    .
    成员选择与混合
    ++ --
    自加1与自减1后缀
    ++ --
    自加1与自减1前缀
  • !
    一元非与逻辑非
  • /
    乘法与除法

加法与减法
< > <= >=
关系运算符
== !=
等于和不等于
&&
逻辑与
^^
逻辑异或
||
逻辑或
?:
选择
= += -= *= /=
赋值运算符

  1. 限定符
    与其他的编程语言一样,着色器中对变量也有很多可选的限定符,主要如下:
    限定符
    说明
    attribute
    一般用于每个顶点都各不相同的量,如顶点位置、颜色等。
    uniform
    一般用于对同一组顶点组成的单个3D物体中所有顶点都相同的量,如当前光源的位置。
    varying
    用于从顶点着色器传递到片元着色器的量
    const
    用于声明常量
    attribute限定符
    顾名思义为属性限定符,其修饰的变量用来接收渲染管线传递进顶点着色器的当前待处理顶点的各种属性值。这些属性值每个顶点各自拥有独立的副本,用于描述顶点的各项特征,如顶点坐标、法向量、颜色、纹理坐标等。
    用attribute限定符修士的变量其值是由宿主程序批量出入渲染管线的,管线进行基本处理后再传递给顶点着色器。数据中有多少个顶点,管线就调用多少次顶点着色器,每次讲一个顶点的各种属性数据传递给顶点着色器中对应atribute变量。因此,顶点着色器每次执行将完成对一个顶点各项属性数据的处理。
    从上面的介绍中可以看出,atribute限定符只能用于顶点着色器中,不能再片元着色器中使用,且attribute限定符只能用来修饰浮点数标量、浮点向量以及矩阵变量,不能用来修饰其他类型的变量。下面的代码片段给出了在顶点着色器中正确使用attribute限定符的情况:
    [代码]xml代码:
//顶点位置
attribute vec3 aPosition;
//顶点法向量
attribute vec3 aNormal;

前面已经提到,对于用attribute限定符修饰的变量的值是由宿主程序批量传入渲染管线的,相关代码如下:
[代码]java代码:

// 声明顶点位置属性引用
int maPositionHandle;
// 获取顶点位置属性引用的值,
// mProgram为着色器程序ID,
// aPosition为着色器中对应属性的变量名称。
maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
// 将顶点位置数据传入渲染管线
// maPositionHandle:顶点位置属性引用
// 3:每顶点一组的数据个数(这里是X,Y,Z坐标,因此为3)
// GLES20.GL_FLOAT:数据类型
// false:是否格式化
// 3 * 4:每组数据的尺寸,这个魅族3个浮点数值(X,Y,Z坐标),每个浮点数4个字节
// mVertexBuffer:存放了数据的缓冲
GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, 3 * 4, mVertexBuffer);

具体代码可以参考第一个教程中的Triangle类。
http://www.apkbus.com/blog-99192-39498.html
uniform限定符
uniform为一致变量限定符,一致变量指的是对于同一组顶点组成的单个3D物体中所有顶点都相同的量。Uniform变量可以用在顶点着色器或片元着色器中,其支持用来修饰所有的基本数据类型。与属性限定符类似,一致变量的值也是从宿主程序传入的。
下面的代码片给出了在顶点或片元着色器中正确使用uniform限定符的情况:
[代码]xml代码:

//总变换矩阵
uniform mat4 uMVPMatrix;
//变换矩阵
uniform mat4 uMMatrix;
//光源位置
uniform vec3 uLightLocation;
//摄像机位置
uniform vec3 uCamera;

将一致变量的值由宿主程序传入渲染管线的代码如下:
[代码]java代码:
//总变换矩阵一致变量引用
int muMVPMatrixHandle;
//获取着色器程序中总变换矩阵一致变量的引用
muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
//通过一致变量引用将一致变量值传入渲染管线
GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, Triangle.getFianlMatrix(mMMatrix), 0);

需要注意的是,随一致性变量类型不同将值传入渲染管线的方法也有所不同,这些方法的名称都以glUniform开头,常用的如下所列:
glUniformNf/glUniformNfv方法,将N个浮点数传入管线,以备管线传递给由N个浮点数组成的一致变量,N的取值为1,2,3,4。
glUniformNi/glUniformNiv方法,将N个整数传入管线,以备管线传递给由N个整数组成的一致变量,N的取值为1,2,3,4。
glUniformMatrixNfv方法,将N * N的矩阵传入管线,以备管线传递给N * N矩阵类型的一致变量,N的取值为2,3,4。
verying限定符
想要将顶点着色器中的信息传入到片元着色器中,必须使用varying限定符。欧诺个varying限定符修饰的全局变量又称为易变变量,易变变量可以看成是顶点着色器和片元着色器之间的动态接口,方便顶点着色器与片元着色器之间信息的传递。下图给出了易变变量的工作原理:


21212.png

从上图可以看出,首先顶点着色器再每个顶点中都对一边变量vPosition进行了赋值。接着在片元着色器中接受易变变量vPosition的值时得到的并不是某个顶点赋的特定值,而是根据片元所在的位置以及图元中各个顶点的位置进行差值计算产生的值。
如图中顶点1、2、3的vPosition值分别为vec3(0,7,0)、vec3(5,0,0)、vec3(-5,0,0),则插值后片元a的vPosition值为vec3(1.45, 2.06, 0)。
从上述介绍中可以看到,光栅化后产生了多少个片元,就会插值计算出多少套易变变量。同时渲染管线就会调用多少次片元着色器。可以看出,3D物体的渲染中,片元着色器执行的次数会大大超过顶点着色器。因此GPU硬件中配置的片元着色器硬件数量往往多于顶点着色器硬件数量以提高渲染速度。
const限定符
用const限定符修饰的变量其值是不可以变的,也就是常量,又称为编译时常量。编译时常量在声明的时候必须进行初始化。例如:
[代码]xml代码:

const int tempx = 1;

推荐阅读更多精彩内容