一.整体思路

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

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

如何渲染最原始的yuv视频数据?的更多相关文章

  1. 使用D3D渲染YUV视频数据

    源代码下载 在PC机上,对于YUV格式的视频如YV12,YUY2等的显示方法,一般是采用DIRECTDRAW,使用显卡的OVERLAY表面显示.OVERLAY技术主要是为了解决在PC上播放VCD而在显 ...

  2. 使用D3D渲染YUV视频数据<转>

    源代码下载 转载地址:http://blog.csdn.net/dengzikun/article/details/5824874 源码地址:http://download.csdn.net/down ...

  3. 使用DirectDraw直接显示YUV视频数据

    最近在编写一个进行视频播放的ActiveX控件,工作已经接近尾声,现将其中显示YUV数据的使用DirectDraw的一些经验总结如下:(解码部分不是我编写的,我负责从网络接收数据,将数据传给解码器,并 ...

  4. 如何使用DirectDraw直接显示RGB、YUV视频数据(播放yuv)

    #include "draw.h"void CTest100Dlg::OnButton1() { // TODO: Add your control notification ha ...

  5. 基于RTP的H264视频数据打包解包类

    from:http://blog.csdn.net/dengzikun/article/details/5807694 最近考虑使用RTP替换原有的高清视频传输协议,遂上网查找有关H264视频RTP打 ...

  6. JavaCV 采集摄像头及桌面视频数据

    javacv 封装了javacpp-presets库很多native API,简化了开发,对java程序员来说比较友好. 之前使用JavaCV库都是使用ffmpeg native API开发,这种方式 ...

  7. FFmpeg YUV视频序列编码为视频

    上一篇已经写了如何配置好开发环境,这次就先小试牛刀,来个视频的编码.搞视频处理的朋友肯定比较熟悉YUV视频序列,很多测试库提供的视频数据都是YUV视频序列,我们这里就用用YUV视频序列来做视频.关于Y ...

  8. 使用ffmpeg将BMP图片编码为x264视频文件,将H264视频保存为BMP图片,yuv视频文件保存为图片的代码

    ffmpeg开源库,实现将bmp格式的图片编码成x264文件,并将编码好的H264文件解码保存为BMP文件. 实现将视频文件yuv格式保存的图片格式的測试,图像格式png,jpg, gif等等測试均O ...

  9. Android 音视频开发(四):使用 Camera API 采集视频数据

    本文主要将的是:使用 Camera API 采集视频数据并保存到文件,分别使用 SurfaceView.TextureView 来预览 Camera 数据,取到 NV21 的数据回调. 注: 需要权限 ...

  10. 图像处理之基础---2个YUV视频 拼接技术

    /************************************************* * 主要功能:两路 YUV4:2:0拼接一路左右半宽格式YUV视频 参考资料:http://www ...

随机推荐

  1. 物理机和虚拟机上CPU睿频的区别

    物理机和虚拟机上CPU睿频的区别 关于睿频 睿频是指当启动一个运行程序后,处理器会自动加速到合适的频率, 而原来的运行速度会提升 10%~20% 以保证程序流畅运行的一种技术. 一般max的睿频不能超 ...

  2. [转帖]Dapper,大规模分布式系统的跟踪系统

    http://bigbully.github.io/Dapper-translation/ 作者:Benjamin H. Sigelman, Luiz Andr´e Barroso, Mike Bur ...

  3. [转帖]关于gdb相关的几个工具的说明

    https://phpor.net/blog/post/846 使用rpm命名查看gdb的rpm包,主要由下面几个程序:/usr/bin/gcore/usr/bin/gdb/usr/bin/gdbse ...

  4. [转帖]Windows磁盘性能压测(2)-Fio

    http://www.manongjc.com/detail/59-qftscgqzitmxpaw.html 目录 一.腾讯云官网硬盘性能指标 二.使用fio测试硬盘性能指标 1. 测试工具相关 2. ...

  5. expect 的简单学习与使用

    背景 最近工作中总有很多重复的事项. 比较繁琐,想着能够简单一点是一点的角度 准备采用expect来建华部分工作量. 其实还可以使用其他方式来处理. 但是感觉expect还是能够简单明了的. 所以暂时 ...

  6. Zabbix6.0的安装与IPMI的简单使用

    zabbix简介 1.zabbix的安装与使用 建议使用CentOS8进行部署, 不建议使用CentOS7, rpm包直接部署的话,CentOS8比较容易一些 支持mysql数据库.建议先期部署mys ...

  7. Linux 通过yum 方式离线下载依赖rpm包的操作步骤

    离线下载依赖rpm包的方法 1.简单获取依赖关系 yum deplist rpm-build 注意 deplist 为依赖项目 我在arm 上面简单跑一下结果为 package: rpm-build- ...

  8. 在K8S中,Pod生命周期包含哪些?

    在Kubernetes(简称K8s)中,Pod的生命周期经历了一系列状态变化.以下是Pod可能处于的一些主要状态: Pending: 当创建一个Pod时,它首先会进入Pending状态.这个状态下,K ...

  9. Go 1.21发布!

    原文在这里. 由Eli Bendersky, on behalf of the Go team 发布于 8 August 2023 Go团队今天非常高兴地发布了Go 1.21版本,你可以通过访问下载页 ...

  10. Typora 1.6.7永久激活

    介绍Typora介绍 具体看上面的我就不多介绍了 接下来我们开始教程 需要的文件 Typora安装包 破解补丁包 安装包下载 破解补丁下载 接下来我们全部下载后获得一个安装包一个补丁 安装包直接安装就 ...