QT_OpenGL渲染总结

1 篇文章 0 订阅

Qt 5的图形架构非常依赖OpenGL作为底层3D图形API,但近年来,随着Metal和Vulkan的推出,Qt 6完全改变了局面。Qt Quick中的所有3D图形现在都建立在新的3D图形抽象层之上,该抽象层称为 渲染硬件接口(RHI) 。这使Qt可以使用目标OS /平台上原生的3D图形API。所以Qt Quick现在默认会在Windows上使用Direct3D,在macOS上使用Metal。有关RHI的学习资料可参照QT官网

本文主要使用QT5.14来学习QT封装的OpenGL的渲染。

一、QT中实现OpenGL渲染

1.1 QWindow实现渲染

1.1.1 框架介绍

首先必须继承QWindow类,之后重载两个虚函数event、exposeEvent。

  • event(): 重写此方法以处理发送到窗口的任何事件。如果事件已被识别并处理,则返回true.
  • exposeEvent(): 每当窗口的某个区域无效时(例如,由于窗口系统中的曝光发生更改),窗口系统都会发送暴露事件。
    该应用程序一旦获得isExposed()为true的值,便可以使用QBackingStore和QOpenGLContext开始渲染到窗口中。如果将窗口移出屏幕,使其完全被另一个窗口(图标化或类似窗口)遮盖,则可能会调用此函数,并且isExposed()的值可能更改为false。发生这种情况时,应用程序应停止其呈现,因为它不再对用户可见。
  • isExposed(): 返回此窗口是否在窗口系统中公开。
class OpenGLWindow : public QWindow {
	Q_OBJECT
public:
	explicit OpenGLWindow(QWindow *parent = 0);

    // ...

protected:
	bool event(QEvent *event) override;
	void exposeEvent(QExposeEvent *event) override;

private:
    // ...
};

1.1.2 设置窗口的surfaceType

您可以使用基于网格的QPainter或OpenGL进行绘制。最好在类构造函数中进行设置,例如:

OpenGLWindow::OpenGLWindow(QWindow *parent) :
	QWindow(parent)
{
	setSurfaceType(QWindow::OpenGLSurface);
}

通过调用函数setSurfaceType(QWindow :: OpenGLSurface),您可以指定要创建本机OpenGL窗口。

此时,QT框架会发送以下两个事件:

  • QEvent::UpdateRequest: 部件应该被重绘;
  • QEvent::Expose: 当其屏幕上的内容无效,发送到窗口,并需要从后台存储刷新。

在实现重载的函数void ExposureEvent(QExposeEvent * event)中:

void OpenGLWindow::exposeEvent(QExposeEvent * /*event*/) {
	renderNow(); //渲染操作
}

此处我们仅需要进行渲染即可(下一小节介绍)。

在通用事件处理函数的实现中,event()我们仅UpdateRequest选择事件:

bool OpenGLWindow::event(QEvent *event) {
	switch (event->type()) {
    	case QEvent::UpdateRequest:
    		renderNow(); //渲染操作
		    return true;
	    default:
		    return QWindow::event(event);
	}
}

然后我们的任务将很清楚 renderNow()实现使用OpenGL绘制的函数

1.1.3 OpenGL绘制实现

在Qt中QOpenGLFunctions封装了对本机OpenGL函数的访问。它既可以保留为数据成员,也可以作为实现继承。

1.1.3.1 框架调整
class OpenGLWindow : public QWindow, protected QOpenGLFunctions {
	Q_OBJECT
public:
	explicit OpenGLWindow(QWindow *parent = 0);

	virtual void initialize() = 0;
	virtual void render() = 0;

public slots:
	void renderLater();
	void renderNow();

protected:
	bool event(QEvent *event) override;
	void exposeEvent(QExposeEvent *event) override;

	QOpenGLContext *m_context; // wraps the OpenGL context
};

有两个纯粹的虚函数initialize()和render(),没有它们,OpenGL程序将无法执行。因此,该基类的用户提供这些功能(内容将在后面说明)。

除了renderNow()上面已经调用过的函数(其任务是即时OpenGL绘图)之外,还有另一个函数renderLater()。他们的任务最终是请求一个与垂直同步匹配的新字符调用,这最终对应UpdateRequest于在应用程序事件循环中发送事件。

void OpenGLWindow::renderLater() {
	requestUpdate(); 
}

严格来说,您还可以保存函数并直接requestUpdate()调用插槽,但是该名称最终指示在下一个VSync之前不会进行绘制。

在这一点上,可以预期与帧速率同步的两件事:

  • 用双缓冲区绘制
  • 默认情况下,Qt配置为QEvent::UpdateRequest始终将其发送到VSync。对于60Hz的刷新率,当然可以假设直到切换字符缓冲区为止的时间不超过16 ms

