引自:http://www.jianshu.com/p/291ff6ddc164
做好的Demo截图
前言
讲了这么多,可能有人要问了,播放视频用个android封装的VideoView或者用MediaPlayer+SurfaceView来进行播放视频不就得了吗,干嘛还要整这么麻烦。OK,为了回答这个问题,我们先看看OpenGL ES干什么的,它是OpenGL三维图形API的子集,图形硬件的一种软件接口,针对手机、PDA和游戏主机等嵌入式设备而设计。我想如果是做游戏类开发的肯定对一些图形库不会陌生,其实很多游戏引擎内部都是封装的像OpenGL,DirectX 3D之类的图形库,然后开发者就可以通过这些图形库,来开发很多好玩的东西,如游戏,动画等等,那么我现在用Opengl es来绘制视频是不是也可以定制很多有意思的东西,比如开发一个左右分屏视频播放器,然后在虚拟现实(VR)头盔上来观看2d的视频,如果用opengl去绘制,那简直分分中搞定,因为这里,每一帧的视频在opengl 看来只是一张纹理贴图而已,那么我想把这个贴图贴在哪里就贴在哪里。总之,用opengl可以开发出很多有意思的二维,三维的图形应用出来。
正文
说了这么多,咱们开始吧,
/*** * 在这个类里面对视频纹理进行绘制工作,继承了 {@link TextureSurfaceRenderer}, * 并实现了{@link SurfaceTexture.OnFrameAvailableListener} */public class VideoTextureSurfaceRenderer extends TextureSurfaceRenderer implements SurfaceTexture.OnFrameAvailableListener{ public static final String TAG = VideoTextureSurfaceRenderer.class.getSimpleName(); /**绘制的区域尺寸*/ private static float squareSize = 1.0f; private static float squareCoords[] = { -squareSize, squareSize, 0.0f, // top left -squareSize, -squareSize, 0.0f, // bottom left squareSize, -squareSize, 0.0f, // bottom right squareSize, squareSize, 0.0f // top right }; /**绘制次序*/ private static short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; /** * 用来缓存纹理坐标,因为纹理都是要在后台被绘制好,然 * 后不断的替换最前面显示的纹理图像 */ private FloatBuffer textureBuffer; /**纹理坐标*/ private float textureCoords[] = { 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f }; /**生成的真实纹理数组*/ private int[] textures = new int[1]; /**着色器脚本程序的handle(句柄)*/ private int shaderProgram; /**squareCoords的的顶点缓存*/ private FloatBuffer vertexBuffer; /**绘制次序的缓存*/ private ShortBuffer drawOrderBuffer; /**矩阵来变换纹理坐标,(具体含义下面再解释)*/ private float[] videoTextureTransform; /**当前的视频帧是否可以得到*/ private boolean frameAvailable = false; private Context context; public VideoTextureSurfaceRenderer(Context context, SurfaceTexture texture, int width, int height) { super(texture, width, height); //先调用父类去做EGL初始化工作 this.context = context; videoTextureTransform = new float[16]; surfaceTexture = new SurfaceTexture(textures[0]); surfaceTexture.setOnFrameAvailableListener(this); //注册视频帧是否可得到的监听器 } // 代码略}
对上面的代码再稍稍解释一下吧,在绘制之前,我们首先选定一块区域来让图像就在这块区域来绘制,则有如下定义:
private static float squareSize = 1.0f; private static float squareCoords[] = { -squareSize, squareSize, 0.0f, // top left -squareSize, -squareSize, 0.0f, // bottom left squareSize, -squareSize, 0.0f, // bottom right squareSize, squareSize, 0.0f // top right };
上幅图来解释解释:
squareSize=1.0
时,square的面积就是整个手机屏幕,若squareSize=0.5f
则每个边长都为屏幕的一半。数组的坐标顺序为:左上->左下->右下->右上。 然后接着定义一个纹理坐标数组:
private float textureCoords[] = { 0.0f, 1.0f, 0.0f, 1.0f, //左上 0.0f, 0.0f, 0.0f, 1.0f, //左下 1.0f, 0.0f, 0.0f, 1.0f, //右下 1.0f, 1.0f, 0.0f, 1.0f //右上 };
接着上图:
然后则看看纹理的绘制次序drawOrder[] = {0,1,2, 0, 2, 3}
; 这又是个什么次序呢? 好,再来个图看看
OK,接下来再贴出省略的代码:
/** * 重写父类方法,初始化组件 */ @Override protected void initGLComponents() { setupVertexBuffer(); setupTexture(); loadShaders(); }/**** 设置顶点缓存*/ private void setupVertexBuffer() { /** Draw Order buffer*/ ByteBuffer orderByteBuffer = ByteBuffer.allocateDirect(drawOrder. length * 2); orderByteBuffer.order(ByteOrder.nativeOrder()); //Modifies this buffer's byte order drawOrderBuffer = orderByteBuffer.asShortBuffer(); //创建此缓冲区的视图,作为一个short缓冲区. drawOrderBuffer.put(drawOrder); drawOrderBuffer.position(0); //下一个要被读或写的元素的索引,从0 开始 // Initialize the texture holder ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4); bb.order(ByteOrder.nativeOrder()); vertexBuffer = bb.asFloatBuffer(); vertexBuffer.put(squareCoords); vertexBuffer.position(0); }ByteBuffer.allocateDirect(drawOrder. length * 2)表示的是直接从系统中分配大小为 (drawOrder. length * 2)的内存,2 代表一个short型占两个字节,(int占4个字节)。/**接着初始化纹理*/private void setupTexture() { ByteBuffer texturebb = ByteBuffer.allocateDirect(textureCoords.length * 4); texturebb.order(ByteOrder.nativeOrder()); textureBuffer = texturebb.asFloatBuffer(); textureBuffer.put(textureCoords); textureBuffer.position(0); // 启用纹理 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); //生成纹理对象textures(用于存储纹理数据) GLES20.glGenTextures(1, textures, 0); // 将绑定纹理(texuture[0]表示指针指向纹理数据的初始位置) GLES20.glBindTexture(GLES11Ext.GL_BLEND_EQUATION_RGB_OES, textures[0]); }关于glBindTexure(int target, int texture) 中的参数target,表示指定这是一张什么类型的纹理,在此是GLES11Ext.GL_BLEND_EQUATION_RGB_OES,也可以是常用的2D纹理如GLES20.GL_TEXTURE_2D等;第二个参数texture,在程序第一次使用这个参数时,这个函数会创建一个新的对象,并把这个对象分配给它,之后这个texture就成了一个活动的纹理对象。如果texture=0 则OpenGL就停止使用纹理对象,并返回到初始的默认纹理。/**加载顶点与片段着色器*/ private void loadShaders() { final String vertexShader = RawResourceReader.readTextFileFromRawResource(context, R.raw.vetext_sharder); final String fragmentShader = RawResourceReader.readTextFileFromRawResource(context, R.raw.fragment_sharder); final int vertexShaderHandle = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertexShader); final int fragmentShaderHandle = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader); shaderProgram = ShaderHelper.createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle, new String[]{"texture","vPosition","vTexCoordinate","textureTransform"}); }
关于可编程着色器在计算机图形领域本身就是个很大的主题,已超出了本文的范围。那么就稍稍解释下,OpenGL着色语言(OpenGL shading Language),它是一种编程语言,用于创建可编程的着色器。在opengl es 2.0以前使用的是一种“固定功能管线“,而2.0以后就是使用的着这钟可“编程的管线“,这种管线又分为顶点处理管线与片段处理管线(涉及到OpenGL的渲染管线的一些机制,大家自行的查吧)。
我现在就说说这个GLSL着色器是怎么使用的吧:有关使用GLSL创建着色器流程如下图(参照OpenGL 编程指南):attribute vec4 vPosition; //顶点着色器输入变量由attribute来声明attribute vec4 vTexCoordinate;//uniform表示一个变量的值由应用程序在着色器执行之前指定,//并且在图元处理过程中不会发生任何变化。mat4表示一个4x4矩阵uniform mat4 textureTransform; varying vec2 v_TexCoordinate; //片段着色器输入变量用arying来声明void main () { v_TexCoordinate = (textureTransform * vTexCoordinate).xy; gl_Position = vPosition;}
fragment_shader
/**使用GL_OES_EGL_image_external扩展处理,来增强GLSL*/#extension GL_OES_EGL_image_external : requireprecision mediump float;uniform samplerExternalOES texture; //定义扩展的的纹理取样器amplerExternalOESvarying vec2 v_TexCoordinate;void main () { vec4 color = texture2D(texture, v_TexCoordinate); gl_FragColor = color;}
着色器语言非常的类似C语言,也是从main函数开始执行的。其中的很多语法,变量等等,还是大家自行的查查,这不是几句能说明白的。着色器的创建及编译过程的代码都在项目里的一个util包下的三个工具类,RawRourceReader,ShaderHelper,TextureHelper类中,我就不再贴出来了,有兴趣大家可以fork或clone下来看看。
ok,终于初始化完了,太不容易了,冏。
好吧,绘制工作开始跑起来。@Override protected boolean draw() { synchronized (this) { if (frameAvailable) { surfaceTexture.updateTexImage(); surfaceTexture.getTransformMatrix(videoTextureTransform); frameAvailable = false; } else { return false; } } GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glViewport(0, 0, width, height); this.drawTexture(); return true; }
当视频的帧可得到时,调用surfaceTexture.updateTexImage()
方法,这个方法的官网解释如下:
surfaceTexture.getTransformMatrix(videoTextureTransform)
这又是什么意思呢?当对纹理用amplerExternalOES
采样器采样时,应该首先使用getTransformMatrix(float[])查询得到的矩阵来变换纹理坐标,每次调用updateTexImage()的时候,可能会导致变换矩阵发生变化,因此在纹理图像更新时需要重新查询,该矩阵将传统的2D OpenGL ES纹理坐标列向量(s,t,0,1),其中s,t∈[0,1],变换为纹理中对应的采样位置。该变换补偿了图像流中任何可能导致与传统OpenGL ES纹理有差异的属性。例如,从图像的左下角开始采样,可以通过使用查询得到的矩阵来变换列向量(0,0,0,1),而从右上角采样可以通过变换(1,1,0,1)来得到。 关于 GLES20.glViewport(int x, int y, int width, int height) ,在默认情况下,被设置为占据打开窗口的整个像素矩形,窗口大小和设置视口大小相同,所以为了选择一个更小的绘图区域,就可以用glViewport函数来实现这一变换,在窗口中定义一个像素矩形,
private void drawTexture() { // Draw texture GLES20.glUseProgram(shaderProgram); //绘制时使用着色程序 int textureParamHandle = GLES20.glGetUniformLocation(shaderProgram, "texture"); //返回一个于着色器程序中变量名为"texture"相关联的索引 int textureCoordinateHandle = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinate"); int positionHandle = GLES20.glGetAttribLocation(shaderProgram, "vPosition"); int textureTransformHandle = GLES20.glGetUniformLocation(shaderProgram, "textureTransform"); //在用VertexAttribArray前必须先激活它 GLES20.glEnableVertexAttribArray(positionHandle); //指定positionHandle的数据值可以在什么地方访问。 vertexBuffer在内部(NDK)是个指针,指向数组的第一组值的内存 GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer); GLES20.glBindTexture(GLES20.GL_TEXTURE0, textures[0]); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); //指定一个当前的textureParamHandle对象为一个全局的uniform 变量 GLES20.glUniform1i(textureParamHandle, 0); GLES20.glEnableVertexAttribArray(textureCoordinateHandle); GLES20.glVertexAttribPointer(textureCoordinateHandle, 4, GLES20.GL_FLOAT, false, 0, textureBuffer); GLES20.glUniformMatrix4fv(textureTransformHandle, 1, false, videoTextureTransform, 0); //GLES20.GL_TRIANGLES(以无数小三角行的模式)去绘制出这个纹理图像 GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawOrderBuffer); GLES20.glDisableVertexAttribArray(positionHandle); GLES20.glDisableVertexAttribArray(textureCoordinateHandle); }
好了所有代码就分析到这里了,前两篇链接, , 项目地址在