前言
上一篇文章我们学习了如何在安卓平台搭建opengl es环境,如何通过TextureView加载一张图片。其实,通过前面的学习,那么关于安卓平台如何使用opengl es就掌握了一大部分了,剩下的就是性能等等余下的功能了;本篇文章的目标如下:
1、渲染一张图片之后再在图片上画一个对角线
2、把步骤一种的内容截屏保存到系统相册。
对于第二点,可能有人觉得比较有用,比如视频直播过程中,对某一时刻画面进行截屏。对,实现原理和思路其实是一样的,接下来我们逐一讲解如何实现。
opengl es系列文章
opengl es之-基础概念(一)
opengl es之-GLSL语言(二)
opengl es之-GLSL语言(三)
opengl es之-常用函数介绍(四)
opengl es之-SurfaceView、GLSurfaceView、TextureView环境搭建(十)
截屏实现思路
对于第一点我这里就不过多的讲解了,其实比较简单,这里主要讲一下第二点的实现思路。通过上一篇的学习我们知道,图片最终渲染到屏幕上主要经过了(这里以JPG为例)图片解码为Bitmap->bitmap上传到opengl es->调用glDrawArrays()绘制->调用EGLSurface的swapBuffers()函数,最终图片将呈现在屏幕上。截屏,就是将opengl es的渲染结果读取出来(一块RGBA的像素数据)生成Bitmap然后在编码为JPG的过程,那么这个动作放在哪个阶段呢?显然要在glDrawArrays()之后,swapBuffers()函数之前,下面是流程图
那截取如何实现呢?
其实很简单,关键函数就是glReadPixels(),这个函数的具体介绍已经在opengl es之-常用函数介绍(四)详细介绍了,这里就不多说了。
这个函数的功能就是从渲染结果中读取像素数据到指定的缓冲区,数据读取出来后就可以转换成Bitmap了,这就是具体思路,好,下面详细讲一下,关键代码。
图片上画对角线
讲截屏之前,先讲一下如何实现再图片上画一条对角线。我们知道,加载图片的时候需要一个顶点坐标,纹理坐标,着色器,最后调用glDrawArrays()将图片渲染出来,那画一条对角线呢?一样的思路,需要这些步骤,只不过画线不需要纹理坐标和在片元着色器中对纹理进行操作了,这里讲一下画线和画图片的不一样地方
1、顶点坐标
// 对角线的顶点坐标
private static final float verdata1[] = {
-1.0f,-1.0f,
1.0f,1.0f,
1.0f,-1.0f,
-1.0f,1.0f,
};
2、片元着色器。
顶点着色器和加载图片是一样的
// 片元着色器
private static final String fString = "uniform sampler2D texture;\n" +
" \n" +
" varying highp vec2 tex_coord;\n" +
" \n" +
" void main(){\n" +
" gl_FragColor = texture2D(texture,tex_coord);\n" +
" }";
opengl es的代码流程是一样的,这里我贴一下关键代码,这里就不具体贴出了,具体可以参考我的Demo示例查看。
private void onDraw() {
if (mBitmap == null) {
MLog.log("mBitmap nulll");
return;
}
int width = mSurface.getWidth();
int height = mSurface.getHeight();
MLog.log("width "+width + "height " + height);
GLES20.glViewport(0,0,width,height);
GLES20.glClearColor(1.0f,0,0,1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// 1、这里是加载图片纹理的代码
.......
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D,0,GLES20.GL_RGBA,mBitmap,GLES20.GL_UNSIGNED_BYTE,0);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);
// 接着画线
if (mAddLine) {
// 2、这里是画线的代码
.........
GLES20.glLineWidth(5.0f);
GLES20.glDrawArrays(GLES20.GL_LINES, 0, 4);
}
{
// 3、这里是截屏的代码
}
// 必须要有,否则渲染结果不会呈现到屏幕上
mSurface.swapBuffers();
}
可以看到,画线只需要调用glLineWidth()指定线宽然后再调用glDrawArrays()即可
截屏
上面的代码段我已经标出了截屏代码的位置,没错,就在哪里,那截屏的代码如何写呢?下面贴出具体实现代码
// 获取渲染结果,以bitmap形式返回
public Bitmap framebufferToBitmap() throws IOException {
if (!mEglContext.isCurrent(mEGLSurface)) {
throw new RuntimeException("Expected EGL context/surface is not current");
}
// glReadPixels fills in a "direct" ByteBuffer with what is essentially big-endian RGBA
// data (i.e. a byte of red, followed by a byte of green...). While the Bitmap
// constructor that takes an int[] wants little-endian ARGB (blue/red swapped), the
// Bitmap "copy pixels" method wants the same format GL provides.
//
// Ideally we'd have some way to re-use the ByteBuffer, especially if we're calling
// here often.
//
// Making this even more interesting is the upside-down nature of GL, which means
// our output will look upside down relative to what appears on screen if the
// typical GL conventions are used.
// 读取设置字节对齐
GLES20.glPixelStorei(GLES20.GL_PACK_ALIGNMENT,1);
int width = getWidth();
int height = getHeight();
ByteBuffer buf = ByteBuffer.allocateDirect(width * height * 4);
buf.order(ByteOrder.LITTLE_ENDIAN);
IntBuffer colobu = IntBuffer.allocate(1);
GLES20.glGetIntegerv(GLES20.GL_IMPLEMENTATION_COLOR_READ_FORMAT,colobu);
IntBuffer typebu = IntBuffer.allocate(1);
GLES20.glGetIntegerv(GLES20.GL_IMPLEMENTATION_COLOR_READ_TYPE,typebu);
MLog.log("类型 colobu " + colobu.get(0) + "typebu " + typebu.get(0));
GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);
MLog.log("要读取的长和宽 w="+width + " hei " + height);
/** 遇到问题:魅族 pro 7-s一直返回 0x502错误(GL_INVALID_OPERATION),该错误根据官方文档的解释是glReadPixels()函数的format和type和frame buffer
* 中像素的实际format、type不匹配造成的,返回错误之后buf得不到任何数据
* 分析:但实际上format和type是对应上的,而且buf也读取到了正确的像素数据,仍然返回该错误,不知道为何,有待进一步研究。
* */
int error = GLES20.glGetError();
if (error != GLES20.GL_NO_ERROR) {
String msg = "glReadPixels: glError 0x" + Integer.toHexString(error);
MLog.log(msg);
// throw new RuntimeException(msg);
}
buf.rewind();
android.graphics.Matrix matrix = new android.graphics.Matrix();
matrix.postRotate(180);
Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bmp.copyPixelsFromBuffer(buf);
// 创建新的图片
Bitmap resizedBitmap = Bitmap.createBitmap(bmp, 0, 0,
bmp.getWidth(), bmp.getHeight(), matrix, true);
return resizedBitmap;
}
1、首先GLES20.glPixelStorei(GLES20.GL_PACK_ALIGNMENT,1);设置读取像素数据时的字节对齐方式,这里表示按照一字节对齐,虽然性能低,但是安全。
2、接下来ByteBuffer buf = ByteBuffer.allocateDirect(width * height * 4);来分配一块内存(宽高4)大小,用来接收像素数据,注意,
3、接下来很关键了buf.order(ByteOrder.LITTLE_ENDIAN);将这块内存数据的端序转换为小端序,因为java端都是大端序,而opengl es中的数据是小端序,所以这里要进行转换
4、调用GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);从opengl es的渲染缓冲区中读取RGBA数据到这个buf中,
5、生成Bitmap
Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bmp.copyPixelsFromBuffer(buf);
6、前面的文章中我们知道opengl es的中的纹理坐标系和手机系统中的纹理坐标系是刚刚好180度颠倒的,所以这里我们生成Bitmap之后还要进行一个垂直选择180度的翻转,这样截取的图片才是对的,否则是倒立的(有兴趣的可以做个试验)
android.graphics.Matrix matrix = new android.graphics.Matrix();
matrix.postRotate(180);
// 创建新的图片
Bitmap resizedBitmap = Bitmap.createBitmap(bmp, 0, 0,
bmp.getWidth(), bmp.getHeight(), matrix, true);
以上就是截屏代码的详细讲解
项目地址
https://github.com/nldzsz/opengles-android
1、在图片上画对角线参考MySurfaceView类中的代码
2、截屏参考MySurfaceView类中的代码