发送UpdateRequest到事件循环中的变量的优点是:在同步周期(即16ms内)内多次调用此函数(例如通过信号插槽连接)最终被组合为一个事件,因此每个VSync仅绘制一次。否则会浪费计算时间。

最后,m_context应该指出新的私有成员变量。该上下文最终封装了本机OpenGL上下文,即OpenGL中使用的状态机。尽管这是动态生成的,但我们不需要析构函数,因为我们还会自动m_context清理QObject-parent关系。

在构造函数中,我们使用nullptr初始化指针变量。

OpenGLWindow::OpenGLWindow(QWindow *parent) :
	QWindow(parent),
	m_context(nullptr)
{
	setSurfaceType(QWindow::OpenGLSurface);
}
1.1.3.2 初始化OpenGL窗口

现在有几种初始化OpenGL绘图窗口的方法。您可以立即在构造函数中执行此操作,但是所有必需的资源(包括mesh/纹理等)都应该已经初始化。

现在,您可以实现自己的初始化函数,该函数最初由类的用户调用。或者,您可以在第一次显示窗口时执行此操作。这里有很多回旋余地,根据初始化对错误的复杂性和敏感性,具有显式初始化功能的变量当然是好的。

此处使用首次使用初始化的变体(这是在Qt中使用对话框时的常见模式)。这意味着该函数renderNow()需要初始化:

void OpenGLWindow::renderNow() {
    // only render if exposed
	if (!isExposed())
		return;

	bool needsInitialize = false;

	// initialize on first call
	if (m_context == nullptr) {
		m_context = new QOpenGLContext(this);
		m_context->setFormat(requestedFormat());
		m_context->create();

		needsInitialize = true;
	}

	m_context->makeCurrent(this);

	if (needsInitialize) {
		initializeOpenGLFunctions();
		initialize(); // call user code
	}

	render(); // call user code

	m_context->swapBuffers(this);
}

该函数由exposeEvent()和调用一次event()。在这两种情况下,仅应在窗口实际可见的情况下进行绘制。因此,isExposed()首先检查该功能以查看窗口是否完全可见。如果不可见退出。

1.1.3.3 QOpenGLContext对象

现在是上面提到的首次使用初始化。

首先QOpenGLContext创建对象。接下来,设置各种特定于OpenGL的要求,从而将在QWindow中设置的格式传输到QOpenGLContext。

