OpenGL渲染YUV实战:GPU加速转换与MipMap模糊效果实现

本文介绍如何采用 Qt + OpenGL 绘制 YUV 数据,并通过 OpenGL 来实现画质模糊。

前言

我们在开发音视频程序的时候,对于解码后帧的渲染往往有几个操作需要做:

  • 将 YUV420 格式的图像数据转换成 RGB 格式
  • 渲染 RGB 图像

在普通方案中,格式转换以及渲染都是在 CPU 中去做。而要降低 CPU 使用率,我们可以将上面这两个操作通通移动到 GPU 中去做,具体如果实现,请往下看。

OpenGL 渲染

我们通过 Qt 提供的 QOpenGLWidget 和 QOpenGLFunction 来进行画面的 GPU 渲染(其实 Qt 也只是对 OpenGL 接口的一套封装,类似于 GLAD 和 GLFW)。关于如何使用这两个类,本文不再赘述,如果不明白的同学,可以看我的这篇文章。

YUV420P

下面先让我们简单的学习一下 YUV420P 。YUV420P是一种常用的图像格式,主要用于视频处理和存储。它由三个平面构成:Y(亮度)、U(色度蓝色)和V(色度红色)。Y平面存储所有像素的亮度信息,而U和V平面只存储每四个Y像素对应的色度信息。这种采样方式使得YUV420P比RGB更高效,因为它减少了色度数据的存储量,从而节省了存储空间和带宽

结构

  1. Y平面:包含所有像素的亮度信息。
  2. U平面:包含每个2x2像素块的蓝色色度信息。
  3. V平面:包含每个2x2像素块的红色色度信息。

转换

将YUV420P转换为RGB需要对U和V进行插值,然后应用转换公式。例如,每个Y值对应一个U和V值,通过插值得到全分辨率的U和V平面,再转换为RGB。

YUV 直出渲染

了解完 YUV420P 格式之后,我们来讲一讲应该怎样去渲染。在代码中,我们将 Y、U、V 三个分量分别存放在三个纹理中,将解码后的 AVFrame 中的 YUV 数据分别拷贝到对应的纹理。

cpp 复制代码
// 假设有一个AVFrame对象名为 m_pFrame

// Y纹理
glBindTexture(GL_TEXTURE_2D, m_texture[0]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_pFrame.linesize[0], m_frameSize.height(), 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_pFrame.data[0]);

// U纹理
glBindTexture(GL_TEXTURE_2D, m_texture[1]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_pFrame.linesize[1], m_frameSize.height() / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_pFrame.data[1]);

// V纹理
glBindTexture(GL_TEXTURE_2D, m_texture[2]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_pFrame.linesize[2], m_frameSize.height() / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_pFrame.data[2]);

然后在片段着色器中将三个纹理合成。

glsl 复制代码
attribute vec3 vertexIn;    // xyz顶点坐标
attribute vec2 textureIn;   // xy纹理坐标
varying vec2 textureOut;    // 传递给片段着色器的纹理坐标

void main(void)
{
    gl_Position = vec4(vertexIn, 1.0);  // 1.0表示vertexIn是一个顶点位置
    textureOut = textureIn; // 纹理坐标直接传递给片段着色器
}
glsl 复制代码
#version 330

vec3 yuv2rgb(vec3 yuv) {
	float r = 1.164 * yuv.x + 1.596 * yuv.z;
	float g = 1.164 * yuv.x - 0.392 * yuv.y - 0.813 * yuv.z;
	float b = 1.164 * yuv.x + 2.017 * yuv.y;
	return vec3(r, g, b);
}

varying vec2 textureOut;

uniform sampler2D textureY; 
uniform sampler2D textureU; 
uniform sampler2D textureV; 

void main(void) 
{ 
    vec3 yuv; 
    vec3 rgb; 

    yuv.x = texture(textureY, textureOut).r - 0.063; 
    yuv.y = texture(textureU, textureOut).r - 0.502; 
    yuv.z = texture(textureV, textureOut).r - 0.502; 

    rgb = yuv2rgb(yuv); 

    gl_FragColor = vec4(rgb, 1); 
}

OpenGL 实现画质模糊

在我们将软件商业化时,往往需要一些付费点,例如:画质。我们可以让非会员的画质降低,而降低画质又可以有几种方法:

  • 降低输入的画质
  • FFmpeg 下采样+上采样
  • OpenGL MipMap 多级纹理

