小程序 mathjs渲染公式_视频技术加个餐,学习使用OpenGl来渲染视频!

417887b31662568b3a02e6c4696e8041.png

/   今日科技快讯   /

近日,快手短视频携手春晚签约“品牌强国工程”强国品牌服务项目。快手成为中央广播电视总台2020年《春节联欢晚会》独家互动合作伙伴,开展今年的春晚红包互动,在除夕为全国人民送上祝福。快手因此成为首家参与春晚红包活动的短视频平台。 /   作者简介   / 本篇文章来自易水南风的投稿,分享了android开发中如何基于O penGL 来渲染yuv视频的内容,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。 易水南风的博客地址:
https://blog.csdn.net/sinat_23092639
/   前言   / 本篇博文涉及的知识点主要有三个:
  1. yuv的概念

  2. 基于ndk进行C++程序的基本编写

  3. OpenGL纹理的绘制

本文将重点讲知识点1和3,ndk开发部分就不细谈,由于OpenGL知识体系庞大,本文也是根据重点来分析,所以如果没有ndk开发基础和OpenGL基础的读者看本文可能会比较困难。 /   谈谈YUV   / YUV,是一种颜色编码方法。常使用在各个影像处理组件中。Y”表示明亮度(Luminance、Luma),“U”和“V”则是色度、浓度(Chrominance、Chroma)相对我们都比较熟悉的编码格式RGB,RGB诉求于人眼对色彩的感应,YUV则着重于视觉对于亮度的敏感程度。YUV在对照片或影片编码时,考虑到人类的感知能力,允许降低色度的带宽。换句话说,也就是编码的时候允许Y的量比UV要多,允许对图片的UV分量进行下采样,这样数据占用的空间就比RGB更小(关于下采样,简单来说就是以比原来更低的采样率进行采样。 下面图像由Y, U,和V组成: e5a7dfc850df8e2933488862246112c8.png 这里主要讲yuv的两个方面,分别是采样格式和存储格式。采样格式简单可以理解一张原图,每个像素怎么采样yuv各个分量,比如每隔几个像素采一个y分量(或者u、v)。存储格式简单来说就是采样之后,按照什么方式存储,比如哪个字节存储y,第几个字节存储u。 yuv采样格式 文章里面“YUV Sampling”一节详细说明了各种不同格式的yuv是如何采样的。 以下是对该章节的节选翻译: YUV的优点之一是,感知质量不会显著下降的前提下,色度通道的采样率与Y通道的采样率相比更低。一般用一个叫做A:B:C(即y:u:v)的符号用来描述U和V相对于Y的采样频率,为了方便理解,使用图来描述,图中y分量使用x表示,uv使用o表示: 4:4:4 意味着色度通道没有向下采样,也就是说yuv三个通道都是全采样: d946b48642d99d7f4e5b4924c96ccd15.png
4:2:2
表示2:1水平下采样,没有垂直下采样。每条扫描线包含四个Y样本对应两个U或V样本。也就是水平方向按照y:uv使用2:1进行采样,垂直方向全采样的方式: 0de468ef7f5bf973c0ffd7e2251e98d9.png
4:2:0
表示2:1水平下采样,2:1垂直下采样。也就是水平方向按照y:uv使用2:1进行采样,垂直方向按照y:uv使用2:1的方式: 9d81a8e86b4af89127024ab9cbc3935d.png 注意这里4:2:0并不代表y:u:v = 4:2:0,这里指的是在每一行扫描时,只扫描一种色度分量(U 或者 V),和 Y 分量按照 2 : 1 的方式采样。比如,第一行扫描时,YU 按照 2 : 1 的方式采样,那么第二行扫描时,YV 分量按照 2:1 的方式采样。所以y和u或者v的比都是2:1。 4:1:1 表示4:1水平下采样,没有垂直下采样。每条扫描线包含四个Y样本对应于每一个U或V样本。 4:1:1抽样比其他格式更少见,本文不详细讨论。 yuv存储格式 YUV存储格式有两大类:planar 和 packed。 packed:Y、U和V组件存储在一个数组中。每个像素点的Y,U,V是连续交错存储的。和RGB的存储格式类似。planar :Y、U和V组件存储为三个独立的数组中。y、u、v每个采样点使用8bit存储。接下来详细讲下集中常见的yuv格式存储方式。4:2:2格式主要有两种具体格式: YUY2 属于packed类型,YUY2格式,数据可视为unsigned char数组。第一个字节包含第一个Y样本,第二个字节包含第一U (Cb)样本,第三字节包含第二Y样本,第四个字节包含第V (Cr)样本,以此类推,如图: ab65461cd5241345445ea8ad10f0cd1c.png 可以看到,Y0 和 Y1 公用 U0 V0 分量,Y2 和 Y3 公用 U1 V1 分量,以此类推。
UYVY
也是属于属于packed类型的,和YUY2和类似,只是存储方向是相反的: 77060b476c0fca844aa4cf91e821fac1.png 4:2:0格式又包含多种存储方式,这里重点将以下几种: YUV 420P 和 YUV 420SP 都是基于 Planar 平面模式 进行存储的,先存储所有的 Y 分量后, YUV420P 类型就会先存储所有的 U 分量或者 V 分量,而 YUV420SP 则是按照 UV 或者 VU 的交替顺序进行存储了,具体查看看下图
YUV420P
(这里需要敲黑板,因为本文播放的yuv就是YUV420P格式,熟悉它的存储格式才可以理解代码中读取视频帧数据的逻辑) e382b6b148658db4c2e92fbf6171fc6e.png 正是因为 YUV420P是2:1水平下采样,2:1垂直下采样,所以y分量数量等于视频宽高,u和v分量都是视频宽乘以高/4。
YUV420SP
4:2:0格式还有YV12、YU12、NV12 、NV21等存储格式,这里因为篇幅关系就不做细谈。

yuv转RGB

目前一般解码后的视频格式为yuv,但是一般显卡渲染的格式是RGB,所以需要把yuv转化为RGB。关于yuv转RGB这里有个公式可以自己使用: 3cd89cbd28c8b3bdafe06bd6c9958531.png 或者直接用yuv的矩阵乘以以下矩阵得到对应的RGB矩阵: 33716c7042392deb177c8ccb3e191ccd.png yuv就先介绍到这里,熟悉yuv对于后面yuv视频播放至关重要。 /   谈谈OpenGL   / OpenGL是行业领域中最为广泛接纳的 2D/3D 图形 API。OpenGL是一个跨平台的软件接口语言,用于调用硬件的2D、3D图形处理器。由于只是软件接口,所以具体底层实现依赖硬件设备制造商。

关于OpenGL的知识,可能写20篇博文也介绍不完,这里只介绍和当前播放yuv相关的,不会很详细,详细教程可以看这个网站:

https://www.zhihu.com/question/19913939
安卓使用的是OpenGL ES版本,即OpenGL的一个子集,裁剪了一些功能,专门使用在嵌入式设备。 OpenGL图形渲染管线 首先要解释的是OpenGL的图形渲染管线:指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程。分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。 图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。 当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的相互独立的并行处理小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader),因为它们运行在GPU中,所以解放了CPU的省生产力。 图形渲染管线的每个阶段的展示: c16a4c0f20994c9bbfac547798380884.png 图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(坐标系统的转化),同时顶点着色器允许我们对顶点属性进行一些基本处理。顶点着色器代码是每个顶点执行一次。 图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状。比如将顶点装配为三角形或者矩形。 几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment,OpenGL渲染一个像素所需的所有数据)。 片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。片段着色器是每个片段(像素)执行一次。 而我们要处理的,主要就是顶点着色器和片段着色器的代码逻辑,着色器是用叫GLSL的类C语言写成的,它包含一些针对向量和矩阵操作的有用特性。

