大部分D3D程序都是全屏的,但窗口式的程序也有着它广泛的用途。制作游戏的辅助工具,如各种地图、角色编辑器,或一些产品的演示程序中,都要用到这种窗口式的3D程序。更进一步,如果用来显示3D的表面能够单独作为一个控件,使它与其它功能相对独立就更好了。笔者在VS.NET控件库里翻了半天,正如所料没有这样的东西,于是打算动手写一个。
我们要做的实际上只是让D3D设备把图像渲到一个控件的矩形区域内,而不是弄得满屏都是。任何控件本质上也是一种窗体,在作为渲染表面这一点上,同程序的主窗口(甚至是全屏窗口)没有区别。我们可以把3D场景渲染到任何控件上,按钮、下拉列表、菜单等等,需要做的只是得到这个控件的句柄。而在.NET里,每个控件(窗体)的句柄是存在其Handle属性里的,也就是说可以获得。
还有个问题就是何时渲染。以往是我们自己写主事件循环,并在这里渲染每一桢。但是控件没有什么主事件循环可言,我们可以借助其父窗体的事件循环,这里有一点技巧,稍后会说到。
清楚了这两点就可以开始动手了。无可否认.NET中创建用户控件和窗体程序的方便快捷,因此笔者打算用Visual C++.NET来完成。首先建立一个控件库(WindowsControl Library)工程,我将它命名为D3DBox。
我们看到在自动生成的代码部分定义了该控件类为:
public __gc class D3DBoxControl : public System::Windows::Forms::UserControl |
它从一个UserControl继承,是自动垃圾回收的(__gc)也就是说我们可以不关心它的成员指针的释放问题,.NETFrameWork会自动释放所有生存过期的托管类指针。不过最好还是不要依赖FrameWork,毕竟这不是好的程序员的习惯。
回忆一下我们是如何创建一个D3D设备的:首先创建一个D3D界面指针,然后填充一个描述设备参数的结构,用这个结构来设置要创建的设备。最后用D3D界面指针和作为表面的窗体句柄创建设备。在这里,我们依然如此创建设备,代码本身同以往没有什么不同。需要说明的是窗体的句柄,在.NET里,每个控件、窗体类以及所继承出来的类都有个类型为IntPtr的属性Handle,是.NET中定义的一种用于表示指针或句柄的类。它封装了很多ToType(Type为Pointer,Int等等)方法,用于在各种场合把它所装载的句柄的值转成相应的类型。这里我们要将它转成指针,使用ToPointer()方法。还要注意,ToPointer()的返回值是void*,别忘了强制转型成为需要的HWND。
有个问题是,D3D以及设备界面指针作为什么来声明?大家会想到如果要求更好的封装性,应该把它们声明为这个控件的属性。但是如果这样做,编译器会认为它们都是托管的。而创建各种界面的函数所需的界面指针参数都是非托管的。例如CreateDevice中的参数IDirect3DDevice9**ppReturnedDeviceInterface无法接收一个托管的指针作为参数,编译将报错。对此笔者也没有更好的办法,我只能将它们都声明为全局的,也就是在D3DBox工程里,但是在D3DBox命名空间之外。这样,至少对于该控件之外的对象来说,这些设备是不可见的,也即它们不用考虑任何有关设备的事。
创建设备的过程作为该控件的一个方法是没有问题的。那么创建一个设备就象这样:
HRESULT D3DBoxControl::InitDevice(LPDIRECT3D9 * lplpd3d,LPDIRECT3DDEVICE9 * lplpdevice) else D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(&d3dpp,sizeof(d3dpp)); d3dpp.BackBufferFormat = d3ddm.Format; if (FAILED((*lplpd3d)->CreateDevice(D3DADAPTER_DEFAULT,D3DDEVTYPE_HAL, (*lplpdevice)->SetRenderState(D3DRS_ZENABLE,TRUE); |
笔者更倾向于将这个函数声明为私有方法,并且在这个控件类的构造函数中调用,这样可以保证在程序中能够调用该控件它的其他方法——诸如渲染、设置灯光(这些都要求有可用的3D设备)之前设备已经被成功创建了。在构造函数中向InitDevice传递的参数就是全局的D3D和Device界面指针。
在实际的应用中,由于3D场景要表现的东西是灵活多样的,因此理论上都要从这个控件类中继承适用于当前应用程序的子类。因此最好把它的接口声明为虚函数,尤其是后面的渲染方法。
现在该考虑渲染问题了。既然是一个控件,就应该有这样的效果:我们在其它窗体中绘制出这个控件,不用写任何代码,这个控件就可以自动播放3D动画。这就需要程序的一个地方不停地循环调用渲染方法。刚才说到,我们不能像写窗体程序那样利用控件自己的主事件循环,因此要把调用渲染方法放在使用它的程序的主事件循环中。因此渲染方法应该是公有的,使窗体可以调用。渲染方法的代码如下:
HRESULT D3DBoxControl::Render() g_lpDevice->BeginScene(); g_lpDevice->Present(NULL,NULL,NULL,NULL); return S_OK; |
这个渲染方法什么都没做,只是用黑色刷屏。
Build这个工程,出现链接错误了。因为没有向工程添加所需的库文件,编译器找不到要用的函数体。你可以右击文件浏览器窗口里的工程,选择菜单项“属性(Property)”
在弹出的对话框中Linker->Command Line中加入d3d9.lib d3dx9.lib dxguid.lib winmm.lib libc.lib五项。
现在这已经是一个可用的3D控件了。为了测试它的功能,我们还要新建一个窗体工程(Windows FormsApplication),命名为Test。为了能够使用刚才创建的控件,需要向当前Solution添加控件的工程。右击Solution名,选择Add->Existing Project,在弹出对话框中找到刚才的工程文件。
并且该工程必须在当前Solution里再次Build一下。这时会发现在控件工具栏里的My User Controls标签里多了一项D3DBoxControl。
这个控件不是一个能自动渲染的东西,我们需要在当前程序中调用它的Render()方法。打开Form1.cpp,看到这个程序的主函数:
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { System::Threading::Thread::CurrentThread->ApartmentState = System::Threading::ApartmentState::STA; Application::Run(new Form1()); return 0; } |
窗体的创建及主事件循环被封装在Applicition::Run()方法中,显然需要改动一下才好用。我们首先将Form1类事例化,然后调用它的Show()方法将窗口激活。
Form1 * frmMain = new Form1(); frmMain->Show(); |
之后是主事件循环,窗体类有个Created属性,在窗体的生存期(从创建后到被关闭前)其值为true,可以用来做循环条件。Application::DoEvents()方法封装了消息处理的过程,该函数这样运行:当消息队列里有消息时,处理;消息队列为空时,什么都不作而退出。那么可以在消息处理之后调用每桢一次的渲染方法。但是有一点要注意,尽管D3D控件对外提供了渲染的接口,但是空间本身是窗体Form1的私有成员,在主函数里还是不能直接调用,毕竟主函数不是窗体的成员函数。笔者的作法是让Form1提供一个公有的Render()方法,在里面调用D3D控件的Render()。完整的主事件循环如下:
while (frmMain->Created) Application::DoEvents(); |
还有别忘了如果使用如HRESULT等类形时,要引入windows.h
运行的结果如下所示:
这还不是一个成熟的控件,并不是说没有什么漂亮的动画,而是作为一个控件,其生存价值就在于能够对外提供全面而灵活的接口,是用它的程序员能够轻松地通过控制它的属性、调用它的方法来免去很多与程序逻辑本身没有太大关系的繁杂操作。因此提供这样的接口是编写控件必须的。正如刚才所说,因为3D动画程序的灵活性,这类控件很难提供较为全面的功能,很多时候需要继承更适合程序的子类控件。不过我们可以举个简单的例子来说明一下这种方法。
笔者想加入一个bool型的Playing属性用来控制是否渲染。在窗体中以及运行时可以更改这个属性,控件根据这个属性值判断是否渲染。在控件的Render()方法中加入这样一句:
if (!(this->Playing)) return S_OK; |
一个类的任何公有成员都可以作为对外接口,但要是想要像其他属性那样在属性栏里方便地调整还需要一些特殊的语句,像这样:
private: |
在VC++.NET里,作为接口的属性是用这样一对get/set函数对声明的。
更改过的工程需要Rebuild一下才能在其他工程里看到修改的结果。这里我们看到,在D3DBox的属性列表里多了Playing一项:
我添加了一个按钮,在它的Click事件响应函数里更改了D3DBox的Playing属性。为了演示功能,我扩充了D3DBox的Render()方法,画了一个转动的圆柱,具体方法不再赘述了。
程序的结果就是当我按下按钮时,开始播放动画,再次按下时停止...
好了,制作一个D3D控件的方法大概就是这样了。我想读者的想象力和创造力都不亚于笔者,我相信读者朋友能够创造出更好的,更实用的控件。如果朋友们有什么更好的想法,欢迎来信。也希望朋友们能在论坛上与我讨论。
联系客服