大部分D3D程序都是全屏的,但窗口式的程序也有著(zhù)它廣泛的用途。制作游戲的輔助工具,如各種地圖、角色編輯器,或一些產(chǎn)品的演示程序中,都要用到這種窗口式的3D程序。更進(jìn)一步,如果用來(lái)顯示3D的表面能夠單獨作為一個(gè)控件,使它與其它功能相對獨立就更好了。筆者在VS.NET控件庫里翻了半天,正如所料沒(méi)有這樣的東西,于是打算動(dòng)手寫(xiě)一個(gè)。
我們要做的實(shí)際上只是讓D3D設備把圖像渲到一個(gè)控件的矩形區域內,而不是弄得滿(mǎn)屏都是。任何控件本質(zhì)上也是一種窗體,在作為渲染表面這一點(diǎn)上,同程序的主窗口(甚至是全屏窗口)沒(méi)有區別。我們可以把3D場(chǎng)景渲染到任何控件上,按鈕、下拉列表、菜單等等,需要做的只是得到這個(gè)控件的句柄。而在.NET里,每個(gè)控件(窗體)的句柄是存在其Handle屬性里的,也就是說(shuō)可以獲得。
還有個(gè)問(wèn)題就是何時(shí)渲染。以往是我們自己寫(xiě)主事件循環(huán),并在這里渲染每一楨。但是控件沒(méi)有什么主事件循環(huán)可言,我們可以借助其父窗體的事件循環(huán),這里有一點(diǎn)技巧,稍后會(huì )說(shuō)到。
清楚了這兩點(diǎn)就可以開(kāi)始動(dòng)手了。無(wú)可否認.NET中創(chuàng )建用戶(hù)控件和窗體程序的方便快捷,因此筆者打算用Visual C++.NET來(lái)完成。首先建立一個(gè)控件庫(WindowsControl Library)工程,我將它命名為D3DBox。

我們看到在自動(dòng)生成的代碼部分定義了該控件類(lèi)為:
| public __gc class D3DBoxControl : public System::Windows::Forms::UserControl |
它從一個(gè)UserControl繼承,是自動(dòng)垃圾回收的(__gc)也就是說(shuō)我們可以不關(guān)心它的成員指針的釋放問(wèn)題,.NETFrameWork會(huì )自動(dòng)釋放所有生存過(guò)期的托管類(lèi)指針。不過(guò)最好還是不要依賴(lài)FrameWork,畢竟這不是好的程序員的習慣。
回憶一下我們是如何創(chuàng )建一個(gè)D3D設備的:首先創(chuàng )建一個(gè)D3D界面指針,然后填充一個(gè)描述設備參數的結構,用這個(gè)結構來(lái)設置要創(chuàng )建的設備。最后用D3D界面指針和作為表面的窗體句柄創(chuàng )建設備。在這里,我們依然如此創(chuàng )建設備,代碼本身同以往沒(méi)有什么不同。需要說(shuō)明的是窗體的句柄,在.NET里,每個(gè)控件、窗體類(lèi)以及所繼承出來(lái)的類(lèi)都有個(gè)類(lèi)型為IntPtr的屬性Handle,是.NET中定義的一種用于表示指針或句柄的類(lèi)。它封裝了很多ToType(Type為Pointer,Int等等)方法,用于在各種場(chǎng)合把它所裝載的句柄的值轉成相應的類(lèi)型。這里我們要將它轉成指針,使用ToPointer()方法。還要注意,ToPointer()的返回值是void*,別忘了強制轉型成為需要的HWND。
有個(gè)問(wèn)題是,D3D以及設備界面指針作為什么來(lái)聲明?大家會(huì )想到如果要求更好的封裝性,應該把它們聲明為這個(gè)控件的屬性。但是如果這樣做,編譯器會(huì )認為它們都是托管的。而創(chuàng )建各種界面的函數所需的界面指針參數都是非托管的。例如CreateDevice中的參數IDirect3DDevice9**ppReturnedDeviceInterface無(wú)法接收一個(gè)托管的指針作為參數,編譯將報錯。對此筆者也沒(méi)有更好的辦法,我只能將它們都聲明為全局的,也就是在D3DBox工程里,但是在D3DBox命名空間之外。這樣,至少對于該控件之外的對象來(lái)說(shuō),這些設備是不可見(jiàn)的,也即它們不用考慮任何有關(guān)設備的事。
創(chuàng )建設備的過(guò)程作為該控件的一個(gè)方法是沒(méi)有問(wèn)題的。那么創(chuàng )建一個(gè)設備就象這樣:
| 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); |
筆者更傾向于將這個(gè)函數聲明為私有方法,并且在這個(gè)控件類(lèi)的構造函數中調用,這樣可以保證在程序中能夠調用該控件它的其他方法——諸如渲染、設置燈光(這些都要求有可用的3D設備)之前設備已經(jīng)被成功創(chuàng )建了。在構造函數中向InitDevice傳遞的參數就是全局的D3D和Device界面指針。
在實(shí)際的應用中,由于3D場(chǎng)景要表現的東西是靈活多樣的,因此理論上都要從這個(gè)控件類(lèi)中繼承適用于當前應用程序的子類(lèi)。因此最好把它的接口聲明為虛函數,尤其是后面的渲染方法。
現在該考慮渲染問(wèn)題了。既然是一個(gè)控件,就應該有這樣的效果:我們在其它窗體中繪制出這個(gè)控件,不用寫(xiě)任何代碼,這個(gè)控件就可以自動(dòng)播放3D動(dòng)畫(huà)。這就需要程序的一個(gè)地方不停地循環(huán)調用渲染方法。剛才說(shuō)到,我們不能像寫(xiě)窗體程序那樣利用控件自己的主事件循環(huán),因此要把調用渲染方法放在使用它的程序的主事件循環(huán)中。因此渲染方法應該是公有的,使窗體可以調用。渲染方法的代碼如下:
| HRESULT D3DBoxControl::Render() g_lpDevice->BeginScene(); g_lpDevice->Present(NULL,NULL,NULL,NULL); return S_OK; |
這個(gè)渲染方法什么都沒(méi)做,只是用黑色刷屏。
Build這個(gè)工程,出現鏈接錯誤了。因為沒(méi)有向工程添加所需的庫文件,編譯器找不到要用的函數體。你可以右擊文件瀏覽器窗口里的工程,選擇菜單項“屬性(Property)”

