最近比较忙,还花时间写了几篇Xposed的文章,所以OpenGL 更新有点慢,不过还是要坚持呀~
上一篇文章《OpenGL从入门到放弃08》相机预览,这样讲就好理解了详细介绍了相机预览,并且封装了一个 Camera2BaseFilter
类,
主要是为了方便实现不同滤镜效果。
跟滤镜有关的内容还有滤镜预览、滤镜拍照、开滤镜录视频,这些都会讲,这篇文章主要学习一下滤镜预览、滤镜拍照,而滤镜录视频比较麻烦一些,所以后面单独写一篇吧~
如果各位有读完上一篇文章,这篇文章理解起来就很简单了。
一、滤镜预览
先增加各种不同滤镜对应的片元着色器,然后在 ShaderManager
中预加载着色器代码, ShaderManager
在 《OpenGL从入门到放弃07》滤镜,一定要看,不亏 这一篇文章里有说明了,本次就新增着色器,照猫画虎
先看下最终实现的滤镜预览效果,gif压缩了不清晰,效果还是可以看出来的,大家可以自己去实践一下~
1、黑白滤镜着色器
顶点着色器
camera2_vertex_shader_base.glsl
attribute vec4 aPosition;
uniform mat4 uMVPMatrix;
attribute vec4 aTexCoord;
varying vec2 vTextureCoord; //传给片元着色器
varying vec4 vPosition; //传给片元着色器
void main()
{
vTextureCoord = (uMVPMatrix * aTexCoord).xy;
gl_Position = aPosition;
vPosition = aPosition;
}
片元着色器
camera2_fragment_shader_gray.glsl
#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform samplerExternalOES uTextureSampler;
varying vec2 vTextureCoord;
void main()
{
vec4 vCameraColor = texture2D(uTextureSampler, vTextureCoord);
float fGrayColor = (0.3*vCameraColor.r + 0.59*vCameraColor.g + 0.11*vCameraColor.b);
gl_FragColor = vec4(fGrayColor, fGrayColor, fGrayColor, 1.0);
}
看到这里,是不是很熟悉
《OpenGL从入门到放弃07》滤镜,一定要看,不亏
这一篇文章介绍了好几种滤镜实现,都是关于图片的,这里相机预览同样适用,只不过纹理不一样了,相机预览,用到的纹理是扩展纹理samplerExternalOES
,替换一下,然后其它基本一样。
2、黑白滤镜实现类
public class Camera2FilterGray extends Camera2BaseFilter {
public Camera2FilterGray(Context context, int textureId) {
super(context, textureId);
}
@Override
protected ShaderManager.Param getProgram() {
return ShaderManager.getParam(ShaderManager.CAMERA_GRAY_SHADER);
}
}
黑白着色器的加载在ShaderManager
中
//黑白
insertParam(CAMERA_GRAY_SHADER, GLUtil.loadFromAssetsFile(context, "shader/camera2/camera2_vertex_shader_base.glsl")
, GLUtil.loadFromAssetsFile(context, "shader/camera2/camera2_fragment_shader_gray.glsl"));
添加一个滤镜就只需要以上步骤,然后在切换滤镜的点击事件中替换滤镜即可。
其它滤镜就不再讲了,如果前面文章都看懂,那么这个对你来说应该是so easy了。
二、拍照
这里思考一个问题:
相机返回的预览数据流其实我们并没有更改,只是在渲染到GLSurfaceView的时候我们增加滤镜显示,所以我们拍照保存肯定不能用Camera2的那套,而是要另辟蹊径。
onDrawFrame
这个方法是会一直调用的,每调用一次,就渲染一帧图片,所以我们就可以从onDrawFrame
这个方法入手,当点击拍照的时候,获取渲染的这一帧数据,然后保存到本地。同理录制视频就应该是不断的保存每一帧图片,这个后面会介绍。
2.1 如何获取纹理图片数据?
我们现在是要获取GLSurfaceView中渲染的纹理图片数据,然后保存到本地,
GLES20.glReadPixels(...)
上场
public static native void glReadPixels(
int x, //0 左下角的坐标
int y, //0
int width, //输出的宽度
int height, //输出的高度
int format, //GLES20.GL_RGBA
int type, //GLES20.GL_UNSIGNED_BYTE
java.nio.Buffer pixels //接收输出Buffer
);
这个方法可以获取渲染在GLSurfaceView中的图片数据,
各个参数的意思:
x, y:
图像的左下角的坐标,如果想截取整个opengl显示窗口,取0,0即可
width,height:
截屏范围
format:指的是opengl中像素存储的格式,一般有:
GL_ALPHA
GL_RGB
GL_RGBA
type:这个值要和format匹配上,
GL_UNSIGNED_BYTE,(0-255)
GL_UNSIGNED_SHORT_5_6_5,
GL_UNSIGNED_SHORT_4_4_4_4,
oGL_UNSIGNED_SHORT_5_5_5_1,
这个每一个通道的范围在0-2n次方的范围内
pixels:
接收输出Buffer
2.2 上代码
2.2.1 onDrawFrame
boolean mTakePicture = false;
public void takePicture() {
mTakePicture = true;
}
public void onDrawFrame(GL10 gl) {
...
mCamera2BaseFilter.draw(mTransformMatrix);
if (mTakePicture) {
doTakePicture();
}
}
在 onDrawFrame
中,用一个boolean
变量标识是否要拍照,在点击拍照的时候设置true,然后在doTakePicture
中设置为false
2.2.3 doTakePicture
//回调数据的宽,init 赋值
public int mFrameCallbackWidth;
public int mFrameCallbackHeight;
//用于存储拍照回调数据的buffer,大小是宽乘以高
private IntBuffer mOutPutIntBuffer;
private void doTakePicture() {
mTakePicture = false;
if (mOutPutIntBuffer == null) {
mOutPutIntBuffer = IntBuffer.allocate(mFrameCallbackWidth *
mFrameCallbackHeight);
}
//todo 这里可以用离屏渲染的方式,增加水印,后面再加
// mCamera2BaseFilter.draw(mTransformMatrix);
//通过这个api获取画的这一帧数据
GLES20.glReadPixels(0, 0, mFrameCallbackWidth, mFrameCallbackHeight,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, mOutPutIntBuffer);
//获取到图片数据了
int[] origin = mOutPutIntBuffer.array();
int[] result = new int[mFrameCallbackWidth * mFrameCallbackHeight];
//解决方向颠倒问题
for (int i = 0; i < mFrameCallbackHeight; i++) {
for (int j = 0; j < mFrameCallbackWidth; j++) {
int pix = origin[i * mFrameCallbackWidth + j];
int pb = (pix >> 16) & 0xff;
int pr = (pix << 16) & 0x00ff0000;
int pix1 = (pix & 0xff00ff00) | pr | pb;
result[(mFrameCallbackHeight - i - 1) * mFrameCallbackWidth + j] = pix1;
}
}
//帧数据回调, Activity中做保存图片操作
if (mCameraCallback != null) {
mCameraCallback.onFrameCallBack(result);
}
}
基本都注释清楚了,这里再整理一下这几个步骤:
- 初始化要输出的 IntBuffer mOutPutIntBuffer,大小是宽乘以高
- 调用
GLES20.glReadPixels
,传宽高和接收图片的 mOutPutIntBuffer - 得到mOutPutIntBuffer之后,这时候如果直接保存图片,会发现图片上下颠倒,所以要先解决图片上下颠倒问题(for循环自己去理解理解)
- 最终将数据回调到Activity中
2.2.4 onFrameCallBack
回调到Activity中,大概看一下
public void onFrameCallBack(final int[] bytes) {
final int width = mCameraV2Renderer.mFrameCallbackWidth;
final int height = mCameraV2Renderer.mFrameCallbackHeight;
Log.d(TAG, "onFrameCallBack: bitmap width = "+ width);
Log.d(TAG, "onFrameCallBack: bitmap height = "+ height);
new Thread(new Runnable() {
@Override
public void run() {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
IntBuffer byteBuffer = IntBuffer.wrap(bytes);
bitmap.copyPixelsFromBuffer(byteBuffer);
saveBitmap(bitmap);
bitmap.recycle();
}
}).start();
}
int数组转换成IntBuffer,然后通过Bitmap的copyPixelsFromBuffer方法,将数据拷贝到Bitmap中,然后就常规保存Bitmap即可
2.2.5 保存Bitmap
//图片保存
public void saveBitmap(Bitmap bitmap) {
String path = getSD() + "/OpenGLDemo/photo/";
File folder = new File(path);
if (!folder.exists() && !folder.mkdirs()) {
mHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(Camera2Demo_SurfaceView_Activity.this, "无法保存照片", Toast.LENGTH_SHORT).show();
}
});
return;
}
long dataTake = System.currentTimeMillis();
final String jpegName = path + "_" + dataTake + ".jpg";
try {
FileOutputStream fos = new FileOutputStream(jpegName);
BufferedOutputStream bos = new BufferedOutputStream(fos);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
bos.flush();
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
Log.w(TAG, "saveBitmap: 保存成功,耗时:" + (System.currentTimeMillis() - dataTake));
mHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(Camera2Demo_SurfaceView_Activity.this, "保存成功,路径:" + jpegName, Toast.LENGTH_SHORT).show();
}
});
}
protected String getSD() {
return Environment.getExternalStorageDirectory().getAbsolutePath();
}
6.0 动态权限就不说了,我提前申请的
接下来就是见证效果的时候了,点击拍照,打开图片
方向对了,不过图片不太对,右边黑了一块?
看下日志打印的大小
D/Camera2Demo_SurfaceView: onFrameCallBack: bitmap width = 1670
D/Camera2Demo_SurfaceView: onFrameCallBack: bitmap height = 1252
图片的大小:
这个是个bug,这个大小是Camer2获取到的大小,而实际上拍照应该是取GLSurfaceView的大小
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
glViewport(0, 0, width, height);
Log.i(TAG, "onSurfaceChanged: " + width + ", " + height);
mFrameCallbackWidth = width;
mFrameCallbackHeight = height;
}
在 onSurfaceChanged
中获取这个大小,然后重新拍照看下
嗯,这就对了,
最终几个滤镜拍照效果:
gif有点模糊,原图是很清晰的~
拍照到这里就结束了,后面还有拍照添加贴纸、拍视频等功能要讲,敬请期待吧~
源码学习
之前的文章
《OpenGL从入门到放弃01 》一些基本概念
《OpenGL从入门到放弃02 》GLSurfaceView和Renderer
《OpenGL从入门到放弃03 》相机和视图
《OpenGL从入门到放弃04 》画一个长方形
《OpenGL从入门到放弃05 》着色器语言
《OpenGL从入门到放弃06 》纹理图片显示
《OpenGL从入门到放弃07》滤镜,一定要看,不亏
《OpenGL从入门到放弃08》相机预览,这样讲就好理解了