使用 Vulkan * API 并行渲染对象

如果您是一名游戏开发人员,而且还没有跟上 Vulkan* 的发展速度,应该努力跟上。Vulkan API 是业界最热门的新技术之一。它们支持多线程编程,可以简化跨平台开发,而且主要的芯片、GPU 和设备制造商都为其提供支持。Vulkan API 有望成为未来主流图形渲染平台之一。该平台所具备的特性有助于应用延长使用寿命,并能在更多地方运行。您可能会说 Vulkan 能让应用长期成功运行 — 而且该代码示例可以帮助您入门。

这些 API 由 Khronos Group* 于 2015 年推出,并很快获得了英特尔和谷歌* 的支持。Unity Technologies* 2016 年加入,而且 Khronos* 也证实了授予 Vulkan 自动支持多个独立 GPU 的计划。到 2017年,随着 Vulkan API 逐渐成熟,越来越多的游戏制造商宣布开始采用这些 API。2018 年,Vulkan 开始支持苹果的 macOS* 和 iOS* 平台。

Vulkan 的开销较低,同时还支持更多地控制线程和内存管理,而且直接访问 GPU 的能力较 OpenGL* 和其他前代 API 有显著改进。这些功能相结合,支持开发人员用基本相同的代码库从容开发多种平台。在早期主要行业参与者的支持下,Vulkan 平台具有巨大的潜力,应建议开发人员尽快采用这一平台。Vulkan 正是为了这一目标而构建。

为了帮助经验丰富的专业开发商和独立开发商做好使用 Vulkan 的准备,本文将详细介绍使用 Vulkan API 渲染多个 .fbx 和 .obj 对象的示例应用的代码。该应用使用非触控式图形用户界面 (GUI) 读取和显示常见场景的多个对象文件。文件通过线性或并行处理来加载和渲染,可选择用于比较性能。此外,该应用支持通过简单的 UI 移动、旋转和缩放对象。

Multiple rendered objects displayed simultaneously
图 1.同时显示多个渲染对象;选定的对象用边界框表示。

该应用还具有以下特性:

  • 加载的模型在列表中显示
  • 选定的对象在屏幕上用边界框标识
  • 对象信息和统计信息显示屏显示顶点数量
  • 指定增量或绝对坐标与旋转的功能
  • 在文件资源管理器窗口中打开对象文件
  • 在线框模式下查看对象的选项
  • 在读取和渲染时显示单线程与多线程的统计信息

让开发人员了解和学习最新技术和开发技术,是确保他们成功的重要部分。为此,该项目中的所有源代码和库均可供下载,因此您可以自行构建和学习应用,并调整功能以便应用使用。

对于 Vulkan 新手来说,学习曲线可能非常陡峭。因为它可为开发人员提供丰富的功能和广泛的控制,所以 Vulkan 包含的结构要多得多,而且需要的初始化比 OpenGL 和其他图形库都多。对示例应用来说,仅渲染器 (renderer.cpp) 需要超过 500 行代码。

为尽量减少所需的代码量,此示例应用主要侧重于构建渲染不同对象类型的统一方法。共性在初始化步骤中识别,这些步骤和一般管道是分开的,而且特定于 3D 对象的特定实例的部分从文件中加载和渲染。边界框是另一种类型的对象,需要自己的着色器、设置和管道。但只有一个实例。最大限度地缩小对象类型之间的编码差异也有助于提高灵活性并简化代码。

在开发此示例的过程中,最重要挑战之一会涉及到多线程渲染。尽管 Vulkan API 被认为是“线程安全的”,但是如果应用于命令池和命令缓冲器,某些对象需要在主机端和实施点实现显式同步。当对象请求命令缓冲器时,该缓冲器通过命令池来分配。如果同时从多个线程并行访问命令池,应用将崩溃或在 Vulkan 控制台发布警告。解决方法是使用互斥或互斥体,将对共享命令池的访问进行排序。但这样会消除并行处理的优势,因为线程会相互竞争并发生阻塞。相反,示例应用为每个 3D 对象实例实施单独的命令缓冲器和命令池,然后要求额外代码来释放资源。

需要的组件

在英特尔图形处理器单元 (GPU) 上开发 Vulkan* API,最低要求是使用运行 64 位 Windows* 7、8.1 或 10 的第六代英特尔® 处理器家族的处理器(2015 年 8 月推出)。英特尔还为第六代、第七代或第八代处理器提供仅适用于 64 位 Windows®10 的驱动程序。Vulkan 驱动程序目前包含在英特尔® 核芯显卡驱动程序中,可帮助简化设置过程。有关说明可用于帮助在运行 Unity* Unreal* Engine 4 的基于英特尔® 的系统上安装 Vulkan 驱动程序。

