D3D12 实战 基础框架

发布时间 2023-04-21 09:10:56作者: 爱莉希雅

前言

​ 本系列将用D3D12实现众多实时渲染算法,包括正向渲染、延时渲染、光线追踪,本篇将介绍以后常用到的基本框架

​ 笔者会贴出重要的实现,且解释这些代码的作用,不过不会深入讲解其实现原理具体的还需读者自行学习

实现

Win32Application

​ Win32Application主要用于处理Win32窗口及消息,包含以下内容:

  1. 运行函数

    有如下功能

    1. 解析命令行参数
    2. 初始化窗口
    3. 初始化D3D12的所需资源
    4. 显示窗口
    5. 消息循环
    6. 销毁D3D12并将该消息告诉窗口
  2. 窗口过程函数

    有如下功能

    1. 处理发送到窗口的信息
    2. 调用默认窗口过程为应用程序未处理的任何窗口消息提供默认处理
  • 实现

    #pragma once
    
    class DXSample;
    
    class Win32Application
    {
    public:
    	/*
    		not main loop, it has some feature as follows :
    	*	1. Parse the command line parameters
    	*	2. Initialize the window class to define property of the window
    	*	3. Calculates the required size of the window rectangle
    	*	4. Rectangle of last step will be passed to the CreateWindow(), which will create a window
    	*	5. Init the OnInit() in D3D12Tutorials that init the pipleline and assets of D3D12
    	*	6. show the window
    	*	7. main loop.It respond to the D3D12 and key value 
    	*	8. destory D3D12
    	*	9. Return this part of the WM_QUIT message to Windows
    	*/
    	static int Run(DXSample* pSample, HINSTANCE hInstance, int nCmdShow);	
    
    	//get the handle to the window
    	static HWND GetHwnd() { return m_hwnd; }
    
    protected:
    	/*
    		Processes the information sent to the window
    		Calls the default window procedure to provide default processing for any window messages that an application does not process
    	*/
    	static LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
    
    private:
    	static HWND m_hwnd;	//handle to the window
    };
    
    HWND Win32Application::m_hwnd = nullptr;
    
    int Win32Application::Run(DXSample* pSample, HINSTANCE hInstance, int nCmdShow)
    {
        // Parse the command line parameters
        int argc;
        LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
        pSample->ParseCommandLineArgs(argv, argc);
        LocalFree(argv);
    
        // Initialize the window class.
        WNDCLASSEX windowClass = { 0 };
        windowClass.cbSize = sizeof(WNDCLASSEX);
        windowClass.style = CS_HREDRAW | CS_VREDRAW;    //CS_HREDRAW | CS_VREDRAW represent a redrawn window when width or height changes
        windowClass.lpfnWndProc = WindowProc;   //A pointer to the window procedure function associated with this WNDCLASSEX
        windowClass.hInstance = hInstance;      //A handle to the current app
        //Handle to the cursor resource
        //If NULL, the app must explicitly set the cursor shape each time it moves the mouse over the application's window
        windowClass.hCursor = LoadCursor(NULL, IDC_ARROW);  
        windowClass.lpszClassName = L"DXSampleClass";   //name of window menu
        RegisterClassEx(&windowClass);
    
        //Calculates the required size of the window rectangle
        RECT windowRect = { 0, 0, static_cast<LONG>(pSample->GetWidth()), static_cast<LONG>(pSample->GetHeight()) };
        AdjustWindowRect(&windowRect, WS_OVERLAPPEDWINDOW, FALSE);
    
        // Create the window and use m_hwnd store this handle
        m_hwnd = CreateWindow(
            windowClass.lpszClassName,  // name of window menu
            pSample->GetTitle(),        // name of window
            // the style of window
            // this is an overlapped window.It means that windows with the same properties can be merged together
            // for example, for win11 if we clik on some folder they will overlap not separate
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT,              // init coords of x.for WS_OVERLAPPEDWINDOW,x is the initial x-coor of the window's upper-left corner
            CW_USEDEFAULT,              // In the same way as x
            windowRect.right - windowRect.left, // width of window
            windowRect.bottom - windowRect.top, // height of window
            nullptr,        // Handle to Parent Window.We have no parent window
            nullptr,        // Handle to window menu.We aren't using menus.
            hInstance,      // The associated app handle
            pSample);       // Point to user-defined data. here is D3D12
    
        // Initialize the sample. OnInit is defined in each child-implementation of DXSample.
        pSample->OnInit();
    
        ShowWindow(m_hwnd, nCmdShow);
    
        // Main sample loop.
        MSG msg = {};
        while (msg.message != WM_QUIT)
        {
            // Process any messages in the queue.
            if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
            {
                TranslateMessage(&msg); // Conversion of keyboard keys
                DispatchMessage(&msg);  // Dispatch the message to the corresponding window procedure
            }
        }
    
        // destory of D3D12 
        pSample->OnDestroy();
    
        // Return this part of the WM_QUIT message to Windows.
        return static_cast<char>(msg.wParam);
    }
    
    // Main message handler for the sample.
    LRESULT CALLBACK Win32Application::WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
        DXSample* pSample = reinterpret_cast<DXSample*>(GetWindowLongPtr(hWnd, GWLP_USERDATA));
    
        switch (message)
        {
            // send this message when call the CreateWindow()
            case WM_CREATE:
            {
                // Save the DXSample* passed in to CreateWindow
                LPCREATESTRUCT pCreateStruct = reinterpret_cast<LPCREATESTRUCT>(lParam);
                //Changes an attribute of the specified window
                SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(pCreateStruct->lpCreateParams));
            }
            return 0;
    
            // when a nonsystem key is pressed
            case WM_KEYDOWN:
                if (pSample)
                {
                    pSample->OnKeyDown(static_cast<UINT8>(wParam));
                }
                return 0;
    
            // when a nonsystem key is released 
            case WM_KEYUP:
                if (pSample)
                {
                    pSample->OnKeyUp(static_cast<UINT8>(wParam));
                }
                return 0;
    
            // when system or another app makes a request to paint
            case WM_PAINT:
                if (pSample)
                {
                    pSample->OnUpdate();
                    pSample->OnRender();
                }
                return 0;
    
            //  when a window is being destroyed.Although window is closed, thread maybe still run in background
            case WM_DESTROY:
                //Terminate thread
                PostQuitMessage(0);
                return 0;
        }
    
        // Calls the default window procedure to provide default processing for any window messages that an application does not process
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    

