相关概念
-
纹理
-
纹理(Texture)
纹理是一种图形数据,在OpenGL中可以简单理解成是一张图片
-
纹理单元
纹理的操作容器,值依次为GLES20.GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2等。纹理单元数量在GPU上是确定的,一般OpenGL ES20至少保证有32个纹理单元,意味着能同时操作32个纹理
-
纹理目标
一个纹理单元包含多个不同类型的纹理目标,例如GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP等
-
纹理ID(TextureID)
纹理对象的引用
OpenGL要操作一个纹理,需要把纹理对象绑定到一个纹理单元上
-
纹理单元和纹理对象的关系
因为片元着色器(fragment shader)通过纹理单元得到纹理对象,所以需要将纹理对象绑定到纹理单元上。每个纹理单元可以同时绑定多个纹理对象,绑定一个纹理对象到一个纹理单元的时候,还需要指定纹理目标,比如可以将纹理对象1绑定到纹理单元TEXTURE0的1D目标上,同时可以将纹理对象2绑定到该纹理单元的2D目标上,不仅如此,两个纹理对象还能绑定到同一纹理单元的同一个目标上
-
-
Surface
Surface就是内存中的一段绘图缓冲区,即一段图片buffer。Surface通常由图片buffers的消费者创建,如SurfaceTexture、MediaRecorder等创建,被给到生产者,如OpenGL、MediaPlayer、MediaCodec等,由生产者往其中写入数据
-
SurfaceTexture
SurfaceTexture可以把Surface生成的图像流,转换为纹理。SurfaceTexture从图像流(来自Camera预览,视频解码,GL绘制场景等)中获得帧数据,当调用updateTexImage()时,根据内容流中最近的图像更新SurfaceTexture对应的GL纹理对象,随后可以操作这些GL纹理对象。
SurfaceTexture在创建的时候需要一个Texture对象,而Surface又可以由SurfaceTexture创建,所以SurfaceTexture将Surface和Texture对象绑定在了一起,从而实现将Surface的图像流更新到对应的Texture对象上的目的
//生成纹理 val textureId:Int = createTextureId() //函数createTextureId中会利用GLES20.glGenTextures函数生成纹理 //创建SurfaceTexture对象,SurfaceTexture作为桥梁,将Texture与Surface联系在了一起 val surfaceTexture = SurfaceTexture(textureId) //创建Surface对象 val surface = Surface(surfaceTexture)
本质上SurfaceTexture实现了共享纹理,即实现了CPU与GPU对同一资源的访问,无需拷贝的数据共享,可降低功耗与提高性能
-
GLSurfaceView
GLSurfaceView作为SurfaceView的补充,在SurfaceView的基础上加入了EGL,可方便地使用 OpenGL ES API 进行渲染绘制
-
EGL
全称:Embedded Graphic Interface(嵌入式图形接口),是 OpenGL ES 渲染 API 和本地窗口系统(native platform window system)之间的一个中间接口层,它主要由系统制造商实现。通过 OpenGL ES 操作GPU将图形数据计算结果保存在Surface的buffer 中,再利用EGL取出buffer中的图形信息显示到手机屏幕上
-
GLSL
全称:OpenGL Shading Language(OpenGL ES着色语言),用这个语言可以编写小程序运行在GPU上。GPU 擅长处理大规模数据,使用同一个算法进行计算,而这个算法,就是使用 GLSL 写成 Shader,供 GPU 运算使用
-
OpenGL ES的作用
OpenGL ES不是用来显示视频的,它主要是用来处理图形数据的。
具体来说,它的主要作用是提供了一套在上层应用程序中操作GPU的接口,利用GPU的强大的图形数据处理功能,对图像数据进行处理,就视频数据来说,本来SurfaceView就能直接播放视频,但是利用OpenGL ES,能对视频图像数据进行二次加工,从而实现很多特效,比如灵魂出窍、水印、美白、滤镜、画中画等等,处理过的数据最终还是要利用EGL显示出来,比如利用GLSurfaceView显示到屏幕上
实际上我们设计的界面能显示到手机屏幕上,其底层也是用到了Surface与OpenGL ES
视频数据流
视频从视频文件到屏幕显示出来,Surface起到了非常重要的作用,在视频解码时,需要一个Surface作为已解码数据的载体,保存播放器解码后的数据,在显示时,需要另一个Surface,作为已渲染数据的载体,保存OpenGL渲染后的数据,最后通过EGL显示在屏幕上,数据流如下图所示:
- 解码器(MediaCodec或FFmpge等)将视频文件中的视频编码数据解码成图像流放到Surface中
- SurfaceTexture把Surface生成的图像流,转换为纹理
- OpenGL ES 渲染纹理生成图形数据,放到GLSurfaceView中的Surface中
- EGL从Surface中取出图形数据,显示到屏幕上
解码器与Surface绑定
MediaPlayer:
val surface:Surface = createSurface()
val mediaplayer = MediaPlayer()
//设置Surface,MediaPlayer解码后的数据将写入到此Surface中
mediaplayer.setSurface(surface)
MediaCodec:
val surface:Surface = createSurface()
//比如视频类型为video/avc
val type = "video/avc"
val format:MediaFormat
val mediaCodec = MediaCodec.createDecoderByType(type)
//将Surface配置到MediaCodec对象中,MediaCodec解码后的数据将写入到此Surface中
mediaCodec.configure(format, surface , null, 0)
Surface与SurfaceTexture绑定
通过SurfaceTexture创建Surface对象即可。Surface也可以与SurfaceView绑定,拿SurfaceTexture与SurfaceView作比较,SurfaceView是将Surface的图像流直接显示出来,而SurfaceTexture是将图像流转换为OpenGL的外部纹理,转换的目的就是可以拿出这个外部纹理进行二次处理,比如利用OpenGL ES进行各种变换
val oesTextureId:Int = createTextureId()
//构造一个绑定了OES纹理的SurfaceTexture
val surfaceTexture = SurfaceTexture(oesTextureId)
//通过SurfaceTexture对象来创建构造一个输出Surface,当解码结果写入到 Surface 的 BufferQueue 之后,再利用 //SurfaceTexture 将结果从 BufferQueue 渲染到OES纹理上
val surface = Surface(surfaceTexture)
说明:之所以使用OES纹理,是因为视频解码的格式是YUV的,而屏幕的画面是RGB的,OES纹理实现了YUV格式到RGB的自动转化,这样就不用在着色器的GLSL中写YUV转RGB的代码
SurfaceTexture与OpenGL ES绑定
OpenGL ES中的一个TextureId创建SurfaceTexture,所以纹理就是SurfaceTexture与OpenGL ES进行联系的纽带,视频数据由SurfaceTexture转换为外纹理后,绑定到OpenGL纹理对象,最后由OpenGL ES将纹理对象中的视频数据进行渲染变换生成新的视频数据
val oesTextureId:Int = createTextureId()
//构造一个绑定了OES纹理的SurfaceTexture
val surfaceTexture = SurfaceTexture(oesTextureId)
fun createTextureId:Int {
val textures = IntArray(1)
//生成纹理
GLES20.glGenTextures(1, textures, 0)
return textures[0]
}
class render: GLSurfaceView.Renderer {
...
override fun onDrawFrame(gl: GL10?) {
...
//updateTexImage更新接收到的数据并将其更新到OpenGL纹理对象中
surfaceTexture.updateTexImage()
...
//激活指定纹理单元
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
//绑定纹理对象到纹理目标
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oesTextureId)
...
}
...
}
onDrawFrame函数中的逻辑也揭示了数据从SurfaceView到OpenGL ES的过程:
-
surfaceTexture.updateTexImage
将最新的图像流数据更新到GL纹理对象中(纹理对象由oesTextureId表示)
-
GLES20.glActiveTexture
激活指定纹理单元,后续的glBindTexture(GL_TEXTURE_EXTERNAL_OES, oesTextureId)作用于此所选的纹理单元
-
GLES20.glBindTexture
绑定纹理对象到上一步中激活的纹理单元的纹理目标,到这一步时OpenGL ES就可以从纹理单元中获取到纹理对象中的图片纹理。glBindTexture函数还能在多个纹理之间实现切换,这样在多个纹理对象绑定到同一纹理单元的同一个纹理目标时,也能很好的控制从多个不同纹理对象中取出图片纹理数据
-
GLES20.glUniform1i
将当前纹理单元绑定的纹理对象的数据传递到着色器,OpenGL ES会在着色器进行瑄染
这一步在此不是必须的,因为作为数据的载体SurfaceTexture 绑定的是OES纹理,所以片元着色器(Fragment Shader)中需要使用OES 纹理,在片元着色器脚本的头部增加扩展纹理的声明:
#extension GL_OES_EGL_image_external : require
这样一来不需要调用glUniform1i,纹理对象的数据会自动传递到片元着色器
如何将OpenGL ES处理后的数据交给GLSurfaceView
- GLSurfaceView中初始化EGL环境,管理EGLSurface、EGLDisplay、EGLContext
- EGLSurface、EGLContext、EGLDisplay 三者会绑定一起
- EGLDisplay 为 OpenGL ES 的渲染目标,可以接收到 OpenGl ES 渲染出来的图形数据
- EGLDisplay让OpenGl ES把内容渲染到EGLSurface中
- EGLSurface是一块特殊的内存,实质就是Surface,能直接排版到Android的视图View上
理解了视频数据流的变换过程与对纹理对象的操作方法,就能利用OpenGL ES对视频数据进行加工,实现我们想要的效果,下面举了两个例子,加以说明
实现画中画
主要是实现两个不同视频绘制到同一视图上,可叠加显示
说明:
- 只需要一个GLSurfaceView,在GLSurfaceView中绘制两个视频画面
- 为了避免发生遮挡,而是要生成叠加的效果,需要启动OpenGL混合功能
- TextureID、SurfaceTexture与Surface三者一起作为一个组合,需要创建了两套这样的组合,每一套的渲染过程是按顺序进行,互相独立的
- SurfaceTexture是纹理对象与Surface的纽带,Surface的图形数据经SurfaceTexture转为纹理,交给OpenGL ES加工
- 每一次onDrawFrame,会按序绘制两个纹理对象
- 绘制时可通过矩阵变换,改变画面的大小与透明度等
- 该功能两个纹理对象能绑定到同一纹理单元的同一个目标(比如两个TextureID都绑定到纹理单元GLES20.GL_TEXTURE0 的 GLES11Ext.GL_TEXTURE_EXTERNAL_OES纹理目标上),因为在绘制流程时都会进行重新绑定,所以不会出现混乱
实现双屏同步播放
主要实现同一个视频同步播放到两个屏或者两个视图
说明:
- 需要两个GLSurfaceView,作为两个播放窗口,共享同一个播放源,即共享同一个SurfaceTexture
- 创建了3个纹理对象,SurfaceTexture对象绑定的TextureID与两个GLSurfaceView绑定的TextureID是无关的,所以Surface的图形数据无法直接给到OpenGL ES加工
- attachToGLContext函数将SurfaceTexture附加到调用线程上当前的OpenGL ES上下文,这样updateTexImage会将Surface的图形数据给到OpenGL ES加工
- 因为是双屏,所以每次绘制的最后,需要调用detachFromGLContext() ,从当前OpenGL ES上下文中分离SurfaceTexture,以便另一屏再进行绘制
- GLSurfaceView的绘制是在自己的子线程中,两个GLSurfaceView同时绘制,共享一个SurfaceTexture,需要做好多线程同步