在Android中使用OpenGL ES开发第(四)节:相机预览

笔者之前写了三篇Android中使用OpenGL ES入门级的文章,从OpenGL ES的相关概念出发,分析了利用OpenGL ES实现3D绘图的重要的两个步骤:定义形状和绘制形状,简单的绘制了一个三角形。
这里再简单回顾下:Android中使用OpenGL一共会涉及到四个类:

1)Activity——自不用说,Android界面展示的类;
2)GLSurfaceView——SurfaceView(API Level 1)的子类,View的孪生兄弟-SurfaceView专用于要求频繁刷新的界面中,比如相机预览界面;而GLSurfaceVIew(API Level 3)是继承于SurfaceView,且实现了SurfaceHolder.Callback2接口,专门用于OpenGL 中界面的展示;
3)GLSurfaceView.Renderer——这是一个接口,我们会自定义一个渲染器类来说实现这个接口。GLSurfaceView本身并不处理很多图像展示的相关任务,更多的实现是放在了该渲染器类中进行实现;该接口的实现会要求实现三个方法,每个方法会最终对应Camera的一种状态;
4)图像类——具体的类,具体实现看需求,比如笔者前面的文章中,该图像类就是Triangle,三角形类,关键的类,最终图像预览时呈现何种状态是在这个类中被定义的。

前期基础知识储备
SurfaceTexture了解
SurfaceTexture(API Level 11),掌握这个类是利用OpenGL实现相机预览的关键所在。等会看完代码,你就会发现,利用OpenGL画一个简单的形状和利用OpenGL实现相机预览两个看起来难度相差很大的任务实际上用到的OpenGL的代码却是十分相似的,唯一的区别就是在于相机预览时会用到SurfaceTexture进行纹理数据的处理。首先看下开发者文档中的描述:

Captures frames from an image stream as an OpenGL ES texture.
The image stream may come from either camera preview or video decode. A Surface created from a SurfaceTexture can be used as an output destination for the android.hardware.camera2, MediaCodec, MediaPlayer, and Allocation APIs. When updateTexImage() is called, the contents of the texture object specified when the SurfaceTexture was created are updated to contain the most recent image from the image stream. This may cause some frames of the stream to be skipped.

从文档中的描述,我们可以知道:SurfaceTexture的作用就是从Image Stream中捕获帧数据,用作OpenGL的纹理(纹理用于填充片元着色器,即“上色”),其中Image Stream来自相机预览或视频解码。 所以我们可以使用SurfaceTexture来和Camera进行连接(调用camera.setPreviewTexture(mSurfaceTexture)方法)从而获取相机传过来的预览图像流数据,而SurfaceTexture类获取到图像流数据之后并不会进行显示,而是对其处理后传给GLSurfaceView或者TextureView进行显示,因此SurfaceTexture和GLSurfaceView的搭配使用十分合适。

上代码,具体实现
1)自定义相机预览类,实现渲染器接口和SurfaceTexture的接口;

public class CameraGLSurfaceView extends GLSurfaceView implements Renderer, SurfaceTexture.OnFrameAvailableListener
1
2)写入详解预览类CameraGLSurfaceView的构造方法;

public CameraGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
mContext = context;
setEGLContextClientVersion(2);
setRenderer(this);
setRenderMode(RENDERMODE_WHEN_DIRTY);
}
看过笔者之前OpenGL文章的朋友理解这段代码是没有困难的,这里首先指定使用的OpenGL ES的版本为2.0,然后调用setRenderer()方法绑定适渲染器接口,最后调用setRendererMode()的方法指定模式为RENDERMODE_WHEN_DIRTY-即有新数据时进行刷新,否则等待。