OpenGL坐标系

要写顶点着色器代码,首先就要知道OpenGL顶点坐标系: 按照惯例,OpenGL是一个右手坐标系。简单来说,就是正x轴在你的右手边,正y轴朝上,而正z轴是朝向后方的。想象你的屏幕处于三个轴的中心,则正z轴穿过你的屏幕朝向你: 9ab69cbfa880b11241d06b0461d2841f.png (这里要提的一点事,OpenGL在执行顶点着色器之后,会像流水线一样将坐标进行5个步骤的变换:局部坐标–世界坐标–观察坐标–裁剪坐标–屏幕坐标,这里因为实例是2D的,暂时还不需要关心这些) 现在需要记得的是,OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。所有在所谓的标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。 2D情况下,既不考虑z轴,则一般来说顶点坐标系如下所示: 60e568a99f3634602109026daa141be6.png OpenGL纹理绘制 通过顶点着色器和片段着色器,我们可以指定要绘制的物体形状大小以及颜色,但是如果我们要做类似将一张图片绘制上去,该如何做呢? OpenGL提供了纹理这个概念,让你可以将一张图片“贴”到你想要的位置。 那么纹理是如何“贴”到图形上去的呢?其实就是对图片进行采样,再将采样到的颜色数据绘制到图形相应的位置。 为了能够把纹理映射(Map)到我们的图形上,我们需要指定图形的每个顶点各自对应纹理的哪个部分。所以图形的每个顶点都会关联一个纹理的坐标,用来标明该从纹理图像的哪个部分采样。 通俗来说,就是比方你顶点坐标提供的是一个矩形,现在要将一张图片(纹理)“贴”到矩形上,那么需要指定一个纹理坐标,告诉OpenGL矩形光栅化处理后的每个片段对应图片的哪个像素的颜色。纹理坐标,简单来说就是以一张纹理图片的某个点作为原点的坐标系。类似下图所示: d9cbe7f09a838c2093ebb756262880d8.png 由上图可以看到纹理坐标系的模样了,不过在Android平台,纹理坐标如下: 9f513d86c5e9f3c9788319decfa2e118.png 即以图片的左上角为原点的坐标系。 所以在提供了顶点坐标和纹理坐标之后,OpenGL就知道如何通过采样纹理上的像素的颜色数据,将颜色绘制到顶点坐标所表达的图形上的对应位置。 /   程序实例分析   / 所谓工欲善其事必先利其器,基础知识讲得差不多了,那么又要进入最重要的将代码环节了,这里使用的yuv格式为yuv420p。 这里使用cmake进行构建,native-lib为项目自定义的动态库名称,其余需要链接的动态库如下配置:
find_library( # Sets the name of the path variable.
              log-lib
              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )
