概述
这是一个新的系列,学习OpengGl Es,其实是《OpenGl Es 应用开发实践指南 Android卷》的学习笔记,感兴趣的可以直接看这本书,当然这个会记录自己的理解,以下只作为笔记,以防以后忘记
之后会对本书的前九章依次分析记录
Android OpenGl Es 学习(一):创建一个OpenGl es程序
Android OpenGl Es 学习(二):定义顶点和着色器
Android OpenGl Es 学习(三):编译着色器
Android OpenGl Es 学习(四):增填颜色
Android OpenGl Es 学习(五):调整宽高比
Android OpenGl Es 学习(六):进入三维
Android OpenGl Es 学习(七):使用纹理
Android OpenGl Es 学习(八):构建简单物体
Android OpenGl Es 学习(九):增添触摸反馈
最终是要实现一个曲棍球的简单游戏,类似这样的
定义顶点
我们看到上面是最终实现的效果,首先我们先简单实现,一个矩形,一根线,俩个点
我们看到其实一个矩形是由4个顶点组成,然后把四个顶点连成线
点,直线,三角形
在OpenGl中只能绘制点,子线,三角形,也就是所我们不能直接绘制矩形,我们需要把矩形分解成若干三角形,然后在组成成矩形
如图所示,就把一个矩形划分为俩个三角形
在代码中定义顶点
在代码中这些顶点会用浮点数数组来表示,因为是二维坐标,所以每个顶点要用俩个浮点数来记录,一个标记x轴位置,一个标记y轴位置,这个数组通常被称为属性(attribute)数组
float[] tableVertices = {
//第一个三角
0f, 0f,
9f, 14f,
0,14f,
//第二个三角
0f,0f,
9f, 0f,
9f, 14f,
};
这个数组表示俩个三角形,每个三角形都以逆时针表示,一共四个顶点,俩个三角形共用俩个顶点
添加线和点
float[] tableVertices = {
//第一个三角
0f, 0f,
9f, 14f,
0, 14f,
//第二个三角
0f, 0f,
9f, 0f,
9f, 14f,
//线
0f, 7f,
9f, 7f,
//点
4.5f, 2f,
4.5f, 12f
};
让数据可以被opengl使用
上面我们已经定义好顶点了,但是我们的java代码是运行在虚拟机上,而opengl是运行在本地的硬件上的,那么如何才能把java数据可以让opengl使用呢?
- 第一种就是JNI,如果需要了解可以看我之前的博客
- 第二种就是改变内存的分配方式,java有一个特殊的类集合,可以分配本地的内存块,并且把java数据复制到本地内存
我们看下代码
private final int BYTES_PER_FLOAT = 4;
FloatBuffer verticeData = ByteBuffer.allocateDirect(tableVertices.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(tableVertices);
verticeData.position(0);
每个浮点数有32位精度,而每个byte有8位精度,所以每个浮点数都占4个字节
方法 | 描述 |
---|---|
ByteBuffer.allocateDirect | 分配一块本地内存,分配大小由外部传入 |
order(ByteOrder.nativeOrder()) | 告诉缓冲区,按照本地字节序组织内容 |
asFloatBuffer() | 我们希望操作Float,调用这个方法会返回FloatBuffer |
put() | 填充数据 |
position() | 把数据下标移动到指定位置 |
opengl管道
现在opengl已经拥有了数据,在把矩形画到屏幕之前,他们还需要在opengl的管道(pipeline)中传递,这一步就需要使用着色器(shader),这些着色器会告诉图形处理单元(CPU)如何绘制数据,有俩种着色器我们需要定义
- 顶点着色器(vertex shader):生成每个顶点的最终位置,针对每个顶点他都会执行一次,一旦位置确定,opengl就可以把这些顶点组装成点,线和三角形
- 片段着色器(fragment shader):为组成点,线,三角形的每个片段生成最终的颜色,针对每个片段他都会执行一次,一个片段是一个小的,单一颜色的长方形区域,类似计算机屏幕上的一个像素
一旦最终的颜色生成后,opengl会把他们写到一块称为帧缓冲区(frame buffer)的内存块中,然后Android会把这个帧缓冲区显示到屏幕上
光栅化技术
移动设备的显示屏由成千上百万个独立小组件组成,他们被称为像素,这些像素中每一个都有能力显示几百万颜色中的一种,其实每个像素都是由三个独立的子组件组成,他们发出红色,蓝色,绿色的光,因为每个像素很小,人的眼睛就会把,红绿蓝管混合,从而创造出巨量的颜色,只需要局狗多的像素就可以显示出你想要的画面
opengl通过光栅化的过程,把每个点,线,三角形,分解成大量的小片段,映射到移动设备的像素上,从而生成图像,这些小片段类似显示屏上的像素,每一个都包含单一的纯色,为了表示颜色,每个片段都有4个分量,红色,蓝色,绿色,来表示颜色,阿尔发(alpha)分量用来表示透明度
GLSL
要创建顶点着色器,首先要了解opengl的着色语言OpenGl Shader Language(GLSL)
,想要详学习换这个语言,可以参考这个OpenGL shader GLSL 中文手册
,下面记录一些基本信息
基本数据类型
类型 | 说明 |
---|---|
void | 空类型,不返回任何值 |
bool | 布尔类型true,false |
int | 带符号的整数 signed integer |
float | 带符号的浮点数 |
vec2, vec3, vec4 | n维浮点数向量,包括2,3,4个元素的浮点型向量 |
bvec2, bvec3, bvec4 | n维布尔向量,包含2,3,4个元素的布尔向量 |
ivec2, ivec3, ivec4 | n维整形向量,包含2,3,4个元素的整形向量 |
mat2, mat3, mat4 | 2✖️2,3✖️3,4✖️4,浮点数矩阵 |
sampler2D | 2D纹理 |
samplerCube | 立方体纹理 |
基本结构和数组
类型 | 描述 |
---|---|
结构 | struct type-name{} 类似c语言中的 结构体 |
数组 | float foo[3] glsl只支持1维数组,数组可以是结构体的成员 |
变量限定符
修饰符 | 描述 |
---|---|
none | (默认可省略)本地变量可读可写,函数的入参就是这种变量 |
const | 声明变量或函数的参数为只读类型 |
attribute | 只能存在vertex shader(顶点着色器),一般用于保存顶点或法线数据,可以在缓冲区中读取数据 |
uniform | 在运行时shader无法改变uniform变量,一版用来放置程序传递给shader的变换矩阵,材质,光照参数 |
varying | 主要负责vertex和fragment之间传递数据 |
参数限定符
函数的参数默认是以拷贝的形式传递的,也就是值传递,任何传递给函数的变量,其值都会拷贝一份,然后再交给函数内部进行处理,我们也可以添加限定符,来达到引用传递的效果
类型 | 描述 |
---|---|
默认 | 默认使用in限定符 |
in | 真正传入函数的是参数的一份拷贝,在函数内修改参数值,不会影响参数变量本身 |
out | 参数的值不会传递给函数,但是在函数内部修改值,会在函数结束后参数的值会改变 |
inout | 传入函数的参数是引用,函数内部修改值,参数也会改变 |
函数
GLSL允许在程序的最外部声明函数,函数不能嵌套,不能递归调用,且必须声明返回值类型(无返回值时返回void),在其他方面与c函数一样
vec4 getPosition(){
vec4 v4 = vec4(0.,0.,0.,1.);
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;
}
类型转换
GLSL可以使用构造函数进行显示类型转换
bool t= true;
bool f = false;
int a = int(t); //true转换为1或1.0
int a1 = int(f);//false转换为0或0.0
float b = float(t);
float b1 = float(f);
bool c = bool(0);//0或0.0转换为false
bool c1 = bool(1);//非0转换为true
bool d = bool(0.0);
bool d1 = bool(1.0);
精度限制
GLSL在进行光栅化的时候,会进行大量的浮点运算,这些运算可能是设备不能承受的,所以GLSL提供了三种浮点精度,可以根据不同的设备选择不同的精度
在变量前加highp mediump lowp
,即可对精度的声明
lowp float color;
varying mediump vec2 Coord;
lowp ivec2 foo(lowp mat3);
highp mat4 m;
除了精度限定符,我们还可以指定默认使用的精度,如果某个变量没有使用精度限定符,则会使用默认精度,默认精度限定符放在着色器代码初始位置,如:
precision highp float;//默认高精度float
precision mediump int;//默认中精度int
invariant关键字:
由于shader在编译的时候内部会做一些优化,可能导致同样的运算在不同的shader中结果不一致的问题,这会引起一些问题,尤其是在vertex shader向fragment shader传值的时候,所以我们需要使用invariant
关键字要求计算结果必须一致,除了这个,我们也可以用#pragma STDGL invariant(all)
命令来保证所有的输出一致,这样会限制编译器的优化程度,降低性能
#pragma STDGL invariant(all) //所有输出变量为 invariant
invariant varying texCoord; //varying在传递数据的时候声明为invariant
内置的特殊变量
GLSL使用一些特殊的内置变量与硬件进行沟通,他们大致分为俩种,一种是input类型,他们负责向硬件发送数据,另一种是output类型,负责向程序回传数据,方便编程需要
在vertex shader中
output的内置变量
变量 | 描述 |
---|---|
highp vec4 gl_Position; | gl_Position 放置顶点坐标信息 |
mediump float gl_PointSize; | gl_PointSize 绘制点的大小 |
在fragment shader中
input类型的内置变量
变量 | 描述 |
---|---|
mediump vec4 gl_FragCoord; | 片元在framebuffer画面的相对位置 |
bool gl_FrontFacing; | 标志当前图元是不是正面图元的一部分 |
mediump vec2 gl_PointCoord; | 经过插值计算后的纹理坐标,点的范围是0.0到1.0 |
output类型内置变量
变量 | 描述 |
---|---|
mediump vec4 gl_FragColor; | 片元在framebuffer画面的相对位置 |
mediump vec4 gl_FragData[n] | 设置当前片点的颜色,使用glDrawBuffers数据数组 |
内置函数
参考上面链接吧,这里就不继续写了
创建顶点着色器
首先我们在res
新建一个raw
文件夹,然后新建一个文件vertex_shader.glsl
,然后用GLSL书写代码
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
gl_PointSize=10.0;
}
我们定义的每一个顶点,顶点着色器都会被调用一次,当他被调用时,他会在gl_Position属性接受当前顶点的位置
main函数是着色器的入口,他需要做的就是把前面定义的位置,赋值到指定输出变量gl_Position,这个着色器给gl_Position赋值,opengl会把gl_Position储存的值当做顶点的最终位置,然后把这些顶点组装成点,线,三角形
创建片段着色器
我们新建一个文件fragment_shader.glsl
precision mediump float;
uniform vec4 u_Color;
void main() {
gl_FragColor = u_Color;
}
第一行设置精度
uniform
不像属性每个顶点都要设置一个,一个uniform
会让每个顶点都使用同一个值,除非我们改变他们
main
函数是着色器的入口,然后把uniform
定义的颜色赋值到特殊的输出变量gl_FragColor