3)关键-实现渲染器接口的三个方法

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// TODO Auto-generated method stub
Log.i(TAG, "onSurfaceCreated...");
mTextureID = createTextureID();
mSurface = new SurfaceTexture(mTextureID);
mSurface.setOnFrameAvailableListener(this);
mDirectDrawer = new DirectDrawer(mTextureID);
CameraInterface.getInstance().doOpenCamera(null); //①在创建surface时打开相机
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// TODO Auto-generated method stub
Log.i(TAG, "onSurfaceChanged...");
GLES20.glViewport(0, 0, width, height);
if(!CameraInterface.getInstance().isPreviewing()){
CameraInterface.getInstance().doStartPreview(mSurface, 1.33f); //②预览
}
}
@Override
public void onDrawFrame(GL10 gl) {
// TODO Auto-generated method stub
Log.i(TAG, "onDrawFrame...");
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
mSurface.updateTexImage(); //SurfaceTexture的关键方法
float[] mtx = new float[16];
mSurface.getTransformMatrix(mtx); //SurfaceTexture的关键方法
mDirectDrawer.draw(mtx); //③调用图形类的draw()方法
}
分析:这三个方法里面的代码非常关键,(1)onSurfaceCreated()方法中实例化SurfaceTexture对象mSurface和图形类对象mDirectDrawer,并且在这里调用了相机的开启方法doOpenCamera(),这是使用OpenGL实现相机预览时OpenGL代码对相机的第一次控制;
(2)onSurfaceChanged()方法中调用相机的doStartPreview()方法,这个方法调用在相机界面发生变化时,会关闭相机预览然后重新打开相机预览,即doStartPreview()方法内部实现实际是连续调用了mCamera.stopPreview(); mCamera.setPreviewDisplay(mHolder); mCamera.startPreview();三个方法;
(3)onDrawFrame()方法最为关键,首先是两行OpenGL的常规设置代码(glClearColor()、glClear()),用于设置背景;接着是连续调用了SurfaceTexture的两个关键的方法:①updateTexImage(), 当updateTexImage()方法被调用时,SurfaceTexture对象所关联的OpenGLES中纹理对象的内容将被更新为相机预览传出的图像流中最新的图片;②getTransformMatrix(),该方法也是必须调用的,用以转换已经发生变换的纹理矩阵的坐标;最后是调用了图像类的draw()方法用以绘制预览的界面。

4)剩余的代码段,辅助作用

@Override
public void onPause() {
// TODO Auto-generated method stub
super.onPause();
CameraInterface.getInstance().doStopCamera(); //④暂停预览时调用的相机方法
}
private int createTextureID()
{
int[] texture = new int[1];

GLES20.glGenTextures(1, texture, 0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MIN_FILTER,GL10.GL_LINEAR);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);

return texture[0];
}

public SurfaceTexture _getSurfaceTexture(){
return mSurface;
}

//该方法是实现SurfaceTexture.OnFrameAvailableListener接口时实现的方法
//用于提示新的数据流的到来
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
// TODO Auto-generated method stub
this.requestRender();
}

以上的代码就是自定义的相机预览类的中的关键代码,接下来,我们再看看另外的一个关键类——图像类
1)自定义一个图像类 DirectDrawer

public class DirectDrawer
1
2)相关变量的声明:顶点着色器的声明,片元着色器的声明,缓冲区变量的声明,程式的声明,顶点坐标的声明,纹理坐标的声明

private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"attribute vec2 inputTextureCoordinate;" +
"varying vec2 textureCoordinate;" + //传给片元着色器的变量
"void main()" +
"{"+
"gl_Position = vPosition;"+
"textureCoordinate = inputTextureCoordinate;" +
"}";

private final String fragmentShaderCode =
"#extension GL_OES_EGL_image_external : require\n"+ //固定指令
"precision mediump float;" + //固定指令
"varying vec2 textureCoordinate;\n" + //顶点着色器中传递过来的变量
"uniform samplerExternalOES s_texture;\n" + //固定指定
"void main() {" +
" gl_FragColor = texture2D( s_texture, textureCoordinate );\n" +
"}";