DXSample

​ DXSample是一个抽象class "DXSample",以后D3D12的不同实现均派生自此class。它包含以下功能:

  1. 用给定的宽高和窗口名初始化窗口
  2. D3D12渲染流程所需函数:初始化、更新、渲染、销毁
  3. 自定义窗口过程处理的messages
  4. 获取窗口指定的宽高、名字
  5. 解析命令行参数
  6. 获取文件路径
  7. 帮助函数:获取第一个支持 Direct3D12的可用硬件适配器
  8. 帮助函数:设置窗口的标题文本
  • 实现

    #pragma once
    
    class DXSample
    {
    public:
        DXSample(UINT width, UINT height, std::wstring name);
        virtual ~DXSample();
    
        virtual void OnInit() = 0;
        virtual void OnUpdate() = 0;
        virtual void OnRender() = 0;
        virtual void OnDestroy() = 0;
    
        // Samples override the event handlers to handle specific messages.
        virtual void OnKeyDown(UINT8 /*key*/)   {}
        virtual void OnKeyUp(UINT8 /*key*/)     {}
    
        // Accessors.
        UINT GetWidth() const           { return m_width; }
        UINT GetHeight() const          { return m_height; }
        const WCHAR* GetTitle() const   { return m_title.c_str(); }
    
        void ParseCommandLineArgs(_In_reads_(argc) WCHAR* argv[], int argc);
    
    protected:
        std::wstring GetAssetFullPath(LPCWSTR assetName);
    
        void GetHardwareAdapter(
            _In_ IDXGIFactory1* pFactory,
            _Outptr_result_maybenull_ IDXGIAdapter1** ppAdapter,
            bool requestHighPerformanceAdapter = false);
    
        void SetCustomWindowText(LPCWSTR text);
    
        // Viewport dimensions.
        UINT m_width;
        UINT m_height;
        float m_aspectRatio;
    
        // Adapter info.
        bool m_useWarpDevice;
    
    private:
        // Root assets path.
        std::wstring m_assetsPath;
    
        // Window title.
        std::wstring m_title;
    };
    
    using namespace Microsoft::WRL;
    
    DXSample::DXSample(UINT width, UINT height, std::wstring name) :
        m_width(width),
        m_height(height),
        m_title(name),
        m_useWarpDevice(false)
    {
        WCHAR assetsPath[512];
        GetAssetsPath(assetsPath, _countof(assetsPath));
        m_assetsPath = assetsPath;
    
        m_aspectRatio = static_cast<float>(width) / static_cast<float>(height);
    }
    
    DXSample::~DXSample()
    {
    }
    
    // Helper function for resolving the full path of assets.
    std::wstring DXSample::GetAssetFullPath(LPCWSTR assetName)
    {
        return m_assetsPath + assetName;
    }
    
    // Helper function for acquiring the first available hardware adapter that supports Direct3D 12.
    // If no such adapter can be found, *ppAdapter will be set to nullptr.
    _Use_decl_annotations_
    void DXSample::GetHardwareAdapter(
        IDXGIFactory1* pFactory,
        IDXGIAdapter1** ppAdapter,
        bool requestHighPerformanceAdapter) //The preference of GPU for the app to run on
    {
        *ppAdapter = nullptr;
    
        ComPtr<IDXGIAdapter1> adapter;
    
        ComPtr<IDXGIFactory6> factory6; 
        if (SUCCEEDED(pFactory->QueryInterface(IID_PPV_ARGS(&factory6))))   //Queries a COM object for a pointer to one of its interface
        {
            for (
                UINT adapterIndex = 0;
                SUCCEEDED(factory6->EnumAdapterByGpuPreference( //Enumerates graphics adapters based on a given GPU preference
                    adapterIndex,
                    // DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE : Preference for the highest performing GPU
                    // DXGI_GPU_PREFERENCE_UNSPECIFIED : No preference of GPU
                    requestHighPerformanceAdapter == true ? DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE : DXGI_GPU_PREFERENCE_UNSPECIFIED,
                    IID_PPV_ARGS(&adapter)));
                ++adapterIndex)
            {
                DXGI_ADAPTER_DESC1 desc;
                adapter->GetDesc1(&desc);
    
                if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
                {
                    // Don't select the Basic Render Driver adapter.
                    // If you want a software adapter, pass in "/warp" on the command line.
                    continue;
                }
    
                // Check to see whether the adapter supports Direct3D 12, but don't create the
                // actual device yet.
                if (SUCCEEDED(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, _uuidof(ID3D12Device), nullptr)))
                {
                    break;
                }
            }
        }
    
        // no such adapter can be found
        if(adapter.Get() == nullptr)
        {
            for (UINT adapterIndex = 0; SUCCEEDED(pFactory->EnumAdapters1(adapterIndex, &adapter)); ++adapterIndex)
            {
                DXGI_ADAPTER_DESC1 desc;
                adapter->GetDesc1(&desc);
    
                if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
                {
                    // Don't select the Basic Render Driver adapter.
                    // If you want a software adapter, pass in "/warp" on the command line.
                    continue;
                }
    
                // Check to see whether the adapter supports Direct3D 12, but don't create the
                // actual device yet.
                if (SUCCEEDED(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, _uuidof(ID3D12Device), nullptr)))
                {
                    break;
                }
            }
        }
        
        *ppAdapter = adapter.Detach();
    }
    
    // Helper function for setting the window's title text.
    void DXSample::SetCustomWindowText(LPCWSTR text)
    {
        std::wstring windowText = m_title + L": " + text;
        SetWindowText(Win32Application::GetHwnd(), windowText.c_str());
    }
    
    // Helper function for parsing any supplied command line args.
    _Use_decl_annotations_
    void DXSample::ParseCommandLineArgs(WCHAR* argv[], int argc)
    {
        for (int i = 1; i < argc; ++i)
        {
            if (_wcsnicmp(argv[i], L"-warp", wcslen(argv[i])) == 0 || 
                _wcsnicmp(argv[i], L"/warp", wcslen(argv[i])) == 0)
            {
                m_useWarpDevice = true;
                m_title = m_title + L" (WARP)";
            }
        }
    }
    