直接降低输入画质是最简单的,但可能存在某些特殊情况不能直接在源头降低。而采用 FFmpeg 的方式则会增加两次重采样,白白消耗 CPU。而采用 MipMap 的模式,则能在不增加太多 GPU 占用率的情况下,完全不占用 CPU。

接下来我将为大家介绍一种使用 MipMap 来让画质模糊的方式。首先,让我们了解了解什么叫 MipMap。

The height and width of each image, or level, in the mipmap is a factor of two smaller than the previous level.

Mipmap(多级渐减图像)是OpenGL中用于优化纹理采样的一种技术。具体而言,Mipmap为每个纹理生成一系列不同分辨率的版本 ,每个级别(Level of Detail, LOD)的尺寸是前一级别的1/2。当渲染场景时,渲染器会根据纹理在屏幕上的实际大小自动选择合适的Mipmap级别,从而实现平滑过渡和抗锯齿效果。需要特别指出的是:Mipmap并不是简单的上采样(Upsampling)和下采样(Downsampling),而是通过预生成不同分辨率的纹理版本来实现高质量的模糊效果。这种预处理方式可以显著提升渲染效率,并确保纹理在不同缩放比例下都能保持视觉质量。

在实际应用中,可以通过以下方式配置Mipmap:

  1. 调用glGenerateMipmap自动生成Mipmap
  2. 设置合适的过滤器(Filter)来控制Mipmap的使用
  3. 配置LOD偏移量(LOD bias)来调整Mipmap的使用级别

在 OpenGL 中,我们使用 glGenerateMipmap来生成 Mipmap。同时,我们需要将纹理环绕模式改成GL_LINEAR_MIPMAP_LINEAR。同时根据需要显示的不同模糊级别,为着色器中的 lodLevel设置不同的数值。

着色器代码如下:

glsl 复制代码
#version 330

vec3 yuv2rgb(vec3 yuv) {
	float r = 1.164 * yuv.x + 1.596 * yuv.z;
	float g = 1.164 * yuv.x - 0.392 * yuv.y - 0.813 * yuv.z;
	float b = 1.164 * yuv.x + 2.017 * yuv.y;
	return vec3(r, g, b);
}

varying vec2 textureOut;

uniform sampler2D textureY; 
uniform sampler2D textureU; 
uniform sampler2D textureV; 
uniform float lodLevel;

void main(void) 
{ 
    vec3 yuv; 
    vec3 rgb; 

    // 跟普通纹理采样函数不同的是,这里我们调用的是textureLod,也就是选择不同等级的纹理。
    yuv.x = textureLod(textureY, textureOut, lodLevel).r - 0.063; 
    yuv.y = textureLod(textureU, textureOut, lodLevel).r - 0.502; 
    yuv.z = textureLod(textureV, textureOut, lodLevel).r - 0.502; 

    rgb = yuv2rgb(yuv); 

    gl_FragColor = vec4(rgb, 1); 
}

textureLod的各个参数意义为:

  • _sampler_:指定绑定到纹理的采样器,即哪个纹理要被采样。
  • _P_:采样的纹理坐标。
  • _lod_:指定采样级别。

代码如下:

cpp 复制代码
// 假设有一个AVFrame对象名为 m_pFrame

// Y纹理
glBindTexture(GL_TEXTURE_2D, m_texture[0]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_pFrame.linesize[0], m_frameSize.height(), 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_pFrame.data[0]);
// 生成mipmap
glGenerateMipmap(m_texture[0]);

// U纹理
glBindTexture(GL_TEXTURE_2D, m_texture[1]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_pFrame.linesize[1], m_frameSize.height() / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_pFrame.data[1]);
// 生成mipmap
glGenerateMipmap(m_texture[1]);