target_link_libraries( # Specifies the target library.native-lib
                       GLESv2
                       EGL
                       android
                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )
Java层首先创建一个集成GLSurfaceView的类:
public class YuvPlayer extends GLSurfaceView implements Runnable, SurfaceHolder.Callback, GLSurfaceView.Renderer {//这里将yuv视频文件放在sdcard目录中private final static String PATH = "/sdcard/sintel_640_360.yuv";public YuvPlayer(Context context, AttributeSet attrs) {super(context, attrs);
        setRenderer(this);
    }@Overridepublic void surfaceCreated(SurfaceHolder holder) {new Thread(this).start();
    }@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {
    }@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
    }@Overridepublic void run() {
        loadYuv(PATH,getHolder().getSurface());
    }//定义一个native方法加载yuv视频文件public native void loadYuv(String url, Object surface);@Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {
    }@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {
    }@Overridepublic void onDrawFrame(GL10 gl) {
    }
}
进入native层的loadYuv方法:
Java_com_example_yuvopengldemo_YuvPlayer_loadYuv(JNIEnv *env, jobject thiz, jstring jUrl,
                                                 jobject surface) {const char *url = env->GetStringUTFChars(jUrl, 0);//打开yuv视频文件 
    FILE *fp = fopen(url, "rb");if (!fp) {//打Log方法
        LOGD("oepn file %s fail", url);return;
    }
    LOGD("open ulr is %s", url);
首先是从Java层传入的jstring变量转为char*,然后打开yuv视频文件。接下来是初始化EGL。这里简单解释下EGL是什么。 EGL是Khronos呈现api(如OpenGL ES或OpenVG)与底层本机平台窗口系统之间的接口。它处理图形上下文管理、表面/缓冲区绑定和呈现同步,并使用其他Khronos api支持高性能、加速、混合模式的2D和3D呈现。EGL还提供了Khronos之间的互操作能力,以支持在api之间高效地传输数据——例如在运行OpenMAX AL的视频子系统和运行OpenGL ES的GPU之间。 通俗来讲就是,EGL是渲染API(如OpenGL, OpenGL ES, OpenVG)和本地窗口系统之间的接口。EGL可以理解为OpenGL ES ES和设备之间的桥梁,EGL是为OpenGL提供绘制表面的。因为OpenGL是跨平台的,当它访问不同平台的设备的时候需要EGL作为中间的适配器。 df447fc70adb1471cd6da938d02f8f10.png EGL的使用步骤: 76d24ed7a5d2a355423e0002f46f8e3d.png 具体的代码:
//1.获取原始窗口
ANativeWindow *nwin = ANativeWindow_fromSurface(env, surface);//获取OpenGl ES的渲染目标。Display(EGLDisplay) 是对实际显示设备的抽象。
    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);if (display == EGL_NO_DISPLAY) {
        LOGD("egl display failed");return;
    }//2.初始化egl与 EGLDisplay 之间的连接,后两个参数为主次版本号if (EGL_TRUE != eglInitialize(display, 0, 0)) {
        LOGD("eglInitialize failed");return;
    }//创建渲染用的surface//2.1 surface配置
    EGLConfig eglConfig;
    EGLint configNum;
    EGLint configSpec[] = {
            EGL_RED_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE, 8,
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
            EGL_NONE
    };if (EGL_TRUE != eglChooseConfig(display, configSpec, &eglConfig, 1, &configNum)) {
        LOGD("eglChooseConfig failed");return;
    }//2.2创建surface(将egl和NativeWindow进行关联,即将EGl和设备屏幕连接起来。最后一个参数为属性信息,0表示默认版本)。Surface(EGLSurface)是对用来存储图像的内存区FrameBuffer 的抽象。这就是我们要渲染的Surface
    EGLSurface winSurface = eglCreateWindowSurface(display, eglConfig, nwin, 0);if (winSurface == EGL_NO_SURFACE) {
        LOGD("eglCreateWindowSurface failed");return;
    }//3 创建关联上下文const EGLint ctxAttr[] = {
            EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
    };//创建egl关联OpenGl的上下文环境 EGLContext 实例。EGL_NO_CONTEXT表示不需要多个设备共享上下文。Context (EGLContext) 存储 OpenGL ES绘图的一些状态信息。上面的代码只是egl和设备窗口的关联,这里是和OpenGl的关联
    EGLContext context = eglCreateContext(display, eglConfig, EGL_NO_CONTEXT, ctxAttr);if (context == EGL_NO_CONTEXT) {
        LOGD("eglCreateContext failed");return;
    }//将EGLContext和opengl真正关联起来。绑定该线程的显示设备及上下文//两个surface一个读一个写。if (EGL_TRUE != eglMakeCurrent(display, winSurface, winSurface, context)) {
        LOGD("eglMakeCurrent failed");return;
    }
