@(读书笔记)[读书笔记, 技术交流]
当我们一帧帧渲染动画时,我们更新物体时需要知道距离上一帧的时间间隔。
以下说明计算两帧的时间间隔的方式。设 ti t i 为第 i i 帧的Windows性能计数器的值,则 ti−1 t i − 1 就是第 i−1 i − 1 帧的性能计数器的值。显然两帧的间隔 Δt=ti−ti−1 Δ t = t i − t i − 1 。为了使得实时渲染看起来更平滑流畅,一秒钟渲染的帧数至少需30帧,因此 Δt Δ t 的值必须较小。
以下给出获取 Δt Δ t 的代码
void GameTimer::Tick()
{
if (mStopped)
{
mDeltaTime = 0.0;
return;
}
// Get the time this frame.
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mCurrTime = currTime;
// Time difference between this frame and the previous.
mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;
// Prepare for next frame.
mPrevTime = mCurrTime;
// Force nonnegative. The DXSDK's CDXUTTimer mentions that if the
// processor goes into a power save mode or we get shuffled to another
// processor, then mDeltaTime can be negative.
if (mDeltaTime < 0.0)
{
mDeltaTime = 0.0;
}
}
float GameTimer::DeltaTime()const
{
return (float)mDeltaTime;
}
在以下循环中调用上述函数Tick
int D3DApp::Run()
{
MSG msg = { 0 };
mTimer.Reset();
while (msg.message != WM_QUIT)
{
// If there are Window messages then process them.
if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// Otherwise, do animation/game stuff.
else
{
mTimer.Tick();
if (!mAppPaused)
{
CalculateFrameStats();
UpdateScene(mTimer.DeltaTime());
DrawScene();
}
else
{
Sleep(100);
}
}
}
return (int)msg.wParam;
}
上述代码中,每帧都会重新计算 Δt Δ t ,UpdateScene函数根据 Δt Δ t 更新动画。
让我们再看看初始化 Δt Δ t 的操作:
void GameTimer::Reset()
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mBaseTime = currTime;
mPrevTime = currTime;
mStopTime = 0;
mStopped = false;
}
上述代码中的一些变量将在下节讨论(详见 $4.3.4)。不过我们可以看到当Reset函数被调用时,变量mPrevTime被设置为当前计数器的值,这样做是因为动画开始的第一帧之前是没有帧的,因此必须在初始化时调用Reset函数。
上节是根据程序暂停时间来计算两帧之间的时间间隔,另一个行之有效的方式是利用程序已运行的时间(我们称之为总时间)。在以下情形中使用非常有效:假设一个玩家的任务是300秒升一级,如何判断玩家任务失败呢?我们在任务开始前记录时间 tstart t s t a r t ;然后任务开始了;我们不停地检查是否已经过去了300秒,若当前时间 t t 满足 t−tstart>300s t − t s t a r t > 300 s ,玩家未升级则任务超时失败。当然还要考虑游戏暂停的情况。
Demo使用的代码依赖头文件 d3dUtil.h, d3dApp.h, 和d3dApp.cpp,这几个文件是较为常用的,因此本书后续不会再复述。
D3DApp class定义如下:
class D3DApp
{
public:
D3DApp(HINSTANCE hInstance);
virtual ~D3DApp();
HINSTANCE AppInst()const;
HWND MainWnd()const;
float AspectRatio()const;
int Run();
// Framework methods. Derived client class overrides these methods to
// implement specific application requirements.
virtual bool Init();
virtual void OnResize();
virtual void UpdateScene(float dt) = 0;
virtual void DrawScene() = 0;
virtual LRESULT MsgProc(HWND hwnd, UINT msg,
WPARAM wParam, LPARAM lParam);
// Convenience overrides for handling mouse input.
virtual void OnMouseDown(WPARAM btnState, int x, int y) { }
virtual void OnMouseUp(WPARAM btnState, int x, int y) { }
virtual void OnMouseMove(WPARAM btnState, int x, int y) { }
protected:
bool InitMainWindow();
bool InitDirect3D();
void CalculateFrameStats();
protected:
HINSTANCE mhAppInst; // application instance handle
HWND mhMainWnd; // main window handle
bool mAppPaused; // is the application paused?
bool mMinimized; // is the application minimized?
bool mMaximized; // is the application maximized?
bool mResizing; // are the resize bars being dragged?
UINT m4xMsaaQuality; // quality level of 4X MSAA
// Used to keep track of the "delta-time" and game time (§4.3).
GameTimer mTimer;
// The D3D11 device (§4.2.1), the swap chain for page flipping
// (§4.2.4), the 2D texture for the depth/stencil buffer (§4.2.6),
// the render target (§4.2.5) and depth/stencil views (§4.2.6), and
// the viewport (§4.2.8).
ID3D11Device* md3dDevice;
ID3D11DeviceContext* md3dImmediateContext;
IDXGISwapChain* mSwapChain;
ID3D11Texture2D* mDepthStencilBuffer;
ID3D11RenderTargetView* mRenderTargetView;
ID3D11DepthStencilView* mDepthStencilView;
D3D11_VIEWPORT mScreenViewport;
// The following variables are initialized in the D3DApp constructor
// to default values. However, you can override the values in the
// derived class constructor to pick different defaults.
// Window title/caption. D3DApp defaults to "D3D11 Application".
std::wstring mMainWndCaption;
// Hardware device or reference device? D3DApp defaults to
// D3D_DRIVER_TYPE_HARDWARE.
D3D_DRIVER_TYPE md3dDriverType;
// Initial size of the window's client area. D3DApp defaults to
// 800x600. Note, however, that these values change at runtime
// to reflect the current client area size as the window is resized.
int mClientWidth;
int mClientHeight;
// True to use 4X MSAA (§4.1.8). The default is false.
bool mEnable4xMsaa;
};
后续几节我们会直接使用上述代码的函数和变量来讲述。
- D3DApp: 构造器,用于成员变量初始化。
- ~D3DApp: 析构。
- AppInst: 返回class的副本。
- MainWnd: 返回主窗口的副本。
- AspectRatio: 返回back buffer的宽高比(即 mClientWidthmClientHeight m C l i e n t W i d t h m C l i e n t H e i g h t )
- Run: 主循环函数。
- InitMainWindow: 初始化主窗口。详见$4.3.3。
- InitDirect3D: 初始化Driect3D, 在上一篇Directx11入门之D3D程序初始化已展示了初始化的代码。详见 $4.2。
- CalculateFrameStats: 统计帧率等信息,将在$4.4.4中展示。
本书中的程序都会重载5个虚函数,以下说明这5个虚函数。
Ⅰ. Init:初始化程序,分配资源,初始化物体,设置光照。D3DApp类的初始化函数将会调用InitMainWindow函数和InitDirect3D函数,因此你需要在你的类初始化函数开始时先完成D3DApp类的初始化。示例:
bool TestApp::Init()
{
if (!D3DApp::Init())
return false;
/* Rest of initialization code goes here */
}
这样才能保证后续你的初始化代码中ID3D11Device可以正常使用。
Ⅱ. OnResize:当收到消息WM_SIZE, D3DApp::MsgPro处理此消息时会调用本函数。当窗口大小发生改变时,本函数需要调整Direct3D中的一些属性。特别是back buffer和depth/stencil buffers需要重新适配窗口大小。back buffer的修改可以调用IDXGISwapChain::ResizeBuffers函数完成。而depth/stencil buffers需要重新创建。
Ⅲ. UpdateScene:每帧调用这个抽象方法,用来更新3D程序。(如执行动画,移动摄像头,碰撞处理,检查用户输入等)
Ⅳ. DrawScene:这个抽象方法每帧会被委托并把当前帧渲染到back buffer上。当我们完成这一帧后,就调用
IDXGISwapChain::Present函数把back buffer展示到屏幕上。
Ⅴ. MsgProc:消息处理函数。
除了上述5个 框架函数外,我们还提供了3个虚函数以方便处理鼠标事件:
virtual void OnMouseDown(WPARAM btnState, int x, int y){ }
virtual void OnMouseUp(WPARAM btnState, int x, int y) { }
virtual void OnMouseMove(WPARAM btnState, int x, int y){ }
若你要想处理鼠标事件,你可以重载MsgProc函数。上述3个鼠标事件函数中第一个参数WPARAM 表示鼠标案件状态,第二个参数表示鼠标坐标(x,y)。
帧统计常用于测量游戏和图形学程序中每秒钟渲染次数(FPS: the number of frames being rendered per second)。
我们简单地计算一下在 t t 秒内的FPS。(设其结果是 n n )。那么 t t 秒内平均FPS的值即 fpsavg=nt f p s a v g = n t . 若使 t=1 t = 1 , 那么 fpsavg=n1=n f p s a v g = n 1 = n ,因此当设 t=1 t = 1 时,可以避免除法。因此在代码中通常使用 t=1 t = 1 。以下给出用来计算FPS的函数D3DApp::CalculateFrameStats的代码:
void D3DApp::CalculateFrameStats()
{
// Code computes the average frames per second, and also the
// average time it takes to render one frame. These stats
// are appeneded to the window caption bar.
static int frameCnt = 0;
static float timeElapsed = 0.0f;
frameCnt++;
// Compute averages over one second period.
if ((mTimer.TotalTime() - timeElapsed) >= 1.0f)
{
float fps = (float)frameCnt; // fps = frameCnt / 1
float mspf = 1000.0f / fps;
std::wostringstream outs;
outs.precision(6);
outs << mMainWndCaption << L" "
<< L"FPS: " << fps << L" "
<< L"Frame Time: " << mspf << L" (ms)";
SetWindowText(mhMainWnd, outs.str().c_str());
// Reset for next average.
frameCnt = 0;
timeElapsed += 1.0f;
}
}
这份代码也可以得出每帧的时间,我们习惯用毫秒来表示:
float mspf = 1000.0f / fps;
注意FPS是一秒钟内的平均帧率,所以并不能体现真实的每帧实际情况,也许某一帧耗时过久导致拉高了FPS。
我们尽可能少地在框架函数中实现窗口程序,通常也不会过多处理Win32的消息。但仍有些较为重要的消息需要处理。
第一个需要处理的消息是WM_ACTIVATE, 当程序激活或失效时会发出此消息,处理如下:
case WM_ACTIVATE:
if (LOWORD(wParam) == WA_INACTIVE)
{
mAppPaused = true;
mTimer.Stop();
}
else
{
mAppPaused = false;
mTimer.Start();
}
return 0;
如你所见,当程序失效时,我们把变量mAppPaused设置为false,且暂停计时。联合函数D3DApp::Run($4.3.3)一起看,此时不会更新程序。
下一个我们希望处理的是WM_SIZE,已在D3DApp:OnResize中提过,不再赘述。用户可能会出现持续拖动窗口的行为,此时我们会收到许多的WM_SIZE,显然我们不希望过多调用D3DApp::OnResize ,因此我们可以利用WM_EXITSIZEMOVE消息来达到目的。这个消息是在用户不再拖动窗口时发出的,相对应用户开始拖动窗口会发出WM_ENTERSIZEMOVE。
// WM_ENTERSIZEMOVE is sent when the user grabs the resize bars.
case WM_ENTERSIZEMOVE:
mAppPaused = true;
mResizing = true;
mTimer.Stop();
return 0;
// WM_EXITSIZEMOVE is sent when the user releases the resize bars.
// Here we reset everything based on the new window dimensions.
case WM_EXITSIZEMOVE:
mAppPaused = false;
mResizing = false;
mTimer.Start();
OnResize();
return 0;
以下几个消息就直接上代码,不过多描述了:
// WM_DESTROY is sent when the window is being destroyed.
case WM_DESTROY:
PostQuitMessage(0);
return 0;
// The WM_MENUCHAR message is sent when a menu is active and the user presses
// a key that does not correspond to any mnemonic or accelerator key.
case WM_MENUCHAR:
// Don't beep when we alt-enter.
return MAKELRESULT(0, MNC_CLOSE);
// Catch this message to prevent the window from becoming too small.
case WM_GETMINMAXINFO:
((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;
((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200;
return 0;
Finally, to support our mouse input virtual functions, we handle the following mouse messages as follows :
case WM_LBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
OnMouseDown(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
return 0;
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;
注意:我们需要使用宏GET_X_LPARAM和GET_Y_LPARAM ,需包含头文件 #include
当我们创建IDXGISwapChain接口时会自动绑定快捷键ALT+ENTER,
现在我们来讨论一下程序的框架,本书后续出现的代码都将使用以下框架模板。
#include "d3dApp.h"
class InitDirect3DApp : public D3DApp
{
public:
InitDirect3DApp(HINSTANCE hInstance);
~InitDirect3DApp();
bool Init();
void OnResize();
void UpdateScene(float dt);
void DrawScene();
};
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
PSTR cmdLine, int showCmd)
{
// Enable run-time memory check for debug builds.
#if defined(DEBUG) | defined(_DEBUG)
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
InitDirect3DApp theApp(hInstance);
if (!theApp.Init())
return 0;
return theApp.Run();
}
InitDirect3DApp::InitDirect3DApp(HINSTANCE hInstance)
: D3DApp(hInstance)
{
}
InitDirect3DApp::~InitDirect3DApp()
{
}
bool InitDirect3DApp::Init()
{
if (!D3DApp::Init())
return false;
return true;
}
void InitDirect3DApp::OnResize()
{
D3DApp::OnResize();
}
void InitDirect3DApp::UpdateScene(float dt)
{
}
void InitDirect3DApp::DrawScene()
{
assert(md3dImmediateContext);
assert(mSwapChain);
// Clear the back buffer blue. Colors::Blue is defined in d3dUtil.h.
md3dImmediateContext->ClearRenderTargetView(mRenderTargetView,
reinterpret_cast<const float*>(&Colors::Blue));
// Clear the depth buffer to 1.0f and the stencil buffer to 0.
md3dImmediateContext->ClearDepthStencilView(mDepthStencilView,
D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
// Present the back buffer to the screen.
HR(mSwapChain->Present(0, 0));
}
调试时我们会用一个宏来检查函数的返回值HRESULT:
#if defined(DEBUG) | defined(_DEBUG)
#ifndef HR
#define HR(x) \
{ \
HRESULT hr = (x); \
if(FAILED(hr)) \
{ \
DXTrace(__FILE__, (DWORD)__LINE__, hr, L#x, true); \
} \
}
#endif
#else
#ifndef HR
#define HR(x) (x)
#endif
#endif
- Direct3D 可以认为是程序员与图形硬件的媒介。比如说程序员调用Direct3D函数把资源视图绑定到图形硬件的渲染管线上,配置渲染管线的输出,绘制3D图形。
- 若想要Direct3D 11能够提供服务,需要硬件设备支持所有Direct3D 11的功能。
- COM(Component Object Model)组件是为了计算机工业的软件生产更加符合人类的行为方式开发的一种新的软件开发技术,独立于语言和平台,并且向下兼容。Direct3D 程序员不需要关注COM组件的细节实现,只需要关心获得和释放COM组件的接口。
- 一维纹理(1D texture)就像一维数组,二维纹理(2D texture)就像二维数组。同理三维纹理(3D texture)就像三维数组。而数组中的元素的类型必须是DXGI_FORMAT的其中一个枚举型。纹理通常用来包含图像数据,但也可以用来包含其他任意数据,如depth buffer的深度信息。GPU也可以对纹理做一些特定的操作,如过滤和多重采样。
- 在Direct3D中,资源是不会直接绑定到渲染管线上的,而是利用资源视图代替绑定。一个资源可能会创建出多个视图,从而一个资源可以被绑定在渲染管线的不同阶段。若创建资源时未指定资源类型,则绑定资源视图时必须指定类型。
- ID3D11Device和ID3D11DeviceContext接口可以认为是物理引擎的控制器,利用这些接口我们可以与硬件交互控制硬件。ID3D11Device接口提供检查硬件支持的功能,分配资源;ID3D11DeviceContext接口提供设置渲染阶段,把资源绑定到渲染管线,发起渲染指令。
- 为了避免动画出现闪烁,最好在back buffer上预先绘制完整的一帧,再展现在屏幕上。一旦back buffer绘制完成,就与front buffer交换,即back buffer变成front buffer。这个过程称为交换链(swap chain)。这个操作可以利用IDXGISwapChain接口完成。使用两个buffer(front and back)称之为double buffer。
- Depth buffering是用来计算场景物件与摄像机间的距离。因此我们无需关心绘制物件的顺序。
- 性能计数器(The performance counter)是用来测量帧间隔。QueryPerformanceFrequency函数返回计数器每秒执行次数,利用此可以得出性能计数器的运行时间。当前时间可以利用QueryPerformanceCounter函数获取。
- 为了计算FPS(the average frames per second,每秒平均帧数)。我们统计一定时间内的帧数来得出FPS,为了避免除法运算,通常我们统计一秒钟内的帧数。
- 我们还展示了本书中的所有Demo程序使用的一个统一框架,这些代码已经在 d3dUtil.h, d3dApp.h, 和d3dApp.cpp给出,并在此后不会重复给出上述代码。
- 为了能够调试代码,我们在创建Direct3D设备时加上
D3D11_CREATE_DEVICE_DEBUG标记。此后Direct3D将会在VC++的输出窗口显示Direct3D的调式信息。
文章符号使用备注:
此图样中的内容是博主对书中的原文进行翻译摘录的。