// V纹理
glBindTexture(GL_TEXTURE_2D, m_texture[2]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// 生成mipmap
glGenerateMipmap(m_texture[2]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_pFrame.linesize[2], m_frameSize.height() / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_pFrame.data[2]);


m_shaderProgram.setUniformValue("textureY", 0);
m_shaderProgram.setUniformValue("textureU", 1);
m_shaderProgram.setUniformValue("textureV", 2);

// 设置0为原画
m_shaderProgram.setUniformValue("lodLevel", 0);
// 设置1为一级模糊
//m_shaderProgram.setUniformValue("lodLevel", 1);

其中,GL_TEXTURE_MIN_FILTER 表示我们要配置纹理的缩小过滤方式(当纹理被应用到比其实际尺寸小的区域时如何采样)。GL_LINEAR_MIPMAP_LINEAR 是一个三线性过滤(Trilinear Filtering)模式,具体含义是:

  • 在相邻mipmap层级之间执行线性插值(例如介于1级和2级mipmap之间)
  • 在每个mipmap层级内部也执行线性插值(即双线性过滤)

陷阱

在我实际运用到项目上时发现,窗口内容竟然变黑了?一查日志才知道,着色器竟然编译失败了,报错内容如下:

ERROR: 0:27: 'textureLod' : no matching overloaded function found

ERROR: 0:27: 'r' : field selection requires structure, vector, or matrix on left hand side

ERROR: 0:28: 'textureLod' : no matching overloaded function found

ERROR: 0:28: 'r' : field selection requires structure, vector, or matrix on left hand side

ERROR: 0:29: 'textureLod' : no matching overloaded function found

ERROR: 0:29: 'r' : field selection requires structure, vector, or matrix on left hand side

但都 5202 年了,一般不会有显卡不支持 OpenGL 3.3 吧,那为什么会这样报错呢?当我们调用 OpenGL API 打印 OpenGL 版本和当前渲染器名称时:

cpp 复制代码
const GLubyte* version = glGetString(GL_VERSION);   // 获取OpenGL版本(如 "OpenGL ES 2.0")
const GLubyte* renderer = glGetString(GL_RENDERER); // 获取渲染器名称(如GPU型号)

结果显示为:

OpenGL ES 2.0

Microsoft Basic Render Driver

为什么渲染器是"Microsoft Basic Render Driver"?同时,OpenGL 的版本为什么是 OpenGL ES 2.0?

OpenGL ES 2.0 是移动设备和嵌入式系统的常见标准,与桌面版 OpenGL 存在差异(如精度、扩展支持)。OpenGL ES 并不是桌面端应用的,为什么会在桌面应用中出现 OpenGL ES 呢?通过一番搜索,最终我找到了答案:

如果系统没有硬件加速(如显卡驱动未安装),Windows 会使用 "Microsoft Basic Render Driver " ,此时需要降级到兼容模式(如使用 OpenGL ES 2.0 类似的特性),而 OpenGL ES 2.0 中不支持 textureLod 函数,所以自然就会报错。并且完全基于 CPU 计算(软件渲染),不支持 GPU 硬件加速,因此性能极低。有关Microsoft Basic Render Driver, 可以看这:Microsoft 基本显示驱动程序 - Windows drivers

其实这个问题根本原因是找到为什么会降级 ,但是苦于找不到具体原因,手上有没有一台可以复现的机器。所以,只能采用另外一条路,使用 OpenGL ES 2.0 的拓展:GL_EXT_shader_texture_lod

我们只需要小小的修改着色器代码,将 textureLod 改为 texture2DLodEXT,就能使用 MipMap 了:

glsl 复制代码
#extension GL_EXT_shader_texture_lod : require
precision mediump float;

vec3 yuv2rgb(vec3 yuv) {
	float r = 1.164 * yuv.x + 1.596 * yuv.z;
	float g = 1.164 * yuv.x - 0.392 * yuv.y - 0.813 * yuv.z;
	float b = 1.164 * yuv.x + 2.017 * yuv.y;
	return vec3(r, g, b);
}

varying vec2 textureOut;

uniform sampler2D textureY; 
uniform sampler2D textureU; 
uniform sampler2D textureV; 
uniform float lodLevel;

void main(void) 
{ 
    vec3 yuv; 
    vec3 rgb; 

    yuv.x = texture2DLodEXT(textureY, textureOut, lodLevel).r - 0.063; 
    yuv.y = texture2DLodEXT(textureU, textureOut, lodLevel).r - 0.502; 
    yuv.z = texture2DLodEXT(textureV, textureOut, lodLevel).r - 0.502; 

    rgb = yuv2rgb(yuv); 

    gl_FragColor = vec4(rgb, 1); 
}

首先,在开头添加一行

extension GL_EXT_shader_texture_lod : require

代表开启 GL_EXT_shader_texture_lod拓展,其次调用 texture2DLodEXT来加载不同的 mipmap。

完整代码

cpp 复制代码
#pragma once

#include <vector>

#include <QOpenGLBuffer>
#include <QOpenGLWidget>
#include <QOpenGLShaderProgram>
#include <QOpenGLFunctions>

#include "FrameObserver.h"

struct AVFrame;
class COpenGLRenderWidget : public QOpenGLWidget, protected QOpenGLFunctions, public IFrameObserver
{
    Q_OBJECT

public:
    explicit COpenGLRenderWidget(QWidget *parent = nullptr);
    ~COpenGLRenderWidget() override;

    void OnFrame(const AVFrame* frame, bool isHardwareFrame) override;
    void OnInputCodecContext(const AVCodecContext* ctx) override;
    HWND GetWindowHandle() const override;

private:
    void InitShaders();
    void InitTextures();
    void DeinitTextures();

private:
    void initializeGL() override;
    void paintGL() override;
    void resizeGL(int w, int h) override;
    
private:
    AVFrame* m_pFrame = nullptr;

    QOpenGLShaderProgram m_shaderProgram;
    QOpenGLBuffer m_vbo;
    std::vector<GLuint> m_textures;

    bool m_bIsNeedUpdate = false;
};
cpp 复制代码
#include "OpenGLRenderWidget.h"

extern "C"
{
#include "libavformat/avformat.h"
}

static const GLfloat coordinate[] = {
    // 顶点坐标,存储4个xyz坐标
    // 坐标范围为[-1,1],中心点为 0,0
    // 二维图像z始终为0
    // GL_TRIANGLE_STRIP的绘制方式:
    // 使用前3个坐标绘制一个三角形,使用后三个坐标绘制一个三角形,正好为一个矩形
    // x     y     z
    -1.0f, -1.0f, 0.0f,
     1.0f, -1.0f, 0.0f,
    -1.0f,  1.0f, 0.0f,
     1.0f,  1.0f, 0.0f,

    // 纹理坐标,存储4个xy坐标
    // 坐标范围为[0,1],左下角为 0,0
    0.0f, 1.0f,
    1.0f, 1.0f,
    0.0f, 0.0f,
    1.0f, 0.0f
};

constexpr auto VERTEX_SHADER = R"(
attribute vec3 vertexIn;    // xyz顶点坐标
attribute vec2 textureIn;   // xy纹理坐标
varying vec2 textureOut;    // 传递给片段着色器的纹理坐标

void main(void)
{
    gl_Position = vec4(vertexIn, 1.0);  // 1.0表示vertexIn是一个顶点位置
    textureOut = textureIn; // 纹理坐标直接传递给片段着色器
}
)";