requestFormat()函数返回QWindow为表面(设置的格式QSurfaceFormat。其中包含颜色和深度缓冲区以及OpenGL渲染的抗锯齿设置。

  • requestFormat(): 返回此窗口的请求表面格式。如果平台实现不支持所请求的格式,则requestedFormat将与实际的窗口格式不同。

在初始化OpenGL上下文时,必须已经为QWindow定义了这种格式,即在第一次show()调用OpenGLWindow之前。

如果要避免这种错误源,则QSurfaceFormat实际上必须在请求所需函数时将初始化移至特殊函数。

通过调用m_context->create() OpenGL上下文(即状态)来创建,从而使用先前设置的格式参数。

如果要稍后更改格式参数(例如,抗锯齿),则必须首先在上下文对象中重置格式,然后create()再次调用。这将清除并替换之前的上下文。

上下文创建后,最重要的功能makeCurrent()和swapBuffers()服务。

  1. 调用m_context->makeCurrent(this)将上下文对象的内容传输到OpenGL状态。
  2. 初始化的第二步在于调用函数 QOpenGLFunctions :: initializeOpenGLFunctions()。最后,平台特定的OpenGL库是动态集成的,并且将函数指针glXXX…提取到本地OpenGL函数()。
  3. 最后,调用initialize()具有用户特定初始化的函数。
  4. 然后,用户必须在功能中进行3D场景render()的实际渲染。
  5. 最后,我们m_context->swapBuffers(this)将窗口缓冲区与渲染缓冲区交换。
  • QOpenGLFunctions :: initializeOpenGLFunctions(): 为当前上下文初始化OpenGL函数解析。调用此函数后,QOpenGLFunctions对象只能与当前上下文以及与其共享的其他上下文一起使用。再次调用initializeOpenGLFunctions()以更改对象的上下文关联。
  • QOpenGLContext :: makeCurrent(): 在给定的surface上使当前线程中的上下文成为当前上下文。成功返回true,否则返回false。如果表面未暴露,或者由于例如应用程序被挂起而导致图形硬件不可用,则后者可能发生。
  • QOpenGLContext :: swapBuffers(): 交换渲染表面的前后缓冲区。调用此命令以完成OpenGL渲染的框架,并确保在发出任何其他OpenGL命令(例如,作为新框架的一部分)之前再次调用makeCurrent()。

更新窗口缓冲区后,无需重新渲染即可将窗口移动到屏幕上的任何位置,甚至最小化。至少在我们开始处理场景中的动画之前,这是正确的。对于没有动画的应用程序,不要像Unity / Unreal / Irrlicht等游戏引擎那样自动重新渲染每一帧是有意义的。

如果我们仍然要设置动画(如果只是平滑的跟踪镜头),则应在函数结尾处调用renderNow()该函数renderLater(),以便在下一个VSync处接收新的调用。哦,是的:如果窗口是隐藏的(未暴露),则该函数当然会快速退出,并且renderLater()不会调用该函数。那将停止动画。为了使其再次开始运行,有一个已实现的事件函数exposeEvent()可以再次触发渲染。

这样,将完成OpenGL渲染窗口的中央基类。现在,我们便可以使用此基类实现渲染。

1.1.3.4 渲染窗口的实现

用户可自行创建类对象继承OpenGLWindow ,使用头文件调用特定的呈现窗口。比如:

class TriangleWindow : public OpenGLWindow {
public:
	TriangleWindow();
	~TriangleWindow() Q_DECL_OVERRIDE;

	void initialize() Q_DECL_OVERRIDE;
	void render() Q_DECL_OVERRIDE;

private:
	// VAO
	QOpenGLVertexArrayObject	m_vao;
	// Vertex buffer
	QOpenGLBuffer				m_vertexBufferObject;

	// shader programs
	QOpenGLShaderProgram		*m_program;
};

上述案例中的私有成员变量则是QT中对OpenGL的封装,具体使用下文介绍。

1.2 QOpenGLWindow实现渲染

QOpenGLWindow是增强的QWindow,它允许使用兼容QOpenGLWidget且类似于旧版QGLWidget的API轻松创建执行OpenGL渲染的窗口。与QOpenGLWidget不同,QOpenGLWindow不依赖于widgets模块,并提供更好的性能。

一个典型的应用程序将继承QOpenGLWindow并重新实现以下虚函数:

  • initializeGL(): 执行OpenGL资源初始化
  • resizeGL(): 设置转换矩阵和其他与窗口大小有关的资源
  • paintGL(): 发出OpenGL命令或使用QPainter绘制

要计划重绘,请调用update()函数。请注意,这不会立即导致对paintGL()的调用。连续多次调用update()不会以任何方式改变行为。

这是一个插槽,因此可以将其连接到QTimer::timeout()信号以执行动画。但是请注意,在现代OpenGL世界中,依靠同步到显示器的垂直刷新率是一个更好的选择。有关交换间隔的说明,请参见setSwapInterval()。交换间隔为1,这在大多数系统上都是默认情况下的情况,每次重新粉刷后QOpenGLWindow在内部执行的swapBuffers()调用将阻塞并等待vsync。这意味着只要交换完成,就可以通过调用update()再次调度更新,而无需依赖计时器。

要请求上下文的特定配置,请像其他任何QWindow一样使用setFormat()。除其他外,这允许请求给定的OpenGL版本和配置文件,或启用深度和模板缓冲区。

与QWindow不同,QOpenGLWindow允许自己打开一个画家并执行基于QPainter的绘制。

QOpenGLWindow支持多种更新行为。默认值NoPartialUpdate等效于基于OpenGL的常规QWindow或旧版QGLWidget。相比之下,PartialUpdateBlit以及PartialUpdateBlend更符合QOpenGLWidget工作的方式,其中总有一个额外的,专用的帧缓冲区对象存在。通过牺牲一些性能,这些模式可以在每个绘画上仅重画一个较小的区域,并保留前一帧的其余内容。这对于使用QPainter进行增量渲染的应用程序很有用,因为这样一来,它们不必在每个paintGL()调用上重新绘制整个窗口内容。

与QOpenGLWidget相似,QOpenGLWindow支持Qt :: AA_ShareOpenGLContexts属性。启用后,所有QOpenGLWindow实例的OpenGL上下文将彼此共享。这允许访问彼此的共享OpenGL资源。

1.2.1 QOpenGLWindow类的实现

1.2.1.1 QWindow与OpenGLWindow区别

要了解教程QWindow与OpenGLWindow类的异同,得具体看一下该类的实现,其中最重要的区别是继承层次结构。QOpenGLWindow从中得出QOpenGLPaintDevice基于光栅的硬件加速工程图QPainter。

但是有一个小问题。引用手册:

  • OpenGL绘制引擎中的抗锯齿是使用多重采样完成的。大多数硬件需要大量内存来进行多重采样,因此产生的质量与软件绘画引擎的质量不相称。OpenGL绘画引擎的优势在于其性能,而不是视觉渲染质量。(适用于QOpenGLPaintDevice的Qt文档5.9)

如果在OpenGL窗口中绘制了褪色的小部件或控件,则还会影响应用程序的整体外观,而且还会影响具有尖锐边缘的经典小部件。当Windows 10中的应用程序最终绘制一个像素缓冲区时,您可能会从Windows的模糊窗口中熟悉该问题,然后将该像素缓冲区作为纹理插入到3D窗口表面中。

如果要在OpenGL小部件中使用现有的绘图功能(基于QPainter),这仍然很有帮助。如果不需要该功能,PaintDevice及其所需的功能会带来一些不必要的开销(尤其是内存消耗)。

1.2.1.2 QWindow与OpenGLWindow相似之处
  • a.构造函数

构造函数看起来几乎与OpenGLWindow最初的类完全一样。除了将参数传递到私有Pimpl类中。

QOpenGLWindow::QOpenGLWindow(QOpenGLWindow::UpdateBehavior updateBehavior, QWindow *parent)
    : QPaintDeviceWindow(*(new QOpenGLWindowPrivate(nullptr, updateBehavior)), parent)
{
    setSurfaceType(QSurface::OpenGLSurface);
}
  • b.事件处理函数
void QOpenGLWindow::paintEvent(QPaintEvent * /*event*/ ) {
    paintGL();
}

void QOpenGLWindow::resizeEvent(QResizeEvent * /*event*/ ) {
    Q_D(QOpenGLWindow);
    d->initialize();
    resizeGL(width(), height());
}

这paintEvent()简单地传递给用户要实现的功能paintGL()。在这方面,类似于QEvent::UpdateRequest等待的OpenGLWidget中的事件处理。但是,在调用paintEvent()函数直至创建QPaintEvent对象的过程中,将执行许多中间步骤,而这完全不需要。当您查看呼叫链时,思路将变得很清楚:

QPaintDeviceWindow::event(QEvent *event)  // waits for QEvent::UpdateRequest
QPaintDeviceWindowPrivate::handleUpdateEvent()
QPaintDeviceWindowPrivate::doFlush()  // calls QPaintDeviceWindowPrivate::paint()

    bool paint(const QRegion &region)
    {
        Q_Q(QPaintDeviceWindow);
        QRegion toPaint = region & dirtyRegion;
        if (toPaint.isEmpty())
            return false;

        // Clear the region now. The overridden functions may call update().
        dirtyRegion -= toPaint;

        beginPaint(toPaint); // here we call QOpenGLWindowPrivate::beginPaint()

        QPaintEvent paintEvent(toPaint);
        q->paintEvent(&paintEvent); // here we call QOpenGLWindowPrivate::paintEvent()

        endPaint(); // here we call QOpenGLWindowPrivate::endPaint()

        return true;
    }

或者,paintGL()可以从事件QPaintDeviceWindow::exposeEvent()处理例程进行调用,在该例程中直接进行调用QPaintDeviceWindowPrivate::doFlush()。这些功能beginPaint()并 endPaint()照顾临时帧缓冲区,在其中进行UpdateBehaviorQOpenGLWindow::PartialUpdateBlit和QOpenGLWindow::PartialUpdateBlend渲染。没有这些模式,该功能几乎不会发生。

  • c.初始化

resizeEvent()事件处理例程中的初始化调用也很有意思

void QOpenGLWindowPrivate::initialize()
{
    Q_Q(QOpenGLWindow);

    if (context)
        return;

    if (!q->handle())
        qWarning("Attempted to initialize QOpenGLWindow without a platform window");

    context.reset(new QOpenGLContext);
    context->setShareContext(shareContext);
    context->setFormat(q->requestedFormat());
    if (!context->create())
        qWarning("QOpenGLWindow::beginPaint: Failed to create context");
    if (!context->makeCurrent(q))
        qWarning("QOpenGLWindow::beginPaint: Failed to make context current");

    paintDevice.reset(new QOpenGLWindowPaintDevice(q));
    if (updateBehavior == QOpenGLWindow::PartialUpdateBlit)
        hasFboBlit = QOpenGLFramebufferObject::hasOpenGLFramebufferBlit();

    q->initializeGL();
}

实际上,该函数几乎OpenGLWindow::renderNow()与QWindow中的函数初始化部分完全一样。当然,除了QOpenGLWindowPaintDevice创建另一个实例。

1.3 QOpenGLWidget实现渲染

1.3.1 QOpenGLWidget介绍

在所有Qt OpenGL类中,这QOpenGLWidget是迄今为止记录最好的类。
具体列出以下几点:

  • 所有渲染都发生在OpenGL帧缓冲区对象中。
  • 由于由帧缓冲区对象支持,因此QOpenGLWidget的行为与QOpenGLWindow非常相似,其更新行为设置为PartialUpdateBlit或PartialUpdateBlend。这意味着在两次paintGL()调用之间保留了内容,从而可以进行增量渲染。
    注意: 大多数应用程序不需要增量渲染,因为它们将在每次绘画调用时渲染视图中的所有内容。
  • 将QOpenGLWidget添加到窗口中会为整个窗口打开基于OpenGL的合成。在某些特殊情况下,这可能并不理想,因此需要具有单独的本机子窗口的旧QGLWidget样式行为。了解此方法局限性的桌面应用程序(例如,涉及重叠,透明,滚动视图和MDI区域),可以将QOpenGLWindow与QWidget::createWindowContainer()一起使用。这是QGLWidget的现代替代方案,由于缺少附加的合成步骤,因此它比QOpenGLWidget更快。强烈建议将这种方法的使用限制在没有其他选择的情况下。请注意,此选项不适用于大多数嵌入式和移动平台,并且已知在某些台式机平台(例如macOS)上也存在问题。

基本上:OpenGL图像QOpenGLWidget 始终总是首先在缓冲区中渲染,然后根据构图规则(合成)在屏幕上绘制。当然,这比直接绘制要花费更长的时间。

缓冲绘图的主要优点是可以进行增量渲染。是否需要它在很大程度上取决于实际应用。实际上,仅当要渲染的窗口由几个单独的部分组成时,这才有意义。在这种情况下,应用程序也可以由几个OpenGL窗口组成,并且每个窗口都可以单独绘制。

关于可移植性和稳定性的最后一点也许并不完全无关紧要。因此,您可以从两个方面看整个事情:

  • 使用QOpenGLWidget,但如果出现性能问题可以考虑进行切换其他;
  • QWindow与OpenGLWindow是一个自写的轻量级类,如果在发生兼容性问题时可以切换到QOpenGLWidget
1.3.1.1 继承关系

如下类:

class Window : public QOpenGLWidget, protected QOpenGLFunctions {
public:
    Window(QWidget * parent = nullptr);

    ....

protected:
	void initializeGL() override;
	void paintGL() override;

    ....
};

该类QOpenGLWidget本身并不继承自QOpenGLFunctions,这就是为什么必须将此类指定为附加基类的原因(还有另一种方法,但是不必在源代码中进行太多调整)。与其他小部件一样,构造函数也将父指针作为参数。

功能initializeGL()和 paintGL()是在QOpenGLWidget受保护的。

1.3.1.2 初始化

必须相应地扩展构造函数,以便将parent指针传递给基类:

Window::Window(QWidget * parent) :
	QOpenGLWidget(parent),
	m_vertexColors{ 		QColor("#f6a509"),
							QColor("#cb2dde"),
							QColor("#0eeed1"),
							QColor("#068918") },
	m_program(nullptr),
	m_frameCount(5000)
{
	setMinimumSize(600,400);
}

如果该类是一个小部件,您也可以在此处设置最小大小。必须在第一次显示之前设置大小,否则窗口小部件将不可见(并且无法放大)。

使用继承的QOpenGLFunctions函数还需要初始化,但这必须通过调用ininitializeOpenGLFunctions()中的函数在initializeGL()完成。

void Window::initializeGL() {
	initializeOpenGLFunctions();
    ....
}

这UpdateBehavior被设置为QOpenGLWidget默认QOpenGLWidget::NoPartialUpdate,所以不必另行调整。

1.3.1.3 嵌入到QWidget

可以省略窗口小部件容器,并且像其他任何窗口小部件一样嵌入窗口小部件。
具体调用如下:

....

m_window= new Window(this);
m_window->setFormat(format);

// *** create the layout and insert widget container

QVBoxLayout * vlay = new QVBoxLayout;
vlay->setMargin(0);
vlay->setSpacing(0);
vlay->addWidget(m_window);

....

1.3.2 性能比较

QOpenGLWidget与直接通过QWindow使用或QOpenGLWindow使用您自己的OpenGLWindow类进行绘制相比,它要慢多少?

  • 调整窗口大小行为是不同的。调整窗口小部件的大小(在Windows和其他平台上)以及在发布模式下编译程序时都存在明显的延迟。

有区别,但貌似对整体来说影响很小。在动画期间放大/缩小窗口时,优化的延迟效果可能是个问题。

二、QT封装的OpenGL与原生OpenGL API对比

2.1 着色器程序

该类QOpenGLShaderProgram封装了着色器程序,并提供了在本机OpenGL调用中实现的各种便利功能。

m_program = new QOpenGLShaderProgram();

这大致对应于以下OpenGL命令:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

2.2 着色器程序的编译和链接

QOpenGLShaderProgram::addShaderFromSourceFile()类中有几个重载函数,在这里使用带有文件名传输的变量。这些文件在资源文件中引用,因此是通过资源路径指定的。在此处和指定着色器程序的类型很重要。

通过返回代码指示成功或失败。错误处理的功能以后有时间再整理。

最后一步是着色器程序的链接,即,自定义变量的链接(着色器程序之间的通信)。

该类的函数QOpenGLShaderProgram最终封装了以下类型的OpenGL命令:

if (!m_program->addShaderFromSourceFile(
	    QOpenGLShader::Vertex, ":/Vertex.vert"))
	{
		qDebug() << "Vertex shader errors :\n" << m_program->log();
	}

	if (!m_program->addShaderFromSourceFile(
	    QOpenGLShader::Fragment, ":/Fragment.frag"))
	{
		qDebug() << "Fragment shader errors :\n" << m_program->log();
	}

	if (!m_program->link())
		qDebug() << "Shader linker errors :\n" << m_program->log();

原生OpenGL着色器程序初始化

// create the shader
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

// pass shader program in C string
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);

