yangsenabc12 发表于 2023-3-24 13:29

现代图形引擎入门指南(九)— 渲染窗口

Github Pages
在计算机上,窗口是图形的载体,在之前GUI的章节中,我们简单了解了窗口的概念,下面将开始使用Qt封装好的窗口搭建基本的图形渲染结构:
新建一个CMakeLists.txt和main.cpp
在CMakeLists.txt中创建一个Target并链接Qt的模块:
cmake_minimum_required(VERSION 3.12)
project(FirstRhiWindowProj)
add_executable(FirstRhiWindow main.cpp)
find_package(Qt6 COMPONENTS Core Widgets Gui REQUIRED)   
target_link_libraries(FirstRhiWindow                        
    PRIVATE
      Qt6::Core
      Qt6::Widgets
      Qt6::Gui
      Qt6::GuiPrivate         //qrhi是GUI的私有模块      
)使用QRhi,需要包含:
#include <private/qrhi_p.h>
下面的代码可以创建一个以DX11进行渲染的QRhi,可以用于测试环境是否正常:
#include <private/qrhi_p.h>
#include <private/qrhid3d11_p.h>

int main(int argc, char** argv) {
    QRhiD3D11InitParams params;
    QRhi::Flags flags;
    QSharedPointer<QRhi> rhi(QRhi::create(QRhi::D3D11, &params, flags));
    qDebug() << rhi->driverInfo();      //打印显卡设备信息
    return 0;
}
如果想让Vulkan正常工作,需要下载 Vulkan SDKQRhiWindow

操作系统提供的窗口有非常细粒度的控制,在开发者想要调整窗口的某个效果时,可能会需要改变窗口的多个属性和事件,当大量的效果堆叠设置的时候,就很比较容易出现冲突。打个比方:
假如 A 效果需要开启属性a和b, B 效果需要开启属性b和c,如果窗口开启了效果 A 和 B (即开启属性a、b、c),之后如果想要关闭效果 B ,如果不稍加验证,直接关闭了与之关联的属性b和c,那么开启的属性只剩下a,效果 A 的显示就会出现问题。开发者往往只希望关注窗口的表现效果,而不是操作系统级别的窗口属性和事件,所以在窗口管理中,效果间的设置就存在了大量属性和事件的重叠
如果给每个属性的设置都加一遍关联验证,会使得代码变得很臃肿,且难以维护,也更容易出Bug
因此Qt采用了状态机的方式来跟管理原生窗口:

[*]在Qt层面对Window的各种设置并没有立即生效,而是将这些设置存储起来,等到特定时机,比如说show,Qt才会根据当前上下文的配置来创建实际的操作系统窗口
换而言之:

[*]对于Qt的窗口,只有在调用了show函数之后,才实际创建了Window,才能拿到它的窗口句柄
而我们如果要创建窗口的图形渲染结构,由于需要实际的窗口句柄,也必须在show之后进行初始化,不过好在Qt提供了相关的事件 :

[*]void QWindow:: exposeEvent (QExposeEvent* ev )

[*]当窗口从 un-exposed 切换到 exposed 状态时,会执行此事件
[*]在第一次显示窗口时,会在此事件之前执行resizeEvent

需要注意的是,对于一个窗口而言,exposeEvent会多次执行,但初始化只需要进行一次,因此需要增加一些逻辑验证。而图形渲染结构主要指的是:

[*]QScopedPointer<QRhiEx> mRhi
[*]QScopedPointer<QRhiSwapChain> mSwapChain:交换链
[*]QScopedPointer<QRhiRenderBuffer> mDSBuffer:深度模板缓冲区
[*]QScopedPointer<QRhiRenderPassDescriptor> mSwapChainPassDesc:交换链的RenderPass描述符
成功初始化之后,我们只需要在窗口上增加渲染和Resize的逻辑即可,这里有一个标准的Window实现示例:

[*]https://github.com/qt/qtbase/blob/dev/tests/manual/rhi/shared/examplefw.h
还需要注意的是:

[*]使用QRhi创建的资源,必须在QRhi销毁之间进行销毁,使用智能指针可以很好的管理这些资源的生命周期。
QRhiWidget

Widget与Window最大的不同是:

[*]Widget通过某些事件来触发界面的重绘,而Window则是每帧都在重绘
使用Widget能更好的节约性能,避免不必要的刷新,但在游戏和图形这种需要实时刷新的界面时,使用Window能更好地分摊绘制性能。
这里有一个标准的Widget实现:

[*]https://github.com/qt/qtbase/tree/dev/tests/manual/rhi/rhiwidget
封装

为了能够简化上述结构的使用,笔者在QEngineUtilities中做了一些简单的封装,你可以把这个仓库Pull下来:

[*]在CMakeLists.txt中使用add_subdirectory 添加 QEngineUtilities
[*]并且给 Target 链接 QEngineUtilities
如果成功,你可以使用如下代码来创建Widget和Window:
#include <QApplication>
#include "Render/RHI/QRhiWidget.h"
#include "Render/RHI/QRhiWindow.h"

class ExampleRhiWindow : public QRhiWindow {
public:
    ExampleRhiWindow(QRhiWindow::InitParams inInitParams) :QRhiWindow(inInitParams) {}
protected:
    virtual void onRenderTick() override {
      QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget();
      QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer();

      const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 1.0f, 1.0f);         //使用蓝色清屏
      const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 };

      cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue);
      cmdBuffer->endPass();
    }
};

class ExampleRhiWidget : public QRhiWidget {
public:
    ExampleRhiWidget() {
      setDebugLayer(true);      //开启验证层
    }
    void render(QRhiCommandBuffer* inCmdBuffer) override {
      const QColor clearColor = QColor::fromRgbF(1.0f, 0.0f, 0.0f, 1.0f);         //使用红色清屏
      const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 };
      inCmdBuffer->beginPass(mRenderTarget.data(), clearColor, dsClearValue);
      inCmdBuffer->endPass();
    }
};

int main(int argc, char **argv){
    QApplication app(argc, argv);

    QRhiWindow::InitParams initParams;
    ExampleRhiWindow window(initParams);
    window.setTitle("01-RhiWindow");
    window.resize({ 400,400 });
    window.show();

    ExampleRhiWidget widget;
    widget.setWindowTitle("01-RhiWidget");
    widget.setApi(QRhiWidget::Vulkan);
    widget.resize({ 400,400 });
    widget.show();

    return app.exec();
}
运行它可以看到:


如果遇到困难,可以参考 ModernGraphicsEngineGuide 中的 01-WindowAndWidget 项目
基础概念

上面的代码中出现了很多莫名其妙的结构,暂时不用去深入考虑它们的意义,这些名词只不过是给一些特定结构和流程一个能够称呼的简短昵称,当你了解它的应用场景和熟悉它的使用流程,自然会明白它们的意义。
这里会做一个简单的介绍,在后面的章节中会有更多细节。
交换链(SwapChain)

在前两节中,我们通过控制台程序阐述了交换链的作用,它能有效的避免窗口绘制的闪烁和撕裂。



https://mkblog.co.kr/vulkan-tutorial-10-create-swap-chain/

QWidget拥有一套自己的SwapChain机制,而在QRhiWindow中,需要我们通过 QRhiSwapChain 来获取当前的操作对象。
流水线(Pipeline)

也叫做管线,它们在GPU上运行,在计算机图形中,有两类管线:

[*]图形渲染管线(Graphics Pipeline):将一系列数据转换为图形的过程
[*]计算管线(Compute Pipeline):对一系列数据进行处理的过程
上一节中我们通过控制台程序以及简单的了解了一下图形渲染管线,而这里有一个更完整的流程图:



https://graphicscompendium.com/intro/01-graphics-pipeline

着色器(Shader)

着色器是一种运行在GPU上的微小程序,它们主要用于扩展管线的功能,比如上面的图形渲染管线中:

[*]输入的顶点数组被 顶点着色器(Vertex Shader) 进行处理
[*]光栅化之后,每个像素又被 片段着色器(Fragment Shader ) 处理
一条图形渲染管线要求至少有一个顶点着色器和片段着色器,但实际上,现代图形API还支持额外的着色器扩展:

[*]镶嵌控制着色器(Tessellation Control Shader)
[*]镶嵌评估着色器(Tessellation Evaluation Shader)
[*]几何着色器(Geometry Shader)
[*]计算着色器(Compute Shader)
它们使用其API特有的 Shader Language 进行编写,比如:

[*]GLSL:用于OpenGL、Vulkan
[*]HLSL:用于Direct3D
[*]MSL:用于Metal
QRhi使用Vulkan风格的GLSL,因此这个文档是非常重要的:

[*]https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html
缓冲区(Buffer)

这里的Buffer是指GPU上的一段内存(Memory)
在类型(Type)上,QRhi将之划分为:

[*]Immutable:用于存放希望永远不会发生改变的数据,具有非常高效的GPU读写性能,它通常放置在Devices Local 的 GPU 内存上,无法被CPU直接读写,但QRhi却支持它的上传,其原理是:每次上传数据新建一个 Host Local 的 Staging Buffer 作为中转来上传新数据,这样操作的代价是非常高昂的。
[*]Static:同样存储在 Devices Local 的 GPU 内存上,与 Immutable 不同的是,首次上传数据创建的 Staging Buffer 会一直保留。
[*]Dynamic:用于存放频繁发生变化的数据,它放置 Host Local 的GPU内存中,为了不拖延图形渲染管线,它通常会使用双缓冲机制。
关于 Host Local和 Devices Local 可以查阅:

[*]https://zhuanlan.zhihu.com/p/166387973
在用途(Usage)上,可以具备如下标识:

[*]VertexBuffer:用于存放顶点数据
[*]IndexBuffer:用于存放索引数据
[*]UniformBuffer:用于存储常量数据
[*]StorageBuffer:用于Compute管线中的数据计算
[*]IndirectDrawBuffer:用于间接渲染提供渲染参数
纹理(Texture)和采样器(Sampler)

纹理可以当作是一张存储在GPU上的图像,它往往作为图形渲染管线的参数,或者作为RenderTarget的附件
采样器用于:

[*]在UV值超出值域时,使用何种方式处理越界采样:



https://learnopengl.com/Getting-started/Textures


[*]在精度过高或过低时,使用何种方式对纹理进行采用:



https://learnopengl.com/Getting-started/Textures

分别对应QRhi中的 QRhiTexture 和 QRhiSampler
渲染目标(RenderTarget)

它等价于OpenGL中的 Frame Buffer Object,它由一个或多个颜色附件组成,可能包含深度附件和模板附件。


通常,我们的绘制其实就是在RenderTarget上填充颜色数据,对于深度和模板的数据,GPU会自动进行处理,我们只需要去配置一些处理规则。
它对应Qt中的 QRhiRenderTarget ,在上面的ExampleRhiWindow示例中,对SwapChain中的当前RenderTarget使用蓝色进行清屏。
virtual void onRenderTick() override {
    QRhiRenderTarget* renderTarget = mSwapChain->currentFrameRenderTarget();
    QRhiCommandBuffer* cmdBuffer = mSwapChain->currentFrameCommandBuffer();

    const QColor clearColor = QColor::fromRgbF(0.0f, 0.0f, 1.0f, 1.0f);
    const QRhiDepthStencilClearValue dsClearValue = { 1.0f,0 };

    cmdBuffer->beginPass(renderTarget, clearColor, dsClearValue);
    cmdBuffer->endPass();
}
在QRhi中,创建一个RenderTarget是一件非常容易的事情:
/*创建带有一个颜色附件的RT*/
QSharedPointer<QRhiTexture> colorAttachment;
colorAttachment.reset(rhi->newTexture(QRhiTexture::RGBA32F, QSize(800, 600), 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
colorAttachment->create();
QSharedPointer<QRhiTextureRenderTarget> renderTarget;
QSharedPointer<QRhiRenderPassDescriptor> renderPassDesc;
renderTarget.reset(rhi->newTextureRenderTarget({ colorAttachment.get() }));
renderPassDesc.reset(renderTarget->newCompatibleRenderPassDescriptor());
renderTarget->setRenderPassDescriptor(renderPassDesc.get());
renderTarget->create();
命令缓冲区(CommandBuffer)

CommandBuffer是现代图形API提出的概念,它用于存储GPU的操作指令,最后统一提交到GPU上进行处理
在QRhi中,不能直接创建CommandBuffer,得到它的途径有两种:

[*]通过 SwapChain 可以拿到当前的 CommandBuffer
[*]rhi->beginOffscreenFrame 会创建一个新的 CommandBuffer
QRhiCommandBuffer 支持如下指令:


渲染通道(RenderPass)

RenderPass可以当作是一次或多次Pipeline的执行过程
在游戏和影视作品中,为朴素的图形增加一些后期效果,能极大程度的提升画面的艺术感,而在图形渲染管线中,Pass可以当作是一帧图像(RenderTarget)的绘制。
在游戏引擎中,绘制朴素几何物体的Pass我们一般称为 BasePass
下面的 FrameGraph 展示了如何使用多个Pass来实现Bloom的效果


渲染通道描述符(RenderPassDescriptor )

RenderPassDescriptor用于规定何种格式的RenderTarget符合该RenderPass的使用,在Vulkan中,它的创建极其繁琐,而在QRhi,只需要使用:
class QRhiTextureRenderTarget : public QRhiRenderTarget
{
    virtual QRhiRenderPassDescriptor* newCompatibleRenderPassDescriptor() = 0;
};
尝试

如果你成功搭建了QRhi的渲染窗口,可以尝试复现上一节中QRhi中的示例工程,也可以运行一下 ModernGraphicsEngineGuide 中的一些Demo
如果你有其他图形API的基础,QRhi一定能让你爱不释手~
如果遇到问题,可以在此处提问

Ylisar 发表于 2023-3-24 13:33

[赞同]加油加油
页: [1]
查看完整版本: 现代图形引擎入门指南(九)— 渲染窗口