Step 1 搭建一个简单的渲染框架

Step 1 搭建一个简单的渲染框架

万事开头难。从萌生到自己到处看源码手抄一个mini engine出来的想法,到真正敲键盘去抄,转眼过去了很久的时间。这次大概的确是抱着认真的想法,打开VS从零开始抄代码。不知道能坚持多久呢。。。

本次的主题是搭一个简单的渲染框架。这里我们先假定要使用的底层图形API为DX12,当然代码设计上渲染和API层面肯定是要解耦的,如果有一天去抄OpenGL或是Vulcan的代码的时候,总不可能要重写已有的逻辑。另外,由于DX12的初始化逻辑中需要HWND类型的窗口句柄,所以这里也就需要引入原生的Windows API来绘制窗口了。当然同样的道理,窗口系统与绘制的API也没有什么耦合关系。

那么先从Main函数写起,它是最简单的,只做初始化和运行两件事情:

c++ 复制代码
int main()
{
	EngineLoop loop(hInstance);
	if (!loop.Initialize())
		return 0;

	return loop.Run();
}

EngineLoop类里目前包含我们这里需要的窗口系统和图形系统。初始化的逻辑是有顺序的,这里我们先初始化窗口系统,再初始化图形系统:

c++ 复制代码
bool EngineLoop::Initialize()
{
    m_pWindowSystem = MakeShared<WindowSystem>();
    if (!m_pWindowSystem->Initialize(windowSystemCfg))
    {
        return false;
    }

    m_pGraphicsSystem = MakeShared<GraphicsSystem>();
    if (!m_pGraphicsSystem->Initialize(graphicsSystemCfg))
    {
        return false;
    }
    return true;
}

我们用智能指针shared_ptr管理这些System。这样它们的生命周期就和EngineLoop保持一致,不用担心析构的时候忘记释放它们。

为了把创建绘制窗口的API与窗口本身的逻辑分开,窗口系统持有一个LowLevelWindow抽象类的指针。通过这个指针去真正地创建窗口,绘制窗口,以及响应窗口的事件等等:

c++ 复制代码
bool WindowSystem::Initialize(const WindowSystemCfg& cfg)
{
    m_pWindow = MakeShared<WindowsAPIWindow>();

    if (!m_pWindow->Initialize(windowCfg))
    {
        return false;
    }
    return true;
}

同样图形系统也是,我们使用RHI抽象类的指针来真正跟图形硬件打交道:

c++ 复制代码
bool GraphicsSystem::Initialize(const GraphicsSystemCfg& cfg)
{
    m_pRHI = MakeShared<D3D12RHI>();

    if (!m_pRHI->Initialize(rhiCfg))
    {
        return false;
    }
    return true;
}

对于DX12来说,那么就有一个D3D12RHI的子类啦。目前我们先不考虑渲染任何东西,只是把初始化的工作做掉,那需要哪些东西呢?

首先IDXGIFactory和ID3D12Device这两货肯定是需要的,如果没有它们,整个初始化逻辑就没法跑;IDXGISwapChain也是必要的,不然连back buffer都没有;然后我们需要使用绘制指令来进行各种底层操作,那就需要ID3D12CommandQueue,ID3D12CommandAllocator和ID3D12GraphicsCommandList这三剑客了。ID3D12CommandQueue是指令的执行者,它可以包含多个command list;command allocator是存储指令的数据结构,command list里记录的指令实际上是保存到这里。另外,由于GPU指令的执行对CPU来说是异步的,因此还需要一个ID3D12Fence用于同步。我们使用ComPtr来管理这些类,ComPtr对象当引用计数为0时,会自动调用Release接口,从而避免内存泄漏。

c++ 复制代码
class D3D12RHI : public RHI
{
private:

    ComPtr<ID3D12Device> m_pDevice;
    ComPtr<IDXGIFactory4> m_pDxGiFactory;
    ComPtr<ID3D12Fence> m_pFence;
    ComPtr<ID3D12CommandQueue> m_pCommandQueue;
    ComPtr<ID3D12CommandAllocator> m_pDirectCmdListAlloc;
    ComPtr<ID3D12GraphicsCommandList> m_pCommandList;
    ComPtr<IDXGISwapChain> m_pSwapChain;
};

DX12的资源和view是分开的,一个资源可以对应多种view,这里的view以D3D12_CPU_DESCRIPTOR_HANDLE来区分。一个资源每使用一个view,就需要往ID3D12DescriptorHeap申请一个空闲的handle。那么我们可以把这个过程抽象一下,封装一个D3D12DescriptorHeap类,它负责分配空闲的handle给申请者:

c++ 复制代码
class D3D12DescriptorHeap
{
public:
    D3D12DescriptorHeap(ID3D12Device* pDevice, D3D12_DESCRIPTOR_HEAP_TYPE type, UInt32 numDescriptors);
    void Initialize();
    D3D12_CPU_DESCRIPTOR_HANDLE Allocate();

private:
    ID3D12Device* m_pDevice = nullptr;
    D3D12_DESCRIPTOR_HEAP_TYPE m_type;
    UInt32 m_numDescriptors = 0;
    UInt32 m_descriptorSize = 0;
    UInt32 m_remainingFreeHandles;
    CD3DX12_CPU_DESCRIPTOR_HANDLE m_cpuHandle;
    ComPtr<ID3D12DescriptorHeap> m_pDescriptorHeap;
};

初始化过程中我们需要创建若干back buffer和一个depth buffer,这两个buffer的创建方式有区别,但它们本质上都属于资源,因此给它们各自一个类,然后共同继承D3D12Resource这个类,这个类包含一些对资源的通用操作。

c++ 复制代码
class D3D12Resource : public D3D12RHIChild
{
public:
    D3D12Resource(D3D12RHI* pRHI) : D3D12RHIChild(pRHI), m_state(D3D12_RESOURCE_STATE_COMMON) {}
    void SetState(D3D12_RESOURCE_STATES state);

protected:
    D3D12_RESOURCE_STATES m_state;
    ComPtr<ID3D12Resource> m_pResource;
};

class D3D12BackBuffer : public D3D12Resource
{
public:
    D3D12BackBuffer(D3D12RHI* pRHI) : D3D12Resource(pRHI), m_rtvHandle() {}
    void Initialize(Int32 index);
    void Clear(const Color& color);
    D3D12_CPU_DESCRIPTOR_HANDLE GetRtvHandle() const { return m_rtvHandle; }
private:
    D3D12_CPU_DESCRIPTOR_HANDLE m_rtvHandle;
};

class D3D12DepthBuffer : public D3D12Resource
{
public:
    D3D12DepthBuffer(D3D12RHI* pRHI) : D3D12Resource(pRHI), m_dsvHandle() {}
    void Initialize(Int32 clientWidth, Int32 clientHeight, DXGI_FORMAT format);
    void Clear(D3D12_CLEAR_FLAGS clearFlags, Float depth, UInt8 stencil);
    D3D12_CPU_DESCRIPTOR_HANDLE GetDsvHandle() const { return m_dsvHandle; }

private:
    D3D12_CPU_DESCRIPTOR_HANDLE m_dsvHandle;
};

初始化流程完毕之后,我们就要准备update了。现阶段我们啥也不做,就准备一下渲染环境吧。我们在RHI类中添加了PrepareRender和FinishRender两个抽象接口,分别表示准备渲染以及完成渲染提交显示的逻辑。对于DX12来说,在渲染前/后要准备哪些事情呢?

渲染前,首先是清空command,让command相关的数据结构保证可用;然后获取当前要渲染的back buffer,设置其状态为D3D12_RESOURCE_STATE_RENDER_TARGET;然后对back buffer和depth buffer执行clear操作,最后提交给硬件。在渲染结束之后,同样我们要把当前back buffer的状态切回渲染前的,如果是多缓冲要切到下一个可用的back buffer,执行掉中间产生的所有渲染指令,显示到屏幕上。

自此,一个最简单的渲染框架就搭好了,运行起来也就是一个填充满clear color的窗口,并没有什么稀奇,然而背后的代码量却有数百行了。

相关推荐
不吃斋的和尚5 小时前
Unity中一个节点实现植物动态(Shader)
unity·游戏引擎
虾球xz6 小时前
游戏引擎学习第117天
学习·游戏引擎
千年奇葩8 小时前
Unity shader glsl着色器特效之 模拟海面海浪效果
unity·游戏引擎·着色器
太妃糖耶10 小时前
Unity摄像机与灯光相关知识
unity·游戏引擎
程序趣谈1 天前
UE5中按钮圆角,设置边框
ue5·游戏引擎
龚子亦1 天前
Unity结合Vuforia虚拟按键实现AR机械仿真动画效果
unity·游戏引擎·ar·数字孪生·虚拟仿真
虾球xz1 天前
游戏引擎学习第115天
学习·游戏引擎
虾球xz1 天前
游戏引擎学习第116天
java·学习·游戏引擎
虾球xz2 天前
游戏引擎学习第114天
学习·游戏引擎
虾球xz2 天前
游戏引擎学习第109天
学习·游戏引擎