// compile the shader
glCompileShader(vertexShader);

// check success of compilation
int  success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);

// print out an error if any
if (!success) {
    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
    std::cout << "Vertex shader error:\n" << infoLog << std::endl;
}


// ... same for fragment shader

// attach shaders to shader program
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);

// and link
glLinkProgram(shaderProgram);

2.3 顶点缓冲对象(VBO)和顶点数组对象(VBA)

着色器程序完成后,我们首先创建具有三角形坐标的顶点缓冲区对象。然后,将顶点数据分配给属性。因此,您不必一次又一次地进行这些分配,可以在VertexArrayObject(VBA)中进行标注。

  • 顶点缓冲对象(ENGL。VertexBuffer Objects(VBO))最终包含发送到顶点着色器的数据。从OpenGL的角度来看,必须首先创建这些对象,然后将它们绑定(即,后续的OpenGL命令引用缓冲区),然后再次释放。

例如:

float vertices[] = {
		-0.5f, -0.5f, 0.0f,
		 0.5f, -0.5f, 0.0f,
		 0.0f,  0.5f, 0.0f
	};

	// create a new buffer for the vertices
	m_vertexBufferObject = QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); // VBO
	m_vertexBufferObject.create(); // create underlying OpenGL object
	m_vertexBufferObject.setUsagePattern(QOpenGLBuffer::StaticDraw); // must be called before allocate

	m_vertexBufferObject.bind(); // set it active in the context, so that we can write to it
	// int bufSize = sizeof(vertices) = 9 * sizeof(float) = 9*4 = 36 bytes
	m_vertexBufferObject.allocate(vertices, sizeof(vertices) ); // copy data into buffer

