这一学习系列,基于Qt UI组件以及QOpenGL,学习三维软件的开发。 暂定目标是做一个简单的机器人仿真软件。如果快速开发,可以直接用OSG 或者 OCCT 等开源3d引擎,加上自己熟悉的图形用户界面应用程序框架(如Qt,C#,MFC,Java等),整合出一个三维软件。这些内容在后面有空慢慢学习。这一系列就关注Qt 以及其opengl模块。
OpenGL是Khronos Group开发维护的一个规范,它主要为我们定义了用来操作图形和图片的一系列函数的API,需要注意的是OpenGL本身并非API。GPU的硬件开发商则需要提供满足OpenGL规范的实现,这些实现通常被称为“驱动”,它们负责将OpenGL定义的API命令翻译为GPU指令。
Qt 是一个开源图形用户界面应用程序框架
这里用的QOpenGL是经过Qt封装后的一个模块。因此,opengl更新的一些新特性,如果使用了QOpengl模块,我们就没办法第一时间体验,毕竟Qt那个并不会第一时间更新QOpenGL这个模块,不过这并不影响我们学习,Qt只是按照类的思想对opengl做了一层封装,其使用方法是没多大区别的。 之所以用QOpeng,主要是想结合Qt 开发一款3d软件。
这个Demo目的是使用OpengL,实现基于应用程序创建一个最小QWindow
。
【注意】这个低等级的例子是将QWindow上OpengL结合起来, 而在实际应用中我们会采用更高级的窗口类QOpenGLWindow
。
OpenGLWindow
超级类OpenGLWindow
类充当一个API,它的子类做实际渲染。 它的一个函数render()
用于请求渲染。要么调函数renderNow()
做立即渲染,要么通过调用renderLater()
实现在当前事件循环完后再处理渲染的批事件 。OpenGLWindow
的子类可以通过基于OpenGL的渲染重新实现render() 方法,或者使用QPainter
渲染来重新实现render(QPainter *)
方法。假设底层OpenGl驱动的使能垂直同步, 设置OpenGLWindow::setAnimating(true)
时,在垂直刷新率 render()
会被调用。
在窗口类中要使用OpenGL渲染,我们通常会想到继承 QOpenGLFunctions
,OpenGLWindow
这么实现是为了做到平台无关的访问OpenGL ES 2.0 functions。如你的应用程序使OpenGL以及OpenGL ES 2.0,继承 QOpenGLFunctions
所包含的OpenGL的函数将会被优先使用,你不用关心如何解析这些函数的调用。
这里我是使用OpenGL3.3版本进行开发,因此直接继承QOpenGLFunctions_3_3_Core
OpenGLWindow.h
的实现
class OpenGLWindow : public QWindow, protected QOpenGLFunctions
{
Q_OBJECT
public:
explicit OpenGLWindow(QWindow *parent = 0);
~OpenGLWindow();
virtual void render(QPainter *painter);
virtual void render();
virtual void initialize();
void setAnimating(bool animating);
public slots:
void renderLater();
void renderNow();
protected:
bool event(QEvent *event) override;
void exposeEvent(QExposeEvent *event) override;
private:
bool m_animating;
QOpenGLContext *m_context;
QOpenGLPaintDevice *m_device;
};
QSurface::OpenGLSurface
用于指示窗口是被用于OpenGL渲染,以及不使用QPainter
在QBackingStore
(为窗口提供一个绘制区域)呈现光栅化内容。OpenGLWindow::OpenGLWindow(QWindow *parent)
: QWindow(parent)
{
/*使用OpenGL渲染*/
setSurfaceType(QWindow::OpenGLSurface);
}
initialize()
函数来完成。initialize()
函数必须在调用render()
函数之前被调用一次,从而获得一个当前有效的 QOpenGLContext
。下面的实现中,默认的 the default render(QPainter *)
和initialize()
是没有实现的,而默认的 render()
函数实现了QOpenGLPaintDevice
的初始化,以及紧接着调用了render(QPainter *)
函数void OpenGLWindow::render(QPainter *painter)
{
Q_UNUSED(painter);
}
void OpenGLWindow::initialize()
{
}
void OpenGLWindow::render()
{
/*新建OpenGL绘制设备*/
if (!m_device)
m_device = new QOpenGLPaintDevice;
/*清空颜色缓存,深度缓存,模板缓存*/
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
/*初始化OpenGL绘制设备*/
m_device->setSize(size() * devicePixelRatio());
m_device->setDevicePixelRatio(devicePixelRatio());
QPainter painter(m_device);
render(&painter);
}
【备注】 几个专业术语:
颜色缓冲区: 用于绘图的缓冲区,它包含了颜色索引或者RGBA颜色数据。
深度缓冲区: 存储每个像素的深度值,当启动深度测试时,片段像素深度值和深度缓冲区深度值进行比较,决定片段哪些像素点数据可以替换到颜色缓冲区中。
模板缓冲区(Stencil Buffer): 与颜色缓冲区和深度缓冲区类似,模板缓冲区可以为屏幕上的每个像素点保存一个无符号整数值。这个值的具体意义视程序的具体应用而定。在渲染的过程中,可以用这个值与一个预先设定的参考值相比较,根据比较的结果来决定是否更新相应的像素点的颜色值。这个比较的过程被称为模板测试。模板测试发生在透明度测试(alpha test)之后,深度测试(depth test)之前。如果模板测试通过,则相应的像素点更新,否则不更新。就像使用纸板和喷漆一样精确的混图一样,当启动模板测试时,通过模板测试的片段像素点会被替换到颜色缓冲区中,从而显示出来,未通过的则不会保存到颜色缓冲区中,从而达到了过滤的功能。下图描述了模板缓冲区的原理:
当系统准备好重绘时,renderLater()
函数可以直接的调用QWindow::requestUpdate()
来安排更新。
当接收到一个 expose event(曝光事件)时,我们也可以直接调用renderNow()
函数。当屏幕以及发生变化时,exposeEvent()
通告窗口的曝光度,也就是可见度。当接收到expose event,你可以查询QWindow::isExposed()
找到是否窗口目前已经显示。在接收到expose event之前,不能渲染和调用 QOpenGLContext::swapBuffers()
,在那之前窗口的大小可能都不知道,另外要在屏幕上渲染的内容可能还没结束。
void OpenGLWindow::renderLater()
{
/*将更新事件提交消息队列*/
requestUpdate();
}
bool OpenGLWindow::event(QEvent *event)
{
switch (event->type()) {
case QEvent::UpdateRequest:
renderNow();
return true;
default:
return QWindow::event(event);
}
}
void OpenGLWindow::exposeEvent(QExposeEvent *event)
{
Q_UNUSED(event);
if (isExposed())
renderNow();
}
renderNow()
initialize()
用于子类初始化, 接着调用initializeOpenGLFunctions()
是为了高级类QOpenGLFunctions
能够关联到准确的 QOpenGLContext
。QOpenGLContext::makeCurrent()关联当前的context, 在调用
render()实现实际的渲染,最后调用
QOpenGLContext::swapBuffers(OpenGLWindow*)`使我们制定的需要呈现的内容显示出来.QOpenGLContext::makeCurrent()
将上下文和当前窗口关联起来,我们就可以使用OpenGL的指令,使用方式包括:通过包含 QOpenGLFunctions
(为了方便我们就直接继承),或者通过QOpenGLContext::functions()访问。QOpenGLFunctions
提供对所有OpenGL ES 2.0级别的OpenGL调用的访问,这些调用在OpenGL ES 2.0和桌面OpenGL中都不是标准的。OpenGLWindow::setAnimating(true)
启用动画,我们需要调用 renderLater()
去安排另一个更新请求。void OpenGLWindow::renderNow()
{
/*检查显示事件是否触发*/
if (!isExposed())
return;
bool needsInitialize = false;
/*检查上下文是否有效*/
if (!m_context) {
/*新建上下文*/
m_context = new QOpenGLContext(this);
/*设置相同的 QSurfaceFormat*/
m_context->setFormat(requestedFormat());
/*根据当前的配置创建上下文*/
m_context->create();
needsInitialize = true;
}
/*上下文关联当前窗口*/
m_context->makeCurrent(this);
if (needsInitialize) {
/*QOpenGLFunctions关联到当前上下文*/
initializeOpenGLFunctions();
/*调用子类初始化的实现*/
initialize();
}
/*实现实际渲染*/
render();
/*交换缓冲区*/
m_context->swapBuffers(this);
/*跟新动画*/
if (m_animating)
renderLater();
}
void OpenGLWindow::setAnimating(bool animating)
{
/*使能动画*/
m_animating = animating;
/*更新*/
if (animating)
renderLater();
}
TriangleWindow
继承OpenGLWindow
,展示OpenGL如何渲染一个旋转的三角形。通过间接子类化QOpenGLFunctions,我们可以访问所有OpenGL ES 2.0级别的功能。class TriangleWindow : public OpenGLWindow
{
public:
using OpenGLWindow::OpenGLWindow;
void initialize() override;
void render() override;
private:
GLint m_posAttr = 0;
GLint m_colAttr = 0;
GLint m_matrixUniform = 0;
QOpenGLShaderProgram *m_program = nullptr;
int m_frame = 0;
};
static const char *vertexShaderSource =
"attribute highp vec4 posAttr;\n"
"attribute lowp vec4 colAttr;\n"
"varying lowp vec4 col;\n"
"uniform highp mat4 matrix;\n"
"void main() {\n"
" col = colAttr;\n"
" gl_Position = matrix * posAttr;\n"
"}\n";
static const char *fragmentShaderSource =
"varying lowp vec4 col;\n"
"void main() {\n"
" gl_FragColor = col;\n"
"}\n";
TriangleWindow::initialize()
void TriangleWindow::initialize()
{
//新建着色器程序处理工具
m_program = new QOpenGLShaderProgram(this);
//加载顶点着色器程序
m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource);
//加载片段着色器程序
m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource);
//链接着色器程序
m_program->link();
//着色器程序位置属性
m_posAttr = m_program->attributeLocation("posAttr");
//断言是否成功
Q_ASSERT(m_posAttr != -1);
//着色器程序颜色属性
m_colAttr = m_program->attributeLocation("colAttr");
//断言是否成功
Q_ASSERT(m_colAttr != -1);
//返回统一值
m_matrixUniform = m_program->uniformLocation("matrix");
//断言是否成功
Q_ASSERT(m_matrixUniform != -1);
}
void TriangleWindow::render()
{
/*获取设备像素比*/
const qreal retinaScale = devicePixelRatio();
//设置视口
glViewport(0, 0, width() * retinaScale, height() * retinaScale);
//清空颜色缓存
glClear(GL_COLOR_BUFFER_BIT);
//绑定着色器程序
m_program->bind();
QMatrix4x4 matrix;
//设置透视矩阵
matrix.perspective(60.0f, 4.0f / 3.0f, 0.1f, 100.0f);
//沿着Z轴负方向平移-2
matrix.translate(0, 0, -2);
//绕Y轴旋转
matrix.rotate(100.0f * m_frame / screen()->refreshRate(), 0, 1, 0);
//为着色器程序传入当前变换
m_program->setUniformValue(m_matrixUniform, matrix);
//三角形三个顶点
static const GLfloat vertices[] = {
0.0f, 0.707f,
-0.5f, -0.5f,
0.5f, -0.5f
};
//三角形每个顶点对应的颜色值
static const GLfloat colors[] = {
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f
};
//为着色器设置顶点属性
glVertexAttribPointer(m_posAttr, 2, GL_FLOAT, GL_FALSE, 0, vertices);
//为着色器设置颜色属性
glVertexAttribPointer(m_colAttr, 3, GL_FLOAT, GL_FALSE, 0, colors);
//启用顶点着色器属性
glEnableVertexAttribArray(m_posAttr);
//启用颜色着色器属性
glEnableVertexAttribArray(m_colAttr);
//绘制三角形
glDrawArrays(GL_TRIANGLES, 0, 3);
//关闭顶点着色器属性
glDisableVertexAttribArray(m_colAttr);
//关闭颜色着色器属性
glDisableVertexAttribArray(m_posAttr);
//解绑着色器
m_program->release();
//为下一一次绕y轴旋转做准备
++m_frame;
}
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QSurfaceFormat format;
//设置每个每个像素采样样本个数,用于抗锯齿
format.setSamples(16);
//实例化窗口
TriangleWindow window;
window.setFormat(format);
window.resize(640, 480);
window.show();
//设置动画
window.setAnimating(true);
return app.exec();
}
OpenGLWindow