FPS Camera

​ 既然是游戏开发,我个人倾向FPS相机——调整相机朝向,WSAD键在xz平面上进行移动,而非环绕相机——相机有一个目标点,可以对该目标点环绕、调整到目标点的距离、平移目标点

​ 我们将其封装到一个独立的FpsCamera class,既然是相机,那他肯定要包含位置、x轴、y轴、z轴(都是以世界空间表示的观察空间坐标系中的点)以及视锥体,可以看出FPS相机其实就是一个可移动的视锥体

​ FPS相机的实现如下:

using namespace DirectX;

class FpsCamera
{
public:
    FpsCamera();

    void Init(XMFLOAT3 position);	//初始化相机
    void Update(float elapsedSeconds);	//更新相机的位置、航向角、俯仰角、look方向
    XMMATRIX GetViewMatrix();	//求得观察矩阵
    XMMATRIX GetProjectionMatrix(float fov, float aspectRatio, float nearPlane = 1.0f, float farPlane = 1000.0f);	//求得透视投影矩阵
    void SetMoveSpeed(float unitsPerSecond);	//设置移动速率
    void SetTurnSpeed(float radiansPerSecond);	//设置转动速率

    void OnKeyDown(WPARAM key);	//按下键盘
    void OnKeyUp(WPARAM key);	//松开键盘

private:
    void Reset();	//重置相机信息

    struct KeysPressed
    {
        //控制相机移动
        bool w;
        bool a;
        bool s;
        bool d;

        //控制相机旋转
        bool left;
        bool right;
        bool up;
        bool down;
    };

    XMFLOAT3 m_initialPosition;
    XMFLOAT3 m_position;
    //这两个参数用于表示球面坐标系
    float m_yaw;                  // 航向角.与z轴正方向有关.向量在 xz 平面上的投影和 z 轴的逆时针夹角
    float m_pitch;                // 俯仰角.与xz平面相关.向上/向下看的俯仰值
    XMFLOAT3 m_lookDirection;
    XMFLOAT3 m_upDirection;
    float m_moveSpeed;            // 摄像机移动的速度,单位是每秒
    float m_turnSpeed;            // 相机转动的速度,单位是弧度每秒

    KeysPressed m_keysPressed;
};

//-------------------------------------------------------------------------------------------------------------------------------------------------------------

//初始化相机属性
FpsCamera::FpsCamera():
    m_initialPosition(0, 0, 0),
    m_position(m_initialPosition),
    m_yaw(XM_PI),
    m_pitch(0.0f),
    m_lookDirection(0, 0, -1),
    m_upDirection(0, 1, 0),
    m_moveSpeed(20.0f),
    m_turnSpeed(XM_PIDIV2),
    m_keysPressed{}
{
}

void FpsCamera::Init(XMFLOAT3 position)
{
    m_initialPosition = position;
    Reset();
}

void FpsCamera::SetMoveSpeed(float unitsPerSecond)
{
    m_moveSpeed = unitsPerSecond;	//每秒移动距离
}

void FpsCamera::SetTurnSpeed(float radiansPerSecond)
{
    m_turnSpeed = radiansPerSecond;	//每秒变化的弧度值
}

//按下"Esc"键时会进行重置
void FpsCamera::Reset()
{
    m_position = m_initialPosition;
    m_yaw = XM_PI;
    m_pitch = 0.0f;
    m_lookDirection = { 0, 0, -1 };
}