在上面的源代码中,首先定义了具有9个浮点数(3 x 3矢量)的静态数组。Z坐标为0。现在我们创建一个类型为的新VertexBufferObject QOpenGLBuffer::VertexBuffer。该调用create()创建对象本身。这对应于原生OpenGL调用如下:

unsigned int VBO;
glGenBuffers(1, &VBO);

然后,通过setUsagePattern()通知QOpenGLBuffer缓冲区对象计划的访问类型。这不会执行OpenGL调用,但是会保存此属性以供以后使用。

通过bind()此VBO的调用,在OpenGL上下文中将其设置为活动状态,即,随后引用VBO的函数调用将引用我们创建的VBO。这对应于原生OpenGL调用如下:

glBindBuffer(GL_ARRAY_BUFFER, VBO);

最后,将数据allocate()复制到对的调用中的缓冲区中。这大致相当于一个memcpy命令,即,缓冲区的源地址已传送,字节长度是第二个参数。在这种情况下,有9个浮点数,即9 * 4 = 36个字节。这对应于原生OpenGL调用如下:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

这里使用预先设置的使用类型(usagePattern)。因此,setUsagePattern()始终保持领先 很重要allocate()。

现在绑定了缓冲区,您现在可以将顶点数据与着色器程序中的输入参数链接起来。由于我们不想每次都在绘制之前执行此操作,因此我们使用VertexArrayObject(VBA),它最终表示类似此类链接的容器。您可以想象一个VBA,就像下面的链接命令的记录一样,其中,当前活动的顶点缓冲区和链接的变量被一起保存。稍后,在实际绘制过程中,您仅需集成VBA,然后VBA将在引擎盖下播放所有记录的链接,从而相应地恢复OpenGL状态。