private FloatBuffer vertexBuffer, textureVerticesBuffer;
private ShortBuffer drawListBuffer;
private final int mProgram;
private int mPositionHandle;
private int mTextureCoordHandle;

private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; //绘制顶点的顺序

// number of coordinates per vertex in this array
private static final int COORDS_PER_VERTEX = 2;

private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

//顶点坐标-坐标值是固定的搭配,不同的顺序会出现不同的预览效果
static float squareCoords[] = {
-1.0f, 1.0f,
-1.0f, -1.0f,
1.0f, -1.0f,
1.0f, 1.0f,
};
//纹理坐标-相机预览时对片元着色器使用的是纹理texture而不是颜色color
static float textureVertices[] = {
0.0f, 1.0f,
1.0f, 1.0f,
1.0f, 0.0f,
0.0f, 0.0f,
};

2)图像类的构造方法-相机预览的图像类和画图时的图像类的构造方法极为相似

private int texture;

public DirectDrawer(int texture)
{
this.texture = texture;
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords); //第一个装载的数据是顶点坐标
vertexBuffer.position(0);

// initialize byte buffer for the draw list
ByteBuffer dlb = ByteBuffer.allocateDirect(drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder); //第二个装载的数据是绘制顶点的顺序
drawListBuffer.position(0);

ByteBuffer bb2 = ByteBuffer.allocateDirect(textureVertices.length * 4);
bb2.order(ByteOrder.nativeOrder());
textureVerticesBuffer = bb2.asFloatBuffer();
textureVerticesBuffer.put(textureVertices); //第三个装载的数据是纹理坐标
textureVerticesBuffer.position(0);

//解析之前变量中声明的两个着色器
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);

mProgram = GLES20.glCreateProgram(); // 创建空程式
GLES20.glAttachShader(mProgram, vertexShader); // 添加顶点着色器到程式中
GLES20.glAttachShader(mProgram, fragmentShader); // 添加片元着色器到程式中
GLES20.glLinkProgram(mProgram); // 链接程式
}
}

3)千呼万唤始出来-绘制方法draw()

public void draw(float[] mtx)
{
GLES20.glUseProgram(mProgram);

GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture);

// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);

// Prepare the <insert shape here> coordinate data
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);

//使用一次glEnableVertexAttribArray方法就要使用一次glVertexAttribPointer方法
mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");
GLES20.glEnableVertexAttribArray(mTextureCoordHandle);

GLES20.glVertexAttribPointer(mTextureCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, textureVerticesBuffer);

GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);

// 使用了glEnableVertexAttribArray方法就必须使用glDisableVertexAttribArray方法
GLES20.glDisableVertexAttribArray(mPositionHandle);
GLES20.glDisableVertexAttribArray(mTextureCoordHandle);
}
 
代码看上去有点复杂,但是结合笔者之前利用OpenGL实现画图的draw()方法的代码和整理使用OpenGL实现相机预览的draw()方法来看,就会发现这段代码看上去复杂,实际是几乎固定的代码,每个方法调用都是有规律可循。

程式相关方法
glUseProgram()
顶点着色器相关方法
glGetAttribLocation()
glEnableVertexAttribArray()
glVertexAttribPointer()
片元着色器相关方法
glGetUniformLocation()
glUniform4fv()
绘制相关方法
glDrawArrays-绘制方法
收尾相关方法
glDisableVertexAttribArray()

前面OpenGL相关文章的读者朋友会发现使用OpenGL画图和使用OpenGL实现相机预览时,图像类的变量声明、构造方法和绘制方法draw()都是十分的相似的,换言之,这里的使用OpenGL的代码几乎是可以看成是模板代码,而实现两种功能的不同就在于相机预览类中使用了SurfaceTexture进行相关数据传输和控制。

上一篇:codeforces 235 div2 A. Vanya and Cards


下一篇:(转)[SQL Server] 动态sql给变量赋值(或返回值给变量)