void FpsCamera::Update(float elapsedSeconds)	// elapsedSeconds:已用秒数
{
    // 在相机空间中计算在xz轴上对应的变化占比
    XMFLOAT3 move(0, 0, 0);

    // 若当按下wsad键时,计算移动的向量
    if (m_keysPressed.a)
        move.x -= 1.0f;
    if (m_keysPressed.d)
        move.x += 1.0f;
    if (m_keysPressed.w)	//注意:这里是向负z方向
        move.z -= 1.0f;
    if (m_keysPressed.s)
        move.z += 1.0f;

    // 归一化xz向量
    if (fabs(move.x) > 0.1f && fabs(move.z) > 0.1f)
    {
        XMVECTOR vector = XMVector3Normalize(XMLoadFloat3(&move));
        move.x = XMVectorGetX(vector);
        move.z = XMVectorGetZ(vector);
    }

    //计算移动距离和旋转弧度
    float moveInterval = m_moveSpeed * elapsedSeconds;
    float rotateInterval = m_turnSpeed * elapsedSeconds;

    if (m_keysPressed.left)
        m_yaw += rotateInterval;	// 逆时针旋转
    if (m_keysPressed.right)
        m_yaw -= rotateInterval;
    if (m_keysPressed.up)
        m_pitch += rotateInterval;
    if (m_keysPressed.down)
        m_pitch -= rotateInterval;

    // 限制俯仰角,防止它超出范围
    m_pitch = min(m_pitch, XM_PIDIV4);
    m_pitch = max(-XM_PIDIV4, m_pitch);

    // Move the camera in model space
    // 相当于绕y轴旋转,但贡献值的坐标系内的z轴为原坐标系的负方向
    float x = move.x * -cosf(m_yaw) - move.z * sinf(m_yaw);
    float z = move.x * sinf(m_yaw) - move.z * cosf(m_yaw);
    m_position.x += x * moveInterval;
    m_position.z += z * moveInterval;

    // Determine the look direction.
    float r = cosf(m_pitch);
    m_lookDirection.x = r * sinf(m_yaw);
    m_lookDirection.y = sinf(m_pitch);
    m_lookDirection.z = r * cosf(m_yaw);
}

XMMATRIX FpsCamera::GetViewMatrix()
{
    return XMMatrixLookToRH(XMLoadFloat3(&m_position), XMLoadFloat3(&m_lookDirection), XMLoadFloat3(&m_upDirection));
}

XMMATRIX FpsCamera::GetProjectionMatrix(float fov, float aspectRatio, float nearPlane, float farPlane)
{
    return XMMatrixPerspectiveFovRH(fov, aspectRatio, nearPlane, farPlane);
}

void FpsCamera::OnKeyDown(WPARAM key)
{
    switch (key)
    {
    case 'W':
        m_keysPressed.w = true;
        break;
    case 'A':
        m_keysPressed.a = true;
        break;
    case 'S':
        m_keysPressed.s = true;
        break;
    case 'D':
        m_keysPressed.d = true;
        break;
    case VK_LEFT:
        m_keysPressed.left = true;
        break;
    case VK_RIGHT:
        m_keysPressed.right = true;
        break;
    case VK_UP:
        m_keysPressed.up = true;
        break;
    case VK_DOWN:
        m_keysPressed.down = true;
        break;
    case VK_ESCAPE: //esc键
        Reset();
        break;
    }
}

void FpsCamera::OnKeyUp(WPARAM key)
{
    switch (key)
    {
    case 'W':
        m_keysPressed.w = false;
        break;
    case 'A':
        m_keysPressed.a = false;
        break;
    case 'S':
        m_keysPressed.s = false;
        break;
    case 'D':
        m_keysPressed.d = false;
        break;
    case VK_LEFT:
        m_keysPressed.left = false;
        break;
    case VK_RIGHT:
        m_keysPressed.right = false;
        break;
    case VK_UP:
        m_keysPressed.up = false;
        break;
    case VK_DOWN:
        m_keysPressed.down = false;
        break;
    }
}

Step Timer

​ 相机定义了位置方向和视锥体,但我们需要一个计时器用于度量相机移动的距离,因此这此处我们借用头文件<windows.h>中的性能计数器(performance timer),这是一个高精度的计时器

​ 它的实现如下:

// Helper class for animation and simulation timing.
class StepTimer
{
public:
    StepTimer() :
        m_elapsedTicks(0),
        m_totalTicks(0),
        m_leftOverTicks(0),
        m_frameCount(0),
        m_framesPerSecond(0),
        m_framesThisSecond(0),
        m_qpcSecondCounter(0),
        m_isFixedTimeStep(false),
        m_targetElapsedTicks(TicksPerSecond / 60)
    {
        QueryPerformanceFrequency(&m_qpcFrequency);
        QueryPerformanceCounter(&m_qpcLastTime);

        // 将最大增量初始化为1/10秒
        m_qpcMaxDelta = m_qpcFrequency.QuadPart / 10;
    }

    // 求取上一帧到当前帧的时间差
    // Tick:时间的最小单位
    UINT64 GetElapsedTicks() const                        { return m_elapsedTicks; }
    double GetElapsedSeconds() const                    { return TicksToSeconds(m_elapsedTicks); }	//转换为以秒为单位

    // 求取程序开始到现在的时间差
    UINT64 GetTotalTicks() const                        { return m_totalTicks; }
    double GetTotalSeconds() const                        { return TicksToSeconds(m_totalTicks); }

    // 求取程序开始到现在调用的Update()的次数(帧数)
    UINT32 GetFrameCount() const                        { return m_frameCount; }

    // 求取目前的帧率
    UINT32 GetFramesPerSecond() const                    { return m_framesPerSecond; }

    // 设置使用固定时间步长模式还是可变时间步长模式
    void SetFixedTimeStep(bool isFixedTimestep)            { m_isFixedTimeStep = isFixedTimestep; }

    // 当处于固定时间步长模式时设置调用Update()的频率
    void SetTargetElapsedTicks(UINT64 targetElapsed)    { m_targetElapsedTicks = targetElapsed; }
    void SetTargetElapsedSeconds(double targetElapsed)    { m_targetElapsedTicks = SecondsToTicks(targetElapsed); }

    // 表示每秒10,000,000ticks
    static const UINT64 TicksPerSecond = 10000000;

    // 单位转换:ticks和秒
    static double TicksToSeconds(UINT64 ticks)            { return static_cast<double>(ticks) / TicksPerSecond; }
    static UINT64 SecondsToTicks(double seconds)        { return static_cast<UINT64>(seconds * TicksPerSecond); }