代码介绍

该应用的构建是为了帮助开发人员学习如何使用 Vulkan。此次介绍解释了用于制作示例应用的技术,可简化您的入门准备。为了缩短规划架构的时间,使用增量迭代过程开发该应用,有助于最大限度地减少编码阶段的变化。该项目分为三个部分:UI(MainWindow 和 VulkanWindow)、模型加载器 (Model.cpp/h) 和渲染 (Renderer.cpp/h)。特性列表按优先级划分,并按实施难度排序。然后从最简单的特性开始编码 — 仅在需要时重构和更改设计。

MainWindow.cpp

在示例应用的主窗口中,使用单个进程或并行加载对象文件。无论哪种方式,计时器都会计算总加载时间以进行比较。并行处理文件时,QtConcurrent 组件用于处理工作线程。

"loadModels()" 函数启动并行或线性处理文件。在前几行中,启动计数器。然后计算文件的加载时间,并使用 Assimp* 外部库创建 aiScene。接下来,aiScene 将转换为为此应用创建的类模型,这对 Vulkan 来说更方便。并行处理文件时,会创建并显示进度对话框。

void MainWindow::loadModels()
{
    clearModels();
    m_elapsedTimer.start(); // counts total loading time

    std::function<QSharedPointer<Model>(const QString &)> load = [](const QString &path) {
        QSharedPointer<Model> model;
        QFileInfo info(path);
        if (!info.exists())
            return model;
        QElapsedTimer timer;
        timer.start(); // loading time for this file
        Assimp::Importer importer;
// read file from disk and create aiScene (external library Asimp) instance
        const aiScene* scene = importer.ReadFile(path.toStdString(),
                                                 aiProcess_Triangulate |
                                                 aiProcess_RemoveComponent |
                                                 aiProcess_GenNormals |
                                                 aiProcess_JoinIdenticalVertices);

        qDebug() << path << (scene ?"OK" : importer.GetErrorString());
        if (scene) {
// aiScene format is not very convenient for renderer so we designed class Model to keep data ready for Vulkan renderer.
            model = QSharedPointer<Model>::create(info.fileName(), scene);  //convert aiScene to class Model (Model.cpp) that’s convenient for Vulkan renderer

            if (model->isValid()) {
                model->loadingTime = timer.elapsed();
            } else {
                model.clear();
            }
        }
        return model;
    };
// create a progress dialog for app user
    if (m_progressDialog == nullptr) {
        m_progressDialog = new QProgressDialog(this);
        QObject::connect(m_progressDialog, &QProgressDialog::canceled, &m_loadWatcher, &QFutureWatcher<void>::cancel);
        QObject::connect(&m_loadWatcher,  &QFutureWatcher<void>::progressRangeChanged, m_progressDialog, &QProgressDialog::setRange);
        QObject::connect(&m_loadWatcher, &QFutureWatcher<void>::progressValueChanged,  m_progressDialog, &QProgressDialog::setValue);
    }
    // using QtConcurrent for parallel file processing in worker threads
    QFuture<QSharedPointer<Model>> future = QtConcurrent::mapped(m_files, load);
    m_loadWatcher.setFuture(future);
//present the progress dialog to app user
    m_progressDialog->exec();
}

"loadFinished()” 函数处理并行或线性处理的结果,将对象文件名添加到 “listView”,并将模型传递给渲染器。

