windows下使用OpenGL实现yuv420p转rgb播放视频(三重纹理实现)

一、简述

  在博主之前的博文《windows下使用FFmpeg生成YUV视频文件并播放(通过命令的方式)》中,讲述了使用ffplay播放YUV视频文件的方法。本文讲述使用OpenGL播放YUV(yuv420p)文件的方法。

二、代码

  为了降低代码的复杂度和让不懂FFmpeg的新手便于理解,在本文演示的代码中没有使用FFmpeg的api进行解码,而是首先通过FFmpeg命令将mp4文件转成包含yuv420p数据的原始视频文件,再通过代码读取该文件,使用OpenGL的shader将读取到的YUV数据转成RGB数据再播放显示。

代码使用visual studio可以编译,核心代码(整个工程可以在https://download.csdn.net/download/u014552102/20065011?spm=1001.2014.3001.5501下载,建议下载,否则可能会因为缺少某些库导致编译不通过)如下:

ggl.h

#pragma once
#include 
#include "glew.h"
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "glm.hpp"
#include "ext.hpp"

GLContext.h

#pragma once

//#include 
#include "ggl.h"

class GLContext
{
protected:
    int _format;
    HWND _hWnd;          //窗口句柄
    HDC _hDC;            //绘制设备上下文
    HGLRC _hRC;          //OpenGL上下文
public:
    GLContext()
    {
        _format = 0;
        _hWnd = 0;
        _hDC = 0;
        _hRC = 0;
    }
    ~GLContext()
    {
        shutdown();
    }
    bool setup(HWND hWnd, HDC hDC)         //初始化OpenGL
    {
        _hWnd = hWnd;
        _hDC = hDC;
        unsigned PixelFormat;
        PIXELFORMATDESCRIPTOR pfd =        //像素格式结构变量
        {
            sizeof(PIXELFORMATDESCRIPTOR), //pfd的大小
            1,                             //这里固定设置为1,不要管为什么,微软也没有解释为什么。
            PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, //支持windows,支持opengl,支持双缓冲区
            PFD_TYPE_RGBA,                 //当前采用RGBA颜色模式
            32,                            //32位的颜色深度。即一个像素占4个字节,R占8位,G占8位,B占8位,A占8位
            0,
            0,
            0, 
            0,
            0,
            0, 
            0,
            0,
            0, 
            0, 
            0, 
            0, 
            0, 
            24,                            //深度缓存设置为24位,这个缓存能解决三维场景的消隐问题
            8,                             //模板缓冲区占用8位
            0,
            PFD_MAIN_PLANE, 
            0,
            0, 
            0, 
            0
        };
        if(_format == 0)
        {
            PixelFormat = ChoosePixelFormat(_hDC, &pfd); //选择一个像素格式,并将像素格式索引号返回给pixelFormat变量
        }
        else
        {
            PixelFormat	= _format;
        }
        if(!SetPixelFormat(_hDC, PixelFormat, &pfd))     //设置像素格式
        {
            return false;
        }
        _hRC = wglCreateContext(_hDC);                   //创建一个新的OpenGL渲染描述表
        if(!wglMakeCurrent(_hDC, _hRC))                  //使一个指定的OpenGL渲染上下文调用线程的当前呈现上下文
        {
            return false;
        }
        return true;
    }
    void shutdown()           //销毁EGL
    {
        if( _hRC != NULL )
        {
            wglMakeCurrent( NULL, NULL );
            wglDeleteContext( _hRC );
            _hRC = NULL;							
        }
        if( _hDC != NULL )
        {
            ReleaseDC( _hWnd, _hDC );
            _hDC = NULL;
        }
    }
    void swapBuffer()      //交换缓冲区
    {
        SwapBuffers(_hDC); //交换opengl前后的两个缓冲区。一个对应的是前面的屏幕的缓存,一个对应的是后面的缓存,之所以用交换的方式,是内部进行了指针的交换,如此速度很快 
    }

};

GLVideo.h

#pragma once
#include "ggl.h"
#include "vertexbuffer.h"
#include "FFVideoReader.hpp"
#include "shader.h"

struct VideoVertex                 //存放视频顶点坐标和纹理坐标的结构体
{
	float x, y;                    //顶点坐标
	float u, v;                    //纹理坐标
};

class GLVideo
{
	VertexBuffer *mVertexBuffer;      //使用时申请动态数组,存放视频中所有的顶点坐标和纹理坐标
	Shader *mShader;
	int mVideoWidth;                  //纹理图片(视频)的宽度
	int mVideoHeight;                 //纹理图片(视频)的高度
public:
	GLVideo();
	void Init(int VideoWidth, int VideoHeight);
	void Draw(int ClientWidth, int ClientHeight, FrameInfor *infor);
};

GLVideo.cpp

#include "GLVideo.h"
#include "utils.h"
#include "CELLMath.hpp"

GLVideo::GLVideo()
{
}

/**
* @brief 初始化视频的opengl部分
* @param[in] VideoWidth : 纹理图片(视频)的宽度
* @param[in] VideoHeight : 纹理图片(视频)的高度
* @return 无
*/
void GLVideo::Init(int VideoWidth, int VideoHeight)
{
	mVertexBuffer = new VertexBuffer;
	mVertexBuffer->SetSize(4);
	mShader = new Shader;
	mShader->Init("Res/video.vs", "Res/video.fs");
	mVideoWidth = VideoWidth;
	mVideoHeight = VideoHeight;
	mShader->SetTexture("U_TextureY", mVideoWidth, mVideoHeight);
	mShader->SetTexture("U_textureU", mVideoWidth /2, mVideoHeight /2);
	mShader->SetTexture("U_textureV", mVideoWidth /2, mVideoHeight /2);
}

/**
* @brief 使用opengl绘制视频
* @param[in] ClientWidth : 窗口客户区的宽度
* @param[in] ClientHeight : 窗口客户区的高度
* @param[in] infor : 使用FFmpeg解码出来的一帧数据的信息
* @return 无
*/
void GLVideo::Draw(int ClientWidth, int ClientHeight, FrameInfor *infor)
{
	CELL::matrix4 matMVP = CELL::ortho(0, ClientWidth, ClientHeight, 0, -100, 100); //MVP矩阵

	VideoVertex vertexs[] =        //视频有四个顶点坐标和纹理坐标
	{
		{ 0,                     0,  0,  0 },
		{ 0,          ClientHeight,  0,  1 },
		{ ClientWidth,           0,  1,  0 },
		{ ClientWidth,ClientHeight,  1,  1 },
	};

	mVertexBuffer->SetPosition(0, vertexs[0].x, vertexs[0].y, 0);
	mVertexBuffer->SetPosition(1, vertexs[1].x, vertexs[1].y, 0);
	mVertexBuffer->SetPosition(2, vertexs[2].x, vertexs[2].y, 0);
	mVertexBuffer->SetPosition(3, vertexs[3].x, vertexs[3].y, 0);

	mVertexBuffer->SetTexcoord(0, vertexs[0].u, vertexs[0].v);
	mVertexBuffer->SetTexcoord(1, vertexs[1].u, vertexs[1].v);
	mVertexBuffer->SetTexcoord(2, vertexs[2].u, vertexs[2].v);
	mVertexBuffer->SetTexcoord(3, vertexs[3].u, vertexs[3].v);

	mShader->UpdateTexture("U_TextureY", mVideoWidth, mVideoHeight, infor->m_datas[0]);
	mShader->UpdateTexture("U_textureU", mVideoWidth / 2, mVideoHeight / 2, infor->m_datas[1]);
	mShader->UpdateTexture("U_textureV", mVideoWidth / 2, mVideoHeight / 2, infor->m_datas[2]);
	
	mVertexBuffer->Bind();
	mShader->Bind((float*)(matMVP.data()));
	glDrawArrays(GL_TRIANGLE_STRIP, 0, mVertexBuffer->mVertexCount); //从数组缓存中的第0位开始,绘制mVertexBuffer->mVertexCount个点。调用该函数之前需要调用glEnableVertexAttribArray、glVertexAttribPointer等函数设置顶点属性和数据
	mVertexBuffer->Unbind();
}

shader.h

#pragma once
#include "ggl.h"

struct UniformTexture  //存放shader纹理相关信息的类
{
public:
	UniformTexture()
	{
		mLocation = -1;
		mTexture = 0;
	}
public:
	GLint mLocation;    //U_TextureY、U_textureU、U_textureV的插槽(位置信息)
	GLuint mTexture;    //纹理对象
};


class Shader             //包含跟shader相关操作的类
{
public:
	GLuint mProgram;     //存放program ID
	std::map mUniformTextures;  //容器,存贮多张纹理
	GLint mMVPMatrixLocation;                                  //MVPMatrix的插槽
	GLint mPositionLocation, mColorLocation, mTexcoordLocation;//position的插槽,color的插槽,texcoord的插槽,normal的插槽
	void Init(const char *vs, const char *fs);
	void Bind(float *MVP);
	void SetTexture(const char *name, const char *imagePath);
	void SetTexture(const char * name, GLuint texture);
	void SetTexture(const char *name, int w, int h);
	void UpdateTexture(const char *name, int w, int h, const void *pixels);
};

shader.cpp

#include "shader.h"
#include "utils.h"
#include "vertexbuffer.h"


/**
* @brief 初始化shader相关的程序
* @param[in] vs : 存放vertex shader代码的文件路径
* @param[in] fs : 存放fragment shader代码的文件路径
* @return 无
*/
void Shader::Init(const char *vs, const char *fs) 
{
	int nFileSize = 0;
	const char *vsCode = (char *)LoadFileContent(vs,nFileSize);    //加载路径为vs的文件到内存中
	const char *fsCode = (char *)LoadFileContent(fs,nFileSize);    //加载路径为fs的文件到内存中
	GLuint vsShader = CompileShader(GL_VERTEX_SHADER, vsCode);     //编译VERTEX_SHADER,shader代码存放在指针vsCode指向的内存块中
	if (vsShader==0)
	{
		return;
	}
	GLuint fsShader = CompileShader(GL_FRAGMENT_SHADER, fsCode);   //编译FRAGMENT_SHADER,shader代码存放在指针fsCode指向的内存块中
	if (fsShader == 0) 
	{
		return;
	}
	mProgram=CreateProgram(vsShader, fsShader);                    //将编译好的shader链接成一个绘制图形的程序(创建GPU程序)
	glDeleteShader(vsShader);                                      //删除着色器对象“vsShader”。释放内存并使与着色器指定的着色器对象关联的ID无效
	glDeleteShader(fsShader);                                      //删除着色器对象“fsShader”。释放内存并使与着色器指定的着色器对象关联的ID无效
	if (mProgram!=0)
	{
		mMVPMatrixLocation = glGetUniformLocation(mProgram, "MVPMatrix");

		mPositionLocation = glGetAttribLocation(mProgram, "position");        //查询由mProgram指定的先前链接的程序对象,用于"position"指定的属性变量,并返回绑定到该属性变量的通用顶点属性的索引mPositionLocation
		mColorLocation = glGetAttribLocation(mProgram, "color");
		mTexcoordLocation = glGetAttribLocation(mProgram, "texcoord");
	}
}


/**
* @brief 对shader程序进行绑定操作
* @param[in] MVP 要绑定的MVP矩阵的地址
* @return 无
*/
void Shader::Bind(float* MVP)
{
	glUseProgram(mProgram);    //使用程序对象mProgram作为当前渲染状态的一部分
	glUniformMatrix4fv(mMVPMatrixLocation, 1, GL_FALSE, MVP); //设置uniform变量。设置1个4*4的矩阵,矩阵从CPU到GPU的传输过程中不需要转置,数据的起始地址为M

	int iIndex = 0;
	for (auto iter = mUniformTextures.begin(); iter != mUniformTextures.end(); ++iter)
	{
		glActiveTexture(GL_TEXTURE0 + iIndex);                 //设置激活的纹理单元
		glBindTexture(GL_TEXTURE_2D, iter->second->mTexture);  //告诉OpenGL下面代码中对2D纹理的任何设置都是针对索引为iter->second->mTexture的纹理的。当纹理iter->second->mTexture被绑定后,对于GL_TEXTURE_2D的操作都会影响到纹理iter->second->mTexture
		glUniform1i(iter->second->mLocation, iIndex++);        //要更改的uniform变量的位置为iter->second->mLocation,设置其新值为iIndex++
	}

	glEnableVertexAttribArray(mPositionLocation);              //启用mPositionLocation指定的通用顶点属性数组
/*指定了渲染时索引值为mPositionLocation的顶点属性数组的数据格式和位置。每个顶点属性的组件数量为4,数组中每个组件的数据类型为GL_FLOAT,
当被访问时,固定点数据值直接转换为固定点值,连续顶点属性之间的偏移量为sizeof(Vertex),第一个组件在数组的第一个顶点属性中的偏移量为0*/
	glVertexAttribPointer(mPositionLocation, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
	glEnableVertexAttribArray(mColorLocation);
	glVertexAttribPointer(mColorLocation, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(sizeof(float) * 4));
	glEnableVertexAttribArray(mTexcoordLocation);
	glVertexAttribPointer(mTexcoordLocation, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)(sizeof(float) * 8));
}


/**
* @brief 给shader程序设置纹理贴图(主要用于BMP图片的纹理贴图)
* @param[in] name shader代码中uniform类型变量纹理采样器的名称
* @param[in] imagePath 纹理图片的路径
* @return 无
*/
void Shader::SetTexture(const char *name, const char *imagePath) 
{
	auto iter = mUniformTextures.find(name);
	if (iter == mUniformTextures.end())     //如果mUniformTextures里面不存在名称为name的纹理采样器名称,则创建该纹理对象
	{
		GLint location = glGetUniformLocation(mProgram, name);
		if (location != -1) 
		{
			UniformTexture *t = new UniformTexture;
			t->mLocation = location;
			t->mTexture = CreateTexture2DFromBMP(imagePath);
			mUniformTextures.insert(std::pair(name, t));
		}
	}
	else                                   //如果存在,则替换掉该纹理对象
	{
		glDeleteTextures(1, &iter->second->mTexture);               //删除纹理对象数组元素。删除后,该纹理iter->second->mTexture不再可用
		iter->second->mTexture = CreateTexture2DFromBMP(imagePath); //进行上述删除操作后,重新根据新的路径为imagePath的图片创建纹理对象
	}
}

/**
* @brief 给shader程序设置程序纹理(主要用于粒子)
* @param[in] name shader代码中uniform类型变量纹理采样器的名称
* @param[in] texture 程序纹理对象
* @return 无
*/
void Shader::SetTexture(const char *name, GLuint texture) 
{
	auto iter = mUniformTextures.find(name);
	if (iter == mUniformTextures.end()) 
	{
		GLint location = glGetUniformLocation(mProgram, name);
		if (location != -1) 
		{
			UniformTexture*t = new UniformTexture;
			t->mLocation = location;
			t->mTexture = texture;
			mUniformTextures.insert(std::pair(name, t));
		}
	}
	else 
	{
		glDeleteTextures(1, &iter->second->mTexture);
		iter->second->mTexture = texture;
	}
}

/**
* @brief 给shader程序设置纹理贴图(主要用于视频)
* @param[in] name shader代码中uniform类型变量纹理采样器的名称
* @param[in] w 纹理图片(视频)的宽度
* @param[in] h 纹理图片(视频)的高度
* @return 无
*/
void Shader::SetTexture(const char *name, int w, int h)
{
	auto iter = mUniformTextures.find(name);
	if (iter == mUniformTextures.end())     //如果mUniformTextures里面不存在名称为name的纹理采样器名称,则创建该纹理对象
	{
		GLint location = glGetUniformLocation(mProgram, name);
		if (location != -1)
		{
			UniformTexture *t = new UniformTexture;
			t->mLocation = location;
			t->mTexture = CreateTexture2D(NULL, w, h, GL_ALPHA);
			mUniformTextures.insert(std::pair(name, t));
		}
	}
	else                                   //如果存在,则替换掉该纹理对象
	{
		glDeleteTextures(1, &iter->second->mTexture);                   //删除纹理对象数组元素。删除后,该纹理iter->second->mTexture不再可用
		iter->second->mTexture = CreateTexture2D(NULL, w, h, GL_ALPHA); //进行上述删除操作后,重新根据新的路径为imagePath的图片创建纹理对象
	}
}

/**
* @brief 给shader程序更新纹理贴图(主要用于视频)
* @param[in] name shader代码中uniform类型变量纹理采样器的名称
* @param[in] w 纹理图片(视频)的宽度
* @param[in] h 纹理图片(视频)的高度
* @param[in] pixels 指向内存中图像数据的指针
* @return 无
*/
void Shader::UpdateTexture(const char *name, int w, int h, const void *pixels)
{
	auto iter = mUniformTextures.find(name);
	if(iter != mUniformTextures.end())        //如果容器中存在名称为name的纹理采样器
	{
		glBindTexture(GL_TEXTURE_2D, iter->second->mTexture); //告诉OpenGL下面代码中对2D纹理的任何设置都是针对索引为iter->second->mTexture的纹理的。当纹理iter->second->mTexture被绑定后,对于GL_TEXTURE_2D的操作都会影响到纹理
/*重新定义现有二维纹理图像的连续子区域。指定详细级别为基本图像级别,纹理数组中x方向的纹素偏移为0,纹理数组中y方向的纹素偏移为0,
纹理子图像的宽度为w,纹理子图像的高度为h,像素数据的格式为GL_ALPHA,像素数据的数据类型为GL_UNSIGNED_BYTE,指向内存中图像数据的指
针为pixels。调用该函数只会更新,所以效率会比函数glTexImage2D要高*/
		glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_ALPHA, GL_UNSIGNED_BYTE, pixels);
	}
}