    // 当发生时间性的不连续(比如IO的阻塞操作),就调用该函数来避免固定时间步长试图调用一系列的Update()
    void ResetElapsedTime()
    {
        QueryPerformanceCounter(&m_qpcLastTime);

        m_leftOverTicks = 0;
        m_framesPerSecond = 0;
        m_framesThisSecond = 0;
        m_qpcSecondCounter = 0;
    }

    typedef void(*LPUPDATEFUNC) (void);

    // 更新计时器状态,以适当次数调用Update()
    void Tick(LPUPDATEFUNC update = nullptr)
    {
        LARGE_INTEGER currentTime;

        // 获取当前时刻值.单位count
        QueryPerformanceCounter(&currentTime);

        // 当前时刻和前一时刻的差值
        UINT64 timeDelta = currentTime.QuadPart - m_qpcLastTime.QuadPart;

        // 更新
        m_qpcLastTime = currentTime;
        m_qpcSecondCounter += timeDelta;

        // 限制时间增量(e.g. after paused in the debugger)
        if (timeDelta > m_qpcMaxDelta)
        {
            timeDelta = m_qpcMaxDelta;
        }

        // 将QPC的单位转换为以tick为单位
        timeDelta *= TicksPerSecond;
        timeDelta /= m_qpcFrequency.QuadPart;

        UINT32 lastFrameCount = m_frameCount;

        if (m_isFixedTimeStep)
        {
            /* 
            	若app运行小于目标差值(1/4ms)内,只需钳制时钟使得完全匹配目标值
            	随着时间的积累,该方法可以防止微小的且不恰当的错误
            	若没有钳制,一个要求60fps的游戏在刷新率为59.94的显示器上运行,会使得积累足够多的微小的错误以至于丢失一个帧
            	为了顺利运行,最好将偏差四舍五入到零
            */
            
			// 若小于目标差值
            if (abs(static_cast<int>(timeDelta - m_targetElapsedTicks)) < TicksPerSecond / 4000)
            {
                timeDelta = m_targetElapsedTicks;
            }

            m_leftOverTicks += timeDelta;

            //钳制
            while (m_leftOverTicks >= m_targetElapsedTicks)
            {
                m_elapsedTicks = m_targetElapsedTicks;
                m_totalTicks += m_targetElapsedTicks;
                m_leftOverTicks -= m_targetElapsedTicks;
                m_frameCount++;

                if (update)
                {
                    update();
                }
            }
        }
        else
        {
            // Variable timestep update logic.
            m_elapsedTicks = timeDelta;
            m_totalTicks += timeDelta;
            m_leftOverTicks = 0;
            m_frameCount++;

            if (update)
            {
                update();
            }
        }

        // 跟踪当前帧率
        if (m_frameCount != lastFrameCount)
        {
            m_framesThisSecond++;
        }

        if (m_qpcSecondCounter >= static_cast<UINT64>(m_qpcFrequency.QuadPart))
        {
            m_framesPerSecond = m_framesThisSecond;
            m_framesThisSecond = 0;
            m_qpcSecondCounter %= m_qpcFrequency.QuadPart;
        }
    }

private:
    // Source timing data uses QPC units.
    // QPC以count(计数)为单位
    LARGE_INTEGER m_qpcFrequency;	//QPC频率
    LARGE_INTEGER m_qpcLastTime;	//QPC的上一时刻值
    UINT64 m_qpcMaxDelta;

    // Derived timing data uses a canonical tick format.
    UINT64 m_elapsedTicks;	// 上一帧到目前帧的时间差
    UINT64 m_totalTicks;	// 从启动程序开始到现在的时间差
    UINT64 m_leftOverTicks;

    // Members for tracking the framerate.
    UINT32 m_frameCount;	// 程序开始到现在的帧数
    UINT32 m_framesPerSecond;	// 帧率
    UINT32 m_framesThisSecond;	//用以跟踪当前帧率
    UINT64 m_qpcSecondCounter;

    // Members for configuring fixed timestep mode.
    bool m_isFixedTimeStep;		// 是否使用固定时间步长模式
    UINT64 m_targetElapsedTicks;	// 在固定时间步长模式下 调用Update()函数的频率
};

Ring Buffer

环形缓冲区(ring buffer)是管理上传堆的一种方法,它会保存接下来几帧所需的数据,app维护一个当前数据的输入指针和一个帧偏移队列来记录每个帧和该帧资源数据的起始偏移量
insufficient free memory in this ring buffer

来看一个例子。假设当前需要为frame 6分配一个常量缓冲区,但从图中可见空间不足.通过fence查询,发现frame3已经渲染完毕,帧偏移队列进行更新,但目前空间依旧不足以为frame 6进行分配。在当前这种情况下,CPU会进行自我阻塞(等待),直到frame 4也渲染完毕,释放frame 4的空间,帧偏移队列进行更新,现在空间的大小足以为frame 6进行分配,app将frame 6的常量缓冲区数据复制到帧3和帧4使用的内存中并更新current ptr
still insufficient memory after frame 3 has rendered
now there is room from frame 6 in the ring buffer

可以看出,ring buffer的空间需要足够大以应付最坏情况

​ ring buffer的实现如下:

using namespace DirectX;
using Microsoft::WRL::ComPtr;

// 和物体有关的常量
struct ObjectConstantBuffer
{
    XMFLOAT4X4 world = MathHelper::Identity4x4();
};

