一、前期基础储备
笔者之前的四篇文综述了Android中使用OpenGL ES绘制基本图形和实现了简单的相机预览,初次接触OpenGL ES开发的读者可能对其中新的概念比较迷惑,尤其是其中的顶点着色器(Vertex Shader)和片元着色器(Fragment Shader),我们知道,在OpenGL中顶点着色器是针对每个顶点执行一次,用于确定顶点的位置。片元着色器是针对每个片元,片元可以理解为每个像素,用于确定每个片元(像素)的颜色或者纹理。如下是在相机预览中使用的两个着色器的代码段:
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"attribute vec2 inputTextureCoordinate;" +
"varying vec2 textureCoordinate;" +
"void main()" +
"{"+
"gl_Position = vPosition;"+
"textureCoordinate = inputTextureCoordinate;" +
"}";
private final String fragmentShaderCode =
"#extension GL_OES_EGL_image_external : require\n"+ //相机预览时的片元指令
"precision mediump float;" + //相机预览时的片元指令
"varying vec2 textureCoordinate;\n" +
"uniform samplerExternalOES s_texture;\n" + //相机预览时的片元指令
"void main() {" +
" gl_FragColor = texture2D( s_texture, textureCoordinate );\n" +
"}";
两段着色器中,使用的代码不是Java代码,语法也不是Java层面的,而是一种OpenGL特有的语法:GLSL语法
GLSL又叫OpenGL着色语言(OpenGL Shading Language),是用来在OpenGL中着色编程的语言,是一种面向过程的语言,基本的语法和C/C++基本相同,他们是在图形卡的GPU (Graphic Processor Unit图形处理单元)上执行的,代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程性。比如:视图转换、投影转换等。GLSL(GL Shading Language)的着色器代码分成2个部分:Vertex Shader(顶点着色器)和Fragment Shader(片元着色器)。
在前面的文章中,我们基本上使用的都是非常简单的着色器,基本上没有使用过GLSL的内置函数,但是在后面我们完成其他的功能的时候应该就会用到这些内置函数了。比如说相机的各种滤镜、各种静态、动态贴纸,其实现少不了特定片元着色器的支持。
在笔者的文章《Android使用GPUImage实现滤镜效果精炼详解(一)》中通过一些示例代码实现了调用GPUImage处理静态图片,为其设置各种滤镜效果,在GitHub上找到GPUImage的开源库打开之后,就可以发现,其实现各种滤镜效果底层依靠的也是OpenGL,每种不同滤镜效果,其片元着色器也是特定的,以黑白滤镜的片元着色器为例:
public static final String GRAYSCALE_FRAGMENT_SHADER = "" +
"precision highp float;\n" +
"\n" +
"varying vec2 textureCoordinate;\n" +
"\n" +
"uniform sampler2D inputImageTexture;\n" +
"\n" +
"const highp vec3 W = vec3(0.2125, 0.7154, 0.0721);\n" +
"\n" +
"void main()\n" +
"{\n" +
" lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);\n" +
" float luminance = dot(textureColor.rgb, W);\n" +
"\n" +
" gl_FragColor = vec4(vec3(luminance), textureColor.a);\n" +
"}";
看起来,一头雾水,实际上该代码段是由OpenGL的特有语言GLSL构成的,GLSL的变量申明、GLSL语言的内置函数、GLSL的各种修饰符…只要掌握了GLSL语法,那么后期构造片元着色器会十分的便利,笔者是做相机开发的,工作中创建新滤镜效果,离不开GPUImage,其原生的片元着色器一部分可以拿来直接使用,但是更多的都是要在其基础上做修改的,使之符合自己的滤镜需要,而在改造过程中,GLSL语法掌握就更为迫切了。
注:任何一种OpenGL程序本质上都可以被分为两部分:CPU端运行的部分,采用C++、Java之类的语言编写;以及GPU端运行的部分,使用GLSL语言编写。
二、上代码,具体实现
(1)基本数据类型
GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构体、数组、空类型七种类型:
1)标量
标量表示的是只有大小没有方向的量,在GLSL中标量只有bool、int和float三种。对于int,和C一样,可以写为十进制、八进制或者十六进制。对于标量的运算,我们最需要注意的是精度,防止溢出问题。
2)向量
向量我们可以看做是数组,在GLSL通常用于储存颜色、坐标等数据,针对维数,可分为二维、三维和四位向量。针对存储的标量类型,可以分为bool、int和float。共有vec2、vec3、vec4,ivec2、ivec3、ivec4、bvec2、bvec3和bvec4九种类型,数组代表维数、i表示int类型、b表示bool类型。需要注意的是,GLSL中的向量表示竖向量,所以与矩阵相乘进行变换时,矩阵在前,向量在后(与DirectX正好相反)。
作为颜色向量时,用rgba表示分量,就如同取数组的中具体数据的索引值。三维颜色向量就用rgb表示分量。比如对于颜色向量vec4 color,color[0]和color.r都表示color向量的第一个值,也就是红色的分量。
作为位置向量时,用xyzw表示分量,xyz分别表示xyz坐标,w表示向量的模。三维坐标向量为xyz表示分量,二维向量为xy表示分量。
作为纹理向量时,用stpq表示分量,三维用stp表示分量,二维用st表示分量。
3)矩阵
在GLSL中矩阵拥有22、33、4*4三种类型的矩阵,分别用mat2、mat3、mat4表示。我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值。
4)采样器
采样器是专门用来对纹理进行采样工作的,在GLSL中一般来说,一个采样器变量表示一副或者一套纹理贴图。所谓的纹理贴图可以理解为我们看到的物体上的皮肤。纹理查找需要指定哪个纹理或者纹理单元将指定查找。
sampler1D // 访问一个一维纹理
sampler2D // 访问一个二维纹理
sampler3D // 访问一个三维纹理
samplerCube // 访问一个立方贴图纹理
sampler1DShadow // 访问一个带对比的一维深度纹理
sampler2DShadow // 访问一个带对比的二维深度纹理
uniform sampler2D grass;
vcc2 coord = vec2(100, 100);
vec4 color = texture2D(grass, coord);
5)结构体
和C语言中的结构体相同,用struct来定义结构体,这是唯一的用户定义类型。
struct light
{
vec3 position;
vec3 color;
};
light ceiling_light;
6)数组
数组知识也和C中相同,不同的是数组声明时可以不指定大小,但是建议在不必要的情况下,还是指定大小的好。
// 创建一个10个元素的数组
vec4 points[10];
// 创建一个不指定大小的数组
vec4 points[];
points[2] = vec4(1.0); // points现在大小为3
points[7] = vec4(2.0); // points现在大小为8
7)空类型
空类型用void表示,仅用来声明不返回任何值得函数。
数据声明示例:
float a=1.5;
int b=2;
bool c=true;
vec2 d=vec2(1.1,2.2);
vec3 e=vec3(1.1,2.2,3.3)
vec4 f=vec4(vec3,1.1);
vec4 g=vec4(1.0); //相当于vec(1.0,1.0,1.0,1.0)
vec4 h=vec4(a,a,1.5,a);
mat2 i=mat2(1.1,1.5,2.2,4.4);
mat2 j=mat2(1.8); //相当于mat2(1.8,1.8,1.8,1.8)
mat3 k=mat3(e,e,1.2,1.4,1.6);
(2)运算符
GLSL中的运算符包括(越靠前,运算优先级越高):
索引:[ ]
前缀自加和自减:++,–-
一元非和逻辑非:~,!
加法和减法:+,-
等于和不等于:==,!=
逻辑异或:^^
三元运算符号,选择:? :
成员选择与混合:.
后缀自加和自减:++,–-
乘法和除法:*,/
关系运算符:>,<,=,>=,<=,<>
逻辑与:&&
逻辑或:||
赋值预算:=,+=,-=,*=,/=
(3)类型转换
GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如float a=1;就是一种错误的写法,必须严格的写成float a=1.0,也不可以强制转换,即float a=(float)1; 也是错误的写法,但是可以用内置函数来进行转换,如float a=float(1);还有float a=float(true);(true为1.0,false为0.0)等,值得注意的是,低精度的int不能转换为高精度的float。
(4)限定符
观察文首的两段着色器,可以看到对于变量使用了不同的修饰符,以下是GLSL修饰符的主要类型:
attritude:一般用于各个顶点各不相同的量。如顶点颜色、坐标等。
uniform:一般用于对于3D物体中所有顶点都相同的量。比如光源位置,统一变换矩阵等。
varying:表示易变量,一般用于顶点着色器传递到片元着色器的量。
const:常量。 限定符与java限定符类似,放在变量类型之前,并且只能用于全局变量。在GLSL中,没有默认限定符一说。
(5)流程控制
GLSL中的流程控制与C中基本相同,主要有条件判断和各种循环:
if( )、if( )else、if( )else if( )else
while()和dowhile( )
for( ; ; )
break和continue
(6)函数修饰符
GLSL中也可以定义函数,定义函数的方式也与C语言基本相同。函数的返回值可以是GLSL中的除了采样器的任意类型。对于GLSL中函数的参数,可以用参数用途修饰符来进行修饰,常用函数修饰符如下:
in:输入参数,无修饰符时默认为此修饰符。
out:输出参数。
inout:既可以作为输入参数,又可以作为输出参数。
(6)浮点精度
与顶点着色器不同的是,在片元着色器中使用浮点型时,必须指定浮点类型的精度,否则编译会报错。精度有三种,分别为:
lowp:低精度。8位。
mediump:中精度。10位。
highp:高精度。16位。
当然,也可以在片元着色器中设置默认精度,只需要在片元着色器最上面加上precision <精度> <类型>即可制定某种类型的默认精度。其他情况相同的话,精度越高,画质越好,使用的资源也越多。
(7)着色器代码结构
GLSL程序的结构和C语言差不多,main()方法表示入口函数,可以在其上定义函数和变量,在main中可以引用这些变量和函数。定义在函数体以外的叫做全局变量,定义在函数体内的叫做局部变量。与高级语言不同的是,变量和函数在使用前必须声明,不能在使用的后面声明变量或者函数。
三、GLSL内建变量和内置函数
在上面,我们了解了GLSL的基础语法:数据类型、运算符、控制流,接下来,我们要了解另外一些关键的内容:GLSL为方便调用,内置了一些变量和函数,在着色器构造中,少不了这些知识的运用。在上面glsl提供了非常丰富的函数库,供我们使用,这些功能都是非常有用且会经常用到的. 这些函数按功能区分大改可以分成7类:
(1)顶点着色器的内建变量
gl_Position:顶点坐标
gl_PointSize:点的大小,没有赋值则为默认值1,通常设置绘图为点绘制才有意义。
(2)片元着色器的内建变量
gl_FragCoord:当前片元相对窗口位置所处的坐标。
gl_FragFacing:bool型,表示是否为属于光栅化生成此片元的对应图元的正面。 输出变量:
gl_FragColor:当前片元颜色
gl_FragData:vec4类型的数组。向其写入的信息,供渲染管线的后继过程使用。
GLSL提供了非常丰富的函数库,供我们使用,这些功能都是非常有用而且经常会使用到的,这些函数按照功能区分大致可以分为7类:
(1)通用函数库
(2)三角函数&角度
(3)指数函数
(4)几何函数
(5)矩阵函数
(6)向量函数
(7)纹理查询函数
纹理采样函数中,3D在OpenGLES2.0并不是绝对支持。我们再次暂时不管3D纹理采样函数。重点只对texture2D函数进行说明。texture2D拥有三个参数,第一个参数表示纹理采样器。第二个参数表示纹理坐标,可以是二维、三维、或者四维。第三个参数加入后只能在片元着色器中调用,且只对采样器为mipmap类型纹理时有效。
在看完以上的OpenGL内建变量和内置函数之后,在返回前面,查看黑白滤镜的片元着色器,就会发现,各个变量的声明和使用都可以看得懂了,GPUImage中每种不同类型的滤镜实现都是采用了类似的方式,感兴趣的读者可以查看GPUImage的Android库:android-gpuimage
四、延伸知识,JSON语法
(1)JSON简介
JSON指的是JavaScript对象表示法(JavaScript Object Notation)
JSON是轻量级的文本交换格式,类似于XML,是纯文本
JSON独立于语言,具有层级结构(值中存在值)
JSON具有自我描述性,更易理解(人类可读)
JSON使用JavaScript语法来描述数据对象,但是JSON仍然独立于语言和平台。
(2)相比于XML的不同之处
JSON比XML更小、更快、更易解析
没有结束标签
更短,读写速度更快
使用数组
不使用保留字
(3)JSON语法规则
JSON语法是JavaScript对象表示语法的子集
数据在 名称/值 对中
数据由逗号隔开
花括号保存对象
方括号保存数组
(4)JSON 名称/值 对
JSON 数据的书写格式是:名称/值对。
名称/值对包括:字段名称(在双引号中),后面写一个冒号,然后是值:
“firstName” : “John”
(5)JSON值
JSON 值可以是:
数字(整数或浮点数)
字符串(在双引号中)
逻辑值(true 或 false)
数组(在方括号中)
对象(在花括号中)
Null
(6)JSON对象
JSON 对象在花括号中书写:对象可以包含多个名称/值对:
{ “firstName”:”John” , “lastName”:”Doe” }
(7)JSON 数组
JSON 数组在方括号中书写:数组可包含多个对象:
{
“employees”: [
{ “firstName”:”John” , “lastName”:”Doe” },
{ “firstName”:”Anna” , “lastName”:”Smith” },
{ “firstName”:”Peter” , “lastName”:”Jones” }
]
}
相关语法可以搜索 GLSL 中文手册 获取更多的语法知识