创建初始化EGL,接下来就是真正的OpenGL绘制代码。先看下着色器代码。看着色器代码之前,先了解下GLSL一些基础:常见的变量类型:
  • attritude:一般用于各个顶点各不相同的量。如顶点位置、纹理坐标、法向量、颜色等等。

  • uniform:一般用于对于物体中所有顶点或者所有的片段都相同的量。比如光源位置、统一变换矩阵、颜色等。

  • varying:表示易变量,一般用于顶点着色器传递到片段着色器的量。

  • vec2:包含了2个浮点数的向量

  • vec3:包含了3个浮点数的向量

  • vec4:包含了4个浮点数的向量

  • sampler1D:1D纹理着色器

  • sampler2D:2D纹理着色器

  • sampler3D:3D纹理着色器

首先编写顶点着色器代码。
//顶点着色器,每个顶点执行一次,可以并行执行
#define GET_STR(x) #xstatic const char *vertexShader = GET_STR(
        attribute vec4 aPosition;//输入的顶点坐标,会在程序指定将数据输入到该字段
        attribute vec2 aTextCoord;//输入的纹理坐标,会在程序指定将数据输入到该字段
        varying vec2 vTextCoord;//输出的纹理坐标,输入到片段着色器void main() {//这里其实是将上下翻转过来(因为安卓图片会自动上下翻转,所以转回来。也可以在顶点坐标中就上下翻转)
            vTextCoord = vec2(aTextCoord.x, 1.0 - aTextCoord.y);//直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的
            gl_Position = aPosition;
        }
);
这里逻辑很简单。使用两个attribute变量,一个接受顶点坐标,一个接收纹理坐标,这里以标准的OpenGL的纹理坐标为标准,即和安卓平台是上下翻转关系的(在本文OpenGL纹理绘制一节有说到),所以对传进来的纹理坐标在0.0~1。0之间进行上下翻转,再赋值给varying类型变量vTextCoord,vTextCoord将通过渲染管线传给片段着色器。最后将传进来的顶点坐标赋值给gl_Position ,gl_Position 是OpenGL内置的表示顶点坐标的变量。gl_Position 被赋值之后,将通过渲染管线传给后面的阶段,在图元装配的时候,将顶点连接起来。在光栅化图元的时候,将两个顶点之间的线段分解成大量的小片段,varying数据在这个过程中计算生成,记录在每个片段中,之后传递给片段着色器。 然后编写片段着色器代码:
//图元被光栅化为多少片段,就被调用多少次static const char *fragYUV420P = GET_STR(
        precision mediump float;//接收从顶点着色器、光栅化处理传来的纹理坐标数据
           varying vec2 vTextCoord;//输入的yuv三个纹理
        uniform sampler2D yTexture;//y分量纹理
        uniform sampler2D uTexture;//u分量纹理
        uniform sampler2D vTexture;//v分量纹理void main() {//存放采样之后的yuv数据
            vec3 yuv;//存放yuv数据转化后的rgb数据
            vec3 rgb;//对yuv各个分量对应vTextCoord的像素进行采样。这里texture2D得到的结果是一个vec4变量,它的r、g、b、a的值都为采样到的那个分量的值//将采样到的y、u、v分量的数据分别保存在vec3 yuv的r、g、b(或者x、y、z)分量
             yuv.r = texture2D(yTexture, vTextCoord).g;
            yuv.g = texture2D(uTexture, vTextCoord).g - 0.5;
            yuv.b = texture2D(vTexture, vTextCoord).g - 0.5;//这里必须把yuv转化为RGB
            rgb = mat3(1.0, 1.0, 1.0,0.0, -0.39465, 2.03211,1.13983, -0.5806, 0.0
            ) * yuv;//gl_FragColor是OpenGL内置的,将rgb数据赋值给gl_FragColor,传到渲染管线的下一阶段 ,gl_FragColor 表示正在呈现的像素的 R、G、B、A 值。 
            gl_FragColor = vec4(rgb, 1.0);
        }
);
这里要将yuv三个分量分别用三层纹理来渲染,然后将多层纹理混合一起显示。代码中三个sampler2D类型变量就是纹理图片,需要从外部程序传入。然后通过texture2D方法采样得到对应纹理坐标位置的颜色数据,将yuv三个分量的采样值放入vec3 类型变量yuv的三个分量中,因为OpenGL只支持RGB的渲染,所以需要将vec3类型的 yuv通过公式转为一个rgb 的vec3 类型变量。最后将rgb 变量构建一个vec4变量,作为最终颜色赋值给gl_FragColor 。 着色器代码定义完,接下来就是渲染逻辑部分。 首先是将前面的定义的着色器加载、编译以及创建、链接、激活着色器程序:
GLint vsh = initShader(vertexShader, GL_VERTEX_SHADER);
    GLint fsh = initShader(fragYUV420P, GL_FRAGMENT_SHADER);//创建渲染程序
    GLint program = glCreateProgram();if (program == 0) {
        LOGD("glCreateProgram failed");return;
    }//向渲染程序中加入着色器
    glAttachShader(program, vsh);
    glAttachShader(program, fsh);//链接程序
    glLinkProgram(program);
    GLint status = 0;
    glGetProgramiv(program, GL_LINK_STATUS, &status);if (status == 0) {
        LOGD("glLinkProgram failed");return;
    }
    LOGD("glLinkProgram success");//激活渲染程序
    glUseProgram(program);
