前言
随着VR/AR技术的普及,人机交互的模式将产生新的变革。OpenGL ES作为移动端上的图像渲染框架,将变得越来越重要。在此将学习OpenGL ES作为Q3的主要目标。在10月1日前,希望能有阶段性成果。
快速开始
判断设备是否支持OpenGL ES
代码语言:javascript复制 fun checkSupported() : Boolean{
var supportsEs2 = false;
val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
val configurationInfo = activityManager.getDeviceConfigurationInfo();
supportsEs2 = configurationInfo.reqGlEsVersion >= 0x2000;
val isEmulator = Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1
&& (Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86"));
supportsEs2 = supportsEs2 || isEmulator
return supportsEs2
}
生命周期
代码语言:javascript复制 override fun onPause() {
super.onPause()
glSurfaceView.let { glSurfaceView.onPause() }
}
override fun onResume() {
super.onResume()
glSurfaceView.let { glSurfaceView.onResume() }
}
用OpenGL渲染Activity
代码语言:javascript复制 lateinit var glSurfaceView: GLSurfaceView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (checkSupported()) {
glSurfaceView = GLSurfaceView(this);
glSurfaceView.let { glSurfaceView.setRenderer(GLRender2())
setContentView(glSurfaceView); }
} else {
Toast.makeText(this, "当前设备不支持OpenGL ES 2.0!", Toast.LENGTH_SHORT).show();
}
}
我们可以看到,OpenGL实际的渲染逻辑,全部封装在了我自己创建的GLRender2中。
以上代码,就是OpenGL渲染Activity最简单的外部框架。
渲染逻辑
代码语言:javascript复制public class GLRender2 implements GLSurfaceView.Renderer {
private float[] mTriangleArray = {
0f, 1f, 0f,
-1f, -1f, 0f,
1f, -1f, 0f
};
//三角形各顶点颜色(三个顶点)
private float[] mColor = new float[]{
1, 1, 0, 1,
0, 1, 1, 1,
1, 0, 1, 1
};
private FloatBuffer mTriangleBuffer;
private FloatBuffer mColorBuffer;
public GLRender2() {
Log.d("GLRender2" , "call GLRender init");
//点相关
//先初始化buffer,数组的长度*4,因为一个float占4个字节
ByteBuffer bb = ByteBuffer.allocateDirect(mTriangleArray.length * 4);
//以本机字节顺序来修改此缓冲区的字节顺序
bb.order(ByteOrder.nativeOrder());
mTriangleBuffer = bb.asFloatBuffer();
//将给定float[]数据从当前位置开始,依次写入此缓冲区
mTriangleBuffer.put(mTriangleArray);
//设置此缓冲区的位置。如果标记已定义并且大于新的位置,则要丢弃该标记。
mTriangleBuffer.position(0);
//颜色相关
ByteBuffer bb2 = ByteBuffer.allocateDirect(mColor.length * 4);
bb2.order(ByteOrder.nativeOrder());
mColorBuffer = bb2.asFloatBuffer();
mColorBuffer.put(mColor);
mColorBuffer.position(0);
}
@Override
public void onDrawFrame(GL10 gl) {
Log.d("GLRender2" , "call onDrawFrame");
// 清除屏幕和深度缓存
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
// 重置当前的模型观察矩阵
gl.glLoadIdentity();
// 允许设置顶点
//GL10.GL_VERTEX_ARRAY顶点数组
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// 允许设置颜色
//GL10.GL_COLOR_ARRAY颜色数组
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
//将三角形在z轴上移动
gl.glTranslatef(0f, 0.0f, -2.0f);
// 设置三角形
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mTriangleBuffer);
// 设置三角形颜色
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
// 绘制三角形
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);
// 取消颜色设置
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
// 取消顶点设置
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
//绘制结束
gl.glFinish();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.d("GLRender2" , "call onSurfaceChanged");
float ratio = (float) width / height;
// 设置OpenGL场景的大小,(0,0)表示窗口内部视口的左下角,(w,h)指定了视口的大小
gl.glViewport(0, 0, width, height);
// 设置投影矩阵
gl.glMatrixMode(GL10.GL_PROJECTION);
// 重置投影矩阵
gl.glLoadIdentity();
// 设置视口的大小
gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
//以下两句声明,以后所有的变换都是针对模型(即我们绘制的图形)
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.d("GLRender2" , "call onSurfaceCreated");
// 设置白色为清屏
gl.glClearColor(1, 1, 1, 1);
}
}
以上代码,渲染出一个变色的三角形:
详细介绍
GLRender2
这个平平无奇的三角形,它的渲染逻辑究竟是什么样的呢? 在此之前,我们需要先了解GLRender2是一个怎样的类。
GLRender2实现了GLSurfaceView.Renderer接口。需要实现三个方法:
代码语言:javascript复制 @Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.d("GLRender" , "call onSurfaceCreated");
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.d("GLRender" , "call onSurfaceChanged");
}
@Override
public void onDrawFrame(GL10 gl) {
Log.d("GLRender" , "call onDrawFrame");
}
生命周期
这是GLSurfaceView生命周期的三个环节。
onSurfaceCreated
onSurfaceCreated在GLRender2被初始化后首先调用。通常用于初始化伴随GLSurfaceView整个生命周期的数据和设置初始颜色。
onSurfaceChanged
onSurfaceChanged 当GLSurfaceView大小改变时,对应的Surface大小也会改变。值得注意的是,在Surface刚创建的时候,它的size其实是0,也就是说在画第一次图之前它也会被调用一次的。(而且对于很多时候,Surface的大小是不会改变的,那么此函数就只在创建之初被调用一次而已)
原型如下:
代码语言:javascript复制public abstract void onSurfaceChanged (GL10 gl, int width, int height)
另外值得注意的是,它告诉了我们这张纸有多高多宽。这点很重要。因为在onSurfaceCreated的时候我们是不知道纸的宽高的,所以有一些和长宽相关的初始化工作还得在此函数中来做。
onDrawFrame
以后会有两种模式供你选择:
- RENDERMODE_CONTINUOUSLY
- RENDERMODE_WHEN_DIRTY
第一种模式(RENDERMODE_CONTINUOUSLY): 连续不断的刷,画完一幅图马上又画下一幅。这种模式很明显是用来画动画的;
第二种模式(RENDERMODE_WHEN_DIRTY): 只有在需要重画的时候才画下一幅。这种模式就比较节约CPU和GPU一些,适合用来画不经常需要刷新的情况。多说一句,系统如何知道需要重画了呢?当然是你要告诉它…… 调用GLSurfaceView的requestRender ()方法,使其重绘。
GLSurfaceView的setRenderMode(int renderMode)方法。可以供你设置你需要的刷新模式。
设置背景色
代码语言:javascript复制// 设置白色为清屏
gl.glClearColor(1, 1, 1, 1);
设置场景大小
代码语言:javascript复制 // 设置OpenGL场景的大小,(0,0)表示窗口内部视口的左下角,(w,h)指定了视口的大小
gl.glViewport(0, 0, width, height);
设置投影矩阵
在渲染中,我们只绘制可见的东西。所以我们需要将真实物体转化到可见区域,即谓之投影矩阵。
代码语言:javascript复制// 设置投影矩阵
gl.glMatrixMode(GL10.GL_PROJECTION);
// 重置投影矩阵
gl.glLoadIdentity();
// 设置视口的大小
gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
这三句将真实物体映射到坐标系中。这个地方可能有点难以理解。
虽然在OpenGL中,我们画的是3D物体,但手机屏幕毕竟是一个平面。我们在生活中,看见的也只是一个平面。那么,一个3D物体,我们看到的应该是什么样的,取决于我们的投影矩阵如何设置。
假设,我们的三角形,三个点分别是:
代码语言:javascript复制 private float[] mTriangleArray = {
0f, 1f, -2f,
-1f, -1f, -2f,
1f, -1f, -2f
};
那么,这个三角形其实是在z轴为-2处的一个平面。我们用下图的方式,进行观察。
下图,近处的平面,距离视点为1,远处的为10。我们画的三角平面,就在距离视点2的位置。在距离视点1处,我们的视口大小是 2ratio x 2。到距离2处,我们的视口大小一定为2ratio x 2。
所以此时,我们渲染我们的三角形,它的高一定为画布高度的1/2。
如果我们将近平面,视点距离改为0.5f。同样的三角形,我们渲染出来高度一定为画布高度的1/4。
如果我们将三角形改为:
代码语言:javascript复制 private float[] mTriangleArray = {
0f, 1f, -1f,
-1f, -1f, -2f,
1f, -1f, -2f
};
视点距离改为1.0f 。三角形的高度将变为画布高度的3/4。
而远平面的视点距离,则决定了我们可以看到多远的元素。比如我们还是三角形为:
代码语言:javascript复制 private float[] mTriangleArray = {
0f, 1f, -1f,
-1f, -1f, -2f,
1f, -1f, -2f
};
将远平面视点距离改为1.5f,此时我们将只能看到上个例子中三角形的上半部分。
其中变化读者可以画立体图,慢慢感受一下。
回归模型
完成了对投影的操作后,我们将操作模式设置到模型操作。
代码语言:javascript复制 //以下两句声明,以后所有的变换都是针对模型(即我们绘制的图形)
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
完成了种种矩阵的设置后,我们可以开始进行绘制了。
图形和色彩数据
OpenGL并不是对堆里面的数据进行操作,而是在直接内存中(Direct Memory),即操作的数据需要保存到NIO里面的Buffer对象中。而我们上面声明的float[]对象保存在堆中,因此,需要我们将float[]对象转为java.nio.Buffer对象。
代码语言:javascript复制 private float[] mTriangleArray = {
0f, 1f, 1f,
-1f, -1f, 0f,
1f, -1f, 0f
};
//三角形各顶点颜色(三个顶点)
private float[] mColor = new float[]{
1, 1, 0, 1,
0, 1, 1, 1,
1, 0, 1, 1
};
private FloatBuffer mTriangleBuffer;
private FloatBuffer mColorBuffer;
public GLRender2() {
Log.d("GLRender2" , "call GLRender init");
//点相关
//先初始化buffer,数组的长度*4,因为一个float占4个字节
ByteBuffer bb = ByteBuffer.allocateDirect(mTriangleArray.length * 4);
//以本机字节顺序来修改此缓冲区的字节顺序
bb.order(ByteOrder.nativeOrder());
mTriangleBuffer = bb.asFloatBuffer();
//将给定float[]数据从当前位置开始,依次写入此缓冲区
mTriangleBuffer.put(mTriangleArray);
//设置此缓冲区的位置。如果标记已定义并且大于新的位置,则要丢弃该标记。
mTriangleBuffer.position(0);
//颜色相关
ByteBuffer bb2 = ByteBuffer.allocateDirect(mColor.length * 4);
bb2.order(ByteOrder.nativeOrder());
mColorBuffer = bb2.asFloatBuffer();
mColorBuffer.put(mColor);
mColorBuffer.position(0);
}
绘制
我们在onDrawFrame的生命周期中进行绘制。
代码语言:javascript复制 @Override
public void onDrawFrame(GL10 gl) {
Log.d("GLRender2" , "call onDrawFrame");
// 清除屏幕和深度缓存
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
// 重置当前的模型观察矩阵
gl.glLoadIdentity();
// 允许设置顶点
//GL10.GL_VERTEX_ARRAY顶点数组
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// 允许设置颜色
//GL10.GL_COLOR_ARRAY颜色数组
gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
//将画笔在z轴上移动
gl.glTranslatef(0f, 0.0f, -2.0f);
// 设置三角形
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mTriangleBuffer);
// 设置三角形颜色
gl.glColorPointer(4, GL10.GL_FLOAT, 0, mColorBuffer);
// 绘制三角形
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);
// 取消颜色设置
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
// 取消顶点设置
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
//绘制结束
gl.glFinish();
}
绘制的过程比较模式化,不再赘述。大致包含了:
- 清除缓存
- 启动顶点数组模式
- 启动颜色数组模式
- 移动画笔
- 设置图形
- 设置颜色
- 关闭顶点数组模式
- 关闭颜色数组模式
- 绘制结束
至此,便完成了一个平平无奇的三角形的绘制过程。
如有问题,欢迎指正。