第15章 DirectInput接口
DirectInput作為DirectX的組件之一,依然是一些COM對象的集合。DirectInput由IDirectinput8、IDirectInputDevice8和IDirectInputEffect這3個接口組成。其中IDirectInput8作為DirectInput API中最主要的接口,用於初始化系統以及創建輸入設備接口,DirectInput中其他所有接口都需要依賴於我們的IDirectInput8之上,都是通過這個接口進行查詢的。而DirectInputDevice8接口用於表示各種輸入設備,並提供了相同的訪問和控制方法。對於某些輸入設備(如鼠標),都能夠通過查詢各自的IDirectInputDevice8接口對象,得到另一個接口IDirectInputEffect8。而IDirectInput8接口則用於控制設備的力反饋效果。
DirectInput使用步驟:
首先是包含 DInput.h 頭文件,並且項目屬性頁中已經鏈接了 DInput.lib 等庫文件:
然后是創建DirectInput接口和設備:
通過調用DirectInputCreate函數創建並初始化IDirectInput接口:
HRESULT DirectInput8Create( HINSTANCE hinst, //當前創建的DirectInput的Windows程序句柄 DWORD dwVersion, //DirectInput的版本號 REFIID riidltf, //接口的標志,通常取IID_IDirectInput8就可以了 LPVOID * ppvOut, //用於返回我們新創建的IDrectInput8指針 LPUNKNOWN punkOuter //設為NULL即可 )
下面是個調用的例子:
//創建DirectInput設備 LPDIRECTINPUT8 g_DirectInput = NULL; if(FAILED(DirectInput8Create(hInstance, 0x800, IID_IDirectInput8, (void**)&g_pDirectInput, NULL))) return E_FAIL;
在IDirectInput8接口中包含了很多了很多用於初始化設備及獲得設備接口的方法。其中,常用的方法為EnumDevices和CreateDevices。前者用於獲得輸入設備的類型,而后者用於為輸入設備創建IDirectInputDevice8接口對象。需要注意的是,系統中每一個已安裝的設備都有一個系統分配的全局唯一標識符(GUID)。要使用某個設備的話,首先我們就需要知道它的GUID。
鼠標和鍵盤作為我們電腦中最為重要的外設,DirectInput對它們做了特殊對待,定義了它們的GUID分別為GUID_Keyboard和GUID_SysMouse。而對於其他的輸入設備,我們就用上面提到過的EnumDevices方法枚舉出這些設備,已得到它們的GUID:
HRESULT EnumDevices(
DWORD dwDevType, //指定我們需要枚舉的設備類型
LPDIENUMDEVICESCALLBACK lpCallback, //用於指定一個回調函數的地址,當系統中每找到一個匹配的設備時,就會自動調用這個回調函數
LPVOID pvRef, //返回我們當前匹配設備的GUID值
DWORD dwFlags //指定我們枚舉設備的方式
)
取得我們需要使用的設備的GUID后,就可以根據這個GUID來調用IDrectInput9接口中的CreateDevice方法,進而來創建設備的IDirectInputDevice接口對象了。
HRESULT CreateDevice( REFGUID rguid, //輸出設備的GUID LPDIRECTINPUTDEVICE * lplpDirectInputDevice, //創建的輸入設備對象的指針地址,可以說調用這個CreateDevice參數就是在初始化這個參數 LPUNKNOWN pUnkOuter //取NULL即可 )
下面的代碼中CreateDevice方法的第一個參數我們填的是GUID_SysMouse,為系統鼠標創建一個Direct設備接口對象:
LPDIRECTINPUTDEVICE8 g_pMouseDevice = NULL; if(FAILED(g_pDirectInput->CreateDevice(GUID_SysKeyboard,&g_pKeyboardDevice,NULL)) return E_FAIL;
設置數據格式:數據格式用於表示設備狀態信息的存儲方式,每種設備都有一種用於讀取對應數據的特定數據格式,所以對每種設備都要區別對待。設置數據格式通常都是用IDrectInputDevice8接口的SetDataFormat方法來做到,這個方法可以把設備的數據格式填充到一個DIDATAFORMAT接口類型的對象。
HRESULT SetDataFormat(
LPCDIDATAFORMAT lpdf
)
關於LPCDIDATAFORMAT類型的lpdf,下面是一些備選參數:
設置協作級別:因為每個應用程序通常會使用多個輸入設備,並且同一輸入設備也可能被多個應用程序同時使用。因此,需要一種方式來共享和協調應用程序對舍設備的訪問。協作級別定義了進程與其他應用程序和操作系統共享設備的方式。設備一旦創建就需要設置它的協作級別。DirectInput的協作級別可以以兩套方案來分類:前台,后台模式和共享,獨占模式。
平常通過IDirectInputDevice8接口的SetCooperativeLevel方法來設置設備的協作級別:
HRESULT SetCooperativeLevel(
HWND hwnd, //與當前設備想關聯的窗口句柄
DWORD dwFlags //描述了當前設備的協作級別類型
)
設置特殊屬性:設備的特殊屬性包含設備的數據模式,緩沖區大小以及設備的最小最大范圍等等。DirectInput為我們提供了SetProperty方法來設置設備的特殊屬性:
HRESULT SetProperty(
REFGUID rguidProp,
LPCDIPROPHEADER pdiph
)
獲取和輪詢設備:使用設備之前往往要重新獲取一下設備的控制權。通過IDirectInput8接口的Acquire方法:
g_pMouseDevice->Acquire()
另外需要注意的是,在獲得設備的控制權之前,必須先調用IDirectInputDevice8接口的SetDataFormat或者SetActionMap方法來設置一下數據格式,否則調用Acquire方法的話,將直接給我們返回DIERR_INVALIDPARAM錯誤。
輪詢可以准備在合適的情況下讀取設備數據。因為數據可能具有臨界時間。用法如下:
g_pMouseDevice->Poll();
讀取設備信息:拿到對輸入設備的控制權之后,就可以調用IDirectInputDevice8接口的GetDeviceState方法來讀取設備的數據。為了存儲設備的數據信息,在調用該方法時,須傳遞一個數據緩沖區給GetDevice方法。
HRESULT GetDeviceState(
DWORD cbData, //指定緩沖區的大小
LPVOID lpvData //一個獲取當前設備狀態的結構體的地址
)
第二個參數的數據格式和之前調用的SetDataFormat方法有着前呼后應的密切聯系:
比如,我們先調用了SetDataFormat設置了設備的數據格式為c_dfDIMouse:
g_pMouseDevice->SetDataFormat(&c_dfDIMouse);
那么在讀取設備信息的時候調用GetDeviceState就需要把第二個參數填與dfDIMouse對應的DIMOUSESTATE結構體中的一個實例:
DIMOUSESTATE dimouse; g_pMouseDevice->GetDeviceState(sizeof(dimouse), (LPVOID)&dimouse);
DirectInuput使用五步曲(以鍵盤為例):
- 創建DirectInput接口和設備
- 設置數據格式和協作級別
- 獲取設備控制權
- 獲取按鍵情況並做響應
- 釋放控制權和接口對象
示例代碼如下:
//首先是全局變量的定義 LPDIRCTINPUTDEVICE8 g_pKeyboardDevice = NULL; char g_pKeyStateBuffer[256]={0}; //【DirectInput使用五步曲之一】,創建DirectInput接口和設備 DirectInput8Create(hInstance, 0x800, IID_IDirectInput8, (void**)&g_pDirectInput, NULL); g_pDirectInput->CreateDevice(GUID_Syskeyboard, &g_pKeyboardDevice, NULL); //【DirectInput使用五步曲之二】,設置數據格式和協作級別 g_pKeyboardDevice->setDataFormat(&c_dfDIKeyboard); g_pKeyboardDevice->setCooperativeLevel(hwnd,DISCL_FOreGROUND|DISCL_NONEXECLUSIVE); //【DirectInput使用五步曲之三】,獲取設備控制權 g_pKeyboardDevice->Acquire(); //【DirectInput使用五步曲之四】,獲取按鍵情況並作出響應 //讀取鍵盤輸入 ZeroMemory(g_pKeyStateBuffer,sizeof(g_pKeyStateBuffer); Device_Read(g_pKeyboardDevice,(LPVOID)g_pKeyStateBuffer,sizeof(g_pKeyStateBuffer)); //定義的全局函數 BOOL Device_Read(IDirectInputDevice8* pDIDevice, void* pBuffer, longlSize) { HRESULT hr; while(true) { pDIDevice->Poll(); //輪詢設備 pDIDevice->Acquire(); if(SUCCEEDED(hr=pDIDevice->GetDeviceState(lSize,pBuffer))) break; if(hr!=DIERR_INPUTLOST||hr!=DIERR_NOTACQUIRED) return false; if(FAILED(pDIDevice->Acquire())) return false; } return TRUE; } //然后就是用if判斷並作響應了 if(g_pKeyStateBuffer[DIK_A]&0x800)fPosX-=0.005f; //【DirectInput使用五步曲之五】,釋放控制權和接口對象 g_pKeyboardDevice->Unacquire(); SAFE_RELEASE(g_pKeyboardDevice);
注:上面判斷鍵碼為什么要按位與0x800? 現在還不是很明白
DirectInput鍵盤按鍵值:簡而言之就是DirectInput並非使用Windows中的消息機制來讀取鍵盤的狀態,而是直接讀取硬件的狀態獲取按鍵的掃描碼的,DirectInput中與鍵對應的宏是以DIK_開頭。在程序中,需要定義一個大小為256字節的數組,其中每一個字節都存儲一個按鍵的狀態,這樣就可以保存256個按鍵的狀態信息了。關於DirectInput的鍵碼可以查msdn文檔
DirectInput鼠標按鍵值:在Direct3D中,可以直接同鼠標的驅動程序進行交互,而不用走消息隊列這條慢悠悠的道。另外有絕對模式和相對模式來跟蹤鼠標的移動。在DirectInput中,鼠標的移動信息通常是通過一個DIMOUSESTATE結構體來記錄的:
typedef struct DIMOUSESTATE { LONG lX; //x軸坐標 LONG lY; //y軸坐標 LONG lZ; //滾輪的相對移動量,沒移動的話就是0 BYTE rgbButtons[4]; //rgbButtons[0]代表鼠標左鍵,rgbButtons[1]代表鼠標右鍵 } DIMOUSESTATE, *LPDIMOUSESTATE
第16章 紋理映射
計算機圖形學中比較重要的一塊,首先是明白其概念。比如現在要繪制出如下效果的一個貼了瓷磚的立方體:
問題在於如何繪制出像磚塊那樣坑坑窪窪的效果呢?首先是線繪制一個立方體,接着注備一副描繪着磚塊狀的2D圖片,然后把這幅圖片像貼畫一樣貼到這個立方體的六個面:
上面的方法就是紋理映射的思想。即將2D圖像映射到3D物體上的技術。
紋理映射使用四步曲
1. 紋理坐標的定義:一般把紋理映射所使用的2D圖像稱作為紋理貼圖。Direct3D支持多種格式圖片的紋理貼圖,一般使用邊長為2的N次冪正方形圖片。紋理貼圖往往都通過一個二維數組存儲每個點的顏色值,稱之為紋理元素,而每個紋理元素在紋理中都有唯一的地址。為了將紋理貼圖映射到三維圖形中,Direct3D使用了紋理坐標確定紋理貼圖上的每個紋理元素。
紋理坐標由一個二維坐標系指定,這個坐標系由沿水平方向的u軸和沿垂直方向的v軸構成,(u,v)。其中u軸正方向為水平右,v軸正方向為垂直向下,它們的取值都在[0,1]之間。
紋理坐標位於紋理空間之中,是相對坐標,相對於紋理坐標系中的源點(0,0)。當把紋理映到三維模型表面上時,紋理元素首先被映射到物體模型的局部坐標系中,然后再變換到屏幕坐標系中對應的像素位置。
2. 頂點的訪問:在FVF靈活頂點格式中定義好紋理坐標之后,要想把紋理坐標存到緩存頂點中,需要訪問頂點緩存。比如在第一步中定義的頂點格式是這樣的(看到這里要點迷迷糊糊的,頂點為什要定義紋理坐標,現在猜測是因為這個頂點要根據它里面的紋理坐標來去紋理坐標系中取相應的紋理元素來貼圖):
struct CUSTOMVERTEX { FLOAT _x, _y, _z; // 頂點的位置 FLOAT _u, _v; // 紋理坐標 CUSTOMVERTEX(FLOAT x, FLOAT y, FLOAT z, FLOAT u, FLOAT v) : _x(x), _y(y), _z(z), _u(u), _v(v) {} }; #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_TEX1)
上面的程序看不懂了,哦哦,難道這里要gg?開玩笑,百度大法 ,找到了其中一部分的解答:
也就是說,CUSTOMVERTEX(FLOAT x, FLOAT y, FLOAT z, FLOAT u, FLOAT v) : _x(x), _y(y), _z(z), _u(u), _v(v) { } ,這句話的作用是利用CUSTOMVERTEX構造函數對成員_x, _y, _z, _u和_v進行初始化,_x(x) 的意思就是將構造函數的輸入參數x賦給_x,其他的同理。
回到正題,如果第一步定義的頂點格式如上,接着下一步就要做如下的填空題:
//填充頂點緩存 CUSTOMVERTEX *pVertices; if(FAILED(g_pVertextBuffer->Lock(0,sizeof(CUSTOMVERTEX),(void**)&pVertices,0))) return E_FAIL; //正面頂點數據 pVertices[0]=CUSTOMVERTEX(-10.0f,10.0f,-10.0f,0.0f,0.0f); pVertices[1]=CUSTOMVERTEX(10.0f,10.0f,-10.0f,1.0f,0.0f); pVertices[2]=CUSTOMVERTEX(10.0f,-10.0f,-10.0f,1.0f,1.0f); pVertices[3]=CUSTOMVERTEX(-10.0f,-10.0f,-10.0f,0.0f,1.0f); g_pVertextBuffer->Unlock();
3. 紋理的創建:頂點這邊的內同經過以上兩步已經大功告成,接下里就是創建一個紋理對象,從文件中讀取一副紋理並保存在這個對象中。在Direct3D中,紋理是以COM對象的形式存在的,也就是IDirect3DTexture9這個接口。要對物體表面進行紋理映射的話,首先要創建紋理對象,指定紋理的寬,高,格式等屬性,然后還需要將圖形文件加載到紋理對象中。可以用D3DX庫中的D3DXCreateTexture函數創建一個紋理對象。
HRESULT D3DXCreateTexture( _In_ LPDIRECT3DDEVICE9 pDevice, //Direct3D設備對象 _In_ UINT Width, //紋理對象寬度 _In_ UINT Height, //高度 _In_ UINT MipLevels, //漸進級別 _In_ DWORD Usage, //紋理的使用方式 _In_ D3DFORMAT Format, //紋理中保存每個顏色成分所使用的位數 _In_ D3DPOOL Pool, //紋理對象停駐的內存類別 _Out_ LPDIRECT3DTEXTURE9 *ppTexture //我們要的東西,指向IDirect3DTexture9接口的指針 );
更多的情況是從文件中讀取紋理圖形的,用的函數是D3DXCreateTextureFromFile函數:
HRESULT D3DXCreateTextureFromFile( _In_ LPDIRECT3DDEVICE9 pDevice, //Direct設備對象 _In_ LPCTSTR pSrcFile, //用於創建紋理的圖標文件名字的字符串 _Out_ LPDIRECT3DTEXTURE9 *ppTexture );
4. 紋理的啟用:加載完紋理后,就可以調用IDirect3DDevice接口的SetTexture方法,設置當前需要啟用的紋理:
HRESULT SetTexture( [in] DWORD Sampler, //指定了應用的紋理是哪一層 [in] IDirect3DBaseTexture9 *pTexture //將要啟用的紋理的IDirect3DBaseTexture9對象 );
另外,如果物體模型所使用的紋理不相同,那么在每個繪制物體模型之前都需要調用該方法設置對應的紋理:
四大紋理過濾方式
當Direxct3D渲染一個圖元或者三維圖形時,必須將它們通過坐標變換映射到二維屏幕之上。而但我們使用紋理來進行輔助渲染的時候,Direct3D就必須使用該紋理為二維圖像上的每個像素進行着色。這里的每個像素都包含一個來自紋理的顏色值,而從紋理中為每個像素獲取顏色的過程,就是所謂的紋理過濾了。大多數情況下,屏幕顯示的圖形與紋理貼圖大小是不相同的。這個紋理會被映射到一個比他大或者小的圖元圖像上。紋理會被放大或者縮小,對紋理的放大會造成很多像素被映射到同一個紋理元素上,這樣的圖形渲染的結果就會有色塊的感覺。縮小一個紋理意味着一個像素被映射到許多紋理上,圖形看上去閃爍或者失真或者有鋸齒。為了解決這些問題,就有了紋理過濾方式。
Direct3D,有四種過濾方式:
- 最近點采樣過濾
- 線性紋理過濾
- 各項異性過濾
- 多級漸進過濾
設置紋理過濾方式通常采用的是IDirect3DDevice9::SetSamplerState函數:
HRESULT SetSamplerState( [in] DWORD Sampler, //指定為哪一層紋理設置采樣狀態 [in] D3DSAMPLERSTATETYPE Type, //指定對哪種紋理采用屬性來進行操作 [in] DWORD Value //對第二個參數指定的屬性進行值的設置 );
1. 最近點采樣過濾:速度最快,效果最差。Direct3D計算得到的紋理過濾元素通常是一個浮點值,使用最近點采樣時,Direct3D會復制與這個浮點值地址最接近的整數地址的紋理元素的顏色。
下面是一個調用實例,把紋理層0的過濾方式設置為最近點采樣:
//最近點采樣過濾 g_pd3dDevice->setSamplerState(0,D3DSAMP_MAGFILTER,D3DTEXF_POINT); g_pd3dDevice->setSamplerState(0,D3DSAMP_MINFILTER,D3DTEXF_POINT);
2. 線性紋理過濾:目前使用最廣泛的紋理過濾方式。線性紋理過濾取得與計算得到的紋理元素的浮點地址最接近的上下左右4個紋理元素,對這四個紋理元素進行加權平均,從而得到最終顯示的顏色值。依舊來個調用實例,將第0層紋理的放大和縮小過濾器設置為線性過濾器:
//最近點采樣過濾 g_pd3dDevice->setSamplerState(0,D3DSAMP_MAGFILTER,D3DTEXF_LINEAR); g_pd3dDevice->setSamplerState(0,D3DSAMP_MINFILTER,D3DTEXF_LINEAR);
3. 各向異性紋理過濾:在很多時候,三維物體的表面不可能是完全平面的,當三維物體的表面和投影平面不平行時,它在屏幕上的投影會有拉長和扭曲的現象,這種現象稱為各向異性。當一個各項異性的圖元映射到紋理元素上時,就會發生扭曲。而Direct3D會根據屏幕像素反向轉換到紋理元素的延長度,來決定各項異性的程度。
關於使用方法,基本和之前類似,但是還需要專門設置一下最大各項異性的程度值:
//各項異性過濾 g_pd3dDevice->setSamplerState(1,D3DSAMP_MAXANISOTROPY,3) g_pd3dDevice->setSamplerState(1,D3DSAMP_MAGFILTER,D3DTEXF_ANISOTROPIC); g_pd3dDevice->setSamplerState(1,D3DSAMP_MINFILTER,D3DTEXF_ANISOTROPIC);
4. 多級漸進紋理過濾:這種過濾方式要和上面的過濾方式中的一種結合使用。多級漸進紋理就是由一組分辨率逐漸降低的紋理序列組成,每一級紋理的寬度和高度都是上一級紋理的寬度和高度的一半。這些圖片不一定要是正方形。在Direct3D映射紋理時,會自動選擇一副與物體大小最接近的紋理進行渲染。
對於多級漸進紋理的生成,使用的函數是D3DXCreateTextureFromFileEx:
HRESULT D3DXCreateTextureFromFileEx( _In_ LPDIRECT3DDEVICE9 pDevice, //D3D設備接口對象 _In_ LPCTSTR pSrcFile, //紋理貼圖的文件地址 _In_ UINT Width, //紋理寬度,0表示使用貼圖的寬度 _In_ UINT Height, //高度 _In_ UINT MipLevels, //生成的漸進紋理的級數目 _In_ DWORD Usage, //用法的標志,通常設為0 _In_ D3DFORMAT Format, //紋理貼圖的格式 _In_ D3DPOOL Pool, //保存紋理的方式 _In_ DWORD Filter, //紋理過濾方式 _In_ DWORD MipFilter, //生成紋理序列的過濾方式 _In_ D3DCOLOR ColorKey, //替換Alpha值的顏色值 _Inout_ D3DXIMAGE_INFO *pSrcInfo, //設為NULL即可 _Out_ PALETTEENTRY *pPalette, //同上 _Out_ LPDIRECT3DTEXTURE9 *ppTexture //紋理接口對象 );
下面是這個函數的一個調用實例:
對於多級漸進紋理的使用,要和之前的3種過濾方式的其中一種組合使用。用的還是SetSamplerState函數,第一個參數還是紋理層數的序號,第二個參數為設為D3DSAMP_MIPFILTER,第三個參數設置為在相鄰紋理之間的過濾方式。比如下面這個代碼就把相鄰紋理級之間的過濾方式設置為線性過濾:
g_pd3dDevice->setSamplerState(0,D3DSAMP_MIPFILTER,D3DTEXF_LINEAR)
四大紋理尋址方式
Direct3D應用程序可以為任何圖元的任何頂點指定紋理坐標。通常使用的u、v紋理坐標的取值范圍是[0.0, 1.0]。但如何控制[0.0, 1.0]坐標范圍之外的紋理坐標值了,這時候需要用到紋理尋址模式。Direct3D中有四種紋理模式供我們選擇,來處理超出[0.0, 1.0]坐標范圍之外的紋理映射情況。分別是:重復紋理尋址模式,鏡像紋理尋址模式,夾取紋理尋址模式和邊框顏色紋理尋址。
1. 重復紋理尋址模式:默認的尋址模式,允許在每個整數連接點處重復上一個整數的紋理。
我們可以用那個SetSamplerState函數來手動啟用這種重復尋址模式,如下兩句連用,分別對U軸和V軸啟用重復紋理尋址模式:
g_pd3dDevice->setSamplerState(0,D3DSAMP_ADDRESSU,D3DTADDRESS_WRAP); g_pd3dDevice->setSamplerState(0,D3DSAMP_ADDRESSV,D3DTADDRESS_WRAP);
2. 鏡像尋址模式:會在每個整數紋理坐標連接處自動復制並翻轉紋理,且為兩兩成對翻轉,這樣的話上面那個正方形圖元就會變成這樣:
3. 夾取尋址模式:將紋理坐標夾取在[0.0,1.0]之間,就是說,在[0.0,1.0]之間把紋理復制一遍,然后對於這之外的內容,將邊緣的u軸和v軸進行一下延伸。對於上面的那個圖元,在4個頂點的紋理坐標相同的情況下,就會變成這樣:
4. 邊框顏色紋理尋址:在[0.0,1.0]之間繪制一下紋理,然后[0.0,1.0]之外的內容就用邊框顏色填充:
參考博客:
【Visual C++】游戲開發筆記四十一 淺墨DirectX教程之九 為三維世界添彩:紋理映射技術(一)
【Visual C++】游戲開發筆記四十二 淺墨DirectX教程之十 游戲輸入控制利器:DirectInput專場
【Visual C++】游戲開發筆記四十三 淺墨DirectX教程十一 為三維世界添彩:紋理映射技術(二)