其中initShader函数:
GLint initShader(const char *source, GLint type) {//创建shader
    GLint sh = glCreateShader(type);if (sh == 0) {
        LOGD("glCreateShader %d failed", type);return 0;
    }//加载shader
    glShaderSource(sh,1,//shader数量
                   &source,0);//代码长度,传0则读到字符串结尾//编译shader
    glCompileShader(sh);
    GLint status;
    glGetShaderiv(sh, GL_COMPILE_STATUS, &status);if (status == 0) {
        LOGD("glCompileShader %d failed", type);
        LOGD("source %s", source);return 0;
    }
    LOGD("glCompileShader %d success", type);return sh;
}
传入顶点坐标数组给顶点着色器:
//加入三维顶点数据。这里就是整个屏幕的矩形。static float ver[] = {1.0f, -1.0f, 0.0f,
            -1.0f, -1.0f, 0.0f,1.0f, 1.0f, 0.0f,
            -1.0f, 1.0f, 0.0f
    };//获取顶点着色器的aPosition属性引用
    GLuint apos = static_cast(glGetAttribLocation(program, "aPosition"));
    glEnableVertexAttribArray(apos);//将顶点坐标传入顶点着色器的aPosition属性//各个参数意义:apos:顶点着色器中aPosition变量的引用。3表示数组中三个数字表示一个顶点。GL_FLOAT表示数据类型是浮点数。//GL_FALSE表示不进行归一化。0表示stride(跨距),在数组表示多种属性的时候使用到,这里因为这有一个属性,设置为0即可。ver表示所传入的顶点数组地址
    glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 0, ver);
