友情提醒:所谓的框架是指SDK目录下/Samples/C++/Common路径下的DXUT系列函数包装。学习框架的前提是必须有足够的 Windows API,GUI编程经验,必须熟悉Windows的消息机制,回调机制,最好有万行左右的C/C++编程经验。MFC在这里没有任何用处。另外我觉得最好 在看程序之前对于D3D的所有概念有点了解,什么是vertex,texture,matrix,lighting,mesh等等,以及相关的数学概念。 这些都可以在网上找到中文翻译,帮助你快速入门。
DXSDK2006和2003版的比起来更新了不少东西,比如DirectX10,还有Managed
DirectX等等。不过我关心的还是D3D9。除了个别接口的更改之外,DXSDK2006还提供了一套图形控件的类库,它的界面还是很漂亮的:)如图:
学习一个框架还是从它的入口学习比较方便,否则容易迷失在无穷无尽的API和层层包装之中。DXSDK2006的框架和2003版的DX9.0c框架有 很大的不同。首先是2003版的框架中提供了一个CD3DApplication类,这个类对于初始化,清除,以及游戏窗口的创建,游戏主循环进行了包 装。这是一个不错的类,不知道为什么在2006版中去掉了。不过不要紧,2006版的框架中提供的一些C包装函数已经足够了。在看这些函数之前,我们还是 先来看看SDK目录下/Samples/C++ /Direct3D/Tutorials中有些什么吧。Tut01_CreateDevice是创建框架,这个程序不用框架,研究一下有助于了解D3D的 大致工作流程。下面是winmain函数中的一部分。
// Initialize Direct3D
if(
SUCCEEDED( InitD3D( hWnd ) ) )
{
// Show the window
ShowWindow(
hWnd, SW_SHOWDEFAULT );
UpdateWindow(
hWnd );
// Enter the message loop
MSG
msg;
while(
GetMessage( &msg, NULL, 0, 0 ) )
{
TranslateMessage( &
msg );
DispatchMessage( &
msg );
}
}
在消息循环之前有个初始化设备的函数InitD3D( hWnd ),其代码如下:
HRESULT
InitD3D(
HWNDhWnd )
{
if(
NULL == ( g_pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) )
return
E_FAIL;
D3DPRESENT_PARAMETERS
d3dpp;
ZeroMemory( &
d3dpp, sizeof(d3dpp) );
d3dpp.
Windowed = TRUE;
d3dpp.
SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.
BackBufferFormat = D3DFMT_UNKNOWN;
if(
FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&
d3dpp,
&
g_pd3dDevice ) ) )
{
return
E_FAIL;
}
return
S_OK;
}
主要是调用
Direct3DCreate9
和
g_pD3D->
CreateDevice
这两个函数。查看
DXSDK
文档中关于
D3DPRESENT_PARAMETERS
的定义,大致了解一下。
接下来要关心的就是消息循环了,在回调函数
MsgProc中处理了两个消息,一个是
WM_DESTROY,里面调用了Cleanup函数,另一个是WM_PAINT函数,里面调用了Render函数。Cleanup函数很简单,就是调用D3D对象及其设备对象的Release函数释放资源,而Render函数就是D3D中最重要的函数了。
VOID
Render()
{
if(
NULL==g_pd3dDevice)
return;
// Clear the backbuffer to a blue color
g_pd3dDevice->
Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );
// Begin the scene
if(
SUCCEEDED( g_pd3dDevice->BeginScene() ) )
{
// Rendering of scene objects can happen here
// End the scene
g_pd3dDevice->
EndScene();
}
// Present the backbuffer contents to the display
g_pd3dDevice->
Present( NULL, NULL, NULL, NULL);
}
主要调用的函数有BeginScene, EndScene和Present函数。
对D3D应用程序有了大概了解之后就可以看空框架程序了。这个程序可以在Samples/C++/Direct3D/EmptyProject中找到。
从WinMain中的调用可以看到,框架首先设定一堆回调函数,很多事情的是在用户自己写的回调函数中实现。从DXUTInit开始,程序开始调用框架 内的API来完成初始化——创建窗口——创建设备——主消息循环——退出等一系列操作。调查Common目录下DXUT.cpp文件就可以发现 DXUTInit函数干了以下几件事情
① 设定开始调用这个函数的标志符
② InitCommonControls
③ 保存当前的sticky/toggle/filter键
④ 通过事先导入winmm.dll的方法timeBeginPeriod来确保调用Sleep的准确性
⑤ 设定一些标志附,读取命令行参数
⑥ 检查版本
⑦ 获 得D3D对象指针。值得一提的是框架中大部分全局变量是通过类DXUTState的静态变量state的get/set方法得到的。这些get/set方 法是用宏定义的,里面调用了加锁和解锁,因此保证了全局变量设定的线程安全。这些全局性的变量包括D3D对象指针,D3D设备对象指针, BackBufferSurfaceDesc,DeviceCaps,窗口HINSTANCE,窗口句柄HWND,焦点句柄HWNDFocus,全屏设备 句柄,窗口设备句柄,窗口客户端矩形,模式切换时窗口客户端矩形,模式切换时全屏客户端矩形,Time,ElapsedTime,FPS数,窗口标题,设 备数据DeviceStats,以及是否暂停渲染,时间是否暂停,窗口是否激活等标志,一些窗口事件等等。这些都可以通过 DXUTGETXXX/DXUTSETXXX/DXUTISXXX系列包装函数获得。
⑧ 通过DXUT_Dynamic_Direct3DCreate9创建D3D对象。很多D3D底层API都是通过动态的方式加载的,这样有利于效率的提高。
⑨ 重设全局时钟
⑩ 设 定DXUTInited为true。很多DXUT系列的函数都喜欢在入口设定一个开始调这个函数的标志,在出口设定一个这个函数已经被调过的标志,这样可 以在以后再次调用这个函数的时候了解当前什么工作已经做了,什么工作没做需要补做。我想这个主要是用来防止函数重入问题的吧。其他函数中的这一对函数就不 再提了
呼~第一个函数大致看完了,接下来是DXUTCreateWindow函数。什么?要问DXUTSetCursorSettings为什么被无视?因为这个函数不重要。DXUTCreateWindow的工作大致是这样的
① 判断关于设备的CallBack有没有设定好
② 判断DXUTInit()有没有被调用成功(注意不是有没有调用)。
③ 获得焦点句柄,因为窗口还没有创建,所以这个句柄应该是NULL
④ 设定HInstance
⑤ 设定窗口类
⑥ 注册窗口类
⑦ 设定窗口位置和大小。好长一段代码,汗
⑧ 创建窗口。终于。。。
⑨ 设定窗口焦点句柄,全屏设备句柄,窗口设备句柄
接下来的函数是DXUTCreateDevice。这个函数就是用来选择最优设备并创建的。
① 设定参数中的回调函数和上下文,以备后用
② 检查窗口是否被成功创建,否则再调用一次DXUTCreateWindow
③ 枚举所有可能的显示模式。枚举过程非常复杂,用到了CD3DEnumeration中的一些包装函数,这些设备信息包括分辨率,颜色位深等等。这里会用到DXUTCreateDevice传进来的参数IsDeviceAcceptable
④ 如果命令行设定过显示模式,那么将刚才得到的信息覆盖。
⑤ 采用某种权重的算法找出最优显示模式(DXUTFindValidDeviceSettings)
⑥ 切 换设备。这里用到了DXUTCreateDevice传进来的参数ModifyDeviceSettings。切换设备时要考虑很多问题:比如需要暂时忽 略WM_SIZE消息;只有在第一次创建设备的时候才用命令行参数;按照需要调用DXUTCreate3DEnvironment和 DXUTReset3DEnvironment;分全屏和窗口设备重设;重设完了根据需要处理WM_SIZE消息;显示窗口,允许WM_SIZE消息等等
最后是DXUTMainLoop。
① 检查是否有重入问题
② 设定进入主循环标志
③ 检查设备是否已经被成功创建,没创建的话用默认参数创建一次
④ 检查前面三个函数是否成功调用。汗,又是检查
⑤ 处理窗口消息,注意只有在没有消息处理的时候才调用DXUTRender3DEnvironment()
⑥ 在消息循环退出之后清除加速表。应该是类似SHIFT+X这种键盘加速表的清除吧
⑦ 更改主循环标志
还是有必要看一下主消息循环中的DXUTRender3DEnvironment
① 检查设备是否丢失
② 在窗口模式下检查桌面分辨率位深设定,以便重设设备
③ 尝试重设设备DXUTReset3DEnvironment
④ 判断上次渲染到现在时间(elapsed time)决定是否要进行渲染
⑤ 调用用户的FrameMove函数
⑥ 调用用户的FrameRender函数
⑦ 调用Present函数
⑧ 更新当前Frame
⑨ 根据命令行检查是否需要关闭应用程序
主函数看完之后,剩下的就是一些回调函数了。要正确使用这些回调函数,除了知道它们的作用之外,还需要知道这些函数是何时被调用的。下面是调用顺序
下面是各函数的作用:
以上函数均可以更换名字,这里只是用框架默认的函数名字。
|
D3D9以一种比较易于理解的方式让程序员来组织游戏画面,这种方式就是顶点缓冲。程序员可以自己定义一组记录多 边形定点颜色,纹理位置等的数组,让D3D9去自动生成多边形内部每个像素的信息。为了和以后的vertex shader相区别,我们现在谈论的都是固定功能的顶点处理( Fixed-Function )。
上面一段话似乎有点晦涩,让我们首先来看看这些术语的定义吧:
首先我们来学习一下Vertex Buffer的技术。我喜欢先看代码再讲理论,否则看了理论也找不到北。
下面就是一个自定义的顶点缓冲的结构
struct
CUSTOMVERTEX
{
FLOAT
x,
y, z, rhw; // The transformed position for the vertex
DWORD
color;
// The vertex color
};
其中,x, y, z是顶点在3维空间的最终位置,rhw是3维矩阵的倒数(不明白的话找本图形学的书研究一下),color自然就是顶点的颜色了。由于这个结构是自定义的,所以我们需要告诉D3D应该如何识别这个结构,这就需要我们定义一个常量了:
// Our custom FVF, which describes our custom vertex structure
#define
D3DFVF_CUSTOMVERTEX
(
D3DFVF_XYZRHW
|D3DFVF_DIFFUSE)
这个D3DFVF_CUSTOMVERTEX就是用来告诉D3D上面那个CUSTOMVERTEX结构是如何组织的:首先是 D3DFVF_XYZRHW,即顶点在3维坐标系的最终位置,D3DFVF_DIFFUSE是色彩模式,告诉D3D紧接着D3DFVF_XYZRHW信息 的是顶点的色彩信息,并且这种色彩信息是漫反射模式的。类似的标志可以学习DirectX SDK文档(搜索D3DFVF),这里就不再赘述。
上面所提到的顶点缓冲定义方式就是为大家熟知Flexible Vertex Format,简称FVF,是不是有点眼熟呢。
下面是Direct3D游戏编程入门一书中对复杂的FVF举的一个例子,供参考
typedef
struct
SObjVertex
{
FLOAT
x,
y, z; // position
FLOAT
nx,
ny, nz; // normal
DWORD
diffuse;
// diffuse color
DWORD
specular;
// specular color
FLOAT
tu,
tv; // first pair of texture coordinates
FLOAT
tu2,
tv2, tw2; // second pair of texture coordinates
FLOAT
tu3,
tv3; // third pair of texture coordinates
FLOAT
tu4,
tv4; // fourth pair of texture coordinates
}
SObjVertex;
const
DWORD
gSObjVertexFVF = (
D3DFVF_XYZ |
D3DFVF_DIFFUSE |
D3DFVF_SPECULAR |
D3DFVF_NORMAL |
D3DFVF_TEX4 |
D3DFVF_TEXCOORDSIZE2(0) |
D3DFVF_TEXCOORDSIZE3(1) |
D3DFVF_TEXCOORDSIZE2(2) | D3DFVF_TEXCOORDSIZE2(3) );
看完了FVF顶点格式的定义,就可以看它是如何使用的。使用方法非常简单,在OnCreateDevice中添加创建和初始化代码,在OnFrameRender中添加渲染代码就可以了,具体如下:
LPDIRECT3DVERTEXBUFFER9
g_pVB
=
NULL;
// Buffer to hold vertices
HRESULT
CALLBACK
OnCreateDevice(
IDirect3DDevice9* pd3dDevice, constD3DSURFACE_DESC* pBackBufferSurfaceDesc, void* pUserContext )
{
…
// Initialize three vertices for rendering a triangle
CUSTOMVERTEX
vertices[] =
{
// x, y, z, rhw, color
{ 150.0f, 50.0f, 0.5f, 1.0f, 0xffff0000, },
{ 250.0f, 250.0f, 0.5f, 1.0f, 0xff00ff00, },
{ 50.0f, 250.0f, 0.5f, 1.0f, 0xff00ffff, },
};
if(
FAILED( pd3dDevice->CreateVertexBuffer(
3*
sizeof(CUSTOMVERTEX),
0,
D3DFVF_CUSTOMVERTEX,
D3DPOOL_MANAGED, &
g_pVB, NULL ) ) )
{
return
E_FAIL;
}
// Now we fill the vertex buffer. To do this, we need to Lock()
//
the VB to
gain access to the vertices. This mechanism is
//
required becuase vertex
buffers may be in device memory.
VOID*
pVertices;
if(
FAILED( g_pVB->Lock( 0, sizeof(vertices),
(
void**)&pVertices, 0 ) ) )
return
E_FAIL;
memcpy(
pVertices, vertices, sizeof(vertices) );
g_pVB->
Unlock();
}
为了统一和强调精度,D3D采用了float作为其主要的数值类型。上面的程序调用了CreateVertexBuffer和 LPDIRECT3DVERTEXBUFFER9-> Lock和 Unlock函数。另外需要在OnFrameRender中的BeginScene和EndScene中调用如下代码
pd3dDevice->
SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX) );
pd3dDevice->
SetFVF( D3DFVF_CUSTOMVERTEX );
pd3dDevice->
DrawPrimitive( D3DPT_TRIANGLELIST, 0, 1 );
其中
SetStreamSource
函数第一个参数
StreamNumber
用来设定使用哪条数据流,这个数据流的最大值根据显卡硬件的不同而不同,现在好的显卡可以支持
8
条或者
16
条数据流。
SetFVF
函数用来把我们自定义的顶点缓冲布局常量传递给
D3D
。
DrawPrimitive
函数用来告诉
D3D
以那种图元绘制这个图形。其中图元这个概念很重要,它是
D3D
世界中最基本的单位,分为点列表,线列表,线带,三角形列表,三角形带,三角扇形六中,在
D3D
中的具体定义如下:
typedef enum _D3DPRIMITIVETYPE
{
D3DPT_POINTLIST = 1,
D3DPT_LINELIST = 2,
D3DPT_LINESTRIP = 3,
D3DPT_TRIANGLELIST = 4,
D3DPT_TRIANGLESTRIP = 5,
D3DPT_TRIANGLEFAN = 6,
D3DPT_FORCE_DWORD = 0x7fffffff,
} D3DPRIMITIVETYPE;
其中点列表,线列表,三角形列表很好理解,它们就是独立的点/线/三角形的集合,每个顶点缓冲中的点分别表示一个点/独立线条中的一个端点/独立三角形 中的一个顶点。而线带,三角形带每个顶点缓冲中的点表示连续的线条的端点/连续三角形中的顶点。三角扇形顶点缓冲中的点表示三角扇形中的每个顶点,如图:
三角形列表 三角形带
三角扇形 线列表
点列表 线带
在特殊情况下三角形带和三角扇形都可以组成四边形。只要将
DrawPrimitive
中的参数变换一下,适当改变顶点缓冲就可以得到
D3D
中以不同图元画出的图形。下图是三角形列表形式画出的一个三角形。
不要忘了更改
CreateVertexBuffer
中的第一个表示顶点缓冲大小的参数,否则添加的顶点会被忽略的。最后在
OnDestroyDevice
中调用
SAFE_RELEASE( g_pVB )
来释放资源。
|
3.
索引缓冲
索引缓冲Index Buffer是由用户定义的,用来告诉D3D渲染顶点顺序的WORD或者DWORD数组。
索引缓冲离不开顶点缓冲,但是顶点缓冲却不一定需要索引缓冲。创建索引缓冲的过程和创建顶点缓冲类似,首先是声明,然后在OnCreateDevice中初始化和创建,在OnFrameRender中渲染,在OnDestroyDevice中销毁。相关代码如下:
声明:
LPDIRECT3DINDEXBUFFER9
g_pIB
=
NULL;
// Buffer to hold indeces
OnCreateDevice:
HRESULT
hr
;
V_RETURN
( g_DialogResourceManager.OnCreateDevice( pd3dDevice ) );
V_RETURN
( g_SettingsDlg.OnCreateDevice( pd3dDevice ) );
// Initialize the font
V_RETURN
( D3DXCreateFont( pd3dDevice, 15, 0, FW_BOLD, 1, FALSE, DEFAULT_CHARSET,
OUT_DEFAULT_PRECIS
, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
L
"Arial", &g_pFont ) );
// Initialize vertices for rendering a triangle
CUSTOMVERTEX
vertices
[] =
{
{ 0.0f, 0.0f, 0.5f, 1.0f, 0xffff0000, },
// x, y, z, rhw, color
{ (
float
)pBackBufferSurfaceDesc->Width, 0.0f, 0.5f, 1.0f, 0xff00ff00, },
{ (
float
)pBackBufferSurfaceDesc->Width, (float)pBackBufferSurfaceDesc->Height, 0.5f, 1.0f, 0xffff00ff, },
{ 0.0f, (
float
)pBackBufferSurfaceDesc->Height, 0.5f, 1.0f, 0xff0000ff, },
};
// Create the vertex buffer
V_RETURN
( pd3dDevice->CreateVertexBuffer( sizeof(vertices),
0,
D3DFVF_CUSTOMVERTEX
,
D3DPOOL_MANAGED
, &g_pVB, NULL ) );
VOID
* pVertices;
V_RETURN
( g_pVB->Lock( 0, sizeof(vertices), (void**)&pVertices, 0 ) );
memcpy
( pVertices, vertices, sizeof(vertices) );
g_pVB
->Unlock();
// Initialize
index buffer for rendering
WORD
wIndeces
[] = {0,1,2,0,2,3};
// Create the index buffer
V_RETURN
( pd3dDevice->CreateIndexBuffer( sizeof(wIndeces),
0,
D3DFMT_INDEX16
,
D3DPOOL_MANAGED
, &g_pIB, NULL) );
VOID
* pIndeces;
V_RETURN
( g_pIB->Lock( 0, sizeof(wIndeces), &pIndeces, 0) );
memcpy
( pIndeces, wIndeces, sizeof(wIndeces) );
g_pIB
->Unlock();
return
S_OK
;
OnRender中BeginScene和EndScene之间
pd3dDevice->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX) );
pd3dDevice->SetFVF( D3DFVF_CUSTOMVERTEX );
pd3dDevice->SetIndices( g_pIB );
//pd3dDevice->DrawPrimitive( D3DPT_TRIANGLEFAN, 0, 2 );
pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, 4, 0, 2 );
OnDestroyDevice
SAFE_RELEASE( g_pVB );
SAFE_RELEASE( g_pIB );
下面是这段代码的效果图:
挺PP的吧?
这段代码和顶点缓冲最重要的不同在于DrawIndexedPrimitive函数。这个函数的使用方法请看下面的例子(整理自SDK文档Rendering from Vertex and Index Buffers):
Scenario 1:画两个没有索引缓冲的三角形(为了方便现在仅列出三维坐标的x,y值,下同)
左图上的正方形由两个三角形组成,从右图可以看出顶点缓冲程序采用了TRIANGLELIST的图元方式分别画出了两个三角形,代码如下:
DrawPrimitive( D3DPT_TRIANGLELIST, // PrimitiveType
0, // StartVertex
2 ); // PrimitiveCount
Scenario 2:用索引缓冲画两个三角形
右边图的是顶点缓冲,和场景1相比少了两个重复定义的顶点,左边的数组就是索引缓冲,方框中的数字就是右边顶点缓冲的编号,其中顶点0, 1, 2构成一个独立的三角形,顶点3, 0, 2独立构成另一个独立的三角形,代码如下:
DrawIndexPrimitive( D3DPT_TRIANGLELIST, // PrimitiveType
0, // BaseVertexIndex
0, // MinIndex
4, // NumVertices
0, // StartIndex
2 ); // PrimitiveCount
函数由DrawPrimitive换成了DrawIndexPrimitive,图元类型仍然为TRIANGLELIST。图元数仍然为2。
Scenario 3:使用索引渲染一个三角形
这次需求变更,但是仍然想用上面的索引缓冲和顶点缓冲怎么办呢?具体来说,我们只想要画由顶点0, 2, 3构成的三角形。研究DrawIndexPrimitive之后发现只要这样调用就可以了:
DrawIndexPrimitive( D3DPT_TRIANGLELIST, // PrimitiveType
0, // BaseVertexIndex
0, // MinIndex
4, // NumVertices
3, // StartIndex
1 ); // PrimitiveCount
StartIndex为3,就是说从第三个元素开始使用索引缓冲,上图中第三个元素对应的正好是顶点3。图元数为1,因为我们只需要画一个三角形。顶点个数仍然为4。
Scenario 4:使用索引缓冲的Offset渲染一个三角形
假如一个顶点缓冲特别大,顶点的标号已经达到50, 51, 52, 53这样的数字,我们可以通过填写Offset的方法来调用DrawIndexPrimitive。上图的情况只要将Offset这个参数设为50,就可 以达到不变更上几例中的索引缓冲内容的目的。这种情况下,中间一幅图描述的索引信息就等同于左边的索引信息,而下面函数渲染出的图形就是由顶点53, 50, 52构成的三角形。
DrawIndexPrimitive( D3DPT_TRIANGLELIST, // PrimitiveType
50, // BaseVertexIndex
0, // MinIndex
4, // NumVertices
3, // StartIndex
1 ); // PrimitiveCount
|
纹理在D3D中是一个非常重要的概念,它的出现改变了以前3D物体表面不真实的状况,为程序员提供了将2维图像应用到3维物体上去的功能。请做 好心理准备,因为新的概念将像暴风骤雨般的袭来,而在没有弄懂基本概念之前是不可能进行下一步的学习的。下面是一些术语的大致概念:
包装纹理寻址模式:D3D默认寻址模式。效果如下: 镜像纹理寻址模式:每两个相邻的单位纹理都是镜像效果的。如下: 夹持纹理寻址模式:只映射在区间[0, 1]中的纹理,然后在其他空间中涂上和纹理边界相反的颜色,如图: 边框颜色纹理寻址模式:在区间[0, 1]之外涂上指定颜色,例如: 一次镜像纹理寻址模式:纹理在[-1.0, 1.0]范围内作镜像,在该范围外作夹持。
mipmap:由一系列纹理组成,其中每张纹理的高宽都是前一级高宽的一半。D3D在渲染时会自动挑选出一个图素与像素的比值最接近于1的mip层级。 最近点采样:将纹理坐标对齐到最接近的整数,再将那个位于整数坐标上的纹理像素作为最终的颜色。缺点:容易在图像边界上造成错误,优点:快。 线性纹理过滤:(即双线纹理过滤),计算相对于采样点最近的4各图素(上下左右4个点)的平均值。缺点:有各项异性失真可能。 三线过滤:对于每个像素,三线过滤会先选择两张最接近的mipmap,将它们双线过滤为两张理想大小的mipmap,然后根据理想的mip级组合这两张过滤后的mipmap中的对应像素。缺点:有各项异性失真可能。 各项异性过滤:根据屏幕像素的伸张度来测量各项异性,再将屏幕像素反向映射到纹理空间中。效果:在非水平的渲染时要比三线过滤更加锐化。
FinalColor = SourceColor * SourceBlendFactor + DestColor * DestBlendFactor 如果使SourceBlendFactor + DestBlendFactor = 1,那么就可以实现透明效果。 Example 1 Simple Texture & Wrap Texture Address Mode
一个简单的贴图是很容易实现的,因为它可以简单的建立FVF基础之上,整个过程是这样的:在顶点缓冲之中添加纹理坐标信息,然后在 OnCreateDevice中添加纹理创建代码,在OnFrameRender中添加纹理的设置,最后在OnDestoryDevice中添加纹理资源 释放代码。
定义:在顶点缓冲定义中添加纹理坐标信息
LPDIRECT3DTEXTURE9
g_pTexture
=
NULL
; // Text
u
re
struct
CUSTOMVERTEX
{
FLOAT
x
, y, z, rhw; // The transformed position for the vertex
//DWORD color; // The vertex color, no use here
FLOAT
tu
, tv; // The text
u
re coordinates
};
#define
D3DFVF_CUSTOMVERTEX
(
D3DFVF_XYZRHW
|D3DFVF_TEX1)
OnCreateDevice:添加纹理创建代码,在顶点缓冲中添加纹理坐标信息,横向复制两遍纹理
V_RETURN
( D3DXCreateTextureFromFile(pd3dDevice,
L
"
Da
Ning_Hudie.jpg", &g_pTexture) );
CUSTOMVERTEX
vertices
[] =
{
{ 0.0f, 0.0f, 0.5f, 1.0f, 0.0f, 0.0f,},
// x, y, z, rhw, color
{
(
float
)pBackBufferSurfaceDesc->Width, 0.0f, 0.5f, 1.0f, 2.0f, 0.0f,},
{ (
float
)pBackBufferSurfaceDesc->Width, (float)pBackBufferSurfaceDesc->Height, 0.5f, 1.0f, 2.0f, 1.0f,},
{ 0.0f, (
float
)pBackBufferSurfaceDesc->Height, 0.5f, 1.0f, 0.0f, 1.0f, },
};
OnFrameRender:在BeginScene和EndScene中间添加如下代码。因为D3D默认使用包装纹理寻址模式,所以不必调用SetSampleState来设置纹理模式。
pd3dDevice
->SetTexture( 0, g_pTexture );
OnDestroyDevice:添加释放代码
SAFE_RELEASE
( g_pTexture);
Example 2
Texture Address Mode
包装寻址模式见Example 1。接下来是镜像寻址模式。
顶点缓冲的初始化:横向复制四遍,纵向复制两遍,一共是八个纹理单元
CUSTOMVERTEX
vertices
[] =
{
{ 0.0f, 0.0f, 0.5f, 1.0f, 0.0f, 0.0f,},
// x, y, z, rhw, color
{
(
float
)pBackBufferSurfaceDesc->Width, 0.0f, 0.5f, 1.0f, 4.0f, 0.0f,},
{ (
float
)pBackBufferSurfaceDesc->Width, (float)pBackBufferSurfaceDesc->Height, 0.5f, 1.0f, 4.0f, 2.0f,},
{ 0.0f, (
float
)pBackBufferSurfaceDesc->Height, 0.5f, 1.0f, 0.0f, 2.0f, },
};
OnFrameRender:添加在u方向和v方向的镜像纹理寻址模式代码
pd3dDevice
->SetTexture( 0, g_pTexture );
pd3dDevice->SetSamplerState( 0, D3DSAMP_ADDRESSU, D3DTADDRESS_MIRROR );
pd3dDevice->SetSamplerState( 0, D3DSAMP_ADDRESSV, D3DTADDRESS_MIRROR );
其余函数和Example 1相同。效果如下:
类似的夹持纹理寻址模式效果图
边框颜色纹理寻址模式需要再加一个SetSampleState
pd3dDevice->SetSamplerState( 0, D3DSAMP_ADDRESSU, D3DTADDRESS_BORDER );
pd3dDevice->SetSamplerState( 0, D3DSAMP_ADDRESSV, D3DTADDRESS_BORDER );
pd3dDevice->SetSamplerState( 0, D3DSAMP_BORDERCOLOR, 0xff88aa66 );
Example 3
Texture Wrapping
v方向上的纹理包装,在OnFrameRender添加
pd3dDevice
->SetRenderState( D3DRS_WRAP0, D3DWRAP_V );
其余函数和Example 2类似
u方向上的代码类似,效果图略
|
D3D中世界的运动是通过矩阵变化完成的。这里不打算讲数学知识,相关问题请参考计算机图形学书籍。
在D3D中矩阵 变换分为三种:世界变换,观察变换和投影变换。世界变换描述了物体本身的缩放,旋转和平移,也就是物体本身的运动;观察变换描述了一个观察者在场景中的位 置和朝向;投影变换描述了观察者可以看到内容的范围,类似这个范围类似于削去顶部的金字塔形状,该变换将透视法应用到一个3D场景中。
矩阵在D3D中是按语言分开定义的,如果用的是C语言,那么只能利用D3DMatrix;如果用的是C++,那么还可以用D3DXMatrix。它们的 差别就是后者利用了C++中的操作符重载的功能,例如矩阵乘法。这些矩阵都是4*4的,这样方便进行各种转换。另外还有一个D3DXMatrix16,这 是经过优化的版本,是以16位对齐的。D3DXMatrix定义如下:
typedef struct D3DXMATRIX {
FLOAT _11, FLOAT _12, FLOAT _13, FLOAT _14,
FLOAT _21, FLOAT _22, FLOAT _23, FLOAT _24,
FLOAT _31, FLOAT _32, FLOAT _33, FLOAT _34,
FLOAT _41, FLOAT _42, FLOAT _43, FLOAT _44 );
} D3DXMATRIX;
一个单位矩阵可以这样定义:
D3DXMatrix* m;
D3DMatrixIdentity(&m);
在世界变换中,有以下函数用于进行物体位置的变换:
向量的归一:
因为D3D采用浮点数表示矩阵,在进行多次平移旋转缩放后可能会丢失一定的精度,从而造成原来垂直的向量不垂直。为了解决这个问题,D3D采用了归一(Normalize)叉积的方法。具体代码如下:
D3DXVec3Normalize( &vLook, &vLook ); // 以Look为标准
D3DXVec3Cross( &vRight, &vUp, &vLook ); // 求Up和Look叉积来设定Right
D3DXVec3Normalize( &vRight, &vRight );
D3DXVec3Cross ( &vUp, &vLook, &vRight); // 求Look和Right叉积来设定Up
D3DXVec3Normalize( &vUp, &vUp );
归一就是把向量单位化,计算公式为||A|| = sqrt(x*x + y*y + z*z)。
四元数:
四元数就是一个向量加上一次旋转。四元数将三维空间的旋转概念扩展到四维空间,这对于表示和处理3D中点的旋转很有用。四元数可以用于实现:
n 骨骼动画 ( skeletal animation )
n 反向动力学动画 ( inverse cinematics )
n 3D物理学
我们可以在一个游戏中将所有的矩阵替换为四元数,因为这样不仅节省内存空间,同时也降低了计算成本。并且当需要一个矩阵进行操作时,随时可以将四元数转换成矩阵。
四元数包括一个标量w和一个向量v。其中向量应为单位向量w,标量为绕v轴旋转的数量。通常表示为(x, y, z, w)。它的物理意义是这样的:考虑以角度θ绕轴A(x , y, z)旋转的所有旋转矩阵,四元数Q将是Q=( x sin(θ/2), y sin(θ/2), z sin(θ/2), cos(θ/2) )。因此由一个四元数可以很容易求出旋转角θ= 2 arc cos w,那么旋转轴A也就很容易求出了。
关于四元数的数学意义以及和矩阵的具体转换方法参见游戏编程精粹1中的2.7和2.8章。 在D3D中四元数的典型应用是在一个偏航,俯仰,横滚系统中( yaw, pitch, roll )。如果由键盘控制一架飞机的偏航,俯仰以及横滚,我们就可以用四元数计算取代矩阵计算。从用户的键盘中得到飞机的偏航角度为yaw,俯仰角度为 pitch,横滚角度为roll,那么便可以通过D3DXQuaternionRotationYawPitchRoll( D3DQUATERNION * pOut, FLOAT yaw, FLOAT pitch, FLOAT roll)来获得相应的四元数pOut。然后用四元数的乘法操作取代矩阵的乘法操作。四元数乘法操作Q=q1*q2的意义是绕轴2旋转某角度,然后再绕轴 1旋转某角度。将最终得到的四元数结果应用于D3DXMatrixRotationQuaternion( D3DXMatrix *pOut, CONST D3DXQUATERNION *pQ)就能得到最终矩阵pOut了。另外常用的四元数操作函数还有四元数的归一D3DXQUATERNIONNormalize,四元数乘法 D3DXQUATERNIONMultiply。
观察变换
观察变换通常用于描述一个观察者在场景中的位置和朝向。可以用照相机看场景的例子来描述这种变换。观察变换可以通过一个观察变换矩阵来表示。世界矩阵以 行的次序存储向量的朝向,而观察矩阵则以列的次序存储。D3D中提供了一种比较简单的观察矩阵获取方式,即调用D3DXMatrixLookAtLH。这 个函数必须要传进去三个向量,它们分别定义了照相机所在点eye,照相机拍摄物体位置at,和上方向量up。返回的矩阵第一列前三个值存储了up向量,第 二列的前3个值存储了Right向量,第三列的前3个值存储了Look向量。D3DXMatrixLookAtLH适合用于跟随式照相机,对于太空射击游 戏或者飞行模拟器就不适合了。解决办法有两个,一是将照相机绕向量旋转,二是使用四元数将照相机绕任意轴旋转。
Example 1
A plain’s Simple Transform
将平面做成一个对象,以简化主回调函数的逻辑
#include
"dxstdafx.h"
// Plain vertex struct
struct
PlainVertex
{
FLOAT
x
, y, z;
FLOAT
tu
, tv;
};
// replace D3DFVF_XYZRHW with D3DFVF_XYZ
#define
PLAIN_FVF
(
D3DFVF_XYZ
|
D3DFVF_TEX1
)
// Object PlainVertex wrap class
class
Plain
{
public
:
Plain
(IDirect3DDevice9* device);
~
Plain
();
HRESULT
CreatePlain
();
bool
OnFrameMove
( IDirect3DDevice9* pd3dDevice, double fTime );
bool
OnFrameRender
( D3DMATERIAL9* mtrl, IDirect3DTexture9* tex);
public
:
IDirect3DDevice9
* m_device;
IDirect3DVertexBuffer9
* m_vb;
IDirect3DIndexBuffer9
* m_ib;
};
// Constructor
Plain
::Plain(IDirect3DDevice9* device)
{
m_device
=
device
;
m_vb
=
NULL
;
m_ib
=
NULL
;
}
// Create the plain first when reset device
inline
HRESULT
Plain
::CreatePlain()
{
HRESULT
hr
;
PlainVertex
plainVertex
[] =
{
{ -1.0f, -1.0f, 0.0f, 1.0f, 1.0f },
// x, y, z, tu, tv : left bottom
{ 1.0f, -1.0f, 0.0f, 0.0f, 1.0f },
// right bottom
{ 1.0f, 1.0f, 0.0f, 0.0f, 0.0f },
// right up
{ -1.0f, 1.0f, 0.0f, 1.0f, 0.0f },
// left up
};
V_RETURN
(m_device->CreateVertexBuffer(
sizeof
(plainVertex),
0,
PLAIN_FVF
,
D3DPOOL_MANAGED
,
&
m_vb
,
NULL
));
PlainVertex
* v;
V_RETURN
(m_vb->Lock(0, 0, (void**)&v, 0));
memcpy
( v, plainVertex, sizeof(plainVertex) );
m_vb
->Unlock();
WORD
wIndeces
[] = {3,2,0,2,1,0};
V_RETURN
( m_device->CreateIndexBuffer( sizeof(wIndeces),
0,
D3DFMT_INDEX16
,
D3DPOOL_MANAGED
, &m_ib, NULL) );
VOID
* pIndeces;
V_RETURN
( m_ib->Lock( 0, sizeof(wIndeces), &pIndeces, 0) );
memcpy
( pIndeces, wIndeces, sizeof(wIndeces) );
m_ib
->Unlock();
return
S_OK
;
}
// Things to do when frame move for this plain
inline
bool
Plain
::OnFrameMove( IDirect3DDevice9* pd3dDevice, double fTime )
{
D3DXMATRIX
matRotY
;
D3DXMatrixRotationY
( &matRotY, (float)fTime * 2.0f );
D3DXMATRIX
matTrans
;
D3DXMatrixTranslation
( &matTrans, 0.0f, 0.0f, 0.0f );
D3DXMATRIX
matResult
;
matResult
=
matRotY
*
matTrans
;
pd3dDevice
->SetTransform( D3DTS_WORLD, &matResult );
return
true
;
}
// Things to do when frame render for this plain
inline
bool
Plain
::OnFrameRender(D3DMATERIAL9* mtrl, IDirect3DTexture9* tex)
{
if
( mtrl )
m_device
->SetMaterial(mtrl);
if
( tex )
m_device
->SetTexture(0, tex);
m_device
->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
m_device
->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
m_device
->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
m_device
->SetStreamSource(0, m_vb, 0, sizeof(PlainVertex));
m_device
->SetIndices(m_ib);
m_device
->SetFVF(PLAIN_FVF);
m_device
->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 4,
0,
2);
return
true
;
}
// destructor, must invoked when lost device
Plain
::~Plain()
{
SAFE_RELEASE
(m_vb);
SAFE_RELEASE
(m_ib);
}
接下来的事情就是在主回调函数中调用上面函数了
//
Declaration
Plain*
g_pPlain = NULL;
// codes in OnCreateDevice
// Turn off culling
pd3dDevice
->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );
// Turn off D3D lighting
pd3dDevice
->SetRenderState( D3DRS_LIGHTING, FALSE );
// Turn on the zbuffer
pd3dDevice
->SetRenderState( D3DRS_ZENABLE, TRUE );
// Codes in OnResetDevice
g_pPlain
=
new
Plain
(pd3dDevice);
V_RETURN
(
g_pPlain
->CreatePlain() );
// Codes in OnFrameMove
.
// invoke plain’s OnFrameMove fisrt to set the world matrics
g_pPlain->OnFrameMove( pd3dDevice, fTime );
// Set up our view matrix. A view matrix can be defined given an eye point,
// a point to lookat, and a direction for which way is up. Here, we set the
// eye five units back along the z-axis and up three units, look at the
// origin, and define "up" to be in the y-direction.
D3DXVECTOR3
vEyePt
( 0.0f, 0.0f, 4.0f );
D3DXVECTOR3
vLookatPt
( 0.0f, 0.0f, 0.0f );
D3DXVECTOR3
vUpVec
( 0.0f, 1.0f, 0.0f );
D3DXMATRIXA16
matView
;
D3DXMatrixLookAtLH
( &matView, &vEyePt, &vLookatPt, &vUpVec );
pd3dDevice
->SetTransform( D3DTS_VIEW, &matView );
// For the projection matrix, we set up a perspective transform (which
// transforms geometry from 3D view space to 2D viewport space, with
// a perspective divide making objects smaller in the distance). To build
// a perpsective transform, we need the field of view (1/4 pi is common),
// the aspect ratio, and the near and far clipping planes (which define at
// what distances geometry should be no longer be rendered).
D3DXMATRIXA16
matProj
;
D3DXMatrixPerspectiveFovLH
( &matProj, D3DX_PI/4, 1.0f, 1.0f, 100.0f );
pd3dDevice
->SetTransform( D3DTS_PROJECTION, &matProj );
// Codes in OnFrameRender
g_pPlain->OnFrameRender(0,g_pTexture);
// Codes in OnLostDevice
g_pPlain->~Plain();
从上面的代码可以看出,这个程序是将一个平面绕Y轴旋转。由于定义的定点是在[-1, 1]区间的,所以动画显示出来的效果就是绕平面中心竖线选转的。这里用到了前几章的顶点缓冲和索引缓冲,然后在平面上做了纹理贴图,利用了mipmap并 进行了双线过滤(见Plain::OnFrameRender)。需要注意的一点是D3D中默认打开了背面剔除(culling)和光照效果 (lighting),所以我们必须手动调用SetRenderState手动关闭它们(OnCreateDevice),否则动画将只现实是黑乎乎的旋 转平面正面。关闭后效果如下:
|