一.整体思路

  我们在用纹理增加细节那篇文章中提到过,要将图片渲染在屏幕上,首先要拿到图片的像素数组数据,然后将像素数组数据通过纹理单元传递到片段着色器中,最后通过纹理采样函数将纹理中对应坐标的颜色值采样出来,然后给最终的片段赋予颜色值。现在换成了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. RPM安装的Oracle19c 修改init.ora进行修复以及最简单开机启动Oracle的方法

    RPM安装的Oracle19c 修改init.ora进行修复以及最简单开机启动Oracle的方法 背景 今天开始使用自己心的ThinkBook14 的笔记本 因为已经没有了 Linux测试环境供我使用 ...

  2. ChatGPT 提高工作效率-一例SQL编写的过程

    ChatGPT 提高工作效率-一例SQL编写的过程 前言 遇到一个问题, 怀疑是有一些补丁没有被依赖. 导致第一次更新时没有更新这些没依赖的补丁. 后面更新时又更新了这些游离态的补丁. 导致出现 ol ...

  3. 使用Ant Design Vue的select搜索框出现的问题

    Select 选择器进行搜索 <template> <div> <a-form-item label="分类:"> <a-select p ...

  4. 【K哥爬虫普法】不要沾边!涉案 7k 合判 6 年!

    我国目前并未出台专门针对网络爬虫技术的法律规范,但在司法实践中,相关判决已屡见不鲜,K 哥特设了"K哥爬虫普法"专栏,本栏目通过对真实案例的分析,旨在提高广大爬虫工程师的法律意识, ...

  5. Go中字符串处理:fmt.Sprintf与string.Builder的比较

    在Go语言中,我们通常会遇到两种主要的方式来处理和操作字符串:使用fmt.Sprintf函数和string.Builder类型.尽管两者都可以实现字符串的格式化和连接,但它们在性能和用法上有一些关键区 ...

  6. 从零开始构建一个电影知识图谱,实现KBQA智能问答[下篇]:Apache jena SPARQL endpoint及推理、KBQA问答Demo超详细教学

    从零开始构建一个电影知识图谱,实现KBQA智能问答[下篇]:Apache jena SPARQL endpoint及推理.KBQA问答Demo超详细教学 效果展示: 1.Apache jena SPA ...

  7. Hadoop超详细讲解之单节点搭建

    1 Hadoop介绍 Hadoop是Apache旗下的一个用java语言实现开源软件框架,是一个开发和运行处理大规模数据的软件平台.允许使用简单的编程模型在大量计算机集群上对大型数据集进行分布式处理. ...

  8. 解决.netWebAPI输出时间格式带T问题

    GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters.Add( new Ne ...

  9. git操作 手写稿

  10. Windows安装MySQL到最后卡主无响应处理办法

    安装mysql-5.5.62-winx64到最后Ready to execute ... 生效配置时卡主无响应 最有效,最快的解决办法 就是:重启电脑 或者 关闭电脑,在开机,找到MySQL安装目录, ...