具体来说,如下所示:

	// Initialize the Vertex Array Object (VAO) to record and remember subsequent attribute assocations with
	// generated vertex buffer(s)
	m_vao.create(); // create underlying OpenGL object
	m_vao.bind(); // sets the Vertex Array Object current to the OpenGL context so it monitors attribute assignments

	// now all following enableAttributeArray(), disableAttributeArray() and setAttributeBuffer() calls are
	// "recorded" in the currently bound VBA.

	// Enable attribute array at layout location 0
	m_program->enableAttributeArray(0);
	m_program->setAttributeBuffer(0, GL_FLOAT, 0, 3);
	// This maps the data we have set in the VBO to the "position" attribute.
	// 0 - offset - means the "position" data starts at the begin of the memory array
	// 3 - size of each vertex (=vec3) - means that each position-tuple has the size of 3 floats (those are the 3 coordinates,
	//     mind: this is the size of GL_FLOAT, not the size in bytes!

首先,我们创建并集成顶点数组对象。enableAttributeArray()并随后setAttributeBuffer()记录对和的所有后续调用。

该命令enableAttributeArray(0)激活顶点缓冲区中的属性(或变量),然后可以在着色器程序中使用布局索引0对其进行寻址。在此示例的顶点着色器中(请参见上文),这是位置矢量。

