概要
这个指南涵盖了更多DXUT的高级应用. 这个指南里的大部分功能是可选的, 为了以最小的代价来增强你的应用程序. DXUT提供了一个简单的基于GUI系统的精灵和一个设备设置对话框. 另外, 它还提供了一些摄像机类.
这个指南的结果是一个具有完善功能的GUI 用于更改设备和场景的设置. 它将有按钮, 滑块, 和文本来示范这些功能.
DXUT 摄像机
DXUT中的CModelViewerCamera 类可以简单的的管理视图变换和透视变换, 就像GUI 的功能一样.
CModelViewerCamera g_Camera; // A model viewing camera
摄像机类提供的第一个功能是创建视图和透视矩阵. 有了这个摄像机,没有必要担心这些矩阵. 反而, 你可以指定你在哪, 你看着什么,还有窗口的大小. 然后, 把这些参数传递给摄像机对象, 它会在后台创建这些矩阵.
这里我们设置摄像机的一部分视图参数. 我们指定我们在哪,和我们看着什么.
// Initialize the camera
D3DXVECTOR3 Eye( 0.0f, 0.0f, -800.0f );
D3DXVECTOR3 At( 0.0f, 0.0f, 0.0f );
g_Camera.SetViewParams( &Eye, &At );
接下来,我们指定摄像机的投影参数. 也就是说, 我们需要提供观察角度,纵横比, 和视图截锥的近、远裁剪面. 这些信息跟之前的指南提供的一样, 不同的是, 你不必担心怎么去创建矩阵.
// Setup the camera's projection parameters
float fAspectRatio = pBackBufferSurfaceDesc->Width / (FLOAT)pBackBufferSurfaceDesc->Height;
g_Camera.SetProjParams( D3DX_PI/4, fAspectRatio, 0.1f, 5000.0f );
g_Camera.SetWindow( pBackBufferSurfaceDesc->Width, p00BackBufferSurfaceDesc->Height );
另外, 这个摄像机还封装了简单的鼠标反馈. 在这里, 我们指定三个鼠标按键给提供的鼠标操作: 模型旋转, 放缩, 还有摄像机旋转.试着编译工程并用每个按键体会一下各个操作.
g_Camera.SetButtonMasks( MOUSE_LEFT_BUTTON, MOUSE_WHEEL, MOUSE_MIDDLE_BUTTON );
在这个按钮组的基础上, 摄像机会监听这些输入并产生相应效果. 为了响应用户输入, 需要加入一个监听到MsgProc回调函数(DXUT消息处理函数).
// Pass all remaining windows messages to camera so it can respond to user input
g_Camera.HandleMessages( hWnd, uMsg, wParam, lParam );
最后,在把所有数据都输入摄像机后, 就是取出真正的变换矩阵的时候了. 这里我们用相关函数获取投影矩阵和视图矩阵. 摄像机对象会自己计算这些矩阵.
g_pProjectionVariable->SetMatrix( (float*)g_Camera.GetProjMatrix() );
g_pViewVariable->SetMatrix( (float*)g_Camera.GetViewMatrix() );
DXUT 对话框
用户交互可以用CDXUTDialog来实现, 它在一个包含控件的对话框里接受用户输入,并且通过程序句柄传递它们. 首先, 对话框类要实例化, 然后每个单独的控件才能加入.
在这个指南里, 有两个对话框会被加入, 一个叫 g_HUD跟D3D10示例共享相同的代码, 另一个叫 g_SampleUI 用于显示函数细节给这个指南. 第二个对话框用来控制模型的"胖瘦",就是设置一个变量传递给shaders.
CDXUTDialog g_HUD; // manages the 3D UI
CDXUTDialog g_SampleUI; // dialog for sample specific controls
这些对话框被CDXUTDialogResourceManager控制着.这个管理程序会在对话框之间传递消息并处理共享资源.
CDXUTDialogResourceManager g_DialogResourceManager; // manager for shared resources of dialogs
最后, 一些回调函数与GUI处理的实际消息相关联. 这个函数用于处理控件间的交互.
void CALLBACK OnGUIEvent( UINT nEvent, int nControlID, CDXUTControl* pControl, void* pUserContext );
对话框初始化
既然已经介绍了许多有用的东西, 那么需要被初始化, 这个指南将这些模块的真正初始化转移到一个单独的函数, 叫 InitApp().
每个对话框的控件被初始化就是在这个函数里. 每个对话框都需要调用它的初始化函数, 并传递给资源管理者来指定把控件供给谁. 同样, 它们设置回调函数来处理GUI 响应. 既然这样, 这个相关的回调函数是 OnGUIEvent.
g_HUD.Init( &g_DialogResourceManager );
g_SampleUI.Init( &g_DialogResourceManager );
g_HUD.SetCallback( OnGUIEvent );
g_SampleUI.SetCallback( OnGUIEvent );
在它们被初始化后, 每个都对话框可以加入它们想要使用的控件. 对话框 HUD加入了3个按钮用于基本功能: 切换全屏, 切换引用 (软件) 渲染器, 和改变设备.要加入一个按钮, 你要指定你想要使用的IDC 标识符, 一个用于显示的字符串, 坐标, 宽和高, 还有可选的与它关联的热键. 热键可以用键盘快速地开关这些按钮.
注意指定的坐标与对话框的定位相关.
int iY = 10;
g_HUD.AddButton( IDC_TOGGLEFULLSCREEN, L"Toggle full screen", 35, iY, 125, 22 );
g_HUD.AddButton( IDC_TOGGLEREF, L"Toggle REF (F3)", 35, iY += 24, 125, 22 );
g_HUD.AddButton( IDC_CHANGEDEVICE, L"Change device (F2)", 35, iY += 24, 125, 22, VK_F2 );
同样对于sample UI, 加入了这些控件,一个静态文本,一个滑块, 还有一个复选按钮.
传递给静态文本的参数是IDC 标识符,字符串,坐标, 和宽高.
滑块的参数是IDC 标识符,字符串,坐标, 和宽高, 还有滑块的最大最小值, 最后是存储结果的变量.
复选按钮包括IDC标识符,一个字符串标签,wf坐标, 宽高,还有存储结果的布尔值.
iY = 10;
WCHAR sz[100];
iY += 24;
StringCchPrintf( sz, 100, L"Puffiness: %0.2f", g_fModelPuffiness );
g_SampleUI.AddStatic( IDC_PUFF_STATIC, sz, 35, iY += 24, 125, 22 );
g_SampleUI.AddSlider( IDC_PUFF_SCALE, 50, iY += 24, 100, 22, 0, 2000, (int)(g_fModelPuffiness*100.0f) );
iY += 24;
g_SampleUI.AddCheckBox( IDC_TOGGLESPIN, L"Toggle Spinning", 35, iY += 24, 125, 22, g_bSpinning );
一旦初始化了, 这个对话框要显示在屏幕上. 这个由 OnD3D10ResizedSwapChain 调用来完成,屏幕坐标每次改变这个交换链会重建 (可能由于窗口大小的变化).
g_HUD.SetLocation( pBackBufferSurfaceDesc->Width-170, 0 );
g_HUD.SetSize( 170, 170 );
g_SampleUI.SetLocation( pBackBufferSurfaceDesc->Width-170, pBackBufferSurfaceDesc->Height-300 );
g_SampleUI.SetSize( 170, 300 );
对话框的最后一步在哪需要鉴别它们自己是 OnD3D10FrameRender 函数. 一个对话框如果你不绘制它并且用户看不到不太好吧?
//
// Render the UI
//
g_HUD.OnRender( fElapsedTime );
g_SampleUI.OnRender( fElapsedTime );
资源管理程序初始化
资源管理程序在每次回调初始化和销毁时需要被初始化. 这是因为在每次设备被重建的时候 GUI需被重建, 或者交换链被重建的时候.
CDXUTDialogResourceManager 类包含符合每个这样的回调的函数, 具有一模一样的名字. 因此, 它仅仅是在合适的地方插入代码来调用它们.
V_RETURN( g_DialogResourceManager.OnD3D10CreateDevice( pd3dDevice ) );
V_RETURN( g_DialogResourceManager.OnD3D10ResizedSwapChain( pd3dDevice, pBackBufferSurfaceDesc ) );
g_DialogResourceManager.OnD3D10ReleasingSwapChain();
g_DialogResourceManager.OnD3D10DestroyDevice();
响应GUI事件
当全部初始化完成后, 我们最后可以开始写代码来处理GUI交互了. 在我们初始化对话框的时候,我们设置这些对话框的回调函数做为OnGUIEvent. 现在我们将要创建 OnGUIEvent 函数, 它会监听并处理GUI 相关事件(被框架调用的).
这是一个简单的对每个IDC标识符包含一个case代码块的函数,它在对话框创建时被监听. 在每个case块中会是处理代码,假设用户以某种方式控制.这里的处理控制的代码跟 Win32 代码很像.
跟HUD 相关的控制实际上调用DXUT内置的函数. 有一个DXUT 函数关系到切换全屏, 绑定引用软件渲染器, 并且更改设备设置(它会调用下面提到的3D设置对话框).
SampleUI 对话框包含自定义的代码操作跟滑块相关联的变量. 它会收集数值, 更新与它相关的文本, 并把数值传递给滑块.
void CALLBACK OnGUIEvent( UINT nEvent, int nControlID, CDXUTControl* pControl, void* pUserContext )
{
switch( nControlID )
{
case IDC_TOGGLEFULLSCREEN: DXUTToggleFullScreen(); break;
case IDC_TOGGLEREF: DXUTToggleREF(); break;
case IDC_CHANGEDEVICE: g_D3DSettingsDlg.SetActive( !g_D3DSettingsDlg.IsActive() ); break;
case IDC_TOGGLESPIN:
{
g_bSpinning = g_SampleUI.GetCheckBox( IDC_TOGGLESPIN )->GetChecked();
break;
}
case IDC_PUFF_SCALE:
{
g_fModelPuffiness = (float) (g_SampleUI.GetSlider( IDC_PUFF_SCALE )->Getvalue() * 0.01f);
WCHAR sz[100];
StringCchPrintf( sz, 100, L"Puffiness: %0.2f", g_fModelPuffiness );
g_SampleUI.GetStatic( IDC_PUFF_STATIC )->SetText( sz );
g_pPuffiness->SetFloat( g_fModelPuffiness );
break;
}
}
}
更新消息处理
既然现在我们有对话框消息和用户交互, 那就会有传递给应用程序的消息需要被解析, 如果适用的话.那些代码会在DXUT提供的 MsgProc 回调函数中被处理. 在之前的指南里, 这段是空白因为没有消息需要被处理. 但是现在,我们要确保发送给资源管理程序和对话框的消息被适当地发送.
不需要专门的消息处理代码; 我们只需要对每个对话框调用MsgProcs来确保消息被处理了.这通过调用每个单独的类的相应的 MsgProc 函数来完成. 应该注意到这个函数提供了一个标记通报给框架,不需要更多的处理, 因此,可以退出.
LRESULT CALLBACK MsgProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, bool* pbNoFurtherProcessing, void* pUserContext )
{
// Always allow dialog resource manager calls to handle global messages
// so GUI state is updated correctly
*pbNoFurtherProcessing = g_DialogResourceManager.MsgProc( hWnd, uMsg, wParam, lParam );
if( *pbNoFurtherProcessing )
return 0;
if( g_D3DSettingsDlg.IsActive() )
{
g_D3DSettingsDlg.MsgProc( hWnd, uMsg, wParam, lParam );
return 0;
}
// Give the dialogs a chance to handle the message first
*pbNoFurtherProcessing = g_HUD.MsgProc( hWnd, uMsg, wParam, lParam );
if( *pbNoFurtherProcessing )
return 0;
*pbNoFurtherProcessing = g_SampleUI.MsgProc( hWnd, uMsg, wParam, lParam );
if( *pbNoFurtherProcessing )
return 0;
if( uMsg == WM_CHAR && wParam == '1' )
DXUTToggleFullScreen();
return 0;
}
3D设置对话框
有一个专门的内置对话框用来控制D3DDevice的设置. DXUT提供的这个对话框就是CD3DSettingsDlg. 它的功能像一个自定义对话框,但是它会提供给用户所有的他们需要修改的设置选项.
CD3DSettingsDlg g_D3DSettingsDlg; // Device settings dialog
初始化就像其它对话框一样, 你可以调用初始化函数. 然而, 每次Direct3D 改变它的交换链或者设备,这个对话框必须被更新.因此, 它必须包含一个调用在OnD3D10CreateDevice 和OnD3D10ResizedSwapChain里, 相对应地命名, 来反映这些改变. 同样地,销毁对象的改变必须被通报, 因而, 需要在 OnD3D10DestroyDevice里调用.
g_D3DSettingsDlg.Init( &g_DialogResourceManager );
V_RETURN( g_D3DSettingsDlg.OnD3D10CreateDevice( pd3dDevice ) );
V_RETURN( g_D3DSettingsDlg.OnD3D10ResizedSwapChain( pd3dDevice, pBackBufferSurfaceDesc ) );
g_D3DSettingsDlg.OnD3D10DestroyDevice();
在渲染的这一边, 因为对话框的出现可以被改变,它通过一个叫 IsActive()的标记来转换. 如果这个标记被设成false, 那么这个面板将不会被渲染.这个面板的改变是受上面说过的HUD对话框处理的. 标记The IDC_CHANGEDEVICE与HUD 控件相关, 就像上面提到的那样.
if( g_D3DSettingsDlg.IsActive() )
{
g_D3DSettingsDlg.MsgProc( hWnd, uMsg, wParam, lParam );
return 0;
}
一旦这些初始步骤都完成了, 你就可以在你的程序中加入这个对话框. 试着编译这个指南并与更改设置的面板进行交互来观察它的效果. 真正的D3DDevice 或交换链的重构在DXUT内部完成.
文本渲染
一个程序不会有趣如果用户不知道干什么. 所以就DXUT包含了一个工具类用于在屏幕上绘制2D 文本用来给用户反馈. 这个类CDXUTD3D10TextHelper允许你在屏幕的任意位置绘制一行行的文本, 并有简单的字符串输入. 开始之前, 我们要实例化这个类.
既然文本渲染与大多数初始化过程独立, 在这个指南里我们让大部分代码在RenderText10里.
CDXUTD3D10TextHelper txtHelper( g_pFont, g_pSprite, 15 );
初始化
我们传入的第一个参数是我们要绘制的字体. 字体的类型是D3DX提供的ID3DXFont. 要初始化这个字体, 你调用
D3DX10CreateFont, 并且你要传入设备, 高度, 宽度, 重量, mip层次 (一般取 1), 斜体, 字符集, 精度, 质量,
倾斜度和家族, 字体名称, 还有你的对象的指针. 虽然它看起来像很多字符, 只有开始的4个和最后的2个真正有意义.
V_RETURN( D3DX10CreateFont( pd3dDevice, 15, 0, FW_BOLD, 1, FALSE, DEFAULT_CHARSET,
OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
L"Arial", &g_pFont ) );
第2个参数需要我们初始化一个ID3DXSprite 类. 要做这个的话, 我们可以调用 D3DX10CreateSprite (notice the trend here?). 这个函数只需要设备和对象的指针做为参数.
// Initialize the sprite
V_RETURN( D3DX10CreateSprite( pd3dDevice, &g_pSprite ) );
当然, 像其它对象一样, 字体和精灵在我们完成时必须被销毁. 这可以用常见的 SAFE_RELEASE 宏完成.
SAFE_RELEASE( g_pFont );
SAFE_RELEASE( g_pSprite );
渲染
在这个示例里包含的文本里有渲染的统计信息. 另外, 还有一个区域显示怎样用鼠标操作模型.
渲染必须在OnD3D10FrameRender里调用, 而且在这个指南里, 它是在帧渲染调用里调用RenderText10来完成的.
第一个区域一直在绘制, 因此, 它最先完成. 第一个文本渲染调用是Begin(). 它通知引擎我要开始输出文本到屏幕. 在那之后, 我们设置好“指针”的位置, 文本的颜色, 我们就准备绘制了.
输出一个字符串文本要调用DrawTextLine来完成. 你传入字符串, 它就会在当前位置输出. 它在文本书写的同时增加指针, 因此, 如果q包含了一个\n, 它会自动移动指针到下一行.
// Output statistics
txtHelper.Begin();
txtHelper.SetInsertionPos( 2, 0 );
txtHelper.SetForegroundColor( D3DXCOLOR( 1.0f, 1.0f, 0.0f, 1.0f ) );
txtHelper.DrawTextLine( DXUTGetFrameStats() );
txtHelper.DrawTextLine( DXUTGetDeviceStats() );
还有另一个方法来输出文本, 就好似通常使用的printf. 你用特殊的字符来格式化字符串, 然后插入变量到字符串里. 这通过DrawFormattedTextLine来实现.
txtHelper.SetForegroundColor( D3DXCOLOR( 1.0f, 1.0f, 1.0f, 1.0f ) );
txtHelper.DrawFormattedTextLine( L"fTime: %0.1f sin(fTime): %0.4f", fTime, sin(fTime) );
既然帮助绘制用一种相似的方式, 所以不需要涵盖那部分代码了. 一定要注意你能在任何时候通过调用SetInsertionPos来重新设置指针的位置.
最后, 当你对文本输出满意后, 调用 End() 来通知引擎你完成了.