constexpr auto FRAGMENT_SHADER = R"(
#version 330

vec3 yuv2rgb(vec3 yuv) {
	float r = 1.164 * yuv.x + 1.596 * yuv.z;
	float g = 1.164 * yuv.x - 0.392 * yuv.y - 0.813 * yuv.z;
	float b = 1.164 * yuv.x + 2.017 * yuv.y;
	return vec3(r, g, b);
}

varying vec2 textureOut;

uniform sampler2D textureY; 
uniform sampler2D textureU; 
uniform sampler2D textureV; 
uniform float lodLevel;

void main(void) 
{ 
    vec3 yuv; 
    vec3 rgb; 

    // 跟普通纹理采样函数不同的是,这里我们调用的是textureLod,也就是选择不同等级的纹理。
    //yuv.x = textureLod(textureY, textureOut, lodLevel).r - 0.063; 
    //yuv.y = textureLod(textureU, textureOut, lodLevel).r - 0.502; 
    //yuv.z = textureLod(textureV, textureOut, lodLevel).r - 0.502; 
    yuv.x = texture(textureY, textureOut).r - 0.063; 
    yuv.y = texture(textureU, textureOut).r - 0.502; 
    yuv.z = texture(textureV, textureOut).r - 0.502; 

    rgb = yuv2rgb(yuv); 

    gl_FragColor = vec4(rgb, 1); 
}
)";

COpenGLRenderWidget::COpenGLRenderWidget(QWidget *parent)
    : QOpenGLWidget(parent)
{}

COpenGLRenderWidget::~COpenGLRenderWidget()
{}