传入纹理坐标数组给顶点着色器:
//加入纹理坐标数据,这里是整个纹理。static float fragment[] = {1.0f, 0.0f,0.0f, 0.0f,1.0f, 1.0f,0.0f, 1.0f
    };将纹理坐标数组传入顶点着色器的aTextCoord属性
    GLuint aTex = static_cast(glGetAttribLocation(program, "aTextCoord"));
    glEnableVertexAttribArray(aTex);//各个参数意义:aTex :顶点着色器中aTextCoord变量的引用。2表示数组中三个数字表示一个顶点。GL_FLOAT表示数据类型是浮点数。//GL_FALSE表示不进行归一化。表示stride(跨距),在数组表示多种属性的时候使用到,这里因为这有一个属性,设置为0即可。fragment表示所传入的顶点数组地址
    glVertexAttribPointer(aTex, 2, GL_FLOAT, GL_FALSE, 0, fragment);
如果能把传入顶点坐标数组给顶点着色器理解,这一段就没有什么难度了。 接着是纹理对象的处理: 这里要讲一下几个概念:纹理对象、纹理目标、纹理单元。 纹理对象是我们创建的用来存储纹理的显存,在实际使用过程中使用的是创建后返回的纹理ID。纹理目标可以简单理解为纹理的类型,比如指定是渲染2D还是3D等。纹理单元:纹理的操作容器,有GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2等,纹理单元的数量是有限的,最多16个。所以在最多只能同时操作16个纹理。可以简单理解为第几层纹理。 创建纹理对象:
 //指定纹理变量在哪一层纹理单元渲染
   glUniform1i(glGetUniformLocation(program, "yTexture"), GL_TEXTURE0);
    glUniform1i(glGetUniformLocation(program, "uTexture"), GL_TEXTURE1);
    glUniform1i(glGetUniformLocation(program, "vTexture"), GL_TEXTURE2);//纹理ID
    GLuint texts[3] = {0};//创建3个纹理对象,并且得到各自的纹理ID。之后对纹理的操作就可以通过该纹理ID进行。
    glGenTextures(3, texts);
将纹理对象和相应的纹理目标进行绑定:
 //yuv视频宽高int width = 640;int height = 360;//通过 glBindTexture 函数将纹理目标和以texts[0]为ID的纹理对象绑定后,对纹理目标所进行的操作都反映到该纹理对象上
    glBindTexture(GL_TEXTURE_2D, texts[0]);//缩小的过滤器(关于过滤详细可见 [纹理](https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/))
    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,//指定要Mipmap的等级
                 GL_LUMINANCE,//gpu内部格式,告诉OpenGL内部用什么格式存储和使用这个纹理数据。 亮度,灰度图(这里就是只取一个亮度的颜色通道的意思,因这里只取yuv其中一个分量)
                 width,//加载的纹理宽度。最好为2的次幂
                 height,//加载的纹理高度。最好为2的次幂0,//纹理边框
                 GL_LUMINANCE,//数据的像素格式 亮度,灰度图
                 GL_UNSIGNED_BYTE,//一个像素点存储的数据类型
                 NULL //纹理的数据(先不传,等后面每一帧刷新的时候传)
    );