utils.h

#pragma once
#include "ggl.h"

unsigned char *LoadFileContent(const char *path, int &filesize);
GLuint CompileShader(GLenum shaderType, const char *shaderCode);
GLuint CreateProgram(GLuint vsShader, GLuint fsShader);
float GetFrameTime();
unsigned char *DecodeBMP(unsigned char *bmpFileData, int &width, int &height);
GLuint CreateTexture2D(unsigned char *pixelData, int width, int height, GLenum type);
GLuint CreateTexture2DFromBMP(const char *bmpPath);
GLuint CreateBufferObject(GLenum bufferType, GLsizeiptr size, GLenum usage, void *data = nullptr);
GLuint CreateProcedureTexture(int size);

utils.cpp

#include "utils.h"

//编译shader函数,返回shader ID,如果返回值为0表示编译失败。shaderType为shader的种类(GL_VERTEX_SHADER或GL_FRAGMENT_SHADER),shaderCode为shader的代码
GLuint CompileShader(GLenum shaderType, const char *shaderCode) 
{
	GLuint shader = glCreateShader(shaderType);       //创建一个空的着色器对象,并返回一个可以引用的非零值(shader ID)
	glShaderSource(shader, 1, &shaderCode, nullptr);  //替换着色器对象中的源代码。1表示只有1句代码,但这句代码已经包含了整个shader文件的内容。
	glCompileShader(shader);                          //指定要编译的着色器对象。显卡驱动编译已存储在shader指定的着色器对象中的源代码字符串
	GLint compileResult = GL_TRUE;
	glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult); //在编译阶段获取编译情况
	if (compileResult == GL_FALSE)                    //如果编译失败了,打印错误信息
	{
		char szLog[1024] = { 0 };
		GLsizei logLen = 0;                           //错误日志的长度
		glGetShaderInfoLog(shader, 1024, &logLen, szLog); //返回指定着色器对象“shader”的信息日志
		printf("Compile Shader fail error log : %s \nshader code :\n%s\n", szLog, shaderCode);
		glDeleteShader(shader);                       //删除着色器对象“shader”。释放内存并使与着色器指定的着色器对象关联的ID无效。这个命令有效地撤消了对glCreateShader的调用的影响
		shader = 0;
	}
	return shader;
}