void MainWindow::loadFinished() {
    qDebug("loadFinished");
    Q_ASSERT(m_vulkanWindow->renderer());
    m_progressDialog->close(); // close the progress dialog
// iterate around result of file load
    const auto & end = m_loadWatcher.future().constEnd();

// loop for populating list of file names
    for (auto it = m_loadWatcher.future().constBegin() ; it != end; ++it) {
        QSharedPointer<Model> model = *it;
        if (model) {
            ui->modelsList->addItem(model->fileName); // populates list view
// pass object to renderer (created in vulkanWindow, which is part of the mainWindow)
            m_vulkanWindow->renderer()->addObject(model);
        }
    }

通过用边界框围绕,识别屏幕上的所选对象。

mainwindow.cpp:MainWindow::currentRowChanged(int row)
{
...
   if (m_vulkanWindow->renderer())
           m_vulkanWindow->renderer()->selectObject(row);

renderer.cpp:Renderer::selectObject(int index) - inflates BoundaryBox object’s  model
...

在屏幕上显示所选对象的对象信息和统计信息(即顶点数量)。此处将创建特定于对象的统计信息,并显示场景的加载时间。

MainWindow::currentRowChanged(int row) - shows statistic for selected object:
{
…
// prepare object-specific statistics (verticies, etc)
QString stat = tr("Loading time:%1ms.Vertices:%2, Triangles:%3")
               .arg(item->model->loadingTime)
               .arg(item->model->totalVerticesCount())
               .arg(item->model->totalTrianglesCount());
ui->objectStatLabel->setText(stat);

// display total scene loading time
void MainWindow::loadFinished()
ui->totalStatLabel->setText(tr("Total loading time:%1ms").arg(m_elapsedTimer.elapsed()));

// show rendering performance in frames per second
void MainWindow::timerEvent(QTimerEvent *)
ui->fpsLabel->setText(tr("Performance:%1 fps").arg(renderer->fps(), 0, 'f', 2, '0'));
...

支持应用用户指定绝对坐标和旋转。

void MainWindow::positionSliderChanged(int)
{
    const int row = ui->modelsList->currentRow();
    if (row == -1 || m_ignoreSlidersSignal || !m_vulkanWindow->renderer())
        return;
    m_vulkanWindow->renderer()->setPosition(row, ui->posXSlider->value() / 100.0f, ui->posYSlider->value() / 100.0f,
                                ui->posZSlider->value() / 100.0f );
}

void MainWindow::rotationSliderChanged(int)
{
    const int row = ui->modelsList->currentRow();
    if (row == -1 || m_ignoreSlidersSignal || !m_vulkanWindow->renderer())
        return;
     m_vulkanWindow->renderer()->setRotation(row, ui->rotationXSlider->value(), ui->rotationYSlider->value(),
                                ui->rotationZSlider->value());
}

Sample app uses file explorer for objects to render
图 2.示例应用实施文件浏览器窗口,查找和打开待渲染的对象。

支持应用使用文件资源管理器窗口打开对象文件。

MainWindow::MainWindow(QWidget *parent)
    :QWidget(parent),
      ui(new Ui::MainWindow)
{
…

connect(ui->loadButton, &QPushButton::clicked, this, [this] {
       const QStringList & files = QFileDialog::getOpenFileNames(this, tr("Select one or more files"), QString::null, "3D Models (*.obj *.fbx)");
       if (!files.isEmpty()) {
           m_files = files;
           loadModels();
           ui->reloadButton->setEnabled(true);
       }
   });
...

Objects rendered in wireframe mode
图 3.以线框模式渲染的对象;选定的对象由边界框指示出来。

支持用户以线框模式显示对象。

MainWindow::MainWindow(QWidget *parent)
    :QWidget(parent),
      ui(new Ui::MainWindow)
{
...
 connect(ui->wireframeSwitch, &QCheckBox::stateChanged, this, [this]{
       if (m_vulkanWindow->renderer()) {
           m_vulkanWindow->renderer()->setWirefameMode(ui->wireframeSwitch->checkState() == Qt::Checked);
       }
   });
Renderer.cpp  (line 386-402):
void Renderer::setWirefameMode(bool enabled)
...

Renderer.cpp

由于 Vulkan API 的复杂性,该应用开发人员面临的最大挑战是构建 Renderer,为 VulkanWindow 实施特定于应用的渲染逻辑。

Thread selection is simplified with drop-down window
图 4.通过下拉窗口简化线程选择;理想数量基于主机系统中的内核。

尤其困难的是如何在渲染和资源释放阶段不使用互斥锁的情况下,实现工作线程和 UI 线程的同步。在渲染阶段,可以通过分开每个 Object3D 实例的命令池和辅助命令缓冲器来实现这一目标。在资源释放阶段,必须确保主机和 GPU 渲染阶段已完成。

Total loading time allows comparison
图 5.对象文件的总加载时间和顶点数支持比较单线程和多线程加载时间。

渲染结果可能会有所不同

系统处理器、GPU 和主机系统的其他因素,以及对象文件的大小都会决定单线程和多线程对象的渲染时间。结果可能会有所不同。通常,当 "Renderer::&m_renderWatcher" 发出“完成”信号并调用 "Renderer::endFrame()” 时,主机渲染阶段结束。资源释放阶段可能会在以下情况下启动:

  1. Vulkan 窗口已调整大小或关闭。
    "Renderer::releaseSwapChainResources” 和 "Renderer::releaseResources” 将调用。
  2. 线框模式已更改 — "Renderer::setWirefameMode"
  3. 对象已删除 —"Renderer::deleteObjects"
  4. 对象已添加 —"Renderer::addObject"

在这些情况下,我们首先需要做的是:

  1. 等待所有工作线程完成。
  2. 明确完成渲染阶段,调用 "Renderer::endFrame()",它还将设置标记 "m_framePreparing = false" 以忽略在不久的将来异步出现的工作线程的所有结果。
  3. 等待 GPU 使用 "m_deviceFunctions->vkDeviceWaitIdle(m_window->device())" 调用完成所有图形队列。

这一过程在 “Renderer :: rejectFrame” 中实施:

void Renderer::rejectFrame()
{
   m_renderWatcher.waitForFinished(); // all workers must be finished
   endFrame(); // flushes current frame
   m_deviceFunctions->vkDeviceWaitIdle(m_window->device()); // all graphics queues must be finished
}

并行准备命令缓冲器以渲染 3D 对象 iseature 通过以下三个函数来实施;各个函数的代码如下:

  1. Renderer::startNextFrame — 需要添加当前帧的绘制命令时调用此函数。
  2. Renderer::drawObject — 它将命令记录到辅助命令缓冲器。它在工作线程中运行。完成后,将缓冲器报告给 UI 线程以记录到主命令缓冲器。
  3. Renderer::endFrame — 该函数完成当前命令缓冲器的渲染通道,向 VulkanWindow 报告帧已就绪,并请求即时更新以维持渲染。

函数 1:void Renderer::startNextFrame()

本节主要介绍可能不需要修改的特定于 Vulkan 的代码。该代码片段旨在展示如何使用 Vulkan 加载对象文件。大约十几行,加载的文件被发送到支持辅助命令缓冲器的渲染器,以支持并行加载对象。

void Renderer::startNextFrame()
{
    m_framePreparing = true;

    const QSize imageSize = m_window->swapChainImageSize();

    VkClearColorValue clearColor = { 0, 0, 0, 1 };

    VkClearValue clearValues[3] = {};
    clearValues[0].color = clearValues[2].color = clearColor;
    clearValues[1].depthStencil = { 1, 0 };

    VkRenderPassBeginInfo rpBeginInfo = {};
    memset(&rpBeginInfo, 0, sizeof(rpBeginInfo));
    rpBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
    rpBeginInfo.renderPass = m_window->defaultRenderPass();
    rpBeginInfo.framebuffer = m_window->currentFramebuffer();
    rpBeginInfo.renderArea.extent.width = imageSize.width();
    rpBeginInfo.renderArea.extent.height = imageSize.height();
    rpBeginInfo.clearValueCount = m_window->sampleCountFlagBits() > VK_SAMPLE_COUNT_1_BIT ?3 :2;
    rpBeginInfo.pClearValues = clearValues;

    // starting render pass with secondary command buffer support
    m_deviceFunctions->vkCmdBeginRenderPass(m_window->currentCommandBuffer(), &rpBeginInfo,  VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS);

    if (m_objects.size()) {
        // starting parallel command buffers generation in worker threads using QtConcurrent
        auto drawObjectFn = std::bind(&Renderer::drawObject, this, std::placeholders::_1);
        QFuture<VkCommandBuffer> future = QtConcurrent::mapped(m_objects, drawObjectFn);
        m_renderWatcher.setFuture(future);
} else {
// if no object exists, end immediately
        endFrame();
    }
}


函数 2:Renderer::endFrame()

此函数指示 Vulkan 所有命令缓冲器都已做好使用 GPU 进行渲染的准备。

void Renderer::endFrame()
{
    if (m_framePreparing) {
        m_framePreparing = false;
        m_deviceFunctions->vkCmdEndRenderPass(m_window->currentCommandBuffer());
        m_window->frameReady();
        m_window->requestUpdate();
        ++m_framesCount;
    }
}

函数 3:Renderer::drawObject()

该函数准备好命令缓冲器以发送至 GPU。如上所述,此代码片段中特定于 Vulkan 的代码也在工作线程中运行,而且供其他应用使用时可能不需要修改。

// running in a worker thread
VkCommandBuffer Renderer::drawObject(Object3D * object)
{
    if (!object->model)
        return VK_NULL_HANDLE;

    const PipelineHandlers & pipelineHandlers = object->role == Object3D::Object ? m_objectPipeline : m_boundaryBoxPipeline;
    VkDevice device = m_window->device();

    if (object->vertexBuffer == VK_NULL_HANDLE) {
        initObject(object);
    }

    VkCommandBuffer & cmdBuffer = object->cmdBuffer[m_window->currentFrame()];

    VkCommandBufferInheritanceInfo inherit_info = {};
    inherit_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO;
    inherit_info.renderPass = m_window->defaultRenderPass();
    inherit_info.framebuffer = m_window->currentFramebuffer();

    VkCommandBufferBeginInfo cmdBufBeginInfo = {
        VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
        nullptr,
        VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT,
        &inherit_info
    };
    VkResult res = m_deviceFunctions->vkBeginCommandBuffer(cmdBuffer, &cmdBufBeginInfo);
    if (res != VK_SUCCESS) {
        qWarning("Failed to begin frame command buffer:%d", res);
        return VK_NULL_HANDLE;
    }

    const QSize & imageSize = m_window->swapChainImageSize();

    VkViewport viewport;
    viewport.x = viewport.y = 0;
    viewport.width = imageSize.width();
    viewport.height = imageSize.height();
    viewport.minDepth = 0;
    viewport.maxDepth = 1;
    m_deviceFunctions->vkCmdSetViewport(cmdBuffer, 0, 1, &viewport);

    VkRect2D scissor;
    scissor.offset.x = scissor.offset.y = 0;
    scissor.extent.width = imageSize.width();
    scissor.extent.height = imageSize.height();
    m_deviceFunctions->vkCmdSetScissor(cmdBuffer, 0, 1, &scissor);

    QMatrix4x4 objectMatrix;
    objectMatrix.translate(object->translation.x(), object->translation.y(), object->translation.z());
    objectMatrix.rotate(object->rotation.x(), 1, 0, 0);
    objectMatrix.rotate(object->rotation.y(), 0, 1, 0);
    objectMatrix.rotate(object->rotation.z(), 0, 0, 1);
    objectMatrix *= object->model->transformation;


    m_deviceFunctions->vkCmdBindPipeline(cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineHandlers.pipeline);

    // pushing view-projection matrix to constants
    m_deviceFunctions->vkCmdPushConstants(cmdBuffer, pipelineHandlers.pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, 64, m_world.constData());

    const int nodesCount = object->model->nodes.size();
    for (int n = 0; n < nodesCount; ++n) {
        const Node &node = object->model->nodes.at(n);
        const uint32_t frameUniSize = nodesCount * object->uniformAllocSize;
        const uint32_t frameUniOffset = m_window->currentFrame() * frameUniSize + n * object->uniformAllocSize;
        m_deviceFunctions->vkCmdBindDescriptorSets(cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineHandlers.pipelineLayout, 0, 1,
                                                   &object->descSet, 1, &frameUniOffset);

        // mapping uniform buffer to update matrix
        quint8 *p;
        res = m_deviceFunctions->vkMapMemory(device, object->bufferMemory, object->uniformBufferOffset + frameUniOffset,
                                                      MATRIX_4x4_SIZE, 0, reinterpret_cast<void **>(&p));
        if (res != VK_SUCCESS)
            qFatal("Failed to map memory:%d", res);

        QMatrix4x4 nodeMatrix = objectMatrix * node.transformation;
        memcpy(p, nodeMatrix.constData(), 16 * sizeof(float)); //updating matrix

        m_deviceFunctions->vkUnmapMemory(device, object->bufferMemory);

        // drawing meshes
        for (const int i: qAsConst(node.meshes)) {
            const Mesh &mesh = object->model->meshes.at(i);
            VkDeviceSize vbOffset = mesh.vertexOffsetBytes();
            m_deviceFunctions->vkCmdBindVertexBuffers(cmdBuffer, 0, 1, &object->vertexBuffer, &vbOffset);
            m_deviceFunctions->vkCmdBindIndexBuffer(cmdBuffer, object->vertexBuffer, object->indexBufferOffset + mesh.indexOffsetBytes(), VK_INDEX_TYPE_UINT32);

            m_deviceFunctions->vkCmdDrawIndexed(cmdBuffer, mesh.indexCount, 1, 0, 0, 0);
        }
    }

    m_deviceFunctions->vkEndCommandBuffer(cmdBuffer);

    return cmdBuffer;
}

将整个辅助缓冲器报告回 GUI 线程,而且命令可以在主缓冲器上执行(除非取消帧渲染):

Renderer.cpp (line 31-38):
QObject::connect(&m_renderWatcher, &QFutureWatcher<VkCommandBuffer>::resultReadyAt, [this](int index) {
       // secondary command buffer of some object is ready
       if (m_framePreparing) {
           const VkCommandBuffer & cmdBuf = m_renderWatcher.resultAt(index);
           if (cmdBuf)
               this->m_deviceFunctions->vkCmdExecuteCommands(this->m_window->currentCommandBuffer(), 1, &cmdBuf);
       }
   });
...

开发渲染器的另一个主要挑战源于处理不同类型的图形对象 — 从文件加载的图形对象和以围绕所选对象的边界框形式动态生成的图形对象。这会导致出现问题,因为它们使用不同的着色器、原始拓扑和多边形模式。目标是尽可能为不同的对象统一代码,以避免复制类似的代码。两种类型的对象都由单类 Object3D 表示。

在 "Renderer::initPipelines()" 函数中,差异以函数参数的形式隔离起来并通过以下方式调用:

initPipeline(m_objectPipeline,
QStringLiteral(":/shaders/item.vert.spv"),
 QStringLiteral(":/shaders/item.frag.spv"),
                VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
m_wireframeMode ?VK_POLYGON_MODE_LINE :VK_POLYGON_MODE_FILL);

initPipeline(m_boundaryBoxPipeline,
QStringLiteral(":/shaders/selection.vert.spv"),
QStringLiteral(":/shaders/selection.frag.spv"),
                VK_PRIMITIVE_TOPOLOGY_LINE_LIST, VK_POLYGON_MODE_LINE);

事实证明,根据角色统一特定对象的初始化也很有帮助。这种初始化通过 "Renderer::initObject()" 函数来处理:

const PipelineHandlers & pipelineHandlers = object->role == Object3D::Object ? m_objectPipeline : m_boundaryBoxPipeline;

"Function:Renderer::initPipeline()” 显示完整功能。请注意,除对象文件外,边界框是另一种类型的对象,需要自己的着色器、设置和管道。最大限度地缩小对象类型之间的编码差异也有助于提高灵活性和简化代码。

void Renderer::initPipeline(PipelineHandlers & pipeline, const QString & vertShaderPath, const QString & fragShaderPath,
                            VkPrimitiveTopology topology, VkPolygonMode polygonMode)
{
    VkDevice device = m_window->device();
    VkResult res;
    VkVertexInputBindingDescription vertexBindingDesc = {
        0, // binding
        6 * sizeof(float), //x,y,z,nx,ny,nz
        VK_VERTEX_INPUT_RATE_VERTEX
    };

    VkVertexInputAttributeDescription vertexAttrDesc[] = {
        { // vertex
            0,
            0,
            VK_FORMAT_R32G32B32_SFLOAT,
            0
        },
        { // normal
            1,
            0,
            VK_FORMAT_R32G32B32_SFLOAT,
            6 * sizeof(float)
        }
    };


    VkPipelineVertexInputStateCreateInfo vertexInputInfo = {};
    vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
    vertexInputInfo.vertexBindingDescriptionCount = 1;
    vertexInputInfo.pVertexBindingDescriptions = &vertexBindingDesc;
    vertexInputInfo.vertexAttributeDescriptionCount = 2;
    vertexInputInfo.pVertexAttributeDescriptions = vertexAttrDesc;


    VkDescriptorSetLayoutBinding layoutBinding = {};
    layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
    layoutBinding.descriptorCount = 1;
    layoutBinding.stageFlags =  VK_SHADER_STAGE_VERTEX_BIT;

    VkDescriptorSetLayoutCreateInfo descLayoutInfo = {
        VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
        nullptr,
        0,
        1,
        &layoutBinding
    };

     //!  View-projection matrix going to be pushed to vertex shader constants.
    VkPushConstantRange push_constant = {
            VK_SHADER_STAGE_VERTEX_BIT,
            0,
            64
        };

    res = m_deviceFunctions->vkCreateDescriptorSetLayout(device, &descLayoutInfo, nullptr, &pipeline.descSetLayout);
    if (res != VK_SUCCESS)
        qFatal("Failed to create descriptor set layout:%d", res);


    // Pipeline layout
    VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
    pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
    pipelineLayoutInfo.setLayoutCount = 1;
    pipelineLayoutInfo.pSetLayouts = &pipeline.descSetLayout;
    pipelineLayoutInfo.pushConstantRangeCount = 1;
    pipelineLayoutInfo.pPushConstantRanges = &push_constant;

    res = m_deviceFunctions->vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipeline.pipelineLayout);
    if (res != VK_SUCCESS)
        qFatal("Failed to create pipeline layout:%d", res);

    // Shaders
    VkShaderModule vertShaderModule = loadShader(vertShaderPath);
    VkShaderModule fragShaderModule = loadShader(fragShaderPath);

    // Graphics pipeline
    VkGraphicsPipelineCreateInfo pipelineInfo;
    memset(&pipelineInfo, 0, sizeof(pipelineInfo));
    pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;

    VkPipelineShaderStageCreateInfo shaderStageCreationInfo[2] = {
        {
            VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
            nullptr,
            0,
            VK_SHADER_STAGE_VERTEX_BIT,
            vertShaderModule,
            "main",
            nullptr
        },
        {
            VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
            nullptr,
            0,
            VK_SHADER_STAGE_FRAGMENT_BIT,
            fragShaderModule,
            "main",
            nullptr
        }
    };
    pipelineInfo.stageCount = 2;
    pipelineInfo.pStages = shaderStageCreationInfo;

    pipelineInfo.pVertexInputState = &vertexInputInfo;

    VkPipelineInputAssemblyStateCreateInfo inputAssemblyInfo = {};
    inputAssemblyInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
    inputAssemblyInfo.topology = topology;
    pipelineInfo.pInputAssemblyState = &inputAssemblyInfo;

    VkPipelineViewportStateCreateInfo viewportInfo = {};
    viewportInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
    viewportInfo.viewportCount = 1;
    viewportInfo.scissorCount = 1;
    pipelineInfo.pViewportState = &viewportInfo;

    VkPipelineRasterizationStateCreateInfo rasterizationInfo = {};
    rasterizationInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
    rasterizationInfo.polygonMode = polygonMode;
    rasterizationInfo.cullMode = VK_CULL_MODE_NONE;
    rasterizationInfo.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
    rasterizationInfo.lineWidth = 1.0f;
    pipelineInfo.pRasterizationState = &rasterizationInfo;

    VkPipelineMultisampleStateCreateInfo multisampleInfo = {};
    multisampleInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
    multisampleInfo.rasterizationSamples = m_window->sampleCountFlagBits();
    pipelineInfo.pMultisampleState = &multisampleInfo;

    VkPipelineDepthStencilStateCreateInfo depthStencilInfo = {};
    depthStencilInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
    depthStencilInfo.depthTestEnable = VK_TRUE;
    depthStencilInfo.depthWriteEnable = VK_TRUE;
    depthStencilInfo.depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL;
    pipelineInfo.pDepthStencilState = &depthStencilInfo;

    VkPipelineColorBlendStateCreateInfo colorBlendInfo  = {};
    colorBlendInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
    VkPipelineColorBlendAttachmentState att = {};
    att.colorWriteMask = 0xF;
    colorBlendInfo.attachmentCount = 1;
    colorBlendInfo.pAttachments = &att;
    pipelineInfo.pColorBlendState = &colorBlendInfo;

    VkDynamicState dynamicEnable[] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
    VkPipelineDynamicStateCreateInfo dynamicInfo = {};
    dynamicInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
    dynamicInfo.dynamicStateCount = 2;
    dynamicInfo.pDynamicStates = dynamicEnable;
    pipelineInfo.pDynamicState = &dynamicInfo;

    pipelineInfo.layout = pipeline.pipelineLayout;
    pipelineInfo.renderPass = m_window->defaultRenderPass();

    res = m_deviceFunctions->vkCreateGraphicsPipelines(device, m_pipelineCache, 1, &pipelineInfo, nullptr, &pipeline.pipeline);
    if (res != VK_SUCCESS)
        qFatal("Failed to create graphics pipeline:%d", res);

    if (vertShaderModule)
        m_deviceFunctions->vkDestroyShaderModule(device, vertShaderModule, nullptr);
    if (fragShaderModule)
        m_deviceFunctions->vkDestroyShaderModule(device, fragShaderModule, nullptr);
}

结论

编码灵活性是低级 Vulkan API 的特点,但重点关注每一个 Vulkan 步骤的具体操作至关重要。较低层次的编程还支持精确微调硬件访问(OpenGL 不支持)的某些方面。如果您通过较小的增量步骤慢慢构建项目,那么将可以显著提高渲染性能,减少运行时占用空间,并提高更多的设备和平台的可移植性。

专业人员和独立开发人员都应该为使用 Vulkan 做好准备。本文通过详细介绍一个应用,展示如何使用 Vulkan API 渲染多个 .fbx 和 .obj 对象,以及在常用场景中读取和显示多个对象文件。还介绍了如何集成文件资源管理器窗口,以使用线性或并行处理加载和渲染文件,并在 UI 中对比各个文件的性能。代码还演示了一个简单的 UI 来移动、旋转和缩放对象;将对象封在边界框中;以线框模式渲染对象;显示对象信息和统计信息;并支持指定绝对坐标和旋转。

附录:如何构建项目

如前所述,如果使用英特尔 GPU 上的 Vulkan API 进行开发,最低要求是运行 64 位 Windows 7、8.1 或 10 的第六代智能英特尔® 处理器。Vulkan 驱动程序目前包含在最新英特尔核芯显卡驱动程序中。按照分步说明,在运行 Unity* Unreal* Engine 4 的基于英特尔的系统上安装 Vulkan 驱动程序。

以下步骤用于通过 Windows 命令提示符使用 Microsoft Visual Studio* 2017 构建此项目。

准备构建环境

1.将 Vulkan 3D Object Viewer 示例代码项目下载到硬盘驱动器上方便的文件夹中。

2.确保 Microsoft Visual Studio 2017 设置中有 Visual C++。如果没有,请从 Visual Studio 网站下载并安装。

3.示例应用依赖 Open Asset Import Library (assimp),但此库的预构建版本不适用于 Visual Studio 2017;必须从头开始重建。从 SourceForge*下载。

4.Cmake 是 assimp 的首选构建系统。您可以从 cmake*/ 下载最新版本,或使用 Visual Studio (YOUR_PATH_TO_MSVS\2017\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin) 中的一个版本。按照以下步骤构建 assimp:

a. 打开命令提示符 (cmd.exe)。

b. 设置 "PATH=PATH_TO_CMAKE\bin;%PATH%"(如果已在系统环境变量中永久设置此变量,可跳过此步骤。为此,请前往:Control Panel->System->Advanced System Settings->Environment Variables,并将该行添加至列表)。

c. 输入 "cmake -f CMakeLists.txt -G "Visual Studio 15 2017 Win64"。

d. 在 Visual Studio 中打开生成的 "assimp.sln" 解决方案文件,前往 Build->Configuration Manager 并在 Configuration 下方选择 “Release”(除非您出于某种原因需要调试 assimp,建议构建发行版本以获得最佳性能)。

e. 关闭配置管理器并构建 assimp。

5.从 Vulkan 下载并安装 Vulkan SDK。

6.下载并安装 Qt。示例应用使用 Qt 5.10 UI 库,这是 Vulkan 支持所需的最低版本。开源版本和商业版本都将在这里完成工作,但不管怎样都需要注册。如欲获取 Qt:

a. 请前往 qt.io 并选择一个版本。

b. 登录或注册并按照提示设置 Qt Online Installer。

c. 接下来,系统将提示您选择一个版本。选择 Qt 5.10 或更高版本并按照提示进行安装。

7.将示例应用库克隆或下载到您的硬盘驱动器。

构建应用

8.提供文件 "env_setup.bat" 以帮助您在本地为命令处理器设置环境变量。在执行之前:

a. 打开 "env_setup.bat" 并检查列出的变量是否指向已安装相关性的正确位置:

I. "_VC_VARS” — 前往 Visual Studio 环境设置 vcvarsall.bat 的路径

II."_QTDIR” — 前往 Qt 根的路径

III."_VULKAN_SDK” — Vulkan SDK 根

IV."_ASSIMP” — assimp 根

V. "_ASSIMP_BIN” — 前往二进制文件释放或调试配置的路径

VI."_ASSIMP_INC” — 前往 assimp 标头文件的路径

VII."_ASSIMP_LIB” — 指向 assimp 库释放或调试配置

b. 批处理文件的输出将报告您可能丢失的路径。

c. 另外,将以下添加至系统的(参数)环境变量:

I. 创建新变量:

1."_QTDIR” — 前往 Qt 根的路径

2."_VULKAN_SDK” — Vulkan SDK 根

3."_ASSIMP” — assimp 根

II.添加至可变 "PATH" 值:

1.%_QTDIR%\bin

2.%_VULKAN_SDK%\bin

3.%_ASSIMP%\bin

III.如果没有系统变量 "LIB” ,请创建该变量并添加下列值:%_ASSIMP%\lib

IV.如果没有系统变量 "INCLUDE",请创建该变量并添加下列值:

1.%_VULKAN_SDK%\Include

2.%_ASSIMP%\Include

d. 在命令提示窗口中,将当前目录设为项目根文件夹(包含下载的项目)。

e. 运行 qmake.exe。

f. 开始构建:

I. 关于版本:nmake -f Makefile.Release

II.关于调试:nmake -f Makefile.Debug

9.运行应用:

a. 关于版本:WORK_DIR\release\model-viewer-using-Vulkan.exe

b. 关于调试:WORK_DIR\debug\model-viewer-using-Vulkan.exe

10.执行新创建的 Vulkan object viewer 应用。

11.选择要使用的线程数或选中“单线程”。默认情况下,应用根据主机系统中的逻辑内核选择最佳线程数量。

12.单击 "Open models...” 加载包含选定线程数的某些模型。然后更改线程数并单击 "Reload" 以使用新线程设置加载相同的模型以进行比较。

有关编译器优化的更完整信息,请参阅优化通知