1. OpenGL ES 基础
1.1. OpenGL 坐标
屏幕坐标系中横轴是x轴,纵轴是y轴,分别向右是正方向,向下是正方向.原点在左上角
数学坐标系中原点在左下角,向上是y轴正方向.
在OpenGL3D坐标系中,稍有不同,使用的是笛卡尔坐标系,表示空间坐标位置.和数学坐标系相似,左下角是原点,向右和向上是正值.还要添加第三个量就是z轴.z轴方向指向你.
下图是OpenGL ES笛卡尔坐标系.
实际上我们有几种坐标系和空间,在OpenGL中,每个空间位置会按照如下变换:
- 对象空间,相对于每个对象自身
- 相机,眼睛,空间是针对视点
- 投影,剪裁,空间是平面的屏幕或者是图像上的视口.
1.2. 视景体
也叫平截头体(frustum),是一个棱台,表示的是照相机能够捕捉到画面的一个区域.
下图为视景体
定义视景体非常简单通过定义一个几何体就可以了,空间中,任何平截头体内的物体都能够在屏幕上找到相应的位置,只要没有被其他物体覆盖.
Frustum也用于指定你的field-of-view(FOV),比如相机的广角与长焦镜头.角度越大看到的景象越多,成像就越小.
可以使用gl.glMatrixMode(GL_MODELVIEW)方法对模型视图矩阵进行平移和旋转操作.还需要定义操纵投影矩阵(projection matrix).
透视投影是我们观察世界的正常形势,物体越远越小,如果对frustum内的这种效果移除,我们就会得到正交投影,就是不论物体远近,他们的大小不发生改变,这主要用于机械制图等.
经常需要控制操纵的是哪个矩阵,可通过gl.glMatrixMode()实现,如果不知道操纵的是那个矩阵很容易出错.
投影类型:
- 透视投影:有深度,越远越小.
- 正投影:没有深度,相同大小.
矩阵模式,openGL是基于状态的,操纵很多矩阵,通过该函数指定使用哪个矩阵.
常用的矩阵有: - GL10.GL_PROJECTION:投影矩阵.
- GL10.GL_MODELVIEW:模型视图矩阵.
指定使用哪个矩阵之后,需要先加载单位矩阵(使用gl.loadIdentity()方法,类似于矩阵归零).
1.3. 定义视口
gl.glViewPort(...);需要注意的是,坐标从左下角开始.
1.4. 使用正交投影
gl.glOrtho(..)
//使用正交矩阵栈,需要重新设置坐标系,告诉OpenGL,以后所有变换都会影响该矩阵.
gl.glMatrixMode(GL_PROJECTION);
gl.glLoadIdentity();
1.5. 双缓冲
所有图形程序最重要的特性之一就是双缓冲.他允许在一个屏幕之外的缓冲区中执行绘制代码,然后使用一条交换命令把绘制完成的图形立即显示在屏幕上.
双缓冲用途两个:
一是一些复杂绘图时间较长,不希望在屏幕上看到每个步骤.双缓冲可以合成一幅图像完成后再显示.
用户不会看到不完整的图像.
二是用于动画.每个帧都在屏幕之外的缓冲区绘制,完成后快速交换到屏幕上.
1.6. OpenGL状态机
绘制3D图形是一项复杂任务.状态机是一个抽象的模型,表示一组状态变量的集合.每个状态变量可以有各种不同的值,打开关闭等.OpenGL绘图时,每次指定这些变量明显不合适.因此OpenGL给出了一中状态模型(状态机)来跟踪所有的OpenGL状态变量.一个状态变量被设置后,以后一直保持这个状态,直到下次对他进行修改.使用如下命令可进行状态变
量的切换.
gl.glEnable(xx);//gl.glDisable(xx);
//例如光照
gl.glEnable(GL_LIGHGTING);
2. 使用OpenGL编码步骤
2.1. 创建GLSurfaceView对象
写一个继承自GLSurfaceView的类.
2.2. 创建GLSurfaceView.renderer实现类.
实现GLSurfaceView.Renderer接口,创建渲染器.
2.3. 设置activity的contentView,以及设置view的render对象.
将2.1中的GLSurfaceView设置2.2创建的渲染器,并设置到ContentView中.
2.4. 实现Renderer类的过程
2.4.1. onSurfaceCreate()方法
设置清屏的颜色和启用顶点缓冲区
//设置清屏色
gl.glClearColor(0, 0, 0, 1);
//启用顶点缓冲区.
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
2.4.2. onSurfaceChanged()方法
- 设置viewport(视口) 输出画面的区域(从左下角开始)
gl.glViewport(0, 0, width, height); - 操纵投影矩阵,设置平截头体(比例通常和视口比例相同,否则输出画面会走样)
//矩阵模式,投影矩阵,openGL基于状态机
gl.glMatrixMode(GL10.GL_PROJECTION);
//加载单位矩阵
gl.glLoadIdentity();
//平截头体 zNear:近平面 zFar:远平面
gl.glFrustumf(-ratio, ratio, -1f, 1f, 3, 7);
2.4.3. onDrawFrame()方法:
- 清除颜色缓冲区
gl.glClear(GL10.GL_COLOR_BUFFER_BIT); - 操纵模型视图矩阵,设置眼球的参数
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();//加载单位矩阵
//eyeX, eyeY, eyeZ :放置眼球的坐标
//centerX, centerY, centerZ : 眼球的观察点
//upx, upy, upz : 指定眼球向上的向量(此处比较危险)
GLU.gluLookAt(gl, 0, 0, 5, 0, 0, 0, 0, 1, 0); - 定义图形顶点坐标值数组
float[] coords = {
0f,0.5f,0f,
-0.5f,-0.5f,0f,
0.5f,-0.5f,0f
}; - 将顶点坐标转换成缓冲区数据
//分配字节缓存区空间,存放顶点坐标数据(浮点数4个字节)
ByteBuffer ibb = ByteBuffer.allocateDirect(coords.length * 4);
//设置的顺序(本地顺序, 跟操作系统有关)
ibb.order(ByteOrder.nativeOrder());
//放置顶点坐标数组
FloatBuffer fbb = ibb.asFloatBuffer();
fbb.put(coords);
//定位指针的位置,从该位置开始读取顶点数据
ibb.position(0); - 设置绘图颜色
gl.glColor4f(1f, 0f, 0f, 1f); - 指定顶点缓冲区指针
//3:3维点,使用三个坐标值表示一个点
//type:每个点的数据类型
//stride:0,跨度.
//ibb:指定顶点缓冲区
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, ibb); - 绘图
//mode: 绘制类型
//first: 起始点
//count: 点的个数
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);
2.5. 封装通用的字节缓冲区转换方法
(持续更新该Utils类)
public class BufferUtil {
/**
* 将浮点数组转换成字节缓冲区
* @param arr 输入待转换的浮点数组
* @return 字节缓冲区
*/
public static ByteBuffer arr2ByteBuffer(float[] arr) {
//分配字节缓存区空间, 存放定点坐标数据
// 一个字节 8位 浮点数4个字节
ByteBuffer ibb = ByteBuffer.allocateDirect(arr.length * 4);
//设置顺序(本地顺序,跟操作系统有关)
ibb.order(ByteOrder.nativeOrder());
//放置顶点坐标数据
FloatBuffer fbb = ibb.asFloatBuffer();
fbb.put(arr);
//定位指针的位置, 从该位置开始读取顶点数据
ibb.position(0);
return ibb;
}
/**
* 将List集合转换成字节缓冲区
* @param list 输入待转换的浮点数组
* @return 字节缓冲区
*/
public static ByteBuffer list2ByteBuffer(List<Float> list) {
//分配字节缓存区空间, 存放定点坐标数据
// 一个字节 8位 浮点数4个字节
ByteBuffer ibb = ByteBuffer.allocateDirect(list.size() * 4);
//设置顺序(本地顺序,跟操作系统有关)
ibb.order(ByteOrder.nativeOrder());
//放置顶点坐标数据
FloatBuffer fbb = ibb.asFloatBuffer();
for (Float aFloat : list) {
fbb.put(aFloat);
}
//定位指针的位置, 从该位置开始读取顶点数据
ibb.position(0);
return ibb;
}
}
2.6. 渲染模式
OpenGL ES有两种渲染模式:
GLSurfaceView.RENDERMODE_CONTINUOUSLY: 持续渲染(默认)
GLSurfaceView.RENDERMODE_WHEN_DIRTY: 脏渲染, 命令渲染(需要使用myGLSurfaceView.requestRender();方法主动请求渲染)
2.7. 坐标系旋转
//angle: 旋转角度
//x,y,z: 沿着哪个轴旋转(向量) 迎面轴正方向看去, 顺时针: 负值 逆时针: 正值
gl.glRotatef(xrotate, 1, 0, 0);//绕x轴旋转
gl.glRotatef(yrotate, 0, 1, 0);//绕y轴旋转
2.8. 缓冲区
缓冲区分为:
- 顶点缓冲区(VertexBuffer)
- 颜色缓冲区(ColorBuffer)
使用方式:
gl.glColorPointer(xx);//指定颜色缓冲区
gl.glVertexPointer(xx);//指定顶点指针
注意:
不要忘记在onSurfaceCreated() 方法中启用两个缓冲区
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);//启用顶点缓冲区
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);//启动颜色缓冲区
3. 空间绘图
OpenGL ES支持三种绘图模式:点, 线和三角形.
3.1. 点
像素是计算机监视器上最小的元素.在彩色系统中,像素可以是许多可用颜色的一种.屏幕上的一个位置对应一个点,并为之指定颜色.然后,在此基础上,用最擅长的计算机语言生成直线,多边形,圆形以及其他图形,甚至整个GUI.
下图把可视区域看成是三维画布,可使用OpenGL命令和函数进行绘图.
画点:
gl.glDrawArray(GL_POINTS,..)
设置点大小:
gl.glPiontSize(xx);
示例(点变化大小的螺旋):
//计算点坐标
float r = 0.5f;//半径
// 旋转3圈
float x = 0f, y = 0f, z = 1f;
float zstep = 0.01f;//z的步长
float psize = 1.0f;//点的大小
float pstep = 0.5f;//点的步长
//循环绘制
for (float alpha = 0f; alpha < Math.PI * 6; alpha = (float) (alpha + Math.PI/32)) {
x = (float) (r * Math.cos(alpha));
y = (float) (r * Math.sin(alpha));
z = z - zstep;
//设置点的大小
gl.glPointSize(psize = psize + pstep);
//指定顶点指针
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, BufferUtil.arr2ByteBuffer(new float[]{x, y, z}));
//画数组
gl.glDrawArrays(GL10.GL_POINTS, 0, 1);
}
3.2. 线
画线分为三类:
- lines: 线集(线段集合)
- line_strip: 线带(首尾相接不闭环)
- line_loop: 线环(首尾相接闭环)
画线:
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, fbb);
gl.glDrawArrays(GL10.GL_LINES, 0, 4);
设置直线宽度:
gl.glLinewidth(xx);
示例(使用线带画螺旋线):
float r = 0.5f;//半径
List<Float> coordsList = new ArrayList<>();//坐标点集合
// 旋转3圈
float x = 0f, y = 0f, z = 1f;
float zstep = 0.01f;//步长
for (float alpha = 0f; alpha < Math.PI * 6; alpha = (float) (alpha + Math.PI/32)) {
x = (float) (r * Math.cos(alpha));
y = (float) (r * Math.sin(alpha));
z = z - zstep;
coordsList.add(x);
coordsList.add(y);
coordsList.add(z);
}
//指定顶点指针
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, BufferUtil.list2ByteBuffer(coordsList));
//画数组
gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, coordsList.size() / 3);
3.3. 三角形
使用画线方式绘制的图形的线框样式的,即都是直线无法进行
颜色填充,多边形是闭合形状.可以选择颜色进行填充.
有三种模式:
3.3.1. 环绕(GL_TRIANGLES)
注意连接顶点的箭头方向,第一个三角形的顺序是v0->v1->v2->v0,该顺序是有定点的指定次序决定的.该顺序为顺时针方向.第二个三角形也是一样.顶点的指定次序以及方向的组合成为环绕(winding).左图看成是顺时针环绕,如果将v4和v5两点互换,得到逆时针环绕.如右图:
3.3.2. 三角形带(GL_TRIANGLES_STRIP)
GL_TRIANLTE_STRIP,可绘制一串相邻的三角形,这些顶点不是按照他们指定的顺兴进行遍历的,这是为了保持每个三角形的环绕方向(逆时针).他的绘制模式是v0->v1->v2,接着v2->v1->v3
,然后是v2->v3->v4,依次类推.
3.3.3. 三角形扇(GL_TRIANGLES_FAN)
围绕一个中心点的相连三角形.
3.3.4. 示例(正方形)
float r = 0.5f;//半径
float[] coords = {
-r, r, 0,
-r, -r, 0,
r, r, 0,
r, -r, 0,
};
//指定顶点指针
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, BufferUtil.arr2ByteBuffer(coords));
//画数组
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, coords.length/ 3);
3.4 实心物体
3.4.1. 设置颜色
我们知道画点画线或者三角形都要指定顶点缓冲区,同样的也会颜色缓冲区.
需要注意的是, 顶点缓冲区中的点与颜色缓冲区中的点进行对应.
顶点的着色模式:
- smmoth: 平滑模式,也称渐变(默认)
- flat: 单调模式以三角形最后一个点的颜色值为准
设置着色模式:
gl.glShadeMode(GL_FLAT|GL_SMOOTH);//单调|平滑
注意: 三角形面设置的颜色, 跟最后一个顶点的颜色相同.
3.4.2. 深度测试
深度测试是一种有效地的用于隐藏表面消除的技巧,当一个像素绘制时,他将被设置一个值(称为z值),来表示他和观察者之间的距离.以后,当该位置需要绘制另一个像素时,新像素的z值和原先的值进行比较.如果z值越高,距离观察者越近,就位于以前像素的前面.反之位于后面.内部是通过深度缓冲区实现的.他存储了屏幕上每个像素的深度值.
启用深度测试:(
不要忘记在绘图开始时清除深度缓冲区
)gl.glClear(GL10.GL_DEPTH_BUFFER_BIT);//清除深度缓冲区
gl.glEnable(GL10.GL_DEPTH_TEST);//启用深度测试
3.4.3. 剔除
使用深度测试,性能上需要有开销,因为每次绘制像素都需要比较.如果知道那个面肯定不能绘制,就可以使用剔除这种技巧了.消除已经知道肯定不会绘制的几何图形.也不会向OpenGL驱动程序发送这个几何图形,性能能够显著提升.
剔除技巧之一就是背面剔除,消除一个表面的背面.
//启用表面剔除
gl.glEnable(GL10.GL_CULL_FACE);
//指定正面
//ccw: counter clock wise -> 逆时针(默认)
//cw: clock wise -> 顺时针
gl.glFrontFace(GL10.GL_CCW);
//剔除背面
//GL10.GL_FRONT:正面
//GL10.GL_BACK背面
gl.glCullFace(GL10.GL_BACK);
3.4.4. 使用剪刀进行裁剪
不在ViewPort整个视口进行渲染.
使用方式:
//启用剪裁测试
gl.glEnable(GL_SCISSOR_TEST);
//剪裁: x, y: 剪裁距离 width,height:屏幕宽高
gl.glScissor(160, 240, 160, 50);