//将编译好的shader链接成一个绘制图形的程序,返回program ID,如果返回值为0表示链接失败。vsShader为编译好的Vertex Shader,fsShader为编译好的Fragment Shader
GLuint CreateProgram(GLuint vsShader, GLuint fsShader) 
{
	GLuint program = glCreateProgram();   //创建一个空program并返回一个可以被引用的非零值(program ID)
	glAttachShader(program, vsShader);    //指定的着色器对象vsShader将附加到program对象上(绑定)
	glAttachShader(program, fsShader);    //指定的着色器对象fsShader将附加到program对象上(绑定)
	glLinkProgram(program);               //链接program对象
	glDetachShader(program, vsShader);    //将shader指定的着色器对象vsShader与程序指定的程序对象program分离(解绑定)
	glDetachShader(program, fsShader);    //将shader指定的着色器对象fsShader与程序指定的程序对象program分离(解绑定)
	GLint nResult;
	glGetProgramiv(program, GL_LINK_STATUS, &nResult); //从program对象返回一个参数的值。GL_LINK_STATUS表示如果program的最后一个链接操作成功,则nResult返回GL_TRUE,否则返回GL_FALSE
	if (nResult == GL_FALSE)      //如果链接失败,打印错误信息
	{
		char log[1024] = { 0 };
		GLsizei writed = 0;
		glGetProgramInfoLog(program, 1024, &writed, log);  //返回program对象的信息日志
		printf("create gpu program fail,link error : %s\n", log);
		glDeleteProgram(program); //释放内存并使与着色器指定的着色器对象关联的ID无效。这个命令有效地撤消了对glCreateProgram的调用的影响
		program = 0;
	}
	return program;
}

