OpenGL是一个跨平台的GPU渲染库,Qt对OpenGL做了一部分的封装,可以使我们更加方便的使用OpenGL。
关于OpenGL更多的相关学习内容可以参考(比较全和详细的OpenGL教程):
LearnOpenglcn
在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。
3D坐标转为2D坐标的处理过程是由OpenGL的 图形渲染管线 (Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。
图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。 所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心, 它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做 着色器 (Shader)。 着色器是由 GLSL 语言(OpenGL Shading Language)编写而成,他们运行在GPU上,从而节省了CPU的运行时间。
下图是OpenGL渲染管线的流程,其中蓝色部分表示我们可以 自定义的Shader 。
一般情况下我们只需要自定义 顶点着色器 和 片段着色器 就可以了, 可以使用默认的几何着色器。在现代OpenGL中, 我们必须至少定义一个 顶点着色器 和 片段着色器 , 因为GPU中没有默认的顶点/片段着色器。
标准化设备坐标(Normalized Device Coordinates, NDC),标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。
标准化设备坐标接着会变换为屏幕空间坐标(Screen-space Coordinates),这是使用你通过glViewport函数提供的数据,进行视口变换(Viewport Transform)完成的。
OpenGL的坐标系使用的是右手坐标系,Z轴指向屏幕外。DirectX使用的是左手坐标系。(本篇文章绘制三角形不需要考虑Z轴,因为设置的顶点都在相同的Z轴,可以视为一个平面内)
Qt中提供了类 QOpenGLWidget 可以方便我们使用OpenGL。
使用方法如下:
QOpenglWidget 内部会管理一个 QOpenGLContext , 当调用上面几个函数的时候会自动 makeCurrent() 函数,确保运行在当前OpenGL上下文。 如果在非上述的函数中调用OpenGL的API,则需要手动去执行 makeCurrent() 这个函数。
如果想调用OpenGL的相关API,有两种方法:
class MyGLWidget : public QOpenGLWidget
{
public:
MyGLWidget(QWidget *parent) : QOpenGLWidget(parent) { }
protected:
void initializeGL()
{
// Set up the rendering context, load shaders and other resources, etc.:
QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
f->glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
...
}
void resizeGL(int w, int h)
{
// Update projection matrix and other size related settings:
m_projection.setToIdentity();
m_projection.perspective(45.0f, w / float(h), 0.01f, 100.0f);
...
}
void paintGL()
{
// Draw the scene:
QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
f->glClear(GL_COLOR_BUFFER_BIT);
...
}
};
在OpenGL中,我们要顶点数据传给显存。我们通过 顶点缓冲对象 (Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。
函数 glGenBuffers 用于创建一个缓存对象。(通过ID管理并操作这个对象)
GLuint m_nVBOId;
glGenBuffers(1, &m_nVBOId);
使用函数 glBindBuffer 绑定这个缓存对象的类型。
glBindBuffer(GL_ARRAY_BUFFER, m_nVBOId);
GL_ARRAY_BUFFER 表示数组缓存对象。
使用函数 glBufferData 将顶点数据复制到缓冲内存中。
它的函数原型如下:
glBufferData(GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage)
如果数据不会改变,设置为 GL_STATIC_DRAW 就可以了, 如果设置为 GL_DYNAMIC_DRAW 或者 GL_STREAM_DRAW 显卡会把数据放在高速写入的内存。
这里我们的顶点数据定义如下:
struct VertexAttributeData
{
// Postion
float pos[3];
float color[4];
};
// 创建顶点属性数据
VertexAttributeData vAttrData[3];
createVertexAttributeData(vAttrData);
因此这里可以这么调用:
glBufferData(GL_ARRAY_BUFFER, sizeof(vAttrData), vAttrData, GL_STATIC_DRAW);
函数 glVertexAttribPointer 用于告诉 OpenGL 如何解析顶点数据。
函数原型: glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer)
着色器 Shader 的代码与C语言很像。
Shader的完整代码如下:
attribute highp vec3 color;
attribute highp vec3 pos;
varying vec4 M_Color;
void main(void)
{
gl_Position = vec4(pos, 1.0);
M_Color = vec4(color, 1.0);
}
这里 pos 为顶点数据,color 为颜色数据,
他们均来在VBO。vec3 表示3个元素的向量, vec4 表示四个元素的向量。
gl_Position表示处理后的顶点信息。(处理后的标准设备坐标)
varying vec4 M_Color;
void main(void)
{
gl_FragColor = M_Color;
}
Fragment Shader比较简单,gl_FragColor设置颜色信息。
Qt中为我们提供了着色器相关的类: QOpenGLShaderProgram 和 QOpenGLShader
这里的初始化代码如下:
bool OpenglRenderWidget::initShaderProgram(void)
{
m_pShaderProgram = new QOpenGLShaderProgram(this);
// 加载顶点着色器
QString vertexShaderStr(":/vertexshader.vsh");
m_pVertexShader = new QOpenGLShader(QOpenGLShader::Vertex, this);
bool result = m_pVertexShader->compileSourceFile(vertexShaderStr);
if (!result)
{
qDebug() << m_pVertexShader->log();
return false;
}
// 加载片段着色器
QString fragmentShaderStr(":/fragmentshader.fsh");
m_pFragmentShader = new QOpenGLShader(QOpenGLShader::Fragment, this);
result = m_pFragmentShader->compileSourceFile(fragmentShaderStr);
if (!result)
{
qDebug() << m_pFragmentShader->log();
return false;
}
// 创建ShaderProgram
m_pShaderProgram = new QOpenGLShaderProgram(this);
m_pShaderProgram->addShader(m_pVertexShader);
m_pShaderProgram->addShader(m_pFragmentShader);
return m_pShaderProgram->link();
}
使用函数 compileSourceFile 编译Shader文件,如果出错可以使用函数 log 打印错误信息。
创建完 Shader程序后,可以获取位置和颜色的顶点属性位置值(Attribute Location)。 在函数 glVertexAttribPointer 设置顶点信息属性指针时使用。
下面是完整的初始化部分代码:
void OpenglRenderWidget::initializeGL()
{
this->initializeOpenGLFunctions();
// 初始化GPU程序
bool result = initShaderProgram();
if (!result)
return;
m_shaderProgramId = m_pShaderProgram->programId();
// 获取位置和颜色的locationID
m_nPosAttrLocationId = glGetAttribLocation(m_shaderProgramId, "pos");
m_nColorAttrLocationId = glGetAttribLocation(m_shaderProgramId, "color");
// 创建顶点属性数据
VertexAttributeData vAttrData[3];
createVertexAttributeData(vAttrData);
// 创建VBO
glGenBuffers(1, &m_nVBOId);
// 初始化VBO
glBindBuffer(GL_ARRAY_BUFFER, m_nVBOId);
glBufferData(GL_ARRAY_BUFFER, sizeof(vAttrData), vAttrData, GL_STATIC_DRAW);
// 设置顶点信息属性指针
glVertexAttribPointer(m_nPosAttrLocationId, 3, GL_FLOAT, GL_FALSE, sizeof(VertexAttributeData), (void*)0);
glEnableVertexAttribArray(m_nPosAttrLocationId);
// 设置原色信息属性指针
glVertexAttribPointer(m_nColorAttrLocationId, 3, GL_FLOAT, GL_FALSE, sizeof(VertexAttributeData), (void*)(sizeof (float) * 3));
glEnableVertexAttribArray(m_nColorAttrLocationId);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
这里使用函数 glDrawArrays 绘制这个三角形。
它的函数原型如下:
glDrawArrays(GLenum mode, GLint first, GLsizei count)
绘制部分代码如下:
void OpenglRenderWidget::paintGL()
{
glClearColor(51.0f / 255.0f, 76.0f / 255.0f, 76.0f / 255.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 使用shader
m_pShaderProgram->bind();
// 绑定 VBO
glBindBuffer(GL_ARRAY_BUFFER, m_nVBOId);
// 绘制
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindBuffer(GL_ARRAY_BUFFER, 0);
m_pShaderProgram->release();
}
显示效果:
完整代码如下:
头文件:
#ifndef OPENGLRENDERWIDGET_H
#define OPENGLRENDERWIDGET_H
#include <QOpenGLWidget>
#include <QOpenGLShader>
#include <QOpenGLShaderProgram>
#include <QOpenGLFunctions_2_0>
#include <QOpenGLFunctions_3_3_Core>
class OpenglRenderWidget : public QOpenGLWidget, public QOpenGLFunctions_2_0
{
Q_OBJECT
public:
struct VertexAttributeData
{
// Postion
float pos[3];
float color[3];
};
public:
OpenglRenderWidget(QWidget* parent = nullptr);
~OpenglRenderWidget();
protected:
void initializeGL() override;
void resizeGL(int w, int h) override;
void paintGL() override;
private:
bool initShaderProgram(void);
void createVertexAttributeData(VertexAttributeData* pVetAttr);
GLuint m_shaderProgramId;
QOpenGLShaderProgram* m_pShaderProgram = nullptr;
QOpenGLShader* m_pVertexShader = nullptr;
QOpenGLShader* m_pFragmentShader = nullptr;
GLuint m_nVBOId;
// Attribute Location
GLint m_nPosAttrLocationId;
GLint m_nColorAttrLocationId;
};
#endif
CPP文件
#include "OpenglRenderWidget.h"
#include <QDebug>
OpenglRenderWidget::OpenglRenderWidget(QWidget* parent)
:QOpenGLWidget(parent)
{
}
OpenglRenderWidget::~OpenglRenderWidget()
{
}
void OpenglRenderWidget::initializeGL()
{
this->initializeOpenGLFunctions();
// 初始化GPU程序
bool result = initShaderProgram();
if (!result)
return;
m_shaderProgramId = m_pShaderProgram->programId();
// 获取位置和颜色的locationID
m_nPosAttrLocationId = glGetAttribLocation(m_shaderProgramId, "pos");
m_nColorAttrLocationId = glGetAttribLocation(m_shaderProgramId, "color");
// 创建顶点属性数据
VertexAttributeData vAttrData[3];
createVertexAttributeData(vAttrData);
// 创建VBO
glGenBuffers(1, &m_nVBOId);
// 初始化VBO
glBindBuffer(GL_ARRAY_BUFFER, m_nVBOId);
glBufferData(GL_ARRAY_BUFFER, sizeof(vAttrData), vAttrData, GL_STATIC_DRAW);
// 设置顶点信息属性指针
glVertexAttribPointer(m_nPosAttrLocationId, 3, GL_FLOAT, GL_FALSE, sizeof(VertexAttributeData), (void*)0);
glEnableVertexAttribArray(m_nPosAttrLocationId);
// 设置原色信息属性指针
glVertexAttribPointer(m_nColorAttrLocationId, 3, GL_FLOAT, GL_FALSE, sizeof(VertexAttributeData), (void*)(sizeof (float) * 3));
glEnableVertexAttribArray(m_nColorAttrLocationId);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
void OpenglRenderWidget::resizeGL(int w, int h)
{
this->glViewport(0, 0, w, h);
return QOpenGLWidget::resizeGL(w, h);
}
void OpenglRenderWidget::paintGL()
{
glClearColor(51.0f / 255.0f, 76.0f / 255.0f, 76.0f / 255.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 使用shader
m_pShaderProgram->bind();
glBindBuffer(GL_ARRAY_BUFFER, m_nVBOId);
// 绘制
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindBuffer(GL_ARRAY_BUFFER, 0);
m_pShaderProgram->release();
}
bool OpenglRenderWidget::initShaderProgram(void)
{
m_pShaderProgram = new QOpenGLShaderProgram(this);
// 加载顶点着色器
QString vertexShaderStr(":/vertexshader.vsh");
m_pVertexShader = new QOpenGLShader(QOpenGLShader::Vertex, this);
bool result = m_pVertexShader->compileSourceFile(vertexShaderStr);
if (!result)
{
qDebug() << m_pVertexShader->log();
return false;
}
// 加载片段着色器
QString fragmentShaderStr(":/fragmentshader.fsh");
m_pFragmentShader = new QOpenGLShader(QOpenGLShader::Fragment, this);
result = m_pFragmentShader->compileSourceFile(fragmentShaderStr);
if (!result)
{
qDebug() << m_pFragmentShader->log();
return false;
}
// 创建ShaderProgram
m_pShaderProgram = new QOpenGLShaderProgram(this);
m_pShaderProgram->addShader(m_pVertexShader);
m_pShaderProgram->addShader(m_pFragmentShader);
return m_pShaderProgram->link();
}
void OpenglRenderWidget::createVertexAttributeData(VertexAttributeData* pVetAttr)
{
// 第一个点位置信息
pVetAttr[0].pos[0] = 0.0f;
pVetAttr[0].pos[1] = 0.5f;
pVetAttr[0].pos[2] = 0.0f;
// 第一个点颜色信息
pVetAttr[0].color[0] = 1.0f;
pVetAttr[0].color[1] = 0.0f;
pVetAttr[0].color[2] = 0.0f;
// 第二个点位置信息
pVetAttr[1].pos[0] = -0.5f;
pVetAttr[1].pos[1] = -0.5f;
pVetAttr[1].pos[2] = 0.0f;
// 第二个点颜色信息
pVetAttr[1].color[0] = 0.0f;
pVetAttr[1].color[1] = 1.0f;
pVetAttr[1].color[2] = 0.0f;
// 第三个点位置信息
pVetAttr[2].pos[0] = 0.5f;
pVetAttr[2].pos[1] = -0.5f;
pVetAttr[2].pos[2] = 0.0f;
// 第三个点颜色信息
pVetAttr[2].color[0] = 0.0f;
pVetAttr[2].color[1] = 0.0f;
pVetAttr[2].color[2] = 1.0f;
}