Android OpenGL使用GLSurfaceView預覽視訊
前言
一年之前做過一些即時通訊視訊相關的工作,主要是做視訊渲染這一部分的工作,由於2016畢業來到了華為,華為對研究生的安排就是“哪裡需要去哪裡”,和你專業和擅長的沒有太大的關係,所以一直在適應當下的工作,現在基本上可以勝任現在的工作,可以抽出一些時間來總結一下之前瞭解過的OpenGL相關知識。
第一章 相關知識介紹
在介紹具體的功能之前,先對一些主要的類和方法進行一些介紹,這樣可以更好的理解整個程式
1.1 GLSurfaceView
在谷歌的官方文件中是這樣解釋GLSurfaceView的:
An implementation of SurfaceView that uses the dedicated surface for displaying OpenGL rendering.
大意是GLSurfaceView是一個繼承了SurfaceView類,它是專門用來顯示OpenGL的渲染。通俗的來說,GLSurfaceView可以用來顯示視訊、影象和3D模型等檢視,在接下來的章節中主要是使用它來顯示Camera視訊資料,大家可能會有一些問題,SurfaceView也可用來預覽Camera,那麼這兩者有什麼區別嗎?GLSurfaceView能夠真正做到讓Camera的資料和顯示分離,我們就可以在此基礎上對視訊資料做一些處理,例如美圖,增加特效等。
1.2 GLSurfaceView.Renderer
如果說GLSurfaceView是畫布,那麼僅僅有一張白紙是沒用的,我們還需要一支畫筆,Renderer的功能就是這裡說的畫筆。Renderer是一個介面,主要包含3個抽象的函式:onSurfaceCreated
onDrawFrame
、onSurfaceChanged
,從名字就可以看出,分別是在SurfaceView建立、檢視大小發生改變和繪製圖形時呼叫。
1.3 Camera
從Android 5.0開始(API Level 21),可以完全控制安卓設別相機的新API Camera2(android.hardware.Camera2)
被引進來了。雖然新的Camera2不管在功能上還是友好度上都強於舊的Camera,但是我們這裡還是使用的舊的Camera,由於新的Camera2暫時還沒有找到可以獲取視訊幀的介面,因為後面肯能會對Canmera視訊幀做一些處理,所以這裡暫時還是使用舊的Camera。
第二章 開始繪製
2.1 CameraGLSurfaceView
public class CameraGLSurfaceView extends GLSurfaceView implements Renderer, SurfaceTexture.OnFrameAvailableListener {
private Context mContext;
private SurfaceTexture mSurface;
private int mTextureID = -1;
private DirectDrawer mDirectDrawer;
public CameraGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
// 設定OpenGl ES的版本為2.0
setEGLContextClientVersion(2);
// 設定與當前GLSurfaceView繫結的Renderer
setRenderer(this);
// 設定渲染的模式
setRenderMode(RENDERMODE_WHEN_DIRTY);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// TODO Auto-generated method stub
LOG.logI("onSurfaceCreated...");
mTextureID = GlUtil.createTextureID();
mSurface = new SurfaceTexture(mTextureID);
mSurface.setOnFrameAvailableListener(this);
mDirectDrawer = new DirectDrawer(mTextureID);
CameraCapture.get().openBackCamera();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// TODO Auto-generated method stub
LOG.logI("onSurfaceChanged...");
// 設定OpenGL場景的大小,(0,0)表示視窗內部視口的左下角,(w,h)指定了視口的大小
GLES20.glViewport(0, 0, width, height);
if (!CameraCapture.get().isPreviewing()) {
CameraCapture.get().doStartPreview(mSurface);
}
}
@Override
public void onDrawFrame(GL10 gl) {
// TODO Auto-generated method stub
LOG.logI("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();
mDirectDrawer.draw();
}
@Override
public void onPause() {
// TODO Auto-generated method stub
super.onPause();
CameraCapture.get().doStopCamera();
}
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
// TODO Auto-generated method stub
LOG.logI("onFrameAvailable...");
this.requestRender();
}
}
這個類主要做了以下幾件事情:
- 實現Renderer這個介面,並且實現GLSurfaceView的初始化。在
CameraGLSurfaceView
的建構函式中設定了GLSurfaceView的版本:setEGLContextClientVersion(2)
,如果沒有這個設定,GLSurfaceView是什麼也繪製不出來的,因為Android支援OpenGL ES1.1、2.0以及3.+等版本,而且版本間的差別很大,不宣告版本號,GLSurfaceView是不知道使用哪個版本進行渲染;設定Renderer與當前的View繫結,然後再設定渲染的模式為RENDERMODE_WHEN_DIRTY
。渲染模式的設定也很關鍵,渲染模式有兩種:RENDERMODE_WHEN_DIRTY
和RENDERMODE_CONTINUOUSLY
。DIRYT的含義是隻有當被通知的時候才會去渲染檢視,而CONTINUOUSLY的含義是視訊會一直連續的渲染。 - 在
onSurfaceCreated()
函式中,建立一個渲染的紋理,這個紋理就是用來顯示Camera的影象,所以需要新建立的SurfaceTexture
繫結在一起,而SurfaceTexture
是作為渲染的載體,另一方面需要和DirectDrawer
繫結在一起,DirectDrawer
是用來繪製圖像的,下面會具體介紹。最後是初始化Camera。 - 因為在初始化的時候這是了渲染的模式為
RENDERMODE_WHEN_DIRTY
,所以我們就通知GLSurfaceView什麼時候需要渲染影象,而介面SurfaceTexture.OnFrameAvailableListener
完成這項工作,函式onFrameAvailable()
在有新資料到來時,會被呼叫,在其中呼叫requestRender(),就可以完成新資料的渲染。 - 在
onSurfaceChanged()
函式中,設定了OpenGL視窗的大小,(0,0)表示視窗內部視口的左下角,(w,h)指定了視口的大小;開啟Camera的預覽。 - 最後,在
onDrawFrame()
函式中繪製更新的紋理。
2.2 DirectDrawer
這個類非常重要,負責將SurfaceTexture(紋理的控制代碼)內容繪製到螢幕上。
public class DirectDrawer {
private FloatBuffer vertexBuffer, mTextureCoordsBuffer;
private ShortBuffer drawListBuffer;
private final int mProgram;
private int mPositionHandle;
private int mTextureCoordHandle;
private int mMVPMatrixHandle;
private short drawOrder[] = {0, 2, 1, 0, 3, 2}; // order to draw vertices
// number of coordinates per vertex in this array
private final int COORDS_PER_VERTEX = 2;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
private float mVertices[] = new float[8];
private float mTextureCoords[] = new float[8];
private float mTextHeightRatio = 0.1f;
private int texture;
public float[] mMVP = new float[16];
public void resetMatrix() {
mat4f_LoadOrtho(-1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, mMVP);
}
public DirectDrawer(int texture) {
String vertextShader = TextResourceReader.readTextFileFromResource(MyApplication.getContext()
, R.raw.video_vertex_shader);
String fragmentShader = TextResourceReader.readTextFileFromResource(MyApplication.getContext()
, R.raw.video_normal_fragment_shader);
mProgram = GlUtil.createProgram(vertextShader, fragmentShader);
if (mProgram == 0) {
throw new RuntimeException("Unable to create program");
}
//get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
GlUtil.checkLocation(mPositionHandle, "vPosition");
mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");
GlUtil.checkLocation(mTextureCoordHandle, "inputTextureCoordinate");
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
GlUtil.checkLocation(mMVPMatrixHandle, "uMVPMatrix");
this.texture = texture;
// initialize vertex byte buffer for shape coordinates
updateVertices();
setTexCoords();
// 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);
mat4f_LoadOrtho(-1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, mMVP);
}
public void draw() {
GLES20.glUseProgram(mProgram);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture);
// get handle to vertex shader's vPosition member
// 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);
GLES20.glEnableVertexAttribArray(mTextureCoordHandle);
GLES20.glVertexAttribPointer(mTextureCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, mTextureCoordsBuffer);
// Apply the projection and view transformation
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVP, 0);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
GLES20.glDisableVertexAttribArray(mTextureCoordHandle);
}
public static void mat4f_LoadOrtho(float left, float right, float bottom, float top, float near, float far, float[] mout) {
float r_l = right - left;
float t_b = top - bottom;
float f_n = far - near;
float tx = -(right + left) / (right - left);
float ty = -(top + bottom) / (top - bottom);
float tz = -(far + near) / (far - near);
mout[0] = 2.0f / r_l;
mout[1] = 0.0f;
mout[2] = 0.0f;
mout[3] = 0.0f;
mout[4] = 0.0f;
mout[5] = 2.0f / t_b;
mout[6] = 0.0f;
mout[7] = 0.0f;
mout[8] = 0.0f;
mout[9] = 0.0f;
mout[10] = -2.0f / f_n;
mout[11] = 0.0f;
mout[12] = tx;
mout[13] = ty;
mout[14] = tz;
mout[15] = 1.0f;
}
public void updateVertices() {
final float w = 1.0f;
final float h = 1.0f;
mVertices[0] = -w;
mVertices[1] = h;
mVertices[2] = -w;
mVertices[3] = -h;
mVertices[4] = w;
mVertices[5] = -h;
mVertices[6] = w;
mVertices[7] = h;
vertexBuffer = ByteBuffer.allocateDirect(mVertices.length * 4).order(ByteOrder.nativeOrder())
.asFloatBuffer().put(mVertices);
vertexBuffer.position(0);
}
public void setTexCoords() {
mTextureCoords[0] = 0;
mTextureCoords[1] = 1 - mTextHeightRatio;
mTextureCoords[2] = 1;
mTextureCoords[3] = 1 - mTextHeightRatio;
mTextureCoords[4] = 1;
mTextureCoords[5] = 0 + mTextHeightRatio;
mTextureCoords[6] = 0;
mTextureCoords[7] = 0 + mTextHeightRatio;
mTextureCoordsBuffer = ByteBuffer.allocateDirect(mTextureCoords.length * 4).order(ByteOrder.nativeOrder())
.asFloatBuffer().put(mTextureCoords);
mTextureCoordsBuffer.position(0);
}
}
這個類的主要功能就是繪製圖像。
(1) 定義Vertex Shader(頂點著色器,用來繪製圖形的形狀)、Fragment Shader(片段著色器,用來繪製圖形的顏色或者紋理)和Program(OpenGL ES物件,包含了用來繪製一個或者多個形狀的shader),然後接下來都是圍繞著這三個變數,最後通過呼叫OpenGL方法進行繪製。具體的過程可以參考前面的部落格
(2) 既然我們需要預覽Camera的視訊資料,那麼我們可以知道現實的區域的形狀大部分都是四邊形,但是在OpenGL中只有提供了繪製三角形的方法,我們就需要將兩個三角形拼接成一個正方形,所以需要定義一個大小為8的陣列,如下面程式碼所示:
static float squareCoords[] = {
-1.0f, 1.0f, // 左上點
-1.0f, -1.0f, // 左下點
1.0f, -1.0f, // 右下點
1.0f, 1.0f, // 有上點
};
此時,我們就有了一個四邊形的4個點的資料了。但是,OpenGL並不是對陣列的資料直接進行操作的,而是在直接記憶體中,即操作的資料需要儲存到NIO裡面的Buffer物件中。而我們上面生命的float[]物件儲存在陣列中,因此我們需要將float[]物件轉換為Java.nio.Buffer物件,程式碼如下:
public void updateVertices() {
final float w = 1.0f;
final float h = 1.0f;
mVertices[0] = -w;
mVertices[1] = h;
mVertices[2] = -w;
mVertices[3] = -h;
mVertices[4] = w;
mVertices[5] = -h;
mVertices[6] = w;
mVertices[7] = h;
vertexBuffer = ByteBuffer.allocateDirect(mVertices.length * 4).order(ByteOrder.nativeOrder())
.asFloatBuffer().put(mVertices);
vertexBuffer.position(0);
}
注意,ByteBuffer和FloatBuffer以及IntBuffer都是繼承自抽象類java.nio.Buffer。
另外,OpenGL在底層的實現是C語言,與Java預設的資料儲存位元組順序可能不同,即大端小端問題。因此,為了保險起見,在將資料傳遞給OpenGL之前,我們需要指明使用本機的儲存順序。
此時,我們順利地將float[]轉為了FloatBuffer,後面繪製三角形的時候,直接通過成員變數mTriangleBuffer即可。
(3) 最後就是將準備好的資料繪製到螢幕上,OpenGL 提供了兩個繪製的方法glDrawArrays(int mode, int first, int count)
和glDrawElements(int mode,int count, int type, Buffer indices)
兩個方法,在這裡我們使用的第二種繪製的方法,關於mode有幾種模式供我們選擇:
GL_POINTS
:繪製獨立的點到螢幕
GL_LINE_STRIP
:連續的連線,第n個頂點與第n-1個頂點繪製一條直線
GL_LINE_LOOP
:與上一個相同,但是需要首尾相聯接
GL_LINES
:形成對的獨立的線段
GL_TRIANGLE_STRIP
:繪製一系列的三角形,先是頂點v0,v1,v2,然後是v2,v1,v3(注意規律),然後v2,v3,v4等。該規律確保所有的三角形都以相同的方向繪製
GL_TRIANGLE_FAN
和GL_TRANGLE_STRIP
類似,但其縣繪製v0,v1,v2,再是v0,v2,v3,然後v0,v3,v4等。
(4) 需要注意的是,在這個類中,定義了mMVP這個陣列,這個陣列的功能是對視訊幀資料進行轉換的,例如旋轉影象等。
第三章 總結
到此為止,使用GLSurfaceView預覽Camera的介紹就完了,這篇文章,僅僅介紹了CameraGLSurfaceView
和DirectDrawer
這兩個類,但是如何對Camera進行操作的並沒有介紹,這不是本文的重點,所以就省略了。接下來還會介紹一些有關GLSurfaceView的文章。