/**
* @brief 解码指针bmpFileData指向的存放BMP图片的内存块,得到它的宽度和高度
* @param[in] bmpFileData 指向存放BMP图片的内存块
* @param[out] width 图片的宽度
* @param[out] height 图片的高度
* @return 解码出来得到的像素数据的起始地址。如果解码不成功则指为NULL
*/
unsigned char *DecodeBMP(unsigned char *bmpFileData, int&width, int&height)
{
	if (0x4D42 == *((unsigned short *)bmpFileData))              //如果指针bmpFileData指向的内存块中存放的是BMP文件
	{
		int pixelDataOffset = *((int *)(bmpFileData + 10));      //BMP图片的像素数据在整个文件内存块中的偏移
		width = *((int*)(bmpFileData + 18));                     //得到BMP图片的宽度
		height = *((int*)(bmpFileData + 22));                    //得到BMP图片的高度
		unsigned char *pixelData = bmpFileData + pixelDataOffset;//BMP图片的像素数据的起始地址
		for (int i = 0; i < width*height * 3; i += 3)            //对像素块进行转码,将BGR数据转成RGB数据。注意:这个转换不支持带有alpha通道的图片
		{
			unsigned char temp = pixelData[i];
			pixelData[i] = pixelData[i + 2];
			pixelData[i + 2] = temp;
		}
		return pixelData;
	}
	return nullptr;
}

/**
* @brief 根据解码出来的像素数据pixelData创建一个纹理对象
* @param[in] pixelData 解码出来的像素数据的起始位置
* @param[in] width 图片的宽度
* @param[in] height 图片的高度
* @param[in] type 图片的像素类型(比如图片是RGB像素格式的,还是RGBA像素格式的)
* @return 生成的纹理对象的索引
*/
GLuint CreateTexture2D(unsigned char *pixelData, int width, int height, GLenum type)
{
	GLuint texture;
	glGenTextures(1, &texture);                 //生成一个要操作的纹理对象的索引,存贮到变量texture中
	glBindTexture(GL_TEXTURE_2D, texture);      //告诉OpenGL下面代码中对2D纹理的任何设置都是针对索引为texture的纹理的。当纹理texture被绑定后,对于GL_TEXTURE_2D的操作都会影响到纹理texture。
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //设置2D纹理。当放大过滤(当纹理被放大)时,采用线性过滤的算法去采集像素, 即使用距离当前渲染像素中心最近的4个纹素加权平均值
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); //设置2D纹理。当缩小过滤(当纹理被缩小)时,采用线性过滤的算法去采集像素, 即使用距离当前渲染像素中心最近的4个纹素加权平均值
	//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	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_WRAP_S, GL_CLAMP);      //设置2D纹理。S方向上的贴图模式,将纹理坐标限制在0.0,1.0的范围之内.如果超出则会边缘拉伸填充
	//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);      //设置2D纹理。T方向上的贴图模式,将纹理坐标限制在0.0,1.0的范围之内.如果超出则会边缘拉伸填充
	//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);     //设置重复边界的纹理
	//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

/*将像素数据上传到显卡(从内存上传到显存)。上传一个2D纹理,执行细节级别为0,纹理中的颜色组件(纹理数据在显卡上的像素格式)为type,纹理图像的宽度为width,纹理图像的高度为height,边框的宽度为0(必须写0),
像素数据(纹理数据在内存中的像素格式)的颜色格式为type,像素数据的数据类型(每一个像素数据中,每一个分量的数据类型)为GL_UNSIGNED_BYTE(每种颜色成分用8位无符号整数表示),内存中指向图像数据的指针为
pixelData(为0,表示没有数据)。调用该函数会把原来的显存删掉,重新分配,所以效率会比函数glTexSubImage2D要低*/
	glTexImage2D(GL_TEXTURE_2D, 0, type, width, height, 0, type, GL_UNSIGNED_BYTE, pixelData);
	glBindTexture(GL_TEXTURE_2D, 0);            //把当前纹理设置成0号纹理,避免后面的操作影响到texture
	return texture;
}

//解码路径为bmpPath的BMP图片,并根据解码出来的像素数据创建返回一个纹理对象texture
GLuint CreateTexture2DFromBMP(const char *bmpPath)
{
	int nFileSize = 0;
	unsigned char *bmpFileContent = LoadFileContent(bmpPath, nFileSize);       //加载路径为bmpPath的文件(图片)到指针bmpFileContent指向的内存块中
	if (bmpFileContent == nullptr)       //如果加载不成功返回0
	{
		return 0;
	}
	int bmpWidth = 0, bmpHeight = 0;
	unsigned char *pixelData = DecodeBMP(bmpFileContent, bmpWidth, bmpHeight); //解码指针bmpFileContent指向的存放BMP图片的内存块中的数据,得到图片的宽度和高度
	if (bmpWidth == 0)                   //如果解码不成功返回0
	{
		delete bmpFileContent;
		return 0;
	}
	GLuint texture = CreateTexture2D(pixelData, bmpWidth, bmpHeight, GL_RGB);  //根据解码出来的像素数据pixelData创建一个纹理对象
	delete bmpFileContent;
	return texture;
}

/**
* @brief 创建缓冲对象并返回该对象的句柄
* @param[in] bufferType 要创建的缓冲对象类型。是GL_ARRAY_BUFFER(顶点缓冲对象VBO)还是GL_ELEMENT_ARRAY_BUFFER(索引缓冲对象EBO)
* @param[in] size 存放要上传到GPU端数据的数组的大小
* @param[in] usage GL_STATIC_DRAW(该缓存区不会被修改,使用显卡内存,VBO/EBO中的数据将不会被改动,数据放到显卡上后就不会去修改它了)。
                   GL_DYNAMIC_DRAW(该缓存区会被周期性更改)
* @param[in] data 存放要从CPU端发送到GPU端数据的数组的地址
* @return 被创建的缓冲对象的句柄
*/
GLuint CreateBufferObject(GLenum bufferType, GLsizeiptr size, GLenum usage, void *data /* = nullptr */) 
{
	GLuint object;
	glGenBuffers(1, &object);    //创建一个vbo/ebo对象。开辟(声明/获得)显存空间,并分配VBO/EBO的ID(地址)存贮到变量object中。使变量object指向显卡中的某个内存块(显存)
	glBindBuffer(bufferType, object); //通过分配的ID绑定(bind)一下制定的VBO/EBO。告诉VBO/EBO该缓存对象将保存顶点数组数据/索引数组数据。第一次调用glBindBuffer(),VBO/EBO用0大小的内存缓存初始化该缓存,并且设置VBO/EBO的初始状态,如用途与访问属性
	glBufferData(bufferType, size, data, usage); //将数据从data数组拷贝到缓存对象,待传递数据字节数量为size。执行该语句后,数据将被从CPU端发送到GPU端以待绘制(应用程序到GL)
	glBindBuffer(bufferType, 0); //设置当前的GL_ARRAY_BUFFER/GL_ELEMENT_ARRAY_BUFFER为0。将当前活动内存设置为空,非必需,但是这样做会是个好习惯
	return object;
}

