继续我们的DX12初始化。
我们前面提到了没处理WM_SIZE消息,导致我们改变窗口大小的时候会有问题,现在我们来处理这个部分。窗口大小改变了,我们需要更新高度和宽度,同时堆积在队列里面的命令我们需要Flush掉,然后Reset我们的各种变量,包括命令分配器,命令列表,交换缓冲,深度目标缓冲等。然后因为我们的大小改了,导致我们各种视图的尺寸也要一起修改。不过DX12做了足够多的优化,我们可以直接复用前面申请的内存。
void D3D12App::OnResize()
{
assert(device);
assert(swapChain);
assert(alloctor);
FlushCommandQueue();
cmdList->Reset(alloctor.Get(), nullptr);
for (int i = 0; i < swapChainBufferCount; i++)
{
swapChainBuffer[i].Reset();
}
depthStencilBuffer.Reset();
swapChain->ResizeBuffers(swapChainBufferCount, clientWidth, clientHeight, backBufferFormat, DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH);
currentBackBufferIndex = 0;
createRTV();
createDSVBuffer();
createDSV();
createViewPortAndScissorRect();
}
和initD3D其实有很多重复的部分,而这个部分多执行一些其实没什么大问题,所以我们修改一下原始的代码:
bool D3D12App::initD3D12()
{
#if defined(DEBUG) | defined(_DEBUG)
ComPtr debugController;
ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
debugController->EnableDebugLayer();
#endif
createDevice();
createFence();
collectDescriptorSize();
msaaSet();
createCMDQueueListAlloctor();
createSwapChain();
createHeap();
OnResize();
return true;
}
稍微精简了一些,然后我们要处理最大化最小化。最小化的时候我们需要支持暂停游戏逻辑,而玩家在移动窗口的过程中,我们希望能够暂停Resize过程,防止不必要的卡顿。
case WM_SIZE:
clientWidth = LOWORD(lParam);
clientHeight = HIWORD(lParam);
if (device)
{
if (wParam == SIZE_MINIMIZED)
{
isMinimized = true;
isMaximized = false;
isAppPause = true;
}
else if (wParam == SIZE_MAXIMIZED)
{
isMinimized = false;
isMaximized = true;
isAppPause = false;
OnResize();
}
else if (wParam == SIZE_RESTORED)
{
if (isMinimized)
{
isMinimized = false;
isMaximized = false;
OnResize();
}
else if (isMaximized)
{
isMinimized = false;
isMaximized = false;
OnResize();
}
else if (isResizing)
{
//do nothing
}
else
{
OnResize();
}
}
}
break;
接着,我们想要添加一个高精度的计时器,这是非常基础的一个模块,我们这里想用它来显示帧时间。
#include "GameTimer.h"
#include
GameTimer::GameTimer():
baseTime(0), pausedTime(0), currTime(0), isStop(false),
deltaTime(-1.0)
{
__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
secondsPerCount = 1.0 / countsPerSec;
}
void GameTimer::Start()
{
__int64 startTime;
QueryPerformanceCounter((LARGE_INTEGER*)&startTime);
if (isStop)
{
pausedTime += (startTime - stopTime);
isStop = false;
stopTime = 0;
prevTime = startTime;
}
}
void GameTimer::Stop()
{
if (isStop == false)
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
stopTime = currTime;
isStop = true;
}
}
float GameTimer::TotalTime()
{
if (isStop)
{
return (float)((stopTime - pausedTime - baseTime) * secondsPerCount);
}
return (float)((currTime - pausedTime - baseTime) * secondsPerCount);
}
float GameTimer::getDeltaTime()
{
return (float)deltaTime;
}
void GameTimer::Reset()
{
__int64 time;
QueryPerformanceCounter((LARGE_INTEGER*)&time);
baseTime = time;
prevTime = time;
stopTime = 0;
isStop = false;
}
void GameTimer::Tick()
{
if (isStop)
{
deltaTime = 0.0;
return;
}
__int64 time;
QueryPerformanceCounter((LARGE_INTEGER*)&time);
currTime = time;
deltaTime = (currTime - prevTime) * secondsPerCount;
prevTime = currTime;
if (deltaTime < 0.0)
{
deltaTime = 0.0;
}
}
然后我们可以显示一下帧率:
void D3D12App::ShowFrameStat()
{
static int frameCount = 0;
static float timeElapsed = 0.0f;
frameCount++;
if (timer.TotalTime() - timeElapsed > 1.0f)
{
float fps = (float)frameCount;
float mspf = 1000.0f / fps;
std::wstring fpsStr = std::to_wstring(fps);
std::wstring mspfStr = std::to_wstring(mspf);
std::wstring windowText = Title + L" fps: " + fpsStr +
L" mspf: " + mspfStr;
SetWindowText(mhMainWnd, windowText.c_str());
frameCount = 0;
timeElapsed += 1.0f;
}
}
然后我们添加一些常用的鼠标事件。
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
OnMouseDown(wParam, GET_X_LPARAM(lParam), GET_X_LPARAM(lParam));
return;
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
OnMouseUp(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
return 0;
case WM_MOUSEMOVE:
OnMouseMove(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
return 0;
最后,我们还要做一个简单的重构,那就是Draw函数其实是需要根据实际情况变化的,所以考虑到后面会有不同的内容,我们派生出一个类,去做具体的行为,对于初始化,我们就叫做InitClass吧。然后我们考虑到以后还会有游戏逻辑,所以就顺手把Update函数也加上。
最后再来一个析构函数:
D3D12App::~D3D12App()
{
if (device != nullptr)
{
FlushCommandQueue();
}
}
到这里,我们的整个初始化流程就写完了。
这里附加一下深度相关的知识:
DX里面,深度图存的值是反向的,也就是说1表示离摄像机最近,0表示离摄像机最远。因为深度图里面存的其实是一个1/z的正比值,也就是k/z,所以如果直接采样出来,你会发现在视觉上,它并不是线性的,你需要反变换回到摄像机空间,才能获得一个线性的视觉效果。
使用1/z来存储的主要原因是,顶点位置经过MVP变换后,像素的属性需要通过顶点位置进行插值得到,但因为透视导致近大远小的关系,如果直接对z进行线性插值,得出来的坐标其实是错的,需要用1/z来进行插值,这样得到的坐标才是对的。