自制基于DirectX的渲染框架- Lookof 's DW - 博客园

 

    为了免沾“好高骛远”的臭毛病,我决定弃用现成的ogre引擎,转而自己写一套“定制”的渲染框架(Render Framework)来满足实际需要。我知道这样做会增加很大的工作量和许多枝节的困难,我不得不自行解决SceneManager、LOD、Camera、InputSysTem、Shader等诸多问题。但换个角度看,这恰好意味着,我可以沉浸在这些属于图形学话题的细节之中,玩个痛快。

    另外一个考虑因素在于,为了搭上ogre这班顺风车,我不得不调整自己的步伐,ogre让我的开发进度更快的同时,也限制了我发挥的自由。比如在低于1.7的版本中,我不知道如何给地形加入动态光照和阴影。总之,我认为无论是因为实践还是因为定制的灵活性,打造一款属于自己的渲染框架总归是值得的。我会在这个渲染框架上进行自己的LVSM的实验。


    这个渲染框架(RF)是基于DirectX上的,我觉得D3D比openGl更好用一点。(然而我有预感:不久的未来,我还得返回去和openGL相处一段时间 :-| ) RF的写法参考了ogre的Example-Framework。


    这个RF已经不是白纸计划了,它现在有了一点内容:目前的RF支持Camera, Font, FPS显示,Unbuffered InputSystem。 其中Unbuffered InputSystem参考了OIS在win32下的实现。


    现在就写过的这段RF经历做一点小结。

Framework based on Virtual Function in C++

    框架(Framework)是我印象中最深刻的一个C++多态应用(当然我见的世面也少之又少)。 通常将框架写成基类(based class),在基类中定制好程序的流程,一些实现具体功能的方法留空或是简单地设置(比如CreateScene,RenderScene等),等待子类(derived class)具体实现。子类只需关注重要的环节,重写对应的虚函数即可。

    我在框架中加入了Camera、InputSystem和FPS计算,这个思路模仿了ogre的Example-Framework。所有由该框架派生的子类都有相同的Camera操控方式和FPS的显示。框架从初始化windows窗口开始,到初始化D3D环境,再到对D3D设置相关的渲染状态,然后初始化场景,进入主循环,在主循环中,先处理渲染前逻辑,接下来渲染场景,然后处理渲染后逻辑。所有这一系列步骤都包含在函数go()中。 在子类中,一般根据需要,重写初始化场景函数、渲染前逻辑函数、渲染场景函数和渲染后逻辑函数等即可。

WndProc-Function based on Class

    消息机制是win32程序的一个特征。消息处理函数(wndproc-function)是写作win32程序时必须要写的一个函数。由于框架是一个类,这就面临着如何把“win32的初始化”集成在一个类中的问题。其中把消息处理函数收编入类是一件比较棘手的事情。由于类成员函数内部的参数表里隐含了this指针,在注册窗口类时就会造成类成员函数与WNDPROC的类型不匹配。解决渠道之一是讲成员函数作为静态成员(static),如此便不会自动包含this指针。但新的问题随之而来:static函数不能直接调用同类的非static成员。消息处理函数就是为了当捕获各种消息时作出的对应反应,如果不能访问其他成员,那该函数也失去了存在的意义。

    为了解决这个问题,可以在创建好窗口后立即利用SetWindowLong函数将类实例的指针传给窗口,然后在static的处理函数中利用GetWindowLong函数取出该指针,再然后就可以通过该指针调用非static的处理函数了。下面的代码展示了这一用法。

 

 1 int MyD3DApplication::go()
 2 {
 3     //....
 4     
 5     if(!RegisterClass(&mWndClass))
 6     {
 7         MessageBox(0,"register wndclass failed","error",0);
 8         return false;
 9     }
10 
11     //2.create window
12     mHwnd=CreateWindow(mWCtitle.c_str(),
13         mWintitle.c_str(),
14         WS_OVERLAPPEDWINDOW,
15         0,0,
16         mWindow_width,mWindow_height,
17         0,
18         0,
19         mHinst,
20         0);
21 
22     if(!mHwnd)
23     {
24         MessageBox(0,"createwindow failed","error",0);
25         return false;
26     }
27 
28 
29     ShowWindow(mHwnd,SW_SHOW);
30     UpdateWindow(mHwnd);
31 
32     ::SetWindowLong(mHwnd,GWL_USERDATA, (long)this);
33 
34         //....
35 }
36 
37 
38 
39 LRESULT CALLBACK MyD3DApplication::DefProc(HWND hwnd,UINT msg,WPARAM wparam, LPARAM lparam)
40 {
41 
42     MyD3DApplication* pClass = NULL;
43     pClass = (MyD3DApplication*)::GetWindowLong(hwnd, GWL_USERDATA);
44 
45     switch(msg)
46     {
47           //handle message
48     }
49 }

 