// 每次渲染都将渲染过程常量更新一次
struct PassConstantBuffer
{
    XMFLOAT4X4 view = MathHelper::Identity4x4();
    XMFLOAT4X4 inverseView = MathHelper::Identity4x4();
    XMFLOAT4X4 projection = MathHelper::Identity4x4();
    XMFLOAT4X4 inverseProjection = MathHelper::Identity4x4();
    XMFLOAT4X4 viewProjection = MathHelper::Identity4x4();
    XMFLOAT4X4 inverseViewProjection = MathHelper::Identity4x4();
};

// 实例物体所用的顶点属性
struct InstanceVertex
{
    XMFLOAT3 pos;
    XMFLOAT4 color;
};

struct FrameResource
{
    // 因为在GPU处理完此命令分配器相关的命令前不能对其进行Reset()
    // 所以每一帧都需有它们自己的命令分配器
    ComPtr<ID3D12CommandAllocator> commandAllocator;
    std::unique_ptr<UploadBuffer<ObjectConstantBuffer>> objectUploadCB;
    std::unique_ptr<UploadBuffer<PassConstantBuffer>> passUploadCB;
    // 检测GPU是否还在使用该帧资源
    UINT64 fenceValue;

    FrameResource(ID3D12Device* pDevice, uint32_t passCount, uint32_t objectCount);
    ~FrameResource();

    void PopulateCommandList(ID3D12GraphicsCommandList* pCommandList, std::vector<Renderer*>& renderers);

    void XM_CALLCONV UpdateObjectConstantBuffers(std::vector<std::unique_ptr<Renderer>>& allRenderers);
    void XM_CALLCONV UpdatePassConstantBuffers(XMMATRIX& view, XMMATRIX& projection);
};
FrameResource::FrameResource(ID3D12Device* pDevice, uint32_t passCount, uint32_t objectCount) :
    fenceValue(0)
{

    // The command allocator is used by the main sample class when 
    // resetting the command list in the main update loop. Each frame 
    // resource needs a command allocator because command allocators 
    // cannot be reused until the GPU is done executing the commands 
    // associated with it.
    ThrowIfFailed(pDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)));
    
    objectUploadCB = std::make_unique<UploadBuffer<ObjectConstantBuffer>> (pDevice, objectCount, true);
    passUploadCB = std::make_unique<UploadBuffer<PassConstantBuffer>>(pDevice, passCount, true);
}

FrameResource::~FrameResource()
{

}

void FrameResource::PopulateCommandList(
    ID3D12GraphicsCommandList* pCommandList,
    std::vector<Renderer*>& renderers)
{
    auto currPassUploadCB = this->passUploadCB->Resource();
    pCommandList->SetGraphicsRootConstantBufferView(1, currPassUploadCB->GetGPUVirtualAddress());

    auto objectCBSize = CalcConstantBufferByteSize(sizeof(ObjectConstantBuffer));

    auto currObjectUploadCB = this->objectUploadCB->Resource();

    for (size_t i = 0; i < renderers.size(); ++i)
    {
        auto currRenderer = renderers[i];

        // if use descriptor table
        //ID3D12DescriptorHeap* ppHeaps[] = { pCbvSrvDescriptorHeap };
        //pCommandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);

        pCommandList->IASetPrimitiveTopology(currRenderer->PrimitiveType);
        pCommandList->IASetVertexBuffers(0, 1, &currRenderer->Geo->VertexBufferView());
        pCommandList->IASetIndexBuffer(&currRenderer->Geo->IndexBufferView()); 

        D3D12_GPU_VIRTUAL_ADDRESS objectUploadAddress = currObjectUploadCB->GetGPUVirtualAddress();
        objectUploadAddress += currRenderer->objectIndex * objectCBSize;

        pCommandList->SetGraphicsRootConstantBufferView(0, objectUploadAddress);

        pCommandList->DrawIndexedInstanced(currRenderer->indexCount, 1, currRenderer->startIndex, currRenderer->baseVertex, 0);
    }
}

void XM_CALLCONV FrameResource::UpdateObjectConstantBuffers(std::vector<std::unique_ptr<Renderer>>& allRenderers)
{
    auto currObjectCB = this->objectUploadCB.get();
    for (auto& e : allRenderers)
    {   
        // Only update the cbuffer data if the constants have changed.  
        // This needs to be tracked per frame resource.
        if (e->numFramesDirty > 0)
        {
            XMMATRIX world = XMLoadFloat4x4(&e->world);
            
            ObjectConstantBuffer objConstants;
            XMStoreFloat4x4(&objConstants.world, XMMatrixTranspose(world));

            currObjectCB->CopyData(e->objectIndex, objConstants);

            e->numFramesDirty--;
        }
    }
}

void XM_CALLCONV FrameResource::UpdatePassConstantBuffers(XMMATRIX& view, XMMATRIX& projection)
{
    XMMATRIX viewProjection = XMMatrixMultiply(view, projection);
    XMMATRIX inverseView = XMMatrixInverse(&XMMatrixDeterminant(view), view);
    XMMATRIX inverseProjection = XMMatrixInverse(&XMMatrixDeterminant(projection), projection);
    XMMATRIX inverseViewProjection = XMMatrixInverse(&XMMatrixDeterminant(viewProjection), viewProjection);

    PassConstantBuffer passConstantBuffer;
    XMStoreFloat4x4(&passConstantBuffer.view, XMMatrixTranspose(view));
    XMStoreFloat4x4(&passConstantBuffer.projection, XMMatrixTranspose(projection));
    XMStoreFloat4x4(&passConstantBuffer.viewProjection, XMMatrixTranspose(viewProjection));
    XMStoreFloat4x4(&passConstantBuffer.inverseView, XMMatrixTranspose(inverseView));
    XMStoreFloat4x4(&passConstantBuffer.inverseProjection, XMMatrixTranspose(inverseProjection));
    XMStoreFloat4x4(&passConstantBuffer.inverseViewProjection, XMMatrixTranspose(inverseViewProjection));
    
    auto currentPassCB = this->passUploadCB.get();
    currentPassCB->CopyData(0, passConstantBuffer);
}