在彈出的對話(huà)框中Linker->Command Line中加入d3d9.lib d3dx9.lib dxguid.lib winmm.lib libc.lib五項。

現在這已經(jīng)是一個(gè)可用的3D控件了。為了測試它的功能,我們還要新建一個(gè)窗體工程(Windows FormsApplication),命名為T(mén)est。為了能夠使用剛才創(chuàng )建的控件,需要向當前Solution添加控件的工程。右擊Solution名,選擇Add->Existing Project,在彈出對話(huà)框中找到剛才的工程文件。

并且該工程必須在當前Solution里再次Build一下。這時(shí)會(huì )發(fā)現在控件工具欄里的My User Controls標簽里多了一項D3DBoxControl。

這個(gè)控件不是一個(gè)能自動(dòng)渲染的東西,我們需要在當前程序中調用它的Render()方法。打開(kāi)Form1.cpp,看到這個(gè)程序的主函數:
| 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)被封裝在A(yíng)pplicition::Run()方法中,顯然需要改動(dòng)一下才好用。我們首先將Form1類(lèi)事例化,然后調用它的Show()方法將窗口激活。
| Form1 * frmMain = new Form1(); frmMain->Show(); |
之后是主事件循環(huán),窗體類(lèi)有個(gè)Created屬性,在窗體的生存期(從創(chuàng )建后到被關(guān)閉前)其值為true,可以用來(lái)做循環(huán)條件。Application::DoEvents()方法封裝了消息處理的過(guò)程,該函數這樣運行:當消息隊列里有消息時(shí),處理;消息隊列為空時(shí),什么都不作而退出。那么可以在消息處理之后調用每楨一次的渲染方法。但是有一點(diǎn)要注意,盡管D3D控件對外提供了渲染的接口,但是空間本身是窗體Form1的私有成員,在主函數里還是不能直接調用,畢竟主函數不是窗體的成員函數。筆者的作法是讓Form1提供一個(gè)公有的Render()方法,在里面調用D3D控件的Render()。完整的主事件循環(huán)如下:
| while (frmMain->Created) Application::DoEvents(); |
還有別忘了如果使用如HRESULT等類(lèi)形時(shí),要引入windows.h
運行的結果如下所示:

這還不是一個(gè)成熟的控件,并不是說(shuō)沒(méi)有什么漂亮的動(dòng)畫(huà),而是作為一個(gè)控件,其生存價(jià)值就在于能夠對外提供全面而靈活的接口,是用它的程序員能夠輕松地通過(guò)控制它的屬性、調用它的方法來(lái)免去很多與程序邏輯本身沒(méi)有太大關(guān)系的繁雜操作。因此提供這樣的接口是編寫(xiě)控件必須的。正如剛才所說(shuō),因為3D動(dòng)畫(huà)程序的靈活性,這類(lèi)控件很難提供較為全面的功能,很多時(shí)候需要繼承更適合程序的子類(lèi)控件。不過(guò)我們可以舉個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明一下這種方法。
筆者想加入一個(gè)bool型的Playing屬性用來(lái)控制是否渲染。在窗體中以及運行時(shí)可以更改這個(gè)屬性,控件根據這個(gè)屬性值判斷是否渲染。在控件的Render()方法中加入這樣一句:
| if (!(this->Playing)) return S_OK; |
一個(gè)類(lèi)的任何公有成員都可以作為對外接口,但要是想要像其他屬性那樣在屬性欄里方便地調整還需要一些特殊的語(yǔ)句,像這樣:
| private: |
在VC++.NET里,作為接口的屬性是用這樣一對get/set函數對聲明的。
更改過(guò)的工程需要Rebuild一下才能在其他工程里看到修改的結果。這里我們看到,在D3DBox的屬性列表里多了Playing一項:

我添加了一個(gè)按鈕,在它的Click事件響應函數里更改了D3DBox的Playing屬性。為了演示功能,我擴充了D3DBox的Render()方法,畫(huà)了一個(gè)轉動(dòng)的圓柱,具體方法不再贅述了。
程序的結果就是當我按下按鈕時(shí),開(kāi)始播放動(dòng)畫(huà),再次按下時(shí)停止...

好了,制作一個(gè)D3D控件的方法大概就是這樣了。我想讀者的想象力和創(chuàng )造力都不亞于筆者,我相信讀者朋友能夠創(chuàng )造出更好的,更實(shí)用的控件。如果朋友們有什么更好的想法,歡迎來(lái)信。也希望朋友們能在論壇上與我討論。
聯(lián)系客服