Camera based on DirectX

    Camera的写作我基本照抄了《DirectX 9.0 3D游戏开发编程基础》这本书中的技法。不使用D3DXMatrixLookAtLH方法,而是自己构建View空间的坐标变换矩阵。该矩阵展示如下:

 



    其中,r向量代表相机的右向量(right vector),即局部空间的正x半轴; u向量代表相机的右向量(up vector),即局部空间的正y半轴;f向量代表相机的右向量(front vector),即局部空间的正z半轴;p向量代表相机的位置(position),即局部空间的原点。假设该矩阵存储在名为M的D3DXMATRIX对象中,只要在每一帧调用D3DDevice->SetTransform(D3DTS_VIEW,&M)即可实现view空间坐标变换。


    我们知道r向量和u向量和f向量实际上构成了相机的一个局部坐标系,注意这三个方向向量应该是标准正交的(一个向量集中的向量都彼此正交,且模为1)。这是因为标准正交矩阵具有一个重要的性质:标准正交矩阵的逆矩阵与转置矩阵相等。我们可以利用这个性质,通过简单地将矩阵转置来得到它的逆矩阵。另外一个重要的性质是一个矩阵与它的逆矩阵相乘的结果是一个单位阵。这两个有用的数学性质在求算得到上面这个矩阵时发挥了重要的作用。

 
    该相机可以沿xyz三轴上进行平移,也可以绕xyz三轴进行旋转。因此总共具有6个自由度。需要额外注意一点的是,对于一般的相机应用,在yaw()的时候,应该绕标准空间的y轴旋转相机,而不是绕自己局部坐标系的y轴(即up向量)。否则,旋转时就会感到自己像坐进了飞机中,出现天旋地转的感觉。然而对于其他,如pitch()和roll()时,都应该按照自己局部坐标系的轴进行旋转。


    {zh1}注意一点的是,DX默认使用的是左手坐标系(left-hand coordinate system),openGL默认用的是右手坐标系。 所以在DX中作叉乘运算时也请务必使用左手系。

InputSystem based on DirectX Input

    最初加入InputSystem是为了操控Camera,毕竟Camera需要平移和旋转,而我们需要发出指示来让它这么做。我打算用键盘上WSADQE六个键控制相机的平移,鼠标控制相机的旋转。


    一开始利用的是“消息处理函数”来接受输入消息,然后做出响应。对鼠标的设计需求是,当按下左键时,可以使相机顺着鼠标的移动进行旋转;当鼠标不动时,停止旋转;只要鼠标移动,就一直可以旋转(而不在乎鼠标是否移到了屏幕边界);当松开左键时,解开鼠标与相机的绑定。这也是ogre在demo程序中展现的效果。最初的设计是利用getCursorPos函数与setCursorPos函数对光标坐标进行捕获与控制,达到鼠标永远在窗口内且坐标xx变化的目的(这样一来,当鼠标不停移动时,就可以永远旋转下去)。然而这种设计在WM_MOUSEMOVE消息下是无法响应的,因为setCursorPos函数使程序误以为,鼠标从未移动过,因此就不会发出WM_MOUSEMOVE消息。为了使设计需求不发生变更,只能将该处理方法挪移到消息处理函数之外,专门组织一个函数来处理鼠标输入,并将此函数在渲染环节中进行每帧调用。


    但这样做破坏了输入的整体性(键盘还在消息处理函数中,鼠标却跑到了别处),让代码看上去太不优雅;然而如果将鼠标勉强纳入消息处理中,只能放弃“永远旋转”的设计。这对矛盾让我纠结了很久。{zh1}一拍笨笨的脑袋瓜子:为什么不看看ogre是怎么做的呢?


    ogre利用的是OIS第三方输入系统,这是一个跨平台开源的输入系统。下载到OIS的源码,翻看了在win32下的处理方法,发现了秘密所在:原来它用的是DirectX Input,再结合DirectX SDK的帮助手册来看,{zh1}终于写了一个类似的InputSystem。对键盘的处理使用了非缓冲(unbuffered)模式,对鼠标的处理使用了缓冲(buffered)模式。缓冲相对非缓冲来说,除了在应用级别上侧重不同,在代码写作级别上也不同:缓冲模式可以用比较优雅的方式读取输入信息(“判断”用switch),非缓冲模式则比较笨拙了(“判断”用if)。其本质是由API接口不同造成的:非缓冲模式用GetDeviceState,而缓冲模式用GetDeviceData。缓冲模式的准备工作比非缓冲模式要费事一点。ogre中有MousePressed/Released,MouseMoved, KeyPressed/Released 等重写函数的都是缓冲模式下的应用。


    在初始化DirectX Input的一系列操作时,注意观察返回结果(HRESULT),并不是都能返回DI_OK的,这与SetCooperativeLevel里第二个参数的设定有很大关系。细节就不表了,我也研究得不透,只知道在这上面吃过苦头。有兴趣的请自行查阅网络。


    {zh1}需要说明的一点是,无论是教材还是示例程序,你都能发现建议在处理“输入系统”与“相机的步移”时传一个time_delta进去,让相机的步移与time_delta成正比关系,可以使相机摆脱帧率的影响。我想这是正确的,并且也是必要的(在不同的机子上跑程序时,不至于说速度不同)。不过需要注意到的一点是,只有当处理“键盘按键”时,才传这个time_delta进去。处理“鼠标”时,并不需要这个time_delta; 相反,如果在鼠标处理中传入了time_delta,反而会使相机旋转有停滞、不流畅之感。我查阅了ogre的Example-Framework(见processUnbufferdKeyInput函数与processUnbufferedMouseInput函数),发现它也只是把time_delta传给了键盘,并没有传给鼠标。 对这个见解,你可以亲自试试看。

郑重声明:资讯 【自制基于DirectX的渲染框架- Lookof 's DW - 博客园】由 发布,版权归原作者及其所在单位,其原创性以及文中陈述文字和内容未经(企业库qiyeku.com)证实,请读者仅作参考,并请自行核实相关内容。若本文有侵犯到您的版权, 请你提供相关证明及申请并与我们联系(qiyeku # qq.com)或【在线投诉】,我们审核后将会尽快处理。
—— 相关资讯 ——