void COpenGLRenderWidget::OnFrame(const AVFrame * frame, bool isHardwareFrame)
{
    if (frame->width != m_pFrame->width || frame->height != m_pFrame->height)
    {
        m_bIsNeedUpdate = true;
    }

    av_frame_unref(m_pFrame);
    av_frame_ref(m_pFrame, frame);

    update();
}

void COpenGLRenderWidget::OnInputCodecContext(const AVCodecContext* ctx)
{
}

HWND COpenGLRenderWidget::GetWindowHandle() const
{
    return HWND();
}

void COpenGLRenderWidget::initializeGL()
{
    if (!m_pFrame)
    {
        m_pFrame = av_frame_alloc();
    }

    initializeOpenGLFunctions();
    glDisable(GL_DEPTH_TEST);

    m_vbo.create();
    m_vbo.bind();
    m_vbo.allocate(coordinate, sizeof(coordinate));

    InitShaders();

    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
}

void COpenGLRenderWidget::paintGL()
{
    m_shaderProgram.bind();

    if (m_bIsNeedUpdate)
    {
        DeinitTextures();
        InitTextures();
        m_bIsNeedUpdate = false;
    }

    if (m_textures.size() != 3)
    {
        return;
    }

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, m_textures[0]);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_pFrame->linesize[0], m_pFrame->height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_pFrame->data[0]);

    // U纹理
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, m_textures[1]);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_pFrame->linesize[1], m_pFrame->height / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_pFrame->data[1]);

    // V纹理
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, m_textures[2]);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_pFrame->linesize[2], m_pFrame->height / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_pFrame->data[2]);

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    m_shaderProgram.release();
}

void COpenGLRenderWidget::resizeGL(int w, int h)
{
    glViewport(0, 0, w, h);
    update();
}

void COpenGLRenderWidget::InitShaders()
{
    QOpenGLShader vertexShader(QOpenGLShader::Vertex);
    if (!vertexShader.compileSourceCode(VERTEX_SHADER))
    {
        qDebug() << "Vertex shader compilation failed. Error: " << vertexShader.log();
        return;
    }

    QOpenGLShader fragmentShader(QOpenGLShader::Fragment);
    if (!fragmentShader.compileSourceCode(FRAGMENT_SHADER))
    {
        qDebug() << "Fragment shader compilation failed. Error: " << fragmentShader.log();
        return;
    }
    
    m_shaderProgram.addShader(&vertexShader);
    m_shaderProgram.addShader(&fragmentShader);
    
    m_shaderProgram.link();
    m_shaderProgram.bind();

    m_shaderProgram.setAttributeBuffer("vertexIn", GL_FLOAT, 0, 3, 3 * sizeof(float));
    m_shaderProgram.enableAttributeArray("vertexIn");

    m_shaderProgram.setAttributeBuffer("textureIn", GL_FLOAT, 12 * sizeof(float), 2, 2 * sizeof(float));
    m_shaderProgram.enableAttributeArray("textureIn");

    m_shaderProgram.setUniformValue("textureY", 0);
    m_shaderProgram.setUniformValue("textureU", 1);
    m_shaderProgram.setUniformValue("textureV", 2);
}

void COpenGLRenderWidget::InitTextures()
{
    m_textures.resize(3);
    glGenTextures(3, m_textures.data());

    glBindTexture(GL_TEXTURE_2D, m_textures[0]);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_pFrame->linesize[0], m_pFrame->height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, nullptr);

    glBindTexture(GL_TEXTURE_2D, m_textures[1]);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_pFrame->linesize[1], m_pFrame->height / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, nullptr);

    glBindTexture(GL_TEXTURE_2D, m_textures[2]);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, m_pFrame->linesize[2], m_pFrame->height / 2, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, nullptr);
}

void COpenGLRenderWidget::DeinitTextures()
{
    if (m_textures.size() != 3)
    {
        return;
    }
    glDeleteTextures(3, m_textures.data());
    m_textures.clear();
}

更高效率

虽然我们将画面渲染通过 OpenGL 来实现 GPU 绘制,但是,将纹理从内存拷贝的 GPU 的显存,同样是需要消耗 CPU 的。同样,FFmpeg 的解码也是需要消耗 CPU 的。那有没有一种方法能够将所有的工作都交给 GPU 呢?答案当然是有的,敬请期待接下来的内容:

  • FFmpeg 硬解码
  • DXVA2+D3D9 实现零拷贝渲染