setAttributeBuffer()现在定义with ,可以在顶点缓冲区中找到数据的位置,即数据类型,编号(此处为3个浮点数,对应于3个坐标)和起始偏移量(此处为0)。

这两个调用对应于OpenGL调用:

glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

现在,所有数据都已初始化,并且可以释放缓冲区对象:

// Release (unbind) all
	m_vertexBufferObject.release();
	m_vao.release(); // not really necessary, but done for completeness

这对应于OpenGL调用:

glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);

您可以看到Qt类最终封装了本地OpenGL函数调用(有时非常直接)。

Qt API在这里感觉不太好选择。像这样的调用m_programm->enableAttributeArray(0)表明实际上在这里更改了对象属性;实际上正在使用OpenGL状态机。相应地,对于许多命令来说,调用的顺序很重要,尽管首先设置哪个属性与对象的单独设置属性无关紧要。这就是为什么我在上面的教程中明确说明了其背后的OpenGL命令。

因此,建议您再次在自己的类中打包Qt API,然后设计相应的蛇形且无错的API。

2.4 QT_OpenGL渲染

实际的渲染发生在render()被基类称为纯虚函数的函数中OpenGLWindow。基类还检查是否需要渲染并设置当前OpenGL上下文。这使您可以直接在此功能中开始渲染。

void TriangleWindow::render() {
	// this function is called for every frame to be rendered on screen
	const qreal retinaScale = devicePixelRatio(); // needed for Macs with retina display
	glViewport(0, 0, width() * retinaScale, height() * retinaScale);

	// set the background color = clear color
	glClearColor(0.1f, 0.1f, 0.2f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);

	// use our shader program
	m_program->bind();
	// bind the vertex array object, which in turn binds the vertex buffer object and
	// sets the attribute buffer in the OpenGL context
	m_vao.bind();
	// now draw the triangles:
	// - GL_TRIANGLES - draw individual triangles
	// - 0 index of first triangle to draw
	// - 3 number of vertices to process
	glDrawArrays(GL_TRIANGLES, 0, 3);
	// finally release VAO again (not really necessary, just for completeness)
	m_vao.release();
}

前三个glXXX命令是本机OpenGL调用,实际上应该总是以这种方式出现。调整ViewPort(glViewport(…))对于调整大小操作以及删除颜色缓冲区()是必不可少的glClear(…)(其他缓冲区将在此调用中稍后删除)。devicePixelRatio()函数适用于缩放比例合适的屏幕(主要用于配备Retina显示屏的Mac)。

只要背景颜色(纯色)不变,此调用也可以移至初始化。

然后是有趣的部分。绑定着色器程序(m_programm->bind()),然后绑定顶点数组对象(VAO)(m_vao.bind())。后者确保在OpenGL上下文中也设置了顶点缓冲区对象和属性映射。然后可以将其用于简单绘制,为此,glDrawArrays(…)将再次使用本机OpenGL命令。

程序的这一部分在原生OpenGL代码中如下所示:

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);

2.5 资源释放

剩下的就是清理析构函数中的保留资源。

TriangleWindow::~TriangleWindow() {
	// resource cleanup

	// since we release resources related to an OpenGL context,
	// we make this context current before cleaning up our resources
	m_context->makeCurrent(this);

	m_vao.destroy();
	m_vertexBufferObject.destroy();
	delete m_program;
}

由于某些资源属于当前窗口的OpenGL上下文,因此您应该首先将OpenGL上下文设置为“当前”(m_context->makeCurrent(this);),以便可以安全地释放这些资源。

这样就可以TriangleWindow完成实施。

2.6 初始化纹理

如果使用原生OpenGL代码创建纹理,则其外观如下所示:

// erstelle Texturobjekt
unsigned int texture;
glGenTextures(1, &texture);
// binde Textur
glBindTexture(GL_TEXTURE_2D, texture);
// setze Attribute:

// Wrap style
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);

// Border color in case of GL_CLAMP_TO_BORDER
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

// Texture Filtering
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

