|
OpenSceneGraph与网络浏览器的嵌合,换句话说,就是在用户机的IE等浏览器中加载并显示OSG的渲染窗口。对于试图展示三维设计相关技术的网站而言,这无疑是向客户展示自身能力以及仿真产品的最直观方法;对于从事商品买卖,楼宇销售,地图导航等行业的企业来说,使用OSG在浏览器中实时显示自己的产品,并允许用户使用场景漫游器来即时体验其特色,同样是很好的选择;当然,只要设计合理,OSG同样可以用于网页游戏的制作。
本教程的目的是编写一个MFC ActiveX控件,在其中实现OSG窗口的显示和数据交互,并使用注册OCX的方法将OSG窗口嵌合到浏览器中,实现网页上的三维模型显示和场景漫游功能。本教程不会详细介绍ActiveX控件的高级编程知识,OCX文件的注册方法,以及ActiveX安装程序打包和发放的方法,相信您阅读了这篇拙劣的文章之后,一定可以凭借自己的实力将本教程附带的简陋代码改造和完善,并形成和发布自己的作品。
波尔实验室的Michael Gronager曾实现了一个OSG与网页嵌合的工程:osgAx。该工程基于OSG 0.9.6的版本,原网站为osgax.vr-c.dk,不过很遗憾这个工程已经停止更新,其源代码也无从下载。笔者在多方寻求osgAx源代码无果之后,决定自行从头编写OSG的ActiveX工程,笔者的功力与osgAx的作者恐怕尚不能同日而语,代码中多有错漏之处,请您给予批评和指正。
osgActiveX工程的源代码请在附件中下载。如有任何问题,欢迎您在bbs.osgchina.org发帖讨论;或者直接与我联系:wangray84@gmail.com(不推荐这种方式,因为您遇到的问题也可能是别人想知道的,无论是经验还是困惑,都希望大家一起分享)。
预备工作
有关OpenSceneGraph编程的基本知识,请参看《OpenSceneGraph快速入门指导》一书,以及网络上诸多相关的教程。
有关ActiveX编程以及HTML/VBScript代码实现的更多知识,请参阅网络上繁多的教程,以及各种网络编程相关书籍。
这里我们需要首先建立一个ActiveX工程,对于VS2008的用户,可以选择建立“MFC ActiveX控件”工程,并根据自己的需要设置相关的选项。工程建立后,会自动生成三个类,分别为(假设我们设定工程名为osgMFCAx):
CosgMFCAx:控件程序类。
CosgMFCAxCtrl:控件主窗口类。它是我们代码实现的核心部分,我们将扩展这个类以实现OSG模型的读入,窗口的创建和渲染流程。这个类包括了调度映射(Dispatch Maps)和事件映射(Event Maps)的实现函数,前者提供了外部程序(如浏览器)访问控件的属性和方法,后者用于向控件对象发送事件通知。
CosgMFCAxPropPage:这是控件的属性类,用于显示一个属性对话框并允许用户通过修改参数来改变控件的属性和状态。这里我们暂时不对它进行处理。
此外,系统自动生成的DllRegisterServer和DllUnregisterServer函数用于执行注册和注销控件的操作。这里我们同样不对它进行改动。
这里我们不使用MFC ClassWizard执行添加属性和方法的工作,因为VS2003以后的版本对MFC ClassWizard的支持明显减弱,且笔者以为过于依赖MFC的向导很不利于对程序结构的理解。
ActiveX工程的配置项中,同样需要添加OSG的头文件目录,以及osg.lib等必需的依赖库。而在运行时,除了必要的OCX控件外,也需要在客户机上安装OSG的DLL动态链接库文件,这一点在打包安装程序时需要特别注意。
ActiveX代码实现
首要的问题是:如何在ActiveX控件中嵌入OSG的窗口?
我们可以参考OSG发行版中附带的osgviewerMFC例子,在其中演示了OSG窗口嵌入MFC工程的一种通用思路:
(1)获取MFC窗口的句柄,并据此创建新的图形设备上下文(Graphics Context);
(2)创建一个摄像机,并指定它所使用的GC,视口(Viewport)和透视矩阵;
(3)将摄像机设定为视景器类(Viewer)的主摄像机,从而将OSG视景嵌合到指定的MFC窗口中。
(4)OSG的渲染循环不应当放在MFC的OnDraw或者OnPaint函数中,由于这两个函数只有在窗口需要重绘时方能收到消息,因此无法在其中执行osgViewer::Viewer::frame来渲染场景。此时我们可以建立一个新的线程,在新线程中实现渲染循环,并在程序退出的时候及时终止线程。
这一思路对于ActiveX的编程同样适用。首先,我们需要创建控件窗口的消息响应函数:- afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
- afx_msg void OnDestroy();
- 以及,
- BEGIN_MESSAGE_MAP(CosgMFCAxCtrl, COleControl)
- ON_WM_CREATE()
- ON_WM_DESTROY()
- ……
- END_MESSAGE_MAP()
复制代码 并编写相应的函数内容。
在OnCreate函数中,我们需要创建OSG窗口的图形上下文,并设置场景渲染的摄像机:- // 获取当前显示窗口的大小
- RECT rect;
- GetWindowRect(&rect);
- // 设置窗口的图形属性,包括窗口的显示大小,以及它所使用的Windows句柄等
- // 将窗口属性传递给新建的窗口图形上下文
- m_Traits = new osg::GraphicsContext::Traits;
- osg::ref_ptr<osg::Referenced> windata =
- new osgViewer::GraphicsWindowWin32::WindowData( GetSafeHwnd() );
- m_Traits->x = 0;
- m_Traits->y = 0;
- m_Traits->width = rect.right - rect.left;
- m_Traits->height = rect.bottom - rect.top;
- ……
- m_Traits->inheritedWindowData = windata;
- osg::GraphicsContext* gc =
- osg::GraphicsContext::createGraphicsContext( m_Traits.get() );
- // 将窗口图形上下文传递给新建的摄像机,并设置摄像机的视口,透视矩阵等参数
- osg::ref_ptr<osg::Camera> camera = new osg::Camera;
- camera->setGraphicsContext(gc);
- camera->setViewport(
- new osg::Viewport(m_Traits->x, m_Traits->y, m_Traits->width, m_Traits->height) );
- camera->setProjectionMatrixAsPerspective( 30.0f,
- (double)m_Traits->width/(double)m_Traits->height, 1.0f, 10000.0f );
- // 设置观察场景的视景器,将新的摄像机设置为场景的主摄像机
- // 这里我们还设置了视景器线程模型,由于我们是使用自建的线程来执行渲染,
- // 因此建议使用单线程的模型,以避免处理场景时出现问题
- m_Viewer = new osgViewer::Viewer;
- m_Viewer->setThreadingModel( osgViewer::Viewer::SingleThreaded );
- m_Viewer->setCamera( camera.get() );
- m_Viewer->setCameraManipulator( new osgGA::TrackballManipulator );
- // 这里我们将一个空节点赋予场景,稍后加载模型时,将模型节点追加到该节点
- // 的子节点位置;卸载模型时亦照此办理,以便于控制场景的节点树
- m_Root = new osg::Group;
- m_Viewer->setSceneData( m_Root.get() );
- m_Viewer->realize();
- // 开启新的渲染线程
- _beginthread(&RenderThread, 0, m_Viewer);
复制代码 注意这里我们自行开启了一个新的线程,用于执行OSG的渲染循环,线程执行的函数名为void RenderThread(void* ptr),这里它不属于任何类,当然您也可以参照osgviewerMFC的例子,将其归于某个类的成员函数。
编写RenderThread的内容:- osgViewer::Viewer* viewer = (osgViewer::Viewer*)ptr;
- while( !viewer->done() )
- {
- viewer->frame();
- }
- _endthread();
复制代码 然后不要忘记在类的析构函数中关闭新建的线程以及OSG的渲染线程,以免出现错误以及资源不能释放的情况:- m_Viewer->setDone( true );
- Sleep(1000);
- m_Viewer->stopThreading();
复制代码 现在我们的控件应该可以运行了,使用VS2008自带的“ActiveX控件测试容器”加载新的osgMFCAx控件,可以看到空白的程序界面。如果急于验证执行结果的话,也可以向m_Root加载一个本地的模型文件,例如cow.osg,并观察模型加载的效果。
这之后是读入文件的处理,我们希望要加载的文件名是从用户在网页上的交互输入传递而来,因此,我们需要编写相应的调度映射函数,从HTML部分获取文件名,并设法传递给OSG的readNodeFile函数。
设置调度映射函数如下:- afx_msg BSTR GetFile();
- afx_msg void LoadFile(LPCTSTR lpszName);
- afx_msg void ResetOSG();
复制代码 它们的作用分别是:获取当前加载文件的信息并传递给HTML;从HTML获取文件名并进行加载;重新设置OSG的窗口大小。
同时设置,- BEGIN_DISPATCH_MAP(CosgMFCAxCtrl, COleControl)
- DISP_PROPERTY_EX(CosgMFCAxCtrl, "GetFileName", GetFile, LoadFile, VT_BSTR)
- DISP_FUNCTION(CosgMFCAxCtrl, "ResetOSG", ResetOSG, VT_EMPTY, VTS_NONE)
- ……
- END_DISPATCH_MAP()
复制代码 其中GetFile和LoadFile对应控件的一个属性GetFileName,而ResetOSG则对应了控件的一个方法ResetOSG。在HTML代码中,我们可以使用这些属性和方法来实现与用户与控件的交互工作,但前提是设置了相应的IDL程序接口。打开系统自动生成的osgMFCAx.idl文件,在主调度接口的相应位置编写属性和方法的接口,如下:- dispinterface _DosgMFCAx
- {
- properties:
- [id(1)] BSTR GetFileName;
- methods:
- [id(2)] void ResetOSG();
- [id(DISPID_ABOUTBOX)] void AboutBox();
- };
复制代码 上述工作完成之后,我们再着重讨论一下LoadFile函数的内容。它的作用是获取网页的输入(事实上也就是函数的输入参数lpszName)。为了满足UNICODE编码的格式要求,我们需要恰当地转换输入参数的类型(const TCHAR*是不能直接输入到readNodeFile的),例如采用下面的方法:- setlocale( LC_ALL, "chs" );
- wcstombs_s( NULL, m_Filename, 255, lpszName, 255 );
复制代码 其中m_FileName是char*型的成员变量,在GetFile函数中,我们也可以把这个变量的值返回给HTML部分并显示给用户。这之后我们可以使用函数osgDB::readNodeFile加载模型并显示于场景中了:- osg::ref_ptr<osg::Node> node = osgDB::readNodeFile( m_FileName );
- node->setDataVariance( osg::Object::DYNAMIC );
- m_Root->addChild( node.get() );
- // 重新计算漫游器的原点位置并重置漫游器
- // 这样做的目的是避免在读入某个模型时,因为视角的当前方位不当
- // 以致我们不便于观察模型
- m_Viewer->getCameraManipulator()->computeHomePosition();
- m_Viewer->getCameraManipulator()->home( 0.0 );
复制代码 这里注意两个问题:加载新模型时,旧有节点的处理;以及加载失败时,如何给出恰当的错误信息。
第一个问题可以使用osg::ref_ptr的机制来解决:由于OSG使用了内存引用计数机制,因此当某个节点没有被任何节点或场景引用时,它会被自动删除和释放。因此,我们可以在加载新模型之前,使用removeChildren函数删除m_Root节点的现有子节点,从而将旧有的节点从内存中清除,避免因为没有及时释放内存空间而造成内存泄漏。
第二个问题可以通过改变模型文件读取机制来解决,例如下面的代码:- osg::ref_ptr<osg::Node> node;
- osgDB::ReaderWriter::ReadResult rr =
- osgDB::Registry::instance()->readNode( m_Filename, osgDB::Registry::instance()->getOptions() );
- if ( rr.validNode() )
- node = rr.takeNode();
- if ( !node )
- {
- ……
- return;
- }
复制代码 如果节点加载失败,我们就可以将rr.message()的内容或者自定义的错误文字显示出来,或者通过自定义的控件属性和方法传递给HTML部分。
我们还可以编写图片文件的读入函数,注意此时需要自定义一个四边形面片用于显示图片纹理。具体的实现请参见附带代码。 |
|