Renderer

​ Renderer存储绘制物体所需的数据,在主文件中主要从该struct中获取必要的渲染数据

struct Renderer
{
	XMFLOAT4X4 world = MathHelper::Identity4x4();
    
	/*	
		dirty flags.
		Represents a change in the data associated with an object.
		Because each frame resource has an object constant buffer, so we should set like this "numFramesDirty = FrameCount"
	*/
	uint32_t numFramesDirty;

    // 指向的GPU常量缓冲区对应于当前渲染项中的object constant upload buffer
	uint32_t objectIndex = 0;
	
    //需要绘制的几何体
	Mesh* Geo = nullptr;

	D3D12_PRIMITIVE_TOPOLOGY PrimitiveType = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST;

    // DrawIndexedInstanced()的参数
	uint32_t indexCount = 0;
	uint32_t startIndex = 0;
	uint32_t baseVertex = 0;
};

// 根据不同的PSO所需的渲染项,将他们划分为三个vector
std::vector<std::unique_ptr<Renderer>> m_allRenderers;
std::vector<Renderer*> m_opaqueRenderers;
std::vector<Renderer*> m_transparentRenderers;

Model

Mesh

我们创建一个Mesh struct来存储几何体相关的一些数据,如:顶点缓冲区大小、常量缓冲区索引、PSO、SRV描述符表等等

struct Geometrie
{
	std::string name;

	ComPtr<ID3DBlob> vertexBufferCPU;
	ComPtr<ID3DBlob> indexBufferCPU;

	ComPtr<ID3D12Resource> vertexBufferGPU;
	ComPtr<ID3D12Resource> indexBufferGPU;

	ComPtr<ID3D12Resource> vertexUploadBuffer;
	ComPtr<ID3D12Resource> indexUploadBuffer;

	float bounds[4];		// A bounding sphere
	uint32_t vbOffset;		// BufferLocation - Buffer.GpuVirtualAddress
	uint32_t vbSize;		// SizeInBytes
	uint32_t vbStride;		// StrideInBytes
	uint32_t ibOffset;		// BufferLocation - Buffer.GpuVirtualAddress
	uint32_t ibSize;		// SizeInBytes
	DXGI_FORMAT ibFormat;	// DXGI_FORMAT

	struct Draw
	{
		uint32_t indexCount;		// Number of indices = 3 * number of triangles
		uint32_t startIndex;		// Offset to first index in index buffer
		uint32_t baseVertex;		// Offset to first vertex in vertex buffer
	};

	D3D12_VERTEX_BUFFER_VIEW VertexBufferView() const;

	D3D12_INDEX_BUFFER_VIEW IndexBufferView() const;

	void Destory()
	{
		vertexUploadBuffer = nullptr;
		indexUploadBuffer = nullptr;
	}
};

inline D3D12_VERTEX_BUFFER_VIEW Geometrie::VertexBufferView() const
{
	D3D12_VERTEX_BUFFER_VIEW VBView;
	VBView.BufferLocation = vertexBufferGPU->GetGPUVirtualAddress();
	VBView.SizeInBytes = vbSize;
	VBView.StrideInBytes = vbStride;

	return VBView;
}

inline D3D12_INDEX_BUFFER_VIEW Geometrie::IndexBufferView() const
{
	D3D12_INDEX_BUFFER_VIEW IBView;
	IBView.BufferLocation = indexBufferGPU->GetGPUVirtualAddress();
	IBView.Format = ibFormat;
	IBView.SizeInBytes = ibSize;

	return IBView;
}

ProceduralGeometry

程序性几何体用于生成几何模型,这里偷个懒先引用龙书的Land模型的源代码

class ProceduralGeometry
{
	struct Vertex
	{
		Vertex() {}
		Vertex(
			const XMFLOAT3& p,
			const XMFLOAT3& n,
			const XMFLOAT3& t,
			const XMFLOAT2& uv) :
			position(p),
			normal(n),
			tangentU(t),
			texC(uv)
		{}

		Vertex(
			float px, float py, float pz,
			float nx, float ny, float nz,
			float tx, float ty, float tz,
			float u, float v) :
			position(px, py, pz),
			normal(nx, ny, nz),
			tangentU(tx, ty, tz),
			texC(u, v)
		{}

		XMFLOAT3 position;
		XMFLOAT3 normal;
		XMFLOAT3 tangentU;
		XMFLOAT2 texC;
	};

	class MeshData
	{
	public:
		std::vector<Vertex> vertices;
		std::vector<uint32_t> indices32;

		std::vector<uint16_t>& GetIndices16()
		{
			if (m_indices16.empty())
			{
				m_indices16.resize(indices32.size());
				for (size_t i = 0; i < indices32.size(); ++i)
					m_indices16[i] = static_cast<uint16_t>(indices32[i]);
			}

			return m_indices16;
		}

	private:
		std::vector<uint16_t> m_indices16;
	};

public:
	MeshData CreateGrid(float width, float depth, uint32_t m, uint32_t n);

