目标, 在.NET 2.0 的托管窗口中框出一块区域进行D3D 绘画. 所谓绘画, 为了减少代码, 暂时定义为纯色屏…
DirectX SDK: 2007 June
Documents:
Samples:
问题的关键就在于, 如何在.Net 的托管环境下使用DirectX, 在托管窗口(Windows::Form) 中进行3D 绘画. 我们有两个选择, 一是使用托管的DirectX (Managed DirectX), 另一种方法是使用Native DirectX (有时候是必须的无奈啊…).
无论是使用托管的, 还是原生的DX, 最核心的, 也是最初的步骤, 都是需要初始化一个D3D 的Device, 使得我们能够通过该Device 来进行3D 绘画.
I. 托管DX 的Device 初始化
以一个基于C++/CLI 的Windows Form Application 为例, 为了获得DX支持, 我们必须添加如下三个.Net References
这三个Reference, 在安装了DirectX 的SDK 后, 会出现在Project->Add Reference->.Net 下. 需要主意的是, 托管的DX 是基于.Net 1.1 的Runtime 的, 在.Net 2.0 和VS 2005 的环境下使用时会抛出一个LoderLock 的Exception. LoadLocker 是VS 2005 引入的MDA (Managed Debugging Assistant) 中的一个, 被设计用来寻找Debug 时一些较难发现的Runtime 问题. 所有的.Net 1.1 的DX 程序集在VS 2005 下都会触发这个Exception (仅在Debugger 下). 暂时的解决方法有三个:
在完成了上面3个步骤中的任何一个以后, 我们就可以专心于我们的托管DX 代码了. 在托管环境下, 我们初始化一个Device 的步骤如下:
最简单的代码片断如下:
try {
PresentParameters^ paras = gcnew PresentParameters();
paras->Windowed = true;
paras->SwapEffect = SwapEffect::Discard;
Device^ device = gcnew Device(0, DeviceType::Hardware, AControl, CreateFlags::HardwareVertexProcessing, paras);
} catch(DirectXException^ e) {
//Do Something
}
事实上, PresentParameters 定义了很多字段, 完整列表可以参考DX SDK 的帮助文件(Mananged Language 版本).
Device 的构造函数有四个, 例子中使用的原型为:
Device (
int adapter,
DeviceType deviceType,
Control^ renderWindow,
CreateFlags behaviorFlags,
array<PresentParameters^>^ presentationParameters
);
其中adapter 为显卡的编号, 0 代表默认的主显卡( 就是目前在用的那块); deviceType 是DeviceType 的一个枚举类型; renderWindow 可以替换为需要绘图的区域/窗体, 比如一个Form, 一个Panel 等, 类型为Control^; behaviorFlags 是CreateFlags 的一个枚举类型, presentationParameters 就是我们之前创建的用来描述Device 的PresentParameters 的一个实例. 在创建Device 之前, 必须确定显卡支持这些特性, 完整的支持列表和参数可以参考DX 帮助和显卡制造商的说明.
在Device 创建完成后, 就可以使用该Device 进行图形绘制. 最简单的, 绘制一个纯色屏幕, 或者说清屏, 代码如下:
device->Clear(ClearFlags::Target, Color::SkyBlue, 1.0f, 0);
device->BeginScene();
//You can do something here to draw 3D stuff
device->EndScene();
device->Present();
Clear 方法将Device 所指定的区域填充为纯色, 例子中为天蓝色. Clear 的原型有四个, 例子中的为:
void Clear ( ClearFlags flags, Color color, float zdepth, int stencil );
其中flags 是一个ClearFlag 枚举, 用来指定要清除的表面; color 是一个Color 枚举.
我们可以将该清屏代码放入Control 的Paint 事件函数中, 这样每次Control 被Repaint, 就会被Clear 为指定的纯色, 或者某个3D 图形( 如果有的话).
至此, Managed DX 和C++/CLI 工作地很好.
II. Native Device 的初始化
Native 的DX 要在托管环境下使用就稍微有点麻烦了. 不过由于是Native 的, 所以在性能上会比托管DX 有所优势( 设计良好的前提下), 而且有时候由于一些第三方库的原因, 不得不使用Native 的DX. 下面简要说明以下在托管环境中使用Native DX 时容易遇到的问题.
首先, 我们要设定头文件的include 路径(%DX SDK%\include), 然后, 添加Library 的路径(%DX SDK%\lib), 接着, 将d3d9.lib, d3dx9.lib, winmm.lib 添加到Input Library 中.
然后在需要的地方包含头文件<d3dx9.h>
初始化一个Native 的DX Device 比托管代码要多一些步骤, 也要格外小心一些, 典型代码如下:
IDirect3D9 *_d3d9 = Direct3DCreate9(D3D_SDK_VERSION); //为了包含正确的头文件, 必须使用D3D_SDK_VERSION
D3DCAPS9 caps; //检查显卡的Capability
HRESULT hr = _d3d9->GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps);
int vp = 0;
if( SUCCEEDED( hr ) ) {
if(caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) {
vp = D3DCREATE_HARDWARE_VERTEXPROCESSING;
} else {
vp = D3DCREATE_SOFTWARE_VERTEXPROCESSING;
}
} else {
//FAIL;
}
D3DPRESENT_PARAMETERS paras;
//填充完整的Present Parameters 信息, 完整的字段含义参考DX 帮助
paras.BackBufferWidth = 200;
paras.BackBufferHeight = 192;
paras.BackBufferFormat = D3DFMT_A8R8G8B8;
paras.BackBufferCount = 1;
paras.MultiSampleType = D3DMULTISAMPLE_NONE;
paras.MultiSampleQuality = 0;
paras.Windowed = true;
paras.SwapEffect = D3DSWAPEFFECT_DISCARD;
paras.hDeviceWindow = (HWND)(pnlOther->Handle.ToPointer());
paras.EnableAutoDepthStencil = true;
paras.AutoDepthStencilFormat = D3DFMT_D24S8;
paras.Flags = 0;
paras.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;
paras.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
IDirect3DDevice9* device = 0; //D3D9 Device
hr = _d3d9->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, (HWND)(panel->Handle.ToPointer()),
vp, ¶s, &device);
if( SUCCEEDED( hr ) ) {
//创建Device 成功
} else {
//FAIL
}
大致的步骤和托管DX 是一样的, 事实上, 在任何时候初始化一个Device 最好都要检查显卡的支持, 在上面的托管代码中, 这部分被省略了, 估计.Net 帮我们完成了很多事情, 默认值工作的很好. 但在Native 代码中, 省略初始化任何一个Paras 的字段都有可能导致Device 的创建失败(Investiagting 中).
另外, CreateDevice 方法的第三个参数接受的是一个HWND 类型的窗口句柄, 但是我们是使用的是一个托管窗口的托管Panel, 因此, 我们要获取这个Panel 的句柄, 方法是调用Panel::Handle 属性, 它返回一个IntPtr, 实际上包含了该控件的句柄, 所以, 我们通过ToPointer() 获得void*, 再强制转换为HWND 传给CreateDevice.
获得了Device , 我们同样可以在Paint 时间中重绘屏幕, 如下:
m_device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0×00000000, 1.0f, 0);
m_device->Present(0, 0, 0, 0);
0×00000000 是颜色的16 进制代码, 代表黑色.
这里还有一个问题, 由于IDirect3DDevice9* 是Native 类型, 因此在ref class 定义中, 如果定义IDirect3DDevice9* 为member, 那我们无法直接通过&device 将成员地址传给CreateDevice() 来创建设备, 因为这时候的IDirect3DDevice9* 被wrap 成CLR 的ptr 类型, 因此, 我们必须先创建一个临时变量IDirect3DDevice9* device ,然后再将创建成功的device 赋值给IDirect3DDevice9* 类型的member, 比如m_device. ( if( SUCCEEDED( hr ) ) m_device = device;)
至此, 我们就完成了Native D3D Device 在托管代码中的初始化, 接下来可以用该Device 可以来进行3D 图形的绘制…
//End of Step First D3D
在编译前需要安装DirectX SDK, 并且重新定义Native Code 部分的头文件路径, LIB 路径地址. 需要禁止LoadLocker (见上文).