OpenGL渲染YUV420P
Posted 程序员~彭国庆
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenGL渲染YUV420P相关的知识,希望对你有一定的参考价值。
一 、YUV420P数据格式
结合上图可以看出YUV420P的特点如下:
①无论在横向还是纵向上都是两个亮度(Y)共享一组色度(UV),所以UV的宽度和高度都是Y的1/2
②在内存中有三片数据,也就是三个数据指针分别指向Y、U、V
ffmpeg中avframe保存yuv420p的数据时是直接申请一整个的 image_size, 亮度数据y的地址是起始地址,u 在 y 的基础上偏移, v 在 u 的基础上偏移
//以HD的yuv420p 8 bit数据为例
uint8_t* yuv420_data = new [1920*1080*1.5]();
uint8_t* data_y = yuv420_data;
uint8_t* data_u = yuv420_data + 1920*1080;
uint8_t* data_v = data_u + 1920*1080/4;
delete[] yuv420_data;
二 、GLFW渲染YUV420P
2.1 定义顶点数据
float vertex_coord_data[] =
-1.f, -1.f, 0.f, 0.f, 1.f,
-1.f, 1.f, 0.f, 0.f, 0.f,
1.f, 1.f, 0.f, 1.f, 0.f,
1.f, -1.f, 0.f, 1.f, 1.f,
;
uint32_t vertx_index_data[] =
0, 1, 2,
2, 3, 0
;
uint32_t m_vertex_buffer, m_index_buffer;
glGenBuffers(1, &m_vertex_buffer);
glGenBuffers(1, &m_index_buffer);
glGenVertexArrays(1, &m_vertex_array);
glBindVertexArray(m_vertex_array);
glBindBuffer(GL_ARRAY_BUFFER, m_vertex_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertex_coord_data), vertex_coord_data, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_index_buffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(vertx_index_data), vertx_index_data, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);
2.2 创建YUV三张纹理
// 纹理 y
glGenTextures(1, &m_tex_y);
glBindTexture(GL_TEXTURE_2D, m_tex_y);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, m_tex_width, m_wnd_height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
glGenerateMipmap(GL_TEXTURE_2D);
// 纹理 u
glGenTextures(1, &m_tex_u);
glBindTexture(GL_TEXTURE_2D, m_tex_u);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, m_tex_width/2, m_wnd_height/2, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
glGenerateMipmap(GL_TEXTURE_2D);
// 纹理 v
glGenTextures(1, &m_tex_v);
glBindTexture(GL_TEXTURE_2D, m_tex_v);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, m_tex_width/2, m_wnd_height/2, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
glGenerateMipmap(GL_TEXTURE_2D);
2.3上行YUV420数据
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_tex_y);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_tex_width, m_tex_height, GL_RED, GL_UNSIGNED_BYTE, y);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, m_tex_u);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_tex_width/2, m_tex_height/2, GL_RED, GL_UNSIGNED_BYTE, u);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, m_tex_v);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_tex_width/2, m_tex_height/2, GL_RED, GL_UNSIGNED_BYTE, v);
2.4 渲染纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_tex_y);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, m_tex_u);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, m_tex_v);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
2.5 着色器
/顶点着色器///
#version 330 core
layout (location = 0) in vec3 vertex_pos;
layout (location = 1) in vec2 tex_pos;
out vec2 tex_uv;
void main()
gl_Position = vec4(vertex_pos, 1.f);
tex_uv = vec2(tex_pos);
/像素着色器///
#version 330 core
out vec4 frage_color;
in vec2 tex_uv;
uniform sampler2D tex_y;
uniform sampler2D tex_u;
uniform sampler2D tex_v;
void main()
vec3 yuv = vec3(0.f);
//按照BT709的协议来转换YUV至RGB
yuv.x = texture2D(tex_y, tex_uv).r - 16.f/235.f;
yuv.y = texture2D(tex_u, tex_uv).r - 128.f/240.f;
yuv.z = texture2D(tex_v, tex_uv).r - 128.f/240.f;
yuv = clamp(yuv, 0.f, 1.f);
mat3 yuv_to_rgb = mat3(1.164f, 0.f, 1.793f,
1.164f, -0.213f, -0.533f,
1.164f, 2.112f, 0.f);
vec3 rgb = yuv_to_rgb *yuv;
frage_color = vec4(rgb.r, rgb.g, rgb.b, 1.f);
三、代码地址以及存在的问题
demo中使用封装好的 ffmpeg 来获取YUV420P数据,相关代码地址:https://github.com/pengguoqing/samples_code.git
使用demo中的代码渲染后的效果画面会变紫,我尝试了用其他YUV_RGB的转换矩阵,也是一样的会变紫,各位老师看过后能不能纠正一下哪里不正确
本demo的渲染效果如下:
正确渲染效果如下:
四 、解决存在的问题
经过这两天的思考和排查,以及请教行业前辈终于知道原因了,具体如下:
①y、u、v 三张纹理被声明成了 uniform 类型, 所以每次更新纹理数后都需要更新一下
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_tex_y);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_tex_width, m_tex_height, GL_RED, GL_UNSIGNED_BYTE, y);
m_shader_parse.setInt("tex_y", 0);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, m_tex_u);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_tex_width/2, m_tex_height/2, GL_RED, GL_UNSIGNED_BYTE, u);
m_shader_parse.setInt("tex_u", 1);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, m_tex_v);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_tex_width/2, m_tex_height/2, GL_RED, GL_UNSIGNED_BYTE, v);
m_shader_parse.setInt("tex_v", 2);
//或一次性通知更新
//m_shader_parse.setInt("tex_y", 0);
//m_shader_parse.setInt("tex_u", 1);
//m_shader_parse.setInt("tex_v", 2);
② shader里面需要对左乘矩阵进行转置
因为shader里面的向量是列向量,所以 yuv_rgb 的矩阵需要左乘 采样的 yuv纹理数据
/*mat3 yuv_to_rgb = mat3(1.164f, 0.f, 1.793f,
1.164f, -0.213f, -0.533f,
1.164f, 2.112f, 0.f);*/
//709_YUV_to_RGB
mat3 yuv_to_rgb = mat3(1.164f, 1.164f, 1.164f,
0.f, -0.213f, 2.112f,
1.793f, -0.533f, 0.f);
修改后的渲染效果正确了
通过OpenGL理解前端渲染原理
一、OpenGLOpenGL,是一套绘制3D图形的API,当然它也可以用来绘制2D的物体。OpenGL有一大套可以用来操作模型和图片的函数,通常编写OpenGL库的人是显卡的制造者。我们买的显卡都支持特定版本的OpenGL。
下图是用OpenGL做的旋转的立方体。
二、渲染原理
2.1 渲染管道
在OpenGL中,所有东西都在一个3D的空间里,而我们的屏幕和窗口都是2D的,所以OpenGL需要将3D的坐标转换成2D的坐标,做这件事的是OpenGL中的渲染管道(graphics pipeline)。
渲染管道可以分成两大部分:第一部分将3D坐标转换成2D坐标;第二部分把2D的坐标转换成实际的像素。
2.2 着色器
通常来说,渲染管道把一组3D坐标转换成屏幕上带有颜色的2D像素需要经过很多步。上一步的输出作为下一步的输入,所有步骤都是高度专一的,每步都有一个特定的函数,且可以很容易地并发执行。显卡有数千个处理核心来快速处理渲染管道中的数据,而这些是在每个步骤中通过运行在GPU上的多个小程序来处理的,这些小的程序被称之为程序着色器(shader)。
其中的一些着色器是可以配置的,开发者可以根据需求配置自己的着色器去替代已经存在的那些,这就让我们能够更自由和细粒度地控制渲染的过程。同时,因为它们运行在GPU上,又给我们保留了珍贵的GPU时间,在平时的开发中,我们也要充分利用GPU渲染来提高软件性能。
着色器通常使用GLSL来写,全称是OpenGL Shading Language。
2.3 举个例子
下图展示了一个抽象的渲染管线中的步骤,其中蓝色部分是我们可以注入自己的着色器。
通过上图我们发现,要把顶点数据转换成全渲染的像素要经过很多步,接下来我们对每一个步骤和代码进行简单的解释。
我们在渲染管线中传入一组可以组成三角形的3D坐标数据,这组数据即顶点数据。顶点数据是顶点的集合,而一个顶点是一个3D坐标的集合。
渲染管线的第一步是顶点着色器(Vertex Shader)。我们这里传入的是一个简单的顶点,顶点着色器可以让我们做一些基础的处理操作,比如顶点的属性。
在初始装配阶段,也就是Shape Assembly阶段,从顶点着色器中输出的顶点会形成一个原始的形状。本例中,输出的顶点形成的是一个三角形。
从初始装配阶段到geometry shader阶段,我们可以通过发散其他顶点来形成新的图形,本例中形成了第二个三角形。
在Tessellation Shader阶段,可以把上一阶段给出的原型图再分割成若干个小的原型图。本例中,可以形成更多的三角形来创造一个更加平坦、顺滑的环境。这么说可能难以理解,我们结合下图来进一步阐述,这就是细分曲面着色器的作用。
细分曲面着色器的下一阶段是光栅化阶段(Rasterzation stage),在这一阶段会对最终的原型和呈现在屏幕上的对应像素做一个映射,形成fragment,供下一阶段的fragment shader使用。
Fragment shader最主要的使命是计算出一个像素的最终颜色,在这个阶段我们可以使用OpenGL中一些高级的特效。通常fragment shader会包含3D界面的多个数据,包括灯光、阴影、颜色等等。
当所有对应的颜色都确定以后,最终的原型将会被传入最后一个步骤,我们称之为Alpha test and blending阶段。这个阶段会判断相应的深度,比如一个物体可能在另一个物体的后面,那它可能采用其他的颜色;或者如果该物体被遮挡,可能会被裁掉。
如上文所述,我们可以看到整个渲染管线的步骤和逻辑是十分复杂的,这其中包含了很多个可以改变的步骤,但我们一般只操作Vertex Shader 和 fragment shader,其他的着色器我们会直接采用默认的。在实际的OpenGL编程中,我们至少需要定义一个Vertex Shader和Fragment shader。(需要说明的是,OpenGL 3.1之前的版本包含了固定管线,从3.1版本开始,固定管线从核心中删掉了,因此我们必须使用着色器去工作)。
三、总结
本文为该系列文章的第一篇,先简单介绍OpenGL的一些原理,后续文章中会添加新的代码分析,包括着色器(Shader)、纹理(Textture)、变形(transformation)、坐标系统(Coordinate systems)、相机(Camera)等。
作者:崔晓迪
来源:宜信技术学院
以上是关于OpenGL渲染YUV420P的主要内容,如果未能解决你的问题,请参考以下文章
基于FFmpeg的视频播放器之五:使用SDL2渲染yuv420p