//生成程序纹理。纹理长为size个像素,宽也为size个像素
GLuint CreateProcedureTexture(int size) 
{
	unsigned char *imageData = new unsigned char[size * size * 4];   //由于是RGBA格式,每个像素占用4个字节。所以是size*size * 4
	float halfSize = (float)size / 2.0f;
	float maxDistance = sqrtf(halfSize*halfSize + halfSize*halfSize);
	float centerX = halfSize;
	float centerY = halfSize;
	for (int y = 0; y < size; ++y) 
	{
		for (int x = 0; x < size; ++x) 
		{
			int currentPixelOffset = (x + y*size) * 4;
			imageData[currentPixelOffset] = 255;
			imageData[currentPixelOffset + 1] = 255;
			imageData[currentPixelOffset + 2] = 255;
			float deltaX = (float)x - centerX;
			float deltaY = (float)y - centerY;
			float distance = sqrtf(deltaX*deltaX + deltaY*deltaY);
			float alpha = powf(1.0f - (distance / maxDistance), 8.0f);
			alpha = alpha > 1.0f ? 1.0f : alpha;
			imageData[currentPixelOffset + 3] = (unsigned char)(alpha*255.0f);
		}
	}
	GLuint texture = CreateTexture2D(imageData, size, size, GL_RGBA);
	delete imageData;
	return texture;
}

vertexbuffer.h

#pragma once
#include "ggl.h"

struct Vertex               //存放要绘制的图形的其中一个顶点的信息
{
	float Position[4];      //顶点坐标
	float Color[4];         //顶点颜色
	float Texcoord[4];      //纹理坐标,只有第0和第1个元素才会被用到
};

class VertexBuffer        //VBO在CPU上的数据块,存放要绘制的图形的所有顶点坐标、顶点颜色、纹理坐标的信息
{
public:
	Vertex *mVertexes;    //存放要绘制的图形的顶点信息的数组的首地址
	int mVertexCount;     //有多少个顶点
	GLuint mVBO;          //顶点缓冲对象VBO的句柄
	void SetSize(int vertexCount);
	void SetPosition(int index, float x, float y, float z, float w = 1.0f);
	void SetColor(int index, float r, float g, float b, float a = 1.0);
	void SetTexcoord(int index, float x, float y);
	void Bind();
	void Unbind();
	Vertex &Get(int index);
};

vertexbuffer.cpp

#include "vertexbuffer.h"
#include "utils.h"

//设置VertexBuffer有vertexCount个顶点并分配相应的空间
void VertexBuffer::SetSize(int vertexCount) 
{
	mVertexCount = vertexCount;
	mVertexes = new Vertex[mVertexCount];
	memset(mVertexes, 0, sizeof(Vertex)*mVertexCount);
	mVBO = CreateBufferObject(GL_ARRAY_BUFFER, sizeof(Vertex)*mVertexCount, GL_STATIC_DRAW, nullptr); //当最后一个参数为nullptr的时候,会在显卡上开辟一片区域但并不给它赋值
	//mVBO = CreateBufferObject(GL_ARRAY_BUFFER, sizeof(Vertex)*mVertexCount, GL_STATIC_DRAW, mVertexes);
}

//设置第index个顶点的顶点坐标为(x,y,z,w)
void VertexBuffer::SetPosition(int index, float x, float y, float z, float w) 
{
	mVertexes[index].Position[0] = x;
	mVertexes[index].Position[1] = y;
	mVertexes[index].Position[2] = z;
	mVertexes[index].Position[3] = w;
}

//设置第index个顶点的顶点颜色为(r,g,b,a)
void VertexBuffer::SetColor(int index, float r, float g, float b, float a) 
{
	mVertexes[index].Color[0] = r;
	mVertexes[index].Color[1] = g;
	mVertexes[index].Color[2] = b;
	mVertexes[index].Color[3] = a;
}

//设置第index个顶点的纹理坐标为(x,y)
void VertexBuffer::SetTexcoord(int index, float x, float y) 
{
	mVertexes[index].Texcoord[0] = x;
	mVertexes[index].Texcoord[1] = y;
}

void VertexBuffer::Bind() 
{
	glBindBuffer(GL_ARRAY_BUFFER, mVBO);
	glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(Vertex)*mVertexCount, mVertexes); //将CPU上mVertexes指向的)数据拷贝到GPU上。用mVertexes指向的数据更新与GL_ARRAY_BUFFER相关联的当前绑定缓冲区对象中从0偏移开始的sizeof(Vertex)*mVertexCount个字节数据
}

void VertexBuffer::Unbind() 
{
	glBindBuffer(GL_ARRAY_BUFFER, 0);
}

//获取第index个顶点的引用
Vertex &VertexBuffer::Get(int index)
{
	return mVertexes[index];
}

FFVideoReader.hpp

#pragma once


class  FrameInfor                  
{
public:
    FrameInfor()
    {
        ;
    }
    ~FrameInfor()
    {
        if (NULL != m_datas[0])
        {
            delete m_datas[0];
        }
        if (NULL != m_datas[1])
        {
            delete m_datas[1];
        }
        if (NULL != m_datas[2])
        {
            delete m_datas[2];
        }
    }
    unsigned char *m_datas[3] = { 0 };  //三个数组分别存放yuv数据
    int _width;                         //视频的宽度,单位为像素
	int _height;                        //视频的高度,单位为像素
};


Thread.hpp

#pragma once

#include 

class Thread
{
public:
    DWORD _threadId;    //线程Id
    HANDLE _thread;     //线程的句柄

protected:
    static DWORD CALLBACK threadEnter(void *pVoid) //线程入口函数
    {
        Thread *pThis = (Thread *)pVoid;
        if (pThis)
        {
            pThis->run();
        }
        return  0;
    }
public:
    Thread()
    {
        _thread = 0;
        _threadId = 0;
    }
    virtual ~Thread()
    {
        join();
    }
    virtual bool run()
    {
        return  true;
    }
    virtual bool start() //启动一个子线程。成功返回true,失败返回false
    {
        if (_thread != 0)   //如果已经创建了子线程,并且该子线程正在运行,返回false
        {
            return  false;
        }
        else
        {
            //HIGH_PRIORITY_CLASS
            _thread = CreateThread(0,0,&Thread::threadEnter,this,0,&_threadId); //在主线程的基础上创建一个新线程,新线程所执行的线程入口函数为threadEnter,参数为this指针
            return  true;
        }
    }
    virtual void join() //等待退出函数
    {
        if (_thread)
        {
            WaitForSingleObject(_thread,0xFFFFFFFF); //一直等待,直到Handle为_thread的子线程执行完为止
            CloseHandle(_thread);                    //关闭一个内核对象。其中包括文件、文件映射、进程、线程、安全和同步对象等
            _thread = 0;                                    
        }
    }
};

main.cpp

#pragma once
#include 
#include "FFVideoReader.hpp"
#include "Thread.hpp"
#include "GLContext.h"
#include "CELLMath.hpp"
#include "GLVideo.h"