	void CreateLand(ComPtr<ID3D12Device> device, ComPtr<ID3D12GraphicsCommandList> cmdList, std::unordered_map<std::string, std::unique_ptr<Mesh>>& geometries, std::unordered_map<std::string, std::unique_ptr<Mesh::Draw>>& draws);
};
ProceduralGeometry::MeshData ProceduralGeometry::CreateGrid(float width, float depth, uint32_t m, uint32_t n)
{
	MeshData meshData;

	uint32_t vertexCount = m * n;
	uint32_t faceCount = (m - 1) * (n - 1) * 2;

	//
	// Create the vertices.
	//

	float halfWidth = 0.5f * width;
	float halfDepth = 0.5f * depth;

	float dx = width / (n - 1);
	float dz = depth / (m - 1);

	float du = 1.0f / (n - 1);
	float dv = 1.0f / (m - 1);

	meshData.vertices.resize(vertexCount);
	for (uint32_t i = 0; i < m; ++i)
	{
		float z = halfDepth - i * dz;
		for (uint32_t j = 0; j < n; ++j)
		{
			float x = -halfWidth + j * dx;

			meshData.vertices[i * n + j].position = XMFLOAT3(x, 0.0f, z);
			meshData.vertices[i * n + j].normal = XMFLOAT3(0.0f, 1.0f, 0.0f);
			meshData.vertices[i * n + j].tangentU = XMFLOAT3(1.0f, 0.0f, 0.0f);

			// Stretch texture over grid.
			meshData.vertices[i * n + j].texC.x = j * du;
			meshData.vertices[i * n + j].texC.y = i * dv;
		}
	}

	//
	// Create the indices.
	//

	meshData.indices32.resize(faceCount * 3); // 3 indices per face

	// Iterate over each quad and compute indices.
	uint32_t k = 0;
	for (uint32_t i = 0; i < m - 1; ++i)
	{
		for (uint32_t j = 0; j < n - 1; ++j)
		{
			meshData.indices32[k] = i * n + j;
			meshData.indices32[k + 1] = i * n + j + 1;
			meshData.indices32[k + 2] = (i + 1) * n + j;

			meshData.indices32[k + 3] = (i + 1) * n + j;
			meshData.indices32[k + 4] = i * n + j + 1;
			meshData.indices32[k + 5] = (i + 1) * n + j + 1;

			k += 6; // next quad
		}
	}

	return meshData;
}

void ProceduralGeometry::CreateLand(
	ComPtr<ID3D12Device> device,
	ComPtr<ID3D12GraphicsCommandList> cmdList,
	std::unordered_map<std::string, std::unique_ptr<Mesh>>& geometries,
	std::unordered_map<std::string, std::unique_ptr<Mesh::Draw>>& draws)
{
	MeshData grid = CreateGrid(160.f, 160.f, 50, 50);

	std::vector<InstanceVertex> vertices(grid.vertices.size());
	std::vector<std::uint16_t> indices = grid.GetIndices16();

	for (UINT i = 0; i < grid.vertices.size(); ++i)
	{
		auto& p = grid.vertices[i].position;

		auto GetHillsHeight = [&]() -> float
		{
			return 0.3f * (p.z * sinf(0.1f * p.x) + p.x * cosf(0.1f * p.z));
		};

		vertices[i].pos = p;
		vertices[i].pos.y = GetHillsHeight();

		if (vertices[i].pos.y < -10.f)
		{
			vertices[i].color = XMFLOAT4(1.f, 0.9f, 0.62f, 1.f);
		}
		else if (vertices[i].pos.y < 5.f)
		{
			vertices[i].color = XMFLOAT4(0.48f, 0.77f, 0.46f, 1.f);
		}
		else if (vertices[i].pos.y < 12.f)
		{
			vertices[i].color = XMFLOAT4(0.1f, 0.48f, 0.19f, 1.f);
		}
		else if (vertices[i].pos.y < 20.f)
		{
			vertices[i].color = XMFLOAT4(0.45f, 0.39f, 0.34f, 1.0f);
		}
		else
		{
			vertices[i].color = XMFLOAT4(1.f, 1.f, 1.f, 1.f);
		}
	}

	auto pGeo = std::make_unique<Mesh>();

	pGeo->name = "Land";
	pGeo->vbSize = (uint32_t)vertices.size() * sizeof(InstanceVertex);
	pGeo->vbStride = sizeof(InstanceVertex);
	pGeo->ibSize = (uint32_t)indices.size() * sizeof(uint16_t);
	pGeo->ibFormat = DXGI_FORMAT_R16_UINT;

	ThrowIfFailed(D3DCreateBlob(pGeo->vbSize, &pGeo->vertexBufferCPU));
	CopyMemory(pGeo->vertexBufferCPU->GetBufferPointer(), vertices.data(), pGeo->vbSize);

	ThrowIfFailed(D3DCreateBlob(pGeo->ibSize, &pGeo->indexBufferCPU));
	CopyMemory(pGeo->indexBufferCPU->GetBufferPointer(), indices.data(), pGeo->ibSize);

	pGeo->vertexBufferGPU = CreateDefaultBuffer(device.Get(), cmdList.Get(), vertices.data(), pGeo->vbSize, pGeo->vertexUploadBuffer);
	pGeo->indexBufferGPU = CreateDefaultBuffer(device.Get(), cmdList.Get(), indices.data(), pGeo->ibSize, pGeo->indexUploadBuffer);

	auto landDraw = std::make_unique<Mesh::Draw>();
	landDraw->baseVertex = 0;
	landDraw->indexCount = (uint32_t)indices.size();
	landDraw->startIndex = 0;

	geometries["Land"] = std::move(pGeo);
	draws["Land"] = std::move(landDraw);
}

输出效果图

image-20230416155136433

image-20230416154901904

下一篇

下一篇将展示如何使用assimp导入模型

reference

microsoft/DirectX-Graphics-Samples: This repo contains the DirectX Graphics samples that demonstrate how to build graphics intensive applications on Windows. (github.com)

[Timing (Direct3D 12 Graphics) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/perfctrs/about-performance-counters)