一.整体思路
我们在用纹理增加细节那篇文章中提到过,要将图片渲染在屏幕上,首先要拿到图片的像素数组数据,然后将像素数组数据通过纹理单元传递到片段着色器中,最后通过纹理采样函数将纹理中对应坐标的颜色值采样出来,然后给最终的片段赋予颜色值。现在换成了yuv视频,我们应该如何处理呢?因为最终的片段颜色值是rgba格式的,而我们的视频是yuv格式的,所以我们需要做一个转化:即将yuv转化为rgba。
我们在渲染图像到屏幕的时候,需要用到glteximage2d()函数指定二维纹理图像,这个函数各个参数的含义如下:
- target:指定目标纹理,这个值必须是gl_texture_2d
- level:执行细节级别,0是最基本的图像级别,n表示第n级贴图细化级别
- internalformat:指定纹理中的颜色组件,可选的值有gl_alpha,gl_rgb,gl_rgba,gl_luminance, gl_luminance_alpha 等几种
- width:指定纹理图像的宽度,必须是2的n次方
- height:指定纹理图像的高度,必须是2的n次方
- border:指定边框的宽度,必须为0
- format:像素数据的颜色格式, 不需要和internalformat取值必须相同,可选的值参考internalformat
- type:指定像素数据的数据类型
- pixels:指定内存中指向图像数据的指针
我们可以看到这个函数并没有直接支持yuv格式的图像数据,但是,别担心!它又给我们提供了gl_luminance这种格式,它表示只取一个颜色通道,假如传入的值为r,则在片段着色器中的纹理单元中读出的值为(r,r,r,1)。这样以来,我们就可以将yuv图像拆分为3个通道来读取。但是,拆分为3个通道来读取,最后如何重新合成一个rgba颜色值呢?这个时候,之前学过的纹理单元就可以派上用场了,我们可以定义3个纹理单元,分别读取yuv图像的3个通道的数据,最后在片段着色器中进行合成,然后转化为rgba值即可。
二.读取解析yuv视频文件
想要读取yuv视频数据,我们首先得清楚它的内部结构。为了方便讲解,这里我们以yuv420p格式的视频文件为例,它是一个由宽640,高360的yuv图像构成的视频,并且帧和帧之间无缝衔接。我们知道yuv420p格式的图像帧是先连续存储所有的y分量,然后再连续存储所有的u分量,最后再连续存储所有的v分量。并且,亮度分量y和色度分量uv的比例为4:1:1,也就是4个亮度分量共享一组色度分量。
知道了这些之后,我们就可以来读取yuv视频文件了。首先我们将准备的视频文件input.yuv放入assets文件夹下面,然后写一个函数循环地去读取这个视频文件,代码如下:
fun readyuvdata(w:int,h:int){ val input=context.resources.assets.open("input.yuv") val y=bytearray(w*h) val u=bytearray(w*h/4) val v=bytearray(w*h/4) while(true){ val ysize=input.read(y) val usize=input.read(u) val vsize=input.read(v) if(ysize>0&&usize>0&&vsize>0){ //根据指定的字节数组创建一个新的bytebuffer对象,对返回的bytebuffer对象所做的更改会反映在原始字节数组上,因为它们共享相同的存储区域 buffery=bytebuffer.wrap(y) bufferu=bytebuffer.wrap(u) bufferv=bytebuffer.wrap(v) //请求渲染一个新帧,调用requestrender()后,glsurfaceview会在下一个合适的时机调用opengl渲染器的ondrawframe()方法,从而实现新的场景绘制和渲染 glsurfaceview.requestrender() thread.sleep(1000/30) } else{ break } } }
三.编写顶点着色器和片段着色器
首先,我们来编写顶点着色器,代码如下:
#version 300 es layout(location=0) in vec4 a_position; layout(location=1) in vec2 a_texture_coordinates; layout(location=3) uniform mat4 u_matrix; out vec2 v_texture_coordinates; void main() { gl_position=u_matrix*a_position; v_texture_coordinates=a_texture_coordinates; }
接下来,再来编写片段着色器,代码如下:
#version 300 es precision mediump float; in vec2 v_texture_coordinates; out vec4 fragcolor; layout(location=0) uniform sampler2d texturey; layout(location=1) uniform sampler2d textureu; layout(location=2) uniform sampler2d texturev; void main() { float y,u,v; vec3 rgb; y=texture(texturey,v_texture_coordinates).r; u=texture(textureu,v_texture_coordinates).g-0.5; v=texture(texturev,v_texture_coordinates).b-0.5; rgb.r = y 1.540*v; rgb.g = y - 0.183*u - 0.459*v; rgb.b = y 1.816*u; fragcolor=vec4(rgb,1.0); }
在这里,我们定义了三个纹理采样对象,分别用于对yuv图像3个通道的数据进行采样。然后,我们需要知道rgb.r,rgb.g指的是什么。其实,在glsl中,向量的组件可以通过{x,y,z,w},{r,g,b,a}或{s,t,r,q}来获取,之所以采用这三个不同的命名方法,是因为向量通常会用来表示数学向量,颜色和纹理坐标。所以rgb.r,texture(texturey,v_texture_coordinates).r都是指向量中的第一个元素的值。由于我们之前设置的格式是gl_luminance,假设传入的y分量对应坐标位置的值为r,则在片段着色器中的纹理单元中读出的值为(r,r,r,1),那么我们取r就是取第一个元素的值,其实这里前3个的值都是一样的,取哪个都可以。
但是,我们注意到,u和v后面都减去了0.5,这是为什么呢?我们先来看下yuv转rgb的公式:
我们首先需要知道的是yuv中的u,v指的是红色r和蓝色b与亮度y的偏差,u和v的默认值都是128,我们把128代入公式,正好r=y,r=b。从上面的公式看,代入的u和v都是减去默认值128的,也就是说转化公式中所使用的是u,v和默认值128的偏移值。所以,我们要使用这个公式,也要求出这个偏移值。但是,texture函数计算后得到的是归一化的值,取值范围是[0,1],由于位深是8bit,取值范围是[0,255],减去128相当于减去总范围的一半,所以我们也需要减去总范围的一半,即0.5。
四.绑定顶点数据和纹理数据
首先,我们写一个函数用于绑定顶点数据:
fun bindvertexdata(){ //创建vao glgenvertexarrays(1,vao,0) //创建vbo glgenbuffers(1,vbo,0) //一定要先绑定vao,再绑定vbo glbindvertexarray(vao[0]) glbindbuffer(gl_array_buffer,vbo[0]) //将顶点数组数据存入显存 glbufferdata(gl_array_buffer,floatbuffer.capacity()*4,floatbuffer, gl_static_draw) //最后一个参数改为偏移值offset glvertexattribpointer(0, position_component_count, gl_float,false,stride,0) glenablevertexattribarray(0) glvertexattribpointer(1, texture_coordinate_component_count, gl_float,false,stride, position_component_count*4) glenablevertexattribarray(1) //解除绑定 glbindbuffer(gl_array_buffer,0) glbindvertexarray(0) }
在这里,我们使用了顶点数组对象vao和顶点缓冲对象vbo,这是opengl es3.0中引入的新特性。在opengl es2.0编程中,用于绘制的顶点数组数据首先保存在cpu内存,在调用gldrawarrays函数进行绘制时,需要将顶点数组数据从cpu内存拷贝到gpu显存中。但是,很多时候我们没必要每次绘制时都进行内存拷贝,如果可以直接在显存中存储这些数据,就可以避免每次拷贝所带来的巨大开销。vbo的出现就是为了解决这个问题的,vbo的作用是提前在显存中开辟好一块内存,用于存储顶点数组数据。
那vao是用来干嘛的呢?我们现在思考一个问题,假如我们有两份顶点数组数据,一份用来绘制正方体,一份用来绘制长方体,并且我们将它们都存入vbo开辟的显存中,那么gpu怎么知道取哪一部分数据绘制正方体,哪一部分数据绘制长方体呢?vao就是用于解决这个问题的,vao的作用就相当于一个指针,指向我们所开辟的内存的首地址,如下图所示。这样以来,我们可以开辟两处内存分别用于存储正方体数据和长方体数据,然后,我们再使用两个vao对象,分别指向两个内存块的首地址,这样以来,gpu就知道去哪里取数据了。当然,如果只有一份数据,不使用vao也行。
然后,再写一个函数用来绑定纹理数据,代码如下:
fun bindtexturedata(){ //y平面 glactivetexture(gl_texture0) glbindtexture(gl_texture_2d,textures[0]) glteximage2d(gl_texture_2d,0, gl_luminance,w,h,0, gl_luminance, gl_unsigned_byte,buffery) //u平面 glactivetexture(gl_texture1) glbindtexture(gl_texture_2d,textures[1]) glteximage2d(gl_texture_2d,0, gl_luminance,w/2,h/2,0, gl_luminance, gl_unsigned_byte,bufferu) //v平面 glactivetexture(gl_texture2) glbindtexture(gl_texture_2d,textures[2]) glteximage2d(gl_texture_2d,0, gl_luminance,w/2,h/2,0, gl_luminance, gl_unsigned_byte,bufferv) }
完整的myrenderer.kt的代码如下:
class myrenderer(val context: context,val glsurfaceview:glsurfaceview):glsurfaceview.renderer { private val projectionmatrix:floatarray=floatarray(16)//存储投影矩阵 private val textures=intarray(3) private var floatbuffer:floatbuffer private val vao=intarray(1) private val vbo=intarray(1) private var buffery:bytebuffer?=null private var bufferu:bytebuffer?=null private var bufferv:bytebuffer?=null init{ //存储顶点坐标和纹理坐标,并且让矩形的宽高比和视频像素的宽高比一样,都是16:9 val vertexdata= floatarrayof( 1f, 9/16f, 1.0f, 0.0f, // top right 1f, -9/16f, 1.0f, 1.0f, // bottom right -1f, 9/16f, 0.0f, 0.0f, // top left 1f, -9/16f, 1.0f, 1.0f, // bottom right -1f, -9/16f, 0.0f, 1.0f, // bottom left -1f, 9/16f, 0.0f, 0.0f // top left ) floatbuffer= bytebuffer .allocatedirect(vertexdata.size*4)//一个浮点数占四个字节 .order(byteorder.nativeorder()) .asfloatbuffer() .put(vertexdata) floatbuffer.position(0) } companion object{ val position_component_count=2 val texture_coordinate_component_count=2 val stride=(position_component_count texture_coordinate_component_count)*4 val w=640 val h=360 } override fun onsurfacecreated(gl: gl10?, config: eglconfig?) { glclearcolor(1.0f,1.0f,1.0f,1.0f) val vertexshadercode=textresourcereader.readtextfilefromresource(context,r.raw.vertex_shader) val fragmentshadercode=textresourcereader.readtextfilefromresource(context,r.raw.fragment_shader) shaderhelper.buildprogram(vertexshadercode,fragmentshadercode) gluniform1i(0,0) gluniform1i(1,1) gluniform1i(2,2) glgentextures(3,textures,0) for(i in 0..2){ glbindtexture(gl_texture_2d,textures[i]) gltexparameteri(gl_texture_2d,gl_texture_min_filter, gl_nearest)//处理图片缩小的情况 gltexparameteri(gl_texture_2d,gl_texture_mag_filter,gl_linear)//处理图片放大的情况 //解绑纹理对象 glbindtexture(gl_texture_2d,0) } bindvertexdata() } override fun onsurfacechanged(gl: gl10?, width: int, height: int) { glviewport(0,0,width,height) //根据屏幕方向生成投影矩阵 val aspectratio=if(width>height) width.tofloat()/height.tofloat() else height.tofloat()/width.tofloat() if(width>height){ matrix.orthom(projectionmatrix,0,-aspectratio,aspectratio,-1f,1f,-1f,1f) } else{ matrix.orthom(projectionmatrix,0,-1f,1f,-aspectratio,aspectratio,-1f,1f) } //传递正交投影矩阵 gluniformmatrix4fv(3,1,false,projectionmatrix,0) thread{ readyuvdata(w,h) } } override fun ondrawframe(gl: gl10?) { glclear(gl_color_buffer_bit) bindtexturedata() glbindvertexarray(vao[0]) gldrawarrays(gl_triangles,0,6) buffery?.clear() bufferu?.clear() bufferv?.clear() } fun bindvertexdata(){ glgenvertexarrays(1,vao,0) glgenbuffers(1,vbo,0) glbindvertexarray(vao[0]) glbindbuffer(gl_array_buffer,vbo[0]) glbufferdata(gl_array_buffer,floatbuffer.capacity()*4,floatbuffer, gl_static_draw) glvertexattribpointer(0, position_component_count, gl_float,false,stride,0) glenablevertexattribarray(0) glvertexattribpointer(1, texture_coordinate_component_count, gl_float,false,stride, position_component_count*4) glenablevertexattribarray(1) glbindbuffer(gl_array_buffer,0) glbindvertexarray(0) } fun bindtexturedata(){ //y平面 glactivetexture(gl_texture0) glbindtexture(gl_texture_2d,textures[0]) glteximage2d(gl_texture_2d,0, gl_luminance,w,h,0, gl_luminance, gl_unsigned_byte,buffery) //u平面 glactivetexture(gl_texture1) glbindtexture(gl_texture_2d,textures[1]) glteximage2d(gl_texture_2d,0, gl_luminance,w/2,h/2,0, gl_luminance, gl_unsigned_byte,bufferu) //v平面 glactivetexture(gl_texture2) glbindtexture(gl_texture_2d,textures[2]) glteximage2d(gl_texture_2d,0, gl_luminance,w/2,h/2,0, gl_luminance, gl_unsigned_byte,bufferv) } fun readyuvdata(w:int,h:int){ val input=context.resources.assets.open("input.yuv") val y=bytearray(w*h) val u=bytearray(w*h/4) val v=bytearray(w*h/4) while(true){ val ysize=input.read(y) val usize=input.read(u) val vsize=input.read(v) if(ysize>0&&usize>0&&vsize>0){ //根据指定的字节数组创建一个新的bytebuffer对象,对返回的bytebuffer对象所做的更改会反映在原始字节数组上,因为它们共享相同的存储区域 buffery=bytebuffer.wrap(y) bufferu=bytebuffer.wrap(u) bufferv=bytebuffer.wrap(v) //请求渲染一个新帧,调用requestrender()后,glsurfaceview会在下一个合适的时机调用opengl渲染器的ondrawframe()方法,从而实现新的场景绘制和渲染 glsurfaceview.requestrender() thread.sleep(1000/30) } else{ break } } } }
mainactivity.kt的代码如下:
class mainactivity : appcompatactivity() { private lateinit var glsurfaceview:glsurfaceview override fun oncreate(savedinstancestate: bundle?) { super.oncreate(savedinstancestate) glsurfaceview= glsurfaceview(this) glsurfaceview.seteglcontextclientversion(3) glsurfaceview.setrenderer(myrenderer(this,glsurfaceview)) setcontentview(glsurfaceview) } override fun onpause() { super.onpause() glsurfaceview.onpause() } override fun onresume() { super.onresume() glsurfaceview.onresume() } }