// Lade Texturdaten mittels 'stb_image.h'
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);

// Kopiere Daten in Texture und Erstelle Mipmap
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);

如果使用QOpenGLTexture的话就会变成如下所示:

// erstelle Texturobjekt
QOpenGLTexture * texture = new QOpenGLTexture(QOpenGLTexture::Target2D);
texture->create();
// setze Attribute

// Wrap style
texture->setWrapMode(QOpenGLTexture::ClampToBorder);
texture->setBorderColor(Qt::red);

// Texture Filtering
texture->setMinificationFilter(QOpenGLTexture::NearestMipMapLinear);
texture->setMagnificationFilter(QOpenGLTexture::Linear);

// Lade Bild
QImage img(":/textures/brickwall.jpg");
// Kopiere Daten in Texture und Erstelle Mipmap
texture->setData(img); // allocate() will be called internally

调用mipmap数据时,默认情况下将setData()生成这些数据,而无需其他参数。

调用时setData(),它将自动被调用,allocate()并且图像数据被复制到OpenGL纹理中。如果之后再次调用它allocate(),则会收到错误消息:GL_INVALID_OPERATION error generated. Texture is immutable.

  • 至少在嵌入图像(和mipmap)的属性方面,纹理对象是不可变的。调用后setData(),实际上只能更改影响绑定数据解释的属性(即过滤器和换行样式)。如果要自己更改纹理,则必须销毁并重新创建对象。

2.6.1 着色器纹理链接

如果在一个着色器中使用多个纹理,则必须告诉着色器程序可以在哪个ID下找到纹理。

信息链如下所示:

  • 在着色器程序(片段着色器)中指定纹理(sampler2D),例如brickTexture或roofTiles
  • 您需要提供其参数/统一索引,就好像它们是普通的统一变量→
    brickTextureUniformID,roofTilesUniformID一样。使用这些变量ID,可以指定着色器参数。
  • 这些变量中的每一个都被赋予一个纹理ID,例如BrickTextureUniformID变量获得Texture#0,roofTilesUniformID获得Texture#1。为自己的纹理编号完全独立于统一的ID。
  • 渲染之前,您要集成纹理并指定纹理编号。

在初始化中,它看起来像这样:

SHADER(0)->setUniformValue(m_shaderPrograms[0].m_uniformIDs[1+i],i);

通常,您最多可以使用16个纹理。因此,在具有大量纹理的大型场景中,drawXXX不可避免地要拆分成多个调用。

2.7 帧缓冲区的初始化

使用OpenGL,您必须创建帧缓冲区,深度和模板附件,并为颜色值附加纹理:

// framebuffer configuration
// -------------------------
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
// create a color attachment texture
glGenTextures(1, &textureColorbuffer);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, scr_width, scr_height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureColorbuffer, 0);
// create a renderbuffer object for depth and stencil attachment (we won't be sampling these)
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, scr_width, scr_height); // use a single renderbuffer object for both a depth AND stencil buffer.
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); // now actually attach it
// now that we actually created the framebuffer and added all attachments we want to check if it is actually complete now
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
	qDebug() << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);

而使用QT封装的函数可以直接简单如下(基础版):

m_frameBufferObject = new QOpenGLFramebufferObject(QSize(scr_width, scr_height), QOpenGLFramebufferObject::CombinedDepthStencil);

2.8 窗口尺寸调整

如果更改窗口大小,则还必须调整缓冲区的大小。在resizeGL()中使用原始OpenGL看起来像这样:

// also resize the texture buffer
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
// actual resize operation
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, scr_width, scr_height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
// actual resize operation
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, scr_width, scr_height);

使用QOpenGLFrameBufferObject类时,您只需要重新创建类对象即可:

delete m_frameBufferObject;
m_frameBufferObject = new QOpenGLFramebufferObject(QSize(scr_width, scr_height), QOpenGLFramebufferObject::CombinedDepthStencil);

2.9 使用帧缓冲区

2.9.1 在帧缓冲区中渲染

首先,将帧缓冲区与原始OpenGL集成在一起:

glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

与QOpenGLFrameBufferObject:

m_frameBufferObject->bind();

2.9.2 重置帧缓冲区

渲染场景后,使用本机OpenGL重置正常的渲染缓冲区:

glBindFramebuffer(GL_FRAMEBUFFER, 0);

与QOpenGLFrameBufferObject:

m_frameBufferObject->bindDefault();

2.9.3 帧缓冲纹理的整合

并嵌入纹理以用于ScreenFill着色器和矩形。再次使用OpenGL:

glBindTexture(GL_TEXTURE_2D, textureColorbuffer);

并带有QOpenGLFrameBufferObject:

glBindTexture(GL_TEXTURE_2D, m_frameBufferObject->texture());

三、渲染示例

  • 7
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值