最近在做个游戏,因为不能用游戏引擎,所以一开始就选了MFC+GDI的组合,毕竟CImage类是相当好用的,结果发现游戏竟然在还没有加什么功能的时候就只能跑到30帧出头,我觉得有点悬,将来如果加上更多的功能的话,一旦卡到30帧以下就没法忍了。所以我去学了一下Direct2D,这个传说当中的GDI替代品。
网上现在Direct2D的资料不是很多,其中我感觉MSDN的几个样例比较适合新手入门,上面的例子都是Win32的,可能迁移到MFC有点障碍。这篇博客主要针对已经会用GDI做各种操作的人,使他们能够快速地迁移到Direct2D上。
我打算记录三件事在这里:一是基本准备,就是在正式干活之前你要做的事情(这一部分应该说是跟GDI最不一样的),第二是贴图,第三是渲染文字;这篇博客主要说说第一部分。
这几篇博客代码量会很大,但是这些代码都是可以复用的,也就是说,我们只需要将它扒下来,然后直接用就好了…………虽然我也研究了一整天;难点主要在于每部分之间的关系,这也使这篇笔记将着重讲的,实现当中的细节请去查阅MSDN。
首先,按照微软的示例所说的,我们应该创建一个.h文件,里面包括了我们常用的头文件:
#pragma once
// Windows Header Files:
#include <windows.h>
// C RunTime Header Files:
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <wchar.h>
#include <math.h>
#include <d2d1.h>
#include <d2d1helper.h>
#include <dwrite.h>
#include <wincodec.h>
#pragma comment(lib, "d2d1.lib")
#pragma comment(lib, "dwrite.lib")
template<class Interface>
inline void SafeRelease(Interface **ppInterfaceToRelease)
{
if (*ppInterfaceToRelease != NULL)
{
(*ppInterfaceToRelease)->Release();
(*ppInterfaceToRelease) = NULL;
}
}
#ifndef Assert
#if defined( DEBUG ) || defined( _DEBUG )
#define Assert(b) do {if (!(b)) {OutputDebugStringA("Assert: " #b "\n");}} while(0)
#else
#define Assert(b)
#endif //DEBUG || _DEBUG
#endif
#ifndef HINST_THISCOMPONENT
EXTERN_C IMAGE_DOS_HEADER __ImageBase;
#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase)
#endif
这里面比较重要的可能就是那个SafeRelease宏,我相信各位都知道它是干什么的……其他的我们可以暂时不关心。
然后就要说到资源,如果使用GDI的话,我们不需要什么前期准备,直接拿到DC往上面画就行了,但是D2D不一样,在干活之前,我们要先做些准备。这里是我自己封装的一个类,里面包含了常用的几种D2D需要的资源,在MFC中这些可以直接添加到View中,也可以让View继承这个类,享有这个类的成员和函数。
class CDirect2DMFCBase
{
private:
HWND m_hwnd;
ID2D1Factory* m_pDirect2dFactory;
ID2D1HwndRenderTarget* m_pRenderTarget;
IWICImagingFactory* m_pWICImagingFactory;
IDWriteFactory* m_pWriteFactory;
我们来分条看看这些都是干什么的:
ID2D1Factory* m_pDirect2dFactory;
这是D2D的老大,是设备无关资源,只需要在程序一开始创建一次,之后就不用理了。
ID2D1HwndRenderTarget* m_pRenderTarget;
RenderTarget的地位相当于GDI里面的DC,一会会频繁用到,它是设备相关资源,不着急初始化它;
IWICImagingFactory* m_pWICImagingFactory;
这个东西也是设备无关资源,一开始初始化就好,如果你想贴图,那么就需要使用它;
IDWriteFactory* m_pWriteFactory;
这是管渲染字体的设备无关资源,同样一开始初始化就好。
接下来我们可以写一个初始化的函数(为了代码简洁,我去掉了一些错误检查):
HRESULT CDirect2DMFCBase::InitializeD2D()
{
HRESULT hr;
// Initialize device-independent resources, such
// as the Direct2D factory.
CoInitialize(NULL);//初始化COM接口
hr = CreateDeviceIndependentResources();
return hr;
}
在一开始我们就是要创建所有的设备无关资源,CreateDeviceIndependentResources()
实现如下:
HRESULT CDirect2DMFCBase::CreateDeviceIndependentResources()
{
HRESULT hr = S_OK;
// Create a Direct2D factory.
hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pDirect2dFactory);
hr = CoCreateInstance(
CLSID_WICImagingFactory,
NULL,
CLSCTX_INPROC_SERVER,
IID_IWICImagingFactory,
reinterpret_cast<void **>(&m_pWICImagingFactory)
);
hr = DWriteCreateFactory(
DWRITE_FACTORY_TYPE_SHARED,
__uuidof(IDWriteFactory),
reinterpret_cast<IUnknown**>(&m_pWriteFactory)
);
return hr;
}
这样就把所有的设备无关资源都创建完了。
然后,我们需要提供一个创建设备有关资源的方法,在需要的时候调用:
HRESULT CDirect2DMFCBase::CreateDeviceResources()
{
HRESULT hr = S_OK;
if (!m_pRenderTarget)
{
RECT rc;
GetClientRect(m_hwnd, &rc);
D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top);
// Create a Direct2D render target.
hr = m_pDirect2dFactory->CreateHwndRenderTarget(
D2D1::RenderTargetProperties(),
D2D1::HwndRenderTargetProperties(m_hwnd, size),
&m_pRenderTarget
);
}
return hr;
}
大体上,就是创建了一个RenderTarget。需要注意的是,所有的示例都把这个函数写在了重绘函数中,目的是在设备丢失时重新获取设备,所以一开始才有那句if (!m_pRenderTarget)
。
接下来我们看看关键部分,也就是渲染的写法:
HRESULT CDirect2DMFCBase::OnRender()
{
HRESULT hr = S_OK;
hr = CreateDeviceResources();//这就是上面说的,这句话经常出现在绘图函数的开头,目的是为了防止设备丢失
if (SUCCEEDED(hr))
{
m_pRenderTarget->BeginDraw();
m_pRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity());
m_pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
D2D1_SIZE_F rtSize = m_pRenderTarget->GetSize();
///Draw Something
hr = m_pRenderTarget->EndDraw();
}
if (hr == D2DERR_RECREATE_TARGET)
{
hr = S_OK;
DiscardDeviceResources();
}
return hr;
}
那个BeginDraw和EndDraw是不是看起来有些眼熟?
到时候我们基本上是要重写这个函数的,在//Draw Something
那里填进自己的绘图逻辑。
这个类的完整代码如下所示(目前还只是第一版,不知道有什么问题,用的话请谨慎)
#pragma once
#include "stdafx.h"
#include "D2D1Header.h"
class CDirect2DMFCBase
{
private:
HWND m_hwnd;
ID2D1Factory* m_pDirect2dFactory;
ID2D1HwndRenderTarget* m_pRenderTarget;
IWICImagingFactory* m_pWICImagingFactory;
IDWriteFactory* m_pWriteFactory;
public:
CDirect2DMFCBase(void);
~CDirect2DMFCBase(void);
// call methods for instantiating drawing resources
HRESULT InitializeD2D();
ID2D1HwndRenderTarget* GetRenderTarget();
IWICImagingFactory* GetWICImagingFactory();
IDWriteFactory* GetWriteFactory(){return m_pWriteFactory;}
void SetHwnd(HWND v){m_hwnd = v;}
protected:
// Initialize device-independent resources.
HRESULT CreateDeviceIndependentResources();
// Initialize device-dependent resources.
virtual HRESULT CreateDeviceResources(); //由于通常会在这里加载一些外部资源,所以建议你重写这个函数;
// Release device-dependent resource.
void DiscardDeviceResources();
// Draw content.
virtual HRESULT OnRender();//建议重写这个函数来自己定义怎么重画
// Resize the render target.
void OnResize(UINT width,UINT height);
};
注意那两个virtual函数,你很有可能要重写那两个函数,一个用于加载资源,另一个就是实际的绘图函数。
#include "stdafx.h"
#include "Direct2DMFCBase.h"
CDirect2DMFCBase::CDirect2DMFCBase(void)
{
m_hwnd= NULL;
m_pDirect2dFactory = NULL;
m_pRenderTarget = NULL;
m_pWICImagingFactory = NULL;
}
CDirect2DMFCBase::~CDirect2DMFCBase(void)
{
SafeRelease(&m_pDirect2dFactory);
SafeRelease(&m_pRenderTarget);
}
HRESULT CDirect2DMFCBase::InitializeD2D()
{
HRESULT hr;
CoInitialize(NULL);
// Initialize device-independent resources, such
// as the Direct2D factory.
hr = CreateDeviceIndependentResources();
if (SUCCEEDED(hr))
{
// Because the CreateWindow function takes its size in pixels,
// obtain the system DPI and use it to scale the window size.
FLOAT dpiX, dpiY;
// The factory returns the current system DPI. This is also the value it will use
// to create its own windows.
m_pDirect2dFactory->GetDesktopDpi(&dpiX, &dpiY);
}
return hr;
}
HRESULT CDirect2DMFCBase::CreateDeviceIndependentResources()
{
HRESULT hr = S_OK;
// Create a Direct2D factory.
hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pDirect2dFactory);
hr = CoCreateInstance(
CLSID_WICImagingFactory,
NULL,
CLSCTX_INPROC_SERVER,
IID_IWICImagingFactory,
reinterpret_cast<void **>(&m_pWICImagingFactory)
);
hr = DWriteCreateFactory(
DWRITE_FACTORY_TYPE_SHARED,
__uuidof(IDWriteFactory),
reinterpret_cast<IUnknown**>(&m_pWriteFactory)
);
return hr;
}
HRESULT CDirect2DMFCBase::CreateDeviceResources()
{
HRESULT hr = S_OK;
if (!m_pRenderTarget)
{
RECT rc;
GetClientRect(m_hwnd, &rc);
D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top);
// Create a Direct2D render target.
hr = m_pDirect2dFactory->CreateHwndRenderTarget(
D2D1::RenderTargetProperties(),
D2D1::HwndRenderTargetProperties(m_hwnd, size),
&m_pRenderTarget
);
}
return hr;
}
void CDirect2DMFCBase::DiscardDeviceResources()
{
SafeRelease(&m_pRenderTarget);
SafeRelease(&m_pWICImagingFactory);
}
HRESULT CDirect2DMFCBase::OnRender()
{
HRESULT hr = S_OK;
hr = CreateDeviceResources();
if (SUCCEEDED(hr))
{
m_pRenderTarget->BeginDraw();
m_pRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity());
m_pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
D2D1_SIZE_F rtSize = m_pRenderTarget->GetSize();
hr = m_pRenderTarget->EndDraw();
}
if (hr == D2DERR_RECREATE_TARGET)
{
hr = S_OK;
DiscardDeviceResources();
}
return hr;
}
void CDirect2DMFCBase::OnResize(UINT width, UINT height)
{
if (m_pRenderTarget)
{
// Note: This method can fail, but it‘s okay to ignore the
// error here, because the error will be returned again
// the next time EndDraw is called.
m_pRenderTarget->Resize(D2D1::SizeU(width, height));
}
}
IWICImagingFactory* CDirect2DMFCBase::GetWICImagingFactory()
{
return m_pWICImagingFactory;
}
ID2D1HwndRenderTarget* CDirect2DMFCBase::GetRenderTarget()
{
return m_pRenderTarget;
}
好,大部分教程可能到这儿就完了,我在看的时候就特想说,你说的我都懂,可是怎么用呢?
我们以MFC为例:首先,这个类在地位上应该是跟View差不多的,你既可以把这些成员变量和函数塞进View里去,也可以让View继承它,当然View已经继承了CWnd,不过C++是支持多继承的,虽然我是第一次用……这里我让View继承了CDirect2DMFCBase。
首先在View中找地方调用一发初始化函数,比如在OnCreate里:
int CBomberManOnlineView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CWnd::OnCreate(lpCreateStruct) == -1)
return -1;
Init();
InitializeD2D();
CreateDeviceResources();
SetTimer(TIMER_RENDER, 1000/MAX_FPS, NULL);
last_time = timeGetTime();
}
最后两行是我游戏中设置定时器用的。
然后重写CreateDeviceResources();
:
HRESULT CBomberManOnlineView::CreateDeviceResources()
{
HWND hwnd = AfxGetMainWnd()->m_hWnd;
SetHwnd(hwnd);//告诉CDirect2DMFCBase窗口句柄
HRESULT hr = CDirect2DMFCBase::CreateDeviceResources();
//我们可以在这里加载图片等等
return hr;
}
之后的OnRender也别忘了重写,框架和上面的基本一样,贴过来之后中间加上自己的绘图逻辑即可。
还有一件比较重要的事就是关于加载资源,它为什么要写在CreateDeviceResources()
之后呢?我们在GDI中很少有人这么干。这在之后会解释。新手也会困扰,这样的话岂不是丢失一次设备就要重新加载一次资源?确实是这样的,只不过丢失设备的情况很罕见……
当你做完了上面所有的事情之后,你就搭好了一个D2D程序的框架(的一半),虽然你现在还是什么都画不出来,但是前面那个Base类是可以在所有D2D程序中使用的(大概),而且虽然代码量很大,不过你根本不需要知道太多的细节,尤其是Base类里面的那堆东西,可能你在之后的程序中不会再看他第二眼了。
基本准备工作到这里就结束了,下面是一个激动人心的环节,就是贴图了。