/*加上这句是为了能在Windows应用程序中使用printf函数打印调试信息*/
#pragma comment( linker, "/subsystem:\"console\" /entry:\"WinMainCRTStartup\"")

#define WM_UPDATE_VIDEO WM_USER + 100     //通知窗口进行重绘制更新的消息
#define WIDTH 1280                        //视频文件的宽,单位为像素
#define HEIGHT 720                        //视频文件的高,单位为像素


/**
* @brief 加载路径为path的文件(图片)到内存中
* @param[in] path 被加载的文件(图片)的路径
* @param[out] filesize 被加载的文件(图片)的大小。调该函数后,可以根据filesize得到文件的大小。如果为0,则表示打开图片文件失败了
* @return 指向存贮文件或图片的内存块的指针。如果不成功则值为NULL
*/
unsigned char* LoadFileContent(const char* path, int& filesize)
{
	unsigned char* fileContent = nullptr;
	filesize = 0;
	FILE* pFile = fopen(path, "rb");   //以只读方式打开一个二进制文件(比如图片),只允许读数据
	if (pFile)                         //如果打开成功
	{
		fseek(pFile, 0, SEEK_END);     //把文件读写的位置指针移动到文件结尾处
		int nLen = ftell(pFile);       //获得当前位置(文件结尾处)相对于文件首的位移,该位移值等于文件所含字节数。也就是说得到路径为path的文件的大小,单位为字节
		if (nLen > 0)                  //nLen > 0表示该文件是有效的
		{
			rewind(pFile);                                           //等同于fseek(pFile,0,SEEK_SET)。将文件内部的位置指针重新指向一个文件的开头
			fileContent = new unsigned char[nLen + 1];               //分配一个大小为nLen + 1字节的空间(内存),让指针fileContent指向这个空间
			fread(fileContent, sizeof(unsigned char), nLen, pFile);  //读取路径为path的文件的内容到指针fileContent指向的内存中
			fileContent[nLen] = '\0';
			filesize = nLen;
		}
		fclose(pFile);
	}
	return fileContent;
}


//根据应用程序句柄hInstance得到当前运行程序所在文件夹的路径,并将其存贮到参数pPath[1024]中
void  getResourcePath(HINSTANCE hInstance, char pPath[1024])
{
	char    szPathName[1024];
	char    szDriver[64];
	char    szPath[1024];
	GetModuleFileNameA(hInstance, szPathName, sizeof(szPathName)); //获取当前运行程序的绝对路径
	_splitpath(szPathName, szDriver, szPath, 0, 0);
	sprintf(pPath, "%s%s", szDriver, szPath);
}

class DecodeThread : public Thread
{
public:
	HWND _hWnd;
	bool _exitFlag;
	GLContext _glContext;
	GLVideo _glvideo;
	int _w;           //窗口客户区的宽度(单位为像素)
	int _h;           //窗口客户区的高度(单位为像素)
	FILE *m_fp;       //打开yuv文件的文件指针
public:
	DecodeThread()
	{
		_hWnd = 0;
		_exitFlag = false;
	}
	virtual void setup(HWND hwnd, const char *fileName = "11.flv")
	{
		_hWnd = hwnd;
		_glContext.setup(hwnd, GetDC(hwnd));
        
		glewInit();               //对glew初始化
		_glvideo.Init(WIDTH, HEIGHT);
		m_fp = fopen(fileName, "rb");
	}
	virtual void shutdown()
	{
		_exitFlag = true;
		Thread::join();
		_glContext.shutdown();
	}
	virtual bool run()      //子线程执行函数
	{
		while (!_exitFlag)
		{
			FrameInfor *infor = new FrameInfor();
			infor->m_datas[0] = new unsigned char[WIDTH * HEIGHT];		//Y
			infor->m_datas[1] = new unsigned char[WIDTH * HEIGHT / 4];	//U
			infor->m_datas[2] = new unsigned char[WIDTH * HEIGHT / 4];	//V

			if (feof(m_fp))
			{
				//fseek(m_fp, 0, SEEK_SET);
				break;
			}
			fread(infor->m_datas[0], 1, WIDTH * HEIGHT, m_fp);
			fread(infor->m_datas[1], 1, WIDTH * HEIGHT / 4, m_fp);
			fread(infor->m_datas[2], 1, WIDTH * HEIGHT / 4, m_fp);

			if (infor->m_datas[0] == 0
				|| infor->m_datas[1] == 0
				|| infor->m_datas[2] == 0)
			{
				continue;
			}

			PostMessage(_hWnd, WM_UPDATE_VIDEO, (WPARAM)infor, 0); //这里需要通知窗口进行重绘制更新,显示更新数据
			Sleep(40);
     	}
		return true;
	}
	void render()                          //opengl渲染相关的类
	{
		RECT rt;
		GetClientRect(_hWnd, &rt);         //得到相对于窗口客户区左上角的坐标
		_w = rt.right - rt.left;           //得到客户区的宽度(单位为像素)
		_h = rt.bottom - rt.top;           //得到客户区的高度(单位为像素)
		glClearColor(0.0, 0.0, 0.0, 1.0);  //指定刷新颜色缓冲区时所用的颜色,设置窗口背景颜色为R:0%,G:0%,B:0%,A:100%。切记:此函数仅仅设定颜色,并不执行清除工作
	    glClear(GL_COLOR_BUFFER_BIT);      //清除颜色缓冲
		glViewport(0, 0, _w, _h);          //在默认情况下,视口被设置为占据打开窗口的整个像素矩形,窗口大小和设置视口大小相同,所以为了选择一个更小的绘图区域,就可以用glViewport函数来实现这一变换,在窗口中定义一个像素矩形,最终将图像映射到这个矩形中。该语句作用其实就是让视频可以随窗口进行缩放
		glMatrixMode(GL_PROJECTION);       //将当前矩阵指定为投影矩阵,声明我们接下来会对投影进行相关的操作。也就是把物体投影到一个平面上,就像我们照相一样,把3维物体投到2维的平面上。这样,接下来的语句可以是跟透视相关的函数,比如glFrustum()或gluPerspective()
		glLoadIdentity();                  //对当前矩阵进行初始化。无论以前进行了多少次矩阵变换,在该命令执行后,当前矩阵均恢复成一个单位矩阵,即相当于没有进行任何矩阵变换状态
		glOrtho(0, _w, _h, 0, -100, 100);  //创建一个正交平行的视景体。将窗口映射成屏幕坐标(将窗口映射成立方体/长方体)

		glMatrixMode(GL_MODELVIEW);        //切换到模型视图矩阵,这样才能正确画图
		glLoadIdentity();
	}
};

DecodeThread g_decode;

