本章介绍android-gpuimage实现方式,即通过在C++层实现YUV-RGB转换,通过OpenGL绘制,通过片段着色器运行Shader脚本实现图像处理,虽然将滤镜的一些处理交给GPU来执行,极大的减少了速度,但YUV-RGB过程却拖了后腿。本章将从YUV、GLSL与OpenGL开始,逐步探讨方案5。其中YUV-RGB过程上一章已有粗略探讨,本章不再赘述。
“OpenGL着色语言(OpenGL Shading Language)是用来在OpenGL中着色编程的语言,也即开发人员写的短小的自定义程序,他们是在图形卡的GPU (Graphic Processor Unit图形处理单元)上执行的,代替了固定的渲染管线的一部分,使渲染管线中不同层次具有可编程型。比如:视图转换、投影转换等。GLSL(GL Shading Language)的着色器代码分成2个部分:Vertex Shader(顶点着色器)和Fragment(片断着色器),有时还会有Geometry Shader(几何着色器)。负责运行顶点着色的是顶点着色器。它可以得到当前OpenGL 中的状态,GLSL内置变量进行传递。GLSL其使用C语言作为基础高阶着色语言,避免了使用汇编语言或硬件规格语言的复杂性。”
顶点着色器是一个可编程单元,执行顶点变换、纹理坐标变换、光照、材质等顶点的相关操作,每顶点执行一次。顶点着色器定义了在 2D 或者 3D 场景中几何图形是如何处理的。一个顶点指的是 2D 或者 3D 空间中的一个点。在图像处理中,有 4 个顶点:每一个顶点代表图像的一个角。顶点着色器设置顶点的位置,并且把位置和纹理坐标这样的参数发送到片段着色器。下面是GPUImage中一个顶点着色器:
attribute vec4 position;
attribute vec4 inputTextureCoordinate;
varying vec2 textureCoordinate;
void main()
{
gl_position = position;
textureCoordinate = inputTextureCoordinate.xy;
}
attribute是只能在顶点着色器中使用的变量,来表示一些顶点的数据,如:顶点坐标,法线,纹理坐标,顶点颜色等。varying变量是vertex和fragment shader之间做数据传递用的。一般vertex shader修改varying变量的值,然后fragment shader使用该varying变量的值。因此varying变量在vertex和fragment shader二者之间的声明必须是一致的。
attribute vec4 position;
position变量是我们在程序中传给Shader的顶点数据的位置,是一个矩阵,规定了图像4个点的位置,并且可以在shader中经过矩阵进行平移、旋转等再次变换。在GPUImage中,我们根据GLSurfaceView的大小、PreviewSize的大小实现计算出矩阵,通过glGetAttribLocation获取id,再通过glVertexAttribPointer将矩阵传入。新的顶点位置通过在顶点着色器中写入gl_Position传递到渲染管线的后继阶段继续处理。结合后面绘制过程中的glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);,首先选取第三个点,与前两个点绘制成一个三角形,再选取最后一个点,与第二、第三个点绘制成三角形,最终绘制成多边形区域。
attribute vec2 inputTextureCoordinate;
inputTextureCoordinate是纹理坐标,纹理坐标定义了图像的哪一部分将被映射到多边形。如图所示,下图是OpenGL纹理坐标系统,左下角为原点,
传入此坐标,代表输出图像不会经过变换,在GPUImage中,因为输出图像与应用方向关系,需要将图像旋转90度,即坐标为
<span style="font-size:10px;"> public static final float TEXTURE_ROTATED_90[] = {
1.0f, 1.0f,
1.0f, 0.0f,
0.0f, 1.0f,
0.0f, 0.0f,
};</span>
varying vec2 textureCoordinate
因为顶点着色器负责和片段着色器交流,所以我们需要创建一个变量和它共享相关的信息。在图像处理中,片段着色器需要的唯一相关信息就是顶点着色器现在正在处理哪个像素。
gl_Position = position;
gl_Position是用来传输投影坐标系内顶点坐标的内建变量,GPUImage在Java层已经变换过,在这里不需要经过任何变换。
textureCoordinate = inputTextureCoordinate.xy;
取出这个顶点中纹理坐标的 X 和 Y 的位置(仅需要这两个属性),然后赋值给一个将要和片段着色器通信的变量。到此,顶点着色器建立完毕。
片段着色器:
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
void main()
{
gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
}
片段着色器和顶点着色器会成对出现。片段着色器扮演着显示的角色。我们的滤镜处理大部分都在片段着色器中进行。上段代码是一个无滤镜效果的片段着色器。
varying highp vec2 textureCoordinate;
对应顶点着色器中变量名相同的变量,片段着色器作用在每一个像素上,我们需要一个方法来确定我们当前在分析哪一个像素/片段。它需要存储像素的 X 和 Y 坐标。我们接收到的是当前在顶点着色器被设置好的纹理坐标。
uniform sampler2D inputImageTexture;
uniforms变量(一致变量)用来将数据值从应用程其序传递到顶点着色器或者片元着色器。该变量有点类似C语言中的常量(const),即该变量的值不能被shader程序修改。sampler2D对应2D纹理,在GPUImage中,与onPreviewFrame中经过变换过的RGB数据绑定。GPU将从该纹理中取出点进行处理。
gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
这是我们碰到的第一个 GLSL 特有的方法:texture2D,顾名思义,创建一个 2D 的纹理。它采用我们之前声明过的属性作为参数来决定被处理的像素的颜色。这个颜色然后被设置给另外一个内建变量,gl_FragColor。因为片段着色器的唯一目的就是确定一个像素的颜色,gl_FragColor 本质上就是我们片段着色器的返回语句。一旦这个片段的颜色被设置,接下来片段着色器就不需要再做其他任何事情了,所以你在这之后写任何的语句,都不会被执行。
到此为止,我们的Shader就写完了。
在实际程序例如GPUImage中,操作顺序如下
1.创建shader
1)编写Vertex Shader和Fragment Shader源码。
2)创建两个shader 实例:GLuint glCreateShader(GLenum type);
3)给Shader实例指定源码。 glShaderSource
4)在线编译shaer源码 void glCompileShader(GLuint shader)
public static int loadShader(final String strSource, final int iType){
int[] compiled = new int[1];
int iShader = GLES20.glCreateShader(iType);
GLES20.glShaderSource(iShader, strSource);
GLES20.glCompileShader(iShader);
GLES20.glGetShaderiv(iShader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {
Log.d("Load Shader Failed", "Compilation\n" + GLES20.glGetShaderInfoLog(iShader));
return 0;
}
return iShader;
}
2.创建program
在OpenGL ES中,每个program对象有且仅有一个Vertex Shader对象和一个Fragment Shader对象连接到它。Shader类似于C编译器。Program类似于C链接器。glLinkProgram操作产生最后的可执行程序,它包含最后可以在硬件上执行的硬件指令。
1)创建program : GLuint glCreateProgram(void)
2)绑定shader到program : void glAttachShader(GLuint program, GLuint shader)。每个program必须绑定一个Vertex Shader 和一个Fragment Shader。
3)链接program : void glLinkProgram(GLuint program)
4)使用porgram : void glUseProgram(GLuint program)
public static int loadProgram(final String strVSource, final String strFSource){
int iVShader;
int iFShader;
int iProgId;
int[] link = new int[1];
iVShader = loadShader(strVSource, GLES20.GL_VERTEX_SHADER);
if (iVShader == 0) {
Log.d("Load Program", "Vertex Shader Failed");
return 0;
}
iFShader = loadShader(strFSource, GLES20.GL_FRAGMENT_SHADER);
if (iFShader == 0) {
Log.d("Load Program", "Fragment Shader Failed");
return 0;
}
iProgId = GLES20.glCreateProgram();
GLES20.glAttachShader(iProgId, iVShader);
GLES20.glAttachShader(iProgId, iFShader);
GLES20.glLinkProgram(iProgId);
GLES20.glGetProgramiv(iProgId, GLES20.GL_LINK_STATUS, link, 0);
if (link[0] <= 0) {
Log.d("Load Program", "Linking Failed");
return 0;
}
GLES20.glDeleteShader(iVShader);
GLES20.glDeleteShader(iFShader);
return iProgId;
}
3.获取纹理坐标、顶点坐标、纹理等对应id
通过glGetAttribLocation和glGetUniformLocation获取对应的id
mGLAttribPosition = GLES20.glGetAttribLocation(mGLProgId, "position");
mGLUniformTexture = GLES20.glGetUniformLocation(mGLProgId, "inputImageTexture");
mGLAttribTextureCoordinate = GLES20.glGetAttribLocation(mGLProgId,
"inputTextureCoordinate");
4.绘制
1)首先设置背景颜色和绘制创建绘制区域、清理当前缓冲区
2)使用program(glUseProgram),传递两个矩阵
3)通过glGenTextures(GLsizei n, GLuint *textures)产生你要操作的纹理对象的id,然后通过glBindTexture绑定并获取纹理id,告诉OpenGL下面对纹理的任何操作都是对它所绑定的纹理对象的,比如glBindTexture(GL_TEXTURE_2D,1)告诉OpenGL下面代码中对2D纹理的任何设置都是针对索引为1的纹理的。通过glTexParameteri设置一些属性。最后通过glTexImage2D根据指定参数,包括RGB数据,生成2D纹理。当第二帧绘制的时候,则不需要重新绑定纹理,使用glTexSubImage2D更新现有纹理即可。
public static int loadTexture(final IntBuffer data, final Size size, final int usedTexId){
int textures[] = new int[1];
if (usedTexId == NO_TEXTURE) {
GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, size.width, size.height,
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
} else {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, usedTexId);
GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, size.width,
size.height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
textures[0] = usedTexId;
}
return textures[0];
}
4)然后使用函数glActiveTexture()来指定要对其进行设置的纹理单元,这里为GL_TEXTURE0,使用glBindTexture再次绑定,通过glUniform1i复制,最后glDrawArrays绘制。
© 著作权归作者所有
前面有一篇探讨了如何在片段着色器中将YUV数据转换为RGB数据并显示,但采用samplerExternalOES将SurfaceTexture作为OpenGL外部纹理,需要使用GL_TEXTURE_EXTERNAL_OES作为纹理模板,通过SetPreviewTexture将Camera数据图像输出到SurfaceTexture,调用updateTexImage()时将对应纹理更新为最新的一帧,然后通知OpenGL绘制对应纹理。
1.创建纹理ID
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];
2.根据纹理id创建SurfaceTexture
mSurfaceTexture = new SurfaceTexture(textureId);
mSurfaceTexture.setOnFrameAvailableListener(listener);
private OnFrameAvailableListener listener = new OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
// TODO Auto-generated method stub
mGlSurfaceView.requestRender();
}
};
3.更改Shader
顶点着色器与之前相比多了个textureTransform,用于接收SurfaceTexture变换矩阵,如果不需要也可以不盖被vertext shader。
片段着色器需要增加#extension GL_OES_EGL_image_external : require,因为第一步的纹理都绑定到GL_TEXTURE_EXTERNAL_OES
对应采样方式由Samlpe2D更改成samplerExternalOES。
public static final String NO_FILTER_VERTEX_SHADER = "" +
"attribute vec4 position;\n" +
" attribute vec4 inputTextureCoordinate;\n" +
" \n" +
"uniform mat4 textureTransform;\n" +
"varying vec2 textureCoordinate;\n" +
" \n" +
" void main()\n" +
"{\n" +
"textureCoordinate = (textureTransform * inputTextureCoordinate).xy;\n" +
"gl_Position = position;\n" +
"}";
public static final String NO_FILTER_FRAGMENT_SHADER = "" +
"#extension GL_OES_EGL_image_external : require\n"+
"precision mediump float;" +
"varying vec2 textureCoordinate;\n" +
"uniform samplerExternalOES inputImageTexture;\n" +
" \n" +
"void main() {" +
" gl_FragColor = texture2D( inputImageTexture, textureCoordinate );\n" +
"}";
4.绘制
与之前采用glTexImage2D绑定2D图像纹理不同,这里通过GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureid);绑定外部纹理即可。
如果使用了SurfaceTexture变换矩阵,可采用glUniformMatrix4fv传递给Shader
采用片段着色器方案的时间消耗主要在数据转移中,本方法的格式相关工作交给EGLImage处理,用到了Lock ANativeWindow。具体优劣尚未探讨,如有经验人士希望指点迷津。
© 著作权归作者所有
文章例如该链接通过将YUV分成三个纹理,在shader中取出并且经过公式变换,转换成RGB。我尝试了下,显示的是灰色的,可能是这篇文章采用的是planar格式的YUV,与Android平台的packed格式的YUV不同,因此需要在纹理绑定处进行数据指针的修改。
之前在一篇13年北大硕士的论文基于android平台实时滤镜的设计与实现中提出了一种实现方法,采用双通道,将Y通道与UV通道分别贴图。网上也有单通道经过一些转换再转换的方法,欢迎讨论。
首先我们探讨下YUV格式
Android平台相机预览数据获取接口onPreviewFrame中默认获取的是YUV420sp格式,例如下图为8X4像素的YUV图像示意图
即首先将Y信号排列,然后UV数据分别交错排列。其中Y信号数组长度为width * height,UV信号长度为width * heght / 2,数组首元素位置起始于width * height。总长度为width * height * 1.5,相比于采用传统的rgb格式长度减少一半,因此常用语电视信号传输。
其中Y表示明亮度,也就是灰阶值。UV表示色度,是描述影响色彩及饱和度,用于指定像素颜色。因此,如果我们只使用Y通道,看到的就是原图的灰度图。
因为GPU并不会根据传入的纹理判断格式,所以我们可以将YUV数据作为RGB数据欺骗GPU,将Y通道与UV通道分成两个纹理传入shader,在shader中利用GPU的优势来进行快速转换。注意要使用两个不同的纹理单元,例如GL_TEXTURE0和GL_TEXTURE1,同样修改glUniform1i第二个参数
代码:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, id_y);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, data);
glUniform1i(gvImageTextureY, 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, id_uv);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, width/2, height/2, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, data + width*height);
glUniform1i(gvImageTextureUV, 1);
与直接使用RBGA数据不同,这里的参数采用的是GL_LUMINANCE,与GL_LUMINANCE_ALPHA,与GL_RGBA不同,GL_RGBA单独保存R、G、B、A四个数据,而GL_LUMINANCE将这四个数据合并成一个,因为这样1个Y就可以与1个RGBA对应。GL_LUMINANCE_ALPHA代表首先是亮度,然后是alpha值,这样我们就能将U值与V值分别取出。
之后通过shader将YUV格式转为RGB格式:
precision mediump float;
uniform sampler2D mGLUniformTexture;
uniform sampler2D mGLUniformTexture1;
varying highp vec2 textureCoordinate;
const mat3 yuv2rgb = mat3(
1, 0, 1.2802,
1, -0.214821, -0.380589,
1, 2.127982, 0
);
void main() {
vec3 yuv = vec3(
1.1643 * (texture2D(mGLUniformTexture, textureCoordinate).r - 0.0625),
texture2D(mGLUniformTexture1, textureCoordinate).a - 0.5,
texture2D(mGLUniformTexture1, textureCoordinate).r - 0.5
);\
vec3 rgb = yuv * yuv2rgb;
gl_FragColor = vec4(rgb, 1);
}
其中,texture2D(mGLUniformTexture, textureCoordinate).r即YUV中的Y数据,texture2D(mGLUniformTexture, textureCoordinate).a即YUV中的V数据,剩下一个就是U,经过矩阵公式转换后就是RGB数据,然后设置给gl_FragColor,OpenGL就可以正确的显示了其余部分基本不变,也不再赘述
© 著作权归作者所有
—————————————————————————————————————-
Android OpenGL开发实践 – GLSurfaceView对摄像头数据的再处理
随着移动网络的快速发展,移动端网络速度慢和花费较高的瓶颈逐渐消失,直播和视频随着网络的发展快速兴起。在直播和视频和风口之下,如何获取移动端摄像头数据、如何对摄像头数据进行再处理以及如何保存处理后的数据成为移动端视频开发者的必修课。本文首先对GLSurfaceView相关知识进行讲解,然后介绍Android系统如何获取摄像头数据并利用GLSurfaceView渲染到屏幕上,在此基础上以一个黑白滤镜为例介绍拿到摄像头数据后如何对数据进行再处理,并利用GLSurfaceView展示给用户。
GLSurfaceView简介
OpenGL ES是OpenGL的一个子集,它针对 移动端或嵌入式系统做了部分精简,而Android系统中集成了OpenGL ES,方便我们通过其接口充分使用GPU的计算和渲染能力。
GLSurfaceView是管理OpenGL surface的一个特殊的View,它可以帮助我们把OpenGL的surface渲染到Android的View上,并且封装了很多创建OpenGL环境所需要的配置,使我们能够更方便地使用OpenGL。其实使用GLSurfaceView非常简单,只要实现GLSurfaceView.Renderer
接口就好了,然后通过GLSurfaceView.setRenderer(GLSurfaceView.Render renderer)
方法把实现的接口传到GLSurfaceView即可。我们来看一个最简单的实现:

运行OpenGL程序需要创建OpenGL Context,即EGL Context,而GLSurfaceView的伟大之处就在于它为我们创建了一个OpenGL的渲染线程,此线程中已经包含了OpenGL 运行所需的上下文环境,GLSurfaceView.Renderer
的三个回调方法就运行在OpenGL环境中,省去了复杂和冗长的OpenGL上下文环境的创建过程。下面我们来看看这三个回调:
public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
此方法会在Surface第一次创建或重建时调用。在GLSurfaceView attatch到父View的后,此方法会被调用。从这个回调方法名我们可以大概了解这个方法的用处,即在OpenGL surface被创建时的回调。
public void onSurfaceChanged(GL10 glUnused, int width, int height)
此方法在Surface大小变化时调用,例如横屏转为竖屏、GLSurfaceView大小变化等。在Surface第一次创建时也会调用。
public void onDrawFrame(GL10 glUnused)
此方法在渲染一帧图像时调用。在任意时间调用GLSurfaceView的requestRender()
方法后,GLSurfaceView会优先执行已保存在GL线程队列中的Runnable,然后调用此onDrawFrame(GL10 glUnused)
方法渲染图像。GL线程队列中的所有Runnable和onDrawFrame
方法的调用都执行在GL线程中。
另外,对于上面接口的调用时机,其实有两种方式可以触发onDrawFrame
的调用。GLSurfaceView有接口GLSurfaceView.setRenderMode(int renderMode)
可以设置是连续渲染还是按需渲染。两种模式分别对应下面两个变量:
GLSurfaceView.RENDERMODE_WHEN_DIRTY GLSufaceView.RENDERMODE_CONTINUOUSLY
按需渲染就是前面提到的,在用户调用GLSurfaceView.requestRender()
方法时才会调用onDrawFrame
刷新渲染;连续渲染则不依赖于用户调用,GL线程会每隔一段时间自动刷新渲染。连续渲染消耗GPU资源更多,对本文将要讨论的对摄像头数据的再处理,只需要在摄像头数据回调时再刷新渲染即可,所以本文中都将渲染模式设置为按需渲染。
总结一下,GLSurfaceView主要包括以下能力:
- 提供一个OpenGL的渲染线程,以防止渲染阻塞主线程。
- 提供连续渲染或按需渲染能力。
- 封装EGL相关资源和创建和释放,极大地简化了OpenGL与窗口系统接口的使用方式。
获取摄像头数据
获取摄像头数据有一般有两种方式,一种是为相机设置预览的SurfaceTexture,通过回调获得当前可用的摄像头纹理,另一种是为相机设置Camera.PreviewCallback
回调,通过回调拿到YUV数据。后一种情况下得到YUV数据格式默认为NV21,也可以通过parameter.setPreviewFormat(ImageFormat format)
来指定YUV数据格式。一般来说,NV21和YV12两种格式是所有Android机型都支持的,其他格式可能在不同机型上有兼容性问题。YUV数据格式不是本文关注的重点,在此不对其格式及兼容性作详细说明。
要对摄像头数据做再处理,首先要拿到摄像头数据。我们先来看看打开相机的最简单逻辑:

设置相机参数并打开相机的主要步骤有以下几点:
- 首先需要选择打开哪个摄像头。目前市面上的手机一般有前后两个摄像头,我们首先要确认打开哪个摄像头、找到相应的摄像头id,然后才能调用
Camera.open(int cameraId)
打开指定的摄像头。选取摄像头的代码如下所示: CameraInfo中包含两个const值:CAMERA_FACING_FRONT
和CAMERA_FACING_BACK
,分别标识前置和后置摄像头摄像头。本文中我们选择使用前置摄像头。

- 调用
Camera.open(int cameraId)
打开前面选择的前置摄像头。 - 选取相机预览分辨率。调用相机参数的
cp.getSuppoortedPreviewSizes()
方法获取摄像头支持的预览分辨率列表,然后从中选取一个适合的大小,调用cp.setPreviewSize(size.width, size.height)
设置相机预览分辨率参数。 - 调用
mCamera.setParameters(cp)
应用前面设置好的相机参数。
做过Android Camera开发的人都知道,一般来说,相机的预览(preview)数据流是要输出到一个可见的SurfaceView上的,然后通过Camera.PreviewCallback的public void onPreviewFrame(byte[] data, Camera camera)方法来获得图像帧数据的拷贝。这就存在一些问题,比如希望对每一帧图像数据进行一些处理后再显示到屏幕上,在Android3.0之前是没有办法做到的。或者说非要做的话也需要用一些小技巧,比如用其他控件把SurfaceView给挡住,但是这个显示原始相机图像流的SurfaceView其实是永远存在的,也就是说被挡住的SurfaceView依然在接收从相机传过来的图像数据,而且一直按照一定帧率去刷新是要消耗CPU的。如果一些参数设置的不恰当,后面隐藏的SurfaceView还有可能会露出来。另外从Camera.PreviewCallback拿到的数据如果需要处理也需要用OpenCV等库在CPU上处理,对每一帧都需要处理的实时相机流数据是很消耗CPU资源的,因此这些小技巧并不是好办法。SurfaceTexture是从Android3.0(API 11)加入的一个新类。这个类跟SurfaceView很像,可以从相机预览或者视频解码里面获取图像流。和SurfaceView不同的是,SurfaceTexture在接收图像流之后,不需要显示出来。这样就好办多了,我们可以用SurfaceTexture接收来自相机的图像数据流,然后从SurfaceTexture中取得图像帧的拷贝进行处理,处理完毕后再送给一个SurfaceView用于显示即可。
一般来说,在CPU上处理图片是比较慢的,现在使用最广泛的图片处理库OpenCV,即使在底层做了编译优化,要做到实时处理720P的图像数据还是吃不消,这时候就要发挥GPU的强大能力了。图像数据无非是一个个的像素点,对图像数据的处理无非是对每个像素点进行计算后重新赋值,一般来说对每个像素点的计算都比较独立,计算也相对简单。CPU虽然计算能力强大,但是并行处理能力有限,对一张720P的图片,一共包含720*1280=921600个像素,要进行这么多次运算,CPU也要望洋兴叹了。GPU与CPU相比最大的优势就是并行处理能力,一般移动端的GPU也要包含数千个处理单元,这些处理单元虽然计算能力比不上CPU,但是却可以同时处理几千个像素点。像素点数据的计算相对简单,而且可以同时处理几千个像素点,图像数据用GPU来做计算就非常适合了。而怎么使用GPU呢?这就要介绍到目前使用最广泛的2D、3D矢量图形沉浸API:OpenGL了。
OpenGL是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。这个接口由近350个不同的函数调用组成,用来从简单的图形比特绘制复杂的三维景象。Android系统自带了OpenGL的嵌入式版本:OpenGL ES,相比完整的OpenGL版本接口要少了一些接口,但对一般移动端处理的需求来说足够了。熟悉OpenGL的编程规范,需要学习的东西很多,本文只讲解如何搭建OpenGL渲染相机数据流的过程,以及举例用一个简单的OpenGL的shader程序对相机数据做处理,就不详细讲解OpenGL的编程规范了,有兴趣的同学们可以自己上网搜搜相关教程。OpenGL是实时对摄像头数据做处理的核心,希望以后做这方面工作的同学确实需要好好了解和学习。
言归正传,继续我们的教程。打开摄像头以后,我们需要为相机设置一个预览的SurfaceTexture接收来自相机的图像数据流。SurfaceTexture和OpenGL ES一起使用可以创造出无限可能,下面我们先来看看如何创建一个OpenGL纹理并把它绑定到一个SurfaceTexture,然后将该SurfaceTexture设置为相机预览数据接收器:

经过以上打开相机和设置预览两步,相机就可以正常工作了,相机会源源不断地把摄像头帧数据更新到SurfaceTexture上,即更新到对应的OpenGL纹理上。但是此时我们并不知道相机数据帧何时会更新到SurfaceTexture,也没有在GLSurfaceView的OnDrawFrame方法中将更新后的纹理渲染到屏幕,所以并不能在屏幕上看到预览画面。下面我们先来看看相机如何通知SurfaceTexture其预览数据已更新。
设置SurfaceTexture回调,通知摄像头预览数据已更新
SurfaceTexture有一个很重要的回调:OnFrameAvailableListener。通过名字也可以看出该回调的调用时机,当相机有新的预览帧数据时,此回调会被调用。所以我们为前面的SurfaceTexture设置一个回调,来通知我们相机预览数据已更新:

SurfaceTexture的updateTexImage方法会更新接收到的预览数据到其绑定的OpenGL纹理中。该纹理会默认绑定到OpenGL Context的GL_TEXTURE_EXTERNAL_OES纹理目标对象中。GL_TEXTURE_EXTERNAL_OES是OpenGL中一个特殊的纹理目标对象,与GL_TEXTURE_2D是同级的,有兴趣的同学可以网上搜教程深入了解一下。调用此方法后,我们前面创建的OpenGL纹理中就有了最新的相机预览数据了。要注意的是,此方法只能在生成该纹理的OpenGL线程中调用,所以这个地方通过GLSurfaceView的queueEvent方法将该调用放入GL线程队列中执行。
SurfaceTexture的getTransformMatrix方法可以获取到图像数据流的坐标变换矩阵。一般情况下,相机流数据方向并不是用户正常拿手机的竖屏方向,且前后摄像头数据还存在镜像的问题。如何对摄像头数据进行旋转或镜像得到旋转正确的数据呢?getTransformMatrix获取到的变换矩阵可以帮助我们完成这个看起来很复杂的任务。其实我们不用关心这个矩阵的值到底是什么,只需要在OpenGL 着色器处理顶点数据时直接将其传入作为纹理坐标变换矩阵即可。终于到了我们图像处理的核心:OpenGL着色器程序了。在介绍处理相机流数据的OpenGL着色器之前,我们先来简单了解一下OpenGL的渲染管线,下面这张图是渲染管线每个阶段的抽象显示,蓝色部分是可编程部分,我们可以在这几个部分自己编写着色器程序控制渲染。

我们简单介绍一下这几个阶段。
图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是进行坐标变换,同时顶点着色器允许我们对顶点属性进行一些基本处理。
图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状:点、线、三角形。
图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。本文中没有对图元做变换,故没有用到几何着色器。
几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
片段着色器处理完后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。此阶段涉及到深度和模板缓冲区以及OpenGL颜色混合,细说起来又可以写一篇文章了。本文中因为只对相机流的2D图像做全屏处理,片段着色器颜色采用完全替换的方式,不使用深度和模板缓冲区及OpenGL颜色混合模式,在此就不详细讨论该阶段的处理了。
在上图显示的三个可编程阶段中,我们对相机流数据的处理用到了顶点着色器(Vertex Shader)和片段着色器(Fragment Shader),下面我们就来重点看看如何编写顶点着色器和片段着色器,以相机纹理和变换矩阵作为输入,把相机流数据渲染在GSurfaceView上。
编写及初始化OpenGL着色器程序
着色器程序语法与C语言很像,顶点着色器和片段着色器都包含一个main函数,main函数外定义了三种不同类型的变量:uniform、attribute和varying。uniform变量是外部程序传递给着色器的变量,类似C语言的const变量,在OpenGL着色器程序的一次渲染过程中保持不变;attribute变量只在顶点着色器中使用,一般用来表示一些顶点的数据,如顶点坐标,法线,纹理坐标,顶点颜色等;varying变量是顶点着色器和片段着色器之前传递数据用的,它作为顶点着色器的输出,经过图元装配和栅格化后,作为片段着色器的输入。着色器中也内置了一些变量和函数,本文中介绍两个最最常用的内置变量:
- gl_Position:顶点着色器中必须对其赋值,其输入序列作为图元装配过程的组成点、线或三角形的坐标序列。
- gl_FragColor:片段着色器中必须对其赋值,作为像素点的输出值。
要了解OpenGL着色器语言的使用,本文中的内容只是冰山一角,希望从事OpenGL开发的同学需要花大量时间去深入学习,本文中只对相机流数据用到的着色器程序进行简单介绍。下面我们就来看看相机数据流处理的顶点着色器和片段着色器程序:


顶点着色器主要对顶点坐标进行变换,在相机预览的例子中,我们引入了两个变换矩阵:uMVPMatrix
和uTexMatrix
。其中uMVPMatrix
是投影矩阵,主要进行3D及NDC坐标变换,本文中对全屏相机流数据做处理,传入全屏坐标,且不进行变换,故uMVPMatrix
传入单位矩阵即可;uTexMatrix
是纹理变换矩阵,前文中我们拿到了摄像头纹理的变换矩阵mTransformMatrix
这时候就派上用场了,uTexMatrix
变量传入mTransformMatrix
,相机纹理坐标经过其变换后即可得到旋转正向的坐标序列。
片段着色器对目标点进行颜色赋值。我们在前面拿到了摄像头纹理mPreviewTextureId[0]
,需要注意的是,在Android中Camera产生的预览纹理是以一种特殊的格式传送的,因此片段着色器里的纹理类型并不是普通的sampler2D,而是samplerExternalOES, 在着色器的头部也必须声明OES 的扩展。除此之外,external OES的纹理和Sampler2D在使用时没有差别。
有了顶点着色器和片段着色器程序,我们怎么把它们加在OpenGL渲染管线中运行起来呢?OpenGL着色器程序和普通程序的运行准备过程差不多,也需要通过编译和链接后才可使用。下面就是编译shader和链接program的代码:

经过以上步骤,我们处理相机流数据的顶点着色器和片段着色器程序就准备好了,最后得到的program就是一个OpenGL ES程序对象,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象:
GLES20.glUseProgram(program);
在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。下面还有一个很重要的问题:我们怎么把前面得到的相机纹理和纹理坐标变换矩阵传递给OpenGL ES程序呢?下面我们就来看看如何在OpenGL ES程序中传递各种不同类型的参数。
为着色器程序传递参数
前面提到,着色器中有三种类型的参数:uniform、attribute和varying。varying参数是顶点着色器和片段着色器之前传递参数用的,对外部程序来可见,所以外部程序能传入着色器的参数只有uniform和attribute类型。
不管是uniform还是attribute参数,都需要先拿到其对应的句柄才能进行传参操作。这两种类型参数获取句柄的方法略有不同,以获取上文中attribute类型参数aPosition和uniform类型参数uTexMatrix为例,获取句柄方法分别如下:

attribute类型参数都需要用glGetAttribLocation获取句柄,而uniform参数则是用glGetUniformLocation获取句柄。
获取到句柄后,接下来就是把真正的参数值传进句柄了。我们先来看看两个attribute参数:aPosition和aTextureCoord的传值:

此处涉及到两个OpenGL ES相关的函数调用:
glEnableVertexAttribArray调用后允许顶点着色器读取句柄对应的GPU数据。默认情况下,出于性能考虑,所有顶点着色器的attribute变量都是关闭的,意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU.由glEnableVertexAttribArray启用指定属性,才可在顶点着色器中访问逐顶点的attribute数据。glVertexAttribPointer或VBO只是建立CPU和GPU之间的逻辑连接,从而实现了CPU数据上传至GPU。但是,数据在GPU端是否可见,即着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU数据。 glVertexAttribPointer函数的参数非常多:第一个参数指定句柄;第二个参数指定顶点属性的大小,每个坐标点包含x和y两个float值;第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec都是由浮点数值组成的);第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间,这里我们把它设置为GL_FALSE; 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔,由于下个组位置数据在2个GLfloat之后,我们把步长设置为2 sizeof(GLfloat);最后一个参数就是数据buffer。
uniform参数的传递相对简单,对于uMVPMatrix和uTexMatrix参数,获取到句柄后直接用下面的方法即可传入:

OpenGL ES有很多glUniformX的API,就是不同类型的uniform参数的传递方法。samplerExternalOES纹理或sampler2D纹理的传递方法稍微复杂一点:

纹理参数传递时,需要先绑定某个纹理单元,将纹理输入绑定到纹理单元的目标对象上,然后调用glUniform1i设置其参数为该纹理单元。
至此,我们的着色器程序已准备好,所有参数也已设置完毕。万事俱备,只欠东风,下面我们来看看最后一步:将相机流数据渲染到屏幕上。
渲染帧数据
前面步骤都完成后,调用OpenGL ES的渲染指令倒是比较简单了,只有两行代码:

前面提到,OpenGL ES的基本图元有点、线和面(三角形),我们在glDrawArrays调用中传入的第一个参数就是指定基本图元以何种方式组装。组装方式有很多种,枚举值如下:
GL_POINTS 画离散的点 GL_LINES 画线(每两个点连成一条线) GL_LINE_STRIP 画线(所有点相互相连,首尾不相连) GL_LINE_LOOP 画线(所有点相互相连,首尾相连) GL_TRIANGLES 填充三角形(将每三个点围成的三角形进行填充,相邻的点之间不填充) GL_TRIANGLE_STRIP 填充三角形(将每三个点围成的三角形进行填充,相邻的点之间填充) GL_TRIANGLE_FAN 填充三角形(以第一个点为顶点,之后每两个点合起来围成的三角形进行填充,相邻的点之间填充)

本文是以两个三角形组成一个矩形的方式把相机纹理渲染到屏幕上的,在这里我们用了GL_TRIANGLE_FAN图元组装方式。

其他组装方式本文不详细介绍,有兴趣的同学可以自己深入了解一下。
经过以上步骤,我们应该可以在屏幕GLSurfaceView区域内看到相机预览数据了,赞!
对摄像头数据的再处理
前面我们已经拿到了摄像头纹理并显示在屏幕上,但我们显示到屏幕上的是摄像头原始数据纹理,中间没有做任何其他处理。如果我们想将摄像头原始纹理做一些处理,比如把彩色图变成黑白图像,然后再显示到屏幕上,应该怎么做呢?其实和我们前面将相机纹理渲染到屏幕的过程是一样的!还记得我们前面的片段着色器吗?我们直接调用gl_FragColor = texture2D(sTexture, vTextureCoord);
将目标颜色赋值为输入纹理颜色,所以我们在屏幕上看到的是原图。下面我们来看一个新的片段着色器,它用一个简单的公式对当前像素点的rgb值进行加权,然后将rgb值都设置为此加权值形成灰度图的效果:

对摄像头数据的再处理过程,其实可以看做两个着色器程序串行执行的过程。我们在前面处理摄像头纹理的着色器渲染完成后,暂时保存输出纹理,然后再用上面灰度图的着色器程序将此输出纹理作为输入,再渲染到屏幕上,即可在屏幕上看到对原始彩色纹理处理后生成灰度图纹理的效果,这其实就是我们对摄像头数据的再处理步骤。初始化片段着色器并传参的步骤前面已经详细介绍,对上面的片段着色器再做一遍即可。
这里需要注意的是,暂存第一个着色器的输出纹理需要用到OpenGL的另一个概念:Frame Buffer。 在OpenGL渲染管线中,几何数据和纹理经过多次转化和多次测试,最后以二维像素的形式显示在屏幕上。OpenGL管线的最终渲染目的地被称作帧缓存(framebuffer)。一般情况下,帧缓存完全由window系统生成和管理,由OpenGL使用。这个默认的帧缓存被称作“window系统生成”(window-system-provided)的帧缓存。在OpenGL扩展中,GL_EXT_framebuffer_object提供了一种创建额外的不能显示的帧缓存对象的接口。为了和默认的“window系统生成”的帧缓存区别,这种帧缓冲成为应用程序帧缓存(application-createdframebuffer)。通过使用帧缓存对象(FBO),OpenGL可以将显示输出到引用程序帧缓存对象,而不是传统的“window系统生成”帧缓存。而且,它完全受OpenGL控制。
在一个帧缓存对象中有多个颜色关联点(GL_COLOR_ATTACHMENT0,…,GL_COLOR_ATTACHMENTn),一个深度关联点(GL_DEPTH_ATTACHMENT),和一个模板关联点(GL_STENCIL_ATTACHMENT)。我们可以把纹理图像(Texture Images)或渲染缓存图像(RenderBuffer Images)绑定到这些关联点上。它们之间的关系如下图所示:

GLSurfaceView的onDrawFrame回调中,默认是绑定了window系统生成的FBO的,这个FBO对应屏幕显示,即0号FBO。只要我们中间不切换FBO,所有的glDrawArray或glDrawElements指令调用都是将目标渲染到这个0号FBO的。而对我们对摄像头数据进行处理后再显示到屏幕的需求来说,我们不能将两个着色器程序都直接渲染到屏幕,第一个着色器程序渲染的结果需要输出到一个中间FBO上,然后再切回屏幕对应的0号FBO渲染第二个着色器程序。下面我们来看看如何生成一个中间FBO并绑定到一个纹理图像,这样第一个着色器程序的输出并不直接渲染到屏幕,而是渲染到此FBO绑定的纹理上,然后此纹理再作为灰度图着色器程序的输入,最终渲染到屏幕FBO上。
前面提到FBO可以绑定到纹理对象或者RenderBuffer对象,RenderBuffer是以内部格式存储的经过渲染优化的对象,它的渲染速度更快,缺点是无法对渲染进果进行重采样。如果不需要对FBO的输出再做下一步采样处理,就可以用RenderBuffer。在我们的例子中,因为我们要暂存相机流处理着色器的渲染结果,并作为灰度黑着色器程序的输入,即要对此输出结果进行采样,所以我们必须要用FBO绑定纹理对象的方式。生成FBO并将其绑定到一个纹理的代码如下:

经过上面的代码后,着色器程序的渲染输出都会定位到新生成的FBO上。接下来我们调用相机流处理着色器的渲染流程,渲染完成后,我们再调用GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
即可切回到屏幕对应的0号FBO,然后再以texture[0]
作为灰度图着色器的纹理输入,并调用其渲染流程,我们就可以在屏幕上看到相机流的灰图度效果了。
获取摄像头数据的补充
本文中我们获取摄像头数据是采用SurfaceTexture绑定纹理,相机流数据直接更新到OES纹理上的方式。文章一开始我们提到,获取相机预览数据还有另一种方式,通过为相机设置Camera.PreviewCallback
回调拿到YUV格式数据,这种情况下得到YUV数据格式默认为NV21,也可以通过parameter.setPreviewFormat(ImageFormat format)
来指定YUV数据格式。
对图像数据的处理,为了达到实时性的要求,一般情况下还是需要用OpenGL在GPU上完成。所以在拿到相机YUV数据以后,我们需要把YUV数据转换成GPU可用的普通RGBA纹理才方便对数据进行再处理。从相机拿到的YUV数据格式是NV21或NV12,这种格式下,Y数据在一个平面(planar)上,UV数据在一个平面上。这种格式的YUV字节流转换成RGBA纹理一般有两种方式:
- UV所在的一个平面拆成U和V数据分别在一个平面上,然后将Y、U、V三个平面作为三个GL_LUMINANCE的纹理作为输入,然后用YUV到RGB的转换矩阵在着色器程序中实现。
- 将YUV数据转换成类似RGBA的每个像素点包含YUVA格式的字节流,然后用YUV到RGB的转换矩阵在着色器程序中实现。
两种方式都需要先在CPU上对相机YUV格式字节流做一些预处理,然后上载到GPU上用着色器程序完成转换。这个过程涉及的预处理和着色器程序可以单独再拿一篇文章来写,篇幅有限,本文中就不详细介绍了。
总结
至此,我们经过了选取并打开摄像头、设置相机预览SurfaceTexture、获取相机流数据纹理、使用着色器渲染纹理到屏幕、切换FrameBuffer等等过程,中间很多内容因为篇幅原因没有详细介绍,有兴趣的同学可以自行翻查资料学习。OpenGL在安卓端的应用非常广泛,在移动端直播和视频app中,获取摄像头数据并进行再处理是非常常见的场景,需要充分了解摄像头数据的获取方式、OpenGL的相关知识以及在Android端的使用方式,尤其OpenGL的编程方式与面向方法的编程方式不同,需要了解其渲染管线、shader的参与时机和用法、FrameBuffer相关的知识,才能在现实应用中充分发挥GPU的强大能力,希望本文能对有相关开发需求的同学提供帮助。
参考文献
OpenGL渲染管线 OES纹理扩展 glEnableVertexAttribArray的作用 基本图形定义 OpenGL帧缓存 YUV与RGB格式转换
作者简介:kevinxing(邢雪源),天天P图Android工程师
————————————————————————————————————————
常用的色彩格式
常见的色彩格式主要分为两类,一类是RGBA系列,一类是YUV系列。
RGBA系列
首先就是rgba系列的格式,RGBA色彩主要用于色彩的显示和描述。常见的有RGBA/ARGB/BGRA/ABGR/RGB/BGR。这些格式都比较好理解了。R、G、B、A分别表示红绿蓝及透明通道。
以RGBA为例,就是4个字节表示一个颜色值,排列方式就是RGBARGBARGBA这样排列。而RGB,就是三个字节表示一个颜色值,没有透明通道,排列方式就是RGBRGBRGB。在通常的视频中,也是没有透明通道的(也有例外,比如MOV格式,是可以包含透明通道的)。所以当RGBA编码为视频色彩时,A是会被丢掉的。
当然,上面说的,是将每个色彩都用一个字节来表示的情况。RGBA也有RGBA_8888,RGBA_4444,RGB565等等众多格式,也就是并不是每个颜色都用一个字节来表示。以RGB565为例,是用两个字节来表示RGB三个色彩,R占5位,G占6位,B占5位。RGB565与RGB24相比,色彩上稍有损失,一般情况下,不细致对比,不容易发现这个损失,但是内存上会节约1/3的大小。
YUV系列
YUV主要用于优化彩色视频信号的传输,相比RGBA色彩来说,YUV格式占用更少的内存。YUV系列的格式,与RGBA一样,也是五花八门,常见的有YUY2、YUYV、YVYU、UYVY、AYUV、Y41P、Y411、Y211、IF09、IYUV、YV12、YVU9、YUV411、YUV420、YUV422等等。Y表示亮度,U、V都表示色度。如果只有Y分量,没有UV分量,那么得到的就是黑白灰度图像。
与YUV类似的还有YCrCb,YIQ等色彩格式。YIQ模型与YUV模型类似,用于NTSC制式的电视系统。YIQ颜色空间中的I和Q分量相当于将YUV空间中的UV分量做了一个33度的旋转。而YCrCb是YUV的一种派生色彩,Y包含了绿色色度和亮度,Cr表示红色色度,Cb表示蓝色色度。
我们在实际使用时,遇到最多的大概就是NV21、NV12、YUV420P、YUV420SP、I420等这些格式。他们有什么区别呢?
实际上I420就是标准的YUV420P,以4*4的图像来说,YUV排列顺序为YYYYYYYYYYYYYYYYUUUUVVVV。YUV大小分别为4*4、2*2、2*2。
Y1Y2Y7Y8U1V1 可以表示四个像素点,其他的同色区域一样,都是表示四个像素点,像素位置与Y对应。
借用Wiki上的图片表示下:
这里写图片描述
NV21为标准的YUV420SP,以4*4的图像来说,YUV排列顺序为YYYYYYYYYYYYYYYYUVUVUVUV。Y大小分别为4*4、UV大小为4*2。如图:
这里写图片描述
NV12与NV21类似,也是YUV420SP,只是排列顺序上UV换了个边,变为YYYYYYYYYYYYYYYYVUVUVUVU。
当然像YUV411,YUV420对比,差异主要在于采样点上。YUV虽然格式众多,但是使用起来也是大同小异。更多可参考Wiki上的YUV介绍。
常用RGB与YUV之间的转换
很多时候,我们在网上找RGB转YUV格式或者YUV转RGB格式的转换公式时,总会得到不一样的公式,让我们无法选择,不知道哪个是正确的。实际上,RGB转YUV或者YUV转RGB的确会有不同的公式。这是由于不同的标准以及转换校正造成的。我们利用RGB转成YUV来传输,然后显示时又需要被还原成RGB。
根据BT.601标准(SDTV,标清),定义参数如下:
Wr=0.299
Wb=0.114
Wg=1-Wr-Wb=0.587
Umax=0.436
Vmax=0.615
1
2
3
4
5
RGB转YUV公式如下:
Y‘=WrR+WgG+WbB=0.299R+0.587G+0.114B
U=UmaxB−Y‘1−Wb≈0.492(B−Y‘)
V=VmaxR−Y‘1−Wr≈0.877(R−Y‘)
反向推导YUV转RGB的,得到公式如下:
R=Y‘+1.14V
G=Y‘−0.395U−0.581V
B=Y‘+2.033U
即得到RGB和YUV根据BT.601标准的公式为:
#RGB转YUV
#[Y] [0.299 0.587 0.114 ][R]
#[U] = [-0.147 -0.289 0.436 ][G]
#[V] [0.615 -0.515 -0.100 ][B]
Y = 0.299 R + 0.587 G + 0.114 B
U = -0.147 R – 0.289 G + 0.436 B
V = 0.615 R – 0.515 G – 0.100 B
#YUV转RGB
#[R] [1 0 1.140 ][Y]
#[G] = [1 -0.395 -0.581 ][U]
#[B] [1 2.032 0 ][V]
R = Y + 1.402 V
G= Y – 0.395 U – 0.581V
B= Y + 2.032U
# 在老式的非SIMD体系结构中,浮点运算慢与定点运算,所以变换下:
# RGB转YUV,studio-swing,Y的范围为[16-235],UV的范围为[16-240]
Y = ((66R+129G+25B+128)>>8)+16
U = ((-38R-74G+112B+128)>>8)+128
V = ((112R-94G-18B+128)>>8)+128
# RGB转YUV,full-swing,YUV的范围都为[0-255]
Y = (77R+150G+29B+128)>>8
U = (-43R-84G+127B+128)>>8)+128
V = ((127R-106G-21B+128)>>8)+128
# YUV转RGB
C = Y-16
D = U-128
E = V-128
R = clamp((298*C + 409 * E +128)>>8)
G = clamp((298*C – 100* D – 208* E+ 128)>>8)
B = clamp((298*C + 516* D- 128)>>8)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
RGBA转YUV具体实现,可参考Android视频编码——RGBA、RGB、BGRA、BGR转YUV420P、YUV420SP
libyuv的使用
libyuv提供了非常方面好用的色彩空间转换、旋转、缩放的功能,转换效率也非常高。如果有色彩空间转换、旋转、缩放等功能的需求,不妨使用此库来完成。在使用libyuv进行转换或者自己写代码进行转换的过程中,可以使用RawGfx这个软件,来查看你的转换是否正确,无论是RGBA格式还是YUV格式的原始数据,都可以用它进行查看。
libyuv的jni封装
在libyuv中,提供了非常丰富的方法,我们实际使用时往往只需要使用到其中的一小部分。为了在Android中调用libyuv,我们需要编写Jni代码,调用libyuv中的方法。
以RGBA转I420为例,libyuv中提供了许多不同的方法来针对RGBA、ARGB、RGB565等等一系列的不同的RGBA格式转I420格式的方法。为了简化我们的工作我们可以做一个简单的封装来实现Java层调用RGBA转I420的方法。
我们可以先写出一个native的接口,来表述我们需要的功能:
public class YuvUtils {
//rgba也会有很多类型,所以我们加一个type的值,来表示rgba是什么类型
//也可以用直接写一个rgba转yuv的,rgba和yuv类型都不固定,用type来表示所有类型的rgba到yuv的转换
public static native int RgbaToI420(int type,byte[] rgba,byte[] yuv,int width,int height);
}
1
2
3
4
5
6
7
8
然后就是编写Jni代码了。
Jni代码中定义了一个函数指针数组,包含将会对Java提供的RGBA转I420的类型,值得注意的是在Java层传入byte[]以RGBA顺序排列时,libyuv是用ABGR来表示这个排列,如果期望传入的数据是RGBA排列,使用libyuv是用libyuv::RGBAToI420这个方法,得到的YUV数据将是错误的数据。
#include <assert.h>
#include “libyuv.h”
#include “jni.h”
#include “android/log.h”
#define YUV_UTILS_JAVA “com/wuwang/libyuv/YuvUtils”
#ifdef __cplusplus
extern “C” {
#endif
static int (*rgbaToI420Func[])(const uint8 *,int,uint8 *,int,uint8 *,int ,uint8 *,int,int,int)={
libyuv::ABGRToI420,libyuv::RGBAToI420,libyuv::ARGBToI420,libyuv::BGRAToI420,
libyuv::RGB24ToI420,libyuv::RGB565ToI420
};
int rgbaToI420(JNIEnv * env,jclass clazz,jbyteArray rgba,jint rgba_stride,
jbyteArray yuv,jint y_stride,jint u_stride,jint v_stride,
jint width,jint height,
int (*func)(const uint8 *,int,uint8 *,int,uint8 *,int ,uint8 *,int,int,int)){
size_t ySize=(size_t) (y_stride * height);
size_t uSize=(size_t) (u_stride * height >> 1);
jbyte * rgbaData= env->GetByteArrayElements(rgba,JNI_FALSE);
jbyte * yuvData=env->GetByteArrayElements(yuv,JNI_FALSE);
int ret=func((const uint8 *) rgbaData, rgba_stride, (uint8 *) yuvData, y_stride,
(uint8 *) (yuvData) + ySize, u_stride, (uint8 *) (yuvData )+ ySize + uSize,
v_stride, width, height);
env->ReleaseByteArrayElements(rgba,rgbaData,JNI_OK);
env->ReleaseByteArrayElements(yuv,yuvData,JNI_OK);
return ret;
}
int Jni_RgbaToI420(JNIEnv * env,jclass clazz,jint type,jbyteArray rgba,jbyteArray yuv,jint width,jint height){
uint8 cType=(uint8) (type & 0x0F);
int rgba_stride= ((type & 0xF0) >> 4)*width;
int y_stride=width;
int u_stride=width>>1;
int v_stride=u_stride;
return rgbaToI420(env,clazz,rgba,rgba_stride,yuv,y_stride,u_stride,v_stride,width,height,rgbaToI420Func[cType]);
}
//libyuv中,rgba表示abgrabgrabgr这样的顺序写入文件,java使用的时候习惯rgba表示rgbargbargba写入文件
static JNINativeMethod g_methods[]={
{“RgbaToI420″,”(I[B[BII)I”, (void *)Jni_RgbaToI420},
//…. 其他方法映射
};
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv* env = nullptr;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
return JNI_ERR;
}
assert(env != nullptr);
jclass clazz=env->FindClass(YUV_UTILS_JAVA);
env->RegisterNatives(clazz, g_methods, (int) (sizeof(g_methods) / sizeof((g_methods)[0])));
return JNI_VERSION_1_4;
}
JNIEXPORT void JNI_OnUnload(JavaVM *jvm, void *reserved){
JNIEnv* env = nullptr;
if (jvm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
return;
}
jclass clazz=env->FindClass(YUV_UTILS_JAVA);
env->UnregisterNatives(clazz);
}
#ifdef __cplusplus
}
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
传入的type,被每4位表示一个具体的意义。从低位到高位,0-3 表示转换类型,4-7 表示rgba_stride的宽度的倍数,8-11 表示yuv_stride宽度移位数,12-15 表示uv左移位数。
根据Jni代码中对于type各位的解析与使用,定义出类型转换的几个常量如下。这样我们就封装了一个通用的rgba转I420的方法了。
public final class Key {
private Key(){};
//0-3 表示转换类型
//4-7 表示rgba_stride的宽度的倍数
//8-11 表示yuv_stride宽度移位数
//12-15 表示uv左移位数
public static final int RGBA_TO_I420=0x01001040;
public static final int ABGR_TO_I420=0x01001041;
public static final int BGRA_TO_I420=0x01001042;
public static final int ARGB_TO_I420=0x01001043;
public static final int RGB24_TO_I420=0x01001034;
public static final int RGB565_TO_I420=0x01001025;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
封装后的方法使用与检验
封装后的方法使用也比较简单,直接获取一个Bitmap,然后将Bitmap中的rgba数据copy出来,进行转换就可以了,转换完的结果保存到文件中,然后用RawGfx来检查下转换的结果是否正确。
Bitmap bitmap= BitmapFactory.decodeResource(getResources(),R.mipmap.bg);
width=bitmap.getWidth();
height=bitmap.getHeight();
File file=new File(getExternalFilesDir(null).getAbsolutePath()+”/cache.yuv”);
OutputStream os = new FileOutputStream(file);
ByteBuffer buffer=ByteBuffer.allocate(bitmap.getWidth()*bitmap.getHeight()*4);
bitmap.copyPixelsToBuffer(buffer);
byte[] yuvData=new byte[bitmap.getWidth()*bitmap.getHeight()*3/2];
YuvUtils.RgbaToI420(Key.RGBA_TO_I420,buffer.array(),yuvData,bitmap.getWidth(),bitmap.getHeight());
//rgbToYuv(buffer.array(),bitmap.getWidth(),bitmap.getHeight(),yuvData);
Log.e(“wuwang”,”width*height:”+bitmap.getWidth()+”/”+bitmap.getHeight());
os.write(yuvData);
os.flush();
os.close();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
如下所示,是使用libyuv中的libyuv::RGBAToI420转换得到的结果:
这里写图片描述
由于上面说过,libyuv表示的排列顺序和Bitmap的RGBA表示的顺序是反向的。所以实际要调用libyuv::ABGRToI420才能得到正确的结果。
这里写图片描述
示例代码下载
libyuv提供了丰富的功能,其他功能使用与这个差不多,在github上的示例代码中有其他常用的转换、缩放、旋转等方法。有需要的可自行下载。
————————————————
版权声明:本文为CSDN博主「湖广午王」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/junzia/article/details/76315120
————————————————————————————————————————
http://blog.csdn.net/oshunz/article/details/50171931
前面有一篇探讨了如何在片段着色器中将YUV数据转换为RGB数据并显示,但采用samplerExternalOES将SurfaceTexture作为OpenGL外部纹理,需要使用GL_TEXTURE_EXTERNAL_OES作为纹理模板,通过SetPreviewTexture将Camera数据图像输出到SurfaceTexture,调用updateTexImage()时将对应纹理更新为最新的一帧,然后通知OpenGL绘制对应纹理。
1.创建纹理ID
- 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];
2.根据纹理id创建SurfaceTexture
- mSurfaceTexture = new SurfaceTexture(textureId);
- mSurfaceTexture.setOnFrameAvailableListener(listener);
- private OnFrameAvailableListener listener = new OnFrameAvailableListener() {
- @Override
- public void onFrameAvailable(SurfaceTexture surfaceTexture) {
- // TODO Auto-generated method stub
- mGlSurfaceView.requestRender();
- }
- };
3.更改Shader
顶点着色器与之前相比多了个textureTransform,用于接收SurfaceTexture变换矩阵,如果不需要也可以不盖被vertext shader。
片段着色器需要增加#extension GL_OES_EGL_image_external : require,因为第一步的纹理都绑定到GL_TEXTURE_EXTERNAL_OES
对应采样方式由Samlpe2D更改成samplerExternalOES。
- public static final String NO_FILTER_VERTEX_SHADER = “” +
- “attribute vec4 position;\n” +
- ” attribute vec4 inputTextureCoordinate;\n” +
- ” \n” +
- “uniform mat4 textureTransform;\n” +
- “varying vec2 textureCoordinate;\n” +
- ” \n” +
- ” void main()\n” +
- “{\n” +
- “textureCoordinate = (textureTransform * inputTextureCoordinate).xy;\n” +
- “gl_Position = position;\n” +
- “}”;
- public static final String NO_FILTER_FRAGMENT_SHADER = “” +
- “#extension GL_OES_EGL_image_external : require\n”+
- “precision mediump float;” +
- “varying vec2 textureCoordinate;\n” +
- “uniform samplerExternalOES inputImageTexture;\n” +
- ” \n” +
- “void main() {” +
- ” gl_FragColor = texture2D( inputImageTexture, textureCoordinate );\n” +
- “}”;
4.绘制
与之前采用glTexImage2D绑定2D图像纹理不同,这里通过GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureid);绑定外部纹理即可。
如果使用了SurfaceTexture变换矩阵,可采用glUniformMatrix4fv传递给Shader
采用片段着色器方案的时间消耗主要在数据转移中,本方法的格式相关工作交给EGLImage处理,用到了Lock ANativeWindow。具体优劣尚未探讨,如有经验人士希望指点迷津。
GLSurfaceView是OpenGL中的一个类,也是可以预览Camera的,而且在预览Camera上有其独到之处。独到之处在哪?当使用Surfaceview无能为力、痛不欲生时就只有使用GLSurfaceView了,它能够真正做到让Camera的数据和显示分离,所以搞明白了这个,像Camera只开预览不显示这都是小菜,妥妥的。Android4.0的自带Camera源码是用SurfaceView预览的,但到了4.2就换成了GLSurfaceView来预览。如今到了4.4又用了自家的TextureView,所以从中可以窥探出新增TextureView的用意。
虽说Android4.2的Camera源码是用GLSurfaceView预览的,但是进行了大量的封装又封装的,由于是OpenGL小白,真是看的不知所云。俺滴要求不高,只想弄个可拍照的摸清GLSurfaceView在预览Camera上的使用流程。经过一番百度一无所获,后来翻出去Google一大圈也没发现可用的。倒是很多人都在用GLSurfaceView和Surfaceview同时预览Camera,Surfaceview用来预览数据,在上面又铺了一层GLSurfaceView绘制一些信息。无奈自己摸索,整出来的是能拍照也能得到数据,但是界面上不是一块白板就是一块黑板啥都不显示。后来在stackoverflow终于找到了一个可用的链接,哈哈,苍天啊,终于柳暗花明了!参考此链接,自己又改改摸索了一天才彻底搞定。之所以费这么多时间是不明白OpenGL ES2.0的绘制基本流程,跟简单的OpenGL的绘制还是稍有区别。下面上源码:
一、CameraGLSurfaceView.java 此类继承GLSurfaceView,并实现了两个接口
- <span style=“font-family:Comic Sans MS;font-size:18px;”>package org.yanzi.camera.preview;
- import javax.microedition.khronos.egl.EGLConfig;
- import javax.microedition.khronos.opengles.GL10;
- import org.yanzi.camera.CameraInterface;
- import android.content.Context;
- import android.graphics.SurfaceTexture;
- import android.opengl.GLES11Ext;
- import android.opengl.GLES20;
- import android.opengl.GLSurfaceView;
- import android.opengl.GLSurfaceView.Renderer;
- import android.util.AttributeSet;
- import android.util.Log;
- public class CameraGLSurfaceView extends GLSurfaceView implements Renderer, SurfaceTexture.OnFrameAvailableListener {
- private static final String TAG = “yanzi”;
- Context mContext;
- SurfaceTexture mSurface;
- int mTextureID = –1;
- DirectDrawer mDirectDrawer;
- public CameraGLSurfaceView(Context context, AttributeSet attrs) {
- super(context, attrs);
- // TODO Auto-generated constructor stub
- mContext = context;
- setEGLContextClientVersion(2);
- setRenderer(this);
- setRenderMode(RENDERMODE_WHEN_DIRTY);
- }
- @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);
- }
- @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();
- float[] mtx = new float[16];
- mSurface.getTransformMatrix(mtx);
- mDirectDrawer.draw(mtx);
- }
- @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;
- }
- @Override
- public void onFrameAvailable(SurfaceTexture surfaceTexture) {
- // TODO Auto-generated method stub
- Log.i(TAG, “onFrameAvailable…”);
- this.requestRender();
- }
- }
- </span>
关于这个类进行简单说明:
1、Renderer这个接口里有三个回调: onSurfaceCreated() onSurfaceChanged() onDrawFrame(),在onSurfaceCreated里设置了GLSurfaceView的版本: setEGLContextClientVersion(2); 如果没这个设置是啥都画不出来了,因为Android支持OpenGL ES1.1和2.0及最新的3.0,而且版本间差别很大。不告诉他版本他不知道用哪个版本的api渲染。在设置setRenderer(this);后,再设置它的模式为RENDERMODE_WHEN_DIRTY。这个也很关键,看api:
When renderMode is RENDERMODE_CONTINUOUSLY, the renderer is called repeatedly to re-render the scene. When renderMode is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface is created, or when requestRender
is called. Defaults to RENDERMODE_CONTINUOUSLY.
Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance by allowing the GPU and CPU to idle when the view does not need to be updated.
大意是RENDERMODE_CONTINUOUSLY模式就会一直Render,如果设置成RENDERMODE_WHEN_DIRTY,就是当有数据时才rendered或者主动调用了GLSurfaceView的requestRender.默认是连续模式,很显然Camera适合脏模式,一秒30帧,当有数据来时再渲染。
2、正因是RENDERMODE_WHEN_DIRTY所以就要告诉GLSurfaceView什么时候Render,也就是啥时候进到onDrawFrame()这个函数里。SurfaceTexture.OnFrameAvailableListener这个接口就干了这么一件事,当有数据上来后会进到
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
// TODO Auto-generated method stub
Log.i(TAG, “onFrameAvailable…”);
this.requestRender();
}
这里,然后执行requestRender()。
3、网上有一些OpenGL ES的示例是在Activity里实现了SurfaceTexture.OnFrameAvailableListener此接口,其实这个无所谓。无论是被谁实现,关键看在回调里干了什么事。
4、与TextureView里对比可知,TextureView预览时因为实现了SurfaceTextureListener会自动创建SurfaceTexture。但在GLSurfaceView里则要手动创建同时绑定一个纹理ID。
5、本文在onSurfaceCreated()里打开Camera,在onSurfaceChanged()里开启预览,默认1.33的比例。原因是相比前两种预览,此处SurfaceTexture创建需要一定时间。如果想要开预览时由Activity发起,则要GLSurfaceView利用Handler将创建的SurfaceTexture传递给Activity。
二、DirectDrawer.java 此类非常关键,负责将SurfaceTexture内容绘制到屏幕上
- <span style=“font-family:Comic Sans MS;font-size:18px;”>package org.yanzi.camera.preview;
- import java.nio.ByteBuffer;
- import java.nio.ByteOrder;
- import java.nio.FloatBuffer;
- import java.nio.ShortBuffer;
- import android.opengl.GLES11Ext;
- import android.opengl.GLES20;
- import android.opengl.Matrix;
- public class DirectDrawer {
- 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 }; // order to draw vertices
- // 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,
- };
- static float textureVertices[] = {
- 0.0f, 1.0f,
- 1.0f, 1.0f,
- 1.0f, 0.0f,
- 0.0f, 0.0f,
- };
- 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(); // create empty OpenGL ES Program
- GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program
- GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
- GLES20.glLinkProgram(mProgram); // creates OpenGL ES program executables
- }
- 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);
- mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, “inputTextureCoordinate”);
- GLES20.glEnableVertexAttribArray(mTextureCoordHandle);
- // textureVerticesBuffer.clear();
- // textureVerticesBuffer.put( transformTextureCoordinates( textureVertices, mtx ));
- // textureVerticesBuffer.position(0);
- GLES20.glVertexAttribPointer(mTextureCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, textureVerticesBuffer);
- GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
- // Disable vertex array
- GLES20.glDisableVertexAttribArray(mPositionHandle);
- GLES20.glDisableVertexAttribArray(mTextureCoordHandle);
- }
- private int loadShader(int type, String shaderCode){
- // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
- // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
- int shader = GLES20.glCreateShader(type);
- // add the source code to the shader and compile it
- GLES20.glShaderSource(shader, shaderCode);
- GLES20.glCompileShader(shader);
- return shader;
- }
- private float[] transformTextureCoordinates( float[] coords, float[] matrix)
- {
- float[] result = new float[ coords.length ];
- float[] vt = new float[4];
- for ( int i = 0 ; i < coords.length ; i += 2 ) {
- float[] v = { coords[i], coords[i+1], 0 , 1 };
- Matrix.multiplyMV(vt, 0, matrix, 0, v, 0);
- result[i] = vt[0];
- result[i+1] = vt[1];
- }
- return result;
- }
- }
- </span>
三、有了上面两个类就完成95%的工作,可以将GLSurfaceView看成是有生命周期的。在onPause里进行关闭Camera,在Activity里复写两个方法:
- <span style=“font-family:Comic Sans MS;font-size:18px;”> @Override
- protected void onResume() {
- // TODO Auto-generated method stub
- super.onResume();
- glSurfaceView.bringToFront();
- }
- @Override
- protected void onPause() {
- // TODO Auto-generated method stub
- super.onPause();
- glSurfaceView.onPause();
- }</span>
这个glSurfaceView.bringToFront();其实不写也中。在布局里写入自定义的GLSurfaceView就ok了:
- <span style=“font-family:Comic Sans MS;font-size:18px;”> <FrameLayout
- android:layout_width=“wrap_content”
- android:layout_height=“wrap_content” >
- <org.yanzi.camera.preview.CameraGLSurfaceView
- android:id=“@+id/camera_textureview”
- android:layout_width=“0dip”
- android:layout_height=“0dip” />
- </FrameLayout></span>
CameraActivity里只负责UI部分,CameraGLSurfaceView负责开Camera、预览,并调用DirectDrawer里的draw()进行绘制。其他代码就不上了。
注意事项:
1、在onDrawFrame()里,如果不调用mDirectDrawer.draw(mtx);是啥都显示不出来的!!!这是GLSurfaceView的特别之处。为啥呢?因为GLSurfaceView不是Android亲生的,而Surfaceview和TextureView是。所以得自己按照OpenGL ES的流程画。
2、究竟mDirectDrawer.draw(mtx)里在哪获取的Buffer目前杂家还么看太明白,貌似么有请求buffer,而是根据GLSurfaceView里创建的SurfaceTexture之前,生成的有个纹理ID。这个纹理ID一方面跟SurfaceTexture是绑定在一起的,另一方面跟DirectDrawer绑定,而SurfaceTexture作渲染载体。
3、参考链接里有,有人为了解决问题,给出了下面三段代码:
@Override
public void onDrawFrame(GL10 gl)
{
float[] mtx = new float[16];
mSurface.updateTexImage();
mSurface.getTransformMatrix(mtx);
mDirectVideo.draw(mtx);
}
private float[] transformTextureCoordinates( float[] coords, float[] matrix)
{
float[] result = new float[ coords.length ];
float[] vt = new float[4];
for ( int i = 0 ; i < coords.length ; i += 2 ) {
float[] v = { coords[i], coords[i+1], 0 , 1 };
Matrix.multiplyMV(vt, 0, matrix, 0, v, 0);
result[i] = vt[0];
result[i+1] = vt[1];
}
return result;
}
textureVerticesBuffer.clear();
textureVerticesBuffer.put( transformTextureCoordinates( textureVertices, mtx ));
textureVerticesBuffer.position(0);
我已经把代码都融入到了此demo,只不过在draw()方法里么有使用。原因是使用之后,得到的预览画面反而是变形的,而不用的话是ok的。上面的代码是得到SurfaceTexture的变换矩阵:mSurface.getTransformMatrix
然后将此矩阵传递给draw(),在draw的时候对textureVerticesBuffer作一个变化,然后再画。
下图是未加这个矩阵变换效果时:
下图为使用了变换矩阵,划片扭曲的还真说不上来咋扭曲的,但足以说明OpenGL ES在渲染效果上的强大,就是设置了个矩阵,不用一帧帧处理,就能得到不一样显示效果。
—————————–本文系原创,转载请注明作者yanzi1225627
版本号:PlayCamera_V3.0.0[2014-6-22].zip
CSDN下载链接:http://download.csdn.net/detail/yanzi1225627/7547263
百度云盘:
附个OpenGL ES简明教程:http://www.apkbus.com/android-20427-1-1.html
一、设置OpenGL ES视图
设置OpenGL视图并不难,Android上也较简单。我们一般只需要2个步骤。
GLSurfaceView
我们要为GLSurfaceView提供一个专门用于渲染的接口
public void setRenderer(GLSurfaceView.Renderer renderer)
GLSurfaceView.Renderer
GLSurfaceView.Renderer是一个通用渲染接口。我们必须实现下面的三个抽象方法:
// 画面创建
public void onSurfaceCreated(GL10 gl, EGLConfig config)
// 画面绘制
public void onDrawFrame(GL10 gl)
// 画面改变
public void onSurfaceChanged(GL10 gl, int width, int height)
onSurfaceCreated
在这里我们主要进行一些初始化工作,比如对透视进行修正、设置清屏所用颜色等。
onDrawFrame
绘制当前画面
onSurfaceChanged
当设备水平或者垂直变化时调用此方法,设置新的显示比例
案例代码:
- public class OpenGLDemo extends Activity {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- GLSurfaceView view = new GLSurfaceView(this);
- view.setRenderer(new OpenGLRenderer());
- setContentView(view);
- }
- }
复制代码
实现renderer需要更多的设置
- public void onSurfaceCreated(GL10 gl, EGLConfig config) {
- // 黑色背景
- gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
- // 启用阴影平滑(不是必须的)
- gl.glShadeModel(GL10.GL_SMOOTH);
- // 设置深度缓存
- gl.glClearDepthf(1.0f);
- // 启用深度测试
- gl.glEnable(GL10.GL_DEPTH_TEST);
- // 所作深度测试的类型
- gl.glDepthFunc(GL10.GL_LEQUAL);
- // 对透视进行修正
- gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
- }
- public void onDrawFrame(GL10 gl) {
- // 清除屏幕和深度缓存
- gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
- }
- public void onSurfaceChanged(GL10 gl, int width, int height) {
- // 设置画面的大小
- gl.glViewport(0, 0, width, height);
- // 设置投影矩阵
- gl.glMatrixMode(GL10.GL_PROJECTION);
- // 重置投影矩阵
- gl.glLoadIdentity();
- // 设置画面比例
- GLU.gluPerspective(gl, 45.0f, (float) width / (float) height, 0.1f,100.0f);
- // 选择模型观察矩阵
- gl.glMatrixMode(GL10.GL_MODELVIEW);
- // 重置模型观察矩阵
- gl.glLoadIdentity();
- }
- }
复制代码
只要加入这段代码到OpenGLDemo class里就可实现全屏this.requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
设置完视图后,即可编译运行,可以看到一个“漂亮”的黑屏 = =!
OpenGLDemo01.rar (48.38 KB, 下载次数: 259)
二、绘制多边形前面的教程都是关于设置GLSurfaceView.的,接下来的教程将教我们渲染出一个多边形。3D模型用较小的元素创建(点,边,面),他们可以被分别操作。
顶点
在Android中,我们通过float数组定义顶点,并将它放到字节型缓冲区内来获取更好的性能。下例的代码即为上图所示顶点。OpenGL ES的很多功能都必须手动的开启和关闭。
- gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
- // 设置顶点数据,3代表XYZ坐标系
- gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
- // 关闭顶点设置
- gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
复制代码
边
面
计算多边形面的时候,一定要注意正确的方向.。因为这将决定哪一面为正面哪一面为背面。 所以我们尽量保证整个项目都使用相同的环绕。gl.glFrontFace(GL10.GL_CCW);控制多边形的正面是如何决定的。在默认情况下,mode是GL_CCW。mode的值为: GL_CCW 表示窗口坐标上投影多边形的顶点顺序为逆时针方向的表面为正面。 GL_CW 表示顶点顺序为顺时针方向的表面为正面。顶点的方向又称为环绕。gl.glEnable(GL10.GL_CULL_FACE);gl.glCullFace(GL10.GL_BACK);剔除多边形的背面,禁用多边形背面上的光照、阴影和颜色计算及操作。gl.glDisable(GL10.GL_CULL_FACE);
多边形
到了绘制面的时候了, 我们使用默认的逆时针环绕。下例代码将绘制上图多边形。
- // 将坐标数组放入字节缓存中
- // (1) 分配缓存,一个short为2个字节,所以要乘以2
- ByteBuffer ibb = ByteBuffer.allocateDirect(indices.length * 2);
- // (2) 设置字节处理规则
- ibb.order(ByteOrder.nativeOrder());
- // (3) 转换为short型字符
- ShortBuffer indexBuffer = ibb.asShortBuffer();
- // (4) 放入坐标数组
- indexBuffer.put(indices);
- // (5) 复位
- indexBuffer.position(0);
复制代码
渲染是时候弄些玩意儿到屏幕上去了,绘制时我们将用到两个函数public abstract void glDrawArrays(int mode, int first, int count)通过我们构造的顶点缓存来绘制顶点public abstract void glDrawElements(int mode, int count, int type, Buffer indices)和glDrawArrays类似,但需要直接传入type(索引值的类型,如GL_UNSIGNED_SHORT, or GL_UNSIGNED_INT),和indices(索引缓存)两者的共同点是,都必须知道他们需要画什么。怎样渲染图元,有不同方式,为了帮助调试,我们应该了解它们。
Mode:GL_POINTS绘制独立的点到屏幕
GL_LINE_STRIP连续的连线,第n个顶点与第n-1个顶点绘制一条直线
GL_LINE_LOOP和上面相同,但首尾相连
GL_LINES各对独立的线段
GL_TRIANGLES各个独立的三角形
GL_TRIANGLE_STRIP
绘制一系列的三角形,先是顶点 v0, v1, v2, 然后是 v2, v1, v3 (注意规律), 然后v2, v3, v4等。该规律确保所有的三角形都以相同的方向绘制。
GL_TRIANGLE_FAN和GL_TRIANGLE_STRIP类似, 但其先绘制 v0, v1, v2, 再是 v0, v2, v3, 然后 v0, v3, v4等。
我认为GL_TRIANGLES是使用最方便的,所以我们将先使用它。
- public class Square {
- // 顶点坐标数组
- private float vertices[] = { -1.0f, 1.0f, 0.0f, // 0, 左上
- -1.0f, -1.0f, 0.0f, // 1, 左下
- 1.0f, -1.0f, 0.0f, // 2, 右下
- 1.0f, 1.0f, 0.0f, // 3, 右上
- };
- // 连接规则
- private short[] indices = { 0, 1, 2, 0, 2, 3 };
- // 顶点缓存
- private FloatBuffer vertexBuffer;
- // 索引缓存
- private ShortBuffer indexBuffer;
- public Square() {
- // 一个float为4 bytes, 因此要乘以4
- ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
- vbb.order(ByteOrder.nativeOrder());
- vertexBuffer = vbb.asFloatBuffer();
- vertexBuffer.put(vertices);
- vertexBuffer.position(0);
- // short类型同理
- ByteBuffer ibb = ByteBuffer.allocateDirect(indices.length * 2);
- ibb.order(ByteOrder.nativeOrder());
- indexBuffer = ibb.asShortBuffer();
- indexBuffer.put(indices);
- indexBuffer.position(0);
- }
- /**
- * 绘制正方形到屏幕
- *
- * @param gl
- */
- public void draw(GL10 gl) {
- // 逆时针环绕
- gl.glFrontFace(GL10.GL_CCW);
- // 开启剔除功能
- gl.glEnable(GL10.GL_CULL_FACE);
- // 剔除背面
- gl.glCullFace(GL10.GL_BACK);
- // 开启顶点缓存写入功能
- gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
- // 设置顶点
- // size:每个顶点有几个数指描述。
- // type:数组中每个顶点的坐标类型。
- // stride:数组中每个顶点间的间隔,步长(字节位移)。
- // pointer:存储着每个顶点的坐标值。初始值为0
- gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
- gl.glDrawElements(GL10.GL_TRIANGLES, indices.length,
- GL10.GL_UNSIGNED_SHORT, indexBuffer);
- // 关闭各个功能
- gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
- gl.glDisable(GL10.GL_CULL_FACE);
- }
- }
复制代码
我们必须在OpenGLRenderer类中初始化square
- square = new Square();<!–EndFragment–>
复制代码
并在主绘制方法中调用square的绘制方法
- public void onDrawFrame(GL10 gl) {
- // 清除屏幕和深度缓存
- gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
- // 绘制正方形
- square.draw(gl);
- }
复制代码
如果你现在运行应用,我们又看到了华丽的黑屏,为什么?因为OpenGL ES渲染默认的当前位置为(0,0,0),窗口的定位也一样。而且OpenGL ES不渲染太靠近窗体定位的东西。解决方法就是移动绘制的位置。
- gl.glTranslatef(0, 0, -4); <!–EndFragment–>
复制代码
再次运行应用你将看到该正方形已经被绘制,但是它好像离我们越来越远一样,最后消失了。OpenGL ES不会在画面之间复位绘制点,所以我们要自己完成。
- // 重置当前的模型观察矩阵
- gl.glLoadIdentity();<!–EndFragment–>
复制代码
现在,我们运行应用将会看到一个固定位置的正方形。
OpenGLDemo02.rar (64.87 KB, 下载次数: 347)