上一章简单介绍了一下着色器,对于Android开发者比较直白的理解就是两个特定时间触发的回调函数。只不过这两个回调函数有着稍微特殊的传值方式,以及用了一门新的编程语言。这篇主要介绍的就是着色器语言基础语法。
特点
Open GL ES 着色器语言是一种高级的面向图形编辑语言,主要特性:
- OpenGL ES着色器语言是一种高级的过程语言,对图形计算需要的数学工具支持的非常好。
- 对顶点着色器,片元着色器使用的是同样的语言,不做区分。
- 基于C/C++的语法及流程控制。
- 完美支持向量与矩阵的各种操作。
- 拥有大量的内置函数来提供丰富的功能。
着色器版本规范
OpenGL ES3.0顶点着色器和片段着色器的第1行总是声明着色器版本。声明着色器版本通知着色器编译器预期在着色器中出现的语法和结构。编译器按照声明的着色语言版本检查着色器语法。采用如下语法声明着色器使用OpenGLES着色语言3.00版本∶
#version 300 es
没有声明版本号的着色器被认定为使用OpenGL ES着色语言的1.00版本。着色语言的1.00版本用于OpenGL ES2.0。对于OpenGLES 3.0,规范的作者决定匹配API和着色语言的版本号,这就是版本号从1.00跳到3.00的原因。OpenGLES着色语言3.0增加了许多新功能,包括非方矩阵、全整数支持、插值限定符、统一变量块、布局限定符、新的内建函数、全循环、全分支支持以及无限的着色器指令长度。
数据类型
着色器作为一个强数据类型的编程语言有着丰富的数据类型。OpenGL ES虽然是基于C/C++语法的,但是还是有很大的不同。该语言不支持double,byte,short,long,也取消了C中的union,enum,unsigned以及位运算。
基本数据类型
在计算机图形中,两个基本数据类型组成了变换的基础∶向量和矩阵。这两种数据类型在OpenGL ES着色语言中也是核心。
GLSL中,向量可以看做是用同样类型的标量组成,标量分为bool,int,uint(无符号int)和float四种。每个向量可以由2个,3个或者4个相同的标量组成:
变量分类 | 说明 |
---|---|
标量 | float,int,uint,bool |
浮点向量 | float,vec2,vec3,vec4 |
整数向量 | int,ivec2,ivec3,ivec4 |
布尔向量 | bool,bvec2,bvec3,bvec |
矩阵 | mat2(或mat2x2),mat2x3,at2×4,mat3x2,mat3(或国mat3x3),mat3x4,mat4×2, mat4x3,mat4(或mat4×4) |
着色语言中的变量必须以某个类型声明。例如,下面的声明描述如何声明一个标量、一个向量和一个矩阵∶
float specularAtten; //A floating-point-based scalar
vec4 vPosition; //A floating-point-based 4-tuple vector
mat4 mViewProjection; //A 4 x 4 matrix variable declaration
ivec2 vOffset; //An integer-based 2-tuple vector
变量可以在声明时或者声明以后初始化。初始化通过使用构造器进行,构造器也用于类型转换。
变量构造器
OpenGL ES着色语言在类型转换方面有非常严格的规则。也就是说,变量只能赋值为相同类型的其他变量或者与相同类型的变量进行运算。在语言中不允许隐含类型转换的原因是,这样可以避免着色器作者遇到可能导致难以跟踪的缺陷的意外转换。为了应付类型转换,语言中有一些可用的构造器。你可以使用构造器初始化变量,并作为不同类型变量之间的转换手段。变量可以在声明(或者以后在着色器中)时使用构造器初始化。每种内建变量类型都有一组相关的构造器。
我们首先来看看如何使用构造器初始化和转换标量值。
float myFloat=1.0;
float myFloat2= 1;//ERROR:invalid type conversion
bool myBool= true;
int myInt= 0;
int myInt2= 0.0;//ERROR:invalid type conversion
myFloat = float(myBool);//Convert from bool->float
myFloat =float(myInt);// Convert from int ->float
myBool = bool(myInt); // Convert from int ->bool
类似地,构造器可以用于转换和初始化向量数据类型。向量构造器的参数将被转换为与被构造的向量相同的基本类型(foat、int或bool)。向量构造器的参数传递有两种基本方法∶
●如果只为向量构造器提供一个标量参数,则该值用于设置向量的所有值。
●如果提供了多个标量或者向量参数,则向量的值从左到右使用这些参数设置。如果提供了多个标量参数,那么在向量中必须有至少和参数中一样多的分量。
下面是构造向量的一些例子∶
vec4 myVec4 = vec4(1.0); // {1.0, 1.0, 1.0, .10}
vec3 myVec3 = vec3(1.0, 1.0, 1.0); // {1.0, 1.0, 1.0}
vec3 temp = vec3(myVec3);
vec2 myVec2 = vec2(myVec3); // {myVec3.x, myVec3.y}
myVec4 = vec4(myVec2, myVec3); // {myVec2.x, myVec2.y, myVec3.x, myVec3.y}
常量
可以将任何基本类型声明为常数变量。常数变量是着色器中不变的值。声明常量时,在声明中加入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++中那样,声明为const的变量是只读的,不能在源代码中修改。
向量
向量在着色器代码的开发中十分重要,可以很方便的存储以及操作颜色,位置,纹理坐标等。也可以单独访问向量中的某个分量,基本语法为< 向量名>.<分量名>。 å根据组成向量的分量数量,每个分量可以通过使用{x,y,z,w}、{r,g,b,a}或者{s,t,p,q}组合访问。3种不同命名方案的原因是向量可以互换地用于表示数学上的向量、颜色和纹理坐标。x、r、s分量总是引用向量的第一个分量。不同的命名约定只是为了方便。但是,不能在访问向量时混合使用命名约定(换言之,不能采用xgr这样的引用方法,因为一次只能使用一种命名约定)。使用"."运算符时,可以在操作中重新排列向量的分量,下面是一个例子。
-
将向量看做颜色时,可以使用r,g,b,a 4个分量名,分别代码红,绿,蓝,透明度
aColor.r = 0.6;
aColor.g = 0,8; -
将一个向量看做位置时。可以使用x,y,z,w等4个分量名,其分别代表x轴,y轴,z轴,向量的模
aPosition.x = 67.2;
aPosition.z = 34.5; 将一个向量看做纹理坐标时,可以使用s,t,p,q 4个分量名,其分别代表纹理坐标中的1维,2维,3维
矩阵
矩阵类型 | 说明 |
---|---|
mat2 | 2x2的浮点数矩阵 |
mat3 | 3x3的浮点数矩阵 |
mat4 | 4x4的浮点数矩阵 |
OpenGL ES着色器语言中,矩阵是按列顺序组织的,也就是一个矩阵可以看做由几个列向量组成。例如,mat3就可以看做由3个vec3组成。
对于矩阵的访问,可以将矩阵作为列向量的数组来访问。如; matrix 为一个mat4,可以使用matrix[2]取到该矩阵的第三列,其为一个vec4;也可以使用matrix[2][2]取得第三列的向量的第三个分量,其为一个float。
混合选择
通过运算符“.”可以进行混合选择操作,在运算符“.” 之后列出一个向量中需要的各个分量的名称,就可以选择并重新排列这些分量
vec4 color = vec4(0.7,0.1,0.5,1.0);
vec3 temp = color.agb;//相当于拿一个向量(1.0,0.1,0。5)赋值给temp
vec4 tempL = color.aabb; // 相当于拿了一个向量(1.0,1.0,0.5,0.5)赋值给tempL
vec3 tempLL;
tempLL.grb = color.aab; // 对向量tempLL的3个分量赋值
一次混合最多只能列出4个分量名称,且一次出现的各部分的分量名称必须是来自同一名称组。
内建函数
前一节描述了着色器作者如何创建一个函数。OpenGL ES着色语言中最强大的功能之一是该语言中提供的内建函数。下面是几句着色器示例代码∶
float nDotL = dot(normal,light);
float rDotV=dot(viewDir,(2.0* normal)* nDotL-light);
float specular = specularColor * pow(rDotV, specularPower);
如你所见,这个着色器代码块使用了内建函数dot计算两个向量的点积,用内建函数pow计算标量的幂次。这只是两个简单的例子,OpenGL ES着色语言有许多内建函数,可以处理通常在着色器中进行的各种计算任务。篇幅有限不展示所有内建函数,只需要记住常见的数学运算OpenGLES基本都有对应的内建函数。现在只是想让你知道OpenGLES着色语言中有许多内建函数。为了高效编写着色器,你必须熟悉最常见的内建函数。
内建变量
内建变量类似于内建函数,都是OpenGL ES环境提前帮你定义好的特殊变量,
变量 | 描述 |
---|---|
gl_VertexID | 输入变量,用于保存顶点的整数索引。highp 精度的整数变量 |
gl_InstanceID | 输入变量,用于保存实例化绘图调用中图元的实例编号。highp 精度的整数变量,通常情况下为 0 |
gl_Position | 输出变量,用于输出顶点作为的裁剪坐标。highp 精度的浮点变量 |
gl_PointSize | 用于指定点精灵的尺寸,单位为像素。highp 精度的浮点变量 |
gl_FrontFacing | 不由顶点着色器直接写入,而是根据顶点着色器生成的位置值和渲染的图元类型生成,它是一个布尔变量 |
最长用的就是gl_Position,代表了顶点的位置。
修饰符
修饰全局变量的关键字。
限定符 | 说明 |
---|---|
uniform | 一般用于对同一组顶点组成的单个3D物体中所有顶点都相同的量,如当前光源位置 |
in | 顶点/片元输入变量 |
out | 顶点/片元输出变量 |
layout | 布局限定符,为属性指定位置 |
const | 用于声明常量 |
上述内容基本上就是OpenGL ES3.0着色器语言语法和c语言区别比较大的地方,其他的类似结构体,数组,控制流语句基本都是和c一样的,在这不做过多赘述。
读完上面的内容之后大家肯定还是有很多不懂的地方,因为很多名词的解释语句中也有很多专业名词,说白了就是用一句看不懂的话解释一个看不懂的词,这种感觉就很容易劝退新手。大家不要慌,后面会结合实际代码再次穿插的复习这篇文章的一些关键知识点的。
复盘着色器
现在咱们在有了一定的基础知识之后,咱们来复盘一下上一篇文章中写的着色器。
顶点着色器:
#version 300 es
layout (location = 0) in vec4 av_Position;
void main() {
gl_Position = av_Position;
gl_PointSize = 10.0;
}
先来看第一行
#version 300 es
指定了着色器规范的版本是3.0
layout (location = 0) in vec4 av_Position;
这一行则是定义了一个输入类型的 四维float向量,并且限定了变量的内存布局位置为 0 。这一行设计到了上边3个知识点。
首先变量类型,vec4代表了av_Position是一个四个float数据构成的四维向量。然后in代表了这是一个输入型的变量,是可以被外界(Android环境)赋值的。然后就是比较绕嘴的内存布局限定符layout,它制定了这个变量的内存位置。大家翻看第一章的代码:
var avPosition:Int = GLES30.glGetAttribLocation(pointProgram, "av_Position")//寻找变量av_Position的位置索引
GLES30.glEnableVertexAttribArray(avPosition)//启用变量av_Position
GLES30.glVertexAttribPointer(avPosition, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer)//给变量av_Position赋值(这是顶点数组的赋值方式,后续会详细讲述)
GLES30.glDisableVertexAttribArray(avPosition)//关闭变量av_Position
给顶点着色器中被in修饰的变量(顶点属性)赋值的流程大致就是上面这四步,使用变量的内存索引位置做赋值的依据的。而layout的作用就是制定变量在内存中的索引位置。比如在上述代码中,由于顶点着色器已经使用了layout (location = 0) 来修饰av_Position,所以用glGetAttribLocation获取到的av_Position的内存索引值肯定是0,上边赋值的代码可以改成
GLES30.glEnableVertexAttribArray(0)//启用变量av_Position av_Position内存索引恒为0
GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer)//给变量av_Position赋值(这是顶点数组的赋值方式,后续会详细讲述)
GLES30.glDisableVertexAttribArray(0)//关闭变量av_Position
如果不用layout修饰的话,其实变量的索引值是会依次累加的,比如第一个变量索引为0,第二个是1,第三个是2.....
void main() {}
着色器的main方法
gl_Position = av_Position;
gl_PointSize = 10.0;
这两个语句则是给两个内建变量赋值,顶点位置是av_Position,画笔粗细是10。
再看片元着色器:
#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
fragColor =vec4(1.0,0.0,0.0,1.0);
}
和顶点着色器相同的就不说了,主要看一下不同的
precision mediump float;
这一句是指定着色器的精度限制,高精度可以带来更准确的图形绘制效果但是也会增加GPU的负担,相应的低精度会减少GPU计算量,但是对于高度复杂的模型可能会导致绘制的不准确。mediump代表中等精度。
out vec4 fragColor;
定义了一个 out( 输出类型)的四维向量,这个向量的作用是输出片元的颜色,作用类似顶点着色器的gl_PointSize(指定顶点的位置),但是不同的是gl_PointSize是内建变量,而片元着色器的颜色指定fragColor则是自己定义的一个输出变量。
fragColor =vec4(1.0,0.0,0.0,1.0);
给fragColor赋值,颜色使用的rgba规范,既红色。
好了这就是第二章的所有内容,讲述了着色器的基本语法,使用方式,以及复盘了两个示例着色器。下一章会详细介绍应用层(Android层)给着色器的两种最重要的传值方式——顶点数组、统一变量,以及绘制更多个点、绘制三角形和绘制不同颜色的点。