第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教程十一 为三维世界添彩:纹理映射技术(二)