//窗口处理函数/消息过程处理函数(回调函数)
LRESULT CALLBACK windowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
	case WM_UPDATE_VIDEO:
	{
		FrameInfor *infor = (FrameInfor *)wParam;
		g_decode.render();
		g_decode._glvideo.Draw(g_decode._w, g_decode._h, infor);
		g_decode._glContext.swapBuffer();
		delete infor;
	}
	break;
    case WM_SIZE:
        break;
    case WM_CLOSE:
    case WM_DESTROY:
		g_decode.shutdown();
        PostQuitMessage(0); //该函数向系统表明有个线程有终止请求。如果没有该语句,鼠标点击程序窗口右上方的关闭按钮时会无法关闭。
        break;
    default:
        break;
    }
    return DefWindowProc(hWnd, msg, wParam, lParam); //该函数确保每一个消息得到处理。如果没有该语句,窗口会卡死。
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
    /*注册窗口类*/
    WNDCLASSEX winClass;                                     //定义窗口类结构体变量winClass
	winClass.lpszClassName =    L"FFVideoPlayer";            //注册窗口使用的窗口名称
    winClass.cbSize         =   sizeof(WNDCLASSEX);          //WNDCLASSEX 的大小
    winClass.style          =   CS_HREDRAW | CS_VREDRAW | CS_OWNDC | CS_DBLCLKS; //窗口更新时的重绘方式
    winClass.lpfnWndProc    =   windowProc;                  //指向窗口处理函数(回调函数)。处理窗口事件,像单击鼠标会怎样,右击鼠标会怎样,都是由此函数控制的。
    winClass.hInstance      =   hInstance;                   //本模块的事例句柄
    winClass.hIcon	        =   0;                           //窗口类的图标,为资源句柄,如果设置为NULL,系统将为窗口提供一个默认的图标
    winClass.hIconSm	    =   0;                           //小图标的句柄,在任务栏显示的图标
    winClass.hCursor        =   LoadCursor(NULL, IDC_ARROW); //窗口类的鼠标样式,为鼠标样式资源的句柄
    winClass.hbrBackground  =   (HBRUSH)(BLACK_BRUSH);       //窗口类的背景刷,为背景刷句柄
    winClass.lpszMenuName   =   NULL;                        //菜单名称,为NULL则为没有菜单
    winClass.cbClsExtra     =   0;                           //为窗口类的额外信息做记录,初始化为0
    winClass.cbWndExtra     =   0;                           //记录窗口实例的额外信息,系统初始为0
	ATOM atom = RegisterClassEx(&winClass);                  //注册窗口winClass
	if (!atom)                                               //如果注册失败,显示提示框
	{
		MessageBox(NULL, L"Register Fail", L"Error", MB_OK);
		return 0;
	}

    HWND hWnd = CreateWindowEx(NULL,L"FFVideoPlayer",L"FFVideoPlayer",WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,0,0,960,640,0,0,hInstance,0); //创建一个960*640的窗口

    UpdateWindow(hWnd);                   //绕过消息队列(不进队),直接向窗口客户区发送WM_PAINT消息,使得窗口立即更新
    ShowWindow(hWnd,SW_SHOW);             //该函数设置指定窗口的显示状态。如果没有该语句,执行程序时窗口不会显示

	char szPath[1024];                    //媒体文件所在文件夹的绝对路径
	char szPathName[1024];                //媒体文件的绝对路径
	getResourcePath(hInstance, szPath);
	sprintf(szPathName, "%sdata/video1.yuv", szPath); //将媒体文件的路径存入变量szPathName里面

	g_decode.setup(hWnd, szPathName);
	g_decode.start();

    MSG msg = {0};
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);  //将虚拟键消息转换成字符消息
        DispatchMessage(&msg);   //该函数分发一个消息给窗口程序。如果没有该语句,无法拖动窗口和通过鼠标对窗口进行放大,最小化,关闭等操作。
    } 
	g_decode.shutdown();
    return  0;
}

video.vs

attribute vec4 position;             //视频的顶点坐标
attribute vec2 texcoord;             //视频的纹理坐标
uniform   mat4 MVPMatrix;            //MVP矩阵
varying vec2 V_Texcoord;             //fragment shader使用该varying变量的值作为纹理坐标

void main()
{
	V_Texcoord=texcoord;
    gl_Position=MVPMatrix*position; //经过转换后得到的顶点位置
}

video.fs

#ifdef GL_ES
precision mediump float;        //如果当前的运行环境为opengles,定义浮点数的精度。写了该语句后才能保证shader是跨平台的
#endif

uniform sampler2D U_TextureY;   //纹理采样器Y
uniform sampler2D U_textureU;   //纹理采样器U
uniform sampler2D U_textureV;   //纹理采样器V
varying vec2 V_Texcoord;        //fragment shader使用vertex shader中该变量的值作为纹理坐标

void main()
{
    vec3 yuv;
    vec3 rgb;  
    yuv.x = texture2D(U_TextureY, V_Texcoord).a;
    yuv.y = texture2D(U_textureU, V_Texcoord).a - 0.5;
    yuv.z = texture2D(U_textureV, V_Texcoord).a - 0.5;
    rgb.r = yuv.r + 1.13983 * yuv.b;
    rgb.g = yuv.r - 0.39465 * yuv.g - 0.58060 * yuv.b;
    rgb.b = yuv.r + 2.03210 * yuv.g;	
	gl_FragColor = vec4(rgb, 1);
}

三、运行

首先用如下命令将所在路径下的video1.mp4文件转换为帧宽度为1280,帧高度为720,像素格式为yuv420p的YUV文件:video1.yuv

ffmpeg -i video1.mp4 -s 1280x720 -pix_fmt yuv420p video1.yuv

然后在代码编译后生成的可执行文件的所在目录下新建文件夹data,将video1.yuv文件放到该目录下,运行程序,可以看到效果如下:

四、代码总流程分析

首先在main.cpp中通过语句

m_fp = fopen(fileName, "rb");

打开video1.yuv。

在main.cpp中通过语句

g_decode.start();

创建子线程(子线程的执行函数为main.cpp的类DecodeThread中的run函数)。

在子线程中通过语句

fread(infor->m_datas[0], 1, WIDTH * HEIGHT, m_fp);
fread(infor->m_datas[1], 1, WIDTH * HEIGHT / 4, m_fp);
fread(infor->m_datas[2], 1, WIDTH * HEIGHT / 4, m_fp);

不断循环读取每一帧视频的YUV数据,将Y分量的数据存贮到数组m_datas[0]中,U分量的数据存贮到数组m_datas[1],V分量的数据存贮到数组m_datas[2]中。对于yuv420p的格式,每一个像素都需要一个亮度(y)值的,每四个像素都需要一个色度(u)值,每四个像素都需要一个色度(v)值。而yuv420p是planar格式的YUV格式,其先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V,所以对于每一帧视频数据,首先用fread函数先读取WIDTH * HEIGHT(1280*720)个字节的数据(Y分量),将其存贮到数组m_datas[0]中,再读取WIDTH * HEIGHT/4个字节的数据(U分量),最后读取WIDTH * HEIGHT/4个字节的数据(V分量)。

在子线程中通过main.cpp的语句

PostMessage(_hWnd, WM_UPDATE_VIDEO, (WPARAM)infor, 0); 
Sleep(40);

向主窗口Post消息,将上述读取到的YUV数据作为参数传递。加上Sleep(40)是为了避免读取视频文件过快导致视频一下就播完了。

收到消息后,主线程(main.cpp的函数windowProc中)会调用OpenGL的api将数据(顶点坐标、纹理坐标、YUV数据)从CPU传递到GPU,进行绘制(本质上就是画两个三角形,构成一个矩形,然后将一帧帧视频的YUV数据分别通过三重纹理贴图贴到这个矩形上去,从而造成播放视频的效果)。

基本流程说完了,下面讲一下可能比较难理解的部分。

