大部分D3D程序都是全屏的,但窗口式的程序也有著它廣泛的用途。制作游戲的輔助工具,如各種地圖、角色編輯器,或一些產(chǎn)品的演示程序中,都要用到這種窗口式的3D程序。更進一步,如果用來顯示3D的表面能夠單獨作為一個控件,使它與其它功能相對獨立就更好了。筆者在VS.NET控件庫里翻了半天,正如所料沒有這樣的東西,于是打算動手寫一個。
我們要做的實際上只是讓D3D設(shè)備把圖像渲到一個控件的矩形區(qū)域內(nèi),而不是弄得滿屏都是。任何控件本質(zhì)上也是一種窗體,在作為渲染表面這一點上,同程序的主窗口(甚至是全屏窗口)沒有區(qū)別。我們可以把3D場景渲染到任何控件上,按鈕、下拉列表、菜單等等,需要做的只是得到這個控件的句柄。而在.NET里,每個控件(窗體)的句柄是存在其Handle屬性里的,也就是說可以獲得。
還有個問題就是何時渲染。以往是我們自己寫主事件循環(huán),并在這里渲染每一楨。但是控件沒有什么主事件循環(huán)可言,我們可以借助其父窗體的事件循環(huán),這里有一點技巧,稍后會說到。
清楚了這兩點就可以開始動手了。無可否認.NET中創(chuàng)建用戶控件和窗體程序的方便快捷,因此筆者打算用Visual C++.NET來完成。首先建立一個控件庫(WindowsControl Library)工程,我將它命名為D3DBox。
我們看到在自動生成的代碼部分定義了該控件類為:
public __gc class D3DBoxControl : public System::Windows::Forms::UserControl |
它從一個UserControl繼承,是自動垃圾回收的(__gc)也就是說我們可以不關(guān)心它的成員指針的釋放問題,.NETFrameWork會自動釋放所有生存過期的托管類指針。不過最好還是不要依賴FrameWork,畢竟這不是好的程序員的習(xí)慣。
回憶一下我們是如何創(chuàng)建一個D3D設(shè)備的:首先創(chuàng)建一個D3D界面指針,然后填充一個描述設(shè)備參數(shù)的結(jié)構(gòu),用這個結(jié)構(gòu)來設(shè)置要創(chuàng)建的設(shè)備。最后用D3D界面指針和作為表面的窗體句柄創(chuàng)建設(shè)備。在這里,我們依然如此創(chuàng)建設(shè)備,代碼本身同以往沒有什么不同。需要說明的是窗體的句柄,在.NET里,每個控件、窗體類以及所繼承出來的類都有個類型為IntPtr的屬性Handle,是.NET中定義的一種用于表示指針或句柄的類。它封裝了很多ToType(Type為Pointer,Int等等)方法,用于在各種場合把它所裝載的句柄的值轉(zhuǎn)成相應(yīng)的類型。這里我們要將它轉(zhuǎn)成指針,使用ToPointer()方法。還要注意,ToPointer()的返回值是void*,別忘了強制轉(zhuǎn)型成為需要的HWND。
有個問題是,D3D以及設(shè)備界面指針作為什么來聲明?大家會想到如果要求更好的封裝性,應(yīng)該把它們聲明為這個控件的屬性。但是如果這樣做,編譯器會認為它們都是托管的。而創(chuàng)建各種界面的函數(shù)所需的界面指針參數(shù)都是非托管的。例如CreateDevice中的參數(shù)IDirect3DDevice9**ppReturnedDeviceInterface無法接收一個托管的指針作為參數(shù),編譯將報錯。對此筆者也沒有更好的辦法,我只能將它們都聲明為全局的,也就是在D3DBox工程里,但是在D3DBox命名空間之外。這樣,至少對于該控件之外的對象來說,這些設(shè)備是不可見的,也即它們不用考慮任何有關(guān)設(shè)備的事。
創(chuàng)建設(shè)備的過程作為該控件的一個方法是沒有問題的。那么創(chuàng)建一個設(shè)備就象這樣:
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); |
筆者更傾向于將這個函數(shù)聲明為私有方法,并且在這個控件類的構(gòu)造函數(shù)中調(diào)用,這樣可以保證在程序中能夠調(diào)用該控件它的其他方法——諸如渲染、設(shè)置燈光(這些都要求有可用的3D設(shè)備)之前設(shè)備已經(jīng)被成功創(chuàng)建了。在構(gòu)造函數(shù)中向InitDevice傳遞的參數(shù)就是全局的D3D和Device界面指針。
在實際的應(yīng)用中,由于3D場景要表現(xiàn)的東西是靈活多樣的,因此理論上都要從這個控件類中繼承適用于當前應(yīng)用程序的子類。因此最好把它的接口聲明為虛函數(shù),尤其是后面的渲染方法。
現(xiàn)在該考慮渲染問題了。既然是一個控件,就應(yīng)該有這樣的效果:我們在其它窗體中繪制出這個控件,不用寫任何代碼,這個控件就可以自動播放3D動畫。這就需要程序的一個地方不停地循環(huán)調(diào)用渲染方法。剛才說到,我們不能像寫窗體程序那樣利用控件自己的主事件循環(huán),因此要把調(diào)用渲染方法放在使用它的程序的主事件循環(huán)中。因此渲染方法應(yīng)該是公有的,使窗體可以調(diào)用。渲染方法的代碼如下:
HRESULT D3DBoxControl::Render() g_lpDevice->BeginScene(); g_lpDevice->Present(NULL,NULL,NULL,NULL); return S_OK; |
這個渲染方法什么都沒做,只是用黑色刷屏。
Build這個工程,出現(xiàn)鏈接錯誤了。因為沒有向工程添加所需的庫文件,編譯器找不到要用的函數(shù)體。你可以右擊文件瀏覽器窗口里的工程,選擇菜單項“屬性(Property)”
在彈出的對話框中Linker->Command Line中加入d3d9.lib d3dx9.lib dxguid.lib winmm.lib libc.lib五項。
現(xiàn)在這已經(jīng)是一個可用的3D控件了。為了測試它的功能,我們還要新建一個窗體工程(Windows FormsApplication),命名為Test。為了能夠使用剛才創(chuàng)建的控件,需要向當前Solution添加控件的工程。右擊Solution名,選擇Add->Existing Project,在彈出對話框中找到剛才的工程文件。
并且該工程必須在當前Solution里再次Build一下。這時會發(fā)現(xiàn)在控件工具欄里的My User Controls標簽里多了一項D3DBoxControl。
這個控件不是一個能自動渲染的東西,我們需要在當前程序中調(diào)用它的Render()方法。打開Form1.cpp,看到這個程序的主函數(shù):
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; } |
窗體的創(chuàng)建及主事件循環(huán)被封裝在Applicition::Run()方法中,顯然需要改動一下才好用。我們首先將Form1類事例化,然后調(diào)用它的Show()方法將窗口激活。
Form1 * frmMain = new Form1(); frmMain->Show(); |
之后是主事件循環(huán),窗體類有個Created屬性,在窗體的生存期(從創(chuàng)建后到被關(guān)閉前)其值為true,可以用來做循環(huán)條件。Application::DoEvents()方法封裝了消息處理的過程,該函數(shù)這樣運行:當消息隊列里有消息時,處理;消息隊列為空時,什么都不作而退出。那么可以在消息處理之后調(diào)用每楨一次的渲染方法。但是有一點要注意,盡管D3D控件對外提供了渲染的接口,但是空間本身是窗體Form1的私有成員,在主函數(shù)里還是不能直接調(diào)用,畢竟主函數(shù)不是窗體的成員函數(shù)。筆者的作法是讓Form1提供一個公有的Render()方法,在里面調(diào)用D3D控件的Render()。完整的主事件循環(huán)如下:
while (frmMain->Created) Application::DoEvents(); |
還有別忘了如果使用如HRESULT等類形時,要引入windows.h
運行的結(jié)果如下所示:
這還不是一個成熟的控件,并不是說沒有什么漂亮的動畫,而是作為一個控件,其生存價值就在于能夠?qū)ν馓峁┤娑`活的接口,是用它的程序員能夠輕松地通過控制它的屬性、調(diào)用它的方法來免去很多與程序邏輯本身沒有太大關(guān)系的繁雜操作。因此提供這樣的接口是編寫控件必須的。正如剛才所說,因為3D動畫程序的靈活性,這類控件很難提供較為全面的功能,很多時候需要繼承更適合程序的子類控件。不過我們可以舉個簡單的例子來說明一下這種方法。
筆者想加入一個bool型的Playing屬性用來控制是否渲染。在窗體中以及運行時可以更改這個屬性,控件根據(jù)這個屬性值判斷是否渲染。在控件的Render()方法中加入這樣一句:
if (!(this->Playing)) return S_OK; |
一個類的任何公有成員都可以作為對外接口,但要是想要像其他屬性那樣在屬性欄里方便地調(diào)整還需要一些特殊的語句,像這樣:
private: |
在VC++.NET里,作為接口的屬性是用這樣一對get/set函數(shù)對聲明的。
更改過的工程需要Rebuild一下才能在其他工程里看到修改的結(jié)果。這里我們看到,在D3DBox的屬性列表里多了Playing一項:
我添加了一個按鈕,在它的Click事件響應(yīng)函數(shù)里更改了D3DBox的Playing屬性。為了演示功能,我擴充了D3DBox的Render()方法,畫了一個轉(zhuǎn)動的圓柱,具體方法不再贅述了。
程序的結(jié)果就是當我按下按鈕時,開始播放動畫,再次按下時停止...
好了,制作一個D3D控件的方法大概就是這樣了。我想讀者的想象力和創(chuàng)造力都不亞于筆者,我相信讀者朋友能夠創(chuàng)造出更好的,更實用的控件。如果朋友們有什么更好的想法,歡迎來信。也希望朋友們能在論壇上與我討論。