这里要注意视频的宽高一定设置正确,不然渲染的数据就都是错误的。 这里要说明下glTexImage2D第三个参数,告诉OpenGL内部用什么格式存储和使用这个纹理数据(一个像素包含多少个颜色成分,是否压缩)。常用的常量如下: 43ade62f9fb6aa68466dbd0b500dbe90.png 这里yuv三个分量的代码都是一样的,只是传入的宽高不同,对于u和v来说,宽高各位视频宽高的二分之一:
//设置纹理的格式和大小
    glTexImage2D(GL_TEXTURE_2D,0,//细节基本 默认0
                 GL_LUMINANCE,//gpu内部格式 亮度,灰度图(这里就是只取一个颜色通道的意思)
                 width / 2,
                 height / 2,//v数据数量为屏幕的4分之10,//边框
                 GL_LUMINANCE,//数据的像素格式 亮度,灰度图
                 GL_UNSIGNED_BYTE,//像素点存储的数据类型
                 NULL //纹理的数据(先不传)
    );
为什么是width / 2,height / 2呢?还记得上文说过的yuv420p的采样和存储格式么?YUV420P是2:1水平下采样,2:1垂直下采样,所以y分量数量等于视频宽乘以高,u和v分量都是视频宽/2乘以高/2。 从视频文件中读取yuv数据到内存中:
unsigned char *buf[3] = {0};
    buf[0] = new unsigned char[width * height];//y
    buf[1] = new unsigned char[width * height / 4];//u
    buf[2] = new unsigned char[width * height / 4];//v//循环读出每一帧for (int i = 0; i 10000; ++i) {//读一帧yuv420p数据if (feof(fp) == 0) {//读取y数据
            fread(buf[0], 1, width * height, fp);//读取u数据
            fread(buf[1], 1, width * height / 4, fp);//读取v数据
            fread(buf[2], 1, width * height / 4, fp);
        }
还是回顾刚才敲黑板的地方,由图可得yuv420p中,是先存储视频宽高个y元素,再存储视频宽乘以高/4个u,再存储视频宽乘以高/4个v,所以for循环中读取一帧才按照yuv的顺序和数量依次读到内存的数组中。 e382b6b148658db4c2e92fbf6171fc6e.png 在读出一帧后,更新数据到纹理对象上。buf[0]即y分量的数据渲染到纹理上:
//激活第一层纹理,绑定到创建的纹理
        glActiveTexture(GL_TEXTURE0);//绑定y对应的纹理
        glBindTexture(GL_TEXTURE_2D, texts[0]);//替换纹理,比重新使用glTexImage2D性能高多
        glTexSubImage2D(GL_TEXTURE_2D, 0,0, 0,//相对原来的纹理的offset
                        width, height,//加载的纹理宽度、高度。最好为2的次幂
                        GL_LUMINANCE, GL_UNSIGNED_BYTE,
                        buf[0]);
u和v也是一样,只是宽高换为width / 2, height / 2。最后将画面显示出来:
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);//窗口显示,交换双缓冲区
eglSwapBuffers(display, winSurface);
如此循环,就将每一帧渲染出来,也就播放了yuv视频。这里我使用ffmpeg命令将《龙猫》中截取10秒的视频转化为yuv,录屏的gif不知为何总是上传不了,所以这里只上传了一张截图 = = 。 ff252003f8e725af205a13314d7ad139.png 虽然只是10秒的视频,但是已经超过github的最大上传量,所以视频没有上传。各位如果需要可以自己用ffmpeg命令转换任何一个格式支持视频文件为yuv420p格式来运行。

接触音视频开发领域时间不长,如有错误疏漏,请各位指正~项目地址:

https://github.com/yishuinanfeng/YuvVideoPlayerDemo
推荐阅读: 使用HashMap不如使用SparseArray? 网络请求只会用Retrofit?外国人已经在用Graphql了 给TextView文本加标签,小功能大秘密 欢迎关注我的公众号 学习技术或投稿

1f4f2b805e0c3ed304eeff7f7a172478.png

bf01a4bd4e32e6f75d723d7ae7f10fd0.png 长按上图,识别图中二维码即可关注
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值