五、关于Fragment shader(video.fs)中texture2D函数后面的.a的理解

在video.fs中有如下语句:

yuv.x = texture2D(U_TextureY, V_Texcoord).a;
yuv.y = texture2D(U_textureU, V_Texcoord).a - 0.5;
yuv.z = texture2D(U_textureV, V_Texcoord).a - 0.5;

可以看到texture2D后面接了个.a。函数texture2D返回的是vec4类型的向量,.a意味着取返回值的第四个组成部分("."后面可以为rgba,分别代表取返回值的第一、二、三、四部分,可以参考《what do texture2D().r and texture2D().a mean?》)。所以这里为什么是.a,而不是.r、.g或者.b呢?这是因为我们将像素数据上传到显卡时调用的函数glTexImage2D和glTexSubImage2D中,传的是GL_ALPHA。在C++函数glTexImage2D和glTexSubImage2D中传的像素数据的格式必须得跟Fragment shader对应起来,如果传的是GL_RED,则Fragment shader的texture2D后得是.r,如果传的是GL_ALPHA,则shader中必须是.a。

六、关于Fragment shader(video.fs)中texture2D函数后减0.5的理解

 在video.fs中有如下语句:

yuv.y = texture2D(U_textureU, V_Texcoord).a - 0.5;
yuv.z = texture2D(U_textureV, V_Texcoord).a - 0.5;

在获取u和v的数据时,需要减去0.5。因为uv的数据值从-100返回到大于100。最后,传入的着色器将规格化为0到1,因此为了正确转换为rgb,需要将uv规格化为-0.5到0.5,与原始rgb相同。

参考:《Yuv format introduction and opengl display yuv data》(这里如果上不了外网是访问不了该网址的)

七、关于Fragment shader(video.fs)中yuv转rgb公式的由来

 在video.fs中有如下语句:

rgb.r = yuv.r + 1.13983 * yuv.b;
rgb.g = yuv.r - 0.39465 * yuv.g - 0.58060 * yuv.b;
rgb.b = yuv.r + 2.03210 * yuv.g;

这个公式是怎么来的呢?我们访问维基百科中关于YUV的说明:https://en.wikipedia.org/wiki/YUV

可以看到对于BT.470的标准来讲有如下公式:

windows下使用OpenGL实现yuv420p转rgb播放视频(三重纹理实现)_第1张图片

windows下使用OpenGL实现yuv420p转rgb播放视频(三重纹理实现)_第2张图片

将上图中的公式精确到小数点后5位即可得到:

R = Y + 1.13983 * V
G = Y - 0.39465 * U - 0.58060 * V
B = Y + 2.03210 * U

这就是video.fs中这个公式的由来。

八、 关于BT.601和BT.709

  yuv有BT.601、BT.709等标准,每个标准的YUV转RGB的公式都是不一样的。根据维基百科:https://en.wikipedia.org/wiki/YUV,可以查看不同标准的YUV转RGB的公式。从采集编码到解码渲染,整个过程中,所用的标准必须保持一致,否则播放视频时会有颜色失真/偏色的问题。

比如:

windows下使用OpenGL实现yuv420p转rgb播放视频(三重纹理实现)_第3张图片

windows下使用OpenGL实现yuv420p转rgb播放视频(三重纹理实现)_第4张图片

参考文章:《709 With 601 Matrix in Vegas》、《Your browser and my browser see different colors》

九、关于怎么判断是用BT.601还是BT.709的转换公式

  很遗憾,很多媒体文件都不会携带到底是BT.601,还是BT.709的信息。在不知道该信息的情况下,部分视频播放器在播放视频时会根据分辨率选择到底使用哪一种标准回放,如果小于等于576(如480),就用标清的标准BT.601;如果大于576(如720,1080),就用高清的标准BT.709。

参考文章:《ios - 使用哪个YCbCr基质? BT.709或BT.601》、《ffmpeg yuv colorspace is BT.601 or BT.709》、《关于BT709toBT601的问题》、《How the "see" correct BT.709 color using VLC in a PC?》、《很好奇突然出现了bt470bg 》。

十、将Fragment shader(video.fs)中的yuv转rgb运算换成矩阵的形式

在video.fs中有如下代码:

rgb.r = yuv.r + 1.13983 * yuv.b;
rgb.g = yuv.r - 0.39465 * yuv.g - 0.58060 * yuv.b;
rgb.b = yuv.r + 2.03210 * yuv.g;	

为了简洁可以将其改成矩阵的形式:

rgb = mat3(1,1,1, 0,-0.39465,2.03210, 1.13983,-0.58060,0) * yuv; 

效果是完全一样的。上述矩阵相当于:

windows下使用OpenGL实现yuv420p转rgb播放视频(三重纹理实现)_第5张图片

如果还是看不懂,可以复习一下高中的数学课程。

另外上面的矩阵转换公式是基于BT.601标准的,如果是BT.709,则根据维基百科的公式:

windows下使用OpenGL实现yuv420p转rgb播放视频(三重纹理实现)_第6张图片

将矩阵修改为:

rgb = mat3(1, 1, 1, 0, -0.21482, 2.12798, 1.28033, -0.38059, 0) * yuv;

参考GPUImage源码:

https://git.polyv.net/ios/PolyvLiveKit/-/blob/1.2.2/PLVLiveKit/LFLiveKit/Vendor/GPUImage/GPUImageColorConversion.m


十一、关于顶点个数

GLVideo.cpp的GLVideo::Draw函数中有如下代码:

VideoVertex vertexs[] =        //视频有四个顶点坐标和纹理坐标
	{
		{ 0,                     0,  0,  0 },
		{ 0,          ClientHeight,  0,  1 },
		{ ClientWidth,           0,  1,  0 },
		{ ClientWidth,ClientHeight,  1,  1 },
	};

可以看到传了4个顶点坐标,这是因为我们调用glDrawArrays函数时用了GL_TRIANGLE_STRIP这种绘制三角形的方式。当使用这种绘制三角形的方式时,我们画一个四边形时,需要4个顶点。在OpenGL中有三种绘制一系列三角形的方式,分别是GL_TRIANGLES、GL_TRIANGLE_STRIP和GL_TRIANGLE_FAN。如果我们使用GL_TRIANGLES的绘制方式,则画一个四边形时,需要6个顶点。

如下图所示:

windows下使用OpenGL实现yuv420p转rgb播放视频(三重纹理实现)_第7张图片

参考:《理解GL_TRIANGLE_STRIP等绘制三角形序列的三种方式》

十二、优化和思考:

本文演示用的代码是最初级的让大家学习用的代码,事实上有很多优化的空间。

1.本文使用的代码使用了三重纹理,将Y、U、V分量分别作为一张纹理贴图贴到四边形上。但这样做效率较差,实际上可以优化成只使用一重纹理(单纹理),将Y、U、V分量合为一张纹理贴图传给GPU。

2.代码中没有使用PBO,实际上可以使用双PBO交换读取,这样读取效率会高很多。

十三、资源下载

本文所演示代码可以在https://download.csdn.net/download/u014552102/20065011?spm=1001.2014.3001.5501下载。

你可能感兴趣的:(音视频技术,opengl,windows编程,opengl,视频处理)