首页 > 其他分享 >如何渲染最原始的yuv视频数据?

如何渲染最原始的yuv视频数据?

时间:2024-02-26 18:00:31浏览次数:18  
标签:视频 val 渲染 TEXTURE yuv 2D 纹理 GL

一.整体思路

  我们在用纹理增加细节那篇文章中提到过,要将图片渲染在屏幕上,首先要拿到图片的像素数组数据,然后将像素数组数据通过纹理单元传递到片段着色器中,最后通过纹理采样函数将纹理中对应坐标的颜色值采样出来,然后给最终的片段赋予颜色值。现在换成了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{
        //存储顶点坐标和纹理坐标
        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_WRAP_S, GL_REPEAT)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
            //纹理过滤
            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()
    }
}

  其他代码我之前的文章中有写。

 

标签:视频,val,渲染,TEXTURE,yuv,2D,纹理,GL
From: https://www.cnblogs.com/luqman/p/18034409

相关文章

  • 可视化视频监控云平台EasyCVR使用RTMP_PUSH推流不成功是什么原因?
    高清可视化视频监控云平台EasyCVR支持高清视频的接入和传输、分发,支持7*24小时不间断监控,平台可提供实时远程视频监控、视频录像、录像回放与存储、告警、语音对讲、云台控制、平台级联、磁盘阵列存储、视频集中存储、云存储等丰富的视频能力,并具备权限管理、设备管理、鉴权管理、......
  • 智慧安防视频监控平台EasyCVR通道播放支持添加水印及操作步骤介绍
    智慧安防视频监控平台EasyCVR采用了开放式的网络结构,系统可支持的接入协议包括:国标GB28181、RTSP/Onvif、RTMP,以及厂家的私有协议与SDK,如:海康ehome、海康sdk、大华sdk、宇视sdk、华为sdk、萤石云sdk、乐橙sdk等,兼容各品牌的IPC、NVR、移动手持终端、执法仪、布控球、无人机等设备......
  • GB28181视频监控平台EasyCVR如何通过配置实现级联不响应下级平台的检索消息?
    AI视频智能分析/视频监控管理平台EasyCVR能在复杂的网络环境中(专网、内网、局域网、广域网、公网等)将前端海量的设备进行统一集中接入与视频汇聚管理,平台支持设备通过4G、5G、WIFI、有线等方式进行视频流的快捷接入和传输。平台能将接入的视频流进行汇聚、转码与多格式分发,可分发......
  • 视频汇聚平台EasyDarwin音视频流媒体视频平台新增视频拉流转推流功能
    ​在这个信息爆炸的时代,视频内容的生产和消费已经成为我们日常生活的一部分。随着技术的不断进步,视频处理和传输的方式也在不断地演变和升级。今天,我要向大家介绍的是一个令人振奋的消息——视频汇聚平台EasyDarwin新增了视频拉流转推流功能,这一创新之举无疑将为视频内容的传......
  • 浏览器渲染原理
    浏览器渲染原理浏览器渲染原理第一步:URL地址解析。第二步:缓存检查通过一个url网址,首先得到的是一个html。渲染过程中,遇到link标签向服务器发送拿到css代码,遇到script标签向服务器发送拿到js代码,遇到img标签向服务器发送拿到图片文件。如果浏览器上有......
  • 一个支持Sora模型文本生成视频的Web客户端
    大家好,我是Java陈序员。最近OpenAI又火了一把,其新推出的文本生成视频模型——Sora,引起了巨大的关注。Sora目前仅仅只是发布预告视频,还未开放出具体的API.今天,给大家推荐一个最近十分火热的开源项目,一个支持使用Sora模型将文本生成视频的Web客户端。关注微信公众......
  • Airtest-Selenium实操小课:刷B站视频
    1.前言上一课我们讲到用Airtest-Selenium爬取网站上我们需要的信息数据,还没看的同学可以戳这里看看~那么今天的推文,我们就来说说看,怎么实现看b站、刷b站的日常操作,包括点击暂停,发弹幕,点赞,收藏等操作,仅供大家参考学习~2.需求分析和准备整体的需求大致可以分为以下步骤:打开chr......
  • 常见问题 --- 语音视频呼叫系统调用嗨糠道闸一体机设备查看视频闪退问题排查
    调试设备是一个非常耗时的工作,主要是理解架构和分析流量上花去了很多时间,我这里大致说一下排查的流程问题描述在道闸一体机设备上点击呼叫按钮,呼叫后台,后台视频呼叫窗口没有响应或者出现一次后就立即消失。我们必须知道的常识,嗨糠openapi,嗨糠官方对他们自家的设备开放了第三方调......
  • 【vue3】【router】跳转页面渲染两次问题
    最近做一款毕设遇到一个问题:如图所示部分是因为渲染了两次组件导致执行了两次onMounted加载了相同数据。 router.ts以下是我的路由相关配置: default布局:keepalive App.vue 最后导致这个原因正是因为布局组件default.vue和App.vue同时使用了keepalive,只要任意一边取消......
  • 当今最强最有诚意的安卓平板!Xiaomi Pad 6S Pro 12.4 评测:在线视频续航19小时
    一、前言:全方位升级的平板电脑2024年2月22日,小米正式推出了新一代平板XiaomiPad6SPro12.4。如果单从名字上看,它似乎是小米平板5Pro的升级版,但实际上,它是小米平板6Pro的下一代产品。XiaomiPad6SPro12.4搭载了高通骁龙8Gen2处理器,带来了35%的性能提升和40%的能效提升......